iimon TECH BLOG

iimonエンジニアが得られた経験や知識を共有して世の中をイイモンにしていくためのブログです

github PRを管理するツールghstackを使ってみた!

はじめに

 こんにちは、iimonエンジニアのみやこしです、いきなりですが、皆さんは大きなプロジェクトを開発する際に、機能ごとに複数のプルリクエストを作成するのが面倒だと感じたことはありませんか? プルリクを小さな単位でレビューしやすく、かつ管理もしやすいツールがないか調べていたところ、ghstack という GitHub 向けの管理ツールを見つけました。 実際に使ってみたところ便利だったので、今回は ghstack を紹介したいと思います。

依存関係のあるプルリクエス

 開発中に大規模なプロジェクトや複雑な機能を実装する際は、複数の小さなブランチとプルリクエストに分割し、順番に積み上げるように管理するのが一般的です。 このようにすることで、プルリクエストを小さな単位でレビューしやすくなるほか、機能間の依存関係に対応しやすくなるというメリットがあります。 上記のようにPR2がPR1に依存、PR3がPR2に依存といった依存関係になります しかし、レビュワーが最初のプルリクエスト(PR1)にコメントをし、その内容に基づいて修正を加えた場合、その変更は後続のプルリクエスト(PR2、PR3など)にも反映させる必要があります。 このような場合、すべてのプルリクエストを順番に修正・マージし直す必要があり、手間がかかるというデメリットがあります。

ghstack

そこで役に立つのがghstackです、ghstack は、1つのブランチ内の各コミットに対して PR を作成でき、前のコミットを変更するとそれに依存している後ろの PR を自動で更新できます。

PRの提出と更新
  • コミットを追加・更新した後は、そのブランチで ghstack を実行するだけでOK

  • コミットの追加・削除・修正・順序の入れ替えなど、どのような変更でも自由に行うことができ、ghstack は既存の PR を自動で更新します。

  • 新しく追加したコミットには、新しい PR が自動的に作成されます。

  • 削除されたコミットに対応する PR は自動では削除されません。手動で削除することは可能ですが、削除しなくても他の(残っている)コミットには影響しません。

    PRの構造

  • gh/ユーザー名/1/base: このブランチは、該当コミットが元にしているベースブランチで、masterのような存在になります

  • gh/ユーザー名/1/head: このブランチが実際の変更内容なり、ブランチに対して PR が作成され、base にマージされます

  • gh/ユーザー名/1/orig: ローカルでの実際のコミットです、GitHub 上の PR ではこのブランチは使われません。

自分的には少し名前がややこしいと思いましたが ghstack は、スタック内の順序や依存関係を追跡するために gh/username/N/head という命名規則を使用するので、基本的の変更はできないです😢

PRのマージ

ghstackではPRをマージする時絶対git mergeを使用しないでください

ghstackの仕組み上、各コミット=PR なので、それぞれに対してリベースが必要があるからです。 代わりにghstack landをを使います!ghstack によって GitHub 上に作成された複数の PRを、 ローカルのブランチにまとめて rebase して、main にまとめて push してくれます。 例:2番目のプルリクとそれ以前全部のプルリクをマージしたい場合

ghstack land https://github.com/user_name/use_ghstack/pull/51

git rebase interactive(インタラクティブ)モード

git rebaseについてはこちら→ git rebaseの具体的なメリット

git rebase -iはコミットの修正・調整・削除などを行い、履歴をきれいに整えるために使用します。 たとえば、下記の画像のように、同じような内容のコミットを複数回に分けてしまった場合、それらを1つにまとめて履歴を整理することができます。 これは、履歴を後から確認しやすくするのと、レビュワーにとっても内容を把握しやすくなります

使い方

git rebase -i HEAD~3

のコマンドを打つとHEADから3つまでのコミットが表示されます

pick ad6276b ChromeStorageを呼ぶところを変更
pick 7c75239 早期return
pick 793ec76 早期return

# Rebase 3db27e9..793ec76 onto 3db27e9 (3 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup [-C | -c] <commit> = like "squash" but keep only the previous
#                    commit's log message, unless -C is used, in which case
#                    keep only this commit's message; -c is same as -C but
#                    opens the editor
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
#         create a merge commit using the original merge commit's
#         message (or the oneline, if no original merge commit was
#         specified); use -c <commit> to reword the commit message
# u, update-ref <ref> = track a placeholder for the <ref> to be updated
#                       to this position in the new commits. The <ref> is
#                       updated at the end of the rebase
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.

