1
# Copyright (C) 2004, 2005 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
18
class PatchSyntax(Exception):
19
def __init__(self, msg):
20
Exception.__init__(self, msg)
23
class MalformedPatchHeader(PatchSyntax):
24
def __init__(self, desc, line):
27
msg = "Malformed patch header. %s\n%s" % (self.desc, self.line)
28
PatchSyntax.__init__(self, msg)
30
class MalformedHunkHeader(PatchSyntax):
31
def __init__(self, desc, line):
34
msg = "Malformed hunk header. %s\n%s" % (self.desc, self.line)
35
PatchSyntax.__init__(self, msg)
37
class MalformedLine(PatchSyntax):
38
def __init__(self, desc, line):
41
msg = "Malformed line. %s\n%s" % (self.desc, self.line)
42
PatchSyntax.__init__(self, msg)
44
def get_patch_names(iter_lines):
46
line = iter_lines.next()
47
if line.startswith("*** "):
48
line = iter_lines.next()
49
if not line.startswith("--- "):
50
raise MalformedPatchHeader("No orig name", line)
52
orig_name = line[4:].rstrip("\n")
54
raise MalformedPatchHeader("No orig line", "")
56
line = iter_lines.next()
57
if not line.startswith("+++ "):
58
raise PatchSyntax("No mod name")
60
mod_name = line[4:].rstrip("\n")
62
raise MalformedPatchHeader("No mod line", "")
63
return (orig_name, mod_name)
65
def parse_range(textrange):
66
"""Parse a patch range, handling the "1" special-case
68
:param textrange: The text to parse
70
:return: the position and range, as a tuple
73
tmp = textrange.split(',')
84
def hunk_from_header(line):
85
if not line.startswith("@@") or not line.endswith("@@\n") \
87
raise MalformedHunkHeader("Does not start and end with @@.", line)
89
(orig, mod) = line[3:-4].split(" ")
91
raise MalformedHunkHeader(str(e), line)
92
if not orig.startswith('-') or not mod.startswith('+'):
93
raise MalformedHunkHeader("Positions don't start with + or -.", line)
95
(orig_pos, orig_range) = parse_range(orig[1:])
96
(mod_pos, mod_range) = parse_range(mod[1:])
98
raise MalformedHunkHeader(str(e), line)
99
if mod_range < 0 or orig_range < 0:
100
raise MalformedHunkHeader("Hunk range is negative", line)
101
return Hunk(orig_pos, orig_range, mod_pos, mod_range)
105
def __init__(self, contents):
106
self.contents = contents
108
def get_str(self, leadchar):
109
if self.contents == "\n" and leadchar == " " and False:
111
return leadchar + self.contents
116
class ContextLine(HunkLine):
117
def __init__(self, contents):
118
HunkLine.__init__(self, contents)
121
return self.get_str(" ")
124
class InsertLine(HunkLine):
125
def __init__(self, contents):
126
HunkLine.__init__(self, contents)
129
return self.get_str("+")
132
return RemoveLine(self.contents)
135
class RemoveLine(HunkLine):
136
def __init__(self, contents):
137
HunkLine.__init__(self, contents)
140
return self.get_str("-")
143
return InsertLine(self.contents)
145
__pychecker__="no-returnvalues"
146
def parse_line(line):
147
if line.startswith("\n"):
148
return ContextLine(line)
149
elif line.startswith(" "):
150
return ContextLine(line[1:])
151
elif line.startswith("+"):
152
return InsertLine(line[1:])
153
elif line.startswith("-"):
154
return RemoveLine(line[1:])
156
raise MalformedLine("Unknown line type", line)
161
def __init__(self, orig_pos, orig_range, mod_pos, mod_range):
162
self.orig_pos = orig_pos
163
self.orig_range = orig_range
164
self.mod_pos = mod_pos
165
self.mod_range = mod_range
168
def get_header(self):
169
return "@@ -%s +%s @@\n" % (self.range_str(self.orig_pos,
171
self.range_str(self.mod_pos,
174
def range_str(self, pos, range):
175
"""Return a file range, special-casing for 1-line files.
177
:param pos: The position in the file
179
:range: The range in the file
181
:return: a string in the format 1,4 except when range == pos == 1
186
return "%i,%i" % (pos, range)
189
lines = [self.get_header()]
190
for line in self.lines:
191
lines.append(str(line))
192
return "".join(lines)
194
def shift_to_mod(self, pos):
195
if pos < self.orig_pos-1:
197
elif pos > self.orig_pos+self.orig_range:
198
return self.mod_range - self.orig_range
200
return self.shift_to_mod_lines(pos)
202
def shift_to_mod_lines(self, pos):
203
assert (pos >= self.orig_pos-1 and pos <= self.orig_pos+self.orig_range)
204
position = self.orig_pos-1
206
for line in self.lines:
207
if isinstance(line, InsertLine):
209
elif isinstance(line, RemoveLine):
214
elif isinstance(line, ContextLine):
221
clone = Hunk(self.mod_pos, self.mod_range, self.orig_pos, self.orig_range)
222
for line in self.lines:
223
clone.lines.append(line.invert())
226
def iter_hunks(iter_lines):
228
for line in iter_lines:
229
if line.startswith("@@"):
232
hunk = hunk_from_header(line)
234
hunk.lines.append(parse_line(line))
240
def __init__(self, oldname, newname):
241
self.oldname = oldname
242
self.newname = newname
246
ret = "--- %s\n+++ %s\n" % (self.oldname, self.newname)
247
ret += "".join([str(h) for h in self.hunks])
251
clone = Patch(self.newname, self.oldname)
252
for hunk in self.hunks:
253
clone.hunks.append(hunk.invert())
257
"""Return a string of patch statistics"""
260
for hunk in self.hunks:
261
for line in hunk.lines:
262
if isinstance(line, InsertLine):
264
elif isinstance(line, RemoveLine):
266
return "%i inserts, %i removes in %i hunks" % \
267
(inserts, removes, len(self.hunks))
269
def pos_in_mod(self, position):
271
for hunk in self.hunks:
272
shift = hunk.shift_to_mod(position)
278
def iter_inserted(self):
279
"""Iteraties through inserted lines
281
:return: Pair of line number, line
282
:rtype: iterator of (int, InsertLine)
284
for hunk in self.hunks:
285
pos = hunk.mod_pos - 1;
286
for line in hunk.lines:
287
if isinstance(line, InsertLine):
290
if isinstance(line, ContextLine):
293
def parse_patch(iter_lines):
294
(orig_name, mod_name) = get_patch_names(iter_lines)
295
patch = Patch(orig_name, mod_name)
296
for hunk in iter_hunks(iter_lines):
297
patch.hunks.append(hunk)
300
def parse_patches(iter_lines):
303
return [ parse_patch(lines.__iter__()) ]
310
try: line = iter_lines.next()
311
except StopIteration:
312
patches.extend(parse(saved_lines))
315
if line.startswith('*** '):
316
patches.extend(parse(saved_lines))
319
elif line.startswith('--- ') and len(saved_lines) > 1:
320
patches.extend(parse(saved_lines))
321
saved_lines = [ line ]
324
saved_lines.append(line)