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 }