Artifact 69647bb0d6a5ee1b0d76b52e6d0378ecf7a848597f41481d03081a9e274e8d70:
- File docs/rfcs/erratum/repo-state-00-s7.3-root-leaf.md — part of check-in [ad32a25dd2] at 2026-10-13 14:26:00 on branch trunk — repo-state §7.3: recheck leaf status only on branch-tag events, as Fossil does (user: dmestas, size: 7280) [annotate] [blame] [check-ins using]
Erratum — draft-fossil-repo-state-00 §7.3
| Document | draft-fossil-repo-state-00 |
| Section | 7.3 — Maintenance algorithm (full rebuild; deferred scheduling) |
| Type | Technical (two divergences from the reference implementation; the draft's leaf definition is unaffected) |
| Status | Applied in draft-fossil-repo-state-01 (2026-10-12); the applied text was then revised by repo-state-01-s7.3-leaf-recheck-branch-tag.md |
| Reported | 2026-10-11 |
| Submitter | Dan Mestas |
| Found via | go-libfossil's conformance suite against Fossil 2.28; confirmed by reading Fossil's source and running it |
1. Summary
§7.2's leaf test and §7.3's full rebuild agree: a check-in with no same-branch child is a leaf, and §7.3 says outright that a check-in with no parent "is a leaf when it has no same-branch child, and MUST be included." Fossil keeps its leaf table by a narrower rule, in two places:
- Its full recompute excludes every check-in with no parent. The query
draws its candidates from the parent-link table's child column, so a root
is never a candidate. On a new repository,
fossil leaves --recomputeleaves no leaves at all. - Its incremental maintenance runs only on branch-tag events. A leaf recheck is scheduled when a branch tag is set on a check-in or propagated to it, never when a parent-link is created. A check-in with no parent and no branch tag receives no such event, so it never becomes a leaf.
Every check-in Fossil itself writes carries a branch tag (its own, or one
propagated from its parent), so for Fossil-authored history the two rules
agree, except under --recompute. They differ for a parentless check-in with
no branch tag, such as the §A.1.1 example of the artifact-format draft: under
the draft it is a leaf, and in Fossil it is not.
2. Location
§7.3, two bullets. Current text:
Full rebuild: … The candidate set is every check-in, not only those that appear as a child of some parent-link. A check-in with no parent at all (for example the sole check-in of a new repository, or any root check-in) is a leaf when it has no same-branch child, and MUST be included.
Deferred scheduling: implementations MUST schedule an incremental leaf recheck for the affected check-in and for each of its direct parents when a parent-link is created or a branch tag is applied or propagated.
3. The defects
Verified against Fossil 2.28 by reading the source and by running it.
3.1 The full recompute has no roots
leaf_rebuild() (src/leaf.c:85):
DELETE FROM leaf;
INSERT OR IGNORE INTO leaf
SELECT cid FROM plink
EXCEPT
SELECT pid FROM plink WHERE <parent and child on the same branch>
Its only caller is fossil leaves --recompute (src/descendants.c). Plain
fossil rebuild does not call it. On a repository holding only the initial
check-in of fossil new:
$ fossil leaves # (1) [eeb2389b69] initial empty check-in
$ fossil leaves --recompute # nothing; the leaf table is empty
The draft's rule is the coherent one: the incremental path keeps that same check-in as a leaf, so Fossil's two paths disagree with each other. This half is a Fossil defect, and the draft needs no change for it beyond recording it.
3.2 Scheduling is driven by branch tags alone
leaf_eventually_check() (src/leaf.c:172), which queues a check-in and its
parents for a recheck, has exactly two callers, both in src/tag.c: setting a
branch tag on a check-in (:195) and propagating one to a child (:101).
Crosslinking a check-in and its parent-links schedules nothing. A parentless
check-in with no branch tag therefore never reaches leaf_check():
Check-in in one repository (Fossil 2.28, after fossil rebuild) |
parents | branch tag | leaf |
|---|---|---|---|
eeb2389b69, the initial check-in of fossil new |
none | trunk |
yes |
a28c83647d, the §A.1.1 artifact, stored with test-content-put |
none | none | no |
4. Impact
Observable, rare. zit leaves follows the draft (src/repostate.zig: the leaf
candidates are every check-in) and lists a parentless check-in with no branch
tag. fossil leaves on the same repository does not. Fossil never writes such
a check-in, so it reaches a repository only by import or by synthesis.
fossil leaves --recompute is more serious for Fossil users, since it drops the
sole leaf of a new repository and every root of a multi-root one, but that is
Fossil's to fix.
5. Correction
The leaf definition (§7.1, §7.2) and the full-rebuild bullet stay as they are. Two options for the rest, which are an operator decision.
Option A (recommended): keep the draft's rule, and record Fossil's divergence. The draft has no compatibility appendix, so add an informative paragraph at the end of §7.3:
This paragraph is informative. Fossil's full leaf recompute (fossil
leaves --recompute) draws its candidates from the child column of the
parent-link table, so it omits every check-in with no parent,
check-ins this section requires to be included. Fossil's incremental maintenance schedules a
recheck only when a branch tag is set on a check-in or propagated to
it, so a check-in with no parent and no branch tag never enters its
leaf table. Every check-in Fossil writes carries a branch tag, so the
two agree on Fossil-authored history.
and make the scheduling bullet say what the leaf test needs, so that every check-in is rechecked at least once:
- parent-link is created or a branch tag is applied or propagated.
+ check-in is crosslinked, when a parent-link is created, or when a
+ branch tag is applied or propagated.
Option B: describe Fossil. Narrow §7.3 so that a check-in is a leaf
candidate only when it is the child of a parent-link or carries a branch tag,
and drop "MUST be included" for parentless check-ins. This matches Fossil's
incremental path, but writes its --recompute defect into the draft, and
contradicts §7.2's test for an untagged root.
6. Rationale
- §7.2 is the normative test. A parentless check-in has no parent-link naming it as parent, so §7.2 makes it a leaf. Option B makes §7.3 disagree with §7.2; option A keeps them in step.
- Fossil disagrees with itself. Its two paths give different answers for a tagged root, so there is no single Fossil behavior to describe; the draft would have to choose one of them anyway.
- The divergence costs Fossil users nothing in practice. Fossil never writes a parentless check-in without a branch tag, so option A changes nothing a Fossil-only user sees.
7. Verification
- Source: Fossil 2.28
src/leaf.c(leaf_rebuild,leaf_check,leaf_eventually_check),src/tag.c:101,:195, andsrc/descendants.c(the oneleaf_rebuildcaller). - Run:
fossil new(one leaf);fossil leaves --recompute(no leaves); store the §A.1.1 artifact withfossil test-content-putandfossil rebuild(it is not a leaf, while the initial check-in is).
Prepared against draft-fossil-repo-state-00. The RFC remains the oracle; the
reference implementation is used only to confirm the observable behavior.