ref: e67fbfc75de011c2c7685ae7a10dfce702b2a620
dir: /refstore/chain/chain.go/
// Package chain provides a wrapper reference storage backend to query a chain
// of backends.
package chain
import (
"errors"
"fmt"
"codeberg.org/lindenii/furgit/ref"
"codeberg.org/lindenii/furgit/refstore"
)
// Chain queries multiple reference stores in order.
type Chain struct {
backends []refstore.Store
}
// New creates an ordered reference store chain.
func New(backends ...refstore.Store) *Chain {
return &Chain{
backends: append([]refstore.Store(nil), backends...),
}
}
// Resolve resolves a reference from the first backend that has it.
func (chain *Chain) Resolve(name string) (ref.Ref, error) {
for i, backend := range chain.backends {
if backend == nil {
continue
}
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
}
// ResolveFully resolves symbolic references through Resolve until detached.
//
// It intentionally does not call backend ResolveFully. This allows symbolic
// references to cross backends in the chain.
func (chain *Chain) ResolveFully(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)
}
}
}
// 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 {
if backend == nil {
continue
}
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
}
// 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
for _, backend := range chain.backends {
if backend == nil {
continue
}
if err := backend.Close(); err != nil {
errs = append(errs, err)
}
}
return errors.Join(errs...)
}