Skip to content

The forest

g2g status, g2g adopt, g2g track and g2g untrack maintain a branch forest g2g owns itself. They read Git and nothing else: no Graphite, no GitHub, no network.

This is the structure that exists for branches you have not pushed yet, and it is the only place a fork can live. GitHub native stacks are linear, and a pull request base cannot describe a branch that has no pull request.

Git supplies ancestry, but it does not keep the intended parent once a branch moves or its parent is squash-merged. Pull request bases describe only published work, and they describe what a merge will do rather than what you meant. Graphite can express a tree, but it may not be there.

So g2g records the one small, local fact every later operation needs: which branch is the intended parent of which.

Every branch has at most one parent, a parent may have many children, and a repository may have several roots, because it may have several trunks:

main
├─ synthetic-auth
│ ├─ synthetic-login
│ └─ synthetic-session
└─ synthetic-billing
└─ synthetic-invoice

Trees are the model even though GitHub is linear. To publish onto a GitHub native stack, one path from the trunk to a tip is selected and projected. The tree is neither flattened nor rejected for containing a fork. See Scope for how a selection is narrowed to a path.

Parents are inferred from commit ancestry, which needs no network and works for branches that have never been pushed. For a branch, the candidates are the local branches its commits sit on top of, ordered nearest first, plus the roots already recorded, because a trunk that has moved on is no longer an ancestor of the branches built from it.

track never chooses a parent when the answer is ambiguous. Without --parent it previews the candidates and blocks. The nearest ancestor is usually right, and “usually” is not a basis for writing down structure every later command trusts. adopt follows the same rule: you assert the trunk, and where ancestry cannot order two branches it refuses and names them. See When a command refuses for what that looks like, and Shape the stack for the commands.

A recorded parent whose tip is no longer an ancestor of its child is reported as needs restack, not silently reparented. A parent that moved is not the same thing as no parent, and treating the two alike would quietly move a stale child onto the trunk.

The fork point is stored. Each edge records the parent’s tip at the moment the edge was written. That answers which commits are mine: everything from the fork point to the branch. Without it a restack cannot work out what to replay, and it is also what lets a restack survive its parent being deleted after a merge. It changes only on structural events, such as adopting an edge or restacking, never on a commit or a force push.

A branch’s own tip is not stored. It moves with every ordinary commit, so recording it would make routine work look like the structure had changed.

Graph identity is derived, not stored. A graph is a connected component of the recorded edges, which is a computation rather than a record, so there is no identifier to generate and no merge or split to handle when two components join. Renaming a branch is a rewrite of its key rather than a migration.

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, which is why create refuses to do that from anything but the repository’s default branch. A second trunk, or one that lands somewhere later, is declared out loud with track --as-trunk. See Shape the stack.

The forest is stored under the repository’s Git common directory. Linked worktrees share it, it never appears in a diff or dirties a checkout, and it is neither pushed nor shared between clones. A fresh clone starts empty, which matches the unpublished branches those edges describe. See Compatibility and storage.

design-docs/g2g-owned-graphs.md describes the model, the storage decisions, and what is deliberately left out.