分かった気になっていたgit reset --hardをちゃんと理解する

git resetは学んだばかりのころは、「--hardをつけて間違った操作をすると実装が消えて戻らなくなる恐れがある」という印象が強く、実装を取り消す怖いコマンドのイメージでした。

使うのはたいてい以下のコマンドでしたが、どういうときに取り戻しがつかなくなるのかあまり分かっていなかったなと思います。

```bash
git reset --hard HEAD~
```

しかし別の使い方を見かけました。
例えば以下のようなツリーになっていて、branch1にいる時に、

```mermaid
%%{init: { 'themeVariables': {
              'tagLabelFontSize': '14px'
       } } }%%
gitGraph
    commit
    commit
    branch branch2
    branch branch1
    checkout branch1
    commit tag: "HEAD" type: HIGHLIGHT
    checkout branch2
    commit
    commit
    checkout main
    commit
```

```bash
git reset --hard branch2
```

とすると、以下のようにbranch2と同じcommitまで移動することができるということを知りました。
変更をリセットする(取り消す)コマンドだと思っていたので、`git checkout branch2`と何が違うのか、｢リセット｣と｢commitの移動｣にどう関係しているのかが分かっていませんでした。

```mermaid
%%{init: { 'themeVariables': {
              'tagLabelFontSize': '14px'
       } } }%%
gitGraph
    commit
    commit
    branch branch2/branch1
    branch xx
    checkout xx
    commit
    checkout branch2/branch1
    commit
    commit tag: "HEAD" type: HIGHLIGHT
    checkout main
    commit
```

このあたりについてresetコマンドを知ることが理解につながるので、resetについて説明していきます。
resetについては以下のドキュメントが分かりやすかったので、こちらを参考にまとめてみました。

https://git-scm.com/book/ja/v2/Git-%E3%81%AE%E3%81%95%E3%81%BE%E3%81%96%E3%81%BE%E3%81%AA%E3%83%84%E3%83%BC%E3%83%AB-%E3%83%AA%E3%82%BB%E3%83%83%E3%83%88%E3%82%B3%E3%83%9E%E3%83%B3%E3%83%89%E8%A9%B3%E8%AA%AC

:::note warn
正確な情報は上記のドキュメントを読んでください
:::

:::note
補足: Gitのオブジェクト等について

以下ではHEADやcommitなどといったGitが扱っている、Gitオブジェクトやリファレンスなどがでてきますが、この記事で説明しようとすると大変なので、省略してざっくり理解できるように進めていきます。
知りたい方は個人的には以下の記事がとてもわかりやすかったです!

https://qiita.com/marchin_1989/items/2ec01553e907f3a9e6bb

またドキュメントも図解しながら説明されているので理解しやすかったです。

https://git-scm.com/book/ja/v2

:::

### 3つのツリー

Gitで扱っているツリーという概念になぞらえて以下3つを挙げています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143317/3b3db27f-6a08-d228-1a8a-0c748f5b3f5a.png)

端的に言うとこんな感じです。

- HEAD → 最新のcommit
- インデックス → git addしたステージングエリア
- 作業ディレクトリ → 編集する実際のファイル

画像を引用しますが、以下のような流れで作業をしていきます。

|ファイルを変更 → add → commit|
|:----:|
|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143317/0a216dee-4453-b323-8c88-d076164f31b1.png)|
|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143317/81062394-07af-e48d-f95a-85c96c84c140.png)|
|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143317/21ef4b63-5e80-eae8-d0c7-d60587bbc9c4.png)|

以下はfile.txtに3回変更を加えた後の状態を示しています。

|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143317/a0280444-9451-aa7a-0a9c-cae02b60a490.png)|
|----|

## --soft

はじめに--softオプションから理解を深めます。
softはHEADを指定したところまで移動させますが、インデックスと作業ディレクトリは変更しません。

|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143317/5b47548e-c8ab-0cf2-51ec-6001d5f8f50a.png)|
|----|

以下のように、`Changes to be committed`に緑色の文字でcommitした内容が存在しています。
そして、`git log`をすると、HEADが移動したことにより最終commitが`file.txt v2`のコミットとなるので、3つ目のcommitが取り消されたように見えます。

```bash
$ git reset --soft HEAD~
$ git status
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        modified:   file.txt
```

インデックスと作業ディレクトリは変更されないため、編集したファイルの変更内容は取り消されずに残っています。

### もとに戻す

さて、次のオプションも試したいので、`file.txt v3`のコミットまで戻りたいのですが、これも`git reset --soft`を使って戻すことができます。

前提として、`file.txt v3`のコミットハッシュを知っておく必要があるのですが、この例だと、以下のように実行することで、元に戻すことができます。

```bash
git reset --soft 38eb946
```

これも指定したコミットまでHEAD (とブランチ) を移動したことにより、元に戻った状態となります。

先程の、↓の画像の矢印が逆に動いたことになります。

|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143317/5b47548e-c8ab-0cf2-51ec-6001d5f8f50a.png)|
|----|

## --mixed (デフォルト)

さて、元に戻ったので、今度はmixedオプションを試します。
これはデフォルトなのでフラグがなくても同様の結果となります。

mixedオプションはHEADとインデックスの内容を変更します。

|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143317/63d85303-bcb6-99c2-94d4-5f985589f651.png)|
|----|

以下のように、`Changes not staged for commit`に赤色の文字でcommitした内容が存在しています。

```bash
$ git reset HEAD~
$ git status
On branch main
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git restore <file>..." to discard changes in working directory)
        modified:   file.txt
```

作業ディレクトリは変更されないため、編集したファイルの変更内容は取り消されずに残っています。

最後のhardオプションを試したいので元に戻します。

```bash
git reset 38eb946
```

## --hard

そして最後に今回の目的のhardです。
hardオプションは作業ディレクトリにも変更を加えます。

|![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/143317/7adf977b-2561-17ea-46de-e0f8b45e3da8.png)|
|----|

hardオプションを実行すると、作業ディレクトリも変更されるので、git statusをしても差分が表示されません。
そして手元のファイルを開いてみると、最後のコミットの変更内容がなかったことのように元に戻っています。

```
$ git reset --hard HEAD~
HEAD is now at 9e5e6a4 file.txt v2
$ git status
On branch main
nothing to commit, working tree clean
```

このように、変更された内容が消されてしまった状態になるので注意が必要です。

でも実際にはcommitが消されたわけではないので､今回の例だと、`file.txt v3`はcommitされており、ハッシュ値も分かっているので、前の例のように元に戻すことができます。

```bash
git reset --hard 38eb946
```

これで作業ディレクトリの内容も復元されます。

### もとに戻せないケース

ただし、commitされていない状態だと元に戻すことができなくなります。例えば、

```bash
$ touch hoge.txt
$ git add hoge.txt
$ git status
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        new file:   hoge.txt
````

とcommitされていないと、

```bash
$ git reset --hard
$ git status
On branch main
nothing to commit, working tree clean
```

このように作業ディレクトリが上書きされて、変更自体がなくなってしまいます。

## 最初の例

```bash
git reset --hard branch2
```

最初の例の上記のコマンドに戻ります。以下の状態のように

```mermaid
%%{init: { 'themeVariables': {
              'tagLabelFontSize': '14px'
       } } }%%
gitGraph
    commit
    commit
    branch branch2
    branch branch1
    checkout branch1
    commit tag: "HEAD" type: HIGHLIGHT
    checkout branch2
    commit
    commit
    checkout main
    commit
```

branch1にいる時に 上記コマンドを実行すると、HEAD、インデックス、作業ディレクトリすべてがbranch2のコミットと同じものに書き換えられるため、`git checkout branch2`をしたときのように移動したかに見えました。

### `git checkout branch2`と違う点

実はcheckoutとの違いとしては2点あります。

https://git-scm.com/book/ja/v2/Git-%E3%81%AE%E3%81%95%E3%81%BE%E3%81%96%E3%81%BE%E3%81%AA%E3%83%84%E3%83%BC%E3%83%AB-%E3%83%AA%E3%82%BB%E3%83%83%E3%83%88%E3%82%B3%E3%83%9E%E3%83%B3%E3%83%89%E8%A9%B3%E8%AA%AC#_%E3%83%81%E3%82%A7%E3%83%83%E3%82%AF%E3%82%A2%E3%82%A6%E3%83%88%E3%81%A8%E3%81%AE%E9%81%95%E3%81%84

1. 未保存の変更の扱い
    1. resetはチェックが行われずにすべてが上書きされる
    1. checkoutは未保存の変更をチェックして、作業ディレクトリを守ろうとする
1. HEADの更新方法
    1. resetはブランチが移動
    1. checkoutはHEADが別ブランチに移動するだけで、branchは変更されない

このような違いを使い分ける必要があります。