I use this tool to help managed stacked pull requests on Github, which are notoriously difficult to manage manually. Here are a few examples:
This tool assumes that:
It then looks for all PRs containing this containing this identifier and builds a dependency graph in memory. This can technically support a "branched stack" instead of a single chain, but I haven't really tried the latter style. With this graph built up, the tool can:
Building from source is the only option at the moment:
```bash
$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
PATH
$ export PATH="$HOME/.cargo/bin:$PATH"
gh-stack
$ cargo install gh-stack ```
```bash
$ export GHSTACKOAUTHTOKEN='
$ gh-stack
USAGE:
gh-stack
FLAGS: -h, --help Prints help information
SUBCOMMANDS: annotate Annotate the descriptions of all PRs in a stack with metadata about all PRs in the stack autorebase Rebuild a stack based on changes to local branches and mirror these changes up to the remote log Print a list of all pull requests in a stack to STDOUT rebase Print a bash script to STDOUT that can rebase/update the stack (with a little help)
$ gh-stack annotate 'stack-identifier'
filename.txt
.$ gh-stack annotate 'stack-identifier' -p filename.txt
$ gh-stack log 'stack-identifier'
$ gh-stack autorebase 'stack-identifier' -C /path/to/repo
$ gh-stack rebase 'stack-identifier' ```
This is a quick overview of the ways this tool could be used in practice.
Write some code, create local commits/branches: ```bash $ git checkout -b first
$ git add -A; git commit -m 'first'
$ git checkout -b second
$ git add -A; git commit -m 'second #1'
$ git add -A; git commit -m 'second #2'
$ git checkout -b third
$ git add -A; git commit -m 'third' ```
Your Git tree now looks like:
```bash
Push each branch: ```bash $ git push origin first:first second:second third:third
Create a PR for each new branch (starting at first
), and:
[EXAMPLE-17399]
here). This identifier (currently) is required to be unique across all GitHub repositories accessible to you (including all public repositories).base
for each PR to the branch preceding it. Here, first
's PR is set to merge into master
, second
's PR is set to merge into first
, and third
's PR is set to merge into second
.Log all PRs in the stack:
bash
$ gh-stack log 'EXAMPLE-13799'
#1: [EXAMPLE-13799] PR for branch `first` (Base)
#2: [EXAMPLE-13799] PR for branch `second` (Merges into #1)
#3: [EXAMPLE-13799] PR for branch `third` (Merges into #2)
Annotate all PRs with information about the stack:
bash
$ gh-stack annotate 'EXAMPLE-13799'
1: [EXAMPLE-13799] PR for branch `first`
2: [EXAMPLE-13799] PR for branch `second`
3: [EXAMPLE-13799] PR for branch `third`
Going to update these PRs ☝️ Type 'yes' to continue: yes
Done!
This (idempotently) adds a table like this to the description of every PR in the stack:
Make changes to a branch that rewrites commits in some way (amend, remove a commit, combine commits): ```bash $ git checkout first
$ git add -A; git commit --amend -m 'amended first' ```
History has now diverged, and this will cause conflicts with dependent PRs when first
is (force-)pushed.
```bash
Use the autorebase
subcommand to fix this inconsistency (it requires a path to a local checkout of the repository):
```bash
$ gh-stack autorebase --repo /tmp/test EXAMPLE-13799
Checking out Commit { id: 803101159653bf4bf92bf098e577abc436458b17, summary: "initial commit" }
Working on PR: "first" Cherry-picking: Commit { id: e7cb9c6cdb03374a6c533cbf1fc23a7d611a73c7, summary: "amended first" }
Working on PR: "second" Cherry-picking: Commit { id: 5746a83aed004d0867d52d40efc9bd800b5b7499, summary: "second #1" } Cherry-picking: Commit { id: 6db2c2817dfed244d5fbd8cbb9b8095965ac9a05, summary: "second #2" }
Working on PR: "third" Cherry-picking: Commit { id: 42315c46b42044ebc4b57a995a75b97699f4855a, summary: "third" }
["b45e5838a93b33411a5f0c9f726bc1987bc71ff5:refs/heads/first", "93170d2199ed9c2ae30d1e7492947acf477fb035:refs/heads/second", "a85a1931c44c3138d993128591af2cad2ef6c68d:refs/heads/third"] Going to push these refspecs ☝️ Type 'yes' to continue: yes Enumerating objects: 12, done. Counting objects: 100% (12/12), done. Delta compression using up to 8 threads Compressing objects: 100% (8/8), done. Writing objects: 100% (11/11), 907 bytes | 453.00 KiB/s, done. Total 11 (delta 3), reused 0 (delta 0) remote: Resolving deltas: 100% (3/3), done. To github.com:timothyandrew/test.git
Updating local branches so they point to the new stack.
Branch third now points to a85a1931c44c3138d993128591af2cad2ef6c68d All done! ```
8031011 U - (origin/master, master) initial commit ```
autorebase
will pause and allow you to fix the conflicts before resuming.This is a quick summary of the strategy the autorebase
subcommand uses:
merge_base
between the local branch of the first PR in the stack and the branch it merges into (usually develop
). This forms the boundary for the initial cherry-pick.develop
). We're going to cherry-pick the entire stack onto this commit.HEAD
.HEAD
.git push -f
.Use at your own risk (and make sure your git repository is backed up), especially because:
autorebase
command is in an experimental state; there are possibly edge cases I haven't considered.