shithub: furgit

Download patch

ref: d5470e5dd11a16cf785a4115deee9ccdea769da0
parent: 9c5d8db0e1dfeb7eba7682ee61a51ab641ada324
author: Runxi Yu <me@runxiyu.org>
date: Tue Nov 25 03:00:00 EST 2025

refs: Merge NamedRef into Ref; add Short

--- a/refs.go
+++ b/refs.go
@@ -24,9 +24,10 @@
 	if strings.HasPrefix(line, "ref: ") {
 		target := strings.TrimSpace(line[5:])
 		if target == "" {
-			return Ref{Kind: RefKindInvalid}, ErrInvalidRef
+			return Ref{Name: refname, Kind: RefKindInvalid}, ErrInvalidRef
 		}
 		return Ref{
+			Name: refname,
 			Kind: RefKindSymbolic,
 			Ref:  target,
 		}, nil
@@ -34,9 +35,10 @@
 
 	id, err := repo.ParseHash(line)
 	if err != nil {
-		return Ref{Kind: RefKindInvalid}, err
+		return Ref{Name: refname, Kind: RefKindInvalid}, err
 	}
 	return Ref{
+		Name: refname,
 		Kind: RefKindDetached,
 		Hash: id,
 	}, nil
@@ -81,10 +83,11 @@
 		hex := string(line[:sp])
 		id, err := repo.ParseHash(hex)
 		if err != nil {
-			return Ref{Kind: RefKindInvalid}, err
+			return Ref{Name: refname, Kind: RefKindInvalid}, err
 		}
 
 		ref := Ref{
+			Name: refname,
 			Kind: RefKindDetached,
 			Hash: id,
 		}
@@ -97,7 +100,7 @@
 
 				peeledID, err := repo.ParseHash(peeledHex)
 				if err != nil {
-					return Ref{Kind: RefKindInvalid}, err
+					return Ref{Name: refname, Kind: RefKindInvalid}, err
 				}
 				ref.Peeled = peeledID
 			}
@@ -104,7 +107,7 @@
 		}
 
 		if scanErr := scanner.Err(); scanErr != nil {
-			return Ref{Kind: RefKindInvalid}, scanErr
+			return Ref{Name: refname, Kind: RefKindInvalid}, scanErr
 		}
 
 		return ref, nil
@@ -111,7 +114,7 @@
 	}
 
 	if scanErr := scanner.Err(); scanErr != nil {
-		return Ref{Kind: RefKindInvalid}, scanErr
+		return Ref{Name: refname, Kind: RefKindInvalid}, scanErr
 	}
 	return Ref{}, ErrNotFound
 }
@@ -130,6 +133,10 @@
 
 // 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.
@@ -144,14 +151,108 @@
 	Peeled Hash
 }
 
-// NamedRef represents a reference entry as returned by NamedRefs.
-type NamedRef struct {
-	// Name is the fully qualified ref name (e.g., refs/heads/main).
-	Name string
-	// Ref describes the reference target.
-	Ref Ref
+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.
@@ -169,6 +270,7 @@
 		id, err := repo.ParseHash(path)
 		if err == nil {
 			return Ref{
+				Name: path,
 				Kind: RefKindDetached,
 				Hash: id,
 			}, nil
@@ -178,7 +280,7 @@
 		// 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
+		return Ref{Name: path, Kind: RefKindInvalid}, ErrInvalidRef
 	}
 
 	loose, err := repo.resolveLooseRef(path)
@@ -186,7 +288,7 @@
 		return loose, nil
 	}
 	if err != ErrNotFound {
-		return Ref{Kind: RefKindInvalid}, err
+		return Ref{Name: path, Kind: RefKindInvalid}, err
 	}
 
 	packed, err := repo.resolvePackedRef(path)
@@ -194,10 +296,10 @@
 		return packed, nil
 	}
 	if err != ErrNotFound {
-		return Ref{Kind: RefKindInvalid}, err
+		return Ref{Name: path, Kind: RefKindInvalid}, err
 	}
 
-	return Ref{Kind: RefKindInvalid}, ErrNotFound
+	return Ref{Name: path, Kind: RefKindInvalid}, ErrNotFound
 }
 
 // ResolveRefFully resolves a ref by recursively following
@@ -244,7 +346,7 @@
 // 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) ([]NamedRef, error) {
+func (repo *Repository) ListRefs(pattern string) ([]Ref, error) {
 	if pattern == "" {
 		pattern = "refs/*"
 	}
@@ -255,7 +357,7 @@
 		return nil, ErrInvalidRef
 	}
 
-	var out []NamedRef
+	var out []Ref
 	seen := make(map[string]struct{})
 
 	globPattern := filepath.Join(repo.rootPath, filepath.FromSlash(pattern))
@@ -290,10 +392,7 @@
 		}
 
 		seen[name] = struct{}{}
-		out = append(out, NamedRef{
-			Name: name,
-			Ref:  ref,
-		})
+		out = append(out, ref)
 	}
 
 	packedPath := repo.repoPath("packed-refs")
@@ -324,7 +423,7 @@
 			if parseErr != nil {
 				return nil, parseErr
 			}
-			out[lastIdx].Ref.Peeled = peeled
+			out[lastIdx].Peeled = peeled
 			continue
 		}
 
@@ -357,12 +456,10 @@
 		if parseErr != nil {
 			return nil, parseErr
 		}
-		out = append(out, NamedRef{
+		out = append(out, Ref{
 			Name: name,
-			Ref: Ref{
-				Kind: RefKindDetached,
-				Hash: hash,
-			},
+			Kind: RefKindDetached,
+			Hash: hash,
 		})
 		lastIdx = len(out) - 1
 	}
--- a/refs_test.go
+++ b/refs_test.go
@@ -333,7 +333,7 @@
 		if _, exists := got[r.Name]; exists {
 			t.Fatalf("duplicate ref %q in results", r.Name)
 		}
-		got[r.Name] = r.Ref
+		got[r.Name] = r
 	}
 
 	mainRef, ok := got["refs/heads/main"]
@@ -392,8 +392,8 @@
 	if refs[0].Name != "refs/heads/feature" {
 		t.Fatalf("unexpected ref name: got %q, want %q", refs[0].Name, "refs/heads/feature")
 	}
-	if refs[0].Ref.Kind != RefKindDetached || refs[0].Ref.Hash != hash1 {
-		t.Fatalf("refs/heads/feature hash: got %s (kind %v), want %s", refs[0].Ref.Hash, refs[0].Ref.Kind, hash1)
+	if refs[0].Kind != RefKindDetached || refs[0].Hash != hash1 {
+		t.Fatalf("refs/heads/feature hash: got %s (kind %v), want %s", refs[0].Hash, refs[0].Kind, hash1)
 	}
 }
 
@@ -483,4 +483,38 @@
 			}
 		})
 	}
+}
+
+func TestRefShort(t *testing.T) {
+	t.Run("unambiguous", func(t *testing.T) {
+		ref := Ref{Name: "refs/heads/main"}
+		short := ref.Short([]Ref{ref}, false)
+		if short != "main" {
+			t.Fatalf("expected short name %q, got %q", "main", short)
+		}
+	})
+
+	t.Run("ambiguous", func(t *testing.T) {
+		ref := Ref{Name: "refs/heads/main"}
+		tags := Ref{Name: "refs/tags/main"}
+		short := ref.Short([]Ref{ref, tags}, false)
+		if short != "heads/main" {
+			t.Fatalf("expected ambiguous ref to shorten to %q, got %q", "heads/main", short)
+		}
+	})
+
+	t.Run("strict", func(t *testing.T) {
+		ref := Ref{Name: "refs/heads/main"}
+		remoteHead := Ref{Name: "refs/remotes/main/HEAD"}
+
+		shortNonStrict := ref.Short([]Ref{ref, remoteHead}, false)
+		if shortNonStrict != "main" {
+			t.Fatalf("expected non-strict short name %q, got %q", "main", shortNonStrict)
+		}
+
+		shortStrict := ref.Short([]Ref{ref, remoteHead}, true)
+		if shortStrict != "heads/main" {
+			t.Fatalf("expected strict ambiguity to shorten to %q, got %q", "heads/main", shortStrict)
+		}
+	})
 }
--