`git rabase -i` を使ってみよう

[git-rebase](https://git-scm.com/docs/git-rebase) はcommitの履歴を任意に書き換え直すコマンドで、使う人は使うけど使わない人は使わない機能だと思う。使い方を知っていると便利なのでちょっとやってみよう。(本記事は社内勉強会で使った文章のリサイクルです)

# 体験コーナー

## ちょっと汚れたコミットログを作ってみる

日々の開発をシミュレートしながらコミットツリーを作ります。業務でコードを書いているところを想像しながら以下のコマンドを実行していってください。全て実行するとちょっと汚れた `dev` ブランチができます。

```bash
# 0. git リポジトリの作成

cd ~
mkdir git_rebase_practice
cd git_rebase_practice
# git init
git init .

# 1. 最初のコミット

# README作成
cat <<EOF > README.md
README
EOF
git add .
git commit -m 'init'

# 2. 機能Aの開発を開始

# ブランチきる
git switch -c dev

# 機能Aの開発
echo 'adding wanderful features!' > feature_a.md
git add .
git commit -m 'add wonderful features'

# 3. バグを作り込んでしまった！---------------------------

# バグった機能Aの開発
echo 'adding bad features!' >> feature_a.md
git add .
git commit -m 'add bugs'

# 4. 機能Bを開発---------------------------

# 機能Bの開発
cat <<EOF > feature_b.md
adding awesome features!
EOF
git add .
git commit -m 'add owesome features'

# 5. PRレビューなどで、指摘を受けたので修正する ----------------------------
# typoの修正
# for macos sed
sed -i '' 's/wanderful/wonderful/' feature_a.md
# for gnu sed
sed -i 's/wanderful/wonderful/' feature_a.md

git add .
git commit -m 'fix typo'

# バグの修正
sed -i '' 's/bad/good/' feature_a.md

git add .
git commit -m 'fix bug'

```

上記の手順で、以下のようなコミットログが完成します。

```mermaid
gitGraph
	checkout main
  commit id: "init"
	branch dev
	checkout dev
	commit id: "add wonderful features"
	commit id: "add bags"
	commit id: "add owesome features"
	commit id: "fix typo"
	commit id: "fix bug"
```

機能は完成したけど、

- `add bugs` ,`fix typo` , `fix bug` は `add wonderful features` にまとめたい
- `add owesome feature` はコミットはそのままでいいけど、メッセージがtypoしてるので直したい

ですね。下のようなコミットログにしたい。

```mermaid
gitGraph
	checkout main
  commit id: "init"
	branch dev
	checkout dev
	commit id: "add wonderful features"
	commit id: "add awesome features"
```

## `git rebase -i` で履歴を書き換える

以下のコマンドを実行します。

```bash
# -i は interactive な rebase を行うオプション
# master は rebase の出発点となるコミットを指します
git rebase -i master
```


上記を実行すると、以下のようなファイルがあなたのテキストエディタで表示されます。これはgit-rebaseが順次実行するコマンドの予定リストです。

```text
pick 96cbfc6 add wonderful feature
pick 731ac7c add bugs
pick 743691c add owesome features
pick c8f1276 fix typo
pick 3be22c8 fix bug

# Rebase 7a4cc9c..3be22c8 onto 7a4cc9c (5 commands)
#
# Commands:
# p, pick <commit> = use commit
# r, reword <commit> = use commit, but edit the commit message
# e, edit <commit> = use commit, but stop for amending
# s, squash <commit> = use commit, but meld into previous commit
# f, fixup [-C | -c] <commit> = like "squash" but keep only the previous
#                    commit's log message, unless -C is used, in which case
#                    keep only this commit's message; -c is same as -C but
#                    opens the editor
# x, exec <command> = run command (the rest of the line) using shell
# b, break = stop here (continue rebase later with 'git rebase --continue')
# d, drop <commit> = remove commit
# l, label <label> = label current HEAD with a name
# t, reset <label> = reset HEAD to a label
# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]
#         create a merge commit using the original merge commit's
#         message (or the oneline, if no original merge commit was
#         specified); use -c <commit> to reword the commit message
# u, update-ref <ref> = track a placeholder for the <ref> to be updated
#                       to this position in the new commits. The <ref> is
#                       updated at the end of the rebase
```

ファイル中のコメントにある通り、`p`, `r`, `e`, `s`, `f`,... 様々なコマンドをコミットごとに実行していく命令リストを指定します。エディタを閉じるとここで指定したコマンド通りにgit-rebaseが順次実行されます。今回は以下を使ってみます。

- `p` (`pick`), そのコミットをそのままコミットする
- `s` (`squash`), 一つ上のコミットにそのコミットを合体させ、コミットメッセージを編集する
- `f` (`fixup`), `squash`と同じ挙動をするが、コミットメッセージの書き換えを行わない
- `r` (`reword`), コミットメッセージの書き換える

上記のコマンドを使って上で示したログになるように書き換えてみよう！
    
```text
pick 96cbfc6 add wonderful feature
s 731ac7c add bugs
s c8f1276 fix typo
s 3be22c8 fix bug
r 743691c add owesome features
```

こんな感じ。これがどんな命令になるかというと、

- `pick ... add wonderful feature`
    - `add wonderful feature` のコミットをそのままコミットします
- `s ... add bugs`　~ `s ... fix bug`
    - `s` = `suquash` 
    - `add wonderful feature` にコミットをくっつけ、一つのコミットにします
    - コミットメッセージは修正できます
- `r ... add owesome features`
    - `r` = `reword`
    - `add owesome features` のコミットをそのままコミットし、コミットメッセージだけを修正します

この状態でテキストエディタを保存して終了すると、 `s` と `r` のコミットメッセージ修正画面が2度表示され、git-rebaseの実行が終わります。以下のような履歴に書き換わっていることが確認できると思います。

```mermaid
gitGraph
	checkout main
  commit id: "init"
	branch dev
	checkout dev
	commit id: "add wonderful features"
	commit id: "add awesome features"
```

他にもコミットごと無かったことにしたり、コミットの順番を変えたり、マージコミットだけ残したり、マージコミットを無かったことにしたりなどいろいろできます。[詳しくは公式のドキュメントを読んでください](https://git-scm.com/docs/git-rebase)

# こまったとき

履歴がぶつかりまくったりぐちゃぐちゃになって訳わかんなくなったとき。

```bash
# rebase がまだ終わってない時(git status で確認できる)は以下のコマンドでrebase実行前に戻れます
git rebase --abort

# remote に書き換え前のブランチがあるならそこに戻したり
git reset --hard origin/<branch name>

# remote にブランチがないなら rebase するまえのコミットidを確認して
git reflog
# もとにもどしたり
git reset --hard <commit>

```

# 注意！！

git-rebase は履歴が綺麗にできて嬉しいけど、反面バージョン管理システムで履歴を書き換えることはとても危険な作業なので、以下に注意。

- 自分以外が触っているブランチにgit-rebaseは基本的にしてはいけない。
    - する場合は触ってる人に確認する
    - 変更を `push` した後は 「`git pull` しないで `git fetch` -> `git reset --hard origin/<branch_name>` して」って触ってる人に言うこと
        - `pull` すると `rebase` 前のコミットと `rebase` 後のコミットがぐちゃぐちゃになるので, `fetch` `reset` する必要がある
    - 流派によっては一切しない人もいるし、ガンガンやる人もいるのでプロジェクトの雰囲気を確認しておいたほうがいいかもしれない。
- git-push する前に元のコミットとの `diff` を確認すること
    - `git diff origin/<branch_name>`　してちゃんと思い通りの差分になっているか確認
- git-push するときは `--force-with-lease` すること
    - 履歴の書き換えすると基本的に force push しないといけないが `--force` はとても怖いので、 `--force-with-lease` でちょっとでも安全にpushする。`--force-with-lease` はサーバと手元のremoteとの差分がある時に `push` が失敗するのでちょっと安心


# リンク

- [git-rebase](https://git-scm.com/docs/git-rebase)
- [3.6 Git のブランチ機能 - リベース](https://git-scm.com/book/ja/v2/Git-%E3%81%AE%E3%83%96%E3%83%A9%E3%83%B3%E3%83%81%E6%A9%9F%E8%83%BD-%E3%83%AA%E3%83%99%E3%83%BC%E3%82%B9)
- [git-push --force-with-lease](https://git-scm.com/docs/git-push#Documentation/git-push.txt---no-force-with-lease)