Landing & cleanup
Landing moves a review's content onto the base branch. Reviews land bottom-up,
so submitting one review lands everything below it too. Once every review on a
branch has landed, cleanup returns you to the base and removes the branch. This
page covers gs submit, gs cleanup, and the repo-wide gs repo prune.
gs submit#
Land a review, or the whole stack, onto the base branch.
gs submit [<review-id>]
gs submit --stack
| Flag | Description |
|---|---|
--stack |
Land the whole stack as one operation. |
-m, --message |
Commit message for the squash commit of the review you submit. Not with --rebase or --stack. |
--squash |
Land each review as one commit (the default). |
--rebase |
Land each review's commits as they are, on top of the base branch. |
--repo |
Repo name. Defaults to the repo inferred from origin. |
--stack-m, --message--rebase or --stack.--squash--rebase--repo--squash and --rebase are mutually exclusive. With neither, each review
lands with its own strategy, which is squash by default.
History on every base branch is linear: a land puts commits on top of the
base and never makes a merge commit. gs submit --merge is refused with that
reason. A review created with --merge by an older gs lands as squash.
Reviews land bottom-up. Submitting review rN lands the contiguous prefix
r1..rN, so every review below rN has to be landable too. A review is
landable when all of the following hold:
- It has a pushed patchset.
- It meets the repo's approval policy. By default no LGTM is needed. The
[approvals]table of.gitstack/policy.tomlcan require LGTMs from non-authors with write access, require every requested reviewer, or hold owners to the same rule. - If it changes an owner-controlled file (
.gitstack/policy.tomlor.gitstack/checks.toml), it also has an LGTM from an owner other than its author, unless an owner lands it under the owner bypass. - Its required checks pass, when the repo requires any.
- You are allowed to land it. An owner can land anyone's review; a writer can land only their own.
See Reviews and Feedback & approvals for how patchsets and LGTMs work, and Stacks & navigation for how a stack is ordered.
When a review falls short of the approval policy, gs submit refuses and says
what is missing and who can fix it:
gs: R3: needs 1 more LGTM from a writer or owner (has 1 of 2) (no_lgtm)
next: ask a writer or owner to run gs review approve R3gs stack shows the same thing on the review's row, for example
needs 1 more LGTM (1 of 2) or waiting on dev2.
With no argument, gs submit lands the bottom review of the current branch.
With --stack it lands the whole stack as one operation: every review lands,
or, if any review is not landable, nothing does and the refusal names that
review. To land the part below a blocked review, submit the review just under
it, for example gs submit r2.
The review id is the global r<n> form from gs stack, described in
Reference syntax.
# Land the bottom review, then the rest of the stack
gs submit r1
gs submit --stack
# Land r1 and r2, with a chosen message for r2's squash commit
gs submit r2 -m "billing: subscriptions"
# Land r1 with its commits as they are
gs submit r1 --rebase
A stack still lands directly after the base moves, with squash or rebase:
the server replays each review onto the new base tip for you. Only a
genuine textual conflict stops the land (behind_base, naming the files).
When that happens, run gs sync, resolve the
conflict, push, then submit again.
Each commit a land creates names its review in a Reviewed-on: trailer, for
example Reviewed-on: https://gitstack.sh/acme/api/reviews/R12. Squash
commits carry it, and so does each commit a rebase land replays onto a new
base. A rebase land that fast-forwards keeps the review's own commits as
they are, without the trailer. To get from any landed commit to its review, run
gs review for <commit>.
When a land breaks the branch#
A land does not wait for CI on the commit it creates. When a land turns one of the repo's required checks red on the branch it landed into, GitStack:
- Waits for the commit's CI to finish, then re-runs the failed GitHub Actions jobs once, to rule out a flaky test. This needs the GitHub App's Actions read and write permission.
- If they fail again and the commit before the land passed, reverts that
land: it opens a
Revert: <title>review for each landed review, on anautorevert-<review ids>-<random>branch, and lands it straight away, with no LGTM or checks. Each reverted review shows a Reverted strip with the failing checks and a link to the revert, and gets a comment fromauto-revert. - If the commit before the land has no result, because a newer land cancelled its CI run, re-runs CI on earlier land commits until it finds the last green one, then reverts the land right after it. An earlier commit's failure counts only once a re-run confirms it.
A check that failed while setting up never tested the land, so GitStack
doesn't count it as a failure: it runs it again. A failed step counts as
setup only when it is one of the runner's own steps, an unnamed
actions/checkout, actions/cache or actions/setup-* step (or
oven-sh/setup-bun), or a step whose name starts with Setup: . A step
you named yourself, such as name: Set up Go with uses: actions/setup-go,
counts as a real failure, and so does every other step. If setup keeps
failing, GitStack comments and reverts nothing. The exception is a land that
changes a file CI setup reads: anything under .github/, or a go.mod,
go.work, .nvmrc, .node-version, .tool-versions, package.json,
bun.lock, bun.lockb or Dockerfile. Such a land may have broken setup itself, so its
setup failures count as real failures, and it is reverted like any other.
GitStack only comments, and reverts nothing, when the repo's policy says
on_red = "notify"; when the failed checks can't be re-run (the App lacks
the Actions permission, or the check comes from another CI); when the land
that broke the branch is itself a revert or landed with --rebase; when its
revert conflicts with later lands; or when CI gives no answer within two
hours. If a newer commit on the branch passes before the revert lands,
the branch was fixed forward, and nothing is reverted. A repo with no required checks has
nothing to react to. To bring a reverted change back, fix it and land it
again. Set the behavior with the [landing] table of
.gitstack/policy.toml.
gs submit needs a clean working tree and refuses if you have uncommitted
changes. After the final review lands, it reconciles your local checkout: the
branch resets to the new base tip and the manifest refreshes, so you do not have
to run gs sync by hand. If a landed review rebased the reviews above it, the
reconcile surfaces any that now need a rebase.
gs cleanup#
Return to the base branch and delete a branch whose reviews have all landed.
gs cleanup [<branch>]
| Flag | Description |
|---|---|
--dry-run |
Report whether the branch is fully landed (exit 0 when it is) and what cleanup would delete. Deletes nothing. Squash-aware, unlike git branch --merged. |
--keep-remote |
Delete the local branch but keep the remote branch. |
--discard-unlanded |
Force-delete a named local junk branch whose content has not landed. Prints what it discards. Never touches the remote. |
--force |
Delete the remote branch even when the server reports zero reviews for it. |
--force-base |
Reset a diverged base branch to origin even when gs cannot confirm its unique commits landed. |
--base |
Base branch to return to. Defaults to the remote HEAD or main. |
--remote |
Remote name. Defaults to origin. |
--repo |
Repo name. Defaults to the repo inferred from origin. |
--dry-rungit branch --merged.--keep-remote--discard-unlanded--force--force-base--basemain.--remoteorigin.--repoThe bare form cleans the current branch. It returns you to the base branch, then deletes the local and remote branch once the server confirms every review on it landed or was abandoned. Pass a branch name to clean a different branch from anywhere. That form never moves HEAD or touches your working tree, so it also works when only the remote branch is left after the local one is gone.
Landing the whole stack from a review page offers a Delete branch box,
checked by default. It deletes the branch on GitStack, and on the GitHub mirror
when the repo has one, as part of the land. Your local branch stays until you
run gs cleanup.
When cleanup refuses and deletes nothing, it exits non-zero and names the
reason. That covers a branch that still has open reviews, and a remote-only
branch the server has no reviews for, since gs cannot confirm its work landed.
Pass --force to delete that remote branch anyway. A branch that is already
gone, or a run with --keep-remote, still exits 0.
# Confirm the branch is landed, then clean it up
gs cleanup --dry-run
gs cleanup
--dry-run is the authoritative "is this branch fully landed" check. It exits 0
only when every review has landed or been abandoned and the branch's content is
on the base, and it prints what a real run would delete (including a remote it
would keep for lack of server review state). Use it in scripts instead of
git branch --merged, which reports false negatives after a squash-land.
Cleanup fast-forwards your local base branch to origin as it returns you there.
When a squash-land rewrote a commit you had made directly on the base, the local
base can diverge from origin. Cleanup reconciles it when it can confirm the local
commits are already upstream. When it cannot, it refuses and prints a recovery
hint. Pass --force-base to reset the base to origin anyway.
Use --discard-unlanded only for a branch you mean to throw away, such as a
stray experiment never carved into reviews. It bypasses the content check but
still refuses an open review or a branch checked out in a worktree. Cleanup
refuses any branch that is checked out in a worktree and names the path; switch
away or remove that worktree first.
gs repo prune#
Delete every branch already merged into the default branch, across GitStack and its GitHub mirror.
gs repo prune [<ns>/<name>]
| Flag | Description |
|---|---|
--dry-run |
Show the plan and delete nothing. |
--keep |
Glob of branches to never delete, on top of the repo's [prune] keep list. Repeatable. |
-f, --force |
Skip the confirmation prompt. |
--json |
Print the plan as JSON and exit. Deletes nothing. |
--dry-run--keep[prune] keep list. Repeatable.-f, --force--jsongs cleanup handles one branch against the GitStack origin. gs repo prune is
the repo-wide sweep. In one server-side pass it deletes branches merged into the
default branch from both the GitStack bare repo and the linked GitHub mirror. It
computes the plan first, shows it, asks for confirmation, then deletes. The
default branch and any branch backing an open review are never deleted.
A branch like staging that is often level with the default branch counts as
merged, so a prune would delete it. To keep it for everyone, list it in the
[prune] table of
.gitstack/policy.toml:
[prune]
keep = ["staging", "release/*"]
The plan names the branches the policy kept.
A branch counts as merged when its tip is already on the default branch, or
when its changes landed there as other commits, as a squash merge on GitStack
or GitHub leaves them. Prune finds the commit on the default branch that holds
every change the branch makes and names it in the plan (squash-merged in <sha>), even if later commits reworked the same lines. A branch with any
change the default branch never had is kept.
On a GitHub-linked repo, prune also removes a GitStack copy that fell behind a
merged pull request: the copy is an earlier state of the PR's branch, and the
PR's final head landed. The plan shows these as stale copy of PR #<n>.
Prune only deletes a branch at the commit it checked. When GitHub has a merged branch at a different commit than GitStack, prune keeps it on both sides and lists it in the plan. Without an argument it targets the repo inferred from origin. See Repo lifecycle for the surrounding repo commands.
# Preview, also protecting hotfix branches this once, then prune for real
gs repo prune --dry-run --keep 'hotfix/*'
gs repo prune --keep 'hotfix/*'
Pruning needs write access on the repo. In a non-interactive session without
--force, the command refuses rather than block on the prompt. The report
notes where each branch lived, either GitStack and GitHub or GitStack only.