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
elif line.startswith('--- '):
323
if len(saved_lines) > 0:
326
saved_lines.append(line)
327
if len(saved_lines) > 0:
331
def iter_lines_handle_nl(iter_lines):
333
Iterates through lines, ensuring that lines that originally had no
334
terminating \n are produced without one. This transformation may be
335
applied at any point up until hunk line parsing, and is safe to apply
339
for line in iter_lines:
341
assert last_line.endswith('\n')
342
last_line = last_line[:-1]
344
if last_line is not None:
347
if last_line is not None:
351
def parse_patches(iter_lines):
352
iter_lines = iter_lines_handle_nl(iter_lines)
353
return [parse_patch(f.__iter__()) for f in iter_file_patch(iter_lines)]
356
def difference_index(atext, btext):
357
"""Find the indext of the first character that differs betweeen two texts
359
:param atext: The first text
361
:param btext: The second text
363
:return: The index, or None if there are no differences within the range
364
:rtype: int or NoneType
367
if len(btext) < length:
369
for i in range(length):
370
if atext[i] != btext[i]:
375
def iter_patched(orig_lines, patch_lines):
376
"""Iterate through a series of lines with a patch applied.
377
This handles a single file, and does exact, not fuzzy patching.
379
if orig_lines is not None:
380
orig_lines = orig_lines.__iter__()
382
patch_lines = iter_lines_handle_nl(patch_lines.__iter__())
383
get_patch_names(patch_lines)
385
for hunk in iter_hunks(patch_lines):
386
while line_no < hunk.orig_pos:
387
orig_line = orig_lines.next()
390
for hunk_line in hunk.lines:
391
seen_patch.append(str(hunk_line))
392
if isinstance(hunk_line, InsertLine):
393
yield hunk_line.contents
394
elif isinstance(hunk_line, (ContextLine, RemoveLine)):
395
orig_line = orig_lines.next()
396
if orig_line != hunk_line.contents:
397
raise PatchConflict(line_no, orig_line, "".join(seen_patch))
398
if isinstance(hunk_line, ContextLine):
401
assert isinstance(hunk_line, RemoveLine)
403
if orig_lines is not None:
404
for line in orig_lines: