スカッシュマージのデメリットと解決策|Issue単位運用の実践ガイド

2025.02.15開発技術https://img.morisakiblog.com/posts/squash-merge/Gemini_Generated_Image_41yiuy41yiuy41yi.jpg
スカッシュマージのデメリットと解決策|Issue単位運用の実践ガイド

Git を使って開発を進める際、コミット履歴が煩雑になってしまうことはよくあります。そんなときに便利なのが スカッシュマージ(Squash Merge) です。本記事では、スカッシュマージのメリット・デメリットと、デメリットを最小限に抑える実践的な使い方を解説します。

結論:スカッシュマージには、git bisect・revert・cherry-pick の粒度が荒くなるなどのデメリットがあります。ただし、そのほとんどは「PRが大きすぎる」ときに起きる問題です。Issue単位の小さなPRでスカッシュマージする運用にすれば、デメリットの大半は気にならなくなります。実際にこのブログのリポジトリも、スカッシュマージだけで運用しています。

スカッシュマージとは?

スカッシュマージとは、ブランチをマージする際に 複数のコミットを1つにまとめて 取り込む方法です。通常のマージでは個々のコミットがそのまま残りますが、スカッシュマージを使うと履歴がスッキリします。

スカッシュマージのメリット

スカッシュマージは単に履歴を整理するだけでなく、開発の効率性と保守性を大幅に向上させる手法です。特にチーム開発において、その真価を発揮します。

コミット履歴が整理される

不要な「修正」や「調整」コミットがなくなり、履歴が見やすくなります。開発者が本質的な変更に集中でき、履歴を追跡する際の認知負荷が軽減されます。

レビューがしやすい

大量の細かいコミットを追う必要がなく、変更内容を簡単に把握できます。レビュアーは機能やバグ修正の全体像を一度に理解でき、より効率的で質の高いコードレビューが可能になります。

バグ追跡・原因特定が容易

Issue単位でコミットがまとめられているため、バグが発生した際に「どの機能追加・修正が原因か」を素早く特定できます。

具体例:

  • 「ログイン機能のバグ修正 (#123)」というコミットがあれば、そのコミット前後での動作比較が簡単
  • 細かいコミット(「typo修正」「スペース調整」など)に惑わされることなく、本質的な変更箇所を追跡可能
  • 問題のある変更をリバート(取り消し)する際も、Issue単位でまとまっているため、関連する変更を漏れなく元に戻せる

この利点は、特に本番環境でのトラブルシューティング時や、複雑なバグの原因究明において非常に価値があります。

チーム開発がスムーズに

きれいな履歴は、他の開発者が理解しやすく、共同作業がしやすくなります。新しいチームメンバーが参加した際も、プロジェクトの変更履歴を迅速に把握できるため、オンボーディングが効率化されます。

スカッシュマージのデメリット

スカッシュマージはコミット履歴を整理し、開発フローを効率化する便利な手法ですが、当然トレードオフも存在します。ここでは一般的に指摘されるデメリットを正直にまとめた上で、後述するIssue単位の運用でどこまで軽減できるかも解説します。

git bisect による原因特定が難しくなる

git bisect は、バグが混入したコミットを二分探索で特定する強力なデバッグツールです。しかしスカッシュマージで複数のコミットを1つにまとめてしまうと、bisectの探索粒度が荒くなり、「この巨大コミットのどこかにバグがある」としかわからない状況になります。

通常のマージであれば細かいコミット単位で原因を絞り込めるため、デバッグ効率に差が出る場面があります。

revert の粒度が荒くなる

スカッシュマージされたコミットをリバートすると、PR全体の変更が一括で取り消されます。「この修正だけ戻したいが、他の変更は残したい」というケースでは、手動で該当箇所だけ切り出す作業が必要になります。

通常のマージであれば個々のコミット単位でリバートできるため、より柔軟な対応が可能です。

cherry-pick がしづらくなる

特定の変更だけを別のブランチに持っていきたい場面(ホットフィックスやバックポートなど)で、スカッシュマージされた巨大コミットの中から一部だけを cherry-pick することは困難です。結果として、手動でのパッチ適用が必要になることがあります。

PRが大きいと意味不明な巨大コミットが生まれる

スカッシュマージは「PRの変更を1コミットにまとめる」手法のため、PRそのものが大きいと、数百行〜数千行の変更が1コミットに圧縮されます。こうなると履歴を整理するどころか、「色々やった」としかわからない爆弾コミットが生まれ、本末転倒です。

これはスカッシュマージの問題というよりPRの粒度の問題ですが、スカッシュマージを採用するならPRを小さく保つ運用とセットで考える必要があります。

コミット履歴の消失による作業の形跡の喪失

スカッシュマージを行うと、ブランチ内の個々のコミット履歴が1つのコミットにまとめられ、詳細な履歴が失われます。これにより、以下のような影響が考えられます:

  • 作業の形跡が消える: 開発中にどのような変更が段階的に行われたか(例: 「初期実装」「バグ修正」「コード最適化」など)がわからなくなります。後から特定の変更を追跡したい場合に、詳細なコンテキストが失われるため、デバッグや問題の原因究明が難しくなる可能性があります。
  • 複数人での作業における影響: チームでブランチを共有し、複数人でコミットしていた場合、個々の貢献者のコミットが統合されてしまい、誰がどの変更を担当したかが不明瞭になります。これにより、協力者の貢献が見えづらくなることがあります。

コミット内容を評価指標にしている場合の影響

一部の組織では、コミット数やコミット内容を個人の評価指標として利用する場合があります。スカッシュマージを採用すると、個々のコミットが消滅するため、評価の難しさが生じます。

ただしこれは稀なケースです。ほとんどの組織では、Issueの完了内容やプルリクエストの質、プロジェクト全体への貢献度をもとに評価を行っており、コミット数に依存した評価は一般的ではありません。またGitHubではマージ後のPRページからすべてのコミット・レビューコメント・会話履歴を永久に閲覧可能なため、貢献度の確認に支障はほとんどありません。

Issue単位の運用でデメリットの多くは軽減できる

ここまで挙げたデメリットの多くは、PRの粒度が大きすぎる場合に深刻化します。逆に言えば、Issue単位で小さなPRを作成し、スカッシュマージを行う運用であれば、これらの問題は大幅に軽減されます。

  • git bisect: Issue単位の小さなコミットなら、原因の特定範囲は十分に絞れる
  • revert: Issue単位でまとまっているため、機能ごとの取り消しが容易
  • cherry-pick: 1コミット=1機能/1修正なので、そのまま別ブランチに持っていける
  • 巨大コミット: そもそもPRが小さければ発生しない

このIssue単位でのスカッシュマージ運用は、後半の「スカッシュマージの活用方法」で具体的に解説します。

よくあるスカッシュマージの誤解

よくある誤解 「スカッシュマージを使うと、子ブランチをマージする際にコンフリクトが発生しやすくなる」「コミットハッシュが変わることでGitが混乱してコンフリクトが起きる」

実際のところ スカッシュマージ自体がコンフリクトを引き起こすわけではありません。コンフリクトが発生するのは、同じファイルの同じ箇所に競合する変更がある場合のみです。

コンフリクトが発生する本当の原因

  • 複数のブランチが同じファイルの同じ行を変更している
  • mainに他のPRがマージされていて、その変更と競合している
  • つまり、通常のGitマージで起こる一般的なコンフリクト

コンフリクトが発生しない場合

  • スカッシュマージされた変更と、子ブランチの変更に実質的な差がない
  • 他に競合する変更がmainに含まれていない
  • この場合、マージ方法に関係なくコンフリクトは発生しません

よくある誤解の具体例

親ブランチ(main)から子ブランチ(feature/A)、さらに孫ブランチ(feature/B)を作成し、子ブランチを親ブランチ(main)にスカッシュマージすると、コミットハッシュが変わることでGitが混乱し、孫ブランチをマージする際にコンフリクトが発生する

なぜこの理解が間違っているか

  • コミットハッシュの変更はコンフリクトの直接的な原因ではありません
  • 実際にコンフリクトが起きるのは、ファイル内容に競合がある場合のみ
  • 例:親ブランチでファイルAを変更、その後mainに他の変更が加わってファイルAが再度変更された場合など
  • スカッシュマージの有無に関係なく、同じファイルへの競合する変更があればコンフリクトします

ただし、孫ブランチではコンフリクトが起きやすくなるのは事実

一方で、「スカッシュマージすると孫ブランチでコンフリクトが起きやすい」という話自体は、間違いではありません。原因はハッシュそのものではなく、Gitが「同じ変更」だと認識できなくなることです。

孫ブランチ(feature/B)には、子ブランチ(feature/A)の元のコミットが含まれています。一方 main には、それを1つにまとめた別のコミットが入っています。中身は同じでも、Gitから見ると別々の変更です。そのため、次のような場合にコンフリクトが起きます。

  • feature/B で、feature/A が変更した行をさらに変更している
  • feature/B を main に git rebase し、feature/A の元のコミットが1つずつ再適用される

孫ブランチを main に追従させるときは、feature/A の分を飛ばして、feature/B 自身のコミットだけを載せ替えるのが確実です。

# feature/A の(スカッシュ前の)最後のコミットより後ろだけを main に載せ替える
$ git rebase --onto main <feature/A の最後のコミット> feature/B

とはいえ、これもブランチを何段も重ねるから起きる問題です。Issue単位でPRを小さく保ち、孫ブランチを作る前に子ブランチをマージしてしまえば、そもそも起きません。

スカッシュマージの活用方法

ここでは代表的なブランチ管理方法における、スカッシュマージの活用方法について説明します。

Issue 単位で PR を作成してマージ

Issue 単位でプルリクエスト (PR) を作成し、スカッシュマージを行うことで、Issue とコミットの関連性を明確にすることができます。これにより、履歴を追いやすくなり、リリースの内容も整理されて分かりやすくなります。

重要なポイント:

  • 開発中のマージ: 機能ブランチ間での作業中は通常のマージを使用し、詳細な履歴を保持する
  • 最終マージ: mainブランチやdevelopブランチへの統合時のみスカッシュマージを使用する

そもそも、機能ブランチの細かいコミットは開発完了後は不要です。Issue 単位でコミットをまとめることで十分であり、開発履歴がシンプルになり、チーム全体の開発効率が向上します。

Git Flow でのスカッシュマージ

Git Flow では、機能ブランチ (feature branch) から develop ブランチへ統合する際にスカッシュマージを使用すると、個々の機能開発中の細かいコミットを整理し、開発履歴をシンプルにできます。

開発中のブランチ間でのマージは通常のマージを使用し、developブランチへの最終的な統合でのみスカッシュマージを活用することで、作業履歴を保持しながら最終的な履歴を整理できます。

GitHub Flow でのスカッシュマージ

GitHub Flow では、短期間での開発を前提としており、プルリクエスト (Pull Request) をメインブランチにマージする際にスカッシュマージを利用すると、履歴を一つのまとまりとして管理しやすくなります。特に、頻繁なブランチ作成とマージを行う場合に効果的です。

GitHub Flowでは基本的にメインブランチへの直接マージとなるため、プルリクエスト作成時にスカッシュマージを選択することで、機能単位での履歴管理が実現できます。

スカッシュマージの使い方

GitHubのプルリクエストおよびコマンドラインを使ったスカッシュマージの方法を説明します。

GitHub の場合

GitHub のプルリクエストでスカッシュマージを行うのは簡単です。

  • プルリクエストを開く
  • 「Squash and Merge」ボタンをクリック
  • コミットメッセージを編集して「Confirm」

コマンドラインの場合

ローカル環境でスカッシュマージをするには、以下の手順を実行します。

# 対象ブランチにチェックアウト
$ git checkout main

# スカッシュマージ
$ git merge --squash feature-branch

# まとめた変更をコミット
$ git commit -m "まとめた変更のメッセージ"

# リモートにプッシュ
$ git push origin main

実例:このブログはスカッシュマージだけで運用している

このブログ(Hugo で構築)のリポジトリでは、GitHub の設定でスカッシュマージ以外のマージ方法を無効にしています。

設定項目(Settings → General → Pull Requests)値
Allow merge commitsオフ
Allow squash mergingオン(コミットメッセージは「Default to pull request title and description」)
Allow rebase mergingオフ
Automatically delete head branchesオン

マージ方法を1つに絞っておくと、「このPRはどのボタンでマージするんだっけ?」と迷うことがなくなります。コミットメッセージを PR のタイトルと本文にしておけば、main の履歴を見るだけで、何のための変更だったかがわかります。

その結果、main の履歴はこうなっています。

$ git log --oneline
34148d6 titleタグでseo_titleを使う(Hugo移行で無効になっていたSEOタイトルの復活) (#53)
9ca0fac SD入門: Stability Matrixでの導入手順を追加し表記を修正 (#52)
5014a1c CLAUDE.md: originをSSHに切り替えたのでbot手順を簡素化 (#51)
ff8f112 CLAUDE.md: botのpushでキーチェーンにトークンが残らない手順に変更 (#50)
ca779bd 非公開記事へのリンクを除去し、人気記事からも除外する (#49)

1行が1つのPRで、マージコミットは1つもありません。末尾の (#53) から PR を開けば、途中のコミットやレビューのやりとりはすべて残っています。

このリポジトリでは、記事の修正の多くを Claude Code(AI コーディングツール)に任せて、AI が作ったブランチを PR にしてマージしています。AI に作業させると、試行錯誤のコミットが増えることもありますが、スカッシュマージなら main には「何をしたか」が1行ずつ残るだけです。AI と一緒に開発する時代には、スカッシュマージとの相性はむしろ良くなっていると感じます。

まとめ

スカッシュマージを活用すれば、Git の履歴を整理し、開発の効率を向上させることができます。 Git Flow や GitHub Flow などのワークフローに応じて適切に活用し、チーム開発をスムーズに進めましょう。

特に Issue 単位で PR を作成し、細かいコミットを減らすことで、 履歴の可読性が向上し、リリースの内容も整理しやすくなります。 スカッシュマージを積極的に活用して、きれいな開発フローを実現しましょう!