package gitrepo import ( "crypto/sha256" "encoding/hex" "errors" "os/exec" "path" "sort" "strings" ) var ErrNoIdentity = errors.New("repository has no commits and no origin to identify it by") type Repo struct { Root string GitDir string CommonDir string Key string Name string } func run(dir string, args ...string) (string, error) { out, err := exec.Command("git", append([]string{"-C", dir}, args...)...).Output() return strings.TrimRight(string(out), "\n"), err } func Open(dir string) (*Repo, error) { root, err := run(dir, "rev-parse", "--show-toplevel") if err != nil { return nil, err } gitDir, err := run(root, "rev-parse", "--absolute-git-dir") if err != nil { return nil, err } commonDir, err := run(root, "rev-parse", "--path-format=absolute", "--git-common-dir") if err != nil { return nil, err } origin, _ := run(root, "config", "--get", "remote.origin.url") key, err := identity(root, origin) if err != nil { return nil, err } name := path.Base(normalizeURL(origin)) if origin == "" { name = path.Base(root) } return &Repo{Root: root, GitDir: gitDir, CommonDir: commonDir, Key: key, Name: name}, nil } func identity(root, origin string) (string, error) { shallow, _ := run(root, "rev-parse", "--is-shallow-repository") if shallow != "true" { roots, err := run(root, "rev-list", "--max-parents=0", "HEAD") if err == nil && roots != "" { hashes := strings.Fields(roots) sort.Strings(hashes) return hashes[0], nil } } if origin == "" { return "", ErrNoIdentity } sum := sha256.Sum256([]byte(normalizeURL(origin))) return "tmp-" + hex.EncodeToString(sum[:20]), nil } func normalizeURL(url string) string { url = strings.TrimSuffix(strings.TrimSuffix(strings.ToLower(url), "/"), ".git") if _, rest, ok := strings.Cut(url, "://"); ok { url = rest } if _, rest, ok := strings.Cut(url, "@"); ok { url = rest } return strings.ReplaceAll(url, ":", "/") } func (r *Repo) Branch() string { branch, _ := run(r.Root, "symbolic-ref", "--short", "-q", "HEAD") return branch } func (r *Repo) Head() string { sha, _ := run(r.Root, "rev-parse", "-q", "--verify", "HEAD") return sha } func (r *Repo) Tracked(paths []string) map[string]bool { tracked := map[string]bool{} if len(paths) == 0 { return tracked } out, _ := run(r.Root, append([]string{"ls-files", "-z", "--"}, paths...)...) for _, file := range strings.Split(out, "\x00") { if file != "" { tracked[file] = true } } return tracked }