2009年8月29日土曜日

ステージを理解して git をもっと便利に使う(その3)

前回から引き続き、「ステージを理解して git をもっと便利に使う」というテーマでお送りする、git解説記事の第3弾です。

まずは、前回までのおさらいです。

git にはリポジトリに格納された「最新のコミット」と「ワーキングコピー」の間に「index」と呼ばれる緩衝地帯が存在しています。チェックアウト直後は

「最新のコミット」=「index」=「ワーキングコピー」

ですが、ワーキングコピーを編集した後、git addにより編集内容がstage(indexに登録)され、

「最新のコミット」≠「index(+α)」=「ワーキングコピー(+α)」

となります。

% git add

% git add -p

を積極的に使うことで、編集内容を部分的にstageする/しないをコントロールし、1コミットの内容を洗練しましょう、というのが第1回の内容でした。

第2回はgit diffとgit diff --cachedの違いについて解説しました。

% git diff

は「index」と「ワーキングコピー」の差分を表示し、

% git diff --cached

は「最新のコミット」と「index」の差分を表示する、
という内容でした。

第3回はgit resetになります。

●git reset
実行例

% git reset

git resetを使うと、一度git addでstageした変更内容をindexから削除(unstage)することができます。

例として、
「ワーキングコピーの README と src/abc.c を編集し、git add -uで両方ともstageしたが、よく考えてみると別々にコミットすべき内容だった」
という状況を考えます。git statusで確認すると、以下のように表示されます。

kt@ume% git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD ..." to unstage)
#
# modified: README
# modified: src/abc.c
#

今回はREADMEだけをコミットし、src/abc.cはその後のコミットに含めることにしたとします。
この場合、git reset src/abc.cを実行することでsrc/abc.cをunstageできます。

kt@ume% git reset src/abc.c
src/abc.c: locally modified
kt@ume% git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD ..." to unstage)
#
# modified: README
#
# Changed but not updated:
# (use "git add ..." to update what will be committed)
#
# modified: src/abc.c
#

このとおり、src/abc.cは「Changes to be committed:」から「Changed but not updated:」に移りました。
これでgit commitしても変更が反映されるのはREADMEだけです。

git add -pによりファイルの内容を部分的にstageできますが、unstageはファイル単位でしかできないようです。
git add -pを間違えた場合は、一旦git resetして再度git add -pでstageしなおしましょう。

なお、上の実行例の「Changes to be committed:」の下の行にこんなコメントが書かれているのに気づいたでしょうか?

# (use "git reset HEAD ..." to unstage)

unstageのやりかたがそのものズバリ書いてあります。
もしgit resetを忘れてしまった場合にも「git statusの出力を見れば書いてある」ということさえ覚えておけば、どうすればよいか思い出せるでしょう。



●おまけ
前回紹介したgit diff --cached ってコマンドを打つのが面倒じゃありませんか?こんなときは git configで別名を定義しましょう。

kt@ume% git config --global alias.difc "diff --cached"
kt@ume% git help difc
`git difc' is aliased to `diff --cached'

上記の例では difc を "diff --cached" の別名として定義しています。
CVSやSubversionからの乗り換えユーザには以下のようなaliasもおすすめです。

git config --global alias.ad add
git config --global alias.br branch
git config --global alias.ci commit
git config --global alias.co checkout
git config --global alias.dif diff
git config --global alias.di diff
git config --global alias.st status

2009年8月23日日曜日

ステージを理解して git をもっと便利に使う(その2)

前回から引き続き、「ステージを理解して git をもっと便利に使う」というテーマでコマンドを紹介します。
今回は git diff --cached です。

●git diff --cached
使用例

git diff --cached


前回覚えた git add でstageする/しないを使い分けると、困ったことがひとつ出てきます。
それは git diff が「index」と「ワーキングコピー」の差分だけを表示すること、つまり一旦git addでstageした内容はgit diffで表示されなくなってしまうことです。
前回の復習として、git add直後の「最新のコミット」と「index」、「ワーキングコピー」の関係を思い出してみましょう。
「最新のコミット」≠「index」=「ワーキングコピー」

