aboutsummaryrefslogtreecommitdiff
path: root/refstore
diff options
context:
space:
mode:
Diffstat (limited to 'refstore')
-rw-r--r--refstore/chain/chain.go24
-rw-r--r--refstore/loose/loose_test.go32
-rw-r--r--refstore/loose/shorten.go29
-rw-r--r--refstore/refstore.go6
-rw-r--r--refstore/shorten.go74
-rw-r--r--refstore/shorten_test.go68
6 files changed, 233 insertions, 0 deletions
diff --git a/refstore/chain/chain.go b/refstore/chain/chain.go
index b84aac55..0a78dc94 100644
--- a/refstore/chain/chain.go
+++ b/refstore/chain/chain.go
@@ -102,6 +102,30 @@ func (chain *Chain) List(pattern string) ([]ref.Ref, error) {
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
diff --git a/refstore/loose/loose_test.go b/refstore/loose/loose_test.go
index b56e40ac..63898721 100644
--- a/refstore/loose/loose_test.go
+++ b/refstore/loose/loose_test.go
@@ -147,3 +147,35 @@ func TestLooseMalformedDetachedRef(t *testing.T) {
}
})
}
+
+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)
+ }
+ })
+}
diff --git a/refstore/loose/shorten.go b/refstore/loose/shorten.go
new file mode 100644
index 00000000..17a60def
--- /dev/null
+++ b/refstore/loose/shorten.go
@@ -0,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
+}
diff --git a/refstore/refstore.go b/refstore/refstore.go
index a3c9f201..5653d4ca 100644
--- a/refstore/refstore.go
+++ b/refstore/refstore.go
@@ -29,6 +29,12 @@ type Store interface {
//
// 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
}
diff --git a/refstore/shorten.go b/refstore/shorten.go
new file mode 100644
index 00000000..26fa82c0
--- /dev/null
+++ b/refstore/shorten.go
@@ -0,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
+}
diff --git a/refstore/shorten_test.go b/refstore/shorten_test.go
new file mode 100644
index 00000000..1975ab3f
--- /dev/null
+++ b/refstore/shorten_test.go
@@ -0,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")
+ }
+ })
+}