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/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
|
+
|
data/spec/spec_helper.rb
ADDED
@@ -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
|
+
|