GitのWorking TreeとIndexが分かる記事

　こんにちは。

　この記事は、GitのWorking TreeやIndexについて分かるようになる記事です。

- なぜコミットの前にgit addするのか分からない
- git restoreで何が復元されるのかが分からない
- git reset で何がリセットされるのか分からない

　このような疑問を持つ人向けに、**Working Tree** (Working Directory)と**Index** (Staging Area)について解説していきたいと思います。またよく使うGitコマンドがこれらにどう作用するかについても見ていきたいと思います。

　用語の定義はこちらのGit公式リファレンスから引用しています。

https://git-scm.com/book/en/v2/Git-Tools-Reset-Demystified

## 今回使うリポジトリの準備

　あまりGitリポジトリを初期化したことのない方が読む可能性を考え、このセクションに今回使うリポジトリの準備方法を書いておきます。折りたたんでおきますので、見たい方のみご覧ください。

<details><summary>リポジトリの準備</summary><div>

1. 適当なフォルダを作成し、そこにcmdやbashで`cd`を使って移動します。

`{your-folder}`をご自身で作成したフォルダに読み替えてください。

```bash
$ cd {your-folder}
```

2. Gitリポジトリを作成し、自分の名前やメールアドレスを設定します。

　この名前やメールアドレスは、GitHubなどのサービスに`git push`を行った時点で公開されます。また`git push`が自動的にされることはありません。

```bash
$ git init
$ git config --local user.name "Your Name"
$ git config --local user.email "you@example.com"
```

3. Gitで管理する対象のテキストファイルを作成します。

```bash
$ echo Hello > hello.txt
$ git add hello.txt
$ git commit -m 'initial commit'
```

4. 正しく操作が行えていることを確認します。

    `git log`コマンドを実行して、次のようにコミットが作成されていることを確認してください。
    名前や`commit`の次に続く文字列, コミット日時などは違っていても問題ありません。

```bash
$ git log
commit a0fc295ef07209bcc5b3f5b3f9bd93adb84f7f5a (HEAD -> main)
Author: YourName <youremail@example.com>
Date:   Wed Feb 23 02:40:25 2022 +0900

    initial commit
```

以上で準備は終了です。

5. トラブルシューティング

    上の手順でエラーが出て進めない場合、エラーメッセージに応じて手順をやり直してください。

    次のエラーが出た場合は手順2に戻ってください。Gitリポジトリの初期化(`git init`)ができていません。
    > fatal: not a git repository (or any of the parent directories): .git

    次のエラーが出た場合は手順2に戻ってください。名前やメールアドレスの設定が不足しています。
    > Author identity unknown
    >
    > *** Please tell me who you are.

    次のエラーが出た場合は手順3に戻ってください。コミットができていません。
    > fatal: your current branch 'main' does not have any commits yet
</div></details>

## Working TreeとIndexの概観

　まずは簡単にWorking TreeとIndexの概観を説明します。

　以下の図は、`git add`を行うことでWorking Treeにある変更をIndexに登録し、`git commit`を行うことでIndexに登録した変更をコミットできることを表しています。

　この概観を踏まえて各セクションに進むと読みやすいと思います。

![workingtree-overview.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/641250/d5535889-1ed0-656c-fc2b-09d95ba35c09.png)

## Working Treeとは？

　**Working Tree** (Working Directory) とは、今まさにあなたが触っているファイル群のことです。
　Gitでは、Working Treeの役割を*砂場*と説明しています。このWorking Tree上はコードを修正する実際の作業を行ったり、まだ採用するか分からない修正を試してみる場だからです。

　変更を加えたファイルは、`git status`を実行した時に*Changes not staged for commit:*の下に羅列されます。これによって、どのファイルを変更したかを確認することができます。
　例えば、Git管理されている`hello.txt`に変更を加えると、次のように表示されます。

```bash
(main)$ echo "Bonjour, Monde" > hello.txt
(main *)$ 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:   hello.txt

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

　この`modified:   hello.txt`の部分が、現在Working Tree上に`hello.txt`に対する変更があることを示しています。

### git diffでWorking Treeの差分を確認する

　`git diff`コマンドはこのWorking Treeへの修正の差分を表示するコマンドです。
　Working Treeに差分があるときに`git diff`を実行すると、次のように`hello.txt`の中身がどのように変更されたのかを確認することができます。

```diff:git-diff
diff --git a/hello.txt b/hello.txt
index 3fa0d4b..97f001a 100644
--- a/hello.txt
+++ b/hello.txt
@@ -1 +1 @@
-Hello
+Bonjour
```

　これは`hello.txt`の中身が`Hello`から`Bonjour`に変更されていることを示しています。

　注意すべき点として、`git diff`が表示しているのは**Working Tree**と**Index**の差分であることが挙げられます。
　先ほどはIndexに登録された修正がなかったため、Working TreeとCommit HEAD (一番上のコミットを`HEAD`と呼びます)の差分が表示されているように見えました。しかしIndexに変更を登録しながらWorking Treeを修正する場合には、`git diff`の表示がWorking TreeとIndexとの差分であることを意識する必要があります。

　Git概観の再掲：
![workingtree-diff.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/641250/0883f062-3648-183e-330d-990083581756.png)


### git restoreでWorking Treeの変更を取り消す

　`git restore <FILE>`を実行すると、**Working Tree**上の変更を取り消すことができます。

```bash
$ (main *)$ git restore hello.txt
$ (main)$ git status # hello.txtへの修正が取り消された
On branch main
nothing to commit, working tree clean
```

　くどいようですが、ここで取り消される変更はWorking Treeのもののみです。Indexに登録された変更やコミットに影響を与えることはありません。
　試しに変更をIndexに登録してから`git restore`を実行してみましょう。

```bash
$ (main)$ echo Bonjour > hello.txt # 改めてhello.txtを変更する
$ (main *)$ git add hello.txt      # hello.txtをIndexに登録する
$ (main +)$ git restore hello.txt  # hello.txtのワーキングツリー上の変更を取り消す
$ (main +)$ git status
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        modified:   hello.txt
```

　`git add`を行った後に`git restore`を行っても変更が残っていることが分かります。
　これは、hello.txtにはワーキングツリー上の変更がもはやない(Indexに登録されている)ためです。
![workingtree-restore.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/641250/043c767b-d00d-397b-5455-3bf8ea4c1f0f.png)


#### 余談: 新規ファイルはWorking Treeには含まれない

　新規ファイルは`git add`を行うまではGitの管理対象のファイルではありません。

　`git status`を実行すると、新規ファイルは*Untracked files:*の下に羅列されます。これは「Gitによって追跡されていないファイル」ということです。

```bash
(main *)$ echo Guten Tag > gutentag.txt # 新規ファイルを作成
(main *)$ 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:   hello.txt

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        gutentag.txt

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

　また`git diff`を行っても新規ファイルの差分は表示されません。

## Indexとは？

　**Index** (Staging Area) とは、次に`git commit`する時にコミットされる準備段階のことです。

　　変更を加えたファイルは、`git status`を実行した時に*Changes to be committed:*の下に羅列されます。これによって、どのファイルが次のコミットに取り込まれるかを確認することができます。
　例えば、hello.txtをIndexに登録すると、次のように表示されます。

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

### git diff --cached でIndexの差分を確認する

　`git diff`でWorking TreeとIndexの差分を確認できたように、`git diff --cached`を使うことで、**Index**と`HEAD`の差分を確認することができます。
　`HEAD`とは現在の一番上の(新しい)コミットのことです。

　Indexに何かしらの変更が登録されているときに`git diff --cached`コマンドを実行すると、次のように差分を表示することができます。

```diff:git-diff--cached
diff --git a/hello.txt b/hello.txt
index e965047..632e4fe 100644
--- a/hello.txt
+++ b/hello.txt
@@ -1 +1 @@
-Hello
+Bonjour
```

Git概観で表現すると次の通りです。`git diff`とは比べている場所が違いますね。
![workingtree-diff-cached.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/641250/90add44f-7189-16c9-7c65-aa252fb8be6e.png)


### git restore --stagedでIndexに登録された変更をWorking Treeに差し戻す

　`git restore <FILE>`でWorking Treeの変更を取り消すことができたように、`git restore --staged <FILE>`でIndexに登録した変更をWorking Treeに差し戻すことができます。

　Indexに`hello.txt`が登録してある状態を、`git restore --staged`を使って解除してみます。

```bash
(main +)$ git restore --staged hello.txt
(main *)$ 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:   hello.txt

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

*Changes not staged for commit:*の下に`modified:   hello.txt`があることから、Indexに登録されていた変更がWorking Treeに差し戻されていることが分かります。
![workingtree-restore-staged.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/641250/8b1e764d-be0c-66c7-3991-8eef75554b3b.png)


　このとき注意するべき点として、Working Treeに変更が入っている状態でIndexの修正を差し戻すと、Working Treeの修正が勝ちます。
　例えば、まずある変更が**Index**に登録されているとします。(Hello -> Bonjour)

```bash
(main +)$ git diff --cached
diff --git a/hello.txt b/hello.txt
index e965047..632e4fe 100644
--- a/hello.txt
+++ b/hello.txt
@@ -1 +1 @@
-Hello
+Bonjour
```

　この状態で、**Working Tree**に変更を加えるとします。(Bonjour -> Guten Tag)

```bash
(main +)$ echo Guten Tag > hello.txt
(main *+)$ git diff
diff --git a/hello.txt b/hello.txt
index 632e4fe..9626a13 100644
--- a/hello.txt
+++ b/hello.txt
@@ -1 +1 @@
-Bonjour
+Guten Tag
```

　この時、`hello.txt`はIndexにある変更が登録されており、かつその状態からさらにWorking Treeが変更されている状態になります。
　`git status`を見ると、*Changes to be committed:*と*Changes not staged for commit:*の両方に`hello.txt`がいることを確認できます。

```bash
(main *+)$ git status
On branch main
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)
        modified:   hello.txt

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:   hello.txt
```

　この時、`git restore hello.txt`を行うと、`hello.txt`の中身は当然Working Treeの変更が取り消されて`Bonjour`になります。
　一方、`git restore --staged hello.txt`を行うと、`hello.txt`の中身は`Guten Tag`になるのです。

### git reset HEADでもOK

　Indexに登録した変更をWorking Treeに戻したいとき、`git reset HEAD`を使うこともできます。
　このコマンドを使うとファイルを一つ一つ指定する必要がなく、まとめてWorking Treeに戻したいときには便利です。

```bash
(main +)$ git reset HEAD
Unstaged changes after reset:
M       hello.txt
```

## おまけ: StashとWorking TreeとIndex

　皆さんがお世話になったことがあるであろう`git stash`は、Working TreeとIndexの変更を両方まとめてStashという領域に保存しておくコマンドです。`git stash pop`や`git stash apply`を行うと、その変更の両方ともが**Working Tree**に適用されます。

![workingtree-stash.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/641250/0e4afe6d-0980-5d2b-e3dd-6ba327f7b227.png)


## おまけ: git reset

　`git reset`とは`HEAD`の位置を動かすコマンドです。
　例えば`git reset HEAD~`とというコマンドを打つと(`HEAD`は一番上のコミットを指し、`~`は一つ前のコミットを指しますので、`HEAD~`は一番上から二番目のコミットを指します)、`HEAD`を一番上から二番目のコミットに移すことができます。つまり、一番上のコミットを取り払うことができるのです。
(`@`は`HEAD`の省略記法です)

![workingtree-reset.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/641250/e3f3cb92-3c07-0568-3684-dbbb324efc1d.png)

　ここで、`git reset`は取り払ったコミットのぶんの変更をどのように扱うかでいくつかの選択肢を持ちます。
　Working Treeに取り込むなら`--mixed`(デフォルト), Indexに取り込むなら`--soft`, 一切を取り込まないなら`--hard`といったオプションがあります。

![workingtree-reset-soft.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/641250/5cb4f82f-5c16-2369-18db-df1d05a1d08a.png)


## おわりに

　この記事を読んでWorking TreeとIndexについての理解が少しでも深まったと思って頂けたら幸いです。
　ここで紹介した以外にも、GitコマンドにはWorking TreeやIndexに作用するものが多くあり、またどちらに作用するかをオプションで切り替えられるものも多くあります。そのようなものを使うとき、自分がどのような操作をしたいのかを意識してオプションを選択できるといいですね。

![workingtree-all.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/641250/00513d46-8b9d-ec18-887d-fd14296591d9.png)