戦略を選ぶ問いは一つだ
一言でいうと
ブランチ戦略を選ぶときの問いは、「どの図がきれいか」ではなく、「障害が起きたとき、このリポジトリで原因のコミットをどう見つける計画か」です。
なぜ必要なのか
Git Flow、GitHub Flow、Trunk-Basedは、それぞれ異なるデプロイ周期を前提に作られています。前提を無視して名前だけを持ってくると、毎日摩擦が生じます。
| 戦略 | ブランチの種類 | マージの頻度 | 向いている場面 |
|---|---|---|---|
| Git Flow | 5種類以上 | 週1–2回 | 定期リリース、モバイルアプリ、パッケージ |
| GitHub Flow | 2種類 | 日1–3回 | CDを実践するSaaS |
| Trunk-Based | 1–2種類 | 日5–10回以上 | ブランチの寿命が1日以内、Feature Flagが必須 |
Trunk-BasedはCIへの要求水準が非常に高いです。Feature Flagなしで始めると、未完成のコードがそのままデプロイされます。逆に、GitOps環境では、Git Flowのreleaseブランチが自動同期のパターンと衝突しやすくなります。そのため、GitOpsを使うなら、GitHub FlowまたはTrunk-Basedを勧めます。
共通のルールが1つあり、どこでも通用します。3日以上生きているfeatureブランチは、マージコンフリクトの温床です。
どう動くのか
マージ方式は3つあり、それぞれ残るものが違います。
--no-ffマージ: 親が2つあるコミットができ、「このまとまりがいつ統合されたか」が履歴に残ります。- 早送り(fast-forward): マージコミットなしで、ブランチポインターだけが前に進みます。履歴は一直線になりますが、統合した時点は残りません。
- スカッシュマージ: コミットを1つに押しつぶします。すっきり見えますが、3つのものを持っていきます。二分探索の解像度がPRの大きさに固定され、チェリーピックの精度を失い、Gitから見ると、そのブランチはマージされたことがありません(新しいコミットの親が、元のブランチではないため)。そのため、スカッシュマージを使うチームは、マージ直後にソースブランチを削除するというルールも一緒に持つ必要があります。
チェリーピックは、保存されたdiffを切り貼りするものではありません。Gitはそもそもdiffを保存せず、スナップショットだけを保存します。そのため、対象コミットの親のツリーを共通祖先として、3-wayマージを行います。チェリーピックが衝突する理由が、ここにあります。
現場での姿
ホットフィックスをリリースブランチで修正して、mainに反映せず、次のリリースで同じバグが復活することは、どのチームでも一度は起きます。リリースブランチを運用するなら、「ホットフィックスは必ずmainにも取り込む」が、ポリシーの文として書かれている必要があります。
タグも同じです。軽量タグは単なるポインターなので、誰がいつなぜ付けたのかが残りません。リリースタグには-aで注釈を付けておくと、あとで調査に使えます。
履歴は調査のために残す
ブランチ戦略の価値は、平常時ではなく、事故が起きたときに表れます。「昨日まで動いていたのに、今日は動かない」という状況で、原因のコミットを探す標準的なツールは二分探索です。Gitはgit bisectで、これを半自動で行ってくれます。正常なコミットと壊れたコミットを教えると、中間地点をチェックアウトしてくれ、人は良い/悪いだけを答えれば、範囲が半分ずつ狭まります。コミット1,000個の中から原因を見つけるのに、10回で十分です。
二分探索がうまくいくには、履歴が2つの条件を満たす必要があります。コミット1つ1つがビルドできて動作する必要があり、コミットのサイズが小さい必要があります。途中に動作しないコミットが混じっていると、人が良い/悪いを答えられず、その地点を飛ばすことになりますし、スカッシュマージでPR1つがコミット1つになっていると、探索がそのPRの手前で止まります。「このPRのどこか」までしかわからず、残りは手で読む必要があるという意味です。前に、スカッシュマージが二分探索の解像度をPRのサイズに固定すると述べましたが、それはこの部分のことです。
原因のコミットを見つけたあとの選択も、あらかじめ決めておく必要があります。元に戻す方法は2つあり、性質がまったく違います。
git revertは、その変更を取り消す新しいコミットを作ります。履歴が保存され、すでに共有されているブランチでも安全です。本番ブランチでは、こちらだけを使います。git resetは、ブランチポインターを後ろへ動かします。共有ブランチで行うと、他の人のリポジトリと履歴が分岐し、その後は全員が強制プッシュとコンフリクトに悩まされます。
マージコミットを元に戻すときは、もう1つ必要なことがあります。親が2つあるため、どちらの系統を維持するかを-m 1のように指定する必要があり、通常、1番目の親がマージを受けた側(main)です。そして、マージを元に戻したあと、同じブランチをもう一度マージしても、何の変更も入りません。Gitから見ると、すでにマージされたコミットだからです。元に戻したものを再び入れるには、取り消しのコミットをもう一度元に戻す必要があるという事実を、急いでいる状況で初めて学ばないほうがよいです。
次のラボですること
リポジトリを最初から作り、機能ブランチを--no-ffでマージして統合の記録を残し、小さな修正は早送りで取り込み、リリースタグを付け、リリースブランチのホットフィックスをチェリーピックでmainに反映したあと、マージ済みのブランチだけを選んで整理します。最後に、このリポジトリの実際の数字を数えて、戦略の比較表を書きます。