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.
- data/History.txt +4 -0
- data/License.txt +14 -0
- data/Manifest.txt +35 -0
- data/PostInstall.txt +2 -0
- data/README.txt +293 -0
- data/Rakefile +4 -0
- data/config/hoe.rb +75 -0
- data/config/requirements.rb +15 -0
- data/lib/active_record/acts/list.rb +256 -0
- data/lib/acts_as_list.rb +2 -0
- data/lib/cloud_rcs.rb +7 -0
- data/lib/cloud_rcs/patch.rb +404 -0
- data/lib/cloud_rcs/patch_types/addfile.rb +74 -0
- data/lib/cloud_rcs/patch_types/binary.rb +232 -0
- data/lib/cloud_rcs/patch_types/hunk.rb +263 -0
- data/lib/cloud_rcs/patch_types/move.rb +89 -0
- data/lib/cloud_rcs/patch_types/rmfile.rb +63 -0
- data/lib/cloud_rcs/primitive_patch.rb +147 -0
- data/lib/cloudrcs.rb +12 -0
- data/lib/cloudrcs/version.rb +9 -0
- data/script/console +10 -0
- data/script/destroy +14 -0
- data/script/generate +14 -0
- data/script/txt2html +82 -0
- data/setup.rb +1585 -0
- data/tasks/deployment.rake +34 -0
- data/tasks/environment.rake +7 -0
- data/tasks/website.rake +17 -0
- data/test/test_cloudrcs.rb +11 -0
- data/test/test_helper.rb +2 -0
- data/website/index.html +141 -0
- data/website/index.txt +83 -0
- data/website/javascripts/rounded_corners_lite.inc.js +285 -0
- data/website/stylesheets/screen.css +138 -0
- data/website/template.html.erb +48 -0
- metadata +115 -0
@@ -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
|
data/lib/acts_as_list.rb
ADDED
data/lib/cloud_rcs.rb
ADDED
@@ -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
|