Grit を使って Git リポジトリを Ruby で操作する

Written by @dr_taka_n at 2009/07/17 23:20 [, ]

Git リポジトリを Ruby のアプリのデータストアとして使ってみたいと思い、少し調べてみた。

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::TreeGrit::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.txtGrit::Blob#name が nil を返す。 コミットオブジェクトから辿って取得した同じ test1.txt の Blob オジェクトではちゃんと返すのだが・・・

Grit::Repo#blobGrit::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#addGrit::Repo#commit_index

まずは単純なところから。

新しいファイルをコミットする

リポジトリ直下に新規にファイルを作成して、 addcommit してみる。

まずは、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#addadd する。

>> Dir.chdir(repo.working_dir) { repo.add(test_blob.name) }
=> ""

Grit::Repo#working_dir に移動し、Grit::Repo#addGrit::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::Blobname 属性
  • 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

参考サイト

blog comments powered by Disqus