6.2 Branches, worktrees and review¶
One branch per independent change¶
Never edit main directly. Every change gets a dedicated branch off current
main, and every independent change gets its own, so that two people — or two
agents — working in parallel cannot trash each other's tree.
Worktrees are the practical mechanism¶
Not everyone uses git worktree, so it is worth a sentence on what it is before
the rules about it. A worktree is a second (or third, or tenth) checkout
directory belonging to the same repository: each one has its own working files
and its own checked-out branch, while sharing a single object store and history.
Where git checkout swaps one directory between branches, worktrees give each
branch a directory of its own.
That is what makes "one branch per independent change" workable in practice rather than merely aspirational. You can have a feature branch compiling in one directory while a reviewer's branch runs the regression suite in another, with no stashing and no switching, and without either of them seeing the other's half-finished edits.
There is one project convention about where they go, and it is not negotiable:
worktrees live outside the repository directory — not inside it, not even
git-ignored. The convention is a sibling directory next to the repository
checkout, holding one subdirectory per worktree; if your clone is at
<somewhere>/jnext, the worktrees go in <somewhere>/jnext-worktrees/<name>.
The reason is mechanical rather than aesthetic. Anything that walks the
repository's file list walks the worktrees too, and several things here do: the
traceability generator runs a find for CMakeLists.txt across the whole tree,
and a number of gates read git status. An untracked multi-megabyte tree sitting
inside the checkout is a trap for all of them, and the wasted I/O loads the very
machine that the timing-sensitive tests are trying to run on.
Two practical notes on working with them:
- Run
make worktree-bootstrapin a fresh worktree.git worktree adddoes not populate submodules, androms/is git-ignored, so a brand-new worktree can have an emptythird_party/spdlogand no SD-card image — and will fail in confusing ways as a result. The target reports what is missing and how to get it. It only reports; it never mutates your checkout. - Prefer
git -C <path> <cmd>overcd-ing into a worktree. It keeps your working directory stable across commands and avoids the shell-state surprises that come from half-remembering which directory you are in.
The full triplet, green, on the branch¶
Before review, on the branch, in this order:
$ make clean && make gui-release
$ make unit-test
$ ./build/test/fuse_z80_test build/test/fuse # 1356/1356
$ make regression
No FAIL and no SKIP anywhere: a SKIP is a row that was not tested, and the
harnesses exit non-zero on one. Commit before
make regression: a green run on a tree with no uncommitted non-doc change
leaves a stamp, and a dirty tree gets none.
Starting with make clean is deliberate, not caution. The regression suite runs
build/gui-release/jnext, and a stale binary produces both false FAILs — it
lacks a fix the test expects — and, much worse, false PASSes, because it also
lacks the bug the test would have caught. Rebuilding first makes "the tests ran
against this source" true by construction rather than by discipline, and ccache
makes it cheap enough that there is no reason to skip it; see
5.2.
Run the full regression only when it can tell you something new. The
suite runs its rows in parallel and keeps its real-time-paced rows in a quiet
phase of their own, so it needs no JNEXT_TEST_JOBS; but a full run still
loads the whole machine, and only one runs per host at a time — a second one
waits for the first. So: the author runs it once on the final non-doc state; a
fix round that changed only documentation keeps its stamp; a clean merge of
main into the branch is not re-run (CI on main is the full safety net); and
mutation testing and diagnosis use targeted rows,
make regression-rows ROWS="<row> ...", which builds the binaries the rows run
(the Qt jnext is the non-PGO Release build; ROWS_PGO=1 for the PGO one)
and never takes the lock. A full run whose only FAILs are timing rows that
failed on a loaded host is not lost either: make regression-confirm re-runs
just those rows solo and stamps the run if they pass.
Independent review¶
Never self-review. The review is done by an agent or a person who did not write the change, working in their own worktree — never the author's.
The verdict is binary: APPROVE or REJECT. There is no "approve with nits", because a nit either matters, in which case it is a REJECT with the reason cited, or it does not, in which case it is not part of a verdict. On REJECT, fix and re-review.
Neither the author nor the reviewer runs the full regression on a branch. The
full gate runs on main, once per batch of landings, before anything is
pushed. In their own worktree of the branch, the reviewer runs the targeted
rows and suites the change touches, and checks that the author's
make unit-test is green on the reviewed head.
The reason this rule is absolute is empirical rather than philosophical. On this project a series of real defects were caught by review and none of them by the green test suite that had already run over the same code. That is not a criticism of the suite: a passing suite proves that the tests which exist pass, and it can say nothing at all about the tests that should exist. A reviewer's single most useful move is to revert the fix and confirm that the new tests actually fail without it — which is the discriminative-test requirement from 6.1, checked rather than believed.
Merge, then bump¶
On a green APPROVE, merge — one branch at a time. If a merge conflicts, the person who merged last is the one who fixes it, on their own branch.
Immediately after each merge to main, run make bump-patch. Every change
that lands gets its own patch bump, per merge and never batched. That is a
separate thing from the deliberate minor/major release flow described in
5.3.
Finally: never push to origin without explicit authorisation. Local commits, merges, rebases and bump tags on your own branches are all fine, and they stay local until the owner says to push. The one standing exception is the merge commit of a landing pull request, for the GitHub reason given in 6.1.