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/lib/gitmodel.rb ADDED
@@ -0,0 +1,76 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ require 'active_model'
5
+ require 'active_support/all' # TODO we don't really want all here, clean this up
6
+ require 'grit'
7
+ require 'json'
8
+ require 'lockfile'
9
+ require 'pp'
10
+
11
+ $:.unshift(File.dirname(__FILE__))
12
+ require 'gitmodel/errors'
13
+ require 'gitmodel/persistable'
14
+ require 'gitmodel/transaction'
15
+
16
+ module GitModel
17
+
18
+ # db_root must be an existing git repo. (It can be created with create_db!)
19
+ # Bare repositories aren't supported yet, it must be a normal git repo with a
20
+ # working directory and a '.git' subdirectory.
21
+ mattr_accessor :db_root
22
+ self.db_root = './gitmodel-data'
23
+
24
+ mattr_accessor :default_branch
25
+ self.default_branch = 'master'
26
+
27
+ mattr_accessor :logger
28
+ self.logger = ::Logger.new(STDERR)
29
+ self.logger.level = ::Logger::WARN
30
+
31
+ mattr_accessor :git_user_name
32
+ mattr_accessor :git_user_email
33
+
34
+ def self.repo
35
+ @@repo = Grit::Repo.new(GitModel.db_root)
36
+ end
37
+
38
+ # Create the database defined in db_root. Raises an exception if it exists.
39
+ def self.create_db!
40
+ raise "Database #{db_root} already exists!" if File.exist? db_root
41
+ if db_root =~ /.+\.git/
42
+ #logger.info "Creating database (bare): #{db_root}"
43
+ #Grit::Repo.init_bare db_root
44
+ logger.error "Bare repositories aren't supported yet"
45
+ else
46
+ logger.info "Creating database: #{db_root}"
47
+ Grit::Repo.init db_root
48
+ end
49
+ end
50
+
51
+ # Delete and re-create the database defined in db_root. Dangerous!
52
+ def self.recreate_db!
53
+ logger.info "Deleting database #{db_root}!!"
54
+ FileUtils.rm_rf db_root
55
+ create_db!
56
+ end
57
+
58
+ def self.last_commit(branch = nil)
59
+ branch ||= default_branch
60
+ # PERFORMANCE Cache this somewhere and update it on commit?
61
+ # (Need separate instance per branch)
62
+
63
+ return nil unless repo.commits(branch).any?
64
+
65
+ # We should be able to use just repo.commits(branch).first here but
66
+ # this is a workaround for this bug:
67
+ # http://github.com/mojombo/grit/issues/issue/38
68
+ GitModel.repo.commits("#{branch}^..#{branch}").first || GitModel.repo.commits(branch).first
69
+ end
70
+
71
+ def self.current_tree(branch = nil)
72
+ c = last_commit(branch)
73
+ c ? c.tree : nil
74
+ end
75
+
76
+ end
@@ -0,0 +1,436 @@
1
+ require 'spec_helper'
2
+
3
+ class TestEntity
4
+ include GitModel::Persistable
5
+ end
6
+
7
+ class LintTest < ActiveModel::TestCase
8
+ include ActiveModel::Lint::Tests
9
+
10
+ def setup
11
+ @model = TestEntity.new
12
+ end
13
+ end
14
+
15
+ describe GitModel::Persistable do
16
+
17
+ it 'passes ActiveModel lint tests' do
18
+
19
+ o = LintTest.new("ActiveModel lint test")
20
+ o.setup
21
+
22
+ # TODO get this list of methods dynamically
23
+ o.test_to_key
24
+ o.test_to_param
25
+ o.test_valid?
26
+ o.test_persisted?
27
+ o.test_model_naming
28
+ o.test_errors_aref
29
+ o.test_errors_full_messages
30
+ end
31
+
32
+ describe '#save' do
33
+
34
+ it 'raises an exception if the id is not set' do
35
+ o = TestEntity.new
36
+ lambda {o.save}.should raise_error(GitModel::NullId)
37
+ end
38
+
39
+ it 'stores an instance in a Git repository in a subdir of db_root named with the id' do
40
+ id = 'foo'
41
+ TestEntity.create!(:id => id)
42
+
43
+ repo = Grit::Repo.new(GitModel.db_root)
44
+ (repo.commits.first.tree / File.join(TestEntity.db_subdir, id, 'attributes.json')).data.should_not be_nil
45
+ end
46
+
47
+ it 'stores attributes in a JSON file' do
48
+ id = 'foo'
49
+ attrs = {:one => 1, :two => 2}
50
+ TestEntity.create!(:id => id, :attributes => attrs)
51
+
52
+ repo = Grit::Repo.new(GitModel.db_root)
53
+ attrs = (repo.commits.first.tree / File.join(TestEntity.db_subdir, id, 'attributes.json')).data
54
+ r = JSON.parse(attrs)
55
+ r.size.should == 2
56
+ r['one'].should == 1
57
+ r['two'].should == 2
58
+ end
59
+
60
+ it 'stores blobs in files' do
61
+ id = 'foo'
62
+ blobs = {'blob1.txt' => 'This is blob 1'}
63
+ TestEntity.create!(:id => id, :blobs => blobs)
64
+
65
+ repo = Grit::Repo.new(GitModel.db_root)
66
+ (repo.commits.first.tree / File.join(TestEntity.db_subdir, id, 'blob1.txt')).data.should == 'This is blob 1'
67
+ end
68
+
69
+ it 'can store attributes and blobs' do
70
+ id = 'foo'
71
+ attrs = {:one => 1, :two => 2}
72
+ blobs = {'blob1.txt' => 'This is blob 1'}
73
+ TestEntity.create!(:id => id, :attributes => attrs, :blobs => blobs)
74
+
75
+ r = TestEntity.find('foo')
76
+ r.attributes['one'].should == 1
77
+ r.attributes['two'].should == 2
78
+ r.blobs['blob1.txt'].should == 'This is blob 1'
79
+ end
80
+
81
+ it 'returns false if the validations failed'
82
+
83
+ it 'returns the SHA of the commit if the save was successful'
84
+
85
+ it 'deletes blobs that have been removed'
86
+ end
87
+
88
+ describe '#save!' do
89
+
90
+ it "calls save and returns the non-false and non-nil result"
91
+
92
+ it "calls save and raises an exception if the result is nil"
93
+
94
+ it "calls save and raises an exception if the result is false"
95
+
96
+ end
97
+
98
+ describe '#new' do
99
+ it 'creates a new unsaved instance' do
100
+ TestEntity.new.new_record?.should be_true
101
+ end
102
+
103
+ it 'takes an optional hash to set id, attributes and blobs' do
104
+ o = TestEntity.new(:id => 'foo', :attributes => {:one => 1}, :blobs => {'blob1.txt' => 'This is blob 1'})
105
+ o.id.should == 'foo'
106
+ o.attributes['one'].should == 1
107
+ o.blobs['blob1.txt'].should == 'This is blob 1'
108
+ end
109
+ end
110
+
111
+ describe '.create' do
112
+
113
+ it 'creates a new instance with the given parameters and calls #save on it' do
114
+ id = 'foo'
115
+ attrs = {:one => 1, :two => 2}
116
+ blobs = {'blob1.txt' => 'This is blob 1'}
117
+
118
+ new_mock = mock("new_mock")
119
+ TestEntity.should_receive(:new).with(:id => id, :attributes => attrs, :blobs => blobs).and_return(new_mock)
120
+ new_mock.should_receive(:save)
121
+
122
+ TestEntity.create(:id => id, :attributes => attrs, :blobs => blobs)
123
+ end
124
+
125
+ it 'returns an instance of the record created' do
126
+ o = TestEntity.create(:id => 'lemur')
127
+ o.should be_a(TestEntity)
128
+ o.id.should == 'lemur'
129
+ end
130
+
131
+ describe 'with a single array as a parameter' do
132
+
133
+ it 'creates a new instance with each element of the array as parameters and calls #save on it' do
134
+ args = [
135
+ {:id => 'foo', :attributes => {:one => 1}, :blobs => {'blob1.txt' => 'This is blob 1'}},
136
+ {:id => 'bar', :attributes => {:two => 2}, :blobs => {'blob2.txt' => 'This is blob 2'}}
137
+ ]
138
+
139
+ new_mock1 = mock("new_mock1")
140
+ new_mock2 = mock("new_mock2")
141
+ TestEntity.should_receive(:new).with(args[0]).once.and_return(new_mock1)
142
+ TestEntity.should_receive(:new).with(args[1]).once.and_return(new_mock2)
143
+ new_mock1.should_receive(:save)
144
+ new_mock2.should_receive(:save)
145
+
146
+ TestEntity.create(args)
147
+ end
148
+
149
+ end
150
+
151
+ end
152
+
153
+ describe '.create!' do
154
+
155
+ it 'creates a new instance with the given parameters and calls #save! on it' do
156
+ id = 'foo'
157
+ attrs = {:one => 1, :two => 2}
158
+ blobs = {'blob1.txt' => 'This is blob 1'}
159
+
160
+ new_mock = mock("new_mock")
161
+ TestEntity.should_receive(:new).with(:id => id, :attributes => attrs, :blobs => blobs).and_return(new_mock)
162
+ new_mock.should_receive(:save!)
163
+
164
+ TestEntity.create!(:id => id, :attributes => attrs, :blobs => blobs)
165
+ end
166
+
167
+ it 'returns an instance of the record created' do
168
+ o = TestEntity.create!(:id => 'lemur')
169
+ o.should be_a(TestEntity)
170
+ o.id.should == 'lemur'
171
+ end
172
+
173
+ describe 'with a single array as a parameter' do
174
+ it 'creates a new instance with each element of the array as parameters and calls #save! on it' do
175
+ args = [
176
+ {:id => 'foo', :attributes => {:one => 1}, :blobs => {'blob1.txt' => 'This is blob 1'}},
177
+ {:id => 'bar', :attributes => {:two => 2}, :blobs => {'blob2.txt' => 'This is blob 2'}}
178
+ ]
179
+
180
+ new_mock1 = mock("new_mock1")
181
+ new_mock2 = mock("new_mock2")
182
+ TestEntity.should_receive(:new).with(args[0]).once.and_return(new_mock1)
183
+ TestEntity.should_receive(:new).with(args[1]).once.and_return(new_mock2)
184
+ new_mock1.should_receive(:save!)
185
+ new_mock2.should_receive(:save!)
186
+
187
+ TestEntity.create!(args)
188
+ end
189
+ end
190
+
191
+ end
192
+
193
+ describe '.delete' do
194
+
195
+ it 'deletes the object with the given id from the database' do
196
+ TestEntity.create!(:id => 'monkey')
197
+ TestEntity.delete('monkey')
198
+
199
+ TestEntity.exists?('monkey').should be_false
200
+ end
201
+
202
+ it 'also deletes blobs associated with the given object' do
203
+ id = 'Lemuridae'
204
+ TestEntity.create!(:id => id, :blobs => {:crowned => "Eulemur coronatus", :brown => "Eulemur fulvus"})
205
+ (GitModel.current_tree / File.join(TestEntity.db_subdir, id, 'crowned')).data.should_not be_nil
206
+ (GitModel.current_tree / File.join(TestEntity.db_subdir, id, 'brown')).data.should_not be_nil
207
+ TestEntity.delete(id)
208
+
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
211
+
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
214
+ end
215
+
216
+
217
+ end
218
+
219
+ describe '.delete_all' do
220
+
221
+ it 'deletes all objects of the same type from the database' do
222
+ TestEntity.create!(:id => 'monkey')
223
+ TestEntity.create!(:id => 'ape')
224
+
225
+ TestEntity.delete_all
226
+ TestEntity.find_all.should be_empty
227
+ end
228
+
229
+ end
230
+
231
+ describe '#delete' do
232
+
233
+ it 'deletes the object from the database' do
234
+ o = TestEntity.create!(:id => 'monkey')
235
+ o.delete
236
+
237
+ TestEntity.exists?('monkey').should be_false
238
+ end
239
+
240
+ it 'freezes the object' do
241
+ o = TestEntity.create!(:id => 'monkey')
242
+ o.delete
243
+
244
+ o.frozen?.should be_true
245
+ end
246
+
247
+ end
248
+
249
+ describe '#find' do
250
+
251
+ #it 'can load an object from an empty subdir of db_root' do
252
+ # id = "foo"
253
+ # dir = File.join(GitModel.db_root, TestEntity.db_subdir, id)
254
+ # FileUtils.mkdir_p dir
255
+
256
+ # o = TestEntity.find(id)
257
+ # o.id.should == id
258
+ # o.attributes.should be_empty
259
+ # o.blobs.should be_empty
260
+ #end
261
+
262
+ describe 'with no commits in the repo' do
263
+
264
+ it 'raises GitModel::RecordNotFound if a record with the given id doesn\'t exist' do
265
+ lambda{TestEntity.find('missing')}.should raise_error(GitModel::RecordNotFound)
266
+ end
267
+
268
+ end
269
+
270
+ it 'raises GitModel::RecordNotFound if a record with the given id doesn\'t exist' do
271
+ TestEntity.create!(:id => 'something')
272
+ lambda{TestEntity.find('missing')}.should raise_error(GitModel::RecordNotFound)
273
+ end
274
+
275
+ it 'can load an object with attributes and no blobs' do
276
+ id = "foo"
277
+ attrs = {:one => 1, :two => 2}
278
+ TestEntity.create!(:id => id, :attributes => attrs)
279
+
280
+ o = TestEntity.find(id)
281
+ o.id.should == id
282
+ o.attributes.size.should == 2
283
+ o.attributes['one'].should == 1
284
+ o.attributes['two'].should == 2
285
+ o.blobs.should be_empty
286
+ end
287
+
288
+ it 'can load an object with blobs and no attributes' do
289
+ id = 'foo'
290
+ blobs = {'blob1.txt' => 'This is blob 1', 'blob2' => 'This is blob 2'}
291
+ TestEntity.create!(:id => id, :blobs => blobs)
292
+
293
+ o = TestEntity.find(id)
294
+ o.id.should == id
295
+ o.attributes.should be_empty
296
+ o.blobs.size.should == 2
297
+ o.blobs["blob1.txt"].should == 'This is blob 1'
298
+ o.blobs["blob2"].should == 'This is blob 2'
299
+ end
300
+
301
+ it 'can load an object with both attributes and blobs' do
302
+ id = 'foo'
303
+ attrs = {:one => 1, :two => 2}
304
+ blobs = {'blob1.txt' => 'This is blob 1', 'blob2' => 'This is blob 2'}
305
+ TestEntity.create!(:id => id, :attributes => attrs, :blobs => blobs)
306
+
307
+ o = TestEntity.find(id)
308
+ o.id.should == id
309
+ o.attributes.size.should == 2
310
+ o.attributes['one'].should == 1
311
+ o.attributes['two'].should == 2
312
+ o.blobs.size.should == 2
313
+ o.blobs["blob1.txt"].should == 'This is blob 1'
314
+ o.blobs["blob2"].should == 'This is blob 2'
315
+ end
316
+
317
+ end
318
+
319
+ describe '#find_all' do
320
+
321
+ it 'returns an array of all objects' do
322
+ TestEntity.create!(:id => 'one')
323
+ TestEntity.create!(:id => 'two')
324
+ TestEntity.create!(:id => 'three')
325
+
326
+ r = TestEntity.find_all
327
+ r.size.should == 3
328
+ end
329
+
330
+ end
331
+
332
+ describe '#exists?' do
333
+
334
+ it 'returns true if the record exists' do
335
+ TestEntity.create!(:id => 'one')
336
+ TestEntity.exists?('one').should be_true
337
+ end
338
+
339
+ it "returns false if the record doesn't exist" do
340
+ TestEntity.exists?('missing').should be_false
341
+ end
342
+
343
+ end
344
+
345
+ describe '#attributes' do
346
+ it 'accepts symbols or strings interchangeably as strings' do
347
+ o = TestEntity.new(:id => 'lol', :attributes => {"one" => 1, :two => 2})
348
+ o.save!
349
+ o.attributes["one"].should == 1
350
+ o.attributes[:one].should == 1
351
+ o.attributes["two"].should == 2
352
+ o.attributes[:two].should == 2
353
+
354
+ # Should also be true after reloading
355
+ o = TestEntity.find 'lol'
356
+ o.attributes["one"].should == 1
357
+ o.attributes[:one].should == 1
358
+ o.attributes["two"].should == 2
359
+ o.attributes[:two].should == 2
360
+ end
361
+ end
362
+
363
+ describe '#blobs' do
364
+ it 'accepts symbols or strings interchangeably as strings' do
365
+ o = TestEntity.new(:id => 'lol', :blobs => {"one" => 'this is blob 1', :two => 'this is blob 2'})
366
+ o.save!
367
+ o.blobs["one"].should == 'this is blob 1'
368
+ o.blobs[:one].should == 'this is blob 1'
369
+ o.blobs["two"].should == 'this is blob 2'
370
+ o.blobs[:two].should == 'this is blob 2'
371
+
372
+ # Should also be true after reloading
373
+ o = TestEntity.find 'lol'
374
+ o.blobs["one"].should == 'this is blob 1'
375
+ o.blobs[:one].should == 'this is blob 1'
376
+ o.blobs["two"].should == 'this is blob 2'
377
+ o.blobs[:two].should == 'this is blob 2'
378
+ end
379
+ end
380
+
381
+ describe 'attribute description in the class definition' do
382
+
383
+ it 'creates convenient accessor methods for accessing the attributes hash' do
384
+ o = TestEntity.new
385
+ class << o
386
+ attribute :colour
387
+ end
388
+
389
+ o.colour.should == nil
390
+ o.colour = "red"
391
+ o.colour.should == "red"
392
+ o.attributes[:colour].should == "red"
393
+ end
394
+
395
+ it 'can set default values for attributes, with any ruby value for the default' do
396
+ o = TestEntity.new
397
+
398
+ # Change the singleton class for object o, this doesn't change the
399
+ # TestEntity class
400
+ class << o
401
+ attribute :size, :default => "medium"
402
+ attribute :shape, :default => 2
403
+ attribute :style, :default => nil
404
+ attribute :teeth, :default => {"molars" => 4, "canines" => 2}
405
+ end
406
+
407
+ o.size.should == "medium"
408
+ o.shape.should == 2
409
+ o.style.should == nil
410
+ o.teeth.should == {"molars" => 4, "canines" => 2}
411
+
412
+ o.size = "large"
413
+ o.size.should == "large"
414
+ end
415
+
416
+ end
417
+
418
+ describe 'blob description in the class definition' do
419
+
420
+ it 'creates convenient accessor methods for accessing the blobs hash' do
421
+ o = TestEntity.new
422
+ class << o
423
+ blob :avatar
424
+ end
425
+
426
+ o.avatar.should == nil
427
+
428
+ o.avatar = "image_data_here"
429
+ o.avatar.should == "image_data_here"
430
+ o.blobs[:avatar].should == "image_data_here"
431
+ end
432
+
433
+ end
434
+
435
+ end
436
+
@@ -0,0 +1,59 @@
1
+ require 'spec_helper'
2
+
3
+ describe GitModel::Transaction do
4
+
5
+ describe '#execute' do
6
+ it "yields to a given block" do
7
+ m = mock("mock")
8
+ m.should_receive(:a_method)
9
+ GitModel::Transaction.new.execute do
10
+ m.a_method
11
+ end
12
+ end
13
+
14
+ describe "when called the first time" do
15
+ it "creates a new Git index" do
16
+ index = mock("index")
17
+ index.stub!(:read_tree)
18
+ index.stub!(:commit)
19
+ Grit::Index.should_receive(:new).and_return(index)
20
+ GitModel::Transaction.new.execute {}
21
+ end
22
+
23
+ it "commits after yielding" do
24
+ index = mock("index")
25
+ index.stub!(:read_tree)
26
+ index.should_receive(:commit)
27
+ Grit::Index.should_receive(:new).and_return(index)
28
+ GitModel::Transaction.new.execute {}
29
+ end
30
+
31
+ it "can create the first commit in the repo" do
32
+ GitModel::Transaction.new.execute do |t|
33
+ t.index.add "foo", "foo"
34
+ end
35
+ end
36
+
37
+ # TODO it "locks the branch while committing"
38
+
39
+ # TODO it "merges commits from concurrent transactions"
40
+
41
+ end
42
+
43
+ describe "when called recursively" do
44
+
45
+ it "re-uses the existing git index and doesn't commit" do
46
+ index = mock("index")
47
+ index.stub!(:read_tree)
48
+ index.should_receive(:commit).once
49
+ Grit::Index.should_receive(:new).and_return(index)
50
+ GitModel::Transaction.new.execute do |t|
51
+ t.execute {}
52
+ end
53
+ end
54
+
55
+ end
56
+
57
+ end
58
+
59
+ end
@@ -0,0 +1,39 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe GitModel do
4
+
5
+ describe "#last_commit" do
6
+
7
+ it "returns nil if there are no commits" do
8
+ GitModel.last_commit.should == nil
9
+ end
10
+
11
+ it "returns the most recent commit on a branch" do
12
+ index = Grit::Index.new(GitModel.repo)
13
+ head = GitModel.repo.commits.first
14
+ index.read_tree head.to_s
15
+ index.add "foo", "foo"
16
+ sha = index.commit nil, nil, nil, nil, 'master'
17
+
18
+ GitModel.last_commit.to_s.should == sha
19
+ end
20
+
21
+ end
22
+
23
+ describe "#current_tree" do
24
+
25
+ it "returns nil if there are no commits" do
26
+ GitModel.current_tree.should == nil
27
+ end
28
+
29
+ it "returns the tree for the most recent commit on a branch" do
30
+ last_commit = mock('last_commit')
31
+ last_commit.should_receive(:tree).and_return("yay, a tree!")
32
+ GitModel.should_receive(:last_commit).with('master').and_return(last_commit)
33
+ GitModel.current_tree('master')
34
+ end
35
+
36
+ end
37
+
38
+ end
39
+
@@ -0,0 +1,12 @@
1
+ require 'rspec'
2
+
3
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
4
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
5
+ require 'gitmodel'
6
+
7
+ Dir[File.join(File.dirname(__FILE__), "support/**/*.rb")].each {|f| require f}
8
+
9
+ Rspec.configure do |c|
10
+ c.mock_with :rspec
11
+ end
12
+
@@ -0,0 +1,10 @@
1
+ GitModel.db_root = '/tmp/gitmodel-test-data'
2
+ GitModel.git_user_name = 'GitModel Test'
3
+ GitModel.git_user_email = 'foo@bar.com'
4
+
5
+ RSpec.configure do |config|
6
+ config.before(:each) do
7
+ GitModel.recreate_db!
8
+ end
9
+ end
10
+