こんにちは。
この記事は、GitのWorking TreeやIndexについて分かるようになる記事です。
- なぜコミットの前にgit addするのか分からない
- git restoreで何が復元されるのかが分からない
- git reset で何がリセットされるのか分からない
このような疑問を持つ人向けに、Working Tree (Working Directory)とIndex (Staging Area)について解説していきたいと思います。またよく使うGitコマンドがこれらにどう作用するかについても見ていきたいと思います。
用語の定義はこちらのGit公式リファレンスから引用しています。
今回使うリポジトリの準備
あまりGitリポジトリを初期化したことのない方が読む可能性を考え、このセクションに今回使うリポジトリの準備方法を書いておきます。折りたたんでおきますので、見たい方のみご覧ください。
リポジトリの準備
- 適当なフォルダを作成し、そこにcmdやbashでcdを使って移動します。
{your-folder}をご自身で作成したフォルダに読み替えてください。
$ cd {your-folder}
- Gitリポジトリを作成し、自分の名前やメールアドレスを設定します。
 この名前やメールアドレスは、GitHubなどのサービスにgit pushを行った時点で公開されます。またgit pushが自動的にされることはありません。
$ git init
$ git config --local user.name "Your Name"
$ git config --local user.email "you@example.com"
- Gitで管理する対象のテキストファイルを作成します。
$ echo Hello > hello.txt
$ git add hello.txt
$ git commit -m 'initial commit'
- 
正しく操作が行えていることを確認します。 git logコマンドを実行して、次のようにコミットが作成されていることを確認してください。
 名前やcommitの次に続く文字列, コミット日時などは違っていても問題ありません。
$ git log
commit a0fc295ef07209bcc5b3f5b3f9bd93adb84f7f5a (HEAD -> main)
Author: YourName <youremail@example.com>
Date:   Wed Feb 23 02:40:25 2022 +0900
    initial commit
以上で準備は終了です。
- 
トラブルシューティング 上の手順でエラーが出て進めない場合、エラーメッセージに応じて手順をやり直してください。 次のエラーが出た場合は手順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 
Working TreeとIndexの概観
まずは簡単にWorking TreeとIndexの概観を説明します。
 以下の図は、git addを行うことでWorking Treeにある変更をIndexに登録し、git commitを行うことでIndexに登録した変更をコミットできることを表しています。
この概観を踏まえて各セクションに進むと読みやすいと思います。
Working Treeとは?
 Working Tree (Working Directory) とは、今まさにあなたが触っているファイル群のことです。
 Gitでは、Working Treeの役割を砂場と説明しています。このWorking Tree上はコードを修正する実際の作業を行ったり、まだ採用するか分からない修正を試してみる場だからです。
 変更を加えたファイルは、git statusを実行した時に*Changes not staged for commit:*の下に羅列されます。これによって、どのファイルを変更したかを確認することができます。
 例えば、Git管理されているhello.txtに変更を加えると、次のように表示されます。
(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 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 restoreでWorking Treeの変更を取り消す
 git restore <FILE>を実行すると、Working Tree上の変更を取り消すことができます。
$ (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を実行してみましょう。
$ (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に登録されている)ためです。

余談: 新規ファイルはWorking Treeには含まれない
 新規ファイルはgit addを行うまではGitの管理対象のファイルではありません。
 git statusを実行すると、新規ファイルは*Untracked files:*の下に羅列されます。これは「Gitによって追跡されていないファイル」ということです。
(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に登録すると、次のように表示されます。
(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 a/hello.txt b/hello.txt
index e965047..632e4fe 100644
--- a/hello.txt
+++ b/hello.txt
@@ -1 +1 @@
-Hello
+Bonjour
Git概観で表現すると次の通りです。git diffとは比べている場所が違いますね。

git restore --stagedでIndexに登録された変更をWorking Treeに差し戻す
 git restore <FILE>でWorking Treeの変更を取り消すことができたように、git restore --staged <FILE>でIndexに登録した変更をWorking Treeに差し戻すことができます。
 Indexにhello.txtが登録してある状態を、git restore --stagedを使って解除してみます。
(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に差し戻されていることが分かります。

 このとき注意するべき点として、Working Treeに変更が入っている状態でIndexの修正を差し戻すと、Working Treeの修正が勝ちます。
 例えば、まずある変更がIndexに登録されているとします。(Hello -> Bonjour)
(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)
(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がいることを確認できます。
(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に戻したいときには便利です。
(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に適用されます。
おまけ: git reset
 git resetとはHEADの位置を動かすコマンドです。
 例えばgit reset HEAD~とというコマンドを打つと(HEADは一番上のコミットを指し、~は一つ前のコミットを指しますので、HEAD~は一番上から二番目のコミットを指します)、HEADを一番上から二番目のコミットに移すことができます。つまり、一番上のコミットを取り払うことができるのです。
(@はHEADの省略記法です)
 ここで、git resetは取り払ったコミットのぶんの変更をどのように扱うかでいくつかの選択肢を持ちます。
 Working Treeに取り込むなら--mixed(デフォルト), Indexに取り込むなら--soft, 一切を取り込まないなら--hardといったオプションがあります。
おわりに
 この記事を読んでWorking TreeとIndexについての理解が少しでも深まったと思って頂けたら幸いです。
 ここで紹介した以外にも、GitコマンドにはWorking TreeやIndexに作用するものが多くあり、またどちらに作用するかをオプションで切り替えられるものも多くあります。そのようなものを使うとき、自分がどのような操作をしたいのかを意識してオプションを選択できるといいですね。







