Merge a stack of pull requests
Merging a stack of PRs manually through GitHub can be time-consuming and involve a lot of context switching. You merge, rebase, wait for CI to pass, and merge again all the way up the stack—or you're forced to ditch the clean PRs you've carefully created, squashing your changes back into a single mega-PR for the sake of merge speed.
Graphite offers an automated solution that gives you the best of both worlds.
Once your PR is approved and passes CI (and any other checks or merge protections you've enabled in GitHub), you can merge it from Graphite. Just click the purple "Merge" button on the right side of the PR title bar.
Depending on which PR you are viewing in the stack, this button will behave differently:
If you're on the first PR in the stack or are merging a stack of one, the
Mergebutton will just merge that single PR.
If you're in the middle of a stack,
Nis your position in the stack and will merge all PRs up until the
If you're at the top of the stack, all PRs in the stack will be merged.
Merge (N) PRs lets you fire-and-forget merging your stack. When you click the button, Graphite automatically merges each of your PRs one-by-one.
This feature rebases PRs on an as-needed basis (to avoid merge conflicts generated by GitHub's lack of stack support). And it waits for GitHub checks to pass at each step of the way before merging.
Not all PRs in the stack need to be accepted and passing checks to merge.
For example, if only PRs 1, 2, and 3 in a stack of 5 have been accepted and are passing checks, you can still utilize
Merge all (n) on PR 3 to merge those into trunk—just make sure you
gt sync and
gt submit the rest of your stack before merging again.
merge will first present you with a modal to configure your merge options before starting the merge job. In this merge modal, you have the option to:
Select your preferred merge strategy. Graphite pre-fills with your default merge strategy from GitHub.
Edit a custom commit title and message for your PR. Graphite will use your PR title and message as the commit title and message by default.
Use your GitHub admin merge privileges to merge past blockers (if applicable).
A confirmation appears in the bottom left corner of the screen once your PR is merged.
Make sure to run
gt sync on the Graphite CLI immediately after merging a change to your remote trunk branch—this will zip through and delete your merged branches, ensuring that your local environment is up-to-date and ready for you to keep developing.
Graphite will update a single comment on GitHub with the status of your PR's merge.
Merge (N) fails due to a rebase conflict, go to your terminal and run
gt sync &&
gt restack &&
gt submit --stack (resolving any conflicts along the way and running
gt continue). Once you've done this, go back to the affected PR in Graphite (or to the same place in the stack where you initially kicked off the
Merge (N) job), and click
Merge (N) one more time to re-queue the PR for merging.
When PRs are merged on GitHub using a squash and merge or a rebase and merge, GitHub creates a commit/set of commits for those merged changes.
This means that if you have a stack with
PR A at the base, followed by
PR B and
PR C, when
PR A is merged into trunk:
GitHub creates a commit or set of commits on trunk for the changes in
Critically, because this is a new commit, the common ancestor of
PR Band trunk does not change.
As a result, GitHub thinks
PR Bnow includes the commits of both
PR Band the already-merged commits of
This behavior becomes problematic when you have a
PR A and
PR B that modify the same lines and you merge both PRs using one of the aforementioned merge strategies. For any
PR C that is stacked atop
PR A and
B, the following transpires:
The latest version of the change lines on trunk are those in
GitHub believes that
PR Ccontains the commits in
When GitHub tests mergeability of
PR C, it first tries to apply
PR A—and now gets a merge conflict—even though in reality you're simply replaying history and there's no new change.
If you use the Graphite CLI, you'll notice that the CLI handles this scenario for you intelligently. When GitHub reports these sorts of merge conflicts, a
gt sync will pull down the latest changes and rebase PR C for you, cutting out the problematic commits—and a subsequent re-submit will then eliminate the detected merge conflict for PR C.
Merge (N) is designed to similarly automatically resolve this type of merge conflict without the need for manual intervention or monitoring. When a merge conflict is found, the merge cron performs a shallow clone of the repository, containing just the stack commits and the trunk commit and utilizes Graphite's knowledge of the stack to perform the same set of operations.
Graphite can't resolve any legitimate merge conflicts as a result of racing PRs on trunk that require human intervention. If a Graphite rebase fails, Graphite will cancel the merge job. You must restart the job by fixing the issue locally, re-submitting the PR, and enqueueing a new merge job.
Each time the cron job processes a merge job it runs through the following decision tree:
If the base PR in the stack has pending GitHub checks, do nothing.
If the base PR in the stack is passing all GitHub checks and can be merged, merge. The next PR in the stack is now the new base PR.
If the base PR in the stack has merge conflicts, rebase the PR and re-submit it (re-entering the waiting-for-CI phase).
During the merge process, Graphite prioritizes:
Speed of overall stack merge.
Minimizing the number of total CI runs.
To achieve these principles, it's important to note that:
Graphite merges rather than rebasing each individual PR before merging.
Graphite only rebases PRs lazily. When Graphite detects a merge conflict on a PR, Graphite only rebases that PR, and not the additional PRs further up the stack. This means that if a stack has
mmerge conflicts, there will only be
mtotal rebases (and additional CI runs) kicked off by the merge process.
Graphite's cron job to process outstanding merge jobs runs at a cadence of once per minute.
As a result, the length of a merge job depends on how many PRs need to be rebased. If there are no merge conflicts,
Merge (N) will take
n minutes, but if there are merge conflicts, job time is a byproduct of the time it takes to run GitHub checks on a PR and how many PRs encounter merge conflicts.
Today, merge stack supports label-based merge queues, with future plans to support GitHub's merge queue (currently not compatible).
If you're not sure whether
Merge (N) will work with your team's merge process, feel free to reach out to firstname.lastname@example.org—we'd love to help you unlock this tool.
Merging with the Graphite app saves a substantial amount of time. However, if you'd like to manually merge your PRs, merge the PRs in the stack one at a time:
Merge the bottom PR of your stack into your trunk on the Graphite app (or through GitHub).
gt sync --restackfrom any branch of your stack to pull
trunkto local, delete the merged branch, and restack the rest of your stack on
From any branch in your stack, run
gt submitto force push the restacked branches so the new bottom of your stack can be merged into
Repeat until you've landed all of the branches in your stack.
We recommend always merging from the bottom of the stack. While there are other techniques, we've found that this is the most intuitive and safest model for our users.
Merging in reverse order from the middle or top of the stack and collapsing all of the PRs into one is the fastest way to merge an entire stack, but there are a number of pitfalls for users—namely around syncing this merged state locally (to continue developing on any upstack PRs) or undoing these changes if a user decides not to merge a PR. This may lead to perilous situations where users have felt like they've lost code or can't re-create their previous state.
While certainly not impossible, it's also harder to re-derive the original stack of PRs when looking at the