git_store 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/LICENSE +18 -0
- data/README.md +147 -0
- data/Rakefile +35 -0
- data/git_store.gemspec +40 -0
- data/lib/git_store/blob.rb +32 -0
- data/lib/git_store/commit.rb +65 -0
- data/lib/git_store/diff.rb +76 -0
- data/lib/git_store/handlers.rb +36 -0
- data/lib/git_store/pack.rb +425 -0
- data/lib/git_store/tag.rb +40 -0
- data/lib/git_store/tree.rb +183 -0
- data/lib/git_store/user.rb +29 -0
- data/lib/git_store.rb +392 -0
- data/test/bare_store_spec.rb +33 -0
- data/test/benchmark.rb +30 -0
- data/test/commit_spec.rb +81 -0
- data/test/git_store_spec.rb +257 -0
- data/test/tree_spec.rb +92 -0
- metadata +79 -0
data/LICENSE
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
Copyright (c) 2008 Matthias Georgi <http://www.matthias-georgi.de>
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
of this software and associated documentation files (the "Software"), to
|
5
|
+
deal in the Software without restriction, including without limitation the
|
6
|
+
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
|
7
|
+
sell copies of the Software, and to permit persons to whom the Software is
|
8
|
+
furnished to do so, subject to the following conditions:
|
9
|
+
|
10
|
+
The above copyright notice and this permission notice shall be included in
|
11
|
+
all copies or substantial portions of the Software.
|
12
|
+
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
16
|
+
THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
17
|
+
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
18
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,147 @@
|
|
1
|
+
Git Store - using Git as versioned data store in Ruby
|
2
|
+
=====================================================
|
3
|
+
|
4
|
+
GitStore implements a versioned data store based on the revision
|
5
|
+
management system [Git][1]. You can store object hierarchies as nested
|
6
|
+
hashes, which will be mapped on the directory structure of a git
|
7
|
+
repository. Basically GitStore checks out the repository into a
|
8
|
+
in-memory representation, which can be modified and finally committed.
|
9
|
+
|
10
|
+
GitStore supports transactions, so that updates to the store either
|
11
|
+
fail or succeed completely.
|
12
|
+
|
13
|
+
### Installation
|
14
|
+
|
15
|
+
GitStore can be installed as gem easily:
|
16
|
+
|
17
|
+
$ gem sources -a http://gems.github.com
|
18
|
+
$ sudo gem install georgi-git_store
|
19
|
+
|
20
|
+
### Usage Example
|
21
|
+
|
22
|
+
First thing you should do, is to initialize a new git repository.
|
23
|
+
|
24
|
+
$ mkdir test
|
25
|
+
$ cd test
|
26
|
+
$ git init
|
27
|
+
|
28
|
+
Now you can instantiate a GitStore instance and store some data. The
|
29
|
+
data will be serialized depending on the file extension. So for YAML
|
30
|
+
storage you can use the 'yml' extension:
|
31
|
+
|
32
|
+
store = GitStore.new('/path/to/repo')
|
33
|
+
|
34
|
+
store['users/matthias.yml'] = User.new('Matthias')
|
35
|
+
store['pages/home.yml'] = Page.new('matthias', 'Home')
|
36
|
+
|
37
|
+
store.commit 'Added user and page'
|
38
|
+
|
39
|
+
### Transactions
|
40
|
+
|
41
|
+
GitStore manages concurrent access by a file locking scheme. So only
|
42
|
+
one process can start a transaction at one time. This is implemented
|
43
|
+
by locking the `refs/head/<branch>.lock` file, which is also
|
44
|
+
respected by the git binary.
|
45
|
+
|
46
|
+
If you access the repository from different processes or threads, you
|
47
|
+
should write to the store using transactions. If something goes wrong
|
48
|
+
inside a transaction, all changes will be rolled back to the original
|
49
|
+
state.
|
50
|
+
|
51
|
+
store = GitStore.new('/path/to/repo')
|
52
|
+
|
53
|
+
store.transaction do
|
54
|
+
# If an exception happens here, the transaction will be aborted.
|
55
|
+
store['pages/home.yml'] = Page.new('matthias', 'Home')
|
56
|
+
end
|
57
|
+
|
58
|
+
|
59
|
+
A transaction without a block looks like this:
|
60
|
+
|
61
|
+
store.start_transaction
|
62
|
+
|
63
|
+
store['pages/home.yml'] = Page.new('matthias', 'Home')
|
64
|
+
|
65
|
+
store.rollback # This will restore the original state
|
66
|
+
|
67
|
+
|
68
|
+
### Data Storage
|
69
|
+
|
70
|
+
When you call the `commit` method, your data is written back straight
|
71
|
+
into the git repository. No intermediate file representation. So if
|
72
|
+
you want to have a look at your data, you can use a git browser like
|
73
|
+
[git-gui][6] or checkout the files:
|
74
|
+
|
75
|
+
$ git checkout
|
76
|
+
|
77
|
+
|
78
|
+
### Iteration
|
79
|
+
|
80
|
+
Iterating over the data objects is quite easy. Furthermore you can
|
81
|
+
iterate over trees and subtrees, so you can partition your data in a
|
82
|
+
meaningful way. For example you may separate the config files and the
|
83
|
+
pages of a wiki:
|
84
|
+
|
85
|
+
store['pages/home.yml'] = Page.new('matthias', 'Home')
|
86
|
+
store['pages/about.yml'] = Page.new('matthias', 'About')
|
87
|
+
store['config/wiki.yml'] = { 'name' => 'My Personal Wiki' }
|
88
|
+
|
89
|
+
# Enumerate all objects
|
90
|
+
store.each { |obj| ... }
|
91
|
+
|
92
|
+
# Enumerate only pages
|
93
|
+
store['pages'].each { |page| ... }
|
94
|
+
|
95
|
+
|
96
|
+
### Serialization
|
97
|
+
|
98
|
+
Serialization is dependent on the filename extension. You can add more
|
99
|
+
handlers if you like, the interface is like this:
|
100
|
+
|
101
|
+
class YAMLHandler
|
102
|
+
def read(data)
|
103
|
+
YAML.load(data)
|
104
|
+
end
|
105
|
+
|
106
|
+
def write(data)
|
107
|
+
data.to_yaml
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
Shinmun uses its own handler for files with `md` extension:
|
112
|
+
|
113
|
+
class PostHandler
|
114
|
+
def read(data)
|
115
|
+
Post.new(:src => data)
|
116
|
+
end
|
117
|
+
|
118
|
+
def write(post)
|
119
|
+
post.dump
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
store = GitStore.new('.')
|
124
|
+
store.handler['md'] = PostHandler.new
|
125
|
+
|
126
|
+
|
127
|
+
### GitStore on GitHub
|
128
|
+
|
129
|
+
Download or fork the project on its [Github page][5]
|
130
|
+
|
131
|
+
### Mailing List
|
132
|
+
|
133
|
+
Please join the [GitStore Google Group][3] for further discussion.
|
134
|
+
|
135
|
+
### Related Work
|
136
|
+
|
137
|
+
John Wiegley already has done [something similar for Python][4].
|
138
|
+
|
139
|
+
|
140
|
+
|
141
|
+
[1]: http://git.or.cz/
|
142
|
+
[2]: http://github.com/mojombo/grit
|
143
|
+
[3]: http://groups.google.com/group/gitstore
|
144
|
+
[4]: http://www.newartisans.com/blog_files/git.versioned.data.store.php
|
145
|
+
[5]: http://github.com/georgi/git_store
|
146
|
+
[6]: http://www.kernel.org/pub/software/scm/git/docs/git-gui.html
|
147
|
+
[7]: http://www.matthias-georgi.de/shinmun
|
data/Rakefile
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require "rake/rdoctask"
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'spec/rake/spectask'
|
6
|
+
rescue LoadError
|
7
|
+
puts <<-EOS
|
8
|
+
To use rspec for testing you must install the rspec gem:
|
9
|
+
gem install rspec
|
10
|
+
EOS
|
11
|
+
exit(0)
|
12
|
+
end
|
13
|
+
|
14
|
+
desc "Run all specs"
|
15
|
+
Spec::Rake::SpecTask.new(:spec) do |t|
|
16
|
+
t.spec_opts = ['-cfs']
|
17
|
+
t.spec_files = FileList['test/**/*_spec.rb']
|
18
|
+
end
|
19
|
+
|
20
|
+
desc "Print SpecDocs"
|
21
|
+
Spec::Rake::SpecTask.new(:doc) do |t|
|
22
|
+
t.spec_opts = ["--format", "specdoc"]
|
23
|
+
t.spec_files = FileList['test/*_spec.rb']
|
24
|
+
end
|
25
|
+
|
26
|
+
desc "Generate the RDoc"
|
27
|
+
Rake::RDocTask.new do |rdoc|
|
28
|
+
files = ["README.md", "LICENSE", "lib/**/*.rb"]
|
29
|
+
rdoc.rdoc_files.add(files)
|
30
|
+
rdoc.main = "README.md"
|
31
|
+
rdoc.title = "Git Store - using Git as versioned data store in Ruby"
|
32
|
+
end
|
33
|
+
|
34
|
+
desc "Run the rspec"
|
35
|
+
task :default => :spec
|
data/git_store.gemspec
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'git_store'
|
3
|
+
s.version = '0.3.1'
|
4
|
+
s.summary = 'a simple data store based on git'
|
5
|
+
s.author = 'Matthias Georgi'
|
6
|
+
s.email = 'matti.georgi@gmail.com'
|
7
|
+
s.homepage = 'http://ww.matthias-georgi.de/git_store'
|
8
|
+
s.description = <<END
|
9
|
+
GitStore implements a versioned data store based on the revision
|
10
|
+
management system Git. You can store object hierarchies as nested
|
11
|
+
hashes, which will be mapped on the directory structure of a git
|
12
|
+
repository. GitStore checks out the repository into a in-memory
|
13
|
+
representation, which can be modified and finally committed.
|
14
|
+
END
|
15
|
+
s.require_path = 'lib'
|
16
|
+
s.has_rdoc = true
|
17
|
+
s.extra_rdoc_files = ['README.md']
|
18
|
+
s.files = %w{
|
19
|
+
.gitignore
|
20
|
+
LICENSE
|
21
|
+
README.md
|
22
|
+
Rakefile
|
23
|
+
git_store.gemspec
|
24
|
+
lib/git_store.rb
|
25
|
+
lib/git_store/blob.rb
|
26
|
+
lib/git_store/commit.rb
|
27
|
+
lib/git_store/diff.rb
|
28
|
+
lib/git_store/handlers.rb
|
29
|
+
lib/git_store/pack.rb
|
30
|
+
lib/git_store/tag.rb
|
31
|
+
lib/git_store/tree.rb
|
32
|
+
lib/git_store/user.rb
|
33
|
+
test/bare_store_spec.rb
|
34
|
+
test/benchmark.rb
|
35
|
+
test/commit_spec.rb
|
36
|
+
test/git_store_spec.rb
|
37
|
+
test/tree_spec.rb
|
38
|
+
}
|
39
|
+
end
|
40
|
+
|
@@ -0,0 +1,32 @@
|
|
1
|
+
class GitStore
|
2
|
+
|
3
|
+
# This class stores the raw string data of a blob, but also the
|
4
|
+
# deserialized data object.
|
5
|
+
class Blob
|
6
|
+
|
7
|
+
attr_accessor :store, :id, :data, :mode, :object
|
8
|
+
|
9
|
+
# Initialize a Blob
|
10
|
+
def initialize(store, id = nil, data = nil)
|
11
|
+
@store = store
|
12
|
+
@id = id || store.id_for('blob', data)
|
13
|
+
@data = data
|
14
|
+
@mode = "100644"
|
15
|
+
end
|
16
|
+
|
17
|
+
def ==(other)
|
18
|
+
Blob === other and id == other.id
|
19
|
+
end
|
20
|
+
|
21
|
+
def dump
|
22
|
+
@data
|
23
|
+
end
|
24
|
+
|
25
|
+
# Write the data to the git object store
|
26
|
+
def write
|
27
|
+
@id = store.put(self)
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
class GitStore
|
2
|
+
|
3
|
+
class Commit
|
4
|
+
attr_accessor :store, :id, :tree, :parent, :author, :committer, :message
|
5
|
+
|
6
|
+
def initialize(store, id = nil, data = nil)
|
7
|
+
@store = store
|
8
|
+
@id = id
|
9
|
+
@parent = []
|
10
|
+
|
11
|
+
parse(data) if data
|
12
|
+
end
|
13
|
+
|
14
|
+
def ==(other)
|
15
|
+
Commit === other and id == other.id
|
16
|
+
end
|
17
|
+
|
18
|
+
def parse(data)
|
19
|
+
headers, @message = data.split(/\n\n/, 2)
|
20
|
+
|
21
|
+
headers.split(/\n/).each do |header|
|
22
|
+
key, value = header.split(/ /, 2)
|
23
|
+
case key
|
24
|
+
when 'parent'
|
25
|
+
@parent << value
|
26
|
+
|
27
|
+
when 'author'
|
28
|
+
@author = User.parse(value)
|
29
|
+
|
30
|
+
when 'committer'
|
31
|
+
@committer = User.parse(value)
|
32
|
+
|
33
|
+
when 'tree'
|
34
|
+
@tree = store.get(value)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
self
|
39
|
+
end
|
40
|
+
|
41
|
+
def diff(commit, path = nil)
|
42
|
+
commit = commit.id if Commit === commit
|
43
|
+
Diff.exec(store, "git diff --full-index #{commit} #{id} -- '#{path}'")
|
44
|
+
end
|
45
|
+
|
46
|
+
def diffs(path = nil)
|
47
|
+
diff(parent.first, path)
|
48
|
+
end
|
49
|
+
|
50
|
+
def write
|
51
|
+
@id = store.put(self)
|
52
|
+
end
|
53
|
+
|
54
|
+
def dump
|
55
|
+
[ "tree #{ tree.id }",
|
56
|
+
parent.map { |parent| "parent #{parent}" },
|
57
|
+
"author #{ author.dump }",
|
58
|
+
"committer #{ committer.dump }",
|
59
|
+
'',
|
60
|
+
message ].flatten.join("\n")
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
class GitStore
|
2
|
+
|
3
|
+
# adapted from Grit
|
4
|
+
class Diff
|
5
|
+
attr_reader :store
|
6
|
+
attr_reader :a_path, :b_path
|
7
|
+
attr_reader :a_blob, :b_blob
|
8
|
+
attr_reader :a_mode, :b_mode
|
9
|
+
attr_reader :new_file, :deleted_file
|
10
|
+
attr_reader :diff
|
11
|
+
|
12
|
+
def initialize(store, a_path, b_path, a_blob, b_blob, a_mode, b_mode, new_file, deleted_file, diff)
|
13
|
+
@store = store
|
14
|
+
@a_path = a_path
|
15
|
+
@b_path = b_path
|
16
|
+
@a_blob = a_blob =~ /^0{40}$/ ? nil : store.get(a_blob)
|
17
|
+
@b_blob = b_blob =~ /^0{40}$/ ? nil : store.get(b_blob)
|
18
|
+
@a_mode = a_mode
|
19
|
+
@b_mode = b_mode
|
20
|
+
@new_file = new_file
|
21
|
+
@deleted_file = deleted_file
|
22
|
+
@diff = diff
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.exec(store, cmd)
|
26
|
+
list(store, IO.popen(cmd) { |io| io.read })
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.list(store, text)
|
30
|
+
lines = text.split("\n")
|
31
|
+
|
32
|
+
diffs = []
|
33
|
+
|
34
|
+
while !lines.empty?
|
35
|
+
m, a_path, b_path = *lines.shift.match(%r{^diff --git a/(.+?) b/(.+)$})
|
36
|
+
|
37
|
+
if lines.first =~ /^old mode/
|
38
|
+
m, a_mode = *lines.shift.match(/^old mode (\d+)/)
|
39
|
+
m, b_mode = *lines.shift.match(/^new mode (\d+)/)
|
40
|
+
end
|
41
|
+
|
42
|
+
if lines.empty? || lines.first =~ /^diff --git/
|
43
|
+
diffs << Diff.new(store, a_path, b_path, nil, nil, a_mode, b_mode, false, false, nil)
|
44
|
+
next
|
45
|
+
end
|
46
|
+
|
47
|
+
new_file = false
|
48
|
+
deleted_file = false
|
49
|
+
|
50
|
+
if lines.first =~ /^new file/
|
51
|
+
m, b_mode = lines.shift.match(/^new file mode (.+)$/)
|
52
|
+
a_mode = nil
|
53
|
+
new_file = true
|
54
|
+
elsif lines.first =~ /^deleted file/
|
55
|
+
m, a_mode = lines.shift.match(/^deleted file mode (.+)$/)
|
56
|
+
b_mode = nil
|
57
|
+
deleted_file = true
|
58
|
+
end
|
59
|
+
|
60
|
+
m, a_blob, b_blob, b_mode = *lines.shift.match(%r{^index ([0-9A-Fa-f]+)\.\.([0-9A-Fa-f]+) ?(.+)?$})
|
61
|
+
b_mode.strip! if b_mode
|
62
|
+
|
63
|
+
diff_lines = []
|
64
|
+
while lines.first && lines.first !~ /^diff/
|
65
|
+
diff_lines << lines.shift
|
66
|
+
end
|
67
|
+
diff = diff_lines.join("\n")
|
68
|
+
|
69
|
+
diffs << Diff.new(store, a_path, b_path, a_blob, b_blob, a_mode, b_mode, new_file, deleted_file, diff)
|
70
|
+
end
|
71
|
+
|
72
|
+
diffs
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
|
2
|
+
# This fix ensures sorted yaml maps.
|
3
|
+
class Hash
|
4
|
+
def to_yaml( opts = {} )
|
5
|
+
YAML::quick_emit( object_id, opts ) do |out|
|
6
|
+
out.map( taguri, to_yaml_style ) do |map|
|
7
|
+
sort_by { |k, v| k.to_s }.each do |k, v|
|
8
|
+
map.add( k, v )
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
class GitStore
|
16
|
+
|
17
|
+
class DefaultHandler
|
18
|
+
def read(data)
|
19
|
+
data
|
20
|
+
end
|
21
|
+
|
22
|
+
def write(data)
|
23
|
+
data.to_s
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class YAMLHandler
|
28
|
+
def read(data)
|
29
|
+
YAML.load(data)
|
30
|
+
end
|
31
|
+
|
32
|
+
def write(data)
|
33
|
+
data.to_yaml
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|