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 ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,7 @@
1
+ *.gem
2
+ .DS_Store
3
+ .bundle
4
+ .gitmodel-data
5
+ coverage
6
+ pkg
7
+ rdoc
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --colour
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm use ruby-1.9.2@gitmodel
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # Edit this Gemfile to bundle your application's dependencies.
2
+ source :rubygems
3
+
4
+ gemspec
5
+
data/Gemfile.lock ADDED
@@ -0,0 +1,54 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ gitmodel (0.0.0)
5
+ activemodel (>= 3.0.1)
6
+ activesupport (>= 3.0.1)
7
+ grit (>= 2.3.0)
8
+ lockfile (>= 1.4.3)
9
+
10
+ GEM
11
+ remote: http://rubygems.org/
12
+ specs:
13
+ ZenTest (4.4.0)
14
+ activemodel (3.0.1)
15
+ activesupport (= 3.0.1)
16
+ builder (~> 2.1.2)
17
+ i18n (~> 0.4.1)
18
+ activesupport (3.0.1)
19
+ autotest (4.4.1)
20
+ autotest-fsevent (0.2.3)
21
+ sys-uname
22
+ builder (2.1.2)
23
+ diff-lcs (1.1.2)
24
+ grit (2.3.0)
25
+ diff-lcs (~> 1.1)
26
+ mime-types (~> 1.15)
27
+ i18n (0.4.1)
28
+ lockfile (1.4.3)
29
+ mime-types (1.16)
30
+ rspec (2.0.1)
31
+ rspec-core (~> 2.0.1)
32
+ rspec-expectations (~> 2.0.1)
33
+ rspec-mocks (~> 2.0.1)
34
+ rspec-core (2.0.1)
35
+ rspec-expectations (2.0.1)
36
+ diff-lcs (>= 1.1.2)
37
+ rspec-mocks (2.0.1)
38
+ rspec-core (~> 2.0.1)
39
+ rspec-expectations (~> 2.0.1)
40
+ sys-uname (0.8.4)
41
+
42
+ PLATFORMS
43
+ ruby
44
+
45
+ DEPENDENCIES
46
+ ZenTest (>= 4.4.0)
47
+ activemodel (>= 3.0.1)
48
+ activesupport (>= 3.0.1)
49
+ autotest (>= 4.4.1)
50
+ autotest-fsevent (>= 0.2.3)
51
+ gitmodel!
52
+ grit (>= 2.3.0)
53
+ lockfile (>= 1.4.3)
54
+ rspec (>= 2.0.1)
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Paul Dowman
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,143 @@
1
+ GitModel: distributed, versioned NoSQL for Ruby
2
+ ---------------------------------------------------
3
+
4
+ _http://github.com/pauldowman/gitmodel_
5
+
6
+ GitModel is an
7
+ [ActiveModel](http://yehudakatz.com/2010/01/10/activemodel-make-any-ruby-object-feel-like-activerecord/)-compliant
8
+ persistence framework for Ruby that uses [Git](http://git-scm.com/) for
9
+ versioning and remote syncing.
10
+
11
+ GitModel persists Ruby objects using Git as a data storage engine. It's an
12
+ ActiveModel implementation so it works stand-alone or in Rails 3 as a drop-in
13
+ replacement for ActiveRecord or DataMapper.
14
+
15
+ Because the database is a Git repository it can be synced across multiple
16
+ machines, manipulated with standard Git client tools, can be branched and
17
+ merged, and of course keeps the history of all changes.
18
+
19
+
20
+ Status
21
+ ------
22
+
23
+ _It is nowhere near production ready but I'm working on it. Please feel free to
24
+ contribute tests and/or code to help!_
25
+
26
+
27
+ Why it's awesome
28
+ ----------------
29
+
30
+ * Schema-less NoSQL data store
31
+ * Each record is a normal Ruby object, attributes are any Ruby type or large
32
+ chunks of binary data
33
+ * Never lose data, history is kept forever and can be restored simply using
34
+ standard Git tools
35
+ * Branch and merge your production data
36
+ * GitModel can actually work with different branches
37
+ * Branch or tag snapshots of your data
38
+ * Experiment on production data using branches, for example to test a
39
+ migration
40
+ * Distributed (synced using standard Git push/pull)
41
+ * Transactions
42
+ * Metadata for all database changes (Git commit messages, date & time, etc.)
43
+ * In order to be easily human-editable, the database is simply files and
44
+ directores stored in a Git repository. GitModel uses the Git repo directly
45
+ (rather than Git's checked-out "working copy") but you can do a "git
46
+ checkout" to view and manipulate the database contents, and then "git commit"
47
+ * Test-driven development and excellent test coverage
48
+ * Clean and easy-to-use API
49
+
50
+
51
+ Usage
52
+ -----
53
+
54
+ GitModel.db_root = '/tmp/gitmodel-data'
55
+ GitModel.create_db!
56
+
57
+ class Post
58
+ include GitModel::Persistable
59
+
60
+ attribute :title
61
+ attribute :body
62
+ attribute :categories, :default => []
63
+ attribute :allow_comments, :default => true
64
+
65
+ blob :image
66
+
67
+ end
68
+
69
+ p1 = Post.new(:id => 'lessons-learned', :title => 'Lessons learned', :body => '...')
70
+ p1.image = some_binary_data
71
+ p1.save!
72
+
73
+ p2 = Post.new(:id => 'hotdog-eating-contest', :title => 'I won!')
74
+ p2.body = 'This weekend I won a hotdog eating contest!'
75
+ p2.image = some_binary_data
76
+ p2.blobs['hotdogs.jpg'] = some_binary_data
77
+ p2.blobs['the-aftermath.jpg'] = some_binary_data
78
+ p2.save!
79
+
80
+ p3 = Post.create!(:id => 'running-with-scissors', :title => 'Running with scissors', :body => '...')
81
+
82
+ p4 = Post.find('running-with-scissors')
83
+
84
+
85
+ class Comment
86
+ include GitModel::Persistable
87
+ attribute :text
88
+ end
89
+
90
+ c1 = Comment.create!(:id => '2010-01-03-328', :text => '...')
91
+ c2 = Comment.create!(:id => '2010-05-29-742', :text => '...')
92
+
93
+
94
+ Database file structure
95
+ -----------------------
96
+
97
+ The database is stored in a human-editable format. Simply do "git checkout -f"
98
+ and you'll see directories and files.
99
+
100
+ Each type of object is stored in a top-level directory (this is analogous to
101
+ ActiveRecord tables), and each object is stored in a subdirectory which is
102
+ named using the object's id (i.e. the primary key). Attributes that are Ruby
103
+ types (strings, numbers, hashes, arrays, whatever) are stored in a file named
104
+ attributes.json and binary attributes ("blobs") are stored in their own
105
+ files.
106
+
107
+ For example, the database for the example above would have a directory
108
+ structure that looks like this:
109
+
110
+ * db-root
111
+ * comments
112
+ * 2010-01-03-328
113
+ * _attributes.json_
114
+ * 2010-05-29-742
115
+ * _attributes.json_
116
+ * posts
117
+ * hotdog-eating-contest
118
+ * _attributes.json_
119
+ * _hotdogs.jpg_
120
+ * _image_
121
+ * _the-aftermath.jpg_
122
+ * lessons-learned
123
+ * _attributes.json_
124
+ * _image_
125
+ * running-with-scissors
126
+ * _attributes.json_
127
+
128
+
129
+ To Do
130
+ -----
131
+
132
+ * Querying
133
+ * Use AREL?
134
+ * Finish some pending specs
135
+ * Associations
136
+ * API documentation
137
+ * Rails integration
138
+ * rake tasks
139
+ * generators
140
+ * Performance
141
+ * Haven't optimized for performance yet.
142
+
143
+
data/Rakefile ADDED
@@ -0,0 +1,3 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
@@ -0,0 +1 @@
1
+ Autotest.add_discovery { "rspec2" }
data/gitmodel.gemspec ADDED
@@ -0,0 +1,36 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'gitmodel'
3
+ s.version = '0.0.1'
4
+ s.platform = Gem::Platform::RUBY
5
+
6
+ s.authors = ["Paul Dowman"]
7
+ s.email = 'paul@pauldowman.com'
8
+ s.homepage = 'http://github.com/pauldowman/gitmodel'
9
+
10
+ s.summary = %q{An ActiveModel-compliant persistence framework for Ruby that uses Git for versioning and remote syncing.}
11
+ s.description = <<-DESC.strip.gsub(/\n\s+/, " ")
12
+ GitModel persists Ruby objects using Git as a data storage engine. It's an
13
+ ActiveModel implementation so it works stand-alone or in Rails 3 as a drop-in
14
+ replacement for ActiveRecord or DataMapper. Because the database is a Git
15
+ repository it can be synced across multiple machines, manipulated with standard
16
+ Git client tools, can be branched and merged, and of course keeps the history
17
+ of all changes.
18
+ DESC
19
+
20
+ s.add_dependency 'activemodel', '>= 3.0.1'
21
+ s.add_dependency 'activesupport', '>= 3.0.1'
22
+ s.add_dependency 'grit', '>= 2.3.0'
23
+ s.add_dependency 'lockfile', '>= 1.4.3'
24
+
25
+ s.add_development_dependency 'ZenTest', '>= 4.4.0'
26
+ s.add_development_dependency 'autotest', '>= 4.4.1'
27
+ s.add_development_dependency 'autotest-fsevent', '>= 0.2.3' if RUBY_PLATFORM.downcase.include?("darwin") # OS X only
28
+ s.add_development_dependency 'rspec', '>= 2.0.1'
29
+
30
+ s.files = `git ls-files`.split("\n")
31
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
32
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
33
+ s.require_paths = ["lib"]
34
+
35
+ end
36
+
@@ -0,0 +1,25 @@
1
+ module GitModel
2
+
3
+ # Generic GitModel exception class.
4
+ class GitModelError < StandardError
5
+ end
6
+
7
+ # Raised when GitModel cannot find record by given id or set of ids.
8
+ class RecordNotFound < GitModelError
9
+ end
10
+
11
+ # Raised by GitModel::Persistable.save! and GitModel::Persistable.create! methods when record cannot be
12
+ # saved because record is invalid.
13
+ class RecordNotSaved < GitModelError
14
+ end
15
+
16
+ class RecordExists < GitModelError
17
+ end
18
+
19
+ class RecordDoesntExist < GitModelError
20
+ end
21
+
22
+ class NullId < GitModelError
23
+ end
24
+
25
+ end
@@ -0,0 +1,269 @@
1
+ module GitModel
2
+ module Persistable
3
+
4
+ def self.included(base)
5
+ base.class_eval do
6
+
7
+ extend ActiveModel::Callbacks
8
+ extend ActiveModel::Naming
9
+ include ActiveModel::Validations
10
+ include ActiveModel::Dirty
11
+ include ActiveModel::Observing
12
+ include ActiveModel::Translation
13
+
14
+ define_model_callbacks :initialize, :find, :touch, :only => :after
15
+ define_model_callbacks :save, :create, :update, :destroy
16
+ end
17
+
18
+ base.extend(ClassMethods)
19
+ end
20
+
21
+
22
+ def initialize(args = {})
23
+ _run_initialize_callbacks do
24
+ @new_record = true
25
+ self.attributes = {}
26
+ self.blobs = {}
27
+ args.each do |k,v|
28
+ self.send("#{k}=".to_sym, v)
29
+ end
30
+ end
31
+ end
32
+
33
+ def to_model
34
+ self
35
+ end
36
+
37
+ def to_key
38
+ id ? [id] : nil
39
+ end
40
+
41
+ def to_param
42
+ id && id.to_s
43
+ end
44
+
45
+ def id
46
+ @id
47
+ end
48
+
49
+ def id=(string)
50
+ # TODO ensure is valid as a filename
51
+ @id = string
52
+ end
53
+
54
+ def attributes
55
+ @attributes
56
+ end
57
+
58
+ def attributes=(new_attributes, guard_protected_attributes = true)
59
+ @attributes = HashWithIndifferentAccess.new
60
+ if new_attributes
61
+ new_attributes.each {|k,v| @attributes[k] = v}
62
+ end
63
+ end
64
+
65
+ def blobs
66
+ @blobs
67
+ end
68
+
69
+ def blobs=(new_blobs)
70
+ @blobs = HashWithIndifferentAccess.new
71
+ if new_blobs
72
+ new_blobs.each {|k,v| @blobs[k] = v}
73
+ end
74
+ end
75
+
76
+ def new_record?
77
+ @new_record || false
78
+ end
79
+
80
+ # Valid options are:
81
+ # :transaction
82
+ # OR:
83
+ # :branch
84
+ # :commit_message
85
+ # Returns false if validations failed, otherwise returns the SHA of the commit
86
+ def save(options = {})
87
+ raise GitModel::NullId unless self.id
88
+
89
+ if new_record?
90
+ raise GitModel::RecordExists if self.class.exists?(self.id)
91
+ else
92
+ raise GitModel::RecordDoesntExist unless self.class.exists?(self.id)
93
+ end
94
+
95
+ dir = File.join(self.class.db_subdir, self.id)
96
+
97
+ transaction = options.delete(:transaction) || GitModel::Transaction.new(options)
98
+ result = transaction.execute do |t|
99
+ # Write the attributes to the attributes file
100
+ t.index.add(File.join(dir, 'attributes.json'), JSON.pretty_generate(attributes))
101
+
102
+ # Write the blob files
103
+ blobs.each do |name, data|
104
+ t.index.add(File.join(dir, name), data)
105
+ end
106
+ end
107
+
108
+ return result
109
+ end
110
+
111
+ # Same as #save but raises an exception on error
112
+ def save!(options = {})
113
+ save(options) || raise(GitModel::RecordNotSaved)
114
+ end
115
+
116
+ def delete(options = {})
117
+ freeze
118
+ self.class.delete(id, options)
119
+ end
120
+
121
+ def to_s
122
+ "#<#{self.class.name}:#{__id__} id=#{id}, attributes=#{attributes.inspect}, blobs.keys=#{blobs.keys.inspect}>"
123
+ end
124
+
125
+
126
+ private
127
+
128
+ def load(dir)
129
+ _run_find_callbacks do
130
+ # remove dangerous ".."
131
+ # todo find a better way to ensure path is safe
132
+ dir.gsub!(/\.\./, '')
133
+
134
+ raise GitModel::RecordNotFound if GitModel.current_tree.nil?
135
+
136
+ self.id = File.basename(dir)
137
+ @new_record = false
138
+
139
+ # load the attributes
140
+ object = GitModel.current_tree / File.join(dir, 'attributes.json')
141
+ raise GitModel::RecordNotFound if object.nil?
142
+
143
+ self.attributes = JSON.parse(object.data, :max_nesting => false)
144
+
145
+ # load all other non-hidden files in the dir as blobs
146
+ blobs = (GitModel.current_tree / dir).blobs.reject{|b| b.name[0] == '.' || b.name == 'attributes.json'}
147
+ blobs.each do |b|
148
+ self.blobs[b.name] = b.data
149
+ end
150
+ end
151
+ end
152
+
153
+
154
+ module ClassMethods
155
+
156
+ def db_subdir
157
+ self.to_s.tableize
158
+ end
159
+
160
+ def attribute(name, options = {})
161
+ default = options[:default]
162
+ self.class_eval <<-EOF
163
+ def #{name}; attributes[:#{name}] || #{default.inspect}; end
164
+ def #{name}=(value); attributes[:#{name}] = value; end
165
+ EOF
166
+ end
167
+
168
+ def blob(name, options = {})
169
+ self.class_eval <<-EOF
170
+ def #{name}; blobs[:#{name}]; end
171
+ def #{name}=(value); blobs[:#{name}] = value; end
172
+ EOF
173
+ end
174
+
175
+ def find(id)
176
+ GitModel.logger.debug "Finding #{name} with id: #{id}"
177
+ o = new
178
+ dir = File.join(db_subdir, id)
179
+ o.send :load, dir
180
+ return o
181
+ end
182
+
183
+ def exists?(id)
184
+ GitModel.repo.commits.any? && !(GitModel.current_tree / File.join(db_subdir, id, 'attributes.json')).nil?
185
+ end
186
+
187
+ def find_all(conditions = {})
188
+ GitModel.logger.debug "Finding all #{name.pluralize} with conditions: #{conditions.inspect}"
189
+ results = []
190
+ dirs = (GitModel.current_tree / db_subdir).trees
191
+ dirs.each do |dir|
192
+ if dir.blobs.any?
193
+ o = new
194
+ o.send :load, File.join(db_subdir, dir.name)
195
+ results << o
196
+ end
197
+ end
198
+
199
+ return results
200
+ end
201
+
202
+ def create(args)
203
+ if args.is_a?(Array)
204
+ args.map{|arg| create(arg)}
205
+ else
206
+ o = self.new(args)
207
+ o.save
208
+ end
209
+ return o
210
+ end
211
+
212
+ def create!(args)
213
+ if args.is_a?(Array)
214
+ args.map{|arg| create!(arg)}
215
+ else
216
+ o = self.new(args)
217
+ o.save!
218
+ end
219
+ return o
220
+ end
221
+
222
+ def delete(id, options = {})
223
+ path = File.join(db_subdir, id)
224
+ transaction = options.delete(:transaction) || GitModel::Transaction.new(options)
225
+ result = transaction.execute do |t|
226
+ delete_tree(path, t.index, options)
227
+ end
228
+ end
229
+
230
+ def delete_all(options = {})
231
+ transaction = options.delete(:transaction) || GitModel::Transaction.new(options)
232
+ result = transaction.execute do |t|
233
+ delete_tree(db_subdir, t.index, options)
234
+ end
235
+ end
236
+
237
+
238
+ private
239
+
240
+ def delete_tree(path, index, options = {})
241
+ # This leaves a bunch of empty sub-trees, there must be a way to just
242
+ # replace the tree to be deleted with an empty tree that doesn't even
243
+ # reference the sub-trees.
244
+ current = index.tree
245
+ path.split('/').each do |dir|
246
+ current[dir] ||= {}
247
+ current = current[dir]
248
+ end
249
+
250
+ build_tree_hash(current, (index.current_tree / path))
251
+ end
252
+
253
+ # recusively build the hash representing the objects that grit will commit
254
+ def build_tree_hash(hash, tree)
255
+ tree.blobs.each do |b|
256
+ hash[b.name] = false
257
+ end
258
+ tree.trees.each do |t|
259
+ hash[t.name] = {}
260
+ build_tree_hash(hash[t.name], t)
261
+ end
262
+ return hash
263
+ end
264
+
265
+ end # module ClassMethods
266
+
267
+ end # module Persistable
268
+ end # module GitModel
269
+
@@ -0,0 +1,62 @@
1
+ module GitModel
2
+ class Transaction
3
+
4
+ attr_accessor :index
5
+ attr_accessor :branch
6
+ attr_accessor :commit_message
7
+
8
+ def initialize(options = {})
9
+ self.branch = options[:branch] || GitModel.default_branch
10
+ self.commit_message = options[:commit_message]
11
+ end
12
+
13
+ def execute(&block)
14
+ if index
15
+ # We're already in a transaction
16
+ yield self
17
+ else
18
+ # For now there's a big ugly lock here, this will be fixed!
19
+ # TODO move this lock around the commit only (need to make sure two
20
+ # processes aren't updating refs/heads/<branch> at the same time) and
21
+ # make concurrent transactions can work. This will require some merging
22
+ # magic!
23
+ lock do
24
+ # We're not in a transaction, start a new one
25
+ GitModel.logger.debug "Beginning transaction on #{branch}..."
26
+
27
+ # Save the current head so that concurrent transactions can work. We need
28
+ # to make sure the parent of this commit is the same SHA that this
29
+ # index's tree is based on.
30
+ parent = GitModel.last_commit(branch)
31
+
32
+ self.index = Grit::Index.new(GitModel.repo)
33
+ index.read_tree(parent.to_s)
34
+
35
+ yield self
36
+
37
+ committer = Grit::Actor.new(GitModel.git_user_name, GitModel.git_user_email)
38
+ sha = index.commit(commit_message, parent ? [parent] : nil, committer, nil, branch)
39
+ # TODO return false and log if anything went wrong with the commit
40
+
41
+ GitModel.logger.debug "Finished transaction on #{branch}."
42
+
43
+ return sha
44
+ end
45
+ end
46
+ end
47
+
48
+ # Wait until we can get an exclusive lock on the branch, then execute the
49
+ # block. We lock the branch by creating refs/heads/<branch>.lock, which
50
+ # the git commands also seem to respect
51
+ def lock(&block)
52
+ lockfile = Lockfile.new File.join(GitModel.repo.path, 'refs/heads', branch + '.lock')
53
+ begin
54
+ lockfile.lock
55
+ yield
56
+ ensure
57
+ lockfile.unlock
58
+ end
59
+ end
60
+
61
+ end
62
+ end