gitmodel 0.0.1

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