git rebaseでコミットをまとめたり分割する

Gitを使っていると、1つの機能追加が複数のコミットに分かれていたり、1つのコミットに関係ない修正が2つ以上含まれてしまっていたりすることが良くあると思います。

個人的な理想はリリース用ブランチのコミット (のタイトル) を見るだけで、プロダクトにいつ・どんな修正が加わったか分かるようにしたいのですが、そのためにはコミットの粒度を適切に分割・統合する必要があります。

`git rebase`を使えば、連続していない複数のコミットを1つにまとめたり、1つのコミットを2つ以上に分割することが出来ます。

普段コミットのタイトルや粒度を意識してなかった人も、ぜひここで紹介するテクニックを使って、レビュー前にコミットを分かりやすく整理してみて欲しいと思います。

またこのテクニックを覚えると、余計なコミットを作るのを避けてギリギリまで`git commit`出来なかった人も、一時保存のために気軽に`git commit`が出来るようになると思います。


## rebase練習用リポジトリとコミットの作成
今回は実際にrebaseでのコミットの分割・統合を経験してもらえるよう、練習用のリポジトリとコミットを作って、これを操作する前提で解説していきます。

練習用のコミットして、下記のコミットを作っていきます。

* commit1: A機能用ソースファイル追加
* commit2: A機能の説明をREADMEに追記
* commit3: READMEにタイプミス発覚→修正
* commit4: B機能用ソースファイル追加
* commit5: B機能の説明をREADMEに追記
* commit6: B機能に仕様変更発生→ソース修正
* commit7: B機能の仕様変更は取り下げになりソースを元に戻す(commit6のrevert)
* commit8: A機能でバグ発覚→A機能のソース修正

```sh
#練習用リポジトリ作成
mkdir git-rebase
cd git-rebase/
git init

#first commit作成
touch README.md
git add README.md
git commit -m "first commit"

#開発用ブランチ作成 (メインブランチからforkした機能開発用のブランチ)
git switch -c develop
git branch
* develop         #←現在developブランチで作業していることを確認 (*がアクティブなブランチ)
  master

#commit1作成
echo "feat A" > featA.src
git add featA.src
git commit -m "commit1: A機能用ソースファイル追加"

#commit2作成
echo "fead A's README" >> README.md
git add README.md
git commit -m "commit2: A機能の説明をREADMEに追記"

#commit3作成
sed -i "s/fead/feat/g" README.md
git add README.md
git commit -m "commit3: READMEにタイプミス発覚→修正"

#commit4作成
echo "feat B" > featB.src
git add featB.src
git commit -m "commit4: B機能用ソースファイル追加"

#commit5作成
echo "feat B's README" >> README.md
git add README.md
git commit -m "commit5: B機能の説明をREADMEに追記"

#commit6作成
echo "feat B modified" > featB.src
git add featB.src
git commit -m "commit6: B機能に仕様変更発生→ソース修正"

#commit7作成
git revert HEAD
##エディタが開くので1行目を下記のように修正
commit7: B機能の仕様変更は取り下げになりソースを元に戻す(commit6のrevert)
##:wq or ZZで上書き保存してエディタを抜ける

#commit8作成
echo "feat A modified" > featA.src
git add featA.src
git commit -m "commit8: A機能でバグ発覚→A機能のソース修正"

#コミットグラフを確認
git log --graph --all --pretty="format:%C(red)%h%C(yellow)%d %C(reset)%s"
* b3efbfe (HEAD -> develop) commit8: A機能でバグ発覚→A機能のソース修正
* 9b13b8c commit7: B機能の仕様変更は取り下げになりソースを元に戻す(commit6のrevert)
* b522341 commit6: B機能に仕様変更発生→ソース修正
* 0ce0d64 commit5: B機能の説明をREADMEに追記
* 139de71 commit4: B機能用ソースファイル追加
* 25d6a63 commit3: READMEにタイプミス発覚→修正
* d21ffa1 commit2: A機能の説明をREADMEに追記
* d0290fa commit1: A機能用ソースファイル追加
* b7b9572 (master) first commit
```

最終的にリポジトリのファイル内容は下記の状態になります。
(`#`行が出力内容です。コマンドを区別するために頭に`# `を追加しています)

```sh
ls | sed "s/^/# /g"
# featA.src
# featB.src
# README.md

cat featA.src | sed "s/^/# /g"
# feat A modified

cat featB.src | sed "s/^/# /g"
# feat B

cat README.md | sed "s/^/# /g"
# feat A's README
# feat B's README
```

 
## rebase作業の前準備
今回はコミット履歴を改変するテクニックを紹介しますので、失敗すると元に戻せなくなるおそれがあります。
rebaseの途中であれば、`git rebase --abort`でなかったことに出来ますが、rebaseを完了させてしまうと後戻り出来ません。

そこで、現在のブランチはそのままに、rebase作業用のブランチを作ってそこで作業するのが安全です。
現在のブランチを残しておけば、作業後に元のブランチとrebase後のブランチをdiffして、差分が無いことを確認することも出来ます。

```sh
git branch
* develop         #←現在のブランチを確認 (*がアクティブなブランチ)
  master

#rebase作業用にworkブランチを作成してスイッチ
git switch -c work

git branch
  develop
  master
* work            #←現在workブランチで作業していることを確認 (*がアクティブなブランチ)

git diff develop work
#出力なし。まだ何もしてないので差分はない
#これからコミットをこねくり回すが、最終的にこのコマンドで差分が出ないことを確認する
```


## 複数のコミットをまとめたり順番を変えたりする
バラバラになっているコミットを適切な単位にまとめましょう。

整理前のコミットがこちら。

```sh
git log --graph --all --pretty="format:%C(red)%h%C(yellow)%d %C(reset)%s"
* b3efbfe (HEAD -> work, develop) commit8: A機能でバグ発覚→A機能のソース修正
* 9b13b8c commit7: B機能の仕様変更は取り下げになりソースを元に戻す(commit6のrevert)
* b522341 commit6: B機能に仕様変更発生→ソース修正
* 0ce0d64 commit5: B機能の説明をREADMEに追記
* 139de71 commit4: B機能用ソースファイル追加
* 25d6a63 commit3: READMEにタイプミス発覚→修正
* d21ffa1 commit2: A機能の説明をREADMEに追記
* d0290fa commit1: A機能用ソースファイル追加
* b7b9572 (master) first commit
```

これを下記のように整理します。

* commit1+2+3+8: A機能(バグ修正済み)とA機能の説明を含むREADME
  * A機能で1つにまとめる
  * タイプミスの修正やリリース前のバグ修正の履歴を公開する必要はないので削除する
* commit4+5: B機能とB機能の説明を含むREADME
  * B機能で1つにまとめる
  * 「リリース前に一度追加したけど削除した機能」の履歴を公開する必要はないのでcommit6,7は削除する

ここからrebaseに入りますが、途中で操作を間違えた場合は`git rebase --abort`を実行することでここまで巻き戻ることが出来ます。
abort後はここからやり直してください。

```sh
git branch
  develop
  master
* work            #←現在workブランチで作業していることを確認 (*がアクティブなブランチ)

#インタラクティブモードのrabaseを起動
##first commitから現在までのコミットを整理したいので、rebase先はmasterを指定する。first commitのコミットIDでも可
##仮にcommit4から現在までを整理したい場合はcommit3のコミットIDをrabase先に指定する
git rebase -i master
 
#viエディタが起動し、下記のように表示される
pick d0290fa commit1: A機能用ソースファイル追加
pick d21ffa1 commit2: A機能の説明をREADMEに追記
pick 25d6a63 commit3: READMEにタイプミス発覚→修正
pick 139de71 commit4: B機能用ソースファイル追加
pick 0ce0d64 commit5: B機能の説明をREADMEに追記
pick b522341 commit6: B機能に仕様変更発生→ソース修正
pick 9b13b8c commit7: B機能の仕様変更は取り下げになりソースを元に戻す(commit6のrevert)
pick b3efbfe commit8: A機能でバグ発覚→A機能のソース修正
 
#これを下記のように書き換える
## コミットの順番を変える
## 上と統合したいコミットのpickをsquashに書き換える
## 不要なコミットの行を削除する
pick commit1 A機能用ソースファイル追加
squash commit2 A機能の説明をREADMEに追記
squash commit3 タイプミス発覚→ソース修正
squash commit8 A機能バグ修正
pick commit4 B機能用ソースファイル追加
squash commit5 B機能の説明をREADMEに追記
 
#:wqまたはZZでviエディタを終了する
```

コミット整理用のviエディタを閉じると、commit1+2+3+8のコミットメッセージを編集するviエディタが起動します。
内容は下記の通りです。

```sh
# This is a combination of 4 commits.
# This is the 1st commit message:

commit1: A機能用ソースファイル追加

# This is the commit message #2:

commit2: A機能の説明をREADMEに追記

# This is the commit message #3:

commit3: READMEにタイプミス発覚→修正

# This is the commit message #4:

commit8: A機能でバグ発覚→A機能のソース修正
```

コミットメッセージなので内容は何でも良いのですが、今回は下記のように書き換えます。
(実際の開発現場では他の人にも分かりやすいコミットメッセージを書くように心掛けてください)

```sh
commit1+2+3+8: A機能(バグ修正済み)とA機能の説明を含むREADME

commit1: A機能用ソースファイル追加
commit2: A機能の説明をREADMEに追記
commit3: READMEにタイプミス発覚→修正
commit8: A機能でバグ発覚→A機能のソース修正
```

上記の通り編集したら`:wq`or`ZZ`で上書き保存してviエディタを終了してください。
すると次にcommit4+5のコミットメッセージを編集するviエディタが起動します。
内容は下記の通りです。

```sh
# This is a combination of 2 commits.
# This is the 1st commit message:

commit4: B機能用ソースファイル追加

# This is the commit message #2:

commit5: B機能の説明をREADMEに追記
```

こちらも下記ように書き換えてください。

```sh
ommit4+5: B機能とB機能の説明を含むREADME

commit4: B機能用ソースファイル追加
commit5: B機能の説明をREADMEに追記
```

編集を終えたら`:wq`or`ZZ`で上書き保存してviエディタを終了してください。
その後下記のメッセージが表示されたらrabase完了です。

```sh
[detached HEAD 014d976] commit4+5: B機能とB機能の説明を含むREADME
 Date: Wed Jun 8 07:25:27 2022 +0000
 2 files changed, 2 insertions(+)
 create mode 100644 featB.src
Successfully rebased and updated refs/heads/work.
```

コミットグラフを確認すると、整理前に8個だったコミットが2個にまとまっているのが分かります。

```sh
git log --graph --all --pretty="format:%C(red)%h%C(yellow)%d %C(reset)%s"
* 014d976 (HEAD -> work) commit4+5: B機能とB機能の説明を含むREADME
* 8be3cfc commit1+2+3+8: A機能(バグ修正済み)とA機能の説明を含むREADME
| * b3efbfe (develop) commit8: A機能でバグ発覚→A機能のソース修正
| * 9b13b8c commit7: B機能の仕様変更は取り下げになりソースを元に戻す(commit6のrevert)
| * b522341 commit6: B機能に仕様変更発生→ソース修正
| * 0ce0d64 commit5: B機能の説明をREADMEに追記
| * 139de71 commit4: B機能用ソースファイル追加
| * 25d6a63 commit3: READMEにタイプミス発覚→修正
| * d21ffa1 commit2: A機能の説明をREADMEに追記
| * d0290fa commit1: A機能用ソースファイル追加
|/
* b7b9572 (master) first commit

git diff develop work
#出力なし
#rabase前のdevelopブランチとrebase後のworkブランチを比較して差分が無いことを確認する
```


## 1つのコミットを2つ以上に分割する
今度は、さっきまとめたコミットを、機能追加とREADME追加で別のコミットに分離してみます。

分割前の状態がこちら。

```sh
git log --graph --pretty="format:%C(red)%h%C(yellow)%d %C(reset)%s"
* 014d976 (HEAD -> work) commit4+5: B機能とB機能の説明を含むREADME
* 8be3cfc commit1+2+3+8: A機能(バグ修正済み)とA機能の説明を含むREADME
* b7b9572 (master) first commit
```

このうち、commit1+2+3+8を分割して下記のようにしてみます。

* commit1+8: A機能(バグ修正済み)追加
* commit2+3: A機能のREADME追加
* commit4+5: B機能とB機能の説明を含むREADME

まずは今回も安全のために作業用ブランチを作成します。

```sh
git branch
  develop
  master
* work            #←現在workブランチで作業していることを確認 (*がアクティブなブランチ)

#分割作業用に新しいブランチを作成する
git switch -c work2
git branch
  develop
  master
  work
* work2
```

そしてrebaseしていきます。
rebase中に操作を間違えた場合は`git rebase --abort`を実行することでここまで巻き戻ることが出来ます。
abort後はここからやり直してください。

```sh
#インタラクティブモードのrabaseを起動
git rebase -i master
 
#viエディタが起動し、下記のように表示される
pick 8be3cfc commit1+2+3+8: A機能(バグ修正済み)とA機能の説明を含むREADME
pick 014d976 commit4+5: B機能とB機能の説明を含むREADME
 
#これを下記のように書き換える
##分割したいコミットのpickをeditに書き換える
edit 8be3cfc commit1+2+3+8: A機能(バグ修正済み)とA機能の説明を含むREADME
pick 014d976 commit4+5: B機能とB機能の説明を含むREADME
 
#:wqまたはZZでviエディタを終了すると下記のメッセージが表示される
Stopped at 8be3cfc...  commit1+2+3+8: A機能(バグ修正済み)とA機能の説明を含むREADME
You can amend the commit now, with
  git commit --amend
Once you are satisfied with your changes, run
  git rebase --continue
```

ここからコミットを分離していくのですが、厳密には「コミットを分離する」という機能はrabaseにはありません。
ここからはインタラクティブrabaseの状態で、任意の粒度のコミットを手動で作り直していく作業になります。

まず、現在の状態はこのようになっています。

```sh
git status
interactive rebase in progress; onto b7b9572
Last command done (1 command done):
   edit 8be3cfc commit1+2+3+8: A機能(バグ修正済み)とA機能の説明を含むREADME  #←このコミットを適用した状態
(後略)

git log --graph --pretty="format:%C(red)%h%C(yellow)%d %C(reset)%s"
* 8be3cfc (HEAD) commit1+2+3+8: A機能(バグ修正済み)とA機能の説明を含むREADME
* b7b9572 (master) first commit
```

rebaseするときにcommit1+2+3+8をeditにしておいたのでここでrebaseが一時停止しています。
commit1+2+3+8がすでにコミットされていますが、今回はこのコミットを分割したいので、このコミットはresetで無かったことにします。

```sh
#git resetで強制的に一つ前の状態まで巻き戻す
##--hardオプション付きでgit resetすると、ファイル内容も含めて指定コミットの状態に戻ります
##HEADは現在位置、^は一つ前を意味します。HEAD^で現在の一つ前という意味になります
git reset --hard HEAD^
HEAD is now at b7b9572 first commit

#HEADが一つ前に戻っていることを確認
git log --graph --pretty="format:%C(red)%h%C(yellow)%d %C(reset)%s"
* b7b9572 (HEAD, master) first commit
git log --graph --all --pretty="format:%C(red)%h%C(yellow)%d %C(reset)%s"
* 014d976 (work2, work) commit4+5: B機能とB機能の説明を含むREADME
* 8be3cfc commit1+2+3+8: A機能(バグ修正済み)とA機能の説明を含むREADME
| * b3efbfe (develop) commit8: A機能でバグ発覚→A機能のソース修正
| * 9b13b8c commit7: B機能の仕様変更は取り下げになりソースを元に戻す(commit6のrevert)
| * b522341 commit6: B機能に仕様変更発生→ソース修正
| * 0ce0d64 commit5: B機能の説明をREADMEに追記
| * 139de71 commit4: B機能用ソースファイル追加
| * 25d6a63 commit3: READMEにタイプミス発覚→修正
| * d21ffa1 commit2: A機能の説明をREADMEに追記
| * d0290fa commit1: A機能用ソースファイル追加
|/
* b7b9572 (HEAD, master) first commit

#git statusでもクリーンなことを確認
git status
interactive rebase in progress; onto b7b9572
Last command done (1 command done):
   edit 8be3cfc commit1+2+3+8: A機能(バグ修正済み)とA機能の説明を含むREADME
(中略)
nothing to commit, working tree clean         #←このメッセージが出ることを確認
```

これでcommit1+2+3+8を無かったことにしたので、まずは「commit1+8: A機能(バグ修正済み)追加」というコミットを作っていきます。

```sh
#commit1+2+3+8 (コミットID: 8be3cfc) からfeatA.srcを復元してコミットする
git restore --source 8be3cfc featA.src
git add featA.src
git commit -m "commit1+8: A機能(バグ修正済み)追加"
[detached HEAD 3e4a18d] commit1+8: A機能(バグ修正済み)追加
 1 file changed, 1 insertion(+)
 create mode 100644 featA.src

#コミットグラフを確認
git log --graph --pretty="format:%C(red)%h%C(yellow)%d %C(reset)%s"
* 2ca3823 (HEAD) commit1+8: A機能(バグ修正済み)追加
* b7b9572 (master) first commit

#現状確認
git status
interactive rebase in progress; onto b7b9572
Last command done (1 command done):
   edit 8be3cfc commit1+2+3+8: A機能(バグ修正済み)とA機能の説明を含むREADME  #←まだここのedit中
(後略)
```

これでcommit1+8のコミットが出来上がったので、次は「commit2+3: A機能のREADME追加」を作成します。

```sh
#commit1+2+3+8 (コミットID: 8be3cfc) からREADME.mdを復元してコミットする
git restore --source 8be3cfc README.md
git add README.md
git commit -m "commit2+3: A機能のREADME追加"
[detached HEAD 5875d0f] commit2+3: A機能のREADME追加
 1 file changed, 1 insertion(+)

#コミットグラフを確認
git log --graph --pretty="format:%C(red)%h%C(yellow)%d %C(reset)%s"
* d4a6299 (HEAD) commit2+3: A機能のREADME追加
* 2ca3823 commit1+8: A機能(バグ修正済み)追加
* b7b9572 (master) first commit

#commit1+2+3+8 (コミットID: 8be3cfc) と差分が無いことを確認
git diff 8be3cfc
#出力なし
```

これでcommit1+2+3+8をcommit1+8とcommit2+3に分割出来たので、rabaseのステータスをcommit1+2+3+8の編集状態から次へと進めます。

```sh
#現状確認
git status
interactive rebase in progress; onto b7b9572
Last command done (1 command done):
   edit 8be3cfc commit1+2+3+8: A機能(バグ修正済み)とA機能の説明を含むREADME  #←まだここのedit中
(後略)

#rebaseのステップを1つ進める
git rebase --continue
Successfully rebased and updated refs/heads/work2.
```

これでrebaseが完了したので状態を確認します。

```sh
git status
On branch work2
nothing to commit, working tree clean

git log --graph --pretty="format:%C(red)%h%C(yellow)%d %C(reset)%s"
* a152d3d (HEAD -> work2) commit4+5: B機能とB機能の説明を含むREADME
* d4a6299 commit2+3: A機能のREADME追加
* 2ca3823 commit1+8: A機能(バグ修正済み)追加
* b7b9572 (master) first commit

git log --graph --all --pretty="format:%C(red)%h%C(yellow)%d %C(reset)%s"
* a152d3d (HEAD -> work2) commit4+5: B機能とB機能の説明を含むREADME
* d4a6299 commit2+3: A機能のREADME追加
* 2ca3823 commit1+8: A機能(バグ修正済み)追加
| * 014d976 (work) commit4+5: B機能とB機能の説明を含むREADME
| * 8be3cfc commit1+2+3+8: A機能(バグ修正済み)とA機能の説明を含むREADME
|/
| * b3efbfe (develop) commit8: A機能でバグ発覚→A機能のソース修正
| * 9b13b8c commit7: B機能の仕様変更は取り下げになりソースを元に戻す(commit6のrevert)
| * b522341 commit6: B機能に仕様変更発生→ソース修正
| * 0ce0d64 commit5: B機能の説明をREADMEに追記
| * 139de71 commit4: B機能用ソースファイル追加
| * 25d6a63 commit3: READMEにタイプミス発覚→修正
| * d21ffa1 commit2: A機能の説明をREADMEに追記
| * d0290fa commit1: A機能用ソースファイル追加
|/
* b7b9572 (master) first commit

git diff work work2
#出力なし。コミット分割前と比べて差分なし

git diff develop work2
#出力なし。コミット結合・分割前と比べても差分なし
```

commit1+8とcommit2+3はrebase中に手動で作成したコミットで、commit4+5はrebaseが自動的に作成したコミットです。
work2ブランチをmasterまでresetして手動で1つ1つ任意のコミットを作っていっても同じことが出来ますが、コミットの数が多いときは手動編集したいコミット以外はrebaseにお任せ出来るので楽です。