Skip to content
zit Artifact [aadd98a082]
Login

Artifact [aadd98a082]

Artifact aadd98a082ff12d868ad1ea7d8c044245acd0921f323cc2a1dd063a45b6dcf1e:


# 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) |
| **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:

1. **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 --recompute`
   leaves no leaves at all.
2. **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`):

```sql
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:

```diff
-      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`, and
  `src/descendants.c` (the one `leaf_rebuild` caller).
- Run: `fossil new` (one leaf); `fossil leaves --recompute` (no leaves); store
  the §A.1.1 artifact with `fossil test-content-put` and `fossil 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.*