いものやま。

雑多な知識の寄せ集め

Gitによる歴史改変。(その1)

バージョン管理システムとして一般的になった(と信じたい)Git。

チームで開発するときに限らず、個人で何か作業するときにもすごく便利なので、よくお世話になってる。
(実際、変種オセロのコードを書いてるときに使ってた)

Gitの基本的な考え方、使い方や、模範的なフローについてはまた別途扱うとして、今回扱うのはGitによる歴史改変の話。

リベース

Gitで歴史改変をするときに使う定番コマンドが、リベース(rebase)。

ただ、一言でリベースと言っても、実際には何通りかの使い方があるので、意外とヤヤコシイ・・・

そこで、主な使い方についてまとめてみた。

リベースの考え方

まずおさえておきたい重要なこと。
それは、リベースとはパッチを順番に当てていく操作だということ。
これを理解しておくだけで、だいぶ違ってくる。

パッチを順番に当てていくとなると、3つの要素が出てくる。
それは、

  1. パッチを当てる対象となるコミット
  2. パッチの開始地点となるコミット
  3. パッチの終了地点となるコミット

の3つ。

リベースを行うと、パッチを当てる対象となるコミットに、パッチの開始地点となるコミットからパッチの終了地点となるコミットまでの各コミットの差分が、順番に当てられていくことになる。

Gitの言葉では、この3つの要素をそれぞれ

  1. newbase
  2. upstream
  3. branch

と呼んでいる。

パッチの終了地点となるコミットをbranchと呼んでいることからも分かる通り、「リベースの対象となるブランチ」とは、パッチの終了地点となるコミットを指しているブランチのことを言う。
リベースを行うと、パッチの終了地点のコミットを指していたブランチは、パッチ適用後の終了地点のコミットを指すように変更される。
これにより、元々はupstream→(いくつかのコミット)→branchという流れだったのが、newbase→(いくつかのコミット)→branchという流れに変更されることになる。
これは、見ての通り、ブランチの根元が変更されていることになってる。
それゆえ、リベースというわけだ。

リベースの実行方法

基本的な使い方

リベースの基本的な形は、以下。

$ git rebase <newbase>

このとき、3つの要素はそれぞれ、

  1. <newbase>で指定されたコミット
  2. <newbase>で指定されたコミットと現在のブランチの共通の親
  3. 現在のブランチの指すコミット

となる。

なので、このコマンドを実行すると、引数で指定されたコミットと現在のブランチの共通の親から現在のブランチに至るまでのコミットが引数で指定されたコミットの後ろに順番に適用されていき、その一番最後が新しい現在のブランチになる。

例えば、次のような感じ。(※HEADは現在のブランチ)

[元のコミットグラフ]
           HEAD
           topic
        E---F
       /
  A---B---C---D
            master
<-old                    new->

<リベースを実行>
$ git rebase master

<実行後のコミットグラフ>
                   HEAD
                   topic
                E'--F'
               /
  A---B---C---D
            master
<-old                    new->

見ての通り、結果としてコミットグラフがキレイな一直線になる。

リベース対象のブランチを指定する場合

一番基本的な形では、現在のブランチがそのままリベースの対象のブランチとなるけど、現在のブランチ以外をリベース対象のブランチとして指定したい場合もある。
その場合、一つの方法としてあるのは、リベースしたいブランチをチェックアウトして、それからリベースを行うという方法だけど、次のようにしてもいい。

$ git rebase <newbranch> <branch>

こうすると、3つの要素はそれぞれ、

  1. <newbase>で指定されたコミット
  2. <newbase>で指定されたコミットと<branch>の共通の親
  3. <branch>で指定されたコミット

となる。

なお、この場合、一番最初にbranchをチェックアウトして、基本的な形のリベースを適用したことになるので、終わった時点ではbranchをチェックアウトした状態になってる。

一例をコミットグラフでみると、こんな感じ。

<元のコミットグラフ>
           topic
        E---F
       /
  A---B---C---D
            master
             HEAD
<-old                    new->

<リベースを実行>
$ git rebase master topic

<実行後のコミットグラフ>
                   HEAD
                   topic
                E'--F'
               /
  A---B---C---D
            master
<-old                    new->

現在のブランチがリベース対象のブランチじゃなくても動作し、リベース後は現在のブランチがリベース対象のブランチに移っていることが分かるかと思う。

パッチの開始地点を指定する場合

基本的な形では、パッチの開始地点はnewbaseと現在のブランチの共通の親だったけれど、場合によっては、パッチの開始地点を指定したい場合もある。
その場合、--ontoというオプションを使う。

$ git rebase --onto <newbase> <upstream> [<branch>]

こうすると、3つの要素はそれぞれ、

  1. <newbase>で指定されたコミット
  2. <upstream>で指定されたコミット
  3. <branch>で指定されたコミット(指定なしなら現在のブランチの指すコミット)

となる。

使い方の一例としては、次のような感じ。

<元のコミットグラフ>
                G---H
              /    topic
         E---F
        /  develop
       /
  A---B---C---D
            master
             HEAD
<-old                    new->

<リベースを実行>
# topicの開発をdevelopでなくmasterへ移行する
$ git rebase --onto master develop topic

<実行後のコミットグラフ>
           develop
         E---F
        /       G'--H'
       /       /   topic
  A---B---C---D    HEAD
            master
<-old                    new->

このように、--ontoを使うとコミットグラフの枝を思い通りに移動させることが出来る。
これだけでも、リベースがかなり強力なツールであることが分かると思う。

さらに強力なコミットの細かい指定や、リベース中のコントロールについては、また明日ということで。

今日はここまで!