--- orig-6 2005-09-23 16:27:16.000000000 -0500 +++ mod-6 2005-09-23 16:27:32.000000000 -0500 @@ -1,558 +1 @@ -# Copyright (C) 2004, 2005 Aaron Bentley -# -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation; either version 2 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - -class PatchSyntax(Exception): - def __init__(self, msg): - Exception.__init__(self, msg) - - -class MalformedPatchHeader(PatchSyntax): - def __init__(self, desc, line): - self.desc = desc - self.line = line - msg = "Malformed patch header. %s\n%r" % (self.desc, self.line) - PatchSyntax.__init__(self, msg) - -class MalformedHunkHeader(PatchSyntax): - def __init__(self, desc, line): - self.desc = desc - self.line = line - msg = "Malformed hunk header. %s\n%r" % (self.desc, self.line) - PatchSyntax.__init__(self, msg) - -class MalformedLine(PatchSyntax): - def __init__(self, desc, line): - self.desc = desc - self.line = line - msg = "Malformed line. %s\n%s" % (self.desc, self.line) - PatchSyntax.__init__(self, msg) - -def get_patch_names(iter_lines): - try: - line = iter_lines.next() - if not line.startswith("--- "): - raise MalformedPatchHeader("No orig name", line) - else: - orig_name = line[4:].rstrip("\n") - except StopIteration: - raise MalformedPatchHeader("No orig line", "") - try: - line = iter_lines.next() - if not line.startswith("+++ "): - raise PatchSyntax("No mod name") - else: - mod_name = line[4:].rstrip("\n") - except StopIteration: - raise MalformedPatchHeader("No mod line", "") - return (orig_name, mod_name) - -def parse_range(textrange): - """Parse a patch range, handling the "1" special-case - - :param textrange: The text to parse - :type textrange: str - :return: the position and range, as a tuple - :rtype: (int, int) - """ - tmp = textrange.split(',') - if len(tmp) == 1: - pos = tmp[0] - range = "1" - else: - (pos, range) = tmp - pos = int(pos) - range = int(range) - return (pos, range) - - -def hunk_from_header(line): - if not line.startswith("@@") or not line.endswith("@@\n") \ - or not len(line) > 4: - raise MalformedHunkHeader("Does not start and end with @@.", line) - try: - (orig, mod) = line[3:-4].split(" ") - except Exception, e: - raise MalformedHunkHeader(str(e), line) - if not orig.startswith('-') or not mod.startswith('+'): - raise MalformedHunkHeader("Positions don't start with + or -.", line) - try: - (orig_pos, orig_range) = parse_range(orig[1:]) - (mod_pos, mod_range) = parse_range(mod[1:]) - except Exception, e: - raise MalformedHunkHeader(str(e), line) - if mod_range < 0 or orig_range < 0: - raise MalformedHunkHeader("Hunk range is negative", line) - return Hunk(orig_pos, orig_range, mod_pos, mod_range) - - -class HunkLine: - def __init__(self, contents): - self.contents = contents - - def get_str(self, leadchar): - if self.contents == "\n" and leadchar == " " and False: - return "\n" - if not self.contents.endswith('\n'): - terminator = '\n' + NO_NL - else: - terminator = '' - return leadchar + self.contents + terminator - - -class ContextLine(HunkLine): - def __init__(self, contents): - HunkLine.__init__(self, contents) - - def __str__(self): - return self.get_str(" ") - - -class InsertLine(HunkLine): - def __init__(self, contents): - HunkLine.__init__(self, contents) - - def __str__(self): - return self.get_str("+") - - -class RemoveLine(HunkLine): - def __init__(self, contents): - HunkLine.__init__(self, contents) - - def __str__(self): - return self.get_str("-") - -NO_NL = '\\ No newline at end of file\n' -__pychecker__="no-returnvalues" - -def parse_line(line): - if line.startswith("\n"): - return ContextLine(line) - elif line.startswith(" "): - return ContextLine(line[1:]) - elif line.startswith("+"): - return InsertLine(line[1:]) - elif line.startswith("-"): - return RemoveLine(line[1:]) - elif line == NO_NL: - return NO_NL - else: - raise MalformedLine("Unknown line type", line) -__pychecker__="" - - -class Hunk: - def __init__(self, orig_pos, orig_range, mod_pos, mod_range): - self.orig_pos = orig_pos - self.orig_range = orig_range - self.mod_pos = mod_pos - self.mod_range = mod_range - self.lines = [] - - def get_header(self): - return "@@ -%s +%s @@\n" % (self.range_str(self.orig_pos, - self.orig_range), - self.range_str(self.mod_pos, - self.mod_range)) - - def range_str(self, pos, range): - """Return a file range, special-casing for 1-line files. - - :param pos: The position in the file - :type pos: int - :range: The range in the file - :type range: int - :return: a string in the format 1,4 except when range == pos == 1 - """ - if range == 1: - return "%i" % pos - else: - return "%i,%i" % (pos, range) - - def __str__(self): - lines = [self.get_header()] - for line in self.lines: - lines.append(str(line)) - return "".join(lines) - - def shift_to_mod(self, pos): - if pos < self.orig_pos-1: - return 0 - elif pos > self.orig_pos+self.orig_range: - return self.mod_range - self.orig_range - else: - return self.shift_to_mod_lines(pos) - - def shift_to_mod_lines(self, pos): - assert (pos >= self.orig_pos-1 and pos <= self.orig_pos+self.orig_range) - position = self.orig_pos-1 - shift = 0 - for line in self.lines: - if isinstance(line, InsertLine): - shift += 1 - elif isinstance(line, RemoveLine): - if position == pos: - return None - shift -= 1 - position += 1 - elif isinstance(line, ContextLine): - position += 1 - if position > pos: - break - return shift - -def iter_hunks(iter_lines): - hunk = None - for line in iter_lines: - if line == "\n": - if hunk is not None: - yield hunk - hunk = None - continue - if hunk is not None: - yield hunk - hunk = hunk_from_header(line) - orig_size = 0 - mod_size = 0 - while orig_size < hunk.orig_range or mod_size < hunk.mod_range: - hunk_line = parse_line(iter_lines.next()) - hunk.lines.append(hunk_line) - if isinstance(hunk_line, (RemoveLine, ContextLine)): - orig_size += 1 - if isinstance(hunk_line, (InsertLine, ContextLine)): - mod_size += 1 - if hunk is not None: - yield hunk - -class Patch: - def __init__(self, oldname, newname): - self.oldname = oldname - self.newname = newname - self.hunks = [] - - def __str__(self): - ret = self.get_header() - ret += "".join([str(h) for h in self.hunks]) - return ret - - def get_header(self): - return "--- %s\n+++ %s\n" % (self.oldname, self.newname) - - def stats_str(self): - """Return a string of patch statistics""" - removes = 0 - inserts = 0 - for hunk in self.hunks: - for line in hunk.lines: - if isinstance(line, InsertLine): - inserts+=1; - elif isinstance(line, RemoveLine): - removes+=1; - return "%i inserts, %i removes in %i hunks" % \ - (inserts, removes, len(self.hunks)) - - def pos_in_mod(self, position): - newpos = position - for hunk in self.hunks: - shift = hunk.shift_to_mod(position) - if shift is None: - return None - newpos += shift - return newpos - - def iter_inserted(self): - """Iteraties through inserted lines - - :return: Pair of line number, line - :rtype: iterator of (int, InsertLine) - """ - for hunk in self.hunks: - pos = hunk.mod_pos - 1; - for line in hunk.lines: - if isinstance(line, InsertLine): - yield (pos, line) - pos += 1 - if isinstance(line, ContextLine): - pos += 1 - -def parse_patch(iter_lines): - (orig_name, mod_name) = get_patch_names(iter_lines) - patch = Patch(orig_name, mod_name) - for hunk in iter_hunks(iter_lines): - patch.hunks.append(hunk) - return patch - - -def iter_file_patch(iter_lines): - saved_lines = [] - for line in iter_lines: - if line.startswith('=== '): - continue - elif line.startswith('--- '): - if len(saved_lines) > 0: - yield saved_lines - saved_lines = [] - saved_lines.append(line) - if len(saved_lines) > 0: - yield saved_lines - - -def iter_lines_handle_nl(iter_lines): - """ - Iterates through lines, ensuring that lines that originally had no - terminating \n are produced without one. This transformation may be - applied at any point up until hunk line parsing, and is safe to apply - repeatedly. - """ - last_line = None - for line in iter_lines: - if line == NO_NL: - assert last_line.endswith('\n') - last_line = last_line[:-1] - line = None - if last_line is not None: - yield last_line - last_line = line - if last_line is not None: - yield last_line - - -def parse_patches(iter_lines): - iter_lines = iter_lines_handle_nl(iter_lines) - return [parse_patch(f.__iter__()) for f in iter_file_patch(iter_lines)] - - -def difference_index(atext, btext): - """Find the indext of the first character that differs betweeen two texts - - :param atext: The first text - :type atext: str - :param btext: The second text - :type str: str - :return: The index, or None if there are no differences within the range - :rtype: int or NoneType - """ - length = len(atext) - if len(btext) < length: - length = len(btext) - for i in range(length): - if atext[i] != btext[i]: - return i; - return None - -class PatchConflict(Exception): - def __init__(self, line_no, orig_line, patch_line): - orig = orig_line.rstrip('\n') - patch = str(patch_line).rstrip('\n') - msg = 'Text contents mismatch at line %d. Original has "%s",'\ - ' but patch says it should be "%s"' % (line_no, orig, patch) - Exception.__init__(self, msg) - - -def iter_patched(orig_lines, patch_lines): - """Iterate through a series of lines with a patch applied. - This handles a single file, and does exact, not fuzzy patching. - """ - if orig_lines is not None: - orig_lines = orig_lines.__iter__() - seen_patch = [] - patch_lines = iter_lines_handle_nl(patch_lines.__iter__()) - get_patch_names(patch_lines) - line_no = 1 - for hunk in iter_hunks(patch_lines): - while line_no < hunk.orig_pos: - orig_line = orig_lines.next() - yield orig_line - line_no += 1 - for hunk_line in hunk.lines: - seen_patch.append(str(hunk_line)) - if isinstance(hunk_line, InsertLine): - yield hunk_line.contents - elif isinstance(hunk_line, (ContextLine, RemoveLine)): - orig_line = orig_lines.next() - if orig_line != hunk_line.contents: - raise PatchConflict(line_no, orig_line, "".join(seen_patch)) - if isinstance(hunk_line, ContextLine): - yield orig_line - else: - assert isinstance(hunk_line, RemoveLine) - line_no += 1 - -import unittest -import os.path -class PatchesTester(unittest.TestCase): - def datafile(self, filename): - data_path = os.path.join(os.path.dirname(__file__), "testdata", - filename) - return file(data_path, "rb") - - def testValidPatchHeader(self): - """Parse a valid patch header""" - lines = "--- orig/commands.py\n+++ mod/dommands.py\n".split('\n') - (orig, mod) = get_patch_names(lines.__iter__()) - assert(orig == "orig/commands.py") - assert(mod == "mod/dommands.py") - - def testInvalidPatchHeader(self): - """Parse an invalid patch header""" - lines = "-- orig/commands.py\n+++ mod/dommands.py".split('\n') - self.assertRaises(MalformedPatchHeader, get_patch_names, - lines.__iter__()) - - def testValidHunkHeader(self): - """Parse a valid hunk header""" - header = "@@ -34,11 +50,6 @@\n" - hunk = hunk_from_header(header); - assert (hunk.orig_pos == 34) - assert (hunk.orig_range == 11) - assert (hunk.mod_pos == 50) - assert (hunk.mod_range == 6) - assert (str(hunk) == header) - - def testValidHunkHeader2(self): - """Parse a tricky, valid hunk header""" - header = "@@ -1 +0,0 @@\n" - hunk = hunk_from_header(header); - assert (hunk.orig_pos == 1) - assert (hunk.orig_range == 1) - assert (hunk.mod_pos == 0) - assert (hunk.mod_range == 0) - assert (str(hunk) == header) - - def makeMalformed(self, header): - self.assertRaises(MalformedHunkHeader, hunk_from_header, header) - - def testInvalidHeader(self): - """Parse an invalid hunk header""" - self.makeMalformed(" -34,11 +50,6 \n") - self.makeMalformed("@@ +50,6 -34,11 @@\n") - self.makeMalformed("@@ -34,11 +50,6 @@") - self.makeMalformed("@@ -34.5,11 +50,6 @@\n") - self.makeMalformed("@@-34,11 +50,6@@\n") - self.makeMalformed("@@ 34,11 50,6 @@\n") - self.makeMalformed("@@ -34,11 @@\n") - self.makeMalformed("@@ -34,11 +50,6.5 @@\n") - self.makeMalformed("@@ -34,11 +50,-6 @@\n") - - def lineThing(self,text, type): - line = parse_line(text) - assert(isinstance(line, type)) - assert(str(line)==text) - - def makeMalformedLine(self, text): - self.assertRaises(MalformedLine, parse_line, text) - - def testValidLine(self): - """Parse a valid hunk line""" - self.lineThing(" hello\n", ContextLine) - self.lineThing("+hello\n", InsertLine) - self.lineThing("-hello\n", RemoveLine) - - def testMalformedLine(self): - """Parse invalid valid hunk lines""" - self.makeMalformedLine("hello\n") - - def compare_parsed(self, patchtext): - lines = patchtext.splitlines(True) - patch = parse_patch(lines.__iter__()) - pstr = str(patch) - i = difference_index(patchtext, pstr) - if i is not None: - print "%i: \"%s\" != \"%s\"" % (i, patchtext[i], pstr[i]) - self.assertEqual (patchtext, str(patch)) - - def testAll(self): - """Test parsing a whole patch""" - patchtext = """--- orig/commands.py -+++ mod/commands.py -@@ -1337,7 +1337,8 @@ - - def set_title(self, command=None): - try: -- version = self.tree.tree_version.nonarch -+ version = pylon.alias_or_version(self.tree.tree_version, self.tree, -+ full=False) - except: - version = "[no version]" - if command is None: -@@ -1983,7 +1984,11 @@ - version) - if len(new_merges) > 0: - if cmdutil.prompt("Log for merge"): -- mergestuff = cmdutil.log_for_merge(tree, comp_version) -+ if cmdutil.prompt("changelog for merge"): -+ mergestuff = "Patches applied:\\n" -+ mergestuff += pylon.changelog_for_merge(new_merges) -+ else: -+ mergestuff = cmdutil.log_for_merge(tree, comp_version) - log.description += mergestuff - log.save() - try: -""" - self.compare_parsed(patchtext) - - def testInit(self): - """Handle patches missing half the position, range tuple""" - patchtext = \ -"""--- orig/__init__.py -+++ mod/__init__.py -@@ -1 +1,2 @@ - __docformat__ = "restructuredtext en" -+__doc__ = An alternate Arch commandline interface -""" - self.compare_parsed(patchtext) - - - - def testLineLookup(self): - import sys - """Make sure we can accurately look up mod line from orig""" - patch = parse_patch(self.datafile("diff")) - orig = list(self.datafile("orig")) - mod = list(self.datafile("mod")) - removals = [] - for i in range(len(orig)): - mod_pos = patch.pos_in_mod(i) - if mod_pos is None: - removals.append(orig[i]) - continue - assert(mod[mod_pos]==orig[i]) - rem_iter = removals.__iter__() - for hunk in patch.hunks: - for line in hunk.lines: - if isinstance(line, RemoveLine): - next = rem_iter.next() - if line.contents != next: - sys.stdout.write(" orig:%spatch:%s" % (next, - line.contents)) - assert(line.contents == next) - self.assertRaises(StopIteration, rem_iter.next) - - def testFirstLineRenumber(self): - """Make sure we handle lines at the beginning of the hunk""" - patch = parse_patch(self.datafile("insert_top.patch")) - assert (patch.pos_in_mod(0)==1) - -def test(): - patchesTestSuite = unittest.makeSuite(PatchesTester,'test') - runner = unittest.TextTestRunner(verbosity=0) - return runner.run(patchesTestSuite) - - -if __name__ == "__main__": - test() -# arch-tag: d1541a25-eac5-4de9-a476-08a7cecd5683 +Total contents change