Artifact bbdcb40dc1dfba988c7a50875305ea3ffa0a5e3c5b99149a991553480a48fc12:
- File
internal/sync/horizon_mergebase.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: 6205) [annotate] [blame] [check-ins using] [more...]
package sync import ( "context" "database/sql" "errors" "fmt" "slices" "strings" libfossil "github.com/danmestas/go-libfossil/internal/fsltype" "github.com/danmestas/go-libfossil/internal/hash" "github.com/danmestas/go-libfossil/internal/merge" "github.com/danmestas/go-libfossil/internal/repo" "github.com/danmestas/go-libfossil/internal/xfer" ) // horizonMergeBasePragma asks an origin for Fossil's pivot of a primary // check-in over one or two secondaries, `pragma horizon-mergebase PRIMARY // SECONDARY...`, and carries its answer, the query's hashes repeated in the // order asked with the base appended, - when they share no ancestor // (draft-fossil-horizon-00 §6.2.4). const horizonMergeBasePragma = "horizon-mergebase" // noMergeBase is the answer naming no common ancestor. const noMergeBase = "-" // maxMergeBaseSecondaries is the most secondaries one query carries. A card // handler sees six tokens, so an answer repeats at most three hashes before // the base (§6.2.4); an origin answers no query naming more. const maxMergeBaseSecondaries = 2 // mergeBaseNodes bounds the check-ins one merge-base answer visits (§8: the // bound of the origin's other graph walks, horizon.HeldSet's among them). const mergeBaseNodes = 1 << 20 // MergeBaseQuery is one merge-base question (§6.2.4), every check-in named // by full hash: in a merge the primary is the check-in merged in, and the // secondaries the checkout's version and each plain merge pending in it. // The zero value asks nothing. type MergeBaseQuery struct { Primary string Secondaries []string } // hashes is the query as the wire spells it, primary first. func (q MergeBaseQuery) hashes() []string { return append([]string{q.Primary}, q.Secondaries...) } // valid reports whether q is a query a card can carry: a primary and one // or two secondaries, every one a well-formed hash. func (q MergeBaseQuery) valid() bool { if len(q.Secondaries) == 0 || len(q.Secondaries) > maxMergeBaseSecondaries { return false } for _, h := range q.hashes() { if !hash.IsValidHash(h) { return false } } return true } // parseMergeBaseQuery reads a merge-base query, or ok false, which leaves // the pragma ignored like any it does not know: a malformed hash, or more // secondaries than an answer could repeat. func parseMergeBaseQuery(p *xfer.PragmaCard) (MergeBaseQuery, bool) { if len(p.Values) < 2 { return MergeBaseQuery{}, false } q := MergeBaseQuery{Primary: p.Values[0], Secondaries: slices.Clone(p.Values[1:])} return q, q.valid() } // answerMergeBase appends the answer to the request's merge-base query, if // it carried one this origin can answer. It answers in a pull's reply only, // where zit answers it, so the requester has read access to the history the // base is drawn from. func (h *handler) answerMergeBase(ctx context.Context) error { q := h.mergeBaseQuery if q.Primary == "" || !h.pullOK { return nil } base, ok, err := originMergeBase(ctx, h.repo, q, mergeBaseNodes) if errors.Is(err, merge.ErrBound) { return nil // unanswered: the client reads it as "deepen by hand" } if err != nil || !ok { return err } h.resp = append(h.resp, &xfer.PragmaCard{Name: horizonMergeBasePragma, Values: append(q.hashes(), base)}) return nil } // originMergeBase is the base of q, Fossil's pivot of its primary over its // secondaries (merge.FindPivot), bounded by limit: the base's hash, or // noMergeBase when they share no ancestor. ok is false when any named // check-in is not one the repository holds, or the walk reaches one it does // not hold: a store answers only from history it holds, since - would be a // claim about history it cannot see. func originMergeBase(ctx context.Context, r *repo.Repo, q MergeBaseQuery, limit int) (string, bool, error) { if ctx == nil || r == nil { panic("sync.originMergeBase: nil context or repo") } if !q.valid() { panic("sync.originMergeBase: malformed query") } primary, ok, err := heldCheckin(r, q.Primary) if err != nil || !ok { return "", false, err } var secondaries []libfossil.FslID for _, s := range q.Secondaries { rid, ok, err := heldCheckin(r, s) if err != nil || !ok { return "", false, err } if rid != primary { secondaries = append(secondaries, rid) } } base := primary if len(secondaries) == len(q.Secondaries) { p, err := merge.FindPivot(ctx, r, merge.PivotQuery{Primary: primary, Secondaries: secondaries, Limit: limit}) if p.Unheld { return "", false, nil } if errors.Is(err, merge.ErrNoCommonAncestor) { return noMergeBase, true, nil } if err != nil { return "", false, err } base = p.RID } var uuid string err = r.DB().QueryRow(`SELECT uuid FROM blob WHERE rid=? AND size>=0 AND EXISTS(SELECT 1 FROM event WHERE objid=blob.rid AND type='ci')`, base).Scan(&uuid) if errors.Is(err, sql.ErrNoRows) { return "", false, nil } if err != nil { return "", false, fmt.Errorf("sync: merge base: %w", err) } return uuid, true, nil } // heldCheckin resolves a full hash to a check-in the repository holds. func heldCheckin(r *repo.Repo, uuid string) (libfossil.FslID, bool, error) { var rid libfossil.FslID err := r.DB().QueryRow(`SELECT rid FROM blob WHERE uuid=? AND size>=0 AND EXISTS(SELECT 1 FROM event WHERE objid=blob.rid AND type='ci')`, strings.ToLower(uuid)).Scan(&rid) if errors.Is(err, sql.ErrNoRows) { return 0, false, nil } if err != nil { return 0, false, fmt.Errorf("sync: merge base: %w", err) } return rid, true, nil } // mergeBaseAnswer reads an answer to the query asked: the base's hash, or "" // when the origin names no common ancestor. ok is false for any other pragma // of the name, one whose hashes do not repeat the query in order among them, // which is ignored. func mergeBaseAnswer(p *xfer.PragmaCard, asked MergeBaseQuery) (string, bool) { want := asked.hashes() if len(p.Values) != len(want)+1 { return "", false } for i, h := range want { if !strings.EqualFold(p.Values[i], h) { return "", false } } base := p.Values[len(want)] if base == noMergeBase { return "", true } if !hash.IsValidHash(strings.ToLower(base)) { return "", false } return strings.ToLower(base), true }