Sync model
A splice's folder has two histories: the monorepo's, where it's a folder, and its upstream's, where it's the whole repository. Commits don't cross between them, changes do. This is the README's drawing, with letters for its names:
Before the pull
Circles are commits, newest on top, and boxes point at them. The lane
lib/ holds the splice's upstream commits
(scenario).
H is HEAD, the monorepo commit whose splice is being examined. From H,
the model finds five other commits:
| Name | Meaning | Also called | Lives in |
|---|---|---|---|
| B | The boundary, where the rebuild starts: usually the last pull (where exactly). | boundary | the monorepo |
| U(H) | The synced commit: the upstream commit .splice records at HEAD. | the upstream | |
| U(B) | The upstream commit recorded by B's .splice. R starts there. Usually U(H). | the upstream | |
| T | The fetched tip of tracked(X), in refs/splices/: the branch tracked for monorepo branch X, or the upstream branch named X. | tracked, theirs | the upstream |
| R | The rebuild, ours: the folder's history since B, rebuilt as upstream commits on top of U(B). | rebuild, ours | computed |
These commands locate the five commits in the diagram:
$ B="$(git splice boundary lib)" &&
> U_H="$(git config --file lib/.splice splice.commit)" &&
> U_B="$(git config --blob "$B:lib/.splice" splice.commit)" &&
> T="$(git rev-parse "splices/$(git splice key lib)/heads/main")" &&
> R="$(git splice rebuild lib)" &&
> git show -s --format='B: %s' "$B" &&
> git show -s --format='U(H): %s' "$U_H" &&
> git show -s --format='U(B): %s' "$U_B" &&
> git show -s --format='T: %s' "$T" &&
> git show -s --format='R: %s' "$R"
B: a
U(H): k
U(B): k
T: n
R: d
The letters now match the diagram: B is a, U(H) and U(B) are k, T is n, and R is d', the rebuilt copy of d.
A monorepo commit X has a sync point U(X): the upstream commit recorded
in X's .splice.
The sync point changes only when .splice changes, for example in a pull.
The monorepo commits between two such changes share the same sync point. A
pull commit X normally brought in U(X^)..U(X), the range between the
previous and new sync points. A push between them is the exception
(scenario).
How R and T determine the status
R and T are two histories of the same folder, as upstream commits. How they relate is the sync state:
| R and T | status shows |
|---|---|
| the same files, or the same commit | up to date |
| T is an ancestor of R | ahead N |
| R is an ancestor of T | behind N |
| neither, but with a common ancestor | ahead N, behind M |
| no common ancestor | !! unrelated history |
U(H) and T may be missing: right after init, there's no U(H) yet, and R
starts as a root commit; before the first fetch, there's no T, and the
splice is never fetched, or the upstream has no such branch.
A pull is a three-way merge, in the monorepo's layout:
- base: normally the newest commit R and T have in common,
git merge-base R T; after an upstream rewrite, pull can use the fetched branch's fork point - ours:
HEAD, with the folder as it is - theirs:
HEADwith T's files in the folder
Git merges them as usual. Only the folder differs between base and theirs, so only the folder can conflict. The result is one ordinary monorepo commit, which records T as the new U(H). It's the new B too, unless it merged commits that were never pushed.
The base is usually U(H). After a push, it's the pushed commit: R contains it, so the merge doesn't replay changes the upstream already has. That's also why U(H) alone says little about where a splice stands. A push moves T, and with it R's relation to T, but not U(H).
Where B is
B is the newest commit on the monorepo's first-parent history that
changed .splice and whose folder is the upstream commit it recorded.
A pull that merged unpushed commits is not B because its folder also
contains those local changes. Two exceptions:
- Where the splice starts, its mount (its first
.splice), B is that commit whatever its folder. The rebuild copies it too where its folder isn't U(B), e.g. afterinit. Agit mvof the folder isn't a start: the rebuild follows it to the old path (scenario). - For a nested splice, B can be the last pull of the splice above, and R's history up to there comes from the upstream above, at its synced commit (nesting).
boundary prints B.
R, the rebuild
git splice rebuild computes R. It
recreates each monorepo commit since B that changed the folder as an
upstream commit on top of U(B). A rebuilt commit contains the folder's
tree without .splice and keeps the original author, dates and message.
After the pull
Pulling the upstream creates commit e in the next diagram:
$ git splice pull -m e lib
ok lib fetched
ok lib: pulled df79a48
A box U(X) points at the commit recorded in X's .splice. After the
pull in the drawing:
- The pull commit e records n, so U(H) = n.
- The earlier changes b and d were never pushed. They remain in e's folder, so that folder is not the tree of n. Therefore e is not B.
- B stays at a, and U(B) stays at k.
- When the rebuild reaches e, it has two histories to preserve: the unpushed changes ending at d', and the pulled history ending at n. Therefore e' has both as parents and is a merge commit.
The scenario builds this state.
After the push and bump
A push sends b', d' and e'. B and U(B) stay where they are until
git splice pull --bump records e' as U(H),
in a new commit f:
$ git splice push lib
ok lib: pushed 8430dba to main
$ git splice pull --bump -m f lib
ok lib fetched
ok lib: bumped the synced commit to 8430dba
f's folder is e', so f is the new B, and the rebuild has nothing to add: R = T = U(H) = U(B) = e' (scenario).
Parents of rebuilt commits
In the situation after the pull, the rebuild walks through the monorepo commits from B to H. It rebuilds b and d, then reaches H, the pull commit e. At that point, the rebuilt tip—the newest commit produced so far—is d', and the synced commit U(H) is n. The parents of e' depend on the relationship between these commits. Neither contains the other, so e' gets d' as its first parent and n as its second parent. The rebuild has reached H, so e' is R.
Git numbers these parents first and second:
$ R="$(git splice rebuild lib)" &&
> git show -s --format='R^1 (first parent): %s' "$R^1" &&
> git show -s --format='R^2 (second parent): %s' "$R^2"
R^1 (first parent): d
R^2 (second parent): n
Most monorepo commits are copied on top of the rebuilt tip. A pull also records a new synced commit, so its rebuilt copy may need a second parent. The table shows how the relationship between those two commits determines the parents. A scenario with two pulls shows each pull becoming a separate merge in R's history.
| Rebuilt tip and synced commit | The copy's parents | Common situation |
|---|---|---|
| neither contains the other | rebuilt tip as first parent, synced commit as second: a merge | pulling after both the monorepo and upstream changed, without rewritten upstream history (scenario) |
| synced commit contains the rebuilt tip, or there is no rebuilt tip | synced commit | squash-merging a feature branch that also changed the folder (scenario), or starting a splice |
| rebuilt tip contains the synced commit | rebuilt tip, and no copy if the folder didn't change | bumping after a push while newer commits remain local (scenario), or editing .splice by hand |
The rebuilt copy becomes the new tip. Once the rebuild reaches H, its final tip is R.
R is computed, not stored
R is never stored. It's computed from HEAD each time, and the same
HEAD always gives the same R. So a commit pushed once is rebuilt
identically, and the next push is a fast-forward. Since R is computed,
.splice needs no monorepo commit ids, which a rebase or a squash merge
would invalidate
(design).
A push moves T but doesn't write to the monorepo, so U(H) stays at the
last pull. It need not record anything: status compares R with T, and
the deterministic rebuild contains the pushed commit with the same hash.
A later pull normally starts where R and T meet
(example).
After git splice push --rebuilt-commit, R doesn't contain the edited
commit. git splice pull --bump must record it as U(H) before the next
change, as push reminds you
(scenario).
Rebuilding walks every commit since B, pushed or not;
pull --bump moves B forward where that cost
matters.
A pull could have been a merge, with U(H) as its second parent: weak parents compares the two.