~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/patches.py

  • Committer: John Arbash Meinel
  • Author(s): Mark Hammond
  • Date: 2008-09-09 17:02:21 UTC
  • mto: This revision was merged to the branch mainline in revision 3697.
  • Revision ID: john@arbash-meinel.com-20080909170221-svim3jw2mrz0amp3
An updated transparent icon for bzr.

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
 
 
 
16
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
29
17
import re
30
18
 
31
19
 
32
 
binary_files_re = 'Binary files (.*) and (.*) differ\n'
 
20
class PatchSyntax(Exception):
 
21
    def __init__(self, msg):
 
22
        Exception.__init__(self, msg)
 
23
 
 
24
 
 
25
class MalformedPatchHeader(PatchSyntax):
 
26
    def __init__(self, desc, line):
 
27
        self.desc = desc
 
28
        self.line = line
 
29
        msg = "Malformed patch header.  %s\n%r" % (self.desc, self.line)
 
30
        PatchSyntax.__init__(self, msg)
 
31
 
 
32
 
 
33
class MalformedHunkHeader(PatchSyntax):
 
34
    def __init__(self, desc, line):
 
35
        self.desc = desc
 
36
        self.line = line
 
37
        msg = "Malformed hunk header.  %s\n%r" % (self.desc, self.line)
 
38
        PatchSyntax.__init__(self, msg)
 
39
 
 
40
 
 
41
class MalformedLine(PatchSyntax):
 
42
    def __init__(self, desc, line):
 
43
        self.desc = desc
 
44
        self.line = line
 
45
        msg = "Malformed line.  %s\n%s" % (self.desc, self.line)
 
46
        PatchSyntax.__init__(self, msg)
 
47
 
 
48
 
 
49
class PatchConflict(Exception):
 
50
    def __init__(self, line_no, orig_line, patch_line):
 
51
        orig = orig_line.rstrip('\n')
 
52
        patch = str(patch_line).rstrip('\n')
 
53
        msg = 'Text contents mismatch at line %d.  Original has "%s",'\
 
54
            ' but patch says it should be "%s"' % (line_no, orig, patch)
 
55
        Exception.__init__(self, msg)
33
56
 
34
57
 
35
58
def get_patch_names(iter_lines):
36
 
    line = iter_lines.next()
37
59
    try:
38
 
        match = re.match(binary_files_re, line)
39
 
        if match is not None:
40
 
            raise BinaryFiles(match.group(1), match.group(2))
 
60
        line = iter_lines.next()
41
61
        if not line.startswith("--- "):
42
62
            raise MalformedPatchHeader("No orig name", line)
43
63
        else:
73
93
    range = int(range)
74
94
    return (pos, range)
75
95
 
76
 
 
 
96
 
77
97
def hunk_from_header(line):
78
 
    import re
79
98
    matches = re.match(r'\@\@ ([^@]*) \@\@( (.*))?\n', line)
80
99
    if matches is None:
81
100
        raise MalformedHunkHeader("Does not match format.", line)
145
164
        return InsertLine(line[1:])
146
165
    elif line.startswith("-"):
147
166
        return RemoveLine(line[1:])
 
167
    elif line == NO_NL:
 
168
        return NO_NL
148
169
    else:
149
170
        raise MalformedLine("Unknown line type", line)
150
171
__pychecker__=""
216
237
        return shift
217
238
 
