shithub: furgit

ref: 918a17362c8a07221a65d4d60af0667ee7f0dd61
dir: /refstore/chain/chain.go/

View raw version
// 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...)
}