[Git] 人生をリベースしたい

という話はさておき、リベースがわかるとGitがわかるってばっちゃが言ってました。

結論から言うとrebaseはリビジョンの一括コピー+リファレンスの張替です。
gitはブランチを自由に張り替えることができるので新たにリビジョンを作り直してもあたかも同じブランチが書き換わったかのように見せることができます。

この記事の操作は手元で再現することができます。
試したい方は「git clone https://github.com/righ/gittest.git」としましょう。Gitクライアントがインストールされてることが前提です。各操作毎にブランチを分けているのでチェックアウトして使ってください。気軽にStarつけてね!
どのようなコミットグラフになっているかはGithubで見るか、クローンしてGUIクライアントで見るなりしてください。
どのブランチ使うかは例題の中で指定してるはずです。

cherry-pick

さて、リビジョンのコピーというとcherry-pickというコマンドがあります。これはrebaseとは違い、単に一つだけのリビジョンを対象としたコピーです。コピーというと少しわかりにくいですが、差分の適用だと思えばよいでしょう。diffとpatchで再現することができます。

cherry-pick diff and patch
移動
$ git checkout cherry-pick-to
$ git checkout patch
適用
$ git cherry-pick cherry-pick-from
[cherry-pick-to 58c5dd9] b3
 1 file changed, 1 insertion(+)
$ git diff cherry-pick-from^ cherry-pick-from|patch
patching file b
確認
$ cat a b c
a1
a2
b1
b2
b3
$ cat a b c
a1
a2
b1
b2
b3

patchブランチで「git diff cherry-pick-to」とすると差分がないことが分かると思います。

もし言葉の通り本当にリビジョンの内容をコピーするのであれば、直前に行った変更(aの更新とcの追加)は上書きされてしまうはずですよね。
リビジョンのコピーとは「’対象リビジョンの親リビジョン’と’対象リビジョン’の差分を適用する」ことです。

リベースは同じことを古い順に繰り返すことになります。これが基礎です。

リベース

rebaseは分岐元のリビジョンを変更する操作です。というと難しく感じる方もいるかもしれませんが結局は差分の適用です。
先ほど使ったcherry-pickで表現してみましょう。

rebase cherry-pick * n
移動
$ git checkout rebase-from
Switched to branch 'rebase-from'
$ git checkout rebase-to-by-cherry-pick
Switched to branch 'rebase-to-by-cherry-pick'
適用
$ git rebase rebase-to
First, rewinding head to replay your work on top of it...
Applying: a2/b3
Applying: a3/-b3
$ git cherry-pick rebase-from-by-cherry-pick^ # 「^」は1つ前という意味
[rebase-to-by-cherry-pick acfb20c] a2/b3
2 files changed, 2 insertions(+)

$ git cherry-pick rebase-from-by-cherry-pick
[rebase-to-by-cherry-pick f582b98] a3/-b3
 2 files changed, 1 insertion(+), 1 deletion(-)
確認
$ git log --oneline
86dfe43 a3/-b3
dadbe22 a2/b3
7196883 (c)
5a98789 a1/b1-b2
c9ea552 wrote README

$ cat a b c
a1
a2
a3
b1
b2

$ git status
On branch rebase-from
git log --oneline
f582b98 a3/-b3
acfb20c a2/b3
7196883 (c)
5a98789 a1/b1-b2
c9ea552 wrote README

$ cat a b c 
a1
a2
a3
b1
b2

$ git status
On branch rebase-to-by-cherry-pick

ファイルの内容はcherry-pickでやったのと同じ結果になりましたね。重要なのは以下の3点だと思います。

①引数には移動を指定する
cherry-pickやmergeが「リビジョンを持ってくる」操作であるのに対し、rebaseは「リビジョンを持っていく」ということを表しています。

②チェリーピックと違いリベースではカレントブランチが自動的にリベース先に移動される
このときタグは移動しないので注意してくださいね。

③リベースは共通祖先までさかのぼったリビジョン差分を古い順に一つずつ適用する
マージも共通祖先との差分適用によりファイルの統合を試みますが、リベースとは違い最終的なファイルとの差分しか見ません。これによりコンフリクトが発生する条件も若干異なってきます。
また、適用される差分の順番が変わったり飛んだりすることで無効な差分パッチとなる場合はリベースに失敗します。

操作

rebaseが差分をひとつずつ適用するという特性上、リベースの途中で操作が求められることがあります。
例えばコンフリクトが発生した時や、後述するインタラクティブオプション(–interactive/-i)を指定した場合です。
ここらでその操作について見ておきましょう。

–continue

リベースを続ける場合に指定します。
変更したファイルがあれば「git add xxxx」で追加する必要があります。

「–skip」オプションはその差分を「無視すること」です。
たとえば、直前のコンフリクト解決によって適用する差分がなくなってしまった場合などが挙げられます。

–abort

やっぱりリベースを取りやめたいときは「–abort」オプションを指定します。
この操作によってリベースはなかったことになります。途中で解決したコンフリクトも吹っ飛ぶので注意してください。
当記事で問題が発生してわけがわからなくなったらとりあえず「git rebase –abort」しましょう。

–interactive/-i

このオプションを使うとリベース対象のリビジョンをgitが提示してくれます。
以下のように起点となるリビジョンを指定する必要があります。

$ git rebase -i HEAD^^^

すると以下のような編集画面に移るのでリビジョンに対してコマンドを指定して保存するとリベースが完了するという具合です。

pick a4cb5cf a2
pick 90ed748 a3
pick 6528fcf a4
pick 8e9cb05 b3

# Rebase 5a98789..8e9cb05 onto 5a98789 (4 command(s))
#
# 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
#
# 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.
#
# Note that empty commits are commented out

重要なのは(この例では)先頭4行だけです。下のはコメントなのでいじる必要はありません。

以下ではこのコマンドについて説明していきます。

pick

pickとはそのリビジョンが存在することです。これが選択されていることがデフォルトです。
pickの行を削除するとそのリビジョンは削除され、行を移動するとリビジョンが移動されます。

$ git checkout i-pick
$ git log --oneline --reverse # ログの確認(リベース画面に合わせて古い順で表示)
c9ea552 wrote README
5a98789 a1/b1-b2
a4cb5cf a2
90ed748 a3
6528fcf a4
8e9cb05 b3

$ git rebase -i HEAD^^^^

i-pickブランチに移動し、HEADから4つ前までを編集対象とします。

pick a4cb5cf a2
pick 8e9cb05 b3
pick 90ed748 a3
#pick 6528fcf a4

最終行にあるb3をa2の次の行に移動、a4行はコメントアウトして保存します。

リベースが成功したみたいなのでログを見てみましょう。

Successfully rebased and updated refs/heads/i-pick.

$ git log --oneline --reverse
c9ea552 wrote README
5a98789 a1/b1-b2
a4cb5cf a2
482e1c8 b3
b6e6af8 a3

b3のコミットはa2の後ろに移動して、a4のコミットが消えてますね。成功です。

この例ではa2より前のログに変更はないためリビジョンIDが変わっていませんが、後続のリビジョンIDがすべて変わっているのがわかるでしょうか。gitでは親リビジョンへのポインタだけ変えて同じリビジョンIDで保存するということができないためこのようになります。

また、先ほども説明しましたがリベースは順番が重要です。
差分によっては削除することで後続がうまくリベースできなくなることがあるので注意しましょう。
たとえばこの例でいえば、a3を残してa2を削除するといったことをするとリベースに失敗します。

error: could not apply 90ed748... a3

When you have resolved this problem, run "git rebase --continue".
If you prefer to skip this patch, run "git rebase --skip" instead.
To check out the original branch and stop rebasing, run "git rebase --abort".
Could not apply 90ed748e09256bbe38c9d6f37ec01e89b52c6bde... a3

reword

rewordはコミットメッセージを変更します。

$ git checkout i-reword
Switched to branch 'i-reword'
Your branch is up-to-date with 'origin/i-reword'.

$ git log --oneline --reverse 
c9ea552 wrote README
5a98789 a1/b1-b2
7a57d3f a2
5a062ac a3
ba96c01 a4

$ git rebase -i HEAD^^^
pick 7a57d3f a2
reword 5a062ac a3
pick ba96c01 a4

a3にrewordを指定して保存します。ここでは変更後のコミットメッセージを指定しなくてよいです。

連続して新メッセージの入力が促されるので編集して保存しましょう。下のシャープついてるところはコメントなので無視していいです。

a3 reword test 

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# interactive rebase in progress; onto 5a98789
# Last commands done (2 commands done):
#    pick 7a57d3f a2
#    reword 5a062ac a3
# Next command to do (1 remaining command):
#    pick ba96c01 a4
# You are currently editing a commit while rebasing branch 'i-reword' on '5a98789'.
#
# Changes to be committed:
#       modified:   a
#

できましたね。当然ですがa3以降のリビジョンIDは変わります。

[detached HEAD 2287d92] a3 reword test
 1 file changed, 1 insertion(+)
Successfully rebased and updated refs/heads/i-reword.

$ git log --oneline --reverse 
c9ea552 wrote README
5a98789 a1/b1-b2
7a57d3f a2
2287d92 a3 reword test
fefb5e6 a4

コメントを見るとわかるかもしれませんが、行頭の「#」はコメント記号として扱われます。
すべてコメントアウトするか、空のメッセージで保存するとリベース処理が中断されます。

edit

editはリビジョンを書き換える操作です。
「他のリビジョンを追加した後に気づいたけどここのリビジョンは本当はこうすべきだった」ときですね。

$ git checkout i-edit
Switched to branch 'i-edit'
Your branch is up-to-date with 'origin/i-edit'.

$ git log --oneline --reverse 
c9ea552 wrote README
5a98789 a1/b1-b2
5d8e7bd a2
b87dc97 a3

$ git rebase -i HEAD^^^
pick 5d8e7bd a2
edit b87dc97 a3
pick 1981b55 a4

とした後、ファイルを書き換えるのですが、今回はファイル「b」の「b2」行を「B2」に書き換え、–amendをつけてコミットしてみましょう。
amendを使わないと差分は別リビジョンとしてコミットされます。

最後にcontinueをお忘れなく。

$ vi b
$ git add b
$ git commit --amend -m 'a3/B2'
[detached HEAD dc7fced] a3/B2
 2 files changed, 2 insertions(+), 1 deletion(-)

$ git rebase --continue
Successfully rebased and updated refs/heads/i-edit.
$ git log --oneline --reverse 
c9ea552 wrote README
5a98789 a1/b1-b2
5d8e7bd a2
dc7fced a3/B2
c1bc7b7 a4

$ cat a b
a1
a2
a3
a4
b1
B2

コミットメッセージは書き換わっているし、もちろんファイルも書き換わってますね。

squash

squashは複数のリビジョンをまとめます。

$ git checkout i-squash
Switched to branch 'i-squash'
Your branch is up-to-date with 'origin/i-squash'.

$ git log --oneline --reverse 
c9ea552 wrote README
5a98789 a1/b1-b2
e676502 a2
35574b0 a3
7e0114d a4

$ git rebase -i HEAD^^^

squashをつけたリビジョンとその一つ手前のリビジョンが対象となります。
では、一番前(古い)リビジョンをスカッシュしようとすると。。。

squash e676502 a2
pick 35574b0 a3
pick 7e0114d a4
Cannot 'squash' without a previous commit

この指定方法は個人的にはあまり直感的じゃないなぁ。
それはそれとしてじゃあ普通にスカッシュしてみましょう。

pick e676502 a2
squash 35574b0 a3
pick 7e0114d a4

これで保存すると。。

コミットメッセージの入力を求められます。
このウィンドウ全体がコミットメッセージとなります。先頭「#」はコメントですが。

# This is a combination of 2 commits.
# The first commit's message is:

a2-a3

# This is the 2nd commit message:

# a3

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
#
# interactive rebase in progress; onto 5a98789
# Last commands done (2 commands done):
#    pick e676502 a2
#    squash 35574b0 a3
# Next command to do (1 remaining command):
#    pick 7e0114d a4
# You are currently editing a commit while rebasing branch 'i-squash' on '5a98789'.
#
# Changes to be committed:
#       modified:   a
#

a2とa3のログをまとめたのでコミットメッセージは「a2-a3」とでもしましょう。
a3の行はいらないのでコメントアウトしましょう。

[detached HEAD 9768a1b] a2-a3
 1 file changed, 2 insertions(+)
Successfully rebased and updated refs/heads/i-squash.

$ git log --oneline --reverse 
c9ea552 wrote README
5a98789 a1/b1-b2
9768a1b a2-a3
0c9efe1 a4

$ cat a
a1
a2
a3
a4

3つ以上のリビジョンをまとめたい場合は以下のように連続してsquashをつけるだけです。簡単ですね。

pick e676502 a2
squash 35574b0 a3
pick 7e0114d a4

fixup

fixupはsquashとほぼ同じです。違う点は、まとめたリビジョンのうち先頭のコミットメッセージを使うという点です。

こんな感じなのでいつもどおりリベースしていきましょう。

$ git checkout i-fixup
Switched to branch 'i-fixup'
Your branch is up-to-date with 'origin/i-fixup'.

$ git log --oneline --reverse 
c9ea552 wrote README
5a98789 a1/b1-b2
5a1a6b2 a2
6ab13b6 a3
aabec2d a4

$ git rebase -i HEAD^^^

今回は全部まとめちゃいましょう。

pick 5a1a6b2 a2
fixup 6ab13b6 a3
fixup aabec2d a4

これで保存すると。。

$ git log --oneline --reverse 
c9ea552 wrote README
5a98789 a1/b1-b2
3a2e932 a2

$ cat a
a1
a2
a3
a4

リビジョンはひとつにまとまってますが、ファイルは変わっていませんね。OK。

exec

execは任意のコマンド実行です。
exec後に任意のshellコマンドを実行できます。

今回の例ではログは書き換えないのでブランチは何でもいいのですが一応「i-exec」ブランチでやります。

いい例が思い浮かばないのでとりあえずファイルの中身を出力してみましょう。

pick f7ce6d7 a2
exec cat a
pick 6aab22b a3
exec cat a
pick 3597589 a4
exec cat a

これで保存すると

git rebase -i HEAD^^^
Executing: cat a
a1
a2
Executing: cat a
a1
a2
a3
Executing: cat a
a1
a2
a3
a4

こんな感じに順にaのテキストが出力されてくれました。

drop

dropはpick行を削除したのと同じことです。たぶんね。(この辺でもうめんどくさくなってる

$ git checkout i-drop
Switched to branch 'i-drop'
Your branch is up-to-date with 'origin/i-drop'.

$ git log --oneline --reverse                    
c9ea552 wrote README
5a98789 a1/b1-b2
93c90c7 a2
c8c5469 a3
b5b6178 a4
pick 93c90c7 a2
pick c8c5469 a3
drop b5b6178 a4

最後を消しましょう。

git log --oneline --reverse 
c9ea552 wrote README
5a98789 a1/b1-b2
93c90c7 a2
c8c5469 a3

消えてます。よかったよかった。

–onto

ontoオプションは差分をどこに適用するかを指定するオプションです。省略可能で、リベースの終点がデフォルトの適用先となります。では適用先が変えられると何が嬉しいのでしょうか。

例えばブランチから他のブランチが生えている場合を考えてみます。実例に沿っていうなら、マイルストーンブランチから生えたトピックブランチを他のマイルストーンブランチ(移動後のマイルストーンブランチ)に移すなどが挙げられます。

今回は簡略化して以下のようなケースを考えてみます。

  1. 「onto-from」と「onto-to」が「rebase-test」ブランチから生えている。(リビジョンはそれぞれ1つずつ)
  2. 「onto-topic」ブランチが「onto-from」から生えている(リビジョンは1つ)
  3. 「onto-from」ブランチの差分は「ファイルaに”a2″という文字列を追加」
  4. 「onto-to」ブランチの差分は「ファイルaに”A2″という文字列を追加」(変更範囲がコンフリクトする)
  5. 「onto-topic」ブランチの差分は「ファイルbの先頭に”b3″という文字列を追加」(変更範囲はコンフリクトしない)
  6. 「onto-topic」ブランチを「onto-to」に移動したい

「onto-to」が移動のマイルストーン、「onto-from」が移動のマイルストーン、「onto-topic」が移動対象のトピックブランチだと思ってください。

オプションを指定せずにonto-toへリベースしてみましょう。

$ git checkout onto-topic
Switched to branch 'onto-topic'

$ git rebase onto-to
First, rewinding head to replay your work on top of it...
Applying: a2
Using index info to reconstruct a base tree...
M       a
Falling back to patching base and 3-way merge...
Auto-merging a
CONFLICT (content): Merge conflict in a
error: Failed to merge in the changes.
Patch failed at 0001 a2
The copy of the patch that failed is found in: .git/rebase-apply/patch

When you have resolved this problem, run "git rebase --continue".
If you prefer to skip this patch, run "git rebase --skip" instead.
To check out the original branch and stop rebasing, run "git rebase --abort".

$ cat a 
a1
<<<<<<< ec630562762966a1f9d6ee5a5ebc3c2c7935eb06
A2
=======
a2
>>>>>>> a2

コンフリクトしました。移動したかったトピックブランチではなく親ブランチ(例で言うならマイルストーンブランチ)の差分のせいです。

そこで–ontoを使います。以下のようにトピックブランチだけが対象になるように調整すると

$ git rebase --abort # 前のリベースは中断しよう
$ git rebase --onto onto-to onto-from onto-topic
First, rewinding head to replay your work on top of it...
Applying: b3

$ cat a b
a1
A2
b1
b2
b3

成功しました。やったぜ。
「–onto」オプションはこのように範囲を限定してリベースしたいときに適してます。使いこなしてドヤ顔しましょう。

pull

ところでみなさんはgit pull使ってますか。巷では使わないほうがいいとかなんとか囁かれていますが実は私も使ってません。まぁそんなことはどうでもいいんです。

pullはデフォルトだと「fetch + merge」で、–rebaseオプションを指定すると「fetch + rebase」となります。
この記事はrebaseの記事ですので、–rebaseオプションだけでも試しておきましょう。

$ git checkout pull
  
$ cat a
a1
a2

$ git reset --hard HEAD^
HEAD is now at 5a98789 a1/b1-b2

$ cat a
a1

$ git pull origin pull --rebase
 * branch            pull       -> FETCH_HEAD
First, rewinding head to replay your work on top of it...
Fast-forwarded pull to 3fd3bfbf9ce08ce8c03ac353b607bea445310511.

$ cat a 
a1
a2

ファストフォワードと書いてあるのがわかるでしょうか。
この例ではローカルブランチをリモート(追跡)ブランチにすすめるだけで対応したためそのように呼ばれるのでしょうね。

続いてわざとリモートブランチよりも進んだ状態でpull –rebaseしてみましょう。

$ git reset --hard HEAD^
HEAD is now at 5a98789 a1/b1-b2
$ echo b3 >> b
$ git add b 
$ git commit -m 'b3'
[pull 6c71e25] b3
 1 file changed, 1 insertion(+)
$ git pull origin pull --rebase
 * branch            pull       -> FETCH_HEAD
First, rewinding head to replay your work on top of it...
Applying: b3

$ cat a b
a1
a2
b1
b2
b3

$ git log --oneline
e62a929 b3
3fd3bfb a2
5a98789 a1/b1-b2
c9ea552 wrote README

神2から,(´_・ω・)_「とりあえずrebase」的なことを言われた意味が理解できました。

疲れた。終了。

参考リンク
図解Gitコミットの修正まとめ git reset, cherry pick, revert, commit –amend, rebase
あのコミットをなかった事に。git rebase -i の使い方
Git pull | アトラシアン Git チュートリアル – Atlassian