GitHub
These commands read or write pull requests, and so invoke gh. That is why
they live under g2g github rather than beside the commands that need only
Git: a command says in its own words when it will talk to GitHub, and someone
who does not use GitHub never has to read about them.
A branch is identified by its single open pull request. Closed and merged pull
requests on a reused branch name are history and never block github link,
github retarget, or github status. Two or more open pull requests for one
branch is the only ambiguity, and it fails closed. See
Push and submit.
A GitHub native stack is linear, so github link, github unlink and
github retarget take --scope stack or path only, and refuse a selection
that forks, naming the remedy rather than choosing a line. Selecting a leaf is
that remedy and needs no flag. See Scope.
Commands
Section titled “Commands”github status
Section titled “github status”g2g github status is the read-only first step for triaging a stack’s pull
requests, and never changes GitHub or Graphite. Where g2g status answers from
this checkout alone, this one asks GitHub.
g2g github statusg2g github status --from graphite # the pull requests, as Graphite groups themg2g github status --from github --scope stack # the structure the pull request bases describeIt renders the selected stack from the resolved structure, a chain as a flat
column and a fork as a tree, with its open pull request mappings and blocked
relationships highlighted. It reports each branch against its own parent
rather than whichever sibling sorts first, and says which record described it.
A branch no source describes is rendered as such rather than refused: “nothing
is stacked here” answers what was asked. --scope defaults to stack, because
reading is free.
The same bounded pull request read reports native stack number, size, and
position for each selected pull request, without a checkout or a second graph,
and marks the members of the native stack running through the tree. A healthy
path ends with one compact GitHub stack #… · selected path … · aligned line;
only missing or conflicting membership is annotated on individual nodes.
Each branch is annotated one axis at a time, so a mark means one thing and
carries its own colour. Read the column for ✗:
| Mark | Means |
|---|---|
base✓ |
the pull request is based where the resolved structure puts it |
base✗ |
it is not |
head✗ |
the pull request is not on the commit the branch is; a current one stays unannotated, so the stale ones stand out |
pr✗ |
a pull request that is missing, closed, or ambiguous — not a statement about a base, because a branch with no pull request has no base to be wrong about |
pr✓ |
a merged one, in the ordinary colour: it did what it was for |
A branch whose work is already in the branch below it reads as landed in …,
in the ordinary colour, and is offered forgetting rather than submitting. That
is a question Git answers and GitHub cannot: a squash merge lands the work
under a pull request whose head the branch never had, and a series somebody
cherry-picked has no pull request at all. Without that check the branch would
look like one merely missing a pull request, and the advice would be to open
one for a change already in the trunk.
--from github reads the structure from the pull request bases themselves,
with nothing recorded locally. It says what GitHub will merge rather than what
you intended, describes published branches only, and right after a parent lands
its children point at the trunk. See Sources for
what that source can and cannot tell you.
github retarget
Section titled “github retarget”After a restack the local stack is correct and GitHub may still record where each pull request used to sit. A base is what a merge follows, so leaving it stale means merging into the wrong branch.
g2g github retarget # which bases would move, and where fromg2g github retarget --applyIt is separate from submit deliberately. Creating a pull request and changing
what an existing one will merge into are different classes of act, and the
second wants its own preview: every line names the pull request, the base it
has, and the base it would get. It writes through
gh pr edit <number> --base <branch>.
It touches only the pull requests whose base disagrees with the resolved stack,
leaves branches with no pull request to submit, ignores merged and closed
ones, and refuses outright when a branch has more than one open pull request,
because nothing here can tell which one you meant. When GitHub already agrees
it is a no-op, which is what makes it safe to run after every restack.
github link
Section titled “github link”github link projects a resolved linear path onto GitHub’s native stack
feature. It works with a g2g-owned or Graphite-described path; Graphite is not
a prerequisite. Creating the relationship and repairing it are the same act, so
there is no separate reconcile command.
g2g github link # the path ending at the current branchg2g github link --branch feature/top # another local branch, without checking it outg2g github link --branch feature/top --from graphite # pin a sourceg2g github link --branch feature/top --trunk main # pin a Graphite multi-trunk ancestry's trunkg2g github link --branch feature/middle --scope path # stop at the selected branchg2g github link --branch feature/top --apply # revalidate, then let gh create or update itWhen at least two branches with pull requests need linking, it prints the exact
bottom-to-top gh stack link command. A path with one pull request is a
successful no-op: it prints Nothing to link and never constructs an invalid
command.
The preview renders the selected stack once, as a fixed-indent column because the path is linear: the trunk marked, the branches bottom-to-top, and pull request numbers and state in their own column.
Target beta · --branch
○ main trunk │ ● alpha #1 ● beta #2 ← target
Command to rungh stack link --base main alpha betaIt always shows the exact gh stack link command it validated, even when apply
is blocked, because the command is the plan’s destination and running it by
hand is a legitimate way to get gh’s own, often more specific, error. A blocked
preview states the reason above the command and heads it
Command to run once unblocked:
Target beta · --branch
○ main trunk │ ● alpha #1 ● beta unresolved: no open pull request ← target
Apply blocked: resolve every unresolved GitHub PR mapping first
Command to run once unblockedgh stack link --base main alpha beta--apply re-discovers and revalidates, prints one Ready to apply graph and
command, flushes that output, and invokes it. Copying the displayed command by
hand is a separate, deliberate snapshot and does not make g2g re-resolve
anything.
A linked stack cannot be landed with g2g land: GitHub will not merge a linked
pull request through gh pr merge, which is how land merges each one. Unlink
it first.
github unlink
Section titled “github unlink”g2g github unlink previews removal of a GitHub native stack relationship.
g2g github unlinkg2g github unlink --applyIt discovers the stack number from the selected path, with the same batched
read github status uses, so the number does not have to be copied by hand.
Discovery refuses rather than guesses: a path that is not linked, or that spans
more than one stack, is an error naming --stack-number, which remains
available to choose deliberately and always wins.
--apply invokes the supported gh stack unstack <number> after the selected
structure and pull request path are revalidated. It never changes Graphite,
branches, pull request metadata, review state, or pull request lifecycle.
Unlinking leaves the pull requests and their bases as they are.
link and unlink are a pair rather than one command with a removal flag on
purpose: unlink removes a whole projection from a published, shared artifact,
which is worth typing on purpose and worth being able to do without first
computing a diff.
github comment
Section titled “github comment”GitHub shows a pull request in isolation. github comment keeps one comment on
each pull request in the stack that lists the rest of it, with that pull
request in bold, so a reviewer can move through the stack without reading bases.
g2g github comment # what each comment would say, and which would changeg2g github comment --apply**Stack**
- base `synthetic-main`- #10 `synthetic-zero` · merged- #11 `synthetic-one` - #14 `synthetic-side` · +1 above- **#12 `synthetic-two`** 👈 this pull request- #13 `synthetic-three`
<sub>Kept up to date by [g2g 0.38.0](https://g2g.paulie.app), which edits this comment when the stack changes.</sub>The path from the base to this pull request is one flat column; what forks off it hangs beside the branch it grew from, counted rather than drawn; and everything built on this pull request is drawn in full. Every line names its branch, because the branch is what a reviewer matches against their own checkout, and GitHub already shows each pull request number with its title and state.
Which comments a run keeps
Section titled “Which comments a run keeps”It keeps the whole stack the branch belongs to, whichever branch you run it
from, and so has no --scope. Each comment draws the stack from its own pull
request, and keeping only part of a stack would leave the rest describing a
different one. Taking the whole stack is what makes every run, from anywhere on
it, converge on the same comments. Run from a trunk, it keeps every stack on it,
each separately.
When the branch a fork grew from merges, one stack becomes two, since each fork is now its own child of the trunk. From then on each one’s comments list only itself. That is correct, because they no longer share an unmerged ancestor, and it can be surprising the first time.
History survives the branch
Section titled “History survives the branch”Rerunning edits the comment it finds rather than adding another, found by an HTML marker in its first line. Pull requests that have merged out of the stack stay listed where they sat, marked merged: each comment records every pull request the stack has listed, so the history survives the branch being pruned and deleted. A pull request that merged before any comment was written was never recorded, and is not recovered.
What it writes, and what it will not
Section titled “What it writes, and what it will not”| The pull request | Its comment |
|---|---|
| open, no comment yet | added, if the stack lists at least two pull requests |
| any, one comment that is out of date | edited |
| any, one comment already saying this | left alone |
| merged, no comment | never added: nobody is reviewing it, and a new comment notifies everyone who did |
| one comment you cannot edit | left alone, and said |
| a conversation that takes no new comment (locked) | left alone, and said |
| two or more comments | left alone, and said: a person deletes the extra |
| a branch with two open pull requests | the whole run refuses, as github link and github retarget do |
A comment is current when what it says, the stack and the pull requests it records, matches what this run would write; the footer is left out of that comparison, so upgrading g2g alone does not edit every comment in every stack. A comment edited by hand stands until the stack changes, and is overwritten then. A comment is never deleted, even when a stack shrinks to one pull request.
If a run stops after writing some of its comments, it exits 3: it did part of
what was asked. See Preview and apply.
Kept by submit and land too
Section titled “Kept by submit and land too”submit and land keep the comments as their last act, because they change
which pull requests the stack is made of: submit once the pull requests are
opened, land on what remains above the branches it landed. Both say so in
their preview, and --no-comment skips it. If keeping the comments fails
there, the command’s own work stands and it exits 3, naming
g2g github comment --apply.
push, github retarget and github link never touch them: push never calls
gh, and the other two change no pull request’s membership of the stack.
See design-docs/stack-comment.md for how history is read back and whose history a comment is believed to hold.
Adopting a published stack
Section titled “Adopting a published stack”A stack someone else published has its structure in one place: the bases of its pull requests. Rather than switching to each branch and tracking it by hand, adopt it from there.
github adopt
Section titled “github adopt”git fetchgit switch synthetic-their-lower # every branch of the stack, heregit switch synthetic-their-topg2g github adopt # preview the stack of the branch you are ong2g github adopt --applyIt reads the stack exactly as g2g github status --from github does, with
--branch picking another branch’s stack and --scope stack (the default) or
trunk saying how much, and records it in g2g’s graph, so the branches can be
restacked. It is the only adoption that needs the network, because reading a
base invokes gh.
Its rules are graphite adopt’s: it
writes only g2g’s graph, adds what is missing, and refuses a branch g2g already
records under a different parent. Nothing is written to GitHub. Adoption is the
authority claim: from then on g2g answers for every branch it adopted.
Three things it will not do:
- Create a branch. The graph records local branches, so a branch the pull
requests place that is only on the remote refuses the adoption by name, with
git fetch && git switch <branch>orgit branch <branch> origin/<branch>as the way out. - Make a trunk of a feature branch. The stack must start from the
repository’s default branch (what
refs/remotes/origin/HEADnames) or from a branch g2g already records; otherwise it names theg2g trackthat establishes one. - Take a base’s tip as the fork point. The base may have moved since the pull request was opened, so each fork point is where the branch and its base last agreed. A base that is not an ancestor of its branch is still recorded, and the preview says the branch will read as needing a restack.
Revalidation re-reads GitHub. What is written is local, but it is written from what the pull requests said, and a base retargeted between the preview and the apply is exactly the change revalidation exists to catch.