aboutsummaryrefslogtreecommitdiff
path: root/refs.go
diff options
context:
space:
mode:
authorGravatar Runxi Yu2026-02-20 19:06:13 +0800
committerGravatar Runxi Yu2026-02-20 19:07:14 +0800
commitaa513c069c1418734aea894dc944e27c6a78a3bb (patch)
tree687f0a11bb550fa088fd82a98ceb8979bbc35f69 /refs.go
parentComment on prior reverts removing the pack writing API (diff)
signatureNo signature
Delete everything, I'm redesigning this.
I'll stop using a flat package and make things much more modular. And also experiment with streaming APIs so large blobs don't OOM us.
Diffstat (limited to 'refs.go')
-rw-r--r--refs.go471
1 files changed, 0 insertions, 471 deletions
diff --git a/refs.go b/refs.go
deleted file mode 100644
index 6efdf5ec..00000000
--- a/refs.go
+++ /dev/null
@@ -1,471 +0,0 @@
-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
-}