/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/patches.py

Merge from bzr.dev, resolving conflicts.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005-2010 Aaron Bentley, Canonical Ltd
 
1
# Copyright (C) 2004 - 2006 Aaron Bentley, Canonical Ltd
2
2
# <aaron.bentley@utoronto.ca>
3
3
#
4
4
# This program is free software; you can redistribute it and/or modify
13
13
#
14
14
# You should have received a copy of the GNU General Public License
15
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 .errors import (
21
 
    BzrError,
22
 
    )
23
 
 
24
 
import re
25
 
 
26
 
 
27
 
binary_files_re = 'Binary files (.*) and (.*) differ\n'
28
 
 
29
 
 
30
 
class PatchSyntax(BzrError):
31
 
    """Base class for patch syntax errors."""
32
 
 
33
 
 
34
 
class BinaryFiles(BzrError):
35
 
 
36
 
    _fmt = 'Binary files section encountered.'
37
 
 
38
 
    def __init__(self, orig_name, mod_name):
39
 
        self.orig_name = orig_name
40
 
        self.mod_name = mod_name
 
16
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
17
 
 
18
 
 
19
class PatchSyntax(Exception):
 
20
    def __init__(self, msg):
 
21
        Exception.__init__(self, msg)
41
22
 
42
23
 
43
24
class MalformedPatchHeader(PatchSyntax):
44
 
 
45
 
    _fmt = "Malformed patch header.  %(desc)s\n%(line)r"
46
 
 
47
 
    def __init__(self, desc, line):
48
 
        self.desc = desc
49
 
        self.line = line
 
25
    def __init__(self, desc, line):
 
26
        self.desc = desc
 
27
        self.line = line
 
28
        msg = "Malformed patch header.  %s\n%r" % (self.desc, self.line)
 
29
        PatchSyntax.__init__(self, msg)
 
30
 
 
31
 
 
32
class MalformedHunkHeader(PatchSyntax):
 
33
    def __init__(self, desc, line):
 
34
        self.desc = desc
 
35
        self.line = line
 
36
        msg = "Malformed hunk header.  %s\n%r" % (self.desc, self.line)
 
37
        PatchSyntax.__init__(self, msg)
50
38
 
51
39
 
52
40
class MalformedLine(PatchSyntax):
53
 
 
54
 
    _fmt = "Malformed line.  %(desc)s\n%(line)r"
55
 
 
56
41
    def __init__(self, desc, line):
57
42
        self.desc = desc
58
43
        self.line = line
59
 
 
60
 
 
61
 
class PatchConflict(BzrError):
62
 
 
63
 
    _fmt = ('Text contents mismatch at line %(line_no)d.  Original has '
64
 
            '"%(orig_line)s", but patch says it should be "%(patch_line)s"')
65
 
 
 
44
        msg = "Malformed line.  %s\n%s" % (self.desc, self.line)
 
45
        PatchSyntax.__init__(self, msg)
 
46
 
 
47
 
 
48
class PatchConflict(Exception):
66
49
    def __init__(self, line_no, orig_line, patch_line):
67
 
        self.line_no = line_no
68
 
        self.orig_line = orig_line.rstrip('\n')
69
 
        self.patch_line = patch_line.rstrip('\n')
70
 
 
71
 
 
72
 
class MalformedHunkHeader(PatchSyntax):
73
 
 
74
 
    _fmt = "Malformed hunk header.  %(desc)s\n%(line)r"
75
 
 
76
 
    def __init__(self, desc, line):
77
 
        self.desc = desc
78
 
        self.line = 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)
79
55
 
80
56
 
81
57
def get_patch_names(iter_lines):
82
 
    line = next(iter_lines)
83
58
    try:
84
 
        match = re.match(binary_files_re, line)
85
 
        if match is not None:
86
 
            raise BinaryFiles(match.group(1), match.group(2))
 
59
        line = iter_lines.next()
87
60
        if not line.startswith("--- "):
88
61
            raise MalformedPatchHeader("No orig name", line)
89
62
        else:
91
64
    except StopIteration:
92
65
        raise MalformedPatchHeader("No orig line", "")
93
66
    try:
94
 
        line = next(iter_lines)
 
67
        line = iter_lines.next()
95
68
        if not line.startswith("+++ "):
96
69
            raise PatchSyntax("No mod name")
97
70
        else:
119
92
    range = int(range)
120
93
    return (pos, range)
121
94
 
122
 
 
 
95
 
123
96
def hunk_from_header(line):
124
 
    import re
125
 
    matches = re.match(r'\@\@ ([^@]*) \@\@( (.*))?\n', line)
126
 
    if matches is None:
127
 
        raise MalformedHunkHeader("Does not match format.", line)
 
97
    if not line.startswith("@@") or not line.endswith("@@\n") \
 
98
        or not len(line) > 4:
 
99
        raise MalformedHunkHeader("Does not start and end with @@.", line)
128
100
    try:
129
 
        (orig, mod) = matches.group(1).split(" ")
130
 
    except (ValueError, IndexError) as e:
 
101
        (orig, mod) = line[3:-4].split(" ")
 
102
    except Exception, e:
131
103
        raise MalformedHunkHeader(str(e), line)
132
104
    if not orig.startswith('-') or not mod.startswith('+'):
133
105
        raise MalformedHunkHeader("Positions don't start with + or -.", line)
134
106
    try:
135
107
        (orig_pos, orig_range) = parse_range(orig[1:])
136
108
        (mod_pos, mod_range) = parse_range(mod[1:])
137
 
    except (ValueError, IndexError) as e:
 
109
    except Exception, e:
138
110
        raise MalformedHunkHeader(str(e), line)
139
111
    if mod_range < 0 or orig_range < 0:
140
112
        raise MalformedHunkHeader("Hunk range is negative", line)
141
 
    tail = matches.group(3)
142
 
    return Hunk(orig_pos, orig_range, mod_pos, mod_range, tail)
 
113
    return Hunk(orig_pos, orig_range, mod_pos, mod_range)
143
114
 
144
115
 
145
116
class HunkLine:
191
162
        return InsertLine(line[1:])
192
163
    elif line.startswith("-"):
193
164
        return RemoveLine(line[1:])
 
165
    elif line == NO_NL:
 
166
        return NO_NL
194
167
    else:
195
168
        raise MalformedLine("Unknown line type", line)
196
169
__pychecker__=""
197
170
 
198
171
 
199
172
class Hunk:
200
 
    def __init__(self, orig_pos, orig_range, mod_pos, mod_range, tail=None):
 
173
    def __init__(self, orig_pos, orig_range, mod_pos, mod_range):
201
174
        self.orig_pos = orig_pos
202
175
        self.orig_range = orig_range
203
176
        self.mod_pos = mod_pos
204
177
        self.mod_range = mod_range
205
 
        self.tail = tail
206
178
        self.lines = []
207
179
 
208
180
    def get_header(self):
209
 
        if self.tail is None:
210
 
            tail_str = ''
211
 
        else:
212
 
            tail_str = ' ' + self.tail
213
 
        return "@@ -%s +%s @@%s\n" % (self.range_str(self.orig_pos,
214
 
                                                     self.orig_range),
215
 
                                      self.range_str(self.mod_pos,
216
 
                                                     self.mod_range),
217
 
                                      tail_str)
 
181
        return "@@ -%s +%s @@\n" % (self.range_str(self.orig_pos, 
 
182
                                                   self.orig_range),
 
183
                                    self.range_str(self.mod_pos, 
 
184
                                                   self.mod_range))
218
185
 
219
186
    def range_str(self, pos, range):
220
187
        """Return a file range, special-casing for 1-line files.
245
212
            return self.shift_to_mod_lines(pos)
246
213
 
247
214
    def shift_to_mod_lines(self, pos):
 
215
        assert (pos >= self.orig_pos-1 and pos <= self.orig_pos+self.orig_range)
248
216
        position = self.orig_pos-1
249
217
        shift = 0
250
218
        for line in self.lines:
262
230
        return shift
263
231
 
264
232
 
265
 
def iter_hunks(iter_lines, allow_dirty=False):
266
 
    '''
267
 
    :arg iter_lines: iterable of lines to parse for hunks
268
 
    :kwarg allow_dirty: If True, when we encounter something that is not
269
 
        a hunk header when we're looking for one, assume the rest of the lines
270
 
        are not part of the patch (comments or other junk).  Default False
271
 
    '''
 
233
def iter_hunks(iter_lines):
272
234
    hunk = None
273
235
    for line in iter_lines:
274
236
        if line == "\n":
278
240
            continue
279
241
        if hunk is not None:
280
242
            yield hunk
281
 
        try:
282
 
            hunk = hunk_from_header(line)
283
 
        except MalformedHunkHeader:
284
 
            if allow_dirty:
285
 
                # If the line isn't a hunk header, then we've reached the end
286
 
                # of this patch and there's "junk" at the end.  Ignore the
287
 
                # rest of this patch.
288
 
                return
289
 
            raise
 
243
        hunk = hunk_from_header(line)
290
244
        orig_size = 0
291
245
        mod_size = 0
292
246
        while orig_size < hunk.orig_range or mod_size < hunk.mod_range:
293
 
            hunk_line = parse_line(next(iter_lines))
 
247
            hunk_line = parse_line(iter_lines.next())
294
248
            hunk.lines.append(hunk_line)
295
249
            if isinstance(hunk_line, (RemoveLine, ContextLine)):
296
250
                orig_size += 1
300
254
        yield hunk
301
255
 
302
256
 
303
 
class BinaryPatch(object):
 
257
class Patch:
304
258
    def __init__(self, oldname, newname):
305
259
        self.oldname = oldname
306
260
        self.newname = newname
307
 
 
308
 
    def __str__(self):
309
 
        return 'Binary files %s and %s differ\n' % (self.oldname, self.newname)
310
 
 
311
 
 
312
 
class Patch(BinaryPatch):
313
 
 
314
 
    def __init__(self, oldname, newname):
315
 
        BinaryPatch.__init__(self, oldname, newname)
316
261
        self.hunks = []
317
262
 
318
263
    def __str__(self):
319
 
        ret = self.get_header()
 
264
        ret = self.get_header() 
320
265
        ret += "".join([str(h) for h in self.hunks])
321
266
        return ret
322
267
 
323
268
    def get_header(self):
324
269
        return "--- %s\n+++ %s\n" % (self.oldname, self.newname)
325
270
 
326
 
    def stats_values(self):
327
 
        """Calculate the number of inserts and removes."""
 
271
    def stats_str(self):
 
272
        """Return a string of patch statistics"""
328
273
        removes = 0
329
274
        inserts = 0
330
275
        for hunk in self.hunks:
333
278
                     inserts+=1;
334
279
                elif isinstance(line, RemoveLine):
335
280
                     removes+=1;
336
 
        return (inserts, removes, len(self.hunks))
337
 
 
338
 
    def stats_str(self):
339
 
        """Return a string of patch statistics"""
340
281
        return "%i inserts, %i removes in %i hunks" % \
341
 
            self.stats_values()
 
282
            (inserts, removes, len(self.hunks))
342
283
 
343
284
    def pos_in_mod(self, position):
344
285
        newpos = position
348
289
                return None
349
290
            newpos += shift
350
291
        return newpos
351
 
 
 
292
            
352
293
    def iter_inserted(self):
353
294
        """Iteraties through inserted lines
354
 
 
 
295
        
355
296
        :return: Pair of line number, line
356
297
        :rtype: iterator of (int, InsertLine)
357
298
        """
364
305
                if isinstance(line, ContextLine):
365
306
                    pos += 1
366
307
 
367
 
def parse_patch(iter_lines, allow_dirty=False):
368
 
    '''
369
 
    :arg iter_lines: iterable of lines to parse
370
 
    :kwarg allow_dirty: If True, allow the patch to have trailing junk.
371
 
        Default False
372
 
    '''
373
 
    iter_lines = iter_lines_handle_nl(iter_lines)
374
 
    try:
375
 
        (orig_name, mod_name) = get_patch_names(iter_lines)
376
 
    except BinaryFiles as e:
377
 
        return BinaryPatch(e.orig_name, e.mod_name)
378
 
    else:
379
 
        patch = Patch(orig_name, mod_name)
380
 
        for hunk in iter_hunks(iter_lines, allow_dirty):
381
 
            patch.hunks.append(hunk)
382
 
        return patch
383
 
 
384
 
 
385
 
def iter_file_patch(iter_lines, allow_dirty=False, keep_dirty=False):
386
 
    '''
387
 
    :arg iter_lines: iterable of lines to parse for patches
388
 
    :kwarg allow_dirty: If True, allow comments and other non-patch text
389
 
        before the first patch.  Note that the algorithm here can only find
390
 
        such text before any patches have been found.  Comments after the
391
 
        first patch are stripped away in iter_hunks() if it is also passed
392
 
        allow_dirty=True.  Default False.
393
 
    '''
394
 
    ### FIXME: Docstring is not quite true.  We allow certain comments no
395
 
    # matter what, If they startwith '===', '***', or '#' Someone should
396
 
    # reexamine this logic and decide if we should include those in
397
 
    # allow_dirty or restrict those to only being before the patch is found
398
 
    # (as allow_dirty does).
399
 
    regex = re.compile(binary_files_re)
 
308
 
 
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)
 
314
    return patch
 
315
 
 
316
 
 
317
def iter_file_patch(iter_lines):
400
318
    saved_lines = []
401
 
    dirty_head = []
402
319
    orig_range = 0
403
 
    beginning = True
404
 
 
405
320
    for line in iter_lines:
406
 
        if line.startswith('=== '):
407
 
            if len(saved_lines) > 0:
408
 
                if keep_dirty and len(dirty_head) > 0:
409
 
                    yield {'saved_lines': saved_lines,
410
 
                           'dirty_head': dirty_head}
411
 
                    dirty_head = []
412
 
                else:
413
 
                    yield saved_lines
414
 
                saved_lines = []
415
 
            dirty_head.append(line)
416
 
            continue
417
 
        if line.startswith('*** '):
 
321
        if line.startswith('=== ') or line.startswith('*** '):
418
322
            continue
419
323
        if line.startswith('#'):
420
324
            continue
421
325
        elif orig_range > 0:
422
326
            if line.startswith('-') or line.startswith(' '):
423
327
                orig_range -= 1
424
 
        elif line.startswith('--- ') or regex.match(line):
425
 
            if allow_dirty and beginning:
426
 
                # Patches can have "junk" at the beginning
427
 
                # Stripping junk from the end of patches is handled when we
428
 
                # parse the patch
429
 
                beginning = False
430
 
            elif len(saved_lines) > 0:
431
 
                if keep_dirty and len(dirty_head) > 0:
432
 
                    yield {'saved_lines': saved_lines,
433
 
                           'dirty_head': dirty_head}
434
 
                    dirty_head = []
435
 
                else:
436
 
                    yield saved_lines
 
328
        elif line.startswith('--- '):
 
329
            if len(saved_lines) > 0:
 
330
                yield saved_lines
437
331
            saved_lines = []
438
332
        elif line.startswith('@@'):
439
333
            hunk = hunk_from_header(line)
440
334
            orig_range = hunk.orig_range
441
335
        saved_lines.append(line)
442
336
    if len(saved_lines) > 0:
443
 
        if keep_dirty and len(dirty_head) > 0:
444
 
            yield {'saved_lines': saved_lines,
445
 
                   'dirty_head': dirty_head}
446
 
        else:
447
 
            yield saved_lines
 
337
        yield saved_lines
448
338
 
449
339
 
450
340
def iter_lines_handle_nl(iter_lines):
457
347
    last_line = None
458
348
    for line in iter_lines:
459
349
        if line == NO_NL:
460
 
            if not last_line.endswith('\n'):
461
 
                raise AssertionError()
 
350
            assert last_line.endswith('\n')
462
351
            last_line = last_line[:-1]
463
352
            line = None
464
353
        if last_line is not None:
468
357
        yield last_line
469
358
 
470
359
 
471
 
def parse_patches(iter_lines, allow_dirty=False, keep_dirty=False):
472
 
    '''
473
 
    :arg iter_lines: iterable of lines to parse for patches
474
 
    :kwarg allow_dirty: If True, allow text that's not part of the patch at
475
 
        selected places.  This includes comments before and after a patch
476
 
        for instance.  Default False.
477
 
    :kwarg keep_dirty: If True, returns a dict of patches with dirty headers.
478
 
        Default False.
479
 
    '''
480
 
    patches = []
481
 
    for patch_lines in iter_file_patch(iter_lines, allow_dirty, keep_dirty):
482
 
        if 'dirty_head' in patch_lines:
483
 
            patches.append({'patch': parse_patch(
484
 
                patch_lines['saved_lines'], allow_dirty),
485
 
                            'dirty_head': patch_lines['dirty_head']})
486
 
        else:
487
 
            patches.append(parse_patch(patch_lines, allow_dirty))
488
 
    return patches
 
360
def parse_patches(iter_lines):
 
361
    iter_lines = iter_lines_handle_nl(iter_lines)
 
362
    return [parse_patch(f.__iter__()) for f in iter_file_patch(iter_lines)]
489
363
 
490
364
 
491
365
def difference_index(atext, btext):
511
385
    """Iterate through a series of lines with a patch applied.
