Git リポジトリを Ruby のアプリのデータストアとして使ってみたいと思い、少し調べてみた。
Table of Contents
Open Table of Contents
Grit とは
Grit は Git リポジトリへのオブジェクトモデルでのアクセスを提供する Ruby バインディングになる。
GitHub も Grit を使用しているらしい。
また、
なんていう Git のリポジトリブラウザなどでも利用されている。
Git を扱える Ruby バインディングは、他にも
がある。どちらが主流なのか?は、本日の GitHub の状況を見てみたところ、
- Grit
- Watch: 470, Folk: 69
- GitStore
- Watch: 153, Folk: 6
となっている。
GitStore もさらっと見てみたが、今回は、Grit の使い方を整理する。
インストール
$ gem install grit
Grit を使ってみる
まずは下準備をする。
先に Git のリポジトリの作成と幾つかファイルを追加し、リビジョンを作っておく。
$ mkdir repos1
$ cd repos1
$ git init
$ echo 'This is first text file.' > test1.txt
$ git add .
$ git commit -m 'first commit'
[master (root-commit)]: created b38a06b: "first commit'"
1 files changed, 1 insertions(+), 0 deletions(-)
create mode 100644 test1.txt
$ echo 'This is second text file.' > test2.txt
$ git add .
$ git commit -m 'second commit'
[master]: created 83bcfc5: "second commit"
1 files changed, 1 insertions(+), 0 deletions(-)
create mode 100644 test2.txt
$ git checkout -b b_one # create 'b_one' and switch to 'b_one'
Switched to a new branch "b_one"
$ echo 'special customize' >> test1.txt
$ git commit -a -m 'special customize'
[b_one]: created aa5d0ce: "special customize"
1 files changed, 1 insertions(+), 0 deletions(-)
$ git checkout master
Switched to branch "master"
$ mkdir -p 2009/07/16
$ cd 2009/07/16/
$ echo 'It's so Hot!' > diary.txt
$ git add .
$ git commit -m 'added diary'
[master]: created bfab1e4: "added diary"
1 files changed, 1 insertions(+), 0 deletions(-)
create mode 100644 2009/07/16/diary.txt
実体のファイルは現状以下のようになっている。
+ repo1
- test1.txt
- test2.txt
+ 2009
+ 07
+ 16
- diary.txt
irb を使って対話的に Grit を使っていく。 まずは、Grit のロード。
$ irb --prompt simple
>> require 'rubygems'
=> true
>> require 'grit'
=> true
カレントディレクトリ配下に Git のリポジトリ repos1
がある。
>> Dir["*"]
=> ["repos1"]
Git リポジトリを開く - Grit::Repo.new
####
Grit::Repo
クラスにリポジトリのパスを引数に与えてインスタンス化する。
>> repo = Grit::Repo.new("repos1")
=> <Grit::Repo "/Users/taka/Dropbox/Misc/Memo/2009/07/grit/repos1/.git">
コミットオブジェクトを取得する - Grit::Repo#commits
「コミットオブジェクト」の配列を取得する。
>> master_commits = repo.commits
=> [<Grit::Commit "bfab1e49cb14a35a915d0a7e4c663f9c5e1f6ea7">,
<Grit::Commit "83bcfc59ebc7372f0e281dfd0451f987c89ea963">,
<Grit::Commit "b38a06ba68dc7cd787b8d67003aa9891ec1d9703">]
Grit::Repo#commits
は引数を省略すると、master ブランチの最新の 10 件の「コミットオブジェクト」をリストで返す。
取得したいブランチを指定したい場合には、引数にブランチ名を指定する。
>> b_one_commits = repo.commits('b_one')
=> [<Grit::Commit "aa5d0ce199d36773245da4659eda0652cd4f55db">,
<Grit::Commit "83bcfc59ebc7372f0e281dfd0451f987c89ea963">,
<Grit::Commit "b38a06ba68dc7cd787b8d67003aa9891ec1d9703">]
master ブランチのコミットと見比べてみると、b_one ブランチは、master ブランチの “83bcfc59ebc7372f0e281dfd0451f987c89ea963” から独自の道を歩んでいることがわかる。
デフォルト 10 件だが、取得する数は指定できる。また、そのスキップしたい数も指定できる。
repo.commits('master', 10, 20)
master ブランチを 10 件取得し、20 件はスキップする。つまり、21 から 30 までの「コミットオブジェクト」のリストを取得できる。
コミットオブジェクトを操作する - Grit::Commit
Git は全てのものをオブジェクトとして管理している。そして、「コミットオブジェクト」は、そのリビジョンの情報を全てオブジェクトとして持っている。
リポジトリから取得した Grit::Commit
を操作してその情報にアクセスする。
最新の「コミットオジェクト」は Git では HEAD
という。HEAD
を取得する。
>> head = master_commits.first
=> <Grit::Commit "bfab1e49cb14a35a915d0a7e4c663f9c5e1f6ea7">
最新の「コミットオジェクト」の名前は、
>> head.id
=> "bfab1e49cb14a35a915d0a7e4c663f9c5e1f6ea7"
その親(たち)は、
>> head.parents
=> [<Grit::Commit "83bcfc59ebc7372f0e281dfd0451f987c89ea963">]
最新の「コミットオジェクト」の Tree オブジェクトを取得する。このオブジェクトから実体のファイル情報にアクセスしていくことになる。
>> head.tree
=> #<Grit::Tree "54968b55cb852d23d717f9c631477aa5b598a1cb">
実体のファイル情報、ディレクトリ情報は、それぞれ Blob オブジェクト、Tree オブジェクトとなっており、Tree オブジェクトはそれらのオブジェクトへの参照を保持している(コンポジットパターンになっている)。最新の「コミットオジェクト」で取得した Tree オブジェクトは、Root Tree オブジェクトとなり、このオブジェクトから階層を辿っていく。
(Tree オブジェクトについては後ほどまた。)
これ以外にも最新の「コミットオジェクト」の情報が取得できる。
>> head.author
=> <Grit::Actor "Taro Yamada <taro@example.com>">
>> head.authored_date
=> Thu Jul 16 14:21:31 +0900 2009
>> head.committer
=> <Grit::Actor "Taro Yamada <taro@example.com>">
>> head.committed_date
=> Thu Jul 16 14:21:31 +0900 2009
>> head.message
=> "Added the diary of 2009/07/16."
3 つ前の「コミットオブジェクト」は、git では master^^^
、master~3
で表現できるが、これはメソッドチェーンを使って、
repo.commits.first.parents[0].parents[0].parents[0]
と表現できる。
ツリーオブジェクトを操作する - Grit::Tree
、Grit::Blob
「ツリーオジェクト」には、実体への参照が詰っている。
HEAD からルートツリーを取得する。
>> root_tree = head.tree
=> <Grit::Tree "54968b55cb852d23d717f9c631477aa5b598a1cb">
上記がHEADのルートツリーになる。ルートツリーが持っているオブジェクトを取得する。
>> contents = root_tree.contents
=> [<Grit::Tree "b57be8502a140fb10cb3e2ebbe9bd53c87eb5ad1">,
<Grit::Blob "518da8bf23ed85b2fde57d698a890e853956d37a">,
<Grit::Blob "1c25748b9ac134438f16cd84b5e3cc647eab618a">]
このルートツリーは、Grit::Tree
オブジェクトと Grit::Blob
オジェクトを保持していることがわかる。
Grit::Tree
はディレクトリ、Grit::Blob
はファイルのことだ。
Grit::Tree
の属性を確認してみる。
>> contents[0].name
=> "2009"
>> contents[0].mode
=> "040000"
“2009” という名前のディレクトリが存在する。mode
メソッドで Git で定義されているそのオブジェクトの型が取得できる。
Grit::Blob
の属性を確認してみる。
>> contents[1].name
=> "test1.txt"
>> contents[1].mode
=> "100644"
>> contents[1].size
=> 50
>> contents[1].data
=> "This is first text file.\n"
名前、モード、サイズ、中身のデータが取得できる。
Grit::Tree を取得するためのショートカット - Grit::Repo#tree
Grit::Repo
オブジェクトから直接 Grit::Tree
オブジェクトを取得するショートカットもある。
>> repo.tree
=> <Grit::Tree "master">
引数無しで Grit::Repo#tree
を呼び出した場合、master ブランチのHEADの「コミットオブジェクト」のルートツリーが取得されているようだが、
先に取得している同じオブジェクトとなるはずの root_tree
との違いは何なのだろうか?
比較してみる。
>> master_tree = repo.tree
=> "<Grit::Tree "master">"
>> master_tree.inspect
=> <Grit::Tree "master">
>> root_tree.inspect
=> "<Grit::Tree "54968b55cb852d23d717f9c631477aa5b598a1cb">"
>> master_tree.id
=> "master"
>> root_tree.id
=> "54968b55cb852d23d717f9c631477aa5b598a1cb"
>> master_tree.contents
=> [<Grit::Tree "b57be8502a140fb10cb3e2ebbe9bd53c87eb5ad1">,
<Grit::Blob "518da8bf23ed85b2fde57d698a890e853956d37a">,
<Grit::Blob "1c25748b9ac134438f16cd84b5e3cc647eab618a">]
>> root_tree.contents
=> [<Grit::Tree "b57be8502a140fb10cb3e2ebbe9bd53c87eb5ad1">,
<Grit::Blob "518da8bf23ed85b2fde57d698a890e853956d37a">,
<Grit::Blob "1c25748b9ac134438f16cd84b5e3cc647eab618a">]
>> master_tree == root_tree
=> false
>> master_tree === root_tree
=> false
>> master_tree.is_a? root_tree.class
=> true
Grit::Repo#tree
の返す Tree オブジェクトって、なんだか紛らわしい。。
ちなみに、引数にツリーオブジェクトの名称を指定することもできる。
>> repo.tree('b57be8502a140fb10cb3e2ebbe9bd53c87eb5ad1')
=> <Grit::Tree "b57be8502a140fb10cb3e2ebbe9bd53c87eb5ad1">
ブランチ名を指定するとブランチのツリーオブジェクトが取得できる。
>> repo.tree('b_one')
=> <Grit::Tree "b_one">
Grit::Blob を取得するためのショートカット - Grit::Repo#blob
Grit::Repo
オブジェクトから直接 Grit::Blob
オブジェクトを取得するショートカットもある。
>> repo.blob('518da8bf23ed85b2fde57d698a890e853956d37a')
=> #<Grit::Blob "518da8bf23ed85b2fde57d698a890e853956d37a">
>> repo.blob('518da8bf23ed85b2fde57d698a890e853956d37a').data
=> "This is first text file.\n"
ただ、仕様的によくわからないところがある。
>> repo.blob('518da8bf23ed85b2fde57d698a890e853956d37a').name
=> nil
>> contents = root_tree.contents
=> [<Grit::Tree "b57be8502a140fb10cb3e2ebbe9bd53c87eb5ad1">,
<Grit::Blob "518da8bf23ed85b2fde57d698a890e853956d37a">,
<Grit::Blob "1c25748b9ac134438f16cd84b5e3cc647eab618a">]
>> test1_blob = contents[1]
=> <Grit::Blob "518da8bf23ed85b2fde57d698a890e853956d37a">
>> test1_blob.name
=> "test1.txt"
>> repo.blob('518da8bf23ed85b2fde57d698a890e853956d37a') == test1_blob
=> false
Grit::blob
で取得した test1.txt
の Grit::Blob#name
が nil を返す。
コミットオブジェクトから辿って取得した同じ test1.txt
の Blob オジェクトではちゃんと返すのだが・・・
Grit::Repo#blob
、 Grit::Repo#tree
については、後でソースを確認しておこう。
実体のファイルを名称で取得する - Grit::Tree#/
リポジトリのワーキングディレクトリは以下の構成になっているものとする。
+ repo1
+ 2009
+ 07
+ 16
- diary.txt
2009/07/16/diary.txt
にアクセスしたい場合、どうするか?Grit::Tree#/
が便利だ。
>> root_tree
=> <Grit::Tree "54968b55cb852d23d717f9c631477aa5b598a1cb">
>> root_tree / '2009'
=> <Grit::Tree "b57be8502a140fb10cb3e2ebbe9bd53c87eb5ad1">
>> root_tree / '2009/07'
=> <Grit::Tree "d15cd64e426f9c5a3d6090719621e462ee199c7b">
>> root_tree / '2009/07/16'
=> <Grit::Tree "edfa8e6646dea2e3cf1abf70f170292954591dd5">
>> root_tree / '2009/07/16/diary.txt'
=> <Grit::Blob "061f07312f98202372e1e9a9090c4927b6d38ca9">
>> (root_tree / '2009/07/16/diary.txt').name
=> "diary.txt"
>> (root_tree / '2009/07/16/diary.txt').data
=> "It's so Hot!\n"
インデックスに追加してコミットする - Grit::Repo#add
、Grit::Repo#commit_index
まずは単純なところから。
新しいファイルをコミットする
リポジトリ直下に新規にファイルを作成して、 add、commit してみる。
まずは、Git でリポジトリを作成。
$ mkdir repos3
$ cd repos3
$ git init
irb を使って、リポジトリを開く。
$ irb --prompt simple
>> require 'rubygems'
=> true
>> require 'grit'
=> true
>> repo = Grit::Repo.new('.')
実体となるファイルを作成しておく。
>> filename = 'test1.txt'
=> "test1.txt"
>> File.open(filename, 'w') { |f| f << 'Committed using Grit' }
=> <File:test1.txt (closed)>
Git で確認してみる。
$ 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” になっている。
このファイルを Grit でコミットするには、まずはファイルの Blob オブジェクトを生成する必要がある。
>> test_blob = Grit::Blob.create(
?> repo,
?> { :name => filename, :data => File.read(filename) }
>> )
=> <Grit::Blob "">
:name
にファイルの名称、:data
にそのファイルの内容を指定している。
Grit::Repo#add
で add
する。
>> Dir.chdir(repo.working_dir) { repo.add(test_blob.name) }
=> ""
Grit::Repo#working_dir
に移動し、Grit::Repo#add
で Grit::Blob#name
を引数に add
している。
ちなみに、Dir.chdir
にブロックを預けると、そのブロック内は引数に指定したディレクトリに移動しているスコープになる。
メインのアプリケーションのディレクトリ(カレントディレクトリ)は移動しない。
Git で確認する。
$ git status
# On branch master
#
# Initial commit
#
# Changes to be committed:
# (use "git rm --cached <file>..." to unstage)
#
# new file: test1.txt
#
確かにインデックスに add
されている。
では Grit::Repo#commit_index
を使ってコミットする。
>> repo.commit_index('Added the new text file using Grit!')
=> "[master (root-commit)]: created 7148100: "Added the new text file using Grit!"\n
1 files changed, 1 insertions(+), 0 deletions(-)\n
create mode 100644 test1.txt\n"
Git で確認する。
$ git show
commit 7148100bca4a6a05af6896889d8dc023cdc6bd41
Author: Taro Yamada <taro@example.com>
Date: Fri Jul 17 22:20:05 2009 +0900
Added the new text file using Grit!
diff --git a/test1.txt b/test1.txt
new file mode 100644
index 0000000..b4e5728
--- /dev/null
+++ b/test1.txt
@@ -0,0 +1 @@
+Committed using Grit
\ No newline at end of file
確かにコミットされた。
ちなみにこの時の先程コミットの際に使用した Grit::Blob
オブジェクトはどうなっているか?
>> test_blob.id
=> nil
>> test_blob.name
=> "test1.txt"
>> test_blob.data
=> "Committed using Grit"
Grit::Blob#id
は nil のままとなっている。
この後、今コミットした**test1.txt
**のオブジェクトをアプリなどで使用する場合には、ストアから引き直した方がよい。
>> test_blob = repo.tree / 'test1.txt'
=> #<Grit::Blob "b4e57287278a633d4c6877a8c1f651cdfa9353aa">
>> test_blob.id
=> "b4e57287278a633d4c6877a8c1f651cdfa9353aa"
>> test_blob.name
=> "test1.txt"
>> test_blob.data
=> "Committed using Grit"
Grit::Blob#id
でオブジェクト名が取得できる。
ディレクトリも含めてコミットする
次に新規にディレクトリを作成し、その配下のファイルをコミットしてみる。
ディレクトリオブジェクトである Grit::Tree
を意識する必要があるのかな?と思ってはみたが、
ソースをみると Git 任せになっているようなので、
Grit::Blob
のname
属性Grit::Repo#add
時のカレントディレクトリ
だけを意識すればよさそうだ。
まずは実体の作成。ディレクトリを作成して、その配下にファイルを作成する。
>> Dir.mkdir('20090717')
=> 0
>> Dir["*"]
=> ["20090717", "test1.txt"]
>> File.open('20090717/test2', 'w') { |f| f << "This file is in '20090717' directory" }
=> <File:20090717/test2 (closed)>
Git で確認してみる。
$ git status
# On branch master
# Untracked files:
# (use "git add <file>..." to include in what will be committed)
#
# 20090717/
nothing added to commit but untracked files present (use "git add" to track)
先程と同様に Blob オジェクトを生成する。この時の注意点としては、ディレクトリのパスも含める点。
>> test_blob2 = Grit::Blob.create(
?> repo,
?> { :name => '20090717/test2', :data => File.read('20090717/test2') }
>> )
=> <Grit::Blob "">
>> test_blob2.id
=> nil
>> test_blob2.name
=> "20090717/test2"
>> test_blob2.data
=> "This file is in '20090717' directory"
この後の操作は対象が新しい Blob オブジェクトとなること以外、先程と全く同じ方法になる。まずは add
。
>> Dir.chdir(repo.working_dir) { repo.add(test_blob2.name) }
=> ""
リポジトリのワーキングディレクトリに移動することは、先程 Blob オブジェクトにセットとした name
属性の値と関連しており、ミソになる。
(実際に Git のコマンドを使う場合も同じことだ。)
Git で確認する。
$ git status
# On branch master
# Changes to be committed:
# (use "git reset HEAD <file>..." to unstage)
#
# new file: 20090717/test2
#
コミットする。
>> repo.commit_index('Great!')
=> "[master]: created 52e3890: "Great!"\n
1 files changed, 1 insertions(+), 0 deletions(-)\n
create mode 100644 20090717/test2\n"
Git で確認する。
$ git show
commit 52e38900d430be2329bb766b73afc4809008a266
Author: Taro Yamada <taro@example.com>
Date: Fri Jul 17 23:09:28 2009 +0900
Great!
diff --git a/20090717/test2 b/20090717/test2
new file mode 100644
index 0000000..81943e3
--- /dev/null
+++ b/20090717/test2
@@ -0,0 +1 @@
+This file is in '20090717' directory
\ No newline at end of file
参考サイト
- mojombo’s grit at master - GitHub
- Grit のマスタリポジトリ。
- lenary’s ginatra at master - GitHub
- “Ginatra”。Grit、Sinatra をベースとした Git のリポジトリブラウザ。 Grit の使い方、Sinatra の使い方を見るのに参考になる。
- sr’s git-wiki at master - GitHub
- Git ベースの Wiki エンジン。機能を絞っているので、ソースを見る最初の一歩にはよい感じ。