1
# Copyright (C) 2004 - 2006, 2008 Aaron Bentley, Canonical Ltd
2
# <aaron.bentley@utoronto.ca>
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
# GNU General Public License for more details.
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
20
class BinaryFiles(Exception):
22
def __init__(self, orig_name, mod_name):
23
self.orig_name = orig_name
24
self.mod_name = mod_name
25
Exception.__init__(self, 'Binary files section encountered.')
28
class PatchSyntax(Exception):
29
def __init__(self, msg):
30
Exception.__init__(self, msg)
33
class MalformedPatchHeader(PatchSyntax):
34
def __init__(self, desc, line):
37
msg = "Malformed patch header. %s\n%r" % (self.desc, self.line)
38
PatchSyntax.__init__(self, msg)
41
class MalformedHunkHeader(PatchSyntax):
42
def __init__(self, desc, line):
45
msg = "Malformed hunk header. %s\n%r" % (self.desc, self.line)
46
PatchSyntax.__init__(self, msg)
49
class MalformedLine(PatchSyntax):
50
def __init__(self, desc, line):
53
msg = "Malformed line. %s\n%s" % (self.desc, self.line)
54
PatchSyntax.__init__(self, msg)
57
class PatchConflict(Exception):
58
def __init__(self, line_no, orig_line, patch_line):
59
orig = orig_line.rstrip('\n')
60
patch = str(patch_line).rstrip('\n')
61
msg = 'Text contents mismatch at line %d. Original has "%s",'\
62
' but patch says it should be "%s"' % (line_no, orig, patch)
63
Exception.__init__(self, msg)
66
def get_patch_names(iter_lines):
68
line = iter_lines.next()
69
match = re.match('Binary files (.*) and (.*) differ\n', line)
71
raise BinaryFiles(match.group(1), match.group(2))
72
if not line.startswith("--- "):
73
raise MalformedPatchHeader("No orig name", line)
75
orig_name = line[4:].rstrip("\n")
77
raise MalformedPatchHeader("No orig line", "")
79
line = iter_lines.next()
80
if not line.startswith("+++ "):
81
raise PatchSyntax("No mod name")
83
mod_name = line[4:].rstrip("\n")
85
raise MalformedPatchHeader("No mod line", "")
86
return (orig_name, mod_name)
89
def parse_range(textrange):
90
"""Parse a patch range, handling the "1" special-case
92
:param textrange: The text to parse
94
:return: the position and range, as a tuple
97
tmp = textrange.split(',')
108
def hunk_from_header(line):
110
matches = re.match(r'\@\@ ([^@]*) \@\@( (.*))?\n', line)
112
raise MalformedHunkHeader("Does not match format.", line)
114
(orig, mod) = matches.group(1).split(" ")
115
except (ValueError, IndexError), e:
116
raise MalformedHunkHeader(str(e), line)
117
if not orig.startswith('-') or not mod.startswith('+'):
118
raise MalformedHunkHeader("Positions don't start with + or -.", line)
120
(orig_pos, orig_range) = parse_range(orig[1:])
121
(mod_pos, mod_range) = parse_range(mod[1:])
122
except (ValueError, IndexError), e:
123
raise MalformedHunkHeader(str(e), line)
124
if mod_range < 0 or orig_range < 0:
125
raise MalformedHunkHeader("Hunk range is negative", line)
126
tail = matches.group(3)
127
return Hunk(orig_pos, orig_range, mod_pos, mod_range, tail)
131
def __init__(self, contents):
132
self.contents = contents
134
def get_str(self, leadchar):
135
if self.contents == "\n" and leadchar == " " and False:
137
if not self.contents.endswith('\n'):
138
terminator = '\n' + NO_NL
141
return leadchar + self.contents + terminator
144
class ContextLine(HunkLine):
145
def __init__(self, contents):
146
HunkLine.__init__(self, contents)
149
return self.get_str(" ")
152
class InsertLine(HunkLine):
153
def __init__(self, contents):
154
HunkLine.__init__(self, contents)
157
return self.get_str("+")
160
class RemoveLine(HunkLine):
161
def __init__(self, contents):
162
HunkLine.__init__(self, contents)
165
return self.get_str("-")
167
NO_NL = '\\ No newline at end of file\n'
168
__pychecker__="no-returnvalues"
170
def parse_line(line):
171
if line.startswith("\n"):
172
return ContextLine(line)
173
elif line.startswith(" "):
174
return ContextLine(line[1:])
175
elif line.startswith("+"):
176
return InsertLine(line[1:])
177
elif line.startswith("-"):
178
return RemoveLine(line[1:])
180
raise MalformedLine("Unknown line type", line)
185
def __init__(self, orig_pos, orig_range, mod_pos, mod_range, tail=None):
186
self.orig_pos = orig_pos
187
self.orig_range = orig_range
188
self.mod_pos = mod_pos
189
self.mod_range = mod_range
193
def get_header(self):
194
if self.tail is None:
197
tail_str = ' ' + self.tail
198
return "@@ -%s +%s @@%s\n" % (self.range_str(self.orig_pos,
200
self.range_str(self.mod_pos,
204
def range_str(self, pos, range):
205
"""Return a file range, special-casing for 1-line files.
207
:param pos: The position in the file
209
:range: The range in the file
211
:return: a string in the format 1,4 except when range == pos == 1
216
return "%i,%i" % (pos, range)
219
lines = [self.get_header()]
220
for line in self.lines:
221
lines.append(str(line))
222
return "".join(lines)
224
def shift_to_mod(self, pos):
225
if pos < self.orig_pos-1:
227
elif pos > self.orig_pos+self.orig_range:
228
return self.mod_range - self.orig_range
230
return self.shift_to_mod_lines(pos)
232
def shift_to_mod_lines(self, pos):
233
position = self.orig_pos-1
235
for line in self.lines:
236
if isinstance(line, InsertLine):
238
elif isinstance(line, RemoveLine):
243
elif isinstance(line, ContextLine):
250
def iter_hunks(iter_lines):
252
for line in iter_lines:
260
hunk = hunk_from_header(line)
263
while orig_size < hunk.orig_range or mod_size < hunk.mod_range:
264
hunk_line = parse_line(iter_lines.next())
265
hunk.lines.append(hunk_line)
266
if isinstance(hunk_line, (RemoveLine, ContextLine)):
268
if isinstance(hunk_line, (InsertLine, ContextLine)):
274
class BinaryPatch(object):
275
def __init__(self, oldname, newname):
276
self.oldname = oldname
277
self.newname = newname
280
return 'Binary files %s and %s differ\n' % (self.oldname, self.newname)
283
class Patch(BinaryPatch):
285
def __init__(self, oldname, newname):
286
BinaryPatch.__init__(self, oldname, newname)
290
ret = self.get_header()
291
ret += "".join([str(h) for h in self.hunks])
294
def get_header(self):
295
return "--- %s\n+++ %s\n" % (self.oldname, self.newname)
297
def stats_values(self):
298
"""Calculate the number of inserts and removes."""
301
for hunk in self.hunks:
302
for line in hunk.lines:
303
if isinstance(line, InsertLine):
305
elif isinstance(line, RemoveLine):
307
return (inserts, removes, len(self.hunks))
310
"""Return a string of patch statistics"""
311
return "%i inserts, %i removes in %i hunks" % \
314
def pos_in_mod(self, position):
316
for hunk in self.hunks:
317
shift = hunk.shift_to_mod(position)
323
def iter_inserted(self):
324
"""Iteraties through inserted lines
326
:return: Pair of line number, line
327
:rtype: iterator of (int, InsertLine)
329
for hunk in self.hunks:
330
pos = hunk.mod_pos - 1;
331
for line in hunk.lines:
332
if isinstance(line, InsertLine):
335
if isinstance(line, ContextLine):
339
def parse_patch(iter_lines):
340
iter_lines = iter_lines_handle_nl(iter_lines)
342
(orig_name, mod_name) = get_patch_names(iter_lines)
343
except BinaryFiles, e:
344
return BinaryPatch(e.orig_name, e.mod_name)
346
patch = Patch(orig_name, mod_name)
347
for hunk in iter_hunks(iter_lines):
348
patch.hunks.append(hunk)
352
def iter_file_patch(iter_lines):
355
for line in iter_lines:
356
if line.startswith('=== ') or line.startswith('*** '):
358
if line.startswith('#'):
361
if line.startswith('-') or line.startswith(' '):
363
elif line.startswith('--- '):
364
if len(saved_lines) > 0:
367
elif line.startswith('@@'):
368
hunk = hunk_from_header(line)
369
orig_range = hunk.orig_range
370
saved_lines.append(line)
371
if len(saved_lines) > 0:
375
def iter_lines_handle_nl(iter_lines):
377
Iterates through lines, ensuring that lines that originally had no
378
terminating \n are produced without one. This transformation may be
379
applied at any point up until hunk line parsing, and is safe to apply
383
for line in iter_lines:
385
if not last_line.endswith('\n'):
386
raise AssertionError()
387
last_line = last_line[:-1]
389
if last_line is not None:
392
if last_line is not None:
396
def parse_patches(iter_lines):
397
return [parse_patch(f.__iter__()) for f in iter_file_patch(iter_lines)]
400
def difference_index(atext, btext):
401
"""Find the indext of the first character that differs between two texts
403
:param atext: The first text
405
:param btext: The second text
407
:return: The index, or None if there are no differences within the range
408
:rtype: int or NoneType
411
if len(btext) < length:
413
for i in range(length):
414
if atext[i] != btext[i]:
419
def iter_patched(orig_lines, patch_lines):
420
"""Iterate through a series of lines with a patch applied.
421
This handles a single file, and does exact, not fuzzy patching.
423
patch_lines = iter_lines_handle_nl(iter(patch_lines))
424
get_patch_names(patch_lines)
425
return iter_patched_from_hunks(orig_lines, iter_hunks(patch_lines))
428
def iter_patched_from_hunks(orig_lines, hunks):
429
"""Iterate through a series of lines with a patch applied.
430
This handles a single file, and does exact, not fuzzy patching.
432
:param orig_lines: The unpatched lines.
433
:param hunks: An iterable of Hunk instances.
437
if orig_lines is not None:
438
orig_lines = iter(orig_lines)
440
while line_no < hunk.orig_pos:
441
orig_line = orig_lines.next()
444
for hunk_line in hunk.lines:
445
seen_patch.append(str(hunk_line))
446
if isinstance(hunk_line, InsertLine):
447
yield hunk_line.contents
448
elif isinstance(hunk_line, (ContextLine, RemoveLine)):
449
orig_line = orig_lines.next()
450
if orig_line != hunk_line.contents:
451
raise PatchConflict(line_no, orig_line, "".join(seen_patch))
452
if isinstance(hunk_line, ContextLine):
455
if not isinstance(hunk_line, RemoveLine):
456
raise AssertionError(hunk_line)
458
if orig_lines is not None:
459
for line in orig_lines: