こんにちは。
この記事は、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に作用するものが多くあり、またどちらに作用するかをオプションで切り替えられるものも多くあります。そのようなものを使うとき、自分がどのような操作をしたいのかを意識してオプションを選択できるといいですね。