hallettj-cloudrcs 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.
@@ -0,0 +1,256 @@
1
+ module ActiveRecord
2
+ module Acts #:nodoc:
3
+ module List #:nodoc:
4
+ def self.included(base)
5
+ base.extend(ClassMethods)
6
+ end
7
+
8
+ # This +acts_as+ extension provides the capabilities for sorting and reordering a number of objects in a list.
9
+ # The class that has this specified needs to have a +position+ column defined as an integer on
10
+ # the mapped database table.
11
+ #
12
+ # Todo list example:
13
+ #
14
+ # class TodoList < ActiveRecord::Base
15
+ # has_many :todo_items, :order => "position"
16
+ # end
17
+ #
18
+ # class TodoItem < ActiveRecord::Base
19
+ # belongs_to :todo_list
20
+ # acts_as_list :scope => :todo_list
21
+ # end
22
+ #
23
+ # todo_list.first.move_to_bottom
24
+ # todo_list.last.move_higher
25
+ module ClassMethods
26
+ # Configuration options are:
27
+ #
28
+ # * +column+ - specifies the column name to use for keeping the position integer (default: +position+)
29
+ # * +scope+ - restricts what is to be considered a list. Given a symbol, it'll attach <tt>_id</tt>
30
+ # (if it hasn't already been added) and use that as the foreign key restriction. It's also possible
31
+ # to give it an entire string that is interpolated if you need a tighter scope than just a foreign key.
32
+ # Example: <tt>acts_as_list :scope => 'todo_list_id = #{todo_list_id} AND completed = 0'</tt>
33
+ def acts_as_list(options = {})
34
+ configuration = { :column => "position", :scope => "1 = 1" }
35
+ configuration.update(options) if options.is_a?(Hash)
36
+
37
+ configuration[:scope] = "#{configuration[:scope]}_id".intern if configuration[:scope].is_a?(Symbol) && configuration[:scope].to_s !~ /_id$/
38
+
39
+ if configuration[:scope].is_a?(Symbol)
40
+ scope_condition_method = %(
41
+ def scope_condition
42
+ if #{configuration[:scope].to_s}.nil?
43
+ "#{configuration[:scope].to_s} IS NULL"
44
+ else
45
+ "#{configuration[:scope].to_s} = \#{#{configuration[:scope].to_s}}"
46
+ end
47
+ end
48
+ )
49
+ else
50
+ scope_condition_method = "def scope_condition() \"#{configuration[:scope]}\" end"
51
+ end
52
+
53
+ class_eval <<-EOV
54
+ include ActiveRecord::Acts::List::InstanceMethods
55
+
56
+ def acts_as_list_class
57
+ ::#{self.name}
58
+ end
59
+
60
+ def position_column
61
+ '#{configuration[:column]}'
62
+ end
63
+
64
+ #{scope_condition_method}
65
+
66
+ before_destroy :remove_from_list
67
+ before_create :add_to_list_bottom
68
+ EOV
69
+ end
70
+ end
71
+
72
+ # All the methods available to a record that has had <tt>acts_as_list</tt> specified. Each method works
73
+ # by assuming the object to be the item in the list, so <tt>chapter.move_lower</tt> would move that chapter
74
+ # lower in the list of all chapters. Likewise, <tt>chapter.first?</tt> would return +true+ if that chapter is
75
+ # the first in the list of all chapters.
76
+ module InstanceMethods
77
+ # Insert the item at the given position (defaults to the top position of 1).
78
+ def insert_at(position = 1)
79
+ insert_at_position(position)
80
+ end
81
+
82
+ # Swap positions with the next lower item, if one exists.
83
+ def move_lower
84
+ return unless lower_item
85
+
86
+ acts_as_list_class.transaction do
87
+ lower_item.decrement_position
88
+ increment_position
89
+ end
90
+ end
91
+
92
+ # Swap positions with the next higher item, if one exists.
93
+ def move_higher
94
+ return unless higher_item
95
+
96
+ acts_as_list_class.transaction do
97
+ higher_item.increment_position
98
+ decrement_position
99
+ end
100
+ end
101
+
102
+ # Move to the bottom of the list. If the item is already in the list, the items below it have their
103
+ # position adjusted accordingly.
104
+ def move_to_bottom
105
+ return unless in_list?
106
+ acts_as_list_class.transaction do
107
+ decrement_positions_on_lower_items
108
+ assume_bottom_position
109
+ end
110
+ end
111
+
112
+ # Move to the top of the list. If the item is already in the list, the items above it have their
113
+ # position adjusted accordingly.
114
+ def move_to_top
115
+ return unless in_list?
116
+ acts_as_list_class.transaction do
117
+ increment_positions_on_higher_items
118
+ assume_top_position
119
+ end
120
+ end
121
+
122
+ # Removes the item from the list.
123
+ def remove_from_list
124
+ if in_list?
125
+ decrement_positions_on_lower_items
126
+ update_attribute position_column, nil
127
+ end
128
+ end
129
+
130
+ # Increase the position of this item without adjusting the rest of the list.
131
+ def increment_position
132
+ return unless in_list?
133
+ update_attribute position_column, self.send(position_column).to_i + 1
134
+ end
135
+
136
+ # Decrease the position of this item without adjusting the rest of the list.
137
+ def decrement_position
138
+ return unless in_list?
139
+ update_attribute position_column, self.send(position_column).to_i - 1
140
+ end
141
+
142
+ # Return +true+ if this object is the first in the list.
143
+ def first?
144
+ return false unless in_list?
145
+ self.send(position_column) == 1
146
+ end
147
+
148
+ # Return +true+ if this object is the last in the list.
149
+ def last?
150
+ return false unless in_list?
151
+ self.send(position_column) == bottom_position_in_list
152
+ end
153
+
154
+ # Return the next higher item in the list.
155
+ def higher_item
156
+ return nil unless in_list?
157
+ acts_as_list_class.find(:first, :conditions =>
158
+ "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i - 1).to_s}"
159
+ )
160
+ end
161
+
162
+ # Return the next lower item in the list.
163
+ def lower_item
164
+ return nil unless in_list?
165
+ acts_as_list_class.find(:first, :conditions =>
166
+ "#{scope_condition} AND #{position_column} = #{(send(position_column).to_i + 1).to_s}"
167
+ )
168
+ end
169
+
170
+ # Test if this record is in a list
171
+ def in_list?
172
+ !send(position_column).nil?
173
+ end
174
+
175
+ private
176
+ def add_to_list_top
177
+ increment_positions_on_all_items
178
+ end
179
+
180
+ def add_to_list_bottom
181
+ self[position_column] = bottom_position_in_list.to_i + 1
182
+ end
183
+
184
+ # Overwrite this method to define the scope of the list changes
185
+ def scope_condition() "1" end
186
+
187
+ # Returns the bottom position number in the list.
188
+ # bottom_position_in_list # => 2
189
+ def bottom_position_in_list(except = nil)
190
+ item = bottom_item(except)
191
+ item ? item.send(position_column) : 0
192
+ end
193
+
194
+ # Returns the bottom item
195
+ def bottom_item(except = nil)
196
+ conditions = scope_condition
197
+ conditions = "#{conditions} AND #{self.class.primary_key} != #{except.id}" if except
198
+ acts_as_list_class.find(:first, :conditions => conditions, :order => "#{position_column} DESC")
199
+ end
200
+
201
+ # Forces item to assume the bottom position in the list.
202
+ def assume_bottom_position
203
+ update_attribute(position_column, bottom_position_in_list(self).to_i + 1)
204
+ end
205
+
206
+ # Forces item to assume the top position in the list.
207
+ def assume_top_position
208
+ update_attribute(position_column, 1)
209
+ end
210
+
211
+ # This has the effect of moving all the higher items up one.
212
+ def decrement_positions_on_higher_items(position)
213
+ acts_as_list_class.update_all(
214
+ "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} <= #{position}"
215
+ )
216
+ end
217
+
218
+ # This has the effect of moving all the lower items up one.
219
+ def decrement_positions_on_lower_items
220
+ return unless in_list?
221
+ acts_as_list_class.update_all(
222
+ "#{position_column} = (#{position_column} - 1)", "#{scope_condition} AND #{position_column} > #{send(position_column).to_i}"
223
+ )
224
+ end
225
+
226
+ # This has the effect of moving all the higher items down one.
227
+ def increment_positions_on_higher_items
228
+ return unless in_list?
229
+ acts_as_list_class.update_all(
230
+ "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} < #{send(position_column).to_i}"
231
+ )
232
+ end
233
+
234
+ # This has the effect of moving all the lower items down one.
235
+ def increment_positions_on_lower_items(position)
236
+ acts_as_list_class.update_all(
237
+ "#{position_column} = (#{position_column} + 1)", "#{scope_condition} AND #{position_column} >= #{position}"
238
+ )
239
+ end
240
+
241
+ # Increments position (<tt>position_column</tt>) of all items in the list.
242
+ def increment_positions_on_all_items
243
+ acts_as_list_class.update_all(
244
+ "#{position_column} = (#{position_column} + 1)", "#{scope_condition}"
245
+ )
246
+ end
247
+
248
+ def insert_at_position(position)
249
+ remove_from_list
250
+ increment_positions_on_lower_items(position)
251
+ self.update_attribute(position_column, position)
252
+ end
253
+ end
254
+ end
255
+ end
256
+ end
@@ -0,0 +1,2 @@
1
+ require 'active_record/acts/list'
2
+ ActiveRecord::Base.class_eval { include ActiveRecord::Acts::List }
data/lib/cloud_rcs.rb ADDED
@@ -0,0 +1,7 @@
1
+ require 'cloud_rcs/patch'
2
+ require 'cloud_rcs/primitive_patch'
3
+
4
+ patch_types_dir = File.dirname(__FILE__) + '/cloud_rcs/patch_types'
5
+ Dir.entries(patch_types_dir).each do |e|
6
+ require [patch_types_dir,e].join('/') unless e =~ /^\./
7
+ end
@@ -0,0 +1,404 @@
1
+ module CloudRCS
2
+
3
+ class CommuteException < RuntimeError
4
+ end
5
+
6
+ class ApplyException < RuntimeError
7
+ end
8
+
9
+ class ParseException < RuntimeError
10
+ end
11
+
12
+ class GenerateException < RuntimeError
13
+ end
14
+
15
+ PATCH_TYPES = []
16
+
17
+ class Patch < ActiveRecord::Base
18
+ PATCH_DATE_FORMAT = '%Y%m%d%H%M%S'
19
+
20
+ has_many(:patches,
21
+ :class_name => "PrimitivePatch",
22
+ :order => "rank",
23
+ :dependent => :destroy)
24
+
25
+ acts_as_list :scope => :owner_id
26
+
27
+ validates_presence_of :author, :name, :date
28
+ validates_presence_of :sha1
29
+ validates_associated :patches
30
+
31
+ def before_validation
32
+ self.sha1 ||= details_hash
33
+
34
+ # Hack to make sure that associated primitive patches get saved
35
+ # too.
36
+ patches.each { |p| p.patch = self }
37
+ end
38
+
39
+ # Generates a new patch that undoes the effects of this patch.
40
+ def inverse
41
+ new_patches = patches.reverse.collect do |p|
42
+ p.inverse
43
+ end
44
+ Patch.new(:author => author,
45
+ :name => name,
46
+ :date => date,
47
+ :inverted => true,
48
+ :patches => new_patches)
49
+ end
50
+
51
+ # Given another patch, generates two new patches that have the
52
+ # same effect as this patch and the given patch - except that the
53
+ # new patches are applied in reversed order. So where self is
54
+ # assumed to be applied before patch, the new analog of self is
55
+ # meant to be applied after the new analog of patch.
56
+ def commute(patch)
57
+ commuted_patches = self.patches + patch.patches
58
+
59
+ left = left_bound = self.patches.length - 1
60
+ right = left + 1
61
+ right_bound = commuted_patches.length - 1
62
+
63
+ until left_bound < 0
64
+ until left == right_bound
65
+ commuted_patches[left], commuted_patches[right] =
66
+ commuted_patches[left].commute commuted_patches[right]
67
+ left += 1
68
+ right = left + 1
69
+ end
70
+ left_bound -= 1
71
+ right_bound -= 1
72
+
73
+ left = left_bound
74
+ right = left + 1
75
+ end
76
+
77
+ patch1 = Patch.new(:author => patch.author,
78
+ :name => patch.name,
79
+ :date => patch.date,
80
+ :comment => patch.comment,
81
+ :inverted => patch.inverted,
82
+ :patches => commuted_patches[0...patch.patches.length])
83
+ patch2 = Patch.new(:author => author,
84
+ :name => name,
85
+ :date => date,
86
+ :comment => comment,
87
+ :inverted => inverted,
88
+ :patches => commuted_patches[patch.patches.length..-1])
89
+ return patch1, patch2
90
+ end
91
+
92
+ # Applies this patch a file or to an Array of files. This is
93
+ # useful for testing purposes: you can try out the patch on a copy
94
+ # of a file from the repository, without making any changes to the
95
+ # official version of the file.
96
+ def apply_to(file)
97
+ patches.each do |p|
98
+ file = p.apply_to file
99
+ end
100
+ return file
101
+ end
102
+
103
+ # Looks up the official versions of any files the patch is
104
+ # supposed to apply to, and applies the changes. The patch is
105
+ # recorded in the patch history associated with the working copy.
106
+ def apply!
107
+ patched_files = []
108
+ patches.each { |p| patched_files << p.apply! }
109
+ return patched_files
110
+ end
111
+
112
+ # Outputs the contents of the patch for writing to a file in a
113
+ # darcs-compatible format.
114
+ def to_s
115
+ "#{details} {\n" +
116
+ patches.join("\n") +
117
+ "\n}\n"
118
+ end
119
+
120
+ def gzipped_contents
121
+ Patch.deflate(to_s)
122
+ end
123
+
124
+ # Returns self as the sole element in a new array.
125
+ def to_a
126
+ [self]
127
+ end
128
+
129
+ # These two methods help to distinguish between named patches and
130
+ # primitive patches.
131
+ def named_patch?; true; end
132
+ def primitive_patch?; false; end
133
+
134
+ # Performs SHA1 digest of author and returns first 5 characters of
135
+ # the result.
136
+ def author_hash
137
+ Digest::SHA1.hexdigest(author)[0...5]
138
+ end
139
+
140
+ # Packs patch details into a single string and performs SHA1 digest
141
+ # of the contents.
142
+ def details_hash
143
+ complete_details = '%s%s%s%s%s' % [name, author, date_string,
144
+ comment ? comment.split("\n").collect do |l|
145
+ l.rstrip
146
+ end.join('') : '',
147
+ inverted ? 't' : 'f']
148
+ return Digest::SHA1.hexdigest(complete_details)
149
+ end
150
+
151
+ # Returns the patch header
152
+ def details
153
+ if comment.blank?
154
+ formatted_comment = ""
155
+ else
156
+ formatted_comment = "\n" + comment.split("\n", -1).collect do |l|
157
+ " " + l
158
+ end.join("\n") + "\n"
159
+ end
160
+ "[#{name}\n#{author}*#{inverted ? '-' : '*'}#{date_string}#{formatted_comment}]"
161
+ end
162
+
163
+ # Returns a darcs-compatible file name for this patch.
164
+ def file_name
165
+ '%s-%s-%s.gz' % [date_string, author_hash, details_hash]
166
+ end
167
+ def filename
168
+ file_name
169
+ end
170
+
171
+ # Returns true if this is the last patch in the patch history of
172
+ # the associated filesystem.
173
+ def last_patch?
174
+ following_patches.empty?
175
+ end
176
+
177
+ # Returns a list of patches that follow this one in the patch
178
+ # history.
179
+ def following_patches
180
+ return @following_patches if @following_patches
181
+ @following_patches =
182
+ Patch.find(:all, :conditions => ["owner_id = ? AND position > ?",
183
+ owner.id, position])
184
+ end
185
+
186
+ protected
187
+
188
+ def date_string
189
+ date ? date.strftime(PATCH_DATE_FORMAT) : nil
190
+ end
191
+
192
+ class << self
193
+
194
+ # Takes two files as arguments and returns a Patch that
195
+ # represents differents between the files. The first file is
196
+ # assumed to be a pristine file, and the second to be a modified
197
+ # version of the same file.
198
+ #
199
+ # Determination of which patch types best describe a change and
200
+ # how patches are generated is delegated to the individual patch
201
+ # type classes.
202
+ #
203
+ # After each patch type generates its patches, those patches are
204
+ # applied to the original file to prevent later patch types from
205
+ # performing the same change.
206
+ def generate(orig_file, changed_file, options={})
207
+
208
+ # Patch generating operations should not have destructive
209
+ # effects on the given file objects.
210
+ orig_file = orig_file.deep_clone unless orig_file.nil?
211
+ changed_file = changed_file.deep_clone unless changed_file.nil?
212
+
213
+ patch = Patch.new(options)
214
+
215
+ PATCH_TYPES.sort { |a,b| a.priority <=> b.priority }.each do |pt|
216
+ new_patches = pt.generate(orig_file, changed_file).to_a
217
+ patch.patches += new_patches
218
+ new_patches.each { |p| p.patch = patch } # Annoying, but necessary, hack
219
+ new_patches.each { |p| orig_file = p.apply_to(orig_file) }
220
+ end
221
+
222
+ # Don't return empty patches
223
+ unless patch.patches.length > 0
224
+ patch = nil
225
+ end
226
+
227
+ # After all patches are applied to the original file, it
228
+ # should be identical to the changed file.
229
+ unless changed_file == orig_file
230
+ raise GenerateException.new(true), "Patching failed! Patched version of original file does not match changed file."
231
+ end
232
+
233
+ return patch
234
+ end
235
+
236
+ # Produces a Patch object along with associated primitive
237
+ # patches by parsing an existing patch file. patch should be a
238
+ # string.
239
+ def parse(patch_file)
240
+ # Try to inflate the file contents, in case they are
241
+ # gzipped. If they are not actually gzipped, Zlib will raise
242
+ # an error.
243
+ begin
244
+ patch_file = inflate(patch_file)
245
+ rescue Zlib::GzipFile::Error
246
+ end
247
+
248
+ unless patch_file =~ /^\s*\[([^\n]+)\n([^\*]+)\*([-\*])(\d{14})\n?(.*)/m
249
+ raise "Failed to parse patch file."
250
+ end
251
+ name = $1
252
+ author = $2
253
+
254
+ # inverted is a flag indicating whether or not this patch is a
255
+ # rollback. Values can be '*', for no, or '-', for yes.
256
+ inverted = $3 == '-' ? true : false
257
+
258
+ # date is a string of digits exactly 14 characters long. Note
259
+ # that in the year 9999 this code should be revised to allow 15
260
+ # digits for date.
261
+ date = $4.to_time
262
+
263
+ # Unparsed remainder of the patch.
264
+ remaining = $5
265
+
266
+ # comment is an optional long-form explanation of the patch
267
+ # contents. It is discernable from the rest of the patch file
268
+ # by virtue of a single space placed at the beginning of every comment line.
269
+ remaining_lines = remaining.split("\n", -1)
270
+ comment_lines = []
271
+ while remaining_lines.first =~ /^ (.*)$/
272
+ comment << remaining_lines.unshift
273
+ end
274
+ comment = comment_lines.join("\n")
275
+
276
+ unless remaining =~ /^\] \{\n(.*)\n\}\s*$/m
277
+ raise "Failed to parse patch file."
278
+ end
279
+
280
+ # contents is the body of the patch. it contains a series of
281
+ # primitive patches. We will split out each primitive patch
282
+ # definition from this string and pass the results to the
283
+ # appropriate classes to be parsed there.
284
+ contents = $1
285
+
286
+ contents = contents.split "\n" unless contents.blank?
287
+ patches = []
288
+ until contents.blank?
289
+ # Find the first line of the next patch
290
+ unless contents.first =~ /^(#{patch_tokens})/
291
+ contents.shift
292
+ next
293
+ end
294
+
295
+ # Record the patch token, which tells us what type of patch
296
+ # this is; and move the line into another variable that tracks
297
+ # the contents of the current patch.
298
+ patch_token = $1
299
+ patch_contents = []
300
+ patch_contents << contents.shift
301
+
302
+ # Keep pulling out lines until we hit the end of the
303
+ # patch. The end of the patch is indicated by another patch
304
+ # token, or by the end of the file.
305
+ until contents.blank?
306
+ if contents.first =~ /^(#{patch_tokens})/
307
+ break
308
+ else
309
+ patch_contents << contents.shift
310
+ end
311
+ end
312
+
313
+ # Send the portion of the file that we just pulled out to be
314
+ # parsed by the appropriate patch class.
315
+ patches << parse_primitive_patch(patch_token, patch_contents.join("\n"))
316
+ end
317
+
318
+ return Patch.new(:author => author,
319
+ :name => name,
320
+ :date => date,
321
+ :comment => comment,
322
+ :inverted => inverted,
323
+ :patches => patches)
324
+ end
325
+
326
+ # Given two parallel lists of patches with a common ancestor,
327
+ # patches_a, and patches_b, returns a modified version of
328
+ # patches_b that has the same effects, but that will apply
329
+ # cleanly to the environment yielded by patches_a.
330
+ def merge(patches_a, patches_b)
331
+ return patches_b if patches_a.empty? or patches_b.empty?
332
+ inverse_of_a = patches_a.reverse.collect { |p| p.inverse }
333
+ commuted_b, commuted_inverse_of_a = commute(inverse_of_a, patches_b)
334
+ return commuted_b
335
+ end
336
+
337
+ # Given two lists of patches that apply cleanly one after the
338
+ # other, returns modified versions that each have the same
339
+ # effect as their original counterparts - but that apply in
340
+ # reversed order.
341
+ def commute(patches_a, patches_b)
342
+ commuted_patches = patches_a + patches_b
343
+
344
+ left = left_bound = patches_a.length - 1
345
+ right = left + 1
346
+ right_bound = commuted_patches.length - 1
347
+
348
+ until left_bound < 0
349
+ until left == right_bound
350
+ commuted_patches[left], commuted_patches[right] =
351
+ commuted_patches[left].commute commuted_patches[right]
352
+ left += 1
353
+ right = left + 1
354
+ end
355
+ left_bound -= 1
356
+ right_bound -= 1
357
+
358
+ left = left_bound
359
+ right = left + 1
360
+ end
361
+
362
+ return commuted_patches[0...patches_b.length], commuted_patches[patches_b.length..-1]
363
+ end
364
+
365
+ # Compress a string into Gzip format for writing to a .gz file.
366
+ def deflate(str)
367
+ output = String.new
368
+ StringIO.open(output) do |str_io|
369
+ gzip = Zlib::GzipWriter.new(str_io)
370
+ gzip << str
371
+ gzip.close
372
+ end
373
+ return output
374
+ end
375
+
376
+ # Decompress string from Gzip format.
377
+ def inflate(str)
378
+ StringIO.open(str, 'r') do |str_io|
379
+ gunzip = Zlib::GzipReader.new(str_io)
380
+ gunzip.read
381
+ end
382
+ end
383
+
384
+ protected
385
+
386
+ # Parse the contents of the primitive patch by locating the class
387
+ # that matches they patch_token and invoking its parse method.
388
+ def parse_primitive_patch(patch_token, contents)
389
+ patch_type = PATCH_TYPES.detect { |t| t.name =~ /^(.+::)?#{patch_token.camelize}$/ }
390
+ patch_type.parse(contents)
391
+ end
392
+
393
+ # Return patch tokens for all known patch types as a single string
394
+ # formatted for a regular expression. Tokens are joined by | so
395
+ # the regex will match any of the tokens.
396
+ def patch_tokens
397
+ PATCH_TYPES.collect { |pt| pt.name.split('::').last.downcase }.join('|')
398
+ end
399
+
400
+ end
401
+
402
+ end
403
+
404
+ end