git@cosmocr.at/gitweb.git @ main · summary refs log
main / internal / gitrepo / refs.go /
internal/gitrepo/refs.go · 4.0 KiB · raw
view: Source ☐ force
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
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 "<ref>/<path>" 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
}