shithub: furgit

Download patch

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")
+		}
+	})
+}
--