Skip to content

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.

Terminal window
g2g land # the descent, as the commands it would run
g2g land --apply
g2g land --apply --admin # merge without waiting for restarted checks

land 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.

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.

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.

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 here

The 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.

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.

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.

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.

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.

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.

--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.

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.

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.

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.

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.