もう一度綺麗な状態に立ち返りたくなったら（gitプロジェクト）

git を使っていると、変更を取り消したいときって度々訪れますよね。
特にチューニング系の何かをしていたりリファクタリングしたりしていると、「あ、前のコミットの方が良かったな...」と思うことが多々あります。
その際、プロジェクトが git管理されていれば取り消しは用意なのですが、git の概念を理解していないと、「あれ？ここ `git reset --` で消えるんだっけ？ `git checkout .`？」となることがあります。

本記事では、`git reset --`、`git checkout .`、`git clean -f` の違いを図示することで、ちゃんと理解しようということを目的としています。

### Excuse

簡単のため、index tree の話とかは出てきません。
厳密な動作を知りたい方はここで読むのを止めてソースコードをあさってくださいm(_ _)m
https://github.com/git/git


### git管理されたリポジトリ上のファイル

git は概念上、ローカルリポジトリだけでバージョン管理することも可能なのですが、通常は github などのリモートリポジトリにあげて分散管理することが多いです。
そこで、リモートリポジトリにプッシュするまでのファイルの動きを図示してみます。

![git_undo.png](https://qiita-image-store.s3.amazonaws.com/0/52780/10f939e5-f686-5658-5073-2b990644e2af.png)

まずは、ローカルのファイルストレージでファイルを新規作成します（図左下）。

```bash
$ touch new_file.txt

$ git status
On branch master
Untracked files:
  (use "git add <file>..." to include in what will be committed)

	new_file.txt

nothing added to commit but untracked files present (use "git add" to track)
```


このファイルは git で管理されていないため、`git add` を実行してファイルをステージに上げる必要があります。

```bash
$ git add new_file.txt

$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

	new file:   new_file.txt

```

このステージにあげられたファイルは、いわば準備状態なので、これをローカルリポジトリで確定するために、`git commit` を実行します。

```bash
$ git commit -m 'initial commit'
[master a0a2837] initial commit
 1 file changed, 0 insertions(+), 0 deletions(-)
 create mode 100644 new_file.txt

$ git status
On branch master
nothing to commit, working tree clean
```

これで、ローカルリポジトリに `new_file.txt` がコミットされました。
このファイルに新しい変更を加えてみます。

```bash
$ echo "hello, world" >> new_file.txt

$ git status
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

	modified:   new_file.txt

no changes added to commit (use "git add" and/or "git commit -a")
```

`new_file.txt` の状態が `modified` となっていますが、同時に `Changes not staged for commit` と書かれています。
つまり、この更新はローカルファイルシステム上のみのもので、リポジトリには何も変更がないことを示しています。
そこで、再度 `git add`、`git commit` します。

```bash
$ git add new_file.txt

$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

	modified:   new_file.txt

$ git commit -m 'second commit'
[master ff64c20] second commit
 1 file changed, 1 insertion(+)

$ git status
On branch master
nothing to commit, working tree clean
```

これでローカルリポジトリに変更が書き込まれたので、あとはリモートリポジトリに `git push`すれば、このファイルの変更がリモートリポジトリに反映されます。

```
$ git push
...（省略）
```

ここまでが git の基本的なファイル追加・変更の流れです。

### ステージの取り消し：`git reset --`

ローカルリポジトリにステージしてしまったファイル（`git add`の直後）の変更を取り消す場合は、
`git reset --` を使用します。

![git_reset.png](https://qiita-image-store.s3.amazonaws.com/0/52780/f4127a70-cac4-4ac4-35be-01c4db0b5002.png)

```bash
# 新しくファイルを追加
$ touch another_new_file.txt

# ファイルをステージ
$ git add another_new_file.txt

# 状態確認
$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

	new file:   another_new_file.txt

# => git にトラックされている

# 取り消し
$ git reset --

# 状態確認
$ git status
On branch master
Untracked files:
  (use "git add <file>..." to include in what will be committed)

	another_new_file.txt

nothing added to commit but untracked files present (use "git add" to track)

# => Untracked files になっている
```

厳密に言うと、`git reset` はローカルリポジトリの履歴を遡るコマンドです（※）。
図で言うと、真ん中の軸で上下方向に辿るコマンドと言えます。

※ より厳密に言えば、index tree を操作するコマンドです。興味のある方は[ソースコード](https://github.com/git/git/blob/master/builtin/reset.c)を読むと良いです。


### ローカルファイルシステムの変更取り消し：`git checkout .`

ローカルファイルシステムで変更したファイルの変更を取り消す場合は、
`git checkout .` を使用します。

![git_checkout.png](https://qiita-image-store.s3.amazonaws.com/0/52780/2ff93458-e66e-8a2b-a64d-44671073aeb0.png)


```bash
# 状態確認
$ git status
On branch master
Changes not staged for commit:
  (use "git add <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

	modified:   new_file.txt

no changes added to commit (use "git add" and/or "git commit -a")

# => コミットされていない変更がある

# 取り消し
$ git checkout .

# 状態確認
$ git status
On branch master
nothing to commit, working tree clean

# => 変更が取り消されている
```

図で言うと、左の軸の上下方向に辿るコマンドと言えます。
ただし、対象は git で管理されているファイルに限ります。

### 新しいファイルの取り消し：`git clean -f`

まだ git で管理されていないファイルを全て消す場合、`git clean -f` を使用します。

![git_clean.png](https://qiita-image-store.s3.amazonaws.com/0/52780/8d6acdc5-05fd-f2e1-3361-ab77104f0391.png)

```bash
# 状態確認
$ git status
On branch master
Untracked files:
  (use "git add <file>..." to include in what will be committed)

	another_new_file.txt

nothing added to commit but untracked files present (use "git add" to track)

# => ステージされていないファイルがある

# 取り消し
$ git clean -f
Removing another_new_file.txt

# 状態確認
$ git status
On branch master
nothing to commit, working tree clean

# => ステージされていないファイルは全てなくなっている
```

こちらも図で言うと、左の軸の上下方向に辿るコマンドと言えます。
ただし、対象は git で管理されていないファイルに限ります。

## おわりに

これら 3つのコマンドを使用すると、試行錯誤途中の変更をリセットできるため、覚えておくと役立ちます。
ただし、これらのコマンドが手癖になってしまうと大切な更新が消えてしまうことがあるため、あまり使いすぎないことをオススメします。
どうでも良い話ですが、僕は `ls` と `gs`（`git status` のエイリアスとして登録している：ghostscript 使うときはちょっと困る）が癖になっています。