章 ▾ 第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ホストもそれに追随しました。したがって、新しく作成された一部のリポジトリでは、デフォルトのブランチ名がmainであり、masterではないことに気付くかもしれません。さらに、デフォルトのブランチ名は変更可能であるため(デフォルトのブランチ名で見たように)、異なるデフォルトブランチ名が表示される場合もあります。

ただし、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 <files> を実行したことを覚えているかもしれません。これは、ディレクトリ内のファイルの追跡を開始するためでした。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コマンドを実行した時点と全く同じようにファイルをステージングすることが判明しています。もし今コミットすると、CONTRIBUTING.mdのバージョンは、最後にgit addコマンドを実行した時点のものがコミットに含まれ、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]
*~

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

.gitignoreファイルに含めることができるパターンのルールは次のとおりです。

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

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

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

  • ディレクトリを指定するために、パターンをスラッシュ(/)で終了できます。

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

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

以下に、もう1つの.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で管理しています。プロジェクトの開始点として役立つでしょう。

注意

単純なケースでは、リポジトリのルートディレクトリに1つの.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行含まれていることがわかります。これらのコメントを削除してコミットメッセージを入力することも、コミットする内容を思い出すために残しておくこともできます。

注意

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

エディターを終了すると、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