git reset、git revertの違い

## はじめに
船井総研デジタルのoswです。要件がペンディングになったため、業務で使っているリモートリポジトリのあるブランチを元に戻す、という作業を行いました。その際、`git revet`、`git reset`がごちゃごちゃになっていたため整理します。

## 結論
両者はコミットをある状態まで戻すという点においては共通していますが、下記のようになっているようです。（用途は私の主観です）

|項目|git reset|git revert|
|:-:|:-:|:-:|
|戻した時点までのコミット履歴が残る|X|O|
|「戻したというコミット」が積まれる|X|O|
|リモートリポジトリへそのままpushできる|X|O|
|用途|個人開発|チーム開発|


`reset`の場合コミット履歴がその時点まで戻るため、戻した時点から先のコミット履歴は見えなくなります。`revert`はリポジトリの状態をその時点まで戻すものの、コミット履歴はそのままで、新たに「戻した」というコミットが積まれます。

上記の関係で、`reset`で過去に戻したリポジトリをpushしようとすると、リモートリポジトリより古い状態になってしまうため`-f`を付けて強制的に実行しないとpushできません。一方`revert`はコミット履歴はそのまま残り、新たに「戻した」というコミットを積むためリモート+αの状態になりそのままpushが可能です。

pushの問題はさておき、`reset`でも戻した後に空コミットを追加し、revertした旨をコミットメッセージに追加は可能かと思いますが、戻したコミット履歴は消えてしまうためどんな作業のコミットを戻したのか全て記述するのはかなり大変かと思います。

`reset`はローカルリポジトリ上でやらかしてしまった時の対策として使うのが良さそうです。成果物を作成する過程で「不必要だった作業を戻した」というコミットを`revert`で履歴に残す意味もないですし、pushしたらリモートリポジトリのコミット履歴がぐちゃぐちゃになりそうです。



## 学習環境
- Windows 11 (WSL2 Ubuntu)
- VSCode 1.76.2
- Git 2.34.1

## 検証環境
話を簡単にするため、ローカルにリポジトリに下記READMEを作成し、それをいじって検証していきます。

ファイルに差分が入っていきますが、基本的には見出しを追加、削除するだけになります。

- 作成するREADME
```markdown
## A
A
A

## B
B
B

## C
C
C

```

- コミット履歴
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2820803/8c17a35e-5378-55f8-cf9c-4d732a736485.png)



## 前提知識
コミット履歴を指定する際、HEADや@、それと合わせて"~"、"^"などが使われますが、これらの意味はこちらの記事で詳細に解説してくださっています。

https://prograshi.com/general/git/meaning-of-head-and-at-mark/

ただ、マージされた場合に関しては"~"、"^"で挙動が異なるようです。

https://www.chihayafuru.jp/tech/index.php/archives/2535

## そもそも`git reset`とは
resetはオプションによって挙動が変わってくると思いますが、こちらの記事が非常にわかりやすいです。

https://qiita.com/shuntaro_tamura/items/db1aef9cf9d78db50ffe


## そもそも`git revert`とは
`revert`は元の習慣、状態に戻るという意味のようです。
> 〔もとの習慣・状態などに〕帰る，逆戻りする 〔to〕
> https://ejje.weblio.jp/content/revert

gitにおけるrevertはリポジトリを特定のコミットの状態に戻し、revertしたことを示すコミットを積み上げるというもののようです。

この時、resetとは異なりコミット履歴はそのまま残り、その上に新しく積まれます。
（gitではrevertすることを「打ち消す」という表現が使われるようです）

### 直近のコミットをrevertしてみる
まずは検証環境で作成したリポジトリの「見出しC」を追加した直近のコミットをrevertしてみます。

すると、見出しCが削除され（見出しCを追加したコミット履歴は残る）、revertした旨のコミット履歴が積まれます。

- 検証前のコミット履歴
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2820803/90e099a9-eb14-d179-f1d6-dd38b3cb6e32.png)

- 直近のコミットをrevert

```bash:
$ git revert @
# コミットメッセージを編集し、保存して完了
```

- revert後のREADME

```markdown
## A
A
A

## B
B
B

```

- revert後のコミット履歴
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2820803/fe868cef-6aae-2a26-d1a8-1ae367f6322f.png)

### 連続したコミットをrevertしてみる
続いて連続したコミットをrevertしてみます。その前に一度revertしてしまっているため、検証環境で作成した状態、revert前に戻しておきます。

