概要
プルリクエストとは、リポジトリへの変更を提案し、その変更についてレビューなどの共同作業を行うための仕組みです。変更はブランチを介して提案されます。適用する内容を含むheadブランチから、変更の適用先であるbaseブランチに向けてプルリクエストを作成し、承認された内容だけが取り込まれます。コミットメッセージ
コミットメッセージは、コミットに含まれる変更内容を要約する説明です。書き方をチームの規約として統一すると、履歴のどのコミットが何を変更したのかを一定の形式で追跡できます。 規約にはConventional Commitsの形式が広く使われます。<型>: <要約>の形式で、先頭に変更の種類を示すプレフィックスを付けます。
Conventional Commitsと合わせて、Semantic Versioningを使うことがあります。
MAJOR.MINOR.PATCHの形式で、変更の互換性に応じて上げるバージョンレベルを決めます。ライブラリやAPIなど、互換性を管理する必要がある開発で利用することがあります。この2つを組み合わせると、コミットメッセージから変更内容を理解しやすくなり、リリースのバージョン番号を機械的に決定できるようになります。
組み合わせる場合は、
<レベル>-<型>: <要約>のように型の前にバージョンレベルを付けます。破壊的変更を伴う機能追加はmajor-feat、互換性に影響しない小さな機能追加はpatch-featのように、型だけでは決まらないバージョンレベルを明示できます。
コミットメッセージは<型>: <要約>の1行だけでも成立します。変更の背景や理由を説明したい場合は、空行を挟んで本文やフッターを加えます。
タイトル
プルリクエストのタイトルは、変更内容を1行で要約する説明です。タイトルは一覧・通知・マージ後の履歴に表示されるため、本文を開かなくても変更内容を判別できる書き方に統一します。 タイトルはコミットメッセージをベースに、同じくConventional CommitsとSemantic Versioningの形式で決めます。コミットが1つのプルリクエストではそのコミットメッセージをそのまま使い、複数のコミットを含むプルリクエストでは変更全体を代表する型と要約を選びます。テンプレート
プルリクエストの作成時に説明欄に自動挿入されるMarkdownのテンプレートを用意することができます。テンプレートがあると、概要・変更点・動作確認といった説明の構成がプルリクエストごとにばらつかず、レビュアーに渡すべき情報を事前にルール化できます。1
テンプレートファイルを作成する
リポジトリに
.github/PULL_REQUEST_TEMPLATE.mdを作成します。2
雛形を書く
レビュアーが必要とする情報を、見出しとコメントで区切って並べます。
3
デフォルトブランチへマージする
テンプレートはデフォルトブランチに存在して初めて適用されます。マージ後に作成されるプルリクエストから、説明欄に雛形が自動で挿入されます。
Draft
Draftとは、レビューの準備が整っていないことを明示するプルリクエストの下書き状態です。Draft状態の間はマージがブロックされるため、作業途中の変更が誤って取り込まれることはありません。 未完成の段階でDraftとしてプルリクエストを開いておくと、実装や設計の方向性について、実コードの差分を見ながら早い段階でフィードバックを得られます。文書や口頭のすり合わせと違い、動くコードを前提に議論できるため、レビュアーと認識を早い段階で合わせられます。 必要なフィードバックが揃い、認識が合ったら、そのまま作業を継続しても、Draftのプルリクエストをcloseして新たにプルリクエストを作り直しても構いません。作業が進むほど方向転換のコストは膨らむため、早い段階で手戻りしてサンクコストを最小限に抑える手段としてDraftは有効です。 作成時に「Create draft pull request」を選択し、レビューを依頼できる状態になったら「Ready for review」でDraft状態を解除します。粒度
プルリクエストの粒度とは、1つのプルリクエストが1つのことに注力できているかという、変更内容の一意性です。差分の行数やファイル数ではなく、変更内容が一意であるかどうかで測ります。サイズと粒度の違い
サイズ(変更行数・ファイル数)と粒度(変更内容の一意性)は別の尺度であり、分割の判断に使うのは粒度です。判断基準は、差分が大きいかどうかではなく、プルリクエストの変更内容が一意である(1つのことに注力できている)かどうかです。なぜ粒度が重要か
焦点の絞られた差分は、レビューする範囲も絞られます。作業量の合計が同じでも、10の変更を1つのプルリクエストでレビューするより、1の変更を10個のプルリクエストに分けてレビューする方が、作成者・レビュアー双方の負担が小さくなります。レビューが速い
変更内容が一意な差分はレビュアーの頭に収まるため、レビューが後回しにされず、数分で終わります。
切り戻しが容易
変更内容が一意なプルリクエストをrevertすれば、その変更だけを巻き戻し、余計なものを巻き込みません。
conflictが減る
生存期間が短いブランチは、conflictが発生しにくくなります。
原因特定が速い
変更内容が一意なプルリクエストは影響範囲も絞られるため、障害時の切り分けが速くなります。
粒度を適切にするコツ
先に作業を分解する
コードを書く前に独立して作業できるタスクへ分解し、各タスクが1つのプルリクエストに対応するようにします。詳細はタスク分解を参照してください。迷ったら小さく分割する
適切な粒度に迷ったら、小さい粒度の方を選択します。レビューのやり取りの中で粒度について議論すれば、認識をレビュアーと揃えられます。 大きく作ったプルリクエストを後から分割するよりも、小さく作って後から粒度を大きくするほうが簡単です。迷ったときは小さく出し、段階的に肉付けしていきます。レビューを最優先にする
粒度を小さくすると、マージしないと次の作業に着手できない場面が増えます。ここでレビューが滞るとボトルネックになり、開発スピード全体が低下します。そのため、レビュー依頼が来たら自分の作業より優先して対応します。 作業を中断するコンテキストスイッチが気になるかもしれませんが、粒度が適切なプルリクエストのレビューは数分、時には1分以内で終わるため、中断のコストはわずかです。粒度を適切に保つことが、レビューを最優先にする習慣がチームに根付く土台となります。そのレビューで実際に何を確認するのかはコードレビューを参照してください。CIを高速に保つ
粒度を小さくするとプルリクエストの数が増え、それに比例してCIの実行回数も増えます。CIが遅いままだと、マージまでの待ち時間が積み重なって開発効率が逆に落ちます。 実行時間は10分切りが目安で、5分を超えたら遅いと判断して改善します。具体的な高速化の手法はCI/CDのCIを高速に保つを参照してください。デプロイとリリースを分ける
コードを本番環境へ反映するデプロイと、機能をユーザーへ公開するリリースを別の出来事として扱うと、機能全体の完成を待たずに未完成の作業をマージできるため、プルリクエストを小さく出し続けられます。 詳細はリリースを参照してください。スタック
スタック(stacked pull requests)とは、依存関係のある一連の変更を、互いに積み重なった小さなプルリクエストの連なりに分割する手法です。スタック内の各プルリクエストは1つの変更内容に注力し、baseにはデフォルトブランチではなく直下のプルリクエストのブランチを指定します。 プルリクエストを小さく保つ方針は、タスク間の依存関係と衝突しがちです。タスクBがタスクAの成果の上に成り立つ場合、Aのプルリクエストがレビュー・マージされるまでBのプルリクエストをデフォルトブランチに向けて開けないため、待つか、AとBを1つの大きなプルリクエストに束ねるかの二択になります。スタックはこれを解消します。AのブランチからBのブランチを切り、BのプルリクエストをAのブランチに向けて開くことで、どちらも小さいまま、どちらも待たずに進められます。タスクの進め方
依存するタスクの進め方は、待つ・束ねる・スタックの3つに整理できます。それぞれの違いをブランチのツリーで見ていきます。待つ
タスクAのプルリクエストがマージされるまで、タスクBに着手できません。プルリクエストの粒度は保てますが、タスクBのブランチはタスクAのマージ後にしか切れず、レビュー待ちの間、後続の作業が止まります。束ねる
待たない代わりに、タスクAとタスクBを1つのブランチで実装し、1つのプルリクエストにまとめる方法です。作業は止まりませんが、異なる変更内容が混在した大きな差分になり、レビューの負荷が上がります。スタック
AのブランチからBのブランチを切り、BのプルリクエストのbaseをAのブランチへ向けます。どちらのプルリクエストも小さいまま、Aのレビューを待たずにBを進められます。仕組み
スタックはブランチの連鎖で、スタック内の個々のプルリクエストはレイヤーとも呼ばれます。最下層のブランチはデフォルトブランチから切り、その上の各ブランチは直下のブランチから切ります。各プルリクエストはbaseに直下のブランチを指定するため、差分にはその層の変更だけが表示されます。 レビューは層ごとに独立して進みます。2つ目のプルリクエスト(図のPR #2)のレビュアーに見えるのはサービス層の差分だけで、その下のモデルの変更は1つ目のプルリクエスト(図のPR #1)に属します。 マージは最下層から順に行います。最下層のプルリクエストがマージされると、GitHubが残りのブランチを自動的にリベースし、次のプルリクエストのbaseをデフォルトブランチに付け替えて、スタックが1層縮みます。最上位のプルリクエストをマージしてスタック全体を一括でマージすることも、下側の一部だけをマージして残りの作業を続けることもできます。プラットフォーム側の挙動の詳細はGitHub Docsを参照してください。 この仕組みによって、スタックには次のメリットがあります。粒度を保てる
依存する変更どうしを1つの大きな差分に束ねず、それぞれを変更内容が一意なプルリクエストのまま保てます。
レビュー待ちでブロックされない
下の層のレビューが進行中でも、そのブランチの上で次の層の作業を続けられます。
レビューが小さいまま
各プルリクエストにはその層の差分だけが表示されるため、レビューがレビュアーの頭に収まる大きさに保たれます。
依存関係が明示される
base連鎖がどの変更がどの変更の上に成り立つかを記録するため、マージ順がプルリクエスト自体から読み取れます。
gh stack拡張でローカルに実行できます。この自動化に加えて、一連のプルリクエストがスタックとして紐付いていることをGitHub自身が理解する点も、GitHubでスタックを運用するメリットです。
スタックは、実際に依存し合う変更のためのものです。独立したタスクにスタックは不要で、デフォルトブランチに向けた別々のプルリクエストとして開きます。
gh stackの使い方
gh stackは、スタックのローカルワークフローを1つの単位として扱うGitHub CLIの拡張です。依存順でのブランチの作成と追跡、リベースの維持、push、base連鎖を設定したプルリクエストの作成、層の間の移動をまとめて担います。
スタックのGitHubサーバー側の対応はpublic previewです。全リポジトリへ段階的にロールアウト中のため、
gh stack submitがリポジトリで失敗する場合は、ロールアウトが未到達の可能性があります。1
拡張をインストールする
2
ブランチの連鎖を作る
各層を専用のブランチで実装します。新しいブランチは直前のブランチから切り、コミットします。できあがった既存ブランチを、下から上の順に並べてスタックとして採用します。新しく作業を始める場合は、最初のブランチで
gh stack initを実行してから、gh stack addで新しい層を上に積みながら実装することもできます。3
プルリクエストを一括作成する
4
最下層からマージする
最下層のプルリクエストから順にマージします。上のプルリクエストはGitHub側で自動的にリベースされます。最上位のプルリクエストをマージすると、スタック全体を一括でマージできます。マージ後にローカルブランチを追従させる(fetch・リベース・push・スタック状態の同期をまとめて行う)には、次を実行します。
AIエージェントによるスタックの自動化
スタックは、AIエージェントのワークフローと相性が良い手法です。作業を依存関係付きのIssueに分解しておけば、エージェントが依存順にIssueを実装してIssueごとにブランチを積み、最後にプルリクエストを一括作成できます。人は、小さな層ごとのレビューとマージに集中します。 自動化されたワークフローは、タスク分解の成果物である親Issueを入力として、次の流れで動きます。1
親Issueから依存関係を読み取る
親IssueのSub-issuesと、Issue間の依存関係を取得します。タスク分解で作った親子Issueと依存関係の設定が、そのまま自動化の入力になります。
2
依存グラフをスタックに変換する
依存関係のグラフを、先に完了すべきIssueが下になる順に並べ、依存を直接・間接にたどってつながるIssueのまとまりを1つのスタックにまとめます。依存のつながりがないまとまりどうしは、複数のエージェントで並列に進められます。たとえば、次の依存関係を持つSub-issuesがあるとします。矢印は依存先のIssue(先に完了すべきIssue)を指します。このグラフは、次の2つのスタックに変換されます。Issue AからDは、依存をたどるとひとつながりになるため1つのスタックになります。Issue BとCの間に直接の依存はありませんが、AとDを介してつながっているため、同じスタック内で直列に積まれます。Issue EとFのまとまりはAからDと依存のつながりがないため、複数のエージェントで並列に実装できます。
3
エージェントがブランチを積みながら実装する
エージェントはスタック内のIssueを、依存の順序を守って下から実装します。Issueごとにブランチを切ってコミットを積み、Issueとブランチが1対1で対応した連鎖を作ります。先ほどの例のIssue BとCのように互いに依存しないIssueは、同時並列で実装してから連鎖に組み込めます。
4
プルリクエストを一括作成する
連鎖ができあがったら、
gh stack submit --autoでスタック全体をpushし、base連鎖を設定したプルリクエストを一括作成します。5
人がレビューして最下層からマージする
人の作業はここからです。層ごとの小さな差分をレビューし、最下層からマージします。下のプルリクエストがマージされると上のプルリクエストはGitHub側で自動的にリベースされるため、マージのたびに手作業でスタックを組み直す必要はありません。スタック内のすべてのプルリクエストがapproveされていれば、最上位のプルリクエストをマージしてスタック全体を一括でマージすることもできます。
関連ページ
コードレビュー
レビュー依頼が届いたときにレビュアーが何を確認するか。セルフレビューと明確なコメントがやり取りを速く保つ理由。
テストコード
そもそも変更を検証可能にするもの。テストがプルリクエストをレビューしやすく保つ仕組み。