はい、承知いたしました。Gitのコミットをまとめる方法に焦点を当て、特にrebaseのインタラクティブモードとsquashについて、約5000語の詳細な記事を記述します。
Gitコミットをまとめる方法:履歴を整理するRebase/Squash完全解説
はじめに:なぜGitコミットを整理する必要があるのか?
ソフトウェア開発において、バージョン管理システムGitは不可欠なツールです。Gitを使うことで、コードの変更履歴を記録し、追跡し、複数人での共同作業を効率的に行うことができます。日々の開発では、機能追加、バグ修正、リファクタリングなど、様々な目的でコミットを積み重ねていきます。
しかし、開発が進むにつれて、コミット履歴が雑然としてしまうことがあります。例えば、以下のような状況です。
- 試行錯誤のコミット: 機能実装中に何度も試行錯誤し、一時的なファイル変更やデバッグコードを含んだままコミットを繰り返してしまう。
- 軽微な修正コミット: 直前のコミットに単純なtypo修正や整形変更を加えただけのコミットが続く。
- 未完成な機能のコミット: まだ動作しない機能の途中で、とりあえずコミットしてしまう。
- 複数の目的が混在したコミット: 一つのコミットに、機能追加とバグ修正、リファクタリングなどがごちゃ混ぜになっている。
このような状態のコミット履歴は、いくつかの問題を引き起こします。
- 履歴の可読性が低い: 重要な変更とノイズ(試行錯誤や軽微な修正)が混在しているため、過去の変更内容を振り返ったり、特定の機能がいつどのように導入されたのかを追跡したりするのが困難になります。
- コードレビューの負担増加: プルリクエストなどで変更内容をレビューする際、無関係な修正や途中の状態を示すコミットが多く含まれていると、レビュワーは本質的な変更を見つけるのに苦労します。
- デバッグの効率低下: バグの原因を特定するために
git bisectなどのツールを使う場合、意味のないコミットや壊れた状態のコミットが含まれていると、原因特定の精度が落ちたり、多くのステップを踏む必要が生じたりします。 - マージコミットの増加: フィーチャーブランチをマージする際に、フィーチャーブランチ側のコミットが多すぎると、メインブランチの履歴が複雑になりがちです。
これらの問題を解決するために、Gitにはコミットを整理・統合する機能が備わっています。最も強力で柔軟な方法の一つが、インタラクティブなリベース(Interactive Rebase)を使用し、その中でコミットをまとめる(SquashまたはFixup)操作を行うことです。
この記事では、Gitのインタラクティブなリベースとは何か、そしてその中でコミットを「まとめる」ためのsquashやfixupといったコマンドをどのように使うのかを、具体的な手順とともに詳細に解説します。さらに、リベースにおける競合の解決方法、共有リポジトリでリベースを行う際の注意点、そして関連する他の方法についても触れ、Gitの履歴をきれいに保つための実践的な知識を提供します。
対象読者は、Gitの基本的なコマンド(add, commit, push, pull, branch, merge, log, statusなど)は理解しているものの、まだコミットの整理・結合に踏み込んだことがない、あるいは難しそうだと感じている方々です。
さあ、Gitの強力な履歴操作機能を学び、よりクリーンで読みやすいコミット履歴を手に入れましょう。
Gitの基本概念のおさらい
インタラクティブなリベースとコミットのまとめ方を学ぶ前に、関連するGitの基本概念を簡単におさらいしておきましょう。
コミット (Commit)
Gitにおけるコミットは、プロジェクトの特定時点でのスナップショットです。各コミットには、以下の情報が含まれます。
- 特定の状態のファイル群: そのコミット時点でのプロジェクト全体のファイル内容。
- コミットID (SHA-1ハッシュ): そのコミットを一意に識別する40文字のハッシュ値(通常、先頭7文字程度で表示される)。
- 親コミット: 直前のコミットを指すポインタ(最初のコミットを除く)。これにより、コミットは鎖のように繋がっていきます。
- コミットメッセージ: そのコミットで行われた変更の目的や内容を説明する文章。
- 作成者、コミット日時: 誰がいつそのコミットを作成したか。
コミットは不変なものであると考えるのが基本です。一度作成されたコミットの内容(コミットID含む)は、原則として変更されません。
ブランチ (Branch)
ブランチは、コミットの連なりを指す軽量なポインタです。新しいコミットを作成すると、現在チェックアウトしているブランチはその新しいコミットを指すように移動します。ブランチを使うことで、メインの開発ラインから分岐して新しい機能を開発したり、バグを修正したりすることができます。
HEAD
HEADは、現在作業しているコミットを指す特別なポインタです。通常、HEADは現在のブランチの先端を指しています。
リベース (Rebase) とは?
Gitのrebaseコマンドは、「あるブランチでの変更を、別のブランチの先端に適用し直す」ための機能です。これにより、コミットの履歴を直線的にすることができます。
例えば、mainブランチからfeatureブランチを分岐させ、featureブランチでいくつかコミットを作成したとします。その間にmainブランチにも新しいコミットが追加されました。この状態でfeatureブランチをmainブランチにマージすると、通常はマージコミットが作成され、履歴は分岐と合流を示すグラフになります。

ここでfeatureブランチでgit rebase mainを実行すると、Gitはfeatureブランチがmainから分岐した時点からのコミットを取り出し、それらを一時的にどこかに保存します。そして、featureブランチの先端をmainブランチの先端に移動させ、保存しておいたコミットをその新しい先端の上に一つずつ適用し直します。

結果として、featureブランチの履歴はmainブランチの先端からまっすぐ伸びる形になり、マージコミットは発生しません。ただし、リベースされたコミットは元のコミットとは別の新しいコミット(コミットIDが変わる)として再作成されます。
マージ (Merge) とは?
git mergeは、異なるブランチの変更を統合するためのコマンドです。
- Fast-forward Merge: マージ元ブランチがマージ先ブランチの先端にあるコミットから直接派生している場合、マージ先ブランチのポインタをマージ元の先端に移動させるだけで統合が完了します。この場合、新しいコミットは作成されません。
- Three-way Merge: 履歴が分岐している場合、両ブランチの最新コミットとそれらの共通の祖先コミットの3つを比較して変更を統合し、新しいマージコミットを作成します。
リベースとマージは、どちらもブランチ間の変更を統合する目的で使用されますが、履歴の形が異なります。マージは元の履歴を保持したまま統合するのに対し、リベースは履歴を書き換えて直線的にします。
なぜコミットをまとめたいのか?(ユースケース)
コミットをまとめる操作は、主にリベースの過程や、マージの一種として行われます。これは、以下のような状況で非常に有効です。
- フィーチャーブランチをメインブランチにマージする前: 開発中に積み重ねた試行錯誤や小さな修正のコミットを、意味のある数個の大きなコミットにまとめ、よりクリーンな状態でメインブランチに取り込みたい場合。これは、特にプルリクエストベースの開発フローで重要視されます。
- バグ修正のコミットを整理する: あるバグ修正に関連する一連のコミット(原因究明、修正、テストコード追加など)を、一つの分かりやすいコミットにまとめたい場合。
- 個人の作業ブランチをクリーンアップする: 他のメンバーと共有する前に、個人的な実験や調査で作成したノイズの多いコミット履歴を整理したい場合。
- 特定の機能や修正を後から取り出しやすくする: 将来的にそのコミットだけをcherry-pickしたり、リバートしたりする可能性を考慮して、関連する変更を一つのコミットに集約しておきたい場合。
これらのユースケースにおいて、rebaseのインタラクティブモードとsquash(またはfixup)は非常に強力なツールとなります。
コミットをまとめる主要な方法:Interactive Rebase (対話型リベース)
Gitでコミットをまとめるための最も一般的で強力な方法は、git rebaseコマンドに-iオプション(--interactive)を付けて実行する、対話型リベースです。
対話型リベースでは、指定したコミット範囲について、Gitが一時停止し、どのコミットをどのように扱うかをユーザーに問いかけます。ユーザーは、表示されるコミットリストを編集することで、コミットの順番を変更したり、特定のコミットを削除したり、そして今回の主題である複数のコミットを一つにまとめたり(squash/fixup)することができます。
対話型リベースの起動
対話型リベースは、以下の形式で実行します。
bash
git rebase -i <ベースとなるコミット>
<ベースとなるコミット>には、リベースの「基点」となるコミットを指定します。Gitは、この<ベースとなるコミット>の直後のコミットから現在のHEADまでのコミットを対象として、対話モードを開始します。
よく使われる<ベースとなるコミット>の指定方法は以下の通りです。
HEAD~N: 現在のHEADからN個前のコミットを基点にする。例えばHEAD~3なら、直近の3つのコミット(HEAD, HEAD~1, HEAD~2)が操作の対象となります。masterまたはmain: 現在のブランチがmasterまたはmainから分岐したと仮定し、master/mainの先端コミットを基点にする。これにより、現在のブランチがmaster/mainから分岐した後の全コミットを対象とできます。- 特定のコミットID:
abcdefgのようなコミットIDを指定する。
例: 直近5つのコミットを整理したい場合
bash
git rebase -i HEAD~5
このコマンドを実行すると、Gitはエディタを開き、以下のような内容を表示します。(エディタは、Gitの設定で指定されているものが使用されます。例: Vim, Nano, VS Codeなど)
“`
pick abcdefg commit message 1
pick hijklmn commit message 2
pick opqrstu commit message 3
pick vwxyz12 commit message 4
pick 3456789 commit message 5
Rebase 1234567..3456789 onto 1234567 (5 commands)
Commands:
p, pick = use commit
r, reword = use commit, but edit the commit message
e, edit = use commit, but stop for amending
s, squash = use commit, but meld into previous commit
f, fixup = like “squash”, but discard this commit’s log message
x, exec = run command (the rest of the line) using shell
d, drop = remove commit
l, label
t, tlabel
m, merge [-C | -c ]
. create a merge commit of the original tip
These lines can be re-ordered; they are executed from top to bottom.
If you remove a line here THAT COMMIT WILL BE LOST.
However, if you remove everything, the rebase will be aborted.
“`
エディタ画面の解説
この画面が、対話型リベースの操作インターフェースです。
- 上部のコミットリスト: リベースの対象となるコミットが、古いものから新しいものの順に表示されています。各行は
コマンド コミットID コミットメッセージの形式です。初期状態ではすべてのコミットにpickコマンドが割り当てられています。 - 下部のコメント行:
#で始まる行はコメントであり、Gitの処理には影響しません。ここには、利用可能なコマンドのリストと簡単な説明、および注意点が書かれています。
主要なコマンド
コミットリストの各行の先頭にあるコマンドを編集することで、Gitにそのコミットをどのように扱うかを指示します。主なコマンドは以下の通りです。
pick(またはp): そのコミットをそのまま使用します。順番を並べ替える際に使ったり、単にリベース対象に含める場合に使います。reword(またはr): そのコミットを使用しますが、リベース中にそのコミットのメッセージを編集する機会を与えます。edit(またはe): そのコミットを使用しますが、そのコミットが適用された直後にリベースの処理を一時停止します。これにより、そのコミットに対して追加の修正を行ったり、git commit --amendでコミット内容を変更したり、あるいはgit reset HEAD~などを使ってそのコミットを分割したり(後述)することが可能になります。処理を再開するにはgit rebase --continueを使います。squash(またはs): そのコミットを使用しますが、直前のコミットに統合します。つまり、このコミットの変更内容は、リスト上でこの行の直前にあるpickやreword、editなどのコマンドが付いたコミットに取り込まれます。統合後、Gitは新しい(統合された)コミットのメッセージを編集するためのエディタを開きます。fixup(またはf):squashと似ていますが、このコミット自身のコミットメッセージを破棄します。直前のコミットに統合され、直前のコミットのメッセージがそのまま使われます。コミットメッセージ編集の手間が省けるため、単純な修正や整形など、メッセージを残す必要がないコミットをまとめる際に便利です。drop(またはd): そのコミットを履歴から削除します。
これらのコマンドを組み合わせることで、柔軟な履歴操作が可能になります。特にsquashとfixupは、複数のコミットを一つにまとめるために使用します。
squash を使用したコミットまとめの詳細
squashコマンドは、「このコミットの変更を、リストの直前のコミットに結合する」という指示です。
例: 3つのコミットを1つにまとめる
仮に、以下のような直近3つのコミットがあるとします。
abcdefg commit message A (最初に着手した作業)
hijklmn commit message B (Aの続きだが、うまく動かず試行錯誤)
opqrstu commit message C (Bの間違いを修正し、ようやく完成)
これらの3つのコミットを、一つの「最初の作業を完成させた」というコミットにまとめたいとします。対象は直近3つなので、git rebase -i HEAD~3 を実行します。
エディタには以下のように表示されます。
“`
pick abcdefg commit message A
pick hijklmn commit message B
pick opqrstu commit message C
… (コメント行) …
“`
ここで、2番目と3番目のコミットを、1番目のコミットに統合します。統合したいコミット(BとC)のコマンドをsquashに変更します。
“`diff
– pick abcdefg commit message A
+ pick abcdefg commit message A
– pick hijklmn commit message B
+ squash hijklmn commit message B
– pick opqrstu commit message C
+ squash opqrstu commit message C
… (コメント行) …
“`
変更を保存してエディタを閉じると、Gitはリベース処理を開始します。
- まず、最初のコミット (
abcdefg) が適用されます (pick)。 - 次に、次のコミット (
hijklmn) が処理されますが、コマンドはsquashなので、これは直前のコミット (abcdefg) に統合されます。 - その次に、最後のコミット (
opqrstu) が処理されますが、これもコマンドはsquashなので、その時点での直前のコミット(つまり、abcdefgとhijklmnが統合されてできた新しいコミット)に統合されます。
Gitはsquashで統合されるすべてのコミットのメッセージを収集し、新しいコミットのメッセージを作成するためのエディタを開きます。
“`
This is a combination of 3 commits.
This is the 1st commit message:
commit message A
This is the commit message #2:
commit message B
This is the commit message #3:
commit message C
Please enter the commit message for your changes. Lines starting
with ‘#’ will be ignored, and an empty message aborts the commit.
… (他のコメント行) …
“`
この画面で、新しい統合されたコミットのメッセージを編集します。デフォルトでは、元のコミットメッセージがすべて含まれています。不要なメッセージを削除したり、全体を要約する新しいメッセージを書いたりします。
例えば、以下のように編集します。
“`
Implement feature X and fix related issues
This consolidates the initial implementation attempts and subsequent bug fixes
into a single, clean commit.
This is a combination of 3 commits.
This is the 1st commit message:
commit message A
This is the commit message #2:
commit message B
This is the commit message #3:
commit message C
Please enter the commit message for your changes. Lines starting
with ‘#’ will be ignored, and an empty message aborts the commit.
… (他のコメント行) …
“`
編集を保存してエディタを閉じると、Gitは新しいコミットを作成し、リベース処理を続行(この例ではこれで完了)します。git log --onelineなどで履歴を確認すると、元の3つのコミットが1つの新しいコミットに置き換わっていることがわかります。
fixup を使用したコミットまとめの詳細
fixupコマンドは、squashによく似ていますが、統合される側のコミットのメッセージを自動的に破棄する点が異なります。直前のコミットに統合される点は同じです。
例: 軽微な修正コミットを直前の機能コミットにまとめる
以下のようなコミット履歴があるとします。
abcdefg Add user authentication feature
hijklmn Fix typo in authentication error message
「Fix typo…」コミットは、直前の「Add user…」コミットに対する軽微な修正であり、独立したコミットとして残す必要はありません。これをfixupでまとめることを考えます。
対象は直近2つなので、git rebase -i HEAD~2 を実行します。
エディタには以下のように表示されます。
“`
pick abcdefg Add user authentication feature
pick hijklmn Fix typo in authentication error message
… (コメント行) …
“`
2番目のコミットを1番目のコミットに統合し、メッセージを破棄したいので、2番目のコマンドをfixupに変更します。
“`diff
– pick abcdefg Add user authentication feature
+ pick abcdefg Add user authentication feature
– pick hijklmn Fix typo in authentication error message
+ fixup hijklmn Fix typo in authentication error message
… (コメント行) …
“`
変更を保存してエディタを閉じると、Gitはリベース処理を開始します。
- 最初のコミット (
abcdefg) が適用されます (pick)。 - 次に、次のコミット (
hijklmn) が処理されますが、コマンドはfixupなので、直前のコミット (abcdefg) に統合され、hijklmnのメッセージは使われません。
この場合、Gitは新しいコミットメッセージを編集するためのエディタを開きません。統合後のコミットのメッセージは、元のabcdefgのメッセージ「Add user authentication feature」のままとなります。
git log --onelineなどで履歴を確認すると、元の2つのコミットが1つの新しいコミット「Add user authentication feature」に置き換わっていることがわかります。
squash と fixup の使い分け
squash: 統合後のコミットに、統合元の複数のコミットメッセージを組み合わせて、新しい、より包括的なメッセージを作成したい場合に使用します。複数の関連する変更をまとめて、その全体の目的を新しいメッセージで記述するのに適しています。fixup: 統合元のコミットが、直前のコミットに対する単純な修正や補足であり、そのコミット自身のメッセージは残す必要がない場合に使用します。直前のコミットのメッセージが新しい統合コミットのメッセージとして適切である場合に便利です。例えば、typo修正、整形、デバッグコードの削除など。
どちらを使うべきかは、まとめるコミットの内容と、統合後のコミットにどのようなメッセージを持たせたいかによって判断します。一般的に、試行錯誤の結果できた複数のコミットをまとめて、その成果を表現する新しいメッセージを書きたい場合はsquash、直前のコミットへの細かい手直しをまとめる場合はfixupを使うことが多いです。
その他の便利なコマンド
reword: コミットの内容は変えずに、メッセージだけを修正したい場合に便利です。pickをrewordに変えると、そのコミットが適用された後、メッセージ編集エディタが開きます。edit: 特定のコミットが適用された時点の状態で処理を一時停止したい場合に非常に強力です。例えば、- そのコミットの内容をさらに修正してから、
git commit --amendでコミット内容を更新する。 - そのコミットを
git reset HEAD^などで一度取り消し、変更内容をステージングに戻してから、内容を分割して複数の新しいコミットとしてコミットし直す。 - そのコミットが意図した通りに動作するかテストを実行する。
- 一時停止後、
git rebase --continueでリベースを続行します。git rebase --abortで中止も可能です。
- そのコミットの内容をさらに修正してから、
drop: そのコミットを完全に削除します。これは、試行錯誤したが結局不要になったコミットなどを履歴から抹消したい場合に有効です。
また、エディタでコミットリストの行の順番を並べ替えることも可能です。これにより、コミットを適用する順番を変更できます。ただし、異なる変更を加えているコミットの順番を大きく変えると、競合が発生する可能性が高くなります。
Interactive Rebase の実演(サンプルリポジトリ)
簡単な例で、squashとfixupを使った Interactive Rebase の流れを見てみましょう。
まず、作業用のディレクトリを作成し、Gitリポジトリを初期化します。
bash
mkdir rebase_demo
cd rebase_demo
git init
いくつかのコミットを作成します。
“`bash
echo “Initial content” > file1.txt
git add file1.txt
git commit -m “Initial commit” # root-commit
echo “Feature A part 1” > fileA.txt
git add fileA.txt
git commit -m “feat: Add feature A part 1” # HEAD~3
echo “Feature A part 2” >> fileA.txt
git add fileA.txt
git commit -m “feat: Add feature A part 2” # HEAD~2
echo “Fix typo in fileA” >> fileA.txt
実際には typo を修正するが、ここでは単に追記で代用
git add fileA.txt
git commit -m “fix: Fix typo in feature A” # HEAD~1
echo “Add cleanup code” >> fileA.txt
git add fileA.txt
git commit -m “refactor: Add cleanup for feature A” # HEAD
“`
現在のコミット履歴を確認します。
“`bash
git log –oneline
出力例:
8765432 (HEAD -> main) refactor: Add cleanup for feature A
6543210 fix: Fix typo in feature A
4321098 feat: Add feature A part 2
2109876 feat: Add feature A part 1
fedcba9 Initial commit
“`
直近3つのコミット(feat: Add feature A part 2、fix: Fix typo in feature A、refactor: Add cleanup for feature A)を整理したいとします。これらはすべて「Feature A」関連のコミットです。
目的:
* feat: Add feature A part 1 (2109876) はそのまま残す。
* 次の2つのコミット (feat: Add feature A part 2, fix: Fix typo...) を、feat: Add feature A part 1 に squash または fixup でまとめる。
* 最後のコミット (refactor: Add cleanup...) を、その時点で統合されたコミットに fixup でまとめる。
対象は feat: Add feature A part 1 (2109876) の次のコミットからHEADまでです。つまり、基点は feat: Add feature A part 1 の親コミットになります。または、単に直近4つのコミットを対象とすることもできます。ここでは直近4つを対象とします。
bash
git rebase -i HEAD~4
エディタが開きます。内容は以下のようになっているはずです(コミットIDは異なります)。
“`
pick 2109876 feat: Add feature A part 1
pick 4321098 feat: Add feature A part 2
pick 6543210 fix: Fix typo in feature A
pick 8765432 refactor: Add cleanup for feature A
… (コメント行) …
“`
これを以下のように編集します。
“`diff
– pick 2109876 feat: Add feature A part 1
+ pick 2109876 feat: Add feature A part 1
– pick 4321098 feat: Add feature A part 2
+ squash 4321098 feat: Add feature A part 2
– pick 6543210 fix: Fix typo in feature A
+ fixup 6543210 fix: Fix typo in feature A
– pick 8765432 refactor: Add cleanup for feature A
+ fixup 8765432 refactor: Add cleanup for feature A
… (コメント行) …
“`
- 1行目 (
feat: Add feature A part 1):pickのまま。これは新しいコミットの基点となります。 - 2行目 (
feat: Add feature A part 2):squashに変更。これは1行目のコミットに統合されます。 - 3行目 (
fix: Fix typo...):fixupに変更。これは(1行目と2行目が統合されてできた)直前のコミットに統合され、メッセージは破棄されます。 - 4行目 (
refactor: Add cleanup...):fixupに変更。これも(直前までのコミットがすべて統合されてできた)直前のコミットに統合され、メッセージは破棄されます。
編集を保存してエディタを閉じます。
Gitはまず、2109876 feat: Add feature A part 1 を処理し、次に 4321098 feat: Add feature A part 2 をsquashするため、新しいコミットのメッセージ編集を求めます。
“`
This is a combination of 2 commits.
This is the 1st commit message:
feat: Add feature A part 1
This is the commit message #2:
feat: Add feature A part 2
Please enter the commit message for your changes. Lines starting
with ‘#’ will be ignored, and an empty message aborts the commit.
…
“`
ここでは、これら2つをまとめた新しいメッセージを記述します。
“`
feat: Implement core of Feature A
This includes the initial part 1 and part 2 of the feature implementation.
This is a combination of 2 commits.
This is the 1st commit message:
feat: Add feature A part 1
This is the commit message #2:
feat: Add feature A part 2
Please enter the commit message for your changes. Lines starting
with ‘#’ will be ignored, and an empty message aborts the commit.
…
“`
編集を保存して閉じると、Gitはリベースを続行します。
次に、6543210 fix: Fix typo... を処理しますが、これは fixup なので、直前のコミット(先ほどメッセージを編集して作成した「feat: Implement core of Feature A」コミット)に統合され、メッセージ編集は不要です。
最後に、8765432 refactor: Add cleanup... を処理しますが、これも fixup なので、現在の先端コミットに統合され、メッセージ編集は不要です。
すべての処理が完了すると、リベース成功のメッセージが表示されます。
bash
Successfully rebased and updated refs/heads/main.
ここで履歴を確認します。
“`bash
git log –oneline
出力例:
9876543 (HEAD -> main) feat: Implement core of Feature A
fedcba9 Initial commit
“`
元の「feat: Add feature A part 2」、「fix: Fix typo…」、「refactor: Add cleanup…」の3つのコミットは消え、それらの変更内容が「feat: Implement core of Feature A」という一つの新しいコミットに統合されていることがわかります。コミットIDも新しくなっています。
このように、Interactive Rebase を使うことで、複数のコミットを目的や意味合いに応じてまとめ、整理された履歴を作成することができます。
RebaseとSquashの応用と注意点
Interactive Rebaseとsquash/fixupは非常に強力ですが、いくつかの応用的な使い方と、特に注意すべき点があります。
競合(Conflict)の解決
リベースの過程で、Gitがコミットを適用しようとしたときに、そのコミットの変更が現在のブランチの変更と衝突する場合があります。これを「競合」と呼びます。特に、異なるブランチで同じファイルの同じ行を変更した場合などに発生します。
競合が発生すると、Gitはリベース処理を一時停止し、どのファイルで競合が発生したかを知らせます。
bash
CONFLICT (content): Merge conflict in fileB.txt
error: could not apply hijklmn... Commit message 2
Resolve all conflicts manually, mark them as resolved with
"git add", then run "git rebase --continue".
競合発生時の状態は以下のようになります。
- Gitはリベース処理を一時停止しています。
- 競合が発生したファイルは、Gitによって特別なマーカー(
<<<<<<<,=======,>>>>>>>)が挿入された状態になっています。 git statusコマンドを実行すると、競合が発生しているファイルと、リベースの途中で一時停止していることがわかります。
“`bash
git status
出力例:
interactive rebase in progress; onto 1234567
Last command done (1 command done):
pick abcdefg Commit message 1
Next command to do (2 remaining commands):
squash hijklmn Commit message 2
squash opqrstu Commit message 3
You are currently rebasing branch ‘main’ on ‘1234567’.
(fix conflicts and then run “git rebase –continue”)
(use “git rebase –skip” to skip this patch)
(use “git rebase –abort” to abort the rebase)
Unmerged paths:
(use “git add …” to mark resolution)
both modified: fileB.txt
no changes added to commit (use “git add” and/or “git commit -a”)
“`
競合を解決するには、以下の手順を行います。
- 競合ファイルを編集: エディタやIDEを使って、競合が発生したファイルを開きます。Gitが挿入したマーカー(
<<<<<<<,=======,>>>>>>>)を手がかりに、どちらの変更を採用するか、あるいは両方の変更を組み合わせて、正しい内容になるようにファイルを編集します。マーカー自身は削除します。 - 解決したファイルをステージング: ファイルの内容が正しい状態になったら、その変更をステージングします。
git add <競合ファイル名>を実行します。複数のファイルで競合が発生した場合は、それぞれgit addします。 -
リベースを続行: すべての競合ファイルを解決し、ステージングし終えたら、以下のコマンドでリベース処理を再開します。
bash
git rebase --continueもし、
squashやfixupで競合が発生した場合、競合を解決してgit addした後にgit rebase --continueを実行すると、Gitは元の対話型リベースの指示に従って処理を続けます。squashやfixupの処理がまだ完了していない場合は、コミットメッセージの編集画面が表示されることもあります。
競合解決時のTips:
git diffを使うと、競合している箇所を詳しく見ることができます。git mergetoolを使うと、GUIツールなどを使って競合解決を効率的に行うことができます。- 途中でリベースを中止したい場合は、
git rebase --abortを実行します。これにより、リベース開始前の状態に戻すことができます。
競合解決はリベースの難しさの一つですが、慣れてくればスムーズに行えるようになります。小規模なリベースから始めたり、こまめにリベースを行ったりすることで、複雑な競合の発生を避けることができます。
共有リポジトリでのRebase
最も重要な注意点: 既にリモートリポジトリにプッシュしたコミットに対して、git rebase(特にInteractive Rebase)を実行することは、原則として避けるべきです。
なぜなら、リベースは元のコミットを捨てて新しいコミットを再作成するため、コミットのIDが変更されるからです。
あなたがプッシュ済みのコミットを含むブランチをリベースし、そのブランチを再びリモートにプッシュしようとすると、履歴が食い違っているため通常は拒否されます。
bash
To origin
! [rejected] main -> main (non-fast-forward)
error: failed to push some refs to 'origin'
hint: Updates were rejected because the tip of your current branch is behind
hint: its remote counterpart. Integrate the remote changes (e.g.
hint: 'git pull ...') before pushing again.
hint: See the 'Note about fast-forwards' in 'git push --help' for details.
このエラーメッセージは「リモートのブランチがあなたのローカルブランチより進んでいるから、先にリモートの変更を取り込め」と言っています。しかし、これはマージやプルをしてリモートのコミットを取り込むべき状況ではありません。あなたのローカルブランチは、リベースによってリモートとは異なる新しい履歴に書き換わってしまっているのです。
このような状況で無理やりプッシュするには、git push --force または git push --force-with-lease コマンドを使う必要があります。これらのコマンドは、リモートの履歴をローカルの履歴で上書きします。
git push --force: リモートの履歴を問答無用でローカルの履歴に置き換えます。これは非常に危険です。もし他の誰かがあなたがリベースする前にリモートのそのブランチにプッシュしていた場合、その他の人のコミットがあなたのForce Pushによって消されてしまいます。git push --force-with-lease:git push --forceと似ていますが、より安全です。リモートブランチの先端が、あなたがリベースを開始した時点から変化していない場合にのみプッシュを成功させます。もしあなたがリベースしている間に他の誰かが同じブランチにプッシュしていた場合は、Force Pushは失敗し、他の人の変更を誤って上書きするのを防ぎます。Force Pushが必要な場合は、可能な限り--force-with-leaseを使うべきです。
共有リポジトリでのベストプラクティス:
- 自分がまだ他の誰とも共有していない(リモートにプッシュしていない)ローカルブランチでのみ、自由にリベースやコミットの整理を行いましょう。 これが Interactive Rebase の最も安全で推奨される使い方です。
- チームメンバーと共有しているブランチ(例: メインの開発ブランチや、複数人で作業するフィーチャーブランチなど)のプッシュ済みのコミットに対しては、リベースによる履歴の書き換えは避けるべきです。
- フィーチャーブランチをメインブランチに統合する際は、マージの種類についてチームで合意しましょう。プルリクエストなどのレビュープロセスを持つ開発フローでは、マージする前にローカルでInteractive Rebase + Squash を行ってフィーチャーブランチの履歴をクリーンアップし、その整理された履歴をプルリクエストとして提出するのが一般的なプラクティスです。マージ自体は通常の
git merge --no-ffや、後述のgit merge --squashで行うこともあります。
大きな変更をRebaseする場合の注意
一度に多くのコミット(例えば数十個)を対象として Interactive Rebase を行うと、競合が発生した場合の解決が非常に困難になることがあります。また、意図しない結果になった場合に原因特定ややり直しが難しくなります。
- 可能であれば、小分けにしてリベースを行いましょう。例えば、関連性の高い数個のコミットずつを対象にするなど。
editコマンドを効果的に使いましょう。例えば、複雑な変更を含むコミットの手前でeditで一時停止し、そのコミットを適用した後で手動でチェックや修正を行ってからgit rebase --continueで進める、といった方法があります。
Squash vs Merge
コミットをまとめるという観点では、git merge --squash というコマンドもあります。これはInteractive Rebase のsquashとは異なる機能ですが、混同されがちなのでここで違いを説明します。
Interactive Rebase + Squash (git rebase -i の中で squash コマンドを使う):
- 目的: 現在のブランチ上にある複数の既存のコミットを、より少ない数の新しいコミットに書き換える。主に、作業途中で生まれたノイズの多いコミットを整理するために使用する。
- 実行場所: コミットを整理したいブランチ上で実行する。
- 結果: 対象となった元のコミットは新しいコミットに置き換わり、コミットIDが変わる。履歴は書き換わる。
Squash Merge (git merge --squash <ブランチ名>):
- 目的: あるブランチ(例えばフィーチャーブランチ)の全ての変更内容を、マージ先のブランチ(例えば
mainブランチ)上の単一の新しいコミットとして取り込む。これにより、フィーチャーブランチの個々のコミット履歴をマージ先のブランチに残さずに、変更内容だけをまとめて取り込める。 - 実行場所: 変更を取り込みたいブランチ(マージ先)上で実行する。
- 結果: マージ元ブランチの複数のコミット内容は、マージ先ブランチに1つのコミットとして追加される。元のマージ元ブランチのコミット履歴はマージ先には取り込まれない。通常のMergeと異なり、Mergeコミットは作成されない。
git merge --squashを実行した後、手動でgit commitを実行する必要がある。
使い分けの例:
- 自分のフィーチャーブランチで、まだ誰にも見せていない(プッシュしていない)状態: 試行錯誤の細かいコミットや修正コミットが溜まっている場合、
git rebase -i HEAD~Nとsquash/fixupを使って履歴を整理するのが適しています。これにより、プルリクエスト提出時などにレビュワーに見せる履歴をきれいにできます。 - フィーチャーブランチの開発が完了し、
mainブランチに取り込む時:- フィーチャーブランチの個々のコミット履歴も
mainに残したい場合:git merge --no-ff feature-branch(通常の3-way merge) - フィーチャーブランチの個々のコミット履歴は
mainに残さず、その変更内容全体を1つのコミットにまとめてmainに取り込みたい場合:git checkout mainしてgit merge --squash feature-branchしてからgit commit。または、先にフィーチャーブランチ側でInteractive Rebase + Squashで履歴を整理してから、mainに通常のMergeを行う。後者の方法が、フィーチャーブランチの履歴も整理され、かつmainへのマージコミットも残るため、トレースしやすさの点で好まれることが多いです。GitHubやGitLabなどのホスティングサービスでは、プルリクエストのマージオプションとして「Squash and Merge」が提供されていることがあり、これはgit merge --squashに近い動作をサービス側で行ってくれます。
- フィーチャーブランチの個々のコミット履歴も
要するに、rebase -i squash はローカルの履歴を書き換えて整理するツール、merge --squash は他のブランチの変更内容を1つのコミットとして取り込むツール、と理解すると良いでしょう。
コミットをまとめるその他の方法
Interactive Rebase + Squash/Fixup が最も強力な方法ですが、他にも状況に応じてコミットをまとめる(あるいは直前のコミットを変更する)方法があります。
git commit --amend
これは、直前のコミットに、現在のステージングエリアの内容を追加したり、コミットメッセージを変更したりするコマンドです。
例えば、直前にコミットしたばかりなのに、小さなtypoを見つけて修正し忘れていたとします。
“`bash
いくつかのファイルを変更してコミット
git add file1.txt
git commit -m “feat: Add login form”
あとから file1.txt の typo を見つけた
file1.txt を修正…
git add file1.txt
新しいコミットを作る代わりに、直前のコミットに修正内容を含めたい
git commit –amend
“`
git commit --amend を実行すると、Gitは直前のコミットのステージング内容(この場合は修正した file1.txt)と、現在のステージングエリアにある内容を合わせた新しいスナップショットを作成します。そして、直前のコミットをその新しいスナップショットで置き換えます。コミットメッセージのエディタも開かれるので、メッセージも修正できます(修正が必要なければそのまま閉じれば元のメッセージが使われます)。
結果として、元の「feat: Add login form」コミットは、typo修正を含む新しいコミットに置き換わります。元のコミットは履歴から消え、新しいコミットIDが生成されます。
注意点:
git commit --amendは、まだリモートにプッシュしていない直前のコミットにのみ使用してください。プッシュ済みのコミットに対して実行すると、他の開発者との間で履歴の矛盾を引き起こします(Rebaseと同様の理由)。--amendは、文字通り直前のコミットを「修正」するものであり、複数の離れたコミットをまとめたり、順番を並べ替えたりすることはできません。
Interactive Rebaseほど柔軟ではありませんが、直前のコミットをちょっと直したい、という場合には非常に手軽で便利なコマンドです。
まとめとベストプラクティス
Gitのコミットをまとめる技術、特にInteractive Rebaseとsquash/fixupコマンドについて詳しく解説してきました。これらの技術を習得することで、Gitの履歴管理能力が飛躍的に向上します。
なぜコミットをまとめるのか?
改めて、コミットをまとめる目的は、ノイズを排除し、意味のある変更単位で履歴を整理することで、可読性を高め、追跡やレビューを容易にし、将来的な履歴活用(bisect、cherry-pick、revertなど)を効率的に行うためです。きれいな履歴は、プロジェクト全体の健全性を保つ上で非常に重要です。
Interactive Rebase + Squash/Fixup の強力さ
git rebase -i は、指定した範囲のコミットを自由に並べ替えたり、削除したり、そして最も重要な複数のコミットをsquashやfixupを使って一つにまとめたりできる、非常に柔軟なツールです。これにより、作業途中の細かなコミットを、機能単位や目的単位のクリーンなコミットに集約できます。
squash: 変更内容を統合し、新しいコミットメッセージを書きたい場合。fixup: 変更内容を統合し、元のコミットメッセージをそのまま使いたい(統合される側のメッセージは不要)場合。
Rebase/Squash を使用する際のガイドラインとベストプラクティス
- 公開済みのコミットには原則適用しない: これが最も重要なルールです。一度リモートにプッシュし、他の開発者がフェッチ/プルする可能性があるコミットに対してRebaseを行うと、履歴が書き換わり、他の開発者との間で履歴の矛盾が発生し、混乱を招きます。Force Pushが必要になり、誤って他の人の作業を上書きする危険性もあります。Interactive Rebaseは、自分がまだ他の誰とも共有していない(ローカルのみの)ブランチでのみ安全に実行できます。
- 小まめに整理する: 試行錯誤のコミットが溜まりすぎる前に、あるいはフィーチャーブランチの区切りごとに、小まめにInteractive Rebaseで履歴を整理することを習慣づけましょう。一度に多くのコミットをRebaseすると、競合解決が複雑になりがちです。
- 分かりやすいコミットメッセージを作成する:
squashで複数のコミットをまとめた場合は、必ずその統合された変更全体を適切に表現する新しいコミットメッセージを書きましょう。fixupを使った場合も、元のコミットメッセージがその後の変更内容を適切に含んでいるか確認しましょう。 - チーム内でルールの合意形成を行う: チーム開発においては、コミットの粒度やRebase/Squashのポリシーについてメンバー間で共通認識を持つことが重要です。例えば、「プルリクエストをマージする前にフィーチャーブランチの履歴をInteractive Rebaseで整理する」「マージはSquash Mergeを使う」など、チームのワークフローに合わせてルールを定めましょう。
- 安全な環境で練習する: 初めてInteractive RebaseやSquashを試す場合は、重要なリポジトリではなく、ダミーのリポジトリを作成して練習することをお勧めします。また、操作の前にブランチをバックアップしておくと安心です(例:
git branch backup-branch-name)。 - 問題発生時は
git reflog: リベースで意図しない状態になってしまった場合でも、慌てないでください。git reflogコマンドは、リポジトリで行われた操作(コミット、リベース、リセットなど)の履歴を記録しており、特定の時点(操作前の状態など)に戻ることができます。git reset --hard HEAD@{n}のように使います。
よくある質問 (FAQ)
Q: RebaseとMerge、どちらを使うべきですか?
A: これはGitユーザーの間でも議論になることが多いトピックです。
* Merge: 元の履歴構造(分岐と合流)を忠実に残します。いつどのブランチからマージされたかが履歴上で明確になります。共有ブランチへの変更を取り込む際や、他の人の変更を自分のブランチに取り込む際に、安全で非破壊的な方法として広く使われます。ただし、頻繁にマージを行うと履歴が複雑になりがちです。
* Rebase: 履歴を直線的にします。特定のブランチの変更を、別のブランチの最新の状態の上に適用し直すことで、よりきれいな「一本の線」の履歴を作成できます。主に、自分のローカルブランチで作業している際に、公開前に履歴を整理するために使われます。既に公開されたコミットに対しては避けるべきです。
どちらが良いかはプロジェクトのポリシーやチームの好みによります。フィーチャーブランチでInteractive Rebaseを使って履歴を整理し、メインブランチにはMerge(通常のMergeまたはSquash Merge)で取り込む、という組み合わせも一般的です。
Q: Interactive Rebase でコミットの順番を並べ替えることはできますか?
A: はい、できます。エディタで表示されるコミットリストの行をドラッグ&ドロップしたり、カット&ペーストしたりして、順番を自由に並べ替えて保存すれば、Gitはその新しい順番でコミットを適用し直します。ただし、順番を大きく変えると競合が発生しやすくなります。
Q: 特定のコミットだけを履歴から削除できますか?
A: はい、Interactive Rebase を使って削除できます。削除したいコミットの行のコマンドを drop に変更するか、その行自体をエディタから削除します。これも履歴を書き換える操作なので、公開済みのコミットには適用しないでください。
Q: Rebase中に間違いを犯してしまいました。やり直すには?
A: Rebase中にいつでも git rebase --abort コマンドで中止できます。これにより、リベース開始前の状態に戻ります。もしリベースが一度完了してしまった後に間違いに気づいた場合は、git reflog コマンドで操作履歴を確認し、リベースを実行する直前のHEADの状態(例えば HEAD@{1} や HEAD@{2} など)を見つけ、git reset --hard HEAD@{n} のようにしてその状態に戻すことができます。ただし、この操作も履歴を書き換えるので慎重に行ってください。
Q: git pull --rebase と Interactive Rebase (git rebase -i) は関係がありますか?
A: git pull --rebase は、git pull の一種のオプションであり、リモートから変更を取得した際に、ローカルでの未プッシュの変更をリモートの変更の上に自動的にリベースするというものです。これは「リモートの履歴の上に自分の変更を積み重ねたい」という目的で行われ、履歴が直線的になります。一方、Interactive Rebase (git rebase -i) は、ブランチ内の指定したコミット範囲に対して、より詳細な操作(squash, fixup, reword, edit, dropなど)を行うための対話モードです。目的と機能範囲が異なりますが、どちらもRebaseというメカニズムを利用しています。
終わりに
GitのInteractive RebaseとSquash/Fixupは、使いこなすには少し慣れが必要なコマンドですが、習得すれば開発の質を大きく向上させることができます。試行錯誤の過程をきれいに整理し、機能単位で意味のあるコミットにまとめることで、自分自身の開発効率が上がるだけでなく、チームメンバーとの共同作業やコードレビューもスムーズになります。
Gitは非常に強力なバージョン管理システムであり、今回解説した機能以外にも多くの便利な機能があります。ぜひ積極的にGitの様々な機能を学び、日々の開発に活かしてください。
クリーンなコミット履歴は、より良いソフトウェア開発への一歩です。この記事が、そのための助けとなれば幸いです。