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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 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%r" % (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%r" % (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 not line.startswith("--- "):
48
raise MalformedPatchHeader("No orig name", line)
50
orig_name = line[4:].rstrip("\n")
52
raise MalformedPatchHeader("No orig line", "")
54
line = iter_lines.next()
55
if not line.startswith("+++ "):
56
raise PatchSyntax("No mod name")
58
mod_name = line[4:].rstrip("\n")
60
raise MalformedPatchHeader("No mod line", "")
61
return (orig_name, mod_name)
63
def iter_hunks(iter_lines):
65
for line in iter_lines:
73
hunk = hunk_from_header(line)
76
while orig_size < hunk.orig_range or mod_size < hunk.mod_range:
77
hunk_line = parse_line(iter_lines.next())
78
hunk.lines.append(hunk_line)
79
if isinstance(hunk_line, (RemoveLine, ContextLine)):
81
if isinstance(hunk_line, (InsertLine, ContextLine)):
87
def __init__(self, oldname, newname):
88
self.oldname = oldname
89
self.newname = newname
93
ret = self.get_header()
94
ret += "".join([str(h) for h in self.hunks])
98
return "--- %s\n+++ %s\n" % (self.oldname, self.newname)
101
"""Return a string of patch statistics"""
104
for hunk in self.hunks:
105
for line in hunk.lines:
106
if isinstance(line, InsertLine):
108
elif isinstance(line, RemoveLine):
110
return "%i inserts, %i removes in %i hunks" % \
111
(inserts, removes, len(self.hunks))
113
def pos_in_mod(self, position):
115
for hunk in self.hunks:
116
shift = hunk.shift_to_mod(position)
122
def iter_inserted(self):
123
"""Iteraties through inserted lines
125
:return: Pair of line number, line
126
:rtype: iterator of (int, InsertLine)
128
for hunk in self.hunks:
129
pos = hunk.mod_pos - 1;
130
for line in hunk.lines:
131
if isinstance(line, InsertLine):
134
if isinstance(line, ContextLine):
137
def parse_patch(iter_lines):
138
(orig_name, mod_name) = get_patch_names(iter_lines)
139
patch = Patch(orig_name, mod_name)
140
for hunk in iter_hunks(iter_lines):
141
patch.hunks.append(hunk)
145
def iter_file_patch(iter_lines):
147
for line in iter_lines:
148
if line.startswith('=== '):
150
elif line.startswith('--- '):
151
if len(saved_lines) > 0:
154
saved_lines.append(line)
155
if len(saved_lines) > 0:
159
def iter_lines_handle_nl(iter_lines):
161
Iterates through lines, ensuring that lines that originally had no
162
terminating \n are produced without one. This transformation may be
163
applied at any point up until hunk line parsing, and is safe to apply
167
for line in iter_lines:
169
assert last_line.endswith('\n')
170
last_line = last_line[:-1]
172
if last_line is not None:
175
if last_line is not None:
179
def parse_patches(iter_lines):
180
iter_lines = iter_lines_handle_nl(iter_lines)
181
return [parse_patch(f.__iter__()) for f in iter_file_patch(iter_lines)]
184
def difference_index(atext, btext):
185
"""Find the indext of the first character that differs betweeen two texts
187
:param atext: The first text
189
:param btext: The second text
191
:return: The index, or None if there are no differences within the range
192
:rtype: int or NoneType
195
if len(btext) < length:
197
for i in range(length):
198
if atext[i] != btext[i]:
202
class PatchConflict(Exception):
203
def __init__(self, line_no, orig_line, patch_line):
204
orig = orig_line.rstrip('\n')
205
patch = str(patch_line).rstrip('\n')
206
msg = 'Text contents mismatch at line %d. Original has "%s",'\
207
' but patch says it should be "%s"' % (line_no, orig, patch)
208
Exception.__init__(self, msg)
211
def iter_patched(orig_lines, patch_lines):
212
"""Iterate through a series of lines with a patch applied.
213
This handles a single file, and does exact, not fuzzy patching.
215
if orig_lines is not None:
216
orig_lines = orig_lines.__iter__()
218
patch_lines = iter_lines_handle_nl(patch_lines.__iter__())
219
get_patch_names(patch_lines)
221
for hunk in iter_hunks(patch_lines):
222
while line_no < hunk.orig_pos:
223
orig_line = orig_lines.next()
226
for hunk_line in hunk.lines:
227
seen_patch.append(str(hunk_line))
228
if isinstance(hunk_line, InsertLine):
229
yield hunk_line.contents
230
elif isinstance(hunk_line, (ContextLine, RemoveLine)):
231
orig_line = orig_lines.next()
232
if orig_line != hunk_line.contents:
233
raise PatchConflict(line_no, orig_line, "".join(seen_patch))
234
if isinstance(hunk_line, ContextLine):
237
assert isinstance(hunk_line, RemoveLine)
242
class PatchesTester(unittest.TestCase):
243
def datafile(self, filename):
244
data_path = os.path.join(os.path.dirname(__file__), "testdata",
246
return file(data_path, "rb")
248
def testValidPatchHeader(self):
249
"""Parse a valid patch header"""
250
lines = "--- orig/commands.py\n+++ mod/dommands.py\n".split('\n')
251
(orig, mod) = get_patch_names(lines.__iter__())
252
assert(orig == "orig/commands.py")
253
assert(mod == "mod/dommands.py")
255
def testInvalidPatchHeader(self):
256
"""Parse an invalid patch header"""
257
lines = "-- orig/commands.py\n+++ mod/dommands.py".split('\n')
258
self.assertRaises(MalformedPatchHeader, get_patch_names,
261
def testValidHunkHeader(self):
262
"""Parse a valid hunk header"""
263
header = "@@ -34,11 +50,6 @@\n"
264
hunk = hunk_from_header(header);
265
assert (hunk.orig_pos == 34)
266
assert (hunk.orig_range == 11)
267
assert (hunk.mod_pos == 50)
268
assert (hunk.mod_range == 6)
269
assert (str(hunk) == header)
271
def testValidHunkHeader2(self):
272
"""Parse a tricky, valid hunk header"""
273
header = "@@ -1 +0,0 @@\n"
274
hunk = hunk_from_header(header);
275
assert (hunk.orig_pos == 1)
276
assert (hunk.orig_range == 1)
277
assert (hunk.mod_pos == 0)
278
assert (hunk.mod_range == 0)
279
assert (str(hunk) == header)
281
def makeMalformed(self, header):
282
self.assertRaises(MalformedHunkHeader, hunk_from_header, header)
284
def testInvalidHeader(self):
285
"""Parse an invalid hunk header"""
286
self.makeMalformed(" -34,11 +50,6 \n")
287
self.makeMalformed("@@ +50,6 -34,11 @@\n")
288
self.makeMalformed("@@ -34,11 +50,6 @@")
289
self.makeMalformed("@@ -34.5,11 +50,6 @@\n")
290
self.makeMalformed("@@-34,11 +50,6@@\n")
291
self.makeMalformed("@@ 34,11 50,6 @@\n")
292
self.makeMalformed("@@ -34,11 @@\n")
293
self.makeMalformed("@@ -34,11 +50,6.5 @@\n")
294
self.makeMalformed("@@ -34,11 +50,-6 @@\n")
296
def lineThing(self,text, type):
297
line = parse_line(text)
298
assert(isinstance(line, type))
299
assert(str(line)==text)
301
def makeMalformedLine(self, text):
302
self.assertRaises(MalformedLine, parse_line, text)
304
def testValidLine(self):
305
"""Parse a valid hunk line"""
306
self.lineThing(" hello\n", ContextLine)
307
self.lineThing("+hello\n", InsertLine)
308
self.lineThing("-hello\n", RemoveLine)
310
def testMalformedLine(self):
311
"""Parse invalid valid hunk lines"""
312
self.makeMalformedLine("hello\n")
314
def compare_parsed(self, patchtext):
315
lines = patchtext.splitlines(True)
316
patch = parse_patch(lines.__iter__())
318
i = difference_index(patchtext, pstr)
320
print "%i: \"%s\" != \"%s\"" % (i, patchtext[i], pstr[i])
321
self.assertEqual (patchtext, str(patch))
324
"""Test parsing a whole patch"""
325
patchtext = """--- orig/commands.py
327
@@ -1337,7 +1337,8 @@
329
def set_title(self, command=None):
331
- version = self.tree.tree_version.nonarch
332
+ version = pylon.alias_or_version(self.tree.tree_version, self.tree,
335
version = "[no version]"
337
@@ -1983,7 +1984,11 @@
339
if len(new_merges) > 0:
340
if cmdutil.prompt("Log for merge"):
341
- mergestuff = cmdutil.log_for_merge(tree, comp_version)
342
+ if cmdutil.prompt("changelog for merge"):
343
+ mergestuff = "Patches applied:\\n"
344
+ mergestuff += pylon.changelog_for_merge(new_merges)
346
+ mergestuff = cmdutil.log_for_merge(tree, comp_version)
347
log.description += mergestuff
351
self.compare_parsed(patchtext)
354
"""Handle patches missing half the position, range tuple"""
356
"""--- orig/__init__.py
359
__docformat__ = "restructuredtext en"
360
+__doc__ = An alternate Arch commandline interface
362
self.compare_parsed(patchtext)
366
def testLineLookup(self):
368
"""Make sure we can accurately look up mod line from orig"""
369
patch = parse_patch(self.datafile("diff"))
370
orig = list(self.datafile("orig"))
371
mod = list(self.datafile("mod"))
373
for i in range(len(orig)):
374
mod_pos = patch.pos_in_mod(i)
376
removals.append(orig[i])
378
assert(mod[mod_pos]==orig[i])
379
rem_iter = removals.__iter__()
380
for hunk in patch.hunks:
381
for line in hunk.lines:
382
if isinstance(line, RemoveLine):
383
next = rem_iter.next()
384
if line.contents != next:
385
sys.stdout.write(" orig:%spatch:%s" % (next,
387
assert(line.contents == next)
388
self.assertRaises(StopIteration, rem_iter.next)
390
def testFirstLineRenumber(self):
391
"""Make sure we handle lines at the beginning of the hunk"""
392
patch = parse_patch(self.datafile("insert_top.patch"))
393
assert (patch.pos_in_mod(0)==1)
396
patchesTestSuite = unittest.makeSuite(PatchesTester,'test')
397
runner = unittest.TextTestRunner(verbosity=0)
398
return runner.run(patchesTestSuite)
401
if __name__ == "__main__":
403
# arch-tag: d1541a25-eac5-4de9-a476-08a7cecd5683