Status: Normative for the state/locking/discovery model specified
here; Draft because §5's out-of-scope surface (worktree
move/lock/repair) is not yet covered by this document.
Scope: the common-dir / per-worktree state split, the linked-tree
pointer-file format, the worktrees/ registry, repository discovery,
cross-worktree locking, and gc root-collection semantics. Issue #493.
Reference implementation: mkit-core/src/layout.rs (the
RepoLayout type is the single path-resolution authority; every
repo-state API takes it).
Every piece of repository state under .mkit/ belongs to exactly one
of two classes:
| Class | Contents |
|---|---|
| common dir (shared by all trees) | objects/, format, refs/ (heads, tags, remotes), shallow, config, keys/, history/, history-v1/, recovery-log, attestations/, applied-packs/, upload-parts/, git/, sparse/, pack-shards/, worktrees/, refs-history-<branch>.lock, refs-<ref>.lock, worktrees.lock, serve.lock (SPEC-CONCURRENCY §2) |
| worktree state dir (private to one tree) | HEAD, index, ORIG_HEAD, MERGE_HEAD/MERGE_MSG, CHERRY_PICK_HEAD/CHERRY_PICK_MSG, REVERT_HEAD/REVERT_MSG, mkit-conflicts, MKIT_OP_RESULT, rebase-apply/, bisect, stash, sparse-checkout, worktree.lock |
In the classic single-worktree layout both directories are the same
<root>/.mkit/; the two-directory model is byte-identical there.
Divergences from git, both deliberate:
shallow is shared (git: per-repo too, listed here because it is
per-worktree-adjacent in some git docs): it constrains the one object
graph every tree reads.refs/stash); mkit's is a worktree-state manifest, and #493
specifies tree-local stash semantics (stash in a linked tree never
touches a sibling's entries).Relative signing-key paths in config (.mkit/keys/…) resolve their
.mkit/ prefix against the common dir, so every tree signs with
the one shared repo key store.
<main>/.mkit/ # common dir
worktrees/<id>/ # one per linked tree (the registry)
commondir # path to the common dir
mkitdir # abs path of the tree's pointer file
HEAD, index, ORIG_HEAD, ... # that tree's per-worktree state
<linked-tree>/.mkit # pointer FILE (not a directory)A linked tree's .mkit is a regular file containing exactly one line:
mkitdir: <path>\n<path> names the tree's state dir; absolute, or relative to the
linked tree root.MAX_POINTER_FILE_BYTES).commondir and the back-pointer): the size cap is checked
against the file itself, and following a hostile link (for example
.mkit -> /dev/zero in an untarred tree) would turn discovery into
an unbounded read.mkitdir: , one
trailing \n); layout::write_pointer_file is the single writer.commondir — path from the state dir back to the common dir;
absolute or relative to the state dir. worktree add writes
../..\n. When ABSENT, readers MUST default to ../.. (the layout
worktree add produces). Same single-line/size rules as the pointer
file.mkitdir — the absolute path of the linked tree's pointer file (the
back-pointer worktree prune verifies). Same single-line/size rules.The registry directory name <id> MUST match: non-empty, ≤ 255 bytes,
ASCII alphanumeric plus ., _, -, and not . or ..
(validate_worktree_id). Ids derive from the target path's basename,
sanitized (invalid bytes → -) and uniquified git-style
(name, name-1, name-2, …).
Given a working directory W (mkit treats the invocation directory as
the worktree root; there is no upward search):
W/.mkit is a directory or absent → the single-worktree
layout (common dir = state dir = W/.mkit). Absence is not a
discovery error; "not a repository" surfaces from the store open,
unchanged from pre-worktree behavior.W/.mkit is a file → parse it as a pointer file. Then:W if
relative); it MUST exist and is canonicalized;<state>/commondir (default ../..),
resolved against the state dir, canonicalized; it MUST exist.DiscoverError):
malformed/oversized/non-UTF-8/multi-line pointer, dangling state
dir, unreadable or dangling commondir. A broken linked tree MUST
NOT silently degrade to operating on some other directory.A registry entry is prunable when its back-pointer is missing or
unreadable, its tree is gone, or the tree's pointer no longer names
that state dir (a moved or re-created tree MUST NOT be claimed by a
stale entry). worktree list reports prunable entries; only
worktree prune deletes them.
Linked trees share the object store and every common-dir source of truth: refs, config, keys, history MMB, recovery log, attestations, transport caches. An object written from any tree is immediately visible to all; a ref moved from any tree moves for all.
A branch may be checked out in at most one worktree. worktree
add, checkout/switch (including -b/-B), branch -d/-D, and
branch -m MUST refuse (naming the holding tree) when the branch is
checked out in another tree. Rationale: branch moves flow through the
history-MMB ref-write path (SPEC-HISTORY-PROOF), which assumes one
writer per branch. Detached HEADs are unconstrained.
The guard reads sibling HEADs via the registry and MUST fail closed if the registry cannot be enumerated.
This document originates the worktree.lock/worktrees.lock primitives
and their per-command holder details below; the total lock order across
all mkit locks (including refs-history-<branch>.lock and the
file-transport's refs/.lock), and the enumeration of every writer, is
owned by SPEC-CONCURRENCY — see that document for the authoritative
order. This section states only the two locks this document defines and how the
commands specified here use them.
| Lock | Location | Guards |
|---|---|---|
worktree.lock | each tree's state dir | that tree's worktree/index read-modify-write |
worktrees.lock | common dir | registry mutations (worktree add/remove/prune) |
Per-command holders:
worktree add cannot register a tree between root
collection and the sweep), then every tree's worktree.lock in
deterministic order — main tree first, then registry ids ascending —
and holds them all through the sweep.checkout/switch hold the registry lock across the
branch-checked-out-elsewhere guard and the HEAD write, making
§4.2's check-then-write atomic against racing checkouts and
worktree adds; branch -d/-m do the same around their guard +
ref mutation; worktree add re-verifies the guard after acquiring
the registry lock.worktree remove holds the registry lock plus the condemned tree's
own worktree.lock before deleting; worktree prune snapshots the
registry only AFTER acquiring the registry lock (a pre-lock snapshot
could condemn a mid-add entry).Per-tree locking is insufficient to serialize two linked trees mutating the same branch/tag/remote ref. Each ref mutation additionally takes the shared full-ref guard specified in SPEC-CONCURRENCY §4, after any history guard. Unconditional operations and deletes participate in this same order.
Shared sources are read once: the strict refs walk, attestations, and
the recovery log. Per-tree sources are unioned over the main tree and
every registry entry whose state dir exists — including prunable
entries (until worktree prune reaps a state dir, whatever it pins
stays pinned): HEAD, staging index, ORIG_HEAD, in-progress
merge/cherry-pick/revert/rebase state, conflict sidecars, and the
tree-local stash.
Fail closed: a registry enumeration error or an unreadable sibling
source aborts collection; gc never prunes on a partial view. The
applied-packs record is a redownload-avoidance cache and MUST NOT be a
root source (#409); gc may delete it freely.
The upload-parts/ receipt store is likewise a deletable cache and MUST NOT
be a GC root. It is distinct from receipts/, reserved for signed storage
receipts in M5. Part upload resume works only while the push plan regenerates
the same pack bytes.
worktree add write ordering (crash safety)commondir, mkitdir back-pointer,
HEAD) — before any pointer to it exists;A crash between (1) and (3) leaves a prunable registry orphan, never a
live tree pointing at half-built state. worktree remove deletes the
tree before the state dir for the same reason.
worktree move/lock/repair (follow-ups), submodule interplay
(submodules are a non-goal), and sharing a repository between mkit
versions with and without worktree support while linked trees exist.
mkit-core/src/layout.rs unit tests: accessor/classification
goldens, discovery fail-closed matrix, pointer-file golden bytes,
id grammar.mkit-cli/tests/layout_phase0_pin.rs: the single-worktree .mkit
shape is a compatibility surface; it must never drift.mkit-cli/tests/linked_worktree_discovery.rs,
worktree_command.rs, worktree_gc_cross_tree.rs: shared-store,
tree-locality, guard, gc-survival, and lock-contention behavior
through the real binary.