対象
gitでrebaseをよくするし、mergeはマージコミットを残す人。
rebaseするとmergeのマージコミットの記録が失われるのが困る人。
概要
付け替えたいブランチの先端を作業コピーにして以下を実施
git rebase --rebase-merge <付け替え先> -i
git rebase --rebase-merge <付け替えたいブランチの根本> --onto <付け替え先> -i
git rebase --rebase-merge=rebase-cousins <付け替え先> -i
なるべく -i
をつけてTODOリストを表示し、作業内容を確認する。
--rebase-merge
とは
By default, a rebase will simply drop merge commits from the todo list, and put the rebased commits into a single, linear branch. With --rebase-merges, the rebase will instead try to preserve the branching structure within the commits that are to be rebased, by recreating the merge commits. Any resolved merge conflicts or manual amendments in these merge commits will have to be resolved/re-applied manually.1
デフォルトでは、リベースは単にマージコミットを TODO リストから削除し、リベースされたコミットを単一の直線的なブランチにまとめます。rebase-merges を指定すると、リベースはマージコミットを再作成することで、リベースされるコミット内の分岐構造を維持しようとします。マージの競合が解決された場合や、マージコミットの修正が手動で行われた場合は、手動で修正を適用しなければなりません。2
すごい!
使い方
今自分のブランチがmyworkという状態でissue1をマージした状態で作り込みを行っています。本流はupstreamで、自分とは別に誰かほかの人がつくったブランチissue2があるとします。
別の人のブランチがテスト完了してマージされ、upstreamが更新されました。
さて、myworkが新しいupstreamでどうなるのか検証するため、rebaseで追従させたいと思います。
普通のrebaseの場合
普通のrebaseだとこうなります。
git checkout mywork
git rebase upstream
ひとつ質問いいかな。マージコミット、どこいった?
……君のような勘のいい(ry
--rebase-merge
の場合
マージコミットも含めてリベースしたい………そんな時に役に立つのが git rebase --rebase-merge
です。
git checkout mywork
git rebase --rebase-merge upstream
すごい!!
ただ、図中のissue1のように、ほかのブランチは別のコミットとして扱われてしまうので、ラベルを張りなおす必要があります。
-i
付きでTODOリストを表示&編集
ちなみに、git rebase --rebase-merge -i upstream
とすると、以下のようなTODOリストが出てきて、rebaseで何をやっているのかがわかります。
label onto
# Branch a
reset onto
pick 4dc5f5f a1
pick 4e24b3f a2
label a
reset onto
pick 44726f9 1
pick df80caa 2
merge -C 8f0e5c8 a # Merge branch 'issue1' into mywork
pick cbab20c 3
pick 22c036e 4
マージコミットを形成するための⑨は、もともと8f0e5c8のコミットでマージした時のコミットメッセージを使って➄でaとラベル付けしたコミットをマージする、という意味です。
ブランチの作成も一緒に行う
これが理解できるようになったら、以下のようにしてブランチ作成もやってしまうといいでしょう。
label onto
# Branch a
reset onto
pick 4dc5f5f a1
pick 4e24b3f a2
label a
exec git branch -f -c issue1 issue1-old
exec git branch issue1-new
reset onto
pick 44726f9 1
pick df80caa 2
merge -C 8f0e5c8 a # Merge branch 'issue1' into mywork
pick cbab20c 3
pick 22c036e 4
exec git branch -f -m issue1-new issue1
ブランチのpickが終わったらもともとのブランチに-old
と付けたブランチを複製で作っておき、pick済みの新しいブランチには、-new
を付けたブランチを新しく作ります。
すべてが終わったら、-new
を取り去って新しいブランチにします。こうすれば途中で失敗して--abort
しても安心です。
一応念のため-old
はつけたままにしておき、検証がすんだら破棄しましょう。
--ontoと一緒に使う
git rebase --rebase-merge <付け替えたいブランチの根本> --onto <付け替え先>
ということもできます
付け替えたいブランチの根本というのは、 --onto
がない場合は付け替え先と現在のブランチの分岐点になるのですが、以下のようなケースですと、やりたいことがうまくできません。
この状況で、git rebase --rebase-merge upstream
をしてみると、以下のようになります。
最も新しい分岐点の➂からの付け替えを行った結果、このようになったようです。
このようにマージコミットのトポロジが複雑で、うまくリベースできなさそうな場合、 --onto で付け替えたいブランチの根本と、付け替え先を明示してやるとうまくいくことがあります。
上図の一番古い分岐点は➀なので、➀のコミットから生えているmyworkを、➃のupstreamのところに付け替えたいとしましょう。ここで、➀のハッシュは"df41069"だとしましょう。
git rebase --rebase-merge df41069 --onto upstream
-i
付きでTODOリストの表示&編集
これで正しいものが得られます。
ただ、この方法ですと、A'を作る際に、コンフリクトが発生します。
理由は以下のTODOリストを見るとわかります。
git rebase --rebase-merge df41069 --onto upstream -i
label onto
# Branch foo
reset onto
pick b8514a7 2
label branch-point
pick 1bcbd43 3
pick c459ffb i
label foo
# Branch hoge
reset onto
pick 4937acb a
pick 303b05e b
label hoge
reset branch-point # 2
pick a7e5334 A
merge -C 018c043 foo # Merge branch 'foo' into mywork
merge -C 02a6784 hoge # Merge branch 'hoge' into mywork
pick 55fefc3 D
pick a2c1508 E
すでにupstreamには、➁のコミットが含まれていますが、さらに pick b8514a7 2
としています。➂もⓘもpickし、
➀からⒺに至るまでの道筋を愚直に再現しているようです。
-i
で最初に編集できるTODOリストを編集して消してしまってもいいのですが、もう少し頭のいい方法を紹介します。
--rebase-merge=rebase-cousins
モードでリベースする場合
cousinというのはいとこという意味ですね。親の違うコミットも含めてrebase対象にしましょう、というモードです。
git rebase --rebase-merge=rebase-cousins upstream -i
以下のようなTODOリストが得られます。
label onto
# Branch hoge
reset onto
pick 4937acb a
pick 303b05e b
label hoge
reset onto
pick a7e5334 A
merge -C 018c043 onto # Merge branch 'foo' into mywork
merge -C 02a6784 hoge # Merge branch 'hoge' into mywork
pick 55fefc3 D
pick a2c1508 E
必要なものだけをピックアップしていますね。
まとめ
git rebase --rebase-merge
は複雑になればなるほど難しい上にコンフリクトも発生しますので、失敗するとものすごく時間の無駄遣いになってしまいます。
できればq-i
でTODOリストを表示し、何を行うか理解したうえで行いたいところですね。
-
git rebase --rebase-merge <付け替え先> -i
: 比較的単純な場合はこれ。 -
git rebase --rebase-merge <付け替えたいブランチの根本> --onto <付け替え先> -i
: 根本を指定したい場合はこれ。 -
git rebase --rebase-merge=rebase-cousins <付け替え先> -i
: 付け替え先と同じ祖先をもっているものの違うコミットから生えていて、自身にマージされたブランチ(いとこ関係のブランチ)もピックアップして取り込みたい場合はこれ。
ちなみに、ここまで--rebase-merge
と長いオプション名で説明していましたが、-r
の短いオプション名でもOKです。
例) git rebase -r upstream