7 个版本

0.1.6 2020年6月22日
0.1.5 2020年6月19日

#2711命令行工具

每月 25 次下载

MIT 许可证

2.5MB
580

gh-stack 检查编译是否成功;尚未进行测试!

我使用这个工具来帮助管理 Github 上的堆叠拉取请求,这些请求通常难以手动管理。以下是一些示例

此工具假定

  • 单个 "堆叠" 中的所有 PR 标题都具有唯一的标识符(我通常使用 Jira 工作单号作为此目的)。
  • 堆叠中的所有 PR 都位于单个 GitHub 仓库中。
  • 这些 PR 所表示的所有远程分支都有一个同名本地分支。

然后它会查找所有包含此标识符的 PR,并在内存中构建一个依赖图。技术上这可以支持 "分支堆叠" 而不是单个链,但我尚未真正尝试后者。构建了此图后,工具可以

  • 为堆叠中每个 PR 的描述(幂等)添加一个 Markdown 表格,描述堆叠中的所有 PR。
  • 将堆叠中所有 PR(+ 依赖项)的简单列表记录到 stdout。
  • 在本地进行更改后自动更新堆叠并推送。

安装

目前仅可通过从源码构建来安装

# 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

用法

$ export GHSTACK_OAUTH_TOKEN='<personal access token>'

$ gh-stack

USAGE:
    gh-stack <SUBCOMMAND>

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'

示例

以下是对此工具在实际应用中使用方式的快速概述。

  1. 编写一些代码,创建本地提交/分支

    $ 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. 您的 Git 树现在看起来像

    * 42315c4 U - (third) third
    |
    * 6db2c28 U - (second) second #2
    |
    * 5746a83 U - second #1
    |
    * e845ded U - (first) first
    |
    * 8031011 U - initial commit
    
  3. 推送每个分支

    $ git push origin first:first second:second third:third
      * [new branch]      first -> first
      * [new branch]      second -> second
      * [new branch]      third -> third
    
  4. 为每个新分支创建一个 PR(从 first 开始),并

    • 确保所有 PR 的标题中都有一个共同的标识符(我将使用 [EXAMPLE-17399])。此标识符(目前)必须在整个您可访问的 GitHub 仓库(包括所有公共仓库)中唯一。
    • 将每个PR的base设置为它之前的分支。在这里,first的PR设置为合并到mastersecond的PR设置为合并到first,而third的PR设置为合并到second
  5. 记录堆栈中的所有PR

    $ 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. 使用堆栈信息注释所有PR

    $ 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!
    

    这(幂等)会在堆栈中每个PR的描述中添加一个类似这样的表格:

  7. 修改一个以某种方式重写提交的分支(修改、删除提交、合并提交)

    $ git checkout first
    # Do some work
    $ git add -A; git commit --amend -m 'amended first'
    

    现在历史已经分叉,当first被(强制)推送时,这会导致与依赖PR的冲突。

    * e7cb9c6 U - (HEAD -> first) amended first
    |
    | * 42315c4 N - (origin/third, third) third
    | |
    | * 6db2c28 N - (origin/second, second) second #2
    | |
    | * 5746a83 N - second #1
    | |
    | * e845ded N - (origin/first) first
    |/
    |
    * 8031011 U - (origin/master, master) initial commit
    
  8. 使用autorebase子命令来修复这种不一致性(它需要一个本地仓库签出的路径)

    $ 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
     + e845ded...b45e583 b45e5838a93b33411a5f0c9f726bc1987bc71ff5 -> first (forced update)
     + 6db2c28...93170d2 93170d2199ed9c2ae30d1e7492947acf477fb035 -> second (forced update)
     + 42315c4...a85a193 a85a1931c44c3138d993128591af2cad2ef6c68d -> third (forced update)
    
    Updating local branches so they point to the new stack.
    
      + Branch first now points to b45e5838a93b33411a5f0c9f726bc1987bc71ff5
      + Branch second now points to 93170d2199ed9c2ae30d1e7492947acf477fb035
      + Branch third now points to a85a1931c44c3138d993128591af2cad2ef6c68d
    All done!
    
    • 这会将本地历史恢复为平列表,并将每个分支的尖端推送到更新PR本身。

      * a85a193 N - (HEAD, origin/third, third) third
      |
      * 93170d2 N - (origin/second, second) second #2
      |
      * 61f64b6 N - second #1
      |
      * b45e583 N - (origin/first, first) amended first
      |
      * 8031011 U - (origin/master, master) initial commit
      
    • 如果遇到冲突,autorebase会暂停,让您在继续之前修复冲突。

策略

这是对autorebase子命令使用的策略的简要总结

  1. 找到堆栈中第一个PR的本地分支与其合并到的分支之间的merge_base(通常是develop)。这构成了初始 cherry-pick 的边界。这是一个启发式方法,并不适用于所有情况,尤其是在更改已经推送或PR直接在GitHub上合并的情况下。接受一个显式的边界来避免这里的歧义。
  2. 检出堆栈中第一个PR合并到的提交/引用(通常是develop)。我们将 cherry-pick 整个堆栈到这个提交。
  3. 从第一个PR cherry-pick 所有提交到HEAD(停止在步骤1中计算的 cherry-pick 边界)。
  4. 移动第一个PR的本地分支,使其指向HEAD
  5. 第一个PR的远程跟踪分支成为下一个 cherry-pick 边界。
  6. 对每个后续PR重复步骤3-5,直到所有PR都已 cherry-pick。
  7. 通过传递多个 refspecs 到 git push -f 的单个调用,一次性推送所有引用。

免责声明

自行承担风险(并确保您的 git 仓库已备份),尤其是因为

  • 此工具适用于我的特定用例,但尚未经过广泛测试。
  • 我在这个时候已经写了3周的Rust。
  • autorebase命令处于实验状态;可能存在我没有考虑到的边缘情况。

依赖项

~21–34MB
~610K SLoC