~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/patches.py

  • Committer: Martin Pool
  • Date: 2009-01-13 03:06:36 UTC
  • mfrom: (3932.2.3 1.11)
  • mto: This revision was merged to the branch mainline in revision 3937.
  • Revision ID: mbp@sourcefrog.net-20090113030636-dqx4t8yaaqgdvam5
MergeĀ 1.11rc1

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, 2008 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)
 
55
 
33
56
 
34
57
def get_patch_names(iter_lines):
35
58
    try:
36
59
        line = iter_lines.next()
37
 
        match = re.match(binary_files_re, line)
38
 
        if match is not None:
39
 
            raise BinaryFiles(match.group(1), match.group(2))
40
60
        if not line.startswith("--- "):
41
61
            raise MalformedPatchHeader("No orig name", line)
42
62
        else:
72
92
    range = int(range)
73
93
    return (pos, range)
74
94
 
75
 
 
 
95
 
76
96
def hunk_from_header(line):
77
97
    import re
78
98
    matches = re.match(r'\@\@ ([^@]*) \@\@( (.*))?\n', line)
144
164
        return InsertLine(line[1:])
145
165
    elif line.startswith("-"):
146
166
        return RemoveLine(line[1:])
 
167
    elif line == NO_NL:
 
168
        return NO_NL
147
169
    else:
148
170
        raise MalformedLine("Unknown line type", line)
149
171
__pychecker__=""
215
237
        return shift
216
238
 
217
239
 
218
 
def iter_hunks(iter_lines, allow_dirty=False):
219
 
    '''
220
 
    :arg iter_lines: iterable of lines to parse for hunks
221
 
    :kwarg allow_dirty: If True, when we encounter something that is not
222
 
        a hunk header when we're looking for one, assume the rest of the lines
223
 
        are not part of the patch (comments or other junk).  Default False
224
 
    '''
 
240
def iter_hunks(iter_lines):
225
241
    hunk = None
226
242
    for line in iter_lines:
227
243
        if line == "\n":
231
247
            continue
232
248
        if hunk is not None:
233
249
            yield hunk
234
 
        try:
235
 
            hunk = hunk_from_header(line)
236
 
        except MalformedHunkHeader:
237
 
            if allow_dirty:
238
 
                # If the line isn't a hunk header, then we've reached the end
239
 
                # of this patch and there's "junk" at the end.  Ignore the
240
 
                # rest of this patch.
241
 
                return
242
 
            raise
 
250
        hunk = hunk_from_header(line)
243
251
        orig_size = 0
244
252
        mod_size = 0
245
253
        while orig_size < hunk.orig_range or mod_size < hunk.mod_range:
253
261
        yield hunk
254
262
 
255
263
 
256
 
class BinaryPatch(object):
 
264
class Patch:
257
265
    def __init__(self, oldname, newname):
258
266
        self.oldname = oldname
259
267
        self.newname = newname
260
 
 
261
 
    def __str__(self):
262
 
        return 'Binary files %s and %s differ\n' % (self.oldname, self.newname)
263
 
 
264
 
 
265
 
class Patch(BinaryPatch):
266
 
 
267
 
    def __init__(self, oldname, newname):
268
 
        BinaryPatch.__init__(self, oldname, newname)
269
268
        self.hunks = []
270
269
 
271
270
    def __str__(self):
272
 
        ret = self.get_header()
 
271
        ret = self.get_header() 
273
272
        ret += "".join([str(h) for h in self.hunks])
274
273
        return ret
275
274
 
276
275
    def get_header(self):
277
276
        return "--- %s\n+++ %s\n" % (self.oldname, self.newname)
278
277
 
279
 
    def stats_values(self):
280
 
        """Calculate the number of inserts and removes."""
 
278
    def stats_str(self):
 
279
        """Return a string of patch statistics"""
281
280
        removes = 0
282
281
        inserts = 0
283
282
        for hunk in self.hunks:
286
285
                     inserts+=1;
287
286
                elif isinstance(line, RemoveLine):
288
287
                     removes+=1;
289
 
        return (inserts, removes, len(self.hunks))
290
 
 
291
 
    def stats_str(self):
292
 
        """Return a string of patch statistics"""
293
288
        return "%i inserts, %i removes in %i hunks" % \
294
 
            self.stats_values()
 
289
            (inserts, removes, len(self.hunks))
295
290
 
296
291
    def pos_in_mod(self, position):
297
292
        newpos = position
301
296
                return None
302
297
            newpos += shift
303
298
        return newpos
304
 
 
 
299
            
305
300
    def iter_inserted(self):
306
301
        """Iteraties through inserted lines
307
 
 
 
302
        
308
303
        :return: Pair of line number, line
309
304
        :rtype: iterator of (int, InsertLine)
310
305
        """
318
313
                    pos += 1
319
314
 
320
315
 
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):
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)
 
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
326
    orig_range = 0
356
 
    beginning = True
357
327
    for line in iter_lines:
358
328
        if line.startswith('=== ') or line.startswith('*** '):
359
329
            continue
362
332
        elif orig_range > 0:
363
333
            if line.startswith('-') or line.startswith(' '):
364
334
                orig_range -= 1
365
 
        elif line.startswith('--- ') or regex.match(line):
366
 
            if allow_dirty and beginning:
367
 
                # Patches can have "junk" at the beginning
368
 
                # Stripping junk from the end of patches is handled when we
369
 
                # parse the patch
370
 
                beginning = False
371
 
            elif len(saved_lines) > 0:
 
335
        elif line.startswith('--- '):
 
336
            if len(saved_lines) > 0:
372
337
                yield saved_lines
373
338
            saved_lines = []
374
339
        elif line.startswith('@@'):
400
365
        yield last_line
401
366
 
402
367
 
403
 
def parse_patches(iter_lines, allow_dirty=False):
404
 
    '''
405
 
    :arg iter_lines: iterable of lines to parse for patches
406
 
    :kwarg allow_dirty: If True, allow text that's not part of the patch at
407
 
        selected places.  This includes comments before and after a patch
408
 
        for instance.  Default False.
409
 
    '''
410
 
    return [parse_patch(f.__iter__(), allow_dirty) for f in
411
 
                        iter_file_patch(iter_lines, allow_dirty)]
 
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)]
412
371
 
413
372
 
414
373
def difference_index(atext, btext):