2
# Copyright (C) 2004, 2005 Aaron Bentley
3
# <aaron.bentley@utoronto.ca>
5
# This program is free software; you can redistribute it and/or modify
6
# it under the terms of the GNU General Public License as published by
7
# the Free Software Foundation; either version 2 of the License, or
8
# (at your option) any later version.
10
# This program is distributed in the hope that it will be useful,
11
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
# GNU General Public License for more details.
15
# You should have received a copy of the GNU General Public License
16
# along with this program; if not, write to the Free Software
17
# 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)
31
class MalformedHunkHeader(PatchSyntax):
32
def __init__(self, desc, line):
35
msg = "Malformed hunk header. %s\n%r" % (self.desc, self.line)
36
PatchSyntax.__init__(self, msg)
38
class MalformedLine(PatchSyntax):
39
def __init__(self, desc, line):
42
msg = "Malformed line. %s\n%s" % (self.desc, self.line)
43
PatchSyntax.__init__(self, msg)
45
def get_patch_names(iter_lines):
47
line = iter_lines.next()
48
if not line.startswith("--- "):
49
raise MalformedPatchHeader("No orig name", line)
51
orig_name = line[4:].rstrip("\n")
53
raise MalformedPatchHeader("No orig line", "")
55
line = iter_lines.next()
56
if not line.startswith("+++ "):
57
raise PatchSyntax("No mod name")
59
mod_name = line[4:].rstrip("\n")
61
raise MalformedPatchHeader("No mod line", "")
62
return (orig_name, mod_name)
64
def parse_range(textrange):
65
"""Parse a patch range, handling the "1" special-case
67
:param textrange: The text to parse
69
:return: the position and range, as a tuple
72
tmp = textrange.split(',')
83
def hunk_from_header(line):
84
if not line.startswith("@@") or not line.endswith("@@\n") \
86
raise MalformedHunkHeader("Does not start and end with @@.", line)
88
(orig, mod) = line[3:-4].split(" ")
90
raise MalformedHunkHeader(str(e), line)
91
if not orig.startswith('-') or not mod.startswith('+'):
92
raise MalformedHunkHeader("Positions don't start with + or -.", line)
94
(orig_pos, orig_range) = parse_range(orig[1:])
95
(mod_pos, mod_range) = parse_range(mod[1:])
97
raise MalformedHunkHeader(str(e), line)
98
if mod_range < 0 or orig_range < 0:
99
raise MalformedHunkHeader("Hunk range is negative", line)
100
return Hunk(orig_pos, orig_range, mod_pos, mod_range)
104
def __init__(self, contents):
105
self.contents = contents
107
def get_str(self, leadchar):
108
if self.contents == "\n" and leadchar == " " and False:
110
if not self.contents.endswith('\n'):
111
terminator = '\n' + NO_NL
114
return leadchar + self.contents + terminator
117
class ContextLine(HunkLine):
118
def __init__(self, contents):
119
HunkLine.__init__(self, contents)
122
return self.get_str(" ")
125
class InsertLine(HunkLine):
126
def __init__(self, contents):
127
HunkLine.__init__(self, contents)
130
return self.get_str("+")
133
class RemoveLine(HunkLine):
134
def __init__(self, contents):
135
HunkLine.__init__(self, contents)
138
return self.get_str("-")
140
NO_NL = '\\ No newline at end of file\n'
141
__pychecker__="no-returnvalues"
143
def parse_line(line):
144
if line.startswith("\n"):
145
return ContextLine(line)
146
elif line.startswith(" "):
147
return ContextLine(line[1:])
148
elif line.startswith("+"):
149
return InsertLine(line[1:])
150
elif line.startswith("-"):
151
return RemoveLine(line[1:])
155
raise MalformedLine("Unknown line type", line)
160
def __init__(self, orig_pos, orig_range, mod_pos, mod_range):
161
self.orig_pos = orig_pos
162
self.orig_range = orig_range
163
self.mod_pos = mod_pos
164
self.mod_range = mod_range
167
def get_header(self):
168
return "@@ -%s +%s @@\n" % (self.range_str(self.orig_pos,
170
self.range_str(self.mod_pos,
173
def range_str(self, pos, range):
174
"""Return a file range, special-casing for 1-line files.
176
:param pos: The position in the file
178
:range: The range in the file
180
:return: a string in the format 1,4 except when range == pos == 1
185
return "%i,%i" % (pos, range)
188
lines = [self.get_header()]
189
for line in self.lines:
190
lines.append(str(line))
191
return "".join(lines)
193
def shift_to_mod(self, pos):
194
if pos < self.orig_pos-1:
196
elif pos > self.orig_pos+self.orig_range:
197
return self.mod_range - self.orig_range
199
return self.shift_to_mod_lines(pos)
201
def shift_to_mod_lines(self, pos):
202
assert (pos >= self.orig_pos-1 and pos <= self.orig_pos+self.orig_range)
203
position = self.orig_pos-1
205
for line in self.lines:
206
if isinstance(line, InsertLine):
208
elif isinstance(line, RemoveLine):
213
elif isinstance(line, ContextLine):
219
def iter_hunks(iter_lines):
221
for line in iter_lines:
229
hunk = hunk_from_header(line)
232
while orig_size < hunk.orig_range or mod_size < hunk.mod_range:
233
hunk_line = parse_line(iter_lines.next())
234
hunk.lines.append(hunk_line)
235
if isinstance(hunk_line, (RemoveLine, ContextLine)):
237
if isinstance(hunk_line, (InsertLine, ContextLine)):
243
def __init__(self, oldname, newname):
244
self.oldname = oldname
245
self.newname = newname
249
ret = self.get_header()
250
ret += "".join([str(h) for h in self.hunks])
253
def get_header(self):
254
return "--- %s\n+++ %s\n" % (self.oldname, self.newname)
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)
301
def iter_file_patch(iter_lines):
303
for line in iter_lines:
304
if line.startswith('=== '):
306
elif line.startswith('--- '):
307
if len(saved_lines) > 0:
310
saved_lines.append(line)
311
if len(saved_lines) > 0:
315
def iter_lines_handle_nl(iter_lines):
317
Iterates through lines, ensuring that lines that originally had no
318
terminating \n are produced without one. This transformation may be
319
applied at any point up until hunk line parsing, and is safe to apply
323
for line in iter_lines:
325
assert last_line.endswith('\n')
326
last_line = last_line[:-1]
328
if last_line is not None:
331
if last_line is not None:
335
def parse_patches(iter_lines):
336
iter_lines = iter_lines_handle_nl(iter_lines)
337
return [parse_patch(f.__iter__()) for f in iter_file_patch(iter_lines)]
340
def difference_index(atext, btext):
341
"""Find the indext of the first character that differs betweeen two texts
343
:param atext: The first text
345
:param btext: The second text
347
:return: The index, or None if there are no differences within the range
348
:rtype: int or NoneType
351
if len(btext) < length:
353
for i in range(length):
354
if atext[i] != btext[i]:
358
class PatchConflict(Exception):
359
def __init__(self, line_no, orig_line, patch_line):
360
orig = orig_line.rstrip('\n')
361
patch = str(patch_line).rstrip('\n')
362
msg = 'Text contents mismatch at line %d. Original has "%s",'\
363
' but patch says it should be "%s"' % (line_no, orig, patch)
364
Exception.__init__(self, msg)
367
def iter_patched(orig_lines, patch_lines):
368
"""Iterate through a series of lines with a patch applied.
369
This handles a single file, and does exact, not fuzzy patching.
371
if orig_lines is not None:
372
orig_lines = orig_lines.__iter__()
374
patch_lines = iter_lines_handle_nl(patch_lines.__iter__())
375
get_patch_names(patch_lines)
377
for hunk in iter_hunks(patch_lines):
378
while line_no < hunk.orig_pos:
379
orig_line = orig_lines.next()
382
for hunk_line in hunk.lines:
383
seen_patch.append(str(hunk_line))
384
if isinstance(hunk_line, InsertLine):
385
yield hunk_line.contents
386
elif isinstance(hunk_line, (ContextLine, RemoveLine)):
387
orig_line = orig_lines.next()
388
if orig_line != hunk_line.contents:
389
raise PatchConflict(line_no, orig_line, "".join(seen_patch))
390
if isinstance(hunk_line, ContextLine):
393
assert isinstance(hunk_line, RemoveLine)
395
for line in orig_lines:
400
class PatchesTester(unittest.TestCase):
401
def datafile(self, filename):
402
data_path = os.path.join(os.path.dirname(__file__), "testdata",
404
return file(data_path, "rb")
406
def testValidPatchHeader(self):
407
"""Parse a valid patch header"""
408
lines = "--- orig/commands.py\n+++ mod/dommands.py\n".split('\n')
409
(orig, mod) = get_patch_names(lines.__iter__())
410
assert(orig == "orig/commands.py")
411
assert(mod == "mod/dommands.py")
413
def testInvalidPatchHeader(self):
414
"""Parse an invalid patch header"""
415
lines = "-- orig/commands.py\n+++ mod/dommands.py".split('\n')
416
self.assertRaises(MalformedPatchHeader, get_patch_names,
419
def testValidHunkHeader(self):
420
"""Parse a valid hunk header"""
421
header = "@@ -34,11 +50,6 @@\n"
422
hunk = hunk_from_header(header);
423
assert (hunk.orig_pos == 34)
424
assert (hunk.orig_range == 11)
425
assert (hunk.mod_pos == 50)
426
assert (hunk.mod_range == 6)
427
assert (str(hunk) == header)
429
def testValidHunkHeader2(self):
430
"""Parse a tricky, valid hunk header"""
431
header = "@@ -1 +0,0 @@\n"
432
hunk = hunk_from_header(header);
433
assert (hunk.orig_pos == 1)
434
assert (hunk.orig_range == 1)
435
assert (hunk.mod_pos == 0)
436
assert (hunk.mod_range == 0)
437
assert (str(hunk) == header)
439
def makeMalformed(self, header):
440
self.assertRaises(MalformedHunkHeader, hunk_from_header, header)
442
def testInvalidHeader(self):
443
"""Parse an invalid hunk header"""
444
self.makeMalformed(" -34,11 +50,6 \n")
445
self.makeMalformed("@@ +50,6 -34,11 @@\n")
446
self.makeMalformed("@@ -34,11 +50,6 @@")
447
self.makeMalformed("@@ -34.5,11 +50,6 @@\n")
448
self.makeMalformed("@@-34,11 +50,6@@\n")
449
self.makeMalformed("@@ 34,11 50,6 @@\n")
450
self.makeMalformed("@@ -34,11 @@\n")
451
self.makeMalformed("@@ -34,11 +50,6.5 @@\n")
452
self.makeMalformed("@@ -34,11 +50,-6 @@\n")
454
def lineThing(self,text, type):
455
line = parse_line(text)
456
assert(isinstance(line, type))
457
assert(str(line)==text)
459
def makeMalformedLine(self, text):
460
self.assertRaises(MalformedLine, parse_line, text)
462
def testValidLine(self):
463
"""Parse a valid hunk line"""
464
self.lineThing(" hello\n", ContextLine)
465
self.lineThing("+hello\n", InsertLine)
466
self.lineThing("-hello\n", RemoveLine)
468
def testMalformedLine(self):
469
"""Parse invalid valid hunk lines"""
470
self.makeMalformedLine("hello\n")
472
def compare_parsed(self, patchtext):
473
lines = patchtext.splitlines(True)
474
patch = parse_patch(lines.__iter__())
476
i = difference_index(patchtext, pstr)
478
print "%i: \"%s\" != \"%s\"" % (i, patchtext[i], pstr[i])
479
self.assertEqual (patchtext, str(patch))
482
"""Test parsing a whole patch"""
483
patchtext = """--- orig/commands.py
485
@@ -1337,7 +1337,8 @@
487
def set_title(self, command=None):
489
- version = self.tree.tree_version.nonarch
490
+ version = pylon.alias_or_version(self.tree.tree_version, self.tree,
493
version = "[no version]"
495
@@ -1983,7 +1984,11 @@
497
if len(new_merges) > 0:
498
if cmdutil.prompt("Log for merge"):
499
- mergestuff = cmdutil.log_for_merge(tree, comp_version)
500
+ if cmdutil.prompt("changelog for merge"):
501
+ mergestuff = "Patches applied:\\n"
502
+ mergestuff += pylon.changelog_for_merge(new_merges)
504
+ mergestuff = cmdutil.log_for_merge(tree, comp_version)
505
log.description += mergestuff
509
self.compare_parsed(patchtext)
512
"""Handle patches missing half the position, range tuple"""
514
"""--- orig/__init__.py
517
__docformat__ = "restructuredtext en"
518
+__doc__ = An alternate Arch commandline interface
520
self.compare_parsed(patchtext)
524
def testLineLookup(self):
526
"""Make sure we can accurately look up mod line from orig"""
527
patch = parse_patch(self.datafile("diff"))
528
orig = list(self.datafile("orig"))
529
mod = list(self.datafile("mod"))
531
for i in range(len(orig)):
532
mod_pos = patch.pos_in_mod(i)
534
removals.append(orig[i])
536
assert(mod[mod_pos]==orig[i])
537
rem_iter = removals.__iter__()
538
for hunk in patch.hunks:
539
for line in hunk.lines:
540
if isinstance(line, RemoveLine):
541
next = rem_iter.next()
542
if line.contents != next:
543
sys.stdout.write(" orig:%spatch:%s" % (next,
545
assert(line.contents == next)
546
self.assertRaises(StopIteration, rem_iter.next)
548
def testFirstLineRenumber(self):
549
"""Make sure we handle lines at the beginning of the hunk"""
550
patch = parse_patch(self.datafile("insert_top.patch"))
551
assert (patch.pos_in_mod(0)==1)
554
patchesTestSuite = unittest.makeSuite(PatchesTester,'test')
555
runner = unittest.TextTestRunner(verbosity=0)
556
return runner.run(patchesTestSuite)
559
if __name__ == "__main__":
561
# arch-tag: d1541a25-eac5-4de9-a476-08a7cecd5683