はい、承知いたしました。【入門】Git rebaseの使い方を徹底解説!Mergeとの違いも、というタイトルで約5000語の詳細な解説記事を作成します。
【入門】Git rebaseの使い方を徹底解説!Mergeとの違いも
Gitを使った開発は、複数人で並行して作業を進める上で不可欠な技術です。その中でも、ブランチの管理や変更履歴の整理は、プロジェクトを円滑に進める上で非常に重要になります。Gitには、異なるブランチの変更内容を統合する方法がいくつか存在しますが、代表的なものとしてgit merge
とgit rebase
があります。
多くの入門書やチュートリアルでは、まずmerge
の使い方を学ぶことが多いでしょう。merge
はシンプルで分かりやすい統合方法ですが、使い方によってはコミットグラフが複雑になりがちという側面も持っています。一方でrebase
は、より線形的な、つまり一直線なきれいなコミット履歴を作成するのに役立ちますが、その仕組みが少し分かりづらく、また使い方を間違えると履歴を書き換えてしまうため慎重な扱いが必要です。
「Rebaseは難しい」「怖い」といったイメージをお持ちの方もいるかもしれません。しかし、rebase
の仕組みと使いどころを正しく理解すれば、あなたのGitワークフローを劇的に改善し、より見やすく管理しやすいコミット履歴を作り出す強力な武器となります。
この記事では、Gitを使った開発にある程度慣れてきた入門者の方を対象に、rebase
の基本的な使い方から、merge
との違い、そして実践的なシナリオでの活用方法までを徹底的に解説します。特に、なぜrebase
が履歴をきれいにするのか、merge
とはどう異なるのかを、図解(文章で図解を表現します)を交えながら分かりやすく説明することに重点を置きます。
この記事を読むことで、あなたは以下のことを習得できます。
- Gitにおける変更履歴の考え方と、ブランチがどのように機能するかを再確認する。
git merge
の仕組みと、その利点・欠点を理解する。git rebase
の仕組みと、その利点・欠点を理解する。merge
とrebase
の決定的な違いと、それぞれの適切な使い分けができるようになる。git rebase -i
(インタラクティブRebase)を使って、コミット履歴を編集・整理する方法を学ぶ。rebase
を使う上での注意点、特に履歴の書き換えに関するリスクと対処法を理解する。- 実際の開発現場でどのように
rebase
を活用できるかのイメージを持つ。
さあ、Git rebase
の世界への扉を開けて、あなたのGitスキルを次のレベルへと引き上げましょう!
Gitの基本的な復習:なぜ履歴管理が重要なのか?
rebase
やmerge
の話に入る前に、まずはGitの基本的な概念、特に「コミット」「ブランチ」「コミットグラフ」について簡単に復習しましょう。これらの概念をしっかり理解しておくことが、rebase
やmerge
の仕組みを把握する上で非常に重要です。
コミット (Commit)
Gitにおけるコミットは、プロジェクトのファイルの状態を特定の時点(スナップショット)で記録したものです。各コミットには、誰が、いつ、どのような変更を行ったのかを示す情報(コミットメッセージ、作成者、タイムスタンプ)が含まれます。また、それぞれのコミットは一意の識別子(ハッシュ値、例: a1b2c3d4
) を持ち、その親コミットへの参照を持っています。これにより、コミットは履歴としてチェーン状につながっていきます。
[コミット D] <- [コミット C] <- [コミット B] <- [コミット A]
このチェーンが、プロジェクトの変更履歴そのものです。
ブランチ (Branch)
ブランチは、これらのコミットの連なりの上を指す「ポインター」のようなものです。デフォルトではmain
(またはmaster
)という名前のブランチが存在します。新しい機能開発やバグ修正を行う際には、通常、既存のブランチ(例えばmain
)から新しいブランチを作成します。
新しいブランチを作成すると、それは元のブランチが指していた最新のコミットを指すようになります。その新しいブランチでコミットを行うと、新しいコミットが作成され、そのブランチはその新しいコミットを指すように移動します。元のブランチは元のコミットを指したままなので、ここから履歴が分岐していくことになります。
[コミット E] <- [featureブランチ] (新しいコミットを追加)
/
[コミット D] <- [コミット C] <- [コミット B] <- [コミット A] <- [mainブランチ] (元のまま)
ブランチを使うことで、複数の開発者がそれぞれ独立した作業を並行して進めることができます。そして、作業が完了したら、そのブランチの変更内容を他のブランチ(例えばmain
ブランチ)に取り込むことになります。この「取り込む」作業が、merge
やrebase
で行われます。
コミットグラフ (Commit Graph)
コミットグラフは、これらのコミットとブランチの関係を視覚的に表現したものです。各ノードがコミットを表し、エッジ(矢印)が親コミットへの参照を示します。ブランチは特定のコミットを指すラベルとして表示されます。Gitにおける履歴の統合操作(merge
やrebase
)は、このコミットグラフの形状に直接影響を与えます。
なぜ履歴管理が重要なのでしょうか?それは、プロジェクトの「進化の過程」を正確に記録し、追跡可能にするためです。問題が発生した際に、いつ、誰が、どのような変更によってその問題が引き起こされたのかを特定したり、過去の特定の状態に戻したりすることが容易になります。また、チームメンバーが互いの変更を理解し、協力して作業を進める上でも、整理された分かりやすい履歴は非常に役立ちます。
さて、この変更履歴を統合する方法として、まずはmerge
から詳しく見ていきましょう。
Git Mergeとは?
git merge
は、あるブランチで行われた変更内容を別のブランチに取り込むための最も一般的で基本的なコマンドです。これは、2つのブランチのコミット履歴を統合し、共通の新しい履歴を作成します。
merge
の基本的な使い方
例えば、feature
ブランチで新しい機能開発を行い、それが完了したのでmain
ブランチに取り込みたいとします。main
ブランチにチェックアウトした状態で、以下のコマンドを実行します。
bash
git checkout main
git merge feature
このコマンドを実行すると、Gitはmain
ブランチの先端とfeature
ブランチの先端、そして両ブランチの共通の祖先(分岐点)の3つのコミットを比較し、変更内容を統合しようとします。これが「3-way merge」と呼ばれる基本的なマージの仕組みです。
merge
の仕組み(3-way mergeとFast-forward merge)
merge
には、大きく分けて二つのパターンがあります。
-
Fast-forward merge (早送りマージ)
- これは、マージ対象のブランチ(上の例では
feature
)が、マージ先のブランチ(main
)から枝分かれした後、マージ先のブランチに新しいコミットが一切追加されていない場合に発生します。 - この場合、Gitは非常にシンプルにマージを行います。
main
ブランチのポインターを、feature
ブランチの最新コミットまで「早送り」させるだけです。新しいマージコミットは作成されません。
[コミット A] <- [コミット B] <- [コミット C] <- [mainブランチ, featureブランチ]
(featureブランチ作成)↓
feature
でコミット[コミット A] <- [コミット B] <- [コミット C] <- [コミット D] <- [コミット E] <- [featureブランチ]
^ ^
| |
[mainブランチ] (まだここにいる)↓
git checkout main
->git merge feature
(Fast-forward merge)[コミット A] <- [コミット B] <- [コミット C] <- [コミット D] <- [コミット E] <- [mainブランチ, featureブランチ]
- Fast-forward mergeは履歴が直線的で分かりやすいですが、元の
feature
ブランチが存在していたという履歴は(ブランチ自体を削除しない限り)視覚的に残りにくいという側面もあります。
- これは、マージ対象のブランチ(上の例では
-
No-fast-forward merge (通常のマージ、3-way merge)
- これは、マージ対象のブランチ(
feature
)で作業している間に、マージ先のブランチ(main
)にも新しいコミットが追加された場合に発生します。つまり、両方のブランチが分岐点からそれぞれ独立して進んでいる状態です。 - この場合、Gitは両方のブランチの先端と、共通の祖先コミットを比較し、それらの変更内容を統合した新しいコミットを作成します。この新しいコミットを「マージコミット (Merge Commit)」と呼びます。マージコミットは、マージされた両方のブランチの先端を親として持つことになります。
[コミット A] <- [コミット B] <- [コミット C] <- [featureブランチ] (featureで作業)
^
|
[mainブランチ] (まだここにいる)↓
main
にも新しいコミットが追加される[コミット A] <- [コミット B] <- [コミット C] <- [コミット D] <- [featureブランチ]
^
|
[コミット E] <- [コミット F] <- [mainブランチ] (mainで作業)↓
git checkout main
->git merge feature
(No-fast-forward merge)[コミット D] <- [featureブランチ]
/ \
[コミット A] <- [コミット B] <- [コミット C] - [マージコミット G] <- [mainブランチ]
\ /
[コミット E] <- [コミット F]- マージコミットを作成することで、
feature
ブランチのすべての変更がmain
ブランチに取り込まれた、という「イベント」自体が履歴として記録されます。元のfeature
ブランチの履歴もそのまま残ります。 - デフォルトでは、Fast-forward可能な場合はFast-forwardが行われます。常にNo-fast-forward mergeを行いたい場合は、
git merge --no-ff <branch>
オプションを使います。これにより、Fast-forward可能な場合でもマージコミットが作成されます。多くのチームでは、機能開発の完了などを明確に履歴に残すために--no-ff
を使用することを推奨しています。
- これは、マージ対象のブランチ(
merge
の利点
- シンプルで分かりやすい: 考え方が直感的であり、初心者にも理解しやすいです。
- 履歴が保存される: 元のブランチで作成されたコミットがそのまま履歴に残ります。
--no-ff
を使えば、マージしたという事実もマージコミットとして明確に残ります。これにより、プロジェクトの進化の過程を後から追跡しやすいというメリットがあります。 - 安全: 基本的に既存の履歴を書き換えることはありません(後述の
rebase
との大きな違いです)。一度プッシュしたコミットを含むブランチをマージしても、履歴が書き換わらないため他の開発者に影響を与えにくいです。
merge
の欠点
- コミットグラフが複雑になりやすい: 特に複数のブランチが頻繁にマージされる大規模なプロジェクトでは、マージコミットが多数生成され、コミットグラフが枝分かれしたり収束したりを繰り返すため、非常に複雑で見づらくなりがちです。
- 「ノイズ」となるマージコミットが増える: 開発の純粋な変更内容を示すコミットの中に、マージのためだけに作成されたコミットが混ざることで、履歴を追いにくく感じることがあります。
まとめると、merge
は履歴を「統合した事実」として残すことに重点を置いた方法と言えます。ブランチ開発の経緯を重視する場合や、履歴の書き換えを避けたい場合に適しています。
次に、もう一つの主要な統合方法であるrebase
について見ていきましょう。
Git Rebaseとは?
git rebase
は、「ブランチの基点(ベース)を変更する」ためのコマンドです。これは、あるブランチで作成したコミット群を、別のブランチの最新コミットの上に「積み木のように積み直す」操作に例えられます。
rebase
の基本的な考え方
例えば、feature
ブランチで作業を開始しましたが、その後にmain
ブランチに新しい変更が加わりました。feature
ブランチをmain
ブランチの最新の状態に追いつかせたい、かつ、feature
ブランチでの変更履歴を、main
ブランチの最新コミットの上に線形に(一直線に)続けたい、という場合にrebase
が役立ちます。
[コミット A] <- [コミット B] <- [コミット C] <- [featureブランチ] (featureで作業)
^
|
[mainブランチ] (まだここにいる)
↓ main
にも新しいコミットが追加される
[コミット A] <- [コミット B] <- [コミット C] <- [コミット D] <- [featureブランチ]
^
|
[コミット E] <- [コミット F] <- [mainブランチ] (mainで作業)
この状態でfeature
ブランチをmain
ブランチの最新状態にrebase
するとどうなるでしょうか?
rebase
の仕組み
git rebase <base_branch>
コマンド(例: git rebase main
)を実行すると、Gitは以下の手順で処理を行います。
- 現在のブランチ(例:
feature
)と、rebase先のブランチ(例:main
)の共通の祖先コミットを見つけます。 - 現在のブランチの、共通の祖先コミットよりも新しいコミット(例:
コミット C
、コミット D
)を一時的な場所に退避させます。(これらのコミットを「再生 (replay)」すると表現されることもあります) - 現在のブランチを、rebase先のブランチの先端(例:
コミット F
)まで移動させます。つまり、ブランチの基点が変更されます。 - 退避させておいたコミットを、新しい基点の上から一つずつ順番に「再生」していきます。Gitは、各コミットで行われた変更内容を、新しい基点の状態に適用しようとします。
- この「再生」の過程で、元のコミットと同じ内容の新しいコミットが新しいハッシュ値で作成されます。元のコミットは参照されなくなり、最終的にはGitのガベージコレクションによって削除されます。(これが「履歴の書き換え」と言われる所以です)
- すべてのコミットの再生が成功すると、現在のブランチは再構築された新しいコミット群の先端を指すようになります。
上記の例で feature
ブランチから main
ブランチに対して git rebase main
を実行した場合、結果は以下のようになります。
[コミット A] <- [コミット B] <- [コミット C] <- [コミット D] <- [featureブランチ] (元の状態)
^
|
[コミット E] <- [コミット F] <- [mainブランチ]
↓ git checkout feature
-> git rebase main
実行後
[コミット A] <- [コミット B]
^
|
[コミット E] <- [コミット F] <- [コミット C'] <- [コミット D'] <- [featureブランチ]
^
|
[mainブランチ]
注目すべき点:
feature
ブランチの基点が、元のコミット B
からmain
ブランチの先端であるコミット F
に変わりました。- 元の
コミット C
とコミット D
はそのままではなく、その内容が新しいコミットコミット C'
とコミット D'
としてコミット F
の上に積み直されました。コミット C'
とコミット D'
は、内容としては元のコミット C
とコミット D
と同じですが、親コミットが異なるため、新しいハッシュ値を持つ全く別のコミットとして扱われます。
このように、rebase
は履歴を線形化し、まるで最初からmain
ブランチの最新状態からfeature
ブランチの開発を始めたかのようなきれいな履歴を作り出します。
rebase
中の競合(コンフリクト)解消
rebase
中に、再生しようとしているコミットの内容と、rebase先のブランチ(例: main
)の内容が衝突することがあります。この場合、Gitはrebase処理を一時停止し、競合が発生したファイルを示します。
競合が発生したら、以下の手順で解消します。
git status
で競合ファイルを確認します。- 競合しているファイルをエディタで開き、手動で競合マーカー(
<<<<<<<
,=======
,>>>>>>>
など)を修正し、 desired な状態にします。 - 競合を解消したファイルをステージングエリアに追加します。
git add <解決したファイル名>
またはgit add .
- 競合解消が終わったら、
rebase
処理を続行します。git rebase --continue
もしrebaseを中止したい場合は、git rebase --abort
コマンドを実行します。これにより、rebaseを開始する前の状態に戻ることができます。
競合解消は、merge
の際と同様に行いますが、rebase
の場合は、rebase対象のコミットごとに競合が発生する可能性があります。複数のコミットをrebaseする場合、それぞれのコミットを再生するたびに競合が発生し、その都度解消を求められる可能性があります。これは、rebase
を使う上での難しさの一つと言えるでしょう。
rebase
の利点
- コミットグラフが線形になる: 複雑なマージコミットが発生せず、コミット履歴が一直線になります。これにより、プロジェクトの履歴が非常に見やすく、追いやすくなります。
- 履歴が整理される: 後述するインタラクティブRebaseを使えば、rebase中に複数の小さなコミットを一つにまとめたり、不要なコミットを削除したり、コミットの順番を並べ替えたりすることができます。これにより、より意味のある、クリーンなコミット履歴を作成できます。
- マージコミットが発生しない: 純粋な変更内容を示すコミットだけで履歴が構成されるため、「ノイズ」が少なくなります。
- プルリクエスト(PR)/マージリクエスト(MR)の準備: 開発ブランチを最新の
main
などに対してrebaseすることで、PR/MRを作成する際にベースブランチとの差分が少なくなり、レビューしやすくなります。
rebase
の欠点
- 履歴が書き換えられる: Rebaseは元のコミットを再利用するのではなく、その内容に基づいて新しいコミットを作成します。これにより、元のコミットのハッシュ値が変わってしまいます。これは、後述するように共有リポジトリで利用する際に大きな問題を引き起こす可能性があります。
- 使い方を間違えると危険: 特にリモートリポジトリにプッシュ済みのコミットに対してrebaseを行うと、他の開発者がその変更を取り込む際に問題が発生します。
- 競合解消が煩雑になる可能性: rebase対象のコミットが多い場合や、rebase先のブランチとの変更が大きく乖離している場合、それぞれのコミットの再生時に何度も競合解消を求められる可能性があります。
まとめると、rebase
は履歴を「きれいな状態に整形する」ことに重点を置いた方法と言えます。ローカルでの作業ブランチを整理したり、共有する前に履歴をクリーンアップしたい場合に強力なツールとなります。しかし、「履歴の書き換え」という特性を理解し、慎重に使う必要があります。
Merge vs Rebase: 違いと使い分け
さて、merge
とrebase
、それぞれの基本的な仕組みと利点・欠点が理解できたところで、両者の違いを明確にし、どのような場合にどちらを使うべきかを考えてみましょう。
決定的な違い
最も大きな違いは、履歴の取り扱い方です。
merge
: 異なるブランチの履歴を結合し、その結合したという事実をマージコミットとして履歴に残します。元のコミット履歴はそのまま保存されます。rebase
: あるブランチのコミット群を別のブランチの先端に積み直し、元のコミットと同じ内容の新しいコミットを作成します。これにより、履歴が線形化されますが、元のコミットのハッシュ値が変わり、履歴が書き換えられます。
コミットグラフへの影響
この履歴の取り扱いの違いが、コミットグラフの形状に直接影響します。
merge
: 履歴が分岐し、マージコミットで再び合流する形で、複雑なグラフになりがちです(--no-ff
使用時)。rebase
: 履歴が一直線になり、分岐や合流が少なくなります。非常にきれいな線形のグラフになります。
(文章による図解の再掲と補足)
Merge (--no-ff
) のグラフ例:
...--o--o--o (mainの履歴)
\ \
o--o--o (featureの履歴) --o (feature先端)
\
o--------------------o (Mergeコミット) --o (main先端)
Rebase のグラフ例:
...--o--o--o (mainの履歴) --o--o (main先端)
\
o'--o'--o' (featureの再構築された履歴) --o' (feature先端)
※ o'
は元のコミットと同じ内容だがハッシュ値が異なる新しいコミットを表します。
履歴の永続性
履歴の永続性の観点からも違いがあります。
merge
: 基本的に履歴を書き換えないため、一度共有された(リモートにプッシュされた)コミットを含むブランチをマージしても問題ありません。履歴の変更は、新しいマージコミットの追加にとどまります。rebase
: 履歴を書き換えます。特にリモートにプッシュ済みのコミットを含むブランチをrebaseし、それを再度プッシュしようとすると、履歴の矛盾が発生し、git push
が拒否されることがあります。この場合、強制プッシュ(git push --force
など)が必要になりますが、これは他の開発者のリポジトリの履歴との間に食い違いを生じさせ、深刻な問題を引き起こす可能性があります。共有リポジトリにプッシュ済みのコミットを含むブランチに対してrebase
を行うことは、原則として避けるべきです。
使用シナリオによる使い分け
これらの違いを踏まえると、merge
とrebase
は以下のように使い分けることが推奨されます。
-
ローカルでの作業ブランチの整理:
- あなたが一人で作業している、あるいはまだ誰とも共有していないローカルの機能開発ブランチがあるとします。
- このブランチを最新の
main
ブランチの状態に追いつかせたい場合。 - このブランチでのコミットが複数あり、それらをよりきれいな形で
main
の履歴に続けたい場合。 - →
rebase
が適しています。git fetch origin main
で最新のmain
を取得した後、git rebase origin/main
(またはローカルの追跡ブランチ名)を実行します。これにより、あなたの作業ブランチのコミットが、最新のmain
の上にきれいに積み直され、プルリクエストなどを送る前にベースブランチとの差分を最小限にできます。 - さらに、後述するインタラクティブRebaseを使って、ローカルの作業ブランチ内のコミットを整理(結合、編集、削除など)することもできます。
-
共有ブランチ(例:
develop
,main
)への取り込み:- 機能開発が完了し、チーム全体で共有しているメインブランチ(
main
やdevelop
など)に変更を取り込みたい場合。 - この場合は、一般的に
merge
を使用することが推奨されます。 - 特に、
git merge --no-ff <feature_branch>
とすることで、機能開発ブランチが完了し、メインブランチにマージされた、という明確なイベントを履歴に残すことができます。これは、後から履歴を追跡したり、リリース履歴を確認したりする上で非常に役立ちます。 - なぜ共有ブランチへの
rebase
は避けるべきか? メインブランチに対して他のブランチをrebaseすると、メインブランチ自体の履歴が書き換わることになります。これは、そのメインブランチをクローンまたはフェッチしている他のすべての開発者のリポジトリの履歴との間に矛盾を生じさせます。他の開発者は、強制プッシュや複雑な履歴操作を行わないと、自分のローカルリポジトリをリモートの最新状態に追いつかせることができなくなります。
- 機能開発が完了し、チーム全体で共有しているメインブランチ(
-
他の人の作業との統合(ローカルでの最新化):
- あなたが作業しているブランチがあり、他の開発者がすでにメインブランチ(例:
main
)に新しい変更をプッシュしたとします。 - あなたの作業を続ける前に、最新の
main
の状態を自分のブランチに取り込みたい場合。 git merge main
を実行することも可能ですが、これによりあなたのブランチとmain
の間にマージコミットが作成され、履歴が分岐する可能性があります。git rebase main
を実行すると、あなたのローカルの作業ブランチの基点が最新のmain
に移り、履歴が線形になります。これは、プルリクエストを出す前などに、自分の変更が最新のコードベースに対して問題なく動作するかを確認するのに役立ちます。多くの開発者は、ローカルでの作業中にはrebase
を使って最新の変更を取り込むことを好みます。
- あなたが作業しているブランチがあり、他の開発者がすでにメインブランチ(例:
一般的な推奨ワークフロー例:
- 作業を開始する前に、最新の
main
ブランチから新しいfeature
ブランチを作成する。
bash
git checkout main
git pull origin main # 最新のmainを取得
git checkout -b my-feature-branch my-feature-branch
で開発を進め、適宜コミットを行う。- 開発中に他の開発者が
main
にプッシュした場合、自分のブランチを最新化するためにrebase
を使用する(共有する前なら安全)。
bash
git fetch origin main
git rebase origin/main # または git rebase main (ローカルのmainが最新なら)
この際、競合が発生したら解消し、git rebase --continue
で続ける。 - 機能開発が完了し、プルリクエストを作成する準備ができた場合。
- ローカルの作業ブランチのコミット履歴を整理したい場合は、後述のインタラクティブRebase (
git rebase -i
) を使ってコミットをまとめたり、メッセージを編集したりする。 - 再度、最終確認として最新の
main
に対してrebase
を行う。
bash
git fetch origin main
git rebase origin/main
注意: ここでrebaseしたブランチをリモートにプッシュする場合は、履歴が書き換わっているため通常の方法ではプッシュできません。後述の注意点をよく理解してから行います。
- ローカルの作業ブランチのコミット履歴を整理したい場合は、後述のインタラクティブRebase (
- プルリクエスト/マージリクエストを作成し、レビューを経てメインブランチに統合する。
- チームのポリシーにもよるが、多くの場合、プルリクエストの受け入れ時に、GitHubやGitLabなどのホスティングサービスの機能を使って、メインブランチ側から
merge --no-ff
またはsquash merge
で取り込むことが多いです。これにより、メインブランチの履歴が保護され、かつ機能開発の完了を履歴に残すことができます。 - ローカルでrebaseしたブランチを、リモートの同じブランチにプッシュしてPR/MRを作成する場合、履歴が書き換わっているため
git push
が拒否されます。この場合、git push --force-with-lease
(またはgit push --force
)が必要になりますが、これは危険を伴うため、そのリスクを理解している場合にのみ行います。
- チームのポリシーにもよるが、多くの場合、プルリクエストの受け入れ時に、GitHubやGitLabなどのホスティングサービスの機能を使って、メインブランチ側から
まとめると、rebase
は主にローカルでの自分の作業ブランチを整理したり、最新の状態に追いつかせたりする際にその真価を発揮します。一方、merge
はブランチの統合というイベントを履歴に残し、共有ブランチの履歴を保護する場合に適しています。どちらか一方が優れているというわけではなく、それぞれの特性を理解し、目的に応じて適切に使い分けることが重要です。
インタラクティブRebase (git rebase -i
) の使い方
git rebase
の基本的な機能は、あるブランチのコミット群を別のブランチの先端に積み直すことでした。これに対し、git rebase -i
コマンドは、この「積み直し」の過程をインタラクティブに(対話形式で)行うための強力な機能です。これにより、単に基点を変更するだけでなく、rebase対象のコミット群に対して様々な編集を行うことができます。
インタラクティブRebaseは、ローカルで作業しているブランチのコミット履歴を、他の開発者に見せる前にきれいに整理したい場合に非常によく使われます。例えば、機能開発の途中で「〇〇を修正」「デバッグプリント追加」「やっぱり〇〇に戻す」のような小さなコミットがたくさんできてしまった場合に、それらを一つにまとめたり、不要なコミットを削除したりすることで、より分かりやすく意味のあるコミット履歴を作成することができます。
インタラクティブRebaseでできること
git rebase -i <base_commit>
コマンドを実行すると、Gitは指定した<base_commit>
以降(現在のブランチのHEADまで)のコミットリストをエディタで表示します。このリストを編集することで、表示されたコミットに対して様々な操作を行うことができます。
表示されるエディタの内容は、通常以下のような形式になっています。
“`
pick a1b2c3d コミット1のメッセージ
pick e4f5g6h コミット2のメッセージ
pick i7j8k9l コミット3のメッセージ
Rebase a1b2c3d..HEAD onto some_base_commit
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) after cherry-picking
b, break = stop here (to break a series of commits)
d, drop = discard commit
l, label
t, tag = tag the commit
m, merge [-C | -c ]
These lines can be reordered; 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.
Note that empty commits are commented out
“`
リストの各行は [コマンド] [コミットハッシュ] [コミットメッセージ]
の形式になっており、デフォルトのコマンドは pick
です。コメントセクションに、使用できるコマンドの説明が書かれています。
主なコマンドとその機能は以下の通りです。
p
,pick <commit>
: そのコミットをそのまま使用します。リストの順番を入れ替えることで、コミットの適用順序を変更できます。r
,reword <commit>
: そのコミットをそのまま使用しますが、rebaseの適用時にコミットメッセージを編集できます。コミットメッセージのtypo修正や、より適切なメッセージへの変更に使います。e
,edit <commit>
: そのコミットを適用した後、rebase処理を一時停止します。ここで追加の変更を行ったり(例: 忘れ物に気づいて修正)、前後のコミットと内容を統合したり、コミットを分割したりできます。一時停止中に変更を加えてgit add
し、git commit --amend
で前のコミットに統合した後、git rebase --continue
で再開します。s
,squash <commit>
: そのコミットを直前のコミットと結合(squash)します。rebaseの適用時に、結合されたコミット全体の新しいコミットメッセージを作成するよう求められます。複数の小さな修正コミットを一つの論理的なコミットにまとめたい場合に使います。f
,fixup <commit>
:squash
と似ていますが、結合されるコミット(fixup
を指定したコミット)のコミットメッセージを破棄し、直前のコミットのメッセージをそのまま使用します。例えば、「〇〇修正」「さらに〇〇修正」のように、前のコミットの単なる追記や微調整であるコミットをまとめたいが、それぞれの細かいメッセージは不要な場合に便利です。d
,drop <commit>
: そのコミットを履歴から完全に削除します。不要になったコミットを消したい場合に使います。
インタラクティブRebaseの使い方の手順は以下のようになります。
- rebaseの対象としたいブランチにいることを確認します。
git rebase -i <base_commit>
またはgit rebase -i <branch_name>
を実行します。<base_commit>
には、rebaseの「基点」としたいコミットのハッシュ値や、HEAD~N
(HEADからN個前のコミット)のような指定方法を使います。例えば、直近3つのコミットを編集したい場合はgit rebase -i HEAD~3
とします。<branch_name>
を指定した場合(例:git rebase -i main
)、現在のブランチと指定したブランチ(main
)の共通の祖先コミットから、現在のブランチのHEADまでのコミットが対象になります。これは、git rebase main
を実行した際にrebase対象となるコミット群をインタラクティブに操作できる、と考えると分かりやすいでしょう。
- エディタが開いたら、表示されたコミットリストに対して、やりたい操作のコマンド(
pick
,reword
,squash
など)を各行の先頭に記述します。行の順番を並べ替えることで、コミットの適用順序を変更することもできます。#
で始まる行はコメントとして無視されます。 - 編集が終わったらエディタを保存して閉じます。
- Gitは、編集したリストの内容に従って、コミットを一つずつ適用していきます。
reword
を指定した場合は、そのコミットの新しいメッセージを入力するエディタが開きます。squash
やfixup
で複数のコミットを結合した場合は、結合されたコミット全体の新しいメッセージを入力するエディタが開きます(fixup
の場合はメッセージ入力なし)。edit
を指定した場合は、rebase処理が一時停止し、シェルに戻ります。必要な作業(ファイルの修正、git add
、git commit --amend
など)を行った後、git rebase --continue
で再開します。rebaseを中止したい場合はgit rebase --abort
を使います。- 競合が発生した場合は、通常通り競合を解消し、
git add
した後、git rebase --continue
で続行します。 - すべての処理が完了すると、rebase後の新しいコミット履歴が作成されます。
インタラクティブRebaseの具体的な使用例:コミットの結合 (squash
/fixup
)
例えば、以下のようなコミット履歴のfeature
ブランチがあるとします。
...--A--B--C--D--E (featureブランチ)
^
mainブランチから分岐
ここで、C
が「〇〇機能追加」、D
が「〇〇機能のバグ修正」、E
が「〇〇機能の微調整」というコミットだとします。これらをまとめて、「〇〇機能を実装」という一つの論理的なコミットにしたい場合、インタラクティブRebaseのsquash
コマンドを使います。
C
, D
, E
の3つのコミットを対象とするので、git rebase -i HEAD~3
を実行します(HEAD~3 は E, D, C の3つのコミットを指します)。
エディタには以下のように表示されます。
“`
pick a1b2c3d コミットCのメッセージ
pick e4f5g6h コミットDのメッセージ
pick i7j8k9l コミットEのメッセージ
… (コメント省略)
“`
これを以下のように編集します。最初のコミット(C
)は pick
のままにし、それに続く結合したいコミット(D
とE
)を squash
または fixup
に変更します。
“`
pick a1b2c3d コミットCのメッセージ
squash e4f5g6h コミットDのメッセージ
fixup i7j8k9l コミットEのメッセージ
… (コメント省略)
“`
この編集内容の意味は、「コミットC
を適用し、その変更内容にコミットD
の変更内容を結合(squash)し、さらにコミットE
の変更内容をメッセージなしで結合(fixup)する」となります。
エディタを保存して閉じると、GitはまずコミットC
を適用し、次にコミットD
、コミットE
の順で変更を適用し、それらを一つの新しいコミットとしてまとめようとします。
squash
を指定したコミットがあるため、Gitは結合されたコミット全体の新しいコミットメッセージを入力するエディタを開きます。デフォルトでは、結合対象となったコミットのメッセージがリストアップされます。
“`
This is a combination of 3 commits.
This is the 1st commit message:
コミットCのメッセージ
This is the 2nd commit message:
コミットDのメッセージ
This is the 3rd commit message:
コミットEのメッセージ
Please enter the commit message for your changes. Lines starting
with ‘#’ will be ignored, and an empty message aborts the commit.
…
“`
ここで不要なメッセージを削除したり、新しいメッセージを記述したりして、最終的なコミットメッセージを作成します。例えば、「〇〇機能を実装」というメッセージに変更します。
“`
〇〇機能を実装
… (コメント省略)
“`
エディタを保存して閉じると、rebase処理が完了し、元の3つのコミット(C
, D
, E
)は新しい一つのコミット(CDE'
)に置き換わります。
...--A--B--CDE' (featureブランチ)
このように、インタラクティブRebaseを使うことで、煩雑だったローカルコミット履歴を、より分かりやすく論理的な単位に整理することができます。
注意点
インタラクティブRebaseも通常のrebase
と同様に履歴を書き換える操作です。したがって、リモートにプッシュ済みのコミットに対してインタラクティブRebaseを行うことは、原則として避けるべきです。もしプッシュ済みのコミットに対して行ってしまった場合、その後リモートにプッシュするには強制プッシュが必要となり、他の開発者に影響を与えるリスクがあります。インタラクティブRebaseは、あくまでローカルの、まだ誰もフェッチしたりクローンしたりしていないコミットに対して行うためのツールと考えるのが安全です。
Rebase利用上の注意点
rebase
は非常に強力なツールですが、その「履歴を書き換える」という特性ゆえに、使い方を間違えるとチーム開発において大きな問題を引き起こす可能性があります。特に、共有リポジトリにプッシュ済みのコミットに対してrebase
を行うことは、最大の注意点であり、多くのチームでは禁止されています。
リモートにプッシュ済みのコミットに対するrebase
の危険性
あなたがfeature
ブランチで作業し、一度そのブランチをリモートリポジトリ(例: origin
)にプッシュしたとします。
ローカル: ...--A--B--C (featureブランチ)
リモート: ...--A--B--C (origin/featureブランチ)
その後、main
ブランチに新しいコミットが追加されたため、あなたのローカルのfeature
ブランチを最新のmain
に対してrebase
したとします。
ローカル: ...--A--B--D--E (mainブランチ)
\
C' (rebase後のfeatureブランチ)
ここで、コミットC
はrebase
によってコミットC'
という新しいコミットに置き換わりました。コミットC
とコミットC'
は内容としては同じですが、親コミットが異なるため、Gitにとっては全く別のコミットです。
この状態で、あなたのローカルのfeature
ブランチをリモートのorigin/feature
ブランチにプッシュしようとするとどうなるでしょうか?
bash
git push origin feature
Gitはプッシュを拒否します。なぜなら、リモートのorigin/feature
の先端はコミットC
であるのに対し、あなたのローカルのfeature
の先端はコミットC'
であり、履歴の繋がりが異なるため、単純な追加プッシュができないからです。「ローカルの履歴がリモートの履歴と異なるため、まずリモートの変更を取り込んでください」という旨のメッセージが表示されます。
しかし、ここでgit pull origin feature
を実行しても問題は解決しません。Gitは通常、マージまたはrebaseによって履歴の差異を解消しようとしますが、今回はあなたがrebase
で履歴を書き換えてしまったために、リモートの履歴とローカルの履歴が互いに追跡できない状態になっています。git pull
はデフォルトではfetch
とmerge
を組み合わせた動作をするため、もし強行すると、ローカルのC'
とリモートのC
をマージしようとして、マージコミットが発生し、さらに履歴が複雑になる可能性があります。
このような状況になった場合、ローカルの履歴をリモートの履歴で上書きするか、あるいはローカルの履歴でリモートの履歴を上書きするかの選択を迫られます。
強制プッシュ (git push --force
or git push --force-with-lease
)
あなたのローカルのrebase
後の履歴(C'
)でリモートの履歴(C
)を上書きしたい場合は、強制プッシュを行う必要があります。
bash
git push --force origin feature
または、より安全な--force-with-lease
オプションを使います。
bash
git push --force-with-lease origin feature
--force
はリモートの履歴を問答無用でローカルの履歴で上書きしますが、--force-with-lease
は、リモートブランチが最後に自分がフェッチした時の状態から変わっていないかを確認し、変わっていなければプッシュ、変わっていれば(他の誰かがその間にプッシュしたということなので)プッシュを中止します。これにより、他の開発者の作業を誤って上書きしてしまうリスクを軽減できます。
しかし、それでも強制プッシュは危険です。
もし他の開発者が、あなたがrebase
する前のorigin/feature
(コミットC
を含む履歴)をすでにフェッチまたはクローンしていて、その上に自分の作業(例えばコミットF
)を積み重ねていたとします。
他の開発者: ...--A--B--C (feature) -- F (他の人のコミット)
あなたがrebase
して強制プッシュした結果、リモートのorigin/feature
の履歴がC'
に書き換わったとします。
リモート: ...--A--B--D--E (main)
\
C' (origin/feature)
この状態で、他の開発者がリモートの最新状態を取り込もうとしてgit pull origin feature
を実行すると、GitはローカルのコミットC
とリモートのコミットC'
が異なる履歴の分岐であると判断し、rebase
を試みようとします。しかし、コミットC
の内容はすでにコミットC'
としてリモートに取り込まれており、その上にコミットF
が乗っています。GitはコミットF
をコミットC'
の上にrebaseしようとしますが、コミットF
は元々コミットC
の上に作成されたものなので、このrebaseはうまくいかない可能性があります。たとえrebaseできたとしても、他の開発者は自分のブランチの履歴が意図せず書き換わってしまうことになり、混乱を招きます。
特に、複数の開発者が同じ共有ブランチ(たとえそれが一時的な機能ブランチであっても)で並行して作業している場合や、そのブランチのコミットがすでに他のブランチに取り込まれている可能性がある場合は、絶対にプッシュ済みのコミットに対してrebase
を行ってはいけません。
チーム開発におけるrebase
のルール決め
rebase
の強力さと危険性を考慮し、多くのチームでは以下のようなルールを定めています。
- 自分のローカルの作業ブランチ内で、まだ誰とも共有していないコミットに対してのみ
rebase
(特にインタラクティブRebase)を使用して良い。 これにより、プルリクエストを出す前に履歴をきれいに整理することができます。 - 一度リモートにプッシュしたコミットに対しては、原則として
rebase
を行わない。 代わりにmerge
を使用するか、修正が必要な場合は新しいコミットを追加するなどで対応する。 - 共有ブランチ(
main
,develop
など)に対しては、履歴の安定性を保つためにrebase
を使用せず、必ずmerge --no-ff
で統合する。 - 緊急時など、やむを得ずプッシュ済みのコミットに対して
rebase
を行い、強制プッシュが必要になった場合は、必ずチームメンバーに事前に周知し、彼らが各自のローカルリポジトリを適切に修正できるよう指示する。 (git pull --rebase
やgit reset --hard
など、状況に応じた対応が必要です)
これらのルールはあくまで一般的なものであり、プロジェクトやチームの文化によって最適な方法は異なります。重要なのは、チーム全体でmerge
とrebase
の使い分けについて認識を合わせ、履歴管理のポリシーを明確にしておくことです。
rebase
が失敗した場合の対処法
rebase
中に競合解消がうまくいかなかったり、意図しない結果になったりして、元の状態に戻したい場合があります。このような場合は、慌てずに以下のコマンドを使います。
git rebase --abort
: rebase処理を中止し、rebaseを開始する前の状態に戻します。競合解消がうまくいかない場合や、rebase自体を取りやめたい場合に安全に戻れるコマンドです。git reflog
:reflog
(Reference Log)は、HEADやブランチ、タグなどが過去にどのコミットを指していたかの履歴を記録しています。rebaseによって履歴が書き換わってしまっても、reflog
を使えばrebase開始前のコミットや、rebase途中で一時的に存在したコミットなどを見つけることができます。もしrebaseで誤った操作をしてしまい、--abort
もできないような状況になったとしても、reflog
で目的の時点のコミットを見つけ出し、git reset --hard <commit_hash>
コマンドでその時点の状態に戻ることで、ほとんどの場合復旧可能です。reflog
はGitの強力なセーフティネットです。
実践的なRebaseワークフロー例
ここでは、一般的な開発フローの中でrebase
がどのように活用できるか、具体的なワークフロー例を見てみましょう。
シナリオ: あなたはmain
ブランチから新しい機能開発のためのfeature/add-user-profile
ブランチを切って作業を開始しました。作業の途中で、他の開発者がmain
ブランチに重要な変更をプッシュしました。あなたの機能開発は完了し、プルリクエストを作成してmain
に取り込みたいと考えています。
-
作業開始:
“`bash
# mainブランチの最新を取得
git checkout main
git pull origin main機能開発ブランチを作成・移動
git checkout -b feature/add-user-profile
ここで開発を開始し、いくつかのコミットを行います。
…–A–B (main)
\
C–D–E (feature/add-user-profile)
“` -
mainブランチに新しい変更が追加される:
他の開発者がmain
にコミットを追加し、プッシュしました。
...--A--B--F--G (main)
\
C--D--E (feature/add-user-profile) -
ローカルのfeatureブランチを最新のmainに追従させる (Rebase):
プルリクエストを作成する前に、自分のブランチが最新のmain
に対して問題なく動作するかを確認したい、かつ履歴をきれいに保ちたいと考えました。ここでrebase
を使います。
“`bash
# まず最新のmainを取得
git fetch origin mainfeatureブランチに移動し、mainの最新状態に対してrebase
git checkout feature/add-user-profile
git rebase origin/main
Rebase処理が開始されます。もし競合が発生したら、その都度解消し、`git add .` -> `git rebase --continue` を繰り返します。
Rebaseが成功すると、履歴は以下のようになります。
…–A–B–F–G (main)
\
C’–D’–E’ (feature/add-user-profile)
``
C’
これで、あなたの機能開発コミット(,
D’,
E’)は最新の
main(
G)の上に積み直され、まるで最初から
main`の最新状態から開発を開始したかのような線形の履歴になりました。 -
ローカルのコミット履歴を整理する (Interactive Rebase):
機能開発中に、細かい修正や試行錯誤のコミットがたくさんできてしまったとします。プルリクエストをレビューしやすくするために、これらのコミットを論理的な単位に整理したいと考えました。インタラクティブRebaseを使います。
例えば、C'
,D'
,E'
の3つのコミットを「ユーザープロフィール機能の実装」という一つのコミットにまとめたいとします。
bash
# 直近3つのコミットを対象にインタラクティブRebaseを開始
git rebase -i HEAD~3
エディタが開くので、一番上のコミットをpick
にし、それに続くコミットをsquash
またはfixup
に変更します。
pick <ハッシュ C'> C'のメッセージ
squash <ハッシュ D'> D'のメッセージ
fixup <ハッシュ E'> E'のメッセージ
エディタを保存して閉じると、Gitはこれらのコミットを結合し、新しいコミットメッセージを入力するよう求められます。ここで「ユーザープロフィール機能の実装」というメッセージを入力します。
Rebaseが完了すると、履歴は以下のようになります。
...--A--B--F--G (main)
\
CDE'' (feature/add-user-profile)
元の3つのコミットは、一つのきれいなコミットにまとまりました。(コミットハッシュが再度変わるのでCDE''
としました) -
リモートへのプッシュとプルリクエストの作成:
これでローカルのfeature/add-user-profile
ブランチの履歴は整理され、最新のmain
の変更も取り込まれました。このブランチをリモートにプッシュして、プルリクエストを作成します。
注意: ステップ3やステップ4でRebaseを行った場合、ローカルのブランチの履歴はリモートのorigin/feature/add-user-profile
ブランチの履歴から書き換わっています。したがって、通常のgit push
は拒否されます。
ここで強制プッシュが必要になります。他の開発者がこの機能開発ブランチをベースに作業している可能性は低いと考えられますが、念のため--force-with-lease
を使うのが安全です。
bash
git push --force-with-lease origin feature/add-user-profile
これにより、リモートのfeature/add-user-profile
ブランチの履歴があなたのローカルの履歴で上書きされます。その後、GitHubやGitLabなどのインターフェースからプルリクエストを作成します。 -
メインブランチへの統合:
プルリクエストがレビューされ、承認されたら、main
ブランチに統合します。チームのポリシーに従いますが、通常はGitHub/GitLabの機能を使ってmerge --no-ff
またはSquash and Merge
で統合します。ローカルでrebaseして履歴をきれいにしている場合、Squash and Merge
を選択すると、あなたのfeatureブランチ全体がmainに対して一つのコミットとして取り込まれ、さらにきれいなmainブランチの履歴を保つことができます。(ただし、元のfeatureブランチの個々のコミット履歴はmain側には残りません。これはどちらを重視するかによります)
このワークフローは、rebase
を主にローカルでの作業ブランチの整理と最新化に使う、という考え方に基づいています。これにより、メインブランチの履歴は--no-ff
マージによって安定性を保ちつつ、フィーチャーブランチの履歴はrebase -i
によってクリーンに保つ、というバランスの良い運用が可能になります。
まとめ
この記事では、Gitのrebase
コマンドの仕組みと使い方、そしてmerge
コマンドとの違いについて詳しく解説しました。
git merge
は、異なるブランチの履歴を「結合した事実」として記録し、比較的安全で履歴の追跡が容易ですが、マージコミットが増えるとグラフが複雑になりがちです。
一方、git rebase
は、ブランチの基点を変更することで履歴を線形に「再構築」します。これにより、きれいな一直線のコミットグラフを作成でき、git rebase -i
を使えばコミットを整理することも可能です。しかし、履歴を書き換えるという特性から、特に共有リポジトリにプッシュ済みのコミットに対する使用は危険を伴います。
両者の使い分けは、以下の原則に基づくと良いでしょう。
- ローカルで作業中の、まだ共有していないブランチの整理や最新化には、
rebase
(特にインタラクティブRebase)が非常に有効です。 - チーム全体で共有しているメインブランチへの統合には、履歴の安定性を保ち、統合の事実を明確に残すために、多くの場合
merge --no-ff
が推奨されます。
rebase
は強力なツールですが、その「履歴書き換え」のリスクを常に意識し、特にチーム開発においては、リモートにプッシュ済みのコミットには使用しない、というルールを徹底することが重要です。もし誤って使用してしまった場合は、git reflog
を使った復旧方法を覚えておくと役立ちます。
Gitのrebase
は、適切に使いこなせば、あなたのGitワークフローを洗練させ、より管理しやすくきれいなコミット履歴を保つのに役立ちます。最初はその挙動に戸惑うことがあるかもしれませんが、実際に手を動かして様々なシナリオで試してみることで、徐々に理解が深まるはずです。
さあ、今日からあなたのGitワークフローにrebase
を取り入れて、より快適な開発体験を目指しましょう!
用語集
- コミット (Commit): プロジェクトのファイルの状態を記録した特定の時点のスナップショット。一意のハッシュ値と親コミットへの参照を持つ。
- ブランチ (Branch): コミットの連なりの先端を指すポインター。開発ラインの分岐を示す。
- コミットグラフ (Commit Graph): コミットとブランチの関係を視覚的に示したもの。
- Mergeコミット (Merge Commit): 2つ以上のブランチの変更内容を統合した際に作成される新しいコミット。複数の親コミットを持つ。
- Fast-forward merge: マージ対象ブランチの先端まで、マージ先ブランチのポインターを移動させるだけのマージ方法。マージコミットは作成されない。
- No-fast-forward merge (
--no-ff
): Fast-forward可能な場合でも、必ずマージコミットを作成するマージ方法。ブランチが統合された事実を履歴に残す。 - Rebase: あるブランチの基点を別のブランチの先端に変更し、そのブランチのコミット群を積み直す操作。履歴が書き換えられる。
- Interactive Rebase (
git rebase -i
): Rebaseの過程で、コミットの並べ替え、編集、結合、削除などを対話形式で行える機能。 - 競合 (Conflict): 異なるブランチ間で同じファイルの同じ箇所などが変更され、Gitが自動的に統合できない状態。手動での解消が必要。
- HEAD: 現在チェックアウトしているブランチの最新コミット、または特定のコミットを指すポインター。
- Origin: クローン元など、リモートリポジトリのデフォルトの名前。