Git rebase –onto とは?基本から理解する詳細な使い方
はじめに:Gitにおける変更履歴の管理とrebase
の役割
Gitを使った開発において、複数の変更を同時に進めるためにブランチは不可欠な機能です。そして、ブランチ間の変更を取り込んだり、変更履歴を整理したりするために、git merge
とgit rebase
という2つの主要なコマンドが使われます。
git merge
は、指定したブランチの変更を現在のブランチに取り込み、マージコミットを作成することで変更履歴を統合します。これは履歴をそのまま保持するため、追跡が容易であるというメリットがありますが、ブランチの分岐やマージのたびにマージコミットが増え、履歴が複雑になりがちです。
一方、git rebase
は、「付け替え」という意味の通り、現在のブランチのコミットを別のブランチ(または特定のコミット)の先端に移動させることで、変更履歴を直線的に保つコマンドです。これにより、ブランチの履歴がシンプルになり、後から追跡しやすくなるというメリットがあります。特に、featureブランチでの開発をmainブランチの最新状態に合わせて整理したい場合などに有効です。
git rebase
の基本的な使い方は、git rebase <base_branch>
のように、現在のブランチのコミットを<base_branch>
の先端に移動させるというものです。しかし、Gitのブランチ運用やプロジェクトの履歴は常にシンプルとは限りません。特定のコミット範囲だけを移動したい、あるいは現在のブランチではなく、全く別のコミットツリーの上にコミット群を移動させたい、といったより複雑なニーズが出てくることがあります。
このような場合に真価を発揮するのが、今回詳細に解説するgit rebase --onto
コマンドです。--onto
オプションを使用することで、どのコミットを新しいベースにするか、どのコミット範囲を移動させるかを、より柔軟かつ細かく指定できるようになります。
この記事では、まずgit rebase
の基本を簡単に復習し、その上でgit rebase --onto
が必要となる背景、基本的な構文と各引数の意味、そして様々な具体的な使用例を通して、その強力な機能を深く理解することを目指します。また、履歴を変更するコマンドであるため、使用上の注意点や、万が一の場合の対処法についても詳しく解説します。
Git rebaseの基本(復習)
git rebase <base>
コマンドは、現在のブランチ(HEAD)のコミットを、指定した<base>
ブランチ(またはコミット)の先端に「付け替える」ためのコマンドです。
例として、以下のようなコミット履歴を考えます。
A -- B -- C (main)
\
D -- E -- F (feature)
ここで、feature
ブランチにいる状態でgit rebase main
を実行すると、以下のようになります。
- Gitは、
feature
ブランチの分岐点(main
ブランチの先端であるコミットC
の一つ前の共通の祖先、この場合はコミットB
)を見つけます。 feature
ブランチ固有のコミット(D
,E
,F
)を一時的な領域に保存します。- 現在のブランチ(
feature
)のHEADを、指定した<base>
ブランチ(main
)の先端(コミットC
)に移動させます。 - 一時保存しておいたコミットを、新しいHEAD(コミット
C
)の上に順番に再適用(リプレイ)します。
このプロセスを経て、履歴は以下のようになります。
A -- B -- C (main)
\
D' -- E' -- F' (feature)
ここで、D'
, E'
, F'
は元のD
, E
, F
と同じ内容ですが、親コミットが変わったため、異なる新しいコミットIDを持つことになります。これが「コミットの付け替え」です。
rebase
のメリット:
- 履歴が直線的になり、追いやすくなります。
- マージコミットが生成されないため、コミットグラフがシンプルになります。
- featureブランチをmainブランチの最新状態に合わせて整理し、その後のmergeをfast-forward mergeにできる可能性があります。
rebase
のデメリット:
- コミットIDが変わる: 上記のように、rebaseされたコミットは新しいIDを持ちます。これは、特に共有リモートリポジトリで公開済みのコミットに対してrebaseを行うと、他の開発者の履歴と食い違いを生じさせ、混乱の原因となる可能性があります。リモートにpush済みの共有ブランチに対してrebaseを行うべきではないと言われる最大の理由です。
- コンフリクト解決の手間: 複数のコミットを再適用する際に、各コミットでコンフリクトが発生する可能性があります。その都度解決が必要になります。
基本的なgit rebase <base>
の動作は、「現在のブランチの、<base>
ブランチとの共通の祖先よりも新しい全てのコミットを、<base>
の先端に移動する」というものです。この「共通の祖先よりも新しい全てのコミット」というルールが、後述する--onto
がどのように役立つかに関わってきます。
git rebase --onto
の登場:なぜ通常のrebaseでは不十分な場合があるのか?
通常のgit rebase <base>
は、「現在のブランチ」と「<base>
」という2つの参照(ブランチやコミット)間の関係に基づいて、移動対象のコミット範囲と移動先を決定します。移動対象は「現在のブランチのHEADまでのコミットのうち、<base>
との共通の祖先よりも新しいもの」という固定的なルールで決まります。移動先は常に<base>
の先端です。
しかし、以下のようなシナリオでは、この固定的なルールでは対応できません。
- 特定のコミット範囲だけを移動したい: ブランチ全体ではなく、そのブランチに含まれる特定の連続したコミット群だけを移動したい場合があります。例えば、一つのブランチで複数の独立した機能の開発を進めてしまい、それらを後から別々のブランチに分割したい、といった状況です。通常の
rebase <base>
では、共通の祖先からHEADまでの全てのコミットが対象となってしまいます。 - 移動先のベースを特定のコミットにしたい: 通常の
rebase <base>
では、移動先は常に<base>
ブランチの最新コミットになります。しかし、特定のタグや、過去の特定のコミットの上にコミット群を移動させたい場合があります。 - ブランチが複雑な構造になっている: 複数のブランチが頻繁にマージされたり、cherry-pickされたりして、履歴が複雑になっている場合、共通の祖先からの範囲指定だけでは意図したコミット群を正確に選択できないことがあります。
これらの課題を解決し、より柔軟にコミットの付け替えを行うために導入されたのが、git rebase --onto
オプションです。
git rebase --onto
の基本的な構文
git rebase --onto
コマンドの基本的な構文は以下の通りです。
bash
git rebase --onto <new_base> <upstream_base> <topic_branch>
このコマンドは、<topic_branch>
に含まれるコミットのうち、<upstream_base>
コミットよりも新しいコミット群を、<new_base>
コミット(またはブランチ)の直後に移動させます。
各引数の意味を詳しく見ていきましょう。
-
<new_base>
:- これは、新しいベースとなるコミットまたはブランチを指定します。
- 移動対象のコミット群は、この
<new_base>
が指すコミットの直後に付け替えられます。 - 例えば、
main
ブランチの最新状態の上にコミットを移動させたい場合は、ここにmain
と指定します。特定のコミットIDの上に移動させたい場合は、そのコミットIDを指定します。
-
<upstream_base>
:- これは、移動対象のコミット範囲を指定する境界となるコミットです。
- Gitは、
<topic_branch>
のHEADまでのコミット履歴を遡り、この<upstream_base>
コミットを見つけます。 - そして、
<topic_branch>
の履歴のうち、<upstream_base>
よりも(ブランチの先端側にある)新しい全てのコミットを移動対象とします。 - 言い換えると、
<upstream_base>..<topic_branch>
の範囲に含まれるコミットが移動対象となります。(ただし、<upstream_base>
自体は含まれません) - これは、通常の
git rebase <base>
で自動的に計算される「共通の祖先」を手動で指定するようなものです。ただし、必ずしも共通の祖先である必要はなく、単なる範囲指定の始点として機能します。 - この引数は省略可能です。省略した場合、通常の
git rebase <new_base>
と同様に、現在のブランチと<new_base>
の共通の祖先が自動的に<upstream_base>
として使用されます。
-
<topic_branch>
:- これは、移動対象のコミットが含まれるブランチまたは特定のコミットを指定します。
- Gitは、この
<topic_branch>
が指すコミットから<upstream_base>
までを遡って、移動対象のコミット範囲を決定します。 - 通常は、現在作業しているブランチを指定するか、省略して現在のブランチ(HEAD)を対象とすることが多いです。別のブランチのコミットを移動させたい場合に、そのブランチ名を指定します。
- この引数も省略可能です。省略した場合、現在のブランチ(HEAD)が対象となります。
これらの引数を理解することで、git rebase --onto
が「<upstream_base>
から<topic_branch>
までのコミット群を、<new_base>
の直後に移動する」という操作であることが分かります。
さて、この構文を理解した上で、具体的な使用例を見ていきましょう。例を通じて、それぞれの引数がどのように使われるのかがより明確になるはずです。
git rebase --onto
の具体的な使い方と例
git rebase --onto
は様々なシナリオで役立ちますが、ここでは特に一般的な使い方をいくつか紹介します。
例1:特定のコミット範囲だけを別のブランチに移動する
これはrebase --onto
の最も典型的な使用例の一つです。例えば、以下のようなコミット履歴があるとします。
A -- B -- C -- D -- E (main)
\
F -- G -- H (feature)
ここで、feature
ブランチで作業しているとします(HEADはfeature
ブランチ)。通常、git rebase main
とすると、F
, G
, H
の全てのコミットがmain
の先端(コミットE
)の上に移動します。
“`bash
featureブランチにいるとして
git rebase main
“`
結果:
A -- B -- C -- D -- E (main)
\
F' -- G' -- H' (feature)
しかし、もしfeature
ブランチに含まれるコミットのうち、G
とH
だけを、develop
という別のブランチの先端(例えばコミットI
)の上に移動したい場合はどうでしょうか?履歴は以下のようになっているとします。
A -- B -- C -- D -- E (main)
\ \
F -- G -- H (feature)
\
I (develop) # developブランチの最新コミット
移動したいのはG
とH
です。移動先の新しいベースはdevelop
の先端(コミットI
)です。移動対象の範囲の始点はF
の次からなので、<upstream_base>
としてコミットF
を指定します。移動対象のコミットが含まれるブランチはfeature
です。
このシナリオにおけるコマンドは以下のようになります。現在いるブランチはfeature
でも、develop
でも構いませんが、操作対象となるのはfeature
ブランチに含まれるコミット群なので、コマンドの最後の引数(<topic_branch>
)としてfeature
を指定します。
“`bash
現在どのブランチにいてもOKだが、ここではmainにいると仮定
git rebase –onto develop F feature
“`
<new_base>
:develop
(移動先の新しいベース)<upstream_base>
:F
(移動しないコミットの境界。F
より新しいコミットを対象とする)<topic_branch>
:feature
(移動対象のコミットが含まれるブランチ)
このコマンドを実行すると、以下のステップが行われます。
- Gitは、
feature
ブランチの履歴のうち、コミットF
よりも新しいコミット(G
,H
)を特定します。 - 特定されたコミット(
G
,H
)を一時的な領域に保存します。 - コマンドの最後にブランチ名(
feature
)が指定されているため、feature
ブランチのHEADを、<new_base>
で指定されたdevelop
の先端(コミットI
)に移動させます。 - 一時保存しておいたコミット(
G
,H
)を、新しいHEAD(コミットI
)の上に順番に再適用します。
結果として、履歴は以下のようになります。
A -- B -- C -- D -- E (main)
\
F (feature) # ここでfeatureブランチは止まったまま
\
I (develop)
\
G' -- H' (feature) # featureブランチのHEADがIの上に移動し、G', H'が追加された
ポイント: <topic_branch>
をコマンドの最後に指定した場合、そのブランチのHEADが新しいベースに移動します。もし現在のブランチ(HEAD)のコミットを移動させたいが、現在のブランチのHEAD自体は動かしたくない場合は、<topic_branch>
を省略するか、別の方法を検討する必要があります(例: git checkout
で一時的に別のブランチに切り替える、あるいは git branch <new_branch> <upstream_base>
で古い時点のブランチを作成し、そちらに対して --onto
するなど)。しかし、上記の例のように特定のブランチの一部コミットを移動してそのブランチのHEADを新しい場所に付け替えたい場合は、このように<topic_branch>
を指定するのが標準的です。
例2:ブランチを分割する (Interactive Rebase との組み合わせ)
一つのブランチで複数の機能や修正を開発してしまい、後からそれらを別々のブランチに分けたい、というシナリオはよくあります。これもgit rebase --onto
、特にインタラクティブモード (-i
) と組み合わせることで効率的に実現できます。
例えば、以下のような履歴で、feature
ブランチには機能Aに関連するコミット (FA1
, FA2
) と機能Bに関連するコミット (FB1
, FB2
) が混在しているとします。
A -- B -- C (main)
\
FA1 -- FB1 -- FA2 -- FB2 (feature)
ここで、feature
ブランチから機能Aだけを含むfeature-A
ブランチと、機能Bだけを含むfeature-B
ブランチを作成したいとします。どちらの新しいブランチも、ベースはmain
の先端(コミットC
)としたいです。
手順:
-
現在のブランチ(
feature
)を保護する: 万が一に備え、現在の状態を指すタグやブランチを残しておくと安全です。あるいは、単にそのままにしておき、新しいブランチを作成後に元のfeature
ブランチは削除またはリセットすることもできます。ここでは、feature
ブランチはそのままにしておき、新しいブランチを作成・操作することにします。 -
機能A用の新しいブランチを作成し、rebase –onto -i を実行:
- 新しいブランチ
feature-A
を、元のfeature
ブランチのベースとなったコミット(ここではmain
の先端、コミットC
)から分岐させます。
bash
git checkout -b feature-A main - これで
feature-A
ブランチはコミットC
を指しています。 - 次に、元の
feature
ブランチから機能Aに関連するコミット (FA1
,FA2
) だけを、現在のfeature-A
ブランチのHEAD(コミットC
)の上に移動させます。 - 移動対象の範囲は、
feature
ブランチの、main
との共通の祖先(コミットB
)からfeature
の先端(コミットFB2
)までです。この範囲に含まれるコミットはFA1, FB1, FA2, FB2
です。 <new_base>
は現在のfeature-A
ブランチのHEAD、つまりコミットC
です。<upstream_base>
はfeature
ブランチがmain
から分岐したコミット、つまりコミットB
です。<topic_branch>
は元のfeature
ブランチです。
“`bash
feature-Aブランチにいる状態で実行
git rebase -i –onto HEAD B feature
または、より明確に指定する場合
git rebase -i –onto feature-A B feature
あるいは、mainの先端をnew_baseとするなら
git rebase -i –onto main B feature
(feature-Aがmainの先端を指しているなら同じ)
``
*:
HEAD(現在のfeature-Aブランチの先端、つまりコミットC)
*:
B(移動対象外とするコミットの境界。Bより新しいfeatureブランチのコミットが対象)
*:
feature` (移動対象のコミットが含まれるブランチ)このコマンドを実行すると、エディタが開かれ、
<upstream_base>
(B) から<topic_branch>
(feature, つまりFB2) までのコミット、すなわちFA1, FB1, FA2, FB2
がリストアップされます。“`
pick fa1fa1f Function A part 1
pick fb1fb1f Function B part 1
pick fa2fa2f Function A part 2
pick fb2fb2f Function B part 2Rebase B..feature onto ccccccc (main)
… (ヘルプメッセージなど)
``
FA1
* ここで、機能Aに関連するコミット (,
FA2) 以外の行 (
FB1,
FB2`) を削除します。pick fa1fa1f Function A part 1
pick fa2fa2f Function A part 2
* エディタを保存して閉じると、Gitはこれらの選択されたコミットだけを、<new_base>
(feature-AのHEAD, コミットC) の上に順番に再適用します。結果:
feature-A
ブランチは以下のようになります。A -- B -- C (main)
\
FA1' -- FA2' (feature-A)元の
feature
ブランチは変更されていません。 - 新しいブランチ
-
機能B用の新しいブランチを作成し、同様に rebase –onto -i を実行:
- 同様に、新しいブランチ
feature-B
をmain
の先端(コミットC
)から分岐させます。
bash
git checkout -b feature-B main feature-B
ブランチにいる状態で、元のfeature
ブランチから機能Bに関連するコミット (FB1
,FB2
) だけを移動させます。
“`bash
feature-Bブランチにいる状態で実行
git rebase -i –onto HEAD B feature
または
git rebase -i –onto feature-B B feature
``
FB1
* エディタが開いたら、機能Bに関連するコミット (,
FB2) 以外の行 (
FA1,
FA2`) を削除します。pick fb1fb1f Function B part 1
pick fb2fb2f Function B part 2
* エディタを保存して閉じると、Gitはこれらの選択されたコミットだけを、<new_base>
(feature-BのHEAD, コミットC) の上に再適用します。結果:
feature-B
ブランチは以下のようになります。A -- B -- C (main)
\
FB1' -- FB2' (feature-B)最終的な履歴のイメージ:
“`
A — B — C (main)
/ |
/ |
/ \
/ \
/ \
FA1′–FA2′ FB1′–FB2′
(feature-A) (feature-B)元のfeatureブランチはそのまま残っているか、必要なら削除/リセットする
… — FA1 — FB1 — FA2 — FB2 (feature)
“`
- 同様に、新しいブランチ
この例のように、git rebase -i --onto
を使うことで、元のブランチのコミット履歴を破壊することなく(ただし、新しいブランチ上でのみ履歴が変わる)、特定のコミット範囲を選択し、新しいベースの上に付け替えることができます。インタラクティブモード (-i
) を併用することで、コミットの順序変更、結合 (squash/fixup)、編集 (edit)、削除 (drop) なども同時に行うことが可能です。これは、汚くなった履歴を整形し、意味のある単位でコミットを整理し直すのに非常に強力な方法です。
例3:別のブランチから来たコミットを整理して自分のブランチに持ってくる
別の開発者が別のブランチで作業した内容の一部を、自分のブランチに取り込みたいが、単にマージするのではなく、自分のブランチの履歴をシンプルに保ちたい、というケースも考えられます。cherry-pick
を繰り返す方法もありますが、複数の連続したコミットを取り込みたい場合はrebase --onto
がより効率的です。
例えば、以下のような履歴があるとします。
A -- B -- C (main)
\ \
D -- E (feature)
\
F -- G -- H (another_feature)
feature
ブランチにいるとして、another_feature
ブランチに含まれるコミットのうち、G
とH
だけを、現在のfeature
ブランチの先端(コミットE
)の上に持ってきたいとします。
この場合、<new_base>
は現在のブランチのHEAD、つまりコミットE
です。移動対象のコミットはanother_feature
ブランチに含まれるG
とH
です。この範囲の始点はF
の次からなので、<upstream_base>
としてコミットF
を指定します。移動対象のコミットが含まれるブランチはanother_feature
です。
“`bash
featureブランチにいる状態で実行
git rebase –onto HEAD F another_feature
または
git rebase –onto feature F another_feature
“`
<new_base>
:HEAD
(現在のfeatureブランチの先端、コミットE)<upstream_base>
:F
(移動対象外とするコミットの境界。Fより新しいanother_featureブランチのコミットが対象)<topic_branch>
:another_feature
(移動対象のコミットが含まれるブランチ)
このコマンドを実行すると、another_feature
ブランチの履歴のうち、コミットF
よりも新しいコミット(G
, H
)が特定され、一時保存されます。次に、現在のブランチ(feature
)のHEADはそのまま(コミットE
を指したまま)で、一時保存しておいたコミットG
とH
が、feature
ブランチの先端(コミットE
)の上に再適用されます。
結果として、履歴は以下のようになります。
A -- B -- C (main)
\ \
D -- E (feature)
\
G' -- H' (feature) # G', H'がfeatureブランチに追加された
\
F -- G -- H (another_feature) # another_featureブランチは変更なし
このように、git rebase --onto
は、別のブランチから特定のコミット範囲だけを抜き出し、現在のブランチの先端に導入する際にも使用できます。これはcherry-pick G H
とほぼ同じ結果になりますが、rebase --onto
は対象が複数の連続したコミットである場合に構文がシンプルになるという利点があります。
例4:<upstream_base>
の指定と範囲指定の詳細
git rebase --onto <new_base> <upstream_base> <topic_branch>
における <upstream_base>
は、「<upstream_base>
より新しく、かつ<topic_branch>
に含まれるコミット」という範囲指定の基準点として機能します。これはGitの範囲指定構文 <upstream_base>..<topic_branch>
と似ていますが、いくつか重要な点があります。
<upstream_base>
は、必ずしも<new_base>
と<topic_branch>
の共通の祖先である必要はありません。単に「このコミットは移動せず、これよりも新しいコミットを移動対象とする」という境界線として機能します。
例として、以下のような履歴を考えます。
L -- M -- N (main)
\
O -- P -- Q (feature)
ここで、feature
ブランチのコミットP
とQ
だけを、main
ブランチの先端(コミットN
)の上に移動したいとします。
<new_base>
:main
(コミットN)- 移動したいコミットは
P
とQ
です。これらはfeature
ブランチに含まれています。移動対象の始点はO
の次からなので、<upstream_base>
はO
を指定します。 <topic_branch>
:feature
(コミットQ)
コマンドは以下のようになります。
“`bash
featureブランチにいるとして
git rebase –onto main O feature
“`
<new_base>
:main
(移動先の新しいベース)<upstream_base>
:O
(移動しないコミットの境界。Oより新しいfeatureブランチのコミットが対象)<topic_branch>
:feature
(移動対象のコミットが含まれるブランチ)
実行されると、feature
ブランチの履歴のうち、コミットO
よりも新しいコミット(P
, Q
)が特定され、一時保存されます。feature
ブランチのHEADはmain
の先端(コミットN
)に移動し、保存しておいたP
, Q
がその上に再適用されます。
結果:
L -- M -- N (main)
\ \
O (feature) # featureブランチはOを指したままか?
\
P' -- Q' (feature) # featureブランチのHEADがNの上に移動し、P', Q'が追加された
コマンドの最後の引数<topic_branch>
にブランチ名を指定した場合、そのブランチの参照(HEAD)が新しいベースに移動し、そのブランチが指していたコミットのうち移動対象となったものが新しいベースの上に再構築されます。一方、<topic_branch>
を省略した場合(つまりgit rebase --onto <new_base> <upstream_base>
とした場合)、対象となるのは現在のブランチ(HEAD)のコミット群となり、現在のブランチのHEADが新しいベースに移動します。
この例では、git rebase --onto main O feature
を実行すると、feature
ブランチのHEADは最終的にQ'
を指すようになります。元のfeature
ブランチが指していたコミットO
は、main
とfeature
の新しい分岐点のように見えますが、厳密には元のコミット履歴は残ったままで、feature
というブランチ名が新しいコミットQ'
を指すように更新された、という挙動になります。
<upstream_base>..<topic_branch>
という範囲指定は、git log <upstream_base>..<topic_branch>
で確認できるコミットの集合と概ね一致しますが、rebase --onto
ではもう少し複雑な内部処理(特にマージコミットの扱いなど)が行われることがあります。しかし、基本的には「<upstream_base>
を含まず、<topic_branch>
を含む範囲」を対象と理解しておけば、多くの場合は適切にコマンドを使用できます。
もし移動させたいコミット群が、共通の祖先から連続している場合は、通常のgit rebase <new_base>
でも実現できるケースがあります。例えば、上記の例でO, P, Q
全てをmain
の上に移動したい場合は、git rebase main feature
とすれば実現できます。(現在のブランチがfeatureの場合はgit rebase main
でも同じ)
しかし、<upstream_base>
を指定することで、移動させたくないコミット(例: 上記のO
)を範囲から除外し、「その次から」を移動対象とすることができるのが--onto
の強力な点です。
git rebase --onto
使用時の注意点
git rebase --onto
は非常に強力なツールですが、履歴を書き換えるコマンドであるため、使用には慎重さが求められます。特に以下の点に注意が必要です。
-
公開済み(共有)ブランチに対するrebase:
git rebase
の最大の注意点です。リモートリポジトリにプッシュされ、他の開発者と共有しているブランチに対してrebaseを行うと、そのブランチ上のコミットIDが変更されます。他の開発者が既に古いコミットIDに基づいた作業をしている場合、プッシュ/プル時に履歴の食い違いが発生し、コンフリクトや混乱の原因となります。- 原則: 公開済みの共有ブランチに対しては
rebase
を使用しない。代わりにgit merge
を使用してください。 - 例外: 自分のローカルブランチをリモートにプッシュしたが、まだ他の誰もその変更を取り込んでいないことが確実な場合に限り、rebase後に
git push --force
またはgit push --force-with-lease
を使用してリモートの履歴を上書きすることが許容される場合があります。しかし、--force
は非常に危険なので、--force-with-lease
を使うことを強く推奨します。--force-with-lease
は、リモートの履歴がローカルでrebaseする前に取得した状態から変化していないことを確認してからプッシュするため、他の開発者が知らない間に変更を加えている可能性を減らせます。
- 原則: 公開済みの共有ブランチに対しては
-
コンフリクトの解決:
rebase
はコミットを一つずつ再適用していくプロセスです。各コミットを再適用する際に、そのコミットの変更内容と新しいベースの変更内容が衝突(コンフリクト)する可能性があります。コンフリクトが発生した場合、Gitはその時点で処理を中断し、ユーザーに解決を求めます。- コンフリクトを解決する手順は以下の通りです。
- コンフリクトが発生したファイルを開き、Gitがマーカーで示した箇所を編集してコンフリクトを解消します。
- 解決したファイルをステージングエリアに追加します (
git add <file>
)。 git rebase --continue
を実行して、rebaseプロセスを再開します。
- もしrebaseを中止したい場合は、
git rebase --abort
を実行します。これにより、rebase開始前の状態に戻すことができます。 - 特定のコミットの適用をスキップしたい場合は、コンフリクト解決後に
git rebase --skip
を実行します。ただし、これはそのコミットで行われた変更を完全に破棄することになるため、慎重に使用してください。
- コンフリクトを解決する手順は以下の通りです。
-
git reflog
の活用:rebase
のような履歴を書き換える操作は、意図しない結果になった場合に元の状態に戻すのが難しいと感じられるかもしれません。しかし、Gitにはgit reflog
という強力なコマンドがあります。git reflog
は、リポジトリのHEADが過去にどのコミットを指していたかの履歴を記録しています。つまり、あなたが実行したほとんど全てのGit操作(コミット、マージ、リベース、リセットなど)の前後でHEADが指していた場所を確認できます。- もしrebaseで失敗したり、結果が意図通りにならなかったりした場合、
git reflog
でrebaseを実行する直前のHEADのコミットIDを確認し、git reset --hard HEAD@{n}
(nはreflogのリストにおける該当操作のインデックス) またはgit reset --hard <commit_id>
を実行することで、rebase前の状態に簡単に戻ることができます。 reflog
はデフォルトではローカルリポジトリにのみ記録され、通常は一定期間(デフォルトでは90日)後に期限切れとなります。
-
<upstream_base>
と<topic_branch>
の正しい理解:git rebase --onto <new_base> <upstream_base> <topic_branch>
の引数の意味、特に<upstream_base>
と<topic_branch>
が指定する範囲を正確に理解することが重要です。<upstream_base>
は「このコミットより新しい」という範囲の始点です。このコミット自体は移動対象に含まれません。<topic_branch>
は「このコミットまで」という範囲の終点です。このコミット自体は移動対象に含まれます(ただし、<upstream_base>
から<topic_branch>
までの間にマージコミットなどが含まれる場合、それらの扱いは少し複雑になることがあります。しかし、多くの場合、直線的な履歴であれば単純な範囲指定として機能します)。- 特に
<topic_branch>
にブランチ名を指定した場合、そのブランチのHEADが最終的に<new_base>
の上に移動した結果を指すようになります。現在のブランチのコミットを移動させたいが、現在のブランチは元の場所に残しておきたい場合は、<topic_branch>
を省略するか、移動させたいコミット範囲の終点を明示的に指定する(例:git rebase --onto <new_base> <upstream_base> <commit_id>
)などの工夫が必要です。
-
インタラクティブモード (
-i
) の検討:rebase --onto
で複数のコミットを移動させる場合、インタラクティブモード (-i
) を併用することを強く推奨します。これにより、移動するコミットを細かく選択したり、移動中にコミットを編集・整理したりすることができます。これは、歴史をよりクリーンで理解しやすいものにするために非常に役立ちます。git rebase -i --onto <new_base> <upstream_base> <topic_branch>
という形で使用します。
git rebase --onto
と他のコマンドとの比較
git rebase --onto
と同様に、あるブランチから別のブランチへコミットを移動させる(コピーする)機能を持つコマンドとして、git cherry-pick
があります。両者は似ていますが、目的や適用される状況が異なります。
-
git cherry-pick <commit_id>...
:- 指定した個別のコミットを、現在のブランチの先端にコピーするコマンドです。
- 履歴は直線的になりません。コピー元のコミットとコピー先の新しいコミットは異なるコミットIDを持ち、それぞれ別の場所に存在します。
- 主に、他のブランチにある一つまたは数個のコミットを、自分のブランチに一時的に取り込みたい場合などに使用されます。
- 連続していないコミットや、特定のコミットだけを選択的に取り込みたい場合に便利です。
- 複数のコミットをcherry-pickする場合、一つずつ適用されるため、それぞれのコミットでコンフリクトが発生する可能性があります。
-
git rebase --onto <new_base> <upstream_base> <topic_branch>
:- 指定したコミット範囲を、
<new_base>
の直後に付け替えるコマンドです。 - 指定したブランチ(または現在のブランチ)のHEADが新しい場所に移動します。
- 移動対象となったコミットは、親コミットが変わるため新しいコミットIDを持ちますが、元の履歴は(ブランチ参照が移動したことを除けば)残ったままです。
- 主に、あるブランチに含まれる連続したコミット群を、別の場所に移動させたり、履歴を整理・分割したりする場合に使用されます。
- 複数のコミットをrebaseする場合、まとめて一連の操作として処理されます。コンフリクトは発生したコミットで一時停止し、解決後に
--continue
で再開します。
- 指定したコミット範囲を、
使い分けの目安:
- 数個のバラバラなコミットを、現在のブランチに「コピー」して取り込みたい場合は
cherry-pick
。 - あるブランチ上の特定の「コミット範囲」を、別のブランチの先端に「移動」させて、そのブランチの履歴を付け替えたい場合は
rebase --onto
。特に、移動中にコミットの整理や編集も行いたい場合はrebase -i --onto
。
実践的なシナリオと rebase --onto
の活用
これまで紹介した例以外にも、git rebase --onto
は様々な実践的なシナリオで活用できます。
-
古いトピックブランチを最新の
main
に合わせて整理する:
長期間開発していたfeatureブランチがあり、その間にmain
ブランチには多数の変更が取り込まれたとします。featureブランチをmainにマージする前に、featureブランチの履歴を最新のmainの上に付け替え、コンフリクトを事前に解消しておきたい場合。“`bash
featureブランチにいるとして
git fetch origin main # 最新のmainを取得
git rebase –onto origin/main main feature
``
*:
origin/main(リモートの最新main)
*:
main(ローカルのmain。rebase前にfeatureが分岐した時点のmainを指していると仮定)
*:
feature` (現在のブランチ)これは、featureブランチの、ローカルのmainから分岐した時点より新しい全てのコミットを、リモートの最新mainの上に付け替える操作です。もしローカルのmainが古い場合でも、
--onto
を使えば新しいベースを明示的に指定できます。ただし、これは一般的なgit rebase main
と結果的に同じになることが多いです(ローカルのmainが最新であれば)。--onto
を使うことで、例えば「ローカルのmainから分岐した時点」ではなく、「特定のタグAが打たれたコミット」から「現在のfeatureブランチのHEAD」までのコミットを、「最新のmainブランチ」の上に移動させる、といった柔軟な指定が可能になります。 -
開発中のブランチから特定の部分だけを早期に提出する(後から分割):
一つのブランチで複数の関連する機能や修正に取り組んでいるが、そのうちの一つだけを先にレビューに出したい場合。上記のブランチ分割の例と同様に、rebase -i --onto
を使って、提出したい機能に関連するコミットだけを新しいブランチに切り出し、ベースブランチの先端に付け替えることで実現できます。 -
複雑な履歴を持つ外部リポジトリからの変更を取り込みつつ、自分の変更を整理する:
Upstreamリポジトリからフォークしたプロジェクトで作業しており、Upstreamの変更を自分のmasterブランチに取り込み、さらに自分のfeatureブランチをそのmasterにrebaseしたいが、Upstreamの履歴が複雑だったり、自分のmasterブランチにUpstream以外のコミットも含まれていたりする場合。rebase --onto
を使って、自分のfeatureブランチのコミットを、最新のUpstreamのmasterの先端の上に移動させる、といったより精密な制御が可能になります。
これらのシナリオでは、git rebase --onto
が単なるrebase <base>
では実現できない細かいコミット範囲の指定や、移動先の柔軟な指定を可能にすることで、複雑な状況でも変更履歴を整理し、より理解しやすい状態に保つために役立ちます。
まとめ:git rebase --onto
の理解と活用
git rebase --onto
は、Gitのrebase
コマンドを拡張し、コミットの「付け替え」操作をより柔軟に行うための強力なオプションです。
基本的な構文は git rebase --onto <new_base> <upstream_base> <topic_branch>
であり、その核心は以下の3つの引数の役割にあります。
<new_base>
: コミット群を移動させる新しいベースとなる場所(コミットまたはブランチ)。移動対象のコミットはこの直後に配置されます。<upstream_base>
: 移動対象のコミット範囲を指定する境界線となるコミット。「このコミットより新しい」コミットが移動対象となります。<topic_branch>
: 移動対象のコミットが含まれるブランチまたはコミット。通常はこのブランチの履歴から<upstream_base>
より新しい部分が選択されます。コマンドの最後に指定された場合、このブランチのHEADが<new_base>
の上に移動した結果を指すようになります。
このコマンドを使うことで、単に現在のブランチを別のブランチの先端に移動させるだけでなく、以下のようなより複雑なシナリオに対応できます。
- あるブランチに含まれるコミットのうち、特定の範囲だけを移動する。
- 移動先のベースを特定のコミットやタグに指定する。
- 一つのブランチで複数の作業を行った結果を、後から機能ごとに別のブランチに分割・整理する。
特に、インタラクティブモード (-i
) と組み合わせたgit rebase -i --onto
は、移動対象のコミットを選択・編集・削除・結合しながら付け替えを行うことができ、汚れた履歴をクリーンアップする上で非常に強力なツールとなります。
ただし、git rebase --onto
は履歴を変更するコマンドであるため、公開済みの共有ブランチに対して使用するのは避けるべきです。これにより、他の開発者との間で履歴の不整合が発生し、混乱を招く可能性があります。個人的なブランチや、まだ共有していないブランチでの使用に限定するか、チーム内でrebaseの運用ルールを明確に定めた上で、git push --force-with-lease
などを慎重に使用する必要があります。
また、rebase中にコンフリクトが発生した場合は、適切に解決し、git rebase --continue
で進める必要があります。予期せぬ結果になったり、操作を間違えたりした場合でも、git reflog
コマンドを使えばrebase前の状態に戻すことができることを覚えておきましょう。
git rebase --onto
は、最初は少し複雑に感じるかもしれませんが、その仕組みと引数の意味を理解すれば、Gitを使った開発ワークフローにおいて履歴を柔軟に操作し、よりクリーンで管理しやすい状態に保つための非常に強力な味方となります。ぜひ、安全な環境(ローカルの実験用リポジトリなど)で実際にコマンドを実行し、その挙動を体験してみてください。
Gitの公式ドキュメントや、他の信頼できるリソースも参照しながら理解を深めることで、git rebase --onto
を効果的に活用できるようになるでしょう。