Skip to content
zit Submodule pins in .zit-modules
Login

Submodule pins in .zit-modules

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

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