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
19
class PatchSyntax(Exception):
20
def __init__(self, msg):
21
Exception.__init__(self, msg)
24
class MalformedPatchHeader(PatchSyntax):
25
def __init__(self, desc, line):
28
msg = "Malformed patch header. %s\n%r" % (self.desc, self.line)
29
PatchSyntax.__init__(self, msg)
32
class MalformedHunkHeader(PatchSyntax):
33
def __init__(self, desc, line):
36
msg = "Malformed hunk header. %s\n%r" % (self.desc, self.line)
37
PatchSyntax.__init__(self, msg)
40
class MalformedLine(PatchSyntax):
41
def __init__(self, desc, line):
44
msg = "Malformed line. %s\n%s" % (self.desc, self.line)
45
PatchSyntax.__init__(self, msg)
48
class PatchConflict(Exception):
49
def __init__(self, line_no, orig_line, patch_line):
50
orig = orig_line.rstrip('\n')
51
patch = str(patch_line).rstrip('\n')
52
msg = 'Text contents mismatch at line %d. Original has "%s",'\
53
' but patch says it should be "%s"' % (line_no, orig, patch)
54
Exception.__init__(self, msg)
57
def get_patch_names(iter_lines):
59
line = iter_lines.next()
60
if not line.startswith("--- "):
61
raise MalformedPatchHeader("No orig name", line)
63
orig_name = line[4:].rstrip("\n")
65
raise MalformedPatchHeader("No orig line", "")
67
line = iter_lines.next()
68
if not line.startswith("+++ "):
69
raise PatchSyntax("No mod name")
71
mod_name = line[4:].rstrip("\n")
73
raise MalformedPatchHeader("No mod line", "")
74
return (orig_name, mod_name)
77
def parse_range(textrange):
78
"""Parse a patch range, handling the "1" special-case
80
:param textrange: The text to parse
82
:return: the position and range, as a tuple
85
tmp = textrange.split(',')
96
def hunk_from_header(line):
98
matches = re.match(r'\@\@ ([^@]*) \@\@( (.*))?\n', line)
100
raise MalformedHunkHeader("Does not match format.", line)
102
(orig, mod) = matches.group(1).split(" ")
103
except (ValueError, IndexError), e:
104
raise MalformedHunkHeader(str(e), line)
105
if not orig.startswith('-') or not mod.startswith('+'):
106
raise MalformedHunkHeader("Positions don't start with + or -.", line)
108
(orig_pos, orig_range) = parse_range(orig[1:])
109
(mod_pos, mod_range) = parse_range(mod[1:])
110
except (ValueError, IndexError), e:
111
raise MalformedHunkHeader(str(e), line)
112
if mod_range < 0 or orig_range < 0:
113
raise MalformedHunkHeader("Hunk range is negative", line)
114
tail = matches.group(3)
115
return Hunk(orig_pos, orig_range, mod_pos, mod_range, tail)
119
def __init__(self, contents):
120
self.contents = contents
122
def get_str(self, leadchar):
123
if self.contents == "\n" and leadchar == " " and False:
125
if not self.contents.endswith('\n'):
126
terminator = '\n' + NO_NL
129
return leadchar + self.contents + terminator
132
class ContextLine(HunkLine):
133
def __init__(self, contents):
134
HunkLine.__init__(self, contents)
137
return self.get_str(" ")
140
class InsertLine(HunkLine):
141
def __init__(self, contents):
142
HunkLine.__init__(self, contents)
145
return self.get_str("+")
148
class RemoveLine(HunkLine):
149
def __init__(self, contents):
150
HunkLine.__init__(self, contents)
153
return self.get_str("-")
155
NO_NL = '\\ No newline at end of file\n'
156
__pychecker__="no-returnvalues"
158
def parse_line(line):
159
if line.startswith("\n"):
160
return ContextLine(line)
161
elif line.startswith(" "):
162
return ContextLine(line[1:])
163
elif line.startswith("+"):
164
return InsertLine(line[1:])
165
elif line.startswith("-"):
166
return RemoveLine(line[1:])
168
raise MalformedLine("Unknown line type", line)
173
def __init__(self, orig_pos, orig_range, mod_pos, mod_range, tail=None):
174
self.orig_pos = orig_pos
175
self.orig_range = orig_range
176
self.mod_pos = mod_pos
177
self.mod_range = mod_range
181
def get_header(self):
182
if self.tail is None:
185
tail_str = ' ' + self.tail
186
return "@@ -%s +%s @@%s\n" % (self.range_str(self.orig_pos,
188
self.range_str(self.mod_pos,
192
def range_str(self, pos, range):
193
"""Return a file range, special-casing for 1-line files.
195
:param pos: The position in the file
197
:range: The range in the file
199
:return: a string in the format 1,4 except when range == pos == 1
204
return "%i,%i" % (pos, range)
207
lines = [self.get_header()]
208
for line in self.lines:
209
lines.append(str(line))
210
return "".join(lines)
212
def shift_to_mod(self, pos):
213
if pos < self.orig_pos-1:
215
elif pos > self.orig_pos+self.orig_range:
216
return self.mod_range - self.orig_range
218
return self.shift_to_mod_lines(pos)
220
def shift_to_mod_lines(self, pos):
221
position = self.orig_pos-1
223
for line in self.lines:
224
if isinstance(line, InsertLine):
226
elif isinstance(line, RemoveLine):
231
elif isinstance(line, ContextLine):
238
def iter_hunks(iter_lines):
240
for line in iter_lines:
248
hunk = hunk_from_header(line)
251
while orig_size < hunk.orig_range or mod_size < hunk.mod_range:
252
hunk_line = parse_line(iter_lines.next())
253
hunk.lines.append(hunk_line)
254
if isinstance(hunk_line, (RemoveLine, ContextLine)):
256
if isinstance(hunk_line, (InsertLine, ContextLine)):
263
def __init__(self, oldname, newname):
264
self.oldname = oldname
265
self.newname = newname
269
ret = self.get_header()
270
ret += "".join([str(h) for h in self.hunks])
273
def get_header(self):
274
return "--- %s\n+++ %s\n" % (self.oldname, self.newname)
276
def stats_values(self):
277
"""Calculate the number of inserts and removes."""
280
for hunk in self.hunks:
281
for line in hunk.lines:
282
if isinstance(line, InsertLine):
284
elif isinstance(line, RemoveLine):
286
return (inserts, removes, len(self.hunks))
289
"""Return a string of patch statistics"""
290
return "%i inserts, %i removes in %i hunks" % \
293
def pos_in_mod(self, position):
295
for hunk in self.hunks:
296
shift = hunk.shift_to_mod(position)
302
def iter_inserted(self):
303
"""Iteraties through inserted lines
305
:return: Pair of line number, line
306
:rtype: iterator of (int, InsertLine)
308
for hunk in self.hunks:
309
pos = hunk.mod_pos - 1;
310
for line in hunk.lines:
311
if isinstance(line, InsertLine):
314
if isinstance(line, ContextLine):
318
def parse_patch(iter_lines):
319
iter_lines = iter_lines_handle_nl(iter_lines)
320
(orig_name, mod_name) = get_patch_names(iter_lines)
321
patch = Patch(orig_name, mod_name)
322
for hunk in iter_hunks(iter_lines):
323
patch.hunks.append(hunk)
327
def iter_file_patch(iter_lines):
330
for line in iter_lines:
331
if line.startswith('=== ') or line.startswith('*** '):
333
if line.startswith('#'):
336
if line.startswith('-') or line.startswith(' '):
338
elif line.startswith('--- '):
339
if len(saved_lines) > 0:
342
elif line.startswith('@@'):
343
hunk = hunk_from_header(line)
344
orig_range = hunk.orig_range
345
saved_lines.append(line)
346
if len(saved_lines) > 0:
350
def iter_lines_handle_nl(iter_lines):
352
Iterates through lines, ensuring that lines that originally had no
353
terminating \n are produced without one. This transformation may be
354
applied at any point up until hunk line parsing, and is safe to apply
358
for line in iter_lines:
360
if not last_line.endswith('\n'):
361
raise AssertionError()
362
last_line = last_line[:-1]
364
if last_line is not None:
367
if last_line is not None:
371
def parse_patches(iter_lines):
372
return [parse_patch(f.__iter__()) for f in iter_file_patch(iter_lines)]
375
def difference_index(atext, btext):
376
"""Find the indext of the first character that differs between two texts
378
:param atext: The first text
380
:param btext: The second text
382
:return: The index, or None if there are no differences within the range
383
:rtype: int or NoneType
386
if len(btext) < length:
388
for i in range(length):
389
if atext[i] != btext[i]:
394
def iter_patched(orig_lines, patch_lines):
395
"""Iterate through a series of lines with a patch applied.
396
This handles a single file, and does exact, not fuzzy patching.
398
patch_lines = iter_lines_handle_nl(iter(patch_lines))
399
get_patch_names(patch_lines)
400
return iter_patched_from_hunks(orig_lines, iter_hunks(patch_lines))
403
def iter_patched_from_hunks(orig_lines, hunks):
404
"""Iterate through a series of lines with a patch applied.
405
This handles a single file, and does exact, not fuzzy patching.
407
:param orig_lines: The unpatched lines.
408
:param hunks: An iterable of Hunk instances.
412
if orig_lines is not None:
413
orig_lines = iter(orig_lines)
415
while line_no < hunk.orig_pos:
416
orig_line = orig_lines.next()
419
for hunk_line in hunk.lines:
420
seen_patch.append(str(hunk_line))
421
if isinstance(hunk_line, InsertLine):
422
yield hunk_line.contents
423
elif isinstance(hunk_line, (ContextLine, RemoveLine)):
424
orig_line = orig_lines.next()
425
if orig_line != hunk_line.contents:
426
raise PatchConflict(line_no, orig_line, "".join(seen_patch))
427
if isinstance(hunk_line, ContextLine):
430
if not isinstance(hunk_line, RemoveLine):
431
raise AssertionError(hunk_line)
433
if orig_lines is not None:
434
for line in orig_lines: