gitmodel 0.0.4 → 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- data/.rvmrc +2 -0
- data/Gemfile.lock +3 -1
- data/README.md +19 -4
- data/gitmodel.gemspec +2 -1
- data/lib/gitmodel.rb +58 -16
- data/lib/gitmodel/index.rb +33 -28
- data/lib/gitmodel/persistable.rb +121 -80
- data/spec/gitmodel/index_spec.rb +2 -2
- data/spec/gitmodel/persistable_spec.rb +18 -17
- data/spec/gitmodel_spec.rb +4 -4
- data/spec/spec_helper.rb +1 -0
- metadata +94 -85
data/.rvmrc
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,11 +1,12 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
gitmodel (0.0.
|
4
|
+
gitmodel (0.0.5)
|
5
5
|
activemodel (>= 3.0.1)
|
6
6
|
activesupport (>= 3.0.1)
|
7
7
|
grit (>= 2.3.0)
|
8
8
|
lockfile (>= 1.4.3)
|
9
|
+
memcache-client (>= 1.8.5)
|
9
10
|
yajl-ruby (>= 0.8.2)
|
10
11
|
|
11
12
|
GEM
|
@@ -26,6 +27,7 @@ GEM
|
|
26
27
|
mime-types (~> 1.15)
|
27
28
|
i18n (0.5.0)
|
28
29
|
lockfile (1.4.3)
|
30
|
+
memcache-client (1.8.5)
|
29
31
|
mime-types (1.16)
|
30
32
|
rspec (2.6.0)
|
31
33
|
rspec-core (~> 2.6.0)
|
data/README.md
CHANGED
@@ -151,6 +151,25 @@ structure that looks like this:
|
|
151
151
|
* _attributes.json_
|
152
152
|
|
153
153
|
|
154
|
+
Performance
|
155
|
+
-----------
|
156
|
+
|
157
|
+
GitModel supports memcached for query results. This is off by default, but can be configured like this:
|
158
|
+
|
159
|
+
GitModel.memcache_servers(['server_1', 'server_2', ...])
|
160
|
+
GitModel.memcache_namespace('optional_namespace')
|
161
|
+
|
162
|
+
The namespace is optional, and usually not necessary because GitModel will prepend the last segment of GitModel.db_root anyway.
|
163
|
+
|
164
|
+
A Git SHA is also prepended to every key, so that outdated versions will not be retrieved from the cache. This is the SHA of the latest commit so unfortunately this is only useful when there are not frequent commits because every commit invalidates the cache. (This is obviously not ideal and I'm sure it can be improved upon.)
|
165
|
+
|
166
|
+
There is still a lot of work to be done to make it faster. First, some analysis is required, but some guesses about things that would help are:
|
167
|
+
|
168
|
+
* Use [Rugged](https://github.com/libgit2/rugged) instead of Grit
|
169
|
+
* Remove the transaction lock (see transaction.rb line 19)
|
170
|
+
* Ability to iterate over result set without eager loading of all instances
|
171
|
+
|
172
|
+
|
154
173
|
Contributing
|
155
174
|
------------
|
156
175
|
|
@@ -179,10 +198,6 @@ To do
|
|
179
198
|
* Generators
|
180
199
|
* Rake tasks
|
181
200
|
* Performance
|
182
|
-
* Haven't optimized for performance yet.
|
183
|
-
* Use [Rugged](https://github.com/libgit2/rugged) instead of Grit
|
184
|
-
* Remove the transaction lock (see transaction.rb line 19)
|
185
|
-
* Ability to iterate over result set without eager loading of all instances
|
186
201
|
* Persistable.find/find_all/etc could be based on staged files so that queries reflect uncommitted changes
|
187
202
|
* Better query support
|
188
203
|
* Associations
|
data/gitmodel.gemspec
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = 'gitmodel'
|
3
|
-
s.version = '0.0.
|
3
|
+
s.version = '0.0.5'
|
4
4
|
s.platform = Gem::Platform::RUBY
|
5
5
|
|
6
6
|
s.authors = ["Paul Dowman"]
|
@@ -21,6 +21,7 @@ Gem::Specification.new do |s|
|
|
21
21
|
s.add_dependency 'activesupport', '>= 3.0.1'
|
22
22
|
s.add_dependency 'grit', '>= 2.3.0'
|
23
23
|
s.add_dependency 'lockfile', '>= 1.4.3'
|
24
|
+
s.add_dependency 'memcache-client', '>= 1.8.5'
|
24
25
|
s.add_dependency 'yajl-ruby', '>= 0.8.2'
|
25
26
|
|
26
27
|
s.add_development_dependency 'ZenTest', '>= 4.4.0'
|
data/lib/gitmodel.rb
CHANGED
@@ -1,12 +1,15 @@
|
|
1
|
+
# coding: UTF-8
|
2
|
+
|
1
3
|
require 'rubygems'
|
2
4
|
require 'bundler/setup'
|
3
5
|
|
4
6
|
require 'active_model'
|
5
7
|
require 'active_support/all' # TODO we don't really want all here, clean this up
|
6
8
|
require 'grit'
|
7
|
-
require 'yajl'
|
8
9
|
require 'lockfile'
|
10
|
+
require 'memcache'
|
9
11
|
require 'pp'
|
12
|
+
require 'yajl'
|
10
13
|
|
11
14
|
$:.unshift(File.dirname(__FILE__))
|
12
15
|
require 'gitmodel/errors'
|
@@ -32,6 +35,9 @@ module GitModel
|
|
32
35
|
mattr_accessor :git_user_name
|
33
36
|
mattr_accessor :git_user_email
|
34
37
|
|
38
|
+
mattr_accessor :memcache_servers
|
39
|
+
mattr_accessor :memcache_namespace
|
40
|
+
|
35
41
|
def self.repo
|
36
42
|
@@repo = Grit::Repo.new(GitModel.db_root)
|
37
43
|
end
|
@@ -56,28 +62,64 @@ module GitModel
|
|
56
62
|
create_db!
|
57
63
|
end
|
58
64
|
|
59
|
-
def self.last_commit(branch
|
60
|
-
branch
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
65
|
+
def self.last_commit(branch)
|
66
|
+
cache(branch, 'last-commit') do
|
67
|
+
unless repo.commits(branch).any?
|
68
|
+
nil
|
69
|
+
else
|
70
|
+
# We should be able to use just repo.commits(branch).first here but
|
71
|
+
# this is a workaround for this bug:
|
72
|
+
# http://github.com/mojombo/grit/issues/issue/38
|
73
|
+
GitModel.repo.commits("#{branch}^..#{branch}").first || GitModel.repo.commits(branch).first
|
74
|
+
end
|
75
|
+
end
|
70
76
|
end
|
71
77
|
|
72
|
-
def self.current_tree(branch
|
78
|
+
def self.current_tree(branch)
|
73
79
|
c = last_commit(branch)
|
74
80
|
c ? c.tree : nil
|
75
81
|
end
|
76
82
|
|
77
|
-
def self.index!
|
78
|
-
dirs = (GitModel.current_tree).trees
|
83
|
+
def self.index!(branch)
|
84
|
+
dirs = (GitModel.current_tree(branch)).trees
|
79
85
|
dirs.each do |dir|
|
80
|
-
dir.name.classify.constantize.index!
|
86
|
+
dir.name.classify.constantize.index!(branch)
|
81
87
|
end
|
82
88
|
end
|
89
|
+
|
90
|
+
# If we're using memcached (i.e. the memcache_servers setting is not nil) and
|
91
|
+
# the key exists in memcached, it's value will be returned and the block will
|
92
|
+
# not be run. If key does not exist in memcached, block will be executed,
|
93
|
+
# it's value stored in memcached under key, and value will be returned.
|
94
|
+
#
|
95
|
+
# There's no need to sweep the cache because the SHA of the latest Git commit
|
96
|
+
# is appended to the key, so any database change invalidates all cached
|
97
|
+
# objects.
|
98
|
+
def self.cache(branch, key, &block)
|
99
|
+
key = "#{key}-#{head_sha(branch)}"
|
100
|
+
value = nil
|
101
|
+
if memcache_servers
|
102
|
+
@@memcache ||= MemCache.new memcache_servers, :namespace => "#{File.basename(db_root)}-#{memcache_namespace}"
|
103
|
+
value = @@memcache.get(key)
|
104
|
+
if value.nil?
|
105
|
+
logger.info("✗ memcache MISS for key #{key}")
|
106
|
+
value = yield
|
107
|
+
@@memcache.set(key, value)
|
108
|
+
else
|
109
|
+
logger.info("✔ memcache HIT for key #{key}")
|
110
|
+
end
|
111
|
+
else
|
112
|
+
logger.debug("No memcache servers defined, not checking cache for key #{key}")
|
113
|
+
value = yield
|
114
|
+
end
|
115
|
+
value
|
116
|
+
end
|
117
|
+
|
118
|
+
private
|
119
|
+
|
120
|
+
# A more efficient way to get the SHA of the HEAD of the given branch
|
121
|
+
def self.head_sha(branch_name)
|
122
|
+
ref = File.join(repo.git.git_dir, "refs/heads/#{branch_name}")
|
123
|
+
File.exist?(ref) ? File.read(ref).chomp : nil
|
124
|
+
end
|
83
125
|
end
|
data/lib/gitmodel/index.rb
CHANGED
@@ -4,11 +4,11 @@ module GitModel
|
|
4
4
|
@model_class = model_class
|
5
5
|
end
|
6
6
|
|
7
|
-
def generate!
|
7
|
+
def generate!(branch)
|
8
8
|
GitModel.logger.debug "Generating indexes for #{@model_class}"
|
9
9
|
# TODO it sucks to load every instance here, optimize later
|
10
10
|
@indexes = {}
|
11
|
-
@model_class.find_all.each do |o|
|
11
|
+
@model_class.find_all(:branch => branch).each do |o|
|
12
12
|
o.attributes.each do |attr, value|
|
13
13
|
@indexes[attr] ||= {}
|
14
14
|
@indexes[attr][value] ||= SortedSet.new
|
@@ -19,24 +19,27 @@ module GitModel
|
|
19
19
|
|
20
20
|
def attr_index(attr)
|
21
21
|
self.load unless @indexes
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
22
|
+
unless @indexes # this is just so that we can stub self.load in tests
|
23
|
+
nil
|
24
|
+
else
|
25
|
+
ret = @indexes[attr.to_s]
|
26
|
+
raise GitModel::AttributeNotIndexed.new(attr.to_s) unless ret
|
27
|
+
ret
|
28
|
+
end
|
27
29
|
end
|
28
30
|
|
29
31
|
def filename
|
30
32
|
File.join(@model_class.db_subdir, '_indexes.json')
|
31
33
|
end
|
32
34
|
|
33
|
-
def generated?
|
34
|
-
(GitModel.current_tree / filename) ? true : false
|
35
|
+
def generated?(branch = GitModel.default_branch)
|
36
|
+
(GitModel.current_tree(branch) / filename) ? true : false
|
35
37
|
end
|
36
38
|
|
37
39
|
def save(options = {})
|
38
40
|
GitModel.logger.debug "Saving indexes for #{@model_class}..."
|
39
41
|
transaction = options.delete(:transaction) || GitModel::Transaction.new(options)
|
42
|
+
branch = transaction.branch || options.delete(:branch) || GitModel.default_branch
|
40
43
|
result = transaction.execute do |t|
|
41
44
|
# convert to array because JSON hash keys must be strings
|
42
45
|
data = []
|
@@ -52,26 +55,28 @@ module GitModel
|
|
52
55
|
end
|
53
56
|
end
|
54
57
|
|
55
|
-
def load
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
58
|
+
def load(branch = GitModel.default_branch)
|
59
|
+
@indexes = GitModel.cache(branch, "#{@model_class.db_subdir}-index-load") do
|
60
|
+
unless generated?(branch)
|
61
|
+
GitModel.logger.debug "No index generated for #{@model_class}, on branch #{branch}, not loading."
|
62
|
+
else
|
63
|
+
GitModel.logger.debug "Loading indexes for #{@model_class}..."
|
64
|
+
indexes = {}
|
65
|
+
blob = GitModel.current_tree(branch) / filename
|
66
|
+
|
67
|
+
data = Yajl::Parser.parse(blob.data)
|
68
|
+
data.each do |attr_and_values|
|
69
|
+
attr = attr_and_values[0]
|
70
|
+
values = {}
|
71
|
+
attr_and_values[1].each do |value_and_ids|
|
72
|
+
value = value_and_ids[0]
|
73
|
+
ids = SortedSet.new(value_and_ids[1])
|
74
|
+
values[value] = ids
|
75
|
+
end
|
76
|
+
indexes[attr] = values
|
77
|
+
end
|
73
78
|
end
|
74
|
-
|
79
|
+
indexes
|
75
80
|
end
|
76
81
|
end
|
77
82
|
|
data/lib/gitmodel/persistable.rb
CHANGED
@@ -132,13 +132,13 @@ module GitModel
|
|
132
132
|
|
133
133
|
private
|
134
134
|
|
135
|
-
def load(dir)
|
135
|
+
def load(dir, branch)
|
136
136
|
_run_find_callbacks do
|
137
137
|
# remove dangerous ".."
|
138
138
|
# todo find a better way to ensure path is safe
|
139
139
|
dir.gsub!(/\.\./, '')
|
140
140
|
|
141
|
-
raise GitModel::RecordNotFound if GitModel.current_tree.nil?
|
141
|
+
raise GitModel::RecordNotFound if GitModel.current_tree(branch).nil?
|
142
142
|
|
143
143
|
self.id = File.basename(dir)
|
144
144
|
@new_record = false
|
@@ -146,13 +146,13 @@ module GitModel
|
|
146
146
|
GitModel.logger.debug "Loading #{self.class.name} with id: #{id}"
|
147
147
|
|
148
148
|
# load the attributes
|
149
|
-
object = GitModel.current_tree / File.join(dir, 'attributes.json')
|
149
|
+
object = GitModel.current_tree(branch) / File.join(dir, 'attributes.json')
|
150
150
|
raise GitModel::RecordNotFound if object.nil?
|
151
151
|
|
152
152
|
self.attributes = Yajl::Parser.parse(object.data)
|
153
153
|
|
154
154
|
# load all other non-hidden files in the dir as blobs
|
155
|
-
blobs = (GitModel.current_tree / dir).blobs.reject{|b| b.name[0] == '.' || b.name == 'attributes.json'}
|
155
|
+
blobs = (GitModel.current_tree(branch) / dir).blobs.reject{|b| b.name[0] == '.' || b.name == 'attributes.json'}
|
156
156
|
blobs.each do |b|
|
157
157
|
self.blobs[b.name] = b.data
|
158
158
|
end
|
@@ -181,95 +181,115 @@ module GitModel
|
|
181
181
|
EOF
|
182
182
|
end
|
183
183
|
|
184
|
-
def find(id)
|
184
|
+
def find(id, branch = GitModel.default_branch)
|
185
185
|
GitModel.logger.debug "Finding #{name} with id: #{id}"
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
186
|
+
result = GitModel.cache(branch, "#{db_subdir}-find-#{id}") do
|
187
|
+
o = new
|
188
|
+
dir = File.join(db_subdir, id)
|
189
|
+
o.send :load, dir, branch
|
190
|
+
o
|
191
|
+
end
|
192
|
+
return result
|
190
193
|
end
|
191
194
|
|
192
|
-
def exists?(id)
|
195
|
+
def exists?(id, branch = GitModel.default_branch)
|
193
196
|
GitModel.logger.debug "Checking existence of #{name} with id: #{id}"
|
194
|
-
|
197
|
+
result = GitModel.cache(branch, "#{db_subdir}-exists-#{id}") do
|
198
|
+
GitModel.repo.commits.any? && !(GitModel.current_tree(branch) / File.join(db_subdir, id, 'attributes.json')).nil?
|
199
|
+
end
|
200
|
+
return result
|
195
201
|
end
|
196
202
|
|
203
|
+
# TODO document conditions
|
204
|
+
# :branch
|
205
|
+
# :cache_key
|
206
|
+
# :order_by
|
207
|
+
# :order
|
208
|
+
# any model attribute
|
197
209
|
def find_all(conditions = {})
|
210
|
+
branch = conditions.delete(:branch) || GitModel.default_branch
|
198
211
|
# TODO Refactor this spaghetti
|
199
212
|
GitModel.logger.debug "Finding all #{name.pluralize} with conditions: #{conditions.inspect}"
|
200
|
-
|
213
|
+
cache_key = "#{db_subdir}-find_all-#{format_conditions_hash_for_cache_key(conditions)}"
|
214
|
+
cached_results = GitModel.cache(branch, cache_key) do
|
215
|
+
current_tree = GitModel.current_tree(branch)
|
216
|
+
unless current_tree
|
217
|
+
[]
|
218
|
+
else
|
219
|
+
order = conditions.delete(:order) || :asc
|
220
|
+
order_by = conditions.delete(:order_by) || :id
|
221
|
+
limit = conditions.delete(:limit)
|
222
|
+
|
223
|
+
matching_ids = []
|
224
|
+
if conditions.empty? # load all objects
|
225
|
+
trees = (current_tree / db_subdir).trees
|
226
|
+
trees.each do |t|
|
227
|
+
matching_ids << t.name if t.blobs.any?
|
228
|
+
end
|
229
|
+
else # only load objects that match conditions
|
230
|
+
matching_ids_for_condition = {}
|
231
|
+
conditions.each do |k,v|
|
232
|
+
matching_ids_for_condition[k] = []
|
233
|
+
if k == :id # id isn't indexed
|
234
|
+
if v.is_a?(Proc)
|
235
|
+
trees = (current_tree / db_subdir).trees
|
236
|
+
trees.each do |t|
|
237
|
+
matching_ids_for_condition[k] << t.name if t.blobs.any? && v.call(t.name)
|
238
|
+
end
|
239
|
+
else
|
240
|
+
# an unlikely use case but supporting it for completeness
|
241
|
+
matching_ids_for_condition[k] << v if (current_tree / db_subdir / v)
|
242
|
+
end
|
243
|
+
else
|
244
|
+
raise GitModel::IndexRequired unless index.generated?
|
245
|
+
attr_index = index.attr_index(k)
|
246
|
+
if v.is_a?(Proc)
|
247
|
+
attr_index.each do |value, ids|
|
248
|
+
matching_ids_for_condition[k] += ids.to_a if v.call(value)
|
249
|
+
end
|
250
|
+
else
|
251
|
+
matching_ids_for_condition[k] += attr_index[v].to_a
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
255
|
+
matching_ids += matching_ids_for_condition.values.inject{|memo, obj| memo & obj}
|
256
|
+
end
|
201
257
|
|
202
|
-
|
203
|
-
|
204
|
-
|
258
|
+
results = nil
|
259
|
+
if order_by != :id
|
260
|
+
GitModel.logger.warn "Ordering by an attribute other than id requires loading all matching objects before applying limit, this will be slow" if limit
|
261
|
+
results = matching_ids.map{|k| find(k)}
|
205
262
|
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
matching_ids << t.name if t.blobs.any?
|
211
|
-
end
|
212
|
-
else # only load objects that match conditions
|
213
|
-
matching_ids_for_condition = {}
|
214
|
-
conditions.each do |k,v|
|
215
|
-
matching_ids_for_condition[k] = []
|
216
|
-
if k == :id # id isn't indexed
|
217
|
-
if v.is_a?(Proc)
|
218
|
-
trees = (GitModel.current_tree / db_subdir).trees
|
219
|
-
trees.each do |t|
|
220
|
-
matching_ids_for_condition[k] << t.name if t.blobs.any? && v.call(t.name)
|
221
|
-
end
|
263
|
+
if order == :asc
|
264
|
+
results = results.sort{|a,b| a.send(order_by) <=> b.send(order_by)}
|
265
|
+
elsif order == :desc
|
266
|
+
results = results.sort{|b,a| a.send(order_by) <=> b.send(order_by)}
|
222
267
|
else
|
223
|
-
|
224
|
-
|
268
|
+
raise GitModel::InvalidParams("invalid order: '#{order}'")
|
269
|
+
end
|
270
|
+
|
271
|
+
if limit
|
272
|
+
results = results[0, limit]
|
225
273
|
end
|
226
274
|
else
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
matching_ids_for_condition[k] += ids.to_a if v.call(value)
|
232
|
-
end
|
275
|
+
if order == :asc
|
276
|
+
matching_ids = matching_ids.sort{|a,b| a <=> b}
|
277
|
+
elsif order == :desc
|
278
|
+
matching_ids = matching_ids.sort{|b,a| a <=> b}
|
233
279
|
else
|
234
|
-
|
280
|
+
raise GitModel::InvalidParams("invalid order: '#{order}'")
|
235
281
|
end
|
236
|
-
|
237
|
-
end
|
238
|
-
matching_ids += matching_ids_for_condition.values.inject{|memo, obj| memo & obj}
|
239
|
-
end
|
240
|
-
|
241
|
-
results = nil
|
242
|
-
if order_by != :id
|
243
|
-
GitModel.logger.warn "Ordering by an attribute other than id requires loading all matching objects before applying limit, this will be slow" if limit
|
244
|
-
results = matching_ids.map{|k| find(k)}
|
282
|
+
if limit
|
245
283
|
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
else
|
251
|
-
raise GitModel::InvalidParams("invalid order: '#{order}'")
|
252
|
-
end
|
253
|
-
|
254
|
-
if limit
|
255
|
-
results = results[0, limit]
|
256
|
-
end
|
257
|
-
else
|
258
|
-
if order == :asc
|
259
|
-
matching_ids = matching_ids.sort{|a,b| a <=> b}
|
260
|
-
elsif order == :desc
|
261
|
-
matching_ids = matching_ids.sort{|b,a| a <=> b}
|
262
|
-
else
|
263
|
-
raise GitModel::InvalidParams("invalid order: '#{order}'")
|
264
|
-
end
|
265
|
-
if limit
|
284
|
+
matching_ids = matching_ids[0, limit]
|
285
|
+
end
|
286
|
+
results = matching_ids.map{|k| find(k)}
|
287
|
+
end
|
266
288
|
|
267
|
-
|
289
|
+
results
|
268
290
|
end
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
return results
|
291
|
+
end # cached block
|
292
|
+
return cached_results
|
273
293
|
end
|
274
294
|
|
275
295
|
def all_values_for_attr(attr)
|
@@ -302,7 +322,8 @@ module GitModel
|
|
302
322
|
path = File.join(db_subdir, id)
|
303
323
|
transaction = options.delete(:transaction) || GitModel::Transaction.new(options)
|
304
324
|
result = transaction.execute do |t|
|
305
|
-
|
325
|
+
branch = t.branch || options[:branch] || GitModel.default_branch
|
326
|
+
delete_tree(path, t.index, branch, options)
|
306
327
|
end
|
307
328
|
end
|
308
329
|
|
@@ -310,19 +331,20 @@ module GitModel
|
|
310
331
|
GitModel.logger.debug "Deleting all #{name.pluralize}"
|
311
332
|
transaction = options.delete(:transaction) || GitModel::Transaction.new(options)
|
312
333
|
result = transaction.execute do |t|
|
313
|
-
|
334
|
+
branch = t.branch || options[:branch] || GitModel.default_branch
|
335
|
+
delete_tree(db_subdir, t.index, branch, options)
|
314
336
|
end
|
315
337
|
end
|
316
338
|
|
317
|
-
def index!
|
318
|
-
index.generate!
|
319
|
-
index.save
|
339
|
+
def index!(branch)
|
340
|
+
index.generate!(branch)
|
341
|
+
index.save(:branch => branch)
|
320
342
|
end
|
321
343
|
|
322
344
|
|
323
345
|
private
|
324
346
|
|
325
|
-
def delete_tree(path, index, options = {})
|
347
|
+
def delete_tree(path, index, branch, options = {})
|
326
348
|
# This leaves a bunch of empty sub-trees, there must be a way to just
|
327
349
|
# replace the tree to be deleted with an empty tree that doesn't even
|
328
350
|
# reference the sub-trees.
|
@@ -347,6 +369,25 @@ module GitModel
|
|
347
369
|
return hash
|
348
370
|
end
|
349
371
|
|
372
|
+
def format_conditions_hash_for_cache_key(hash)
|
373
|
+
# allow setting an explicit cache key, mostly because Proc.hash is
|
374
|
+
# usually different even with the same code and same parameters
|
375
|
+
cache_key = hash.delete(:cache_key)
|
376
|
+
|
377
|
+
unless cache_key
|
378
|
+
cache_key = ""
|
379
|
+
hash.inject('') do |s,kv|
|
380
|
+
key = kv[0]
|
381
|
+
val = kv[1]
|
382
|
+
if val.is_a?(Proc)
|
383
|
+
val = "proc-#{val.hash}"
|
384
|
+
end
|
385
|
+
cache_key += "#{key}:#{val};"
|
386
|
+
end
|
387
|
+
end
|
388
|
+
cache_key
|
389
|
+
end
|
390
|
+
|
350
391
|
end # module ClassMethods
|
351
392
|
|
352
393
|
end # module Persistable
|
data/spec/gitmodel/index_spec.rb
CHANGED
@@ -7,7 +7,7 @@ describe GitModel::Index do
|
|
7
7
|
TestEntity.create!(:id => "baz", :attributes => {:x => 2, :y => 2})
|
8
8
|
|
9
9
|
@i = GitModel::Index.new(TestEntity)
|
10
|
-
@i.generate!
|
10
|
+
@i.generate!(GitModel.default_branch)
|
11
11
|
end
|
12
12
|
|
13
13
|
it "has a hash for each attribute of the model" do
|
@@ -23,7 +23,7 @@ describe GitModel::Index do
|
|
23
23
|
it "can regenerate itself" do
|
24
24
|
@i.attr_index(:x).clear
|
25
25
|
@i.attr_index(:x).should be_empty
|
26
|
-
@i.generate!
|
26
|
+
@i.generate!(GitModel.default_branch)
|
27
27
|
@i.attr_index(:x).should == {1 => SortedSet.new(["foo", "bar"]), 2 => SortedSet.new(["baz"])}
|
28
28
|
end
|
29
29
|
|
@@ -202,15 +202,16 @@ describe GitModel::Persistable do
|
|
202
202
|
it 'also deletes blobs associated with the given object' do
|
203
203
|
id = 'Lemuridae'
|
204
204
|
TestEntity.create!(:id => id, :blobs => {:crowned => "Eulemur coronatus", :brown => "Eulemur fulvus"})
|
205
|
-
|
206
|
-
(GitModel.current_tree / File.join(TestEntity.db_subdir, id, '
|
205
|
+
b = GitModel.default_branch
|
206
|
+
(GitModel.current_tree(b) / File.join(TestEntity.db_subdir, id, 'crowned')).data.should_not be_nil
|
207
|
+
(GitModel.current_tree(b) / File.join(TestEntity.db_subdir, id, 'brown')).data.should_not be_nil
|
207
208
|
TestEntity.delete(id)
|
208
209
|
|
209
|
-
(GitModel.current_tree / File.join(TestEntity.db_subdir, id, 'attributes.json')).should be_nil
|
210
|
-
(GitModel.current_tree / File.join(TestEntity.db_subdir, id, 'attributes.json')).should be_nil
|
210
|
+
(GitModel.current_tree(b) / File.join(TestEntity.db_subdir, id, 'attributes.json')).should be_nil
|
211
|
+
(GitModel.current_tree(b) / File.join(TestEntity.db_subdir, id, 'attributes.json')).should be_nil
|
211
212
|
|
212
|
-
(GitModel.current_tree / File.join(TestEntity.db_subdir, id, 'crowned')).should be_nil
|
213
|
-
(GitModel.current_tree / File.join(TestEntity.db_subdir, id, 'brown')).should be_nil
|
213
|
+
(GitModel.current_tree(b) / File.join(TestEntity.db_subdir, id, 'crowned')).should be_nil
|
214
|
+
(GitModel.current_tree(b) / File.join(TestEntity.db_subdir, id, 'brown')).should be_nil
|
214
215
|
end
|
215
216
|
|
216
217
|
|
@@ -223,7 +224,7 @@ describe GitModel::Persistable do
|
|
223
224
|
TestEntity.create!(:id => 'ape')
|
224
225
|
|
225
226
|
TestEntity.delete_all
|
226
|
-
TestEntity.index!
|
227
|
+
TestEntity.index!(GitModel.default_branch)
|
227
228
|
TestEntity.find_all.should be_empty
|
228
229
|
end
|
229
230
|
|
@@ -347,7 +348,7 @@ describe GitModel::Persistable do
|
|
347
348
|
TestEntity.create!(:id => 'one', :attributes => {:a => 1, :b => 1})
|
348
349
|
TestEntity.create!(:id => 'two', :attributes => {:a => 2, :b => 2})
|
349
350
|
TestEntity.create!(:id => 'three', :attributes => {:a => 1, :b => 3})
|
350
|
-
TestEntity.index!
|
351
|
+
TestEntity.index!(GitModel.default_branch)
|
351
352
|
|
352
353
|
r = TestEntity.find_all(:a => 1)
|
353
354
|
r.size.should == 2
|
@@ -361,7 +362,7 @@ describe GitModel::Persistable do
|
|
361
362
|
TestEntity.create!(:id => 'one', :attributes => {:a => 1, :b => 1})
|
362
363
|
TestEntity.create!(:id => 'two', :attributes => {:a => 2, :b => 2})
|
363
364
|
TestEntity.create!(:id => 'three', :attributes => {:a => 1, :b => 3})
|
364
|
-
TestEntity.index!
|
365
|
+
TestEntity.index!(GitModel.default_branch)
|
365
366
|
|
366
367
|
r = TestEntity.find_all(:b => lambda{|b| b > 1}, :order => :asc)
|
367
368
|
r.size.should == 2
|
@@ -377,7 +378,7 @@ describe GitModel::Persistable do
|
|
377
378
|
TestEntity.create!(:id => 'one', :attributes => {:a => 1, :b => 2})
|
378
379
|
TestEntity.create!(:id => 'two', :attributes => {:a => 1, :b => 2})
|
379
380
|
TestEntity.create!(:id => 'three', :attributes => {:a => 1, :b => 1})
|
380
|
-
TestEntity.index!
|
381
|
+
TestEntity.index!(GitModel.default_branch)
|
381
382
|
|
382
383
|
r = TestEntity.find_all(:a => 1, :b => 2, :order => :asc)
|
383
384
|
r.size.should == 2
|
@@ -392,7 +393,7 @@ describe GitModel::Persistable do
|
|
392
393
|
TestEntity.create!(:id => 'two', :attributes => {:a => 2, :b => 2})
|
393
394
|
TestEntity.create!(:id => 'three', :attributes => {:a => 1, :b => 1})
|
394
395
|
TestEntity.create!(:id => 'four', :attributes => {:a => 3, :b => 3})
|
395
|
-
TestEntity.index!
|
396
|
+
TestEntity.index!(GitModel.default_branch)
|
396
397
|
|
397
398
|
r = TestEntity.find_all(:a => lambda{|a| a > 1}, :b => lambda{|b| b > 2}, :order => :asc)
|
398
399
|
r.size.should == 1
|
@@ -434,7 +435,7 @@ describe GitModel::Persistable do
|
|
434
435
|
TestEntity.create!(:id => 'one', :attributes => {:a => 1, :b => 1})
|
435
436
|
TestEntity.create!(:id => 'two', :attributes => {:a => 2, :b => 2})
|
436
437
|
TestEntity.create!(:id => 'three', :attributes => {:a => 1, :b => 3})
|
437
|
-
TestEntity.index!
|
438
|
+
TestEntity.index!(GitModel.default_branch)
|
438
439
|
|
439
440
|
r = TestEntity.find_all(:a => 1, :order => :asc)
|
440
441
|
r.size.should == 2
|
@@ -446,7 +447,7 @@ describe GitModel::Persistable do
|
|
446
447
|
TestEntity.create!(:id => 'one', :attributes => {:a => 1, :b => 1})
|
447
448
|
TestEntity.create!(:id => 'two', :attributes => {:a => 2, :b => 2})
|
448
449
|
TestEntity.create!(:id => 'three', :attributes => {:a => 1, :b => 3})
|
449
|
-
TestEntity.index!
|
450
|
+
TestEntity.index!(GitModel.default_branch)
|
450
451
|
|
451
452
|
r = TestEntity.find_all(:a => 1, :order => :desc)
|
452
453
|
r.size.should == 2
|
@@ -458,7 +459,7 @@ describe GitModel::Persistable do
|
|
458
459
|
TestEntity.create!(:id => 'one', :attributes => {:a => 1, :b => 1})
|
459
460
|
TestEntity.create!(:id => 'two', :attributes => {:a => 1, :b => 2})
|
460
461
|
TestEntity.create!(:id => 'three', :attributes => {:a => 1, :b => 3})
|
461
|
-
TestEntity.index!
|
462
|
+
TestEntity.index!(GitModel.default_branch)
|
462
463
|
|
463
464
|
r = TestEntity.find_all(:a => 1, :order => :asc, :limit => 2)
|
464
465
|
r.size.should == 2
|
@@ -470,7 +471,7 @@ describe GitModel::Persistable do
|
|
470
471
|
TestEntity.create!(:id => 'one', :attributes => {:a => 1, :b => 1})
|
471
472
|
TestEntity.create!(:id => 'two', :attributes => {:a => 1, :b => 2})
|
472
473
|
TestEntity.create!(:id => 'three', :attributes => {:a => 1, :b => 3})
|
473
|
-
TestEntity.index!
|
474
|
+
TestEntity.index!(GitModel.default_branch)
|
474
475
|
|
475
476
|
r = TestEntity.find_all(:a => 1, :order => :desc, :limit => 2)
|
476
477
|
r.size.should == 2
|
@@ -497,7 +498,7 @@ describe GitModel::Persistable do
|
|
497
498
|
it "generates and saves the index" do
|
498
499
|
TestEntity.index.should_receive(:generate!)
|
499
500
|
TestEntity.index.should_receive(:save)
|
500
|
-
TestEntity.index!
|
501
|
+
TestEntity.index!(GitModel.default_branch)
|
501
502
|
end
|
502
503
|
end
|
503
504
|
|
@@ -505,7 +506,7 @@ describe GitModel::Persistable do
|
|
505
506
|
it 'returns a list of all values that exist for a given attribute' do
|
506
507
|
o = TestEntity.create!(:id => 'first', :attributes => {"a" => 1, "b" => 2})
|
507
508
|
o = TestEntity.create!(:id => 'second', :attributes => {"a" => 3, "b" => 4})
|
508
|
-
TestEntity.index!
|
509
|
+
TestEntity.index!(GitModel.default_branch)
|
509
510
|
TestEntity.all_values_for_attr("a").should == [1, 3]
|
510
511
|
end
|
511
512
|
end
|
data/spec/gitmodel_spec.rb
CHANGED
@@ -4,7 +4,7 @@ describe GitModel do
|
|
4
4
|
|
5
5
|
describe ".last_commit" do
|
6
6
|
it "returns nil if there are no commits" do
|
7
|
-
GitModel.last_commit.should == nil
|
7
|
+
GitModel.last_commit(GitModel.default_branch).should == nil
|
8
8
|
end
|
9
9
|
|
10
10
|
it "returns the most recent commit on a branch" do
|
@@ -14,13 +14,13 @@ describe GitModel do
|
|
14
14
|
index.add "foo", "foo"
|
15
15
|
sha = index.commit nil, nil, nil, nil, 'master'
|
16
16
|
|
17
|
-
GitModel.last_commit.to_s.should == sha
|
17
|
+
GitModel.last_commit(GitModel.default_branch).to_s.should == sha
|
18
18
|
end
|
19
19
|
end
|
20
20
|
|
21
21
|
describe ".current_tree" do
|
22
22
|
it "returns nil if there are no commits" do
|
23
|
-
GitModel.current_tree.should == nil
|
23
|
+
GitModel.current_tree(GitModel.default_branch).should == nil
|
24
24
|
end
|
25
25
|
|
26
26
|
it "returns the tree for the most recent commit on a branch" do
|
@@ -40,7 +40,7 @@ describe GitModel do
|
|
40
40
|
it "calls index! on each Persistable model class" do
|
41
41
|
TestEntity.should_receive(:index!)
|
42
42
|
TestEntity2.should_receive(:index!)
|
43
|
-
GitModel.index!
|
43
|
+
GitModel.index!(GitModel.default_branch)
|
44
44
|
end
|
45
45
|
end
|
46
46
|
|
data/spec/spec_helper.rb
CHANGED
metadata
CHANGED
@@ -1,114 +1,125 @@
|
|
1
|
-
--- !ruby/object:Gem::Specification
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
2
|
name: gitmodel
|
3
|
-
version: !ruby/object:Gem::Version
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.5
|
4
5
|
prerelease:
|
5
|
-
version: 0.0.4
|
6
6
|
platform: ruby
|
7
|
-
authors:
|
7
|
+
authors:
|
8
8
|
- Paul Dowman
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
- !ruby/object:Gem::Dependency
|
12
|
+
date: 2011-08-03 00:00:00.000000000Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
16
15
|
name: activemodel
|
17
|
-
|
18
|
-
requirement: &id001 !ruby/object:Gem::Requirement
|
16
|
+
requirement: &70218492110080 !ruby/object:Gem::Requirement
|
19
17
|
none: false
|
20
|
-
requirements:
|
21
|
-
- -
|
22
|
-
- !ruby/object:Gem::Version
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
23
21
|
version: 3.0.1
|
24
22
|
type: :runtime
|
25
|
-
version_requirements: *id001
|
26
|
-
- !ruby/object:Gem::Dependency
|
27
|
-
name: activesupport
|
28
23
|
prerelease: false
|
29
|
-
|
24
|
+
version_requirements: *70218492110080
|
25
|
+
- !ruby/object:Gem::Dependency
|
26
|
+
name: activesupport
|
27
|
+
requirement: &70218492109600 !ruby/object:Gem::Requirement
|
30
28
|
none: false
|
31
|
-
requirements:
|
32
|
-
- -
|
33
|
-
- !ruby/object:Gem::Version
|
29
|
+
requirements:
|
30
|
+
- - ! '>='
|
31
|
+
- !ruby/object:Gem::Version
|
34
32
|
version: 3.0.1
|
35
33
|
type: :runtime
|
36
|
-
version_requirements: *id002
|
37
|
-
- !ruby/object:Gem::Dependency
|
38
|
-
name: grit
|
39
34
|
prerelease: false
|
40
|
-
|
35
|
+
version_requirements: *70218492109600
|
36
|
+
- !ruby/object:Gem::Dependency
|
37
|
+
name: grit
|
38
|
+
requirement: &70218492135740 !ruby/object:Gem::Requirement
|
41
39
|
none: false
|
42
|
-
requirements:
|
43
|
-
- -
|
44
|
-
- !ruby/object:Gem::Version
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
45
43
|
version: 2.3.0
|
46
44
|
type: :runtime
|
47
|
-
version_requirements: *id003
|
48
|
-
- !ruby/object:Gem::Dependency
|
49
|
-
name: lockfile
|
50
45
|
prerelease: false
|
51
|
-
|
46
|
+
version_requirements: *70218492135740
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: lockfile
|
49
|
+
requirement: &70218492135280 !ruby/object:Gem::Requirement
|
52
50
|
none: false
|
53
|
-
requirements:
|
54
|
-
- -
|
55
|
-
- !ruby/object:Gem::Version
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
56
54
|
version: 1.4.3
|
57
55
|
type: :runtime
|
58
|
-
version_requirements: *id004
|
59
|
-
- !ruby/object:Gem::Dependency
|
60
|
-
name: yajl-ruby
|
61
56
|
prerelease: false
|
62
|
-
|
57
|
+
version_requirements: *70218492135280
|
58
|
+
- !ruby/object:Gem::Dependency
|
59
|
+
name: memcache-client
|
60
|
+
requirement: &70218492134820 !ruby/object:Gem::Requirement
|
63
61
|
none: false
|
64
|
-
requirements:
|
65
|
-
- -
|
66
|
-
- !ruby/object:Gem::Version
|
62
|
+
requirements:
|
63
|
+
- - ! '>='
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
version: 1.8.5
|
66
|
+
type: :runtime
|
67
|
+
prerelease: false
|
68
|
+
version_requirements: *70218492134820
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: yajl-ruby
|
71
|
+
requirement: &70218492134360 !ruby/object:Gem::Requirement
|
72
|
+
none: false
|
73
|
+
requirements:
|
74
|
+
- - ! '>='
|
75
|
+
- !ruby/object:Gem::Version
|
67
76
|
version: 0.8.2
|
68
77
|
type: :runtime
|
69
|
-
version_requirements: *id005
|
70
|
-
- !ruby/object:Gem::Dependency
|
71
|
-
name: ZenTest
|
72
78
|
prerelease: false
|
73
|
-
|
79
|
+
version_requirements: *70218492134360
|
80
|
+
- !ruby/object:Gem::Dependency
|
81
|
+
name: ZenTest
|
82
|
+
requirement: &70218492133900 !ruby/object:Gem::Requirement
|
74
83
|
none: false
|
75
|
-
requirements:
|
76
|
-
- -
|
77
|
-
- !ruby/object:Gem::Version
|
84
|
+
requirements:
|
85
|
+
- - ! '>='
|
86
|
+
- !ruby/object:Gem::Version
|
78
87
|
version: 4.4.0
|
79
88
|
type: :development
|
80
|
-
version_requirements: *id006
|
81
|
-
- !ruby/object:Gem::Dependency
|
82
|
-
name: autotest
|
83
89
|
prerelease: false
|
84
|
-
|
90
|
+
version_requirements: *70218492133900
|
91
|
+
- !ruby/object:Gem::Dependency
|
92
|
+
name: autotest
|
93
|
+
requirement: &70218492133440 !ruby/object:Gem::Requirement
|
85
94
|
none: false
|
86
|
-
requirements:
|
87
|
-
- -
|
88
|
-
- !ruby/object:Gem::Version
|
95
|
+
requirements:
|
96
|
+
- - ! '>='
|
97
|
+
- !ruby/object:Gem::Version
|
89
98
|
version: 4.4.1
|
90
99
|
type: :development
|
91
|
-
version_requirements: *id007
|
92
|
-
- !ruby/object:Gem::Dependency
|
93
|
-
name: rspec
|
94
100
|
prerelease: false
|
95
|
-
|
101
|
+
version_requirements: *70218492133440
|
102
|
+
- !ruby/object:Gem::Dependency
|
103
|
+
name: rspec
|
104
|
+
requirement: &70218492132980 !ruby/object:Gem::Requirement
|
96
105
|
none: false
|
97
|
-
requirements:
|
98
|
-
- -
|
99
|
-
- !ruby/object:Gem::Version
|
106
|
+
requirements:
|
107
|
+
- - ! '>='
|
108
|
+
- !ruby/object:Gem::Version
|
100
109
|
version: 2.0.1
|
101
110
|
type: :development
|
102
|
-
|
103
|
-
|
111
|
+
prerelease: false
|
112
|
+
version_requirements: *70218492132980
|
113
|
+
description: GitModel persists Ruby objects using Git as a data storage engine. It's
|
114
|
+
an ActiveModel implementation so it works stand-alone or in Rails 3 as a drop-in
|
115
|
+
replacement for ActiveRecord or DataMapper. Because the database is a Git repository
|
116
|
+
it can be synced across multiple machines, manipulated with standard Git client
|
117
|
+
tools, can be branched and merged, and of course keeps the history of all changes.
|
104
118
|
email: paul@pauldowman.com
|
105
119
|
executables: []
|
106
|
-
|
107
120
|
extensions: []
|
108
|
-
|
109
121
|
extra_rdoc_files: []
|
110
|
-
|
111
|
-
files:
|
122
|
+
files:
|
112
123
|
- .document
|
113
124
|
- .gitignore
|
114
125
|
- .rspec
|
@@ -132,32 +143,30 @@ files:
|
|
132
143
|
- spec/support/setup.rb
|
133
144
|
homepage: http://github.com/pauldowman/gitmodel
|
134
145
|
licenses: []
|
135
|
-
|
136
146
|
post_install_message:
|
137
147
|
rdoc_options: []
|
138
|
-
|
139
|
-
require_paths:
|
148
|
+
require_paths:
|
140
149
|
- lib
|
141
|
-
required_ruby_version: !ruby/object:Gem::Requirement
|
150
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
142
151
|
none: false
|
143
|
-
requirements:
|
144
|
-
- -
|
145
|
-
- !ruby/object:Gem::Version
|
146
|
-
version:
|
147
|
-
required_rubygems_version: !ruby/object:Gem::Requirement
|
152
|
+
requirements:
|
153
|
+
- - ! '>='
|
154
|
+
- !ruby/object:Gem::Version
|
155
|
+
version: '0'
|
156
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
148
157
|
none: false
|
149
|
-
requirements:
|
150
|
-
- -
|
151
|
-
- !ruby/object:Gem::Version
|
152
|
-
version:
|
158
|
+
requirements:
|
159
|
+
- - ! '>='
|
160
|
+
- !ruby/object:Gem::Version
|
161
|
+
version: '0'
|
153
162
|
requirements: []
|
154
|
-
|
155
163
|
rubyforge_project:
|
156
|
-
rubygems_version: 1.8.
|
164
|
+
rubygems_version: 1.8.6
|
157
165
|
signing_key:
|
158
166
|
specification_version: 3
|
159
|
-
summary: An ActiveModel-compliant persistence framework for Ruby that uses Git for
|
160
|
-
|
167
|
+
summary: An ActiveModel-compliant persistence framework for Ruby that uses Git for
|
168
|
+
versioning and remote syncing.
|
169
|
+
test_files:
|
161
170
|
- spec/gitmodel/index_spec.rb
|
162
171
|
- spec/gitmodel/persistable_spec.rb
|
163
172
|
- spec/gitmodel/transaction_spec.rb
|