512
386
    This handles a single file, and does exact, not fuzzy patching.
513
387
    """
514
 
    patch_lines = iter_lines_handle_nl(iter(patch_lines))
 
388
    if orig_lines is not None:
 
389
        orig_lines = orig_lines.__iter__()
 
390
    seen_patch = []
 
391
    patch_lines = iter_lines_handle_nl(patch_lines.__iter__())
515
392
    get_patch_names(patch_lines)
516
 
    return iter_patched_from_hunks(orig_lines, iter_hunks(patch_lines))
517
 
 
518
 
 
519
 
def iter_patched_from_hunks(orig_lines, hunks):
520
 
    """Iterate through a series of lines with a patch applied.
521
 
    This handles a single file, and does exact, not fuzzy patching.
522
 
 
523
 
    :param orig_lines: The unpatched lines.
524
 
    :param hunks: An iterable of Hunk instances.
525
 
    """
526
 
    seen_patch = []
527
393
    line_no = 1
528
 
    if orig_lines is not None:
529
 
        orig_lines = iter(orig_lines)
530
 
    for hunk in hunks:
 
394
    for hunk in iter_hunks(patch_lines):
531
395
        while line_no < hunk.orig_pos:
532
 
            orig_line = next(orig_lines)
 
396
            orig_line = orig_lines.next()
533
397
            yield orig_line
534
398
            line_no += 1
535
399
        for hunk_line in hunk.lines:
537
401
            if isinstance(hunk_line, InsertLine):
538
402
                yield hunk_line.contents
539
403
            elif isinstance(hunk_line, (ContextLine, RemoveLine)):
540
 
                orig_line = next(orig_lines)
 
404
                orig_line = orig_lines.next()
541
405
                if orig_line != hunk_line.contents:
542
406
                    raise PatchConflict(line_no, orig_line, "".join(seen_patch))
543
407
                if isinstance(hunk_line, ContextLine):
544
408
                    yield orig_line
545
409
                else:
546
 
                    if not isinstance(hunk_line, RemoveLine):
547
 
                        raise AssertionError(hunk_line)
 
410
                    assert isinstance(hunk_line, RemoveLine)
548
411
                line_no += 1
549
412
    if orig_lines is not None:
550
413
        for line in orig_lines: