Git入門:バージョン管理のきほん
ブランチ戦略(GitHub Flow)
このレッスンで分かること
- GitHub Flow の 5 つのステップと、その背後にある「main は常にデプロイ可能」という原則
- ブランチ名の付け方と、マージ後に削除するまでの日々の手順
- git-flow との違いと、なぜ今 GitHub Flow が選ばれやすいのか
ブランチ戦略はチームの交通ルール
ブランチの作り方も合流のさせ方も、ここまでのレッスンで揃いました。ただし技術が分かっていても、チーム全員がバラバラの流儀で使えば履歴は荒れます。誰がどのブランチをいつ作り、いつ消すか。この取り決めがブランチ戦略です。
戦略は好みではなく、リリースの仕方で決まります。1 日に何度でもデプロイできる Web サービスと、半年に一度だけ出荷して古いバージョンも保守するパッケージソフトでは、必要な型が違います。ここでは前者の代表である GitHub Flow を扱います。覚えることが少なく、学習中の個人開発から実務まで同じ形で使えます。
GitHub Flow の 5 ステップ
GitHub Flow はこれだけです。
mainから作業ブランチを切る- そのブランチでコミットする
- プルリクエストを出す
- レビューを受けて直す
mainにマージして、ブランチを削除する
長く生きるブランチは main ただ 1 本です。それ以外はすべて数日で生まれて消える使い捨てになります。
図にするとこの形です。
プレーンテキスト
A---B feature/login-form
/ \
o---o---o-----------M---o mainmain という 1 本の幹があり、そこから短い枝が生えては戻る。それを延々と繰り返すだけです。枝が幹から離れている時間が短いほど、この形はうまく回ります。
| ブランチ | 寿命 | 役割 |
|---|---|---|
main | 永続 | 常にデプロイ可能な状態を保つ唯一の本流 |
| 作業ブランチ | 数時間から数日 | 1 つの機能や修正。マージしたら削除する |
main は常にデプロイ可能に保つ
GitHub Flow を支えている原則は 1 つです。main の最新は、いつ本番へ出しても壊れない状態でなければならない。
この約束があると、いくつもの判断が自動的に決まります。
mainに直接コミットしない。壊れた状態が一瞬でも本流に乗る余地をなくす- 動かないコードをマージしない。テストが通ってからマージする
- 緊急の修正も
mainから切って、同じ流れでマージする。特別扱いの近道を作らない
逆に言えば、main が信用できないチームは、リリースのたびに「今の main って出して大丈夫でしたっけ」という確認が発生します。この確認が消えることが、この原則のいちばんの見返りです。
GitHub には main への直接 push を禁止する ブランチ保護 の設定があります。リポジトリの Settings から Branches を開き、main に対して「Require a pull request before merging」を有効にすると、プルリクエスト経由でしかマージできなくなります。習慣に頼らず仕組みで守れるので、チームで開発するなら最初に入れてください。
保護をかけた main に直接 push しようとすると、こう拒否されます。
ターミナル
$ git push
remote: error: GH006: Protected branch update failed for refs/heads/main.
remote: error: Changes must be made through a pull request.このとき、コミット自体はローカルに残っています。作業ブランチを切ってそこへ移し、プルリクエストを出し直せば無駄にはなりません。
ターミナル
$ git switch -c fix/typo-in-readme
$ git push -u origin fix/typo-in-readmemain に立ったまま切ったので、直前のコミットはそのまま新しいブランチに乗ります。あとは main を元の位置に戻せば片付きます。
もう 1 つ入れておきたいのが「Require status checks to pass before merging」です。テストが通ったプルリクエストしかマージできなくなり、main が壊れる経路が実質的に塞がります。テストの自動実行は GitHub Actions で自動化のさわり で扱います。
学習中の個人リポジトリでも、
mainに直接コミットする癖はつけないでください。1 人だと横着したくなりますが、チームに入ってから直すほうがずっと大変です。
作業ブランチを切る
必ず最新の main から切ります。古い main から切ると、その時点で他人の変更ぶんの遅れを背負います。
ターミナル
$ git switch main
Switched to branch 'main'
$ git pull
Updating 4c2b70e..7e2d804
Fast-forward
README.md | 5 +++++
1 file changed, 5 insertions(+)
$ git switch -c feature/login-form
Switched to a new branch 'feature/login-form'名前は他人が読んで何をするブランチか分かることが条件です。種類を頭に付ける形がよく使われます。
プレーンテキスト
feature/login-form
fix/header-overflow
docs/setup-guide
chore/update-depswip、test、tmp、myname のような名前は避けてください。1 週間後の自分も含めて誰も中身を思い出せません。チケット番号を運用しているチームなら feature/1234-login-form のように番号を混ぜます。ブランチ名はプルリクエストの一覧にそのまま並ぶので、チーム全員が毎日読む文字列だと思って付けてください。
大文字と小文字だけが違う名前は、Windows と Mac で扱いが食い違って事故のもとになります。全部小文字とハイフンに統一しておくのが無難です。
ブランチは小さく保ちます。目安は 1 ブランチ 1 目的、1 日から 3 日で終わる大きさです。長生きするほど main との差が開き、マージのときのコンフリクトが大きくなります。終わりが見えないときは、機能を分割して動く単位ごとにマージしてください。
プルリクエストとマージ
作業が終わったら push して、プルリクエストを出します。origin はリモートリポジトリにつけられた既定の呼び名です。
ターミナル
$ git push -u origin feature/login-form-u は「このローカルブランチはリモートのこのブランチと対応する」という紐付けを保存するオプションで、初回だけ必要です。次からは git push だけで通ります。
プルリクエストは、コードを見せて意見をもらうための場所です。出し方と書き方は プルリクエストを出す、レビューのやり取りは コードレビューの受け方・出し方 で詳しく扱います。
レビュー中に main が進むこともあります。差が開いてきたら、作業ブランチ側で最新を取り込んでおきます。
ターミナル
$ git switch feature/login-form
$ git fetch origin
$ git rebase origin/main一直線の履歴を保ちたいなら rebase、履歴の書き換えを避けたいなら git merge origin/main を使います。すでに push してレビュー中のブランチを rebase すると相手の手元と食い違うので、レビュアーがいる場合は merge を選ぶほうが無難です。
承認されたら main にマージします。GitHub の画面上でマージするのが基本です。ボタンには 3 つの選択肢があります。
| マージ方法 | 履歴の見え方 |
|---|---|
| Create a merge commit | 合流点が残り、作業の単位が履歴に見える |
| Squash and merge | 全コミットが 1 つにまとまる。試行錯誤が消えて読みやすい |
| Rebase and merge | 一直線につながる。マージコミットは作られない |
どれを使うかはチームの決めごとです。迷ったら Squash and merge が扱いやすく、main の履歴が「1 機能 1 コミット」で並ぶので後から追いやすくなります。
3 つの詳しい違いは マージの仕組み(fast-forward と マージコミット) で扱った内容と同じです。GitHub のボタンは、裏側で git merge、git merge --squash、git rebase を実行しているだけだと思ってください。
なお、緊急の本番修正も同じ流れを通します。main から fix/ のブランチを切り、プルリクエストを出し、レビューを受けてマージする。急いでいるからこそ、確認の目を 1 つ通す価値があります。急ぎだから直接 push、を一度でも許すと、その例外は必ず常態化します。
マージしたらブランチを削除する
GitHub 上でマージすると「Delete branch」のボタンが出ます。押してください。ローカルにも残っているので、そちらも片付けます。
ターミナル
$ git switch main
Switched to branch 'main'
$ git pull
$ git branch -d feature/login-form
Deleted branch feature/login-form (was 9c4e71a).-d はマージ済みかを確認したうえで削除するので、消し忘れの検出も兼ねます。リモートで削除済みのブランチの参照がローカルに残っている場合は、まとめて掃除できます。
ターミナル
$ git fetch --prune消すことに抵抗を感じる必要はありません。マージ済みなら、そのコミットは main の履歴に残っています。ブランチ名は名札にすぎず、消しても中身は失われません。むしろ古いブランチが 30 本並んだ git branch は、どれが生きているのか分からず邪魔になります。
やりがちな失敗を 1 つ挙げます。作業が終わったブランチをそのまま置いて、次の作業をその上から切ってしまうパターンです。
ターミナル
$ git switch -c feature/logout-formfeature/login-form に立ったままこれを打つと、まだレビュー中の login の変更を丸ごと抱えた logout ブランチができます。プルリクエストには関係ない差分が混ざり、レビュアーが読めません。新しい作業は必ず main に戻ってから切る、と手順を固定してください。間違えて切ってしまった場合は、コミット前なら git switch -c をやり直すだけです。
git-flow との対比
もう 1 つ有名なのが git-flow です。main と develop の 2 本を常時運用し、feature、release、hotfix の 3 種類を使い分けます。リリースを固定バージョンで出荷し、古いバージョンも並行して保守する製品には合理的な形です。
ただしブランチが 5 種類あり、どこから切ってどこへ戻すかの規則も多く、運用は重くなります。Web サービスのように「マージしたらその日のうちにデプロイする」開発では、develop と release はほとんど何も生みません。だから今は GitHub Flow のほうが選ばれます。
| 観点 | GitHub Flow | git-flow |
|---|---|---|
| 常設ブランチ | main のみ | main と develop |
| ブランチの種類 | 作業ブランチのみ | feature / release / hotfix |
| 向いている開発 | いつでもデプロイする Web サービス | バージョンを固定して出荷する製品 |
| 学習コスト | 低い | 高い |
中間の形もあります。GitHub Flow に「リリース用のタグを打つ」だけを足す運用です。マージのたびにデプロイはせず、区切りのついた時点でタグを打って出荷する。ブランチは増やさずリリースの目印だけ残せるので、develop を用意するより軽く済みます。タグの打ち方は タグでリリースに印をつける で扱います。
まずは GitHub Flow を身体に入れてください。それで回らない事情が出てきたときに、初めて重い型を検討すれば十分です。戦略は増やすより減らすほうが難しいので、最小の形から始めるのが結局いちばん近道になります。
- GitHub Flow は「
mainから切る、コミット、プルリクエスト、レビュー、マージして削除」の 5 ステップ - 支えている原則は
mainを常にデプロイ可能に保つこと。直接 push はブランチ保護で禁じる - 作業ブランチは 1 目的で小さく、数日で終わらせる。マージしたら必ず削除する
- git-flow は複数バージョンを保守する製品向け。運用が重いので今は選ばれにくい