1
# Copyright (C) 2004 - 2006 Aaron Bentley, Canonical Ltd
 
 
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
 
 
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)
 
 
32
class MalformedHunkHeader(PatchSyntax):
 
 
33
    def __init__(self, desc, line):
 
 
36
        msg = "Malformed hunk header.  %s\n%r" % (self.desc, self.line)
 
 
37
        PatchSyntax.__init__(self, msg)
 
 
40
class MalformedLine(PatchSyntax):
 
 
41
    def __init__(self, desc, line):
 
 
44
        msg = "Malformed line.  %s\n%s" % (self.desc, self.line)
 
 
45
        PatchSyntax.__init__(self, msg)
 
 
48
class PatchConflict(Exception):
 
 
49
    def __init__(self, line_no, orig_line, patch_line):
 
 
50
        orig = orig_line.rstrip('\n')
 
 
51
        patch = str(patch_line).rstrip('\n')
 
 
52
        msg = 'Text contents mismatch at line %d.  Original has "%s",'\
 
 
53
            ' but patch says it should be "%s"' % (line_no, orig, patch)
 
 
54
        Exception.__init__(self, msg)
 
 
57
def get_patch_names(iter_lines):
 
 
59
        line = iter_lines.next()
 
 
60
        if not line.startswith("--- "):
 
 
61
            raise MalformedPatchHeader("No orig name", line)
 
 
63
            orig_name = line[4:].rstrip("\n")
 
 
65
        raise MalformedPatchHeader("No orig line", "")
 
 
67
        line = iter_lines.next()
 
 
68
        if not line.startswith("+++ "):
 
 
69
            raise PatchSyntax("No mod name")
 
 
71
            mod_name = line[4:].rstrip("\n")
 
 
73
        raise MalformedPatchHeader("No mod line", "")
 
 
74
    return (orig_name, mod_name)
 
 
77
def parse_range(textrange):
 
 
78
    """Parse a patch range, handling the "1" special-case
 
 
80
    :param textrange: The text to parse
 
 
82
    :return: the position and range, as a tuple
 
 
85
    tmp = textrange.split(',')
 
 
96
def hunk_from_header(line):
 
 
97
    if not line.startswith("@@") or not line.endswith("@@\n") \
 
 
99
        raise MalformedHunkHeader("Does not start and end with @@.", line)
 
 
101
        (orig, mod) = line[3:-4].split(" ")
 
 
103
        raise MalformedHunkHeader(str(e), line)
 
 
104
    if not orig.startswith('-') or not mod.startswith('+'):
 
 
105
        raise MalformedHunkHeader("Positions don't start with + or -.", line)
 
 
107
        (orig_pos, orig_range) = parse_range(orig[1:])
 
 
108
        (mod_pos, mod_range) = parse_range(mod[1:])
 
 
110
        raise MalformedHunkHeader(str(e), line)
 
 
111
    if mod_range < 0 or orig_range < 0:
 
 
112
        raise MalformedHunkHeader("Hunk range is negative", line)
 
 
113
    return Hunk(orig_pos, orig_range, mod_pos, mod_range)
 
 
117
    def __init__(self, contents):
 
 
118
        self.contents = contents
 
 
120
    def get_str(self, leadchar):
 
 
121
        if self.contents == "\n" and leadchar == " " and False:
 
 
123
        if not self.contents.endswith('\n'):
 
 
124
            terminator = '\n' + NO_NL
 
 
127
        return leadchar + self.contents + terminator
 
 
130
class ContextLine(HunkLine):
 
 
131
    def __init__(self, contents):
 
 
132
        HunkLine.__init__(self, contents)
 
 
135
        return self.get_str(" ")
 
 
138
class InsertLine(HunkLine):
 
 
139
    def __init__(self, contents):
 
 
140
        HunkLine.__init__(self, contents)
 
 
143
        return self.get_str("+")
 
 
146
class RemoveLine(HunkLine):
 
 
147
    def __init__(self, contents):
 
 
148
        HunkLine.__init__(self, contents)
 
 
151
        return self.get_str("-")
 
 
153
NO_NL = '\\ No newline at end of file\n'
 
 
154
__pychecker__="no-returnvalues"
 
 
156
def parse_line(line):
 
 
157
    if line.startswith("\n"):
 
 
158
        return ContextLine(line)
 
 
159
    elif line.startswith(" "):
 
 
160
        return ContextLine(line[1:])
 
 
161
    elif line.startswith("+"):
 
 
162
        return InsertLine(line[1:])
 
 
163
    elif line.startswith("-"):
 
 
164
        return RemoveLine(line[1:])
 
 
168
        raise MalformedLine("Unknown line type", line)
 
 
173
    def __init__(self, orig_pos, orig_range, mod_pos, mod_range):
 
 
174
        self.orig_pos = orig_pos
 
 
175
        self.orig_range = orig_range
 
 
176
        self.mod_pos = mod_pos
 
 
177
        self.mod_range = mod_range
 
 
180
    def get_header(self):
 
 
181
        return "@@ -%s +%s @@\n" % (self.range_str(self.orig_pos, 
 
 
183
                                    self.range_str(self.mod_pos, 
 
 
186
    def range_str(self, pos, range):
 
 
187
        """Return a file range, special-casing for 1-line files.
 
 
189
        :param pos: The position in the file
 
 
191
        :range: The range in the file
 
 
193
        :return: a string in the format 1,4 except when range == pos == 1
 
 
198
            return "%i,%i" % (pos, range)
 
 
201
        lines = [self.get_header()]
 
 
202
        for line in self.lines:
 
 
203
            lines.append(str(line))
 
 
204
        return "".join(lines)
 
 
206
    def shift_to_mod(self, pos):
 
 
207
        if pos < self.orig_pos-1:
 
 
209
        elif pos > self.orig_pos+self.orig_range:
 
 
210
            return self.mod_range - self.orig_range
 
 
212
            return self.shift_to_mod_lines(pos)
 
 
214
    def shift_to_mod_lines(self, pos):
 
 
215
        assert (pos >= self.orig_pos-1 and pos <= self.orig_pos+self.orig_range)
 
 
216
        position = self.orig_pos-1
 
 
218
        for line in self.lines:
 
 
219
            if isinstance(line, InsertLine):
 
 
221
            elif isinstance(line, RemoveLine):
 
 
226
            elif isinstance(line, ContextLine):
 
 
233
def iter_hunks(iter_lines):
 
 
235
    for line in iter_lines:
 
 
243
        hunk = hunk_from_header(line)
 
 
246
        while orig_size < hunk.orig_range or mod_size < hunk.mod_range:
 
 
247
            hunk_line = parse_line(iter_lines.next())
 
 
248
            hunk.lines.append(hunk_line)
 
 
249
            if isinstance(hunk_line, (RemoveLine, ContextLine)):
 
 
251
            if isinstance(hunk_line, (InsertLine, ContextLine)):
 
 
258
    def __init__(self, oldname, newname):
 
 
259
        self.oldname = oldname
 
 
260
        self.newname = newname
 
 
264
        ret = self.get_header() 
 
 
265
        ret += "".join([str(h) for h in self.hunks])
 
 
268
    def get_header(self):
 
 
269
        return "--- %s\n+++ %s\n" % (self.oldname, self.newname)
 
 
272
        """Return a string of patch statistics"""
 
 
275
        for hunk in self.hunks:
 
 
276
            for line in hunk.lines:
 
 
277
                if isinstance(line, InsertLine):
 
 
279
                elif isinstance(line, RemoveLine):
 
 
281
        return "%i inserts, %i removes in %i hunks" % \
 
 
282
            (inserts, removes, len(self.hunks))
 
 
284
    def pos_in_mod(self, position):
 
 
286
        for hunk in self.hunks:
 
 
287
            shift = hunk.shift_to_mod(position)
 
 
293
    def iter_inserted(self):
 
 
294
        """Iteraties through inserted lines
 
 
296
        :return: Pair of line number, line
 
 
297
        :rtype: iterator of (int, InsertLine)
 
 
299
        for hunk in self.hunks:
 
 
300
            pos = hunk.mod_pos - 1;
 
 
301
            for line in hunk.lines:
 
 
302
                if isinstance(line, InsertLine):
 
 
305
                if isinstance(line, ContextLine):
 
 
309
def parse_patch(iter_lines):
 
 
310
    (orig_name, mod_name) = get_patch_names(iter_lines)
 
 
311
    patch = Patch(orig_name, mod_name)
 
 
312
    for hunk in iter_hunks(iter_lines):
 
 
313
        patch.hunks.append(hunk)
 
 
317
def iter_file_patch(iter_lines):
 
 
319
    for line in iter_lines:
 
 
320
        if line.startswith('=== ') or line.startswith('*** '):
 
 
322
        if line.startswith('#'):
 
 
324
        elif line.startswith('--- '):
 
 
325
            if len(saved_lines) > 0:
 
 
328
        saved_lines.append(line)
 
 
329
    if len(saved_lines) > 0:
 
 
333
def iter_lines_handle_nl(iter_lines):
 
 
335
    Iterates through lines, ensuring that lines that originally had no
 
 
336
    terminating \n are produced without one.  This transformation may be
 
 
337
    applied at any point up until hunk line parsing, and is safe to apply
 
 
341
    for line in iter_lines:
 
 
343
            assert last_line.endswith('\n')
 
 
344
            last_line = last_line[:-1]
 
 
346
        if last_line is not None:
 
 
349
    if last_line is not None:
 
 
353
def parse_patches(iter_lines):
 
 
354
    iter_lines = iter_lines_handle_nl(iter_lines)
 
 
355
    return [parse_patch(f.__iter__()) for f in iter_file_patch(iter_lines)]
 
 
358
def difference_index(atext, btext):
 
 
359
    """Find the indext of the first character that differs between two texts
 
 
361
    :param atext: The first text
 
 
363
    :param btext: The second text
 
 
365
    :return: The index, or None if there are no differences within the range
 
 
366
    :rtype: int or NoneType
 
 
369
    if len(btext) < length:
 
 
371
    for i in range(length):
 
 
372
        if atext[i] != btext[i]:
 
 
377
def iter_patched(orig_lines, patch_lines):
 
 
378
    """Iterate through a series of lines with a patch applied.
 
 
379
    This handles a single file, and does exact, not fuzzy patching.
 
 
381
    if orig_lines is not None:
 
 
382
        orig_lines = orig_lines.__iter__()
 
 
384
    patch_lines = iter_lines_handle_nl(patch_lines.__iter__())
 
 
385
    get_patch_names(patch_lines)
 
 
387
    for hunk in iter_hunks(patch_lines):
 
 
388
        while line_no < hunk.orig_pos:
 
 
389
            orig_line = orig_lines.next()
 
 
392
        for hunk_line in hunk.lines:
 
 
393
            seen_patch.append(str(hunk_line))
 
 
394
            if isinstance(hunk_line, InsertLine):
 
 
395
                yield hunk_line.contents
 
 
396
            elif isinstance(hunk_line, (ContextLine, RemoveLine)):
 
 
397
                orig_line = orig_lines.next()
 
 
398
                if orig_line != hunk_line.contents:
 
 
399
                    raise PatchConflict(line_no, orig_line, "".join(seen_patch))
 
 
400
                if isinstance(hunk_line, ContextLine):
 
 
403
                    assert isinstance(hunk_line, RemoveLine)
 
 
405
    if orig_lines is not None:
 
 
406
        for line in orig_lines: