Class: CloudRCS::Patch

Inherits:
ActiveRecord::Base
  • Object
show all
Defined in:
lib/cloud_rcs/patch.rb

Constant Summary collapse

PATCH_DATE_FORMAT =
'%Y%m%d%H%M%S'

Class Method Summary collapse

Instance Method Summary collapse

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, options={})

  # 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(options)
  
  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
  author = $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 => 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_hashObject

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 author_hash
  Digest::SHA1.hexdigest(author)[0...5]
end

#before_validationObject



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.author,
                     :name => patch.name,
                     :date => patch.date,
                     :comment => patch.comment,
                     :inverted => patch.inverted,
                     :patches => commuted_patches[0...patch.patches.length])
  patch2 = Patch.new(:author => author,
                     :name => name,
                     :date => date,
                     :comment => comment,
                     :inverted => inverted,
                     :patches => commuted_patches[patch.patches.length..-1])
  return patch1, patch2
end

#detailsObject

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#{author}*#{inverted ? '-' : '*'}#{date_string}#{formatted_comment}]"
end

#details_hashObject

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, author, date_string, 
                                     comment ? comment.split("\n").collect do |l| 
                                       l.rstrip 
                                     end.join('') : '',
                                     inverted ? 't' : 'f']
  return Digest::SHA1.hexdigest(complete_details)
end

#file_nameObject

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, author_hash, details_hash]
end

#filenameObject



167
168
169
# File 'lib/cloud_rcs/patch.rb', line 167

def filename
  file_name
end

#following_patchesObject

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_contentsObject



120
121
122
# File 'lib/cloud_rcs/patch.rb', line 120

def gzipped_contents
  Patch.deflate(to_s)
end

#inverseObject

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 => 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.

Returns:

  • (Boolean)


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.

Returns:

  • (Boolean)


131
# File 'lib/cloud_rcs/patch.rb', line 131

def named_patch?; true; end

#primitive_patch?Boolean

Returns:

  • (Boolean)


132
# File 'lib/cloud_rcs/patch.rb', line 132

def primitive_patch?; false; end

#to_aObject

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_sObject

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