zit 0.8.0 carries Git submodules through an import and back out. Each
check-in's submodule pins live in a tracked file at the root of its tree,
.zit-modules. export-git turns that file back into gitlinks, and
zit modules checks the modules out. This note records why it is a file,
what the file says, how import and export use it, and what it costs.
The gap
Git records a submodule as a gitlink: a tree entry of mode 160000 that
names a commit in another repository. Fossil's artifact format has no
entry like it. A manifest's F cards name files, and an F card's mode is
plain, executable or symlink. fossil import --git therefore drops every
gitlink, and zit did the same. The import also kept .gitmodules, which
holds a module's path and url but not the commit, so the pin was gone
without any message. Exported back, the history had a .gitmodules and
no gitlinks. Git treats that as a broken superproject.
Why a file
A new card, a new artifact or a side table would each change what a
store holds. Every Fossil peer would then have to understand it, or would
silently drop it on sync. A tracked file needs nothing. Every peer
already stores, syncs, diffs and checks out files, and a file is versioned
with the check-in it belongs to, which is exactly a pin's lifetime. The
cost is that a stock Fossil checkout shows the ledger as an ordinary file
and does nothing with it. That is the same thing Fossil does with
.gitmodules today.
The ledger
One module per line, sorted by path in byte order (the order F cards use), four fields separated by single spaces:
path kind url pin
pathis the module's place in the tree, spelled with the F-card filename escapes, so a space is\sand the path stays one token.kindisgit, a pin on a Git commit, orzit, a pin on a zit check-in.urlis where the module lives, escaped the same way, or-when no.gitmodulesentry names one. A relative url (./,../) resolves against the parent's remote, as Git resolves it.pinis a full lowercase hash, 40 or 64 hex digits, never a prefix or a branch name.
A ledger that breaks any of these rules is refused by line number rather than read loosely, both when zit reads one and before zit writes one. A tree with no modules holds no ledger.
Import
For each commit, the import takes the first parent's pins and applies
the commit's own gitlink commands: a new or moved gitlink sets a pin; a
delete, or a file written over the path, removes it; and a rename or copy
moves it. The urls come from that commit's .gitmodules. The import
writes the ledger when the pins change, deletes it when the last gitlink
goes, and leaves it out of every commit that never had one.
Blobs reach the import before the commit that names their path, so the
import keeps a copy of any small blob that looks like a .gitmodules,
which in practice is only .gitmodules files. A repository without
submodules keeps nothing and pays one scan per commit. With --into, the
pins of a commit already in DEST are read back from its stored ledger.
A source repository's own root .zit-modules is left out of the import,
as other names zit keeps are, so a ledger in DEST is always one the import
wrote.
Export
export-git does not copy the ledger. Each git line becomes a gitlink
naming its pin. A module dropped from the ledger is deleted ahead of the
commit's file commands, and a new pin is written after them, so a file and
a submodule can trade a path within one commit. Tested on histories with
a submodule replaced by a file on a side branch, a pin with no url, a path
with a space, and the last module removed: the exported commits' tree IDs
equal the source's, gitlink rows included.
zit modules
A zit line names a nested zit checkout. zit modules sync clones one
that is not synced into its path and updates it to its pin, pulling only
when the pin is not held yet. A horizon parent clones each module as a
structural horizon clone at its pin. zit modules pin records a module's
current check-in for the next commit, and zit status names a module that
is off its pin. extras, add and clean stop at a nested checkout, so
the parent never takes a module's files as its own. Nothing commits or
pushes a module; it is a repository of its own.
A git line pins a Git commit, which zit cannot check out, so sync
names it and leaves it alone.
--with-submodules
import-git --with-submodules imports each submodule beside the parent
first. It collects every url that any .gitmodules in the source's
history names, mirrors each with Git, and imports it into a sibling of
DEST named <name>.zit, or adds to that sibling with --into when it is
already an import. A pin on a commit the sibling holds is then written as
a zit line naming that check-in and ../<name>.zit. On export, a zit
line is mapped back to its Git commit through the sibling's import map.
A pin the sibling has no commit for refuses the export, naming the path
and the sibling, rather than writing a gitlink to nowhere.
A module that cannot be fetched or imported is reported and its pins stay
git lines. The parent import never refuses because of a neighbour.
What it costs
- A history with submodules imports to different check-in names than 0.7.5 gave it, since its trees now hold the ledger. A history without them imports byte for byte as before.
- zit never rewrites a check-in. 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. A fresh import has the full ledger at every commit. - Stores and the wire are unchanged.