gh-stack Check if compilation works; no tests yet!

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:


Installation

Building from source is the only option at the moment:

```bash

Install Rust

$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Configure PATH

$ export PATH="$HOME/.cargo/bin:$PATH"

Install gh-stack

$ cargo install gh-stack ```

Usage

```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)

Idempotently add a markdown table summarizing the stack

to the description of each PR in the stack.

$ gh-stack annotate 'stack-identifier'

Same as above, but precede the markdown table with the

contents of filename.txt.

$ gh-stack annotate 'stack-identifier' -p filename.txt

Print a description of the stack to stdout.

$ gh-stack log 'stack-identifier'

Automatically update the entire stack, both locally and remotely.

WARNING: This operation modifies local branches and force-pushes.

$ gh-stack autorebase 'stack-identifier' -C /path/to/repo

Emit a bash script that can update a stack in the case of conflicts.

WARNING: This script could potentially cause destructive behavior.

$ gh-stack rebase 'stack-identifier' ```

Examples

This is a quick overview of the ways this tool could be used in practice.

  1. Write some code, create local commits/branches: ```bash $ git checkout -b first

    Write code

    $ git add -A; git commit -m 'first'

    $ git checkout -b second

    Write code

    $ git add -A; git commit -m 'second #1'

    Write code

    $ git add -A; git commit -m 'second #2'

    $ git checkout -b third

    Write code

    $ git add -A; git commit -m 'third' ```

  2. Your Git tree now looks like:
    ```bash

  3. Push each branch: ```bash $ git push origin first:first second:second third:third

  4. Create a PR for each new branch (starting at first), and:

  5. 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)

  6. 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:

  7. Make changes to a branch that rewrites commits in some way (amend, remove a commit, combine commits): ```bash $ git checkout first

    Do some work

    $ 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

  8. 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.

Strategy

This is a quick summary of the strategy the autorebase subcommand uses:

  1. Find the 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.
  2. Check out the commit/ref that the first PR in the stack merges into (usually develop). We're going to cherry-pick the entire stack onto this commit.
  3. Cherry-pick all commits from the first PR (stopping at the cherry-pick boundary calculated in 1.) onto HEAD.
  4. Move the local branch for the first PR so it points at HEAD.
  5. The remote tracking branch for the first PR becomes the next cherry-pick boundary.
  6. Repeat steps 3-5 for each subsequent PR until all PRs have been cherry-picked over.
  7. Push all refs at once by passing multiple refspecs to a single invocation of git push -f.

Disclaimer

Use at your own risk (and make sure your git repository is backed up), especially because: