1
# Copyright (C) 2004 - 2006 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):
97
if not line.startswith("@@") or not line.endswith("@@\n") \
99
raise MalformedHunkHeader("Does not start and end with @@.", line)
101
(orig, mod) = line[3:-4].split(" ")
103
raise MalformedHunkHeader(str(e), line)
104
if not orig.startswith('-') or not mod.startswith('+'):
105
raise MalformedHunkHeader("Positions don't start with + or -.", line)
107
(orig_pos, orig_range) = parse_range(orig[1:])
108
(mod_pos, mod_range) = parse_range(mod[1:])
110
raise MalformedHunkHeader(str(e), line)
111
if mod_range < 0 or orig_range < 0:
112
raise MalformedHunkHeader("Hunk range is negative", line)
113
return Hunk(orig_pos, orig_range, mod_pos, mod_range)
117
def __init__(self, contents):
118
self.contents = contents
120
def get_str(self, leadchar):
121
if self.contents == "\n" and leadchar == " " and False:
123
if not self.contents.endswith('\n'):
124
terminator = '\n' + NO_NL
127
return leadchar + self.contents + terminator
130
class ContextLine(HunkLine):
131
def __init__(self, contents):
132
HunkLine.__init__(self, contents)
135
return self.get_str(" ")
138
class InsertLine(HunkLine):
139
def __init__(self, contents):
140
HunkLine.__init__(self, contents)
143
return self.get_str("+")
146
class RemoveLine(HunkLine):
147
def __init__(self, contents):
148
HunkLine.__init__(self, contents)
151
return self.get_str("-")
153
NO_NL = '\\ No newline at end of file\n'
154
__pychecker__="no-returnvalues"
156
def parse_line(line):
157
if line.startswith("\n"):
158
return ContextLine(line)
159
elif line.startswith(" "):
160
return ContextLine(line[1:])
161
elif line.startswith("+"):
162
return InsertLine(line[1:])
163
elif line.startswith("-"):
164
return RemoveLine(line[1:])
168
raise MalformedLine("Unknown line type", line)
173
def __init__(self, orig_pos, orig_range, mod_pos, mod_range):
174
self.orig_pos = orig_pos
175
self.orig_range = orig_range
176
self.mod_pos = mod_pos
177
self.mod_range = mod_range
180
def get_header(self):
181
return "@@ -%s +%s @@\n" % (self.range_str(self.orig_pos,
183
self.range_str(self.mod_pos,
186
def range_str(self, pos, range):
187
"""Return a file range, special-casing for 1-line files.
189
:param pos: The position in the file
191
:range: The range in the file
193
:return: a string in the format 1,4 except when range == pos == 1
198
return "%i,%i" % (pos, range)
201
lines = [self.get_header()]
202
for line in self.lines:
203
lines.append(str(line))
204
return "".join(lines)
206
def shift_to_mod(self, pos):
207
if pos < self.orig_pos-1:
209
elif pos > self.orig_pos+self.orig_range:
210
return self.mod_range - self.orig_range
212
return self.shift_to_mod_lines(pos)
214
def shift_to_mod_lines(self, pos):
215
assert (pos >= self.orig_pos-1 and pos <= self.orig_pos+self.orig_range)
216
position = self.orig_pos-1
218
for line in self.lines:
219
if isinstance(line, InsertLine):
221
elif isinstance(line, RemoveLine):
226
elif isinstance(line, ContextLine):
233
def iter_hunks(iter_lines):
235
for line in iter_lines:
243
hunk = hunk_from_header(line)
246
while orig_size < hunk.orig_range or mod_size < hunk.mod_range:
247
hunk_line = parse_line(iter_lines.next())
248
hunk.lines.append(hunk_line)
249
if isinstance(hunk_line, (RemoveLine, ContextLine)):
251
if isinstance(hunk_line, (InsertLine, ContextLine)):
258
def __init__(self, oldname, newname):
259
self.oldname = oldname
260
self.newname = newname
264
ret = self.get_header()
265
ret += "".join([str(h) for h in self.hunks])
268
def get_header(self):
269
return "--- %s\n+++ %s\n" % (self.oldname, self.newname)
272
"""Return a string of patch statistics"""
275
for hunk in self.hunks:
276
for line in hunk.lines:
277
if isinstance(line, InsertLine):
279
elif isinstance(line, RemoveLine):
281
return "%i inserts, %i removes in %i hunks" % \
282
(inserts, removes, len(self.hunks))
284
def pos_in_mod(self, position):
286
for hunk in self.hunks:
287
shift = hunk.shift_to_mod(position)
293
def iter_inserted(self):
294
"""Iteraties through inserted lines
296
:return: Pair of line number, line
297
:rtype: iterator of (int, InsertLine)
299
for hunk in self.hunks:
300
pos = hunk.mod_pos - 1;
301
for line in hunk.lines:
302
if isinstance(line, InsertLine):
305
if isinstance(line, ContextLine):
309
def parse_patch(iter_lines):
310
(orig_name, mod_name) = get_patch_names(iter_lines)
311
patch = Patch(orig_name, mod_name)
312
for hunk in iter_hunks(iter_lines):
313
patch.hunks.append(hunk)
317
def iter_file_patch(iter_lines):
320
for line in iter_lines:
321
if line.startswith('=== ') or line.startswith('*** '):
323
if line.startswith('#'):
326
if line.startswith('-') or line.startswith(' '):
328
elif line.startswith('--- '):
329
if len(saved_lines) > 0:
332
elif line.startswith('@@'):
333
hunk = hunk_from_header(line)
334
orig_range = hunk.orig_range
335
saved_lines.append(line)
336
if len(saved_lines) > 0:
340
def iter_lines_handle_nl(iter_lines):
342
Iterates through lines, ensuring that lines that originally had no
343
terminating \n are produced without one. This transformation may be
344
applied at any point up until hunk line parsing, and is safe to apply
348
for line in iter_lines:
350
assert last_line.endswith('\n')
351
last_line = last_line[:-1]
353
if last_line is not None:
356
if last_line is not None:
360
def parse_patches(iter_lines):
361
iter_lines = iter_lines_handle_nl(iter_lines)
362
return [parse_patch(f.__iter__()) for f in iter_file_patch(iter_lines)]
365
def difference_index(atext, btext):
366
"""Find the indext of the first character that differs between two texts
368
:param atext: The first text
370
:param btext: The second text
372
:return: The index, or None if there are no differences within the range
373
:rtype: int or NoneType
376
if len(btext) < length:
378
for i in range(length):
379
if atext[i] != btext[i]:
384
def iter_patched(orig_lines, patch_lines):
385
"""Iterate through a series of lines with a patch applied.
386
This handles a single file, and does exact, not fuzzy patching.
388
if orig_lines is not None:
389
orig_lines = orig_lines.__iter__()
391
patch_lines = iter_lines_handle_nl(patch_lines.__iter__())
392
get_patch_names(patch_lines)
394
for hunk in iter_hunks(patch_lines):
395
while line_no < hunk.orig_pos:
396
orig_line = orig_lines.next()
399
for hunk_line in hunk.lines:
400
seen_patch.append(str(hunk_line))
401
if isinstance(hunk_line, InsertLine):
402
yield hunk_line.contents
403
elif isinstance(hunk_line, (ContextLine, RemoveLine)):
404
orig_line = orig_lines.next()
405
if orig_line != hunk_line.contents:
406
raise PatchConflict(line_no, orig_line, "".join(seen_patch))
407
if isinstance(hunk_line, ContextLine):
410
assert isinstance(hunk_line, RemoveLine)
412
if orig_lines is not None:
413
for line in orig_lines: