【入門】Git rebaseの使い方を徹底解説!Mergeとの違いも

はい、承知いたしました。【入門】Git rebaseの使い方を徹底解説!Mergeとの違いも、というタイトルで約5000語の詳細な解説記事を作成します。


【入門】Git rebaseの使い方を徹底解説!Mergeとの違いも

Gitを使った開発は、複数人で並行して作業を進める上で不可欠な技術です。その中でも、ブランチの管理や変更履歴の整理は、プロジェクトを円滑に進める上で非常に重要になります。Gitには、異なるブランチの変更内容を統合する方法がいくつか存在しますが、代表的なものとしてgit mergegit rebaseがあります。

多くの入門書やチュートリアルでは、まずmergeの使い方を学ぶことが多いでしょう。mergeはシンプルで分かりやすい統合方法ですが、使い方によってはコミットグラフが複雑になりがちという側面も持っています。一方でrebaseは、より線形的な、つまり一直線なきれいなコミット履歴を作成するのに役立ちますが、その仕組みが少し分かりづらく、また使い方を間違えると履歴を書き換えてしまうため慎重な扱いが必要です。

「Rebaseは難しい」「怖い」といったイメージをお持ちの方もいるかもしれません。しかし、rebaseの仕組みと使いどころを正しく理解すれば、あなたのGitワークフローを劇的に改善し、より見やすく管理しやすいコミット履歴を作り出す強力な武器となります。

この記事では、Gitを使った開発にある程度慣れてきた入門者の方を対象に、rebaseの基本的な使い方から、mergeとの違い、そして実践的なシナリオでの活用方法までを徹底的に解説します。特に、なぜrebaseが履歴をきれいにするのか、mergeとはどう異なるのかを、図解(文章で図解を表現します)を交えながら分かりやすく説明することに重点を置きます。

この記事を読むことで、あなたは以下のことを習得できます。

  • Gitにおける変更履歴の考え方と、ブランチがどのように機能するかを再確認する。
  • git mergeの仕組みと、その利点・欠点を理解する。
  • git rebaseの仕組みと、その利点・欠点を理解する。
  • mergerebaseの決定的な違いと、それぞれの適切な使い分けができるようになる。
  • git rebase -i(インタラクティブRebase)を使って、コミット履歴を編集・整理する方法を学ぶ。
  • rebaseを使う上での注意点、特に履歴の書き換えに関するリスクと対処法を理解する。
  • 実際の開発現場でどのようにrebaseを活用できるかのイメージを持つ。

さあ、Git rebaseの世界への扉を開けて、あなたのGitスキルを次のレベルへと引き上げましょう!

Gitの基本的な復習:なぜ履歴管理が重要なのか?

rebasemergeの話に入る前に、まずはGitの基本的な概念、特に「コミット」「ブランチ」「コミットグラフ」について簡単に復習しましょう。これらの概念をしっかり理解しておくことが、rebasemergeの仕組みを把握する上で非常に重要です。

コミット (Commit)

Gitにおけるコミットは、プロジェクトのファイルの状態を特定の時点(スナップショット)で記録したものです。各コミットには、誰が、いつ、どのような変更を行ったのかを示す情報(コミットメッセージ、作成者、タイムスタンプ)が含まれます。また、それぞれのコミットは一意の識別子(ハッシュ値、例: a1b2c3d4) を持ち、その親コミットへの参照を持っています。これにより、コミットは履歴としてチェーン状につながっていきます。

[コミット D] <- [コミット C] <- [コミット B] <- [コミット A]

このチェーンが、プロジェクトの変更履歴そのものです。

ブランチ (Branch)

ブランチは、これらのコミットの連なりの上を指す「ポインター」のようなものです。デフォルトではmain(またはmaster)という名前のブランチが存在します。新しい機能開発やバグ修正を行う際には、通常、既存のブランチ(例えばmain)から新しいブランチを作成します。

新しいブランチを作成すると、それは元のブランチが指していた最新のコミットを指すようになります。その新しいブランチでコミットを行うと、新しいコミットが作成され、そのブランチはその新しいコミットを指すように移動します。元のブランチは元のコミットを指したままなので、ここから履歴が分岐していくことになります。

[コミット E] <- [featureブランチ] (新しいコミットを追加)
/
[コミット D] <- [コミット C] <- [コミット B] <- [コミット A] <- [mainブランチ] (元のまま)

ブランチを使うことで、複数の開発者がそれぞれ独立した作業を並行して進めることができます。そして、作業が完了したら、そのブランチの変更内容を他のブランチ(例えばmainブランチ)に取り込むことになります。この「取り込む」作業が、mergerebaseで行われます。

コミットグラフ (Commit Graph)

コミットグラフは、これらのコミットとブランチの関係を視覚的に表現したものです。各ノードがコミットを表し、エッジ(矢印)が親コミットへの参照を示します。ブランチは特定のコミットを指すラベルとして表示されます。Gitにおける履歴の統合操作(mergerebase)は、このコミットグラフの形状に直接影響を与えます。

なぜ履歴管理が重要なのでしょうか?それは、プロジェクトの「進化の過程」を正確に記録し、追跡可能にするためです。問題が発生した際に、いつ、誰が、どのような変更によってその問題が引き起こされたのかを特定したり、過去の特定の状態に戻したりすることが容易になります。また、チームメンバーが互いの変更を理解し、協力して作業を進める上でも、整理された分かりやすい履歴は非常に役立ちます。

さて、この変更履歴を統合する方法として、まずは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には、大きく分けて二つのパターンがあります。

  1. 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ブランチが存在していたという履歴は(ブランチ自体を削除しない限り)視覚的に残りにくいという側面もあります。
  2. 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は以下の手順で処理を行います。

  1. 現在のブランチ(例: feature)と、rebase先のブランチ(例: main)の共通の祖先コミットを見つけます。
  2. 現在のブランチの、共通の祖先コミットよりも新しいコミット(例: コミット Cコミット D)を一時的な場所に退避させます。(これらのコミットを「再生 (replay)」すると表現されることもあります)
  3. 現在のブランチを、rebase先のブランチの先端(例: コミット F)まで移動させます。つまり、ブランチの基点が変更されます。
  4. 退避させておいたコミットを、新しい基点の上から一つずつ順番に「再生」していきます。Gitは、各コミットで行われた変更内容を、新しい基点の状態に適用しようとします。
  5. この「再生」の過程で、元のコミットと同じ内容新しいコミット新しいハッシュ値で作成されます。元のコミットは参照されなくなり、最終的にはGitのガベージコレクションによって削除されます。(これが「履歴の書き換え」と言われる所以です)
  6. すべてのコミットの再生が成功すると、現在のブランチは再構築された新しいコミット群の先端を指すようになります。

上記の例で 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処理を一時停止し、競合が発生したファイルを示します。

競合が発生したら、以下の手順で解消します。

  1. git statusで競合ファイルを確認します。
  2. 競合しているファイルをエディタで開き、手動で競合マーカー(<<<<<<<, =======, >>>>>>>など)を修正し、 desired な状態にします。
  3. 競合を解消したファイルをステージングエリアに追加します。git add <解決したファイル名> または git add .
  4. 競合解消が終わったら、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: 違いと使い分け

さて、mergerebase、それぞれの基本的な仕組みと利点・欠点が理解できたところで、両者の違いを明確にし、どのような場合にどちらを使うべきかを考えてみましょう。

決定的な違い

最も大きな違いは、履歴の取り扱い方です。

  • 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を行うことは、原則として避けるべきです。

使用シナリオによる使い分け

これらの違いを踏まえると、mergerebaseは以下のように使い分けることが推奨されます。

  1. ローカルでの作業ブランチの整理:

    • あなたが一人で作業している、あるいはまだ誰とも共有していないローカルの機能開発ブランチがあるとします。
    • このブランチを最新のmainブランチの状態に追いつかせたい場合。
    • このブランチでのコミットが複数あり、それらをよりきれいな形でmainの履歴に続けたい場合。
    • rebaseが適しています。 git fetch origin main で最新のmainを取得した後、git rebase origin/main(またはローカルの追跡ブランチ名)を実行します。これにより、あなたの作業ブランチのコミットが、最新のmainの上にきれいに積み直され、プルリクエストなどを送る前にベースブランチとの差分を最小限にできます。
    • さらに、後述するインタラクティブRebaseを使って、ローカルの作業ブランチ内のコミットを整理(結合、編集、削除など)することもできます。
  2. 共有ブランチ(例: develop, main)への取り込み:

    • 機能開発が完了し、チーム全体で共有しているメインブランチ(maindevelopなど)に変更を取り込みたい場合。
    • この場合は、一般的にmergeを使用することが推奨されます。
    • 特に、git merge --no-ff <feature_branch>とすることで、機能開発ブランチが完了し、メインブランチにマージされた、という明確なイベントを履歴に残すことができます。これは、後から履歴を追跡したり、リリース履歴を確認したりする上で非常に役立ちます。
    • なぜ共有ブランチへのrebaseは避けるべきか? メインブランチに対して他のブランチをrebaseすると、メインブランチ自体の履歴が書き換わることになります。これは、そのメインブランチをクローンまたはフェッチしている他のすべての開発者のリポジトリの履歴との間に矛盾を生じさせます。他の開発者は、強制プッシュや複雑な履歴操作を行わないと、自分のローカルリポジトリをリモートの最新状態に追いつかせることができなくなります。
  3. 他の人の作業との統合(ローカルでの最新化):

    • あなたが作業しているブランチがあり、他の開発者がすでにメインブランチ(例: main)に新しい変更をプッシュしたとします。
    • あなたの作業を続ける前に、最新のmainの状態を自分のブランチに取り込みたい場合。
    • git merge mainを実行することも可能ですが、これによりあなたのブランチとmainの間にマージコミットが作成され、履歴が分岐する可能性があります。
    • git rebase mainを実行すると、あなたのローカルの作業ブランチの基点が最新のmainに移り、履歴が線形になります。これは、プルリクエストを出す前などに、自分の変更が最新のコードベースに対して問題なく動作するかを確認するのに役立ちます。多くの開発者は、ローカルでの作業中にはrebaseを使って最新の変更を取り込むことを好みます。

一般的な推奨ワークフロー例:

  1. 作業を開始する前に、最新のmainブランチから新しいfeatureブランチを作成する。
    bash
    git checkout main
    git pull origin main # 最新のmainを取得
    git checkout -b my-feature-branch
  2. my-feature-branchで開発を進め、適宜コミットを行う。
  3. 開発中に他の開発者がmainにプッシュした場合、自分のブランチを最新化するためにrebaseを使用する(共有する前なら安全)。
    bash
    git fetch origin main
    git rebase origin/main # または git rebase main (ローカルのmainが最新なら)

    この際、競合が発生したら解消し、git rebase --continueで続ける。
  4. 機能開発が完了し、プルリクエストを作成する準備ができた場合。
    • ローカルの作業ブランチのコミット履歴を整理したい場合は、後述のインタラクティブRebase (git rebase -i) を使ってコミットをまとめたり、メッセージを編集したりする。
    • 再度、最終確認として最新のmainに対してrebaseを行う。
      bash
      git fetch origin main
      git rebase origin/main

      注意: ここでrebaseしたブランチをリモートにプッシュする場合は、履歴が書き換わっているため通常の方法ではプッシュできません。後述の注意点をよく理解してから行います。
  5. プルリクエスト/マージリクエストを作成し、レビューを経てメインブランチに統合する。
    • チームのポリシーにもよるが、多くの場合、プルリクエストの受け入れ時に、GitHubやGitLabなどのホスティングサービスの機能を使って、メインブランチ側から merge --no-ff または squash merge で取り込むことが多いです。これにより、メインブランチの履歴が保護され、かつ機能開発の完了を履歴に残すことができます。
    • ローカルでrebaseしたブランチを、リモートの同じブランチにプッシュしてPR/MRを作成する場合、履歴が書き換わっているためgit pushが拒否されます。この場合、git push --force-with-lease(またはgit push --force)が必要になりますが、これは危険を伴うため、そのリスクを理解している場合にのみ行います。

まとめると、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の使い方の手順は以下のようになります。

  1. rebaseの対象としたいブランチにいることを確認します。
  2. 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対象となるコミット群をインタラクティブに操作できる、と考えると分かりやすいでしょう。
  3. エディタが開いたら、表示されたコミットリストに対して、やりたい操作のコマンド(pick, reword, squashなど)を各行の先頭に記述します。行の順番を並べ替えることで、コミットの適用順序を変更することもできます。# で始まる行はコメントとして無視されます。
  4. 編集が終わったらエディタを保存して閉じます。
  5. Gitは、編集したリストの内容に従って、コミットを一つずつ適用していきます。
  6. rewordを指定した場合は、そのコミットの新しいメッセージを入力するエディタが開きます。
  7. squashfixupで複数のコミットを結合した場合は、結合されたコミット全体の新しいメッセージを入力するエディタが開きます(fixupの場合はメッセージ入力なし)。
  8. editを指定した場合は、rebase処理が一時停止し、シェルに戻ります。必要な作業(ファイルの修正、git addgit commit --amendなど)を行った後、git rebase --continueで再開します。rebaseを中止したい場合はgit rebase --abortを使います。
  9. 競合が発生した場合は、通常通り競合を解消し、git addした後、git rebase --continueで続行します。
  10. すべての処理が完了すると、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 のままにし、それに続く結合したいコミット(DE)を 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ブランチ)

ここで、コミットCrebaseによってコミット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はデフォルトではfetchmergeを組み合わせた動作をするため、もし強行すると、ローカルの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 --rebasegit reset --hard など、状況に応じた対応が必要です)

これらのルールはあくまで一般的なものであり、プロジェクトやチームの文化によって最適な方法は異なります。重要なのは、チーム全体でmergerebaseの使い分けについて認識を合わせ、履歴管理のポリシーを明確にしておくことです。

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に取り込みたいと考えています。

  1. 作業開始:
    “`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)
    “`

  2. mainブランチに新しい変更が追加される:
    他の開発者がmainにコミットを追加し、プッシュしました。
    ...--A--B--F--G (main)
    \
    C--D--E (feature/add-user-profile)

  3. ローカルのfeatureブランチを最新のmainに追従させる (Rebase):
    プルリクエストを作成する前に、自分のブランチが最新のmainに対して問題なく動作するかを確認したい、かつ履歴をきれいに保ちたいと考えました。ここでrebaseを使います。
    “`bash
    # まず最新のmainを取得
    git fetch origin main

    featureブランチに移動し、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’)は最新のmainG)の上に積み直され、まるで最初からmain`の最新状態から開発を開始したかのような線形の履歴になりました。

  4. ローカルのコミット履歴を整理する (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''としました)

  5. リモートへのプッシュとプルリクエストの作成:
    これでローカルの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などのインターフェースからプルリクエストを作成します。

  6. メインブランチへの統合:
    プルリクエストがレビューされ、承認されたら、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: クローン元など、リモートリポジトリのデフォルトの名前。

コメントする

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

上部へスクロール