git rebase の動作を --onto 指定も含めて正しく理解する

# git rebase の引数と動作解説

## はじめに

リベースコマンド実行の大半は `git rebase main` で済みますが、それはrebaseコマンドの引数省略形に過ぎません。すこし複雑なリベースをしようとすると悩んでしまうのは、引数省略しない使い方を理解していないためです。

本記事は、https://git-scm.com/docs/git-rebase に記載されている説明を、厳密な正確性よりも分かりやすさを優先して書き換えたものです。
git reabse の使い方に悩む方々の参考になればと思い公開します。
これを読めば思い通りのリベースが出来るようになるでしょう。

## コマンドライン指定形式
```
git rebase [--onto 接続先] [切り取り開始点 [切り取り終了点]]
```

`切り取り開始点`から`切り取り終了点`までの複数コミットを、`接続先` へ付け替えるのがリベースの動作であり、私たちが日頃良く使うのは `--onto 接続先` と `切り取り終了点` の引数を省いた形なのです。

例えば `git rebase main` は `git rebase --onto main main current-branch` の省略形です。`切り取り開始点`がmainで、`接続先`もmainでは何も起きないのではないか。と思われるでしょうが、その疑問については [後の章](#先の疑問の答え) にて説明します。
先に引数を省くと何が補われるのかを説明しましょう。

## 引数省略時の説明

### 切り取り開始点（upstream）
省略時は`切り取り終了点（branch）`に設定されたリモートブランチ（上流ブランチ）の最新コミットを使う。

### 切り取り終了点（branch）
省略時はカレントブランチの最新コミットが対象となる。

### 接続先（newbase）
省略時は`切り取り開始点（upstream）`を使う。

## 動作
```
git rebase --onto 接続先 切り取り開始点 切り取り終了点
```
このgitコマンドは下記ステップで実行されます。

1. `切り取り終了点` にスイッチし、それをカレントブランチとする。
`git switch 切り取り終了点`
   - `切り取り終了点`にブランチ名を指定していない場合は、そこを最終コミットとする仮ブランチとして扱う。
   - この時点で`切り取り終了点`が`HEAD`になる。
   - `切り取り終了点`にカレントブランチを指定した場合、この段階では何も変化しない。
2. `切り取り終了点`から遡る全履歴コミットのうち、`切り取り開始点`から遡る全履歴に**含まれていない**コミットのリストである `git log 切り取り開始点..切り取り終了点`（便宜上、**差分コミット**と呼ぶ）を付け替え対象として退避する。
   - `切り取り開始点`にブランチ名を指定した場合、そのブランチの最新コミットが開始点である。
3. カレントブランチを`接続先`に強制リセットする
`git reset --hard 接続先`
   - `切り取り終了点`として指定したブランチの履歴はこの時点でリセットされてしまう。
   - `切り取り終了点`は`ORIG_HEAD`に記録されるので、`git reset --hard ORIG_HEAD` にて復旧可能である。
4. カレントブランチに対して、上記2で退避した**差分コミット**を順番に１つづつ再コミットしてブランチを伸ばしていく。
   - ただし、`git log 切り取り開始点..接続先`に含まれる各コミットと修正差分が同一のコミットはスキップされる。要するに`cherry-pick`で取り込んだ**接続先と重複する内容**は再コミットされない。
5. 退避した差分コミットを全て処理したら動作完了です。

### ここが肝心
上記2にて両者の履歴に**共通して含まれない**コミットを差分コミットとし、上記4にて**重複する内容**を再コミットしないことが、リベース動作を正しく理解するための肝です。

### 先の疑問の答え
先の疑問である「`git rebase --onto main main current-branch` は `切り取り開始点`がmainで`接続先`もmainなので何も起きないのではないか」について答えます。

 - カレントブランチから遡る全履歴コミットのうち、`切り取り開始点`に指定したmainブランチの全履歴に**含まれない**コミットのリストが**差分コミット**となる。それはカレントブランチがmainブランチから分岐した後のコミット履歴と等しい。
 - `接続先`がmainブランチなので、mainブランチの最新コミットが接続先となる。
 - つまり、元のカレントブランチがmainブランチから分岐した後のコミット履歴を、mainブランチの最新コミットに接続したものが新しいカレントブランチとなるという、リベース動作になる。

### 豆知識
`git rebase -i` にてユーザに示されるコミット履歴は、上記4にて重複をスキップした**差分コミット**です。

## `切り取り開始点（upstream）` 省略についての厳密な説明（読まなくて良いです）

> 省略時は`切り取り終了点（branch）`に設定されたリモートブランチ（上流ブランチ）の最新コミットを使う。

という説明は厳密には違います。
正確には`切り取り終了点（branch）`に `git branch --set-upstream-to=` で設定された追跡ブランチが使われます。
そして追跡ブランチの設定が無い場合にはリベースは中止されます。

大半のケースでは `git push -u` を実行することにより「リモートブランチ」が追跡ブランチとして設定されているか、追跡ブランチが設定されていません。よって実質的には「リモートブランチ」を使うか、リベース中止になるかの二択となります。 `git branch --set-upstream-to=main mybranch` として追跡ブランチとしてローカルなmainブランチを設定すると `git rebase main` のかわりに `git rebase` とmainを省略することもできます。

## 具体例

### カレントブランチを、mainの最新コミットから新規ブランチしたものとして再生成する.
```
git rebase main
```
1. `main..HEAD` の差分コミットを退避し
2. カレントブランチをmain最新コミットに強制リセットし
3. 退避した差分コミットを適用する.

```mermaid
gitGraph TB:
    commit id: "M1"
    commit id: "M2"
    branch featX
    commit id: "F1"
    commit id: "F2"
    branch current
    commit id: "C1"
    commit id: "C2" type:HIGHLIGHT tag:"HEAD"
    checkout featX
    commit id: "F3"
    checkout main
    commit id: "M3"
    commit id: "M4"
```
`main..HEAD` の差分コミットは `F1,F2,C1,C2` である。
```mermaid
gitGraph TB:
    commit id: "M1"
    commit id: "M2"
    branch featX
    commit id: "F1"
    commit id: "F2"
    branch detached order: 2
    commit id: "C1"
    commit id: "C2"
    checkout featX
    commit id: "F3"
    checkout main
    commit id: "M3"
    commit id: "M4"
    branch current
    commit id: "F1'"
    commit id: "F2'"
    commit id: "C1'"
    commit id: "C2'" type:HIGHLIGHT tag:"HEAD"
```
カレントブランチをmain最新コミット`M4`に強制リセットし、
続いて差分コミットの内容を`F1',F2',C1',C2'`として再コミットしてゆく。
コミット`C1,C2`はどのブランチにも含まれていない残骸として残り、いずれは `git gc` により消去される。

### featXブランチを、mainの最新コミットから新規ブランチしたものとして再生成する.
```
git rebase main featX
```
1. カレントブランチを featX に切り替え
2. `main..featX` の差分コミットを退避し
3. featX をmain最新コミットに強制リセットし
4. 退避した差分コミットを適用する.

### featXブランチを、tag3から新規ブランチしたものとして再生成する.
```
git rebase --onto tag3 main featX
```
1. カレントブランチを featX に切り替え
2. `main..featX` の差分コミットを退避し
3. featX をtag3コミットに強制リセットし
4. 退避した差分コミットを適用する.

### featXブランチを、tag3から新規ブランチして `featX~3..featX` の内容で再生成する.
```
git rebase --onto tag3 featX~3 featX
```
1. カレントブランチを featX に切り替え
2. `featX~3..featX` の差分コミットを退避し
3. featX をtag3コミットに強制リセットし
4. 退避した差分コミットを適用する. featX~3以前のコミット `main..featX~4` は適用されない.

## 参考資料
https://zenn.dev/yoichi/articles/git-dotted-notations
 
# END