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
113
class ContextLine(HunkLine):
114
def __init__(self, contents):
115
HunkLine.__init__(self, contents)
118
return self.get_str(" ")
121
class InsertLine(HunkLine):
122
def __init__(self, contents):
123
HunkLine.__init__(self, contents)
126
return self.get_str("+")
128
class RemoveLine(HunkLine):
129
def __init__(self, contents):
130
HunkLine.__init__(self, contents)
133
return self.get_str("-")
135
__pychecker__="no-returnvalues"
136
def parse_line(line):
137
if line.startswith("\n"):
138
return ContextLine(line)
139
elif line.startswith(" "):
140
return ContextLine(line[1:])
141
elif line.startswith("+"):
142
return InsertLine(line[1:])
143
elif line.startswith("-"):
144
return RemoveLine(line[1:])
146
raise MalformedLine("Unknown line type", line)
151
def __init__(self, orig_pos, orig_range, mod_pos, mod_range):
152
self.orig_pos = orig_pos
153
self.orig_range = orig_range
154
self.mod_pos = mod_pos
155
self.mod_range = mod_range
158
def get_header(self):
159
return "@@ -%s +%s @@\n" % (self.range_str(self.orig_pos,
161
self.range_str(self.mod_pos,
164
def range_str(self, pos, range):
165
"""Return a file range, special-casing for 1-line files.
167
:param pos: The position in the file
169
:range: The range in the file
171
:return: a string in the format 1,4 except when range == pos == 1
176
return "%i,%i" % (pos, range)
179
lines = [self.get_header()]
180
for line in self.lines:
181
lines.append(str(line))
182
return "".join(lines)
184
def shift_to_mod(self, pos):
185
if pos < self.orig_pos-1:
187
elif pos > self.orig_pos+self.orig_range:
188
return self.mod_range - self.orig_range
190
return self.shift_to_mod_lines(pos)
192
def shift_to_mod_lines(self, pos):
193
assert (pos >= self.orig_pos-1 and pos <= self.orig_pos+self.orig_range)
194
position = self.orig_pos-1
196
for line in self.lines:
197
if isinstance(line, InsertLine):
199
elif isinstance(line, RemoveLine):
204
elif isinstance(line, ContextLine):
210
def iter_hunks(iter_lines):
212
for line in iter_lines:
213
if line.startswith("@@"):
216
hunk = hunk_from_header(line)
218
hunk.lines.append(parse_line(line))
224
def __init__(self, oldname, newname):
225
self.oldname = oldname
226
self.newname = newname
230
ret = "--- %s\n+++ %s\n" % (self.oldname, self.newname)
231
ret += "".join([str(h) for h in self.hunks])
235
"""Return a string of patch statistics"""
238
for hunk in self.hunks:
239
for line in hunk.lines:
240
if isinstance(line, InsertLine):
242
elif isinstance(line, RemoveLine):
244
return "%i inserts, %i removes in %i hunks" % \
245
(inserts, removes, len(self.hunks))
247
def pos_in_mod(self, position):
249
for hunk in self.hunks:
250
shift = hunk.shift_to_mod(position)
256
def iter_inserted(self):
257
"""Iteraties through inserted lines
259
:return: Pair of line number, line
260
:rtype: iterator of (int, InsertLine)
262
for hunk in self.hunks:
263
pos = hunk.mod_pos - 1;
264
for line in hunk.lines:
265
if isinstance(line, InsertLine):
268
if isinstance(line, ContextLine):
271
def parse_patch(iter_lines):
272
(orig_name, mod_name) = get_patch_names(iter_lines)
273
patch = Patch(orig_name, mod_name)
274
for hunk in iter_hunks(iter_lines):
275
patch.hunks.append(hunk)
278
def parse_patches(iter_lines):
281
return [ parse_patch(lines.__iter__()) ]
288
try: line = iter_lines.next()
289
except StopIteration:
290
patches.extend(parse(saved_lines))
293
if line.startswith('*** '):
294
patches.extend(parse(saved_lines))
297
elif line.startswith('--- ') and len(saved_lines) > 1:
298
patches.extend(parse(saved_lines))
299
saved_lines = [ line ]
302
saved_lines.append(line)