zit 0.8.0 carries Git submodules. import-git keeps each commit's
submodule pins in a tracked .zit-modules file, and export-git writes
them back as gitlinks, so a Git history with submodules goes through zit
and back with the same trees. zit modules lists, syncs and pins the
modules a checkout names, and import-git --with-submodules imports each
submodule beside the parent so its pins name zit check-ins. A squash-
merged pull request is recorded as Fossil's cherry-pick of the branch it
took, so the branch no longer ends unjoined on the timeline.
This release is from trunk. Stores are unchanged from 0.7.5.
Submodule pins survive a Git round trip
Git records a submodule as a gitlink: a tree entry of mode 160000 naming
a commit in another repository. Fossil's artifact format has no such
entry, so fossil import --git drops the pin, and so did zit, silently.
Exported back, the history had a .gitmodules and no gitlinks, which Git
treats as a broken superproject.
Now each commit's pins go into a tracked file at the root, .zit-modules,
one module per line, sorted by path:
lib git https://example.test/lib.git 1111111111111111111111111111111111111111
The four fields are path (with the F-card filename escapes, \s for a
space), kind (git or zit), url (from that commit's .gitmodules, or
- when it names none) and the full pin. .gitmodules stays a tracked
file. Fossil and older zits see .zit-modules as a plain file; the
artifact format is untouched. The import writes the file only where a
commit's tree holds a gitlink, and removes it with the last one.
export-git turns the ledger back into gitlinks instead of copying it:
each git line becomes a mode-160000 entry naming its pin. Measured on
the test histories, the exported commits' tree IDs equal the source's,
gitlink rows included.
The import says how many submodule paths it pinned, on stderr and as
submodules.paths in the -F json result. A source repository's own
.zit-modules at the root is left out, as other names zit keeps are.
The design is recorded in the technote "Submodule pins in .zit-modules".
zit modules
A zit module is a nested zit checkout at its path.
zit moduleslists each module's path, kind, pin and state: at pin, off pin and where the checkout is instead, not synced, or not a zit checkout.zit modules sync [PATH..]clones each zit module that is not synced into its path and updates every one to its pin, pulling only when the pin is not yet held. A relative url resolves against the saved remote, as Git resolves it. In a horizon checkout the clone is a structural horizon clone at the pin.--recursivesyncs the modules' own modules, to a bounded depth, once per url.zit modules pin PATH..records a module's current check-in as its pin for the next commit.zit statusnames each zit module that is not at its pin.
Nothing commits or pushes a module: it is a repository of its own. A git
line pins a Git commit zit cannot check out; sync names it and leaves it
alone.
import-git --with-submodules
With --with-submodules, every submodule any .gitmodules in the
source's history names is first mirrored with Git and imported into a
sibling of DEST, <name>.zit, or added to with --into when that sibling
is an import already. A pin on a commit the sibling holds is then written
as a zit line naming that check-in and ../<name>.zit, which
zit modules sync can check out. export-git maps a zit line back to
its Git commit through the sibling's import map, and refuses a pin it
cannot map, naming the path and the sibling.
A module that cannot be fetched or imported is reported and its pins stay
git lines; the parent import goes on without it.
A squash merge is a cherry-pick of its pull request
A pull request merged by squashing leaves one new commit on the default branch with a single parent, so its branch's commits never become its ancestors and the branch ends in mid-air on a mirror's timeline. Fossil already has the relation a squash is: the cherry-pick, drawn as a dashed line from what was taken to the check-in that took it.
When the source holds refs/pull/N/head (GitHub keeps it for every pull
request, after its branch is deleted too) and a single-parent commit on
the default branch's first-parent chain has a subject ending (#N), the
import gives that check-in a Q +HASH card naming the pull request's
head. A head that is already the commit's ancestor, as after a merge or a
fast-forward, gets none, and neither does a commit whose head the import
has not seen. A source without pull refs imports exactly as before. The
import says how many squashes it linked.
A mirror needs refs/pull/*/head in the source clone for this, for
example with a fetch refspec of +refs/pull/*/head:refs/pull/*/head.
The rule is recorded in the technote "A squash merge is a cherry-pick".
The tree diff reads only what changed
The diff between two sub-manifest trees, behind the timeline's file lists
and everything that asks what a check-in changed, paired sub-manifests by
name. When a partition boundary moved, or the two trees had different
heights at the same name, it read whole subtrees and filtered the extra
rows afterwards: the cost tracked the subtree, not the change. It now
walks the two trees side by side in name order, skips any pair of
sub-manifests with the same hash unread, and opens only the taller of two
that tie, so it reads only the sub-manifests on changed paths and reports
exactly the files that differ. The technique is libfossil's
(fsl__deck_tree_diff).
Known limits
zit modules sync checks out zit modules only. A git line needs the
parent re-imported with --with-submodules, or Git.
A store's check-ins are never rewritten. So:
- A mirror imported before 0.8.0 that continues with
--intogets a ledger only from the first commit that changes a gitlink, and it lists only the pins changed since. Its older check-ins hold no ledger to start from. A fresh import has a complete ledger at every commit. - A squash already in a store keeps no
Qcard. A fresh import links every squash whose pull request head the source holds.
Compatibility
Stores and the wire are unchanged from 0.7.5. The submodule ledger is an ordinary tracked file, so every Fossil peer and every earlier zit stores, syncs and checks it out as one.
An import of a history with submodules now writes .zit-modules into the
commits that have gitlinks, and an import of a history with paired squash
merges writes Q cards, so those check-ins' names differ from what 0.7.5
gives the same history. A history with neither imports byte-for-byte as
0.7.5 imports it.
Qualification
The release manifest records native qualification against the exact tagged source archive on macOS ARM64, Linux x86-64 and Linux ARM64, using the pinned Zig 0.17.0 toolchain. Publication is gated on unit and end-to-end tests, Git differentials, deterministic simulation, seeded fuzzing, and Fossil interoperability checks on every distributed target.
For the basis of the conformance claim, see the 0.1.0 release notes.