Skip to content

Shape the stack

These commands decide which branches a stack is made of and how they sit on one another. adopt, track and untrack record structure that already exists in Git; create starts a new branch and records it in the same step; delete, fold and rename change a recorded stack. All of them write g2g’s own graph, and like every command that changes anything they preview first and act only with --apply — see Preview and apply.

None of them replays a commit. Making a branch’s contents match its recorded structure is restack’s job, and these commands say when a restack is the next step rather than running one.

Terminal window
# Record the whole stack you are on, in one step. This is where to start.
g2g adopt --trunk main --apply
# Preview the candidate parents of one branch. It refuses to choose.
g2g track
# Record one parent. Preview first; --apply writes.
g2g track --branch synthetic-login --parent synthetic-auth
g2g track --branch synthetic-login --parent synthetic-auth --apply
# Remove edges. --scope subtree removes descendants too.
g2g untrack --branch synthetic-auth --apply
g2g untrack --branch synthetic-auth --scope subtree --apply

g2g adopt records a whole existing stack at once, which is almost always what a repository that predates g2g needs. You assert one thing — the trunk, with --trunk — and the shape follows from commit ancestry. Even the trunk is inferred when exactly one recorded root is an ancestor, so it only has to be named the first time.

It records a forest, not a chain: branches hanging off the stack join it, and branches hanging off those join in turn, while a branch that merely shares the trunk is left alone, being a separate stack rather than part of this one. Where ancestry cannot order two branches it refuses and names them, exactly as track does.

To adopt what another tool declares instead, g2g graphite adopt records Graphite’s structure (see Graphite), and g2g github adopt records what a published stack’s pull requests declare (see Adopting a published stack).

g2g track records one edge: the --parent you name. This is not Git’s upstream tracking; it records which branch a branch is stacked on, so every other command knows the structure.

Without --parent it previews the candidates — the local branches the branch’s commits sit on top of, ordered nearest first — and blocks:

Target synthetic-login · --branch
● synthetic-login untracked ← target
Apply blocked: no parent chosen
Nearest ancestor: synthetic-auth (1 commit behind)
Then: synthetic-main (1 commit behind)
g2g track --parent synthetic-auth record just this edge
g2g adopt record the whole ancestry at once
Scope branch · 1 branch · /synthetic/repo/.git/g2g/graph.json
No changes were made. Apply would refuse until that is resolved.

It never picks for you. The nearest ancestor is usually right, and “usually” is not a basis for writing down structure every later command trusts.

A parent you name that is not an ancestor is recorded on request rather than refused, since that is how a stack looks before a restack. track says so first, because it explains why the branch will then read as needing one.

A trunk that has moved on is no longer an ancestor of the branches built from it, so recorded roots are always offered as candidates. Adopting the very first branch into an empty graph has neither, so it falls back to measuring from the fork point: one Git call per local branch, and only when the cheap paths found nothing.

track is also how a branch moves between sources. A branch g2g has adopted is answered from g2g’s graph; one it has not falls back to Graphite. Tracking or untracking a branch is all it takes to move it either way — see Where structure comes from.

g2g untrack removes a branch’s recorded parent. Its --scope takes branch (the default) or subtree, which removes the descendants’ edges too.

Untracking a branch in the middle leaves its children pointing at it, and says so:

Target synthetic-auth · --branch
● synthetic-auth ← target
Removes the recorded parent of synthetic-auth.
Leaves synthetic-login and synthetic-session without a tracked parent · they are not reparented.
Scope branch · 1 branch · /synthetic/repo/.git/g2g/graph.json
No changes were made. Rerun with --apply to remove these edges.

Reparenting them onto the grandparent would invent an edge you never asked for. doctor then names track for each of them, and naming the branch they are already recorded on makes it a trunk, rooting the stack where it stands. To remove a branch and put its children on what it sat on, use delete.

Terminal window
# Say a branch is a trunk: a second one, or one that lands somewhere later.
g2g track --branch synthetic-staging --as-trunk --apply
g2g track --branch synthetic-feature --as-trunk --into main --by rebase --apply

A trunk is a branch nothing is recorded under. g2g makes one on its own when you stack something on a branch it does not know, and create refuses to do that from anything but the default branch, so a second trunk — staging beside main — is said out loud first, with track --as-trunk. After that create, pull, restack and land treat it exactly as they treat main.

A trunk can also say where it goes when it is finished, with --into and, for how it lands there, --by squash|merge|rebase. Small branches are reviewed and squash-merged into synthetic-feature one at a time, and synthetic-feature later reaches main whole, by a merge that keeps those commits:

Terminal window
g2g track --branch synthetic-feature --as-trunk --into main --by rebase --apply
g2g land # from the top: lands the stack into synthetic-feature
g2g land --branch synthetic-feature # later: lands synthetic-feature into main, by rebase

