Class: CloudRCS::Patch
- Inherits:
-
ActiveRecord::Base
- Object
- ActiveRecord::Base
- CloudRCS::Patch
- Defined in:
- lib/cloud_rcs/patch.rb
Constant Summary collapse
- PATCH_DATE_FORMAT =
'%Y%m%d%H%M%S'
Class Method Summary collapse
-
.commute(patches_a, patches_b) ⇒ Object
Given two lists of patches that apply cleanly one after the other, returns modified versions that each have the same effect as their original counterparts - but that apply in reversed order.
-
.deflate(str) ⇒ Object
Compress a string into Gzip format for writing to a .gz file.
-
.generate(orig_file, changed_file, options = {}) ⇒ Object
Takes two files as arguments and returns a Patch that represents differents between the files.
-
.inflate(str) ⇒ Object
Decompress string from Gzip format.
-
.merge(patches_a, patches_b) ⇒ Object
Given two parallel lists of patches with a common ancestor, patches_a, and patches_b, returns a modified version of patches_b that has the same effects, but that will apply cleanly to the environment yielded by patches_a.
-
.parse(patch_file) ⇒ Object
Produces a Patch object along with associated primitive patches by parsing an existing patch file.
Instance Method Summary collapse
-
#apply! ⇒ Object
Looks up the official versions of any files the patch is supposed to apply to, and applies the changes.
-
#apply_to(file) ⇒ Object
Applies this patch a file or to an Array of files.
-
#author_hash ⇒ Object
Performs SHA1 digest of author and returns first 5 characters of the result.
- #before_validation ⇒ Object
-
#commute(patch) ⇒ Object
Given another patch, generates two new patches that have the same effect as this patch and the given patch - except that the new patches are applied in reversed order.
-
#details ⇒ Object
Returns the patch header.
-
#details_hash ⇒ Object
Packs patch details into a single string and performs SHA1 digest of the contents.
-
#file_name ⇒ Object
Returns a darcs-compatible file name for this patch.
- #filename ⇒ Object
-
#following_patches ⇒ Object
Returns a list of patches that follow this one in the patch history.
- #gzipped_contents ⇒ Object
-
#inverse ⇒ Object
Generates a new patch that undoes the effects of this patch.
-
#last_patch? ⇒ Boolean
Returns true if this is the last patch in the patch history of the associated filesystem.
-
#named_patch? ⇒ Boolean
These two methods help to distinguish between named patches and primitive patches.
- #primitive_patch? ⇒ Boolean
-
#to_a ⇒ Object
Returns self as the sole element in a new array.
-
#to_s ⇒ Object
Outputs the contents of the patch for writing to a file in a darcs-compatible format.
Class Method Details
.commute(patches_a, patches_b) ⇒ Object
Given two lists of patches that apply cleanly one after the other, returns modified versions that each have the same effect as their original counterparts - but that apply in reversed order.
341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 |
# File 'lib/cloud_rcs/patch.rb', line 341 def commute(patches_a, patches_b) commuted_patches = patches_a + patches_b left = left_bound = patches_a.length - 1 right = left + 1 right_bound = commuted_patches.length - 1 until left_bound < 0 until left == right_bound commuted_patches[left], commuted_patches[right] = commuted_patches[left].commute commuted_patches[right] left += 1 right = left + 1 end left_bound -= 1 right_bound -= 1 left = left_bound right = left + 1 end return commuted_patches[0...patches_b.length], commuted_patches[patches_b.length..-1] end |
.deflate(str) ⇒ Object
Compress a string into Gzip format for writing to a .gz file.
366 367 368 369 370 371 372 373 374 |
# File 'lib/cloud_rcs/patch.rb', line 366 def deflate(str) output = String.new StringIO.open(output) do |str_io| gzip = Zlib::GzipWriter.new(str_io) gzip << str gzip.close end return output end |
.generate(orig_file, changed_file, options = {}) ⇒ Object
Takes two files as arguments and returns a Patch that represents differents between the files. The first file is assumed to be a pristine file, and the second to be a modified version of the same file.
Determination of which patch types best describe a change and how patches are generated is delegated to the individual patch type classes.
After each patch type generates its patches, those patches are applied to the original file to prevent later patch types from performing the same change.
206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 |
# File 'lib/cloud_rcs/patch.rb', line 206 def generate(orig_file, changed_file, ={}) # Patch generating operations should not have destructive # effects on the given file objects. orig_file = orig_file.deep_clone unless orig_file.nil? changed_file = changed_file.deep_clone unless changed_file.nil? patch = Patch.new() PATCH_TYPES.sort { |a,b| a.priority <=> b.priority }.each do |pt| new_patches = pt.generate(orig_file, changed_file).to_a patch.patches += new_patches new_patches.each { |p| p.patch = patch } # Annoying, but necessary, hack new_patches.each { |p| orig_file = p.apply_to(orig_file) } end # Don't return empty patches unless patch.patches.length > 0 patch = nil end # After all patches are applied to the original file, it # should be identical to the changed file. unless changed_file == orig_file raise GenerateException.new(true), "Patching failed! Patched version of original file does not match changed file." end return patch end |
.inflate(str) ⇒ Object
Decompress string from Gzip format.
377 378 379 380 381 382 |
# File 'lib/cloud_rcs/patch.rb', line 377 def inflate(str) StringIO.open(str, 'r') do |str_io| gunzip = Zlib::GzipReader.new(str_io) gunzip.read end end |
.merge(patches_a, patches_b) ⇒ Object
Given two parallel lists of patches with a common ancestor, patches_a, and patches_b, returns a modified version of patches_b that has the same effects, but that will apply cleanly to the environment yielded by patches_a.
330 331 332 333 334 335 |
# File 'lib/cloud_rcs/patch.rb', line 330 def merge(patches_a, patches_b) return patches_b if patches_a.empty? or patches_b.empty? inverse_of_a = patches_a.reverse.collect { |p| p.inverse } commuted_b, commuted_inverse_of_a = commute(inverse_of_a, patches_b) return commuted_b end |
.parse(patch_file) ⇒ Object
Produces a Patch object along with associated primitive patches by parsing an existing patch file. patch should be a string.
239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 |
# File 'lib/cloud_rcs/patch.rb', line 239 def parse(patch_file) # Try to inflate the file contents, in case they are # gzipped. If they are not actually gzipped, Zlib will raise # an error. begin patch_file = inflate(patch_file) rescue Zlib::GzipFile::Error end unless patch_file =~ /^\s*\[([^\n]+)\n([^\*]+)\*([-\*])(\d{14})\n?(.*)/m raise "Failed to parse patch file." end name = $1 = $2 # inverted is a flag indicating whether or not this patch is a # rollback. Values can be '*', for no, or '-', for yes. inverted = $3 == '-' ? true : false # date is a string of digits exactly 14 characters long. Note # that in the year 9999 this code should be revised to allow 15 # digits for date. date = $4.to_time # Unparsed remainder of the patch. remaining = $5 # comment is an optional long-form explanation of the patch # contents. It is discernable from the rest of the patch file # by virtue of a single space placed at the beginning of every comment line. remaining_lines = remaining.split("\n", -1) comment_lines = [] while remaining_lines.first =~ /^ (.*)$/ comment << remaining_lines.unshift end comment = comment_lines.join("\n") unless remaining =~ /^\] \{\n(.*)\n\}\s*$/m raise "Failed to parse patch file." end # contents is the body of the patch. it contains a series of # primitive patches. We will split out each primitive patch # definition from this string and pass the results to the # appropriate classes to be parsed there. contents = $1 contents = contents.split "\n" unless contents.blank? patches = [] until contents.blank? # Find the first line of the next patch unless contents.first =~ /^(#{patch_tokens})/ contents.shift next end # Record the patch token, which tells us what type of patch # this is; and move the line into another variable that tracks # the contents of the current patch. patch_token = $1 patch_contents = [] patch_contents << contents.shift # Keep pulling out lines until we hit the end of the # patch. The end of the patch is indicated by another patch # token, or by the end of the file. until contents.blank? if contents.first =~ /^(#{patch_tokens})/ break else patch_contents << contents.shift end end # Send the portion of the file that we just pulled out to be # parsed by the appropriate patch class. patches << parse_primitive_patch(patch_token, patch_contents.join("\n")) end return Patch.new(:author => , :name => name, :date => date, :comment => comment, :inverted => inverted, :patches => patches) end |
Instance Method Details
#apply! ⇒ Object
Looks up the official versions of any files the patch is supposed to apply to, and applies the changes. The patch is recorded in the patch history associated with the working copy.
106 107 108 109 110 |
# File 'lib/cloud_rcs/patch.rb', line 106 def apply! patched_files = [] patches.each { |p| patched_files << p.apply! } return patched_files end |
#apply_to(file) ⇒ Object
Applies this patch a file or to an Array of files. This is useful for testing purposes: you can try out the patch on a copy of a file from the repository, without making any changes to the official version of the file.
96 97 98 99 100 101 |
# File 'lib/cloud_rcs/patch.rb', line 96 def apply_to(file) patches.each do |p| file = p.apply_to file end return file end |
#author_hash ⇒ Object
Performs SHA1 digest of author and returns first 5 characters of the result.
136 137 138 |
# File 'lib/cloud_rcs/patch.rb', line 136 def Digest::SHA1.hexdigest()[0...5] end |
#before_validation ⇒ Object
31 32 33 34 35 36 37 |
# File 'lib/cloud_rcs/patch.rb', line 31 def before_validation self.sha1 ||= details_hash # Hack to make sure that associated primitive patches get saved # too. patches.each { |p| p.patch = self } end |
#commute(patch) ⇒ Object
Given another patch, generates two new patches that have the same effect as this patch and the given patch - except that the new patches are applied in reversed order. So where self is assumed to be applied before patch, the new analog of self is meant to be applied after the new analog of patch.
56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 |
# File 'lib/cloud_rcs/patch.rb', line 56 def commute(patch) commuted_patches = self.patches + patch.patches left = left_bound = self.patches.length - 1 right = left + 1 right_bound = commuted_patches.length - 1 until left_bound < 0 until left == right_bound commuted_patches[left], commuted_patches[right] = commuted_patches[left].commute commuted_patches[right] left += 1 right = left + 1 end left_bound -= 1 right_bound -= 1 left = left_bound right = left + 1 end patch1 = Patch.new(:author => patch., :name => patch.name, :date => patch.date, :comment => patch.comment, :inverted => patch.inverted, :patches => commuted_patches[0...patch.patches.length]) patch2 = Patch.new(:author => , :name => name, :date => date, :comment => comment, :inverted => inverted, :patches => commuted_patches[patch.patches.length..-1]) return patch1, patch2 end |
#details ⇒ Object
Returns the patch header
152 153 154 155 156 157 158 159 160 161 |
# File 'lib/cloud_rcs/patch.rb', line 152 def details if comment.blank? formatted_comment = "" else formatted_comment = "\n" + comment.split("\n", -1).collect do |l| " " + l end.join("\n") + "\n" end "[#{name}\n#{}*#{inverted ? '-' : '*'}#{date_string}#{formatted_comment}]" end |
#details_hash ⇒ Object
Packs patch details into a single string and performs SHA1 digest of the contents.
142 143 144 145 146 147 148 149 |
# File 'lib/cloud_rcs/patch.rb', line 142 def details_hash complete_details = '%s%s%s%s%s' % [name, , date_string, comment ? comment.split("\n").collect do |l| l.rstrip end.join('') : '', inverted ? 't' : 'f'] return Digest::SHA1.hexdigest(complete_details) end |
#file_name ⇒ Object
Returns a darcs-compatible file name for this patch.
164 165 166 |
# File 'lib/cloud_rcs/patch.rb', line 164 def file_name '%s-%s-%s.gz' % [date_string, , details_hash] end |
#filename ⇒ Object
167 168 169 |
# File 'lib/cloud_rcs/patch.rb', line 167 def filename file_name end |
#following_patches ⇒ Object
Returns a list of patches that follow this one in the patch history.
179 180 181 182 183 184 |
# File 'lib/cloud_rcs/patch.rb', line 179 def following_patches return @following_patches if @following_patches @following_patches = Patch.find(:all, :conditions => ["owner_id = ? AND position > ?", owner.id, position]) end |
#gzipped_contents ⇒ Object
120 121 122 |
# File 'lib/cloud_rcs/patch.rb', line 120 def gzipped_contents Patch.deflate(to_s) end |
#inverse ⇒ Object
Generates a new patch that undoes the effects of this patch.
40 41 42 43 44 45 46 47 48 49 |
# File 'lib/cloud_rcs/patch.rb', line 40 def inverse new_patches = patches.reverse.collect do |p| p.inverse end Patch.new(:author => , :name => name, :date => date, :inverted => true, :patches => new_patches) end |
#last_patch? ⇒ Boolean
Returns true if this is the last patch in the patch history of the associated filesystem.
173 174 175 |
# File 'lib/cloud_rcs/patch.rb', line 173 def last_patch? following_patches.empty? end |
#named_patch? ⇒ Boolean
These two methods help to distinguish between named patches and primitive patches.
131 |
# File 'lib/cloud_rcs/patch.rb', line 131 def named_patch?; true; end |
#primitive_patch? ⇒ Boolean
132 |
# File 'lib/cloud_rcs/patch.rb', line 132 def primitive_patch?; false; end |
#to_a ⇒ Object
Returns self as the sole element in a new array.
125 126 127 |
# File 'lib/cloud_rcs/patch.rb', line 125 def to_a [self] end |
#to_s ⇒ Object
Outputs the contents of the patch for writing to a file in a darcs-compatible format.
114 115 116 117 118 |
# File 'lib/cloud_rcs/patch.rb', line 114 def to_s "#{details} {\n" + patches.join("\n") + "\n}\n" end |