```bash
# コミットのハッシュ値を確認
$ git log --oneline
f20946b (HEAD -> revert) Revert "追加: 見出しC"
37cbc2e (master) 追加: 見出しC
11acd6f 追加: 見出しB
4ea1d6d 追加:  見出しA
c9c6945 initial commit

# revert前の「37cbc2e」に戻す
$ git reset --hard 37cbc2e
```

これでrevert前の環境に戻りました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2820803/c8b51a5f-833e-466b-edaf-f4d56c3b8338.png)

連続したコミットをrevertします。ここでは見出しC, Bをrevertしてみます。

連続したコミットをrevertする場合は、現在のコミットからrevert対象となるコミットまでを"..."で繋ぎ、相対的に記述すれば良いようです。ここで右に指定するコミット-1のコミットまでrevertされるため、右側に指定したコミットの状態に戻ります。

```bash: 連続したコミットをrevertする例
# 現在のコミットから4個前のコミットまでをrevertする
$ git revert @...@~5

# 4個分のコミットメッセージを編集、保存して完了
```

実際にコマンドを叩いてみるとrevertする数だけ、上記例だと4回分のコミットメッセージを修正する画面が開きます。数が多いとさすがに面倒なので、`--no-edit`を指定することでメッセージの編集をスキップする事ができます。

```bash:追加: 見出しAに戻す
$ git revert --no-edit @...@~2
```

- revert後のコミット履歴
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2820803/debe761c-ded4-7426-0a30-2302c0d597fc.png)

#### revert後に自動コミットさせない
revertをデフォルトで実行すると、「revertしたコミットの分」だけrevertした旨のコミットが積まれます。`-n`を指定することでrevertはしても自動的にコミットまではされなくなります。

まずは検証前の環境に戻します。

```bash:revert前の「37cbc2e」に戻す
$ git reset --hard 37cbc2e
```

```bash:見出しAまで戻し、revert後にコミットさせない
$ git revert -n @...@~2
$ git status
On branch revert
You are currently reverting commit 11acd6f.
  (all conflicts fixed: run "git revert --continue")
  (use "git revert --skip" to skip this patch)
  (use "git revert --abort" to cancel the revert operation)

Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        modified:   README.md
```

- revert後のコミット履歴
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2820803/f887bc0e-ca95-250e-ad16-103df432e304.png)

"-n"を指定した場合は、まずrevertでコミットの状態を戻します。この時、作業内容が戻った状態でかつ`git add`された状態となります。そのため、「revertした」という複数のコミットは自動的に積まれず、手動でコミットすることでそれら複数のコミットを1つにまとめることができます。

戻したことを示すコミットが何もないと後々「あの件どうなったんだっけ」とよくわからなくなる可能性があるため、最低でも1つrevertしたコミットはある方が良いかもしれません。

### 特定のコミットをrevertする
特定のハッシュ値を指定してrevertすることも可能です。ただ、注意としてrevertで作業内容を取り消されるファイルは、特定のコミットの作業内容だけが取り消される訳ではなく、その時点に全て戻されるようです。

また、revertしたことで積まれるコミットですが、こちらは「指定したコミット」だけが積まれます。

作成したREADMEに見出しD, E, Fを追加して次の状態で確認してみます。

```markdown
## A
A
A

## E
E
E

## F
F
F

## D
D
D

## B
B
B

## C
C
C

```

- コミット履歴
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2820803/2cd7bf54-e660-8390-c39b-425bf519038a.png)

```bash:見出しBをメッセージ編集なしでrevertする
$ git revert --no-edit 11acd6f
```

- README.md
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2820803/1ba3f909-a824-d04f-252f-e5c6de675341.png)

- コミット履歴
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2820803/de8a75d3-e8be-faf4-5da5-fd4ba50e7559.png)

見出しBから見出しFまで全て取り消されていることが確認できました。範囲を指定した場合と同じ挙動になるようですが、積まれるコミットが1つだけなので明示的にrevertした履歴を残したい場合は範囲を指定する必要がありそうです。


## おわりに
試すまでは[特定のコミットをrevertする](#特定のコミットをrevertする)に関して「特定の作業内容だけ」が取り消されるものだと思っていましたが、そういう訳ではないことがわかりました。

今回の学習で「やっちまったorz」がかなり減らせるのではないかな、と思います。幸いなことにまだ事件は起こしてないので、何かあっても冷静に対処ができるようになったかと思います。