1
# Copyright (C) 2004 - 2006 Aaron Bentley
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):
319
for line in iter_lines:
320
if line.startswith('=== ') or line.startswith('*** '):
322
if line.startswith('#'):
324
elif line.startswith('--- '):
325
if len(saved_lines) > 0:
328
saved_lines.append(line)
329
if len(saved_lines) > 0:
333
def iter_lines_handle_nl(iter_lines):
335
Iterates through lines, ensuring that lines that originally had no
336
terminating \n are produced without one. This transformation may be
337
applied at any point up until hunk line parsing, and is safe to apply
341
for line in iter_lines:
343
assert last_line.endswith('\n')
344
last_line = last_line[:-1]
346
if last_line is not None:
349
if last_line is not None:
353
def parse_patches(iter_lines):
354
iter_lines = iter_lines_handle_nl(iter_lines)
355
return [parse_patch(f.__iter__()) for f in iter_file_patch(iter_lines)]
358
def difference_index(atext, btext):
359
"""Find the indext of the first character that differs between two texts
361
:param atext: The first text
363
:param btext: The second text
365
:return: The index, or None if there are no differences within the range
366
:rtype: int or NoneType
369
if len(btext) < length:
371
for i in range(length):
372
if atext[i] != btext[i]:
377
def iter_patched(orig_lines, patch_lines):
378
"""Iterate through a series of lines with a patch applied.
379
This handles a single file, and does exact, not fuzzy patching.
381
if orig_lines is not None:
382
orig_lines = orig_lines.__iter__()
384
patch_lines = iter_lines_handle_nl(patch_lines.__iter__())
385
get_patch_names(patch_lines)
387
for hunk in iter_hunks(patch_lines):
388
while line_no < hunk.orig_pos:
389
orig_line = orig_lines.next()
392
for hunk_line in hunk.lines:
393
seen_patch.append(str(hunk_line))
394
if isinstance(hunk_line, InsertLine):
395
yield hunk_line.contents
396
elif isinstance(hunk_line, (ContextLine, RemoveLine)):
397
orig_line = orig_lines.next()
398
if orig_line != hunk_line.contents:
399
raise PatchConflict(line_no, orig_line, "".join(seen_patch))
400
if isinstance(hunk_line, ContextLine):
403
assert isinstance(hunk_line, RemoveLine)
405
if orig_lines is not None:
406
for line in orig_lines: