Artifact 64ed78e5fc75c469128c00c93a77783ae0230c73b0c6f78601c848912424b3a5:
- File
internal/content/horizon_crossing.go
— part of check-in
[78483726d9]
at
2026-10-23 01:41:07
on branch feat/horizon-cones
— feat(sync): ask the list-form merge base whenever a merge crosses
The re-pinned draft's 6.2.4 query is a primary and one or two secondaries, `pragma horizon-mergebase <primary> <secondary>...`, and the answer repeats the query in order and appends Fossil's pivot over the set. A two-hash query was answered as the base of a pair, so a merge into a checkout with a merge already pending could be told a base Fossil's merge would not choose. The origin now takes the list (sync.MergeBaseQuery), answers nothing when it lacks any named check-in or is asked more secondaries than a six-token card can repeat, and the client takes only an answer that repeats its query.
5.5 now has the clone ask whenever either side's ancestry crosses the horizon, not only when its own walk does: over part of the graph the common ancestor found need not be the nearest. MergeContext, given a peer, checks the ancestry of the merged-in version, the checkout and its pending plain merges (content.AncestryCrosses), asks the pivot of the merged-in version over the checkout and those merges, and merges over the answer, falling back to Merge when the origin does not answer, as zit's merge does. With two merges already pending the query cannot carry them, and asking without them would name another base, so the merge is refused before anything changes.
The deepen point's query names the target over C, as zit asks it. (user: [email protected], size: 5057) [annotate] [blame] [check-ins using] [more...]
package content import ( "errors" "fmt" "github.com/danmestas/go-libfossil/db" "github.com/danmestas/go-libfossil/internal/cone" libfossil "github.com/danmestas/go-libfossil/internal/fsltype" ) // ErrBelowHorizon is matched by the diagnostic of an operation that needs // history below the store's structural horizon (draft-fossil-horizon-00 // §5.5): the store does not hold it, and a deepen is what brings it. var ErrBelowHorizon = errors.New("history below the structural horizon") // MergeBaseError is a merge whose base lies below the structural horizon at // Horizon (§5.5), or may: Base is the base the store's walk found when the // store does not hold it, "" when the walk crossed the horizon, so that only // the origin can name the base. Side is the check-in being merged in. type MergeBaseError struct { Horizon, Side, Base string } func (e *MergeBaseError) Error() string { if e.Base != "" { return fmt.Sprintf("the merge base of %s is %s, below the structural horizon at %s: it is not held here; "+ "deepen the horizon to it (repo cone deepen %s), or merge with a remote to ask the origin and deepen", e.Side, e.Base, e.Horizon, e.Base) } return fmt.Sprintf("the merge base of %s may lie below the structural horizon at %s: its ancestry crosses the "+ "horizon, so only the origin can name it; merge with a remote to ask the origin for the base and deepen to it", e.Side, e.Horizon) } func (e *MergeBaseError) Is(target error) bool { return target == ErrBelowHorizon } // StructuralHorizon is the store cone's horizon when it is structural. func StructuralHorizon(q db.Querier) (cone.Horizon, bool, error) { if q == nil { panic("content.StructuralHorizon: q must not be nil") } c, err := StoreCone(q) if err != nil { return cone.Horizon{}, false, err } h := horizonOf(c) return h, h.Hash() != "" && h.Depth() == cone.DepthStructural, nil } // MergeBaseCrossing decides whether the merge of side crosses the store's // structural horizon (§5.5), given the base the store's own walk found, 0 // for none, and whether that walk reached a check-in the store does not // hold (merge.Pivot.Unheld). A base the store does not hold is a crossing. // So is any walk that reached the horizon: the store holds only part of the // graph, and a common ancestor found in part of it need not be the one the // whole graph gives, so the origin must be asked. It returns a // *MergeBaseError for a crossing and nil otherwise, so a merge never runs // against a base it does not hold or one the origin would not choose. func MergeBaseCrossing(q db.Querier, side, base libfossil.FslID, unheld bool) error { if q == nil { panic("content.MergeBaseCrossing: q must not be nil") } if side <= 0 || base < 0 { panic("content.MergeBaseCrossing: invalid rid") } h, structural, err := StructuralHorizon(q) if err != nil || !structural { return err } crossing := &MergeBaseError{Horizon: h.Hash()} if err := q.QueryRow("SELECT uuid FROM blob WHERE rid=?", side).Scan(&crossing.Side); err != nil { return fmt.Errorf("content.MergeBaseCrossing: %w", err) } if base > 0 { var held bool var uuid string if err := q.QueryRow(`SELECT size>=0 AND EXISTS(SELECT 1 FROM event WHERE objid=rid AND type='ci') AND NOT EXISTS(SELECT 1 FROM phantom WHERE phantom.rid=blob.rid), uuid FROM blob WHERE rid=?`, base).Scan(&held, &uuid); err != nil { return fmt.Errorf("content.MergeBaseCrossing: %w", err) } if !held { crossing.Base = uuid return crossing } } if unheld { return crossing } return nil } // AncestryCrosses reports whether the ancestry of any of starts, walked back // through every P card, reaches a parent the store does not hold as a // check-in, when the store is at a structural horizon (§5.5): the merge base // of such a check-in is then the origin's to name, even when a walk over // what the store holds finds one. False for any other store. A walk past // limit check-ins counts as crossing, since it cannot say otherwise. func AncestryCrosses(q db.Querier, starts []libfossil.FslID, limit int) (bool, error) { if q == nil { panic("content.AncestryCrosses: q must not be nil") } if len(starts) == 0 || limit <= 0 { panic("content.AncestryCrosses: needs a start and a positive limit") } if _, structural, err := StructuralHorizon(q); err != nil || !structural { return false, err } for _, start := range starts { if start <= 0 { panic("content.AncestryCrosses: invalid rid") } var walked int var crosses bool err := q.QueryRow(`WITH RECURSIVE anc(rid) AS ( SELECT ?1 UNION SELECT plink.pid FROM plink JOIN anc ON plink.cid=anc.rid LIMIT ?2) SELECT count(*), coalesce(max(NOT EXISTS(SELECT 1 FROM blob WHERE blob.rid=anc.rid AND size>=0) OR NOT EXISTS(SELECT 1 FROM event WHERE objid=anc.rid AND type='ci')), 0) FROM anc`, start, limit+1).Scan(&walked, &crosses) if err != nil { return false, fmt.Errorf("content.AncestryCrosses: %w", err) } if crosses || walked > limit { return true, nil } } return false, nil }