aboutsummaryrefslogtreecommitdiff
path: root/ref/store/chain
diff options
context:
space:
mode:
Diffstat (limited to 'ref/store/chain')
-rw-r--r--ref/store/chain/chain.go12
-rw-r--r--ref/store/chain/close.go8
-rw-r--r--ref/store/chain/list.go40
-rw-r--r--ref/store/chain/new.go13
-rw-r--r--ref/store/chain/resolve.go64
5 files changed, 137 insertions, 0 deletions
diff --git a/ref/store/chain/chain.go b/ref/store/chain/chain.go
new file mode 100644
index 00000000..6a4a0eed
--- /dev/null
+++ b/ref/store/chain/chain.go
@@ -0,0 +1,12 @@
+// Package chain provides a wrapper reference storage backend to query a chain
+// of backends.
+package chain
+
+import "codeberg.org/lindenii/furgit/ref/store"
+
+// Chain queries multiple reference stores in order.
+//
+// Chain borrows its backend stores.
+type Chain struct {
+ backends []refstore.ReadingStore
+}
diff --git a/ref/store/chain/close.go b/ref/store/chain/close.go
new file mode 100644
index 00000000..6bd74565
--- /dev/null
+++ b/ref/store/chain/close.go
@@ -0,0 +1,8 @@
+package chain
+
+// Close releases wrapper-local resources.
+//
+// Chain borrows its backends, so Close does not close them.
+//
+// Repeated calls to Close are undefined behavior.
+func (chain *Chain) Close() error { return nil }
diff --git a/ref/store/chain/list.go b/ref/store/chain/list.go
new file mode 100644
index 00000000..c577ca85
--- /dev/null
+++ b/ref/store/chain/list.go
@@ -0,0 +1,40 @@
+package chain
+
+import (
+ "fmt"
+
+ "codeberg.org/lindenii/furgit/ref"
+)
+
+// List lists references from every backend and deduplicates by ref name.
+//
+// First-seen wins, so earlier backends have precedence.
+func (chain *Chain) List(pattern string) ([]ref.Ref, error) {
+ var refs []ref.Ref
+
+ seen := map[string]struct{}{}
+
+ for i, backend := range chain.backends {
+ listed, err := backend.List(pattern)
+ if err != nil {
+ return nil, fmt.Errorf("refstore: backend %d list: %w", i, err)
+ }
+
+ for _, entry := range listed {
+ if entry == nil {
+ continue
+ }
+
+ name := entry.Name()
+ if _, ok := seen[name]; ok {
+ continue
+ }
+
+ seen[name] = struct{}{}
+
+ refs = append(refs, entry)
+ }
+ }
+
+ return refs, nil
+}
diff --git a/ref/store/chain/new.go b/ref/store/chain/new.go
new file mode 100644
index 00000000..dc8c0779
--- /dev/null
+++ b/ref/store/chain/new.go
@@ -0,0 +1,13 @@
+package chain
+
+import "codeberg.org/lindenii/furgit/ref/store"
+
+// New creates an ordered reference store chain.
+//
+// The provided backends must be non-nil and distinct.
+// Chain borrows the provided backends and does not close them in Close.
+func New(backends ...refstore.ReadingStore) *Chain {
+ return &Chain{
+ backends: append([]refstore.ReadingStore(nil), backends...),
+ }
+}
diff --git a/ref/store/chain/resolve.go b/ref/store/chain/resolve.go
new file mode 100644
index 00000000..f69d51ef
--- /dev/null
+++ b/ref/store/chain/resolve.go
@@ -0,0 +1,64 @@
+package chain
+
+import (
+ "errors"
+ "fmt"
+
+ "codeberg.org/lindenii/furgit/ref"
+ "codeberg.org/lindenii/furgit/ref/store"
+)
+
+// Resolve resolves a reference from the first backend that has it.
+//
+//nolint:ireturn
+func (chain *Chain) Resolve(name string) (ref.Ref, error) {
+ for i, backend := range chain.backends {
+ resolved, err := backend.Resolve(name)
+ if err == nil {
+ return resolved, nil
+ }
+
+ if errors.Is(err, refstore.ErrReferenceNotFound) {
+ continue
+ }
+
+ return nil, fmt.Errorf("refstore: backend %d resolve: %w", i, err)
+ }
+
+ return nil, refstore.ErrReferenceNotFound
+}
+
+// ResolveToDetached resolves symbolic references through Resolve until detached.
+//
+// It intentionally does not call backend ResolveToDetached. This allows symbolic
+// references to cross backends in the chain.
+func (chain *Chain) ResolveToDetached(name string) (ref.Detached, error) {
+ cur := name
+
+ seen := map[string]struct{}{}
+ for {
+ if _, ok := seen[cur]; ok {
+ return ref.Detached{}, fmt.Errorf("refstore: symbolic reference cycle at %q", cur)
+ }
+
+ seen[cur] = struct{}{}
+
+ resolved, err := chain.Resolve(cur)
+ if err != nil {
+ return ref.Detached{}, err
+ }
+
+ switch resolved := resolved.(type) {
+ case ref.Detached:
+ return resolved, nil
+ case ref.Symbolic:
+ if resolved.Target == "" {
+ return ref.Detached{}, fmt.Errorf("refstore: symbolic reference %q has empty target", resolved.Name())
+ }
+
+ cur = resolved.Target
+ default:
+ return ref.Detached{}, fmt.Errorf("refstore: unsupported reference type %T", resolved)
+ }
+ }
+}