こんにちは!ブロックチェーンチームでエンジニアをしている id:dorapon2000 です。最近買ってよかったものは「潮の華 あおさといわしふりかけ」です。
今回は Git の Squash マージについての知見を共有したいと思います。端的に言うと、 チーム開発で Non Fast-Forward マージをやめて Squash マージを採用し、再び Non Fast-Forward マージに戻した経緯の説明です。Squash マージを運用に導入するか考えたことがある方の参考になればと思います。
Squash マージとは
マージには 3 種類ありますね。みなさんはトピックブランチを main へマージする際にどのマージ方法を利用していますか?
- Fast-Forward マージ
- git merge --ff-only
- Non Fast-Forward マージ
- git merge --no-ff
- Squash マージ
- git merge --squash
GitHub 上のマージボタンではそれぞれ
- Rebase and merge
- Create a merge commit
- Squash and merge
に対応します。今回注目する Squash マージは、複数コミットを単一のコミットにまとめてしまうマージ方法です。これらを説明するわかりやすい記事は多くあるため、そちらに説明を譲ります。
なぜ Squash マージをやってみたのか
それぞれのマージ方法にはメリット・デメリットがあります。私達が利用していた Non Fast-Forward マージと Squash マージであげると以下のとおりです。
- Non Fast-Forward マージ
- メリット
- マージコミットができるので、緊急時に Revert しやすい
- すべてのコミットログが残るため、コードの意図の調査をしやすい
- デメリット
- マージコミットが大量に発生する
- GitHub Flow において、main を feature へマージするときと feature を main へマージするときにマージコミットが発生する
- WIP なコミットや add 忘れなどの雑なコミットまで残る
- マージコミットが大量に発生する
- メリット
- Squash マージ
- メリット
- マージコミットができるので、緊急時に Revert しやすい
- プルリク内のコミットは単一のコミットにまとめられてスッキリする
- デメリット
- 詳細なコミット履歴が失われる
- メリット
私達のチームで利用していた Non Fast-Forward マージでも開発において不都合はありませんでした。しかし、マージコミットが大量に発生する点が気になっていました。実際に Non Fast-Forward マージで運用している現在の main ブランチの様子です。マージコミットだらけです。
メリットの中で最も重要な点は「緊急時に Revert しやすい」です。それは Squash マージにもあります。そして Non Fast-Forward マージのデメリットが目についたとき、Squash マージが魅力的に映りました。
それから私達のチームは Squash マージを採用しました。具体的には、main → feature は従来どおり Non Fast-Forward マージで、feature → main へのマージが Squash マージです。
やってみてつらかったこと
Squash マージは 3 ヶ月間運用しました。しかし、運用の中で Non Fast-Forward マージのときには想像していなかったコンフリクトの問題が大量に現れました。
- main から feature/α ブランチ (子) を切る
- feature/α にコミットハッシュ A を push
- feature/α から feature/β ブランチ (孫) を切る
- feature/β にコミットハッシュ B を push (画像 1 枚目)
- A と B はコンフリクトの関係にあるとする (補足参照)
- feature/α を main に Squash マージ (画像 2 枚目)
- ここで Squash されるので main にはコミットハッシュ A が入らない (コミットハッシュ C として追加)
- feature/β のベースブランチは main に切り替わる
- (最新にするため) main を feature/β へ Non Fast-Forward マージする (画像 3 枚目)
- main 中にコミットハッシュ A がないことで、 feature/β のコミット A+B と main の C の解決がうまくできずコンフリクトする
Squash マージのデメリットである「詳細なコミット履歴が失われる」がコンフリクトという形で問題になりました。この状況によるコンフリクトを Squash コンフリクトと命名して説明を続けます。
やめた
コンフリクトが辛かったため、チームで相談して対応を 4 つ考えました。
- ① 気合で Squash コンフリクトを解消する
- ② Squash コンフリクトが発生する場合のみ、Non Fast-Forward でマージする
- ③ Squash コンフリクトが発生しないように、派生する feature ブランチがすべてマージされていることを確認してから Squash マージする
- ④ Squash マージをやめる
本記事のタイトルにもある通り、採用したのは ④ の Squash マージをやめることです。つまり、従来の Non Fast-Forward マージの運用に戻しました。
①〜③ を採用しなかった理由をそれぞれ説明します。
① は現実的でないと判断されました。もしロックファイル(pnpm-lock.yml など)でコンフリクトが起きてしまったときにあまりに悲惨です。
② は運用でカバーする方法です。しかし、本来であれば認識する必要のない自身の子ブランチに合わせて 2 種類のマージを使い分ける必要があります。また、運用していると Non Fast-Forward マージすべきところをうっかり Squash マージしてしまったというミスが起きてしまいそうです。議論の余地なく不採用でした。
③ も運用でカバーする方法です。Squash コンフリクトが起きる状況を作らないように、feature/A を feature/B より先にマージしなければいいのです。しかし、feature/A に依存する子ブランチが多いと、あるいは他の子ブランチがマージされるのを待っているうちに新しい子ブランチができるなど、いつまで経っても feature/A を main にマージできません。そして、十分にその状況がありえます。
④ はもともと運用していた方法であり懸念はありません。
考察
当時のチームは新規プロダクトの開発初期段階にあり、 feature/B (孫ブランチ) のようなブランチがよく作成されていました。そのため Squash マージとの相性が良くなかったのだと考えられます。feature/B があまり発生しない状況や環境であれば Squash マージもよい選択肢になるかと思います。
まとめ
記事を読んでいただきありがとうございます。最後にまとめます。
- Non Fast-Forward マージ戦略にはマージコミットが大量に発生するという問題がある
- 問題の解決のために Squash マージで運用した
- 特定の状況でコンフリクトが頻繁に発生した
- 結局 Non Fast-Forward マージ戦略に戻した
(補足) コンフリクトの関係
Squash コンフリクトの例中で以下のように説明した箇所があります。
A と B はコンフリクトの関係にある
適切な用語を思いつかなかったためこのような説明になっていますが、内容はシンプルです。Squash コンフリクトの例を引き継いで、コードで説明します。
コミット A
import bisect
+ import collections
コミット B
import bisect
import collections
+ import math
Squash マージコミット C は
import bisect
+ import collections
のようになり、コミット A+B とコミット C が Squash コンフリクトを起こす、ということです。