ref: d119343e8e7c83a56f87b05249ff0094aa2298a5
dir: /refs.go/
package furgit
import (
"bufio"
"bytes"
"fmt"
"os"
"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{Kind: RefKindInvalid}, ErrInvalidRef
}
return Ref{
Kind: RefKindSymbolic,
Ref: target,
}, nil
}
id, err := repo.ParseHash(line)
if err != nil {
return Ref{Kind: RefKindInvalid}, err
}
return Ref{
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.hashSize*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{Kind: RefKindInvalid}, err
}
ref := Ref{
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{Kind: RefKindInvalid}, err
}
ref.Peeled = peeledID
}
}
if scanErr := scanner.Err(); scanErr != nil {
return Ref{Kind: RefKindInvalid}, scanErr
}
return ref, nil
}
if scanErr := scanner.Err(); scanErr != nil {
return Ref{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 {
// 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
}
// 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{
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{Kind: RefKindInvalid}, ErrInvalidRef
}
loose, err := repo.resolveLooseRef(path)
if err == nil {
return loose, nil
}
if err != ErrNotFound {
return Ref{Kind: RefKindInvalid}, err
}
packed, err := repo.resolvePackedRef(path)
if err == nil {
return packed, nil
}
if err != ErrNotFound {
return Ref{Kind: RefKindInvalid}, err
}
return Ref{Kind: RefKindInvalid}, ErrNotFound
}
// ResolveRefFully resolves a ref by recursively following
// symbolic references until it reaches a detached ref.
// Symbolic cycles are detected and reported.
// Tags are not peeled automatically.
func (repo *Repository) ResolveRefFully(path string) (Hash, error) {
seen := make(map[string]struct{})
return repo.resolveRefFully(path, seen)
}
func (repo *Repository) resolveRefFully(path string, seen map[string]struct{}) (Hash, error) {
if _, found := seen[path]; found {
return Hash{}, fmt.Errorf("symbolic ref cycle involving %q", path)
}
seen[path] = struct{}{}
ref, err := repo.ResolveRef(path)
if err != nil {
return Hash{}, err
}
switch ref.Kind {
case RefKindDetached:
return ref.Hash, nil
case RefKindSymbolic:
if ref.Ref == "" {
return Hash{}, ErrInvalidRef
}
return repo.resolveRefFully(ref.Ref, seen)
default:
return Hash{}, ErrInvalidRef
}
}