~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/patches.py

  • Committer: John Arbash Meinel
  • Date: 2007-07-12 18:10:59 UTC
  • mto: This revision was merged to the branch mainline in revision 2643.
  • Revision ID: john@arbash-meinel.com-20070712181059-xnomv3tzzsb2hpx5
Add NEWS entries for performance improvements.

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 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'
 
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)
 
22
 
 
23
 
 
24
class MalformedPatchHeader(PatchSyntax):
 
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)
 
38
 
 
39
 
 
40
class MalformedLine(PatchSyntax):
 
41
    def __init__(self, desc, line):
 
42
        self.desc = desc
 
43
        self.line = line
 
44
        msg = "Malformed line.  %s\n%s" % (self.desc, self.line)
 
45
        PatchSyntax.__init__(self, msg)
 
46
 
 
47
 
 
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)
33
55
 
34
56
 
35
57
def get_patch_names(iter_lines):
36
 
    line = iter_lines.next()
37
58
    try:
38
 
        match = re.match(binary_files_re, line)
39
 
        if match is not None:
40
 
            raise BinaryFiles(match.group(1), match.group(2))
 
59
        line = iter_lines.next()
41
60
        if not line.startswith("--- "):
42
61
            raise MalformedPatchHeader("No orig name", line)
43
62
        else:
73
92
    range = int(range)
74
93
    return (pos, range)
75
94
 
76
 
 
 
95
 
77
96
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)
 
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)
82
100
    try:
83
 
        (orig, mod) = matches.group(1).split(" ")
84
 
    except (ValueError, IndexError), e:
 
101
        (orig, mod) = line[3:-4].split(" ")
 
102
    except Exception, e:
85
103
        raise MalformedHunkHeader(str(e), line)
86
104
    if not orig.startswith('-') or not mod.startswith('+'):
87
105
        raise MalformedHunkHeader("Positions don't start with + or -.", line)
88
106
    try:
89
107
        (orig_pos, orig_range) = parse_range(orig[1:])
90
108
        (mod_pos, mod_range) = parse_range(mod[1:])
91
 
    except (ValueError, IndexError), e:
 
109
    except Exception, e:
92
110
        raise MalformedHunkHeader(str(e), line)
93
111
    if mod_range < 0 or orig_range < 0:
94
112
        raise MalformedHunkHeader("Hunk range is negative", line)
95
 
    tail = matches.group(3)
96
 
    return Hunk(orig_pos, orig_range, mod_pos, mod_range, tail)
 
113
    return Hunk(orig_pos, orig_range, mod_pos, mod_range)
97
114
 
98
115
 
99
116
class HunkLine:
145
162
        return InsertLine(line[1:])
146
163
    elif line.startswith("-"):
147
164
        return RemoveLine(line[1:])
 
165
    elif line == NO_NL:
 
166
        return NO_NL
148
167
    else:
149
168
        raise MalformedLine("Unknown line type", line)
150
169
__pychecker__=""
151
170
 
152
171
 
153
172
class Hunk:
154
 
    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):
155
174
        self.orig_pos = orig_pos
156
175
        self.orig_range = orig_range
157
176
        self.mod_pos = mod_pos
158
177
        self.mod_range = mod_range
159
 
        self.tail = tail
160
178
        self.lines = []
161
179
 
162
180
    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)
 
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))
172
185
 
173
186
    def range_str(self, pos, range):
174
187
        """Return a file range, special-casing for 1-line files.
199
212
            return self.shift_to_mod_lines(pos)
200
213
 
201
214
    def shift_to_mod_lines(self, pos):
 
215
        assert (pos >= self.orig_pos-1 and pos <= self.orig_pos+self.orig_range)
202
216
        position = self.orig_pos-1
203
217
        shift = 0
204
218
        for line in self.lines:
216
230
        return shift
217
231
 
218
232
 
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
 
    '''
 
233
def iter_hunks(iter_lines):
226
234
    hunk = None
227
235
    for line in iter_lines:
228
236
        if line == "\n":
232
240
            continue
233
241
        if hunk is not None:
234
242
            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
 
243
        hunk = hunk_from_header(line)
244
244
        orig_size = 0
245
245
        mod_size = 0
246
246
        while orig_size < hunk.orig_range or mod_size < hunk.mod_range:
254
254
        yield hunk
255
255
 
256
256
 
257
 
class BinaryPatch(object):
 
257
class Patch:
258
258
    def __init__(self, oldname, newname):
259
259
        self.oldname = oldname
260
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
261
        self.hunks = []
271
262
 
272
263
    def __str__(self):
273
 
        ret = self.get_header()
 
264
        ret = self.get_header() 
274
265
        ret += "".join([str(h) for h in self.hunks])
275
266
        return ret
276
267
 
277
268
    def get_header(self):
278
269
        return "--- %s\n+++ %s\n" % (self.oldname, self.newname)
279
270
 
280
 
    def stats_values(self):
281
 
        """Calculate the number of inserts and removes."""
 
271
    def stats_str(self):
 
272
        """Return a string of patch statistics"""
282
273
        removes = 0
283
274
        inserts = 0
284
275
        for hunk in self.hunks:
287
278
                     inserts+=1;
288
279
                elif isinstance(line, RemoveLine):
289
280
                     removes+=1;
290
 
        return (inserts, removes, len(self.hunks))
291
 
 
292
 
    def stats_str(self):
293
 
        """Return a string of patch statistics"""
294
281
        return "%i inserts, %i removes in %i hunks" % \
295
 
            self.stats_values()
 
282
            (inserts, removes, len(self.hunks))
296
283
 
297
284
    def pos_in_mod(self, position):
298
285
        newpos = position
302
289
                return None
303
290
            newpos += shift
304
291
        return newpos
305
 
 
 
292
            
306
293
    def iter_inserted(self):
307
294
        """Iteraties through inserted lines
