Beagle SCM
Submodules: nested projects mounted as secondary worktrees
A submodule splits a project into smaller reusable parts or vendors a dependency; the goal is a seamless recursive experience where clone, checkout, and commit descend into subs by default. The method reuses Beagle's own machinery: a submodule is just another project in the parent's Store, checked out as a secondary worktree at the gitlink path. Git stays byte-exact — a 160000 gitlink and a root .gitmodules blob name the pin and upstream.
- a submodule tip is pinned by a self-pointing record in the sub's own wtlog:
get //wt/path/sub#hash — no synthetic branches, no store refs (DIS-072);
- the pin hash IS the gitlink in the immediate parent's base commit; the parent rewrites the pin record on any move of its base (post, get);
- a sub not based on the pin shows as "adv" in the parent's listing;
status reports the sub's base ahead/behind the pin, listing the extra/missing commits;
- the logic is URI recursion driven, e.g.
get be://store?/parent invokes get be://store?/submodule in a subdir;
- dirtyness accounts for submodule changes too (dirty if contains anything dirty);
- store layout:
.be/project/..., i.e. flat at the store level;
- submodule run reports get aggregated into parent reports;
- submodules traversal depends on a verb (e.g. preorder for
get, postorder for post);
- recursion is the default (
--nosub opts out), scoped to the context repo: a run at ///jab/dog affects dog and its sub abc, never the parent jab.
Recursion
Mutating verbs recur cleanly only into a sub BASED ON the pin; an ahead/diverged sub is skipped and stays "adv" (except the post commit forms below, which absorb it). Views (status, diff, log, …) recur always, where applicable.
- GET - clone/update:
- pre-order (parent creates/updates all files before a child),
- checkout commits are taken from parent's gitlink,
- a sub based behind the pin is FF'd to the pin when that is clean; an ahead/diverged sub is skipped, stays "adv" (post/patch territory),
- tries to retrieve the child files from the same source, falls back to .gitmodules URI,
- child worktree tracks its parent-mount pin URI, a self-pointing
get //wt/path/child#pin record (DIS-072)
- POST - commit/advance:
- post-order (child commit hash is known before parent can commit),
- commit-all (message, nothing staged): commits ALL changes to make the tree clean — dirty sub files bottom-up, then the resulting gitlinks; an off-pin sub's advance is absorbed too,
- selective (puts/deletes staged): commits exactly the staged set; a sub with put files gets a new commit, and the new gitlink is committed one level up (within scope),
- a commit advances the sub's own worktree only; the sub's pin record is rewritten by the PARENT's commit, never by the child's,
- wire-posts (pushes) take data from the store, independent of the tree state; child changes are pushed to the same remote,
- PATCH - merge:
- store-driven, recurs on all levels: each sub merges the missing store changes from its de-facto base plus local edits,
- PUT, DELETE - stage:
- bare form always recurs, marks all dirty (put) or missing (delete)
- explicit form recurs if a path leads into a submodule
- HEAD - fetch:
Store layout
A submodule is just another project shard in the parent's store, mounted as a secondary worktree that points back; there is no separate submodule store class. The parent tree's 160000 gitlink pins which commit, the root .gitmodules blob names where to fetch it.
- The sub's objects sit in
.be/<title>/, same level as the parent's, see Store,
- The shard's Title comes from the
.gitmodules URL basename, .git and trailing / stripped (…/libabc.git → libabc).
- Submodule worktree log is at
<wt>/<path>/.be (same structure at the root wtlog),
- Sub of a sub still stores as
.be/<title>/; its pin record points at the parent mount, //wt/parentpath/subpath#hash.