Artifact 979c0d592afc636f5197263d3252982dae725452ec57ac4fcb066a4015d4dfb2:
- File
conformance/repostate_api_cases_test.go
— part of check-in
[cd4a11828f]
at
2026-10-12 20:17:22
on branch main
— fix(checkout): make WouldFork fossil's commit-time fork check (#347) (#348)
Since #291 WouldFork was fossil's leaf_ambiguity, which answers no at once for a version that is not a leaf. That is the case a commit forks in: when a pull brings in a child of the checked-out version, the version stops being a leaf, WouldFork said false, and the commit became a second child. EdgeSync's autosync relies on WouldFork to refuse that commit.
WouldFork is now fossil's commit refusal (src/checkin.c "Would fork", taken when !is_a_leaf(vid)): true when the version has a child on its own branch, read from plink as is_a_leaf does, merge children included. Another open leaf elsewhere on the branch no longer counts; fossil commits there, extending the checkout's leaf, and only warns afterwards.
The repo-state draft's Section 7.5 ambiguity test moves to checkout.LeafAmbiguous, and the repostate.7.5.ambiguity-* conformance cases call it. The superseded #291 changelog text and the WouldFork API doc are corrected. (user: [email protected], size: 5716) [annotate] [blame] [check-ins using]
package conformance import ( "fmt" "strings" ) func rsAPICases() []rsAPICase { var all []rsAPICase for _, table := range [][]rsAPICase{ resolveCases(), resolveFormCases(), leafAPICases(), commitLeafCases(), branchAPICases(), writerAPICases(), } { all = append(all, table...) } for i := range all { all[i].Gap = repostateGaps[all[i].ID] } return all } // forked is c1 with two trunk children, a and b, b the newer; b closes // itself when closed is set. func forked(closed bool) []rsArt { var tags []string if closed { tags = []string{"+closed *"} } return []rsArt{ checkin("c1", 1, nil, rsRoot...), checkin("a", 2, of("c1")), checkin("b", 3, of("c1"), tags...), } } // c2Prefix is the first n hex digits of chain's c2, which depends only on // c1 and c2, so a script may use it before it is built. func c2Prefix(n int) string { return rsBuild(chain()).hashes["c2"][:n] } func resolveCases() []rsAPICase { return []rsAPICase{ // Tagged later, c1 carries the newer tag row; c3 has the newer // check-in time, which is what the rule compares. resolves("repostate.8.2.event-mtime", "RS-8.2-1", with(chain(), control("t1", 8, "+sym-rel {c3}"), control("t2", 9, "+sym-rel {c1}")), "rel", "c3"), // c3 and c2 inherit sym-trunk from c1, with c1's tag time; only // their own times tell them apart. resolves("repostate.8.2.trunk-chain", "RS-8.2-1", []rsArt{ checkin("c1", 1, nil, rsRoot...), checkin("c3", 9, of("c1")), checkin("c2", 2, of("c1")), }, "trunk", "c3"), resolves("repostate.8.2.closed-newer", "RS-8.2-2", forked(true), "trunk", "b"), // c2, the newest trunk check-in, has a child, so is no leaf. resolves("repostate.8.2.not-leaf", "RS-8.2-3", []rsArt{ checkin("c1", 1, nil, rsRoot...), checkin("c2", 9, of("c1")), checkin("c3", 3, of("c2")), }, "trunk", "c2"), resolves("repostate.8.2.unknown", "RS-8.2-4", chain(), "nosuch", ""), resolves("repostate.8.2.no-trunk-sym", "RS-8.2-4", []rsArt{checkin("c1", 1, nil), checkin("c2", 2, of("c1"))}, "trunk", ""), resolves("repostate.9.fork-symbol", "RS-9-2", forked(false), "trunk", "b"), { Case: Case{ID: "repostate.8.5.resolution-not-leaves", Req: "RS-8.5-1"}, arts: forked(true), run: func(e *rsEnv) error { if err := resolves("", "", nil, "trunk", "b").run(e); err != nil { return err } return openLeaves("", "", nil, "trunk", "a").run(e) }, }, } } func resolveFormCases() []rsAPICase { release := with(chain(), control("t1", 8, "+sym-release {c3}")) // A tag named like c2's hash prefix, on c1. hexTag := with(chain(), control("t1", 8, "+sym-"+c2Prefix(6)+" {c1}")) mergedIn := []rsArt{ checkin("c1", 1, nil, rsRoot...), checkin("b1", 2, of("c1"), rsFeat...), checkin("c2", 3, of("c1")), checkin("b2", 4, of("b1", "c2")), checkin("b3", 5, of("b2")), } return []rsAPICase{ resolves("repostate.8.3.rid", "RS-8.3-1", chain(), "rid:{rid:c2}", "c2"), // Without the prefix the name would be read as c2's hash. resolves("repostate.8.3.tag-prefix", "RS-8.3-2", hexTag, "tag:"+c2Prefix(6), "c1"), // tip is the newest check-in on any branch, here b3 on feat. resolves("repostate.8.3.tip", "RS-8.3-3", with(branched(), checkin("b3", 6, of("b2"))), "tip", "b3"), resolves("repostate.8.3.root", "RS-8.3-5", branched(), "root:feat", "c1"), resolves("repostate.8.3.start", "RS-8.3-6", with(branched(), checkin("b3", 6, of("b2"))), "start:feat", "b1"), resolves("repostate.8.3.merge-in", "RS-8.3-7", mergedIn, "merge-in:feat", "c2"), resolves("repostate.8.3.name-timestamp", "RS-8.3-8", chain(), "trunk:2026-01-01T00:00:03", "c3"), resolves("repostate.8.3.hash-before-name", "RS-8.3-9", hexTag, c2Prefix(6), "c2"), resolves("repostate.8.3.ambiguous-prefix", "RS-8.3-10", ambiguous(), ambiguousPrefix(), ""), // fossil retries "release." but not a three-letter stem such as // "rel."; the case uses the longer name. resolves("repostate.8.3.trailing-punct", "RS-8.3-11", release, "release.", "c3"), } } // ambiguous is chain[:1] and a second root whose hash shares its first four // hex digits with c1's: the comment is varied until it does. func ambiguous() []rsArt { c1 := chain()[0] prefix := rsBuild([]rsArt{c1}).hashes["c1"][:4] for n := 0; n < 1<<22; n++ { twin := checkin("twin", 2, nil, rsRoot...) twin.body = strings.Replace(twin.body, "C twin", fmt.Sprintf("C twin%d", n), 1) if rsBuild([]rsArt{twin}).hashes["twin"][:4] == prefix { return []rsArt{c1, twin} } } panic("conformance: no artifact shares c1's hash prefix") } func ambiguousPrefix() string { return rsBuild(chain()[:1]).hashes["c1"][:4] } func leafAPICases() []rsAPICase { forkedChain := with(chain(), checkin("f", 5, of("c3"))) closedC4 := with(forkedChain, control("t", 9, "+closed {c4}")) return []rsAPICase{ openLeaves("repostate.7.4.closed-leaf", "RS-7.4-1", closedC4, "trunk", "f"), openLeaves("repostate.7.4.reopened", "RS-7.4-1", with(closedC4, control("t2", 10, "-closed {c4}")), "trunk", "c4", "f"), openLeaves("repostate.7.5.branch-open-leaves", "RS-7.5-1", merged(), "feat", "b1"), // BranchLeaves selects a branch's members by their active branch // row: b2, which inherits feat, is on feat; c2, a leaf too, is not. openLeaves("repostate.6.1.branch-membership", "RS-6.1-1", branched(), "feat", "b2"), openLeaves("repostate.9.branch-not-open", "RS-9-3", with(branched(), control("t", 9, "+closed {b2}")), "feat"), leafAmbiguous("repostate.7.5.ambiguity-non-leaf", "RS-7.5-2", forkedChain, "c3", false), leafAmbiguous("repostate.7.5.ambiguity-open", "RS-7.5-3", forkedChain, "f", true), leafAmbiguous("repostate.7.5.ambiguity-closed", "RS-7.5-3", closedC4, "f", false), } }