~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/patches.py

  • Committer: Robert Collins
  • Date: 2007-05-07 16:48:14 UTC
  • mto: This revision was merged to the branch mainline in revision 2485.
  • Revision ID: robertc@robertcollins.net-20070507164814-wpagonutf4b5cf8s
Move HACKING to docs/developers/HACKING and adjust Makefile to accomodate this.

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)
 
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
 
    import re
78
 
    matches = re.match(r'\@\@ ([^@]*) \@\@( (.*))?\n', line)
79
 
    if matches is None:
80
 
        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)
81
100
    try:
82
 
        (orig, mod) = matches.group(1).split(" ")
83
 
    except (ValueError, IndexError), e:
 
101
        (orig, mod) = line[3:-4].split(" ")
 
102
    except Exception, e:
84
103
        raise MalformedHunkHeader(str(e), line)
85
104
    if not orig.startswith('-') or not mod.startswith('+'):
86
105
        raise MalformedHunkHeader("Positions don't start with + or -.", line)
87
106
    try:
88
107
        (orig_pos, orig_range) = parse_range(orig[1:])
89
108
        (mod_pos, mod_range) = parse_range(mod[1:])
90
 
    except (ValueError, IndexError), e:
 
109
    except Exception, e:
91
110
        raise MalformedHunkHeader(str(e), line)
92
111
    if mod_range < 0 or orig_range < 0:
93
112
        raise MalformedHunkHeader("Hunk range is negative", line)
94
 
    tail = matches.group(3)
95
 
    return Hunk(orig_pos, orig_range, mod_pos, mod_range, tail)
 
113
    return Hunk(orig_pos, orig_range, mod_pos, mod_range)
96
114
 
97
115
 
98
116
class HunkLine:
144
162
        return InsertLine(line[1:])
145
163
    elif line.startswith("-"):
146
164
        return RemoveLine(line[1:])
 
165
    elif line == NO_NL:
 
166
        return NO_NL
147
167
    else:
148
168
        raise MalformedLine("Unknown line type", line)
149
169
__pychecker__=""
150
170
 
151
171
 
152
172
class Hunk:
153
 
    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):
154
174
        self.orig_pos = orig_pos
155
175
        self.orig_range = orig_range
156
176
        self.mod_pos = mod_pos
157
177
        self.mod_range = mod_range
158
 
        self.tail = tail
159
178
        self.lines = []
160
179
 
161
180
    def get_header(self):
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)
 
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))
171
185
 
172
186
    def range_str(self, pos, range):
173
187
        """Return a file range, special-casing for 1-line files.
198
212
            return self.shift_to_mod_lines(pos)
199
213
 
200
214
    def shift_to_mod_lines(self, pos):
 
215
        assert (pos >= self.orig_pos-1 and pos <= self.orig_pos+self.orig_range)
201
216
        position = self.orig_pos-1
202
217
        shift = 0
203
218
        for line in self.lines:
215
230
        return shift
216
231
 
217
232
 
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
 
    '''
 
233
def iter_hunks(iter_lines):
225
234
    hunk = None
226
235
    for line in iter_lines:
227
236
        if line == "\n":
231
240
            continue
232
241
        if hunk is not None:
233
242
            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
 
243
        hunk = hunk_from_header(line)
243
244
        orig_size = 0
244
245
        mod_size = 0
245
246
        while orig_size < hunk.orig_range or mod_size < hunk.mod_range:
253
254
        yield hunk
254
255
 
255
256
 
256
 
class BinaryPatch(object):
 
257
class Patch:
257
258
    def __init__(self, oldname, newname):
258
259
        self.oldname = oldname
259
260
        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
261
        self.hunks = []
270
262
 
271
263
    def __str__(self):
272
 
        ret = self.get_header()
 
264
        ret = self.get_header() 
273
265
        ret += "".join([str(h) for h in self.hunks])
274
266
        return ret
275
267
 
276
268
    def get_header(self):
277
269
        return "--- %s\n+++ %s\n" % (self.oldname, self.newname)
278
270
 
279
 
    def stats_values(self):
280
 
        """Calculate the number of inserts and removes."""
 
271
    def stats_str(self):
 
272
        """Return a string of patch statistics"""
281
273
        removes = 0
282
274
        inserts = 0
283
275
        for hunk in self.hunks:
286
278
                     inserts+=1;
287
279
                elif isinstance(line, RemoveLine):
288
280
                     removes+=1;
289
 
        return (inserts, removes, len(self.hunks))
290
 
 
291
 
    def stats_str(self):
292
 
        """Return a string of patch statistics"""
293
281
        return "%i inserts, %i removes in %i hunks" % \
294
 
            self.stats_values()
 
282
            (inserts, removes, len(self.hunks))
295
283
 
296
284
    def pos_in_mod(self, position):
297
285
        newpos = position
301
289
                return None
302
290
            newpos += shift
303
291
        return newpos
304
 
 
 
292
            
305
293
    def iter_inserted(self):
306
294
        """Iteraties through inserted lines
307
 
 
 
295
        
308
296
        :return: Pair of line number, line
309
297
        :rtype: iterator of (int, InsertLine)
310
298
        """
318
306
                    pos += 1
319
307
 
320
308
 
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)
 
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
319
    orig_range = 0
356
 
    beginning = True
357
320
    for line in iter_lines:
358
321
        if line.startswith('=== ') or line.startswith('*** '):
359
322
            continue
362
325
        elif orig_range > 0:
363
326
            if line.startswith('-') or line.startswith(' '):
364
327
                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:
 
328
        elif line.startswith('--- '):
 
329
            if len(saved_lines) > 0:
372
330
                yield saved_lines
373
331
            saved_lines = []
374
332
        elif line.startswith('@@'):
389
347
    last_line = None
390
348
    for line in iter_lines:
391
349
        if line == NO_NL:
392
 
            if not last_line.endswith('\n'):
393
 
                raise AssertionError()
 
350
            assert last_line.endswith('\n')
394
351
            last_line = last_line[:-1]
395
352
            line = None
396
353
        if last_line is not None:
400
357
        yield last_line
401
358
 
402
359
 
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)]
 
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)]
412
363
 
413
364
 
414
365
def difference_index(atext, btext):
434
385
    """Iterate through a series of lines with a patch applied.
435
386
    This handles a single file, and does exact, not fuzzy patching.
436
387
    """
437
 
    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__())
438
392
    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
 
    """
449
 
    seen_patch = []
450
393
    line_no = 1
451
 
    if orig_lines is not None:
452
 
        orig_lines = iter(orig_lines)
453
 
    for hunk in hunks:
 
394
    for hunk in iter_hunks(patch_lines):
454
395
        while line_no < hunk.orig_pos:
455
396
            orig_line = orig_lines.next()
456
397
            yield orig_line
466
407
                if isinstance(hunk_line, ContextLine):
467
408
                    yield orig_line
468
409
                else:
469
 
                    if not isinstance(hunk_line, RemoveLine):
470
 
                        raise AssertionError(hunk_line)
 
410
                    assert isinstance(hunk_line, RemoveLine)
471
411
                line_no += 1
472
412
    if orig_lines is not None:
473
413
        for line in orig_lines: