Restack, pull, prune
Three commands keep a stack current. restack replays commits so each branch’s
contents match the structure g2g records. pull fetches, advances the base and
replays, which is what you want after the trunk moves. prune forgets
branches whose work has already landed. Each previews first and acts only with
--apply.
They work on the stack as g2g’s own graph
records it, and need no Graphite. Their defaults for --scope differ because
the commands differ — restack defaults to subtree, pull offers only
stack and trunk, and prune defaults to stack and offers all; see
Scope for why.
restack
Section titled “restack”g2g restack replays a stack’s commits so its contents match its recorded
structure. This is what a squash merge upstream breaks: the child keeps its
parent’s pre-squash commits, so its pull request shows the parent’s changes a
second time and merging it reapplies work the trunk already has.
# Preview. Says exactly what will be replayed and whether it will conflict.g2g restack --branch main --scope stack
# Replay.g2g restack --branch main --scope stack --apply
# Move a fragment onto a different base instead of its recorded parent.g2g restack --branch feature/login --scope subtree --onto main --apply
# Resume verbs, with the same meanings they have in git rebase.g2g restack --continueg2g restack --abortg2g restack --skipBy default it replays the branch you are on and what depends on it. It does not replay what sits below you uninvited, because rewriting is not free: a conflict below you may be one you are deliberately deferring, and replaying it anyway is how restacking from the middle walks into it every time.
A clean replay never touches your working tree
Section titled “A clean replay never touches your working tree”The preview knows in advance whether the rewrite applies, and says so. Here
login needs a restack because auth moved underneath it:
Target auth · current Git branch
● auth ← target ● login needs restack ● session
Replays login and session onto auth.Applies without touching your working tree or checked-out branch.
No changes were made. Rerun with --apply to replay these commits.With --apply it revalidates, prints the plan again, and replays:
Ready to applyTarget auth · current Git branch
● auth ← target ● login needs restack ● session
Replays login and session onto auth.Applies without touching your working tree or checked-out branch.Replayed.Branch contents now match the recorded structure.
Suggested next step: g2g pushWhen it conflicts
Section titled “When it conflicts”When the replay cannot apply cleanly, the preview says
This will not apply cleanly before you apply. Rebasing then happens in your
own working tree, because resolving a conflict needs a tree you can edit with
your own tools, and it stops on the conflict for you. Resolve the conflict,
git add the files, and run g2g restack --continue.
Using git rebase --continue or git rebase --abort yourself is fine too:
--continue re-derives what is left from the refs rather than replaying a
stored queue, so your own git commands simply change what remains to do.
g2g restack --abort restores every branch to where it started, including ones
an earlier step already moved. --skip abandons the commit the restack stopped
on.
This is g2g’s only resumable operation, so every other command that changes anything refuses while a restack is unfinished: mid-restack a branch may already have moved while the graph still records where it used to be.
What it reports rather than doing quietly
Section titled “What it reports rather than doing quietly”- A branch it empties. If everything a branch carried is already upstream, it collapses onto its base and its pull request would show no changes.
- Commits the parent dropped. They are dropped from the child too, by
default. Where every one of them was genuinely removed rather than rewritten,
--absorbkeeps them as the child’s own instead — which rewrites nothing and only re-records where the branch forks.
What it refuses
Section titled “What it refuses”- A branch you rebased by hand. Its recorded fork point is no longer in its
history, so the replay range would silently widen to include the base’s own
commits. Re-record it with
g2g trackfirst. - A branch another worktree has checked out, because that worktree would be
left describing a commit its branch no longer points at. Switch that worktree
away or close it, or select less with
--branchor--scope. - A branch Graphite describes and g2g does not. A restack needs a fork
point, which only g2g’s own store records, so it says to
g2g trackthe branch first. See Where structure comes from.
See design-docs/restack.md for how the replay range is chosen, the two engines, and the resumable state.
# Fetch, fast-forward the base, replay the stack.g2g pullg2g pull --apply
# The same, then forget the branches whose work has landed.g2g pull --prune --applyThis is git switch main && git pull && git switch back && restack in one
command.
The fetch writes only into refs/g2g/remotes/, so your own remote-tracking
refs, FETCH_HEAD, and ahead/behind counts are untouched. The base is
fast-forwarded or not at all: a base that has diverged is reported, never
merged or reset, because “you are behind” and “you have diverged” want
different responses and only you can give the second.
If the replay stops on a conflict part-way, what replayed stays replayed and
pull exits 3; see Exit status.
Pruning in the same step
Section titled “Pruning in the same step”On its own pull does not forget anything. Pruning is g2g prune, a
separate command, because it answers a different question on the same boundary
and edits the recorded graph rather than moving branches. --prune runs the
two in order over the same selection, since after a squash merge upstream the
usual thing to want is both.
The preview says it will prune without saying what: what has landed is only
known once the base has moved, and the base does not move in a preview. If the
prune then refuses, the pull has already happened and stays happened, so the
command stops part-way and exits 3. --json and --porcelain refuse
--prune, because it produces two reports and those formats are one document.
When both sides have moved
Section titled “When both sides have moved”When a branch and its published version have each moved, pull refuses rather
than choosing. --take published is the way through, and it is the one path
where pull loses work that exists nowhere else — so the preview names every
commit it would discard.
g2g pull --take published # the whole stackg2g pull --take published --through synthetic-fix # and no furtherIt only ever changes the outcome for a branch that has genuinely diverged. A
branch that is merely ahead of its published version is push’s business and
is left alone; one that is behind, or whose published version supersedes it, is
brought down either way.
That makes --take published all or nothing, and --through narrows it. With
two diverged branches the unbounded form takes both — discarding local work on
the upper one alongside the lower one you meant. --through stops at the
branch you name and refuses the rest, because a boundary says where you
have decided, not that you have decided everywhere. Above the boundary your
commits are kept, and replayed onto what was taken below — which is what pull
does anyway.
The boundary is the branch you name and what it is stacked on, because a branch’s published version is built on its parent’s, so taking one and not the other describes a stack that never existed. A sibling on another fork is outside it; the trunk is inside any boundary, and naming the trunk takes it and nothing else.
Published means the remote
Section titled “Published means the remote”published is a side, not a place. It means the branch as the remote holds
it: the version pull just fetched from the remote --remote names — one of
the names git remote lists, origin unless you say otherwise. It never means
GitHub, and it does not ask gh anything; a pull request is not the published
version of a branch, the remote’s ref is. When the remote is not origin, say
so on both runs, because the command pull suggests carries it:
g2g pull --remote upstream # refuses: both sides movedg2g pull --remote upstream --take published --applyg2g pruneg2g prune --applyPruning forgets a landed branch in the recorded graph, asking Git by content — a squash merge included — whether its work is already in the trunk. It never deletes a branch: that is a separate, deliberate act, not the tail of another command.
Forgetting a branch can leave a child recorded under something that is no
longer there. Where Git shows the branch below is an ancestor of that child —
which is what a pull leaves, having replayed the child onto it — prune records
the child there, with the same check and the same fork point track would use,
because that is where the child already sits.
Where Git does not show it, prune refuses rather than reparenting around it: a
trunk advanced by hand with the stack not replayed, say, or a child outside the
selection. The refusal offers g2g pull --prune first, which replays the child
and then prunes, and then a g2g track --branch <child> --parent <branch> for
each child, to record it by hand.