gitmodel 0.0.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/.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
|