git_store 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|