あのリーナス・トーバルズ氏がそれまで使用していたバージョン管理システムを使用できなくなったことを受け、代替のものを探していたが、どれもお眼鏡に適わず、「2 週間くれ」ということでその基礎部分を作ったという Git。
特にオープンソースの世界おいては、これまで以上の加速度と広がりがおこるのではないかと Secure source code hosting and collaborative development - GitHub を見ていて思う。ただ、非常に有用なものだと思えるのだが、慣れるのに時間がかかる。。
CVS から Subversion への移行はスンナリといったが、集中型と分散型の違いはあれど、Subversion から Git への移行には、それなりに敷居があるように感じる。
頭を整理するために、メモを書いておく。
Table of Contents
Open Table of Contents
まずは概念的なところ
Git をインストールし、コマンドをいろいろ触っていたのだが、どうもピンとこない。。 まずは、概念的なところを抑えないとイカンと思い、整理する。
Git は分散型のバージョン管理システム
改めてだが、Git は分散型のバージョン管理システムである。
各自が持っているリポジトリが独立したリポジトリであり、互いのリポジトリ間は push/pull によって協調できる。 自分でリポジトリを新規に作成していくのはもちろんだが、A さんのリポジトリをベースに開発を行なおうと思ったら、A さんのリポジトリを clone (複製)し、自分のリポジトリとしてリビジョン管理が行えるようになる。そして自身で育んだ変更を A さんに教えてあげて、A さんもそれを取り込みたいと思ったら、本流に還元することも可能となる。
Git の構造
リポジトリはある時点の状態をリビジョンとして管理する。 あるまとまった変更を終えた状態をコミットし、リビジョンとして管理する。
これ自体は Subversion と同様だ。
Subversion の場合、コミットの度にインクリメントされるリビジョン番号でリビジョンを識別するが、 Git は 40 桁の 16 進数のオブジェクトの名前(SHA-1 Hash)でリビジョンを識別する。
このコミットされたオブジェクトの名前だが、「コミットオブジェクト」というオブジェクトの名前になる。
Git の場合、コミットされたリビジョンは、1 つの「コミットオブジェクト」として管理されていく。
また、Git はコミットだけでなく、データの実体であるファイル、ディレクトリなども全てオブジェクトとして管理している。オブジェクトには型があり、その型で実体が何であるかを識別する。これについては、Git を扱うためのライブラリ(Grit、GitStoreなど)を扱いながら理解していくとわかり易かったので、改めて別にメモを書くことにする。(Design Recipe 別館 Blog - Grit を使って Git リポジトリを Ruby で操作する)
リビジョンの関連は、「コミットオブジェクト」の parent 属性が持っている親オブジェクト(1 つ前のコミットツリー)との関連で管理される。 1 度 コミットをかけると、リポジトリには新しい「コミットオブジェクト」が作成され、その 1 つ前の「コミットオブジェクト」は新しいく作成された「コミットオブジェクト」から親として参照される Tree 構造となる。
なので、ある「コミットオブジェクト」を掴まえれば、その親を辿ることで過去の全ての「コミットオブジェクト」を辿れることになる。
まずは抑えておきたい 3 つのレイヤー
Subversion がワーキングコピーとリポジトリの間だけの 2 つレイヤーで管理を行うのに対して、 Git には作業時に意識する必要のある 3 つのレイヤーがある。
- ワーキングツリー
(作業中のファイル) - インデックス(ステージング)
(コミットにより新しいコミットツリーに反映されるもの。「索引」と書いてある説明書もある。) - コミットツリー
上記の 3 つのレイヤーだが、Subversion と比較してみると、
「ワーキングツリー」は、Subversion でいうワーキングコピーと同義と捉えてよい。「インデックス(ステージング)」は、Subversion にそれにあたるレイヤーはない。「コミットーツリー」は、Subversion で言うコミットのかかったリビジョン、というイメージになる。
実際に作業を行う場合には、
-「ワーキングツリー」 -> [インデックスへの追加] -> 「インデックス(ステージング)」 -> [コミット] -> 「コミットツリー」
という状態遷移となる。Git では、それぞれのレイヤーを意識した上でコマンドを発行していく。 上記 3 つのレイヤーの間を行き来するコマンドが揃っている。
実際に触ってみることにする。
Git のインストールと設定
当然ソースからコンパイルして入れる方法があるが、パッケージ管理する場合、
Mac OS X であれば、
$ sudo port install git-core +gitweb +svn
Cent OS であれば、yum を使って、
# yum install git*
Windows であれば、
など。
Git の設定情報を設定する。
コミットした際に名無しの権兵衛では困るので、名前と email の設定はしておく。
$ git config --global user.name "Taro Yamada"
$ git config --global user.email "taro@example.co.jp"
コマンドラインの出力がカラーリングされていた方が見易いので設定しておく。
$ git config --global color.ui auto
ちなみに、これらの設定は、
$HOME/.gitconfig
に保存される。
Git を使ってみる
リポジトリの作成
まずは、リポジトリを作成する。
$ mkdir test-repos
$ cd test-repos/
$ git init
Initialized empty Git repository in /Users/taka/tmp/test-repos/.git/
リポジトリにファイルを追加する
ファイルを作成する。
$ echo 'first git.' > test1.txt
「ワーキングツリー」にファイルが作成されたことになる。
git status
というコマンドで現在の状況の確認ができる。
$ git status
# On branch master
#
# Initial commit
#
# Untracked files:
# (use "git add <file>..." to include in what will be committed)
#
# test1.txt
nothing added to commit but untracked files present (use "git add" to track)
“Untracked files:” ということで先程作成した test1.txt
がリスト表示されている。
この状態は、先程の 3 つのレイヤーの最初のレイヤー「ワーキングツリー」にファイルを作成した状態で、これをリポジトリに含めるためには、まずは「インデックス(ステージング)」レイヤーに持っていく必要がある。
インデックス(ステージング)に追加する。
$ git add .
git add
でインデックスに追加を行う。”.
” を指定しているのは、カレントディレクトリ配下の全てをインデックスに追加する、という意味になる。
状況を確認する。
$ git status
# On branch master
#
# Initial commit
#
# Changes to be committed:
# (use "git rm --cached <file>..." to unstage)
#
# new file: test1.txt
#
test1.txt
のリスティングされる項目が**“Changes to be committed:“**に変わっている。
git add
したことによって、test1.txt
が晴れて「ワーキングツリー」のレイヤーにやってきて、コミットの対象となったためだ。
コミットをかける。
$ git commit
-m
オプションを指定していなければ、標準エディタとして登録されているエディタが起動するので、コミットの理由を記述しておく。
first commit.
# Please enter the commit message for your changes. Lines starting
# with '#' will be ignored, and an empty message aborts the commit.
# On branch master
#
# Initial commit
#
# Changes to be committed:
# (use "git rm --cached <file>..." to unstage)
#
# new file: test1.txt
#
1 行目にコミットの理由を書いている。その下の部分には先程 git status
を行った時に見たメッセージが書かれている。今回 test1.txt
が new file としてコミットされることがわかる。保存して終了すると、そのままコミットがかかる。やめる時は保存しないで終了すればよい。
[master (root-commit)]: created 954545c: "first commit."
1 files changed, 1 insertions(+), 0 deletions(-)
create mode 100644 test1.txt
変更履歴を git log
で確認しておく。
$ git log
commit 954545c8c6b9bd4b789ae5fdade378f2038b565b
Author: Taro Yamada <taro@exameple.com>
Date: Wed Jul 15 23:22:45 2009 +0900
commit
の項目に記載されている “954545c8c6b9bd4b789ae5fdade378f2038b565b” という文字列が現在コミットしたリビジョンを識別する「コミットオブジェクト」の名前になる。この名前でリビジョンを識別する。
差分を管理する
先程コミットしたファイルに変更を加える。
$ echo 'added test. >> test1.txt
状況を確認する。
$ git status
# On branch master
# Changed but not updated:
# (use "git add <file>..." to update what will be committed)
# (use "git checkout -- <file>..." to discard changes in working directory)
#
# modified: test1.txt
#
no changes added to commit (use "git add" and/or "git commit -a")
“Changed but not updated:” にリスティングされている。まだ「ワーキングツリー」にいる状況だ。既存のファイルに修正を加えたので、modified
となっている。
変更の差分は、Subversion の svn diff
同様、git diff
で確認できる。
$ git diff
diff --git a/test1.txt b/test1.txt
index 504d2f9..b93048b 100644
--- a/test1.txt
+++ b/test1.txt
@@ -1 +1,2 @@
first git.
+added text.
確かに変更(文字列の追加)分が確認できる。
ただ、Git の場合には、「何と何の差分なのか?」を意識する必要がある。
ここで言っている”何”とは、先程のレイヤーのことだ。
git diff
- 「ワーキングツリー」と「インデックス(ステージング)」の差分
そう、先程表示された差分は、まだ「インデックス(ステージング)」に変更の登録をまだ行っていないので、「ワーキングツリー」と「インデックス(ステージング)」との差分として表示されている。
先程の変更を「インデックス(ステージング)」に登録する。
$ git add .
状況を確認する。
$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# modified: test1.txt
#
「インデックス(ステージング)」に登録されたので、次のコミットの対象となる。
ここで差分を見てみる。
$ git diff
何も表示されない。。当然だ。現状、既にadd
しているので、「ワーキングツリー」と「インデックス」との差分はなくなっているから。
しかし、現在の「ワーキングツリー」と「コミットツリー」の差分がわからないなんて心細い。この場合、
git diff HEAD
- 「ワーキングツリー」と「(最新の)コミットツリー(HEAD)」の差分
を使う。
$ git diff HEAD
diff --git a/test1.txt b/test1.txt
index 504d2f9..b93048b 100644
--- a/test1.txt
+++ b/test1.txt
@@ -1 +1,2 @@
first git.
+added text.
「ワーキングツリー」と「コミットツリー」との間は異っているので、差分が表示される。
括弧書きで、“最新の”、“HEAD” という言葉を追記しているが、git では、最新の「コミットオブジェクト」のことを「HEAD」といういう。git diff
に HEAD の引数が追加されているが、それはその意味になる。
ここで、tset1.txt
に対して、次のコミットには含めない仕様変更がいきなり入ってきたとする。どこかにメモしておいてもいいのだが、忘れそうなので、「ワーキングツリー」の test1.txt
に今の時点で追加しておく。
<macro:code lang=“ruby”> $ echo ‘just added’ >> test1.txt </macro:code>
差分を確認するのに、3 つの方法がある。これまで使用した git diff
と git diff HEAD
、そして、新たに git diff --cached
。
$ git diff
diff --git a/test1.txt b/test1.txt
index b93048b..0bfa851 100644
--- a/test1.txt
+++ b/test1.txt
@@ -1,2 +1,3 @@
first git.
added text.
+just added
$ git diff --cached
diff --git a/test1.txt b/test1.txt
index 504d2f9..b93048b 100644
--- a/test1.txt
+++ b/test1.txt
@@ -1 +1,2 @@
first git.
+added text.
$ git diff HEAD
diff --git a/test1.txt b/test1.txt
index 504d2f9..0bfa851 100644
--- a/test1.txt
+++ b/test1.txt
@@ -1 +1,3 @@
first git.
+added text.
+just added
それぞれ表示が異なっている。それぞれの表示の意味は以下の通り。
git diff
- 「ワーキングツリー」と「インデックス(ステージング)」の差分
- “+just added は「インデックス(ステージング)」に
add
していないので、差分として表示される。
git diff --cached
- 「インデックス(ステージング)」と「(最新の)コミットツリー(HEAD)」の差分
- 既に
add
している**“+added text.”**だけが差分として表示される。 - 次回のコミットはこの変更だけがコミットされる。
git diff HEAD
- 「ワーキングツリー」と「(最新の)コミットツリー(HEAD)」の差分
- まだコミットをかけていないので、**“first git.**以降の全ての変更が表示される。
また、git status
で状況を確認してみると、
$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# modified: test1.txt
#
# Changed but not updated:
# (use "git add <file>..." to update what will be committed)
# (use "git checkout -- <file>..." to discard changes in working directory)
#
# modified: test1.txt
#
test1.txt
が**“Changes to be committed:”** と “Changed but not updated:” の両方にリスティングされている。
同じファイルの中でも、「インデッス」に登録したものとそうでないものを持っているためである。
コミットをかける。
$ git commit -m 'second commit.'
[master]: created 8fd109a: "second commit."
1 files changed, 1 insertions(+), 0 deletions(-)
状況を確認してみると、
$ git status
# On branch master
# Changed but not updated:
# (use "git add <file>..." to update what will be committed)
# (use "git checkout -- <file>..." to discard changes in working directory)
#
# modified: test1.txt
#
no changes added to commit (use "git add" and/or "git commit -a")
“Changed but not updated:” だけが残っている。
コミットを状況確認するgit show
で確認してみる。
$ git show
commit 8fd109a92f041b3d7e83fce3dcd9bede027988ed
Author: Taro Yamada <taro@example.com>
Date: Wed Jul 15 23:55:39 2009 +0900
second commit.
diff --git a/test1.txt b/test1.txt
index 504d2f9..b93048b 100644
--- a/test1.txt
+++ b/test1.txt
@@ -1 +1,2 @@
first git.
+added text.
確かに次のコミットに含めたい”追加のテキスト。次のコミットに含める。“だけがコミットされたようだ。
この git show
だが、引数を省略した場合、HEAD (つまり、最新のコミットオブジェクト)を指定したことになる。
確認したい過去の「コミットオブジェクト」がある場合には、オブジェクトの名前を指定してあげればよい。上記のオブジェクトの名前は、“8fd109a92f041b3d7e83fce3dcd9bede027988ed” なので、以下の指定を行っても、同じ結果となる。
$ git show 8fd109a92f041b3d7e83fce3dcd9bede027988ed
commit 8fd109a92f041b3d7e83fce3dcd9bede027988ed
Author: Taro Yamada <taro@example.com>
Date: Wed Jul 15 23:55:39 2009 +0900
second commit.
diff --git a/test1.txt b/test1.txt
index 504d2f9..b93048b 100644
--- a/test1.txt
+++ b/test1.txt
@@ -1 +1,2 @@
first git.
+added commit.
とにかく慣れてみる
ほんの触りの部分だけしか書いていないが、Git には Subversion と比べても多くのコマンドとオプションを持っている。
概念的なところを把握した上でとにかく慣れるしかないようだ。。