That is not an edge. synthetic-feature has no parent, so every walk from above stops at it, and it is never replayed — other people land into it — so a colleague rebasing it cannot make it read as moved off anything. Keeping it current with main is a merge of main into it, which g2g does not make.

track --parent on a declared trunk puts it back in the stack below and ends the declaration, and untrack ends it too, stranding what sits on it. adopt, graphite adopt and github adopt treat a declared trunk as a disagreement rather than recording the parent they see under it. Landing the trunk itself is described in Land, and the reasoning in design-docs/declared-trunks.md.

Terminal window
# Start a branch on top of this one, switch to it, and record it. Preview first.
g2g create feature/login
g2g create feature/login --apply
# The same, committing what is staged onto the new branch.
g2g create feature/login -m "Add the login form" --apply
# Start it on another recorded branch instead of the one you are on.
g2g create feature/session --parent feature/auth --apply

create replaces git switch -c, a commit, and a track --parent retyping the branch you were just on. The preview is those commands, in order:

Target login · new branch
○ main trunk
│
● auth
● login new ← target
Commands this would run, in order
1 git switch -c login auth · start login at auth and check it out
2 g2g track --branch login --parent auth --apply · record it under auth
3 git commit -m 'Add login' · commit what is staged
Creates login at ab0c00ad2b9d, the tip of auth, and switches to it.
Records login under auth.
Commits 1 staged file (login) onto it.
Graph store · /work/app/.git/g2g/graph.json
No changes were made. Rerun with --apply to create it.

The parent is the branch you stand on or the one --parent names, so it is stated rather than inferred and there is no candidate list. It must already be in the g2g graph, or be the repository’s default branch (what refs/remotes/origin/HEAD names). Recording a child under a branch the graph does not know would quietly make that branch a trunk, so create refuses and names adopt instead:

Target auth · new branch
● main untracked
● auth new ← target
Apply blocked
main is not in the g2g graph, so recording auth under it would make main a trunk
g2g adopt --branch main record the stack main is on first
pass --parent with a branch the graph records
if main is a trunk, start its first branch with git switch -c and record it with g2g track --parent main
No changes were made. Apply would refuse until that is resolved.

In a repository with no default branch recorded, start the first branch on the trunk by hand and record it with track --parent; create works from there on.

It switches, records, then commits. A recording that fails is undone — you are put back where you were and the new branch is deleted — because nothing is on it yet. A commit that fails after the record (a hook refusing it, say) leaves the branch created, checked out and recorded with the changes still staged, and exits 3.

Terminal window
# Delete a branch, recording what sat on it on what it sat on. Then replay
# those onto their new parent, which drops the deleted branch's commits.
g2g delete --branch feature/abandoned --apply
g2g restack --branch feature/child --apply
# Fold a branch into its parent: the parent fast-forwards to it, it goes.
g2g fold --branch feature/login --apply
# Rename a branch and every record of it.
g2g rename --branch feature/login feature/sign-in --apply

delete, fold and rename change which branches a stack is made of, and act only on branches the g2g graph records — anything else is refused, naming g2g track, or plain git branch, which is all it would do. Each defaults to the branch you are on, or --branch.

Each orders its steps so that everything but the last can be put back, and does put it back if a later step fails. One that could not put back what it had done exits 3.

There is no split: dividing a branch’s commits means choosing which goes where, which git rebase -i and g2g create/g2g track do with a person choosing.

delete removes the local branch and records each branch that sat on it on the branch it sat on. Unlike untrack, which never reparents, this is what asking for the branch to go means.

The children keep their fork points, so the next restack replays only their own commits onto the new parent and the deleted branch’s commits leave them. The preview names every one of those commits that exists nowhere else — not in the parent by content, and on no remote-tracking ref — and suggests the restack.

Deleting the branch you stand on switches to its parent first, which git switch refuses rather than overwrite a local change. The remote branch and any pull request are left alone. A trunk, and a branch checked out in another worktree, are refused.

fold fast-forwards the parent to the branch, so its commits become the parent’s, then removes it as delete does; what sat on it already sits on the parent’s new tip.

Only a parent the branch sits directly on can be fast-forwarded, so one that has moved on is refused, naming g2g restack --branch <branch>: restack, then fold. Folding into a trunk is refused, naming g2g land: a branch joins its trunk through its pull request.

The parent’s other children then need a restack, and the preview says which. If the parent is checked out, the working tree moves with it; a local change in the way stops the fold and puts the parent back.

Terminal window
g2g rename --branch feature/login feature/sign-in --apply

rename runs git branch -m and rewrites the record: the branch’s own edge, the branches recorded on it, its place among the trunks, and its fork-point ref. The new name is checked with git check-ref-format --branch and must be free. Git moves another worktree along with the branch, so that is allowed.

A branch already published stays published under the old name, and so does its pull request. The preview says so when a remote-tracking ref carries the old name, and push then publishes the new name as a new branch.