package gitrepo import ( "errors" "sort" "strings" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" ) // ErrRefNotFound is returned when a ref/revision cannot be resolved. var ErrRefNotFound = errors.New("ref not found") // RefName is a branch or tag for display. type RefName struct { Name string // short name, e.g. "main", "feature/x", "v1.0.0" Hash plumbing.Hash // commit the ref resolves to } // Refs lists the branches and tags of repo, each sorted by name. func Refs(repo *git.Repository) (branches, tags []RefName, err error) { bIter, err := repo.Branches() if err != nil { return nil, nil, err } _ = bIter.ForEach(func(r *plumbing.Reference) error { branches = append(branches, RefName{Name: r.Name().Short(), Hash: r.Hash()}) return nil }) tIter, err := repo.Tags() if err != nil { return nil, nil, err } _ = tIter.ForEach(func(r *plumbing.Reference) error { h := r.Hash() // Peel annotated tags to the commit they point at, for display. if c, err := repo.CommitObject(h); err == nil { h = c.Hash } else if to, err := repo.TagObject(r.Hash()); err == nil { if c, err := to.Commit(); err == nil { h = c.Hash } } tags = append(tags, RefName{Name: r.Name().Short(), Hash: h}) return nil }) sort.Slice(branches, func(i, j int) bool { return branches[i].Name < branches[j].Name }) sort.Slice(tags, func(i, j int) bool { return tags[i].Name < tags[j].Name }) return branches, tags, nil } // refShortNames returns the set of branch and tag short names. func refShortNames(repo *git.Repository) (map[string]bool, error) { branches, tags, err := Refs(repo) if err != nil { return nil, err } set := make(map[string]bool, len(branches)+len(tags)) for _, b := range branches { set[b.Name] = true } for _, t := range tags { set[t.Name] = true } return set, nil } // ResolveRef splits a "/" remainder into a ref and an in-tree path, // then resolves the ref to a commit. Refs may contain slashes, so it matches // the longest leading path segment run that is a known branch/tag rather than // splitting on the first slash. A raw (possibly abbreviated) commit hash is // also accepted as the ref. An empty remainder resolves HEAD. // // Returns the resolved commit hash, the ref name to display (the matched ref, // the hash, or "HEAD"), and the remaining in-tree path (no leading slash). func ResolveRef(repo *git.Repository, refAndPath string) (commit plumbing.Hash, refName, subPath string, err error) { refAndPath = strings.Trim(refAndPath, "/") if refAndPath == "" { h, err := resolveRevision(repo, "HEAD") if err != nil { return plumbing.ZeroHash, "", "", err } return h, "HEAD", "", nil } segs := strings.Split(refAndPath, "/") set, err := refShortNames(repo) if err != nil { return plumbing.ZeroHash, "", "", err } // Longest leading run of segments that names a known ref wins. for i := len(segs); i >= 1; i-- { cand := strings.Join(segs[:i], "/") if set[cand] { h, err := resolveRevision(repo, cand) if err != nil { return plumbing.ZeroHash, "", "", err } return h, cand, strings.Join(segs[i:], "/"), nil } } // No ref matched: treat the first segment as a (possibly short) commit hash. if h, err := resolveRevision(repo, segs[0]); err == nil { return h, segs[0], strings.Join(segs[1:], "/"), nil } return plumbing.ZeroHash, "", "", ErrRefNotFound } // resolveRevision resolves a branch/tag/hash revision to a commit hash, peeling // annotated tags to their commit. func resolveRevision(repo *git.Repository, rev string) (plumbing.Hash, error) { h, err := repo.ResolveRevision(plumbing.Revision(rev)) if err != nil { return plumbing.ZeroHash, ErrRefNotFound } // ResolveRevision already peels to a commit for tags in practice, but be safe. if _, err := repo.CommitObject(*h); err != nil { if to, terr := repo.TagObject(*h); terr == nil { if c, cerr := to.Commit(); cerr == nil { return c.Hash, nil } } return plumbing.ZeroHash, ErrRefNotFound } return *h, nil }