Land
When a stack is finished, land takes it down onto its trunk, bottom branch
first, without waiting for CI. It lands the path from the trunk to the branch
you stand on (or --branch), so from the top that is the whole stack.
g2g land # the descent, as the commands it would rung2g land --applyg2g land --apply --admin # merge without waiting for restarted checksland defaults to --scope path, because standing in the middle of a stack and
typing land means “as far as here”. A GitHub native stack is linear, so it
takes path or stack only, and refuses a selection that forks. See Scope.
Why it exists
Section titled “Why it exists”The constraint that shapes a descent is the squash merge. When the bottom
branch merges, the trunk gains one commit equivalent to none of that branch’s
commits, and every branch above still carries the originals. Doing it by hand
correctly means replaying what is left onto the advanced trunk between merges,
moving each pull request’s base before its turn, and forgetting each branch as
it goes. Doing it quickly means not waiting for CI to re-confirm what it
confirmed a minute ago, and not restarting it on branches whose turn has not
come. land is that sequence, previewed before it runs.
What a descent does
Section titled “What a descent does”Each branch in turn is published, merged, and then forgotten and deleted, and the branches above it are replayed onto the advanced trunk before the next one goes.
Only the branch about to merge is pushed. Republishing the whole stack after
every merge restarts the checks on every branch above it, which is the cost
this exists to avoid. Between merges the branches above are therefore ahead of
their pull requests, and g2g github status reports head✗ for them; that is
the intended state, not drift.
What is left above the last branch landed is published once at the end, before the stack comments are kept, so its pull request shows the replayed version rather than one built on a deleted branch. A branch that was never published is left unpublished.
Every pull request is aimed at the trunk rather than at the branch below, because by the time a branch’s turn comes the branch below has merged and gone. The base is checked immediately before each merge and moved if it is stale, and every base it would move is named in the preview.
It waits on GitHub twice per branch, and never on CI. After a push it waits until GitHub has the commit, the base is where the merge is meant to go, and GitHub has finished deciding whether the pull request can merge. After a merge it waits until the merge commit is in the base, rather than for the base’s tip merely to change, since a colleague’s unrelated push changes that too. Both waits ask first, because the ordinary case is that GitHub already agrees.
The preview is the recipe
Section titled “The preview is the recipe”Every line of the preview is a command you could run yourself, in order, so driving it by hand is a first-class option rather than a fallback:
Target synthetic-two · current Git branch
○ synthetic-main trunk ├─● synthetic-one #41 base✓ merge └─● synthetic-two #42 head✗ publish first base✗ → synthetic-main merge with --admin ← target
Commands this would run, in order 1 gh pr merge 41 --squash · land synthetic-one 2 g2g pull --apply · advance the trunk and replay what is left onto it 3 g2g prune --branch synthetic-one --scope branch --apply · forget it, once what sat on it has been reparented 4 git push origin --delete synthetic-one · remove the published branch, if the merge has not already 5 git branch -D synthetic-one · remove it here 6 g2g push --branch synthetic-two --scope path --apply · publish it as it is here 7 gh pr edit 42 --base synthetic-main · merge into synthetic-main rather than synthetic-one 8 gh pr merge 42 --squash --admin · land synthetic-two 9 g2g pull --apply · advance the trunk and replay what is left onto it 10 g2g prune --branch synthetic-two --scope branch --apply · forget it, once what sat on it has been reparented 11 git push origin --delete synthetic-two · remove the published branch, if the merge has not already 12 git branch -D synthetic-two · remove it hereThe recipe and the work are built from the same steps, so they cannot come to describe different things. A refused descent shows no recipe at all: there is no ordered set of commands that reaches the end, and offering the ones decided before the refusal would invite running half of it.
It needs the stack in g2g’s own graph
Section titled “It needs the stack in g2g’s own graph”Landing reads pull requests from whichever source describes the stack, and then
replays, reparents and forgets in g2g’s graph, and those are not the same
record. Pointed at a Graphite-described stack, it would merge every pull request
and then find nothing to replay and nothing to forget, so it refuses and names
g2g adopt.
It owns no rules of its own
Section titled “It owns no rules of its own”Publishing goes through push, which refuses a branch the remote has moved on.
Advancing and replaying go through pull, which refuses a trunk that has
diverged. “Has this landed” is asked of Git by content, through the same check
prune uses, because a squash merge is invisible to a pull request’s head.
It refuses the whole descent before merging anything. Discovering the fourth branch is a draft after the first three have merged is not a refusal, it is a half-landed stack.
Protected repositories need --admin
Section titled “Protected repositories need --admin”Every branch above the first is force-pushed by its own replay, which restarts
the required checks that were green a moment ago, so GitHub reports it blocked.
On a repository with required status checks, land without --admin will
refuse at the second branch, every time. That is inherent rather than
incidental, and the preview says so before the first merge instead of letting
the run discover it at the second branch. In the recipe above, the step that
will need it already carries --admin.
--admin also bypasses approvals, so a pull request nobody has approved is
refused under its own name rather than folded into the protection refusal.
Someone reaching for the flag to get past restarted checks should not silently
also get past a review nobody gave.
Cleanup
Section titled “Cleanup”Each deletion is on by default and can be turned off on its own:
--no-delete-remote keeps the published branch and --no-delete-local keeps
the local one. Forgetting the landed branch in g2g’s graph cannot be turned
off: the branches above it are reparented onto the trunk as it goes, and leaving
it recorded would put them under a branch that no longer exists.
None of the cleanups can stop a descent. The work is merged, and a ref that would not delete is untidiness, not a failed land. A branch the remote deleted on merge is already in the state it was asked for.
Stack comments
Section titled “Stack comments”Once the descent is done, land keeps the stack comments on what remains above
the branches it landed, so those pull requests list what merged as history. It
is the last line of the recipe, and --no-comment skips it. If keeping the
comments fails, the descent stands and the command exits 3, naming
g2g github comment --apply. See
github comment.
Merge method
Section titled “Merge method”--method squash|merge|rebase defaults to squash, and is refused up front if
the repository does not allow it. Squash is the case a stack needs help with:
the other two leave each parent’s commits in its child under the same identity,
so nothing needs replaying between merges.
Stopping part-way
Section titled “Stopping part-way”A descent that stops after something has merged reports what landed rather
than “not applied”, because those merges are done and stay done, and it exits
3: it did part of what was asked and stopped somewhere you have to act. A
descent that stopped before changing anything is an ordinary failure. See
Preview and apply for the exit statuses.
land is not resumable. Rerunning it recomputes instead: a merged branch is
detected by content and skipped, and a forgotten branch is gone from the graph,
so a rerun continues from wherever the last one stopped. A branch somebody else
merged in the browser is detected the same way.
Uncommitted work is refused only where the descent would touch it: up front,
when the branch checked out here is one the descent moves, and before a replay
that would stop on a conflict in this working tree. In the second case the
descent stops with what merged standing and names the way through: commit,
run g2g pull --apply, then rerun g2g land.
The mutation phase has a budget of 60 seconds plus 180 per selected branch,
because land waits on GitHub between its calls. The root --timeout flag
replaces it; see Timeouts.
Landing a declared trunk
Section titled “Landing a declared trunk”land --branch <trunk> lands a trunk declared with --into (see
Shape the stack) into the branch it lands into, as a
stack of one, by the method it was declared with unless --method says
otherwise. Afterwards only that base is advanced, so its other stacks are not
replayed mid-descent, and the declaration is forgotten. It refuses while
anything is still recorded on the trunk, or another trunk lands into it: land
those first.
A stack linked on GitHub
Section titled “A stack linked on GitHub”GitHub refuses gh pr merge on a pull request in one of its native stacks.
Its own stacked merge takes everything below the pull request at once, which
is not a descent: nothing above would be replayed between merges. So land
refuses a linked stack in the preview, naming the
g2g github unlink that clears it.
Unlinking leaves the pull requests and their bases as they are. This is why
submit links a native stack only with --link.
What it does not do
Section titled “What it does not do”It does not wait for CI; --admin is what it offers instead. Merge queues are
refused: gh pr merge turns a merge into an auto-merge there, so neither of
the waits above would ever settle. It does not land a forked selection. And it
does not undo a descent: a merge cannot be taken back.
See design-docs/land.md for why each of these is the way it is, including the three publishing decisions that were wrong first.