Artifact 4f4299e2ea88f58b22aaf6bff4d0f902642c7e437866ef441cd2591a8705e56f:
- File
checkout_sparse.go
— part of check-in
[e5904fc85b]
at
2026-10-22 14:47:23
on branch feat/horizon-cones
— feat(horizon): expose content horizons in the public API and CLI
A store cone could carry a horizon only through internal/cone, so library users and the CLI had no way to clone into or edit a content-horizon store. Add Horizon, HorizonRule, NewHorizonCone, Cone.Horizon and Cone.WithHorizon over the internal cone (always the content cut), carry it through SetStoreCone/SetStoreConeContext (deepening fetches what the older check-in keeps) and CloneOpts.StoreCone, and add --horizon, --leaves, --no-horizon and --cone to repo clone and repo cone store.
A horizon is a property of the store, not of a checkout (draft-fossil-horizon-00 3.2), so checkout cones carrying one are refused with ErrHorizonCheckout, and widening the store from a checkout cone keeps the store's horizon. (user: [email protected], size: 7786) [annotate] [blame] [check-ins using] [more...]
package libfossil import ( "errors" "fmt" "github.com/danmestas/go-libfossil/internal/checkout" "github.com/danmestas/go-libfossil/internal/cone" ) // ErrOutsideCone is wrapped by the refusal of a checkout operation that would // act on a path its sparse cone does not cover: adding, removing, renaming, // reverting or committing such a path, or a merge that would change one. The // message names the path, the cone and the member that excluded it. var ErrOutsideCone = checkout.ErrOutsideCone // ErrHorizonCheckout is returned when a checkout is given a cone that carries a // horizon. A content horizon (draft-fossil-horizon-00 §3.2) is a property of // the repository's store, not of a checkout: set it on the store cone with // Repo.SetStoreCone. var ErrHorizonCheckout = errors.New("libfossil: a horizon is a property of the repository's store, not of a checkout") // checkoutCone refuses a cone with a horizon and unwraps one without. func checkoutCone(c *Cone) (*cone.Cone, error) { if c != nil { if _, ok := c.inner.Horizon(); ok { return nil, ErrHorizonCheckout } } return unwrapCone(c), nil } // Cone is a sparse checkout's cone (draft-fossil-sparse-cone-00 §3): a set of // repository paths, each a directory subtree or a single file, included or // excluded. The longest member covering a path decides whether the cone // covers it, so an exclusion carves a part out of an inclusion, and a deeper // inclusion re-includes part of an exclusion. Members are literal prefixes, // never patterns. A Cone is immutable; With and Without return new ones. type Cone struct { inner *cone.Cone } // NewCone builds a cone from inclusion and exclusion members. Members are // validated as tracked paths, sorted, and reduced to the minimal set with the // same coverage, so input order never matters. It refuses an empty // inclusion list, more than 255 members, an exclusion no inclusion covers, // and a path given with both polarities. func NewCone(include, exclude []string) (*Cone, error) { inner, err := cone.New(include, exclude) if err != nil { return nil, fmt.Errorf("libfossil: cone: %w", err) } return &Cone{inner: inner}, nil } // HorizonRule is the held-set rule a horizon names (draft-fossil-horizon-00 // §3.1). type HorizonRule int const ( // HorizonDescendants holds the check-in and every descendant of it. It is // the zero value. HorizonDescendants HorizonRule = iota // HorizonLeaves holds the descendants plus every open leaf's ancestry back // to its nearest common ancestor with the check-in. HorizonLeaves ) // String names the rule as the operator spells it. func (r HorizonRule) String() string { return cone.HorizonRule(r).String() } // Horizon is the temporal member of a store cone (draft-fossil-horizon-00 // §3.1): blobs only check-ins outside the held set name are elided. The cut is // always the content horizon; the structural cut is not exposed. type Horizon struct { Checkin string // full artifact hash of the check-in, 40 or 64 hex digits Rule HorizonRule } // NewHorizonCone builds a cone with a horizon and optional path members. With // no include members the cone is horizon-only: it covers every path and only // the horizon narrows what is held. Checkin is normalized to lower case. func NewHorizonCone(include, exclude []string, h Horizon) (*Cone, error) { ih, err := cone.NewHorizon(h.Checkin, cone.HorizonRule(h.Rule), cone.DepthContent) if err != nil { return nil, fmt.Errorf("libfossil: cone: %w", err) } inner, err := cone.NewWithHorizon(include, exclude, ih) if err != nil { return nil, fmt.Errorf("libfossil: cone: %w", err) } return &Cone{inner: inner}, nil } // Horizon returns the cone's horizon, if it has one. func (c *Cone) Horizon() (Horizon, bool) { h, ok := c.inner.Horizon() if !ok { return Horizon{}, false } return Horizon{Checkin: h.Hash(), Rule: HorizonRule(h.Rule())}, true } // WithHorizon returns the cone with its horizon replaced by h, keeping the path // members; nil drops the horizon. Dropping the horizon of a horizon-only cone // is an error, since nothing is left: make the store dense with // Repo.SetStoreCone(nil). func (c *Cone) WithHorizon(h *Horizon) (*Cone, error) { if h == nil { inner, err := cone.New(c.inner.Include(), c.inner.Exclude()) if err != nil { return nil, fmt.Errorf("libfossil: cone: drop horizon: %w", err) } return &Cone{inner: inner}, nil } return NewHorizonCone(c.inner.Include(), c.inner.Exclude(), *h) } // wrapCone is nil for nil, the dense checkout's cone. func wrapCone(inner *cone.Cone) *Cone { if inner == nil { return nil } return &Cone{inner: inner} } // unwrapCone is nil for nil. func unwrapCone(c *Cone) *cone.Cone { if c == nil { return nil } return c.inner } // Include returns the inclusion members, sorted. func (c *Cone) Include() []string { return c.inner.Include() } // Exclude returns the exclusion members, sorted. func (c *Cone) Exclude() []string { return c.inner.Exclude() } // Covers reports whether the cone covers path. func (c *Cone) Covers(path string) bool { return c.inner.Covers(path) } // Decide reports whether the cone covers path and the member that decided: // the longest member covering it, of either polarity, or "" when none does. // Under longest match the deciding member is often not the first one listed, // so naming it is what explains a verdict. func (c *Cone) Decide(path string) (covered bool, member string) { d := c.inner.Decide(path) return d.Covered, d.Member } // Digest is the SHA3-256 of the cone's canonical serialization, in hex: equal // for equal coverage, and the same value zit computes. func (c *Cone) Digest() string { return c.inner.Digest() } // String lists the members in sorted order, an exclusion marked with '!'. A // horizon leads, as "horizon <check-in> <rule> content". func (c *Cone) String() string { return c.inner.String() } // With returns the cone with more members: the incremental form of widening // (or, with exclusions, of narrowing). func (c *Cone) With(include, exclude []string) (*Cone, error) { inner, err := c.inner.With(include, exclude) if err != nil { return nil, fmt.Errorf("libfossil: cone: %w", err) } return &Cone{inner: inner}, nil } // Without returns the cone with every member equal to one of paths removed, // of either polarity. A path that is not a member removes nothing. func (c *Cone) Without(paths []string) (*Cone, error) { inner, err := c.inner.Without(paths) if err != nil { return nil, fmt.Errorf("libfossil: cone: %w", err) } return &Cone{inner: inner}, nil } // ConeChange reports what Checkout.SetCone did to the working directory. type ConeChange struct { Materialized []string // newly covered files written, sorted Dematerialized []string // newly uncovered files removed, sorted } // Cone returns the checkout's cone, or nil when the checkout is dense. func (c *Checkout) Cone() *Cone { return wrapCone(c.inner.Cone()) } // SetCone replaces the checkout's cone; nil makes it dense. Files of the // checked-out version that the new cone covers and the old one did not are // written; files it no longer covers are removed from the working directory // (the repository keeps them). It refuses, changing nothing, when a file // leaving the cone has local changes or a file entering it would overwrite an // untracked file. A cone with a horizon is refused with ErrHorizonCheckout. func (c *Checkout) SetCone(next *Cone) (ConeChange, error) { inner, err := checkoutCone(next) if err != nil { return ConeChange{}, err } res, err := c.inner.SetCone(inner) if err != nil { return ConeChange{}, fmt.Errorf("libfossil: set cone: %w", err) } return ConeChange{Materialized: res.Materialized, Dematerialized: res.Dematerialized}, nil }