| Document | draft-fossil-repo-state-00 |
| Section | 4.2 — Crosslink procedure, step 4 (propagation) |
| Type | Technical (the text diverges from Fossil's implemented behavior, and from §5.3 step 4 of the same draft) |
| Status | Proposed — operator decision pending |
| Reported | 2026-10-11 |
| Submitter | Dan Mestas |
| Found via | Cross-check of the draft against Fossil 2.28 src/tag.c; confirmed by running Fossil 2.28 |
1. Summary
§4.2 step 4 says a singleton (+) tag "affects only its target and triggers no walk". Fossil starts a walk for every tag it applies, singletons included: it downgrades the singleton to a cancel and walks from the target. The walk deletes the same-named tag that the target's descendants had inherited from an ancestor, so a singleton placed in the middle of a propagated run stops the inheritance below it.
§5.3 step 4 of the same draft already describes this ("When the tagged artifact is used as a propagation source, the cancel it becomes blocks a same-named tag from propagating through it"), so the two sections contradict each other. §5.3 matches Fossil; §4.2 does not.
2. Location
§4.2 step 4, last two sentences. Current text:
In addition, whenever a T-card in steps 1-3 applies a propagating (*) or cancel (-) tag to a resolved target, the propagation walk of Section 5.4 MUST be invoked rooted at that target, so that the applied or cancelled tag reaches the target's descendants. A singleton (+) tag affects only its target and triggers no walk.
3. The divergence
Verified against Fossil 2.28 by reading the source and by running it.
3.1 Source
tag_insert() (src/tag.c:157) is the single entry point for every T-card the
crosslink applies (src/manifest.c:2467) and for the implicit cluster tag
(src/manifest.c:2433). After writing the target's own row it ends with:
src/tag.c:239 if( tagtype==1 ) tagtype = 0;
src/tag.c:240 tag_propagate(rid, tagid, tagtype, rid, zValue, mtime);
There is no type test in front of the call: every applied tag starts a walk
rooted at its target. A singleton enters the walk as a cancel. In
tag_propagate() (src/tag.c:34), a cancel deletes a child's row when that
row was inherited (srcid=0) and is older than the singleton
(src/tag.c:57, the doit column), and the walk continues below every child
it changed (src/tag.c:91).
3.2 Run
A five-check-in chain on trunk (initial, c1, c2, c3, c4), using fossil tag
add --raw so the tag names are not prefixed. tagxref rows for foo, as
(tagtype, srcid, value):
| Check-in | after *foo=v1 on c2 |
after +foo=v2 on c3 (1 s later) |
|---|---|---|
| c1 | — | — |
| c2 | 2, direct, v1 | 2, direct, v1 |
| c3 | 2, 0, v1 | 1, direct, v2 |
| c4 | 2, 0, v1 | — |
c4's inherited row is gone. fossil rebuild produces the same table, so it is
the crosslink result, not an artifact of the tag command. Under the text of
§4.2, c4 would keep foo=v1.
4. Impact
Observable whenever a singleton names a tag that the target's descendants
inherit. The usual case is a singleton bgcolor or sym-* placed on a
check-in inside a propagated run: in Fossil it truncates the run, so the
descendants lose the inherited tag (and, for bgcolor, their event colour).
An implementation that follows §4.2 keeps the inherited rows, and its tag
state, tag find output and timeline colours differ from Fossil's for the
same artifacts.
5. Correction
Make §4.2 say what §5.3 step 4 and Fossil do:
- In addition, whenever a T-card in steps 1-3 applies a propagating
- (*) or cancel (-) tag to a resolved target, the propagation walk
- of Section 5.4 MUST be invoked rooted at that target, so that the
- applied or cancelled tag reaches the target's descendants. A
- singleton (+) tag affects only its target and triggers no walk.
+ In addition, whenever a T-card in steps 1-3 applies a tag of any
+ type to a resolved target, the propagation walk of Section 5.4
+ MUST be invoked rooted at that target, so that the applied or
+ cancelled tag reaches the target's descendants. A singleton (+)
+ tag is first downgraded to cancel (Section 5.3, step 4): its value
+ stays on the target, and the walk removes the same-named tag that
+ the target's descendants had inherited.
6. Rationale
- The draft already has the rule. §5.3 step 4 and the opening of §5.4 describe the downgrade and its blocking effect. Only §4.2 says otherwise; the correction removes the contradiction in favour of the sections that match Fossil.
- Fossil applies it on every path. The downgrade appears twice in
src/tag.c, at:239for a newly applied tag and at:131for the re-propagation of a parent's tags (tag_propagate_all), so a singleton blocks inheritance whether it is crosslinked before or after its descendants. Without the walk, the outcome would depend on crosslink order.
7. Verification
- Source: Fossil 2.28
src/tag.c:34-110(tag_propagate),:118-135(tag_propagate_all),:157-242(tag_insert);src/manifest.c:2433,:2467,:2472(the crosslink callers). - Run:
fossil new; four commits;fossil tag add --raw --propagate foo c2 v1;fossil tag add --raw foo c3 v2; querytagxrefforfooon every check-in;fossil rebuild; query again. c4 has nofoorow after the singleton, before and after the rebuild.
Prepared against draft-fossil-repo-state-00. The RFC remains the oracle; the
reference implementation is used only to confirm the observable behavior.