218
239
 
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
 
    '''
 
240
def iter_hunks(iter_lines):
226
241
    hunk = None
227
242
    for line in iter_lines:
228
243
        if line == "\n":
232
247
            continue
233
248
        if hunk is not None:
234
249
            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
 
250
        hunk = hunk_from_header(line)
244
251
        orig_size = 0
245
252
        mod_size = 0
246
253
        while orig_size < hunk.orig_range or mod_size < hunk.mod_range:
254
261
        yield hunk
255
262
 
256
263
 
257
 
class BinaryPatch(object):
 
264
class Patch:
258
265
    def __init__(self, oldname, newname):
259
266
        self.oldname = oldname
260
267
        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
268
        self.hunks = []
271
269
 
272
270
    def __str__(self):
273
 
        ret = self.get_header()
 
271
        ret = self.get_header() 
274
272
        ret += "".join([str(h) for h in self.hunks])
275
273
        return ret
276
274
 
277
275
    def get_header(self):
278
276
        return "--- %s\n+++ %s\n" % (self.oldname, self.newname)
279
277
 
280
 
    def stats_values(self):
281
 
        """Calculate the number of inserts and removes."""
 
278
    def stats_str(self):
 
279
        """Return a string of patch statistics"""
282
280
        removes = 0
283
281
        inserts = 0
284
282
        for hunk in self.hunks:
287
285
                     inserts+=1;
288
286
                elif isinstance(line, RemoveLine):
289
287
                     removes+=1;
290
 
        return (inserts, removes, len(self.hunks))
291
 
 
292
 
    def stats_str(self):
293
 
        """Return a string of patch statistics"""
294
288
        return "%i inserts, %i removes in %i hunks" % \
295
 
            self.stats_values()
 
289
            (inserts, removes, len(self.hunks))
296
290
 
297
291
    def pos_in_mod(self, position):
298
292
        newpos = position
302
296
                return None
303
297
            newpos += shift
304
298
        return newpos
305
 
 
 
299
            
306
300
    def iter_inserted(self):
307
301
        """Iteraties through inserted lines
308
 
 
 
302
        
309
303
        :return: Pair of line number, line
310
304
        :rtype: iterator of (int, InsertLine)
311
305
        """
318
312
                if isinstance(line, ContextLine):
319
313
                    pos += 1
320
314
 
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)
 
315
 
 
316
def parse_patch(iter_lines):
 
317
    (orig_name, mod_name) = get_patch_names(iter_lines)
 
318
    patch = Patch(orig_name, mod_name)
 
319
    for hunk in iter_hunks(iter_lines):
 
320
        patch.hunks.append(hunk)
 
321
    return patch
 
322
 
 
323
 
 
324
def iter_file_patch(iter_lines):
354
325
    saved_lines = []
355
 
    dirty_head = []
356
326
    orig_range = 0
357
 
    beginning = True
358
 
 
359
327
    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('*** '):
 
328
        if line.startswith('=== ') or line.startswith('*** '):
372
329
            continue
373
330
        if line.startswith('#'):
374
331
            continue
375
332
        elif orig_range > 0:
376
333
            if line.startswith('-') or line.startswith(' '):
377
334
                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
 
335
        elif line.startswith('--- '):
 
336
            if len(saved_lines) > 0:
 
337
                yield saved_lines
391
338
            saved_lines = []
392
339
        elif line.startswith('@@'):
393
340
            hunk = hunk_from_header(line)
394
341
            orig_range = hunk.orig_range
395
342
        saved_lines.append(line)
396
343
    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
 
344
        yield saved_lines
402
345
 
403
346
 
404
347
def iter_lines_handle_nl(iter_lines):
422
365
        yield last_line
423
366
 
424
367
 
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
 
368
def parse_patches(iter_lines):
 
369
    iter_lines = iter_lines_handle_nl(iter_lines)
 
370
    return [parse_patch(f.__iter__()) for f in iter_file_patch(iter_lines)]
443
371
 
444
372
 
445
373
def difference_index(atext, btext):
465
393
    """Iterate through a series of lines with a patch applied.
466
394
    This handles a single file, and does exact, not fuzzy patching.
467
395
    """
468
 
    patch_lines = iter_lines_handle_nl(iter(patch_lines))
 
396
    if orig_lines is not None:
 
397
        orig_lines = orig_lines.__iter__()
 
398
    seen_patch = []
 
399
    patch_lines = iter_lines_handle_nl(patch_lines.__iter__())
469
400
    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
401
    line_no = 1
482
 
    if orig_lines is not None:
483
 
        orig_lines = iter(orig_lines)
484
 
    for hunk in hunks:
 
402
    for hunk in iter_hunks(patch_lines):
485
403
        while line_no < hunk.orig_pos:
486
404
            orig_line = orig_lines.next()
487
405
            yield orig_line