{"id":13671654,"url":"https://github.com/timothyandrew/gh-stack","last_synced_at":"2025-04-27T18:31:36.478Z","repository":{"id":48117481,"uuid":"269024738","full_name":"timothyandrew/gh-stack","owner":"timothyandrew","description":"Manage PR stacks/chains on Github","archived":true,"fork":false,"pushed_at":"2021-09-09T17:48:42.000Z","size":2916,"stargazers_count":152,"open_issues_count":5,"forks_count":12,"subscribers_count":2,"default_branch":"master","last_synced_at":"2025-04-14T08:05:59.671Z","etag":null,"topics":["code-review","github","rust"],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/timothyandrew.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-06-03T07:53:12.000Z","updated_at":"2025-04-13T11:22:20.000Z","dependencies_parsed_at":"2022-08-12T19:00:36.091Z","dependency_job_id":null,"html_url":"https://github.com/timothyandrew/gh-stack","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/timothyandrew%2Fgh-stack","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/timothyandrew%2Fgh-stack/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/timothyandrew%2Fgh-stack/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/timothyandrew%2Fgh-stack/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/timothyandrew","download_url":"https://codeload.github.com/timothyandrew/gh-stack/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":251187238,"owners_count":21549606,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["code-review","github","rust"],"created_at":"2024-08-02T09:01:15.613Z","updated_at":"2025-04-27T18:31:33.852Z","avatar_url":"https://github.com/timothyandrew.png","language":"Rust","readme":"# gh-stack [![Check if compilation works; no tests yet!](https://api.travis-ci.org/timothyandrew/gh-stack.svg?branch=master\u0026status=passed)](https://travis-ci.org/timothyandrew/gh-stack)\n\nI use this tool to help managed stacked pull requests on Github, which are notoriously difficult to manage manually. Here are a few examples:\n\n- https://unhashable.com/stacked-pull-requests-keeping-github-diffs-small\n- https://stackoverflow.com/questions/26619478/are-dependent-pull-requests-in-github-possible\n- https://gist.github.com/Jlevyd15/66743bab4982838932dda4f13f2bd02a\n\nThis tool assumes that:\n\n- All PRs in a single \"stack\" all have a unique identifier in their title (I typically use a Jira ticket number for this).\n- All PRs in the stack live in a single GitHub repository.\n- All remote branches that these PRs represent have local branches named identically.\n\nIt 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:\n\n- Add a markdown table to the PR description (idempotently) of each PR in the stack describing _all_ PRs in the stack.\n- Log a simple list of all PRs in the stack (+ dependencies) to stdout.\n- Automatically update the stack + push after making local changes.\n\n---\n\n- [Installation](#installation)\n- [Usage](#usage)\n  - [Examples](#examples)\n- [Strategy](#strategy)\n- [Disclaimer](#disclaimer)\n\n\n## Installation\n\nBuilding from source is the only option at the moment:\n\n```bash\n# Install Rust\n$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh\n\n# Configure `PATH`\n$ export PATH=\"$HOME/.cargo/bin:$PATH\"\n\n# Install `gh-stack`\n$ cargo install gh-stack\n```\n\n## Usage\n\n```bash\n$ export GHSTACK_OAUTH_TOKEN='\u003cpersonal access token\u003e'\n\n$ gh-stack\n\nUSAGE:\n    gh-stack \u003cSUBCOMMAND\u003e\n\nFLAGS:\n    -h, --help    Prints help information\n\nSUBCOMMANDS:\n    annotate      Annotate the descriptions of all PRs in a stack with metadata about all PRs in the stack\n    autorebase    Rebuild a stack based on changes to local branches and mirror these changes up to the remote\n    log           Print a list of all pull requests in a stack to STDOUT\n    rebase        Print a bash script to STDOUT that can rebase/update the stack (with a little help)\n\n# Idempotently add a markdown table summarizing the stack\n# to the description of each PR in the stack.\n$ gh-stack annotate 'stack-identifier'\n\n# Same as above, but precede the markdown table with the \n# contents of `filename.txt`.\n$ gh-stack annotate 'stack-identifier' -p filename.txt\n\n# Print a description of the stack to stdout.\n$ gh-stack log 'stack-identifier'\n\n# Automatically update the entire stack, both locally and remotely.\n# WARNING: This operation modifies local branches and force-pushes.\n$ gh-stack autorebase 'stack-identifier' -C /path/to/repo\n\n# Emit a bash script that can update a stack in the case of conflicts.\n# WARNING: This script could potentially cause destructive behavior.\n$ gh-stack rebase 'stack-identifier'\n```\n\n### Examples\n\n*This is a quick overview of the ways this tool could be used in practice.*\n\n1. Write some code, create local commits/branches:\n    ```bash\n    $ git checkout -b first\n    # Write code\n    $ git add -A; git commit -m 'first'\n  \n    $ git checkout -b second\n    # Write code\n    $ git add -A; git commit -m 'second #1'\n    # Write code\n    $ git add -A; git commit -m 'second #2'\n  \n    $ git checkout -b third\n    # Write code\n    $ git add -A; git commit -m 'third'\n    ```\n\n2. Your Git tree now looks like:  \n    ```bash\n    * 42315c4 U - (third) third\n    |\n    * 6db2c28 U - (second) second #2\n    |\n    * 5746a83 U - second #1\n    |\n    * e845ded U - (first) first\n    |\n    * 8031011 U - initial commit\n    ```\n\n3. Push each branch:\n    ```bash\n    $ git push origin first:first second:second third:third\n      * [new branch]      first -\u003e first\n      * [new branch]      second -\u003e second\n      * [new branch]      third -\u003e third\n    ```\n\n4. Create a PR for each new branch (starting at `first`), and:\n    - Ensure that all the PRs have a common identifier in their title (I'll use `[EXAMPLE-17399]` here). This identifier (currently) is required to be unique across all GitHub repositories accessible to you (including _all_ public repositories).\n    - Set the `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`.\n\n5. Log all PRs in the stack:\n    ```bash\n    $ gh-stack log 'EXAMPLE-13799'\n     #1: [EXAMPLE-13799] PR for branch `first` (Base)\n     #2: [EXAMPLE-13799] PR for branch `second` (Merges into #1)\n     #3: [EXAMPLE-13799] PR for branch `third` (Merges into #2)\n    ```\n\n6. Annotate all PRs with information about the stack:\n    ```bash\n    $ gh-stack annotate 'EXAMPLE-13799'\n     1: [EXAMPLE-13799] PR for branch `first`\n     2: [EXAMPLE-13799] PR for branch `second`\n     3: [EXAMPLE-13799] PR for branch `third`\n     Going to update these PRs ☝️  Type 'yes' to continue: yes\n     Done!\n    ```\n\n   This (idempotently) adds a table like this to the description of every PR in the stack:\n       \u003cimg src=\"img/annotate.png\" width=\"700\" /\u003e\n\n7. Make changes to a branch that rewrites commits in some way (amend, remove a commit, combine commits):\n    ```bash\n    $ git checkout first\n    # Do some work\n    $ git add -A; git commit --amend -m 'amended first'\n    ```\n\n   History has now diverged, and this will cause conflicts with dependent PRs when `first` is (force-)pushed.\n     ```bash\n     * e7cb9c6 U - (HEAD -\u003e first) amended first\n     |\n     | * 42315c4 N - (origin/third, third) third\n     | |\n     | * 6db2c28 N - (origin/second, second) second #2\n     | |\n     | * 5746a83 N - second #1\n     | |\n     | * e845ded N - (origin/first) first\n     |/\n     |\n     * 8031011 U - (origin/master, master) initial commit\n     ```\n\n8. Use the `autorebase` subcommand to fix this inconsistency (it requires a path to a local checkout of the repository):\n    ```bash\n    $ gh-stack autorebase --repo /tmp/test EXAMPLE-13799\n    Checking out Commit { id: 803101159653bf4bf92bf098e577abc436458b17, summary: \"initial commit\" }\n \n    Working on PR: \"first\"\n    Cherry-picking: Commit { id: e7cb9c6cdb03374a6c533cbf1fc23a7d611a73c7, summary: \"amended first\" }\n \n    Working on PR: \"second\"\n    Cherry-picking: Commit { id: 5746a83aed004d0867d52d40efc9bd800b5b7499, summary: \"second #1\" }\n    Cherry-picking: Commit { id: 6db2c2817dfed244d5fbd8cbb9b8095965ac9a05, summary: \"second #2\" }\n \n    Working on PR: \"third\"\n    Cherry-picking: Commit { id: 42315c46b42044ebc4b57a995a75b97699f4855a, summary: \"third\" }\n \n    [\"b45e5838a93b33411a5f0c9f726bc1987bc71ff5:refs/heads/first\", \"93170d2199ed9c2ae30d1e7492947acf477fb035:refs/heads/second\", \"a85a1931c44c3138d993128591af2cad2ef6c68d:refs/heads/third\"]\n    Going to push these refspecs ☝️  Type 'yes' to continue: yes\n    Enumerating objects: 12, done.\n    Counting objects: 100% (12/12), done.\n    Delta compression using up to 8 threads\n    Compressing objects: 100% (8/8), done.\n    Writing objects: 100% (11/11), 907 bytes | 453.00 KiB/s, done.\n    Total 11 (delta 3), reused 0 (delta 0)\n    remote: Resolving deltas: 100% (3/3), done.\n    To github.com:timothyandrew/test.git\n     + e845ded...b45e583 b45e5838a93b33411a5f0c9f726bc1987bc71ff5 -\u003e first (forced update)\n     + 6db2c28...93170d2 93170d2199ed9c2ae30d1e7492947acf477fb035 -\u003e second (forced update)\n     + 42315c4...a85a193 a85a1931c44c3138d993128591af2cad2ef6c68d -\u003e third (forced update)\n \n    Updating local branches so they point to the new stack.\n \n      + Branch first now points to b45e5838a93b33411a5f0c9f726bc1987bc71ff5\n      + Branch second now points to 93170d2199ed9c2ae30d1e7492947acf477fb035\n      + Branch third now points to a85a1931c44c3138d993128591af2cad2ef6c68d\n    All done!\n    ```\n\n    - This restores local history to a flat list and pushes the tip of each branch up to update the PRs themselves.\n      ```bash\n      * a85a193 N - (HEAD, origin/third, third) third\n      |\n      * 93170d2 N - (origin/second, second) second #2\n      |\n      * 61f64b6 N - second #1\n      |\n      * b45e583 N - (origin/first, first) amended first\n      |\n      * 8031011 U - (origin/master, master) initial commit\n      ```\n  \n    - If conflicts are encountered, `autorebase` will pause and allow you to fix the conflicts before resuming.\n\n## Strategy\n\nThis is a quick summary of the strategy the `autorebase` subcommand uses:\n\n1. 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. This is a heuristic and is not suitable for all situations, especially when changes have already been pushed or PRs are merged directly on GitHub. Accept an explicit boundary for the initial cherry-pick to avoid ambiguity here.\n2. 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.\n3. Cherry-pick all commits from the first PR (stopping at the cherry-pick boundary calculated in 1.) onto `HEAD`.\n4. Move the _local_ branch for the first PR so it points at `HEAD`.\n5. The _remote tracking_ branch for the first PR becomes the next cherry-pick boundary.\n6. Repeat steps 3-5 for each subsequent PR until all PRs have been cherry-picked over.\n7. Push all refs at once by passing multiple refspecs to a single invocation of `git push -f`.\n\n## Disclaimer\n\nUse at your own risk (and make sure your git repository is backed up), especially because:\n\n- This tool works for my specific use case, but has _not_ been extensively tested.\n- I've been writing Rust for all of 3 weeks at this point.\n- The `autorebase` command is in an experimental state; there are possibly edge cases I haven't considered.\n","funding_links":[],"categories":["Rust"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftimothyandrew%2Fgh-stack","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftimothyandrew%2Fgh-stack","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftimothyandrew%2Fgh-stack/lists"}