git commit --fixupに慣れる

過去の特定コミットに対する修正をする際、 `git commit --fixup <commit hash> && git rebase -i --autosquash` を使うと便利である。

本記事では、fixup使い方や3種類あるfixupの違いについてまとめる。

## 基本的な使い方

### 手順1: 現状を確認する

```sh
$ git log --oneline -n 2
3a3aadd (HEAD -> feature/commit-fixup) modify hoge::hello_world
ad32860 add hoge.py
```

`ad32860` で追加したファイルを、 `3a3aadd` で修正している状態。

### 手順2: `3a3aadd` に対して追加修正したくなったので、fixupを用いて修正する

```sh
$ git status
On branch feature/commit-fixup
Changes not staged for commit:
        modified:   commit-fixup/hoge.py

$ git add -u

$ git commit --fixup 3a3aadd
[feature/commit-fixup 9bafd0b] fixup! modify hoge::hello_world
 1 file changed, 5 insertions(+), 9 deletions(-)

$ git log --oneline -n 3    
9bafd0b (HEAD -> feature/commit-fixup) fixup! modify hoge::hello_world
3a3aadd modify hoge::hello_world
ad32860 add hoge.py
```

最新commit `9bafd0b` が 「`fixup!` + `3a3aadd のログメッセージ`」という形式になっている。

### 手順3: fixup commitを元commit `3a3aadd` へまとめるためにrebaseする

```sh
$ git rebase -i --autosquash HEAD~2
pick 3a3aadd modify hoge::hello_world
fixup 9bafd0b fixup! modify hoge::hello_world
```

元commitが `pick` で、fixup commit が `fixup` になっているので、この状態でrebaseを完了させる。

### 手順4: 結果を確認する

```sh
$ git log --oneline -n 2
34ad32b (HEAD -> feature/commit-fixup) modify hoge::hello_world
ad32860 add hoge.py
```

fixup commitが消えて、元commitのみ残されていることがわかる。

なお、ログメッセージは元commitから変更なしだが、commit hashは `3a3aadd` から `34ad32b` へ変わっている。
（そのため、「元commitが残っている」という表現は正確でないか？）


## 3種類のfixup

fixupには以下の3種類がある。

|command|指定したcommitの内容を変更するか？|指定したcommitのメッセージを変更するか？|
|---|---|---|
| `--fixup` |yes|no|
| `--fixup amend` |yes|yes|
| `--fixup reword` |no|yes|

## FAQ

自由研究的な感じで試したことを以下にまとめた。

### Q. 1つ前ではなく任意のcommitに対してfixupすることはできる？

**A: できる！**

以下、1つ前の最新commitではなく、2つ前のcommitに対してfixupする手順

```sh
# 現状確認
$ git log --oneline -n 4
e8c1a58 (HEAD -> feature/commit-fixup) modify hoge::main, require argument
613ad7d modify hoge::goodbye_world  # このcommit に対してfixup commit を作る
34ad32b modify hoge::hello_world
ad32860 add hoge.py

# ファイルを修正して、
$ git status            
On branch feature/commit-fixup
Changes not staged for commit:
        modified:   commit-fixup/hoge.py

# 変更内容をaddして、
$ git add -u

# fixup commit
$ git commit --fixup 613ad7d

# 状況確認
$ git log --oneline -n 5
e97d817 (HEAD -> feature/commit-fixup) fixup! modify hoge::goodbye_world
e8c1a58 modify hoge::main, require argument
613ad7d modify hoge::goodbye_world
34ad32b modify hoge::hello_world
ad32860 add hoge.py

# rebaseする
$ git rebase -i --autosquash HEAD~3
pick 613ad7d modify hoge::goodbye_world
fixup e97d817 fixup! modify hoge::goodbye_world  # commit logから順番変わり 613ad7d の次に来る
pick e8c1a58 modify hoge::main, require argument

# 結果確認
$ git log --oneline -n 4
08f2682 (HEAD -> feature/commit-fixup) modify hoge::main, require argument
e553b70 modify hoge::goodbye_world
34ad32b modify hoge::hello_world
ad32860 add hoge.py
```

### Q. 1つ目と2つ目のcommitで同じ箇所を変更していた場合、1つ目のcommitに対するfixupを行うとどうなる？

**A: 修正内容次第ではコンフリクトが発生する！**

```sh
# 1つ目commit (08f2682) 時点のlog
$ git log --oneline -n 4
08f2682 (HEAD -> feature/commit-fixup) modify hoge::main, require argument  # これに対してfixupする予定
e553b70 modify hoge::goodbye_world
34ad32b modify hoge::hello_world
ad32860 add hoge.py
```

上記の状態から、fixupによるコンフリクト発生させてみる。

```sh
# 1つ目commit (08f2682) 時点の実装内容
$ cat main.py
def main():
    if len(sys.argv) != 1:
        print("Usage: python hoge.py <name>")
        sys.exit(1)
    name = sys.argv[1]



# 2つ目commitのための修正を加えた
$ cat main.py
def main():
    name = "world"
    if len(sys.argv) < 1:
        print("Usage: python hoge.py [name]")
        raise SystemExit(1)
    else:
        name = sys.argv[1]

$ git add -u
$ git commit -m "modify hoge::main, set arg optionnal"

# 2つ目commit (9e02a2c) 時点のlog
$ git log --oneline -n 5
9e02a2c (HEAD -> feature/commit-fixup) modify hoge::main, set arg optionnal
08f2682 modify hoge::main, require argument  # これに対してfixupする予定
e553b70 modify hoge::goodbye_world
34ad32b modify hoge::hello_world
ad32860 add hoge.py



# fixup commitのための修正を加えた
$ cat main.py
def main():
    if len(sys.argv) < 1:
        print("Usage: python hoge.py [name]")
        name = "world"
    else:
        name = sys.argv[1]

$ git add -u
$ git commit --fixup 08f2682

# fixup commit (26ca4a1) 時点のlog
$ git log --oneline -n 6
26ca4a1 (HEAD -> feature/commit-fixup) fixup! modify hoge::main, require argument
9e02a2c modify hoge::main, set arg optionnal
08f2682 modify hoge::main, require argument  # これに対してfixupした
e553b70 modify hoge::goodbye_world
34ad32b modify hoge::hello_world
ad32860 add hoge.py
```

準備が整ったのでrebaseする。

```sh
$ git rebase -i --autosquash HEAD~3
pick 08f2682 modify hoge::main, require argument
fixup 26ca4a1 fixup! modify hoge::main, require argument
pick 9e02a2c modify hoge::main, set arg optionnal
```

```python
def main():
<<<<<<< HEAD
    if len(sys.argv) != 1:
        print("Usage: python hoge.py <name>")
        sys.exit(1)

    name = sys.argv[1]
=======
    if len(sys.argv) < 1:
        print("Usage: python hoge.py [name]")
        name = "world"
    else:
        name = sys.argv[1]
>>>>>>> 26ca4a1 (fixup! modify hoge::main, require argument)
```

コンフリクトが発生！
めでたしめでたし（？）

## 所感

綺麗なcommit logが残っているとfixupの効果を存分に発揮できそう。

1. 実装時にcommit logを綺麗にしてPRレビュー依頼出す
1. PRレビューで修正したら適切なcommitに修正内容をfixupで紐づける
1. レビュー完了後、マージする前にautosquashでcommit logを改めて綺麗にする（~~「PRレビュー修正」といったノイズcommitを抹消！~~）

逆に、commit logが綺麗になっていない状態で使うとゴチャゴチャになりそうな予感……
上記PRレビュー修正のフローで取り入れるならば、「PRレビュー修正」commitを1つ作り、そのcommitに対してfixupしていく作戦はアリかもしれない。

兎にも角にも、使いこなすには慣れが必要そう（それはそう）

## 参考

公式Docs

https://git-scm.com/docs/git-commit/2.32.0#Documentation/git-commit.txt---fixupamendrewordltcommitgt

fixupを知るきっかけになった記事

https://qiita.com/inukai-masanori/items/82eb0626fd75f3eb0922