git log と git rebase の濃密な関係

この投稿は [Fujitsu Advent Calendar 2017 その2](https://qiita.com/advent-calendar/2017/fujitsu_part2) の 5日目 の記事です。
この記事に書かれた見解は、個人のものであり、所属する会社・組織を代表するものではありません。

---

その2がまだ空いているので、以前社内の Blog に書いた記事を、転載で恐縮ですが、載せておきます。
なお、その1 に書いた記事がですます調だったのにこっちがである調になっているのは、時間の都合です。

## 1. git log の revision range (リビジョン範囲)

Git で、コミット A から コミット B までのログを見るときは、こういうコマンドを叩く。

```console
git log A..B
```

A, B は、それぞれコミットID だったりブランチだったりタグだったり。
履歴が、こういう具合にまっすぐなときなら、素直に理解できる。
(A から B と言っているのに、コミット A が表示されないのには、若干違和感を感じるかもしれないが。)

    o---o---A---o---o---B

では、こういう具合に分岐している履歴に、`git log A..B` とやったときに表示されるログの範囲どうなるのか？

          o---o---A
         /
    o---C---o---o---B

正解は、`git log C..B` とやったときと同じ。前項と同様に、コミット C は表示されない。
          
         ---o---o---B
         
Git を使い始めた頃は、なぜこうなるか判らなかったり、分かっても覚えられなくて、思うようにログを出せなかった。
実は [Git のドキュメント](https://git-scm.com/docs/gitrevisions#_dotted_range_notations)をよく読むと書いてあるのだが、

```console
git log A..B
```

は、

```console
git log ^A B
```

と同じ。つまり「B から到達可能なコミットを含み、A から到達可能なコミットを含まない」という意味。
(前述のドキュメントではむしろ「^A B は、A..B とも書ける」という言い方になっている。)
さっきの例で説明しよう。

          o---o---A
         /
    o---C---o---o---B

この履歴で「B から到達可能なコミット」は、こうなる。
         
    o---C---o---o---B

「A から到達可能なコミット」はこう。

          o---o---A
         /
    o---C

したがって、「B から到達可能で、A から到達可能でない」コミット、つまりgit log A..Bで表示されるコミットは、こうなるわけだ。
          
         ---o---o---B

ここで C が表示されない理由や、まっすぐな履歴

    o---o---A---o---o---B

で `log A..B` とやったときに A が表示されないのも、`log ^A B` と読み換えてみれば、納得だと思う。

## 2. git rebase

[git rebase](https://git-scm.com/docs/git-rebase) のドキュメントにはこう書いてある。

```console
git rebase [--onto <newbase>] [<upstream> [<branch>]]
```

これが実にわかりにくい。`<branch>` にどのコミットまで含まれるのかが不明確だし、そもそも、`<upstream>` と `<branch>` のどっちがどっちかなかなか覚えられない。間違えて master をフィーチャーブランチの先に乗せてしまったこともしばしば。
実はこれも、前述の `git log A^ B` と同じように理解すれば、問題が解決するんである。つまり、

```console
git log A..B
```

と同じように、

```console
git rebase A B
```

を「B から到達可能で、A から到達可能でないコミットを、A へリベースする」と理解すればいい。
(ただし、この場合は A には、`..` も `^` も付けないことに注意。)
例で説明しよう。
さっきと同じこういう履歴

          o---o---A
         /
    o---C---o---o---B

で、

```console
git rebase A B
```

とやると、「B から到達可能で、Aから到達可能でないコミット」、つまり `git log A..B` とやったときと同じ
         
         ---o---o---B

を、A へリベースして、こうなるわけだ。

          o---o---A---o---o---B
         /
    o---C

## 3. 余談

```
git rebase --onto X A B
```

とやると、A 以外のコミット X へリベースすることもできる。
あんまり使い道がなさそうに思えるが、実はひとつ、覚えておくと便利な使い途がある。

フィーチャーブランチに入れるはずだったのに、チェックアウトしておくのをうっかり忘れて、master にいるまま修正を始めてしまった、というのをよくやってしまう。
コミットする前ならば、慌てず騒がず、

```
git stash
git checkout feature
git stash pop
```

すれば済む。が、修正した挙げ句コミットまでしてしまったらどうすればいいんだ？
それでも慌てて騒ぐ必要はない。`git rebase --onto X` を使えばいいんである。

例で説明しよう。

          o---o---A
         /
    o---C---o---o---B

というブランチがあったときに、

          o---o---A---o---A'
         /
    o---C---o---o---B

のように A に入れたかった一連のコミットを

          o---o---A
         /
    o---C---o---o---B---o---A'

のように、間違えて B に入れてしまったとしよう。
(※ほんとはブランチが `---元B---o---B` になるんじゃ? というツッコミはご容赦の程を.... 説明の都合なので....)
本来の A へ移動させたいコミットは、

                     ---o---A'

である。これは、「A' から到達可能で、B から到達可能でないコミット」、言い換えると log を見るときに `B..A'` とやったのと同じなので、rebase のパラメータとして書くと `B A'`。これを、A へ移動させればよいので `--onto A`。
まとめて

```
git rebase --onto A B A'
```

とやると、

          o---o---A---o---A'
         /
    o---C---o---o---B

となる。めでたし、めでたし。