~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/patches.py

  • Committer: Vincent Ladeuil
  • Date: 2012-03-13 16:42:20 UTC
  • mto: This revision was merged to the branch mainline in revision 6512.
  • Revision ID: v.ladeuil+lp@free.fr-20120313164220-atkou2zprhlspmwg
Mention that a given config option cannot be safely handled via both APIs at the same time.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2004 - 2006 Aaron Bentley, Canonical Ltd
 
1
# Copyright (C) 2005-2010 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., 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
 
 
 
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'
56
33
 
57
34
def get_patch_names(iter_lines):
58
35
    try:
59
36
        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))
60
40
        if not line.startswith("--- "):
61
41
            raise MalformedPatchHeader("No orig name", line)
62
42
        else:
92
72
    range = int(range)
93
73
    return (pos, range)
94
74
 
95
 
 
 
75
 
96
76
def hunk_from_header(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)
 
77
    import re
 
78
    matches = re.match(r'\@\@ ([^@]*) \@\@( (.*))?\n', line)
 
79
    if matches is None:
 
80
        raise MalformedHunkHeader("Does not match format.", line)
100
81
    try:
101
 
        (orig, mod) = line[3:-4].split(" ")
102
 
    except Exception, e:
 
82
        (orig, mod) = matches.group(1).split(" ")
 
83
    except (ValueError, IndexError), e:
103
84
        raise MalformedHunkHeader(str(e), line)
104
85
    if not orig.startswith('-') or not mod.startswith('+'):
105
86
        raise MalformedHunkHeader("Positions don't start with + or -.", line)
106
87
    try:
107
88
        (orig_pos, orig_range) = parse_range(orig[1:])
108
89
        (mod_pos, mod_range) = parse_range(mod[1:])
109
 
    except Exception, e:
 
90
    except (ValueError, IndexError), e:
110
91
        raise MalformedHunkHeader(str(e), line)
111
92
    if mod_range < 0 or orig_range < 0:
112
93
        raise MalformedHunkHeader("Hunk range is negative", line)
113
 
    return Hunk(orig_pos, orig_range, mod_pos, mod_range)
 
94
    tail = matches.group(3)
 
95
    return Hunk(orig_pos, orig_range, mod_pos, mod_range, tail)
114
96
 
115
97
 
116
98
class HunkLine:
162
144
        return InsertLine(line[1:])
163
145
    elif line.startswith("-"):
164
146
        return RemoveLine(line[1:])
165
 
    elif line == NO_NL:
166
 
        return NO_NL
167
147
    else:
168
148
        raise MalformedLine("Unknown line type", line)
169
149
__pychecker__=""
170
150
 
171
151
 
172
152
class Hunk:
173
 
    def __init__(self, orig_pos, orig_range, mod_pos, mod_range):
 
153
    def __init__(self, orig_pos, orig_range, mod_pos, mod_range, tail=None):
174
154
        self.orig_pos = orig_pos
175
155
        self.orig_range = orig_range
176
156
        self.mod_pos = mod_pos
177
157
        self.mod_range = mod_range
 
158
        self.tail = tail
178
159
        self.lines = []
179
160
 
180
161
    def get_header(self):
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))
 
162
        if self.tail is None:
 
163
            tail_str = ''
 
164
        else:
 
165
            tail_str = ' ' + self.tail
 
166
        return "@@ -%s +%s @@%s\n" % (self.range_str(self.orig_pos,
 
167
                                                     self.orig_range),
 
168
                                      self.range_str(self.mod_pos,
 
169
                                                     self.mod_range),
 
170
                                      tail_str)
185
171
 
186
172
    def range_str(self, pos, range):
187
173
        """Return a file range, special-casing for 1-line files.
212
198
            return self.shift_to_mod_lines(pos)
213
199
 
214
200
    def shift_to_mod_lines(self, pos):
215
 
        assert (pos >= self.orig_pos-1 and pos <= self.orig_pos+self.orig_range)
216
201
        position = self.orig_pos-1
217
202
        shift = 0
218
203
        for line in self.lines:
230
215
        return shift
231
216
 
232
217
 
233
 
def iter_hunks(iter_lines):
 
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
    '''
234
225
    hunk = None
235
226
    for line in iter_lines:
236
227
        if line == "\n":
240
231
            continue
241
232
        if hunk is not None:
242
233
            yield hunk
243
 
        hunk = hunk_from_header(line)
 
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
244
243
        orig_size = 0
245
244
        mod_size = 0
246
245
        while orig_size < hunk.orig_range or mod_size < hunk.mod_range:
254
253
        yield hunk
255
254
 
256
255
 
257
 
class Patch:
 
256
class BinaryPatch(object):
258
257
    def __init__(self, oldname, newname):
259
258
        self.oldname = oldname
260
259
        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)
261
269
        self.hunks = []
262
270
 
263
271
    def __str__(self):
264
 
        ret = self.get_header() 
 
272
        ret = self.get_header()
265
273
        ret += "".join([str(h) for h in self.hunks])
266
274
        return ret
267
275
 
268
276
    def get_header(self):
269
277
        return "--- %s\n+++ %s\n" % (self.oldname, self.newname)
270
278
 
271
 
    def stats_str(self):
272
 
        """Return a string of patch statistics"""
 
279
    def stats_values(self):
 
280
        """Calculate the number of inserts and removes."""
273
281
        removes = 0
274
282
        inserts = 0
275
283
        for hunk in self.hunks:
278
286
                     inserts+=1;
279
287
                elif isinstance(line, RemoveLine):
280
288
                     removes+=1;
 
289
        return (inserts, removes, len(self.hunks))
 
290
 
 
291
    def stats_str(self):
 
292
        """Return a string of patch statistics"""
281
293
        return "%i inserts, %i removes in %i hunks" % \
282
 
            (inserts, removes, len(self.hunks))
 
294
            self.stats_values()
283
295
 
284
296
    def pos_in_mod(self, position):
285
297
        newpos = position
289
301
                return None
290
302
            newpos += shift
291
303
        return newpos
292
 
            
 
304
 
293
305
    def iter_inserted(self):
294
306
        """Iteraties through inserted lines
295
 
        
 
307
 
296
308
        :return: Pair of line number, line
297
309
        :rtype: iterator of (int, InsertLine)
298
310
        """
306
318
                    pos += 1
307
319
 
308
320
 
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):
 
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)
318
354
    saved_lines = []
 
355
    orig_range = 0
 
356
    beginning = True
319
357
    for line in iter_lines:
320
358
        if line.startswith('=== ') or line.startswith('*** '):
321
359
            continue
322
360
        if line.startswith('#'):
323
361
            continue
324
 
        elif line.startswith('--- '):
325
 
            if len(saved_lines) > 0:
 
362
        elif orig_range > 0:
 
363
            if line.startswith('-') or line.startswith(' '):
 
364
                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:
326
372
                yield saved_lines
327
373
            saved_lines = []
 
374
        elif line.startswith('@@'):
 
375
            hunk = hunk_from_header(line)
 
376
            orig_range = hunk.orig_range
328
377
        saved_lines.append(line)
329
378
    if len(saved_lines) > 0:
330
379
        yield saved_lines
340
389
    last_line = None
341
390
    for line in iter_lines:
342
391
        if line == NO_NL:
343
 
            assert last_line.endswith('\n')
 
392
            if not last_line.endswith('\n'):
 
393
                raise AssertionError()
344
394
            last_line = last_line[:-1]
345
395
            line = None
346
396
        if last_line is not None:
350
400
        yield last_line
351
401
 
352
402
 
353
 
def parse_patches(iter_lines):
354
 
    iter_lines = iter_lines_handle_nl(iter_lines)
355
 
    return [parse_patch(f.__iter__()) for f in iter_file_patch(iter_lines)]
 
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)]
356
412
 
357
413
 
358
414
def difference_index(atext, btext):
378
434
    """Iterate through a series of lines with a patch applied.
379
435
    This handles a single file, and does exact, not fuzzy patching.
380
436
    """
381
 
    if orig_lines is not None:
382
 
        orig_lines = orig_lines.__iter__()
 
437
    patch_lines = iter_lines_handle_nl(iter(patch_lines))
 
438
    get_patch_names(patch_lines)
 
439
    return iter_patched_from_hunks(orig_lines, iter_hunks(patch_lines))
 
440
 
 
441
 
 
442
def iter_patched_from_hunks(orig_lines, hunks):
 
443
    """Iterate through a series of lines with a patch applied.
 
444
    This handles a single file, and does exact, not fuzzy patching.
 
445
 
 
446
    :param orig_lines: The unpatched lines.
 
447
    :param hunks: An iterable of Hunk instances.
 
448
    """
383
449
    seen_patch = []
384
 
    patch_lines = iter_lines_handle_nl(patch_lines.__iter__())
385
 
    get_patch_names(patch_lines)
386
450
    line_no = 1
387
 
    for hunk in iter_hunks(patch_lines):
 
451
    if orig_lines is not None:
 
452
        orig_lines = iter(orig_lines)
 
453
    for hunk in hunks:
388
454
        while line_no < hunk.orig_pos:
389
455
            orig_line = orig_lines.next()
390
456
            yield orig_line
400
466
                if isinstance(hunk_line, ContextLine):
401
467
                    yield orig_line
402
468
                else:
403
 
                    assert isinstance(hunk_line, RemoveLine)
 
469
                    if not isinstance(hunk_line, RemoveLine):
 
470
                        raise AssertionError(hunk_line)
404
471
                line_no += 1
405
472
    if orig_lines is not None:
406
473
        for line in orig_lines: