チャプター ▾ 第2版

2.2 Gitの基本 - リポジトリへの変更を記録する

リポジトリに変更を記録する

この時点で、ローカルマシンには「正真正銘の」Gitリポジトリがあり、そのすべてのファイルのチェックアウトまたは「ワーキングコピー」が目の前にあるはずです。通常、プロジェクトが記録したい状態に達するたびに、変更を開始し、その変更のスナップショットをリポジトリにコミットしたいと思うでしょう。

ワーキングディレクトリ内の各ファイルは、「追跡対象」または「未追跡」のいずれかの状態にあることを忘れないでください。追跡対象ファイルとは、最後のスナップショットに含まれていたファイル、および新しくステージングされたファイルです。これらは、未変更、変更済み、またはステージング済みのいずれかの状態になります。要するに、追跡対象ファイルとは、Gitが認識しているファイルです。

未追跡ファイルとは、その他すべてのファイルです。つまり、ワーキングディレクトリ内のファイルで、最後のスナップショットに含まれておらず、ステージング領域にもないファイルです。リポジトリを最初にクローンするときは、Gitがチェックアウトしたばかりで、何も編集していないため、すべてのファイルは追跡対象で未変更になります。

ファイルを編集すると、Gitはそれらを変更済みと見なします。なぜなら、最後のコミット以降にそれらを変更したからです。作業を進めるにつれて、これらの変更されたファイルを段階的にステージングし、その後、ステージングされたすべての変更をコミットし、このサイクルが繰り返されます。

The lifecycle of the status of your files
図8. ファイルのステータスのライフサイクル

ファイルのステータスを確認する

どのファイルがどの状態にあるかを判断するために使用する主なツールは、git statusコマンドです。クローン直後にこのコマンドを実行すると、次のような表示になるはずです。

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
nothing to commit, working tree clean

これは、作業ディレクトリがクリーンであることを意味します。つまり、追跡対象のファイルは何も変更されていません。また、Gitは未追跡のファイルを認識していません。認識していればここにリストされます。最後に、このコマンドは、現在どのブランチにいるか、およびサーバー上の同じブランチから分岐していないことを知らせます。今のところ、そのブランチは常にデフォルトのmasterであり、ここでは心配する必要はありません。Gitブランチでは、ブランチとリファレンスについて詳しく説明します。

GitHubは2020年半ばにデフォルトのブランチ名をmasterからmainに変更し、他のGitホストもそれに倣いました。したがって、新しく作成された一部のリポジトリのデフォルトブランチ名がmasterではなくmainであることがわかるかもしれません。さらに、デフォルトのブランチ名は変更可能であるため(デフォルトのブランチ名で見たように)、デフォルトのブランチに異なる名前が表示されるかもしれません。

ただし、Git自体は依然としてmasterをデフォルトとして使用しているため、本書全体でそれを使用します。

プロジェクトに新しいファイル、単純なREADMEファイルを追加したとしましょう。以前にファイルが存在せず、git statusを実行すると、次のように未追跡ファイルが表示されます。

$ echo 'My Project' > README
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Untracked files:
  (use "git add <file>..." to include in what will be committed)

    README

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

新しいREADMEファイルは、ステータス出力の「Untracked files」の見出しの下にあるため、未追跡であることがわかります。未追跡とは基本的に、Gitが以前のスナップショット(コミット)になかったファイルで、まだステージングされていないファイルを見ていることを意味します。Gitは、明示的にそうするように指示するまで、そのファイルをコミットスナップショットに含めません。これは、生成されたバイナリファイルや、意図せず含めてしまった他のファイルを誤って含めてしまうのを防ぐためです。READMEを含めたいので、ファイルを追跡し始めましょう。

新しいファイルを追跡する

新しいファイルを追跡し始めるには、git addコマンドを使用します。READMEファイルを追跡し始めるには、これを実行します。

$ git add README

ステータスコマンドを再度実行すると、READMEファイルが追跡対象となり、コミットされるようにステージングされていることがわかります。

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
  (use "git restore --staged <file>..." to unstage)

    new file:   README

「Changes to be committed」の見出しの下にあることから、ステージングされていることがわかります。この時点でコミットすると、git addを実行した時点のファイルバージョンが、その後の履歴スナップショットに含まれます。以前にgit initを実行したときに、その後にgit add を実行したことを覚えているかもしれません。これは、ディレクトリ内のファイルを追跡し始めるためでした。git addコマンドは、ファイルまたはディレクトリのパス名を受け取ります。ディレクトリの場合、コマンドはそのディレクトリ内のすべてのファイルを再帰的に追加します。

変更されたファイルをステージングする

すでに追跡されているファイルであるCONTRIBUTING.mdを変更し、再度git statusコマンドを実行すると、次のような結果になります。

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   README

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:   CONTRIBUTING.md

CONTRIBUTING.mdファイルは「Changes not staged for commit」というセクションの下に表示されます。これは、追跡対象のファイルがワーキングディレクトリで変更されたが、まだステージングされていないことを意味します。ステージングするには、git addコマンドを実行します。git addは多目的コマンドです。新しいファイルの追跡を開始したり、ファイルをステージングしたり、マージ競合ファイルを解決済みとしてマークしたりするなど、さまざまなことを行います。「このファイルをプロジェクトに追加する」というよりも、「次のコミットにこのコンテンツを正確に追加する」と考える方が役立つかもしれません。CONTRIBUTING.mdファイルをステージングするためにgit addを実行し、その後、もう一度git statusを実行してみましょう。

$ git add CONTRIBUTING.md
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   README
    modified:   CONTRIBUTING.md

両方のファイルがステージングされ、次のコミットに含まれます。この時点で、コミットする前にCONTRIBUTING.mdに小さな変更を加えたいことを思い出したとします。もう一度ファイルを開いて変更を加え、コミットの準備が整いました。しかし、もう一度git statusを実行してみましょう。

$ vim CONTRIBUTING.md
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   README
    modified:   CONTRIBUTING.md

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:   CONTRIBUTING.md

一体どういうことだ?今、CONTRIBUTING.mdはステージング済みとアンステージング済みの両方としてリストされています。どうしてそんなことが可能なのだろうか?実は、Gitはgit addコマンドを実行した時点のファイルをそのままステージングするのです。もし今コミットした場合、git addコマンドを最後に実行した時点のCONTRIBUTING.mdのバージョンがコミットに含まれ、git commitを実行した時点のワーキングディレクトリのファイルの状態ではありません。git addを実行した後にファイルを変更した場合、ファイルの最新バージョンをステージングするために、もう一度git addを実行する必要があります。

$ git add CONTRIBUTING.md
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    new file:   README
    modified:   CONTRIBUTING.md

短いステータス

git statusの出力はかなり包括的ですが、非常に冗長でもあります。Gitには短いステータスフラグもあり、変更をよりコンパクトに表示できます。git status -sまたはgit status --shortを実行すると、コマンドからのはるかに簡略化された出力が得られます。

$ git status -s
 M README
MM Rakefile
A  lib/git.rb
M  lib/simplegit.rb
?? LICENSE.txt

追跡されていない新しいファイルには??、ステージング領域に追加された新しいファイルにはA、変更されたファイルにはMなどが付いています。出力には2つの列があります。左側の列はステージング領域の状態を示し、右側の列は作業ツリーの状態を示します。したがって、その出力の例では、READMEファイルは作業ディレクトリで変更されていますがまだステージングされていませんが、lib/simplegit.rbファイルは変更されステージングされています。Rakefileは変更され、ステージングされ、その後再び変更されたため、ステージング済みと未ステージングの両方の変更があります。

ファイルを無視する

多くの場合、Gitが自動的に追加したり、未追跡として表示したりしたくない種類のファイルがあるでしょう。これらは通常、ログファイルやビルドシステムによって生成されるファイルなど、自動生成されたファイルです。そのような場合、それらに一致するパターンをリストした.gitignoreという名前のファイルを作成できます。以下は.gitignoreファイルの例です。

$ cat .gitignore
*.[oa]
*~

最初の行は、Gitに「.o」または「.a」で終わるファイルを無視するように指示しています。これらは、コードのビルドによって生成される可能性のあるオブジェクトファイルとアーカイブファイルです。2行目は、多くのテキストエディタ(Emacsなど)が一時ファイルをマークするために使用するチルダ(~)で終わるすべてのファイルをGitに無視するように指示しています。また、log、tmp、またはpidディレクトリ、自動生成されたドキュメントなども含めることができます。新しいリポジトリを開始する前に.gitignoreファイルを設定することは、Gitリポジトリに本当に含めたくないファイルを誤ってコミットしないためにも、一般的に良い考えです。

.gitignoreファイルに記述できるパターンのルールは次のとおりです。

  • 空白行または#で始まる行は無視されます。

  • 標準のグロブパターンが機能し、ワーキングツリー全体に再帰的に適用されます。

  • 再帰を避けるために、パターンをスラッシュ (/) で始めることができます。

  • ディレクトリを指定するには、パターンをスラッシュ (/) で終わらせることができます。

  • 感嘆符 (!) で始めることでパターンを否定できます。

グロブパターンは、シェルが使用する単純化された正規表現のようなものです。アスタリスク (*) は0文字以上の文字にマッチし、[abc]は括弧内の任意の文字(この場合はa、b、またはc)にマッチし、疑問符 (?) は単一の文字にマッチし、ハイフンで区切られた文字を括弧で囲んだもの ([0-9]) はその間の任意の文字(この場合は0から9まで)にマッチします。ネストされたディレクトリにマッチさせるために2つのアスタリスクを使用することもできます。a/**/za/za/b/za/b/c/zなどにマッチします。

以下は、別の.gitignoreファイルの例です。

# ignore all .a files
*.a

# but do track lib.a, even though you're ignoring .a files above
!lib.a

# only ignore the TODO file in the current directory, not subdir/TODO
/TODO

# ignore all files in any directory named build
build/

# ignore doc/notes.txt, but not doc/server/arch.txt
doc/*.txt

# ignore all .pdf files in the doc/ directory and any of its subdirectories
doc/**/*.pdf
ヒント

GitHubは、プロジェクトの出発点となる数十のプロジェクトと言語向けの良い.gitignoreファイルの包括的なリストをhttps://github.com/github/gitignoreで維持しています。

単純なケースでは、リポジトリのルートディレクトリに単一の.gitignoreファイルがあり、それがリポジトリ全体に再帰的に適用されることがあります。しかし、サブディレクトリに追加の.gitignoreファイルを持つことも可能です。これらのネストされた.gitignoreファイル内のルールは、それらが配置されているディレクトリ下のファイルにのみ適用されます。Linuxカーネルのソースリポジトリには206個の.gitignoreファイルがあります。

複数の.gitignoreファイルの詳細は本書の範囲外です。詳細についてはman gitignoreを参照してください。

ステージング済みと未ステージングの変更を確認する

git statusコマンドが曖昧すぎる場合、つまりどのファイルが変更されたかだけでなく、何が正確に変更されたかを知りたい場合は、git diffコマンドを使用できます。git diffについては後で詳しく説明しますが、おそらく最も頻繁に次の2つの質問に答えるために使用するでしょう。まだステージングしていないが変更したもの、そしてコミットしようとしているステージング済みのもの。git statusはファイル名をリストアップすることでこれらの質問に非常に大まかに答えますが、git diffは追加および削除された正確な行、いわばパッチを表示します。

もう一度READMEファイルを編集してステージングし、その後CONTRIBUTING.mdファイルをステージングせずに編集したとしましょう。git statusコマンドを実行すると、再び次のような表示になります。

$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    modified:   README

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:   CONTRIBUTING.md

変更したがまだステージングしていないものを見るには、他の引数なしでgit diffと入力します。

$ git diff
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 8ebb991..643e24f 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -65,7 +65,8 @@ branch directly, things can get messy.
 Please include a nice description of your changes when you submit your PR;
 if we have to read the whole diff to figure out why you're contributing
 in the first place, you're less likely to get feedback and have your change
-merged in.
+merged in. Also, split your changes into comprehensive chunks if your patch is
+longer than a dozen lines.

 If you are starting to work on a particular area, feel free to submit a PR
 that highlights your work in progress (and note in the PR title that it's

このコマンドは、ワーキングディレクトリの内容とステージング領域の内容を比較します。結果として、まだステージングしていない変更が表示されます。

次のコミットに含まれるステージングされた変更を確認したい場合は、git diff --stagedを使用できます。このコマンドは、ステージングされた変更を最後のコミットと比較します。

$ git diff --staged
diff --git a/README b/README
new file mode 100644
index 0000000..03902a1
--- /dev/null
+++ b/README
@@ -0,0 +1 @@
+My Project

git diff単独では、最後のコミット以降のすべての変更ではなく、まだステージングされていない変更のみが表示されることに注意することが重要です。すべての変更をステージングしている場合、git diffは何も出力しません。

別の例として、CONTRIBUTING.mdファイルをステージングしてから編集した場合、git diffを使用して、ファイル内のステージングされた変更とステージングされていない変更を確認できます。環境が次のようになっているとします。

$ git add CONTRIBUTING.md
$ echo '# test line' >> CONTRIBUTING.md
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    modified:   CONTRIBUTING.md

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:   CONTRIBUTING.md

これで、git diffを使ってまだステージングされていないものを見ることができます。

$ git diff
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 643e24f..87f08c8 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -119,3 +119,4 @@ at the
 ## Starter Projects

 See our [projects list](https://github.com/libgit2/libgit2/blob/development/PROJECTS.md).
+# test line

そして、git diff --cachedを使って、これまでにステージングしたものを見ることができます(--staged--cachedは同義です)。

$ git diff --cached
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 8ebb991..643e24f 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -65,7 +65,8 @@ branch directly, things can get messy.
 Please include a nice description of your changes when you submit your PR;
 if we have to read the whole diff to figure out why you're contributing
 in the first place, you're less likely to get feedback and have your change
-merged in.
+merged in. Also, split your changes into comprehensive chunks if your patch is
+longer than a dozen lines.

 If you are starting to work on a particular area, feel free to submit a PR
 that highlights your work in progress (and note in the PR title that it's
外部ツールでのGit Diff

本書の残りの部分では、さまざまな方法でgit diffコマンドを使用し続けます。もしグラフィカルな、または外部の差分表示プログラムを好む場合は、これらの差分を見る別の方法があります。git diffの代わりにgit difftoolを実行すると、emerge、vimdiff、その他多くのソフトウェア(商用製品を含む)でこれらの差分を表示できます。システムで利用可能なツールを確認するには、git difftool --tool-helpを実行してください。

変更をコミットする

ステージング領域が思い通りに設定されたので、変更をコミットできます。まだステージングされていないもの、つまり、編集後にgit addを実行していない作成または変更されたファイルは、このコミットには含まれません。それらはディスク上の変更されたファイルのまま残ります。このケースでは、最後にgit statusを実行したときに、すべてがステージングされていることがわかったので、変更をコミットする準備ができています。コミットする最も簡単な方法は、git commitと入力することです。

$ git commit

そうすると、選択したエディタが起動します。

これはシェルのEDITOR環境変数によって設定されます。通常はvimまたはemacsですが、はじめにで見たように、git config --global core.editorコマンドを使用して好きなエディタを設定できます。

エディタには次のテキストが表示されます(この例はVimの画面です)。

# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# On branch master
# Your branch is up-to-date with 'origin/master'.
#
# Changes to be committed:
#	new file:   README
#	modified:   CONTRIBUTING.md
#
~
~
~
".git/COMMIT_EDITMSG" 9L, 283C

デフォルトのコミットメッセージには、コメントアウトされたgit statusコマンドの最新の出力と、一番上に空の行が1つ含まれていることがわかります。これらのコメントを削除してコミットメッセージを入力することも、それらを残して何をコミットしているかを思い出すのに役立てることもできます。

変更内容をさらに明確に確認したい場合は、-vオプションをgit commitに渡すことができます。これにより、変更の差分もエディタに表示され、コミットする変更を正確に確認できます。

エディタを終了すると、Gitはそのコミットメッセージでコミットを作成します(コメントと差分は削除されます)。

あるいは、-mフラグの後に指定することで、commitコマンドと同時にコミットメッセージを記述することもできます。

$ git commit -m "Story 182: fix benchmarks for speed"
[master 463dc4f] Story 182: fix benchmarks for speed
 2 files changed, 2 insertions(+)
 create mode 100644 README

これで最初のコミットが作成されました!コミットはそれ自体に関するいくつかの出力(コミットしたブランチ (master)、コミットが持つSHA-1チェックサム (463dc4f)、変更されたファイルの数、コミットで追加および削除された行に関する統計)を提供していることがわかります。

コミットは、ステージング領域で設定したスナップショットを記録することを忘れないでください。ステージングしなかったものはすべて、まだ変更されたまま残っています。別のコミットを実行して、履歴に追加できます。コミットを実行するたびに、プロジェクトのスナップショットを記録していることになります。これは、後で元に戻したり、比較したりすることができます。

ステージング領域をスキップする

コミットを思い通りに作成するのに驚くほど役立つステージング領域ですが、ワークフローによっては少し複雑すぎる場合があります。ステージング領域をスキップしたい場合、Gitは簡単なショートカットを提供します。git commitコマンドに-aオプションを追加すると、Gitはコミットを実行する前に、すでに追跡されているすべてのファイルを自動的にステージングし、git addの部分をスキップできます。

$ git status
On branch master
Your branch is up-to-date with 'origin/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:   CONTRIBUTING.md

no changes added to commit (use "git add" and/or "git commit -a")
$ git commit -a -m 'Add new benchmarks'
[master 83e38c7] Add new benchmarks
 1 file changed, 5 insertions(+), 0 deletions(-)

この場合、コミットする前にCONTRIBUTING.mdファイルに対してgit addを実行する必要がないことに注目してください。これは、-aフラグが変更されたすべてのファイルを含むためです。これは便利ですが、注意が必要です。このフラグにより、意図しない変更が含まれてしまうことがあります。

ファイルの削除

Gitからファイルを削除するには、そのファイルを追跡対象ファイルから削除し(より正確には、ステージング領域から削除し)、コミットする必要があります。git rmコマンドはそれを行い、さらにワーキングディレクトリからもファイルを削除するため、次回は未追跡ファイルとして表示されません。

作業ディレクトリからファイルを削除するだけでは、git statusの出力で「Changes not staged for commit」(つまりアンステージング済み)領域に表示されます。

$ rm PROJECTS.md
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes not staged for commit:
  (use "git add/rm <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)

        deleted:    PROJECTS.md

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

その後、git rmを実行すると、ファイルの削除がステージングされます。

$ git rm PROJECTS.md
rm 'PROJECTS.md'
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    deleted:    PROJECTS.md

次回のコミット時には、そのファイルは削除され、追跡対象ではなくなります。ファイルを変更していた場合、またはすでにステージング領域に追加していた場合は、-fオプションを指定して強制的に削除する必要があります。これは、まだスナップショットに記録されておらず、Gitから復元できないデータを誤って削除するのを防ぐための安全機能です。

もう一つ役立つこととして、ファイルをワーキングツリーに残したまま、ステージング領域から削除したい場合があります。つまり、ファイルをハードドライブに残したいが、Gitにはもう追跡させたくない場合です。これは、.gitignoreファイルに何かを追加し忘れて、大きなログファイルや大量の.aコンパイル済みファイルなどのファイルを誤ってステージングしてしまった場合に特に便利です。これを行うには、--cachedオプションを使用します。

$ git rm --cached README

git rmコマンドにファイル、ディレクトリ、およびファイルグロブパターンを渡すことができます。つまり、次のようなことができます。

$ git rm log/\*.log

*の前のバックスラッシュ(\)に注目してください。これは、Gitがシェルでのファイル名展開に加えて、独自のファイル名展開を行うために必要です。このコマンドは、log/ディレクトリにある.log拡張子を持つすべてのファイルを削除します。または、次のようなこともできます。

$ git rm \*~

このコマンドは、名前に~が付くすべてのファイルを削除します。

ファイルの移動

他の多くのVCSとは異なり、Gitはファイルの移動を明示的に追跡しません。Gitでファイルの名前を変更しても、Gitにファイルの名前を変更したことを伝えるメタデータは保存されません。しかし、Gitは後からそれをかなり巧妙に特定します。ファイル移動の検出については後ほど詳しく説明します。

したがって、Gitにmvコマンドがあるのは少し混乱します。Gitでファイルの名前を変更したい場合は、次のようなコマンドを実行できます。

$ git mv file_from file_to

そして、それはうまく機能します。実際、このようなコマンドを実行してステータスを見ると、Gitがそれを名前変更されたファイルと見なしていることがわかります。

$ git mv README.md README
$ git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    renamed:    README.md -> README

ただし、これは次のように実行するのと同等です。

$ mv README.md README
$ git rm README.md
$ git add README

Gitはそれを暗黙的に名前変更と判断するため、ファイルをそのように名前変更しても、mvコマンドで名前変更しても問題ありません。唯一の違いは、git mvが3つのコマンドではなく1つのコマンドであるということです。これは便利な機能です。さらに重要なのは、好きなツールを使ってファイルの名前を変更し、コミットする前にadd/rmを後で処理できることです。

scroll-to-top