git rebaseのメモ
ときどき間違うので。
大雑把に言うと、git rebase は「git reset + git cherry-pick × n回 を自動化したもの」と考えられる(適用するコミット群が少なければ、手動でreset & cherry-pickしても良いが、たくさんあるとそうもいかない)
好きな場所にresetして、好きな位置から好きな位置までのコミットを順次適用できる。
つまりコミットを並べ替えたり除外したり、「積み木を積み直す」ようなことが出来る。
git rebase
ポピュラーな使い方。
- 現在のブランチを
にreset から見て現在のブランチにだけ存在していたコミットを順に適用
適用されるコミット群は、
以下の例だと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
--onto を付けると、ちょっと考えないと何だか分からなくなってくる。
- 現在のブランチを
にreset から見て現在のブランチにだけ存在していたコミットを順に適用
最初に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をカレントのブランチにして、
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。