Git
Chapters ▾ 2nd Edition

2.2 Git の基本 - 変更内容のリポジトリへの記録

変更内容のリポジトリへの記録

これで、れっきとした Git リポジトリを準備して、そのプロジェクト内のファイルの作業コピーを取得することができました。 次は、そのコピーに対して何らかの変更を行い、適当な時点で変更内容のスナップショットをリポジトリにコミットすることになります。

作業コピー内の各ファイルには追跡されている(tracked)ものと追跡されてない(untracked)ものの二通りがあることを知っておきましょう。 追跡されているファイルとは、直近のスナップショットに存在したファイルのことです。これらのファイルについては変更されていない(unmodified)」「変更されている(modified)」「ステージされている(staged)」の三つの状態があります。 追跡されていないファイルは、そのどれでもありません。直近のスナップショットには存在せず、ステージングエリアにも存在しないファイルのことです。 最初にプロジェクトをクローンした時点では、すべてのファイルは「追跡されている」かつ「変更されていない」状態となります。チェックアウトしただけで何も編集していない状態だからです。

ファイルを編集すると、Git はそれを「変更された」とみなします。直近のコミットの後で変更が加えられたからです。変更されたファイルをステージし、それをコミットする。この繰り返しです。

ファイルの状態の流れ
Figure 8. ファイルの状態の流れ

ファイルの状態の確認

どのファイルがどの状態にあるのかを知るために主に使うツールが git status コマンドです。 このコマンドをクローン直後に実行すると、このような結果となるでしょう。

$ git status
On branch master
nothing to commit, working directory clean

これは、クリーンな作業コピーである (つまり、追跡されているファイルの中に変更されているものがない) ことを意味します。 また、追跡されていないファイルも存在しません (もし追跡されていないファイルがあれば、Git はそれを表示します)。 最後に、このコマンドを実行するとあなたが今どのブランチにいるのか、サーバー上の同一ブランチから分岐してしまっていないかどうかがわかります。 現時点では常に “master” となります。これはデフォルトであり、ここでは特に気にする必要はありません。 ブランチについては Git のブランチ機能 で詳しく説明します。

ではここで、新しいファイルをプロジェクトに追加してみましょう。シンプルに、READMEファイルを追加してみます。 それ以前に README ファイルがなかった場合、git status を実行すると次のように表示されます。

$ echo 'My Project' > README
$ git status
On branch 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)

出力結果の “Untracked files” 欄にREADMEファイルがあることから、このファイルが追跡されていないということがわかります。 これは、Git が「前回のスナップショット (コミット) にはこのファイルが存在しなかった」とみなしたということです。明示的に指示しない限り、Git はコミット時にこのファイルを含めることはありません。 自動生成されたバイナリファイルなど、コミットしたくないファイルを間違えてコミットしてしまう心配はないということです。 今回は README をコミットに含めたいわけですから、まずファイルを追跡対象に含めるようにしましょう。

新しいファイルの追跡

新しいファイルの追跡を開始するには git add コマンドを使用します。 READMEファイルの追跡を開始する場合はこのようになります。

$ git add README

再び status コマンドを実行すると、READMEファイルが追跡対象となってステージされており、コミットする準備ができていることがわかるでしょう。

$ git status
On branch master
Changes to be committed:
  (use "git reset HEAD <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
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 ファイルは “Changed but not staged for commit” という欄に表示されます。これは、追跡対象のファイルが作業ディレクトリ内で変更されたけれどもまだステージされていないという意味です。 ステージするには git add コマンドを実行します。 git add にはいろんな意味合いがあり、新しいファイルの追跡開始・ファイルのステージング・マージ時に衝突が発生したファイルに対する「解決済み」マーク付けなどで使用します。‘`指定したファイルをプロジェクトに追加(add)する’コマンド、というよりは、``指定した内容を次のコミットに追加(add)する'コマンド、と捉えるほうがわかりやすいかもしれません。 では、git addCONTRIBUTING.md をステージしてもういちど git status を実行してみましょう。

$ git add CONTRIBUTING.md
$ git status
On branch 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
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
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文字の組み合わせです。1文字目はステージされたファイルの状態を、2文字はファイルが変更されたかどうかを示しています。 この例でいうと、`README`ファイルは作業ディレクトリ上にあって変更されているけれどステージされてはいません。 `lib/simplegit.rb`ファイルは変更済みでステージもされています。 `Rakefile`のほうはどうかというと、変更されステージされたあと、また変更された、という状態です。変更の内容にステージされたものとそうでないものがあることになります。

ファイルの無視

ある種のファイルについては、Git で自動的に追加してほしくないしそもそも「追跡されていない」と表示されるのも気になってしまう。そんなことがよくあります。 たとえば、ログファイルやビルドシステムが生成するファイルなどの自動生成されるファイルがそれにあたるでしょう。 そんな場合は、無視させたいファイルのパターンを並べた .gitignore というファイルを作成します。 .gitignore ファイルは、たとえばこのようになります。

$ cat .gitignore
*.[oa]
*~

最初の行は “.o” あるいは “.a” で終わる名前のファイル (コードをビルドする際にできるであろうオブジェクトファイルとアーカイブファイル) を無視するよう Git に伝えています。次の行で Git に無視させているのは、チルダ (~) で終わる名前のファイルです。Emacs をはじめとする多くのエディタが、この形式の一時ファイルを作成します。これ以外には、たとえば log、tmp、pid といった名前のディレクトリや自動生成されるドキュメントなどもここに含めることになるでしょう。実際に作業を始める前に .gitignore ファイルを準備しておくことをお勧めします。そうすれば、予期せぬファイルを間違って Git リポジトリにコミットしてしまう事故を防げます。

.gitignore ファイルに記述するパターンの規則は、次のようになります。

  • 空行あるいは # で始まる行は無視される

  • 標準の glob パターンを使用可能

  • 再帰を避けるためには、パターンの最初にスラッシュ (/) をつける

  • ディレクトリを指定するには、パターンの最後にスラッシュ (/) をつける

  • パターンを逆転させるには、最初に感嘆符 (!) をつける

glob パターンとは、シェルで用いる簡易正規表現のようなものです。 アスタリスク (*) は、ゼロ個以上の文字にマッチします。 [abc] は、角括弧内の任意の文字 (この場合は a、b あるいは c) にマッチします。 疑問符 (?) は一文字にマッチします。 また、ハイフン区切りの文字を角括弧で囲んだ形式 ([0-9]) は、 ふたつの文字の間の任意の文字 (この場合は 0 から 9 までの間の文字) にマッチします。 アスタリクスを2つ続けて、ネストされたディレクトリにマッチさせることもできます。 a/**/z のように書けば、a/za/b/z、`a/b/c/z`などにマッチします。

では、.gitignore ファイルの例をもうひとつ見てみましょう。

# no .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 the build/ directory
build/

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

# ignore all .pdf files in the doc/ directory
doc/**/*.pdf
Tip

GitHubが管理している .gitignore ファイルのサンプル集 https://github.com/github/gitignore はよくまとまっていて、多くのプロジェクト・言語で使えます。 プロジェクトを始めるときのとっかかりになるでしょう。

ステージされている変更 / されていない変更の閲覧

git status コマンドだけではよくわからない (どのファイルが変更されたのかだけではなく、実際にどのように変わったのかが知りたい) という場合は git diff コマンドを使用します。 git diff コマンドについては後で詳しく解説します。 おそらく、最もよく使う場面としては次の二つの問いに答えるときになるでしょう。 「変更したけどまだステージしていない変更は?」「コミット対象としてステージした変更は?」 git status が出力するファイル名のリストを見れば、 これらの質問に対するおおまかな答えは得られますが、 git diff の場合は追加したり削除したりした正確な行をパッチ形式で表示します。

先ほどの続きで、ふたたび README ファイルを編集してステージし、 一方 CONTRIBUTING.md ファイルは編集だけしてステージしない状態にあると仮定しましょう。 ここで git status コマンドを実行すると、次のような結果となります。

$ git status
On branch 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
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
Note
GitのDiffを他のツールで見る

この本では、引き続き`git diff`コマンドを様々な方法で使っていきます。 一方、このコマンドを使わずに差分を見る方法も用意されています。GUIベースだったり、他のツールが好みの場合、役に立つでしょう。 `git diff`の代わりに`git difftool`を実行してください。そうすれば、emerge、vimdiffなどのツールを使って差分を見られます(商用のツールもいくつもあります)。 また、`git difftool --tool-help`を実行すれば、利用可能なdiffツールを確認することもできます。

変更のコミット

ステージングエリアの準備ができたら、変更内容をコミットすることができます。 コミットの対象となるのはステージされたものだけ、 つまり追加したり変更したりしただけでまだ 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
# Changes to be committed:
#	new file:   README
#	modified:   CONTRIBUTING.md
#
~
~
~
".git/COMMIT_EDITMSG" 9L, 283C

デフォルトのコミットメッセージとして、 直近の git status コマンドの結果がコメントアウトして表示され、 先頭に空行があることがわかるでしょう。 このコメントを消して自分でコミットメッセージを書き入れていくこともできますし、 何をコミットしようとしているのかの確認のためにそのまま残しておいてもかまいません (何を変更したのかをより明確に知りたい場合は、git commit-v オプションを指定します。 そうすると、diff の内容がエディタに表示されるので何をコミットしようとしているかが正確にわかるようになります)。 エディタを終了させると、Git はそのメッセージつきのコミットを作成します (コメントおよび diff は削除されます)。

あるいは、コミットメッセージをインラインで記述することもできます。その場合は、commit コマンドの後で -m フラグに続けて次のように記述します。

$ 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)」「変更されたファイルの数」「そのコミットで追加されたり削除されたりした行数」 といった情報が表示されているのがわかるでしょう。

コミットが記録するのは、ステージングエリアのスナップショットであることを覚えておきましょう。 ステージしていない情報については変更された状態のまま残っています。 別のコミットで歴史にそれを書き加えるには、改めて add する必要があります。 コミットするたびにプロジェクトのスナップショットが記録され、あとからそれを取り消したり参照したりできるようになります。

ステージングエリアの省略

コミットの内容を思い通りに作り上げることができるという点でステージングエリアは非常に便利なのですが、 普段の作業においては必要以上に複雑に感じられることもあるでしょう。 ステージングエリアを省略したい場合のために、Git ではシンプルなショートカットを用意しています。 git commit コマンドに -a オプションを指定すると、追跡対象となっているファイルを自動的にステージしてからコミットを行います。 つまり git add を省略できるというわけです。

$ git status
On branch 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 'added new benchmarks'
[master 83e38c7] added new benchmarks
 1 file changed, 5 insertions(+), 0 deletions(-)

この場合、コミットする前に CONTRIBUTING.mdgit add する必要がないことに気づいたでしょうか。 -a というフラグのおかげで、変更したファイルがすべてコミットに含まれたからです。 このように -a は便利なフラグですが、ときには意図しない変更をコミットに含んでしまうことにもなりますので気をつけましょう。

ファイルの削除

ファイルを Git から削除するには、追跡対象からはずし (より正確に言うとステージングエリアから削除し)、そしてコミットします。 git rm コマンドは、この作業を行い、そして作業ディレクトリからファイルを削除します。 つまり、追跡されていないファイルとして残り続けることはありません。

単に作業ディレクトリからファイルを削除しただけの場合は、git status の出力の中では “Changed but not updated” (つまり ステージされていない) 欄に表示されます。

$ 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
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)

    deleted:    PROJECTS.md

次にコミットするときにファイルが削除され、追跡対象外となります。 変更したファイルをすでにステージしている場合は、-f オプションで強制的に削除しなければなりません。 まだスナップショットに記録されていないファイルを誤って削除してしまうと Git で復旧することができなくなってしまうので、それを防ぐための安全装置です。

ほかに「こんなことできたらいいな」と思われるであろう機能として、 ファイル自体は作業ツリーに残しつつステージングエリアからの削除だけを行うこともできます。 つまり、ハードディスク上にはファイルを残しておきたいけれど、もう Git では追跡させたくないというような場合のことです。 これが特に便利なのは、.gitignore ファイルに書き足すのを忘れたために巨大なログファイルや大量の .a ファイルがステージされてしまったなどというときです。 そんな場合は --cached オプションを使用します。

$ git rm --cached README

ファイル名やディレクトリ名、そしてファイル glob パターンを 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
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 コマンドを使おうがどちらでもかまいません。 唯一の違いは、この方法だと 3 つのコマンドが必要になるかわりに mv だとひとつのコマンドだけで実行できるという点です。 より重要なのは、ファイル名の変更は何でもお好みのツールで行えるということです。あとでコミットする前に add/rm を指示してやればいいのです。