git rebase -iとgit rebaseをちゃんと理解する

git rebase -iコマンドについてのメモです。 これまで、単に直前のnコミットをくっつけたり消したりして整理できる便利コマンドとして

$ git rebase -i HEAD~n

と叩いていましたが、なぜこれがrebaseなのかを理解していませんでした。

自分のこれまでの理解が狭かったという話で、同じような状況の方がどれだけいるのかわかりませんが、もしかすると役に立つかもしれないのでメモを書いてみます。

git rebaseで何が起こるのか

まず、git rebaseすると何が起こるかを理解しました。

そのためにちょっと腰を据えて https://git-scm.com/docs/git-rebase#_description を読んでみると、git rebase <upstream>は以下のことをすると書いてあります:

  1. 現在のブランチにはあるが、<upstream>には無いコミットが一時退避される
  2. <upstream>に移動
  3. 1.で退避したコミットを再適用

図で例を示します。 topicブランチ上でgit rebase masterすると

(Before)
          A---B---F---C topic
         /
    D---E---F---G master

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

こんな感じで、確かに

  1. topicにはあるが、masterには無いコミット(AとBとC)が一時退避される
  2. masterに移動
  3. 1.で退避したコミットを再適用

となっています。

なお、Fはmasterにも含まれるので、1.の一時退避からは省かれます。 これは例えば、masterに入ったリファクタコミットをトピックブランチにもcherry-pickして取り込んだような場合にあたります。

git rebase -i との関係

準備ができたので、話をgit rebase -iに戻します。

簡単に言えば、git rebase -iするとエディタが立ち上がってコミットのリストが編集できますが、そこに表示されるコミットのリストが、前項の1.で一時退避されたコミット群そのものになる、ということが大事でした。

なぜこれまでこのことを意識しなかったのかな?と考えると、 git rebase -i HEAD~nのようなHEADからの相対指定の場合、rebaseする範囲にブランチの分岐がなく、Fのようなコミットがありえないため、rebase感が薄かったためかな、と思います。

例で考えてみます。master上でgit rebase -i HEAD~3すると、

    A---B---C---D master
  1. masterにはあるが、master~3(つまりA)には無いコミット(BとCとD)が一時退避される
  2. master~3に移動
  3. 1.で退避したコミットを再適用

となります。

これは、-iオプションを付けなければ、必ずBefore/Afterは同じ結果になることが想像できると思います(実際叩いてみるとわかりますが、Current branch master is up to date.などと表示されるだけです)。

-iオプションを付けることで、3.でにコミットログを改変する手順が追加される感じですね。

応用例・まとめ

以上を頭に入れれば、topicブランチ上でgit rebase -i masterと叩くことで、トピックブランチをメインブランチに追従させつつ、

  • トピックブランチのコミットをまとめたりして整理
  • 途中で入れた共通のcherry-pickやマージコミットを除去

という2つのことを一括して実行できる、とわかりました。 やむを得ずトピックブランチを長期間使わないといけないときに役に立ちそうです。

チーム開発でGitミスすると怖い、という気持ちは誰もが持つものだと思うので、このように仕組みからちゃんと理解することはやはり大事だなと思いました。