ref: 71dd999fd8c13a6e4e707e525e0f5097d48fc48e
dir: /refs.go/
package furgit
import (
"bufio"
"bytes"
"fmt"
"os"
"path"
"path/filepath"
"slices"
"strings"
)
func (repo *Repository) resolveLooseRef(refname string) (Ref, error) {
data, err := os.ReadFile(repo.repoPath(refname))
if err != nil {
if os.IsNotExist(err) {
return Ref{}, ErrNotFound
}
return Ref{}, err
}
line := strings.TrimSpace(string(data))
if strings.HasPrefix(line, "ref: ") {
target := strings.TrimSpace(line[5:])
if target == "" {
return Ref{Name: refname, Kind: RefKindInvalid}, ErrInvalidRef
}
return Ref{
Name: refname,
Kind: RefKindSymbolic,
Ref: target,
}, nil
}
id, err := repo.ParseHash(line)
if err != nil {
return Ref{Name: refname, Kind: RefKindInvalid}, err
}
return Ref{
Name: refname,
Kind: RefKindDetached,
Hash: id,
}, nil
}
func (repo *Repository) resolvePackedRef(refname string) (Ref, error) {
// According to git-pack-refs(1), symbolic refs are never
// stored in packed-refs, so we only need to look for detached
// refs here.
path := repo.repoPath("packed-refs")
f, err := os.Open(path)
if err != nil {
if os.IsNotExist(err) {
return Ref{}, ErrNotFound
}
return Ref{}, err
}
defer func() { _ = f.Close() }()
want := []byte(refname)
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := scanner.Bytes()
if len(line) == 0 || line[0] == '#' || line[0] == '^' {
continue
}
sp := bytes.IndexByte(line, ' ')
if sp != repo.hashAlgo.Size()*2 {
continue
}
name := line[sp+1:]
if !bytes.Equal(name, want) {
continue
}
hex := string(line[:sp])
id, err := repo.ParseHash(hex)
if err != nil {
return Ref{Name: refname, Kind: RefKindInvalid}, err
}
ref := Ref{
Name: refname,
Kind: RefKindDetached,
Hash: id,
}
if scanner.Scan() {
next := scanner.Bytes()
if len(next) > 0 && next[0] == '^' {
peeledHex := strings.TrimPrefix(string(next), "^")
peeledHex = strings.TrimSpace(peeledHex)
peeledID, err := repo.ParseHash(peeledHex)
if err != nil {
return Ref{Name: refname, Kind: RefKindInvalid}, err
}
ref.Peeled = peeledID
}
}
if scanErr := scanner.Err(); scanErr != nil {
return Ref{Name: refname, Kind: RefKindInvalid}, scanErr
}
return ref, nil
}
if scanErr := scanner.Err(); scanErr != nil {
return Ref{Name: refname, Kind: RefKindInvalid}, scanErr
}
return Ref{}, ErrNotFound
}
// RefKind represents the kind of HEAD reference.
type RefKind int
const (
// The HEAD reference is invalid.
RefKindInvalid RefKind = iota
// The HEAD reference points to a detached commit hash.
RefKindDetached
// The HEAD reference points to a symbolic ref.
RefKindSymbolic
)
// Ref represents a reference.
type Ref struct {
// Name is the fully qualified ref name (e.g., refs/heads/main).
// It may be empty for detached hashes that were not looked up
// by name (e.g., ResolveRef on a raw hash).
Name string
// Kind is the kind of the reference.
Kind RefKind
// When Kind is RefKindSymbolic, Ref is the fully qualified ref name.
// Otherwise the value is undefined.
Ref string
// When Kind is RefKindDetached, Hash is the commit hash.
// Otherwise the value is undefined.
Hash Hash
// When Kind is RefKindDetached, and the ref supposedly points to an
// annotated tag, Peeled is the peeled hash, i.e., the hash of the
// object that the tag points to.
Peeled Hash
}
type refParseRule struct {
fmtStr string
prefix string
suffix string
}
func parseRule(rule string) refParseRule {
prefix, suffix, _ := strings.Cut(rule, "%s")
return refParseRule{
fmtStr: rule,
prefix: prefix,
suffix: suffix,
}
}
var refRevParseRules = []refParseRule{
parseRule("%s"),
parseRule("refs/%s"),
parseRule("refs/tags/%s"),
parseRule("refs/heads/%s"),
parseRule("refs/remotes/%s"),
parseRule("refs/remotes/%s/HEAD"),
}
func (rule refParseRule) match(name string) (string, bool) {
if rule.suffix != "" {
if !strings.HasSuffix(name, rule.suffix) {
return "", false
}
name = strings.TrimSuffix(name, rule.suffix)
}
var short string
n, err := fmt.Sscanf(name, rule.prefix+"%s", &short)
if err != nil || n != 1 {
return "", false
}
if fmt.Sprintf(rule.prefix+"%s", short) != name {
return "", false
}
return short, true
}
func (rule refParseRule) render(short string) string {
return rule.prefix + short + rule.suffix
}
// Short returns the shortest unambiguous shorthand for the ref name,
// following the rev-parse rules used by Git. The provided list of refs
// is used to test for ambiguity.
//
// When strict is true, all other rules must fail to resolve to an
// existing ref; otherwise only rules prior to the matched rule must
// fail.
func (ref *Ref) Short(all []Ref, strict bool) string {
if ref == nil {
return ""
}
name := ref.Name
if name == "" {
return ""
}
names := make(map[string]struct{}, len(all))
for _, r := range all {
if r.Name == "" {
continue
}
names[r.Name] = struct{}{}
}
for i := len(refRevParseRules) - 1; i > 0; i-- {
short, ok := refRevParseRules[i].match(name)
if !ok {
continue
}
rulesToFail := i
if strict {
rulesToFail = len(refRevParseRules)
}
ambiguous := false
for j := 0; j < rulesToFail; j++ {
if j == i {
continue
}
full := refRevParseRules[j].render(short)
if _, found := names[full]; found {
ambiguous = true
break
}
}
if !ambiguous {
return short
}
}
return name
}
// ResolveRef reads the given fully qualified ref (such as "HEAD" or "refs/heads/main")
// and interprets its contents as either a symbolic ref ("ref: refs/..."), a detached
// hash, or invalid.
// If path is empty, it defaults to "HEAD".
// (While typically only HEAD may be a symbolic reference, others may be as well.)
func (repo *Repository) ResolveRef(path string) (Ref, error) {
if path == "" {
path = "HEAD"
}
if !strings.HasPrefix(path, "refs/") && !slices.Contains([]string{
"HEAD", "ORIG_HEAD", "FETCH_HEAD", "MERGE_HEAD",
"CHERRY_PICK_HEAD", "REVERT_HEAD", "REBASE_HEAD", "BISECT_HEAD",
}, path) {
id, err := repo.ParseHash(path)
if err == nil {
return Ref{
Name: path,
Kind: RefKindDetached,
Hash: id,
}, nil
}
// For now let's keep this to prevent e.g., random users from
// specifying something crazy like objects/... or ./config.
// There may be other legal pseudo-refs in the future,
// but it's probably the best to stay cautious for now.
return Ref{Name: path, Kind: RefKindInvalid}, ErrInvalidRef
}
loose, err := repo.resolveLooseRef(path)
if err == nil {
return loose, nil
}
if err != ErrNotFound {
return Ref{Name: path, Kind: RefKindInvalid}, err
}
packed, err := repo.resolvePackedRef(path)
if err == nil {
return packed, nil
}
if err != ErrNotFound {
return Ref{Name: path, Kind: RefKindInvalid}, err
}
return Ref{Name: path, Kind: RefKindInvalid}, ErrNotFound
}
// ResolveRefFully resolves a ref by recursively following
// symbolic references until it reaches a detached ref.
// Symbolic cycles are detected and reported.
// Annotated tags are not peeled.
func (repo *Repository) ResolveRefFully(path string) (Ref, error) {
seen := make(map[string]struct{})
return repo.resolveRefFully(path, seen)
}
func (repo *Repository) resolveRefFully(path string, seen map[string]struct{}) (Ref, error) {
if _, found := seen[path]; found {
return Ref{}, fmt.Errorf("symbolic ref cycle involving %q", path)
}
seen[path] = struct{}{}
ref, err := repo.ResolveRef(path)
if err != nil {
return Ref{}, err
}
switch ref.Kind {
case RefKindDetached:
return ref, nil
case RefKindSymbolic:
if ref.Ref == "" {
return Ref{}, ErrInvalidRef
}
return repo.resolveRefFully(ref.Ref, seen)
default:
return Ref{}, ErrInvalidRef
}
}
// ListRefs lists refs similarly to git-show-ref.
//
// The pattern must be empty or begin with "refs/". An empty pattern is
// treated as "refs/*".
//
// Loose refs are resolved using filesystem globbing relative to the
// repository root, then packed refs are read while skipping any names
// that already appeared as loose refs. Packed refs are filtered
// similarly.
func (repo *Repository) ListRefs(pattern string) ([]Ref, error) {
if pattern == "" {
pattern = "refs/*"
}
if !strings.HasPrefix(pattern, "refs/") {
return nil, ErrInvalidRef
}
if filepath.IsAbs(pattern) {
return nil, ErrInvalidRef
}
var out []Ref
seen := make(map[string]struct{})
globPattern := filepath.Join(repo.rootPath, filepath.FromSlash(pattern))
matches, err := filepath.Glob(globPattern)
if err != nil {
return nil, err
}
for _, match := range matches {
info, statErr := os.Stat(match)
if statErr != nil {
return nil, statErr
}
if info.IsDir() {
continue
}
rel, relErr := filepath.Rel(repo.rootPath, match)
if relErr != nil {
return nil, relErr
}
name := filepath.ToSlash(rel)
if !strings.HasPrefix(name, "refs/") {
continue
}
ref, resolveErr := repo.resolveLooseRef(name)
if resolveErr != nil {
if resolveErr == ErrNotFound || os.IsNotExist(resolveErr) {
continue
}
return nil, resolveErr
}
seen[name] = struct{}{}
out = append(out, ref)
}
packedPath := repo.repoPath("packed-refs")
f, err := os.Open(packedPath)
if err != nil {
if os.IsNotExist(err) {
return out, nil
}
return nil, err
}
defer func() { _ = f.Close() }()
scanner := bufio.NewScanner(f)
lastIdx := -1
for scanner.Scan() {
line := scanner.Bytes()
if len(line) == 0 || line[0] == '#' {
continue
}
if line[0] == '^' {
if lastIdx < 0 {
continue
}
peeledHex := strings.TrimPrefix(string(line), "^")
peeledHex = strings.TrimSpace(peeledHex)
peeled, parseErr := repo.ParseHash(peeledHex)
if parseErr != nil {
return nil, parseErr
}
out[lastIdx].Peeled = peeled
continue
}
sp := bytes.IndexByte(line, ' ')
if sp != repo.hashAlgo.Size()*2 {
lastIdx = -1
continue
}
name := string(line[sp+1:])
if !strings.HasPrefix(name, "refs/") {
lastIdx = -1
continue
}
if _, ok := seen[name]; ok {
lastIdx = -1
continue
}
match, matchErr := path.Match(pattern, name)
if matchErr != nil {
return nil, matchErr
}
if !match {
lastIdx = -1
continue
}
hash, parseErr := repo.ParseHash(string(line[:sp]))
if parseErr != nil {
return nil, parseErr
}
out = append(out, Ref{
Name: name,
Kind: RefKindDetached,
Hash: hash,
})
lastIdx = len(out) - 1
}
if scanErr := scanner.Err(); scanErr != nil {
return nil, scanErr
}
return out, nil
}