gitmodel 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.document +5 -0
- data/.gitignore +7 -0
- data/.rspec +1 -0
- data/.rvmrc +1 -0
- data/Gemfile +5 -0
- data/Gemfile.lock +54 -0
- data/LICENSE +20 -0
- data/README.md +143 -0
- data/Rakefile +3 -0
- data/autotest/discover.rb +1 -0
- data/gitmodel.gemspec +36 -0
- data/lib/gitmodel/errors.rb +25 -0
- data/lib/gitmodel/persistable.rb +269 -0
- data/lib/gitmodel/transaction.rb +62 -0
- data/lib/gitmodel.rb +76 -0
- data/spec/gitmodel/persistable_spec.rb +436 -0
- data/spec/gitmodel/transaction_spec.rb +59 -0
- data/spec/gitmodel_spec.rb +39 -0
- data/spec/spec_helper.rb +12 -0
- data/spec/support/setup.rb +10 -0
- metadata +206 -0
data/.document
ADDED
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--colour
|
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm use ruby-1.9.2@gitmodel
|
data/Gemfile
ADDED
data/Gemfile.lock
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
PATH
|
2
|
+
remote: .
|
3
|
+
specs:
|
4
|
+
gitmodel (0.0.0)
|
5
|
+
activemodel (>= 3.0.1)
|
6
|
+
activesupport (>= 3.0.1)
|
7
|
+
grit (>= 2.3.0)
|
8
|
+
lockfile (>= 1.4.3)
|
9
|
+
|
10
|
+
GEM
|
11
|
+
remote: http://rubygems.org/
|
12
|
+
specs:
|
13
|
+
ZenTest (4.4.0)
|
14
|
+
activemodel (3.0.1)
|
15
|
+
activesupport (= 3.0.1)
|
16
|
+
builder (~> 2.1.2)
|
17
|
+
i18n (~> 0.4.1)
|
18
|
+
activesupport (3.0.1)
|
19
|
+
autotest (4.4.1)
|
20
|
+
autotest-fsevent (0.2.3)
|
21
|
+
sys-uname
|
22
|
+
builder (2.1.2)
|
23
|
+
diff-lcs (1.1.2)
|
24
|
+
grit (2.3.0)
|
25
|
+
diff-lcs (~> 1.1)
|
26
|
+
mime-types (~> 1.15)
|
27
|
+
i18n (0.4.1)
|
28
|
+
lockfile (1.4.3)
|
29
|
+
mime-types (1.16)
|
30
|
+
rspec (2.0.1)
|
31
|
+
rspec-core (~> 2.0.1)
|
32
|
+
rspec-expectations (~> 2.0.1)
|
33
|
+
rspec-mocks (~> 2.0.1)
|
34
|
+
rspec-core (2.0.1)
|
35
|
+
rspec-expectations (2.0.1)
|
36
|
+
diff-lcs (>= 1.1.2)
|
37
|
+
rspec-mocks (2.0.1)
|
38
|
+
rspec-core (~> 2.0.1)
|
39
|
+
rspec-expectations (~> 2.0.1)
|
40
|
+
sys-uname (0.8.4)
|
41
|
+
|
42
|
+
PLATFORMS
|
43
|
+
ruby
|
44
|
+
|
45
|
+
DEPENDENCIES
|
46
|
+
ZenTest (>= 4.4.0)
|
47
|
+
activemodel (>= 3.0.1)
|
48
|
+
activesupport (>= 3.0.1)
|
49
|
+
autotest (>= 4.4.1)
|
50
|
+
autotest-fsevent (>= 0.2.3)
|
51
|
+
gitmodel!
|
52
|
+
grit (>= 2.3.0)
|
53
|
+
lockfile (>= 1.4.3)
|
54
|
+
rspec (>= 2.0.1)
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Paul Dowman
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,143 @@
|
|
1
|
+
GitModel: distributed, versioned NoSQL for Ruby
|
2
|
+
---------------------------------------------------
|
3
|
+
|
4
|
+
_http://github.com/pauldowman/gitmodel_
|
5
|
+
|
6
|
+
GitModel is an
|
7
|
+
[ActiveModel](http://yehudakatz.com/2010/01/10/activemodel-make-any-ruby-object-feel-like-activerecord/)-compliant
|
8
|
+
persistence framework for Ruby that uses [Git](http://git-scm.com/) for
|
9
|
+
versioning and remote syncing.
|
10
|
+
|
11
|
+
GitModel persists Ruby objects using Git as a data storage engine. It's an
|
12
|
+
ActiveModel implementation so it works stand-alone or in Rails 3 as a drop-in
|
13
|
+
replacement for ActiveRecord or DataMapper.
|
14
|
+
|
15
|
+
Because the database is a Git repository it can be synced across multiple
|
16
|
+
machines, manipulated with standard Git client tools, can be branched and
|
17
|
+
merged, and of course keeps the history of all changes.
|
18
|
+
|
19
|
+
|
20
|
+
Status
|
21
|
+
------
|
22
|
+
|
23
|
+
_It is nowhere near production ready but I'm working on it. Please feel free to
|
24
|
+
contribute tests and/or code to help!_
|
25
|
+
|
26
|
+
|
27
|
+
Why it's awesome
|
28
|
+
----------------
|
29
|
+
|
30
|
+
* Schema-less NoSQL data store
|
31
|
+
* Each record is a normal Ruby object, attributes are any Ruby type or large
|
32
|
+
chunks of binary data
|
33
|
+
* Never lose data, history is kept forever and can be restored simply using
|
34
|
+
standard Git tools
|
35
|
+
* Branch and merge your production data
|
36
|
+
* GitModel can actually work with different branches
|
37
|
+
* Branch or tag snapshots of your data
|
38
|
+
* Experiment on production data using branches, for example to test a
|
39
|
+
migration
|
40
|
+
* Distributed (synced using standard Git push/pull)
|
41
|
+
* Transactions
|
42
|
+
* Metadata for all database changes (Git commit messages, date & time, etc.)
|
43
|
+
* In order to be easily human-editable, the database is simply files and
|
44
|
+
directores stored in a Git repository. GitModel uses the Git repo directly
|
45
|
+
(rather than Git's checked-out "working copy") but you can do a "git
|
46
|
+
checkout" to view and manipulate the database contents, and then "git commit"
|
47
|
+
* Test-driven development and excellent test coverage
|
48
|
+
* Clean and easy-to-use API
|
49
|
+
|
50
|
+
|
51
|
+
Usage
|
52
|
+
-----
|
53
|
+
|
54
|
+
GitModel.db_root = '/tmp/gitmodel-data'
|
55
|
+
GitModel.create_db!
|
56
|
+
|
57
|
+
class Post
|
58
|
+
include GitModel::Persistable
|
59
|
+
|
60
|
+
attribute :title
|
61
|
+
attribute :body
|
62
|
+
attribute :categories, :default => []
|
63
|
+
attribute :allow_comments, :default => true
|
64
|
+
|
65
|
+
blob :image
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
p1 = Post.new(:id => 'lessons-learned', :title => 'Lessons learned', :body => '...')
|
70
|
+
p1.image = some_binary_data
|
71
|
+
p1.save!
|
72
|
+
|
73
|
+
p2 = Post.new(:id => 'hotdog-eating-contest', :title => 'I won!')
|
74
|
+
p2.body = 'This weekend I won a hotdog eating contest!'
|
75
|
+
p2.image = some_binary_data
|
76
|
+
p2.blobs['hotdogs.jpg'] = some_binary_data
|
77
|
+
p2.blobs['the-aftermath.jpg'] = some_binary_data
|
78
|
+
p2.save!
|
79
|
+
|
80
|
+
p3 = Post.create!(:id => 'running-with-scissors', :title => 'Running with scissors', :body => '...')
|
81
|
+
|
82
|
+
p4 = Post.find('running-with-scissors')
|
83
|
+
|
84
|
+
|
85
|
+
class Comment
|
86
|
+
include GitModel::Persistable
|
87
|
+
attribute :text
|
88
|
+
end
|
89
|
+
|
90
|
+
c1 = Comment.create!(:id => '2010-01-03-328', :text => '...')
|
91
|
+
c2 = Comment.create!(:id => '2010-05-29-742', :text => '...')
|
92
|
+
|
93
|
+
|
94
|
+
Database file structure
|
95
|
+
-----------------------
|
96
|
+
|
97
|
+
The database is stored in a human-editable format. Simply do "git checkout -f"
|
98
|
+
and you'll see directories and files.
|
99
|
+
|
100
|
+
Each type of object is stored in a top-level directory (this is analogous to
|
101
|
+
ActiveRecord tables), and each object is stored in a subdirectory which is
|
102
|
+
named using the object's id (i.e. the primary key). Attributes that are Ruby
|
103
|
+
types (strings, numbers, hashes, arrays, whatever) are stored in a file named
|
104
|
+
attributes.json and binary attributes ("blobs") are stored in their own
|
105
|
+
files.
|
106
|
+
|
107
|
+
For example, the database for the example above would have a directory
|
108
|
+
structure that looks like this:
|
109
|
+
|
110
|
+
* db-root
|
111
|
+
* comments
|
112
|
+
* 2010-01-03-328
|
113
|
+
* _attributes.json_
|
114
|
+
* 2010-05-29-742
|
115
|
+
* _attributes.json_
|
116
|
+
* posts
|
117
|
+
* hotdog-eating-contest
|
118
|
+
* _attributes.json_
|
119
|
+
* _hotdogs.jpg_
|
120
|
+
* _image_
|
121
|
+
* _the-aftermath.jpg_
|
122
|
+
* lessons-learned
|
123
|
+
* _attributes.json_
|
124
|
+
* _image_
|
125
|
+
* running-with-scissors
|
126
|
+
* _attributes.json_
|
127
|
+
|
128
|
+
|
129
|
+
To Do
|
130
|
+
-----
|
131
|
+
|
132
|
+
* Querying
|
133
|
+
* Use AREL?
|
134
|
+
* Finish some pending specs
|
135
|
+
* Associations
|
136
|
+
* API documentation
|
137
|
+
* Rails integration
|
138
|
+
* rake tasks
|
139
|
+
* generators
|
140
|
+
* Performance
|
141
|
+
* Haven't optimized for performance yet.
|
142
|
+
|
143
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
Autotest.add_discovery { "rspec2" }
|
data/gitmodel.gemspec
ADDED
@@ -0,0 +1,36 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = 'gitmodel'
|
3
|
+
s.version = '0.0.1'
|
4
|
+
s.platform = Gem::Platform::RUBY
|
5
|
+
|
6
|
+
s.authors = ["Paul Dowman"]
|
7
|
+
s.email = 'paul@pauldowman.com'
|
8
|
+
s.homepage = 'http://github.com/pauldowman/gitmodel'
|
9
|
+
|
10
|
+
s.summary = %q{An ActiveModel-compliant persistence framework for Ruby that uses Git for versioning and remote syncing.}
|
11
|
+
s.description = <<-DESC.strip.gsub(/\n\s+/, " ")
|
12
|
+
GitModel persists Ruby objects using Git as a data storage engine. It's an
|
13
|
+
ActiveModel implementation so it works stand-alone or in Rails 3 as a drop-in
|
14
|
+
replacement for ActiveRecord or DataMapper. Because the database is a Git
|
15
|
+
repository it can be synced across multiple machines, manipulated with standard
|
16
|
+
Git client tools, can be branched and merged, and of course keeps the history
|
17
|
+
of all changes.
|
18
|
+
DESC
|
19
|
+
|
20
|
+
s.add_dependency 'activemodel', '>= 3.0.1'
|
21
|
+
s.add_dependency 'activesupport', '>= 3.0.1'
|
22
|
+
s.add_dependency 'grit', '>= 2.3.0'
|
23
|
+
s.add_dependency 'lockfile', '>= 1.4.3'
|
24
|
+
|
25
|
+
s.add_development_dependency 'ZenTest', '>= 4.4.0'
|
26
|
+
s.add_development_dependency 'autotest', '>= 4.4.1'
|
27
|
+
s.add_development_dependency 'autotest-fsevent', '>= 0.2.3' if RUBY_PLATFORM.downcase.include?("darwin") # OS X only
|
28
|
+
s.add_development_dependency 'rspec', '>= 2.0.1'
|
29
|
+
|
30
|
+
s.files = `git ls-files`.split("\n")
|
31
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
32
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
33
|
+
s.require_paths = ["lib"]
|
34
|
+
|
35
|
+
end
|
36
|
+
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module GitModel
|
2
|
+
|
3
|
+
# Generic GitModel exception class.
|
4
|
+
class GitModelError < StandardError
|
5
|
+
end
|
6
|
+
|
7
|
+
# Raised when GitModel cannot find record by given id or set of ids.
|
8
|
+
class RecordNotFound < GitModelError
|
9
|
+
end
|
10
|
+
|
11
|
+
# Raised by GitModel::Persistable.save! and GitModel::Persistable.create! methods when record cannot be
|
12
|
+
# saved because record is invalid.
|
13
|
+
class RecordNotSaved < GitModelError
|
14
|
+
end
|
15
|
+
|
16
|
+
class RecordExists < GitModelError
|
17
|
+
end
|
18
|
+
|
19
|
+
class RecordDoesntExist < GitModelError
|
20
|
+
end
|
21
|
+
|
22
|
+
class NullId < GitModelError
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
@@ -0,0 +1,269 @@
|
|
1
|
+
module GitModel
|
2
|
+
module Persistable
|
3
|
+
|
4
|
+
def self.included(base)
|
5
|
+
base.class_eval do
|
6
|
+
|
7
|
+
extend ActiveModel::Callbacks
|
8
|
+
extend ActiveModel::Naming
|
9
|
+
include ActiveModel::Validations
|
10
|
+
include ActiveModel::Dirty
|
11
|
+
include ActiveModel::Observing
|
12
|
+
include ActiveModel::Translation
|
13
|
+
|
14
|
+
define_model_callbacks :initialize, :find, :touch, :only => :after
|
15
|
+
define_model_callbacks :save, :create, :update, :destroy
|
16
|
+
end
|
17
|
+
|
18
|
+
base.extend(ClassMethods)
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
def initialize(args = {})
|
23
|
+
_run_initialize_callbacks do
|
24
|
+
@new_record = true
|
25
|
+
self.attributes = {}
|
26
|
+
self.blobs = {}
|
27
|
+
args.each do |k,v|
|
28
|
+
self.send("#{k}=".to_sym, v)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_model
|
34
|
+
self
|
35
|
+
end
|
36
|
+
|
37
|
+
def to_key
|
38
|
+
id ? [id] : nil
|
39
|
+
end
|
40
|
+
|
41
|
+
def to_param
|
42
|
+
id && id.to_s
|
43
|
+
end
|
44
|
+
|
45
|
+
def id
|
46
|
+
@id
|
47
|
+
end
|
48
|
+
|
49
|
+
def id=(string)
|
50
|
+
# TODO ensure is valid as a filename
|
51
|
+
@id = string
|
52
|
+
end
|
53
|
+
|
54
|
+
def attributes
|
55
|
+
@attributes
|
56
|
+
end
|
57
|
+
|
58
|
+
def attributes=(new_attributes, guard_protected_attributes = true)
|
59
|
+
@attributes = HashWithIndifferentAccess.new
|
60
|
+
if new_attributes
|
61
|
+
new_attributes.each {|k,v| @attributes[k] = v}
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def blobs
|
66
|
+
@blobs
|
67
|
+
end
|
68
|
+
|
69
|
+
def blobs=(new_blobs)
|
70
|
+
@blobs = HashWithIndifferentAccess.new
|
71
|
+
if new_blobs
|
72
|
+
new_blobs.each {|k,v| @blobs[k] = v}
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def new_record?
|
77
|
+
@new_record || false
|
78
|
+
end
|
79
|
+
|
80
|
+
# Valid options are:
|
81
|
+
# :transaction
|
82
|
+
# OR:
|
83
|
+
# :branch
|
84
|
+
# :commit_message
|
85
|
+
# Returns false if validations failed, otherwise returns the SHA of the commit
|
86
|
+
def save(options = {})
|
87
|
+
raise GitModel::NullId unless self.id
|
88
|
+
|
89
|
+
if new_record?
|
90
|
+
raise GitModel::RecordExists if self.class.exists?(self.id)
|
91
|
+
else
|
92
|
+
raise GitModel::RecordDoesntExist unless self.class.exists?(self.id)
|
93
|
+
end
|
94
|
+
|
95
|
+
dir = File.join(self.class.db_subdir, self.id)
|
96
|
+
|
97
|
+
transaction = options.delete(:transaction) || GitModel::Transaction.new(options)
|
98
|
+
result = transaction.execute do |t|
|
99
|
+
# Write the attributes to the attributes file
|
100
|
+
t.index.add(File.join(dir, 'attributes.json'), JSON.pretty_generate(attributes))
|
101
|
+
|
102
|
+
# Write the blob files
|
103
|
+
blobs.each do |name, data|
|
104
|
+
t.index.add(File.join(dir, name), data)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
return result
|
109
|
+
end
|
110
|
+
|
111
|
+
# Same as #save but raises an exception on error
|
112
|
+
def save!(options = {})
|
113
|
+
save(options) || raise(GitModel::RecordNotSaved)
|
114
|
+
end
|
115
|
+
|
116
|
+
def delete(options = {})
|
117
|
+
freeze
|
118
|
+
self.class.delete(id, options)
|
119
|
+
end
|
120
|
+
|
121
|
+
def to_s
|
122
|
+
"#<#{self.class.name}:#{__id__} id=#{id}, attributes=#{attributes.inspect}, blobs.keys=#{blobs.keys.inspect}>"
|
123
|
+
end
|
124
|
+
|
125
|
+
|
126
|
+
private
|
127
|
+
|
128
|
+
def load(dir)
|
129
|
+
_run_find_callbacks do
|
130
|
+
# remove dangerous ".."
|
131
|
+
# todo find a better way to ensure path is safe
|
132
|
+
dir.gsub!(/\.\./, '')
|
133
|
+
|
134
|
+
raise GitModel::RecordNotFound if GitModel.current_tree.nil?
|
135
|
+
|
136
|
+
self.id = File.basename(dir)
|
137
|
+
@new_record = false
|
138
|
+
|
139
|
+
# load the attributes
|
140
|
+
object = GitModel.current_tree / File.join(dir, 'attributes.json')
|
141
|
+
raise GitModel::RecordNotFound if object.nil?
|
142
|
+
|
143
|
+
self.attributes = JSON.parse(object.data, :max_nesting => false)
|
144
|
+
|
145
|
+
# load all other non-hidden files in the dir as blobs
|
146
|
+
blobs = (GitModel.current_tree / dir).blobs.reject{|b| b.name[0] == '.' || b.name == 'attributes.json'}
|
147
|
+
blobs.each do |b|
|
148
|
+
self.blobs[b.name] = b.data
|
149
|
+
end
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
|
154
|
+
module ClassMethods
|
155
|
+
|
156
|
+
def db_subdir
|
157
|
+
self.to_s.tableize
|
158
|
+
end
|
159
|
+
|
160
|
+
def attribute(name, options = {})
|
161
|
+
default = options[:default]
|
162
|
+
self.class_eval <<-EOF
|
163
|
+
def #{name}; attributes[:#{name}] || #{default.inspect}; end
|
164
|
+
def #{name}=(value); attributes[:#{name}] = value; end
|
165
|
+
EOF
|
166
|
+
end
|
167
|
+
|
168
|
+
def blob(name, options = {})
|
169
|
+
self.class_eval <<-EOF
|
170
|
+
def #{name}; blobs[:#{name}]; end
|
171
|
+
def #{name}=(value); blobs[:#{name}] = value; end
|
172
|
+
EOF
|
173
|
+
end
|
174
|
+
|
175
|
+
def find(id)
|
176
|
+
GitModel.logger.debug "Finding #{name} with id: #{id}"
|
177
|
+
o = new
|
178
|
+
dir = File.join(db_subdir, id)
|
179
|
+
o.send :load, dir
|
180
|
+
return o
|
181
|
+
end
|
182
|
+
|
183
|
+
def exists?(id)
|
184
|
+
GitModel.repo.commits.any? && !(GitModel.current_tree / File.join(db_subdir, id, 'attributes.json')).nil?
|
185
|
+
end
|
186
|
+
|
187
|
+
def find_all(conditions = {})
|
188
|
+
GitModel.logger.debug "Finding all #{name.pluralize} with conditions: #{conditions.inspect}"
|
189
|
+
results = []
|
190
|
+
dirs = (GitModel.current_tree / db_subdir).trees
|
191
|
+
dirs.each do |dir|
|
192
|
+
if dir.blobs.any?
|
193
|
+
o = new
|
194
|
+
o.send :load, File.join(db_subdir, dir.name)
|
195
|
+
results << o
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
return results
|
200
|
+
end
|
201
|
+
|
202
|
+
def create(args)
|
203
|
+
if args.is_a?(Array)
|
204
|
+
args.map{|arg| create(arg)}
|
205
|
+
else
|
206
|
+
o = self.new(args)
|
207
|
+
o.save
|
208
|
+
end
|
209
|
+
return o
|
210
|
+
end
|
211
|
+
|
212
|
+
def create!(args)
|
213
|
+
if args.is_a?(Array)
|
214
|
+
args.map{|arg| create!(arg)}
|
215
|
+
else
|
216
|
+
o = self.new(args)
|
217
|
+
o.save!
|
218
|
+
end
|
219
|
+
return o
|
220
|
+
end
|
221
|
+
|
222
|
+
def delete(id, options = {})
|
223
|
+
path = File.join(db_subdir, id)
|
224
|
+
transaction = options.delete(:transaction) || GitModel::Transaction.new(options)
|
225
|
+
result = transaction.execute do |t|
|
226
|
+
delete_tree(path, t.index, options)
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
def delete_all(options = {})
|
231
|
+
transaction = options.delete(:transaction) || GitModel::Transaction.new(options)
|
232
|
+
result = transaction.execute do |t|
|
233
|
+
delete_tree(db_subdir, t.index, options)
|
234
|
+
end
|
235
|
+
end
|
236
|
+
|
237
|
+
|
238
|
+
private
|
239
|
+
|
240
|
+
def delete_tree(path, index, options = {})
|
241
|
+
# This leaves a bunch of empty sub-trees, there must be a way to just
|
242
|
+
# replace the tree to be deleted with an empty tree that doesn't even
|
243
|
+
# reference the sub-trees.
|
244
|
+
current = index.tree
|
245
|
+
path.split('/').each do |dir|
|
246
|
+
current[dir] ||= {}
|
247
|
+
current = current[dir]
|
248
|
+
end
|
249
|
+
|
250
|
+
build_tree_hash(current, (index.current_tree / path))
|
251
|
+
end
|
252
|
+
|
253
|
+
# recusively build the hash representing the objects that grit will commit
|
254
|
+
def build_tree_hash(hash, tree)
|
255
|
+
tree.blobs.each do |b|
|
256
|
+
hash[b.name] = false
|
257
|
+
end
|
258
|
+
tree.trees.each do |t|
|
259
|
+
hash[t.name] = {}
|
260
|
+
build_tree_hash(hash[t.name], t)
|
261
|
+
end
|
262
|
+
return hash
|
263
|
+
end
|
264
|
+
|
265
|
+
end # module ClassMethods
|
266
|
+
|
267
|
+
end # module Persistable
|
268
|
+
end # module GitModel
|
269
|
+
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module GitModel
|
2
|
+
class Transaction
|
3
|
+
|
4
|
+
attr_accessor :index
|
5
|
+
attr_accessor :branch
|
6
|
+
attr_accessor :commit_message
|
7
|
+
|
8
|
+
def initialize(options = {})
|
9
|
+
self.branch = options[:branch] || GitModel.default_branch
|
10
|
+
self.commit_message = options[:commit_message]
|
11
|
+
end
|
12
|
+
|
13
|
+
def execute(&block)
|
14
|
+
if index
|
15
|
+
# We're already in a transaction
|
16
|
+
yield self
|
17
|
+
else
|
18
|
+
# For now there's a big ugly lock here, this will be fixed!
|
19
|
+
# TODO move this lock around the commit only (need to make sure two
|
20
|
+
# processes aren't updating refs/heads/<branch> at the same time) and
|
21
|
+
# make concurrent transactions can work. This will require some merging
|
22
|
+
# magic!
|
23
|
+
lock do
|
24
|
+
# We're not in a transaction, start a new one
|
25
|
+
GitModel.logger.debug "Beginning transaction on #{branch}..."
|
26
|
+
|
27
|
+
# Save the current head so that concurrent transactions can work. We need
|
28
|
+
# to make sure the parent of this commit is the same SHA that this
|
29
|
+
# index's tree is based on.
|
30
|
+
parent = GitModel.last_commit(branch)
|
31
|
+
|
32
|
+
self.index = Grit::Index.new(GitModel.repo)
|
33
|
+
index.read_tree(parent.to_s)
|
34
|
+
|
35
|
+
yield self
|
36
|
+
|
37
|
+
committer = Grit::Actor.new(GitModel.git_user_name, GitModel.git_user_email)
|
38
|
+
sha = index.commit(commit_message, parent ? [parent] : nil, committer, nil, branch)
|
39
|
+
# TODO return false and log if anything went wrong with the commit
|
40
|
+
|
41
|
+
GitModel.logger.debug "Finished transaction on #{branch}."
|
42
|
+
|
43
|
+
return sha
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Wait until we can get an exclusive lock on the branch, then execute the
|
49
|
+
# block. We lock the branch by creating refs/heads/<branch>.lock, which
|
50
|
+
# the git commands also seem to respect
|
51
|
+
def lock(&block)
|
52
|
+
lockfile = Lockfile.new File.join(GitModel.repo.path, 'refs/heads', branch + '.lock')
|
53
|
+
begin
|
54
|
+
lockfile.lock
|
55
|
+
yield
|
56
|
+
ensure
|
57
|
+
lockfile.unlock
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|