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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 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:])
170
raise MalformedLine("Unknown line type", line)
175
def __init__(self, orig_pos, orig_range, mod_pos, mod_range, tail=None):
176
self.orig_pos = orig_pos
177
self.orig_range = orig_range
178
self.mod_pos = mod_pos
179
self.mod_range = mod_range
183
def get_header(self):
184
if self.tail is None:
187
tail_str = ' ' + self.tail
188
return "@@ -%s +%s @@%s\n" % (self.range_str(self.orig_pos,
190
self.range_str(self.mod_pos,
194
def range_str(self, pos, range):
195
"""Return a file range, special-casing for 1-line files.
197
:param pos: The position in the file
199
:range: The range in the file
201
:return: a string in the format 1,4 except when range == pos == 1
206
return "%i,%i" % (pos, range)
209
lines = [self.get_header()]
210
for line in self.lines:
211
lines.append(str(line))
212
return "".join(lines)
214
def shift_to_mod(self, pos):
215
if pos < self.orig_pos-1:
217
elif pos > self.orig_pos+self.orig_range:
218
return self.mod_range - self.orig_range
220
return self.shift_to_mod_lines(pos)
222
def shift_to_mod_lines(self, pos):
223
position = self.orig_pos-1
225
for line in self.lines:
226
if isinstance(line, InsertLine):
228
elif isinstance(line, RemoveLine):
233
elif isinstance(line, ContextLine):
240
def iter_hunks(iter_lines):
242
for line in iter_lines:
250
hunk = hunk_from_header(line)
253
while orig_size < hunk.orig_range or mod_size < hunk.mod_range:
254
hunk_line = parse_line(iter_lines.next())
255
hunk.lines.append(hunk_line)
256
if isinstance(hunk_line, (RemoveLine, ContextLine)):
258
if isinstance(hunk_line, (InsertLine, ContextLine)):
265
def __init__(self, oldname, newname):
266
self.oldname = oldname
267
self.newname = newname
271
ret = self.get_header()
272
ret += "".join([str(h) for h in self.hunks])
275
def get_header(self):
276
return "--- %s\n+++ %s\n" % (self.oldname, self.newname)
278
def stats_values(self):
279
"""Calculate the number of inserts and removes."""
282
for hunk in self.hunks:
283
for line in hunk.lines:
284
if isinstance(line, InsertLine):
286
elif isinstance(line, RemoveLine):
288
return (inserts, removes, len(self.hunks))
291
"""Return a string of patch statistics"""
292
return "%i inserts, %i removes in %i hunks" % \
295
def pos_in_mod(self, position):
297
for hunk in self.hunks:
298
shift = hunk.shift_to_mod(position)
304
def iter_inserted(self):
305
"""Iteraties through inserted lines
307
:return: Pair of line number, line
308
:rtype: iterator of (int, InsertLine)
310
for hunk in self.hunks:
311
pos = hunk.mod_pos - 1;
312
for line in hunk.lines:
313
if isinstance(line, InsertLine):
316
if isinstance(line, ContextLine):
320
def parse_patch(iter_lines):
321
(orig_name, mod_name) = get_patch_names(iter_lines)
322
patch = Patch(orig_name, mod_name)
323
for hunk in iter_hunks(iter_lines):
324
patch.hunks.append(hunk)
328
def iter_file_patch(iter_lines):
331
for line in iter_lines:
332
if line.startswith('=== ') or line.startswith('*** '):
334
if line.startswith('#'):
337
if line.startswith('-') or line.startswith(' '):
339
elif line.startswith('--- '):
340
if len(saved_lines) > 0:
343
elif line.startswith('@@'):
344
hunk = hunk_from_header(line)
345
orig_range = hunk.orig_range
346
saved_lines.append(line)
347
if len(saved_lines) > 0:
351
def iter_lines_handle_nl(iter_lines):
353
Iterates through lines, ensuring that lines that originally had no
354
terminating \n are produced without one. This transformation may be
355
applied at any point up until hunk line parsing, and is safe to apply
359
for line in iter_lines:
361
if not last_line.endswith('\n'):
362
raise AssertionError()
363
last_line = last_line[:-1]
365
if last_line is not None:
368
if last_line is not None:
372
def parse_patches(iter_lines):
373
iter_lines = iter_lines_handle_nl(iter_lines)
374
return [parse_patch(f.__iter__()) for f in iter_file_patch(iter_lines)]
377
def difference_index(atext, btext):
378
"""Find the indext of the first character that differs between two texts
380
:param atext: The first text
382
:param btext: The second text
384
:return: The index, or None if there are no differences within the range
385
:rtype: int or NoneType
388
if len(btext) < length:
390
for i in range(length):
391
if atext[i] != btext[i]:
396
def iter_patched(orig_lines, patch_lines):
397
"""Iterate through a series of lines with a patch applied.
398
This handles a single file, and does exact, not fuzzy patching.
400
patch_lines = iter_lines_handle_nl(iter(patch_lines))
401
get_patch_names(patch_lines)
402
return iter_patched_from_hunks(orig_lines, iter_hunks(patch_lines))
405
def iter_patched_from_hunks(orig_lines, hunks):
406
"""Iterate through a series of lines with a patch applied.
407
This handles a single file, and does exact, not fuzzy patching.
409
:param orig_lines: The unpatched lines.
410
:param hunks: An iterable of Hunk instances.
414
if orig_lines is not None:
415
orig_lines = iter(orig_lines)
417
while line_no < hunk.orig_pos:
418
orig_line = orig_lines.next()
421
for hunk_line in hunk.lines:
422
seen_patch.append(str(hunk_line))
423
if isinstance(hunk_line, InsertLine):
424
yield hunk_line.contents
425
elif isinstance(hunk_line, (ContextLine, RemoveLine)):
426
orig_line = orig_lines.next()
427
if orig_line != hunk_line.contents:
428
raise PatchConflict(line_no, orig_line, "".join(seen_patch))
429
if isinstance(hunk_line, ContextLine):
432
if not isinstance(hunk_line, RemoveLine):
433
raise AssertionError(hunk_line)
435
if orig_lines is not None:
436
for line in orig_lines: