rebase について

rebase 便利だよ、というだけのエントリです。
AA で書いてる部分は時間があれば画像に置き換えます。

rebase とは

ブランチを作成した場所を変更することと理解しています。つまり、そのブランチの「親」を変更する、ということです。
もう少し動作に踏み込むと、指定したコミットの後ろに現在のブランチで行ったコミットをリプレイするように適用します*1。単なるリプレイではなく、その過程をいじくれるのが rebase のすごいところです。


単純な rebase はたとえばこんな感じです。
以下のようなリポジトリの状態があったとして (現在チェックアウトされているブランチは dev ということを表すのに * を使っています)、

          1---2---3 *dev
         /
A---B---C---D master

次のコマンドを実行します。

$ git rebase master

これにより、リポジトリの状態は

              1---2---3 *dev
             /
A---B---C---D master

になります。マージのように分岐をそのままにして統合するのではなく、「歴史を一本化」するのが rebase です*2
ただし、このままでは master ブランチには 1〜3 のコミットが適用されていませんので、ここから更にマージを行います。

$ git checkout master
$ git merge dev
$ git branch -d dev

これで、リポジトリの状態は

A---B---C---D---1---2---3 *master

となります。

コンフリクト

rebase でもマージと同じように、コンフリクトが発生する可能性があります。
コンフリクトが発生した場合、コンフリクトを解消してから

$ git rebase --continue

を実行することで、rebase を続けることができます。
競合を解決した結果、一つ前のコミットとの違いがなくなってしまった場合、

$ git rebase --skip

としてそのコミットをスキップします。
rebase をあきらめる場合、

$ git rebase --abort

とすることで git rebase 実行前の状態に戻ります。

無かった歴史を「あったことに」する

これは上で行った rebase の別視点からのとらえ方です。
dev ブランチから見ると、コミット C の次のコミットは 1 で、D というコミットは存在しません。
しかし、rebase を行うことで「C と 1 の間に D というコミットを作った」ように見えるわけです。


この視点から見ると、rebase で「やり忘れた作業」を「適切な位置」に入れることもできる、と言えます。

消したい歴史を「なかったことに」する

今度は逆に、コミットを「なかったことに」する使い方です。
git rebase に -i を付けることで、インタラクティブに rebase を行うことができます。これを使うことで、歴史を「なかったことに」できます。


例えば、

          D---E *b
         /
A---B---C a

という状態で、ブランチ b をブランチ a(つまりコミット C) に rebase します。
・・・もうなってますね。でもそれでいいんです。

$ git rebase -i a

これを実行すると、GIT_EDITOR に設定したエディタが次のような内容で立ち上がります。

pick d6a3a28 D
pick 82ad00d E

# Rebase 6d0184e..82ad00d onto 6d0184e
#
# Commands:
#  p, pick = use commit
#  e, edit = use commit, but stop for amending
#  s, squash = use commit, but meld into previous commit
#
# If you remove a line here THAT COMMIT WILL BE LOST.
# However, if you remove everything, the rebase will be aborted.
#

ここで、D のコミットをなかったことにしたい場合、D のコミットを表す行を消してしまいます*3

pick 82ad00d E

# Rebase 6d0184e..82ad00d onto 6d0184e
#
# Commands:
#  p, pick = use commit
#  e, edit = use commit, but stop for amending
#  s, squash = use commit, but meld into previous commit
#
# If you remove a line here THAT COMMIT WILL BE LOST.
# However, if you remove everything, the rebase will be aborted.
#

これを保存してエディタを終了すると、

          E *b
         /
A---B---C a

このようにコミット D を「なかったことに」できます。


また、rebase -i ではブランチの「親」を変えないことにも意味がある、ということが分かります。

消してしまった歴史を「復活」させる

コミットをなかったことにできることは分かりました。
でも人間は失敗する生き物です。もし間違ってコミットを消してしまった場合はどうすればいいのでしょうか?
例えば、さっきの操作は実は間違いだったという場合はどうすればいいのでしょうか?


Git には、このように操作を間違ってしまった場合でも簡単に昔の状態を取り戻すことができます。
今の状態は

          E *b
         /
A---B---C a

でしたが、実は Git には昔の状態も格納されていて、

         E *b
        /
        | D---E
        |/
A---B---C a

のようになっているのです。
あとは、ブランチ b を前の状態に git reset で戻してあげるだけです。
前の状態を調べるためには、git reflog というコマンドを使うのが便利です。

$ git reflog
0a4ccff HEAD@{0}: commit: E
6d0184e HEAD@{1}: checkout: moving from b to 6d0184e28844715ba70aa413a50424296a54d38f
82ad00d HEAD@{2}: commit: E
d6a3a28 HEAD@{3}: commit: D
6d0184e HEAD@{4}: checkout: moving from a to b
6d0184e HEAD@{5}: commit: C
edfab32 HEAD@{6}: commit: B
5ad4876 HEAD@{7}: commit (initial): A

これを見ると、どうやら HEAD@{2} の地点に戻ればよさそうです。

$ git reset --hard HEAD@{2}

これで、元通りです。

          D---E *b
         /
A---B---C a

転ばぬ先の杖

例えば失敗しそうなことがあらかじめ予想できるような rebase を行う場合には、「保険」をかけておくと便利です。
先ほどの例をもう一度使うと、最初は

          D---E *b
         /
A---B---C a

の状態でした。
この時に、ブランチなりタグなりを作っておき、これを「マーカー」として使用します。
例えば、

$ git branch base
          D---E *b
         /       base
A---B---C a

と新しいブランチを作成してから rebase を行います。すると、

         E *b
        /
        | D---E base
        |/
A---B---C a

このような状態になるので、reflog で確認しなくても

$ git reset --hard base

とするだけでもとの状態に戻れます。

ブランチとタグ

上では「ブランチなりタグなり」と書きましたが、ブランチとタグのどちらを使えばいいのでしょうか?
Git でのタグは Subversion とは違い、読み取り専用で、一度設定したら動かすことはできません*4
一方ブランチは、そのブランチに切り替えて git reset により動かすことができますが、ブランチはマージしないと削除できません*5


「保険」として使うと言うことは、後でほぼ確実に作った「マーカー」は消すので、タグの方がいいでしょう。
タイプ数も少ないですし。
上の例でブランチを使ったのは、単にタグとブランチを AA で書き分けるのが面倒だっただけで、深い意味はありません。

コミットの入れ替え

行を消すことでコミットを消すことができました。
同じように、行を入れ替えることでコミットを入れ替えることができます。

          D---E
         /
A---B---C a
$ git rebase -i a
pick d6a3a28 D
pick 82ad00d E

# Rebase 6d0184e..82ad00d onto 6d0184e
#
# Commands:
#  p, pick = use commit
#  e, edit = use commit, but stop for amending
#  s, squash = use commit, but meld into previous commit
#
# If you remove a line here THAT COMMIT WILL BE LOST.
# However, if you remove everything, the rebase will be aborted.
#

ここまではいいですね?
ここで、1 行目と 2 行目を入れ替えてみます。

pick 82ad00d E
pick d6a3a28 D

# Rebase 6d0184e..82ad00d onto 6d0184e
#
# Commands:
#  p, pick = use commit
#  e, edit = use commit, but stop for amending
#  s, squash = use commit, but meld into previous commit
#
# If you remove a line here THAT COMMIT WILL BE LOST.
# However, if you remove everything, the rebase will be aborted.
#

これで保存してエディタを終了すると、

          E---D
         /
A---B---C a

のように、コミットの順番を入れ替えることができます。

コミットの圧縮

複数のコミットをひとつにまとめてしまうこともできます。
エディタが立ち上がった際、行の先頭に pick とありましたが、ここを squash、もしくは単に s とすることで、一つ前のコミットとまとめてしまえます。

pick d6a3a28 D
squash 82ad00d E

# Rebase 6d0184e..82ad00d onto 6d0184e
#
# Commands:
#  p, pick = use commit
#  e, edit = use commit, but stop for amending
#  s, squash = use commit, but meld into previous commit
#
# If you remove a line here THAT COMMIT WILL BE LOST.
# However, if you remove everything, the rebase will be aborted.
#

こうすることで、E と D のコミットを圧縮します。
再びエディタが立ち上がるので、コミットメッセージを修正して保存し、終了します。

          F
         /
A---B---C a

D と E が圧縮され、F というコミットになりました。

コミットメッセージの編集

コミットメッセージを編集するためには、pick の部分を edit、もしくは単に e とします。
こうすると、そのコミットを新しいブランチに適用する段階で止まりますので、いろいろな編集が可能になります*6

e d6a3a28 D
pick 82ad00d E

# Rebase 6d0184e..82ad00d onto 6d0184e
#
# Commands:
#  p, pick = use commit
#  e, edit = use commit, but stop for amending
#  s, squash = use commit, but meld into previous commit
#
# If you remove a line here THAT COMMIT WILL BE LOST.
# However, if you remove everything, the rebase will be aborted.
#

こうすることで、E のコミットを適用する段階で止まります。ここで、

$ git commit --amend

とすることでエディタが立ち上がり、コミットメッセージを修正することができます。
その後、

$ git rebase --continue

として rebase を続けます。

          D'---E
         /
A---B---C a

コミットの分割

分割はちょっと面倒なので、後で書きます。書かないかも。

歴史の書き換えができるのは自分の環境だけ

ここまで書いてきたことは、自分のリポジトリに対してのみ行えます。
他人のリポジトリの内容を勝手に書き換えることはできません。
また、一度他人に公開してしまった歴史の書き換えは行わないようにしましょう。
例えば、

A---B---C

という歴史をリモートに git push で公開したとします。
ここで、B のコミットが不要だったことに気付いたとして、次のように書き換えたとしましょう。

A---C

しかし、これを push する前に誰かが元の状態を取得したとします。
そして、その誰かが新たなコミットを追加し、

A---B---C---1

という歴史を作っていたとします。
このコミット 1 の内容がとても素晴らしいので、自分のリポジトリにもこれが欲しい!と思っても、マージの場合は

  C-----------D
 /           /
A---B---C---1

となり、コミット D の内容には消したはずのコミット B の内容も含まれてしまいますし、コミット C はどちらのルートにも含まれるという状態になってしまいます。
リベースの場合でも、

A---B---C---1---C

と、同じ問題があります。
なので、一度公開した歴史は書き換えないようにしてください。もし、過去の公開してしまったコミットを打ち消したいなら、代わりに revert を使います。
revert は、指定したコミットを「打ち消すコミット」を作ります。

おわりに

Git ではあることを実現するために複数の方法があることがよくあります。
今回は rebase を中心に説明しましたが、特殊な状況下では他のコマンドを使った方が効率がいい場合も多々あることを忘れないでください。
例えば、一つ前の修正をしたいだけなら git commit --amend、あるコミットを取り込みたいだけなら git cherry-pick、などといった感じです。
知れば知るほどより便利に使えるようになっていくので、是非どんどん使ってみてください。

*1:実際は現在のブランチ以外のブランチも指定可能

*2:一本になっている歴史を分岐させることも rebase では可能ですが、ここでは触れません

*3:# から始まる行はコメントなので、無視される

*4:削除することはできるので手動で動かすことはできる

*5:-d ではなく -D を使えば可能

*6:コミットメッセージの編集だけでなく、コミットの分割やコミット内容の変更などもできる