308
 
 
 
295
        
309
296
        :return: Pair of line number, line
310
297
        :rtype: iterator of (int, InsertLine)
311
298
        """
318
305
                if isinstance(line, ContextLine):
319
306
                    pos += 1
320
307
 
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)
 
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):
354
318
    saved_lines = []
355
 
    dirty_head = []
356
319
    orig_range = 0
357
 
    beginning = True
358
 
 
359
320
    for line in iter_lines:
360
 
        if line.startswith('=== '):
361
 
            if len(saved_lines) > 0:
362
 
                if keep_dirty and len(dirty_head) > 0:
363
 
                    yield {'saved_lines': saved_lines,
364
 
                           'dirty_head': dirty_head}
365
 
                    dirty_head = []
366
 
                else:
367
 
                    yield saved_lines
368
 
                saved_lines = []
369
 
            dirty_head.append(line)
370
 
            continue
371
 
        if line.startswith('*** '):
 
321
        if line.startswith('=== ') or line.startswith('*** '):
372
322
            continue
373
323
        if line.startswith('#'):
374
324
            continue
375
325
        elif orig_range > 0:
376
326
            if line.startswith('-') or line.startswith(' '):
377
327
                orig_range -= 1
378
 
        elif line.startswith('--- ') or regex.match(line):
379
 
            if allow_dirty and beginning:
380
 
                # Patches can have "junk" at the beginning
381
 
                # Stripping junk from the end of patches is handled when we
382
 
                # parse the patch
383
 
                beginning = False
384
 
            elif len(saved_lines) > 0:
385
 
                if keep_dirty and len(dirty_head) > 0:
386
 
                    yield {'saved_lines': saved_lines,
387
 
                           'dirty_head': dirty_head}
388
 
                    dirty_head = []
389
 
                else:
390
 
                    yield saved_lines
 
328
        elif line.startswith('--- '):
 
329
            if len(saved_lines) > 0:
 
330
                yield saved_lines
391
331
            saved_lines = []
392
332
        elif line.startswith('@@'):
393
333
            hunk = hunk_from_header(line)
394
334
            orig_range = hunk.orig_range
395
335
        saved_lines.append(line)
396
336
    if len(saved_lines) > 0:
397
 
        if keep_dirty and len(dirty_head) > 0:
398
 
            yield {'saved_lines': saved_lines,
399
 
                   'dirty_head': dirty_head}
400
 
        else:
401
 
            yield saved_lines
 
337
        yield saved_lines
402
338
 
403
339
 
404
340
def iter_lines_handle_nl(iter_lines):
411
347
    last_line = None
412
348
    for line in iter_lines:
413
349
        if line == NO_NL:
414
 
            if not last_line.endswith('\n'):
415
 
                raise AssertionError()
 
350
            assert last_line.endswith('\n')
416
351
            last_line = last_line[:-1]
417
352
            line = None
418
353
        if last_line is not None:
422
357
        yield last_line
423
358
 
424
359
 
425
 
def parse_patches(iter_lines, allow_dirty=False, keep_dirty=False):
426
 
    '''
427
 
    :arg iter_lines: iterable of lines to parse for patches
428
 
    :kwarg allow_dirty: If True, allow text that's not part of the patch at
429
 
        selected places.  This includes comments before and after a patch
430
 
        for instance.  Default False.
431
 
    :kwarg keep_dirty: If True, returns a dict of patches with dirty headers.
432
 
        Default False.
433
 
    '''
434
 
    patches = []
435
 
    for patch_lines in iter_file_patch(iter_lines, allow_dirty, keep_dirty):
436
 
        if 'dirty_head' in patch_lines:
437
 
            patches.append({'patch': parse_patch(
438
 
                patch_lines['saved_lines'], allow_dirty),
439
 
                            'dirty_head': patch_lines['dirty_head']})
440
 
        else:
441
 
            patches.append(parse_patch(patch_lines, allow_dirty))
442
 
    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)]
443
363
 
444
364
 
445
365
def difference_index(atext, btext):
465
385
    """Iterate through a series of lines with a patch applied.
466
386
    This handles a single file, and does exact, not fuzzy patching.
467
387
    """
468
 
    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__())
469
392
    get_patch_names(patch_lines)
470
 
    return iter_patched_from_hunks(orig_lines, iter_hunks(patch_lines))
471
 
 
472
 
 
473
 
def iter_patched_from_hunks(orig_lines, hunks):
474
 
    """Iterate through a series of lines with a patch applied.
475
 
    This handles a single file, and does exact, not fuzzy patching.
476
 
 
477
 
    :param orig_lines: The unpatched lines.
478
 
    :param hunks: An iterable of Hunk instances.
479
 
    """
480
 
    seen_patch = []
481
393
    line_no = 1
482
 
    if orig_lines is not None:
483
 
        orig_lines = iter(orig_lines)
484
 
    for hunk in hunks:
 
394
    for hunk in iter_hunks(patch_lines):
485
395
        while line_no < hunk.orig_pos:
486
396
            orig_line = orig_lines.next()
487
397
            yield orig_line
497
407
                if isinstance(hunk_line, ContextLine):
498
408
                    yield orig_line
499
409
                else:
500
 
                    if not isinstance(hunk_line, RemoveLine):
501
 
                        raise AssertionError(hunk_line)
 
410
                    assert isinstance(hunk_line, RemoveLine)
502
411
                line_no += 1
503
412
    if orig_lines is not None:
504
413
        for line in orig_lines: