2016-10-12

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

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

結論から言うとrebaseはリビジョンの 一括コピーリファレンス の張替です。

gitはブランチを自由に張り替えることができるので新たにリビジョンを作り直しても あたかも同じブランチが書き換わったかのように見せることができます。

この記事の操作は手元で再現することができます。

GitHub - righ/gittestContribute to righ/gittest development by creating an account on GitHub.https://github.com/righ/gittest

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点だと思います。

  1. 引数には移動 を指定する

    • cherry-pickmergeリビジョンを持ってくる 操作であるのに対し rebaseリビジョンを持っていく ということを表しています。
  2. チェリーピックと違いリベースではカレントブランチが自動的にリベース先に移動される

    • このときタグは移動しないので注意してくださいね。
  3. リベースは共通祖先までさかのぼったリビジョン差分を 古い順に一つずつ適用 する

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

操作

rebaseが差分をひとつずつ適用するという特性上、リベースの途中で操作が求められることがあります。

例えばコンフリクトが発生した時や、後述するインタラクティブオプション --interactive/-i を指定した場合です。

ここらでその操作について見ておきましょう。

--continue

リベースを続ける場合に指定します。

変更したファイルがあれば git add xxxx で追加する必要があります。

--skip

「--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 squash 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 オプションはこのように範囲を限定してリベースしたいときに適してます。使いこなしてドヤ顔しましょう。

image0

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

疲れた。終了。

参考

図解Gitコミットの修正まとめ git reset, cherry pick, revert, commit --amend, rebase - Qiitaオライリー・ジャパン『実用Git』を最近読み終えたので、コミットの修正方法について、簡単に実例を交えてつらつらとまとめておく。過去にマージについて「Gitマージの基本」としてまとめたので興味があ…https://qiita.com/genre/items/8bc0c9058a69a3d6de97 あのコミットをなかった事に。git rebase -i の使い方以前、Gitの使い方、よく使うGitコマンド という記事を書きましたが、git rebase -i の項目に書き足したい ...https://www.karakaram.com/git-rebase-i-usage/