git diffは「index」と「ワーキングコピー」の差分だけを表示するので、「最新のコミット」と「index」の差分や「最新のコミット」と「ワーキングコピー」の差分は表示できません。
もちろんgit add -uで「ワーキングコピー」の変更点をすべてstageすると、git diffは何も表示しなくなります。
ここで git diff --cached を使えば「最新のコミット」と「index」の差分(これは次回のコミットに含まれる変更点でもあります)を表示できます。

それでは具体的な例を見ていきましょう。
前回使った src/abc.c と README のシチュエーションを考えます。
git addする前であれば「index」は「最新のコミット」と同一なので、以下のようにgit diffはcheckoutしてから編集した内容すべてを示してくれますが・・・

kt@ume% git status
# On branch master
# Changed but not updated:
#   (use "git add ..." 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")
kt@ume% git diff
diff --git a/README b/README
index f6419fa..59fbc52 100644
--- a/README
+++ b/README
@@ -9,4 +9,4 @@ This is test project.
% make
% make install

-3. Have fan.
+3. Have fun.
diff --git a/src/abc.c b/src/abc.c
index 869a851..31c175f 100644
--- a/src/abc.c
+++ b/src/abc.c
@@ -10,7 +10,7 @@ int main(int argc, char **argv)

while ((ch = getopt(argc, argv, "v")) != -1) {
switch (ch) {
-               case 'V':
+               case 'v':
printf("%s %s\n", "hello", "0.1");
return 0;
break;

以下の用にgit addでstageすると、その変更内容はgit diffでは表示されなくなります。
kt@ume% git add src/abc.c
kt@ume% git status
# On branch master
# Changes to be committed:
#   (use "git reset HEAD ..." to unstage)
#
#       modified:   src/abc.c
#
# Changed but not updated:
#   (use "git add ..." to update what will be committed)
#
#       modified:   README
#
kt@ume% git diff
diff --git a/README b/README
index f6419fa..59fbc52 100644
--- a/README
+++ b/README
@@ -9,4 +9,4 @@ This is test project.
% make
% make install

-3. Have fan.
+3. Have fun.

ここでgit diff --cachedを実行することで、「index(stageされた変更内容)」と「最新のコミット」との差分、つまり次回のコミットに含まれる変更内容を表示することができます。
kt@ume% git status
# On branch master
# Changes to be committed:
#   (use "git reset HEAD ..." to unstage)
#
#       modified:   src/abc.c
#
# Changed but not updated:
#   (use "git add ..." to update what will be committed)
#
#       modified:   README
#
kt@ume% git diff --cached
diff --git a/src/abc.c b/src/abc.c
index 869a851..31c175f 100644
--- a/src/abc.c
+++ b/src/abc.c
@@ -10,7 +10,7 @@ int main(int argc, char **argv)

while ((ch = getopt(argc, argv, "v")) != -1) {
switch (ch) {
-               case 'V':
+               case 'v':
printf("%s %s\n", "hello", "0.1");
return 0;
break;

いかがですか?
git diff --cachedを使うことで次回のコミットに含まれる内容を簡単に確認できるようになりました。
今まで使っていたgit diffと合わせて使えばstage管理は簡単です。

次回は間違えてaddしてしまった内容を取り消すgit resetを紹介します(ヒントはgit statusの出力にあります)。

つづく!

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 も覚えると良いでしょう。

つづく!

2009年8月20日木曜日

hello world!

最初はみんなこれでしょ、ということで hello world の書き方です。
まずはドノーマルなC言語から
kt@ume% cat > hello.c
#include <stdio.h>

int main()
{
printf("hello world!\n");
return 0;
}
kt@ume% make hello
cc     hello.c   -o hello
kt@ume% PATH=.:$PATH hello
hello world

ポイントがいくつかあります。
  1. cat を使うことで気分はBill Joy。でも間違ってEnterを押すと前の行には戻れません。行内でも間違いを見つけたらひたすらバックスペースで戻る以外に修正する術はなし。
  2. *.c ファイルのファイル名から .c を除いた文字列を make することで Makefile いらず。
  3. 私が小さい頃はカレントディレクトリをPATHに入れてるとグーで殴られました。他のPATHの前に入れるなんて、もってのほか。でも今はこうして安全な方法を編み出すことができました。./hello は邪道です。sh系限定ですcsh系とは違うんです。
続いてシェル上での hello world
kt@ume% precmd () { echo 'hello world!' }
hello world!
kt@ume% cd /
hello world!
kt@ume% ls
a/    boot/  etc/   lib/    lost+found/  mnt/  proc/  sbin/     srv/  tmp/  var/
bin/  dev/   home@  lib64/  media/       opt/  root/  selinux/  sys/  usr/
hello world!

zsh では precmd という関数を定義することでプロンプト表示前に関数を実行してくれます。
bash の場合は PROMPT_COMMAND という変数に実行したいコマンドを定義しましょう。
[kt@susuki ~]$ PROMPT_COMMAND='echo hello world!'
[kt@susuki ~]$ cd /
hello world!
[kt@susuki /]$ ls
COPYRIGHT       cdrom           entropy         lib             proc            sys
a               compat          etc             libexec         rescue          tmp
bin             dev             home            media           root            usr
boot            dist            jail            mnt             sbin            var
hello world!

いつまでコマンドを叩いても hello world の表示が止まることはありません。良かったですね!

お次はネットワークを利用した hello world です。まずは FreeBSD 編
root@susuki# vi /etc/inetd.conf

# ファイルの最後に以下の2行を加えます。 最後の hello は最初に作ってコンパイルしたコマンドを指定してください。
hello-port      stream  tcp     nowait  root    /home/kt/misc/blog/hello/hello
hello-port      stream  tcp6    nowait  root    /home/kt/misc/blog/hello/hello

root@susuki# /etc/rc.d/inetd forcestart
Starting inetd.
root@susuki# telnet localhost hello-port
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
hello world!
Connection closed by foreign host.

inetd から起動するプログラムは「標準入力=クライアントからの入力」「標準出力=クライアントへの出力」という前提で書かれています。逆に言うと普通に標準入出力でやりとりするプログラムを書けば、inetd から呼び出すことであっというまにサーバを書けます。
もちろんIPv6対応です。
root@susuki# telnet ::1 hello-port
Trying ::1...
Connected to localhost.
Escape character is '^]'.
hello world!
Connection closed by foreign host.

すごいぞinetd!だてにインターネットスーパーサーバなんて大層な名前を背負ってませんね。
これを応用すればWebサーバだって楽勝です。
kt@susuki% cat > hello.c
#include <stdio.h>

int main()
{
printf("HTTP/1.0 200 OK\r\n");
printf("ContentType: text/plain;\r\n\r\n");
printf("hello world!\n");
return 0;
}
kt@susuki% make hello
cc -O2 -fno-strict-aliasing -pipe   hello.c  -o hello
kt@susuki% su -
root@susuki# perl -pi.bak -e 's/^hello-port/http/;' /etc/inetd.conf
root@susuki# /etc/rc.d/inetd forcerestart
Stopping inetd.
Starting inetd
root@susuki# telnet localhost 80
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
HTTP/1.0 200 OK
ContentType: text/plain;

hello world!
Connection closed by foreign host.

ここまでできたらお好みのWebブラウザで設定したホストにアクセスしてみましょう。hello world! と表示されましたか?
うまく行ったら、就職活動で「得意科目はぁープログラミング?ていうかぁ、Webサーバとか書いてましたぁ」と言い張ることができます。すごいですね!(本当にやって過度な期待を持たれても私は知りません)

続いてLinux編を・・・と思ったのですが、もう疲れたのでやめときます。Linuxの場合はxinetdでググれ。

それではここまで読んでいただいてありがとうございました。
気が向いたらまた書くかもしれません。ブログが続くかどうかもわかりませんが・・・


つづく!