# zit 0.8.0

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 modules` lists 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. `--recursive` syncs 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 status` names 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 `--into` gets 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 `Q` card. 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](RELEASE-NOTES-0.1.0.md).
