ref: 6cdf75c5a9e1f660aa2a86938be680c5db07ffd2
parent: 5682de102bdd28741d0b7e371e8ee9bbd003d045
author: Runxi Yu <me@runxiyu.org>
date: Sat Feb 21 06:33:40 EST 2026
refstore: Add ref shortening
--- a/refstore/chain/chain.go
+++ b/refstore/chain/chain.go
@@ -102,6 +102,30 @@
return refs, nil
}
+// Shorten shortens a full reference name using the chain-visible namespace.
+func (chain *Chain) Shorten(name string) (string, error) {+ refs, err := chain.List("")+ if err != nil {+ return "", err
+ }
+ names := make([]string, 0, len(refs))
+ found := false
+ for _, entry := range refs {+ if entry == nil {+ continue
+ }
+ full := entry.Name()
+ names = append(names, full)
+ if full == name {+ found = true
+ }
+ }
+ if !found {+ return "", refstore.ErrReferenceNotFound
+ }
+ return refstore.ShortenName(name, names), nil
+}
+
// Close closes all backends and joins close errors.
func (chain *Chain) Close() error {var errs []error
--- a/refstore/loose/loose_test.go
+++ b/refstore/loose/loose_test.go
@@ -147,3 +147,35 @@
}
})
}
+
+func TestLooseShorten(t *testing.T) {+ testgit.ForEachAlgorithm(t, func(t *testing.T, algo objectid.Algorithm) {+ testRepo := testgit.NewBareRepo(t, algo)
+ _, _, commitID := testRepo.MakeCommit(t, "shorten refs commit")
+ testRepo.UpdateRef(t, "refs/heads/main", commitID)
+ testRepo.UpdateRef(t, "refs/tags/main", commitID)
+ testRepo.UpdateRef(t, "refs/remotes/origin/main", commitID)
+
+ store := openLooseStore(t, testRepo.Dir(), algo)
+
+ shortHead, err := store.Shorten("refs/heads/main")+ if err != nil {+ t.Fatalf("Shorten(head): %v", err)+ }
+ if shortHead != "heads/main" {+ t.Fatalf("Shorten(refs/heads/main) = %q, want %q", shortHead, "heads/main")+ }
+
+ shortRemote, err := store.Shorten("refs/remotes/origin/main")+ if err != nil {+ t.Fatalf("Shorten(remote): %v", err)+ }
+ if shortRemote != "origin/main" {+ t.Fatalf("Shorten(remote) = %q, want %q", shortRemote, "origin/main")+ }
+
+ if _, err := store.Shorten("refs/heads/does-not-exist"); !errors.Is(err, refstore.ErrReferenceNotFound) {+ t.Fatalf("Shorten(not-found) error = %v", err)+ }
+ })
+}
--- /dev/null
+++ b/refstore/loose/shorten.go
@@ -1,0 +1,29 @@
+package loose
+
+import (
+ "codeberg.org/lindenii/furgit/refstore"
+)
+
+// Shorten returns the shortest unambiguous shorthand for a loose ref name.
+func (store *Store) Shorten(name string) (string, error) {+ refs, err := store.List("")+ if err != nil {+ return "", err
+ }
+ names := make([]string, 0, len(refs))
+ found := false
+ for _, entry := range refs {+ if entry == nil {+ continue
+ }
+ full := entry.Name()
+ names = append(names, full)
+ if full == name {+ found = true
+ }
+ }
+ if !found {+ return "", refstore.ErrReferenceNotFound
+ }
+ return refstore.ShortenName(name, names), nil
+}
--- a/refstore/refstore.go
+++ b/refstore/refstore.go
@@ -29,6 +29,12 @@
//
// The exact pattern language is backend-defined.
List(pattern string) ([]ref.Ref, error)
+ // Shorten returns the shortest unambiguous shorthand for a full
+ // reference name within this store's visible namespace.
+ //
+ // If name does not exist in this store, implementations should return
+ // ErrReferenceNotFound.
+ Shorten(name string) (string, error)
// Close releases resources associated with the store.
Close() error
}
--- /dev/null
+++ b/refstore/shorten.go
@@ -1,0 +1,74 @@
+package refstore
+
+import "strings"
+
+type shortenRule struct {+ prefix string
+ suffix string
+}
+
+var shortenRules = [...]shortenRule{+ {prefix: "", suffix: ""},+ {prefix: "refs/", suffix: ""},+ {prefix: "refs/tags/", suffix: ""},+ {prefix: "refs/heads/", suffix: ""},+ {prefix: "refs/remotes/", suffix: ""},+ {prefix: "refs/remotes/", suffix: "/HEAD"},+}
+
+func (rule shortenRule) match(name string) (string, bool) {+ if !strings.HasPrefix(name, rule.prefix) {+ return "", false
+ }
+ if !strings.HasSuffix(name, rule.suffix) {+ return "", false
+ }
+ short := strings.TrimPrefix(name, rule.prefix)
+ short = strings.TrimSuffix(short, rule.suffix)
+ if short == "" {+ return "", false
+ }
+ if rule.prefix+short+rule.suffix != name {+ return "", false
+ }
+ return short, true
+}
+
+func (rule shortenRule) render(short string) string {+ return rule.prefix + short + rule.suffix
+}
+
+// ShortenName returns the shortest unambiguous shorthand for name among all.
+//
+// all must contain full reference names visible to the shortening scope.
+func ShortenName(name string, all []string) string {+ names := make(map[string]struct{}, len(all))+ for _, full := range all {+ if full == "" {+ continue
+ }
+ names[full] = struct{}{}+ }
+
+ for i := len(shortenRules) - 1; i > 0; i-- {+ short, ok := shortenRules[i].match(name)
+ if !ok {+ continue
+ }
+ ambiguous := false
+ for j := range shortenRules {+ if j == i {+ continue
+ }
+ full := shortenRules[j].render(short)
+ if _, found := names[full]; found {+ ambiguous = true
+ break
+ }
+ }
+ if !ambiguous {+ return short
+ }
+ }
+ return name
+}
--- /dev/null
+++ b/refstore/shorten_test.go
@@ -1,0 +1,68 @@
+package refstore_test
+
+import (
+ "testing"
+
+ "codeberg.org/lindenii/furgit/refstore"
+)
+
+func TestShortenName(t *testing.T) {+ t.Parallel()
+
+ t.Run("simple", func(t *testing.T) {+ got := refstore.ShortenName("refs/heads/main", []string{"refs/heads/main"})+ if got != "main" {+ t.Fatalf("ShortenName simple = %q, want %q", got, "main")+ }
+ })
+
+ t.Run("ambiguous with tags", func(t *testing.T) {+ got := refstore.ShortenName(
+ "refs/heads/main",
+ []string{+ "refs/heads/main",
+ "refs/tags/main",
+ },
+ )
+ if got != "heads/main" {+ t.Fatalf("ShortenName tags ambiguity = %q, want %q", got, "heads/main")+ }
+ })
+
+ t.Run("strict remote head ambiguity", func(t *testing.T) {+ // In strict mode, refs/remotes/%s/HEAD blocks shortening to "%s".
+ got := refstore.ShortenName(
+ "refs/heads/main",
+ []string{+ "refs/heads/main",
+ "refs/remotes/main/HEAD",
+ },
+ )
+ if got != "heads/main" {+ t.Fatalf("ShortenName strict ambiguity = %q, want %q", got, "heads/main")+ }
+ })
+
+ t.Run("deep fallback still shortens", func(t *testing.T) {+ // refs/remotes/origin/main conflicts with refs/heads/origin/main for
+ // "origin/main", so it should fall back to "remotes/origin/main".
+ got := refstore.ShortenName(
+ "refs/remotes/origin/main",
+ []string{+ "refs/remotes/origin/main",
+ "refs/heads/origin/main",
+ },
+ )
+ if got != "remotes/origin/main" {+ t.Fatalf("ShortenName deep fallback = %q, want %q", got, "remotes/origin/main")+ }
+ })
+
+ t.Run("refs-prefix fallback", func(t *testing.T) {+ name := "refs/notes/review/topic"
+ got := refstore.ShortenName(name, []string{name})+ if got != "notes/review/topic" {+ t.Fatalf("ShortenName refs-prefix fallback = %q, want %q", got, "notes/review/topic")+ }
+ })
+}
--
⑨