Hatena::ブログ(Diary)

unpushの日記

2010-05-14

git rebaseのメモ

| 15:04

ときどき間違うので。

大雑把に言うと、git rebase は「git reset + git cherry-pick × n回 を自動化したもの」と考えられる(適用するコミット群が少なければ、手動でreset & cherry-pickしても良いが、たくさんあるとそうもいかない)

好きな場所にresetして、好きな位置から好きな位置までのコミットを順次適用できる。

つまりコミットを並べ替えたり除外したり、「積み木を積み直す」ようなことが出来る。


git rebase <upstream>

ポピュラーな使い方。

  1. 現在のブランチを<upstream>にreset
  2. <upstream>から見て現在のブランチにだけ存在していたコミットを順に適用

適用されるコミット群は、<upstream>から見て現在のブランチにだけ存在していたコミット、つまりgit log <upstream>..HEAD で出てくるコミット

以下の例だとA、B、Cのコミットがreset後に適用される予定

      A---B---C topic
     /
D---E---F---G master

topicブランチで git rebase master すると、

              A'--B'--C' topic
             /
D---E---F---G master

こうなる。mergeコミットを作らずにmasterに追いついた。

このときA'、B'、C'はそれぞれA、B、Cをそのままコミットし直したものなので差分もログメッセージも同じだが、親が違うので違うモノとして生まれ変わったことになる(SHA1ハッシュが変わる)

一方、git rebase master~1 とすると、

          A'--B'--C' topic
         /
D---E---F---G master

こうなる。master~1(F)めがけてreset、そしてA、B、Cを適用。

もしもAのパッチが既にmasterに取り込まれてたりした場合(A'はA)

      A---B---C topic
     /
D---E---A'---F master

Aはスルーしてくれる。うまい。

               B'---C' topic
              /
D---E---A'---F master

git rebase --onto <newbase> <upstream>

--onto を付けると、ちょっと考えないと何だか分からなくなってくる。

  1. 現在のブランチを<newbase>にreset
  2. <upstream>から見て現在のブランチにだけ存在していたコミットを順に適用

最初にresetする位置と、その後適用するコミット群を決める位置を、別々に指定出来る。

masterブランチの途中からフォークしているnextブランチがあり、topicブランチはさらにこれをいじくっているとする。

            A---B---C  topic
           /
      *---* next
     /
o---o---o---o---o  master

ここでよく考えたら、実はtopicブランチでやっていることはnextブランチの内容に依存していないので、masterブランチから直で伸ばしても問題ないことに気づいた。なので、topicブランチの根っこを、nextブランチからmasterブランチに切り替えたい。

topicブランチで git rebase --onto master next とすると、こうなる。

          next
      *---*       A'--B'--C' topic
     /           /
o---o---o---o---o  master

行われることは、まずmasterにreset。そして、nextブランチに無くてtopicブランチにだけ存在していたコミット(A, B, C)を、順に適用。

このやり方だと、好きな場所にresetして、好きな位置から好きな位置までのコミットを順次適用できる。

履歴を積み直す例

こういう状況で、

$ git log --oneline master
ed1c0e6 新機能追加4
29f3b9f 新機能追加3
5db3e92 新機能追加2
0868829 新機能追加1
b2d6bcf v1.0.0

よっしゃ公開しちゃおうかな、というところで、新機能追加2に痛恨のバグを発見。

しかしまだ公開していないので、こっそり修正するチャンスです。

修正したいコミットをチェックアウト

$ git checkout master~2
とか
$ git checkout 5db3e92

修正したら、コミットを改竄

$ git commit -a --amend -m '新機能追加2(こっそり修正)'

こうなりました

$ git log --oneline
b762ac0 新機能追加2(こっそり修正)
0868829 新機能追加1
b2d6bcf v1.0.0

やっぱよく考えたら新機能2.1も入れたくなったのでさらにコミット

$ git commit -a -m '新機能追加2.1(後から追加)'

さらにこうなりました

$ git log --oneline
706a17d 新機能追加2.1(後から追加)
b762ac0 新機能追加2(こっそり修正)
0868829 新機能追加1
b2d6bcf v1.0.0

さて、この後に新機能追加3と新機能追加4が来てほしい。

新機能追加3と4は、master~2..masterで取り出せる。そのためには、masterをカレントのブランチにして、<upstream>にmaster~2を指定する。

rebaseで<新機能追加2.1>にリセット、そしてmaster~2..masterのコミットを適用するには、

git rebase --onto HEAD master~2 master

こうする。最後に指定している master は、そのブランチをチェックアウトしてから rebase するという、rebaseのオプション

  • masterをチェックアウト
  • 「HEAD=706a17d 新機能追加2.1(後から追加)」にreset
  • master~2には無くてmasterにのみ存在するコミット(master~2..masterのコミット)を適用

結果こうなる

$ git log --oneline
b08b0a7 新機能追加4
2d2ecbc 新機能追加3
706a17d 新機能追加2.1(後から追加)
b762ac0 新機能追加2(こっそり修正)
0868829 新機能追加1
b2d6bcf v1.0.0

事前にmasterをチェックアウトしてからやるならば、最後のオプションでmasterを指定しなくてもよいが、その場合HEADと指定するとmasterのHEADになってしまうので、ブランチを作っておくかSHA1ハッシュを直接指定するなどが必要。

$ git checkout master
$ git rebase --onto 706a17d master~2

こういう感じ。

でもrebaseは特に--onto付けるとドキドキする

うっかりヘンな指定すると思いもしない方向にがーっと突き進んでしまうので、個人的には名無しブランチでやるのが好きです。

先ほどの例だと、

$ git rebase --onto HEAD master~2 ed1c0e6

こういう感じで(ed1c0e6はmasterのSHA1ハッシュ)。これだと ed1c0e6 を直接チェックアウトするので、masterはとりあえず影響なし。

心配なら git diff master.. などやって、予測通りの結果になっているかを確認して(コミットの並べ替えだけなら差分は無いはず等)、OKならmasterを名無しブランチにreset。