~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/patches.py

(vila) Fix test failures blocking package builds. (Vincent Ladeuil)

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005-2010 Aaron Bentley, Canonical Ltd
 
2
# <aaron.bentley@utoronto.ca>
 
3
#
 
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.
 
8
#
 
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.
 
13
#
 
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
 
17
 
 
18
from __future__ import absolute_import
 
19
 
 
20
from bzrlib.errors import (
 
21
    BinaryFiles,
 
22
    MalformedHunkHeader,
 
23
    MalformedLine,
 
24
    MalformedPatchHeader,
 
25
    PatchConflict,
 
26
    PatchSyntax,
 
27
    )
 
28
 
 
29
import re
 
30
 
 
31
 
 
32
binary_files_re = 'Binary files (.*) and (.*) differ\n'
 
33
 
 
34
 
 
35
def get_patch_names(iter_lines):
 
36
    line = iter_lines.next()
 
37
    try:
 
38
        match = re.match(binary_files_re, line)
 
39
        if match is not None:
 
40
            raise BinaryFiles(match.group(1), match.group(2))
 
41
        if not line.startswith("--- "):
 
42
            raise MalformedPatchHeader("No orig name", line)
 
43
        else:
 
44
            orig_name = line[4:].rstrip("\n")
 
45
    except StopIteration:
 
46
        raise MalformedPatchHeader("No orig line", "")
 
47
    try:
 
48
        line = iter_lines.next()
 
49
        if not line.startswith("+++ "):
 
50
            raise PatchSyntax("No mod name")
 
51
        else:
 
52
            mod_name = line[4:].rstrip("\n")
 
53
    except StopIteration:
 
54
        raise MalformedPatchHeader("No mod line", "")
 
55
    return (orig_name, mod_name)
 
56
 
 
57
 
 
58
def parse_range(textrange):
 
59
    """Parse a patch range, handling the "1" special-case
 
60
 
 
61
    :param textrange: The text to parse
 
62
    :type textrange: str
 
63
    :return: the position and range, as a tuple
 
64
    :rtype: (int, int)
 
65
    """
 
66
    tmp = textrange.split(',')
 
67
    if len(tmp) == 1:
 
68
        pos = tmp[0]
 
69
        range = "1"
 
70
    else:
 
71
        (pos, range) = tmp
 
72
    pos = int(pos)
 
73
    range = int(range)
 
74
    return (pos, range)
 
75
 
 
76
 
 
77
def hunk_from_header(line):
 
78
    import re
 
79
    matches = re.match(r'\@\@ ([^@]*) \@\@( (.*))?\n', line)
 
80
    if matches is None:
 
81
        raise MalformedHunkHeader("Does not match format.", line)
 
82
    try:
 
83
        (orig, mod) = matches.group(1).split(" ")
 
84
    except (ValueError, IndexError), e:
 
85
        raise MalformedHunkHeader(str(e), line)
 
86
    if not orig.startswith('-') or not mod.startswith('+'):
 
87
        raise MalformedHunkHeader("Positions don't start with + or -.", line)
 
88
    try:
 
89
        (orig_pos, orig_range) = parse_range(orig[1:])
 
90
        (mod_pos, mod_range) = parse_range(mod[1:])
 
91
    except (ValueError, IndexError), e:
 
92
        raise MalformedHunkHeader(str(e), line)
 
93
    if mod_range < 0 or orig_range < 0:
 
94
        raise MalformedHunkHeader("Hunk range is negative", line)
 
95
    tail = matches.group(3)
 
96
    return Hunk(orig_pos, orig_range, mod_pos, mod_range, tail)
 
97
 
 
98
 
 
99
class HunkLine:
 
100
    def __init__(self, contents):
 
101
        self.contents = contents
 
102
 
 
103
    def get_str(self, leadchar):
 
104
        if self.contents == "\n" and leadchar == " " and False:
 
105
            return "\n"
 
106
        if not self.contents.endswith('\n'):
 
107
            terminator = '\n' + NO_NL
 
108
        else:
 
109
            terminator = ''
 
110
        return leadchar + self.contents + terminator
 
111
 
 
112
 
 
113
class ContextLine(HunkLine):
 
114
    def __init__(self, contents):
 
115
        HunkLine.__init__(self, contents)
 
116
 
 
117
    def __str__(self):
 
118
        return self.get_str(" ")
 
119
 
 
120
 
 
121
class InsertLine(HunkLine):
 
122
    def __init__(self, contents):
 
123
        HunkLine.__init__(self, contents)
 
124
 
 
125
    def __str__(self):
 
126
        return self.get_str("+")
 
127
 
 
128
 
 
129
class RemoveLine(HunkLine):
 
130
    def __init__(self, contents):
 
131
        HunkLine.__init__(self, contents)
 
132
 
 
133
    def __str__(self):
 
134
        return self.get_str("-")
 
135
 
 
136
NO_NL = '\\ No newline at end of file\n'
 
137
__pychecker__="no-returnvalues"
 
138
 
 
139
def parse_line(line):
 
140
    if line.startswith("\n"):
 
141
        return ContextLine(line)
 
142
    elif line.startswith(" "):
 
143
        return ContextLine(line[1:])
 
144
    elif line.startswith("+"):
 
145
        return InsertLine(line[1:])
 
146
    elif line.startswith("-"):
 
147
        return RemoveLine(line[1:])
 
148
    else:
 
149
        raise MalformedLine("Unknown line type", line)
 
150
__pychecker__=""
 
151
 
 
152
 
 
153
class Hunk:
 
154
    def __init__(self, orig_pos, orig_range, mod_pos, mod_range, tail=None):
 
155
        self.orig_pos = orig_pos
 
156
        self.orig_range = orig_range
 
157
        self.mod_pos = mod_pos
 
158
        self.mod_range = mod_range
 
159
        self.tail = tail
 
160
        self.lines = []
 
161
 
 
162
    def get_header(self):
 
163
        if self.tail is None:
 
164
            tail_str = ''
 
165
        else:
 
166
            tail_str = ' ' + self.tail
 
167
        return "@@ -%s +%s @@%s\n" % (self.range_str(self.orig_pos,
 
168
                                                     self.orig_range),
 
169
                                      self.range_str(self.mod_pos,
 
170
                                                     self.mod_range),
 
171
                                      tail_str)
 
172
 
 
173
    def range_str(self, pos, range):
 
174
        """Return a file range, special-casing for 1-line files.
 
175
 
 
176
        :param pos: The position in the file
 
177
        :type pos: int
 
178
        :range: The range in the file
 
179
        :type range: int
 
180
        :return: a string in the format 1,4 except when range == pos == 1
 
181
        """
 
182
        if range == 1:
 
183
            return "%i" % pos
 
184
        else:
 
185
            return "%i,%i" % (pos, range)
 
186
 
 
187
    def __str__(self):
 
188
        lines = [self.get_header()]
 
189
        for line in self.lines:
 
190
            lines.append(str(line))
 
191
        return "".join(lines)
 
192
 
 
193
    def shift_to_mod(self, pos):
 
194
        if pos < self.orig_pos-1:
 
195
            return 0
 
196
        elif pos > self.orig_pos+self.orig_range:
 
197
            return self.mod_range - self.orig_range
 
198
        else:
 
199
            return self.shift_to_mod_lines(pos)
 
200
 
 
201
    def shift_to_mod_lines(self, pos):
 
202
        position = self.orig_pos-1
 
203
        shift = 0
 
204
        for line in self.lines:
 
205
            if isinstance(line, InsertLine):
 
206
                shift += 1
 
207
            elif isinstance(line, RemoveLine):
 
208
                if position == pos:
 
209
                    return None
 
210
                shift -= 1
 
211
                position += 1
 
212
            elif isinstance(line, ContextLine):
 
213
                position += 1
 
214
            if position > pos:
 
215
                break
 
216
        return shift
 
217
 
 
218
 
 
219
def iter_hunks(iter_lines, allow_dirty=False):
 
220
    '''
 
221
    :arg iter_lines: iterable of lines to parse for hunks
 
222
    :kwarg allow_dirty: If True, when we encounter something that is not
 
223
        a hunk header when we're looking for one, assume the rest of the lines
 
224
        are not part of the patch (comments or other junk).  Default False
 
225
    '''
 
226
    hunk = None
 
227
    for line in iter_lines:
 
228
        if line == "\n":
 
229
            if hunk is not None:
 
230
                yield hunk
 
231
                hunk = None
 
232
            continue
 
233
        if hunk is not None:
 
234
            yield hunk
 
235
        try:
 
236
            hunk = hunk_from_header(line)
 
237
        except MalformedHunkHeader:
 
238
            if allow_dirty:
 
239
                # If the line isn't a hunk header, then we've reached the end
 
240
                # of this patch and there's "junk" at the end.  Ignore the
 
241
                # rest of this patch.
 
242
                return
 
243
            raise
 
244
        orig_size = 0
 
245
        mod_size = 0
 
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)):
 
250
                orig_size += 1
 
251
            if isinstance(hunk_line, (InsertLine, ContextLine)):
 
252
                mod_size += 1
 
253
    if hunk is not None:
 
254
        yield hunk
 
255
 
 
256
 
 
257
class BinaryPatch(object):
 
258
    def __init__(self, oldname, newname):
 
259
        self.oldname = oldname
 
260
        self.newname = newname
 
261
 
 
262
    def __str__(self):
 
263
        return 'Binary files %s and %s differ\n' % (self.oldname, self.newname)
 
264
 
 
265
 
 
266
class Patch(BinaryPatch):
 
267
 
 
268
    def __init__(self, oldname, newname):
 
269
        BinaryPatch.__init__(self, oldname, newname)
 
270
        self.hunks = []
 
271
 
 
272
    def __str__(self):
 
273
        ret = self.get_header()
 
274
        ret += "".join([str(h) for h in self.hunks])
 
275
        return ret
 
276
 
 
277
    def get_header(self):
 
278
        return "--- %s\n+++ %s\n" % (self.oldname, self.newname)
 
279
 
 
280
    def stats_values(self):
 
281
        """Calculate the number of inserts and removes."""
 
282
        removes = 0
 
283
        inserts = 0
 
284
        for hunk in self.hunks:
 
285
            for line in hunk.lines:
 
286
                if isinstance(line, InsertLine):
 
287
                     inserts+=1;
 
288
                elif isinstance(line, RemoveLine):
 
289
                     removes+=1;
 
290
        return (inserts, removes, len(self.hunks))
 
291
 
 
292
    def stats_str(self):
 
293
        """Return a string of patch statistics"""
 
294
        return "%i inserts, %i removes in %i hunks" % \
 
295
            self.stats_values()
 
296
 
 
297
    def pos_in_mod(self, position):
 
298
        newpos = position
 
299
        for hunk in self.hunks:
 
300
            shift = hunk.shift_to_mod(position)
 
301
            if shift is None:
 
302
                return None
 
303
            newpos += shift
 
304
        return newpos
 
305
 
 
306
    def iter_inserted(self):
 
307
        """Iteraties through inserted lines
 
308
 
 
309
        :return: Pair of line number, line
 
310
        :rtype: iterator of (int, InsertLine)
 
311
        """
 
312
        for hunk in self.hunks:
 
313
            pos = hunk.mod_pos - 1;
 
314
            for line in hunk.lines:
 
315
                if isinstance(line, InsertLine):
 
316
                    yield (pos, line)
 
317
                    pos += 1
 
318
                if isinstance(line, ContextLine):
 
319
                    pos += 1
 
320
 
 
321
def parse_patch(iter_lines, allow_dirty=False):
 
322
    '''
 
323
    :arg iter_lines: iterable of lines to parse
 
324
    :kwarg allow_dirty: If True, allow the patch to have trailing junk.
 
325
        Default False
 
326
    '''
 
327
    iter_lines = iter_lines_handle_nl(iter_lines)
 
328
    try:
 
329
        (orig_name, mod_name) = get_patch_names(iter_lines)
 
330
    except BinaryFiles, e:
 
331
        return BinaryPatch(e.orig_name, e.mod_name)
 
332
    else:
 
333
        patch = Patch(orig_name, mod_name)
 
334
        for hunk in iter_hunks(iter_lines, allow_dirty):
 
335
            patch.hunks.append(hunk)
 
336
        return patch
 
337
 
 
338
 
 
339
def iter_file_patch(iter_lines, allow_dirty=False, keep_dirty=False):
 
340
    '''
 
341
    :arg iter_lines: iterable of lines to parse for patches
 
342
    :kwarg allow_dirty: If True, allow comments and other non-patch text
 
343
        before the first patch.  Note that the algorithm here can only find
 
344
        such text before any patches have been found.  Comments after the
 
345
        first patch are stripped away in iter_hunks() if it is also passed
 
346
        allow_dirty=True.  Default False.
 
347
    '''
 
348
    ### FIXME: Docstring is not quite true.  We allow certain comments no
 
349
    # matter what, If they startwith '===', '***', or '#' Someone should
 
350
    # reexamine this logic and decide if we should include those in
 
351
    # allow_dirty or restrict those to only being before the patch is found
 
352
    # (as allow_dirty does).
 
353
    regex = re.compile(binary_files_re)
 
354
    saved_lines = []
 
355
    dirty_head = []
 
356
    orig_range = 0
 
357
    beginning = True
 
358
 
 
359
    for line in iter_lines:
 
360
        if line.startswith('=== '):
 
361
            dirty_head.append(line)
 
362
            continue
 
363
        if line.startswith('*** '):
 
364
            continue
 
365
        if line.startswith('#'):
 
366
            continue
 
367
        elif orig_range > 0:
 
368
            if line.startswith('-') or line.startswith(' '):
 
369
                orig_range -= 1
 
370
        elif line.startswith('--- ') or regex.match(line):
 
371
            if allow_dirty and beginning:
 
372
                # Patches can have "junk" at the beginning
 
373
                # Stripping junk from the end of patches is handled when we
 
374
                # parse the patch
 
375
                beginning = False
 
376
            elif len(saved_lines) > 0:
 
377
                if keep_dirty and len(dirty_head) > 0:
 
378
                    yield {'saved_lines': saved_lines,
 
379
                           'dirty_head': dirty_head}
 
380
                    dirty_head = []
 
381
                else:
 
382
                    yield saved_lines
 
383
            saved_lines = []
 
384
        elif line.startswith('@@'):
 
385
            hunk = hunk_from_header(line)
 
386
            orig_range = hunk.orig_range
 
387
        saved_lines.append(line)
 
388
    if len(saved_lines) > 0:
 
389
        if keep_dirty and len(dirty_head) > 0:
 
390
            yield {'saved_lines': saved_lines,
 
391
                   'dirty_head': dirty_head}
 
392
        else:
 
393
            yield saved_lines
 
394
 
 
395
 
 
396
def iter_lines_handle_nl(iter_lines):
 
397
    """
 
398
    Iterates through lines, ensuring that lines that originally had no
 
399
    terminating \n are produced without one.  This transformation may be
 
400
    applied at any point up until hunk line parsing, and is safe to apply
 
401
    repeatedly.
 
402
    """
 
403
    last_line = None
 
404
    for line in iter_lines:
 
405
        if line == NO_NL:
 
406
            if not last_line.endswith('\n'):
 
407
                raise AssertionError()
 
408
            last_line = last_line[:-1]
 
409
            line = None
 
410
        if last_line is not None:
 
411
            yield last_line
 
412
        last_line = line
 
413
    if last_line is not None:
 
414
        yield last_line
 
415
 
 
416
 
 
417
def parse_patches(iter_lines, allow_dirty=False, keep_dirty=False):
 
418
    '''
 
419
    :arg iter_lines: iterable of lines to parse for patches
 
420
    :kwarg allow_dirty: If True, allow text that's not part of the patch at
 
421
        selected places.  This includes comments before and after a patch
 
422
        for instance.  Default False.
 
423
    :kwarg keep_dirty: If True, returns a dict of patches with dirty headers.
 
424
        Default False.
 
425
    '''
 
426
    patches = []
 
427
    for patch_lines in iter_file_patch(iter_lines, allow_dirty, keep_dirty):
 
428
        if 'dirty_head' in patch_lines:
 
429
            patches.append({'patch': parse_patch(
 
430
                patch_lines['saved_lines'], allow_dirty),
 
431
                            'dirty_head': patch_lines['dirty_head']})
 
432
        else:
 
433
            patches.append(parse_patch(patch_lines, allow_dirty))
 
434
    return patches
 
435
 
 
436
 
 
437
def difference_index(atext, btext):
 
438
    """Find the indext of the first character that differs between two texts
 
439
 
 
440
    :param atext: The first text
 
441
    :type atext: str
 
442
    :param btext: The second text
 
443
    :type str: str
 
444
    :return: The index, or None if there are no differences within the range
 
445
    :rtype: int or NoneType
 
446
    """
 
447
    length = len(atext)
 
448
    if len(btext) < length:
 
449
        length = len(btext)
 
450
    for i in range(length):
 
451
        if atext[i] != btext[i]:
 
452
            return i;
 
453
    return None
 
454
 
 
455
 
 
456
def iter_patched(orig_lines, patch_lines):
 
457
    """Iterate through a series of lines with a patch applied.
 
458
    This handles a single file, and does exact, not fuzzy patching.
 
459
    """
 
460
    patch_lines = iter_lines_handle_nl(iter(patch_lines))
 
461
    get_patch_names(patch_lines)
 
462
    return iter_patched_from_hunks(orig_lines, iter_hunks(patch_lines))
 
463
 
 
464
 
 
465
def iter_patched_from_hunks(orig_lines, hunks):
 
466
    """Iterate through a series of lines with a patch applied.
 
467
    This handles a single file, and does exact, not fuzzy patching.
 
468
 
 
469
    :param orig_lines: The unpatched lines.
 
470
    :param hunks: An iterable of Hunk instances.
 
471
    """
 
472
    seen_patch = []
 
473
    line_no = 1
 
474
    if orig_lines is not None:
 
475
        orig_lines = iter(orig_lines)
 
476
    for hunk in hunks:
 
477
        while line_no < hunk.orig_pos:
 
478
            orig_line = orig_lines.next()
 
479
            yield orig_line
 
480
            line_no += 1
 
481
        for hunk_line in hunk.lines:
 
482
            seen_patch.append(str(hunk_line))
 
483
            if isinstance(hunk_line, InsertLine):
 
484
                yield hunk_line.contents
 
485
            elif isinstance(hunk_line, (ContextLine, RemoveLine)):
 
486
                orig_line = orig_lines.next()
 
487
                if orig_line != hunk_line.contents:
 
488
                    raise PatchConflict(line_no, orig_line, "".join(seen_patch))
 
489
                if isinstance(hunk_line, ContextLine):
 
490
                    yield orig_line
 
491
                else:
 
492
                    if not isinstance(hunk_line, RemoveLine):
 
493
                        raise AssertionError(hunk_line)
 
494
                line_no += 1
 
495
    if orig_lines is not None:
 
496
        for line in orig_lines:
 
497
            yield line