2009年8月22日土曜日

ステージを理解して git をもっと便利に使う

git には「stage(ステージ)する」という概念があります。あるいは「index」と言い換えてもいいかもしれません。

簡単にいうと「stageする」=「特定の変更内容をindexに登録する」=「次回コミットに含めるようgitに指示する」ということなのですが、この概念は今まで主流だった CVS や Subversion といったバージョン管理システムにはありませんでいした。
長年CVSを使っていて、その考え方に凝り固まっていた私は、gitを使い始めてしばらくはstageやindexの概念を理解できなかったので、今回ここで紹介することにしました。

このstageとindexを覚えると「ひとつのコミットには、その主題となる変更と無関係な変更を含めない」という「バージョン管理システムを使う上で重要なはずなのに、つい疎かにしてしまいがち」なポリシーを簡単に実践できるようになります。
今回stageおよびindexについて、その管理に必要ないくつかのコマンドとあわせて紹介しますので、よさそうだなと感じたら是非使って見てください。


●git add
使用例

git add [-p] <filename>


最初に紹介するコマンドはgit addです。
これは編集したファイルをstageする(=次回のcommit対象にする)コマンドです。
「そんなの知ってるし」と思った人も多いかもしれませんが、待ってください。あなたは add を使いこなしていますか?
  • 「いつもadd -uで何も考えずに全部addしている」とか、
  • 「commit -aでコミットするときに一括でaddしてばかり」だったり、
  • 「いちいちaddすんの面倒だなー」と思ってたりしませんか?

心当たりのある人は、addの存在理由を理解して使いこなせるようになればgitが格段に便利になります。

●stageとindex
git addの存在理由を理解するためには、まずstageとindexという概念を理解しましょう。
git はチェックアウトされたファイルを編集した直後にcommitを実行しても、どのファイルも新しいバージョンとして登録されません。
新しいバージョンとして登録するにはこのgit addをつかって、「このファイルは次回のコミットに含める」と宣言してやる必要があります。
このようにある変更点を次回のコミットに含めるようgitに指示することをgit用語で 「stageする」とかstagingと言います。
また、ワーキングコピーからstageした内容を保持する領域のことをindexと呼びます。

indexは、コミットを永遠に記録するリポジトリと、ワーキングコピーとの間にある緩衝地帯であると考えると理解しやすいと思います。
* リポジトリに格納された「最新のコミット」
* stageした内容を保持する「index」
* ユーザが通常のファイルとして編集する「ワーキングコピー」
の関係を図示すると、checkout直後は
「(リポジトリに格納された)最新のコミット」=「index」=「ワーキングコピー」

となっています。ワーキングコピーを編集すると
「最新のコミット」=「index」≠「ワーキングコピー」

となりますね。次にaddの操作をを図示すると、
「最新のコミット」=「index」←(git add)←「ワーキングコピー」

であり、その結果、
「最新のコミット」≠「index」=「ワーキングコピー」

となります。
なお、「index」の内容を「最新のコミット」として反映するリポジトリに反映するコマンドはgit commitです。
commitの操作を図示すると、
「最新のコミット」←(git commit)←「index」=「ワーキングコピー」

であり、その結果
「最新のコミット」=「index」=「ワーキングコピー」

となります。

このstage機構を利用することで「ひとつのコミットには主題となる変更とは無関係な変更を含めない」というポリシーを実現しやすくなります。
例えば「あー、調子に乗って途中で別のバグも修正しちゃったけど、まぁいいかcommitしたれ」と、複数の意味を持った変更を、ひとつのチェンジセットとしてcommitしてしまった経験がないでしょうか?
このようなコミットは後から変更点のdiffを見直したとき別々の意味を持った変更がごっちゃになって表示されたり、commit logには片方の変更にしか言及してなかったり、と理解の妨げになることが多く、あまり行儀の良いコミットとは言えません。
このように変更を加えすぎてしまったときは次回コミットに含めたいファイルだけをgit addしてやれば良いので、行儀の良いコミットを実践しやすくなります。

●操作例
それでは具体的な操作例を見てみましょう。
今、あなたは自分が管理しているプロジェクトのsrc/abc.cというファイルのバグ修正を終えたところで、READMEにスペルミスがあることを見つけ、これも修正したとします。
ここでgit statusを実行すると、以下のように出力されます。

kt@ume% git status# On branch master
# Changed but not updated:
#   (use "git add <file>..." to update what will be committed)
#
#       modified:   README
#       modified:   src/abc.c
#no changes added to commit (use "git add" and/or "git commit -a")

この状態から
% git add -u
% git commit -m "Fix bug in src/abc.c and Fix typo in README."

や、または
% git commit -a -m "Fix bug in src/abc.c and Fix typo in README."

などを実行すると、「src/abc.cのバグ修正」と「READMEのスペルミス修正」という二つの主題をひとつのチェンジセットとしてコミットしてしまうことになります。
このような場合は、本来「src/abc.cのバグ修正」と「READMEのスペルミス修正」は別々のコミットに分けるべきですので、正しい操作を git add していない状態から実行する例を見てみます。

まずは src/abc.c だけを add することで、次のコミットでは src/abc.c の変更だけを登録できるようになります。
kt@ume% git add src/abc.c

ここで git status を確認してみましょう。
kt@ume% git status
# On branch master
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#       modified:   src/abc.c
#
# Changed but not updated:
#   (use "git add <file>..." to update what will be committed)
#
#       modified:   README#

このように"Changes to be committed:"、つまり「コミットされる変更点」として src/abc.c だけが登録されていることが分かります。
ここで1回めのgit commitを実行します。
kt@ume% git commit -m "Fix bug in src/abc.c."

と commit を実行してやれば、「src/abc.cのバグ修正」と「READMEのスペルミス修正」のうち、
「src/abc.cのバグ修正」だけをコミットできました。
続いて、「READMEのスペルミス修正」をgit addしてgit commitします。
kt@ume% git add README
kt@ume% git commit -m "Fix typo in README."


このように、git addを使えば、ひとつのコミットに含めるべきでない複数の変更をしてしまっても、簡単に複数のコミットに分割できるようになります。

●git add -p
続いて使用例に挙げた -p オプションについて解説します。

先ほど挙げた「あー、調子に乗って途中で別のバグも修正しちゃったけど、まぁいいかcommitしたれ」というシチュエーションとして、1ファイル内で複数の箇所を編集してしまった場合も考えられます。
そんなときは、このgit add -p を実行すれば、1ファイル内の差分ごとにstageする/しないを対話的に選択できます。(1ファイル内の変更点から一部だけstageできます)

git add -p は対話的な処理であり、プロンプトに?と打てば使用方法が表示されますので、詳細は省略します。

●おまけ
なお、すでに見たように、git statusは編集されているファイルを「stageされているファイル」と「stageされていないファイル」に分けて表示してくれます。(git status 時に表示されるコメントをよく読んでみましょう)
さらに、git add を使いこなせるようになったら次回紹介する git diff --cached も覚えると良いでしょう。

つづく!

0 件のコメント:

コメントを投稿