gitmodel 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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
+