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.
Record what exists
Section titled “Record what exists”# 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-authg2g track --branch synthetic-login --parent synthetic-auth --apply
# Remove edges. --scope subtree removes descendants too.g2g untrack --branch synthetic-auth --applyg2g untrack --branch synthetic-auth --scope subtree --applyg2g 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 edgeg2g adopt record the whole ancestry at onceScope 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.
untrack
Section titled “untrack”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.
Trunks
Section titled “Trunks”# Say a branch is a trunk: a second one, or one that lands somewhere later.g2g track --branch synthetic-staging --as-trunk --applyg2g track --branch synthetic-feature --as-trunk --into main --by rebase --applyA 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:
g2g track --branch synthetic-feature --as-trunk --into main --by rebase --applyg2g land # from the top: lands the stack into synthetic-featureg2g land --branch synthetic-feature # later: lands synthetic-feature into main, by rebaseThat 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.
Add a branch
Section titled “Add a branch”create
Section titled “create”# Start a branch on top of this one, switch to it, and record it. Preview first.g2g create feature/loging2g 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 --applycreate 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.
Reshape a recorded stack
Section titled “Reshape a recorded stack”# 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 --applyg2g 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 --applydelete, 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
Section titled “delete”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.
rename
Section titled “rename”g2g rename --branch feature/login feature/sign-in --applyrename 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.