例えば7c75239と793ec76をまとめたい場合はsquash を使います

pick ad6276b ChromeStorageを呼ぶところを変更
pick 7c75239 早期return
squash 793ec76 早期return

保存して終了すると一個目の早期returnと二個目の早期returnのコミットが統合されます。

他にもコミットを操作するコマンド:

  • reword コミットメッセージを変更する

  • edit コミット内容を修正する

  • fixup 前のコミットと統合(メッセージは破棄)

  • drop コミットを削除

ghstack使ってみた

事前準備:

  • ターミナルで以下のコマンドを打つ
pip install ghstack

1.Settings(設定) → Developer Settings(開発者設定) → Personal Access Tokens(個人用アクセストークン) に進む

2.public_repo のアクセス権限のみを付与したトークンを生成します

3.以下の内容で ~/.ghstackrc ファイルを作成します

[ghstack]
github_url = github.com
github_oauth = [your_own_token]
github_username = [your_username]
remote_name = upstream [if remote is called upstream and not origin]

ブランチを作成し、それぞれ異なる2つの変更に対してコミットを行った後、ghstack を実行すると、以下のように2つのプルリクエストが作成されます。

# Summary of changes (ghstack 0.11.0)

 - Created https://github.com/MiyakoshiMari/use_ghstack/pull/44
 - Created https://github.com/MiyakoshiMari/use_ghstack/pull/45

1個目のプルリク差分:

2個目のプルリク差分:

このように、2つ目のプルリクエストは1つ目のプルリクエストに対する差分が表示されており、両者の間に依存関係があることがわかります。

例えば、レビュワーが1つ目のプルリクエストに対してコメントをしたとします。その場合、その内容に基づいて修正を加えることになります。 ghstack では「1つのコミット = 1つのプルリクエスト」なので、特定のプルリクに対する修正は該当のコミットを修正する形になります。

その際にgit rebase -iを使います。 直前に2つコミットしたので下記のコマンドを打ちます

git rebase -i HEAD~2

今回はコミットに対して修正をしたいのでeditコマンドを使います

edit a97b94f step1
pick 42602ad step2

この場合a97b94f のコミットで処理が一時停止し、保存して終了すると以下のようなメッセージが表示されます

Stopped at a97b94f...  step1
You can amend the commit now, with

  git commit --amend 

Once you are satisfied with your changes, run

  git rebase --continue

1個目のコミットの内容に戻りそこで修正を加えます、修正後以下のコマンドを打ちコミットを修正内容に上書きします

git commit --amend

git rebase --continue を実行してリベースを再開します。 (1つ目のコミットを修正すると、それに依存する2つ目のコミットでコンフリクトが発生することがあるため、その都度コンフリクトを解消する必要があります。)

ghstackを実行すると下記のようにそれぞれ先ほど作ったプルリクに変更分がアップデートされるので強制プッシュする必要がなくなります。

# Summary of changes (ghstack 0.11.0)

 - Updated https://github.com/MiyakoshiMari/use_ghstack/pull/44
 - Updated https://github.com/MiyakoshiMari/use_ghstack/pull/45

1個目のプルリク:

2個目のプルリク:

最後に以下のコマンドでマージします

ghstack land <PRのURL>

最後に

ghstack はこのように依存関係のある複数の PRを自動的に管理し、ある PR が別の PR に依存している場合でも、ghstack はそれらの関係を把握して順番に投稿・更新してくれます、修正を加えた際にも、依存元・依存先の PR も自動で再アップロードされ、手動で差分を管理する必要がなくなります。また、ghstack land コマンドを使えば、依存しているすべての PR を正しい順序で一括マージすることができてとても便利だなと感じました、用途に応じて使ってみてもいいかなと思いました。 この記事を読んで興味を持って下さった方がいらっしゃれば、カジュアルにお話させていただきたいです。是非ご応募をお願いいたします!

Wantedly / Green

参考文献

github.com

zenn.dev

qiita.com