Artifact d06a184e0faf171f7676d4a9d8e25e4dd9f0c151d33365a98d61ecb07510671a:
- File
required_content_merge.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: 5383) [annotate] [blame] [check-ins using] [more...]
package libfossil import ( "context" "database/sql" "errors" "fmt" "github.com/danmestas/go-libfossil/db" "github.com/danmestas/go-libfossil/internal/content" "github.com/danmestas/go-libfossil/internal/fsltype" internalsync "github.com/danmestas/go-libfossil/internal/sync" ) // ErrBelowHorizon is matched by the error of an operation that needs history // below the store's structural horizon (draft-fossil-horizon-00 §5.5), such // as a merge whose base lies there. The message names the horizon and the // remedy, a deepen (Repo.DeepenHorizon). var ErrBelowHorizon = content.ErrBelowHorizon // mergeBaseNodes bounds the walk that decides whether a merge's ancestry // crosses the horizon, as the origin bounds its merge-base walk. const mergeBaseNodes = 1 << 20 // maxMergeBaseSecondaries is the most secondaries a merge-base query carries // (draft-fossil-horizon-00 §6.2.4): the checkout and one pending merge. const maxMergeBaseSecondaries = 2 // MergeContext is Merge with the remedy of draft-fossil-horizon-00 §5.5. // Under a structural horizon, when Peer is given and the ancestry of either // side crosses the horizon, it asks the origin for the base (§6.2.4) even // when the store's own walk finds one, since over part of the graph the // common ancestor found need not be the nearest. It deepens the horizon to // the base the origin names when the store does not hold it // (Repo.DeepenHorizon) and merges over that base. When the origin does not // answer it is Merge, which refuses a merge whose own walk crossed the // horizon. Without a Peer, or when nothing crosses, it is Merge. A merge // never runs against a base the store does not hold. // // The query names the merged-in version over the checkout's version and each // plain merge pending in it. It carries at most two such secondaries, so a // merge that must ask with two merges already pending is refused: asking // without them would name a base Fossil's merge would not choose. func (c *Checkout) MergeContext( ctx context.Context, opts CheckoutMergeOpts, required RequiredContentOpts, ) (CheckoutMergeResult, error) { if err := required.validate(ctx); err != nil { return CheckoutMergeResult{}, err } if required.Peer == nil { return c.Merge(opts) } r := c.inner.Repo() secondaries, err := c.inner.MergeSecondaries() if err != nil { return CheckoutMergeResult{}, fmt.Errorf("libfossil: merge: %w", err) } starts := append([]fsltype.FslID{fsltype.FslID(opts.Version)}, secondaries...) crosses, err := content.AncestryCrosses(r.DB(), starts, mergeBaseNodes) if err != nil { return CheckoutMergeResult{}, fmt.Errorf("libfossil: merge: %w", err) } if !crosses || opts.Version == int64(secondaries[0]) { return c.Merge(opts) } if len(secondaries) > maxMergeBaseSecondaries { return CheckoutMergeResult{}, fmt.Errorf("libfossil: merge: %w: the merge base may lie below it, and a "+ "merge-base query carries the checkout and at most one pending merge (horizon §6.2.4), "+ "not the %d pending here; commit or undo the pending merges first", ErrBelowHorizon, len(secondaries)-1) } q := internalsync.MergeBaseQuery{} for i, rid := range starts { var uuid string if err := r.DB().QueryRow("SELECT uuid FROM blob WHERE rid=?", rid).Scan(&uuid); err != nil { return CheckoutMergeResult{}, fmt.Errorf("libfossil: merge: %w", err) } if i == 0 { q.Primary = uuid } else { q.Secondaries = append(q.Secondaries, uuid) } } peer := &transportAdapter{pub: required.Peer} ask := required.internalSync() ask.MergeBaseOf = q answer, err := internalsync.Sync(ctx, r, peer, ask) if err != nil { return CheckoutMergeResult{}, fmt.Errorf("libfossil: merge: asking the origin for the merge base: %w", err) } if !answer.MergeBaseAnswered { res, err := c.Merge(opts) if errors.Is(err, ErrBelowHorizon) { return res, fmt.Errorf("%w; the origin did not answer a merge-base query", err) } return res, err } if answer.MergeBase == "" { return CheckoutMergeResult{}, fmt.Errorf("libfossil: merge: %s shares no history with %s, the origin says", q.Primary, q.Secondaries[0]) } base, held, err := heldCheckin(r.DB(), answer.MergeBase) if err != nil { return CheckoutMergeResult{}, fmt.Errorf("libfossil: merge: the base %s: %w", answer.MergeBase, err) } if !held { if _, err := internalsync.Deepen(ctx, r, peer, answer.MergeBase, required.internalSync()); err != nil { return CheckoutMergeResult{}, fmt.Errorf("libfossil: merge: deepening to the base %s: %w", answer.MergeBase, err) } if base, _, err = heldCheckin(r.DB(), answer.MergeBase); err != nil { return CheckoutMergeResult{}, fmt.Errorf("libfossil: merge: the base %s: %w", answer.MergeBase, err) } } // The store still holds part of the graph, so its own walk may cross // the horizon again: the origin's base is the merge's, as --baseline. return c.merge(opts, base) } // heldCheckin is the rid of the artifact named uuid, 0 when the store has no // such artifact, and whether it holds it as a check-in. func heldCheckin(q db.Querier, uuid string) (int64, bool, error) { var rid int64 var held bool err := q.QueryRow(`SELECT rid, size>=0 AND EXISTS(SELECT 1 FROM event WHERE objid=rid AND type='ci') FROM blob WHERE uuid=?`, uuid).Scan(&rid, &held) if errors.Is(err, sql.ErrNoRows) { return 0, false, nil } return rid, held, err }