~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/patches.py

  • Committer: Robert Collins
  • Date: 2007-07-04 01:39:50 UTC
  • mto: This revision was merged to the branch mainline in revision 2581.
  • Revision ID: robertc@robertcollins.net-20070704013950-7pp23plwyqjvgkxg
Review feedback.

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
 
import re
18
 
 
19
 
 
20
 
binary_files_re = 'Binary files (.*) and (.*) differ\n'
21
 
 
22
 
 
23
 
class BinaryFiles(Exception):
24
 
 
25
 
    def __init__(self, orig_name, mod_name):
26
 
        self.orig_name = orig_name
27
 
        self.mod_name = mod_name
28
 
        Exception.__init__(self, 'Binary files section encountered.')
 
16
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
29
17
 
30
18
 
31
19
class PatchSyntax(Exception):
69
57
def get_patch_names(iter_lines):
70
58
    try:
71
59
        line = iter_lines.next()
72
 
        match = re.match(binary_files_re, line)
73
 
        if match is not None:
74
 
            raise BinaryFiles(match.group(1), match.group(2))
75
60
        if not line.startswith("--- "):
76
61
            raise MalformedPatchHeader("No orig name", line)
77
62
        else:
107
92
    range = int(range)
108
93
    return (pos, range)
109
94
 
110
 
 
 
95
 
111
96
def hunk_from_header(line):
112
 
    import re
113
 
    matches = re.match(r'\@\@ ([^@]*) \@\@( (.*))?\n', line)
114
 
    if matches is None:
115
 
        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)
116
100
    try:
117
 
        (orig, mod) = matches.group(1).split(" ")
118
 
    except (ValueError, IndexError), e:
 
101
        (orig, mod) = line[3:-4].split(" ")
 
102
    except Exception, e:
119
103
        raise MalformedHunkHeader(str(e), line)
120
104
    if not orig.startswith('-') or not mod.startswith('+'):
121
105
        raise MalformedHunkHeader("Positions don't start with + or -.", line)
122
106
    try:
123
107
        (orig_pos, orig_range) = parse_range(orig[1:])
124
108
        (mod_pos, mod_range) = parse_range(mod[1:])
125
 
    except (ValueError, IndexError), e:
 
109
    except Exception, e:
126
110
        raise MalformedHunkHeader(str(e), line)
127
111
    if mod_range < 0 or orig_range < 0:
128
112
        raise MalformedHunkHeader("Hunk range is negative", line)
129
 
    tail = matches.group(3)
130
 
    return Hunk(orig_pos, orig_range, mod_pos, mod_range, tail)
 
113
    return Hunk(orig_pos, orig_range, mod_pos, mod_range)
131
114
 
132
115
 
133
116
class HunkLine:
179
162
        return InsertLine(line[1:])
180
163
    elif line.startswith("-"):
181
164
        return RemoveLine(line[1:])
 
165
    elif line == NO_NL:
 
166
        return NO_NL
182
167
    else:
183
168
        raise MalformedLine("Unknown line type", line)
184
169
__pychecker__=""
185
170
 
186
171
 
187
172
class Hunk:
188
 
    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):
189
174
        self.orig_pos = orig_pos
190
175
        self.orig_range = orig_range
191
176
        self.mod_pos = mod_pos
192
177
        self.mod_range = mod_range
193
 
        self.tail = tail
194
178
        self.lines = []
195
179
 
196
180
    def get_header(self):
197
 
        if self.tail is None:
198
 
            tail_str = ''
199
 
        else:
200
 
            tail_str = ' ' + self.tail
201
 
        return "@@ -%s +%s @@%s\n" % (self.range_str(self.orig_pos,
202
 
                                                     self.orig_range),
203
 
                                      self.range_str(self.mod_pos,
204
 
                                                     self.mod_range),
205
 
                                      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))
206
185
 
207
186
    def range_str(self, pos, range):
208
187
        """Return a file range, special-casing for 1-line files.
233
212
            return self.shift_to_mod_lines(pos)
234
213
 
235
214
    def shift_to_mod_lines(self, pos):
 
215
        assert (pos >= self.orig_pos-1 and pos <= self.orig_pos+self.orig_range)
236
216
        position = self.orig_pos-1
237
217
        shift = 0
238
218
        for line in self.lines:
250
230
        return shift
251
231
 
252
232
 
253
 
def iter_hunks(iter_lines, allow_dirty=False):
254
 
    '''
255
 
    :arg iter_lines: iterable of lines to parse for hunks
256
 
    :kwarg allow_dirty: If True, when we encounter something that is not
257
 
        a hunk header when we're looking for one, assume the rest of the lines
258
 
        are not part of the patch (comments or other junk).  Default False
259
 
    '''
 
233
def iter_hunks(iter_lines):
260
234
    hunk = None
261
235
    for line in iter_lines:
262
236
        if line == "\n":
266
240
            continue
267
241
        if hunk is not None:
268
242
            yield hunk
269
 
        try:
270
 
            hunk = hunk_from_header(line)
271
 
        except MalformedHunkHeader:
272
 
            if allow_dirty:
273
 
                # If the line isn't a hunk header, then we've reached the end
274
 
                # of this patch and there's "junk" at the end.  Ignore the
275
 
                # rest of this patch.
276
 
                return
277
 
            raise
 
243
        hunk = hunk_from_header(line)
278
244
        orig_size = 0
279
245
        mod_size = 0
280
246
        while orig_size < hunk.orig_range or mod_size < hunk.mod_range:
288
254
        yield hunk
289
255
 
290
256
 
291
 
class BinaryPatch(object):
 
257
class Patch:
292
258
    def __init__(self, oldname, newname):
293
259
        self.oldname = oldname
294
260
        self.newname = newname
295
 
 
296
 
    def __str__(self):
297
 
        return 'Binary files %s and %s differ\n' % (self.oldname, self.newname)
298
 
 
299
 
 
300
 
class Patch(BinaryPatch):
301
 
 
302
 
    def __init__(self, oldname, newname):
303
 
        BinaryPatch.__init__(self, oldname, newname)
304
261
        self.hunks = []
305
262
 
306
263
    def __str__(self):
307
 
        ret = self.get_header()
 
264
        ret = self.get_header() 
308
265
        ret += "".join([str(h) for h in self.hunks])
309
266
        return ret
310
267
 
311
268
    def get_header(self):
312
269
        return "--- %s\n+++ %s\n" % (self.oldname, self.newname)
313
270
 
314
 
    def stats_values(self):
315
 
        """Calculate the number of inserts and removes."""
 
271
    def stats_str(self):
 
272
        """Return a string of patch statistics"""
316
273
        removes = 0
317
274
        inserts = 0
318
275
        for hunk in self.hunks:
321
278
                     inserts+=1;
322
279
                elif isinstance(line, RemoveLine):
323
280
                     removes+=1;
324
 
        return (inserts, removes, len(self.hunks))
325
 
 
326
 
    def stats_str(self):
327
 
        """Return a string of patch statistics"""
328
281
        return "%i inserts, %i removes in %i hunks" % \
329
 
            self.stats_values()
 
282
            (inserts, removes, len(self.hunks))
330
283
 
331
284
    def pos_in_mod(self, position):
332
285
        newpos = position
336
289
                return None
337
290
            newpos += shift
338
291
        return newpos
339
 
 
 
292
            
340
293
    def iter_inserted(self):
341
294
        """Iteraties through inserted lines
342
 
 
 
295
        
343
296
        :return: Pair of line number, line
344
297
        :rtype: iterator of (int, InsertLine)
345
298
        """
353
306
                    pos += 1
354
307
 
355
308
 
356
 
def parse_patch(iter_lines, allow_dirty=False):
357
 
    '''
358
 
    :arg iter_lines: iterable of lines to parse
359
 
    :kwarg allow_dirty: If True, allow the patch to have trailing junk.
360
 
        Default False
361
 
    '''
362
 
    iter_lines = iter_lines_handle_nl(iter_lines)
363
 
    try:
364
 
        (orig_name, mod_name) = get_patch_names(iter_lines)
365
 
    except BinaryFiles, e:
366
 
        return BinaryPatch(e.orig_name, e.mod_name)
367
 
    else:
368
 
        patch = Patch(orig_name, mod_name)
369
 
        for hunk in iter_hunks(iter_lines, allow_dirty):
370
 
            patch.hunks.append(hunk)
371
 
        return patch
372
 
 
373
 
 
374
 
def iter_file_patch(iter_lines, allow_dirty=False):
375
 
    '''
376
 
    :arg iter_lines: iterable of lines to parse for patches
377
 
    :kwarg allow_dirty: If True, allow comments and other non-patch text
378
 
        before the first patch.  Note that the algorithm here can only find
379
 
        such text before any patches have been found.  Comments after the
380
 
        first patch are stripped away in iter_hunks() if it is also passed
381
 
        allow_dirty=True.  Default False.
382
 
    '''
383
 
    ### FIXME: Docstring is not quite true.  We allow certain comments no
384
 
    # matter what, If they startwith '===', '***', or '#' Someone should
385
 
    # reexamine this logic and decide if we should include those in
386
 
    # allow_dirty or restrict those to only being before the patch is found
387
 
    # (as allow_dirty does).
388
 
    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):
389
318
    saved_lines = []
390
319
    orig_range = 0
391
 
    beginning = True
392
320
    for line in iter_lines:
393
321
        if line.startswith('=== ') or line.startswith('*** '):
394
322
            continue
397
325
        elif orig_range > 0:
398
326
            if line.startswith('-') or line.startswith(' '):
399
327
                orig_range -= 1
400
 
        elif line.startswith('--- ') or regex.match(line):
401
 
            if allow_dirty and beginning:
402
 
                # Patches can have "junk" at the beginning
403
 
                # Stripping junk from the end of patches is handled when we
404
 
                # parse the patch
405
 
                beginning = False
406
 
            elif len(saved_lines) > 0:
 
328
        elif line.startswith('--- '):
 
329
            if len(saved_lines) > 0:
407
330
                yield saved_lines
408
331
            saved_lines = []
409
332
        elif line.startswith('@@'):
424
347
    last_line = None
425
348
    for line in iter_lines:
426
349
        if line == NO_NL:
427
 
            if not last_line.endswith('\n'):
428
 
                raise AssertionError()
 
350
            assert last_line.endswith('\n')
429
351
            last_line = last_line[:-1]
430
352
            line = None
431
353
        if last_line is not None:
435
357
        yield last_line
436
358
 
437
359
 
438
 
def parse_patches(iter_lines, allow_dirty=False):
439
 
    '''
440
 
    :arg iter_lines: iterable of lines to parse for patches
441
 
    :kwarg allow_dirty: If True, allow text that's not part of the patch at
442
 
        selected places.  This includes comments before and after a patch
443
 
        for instance.  Default False.
444
 
    '''
445
 
    return [parse_patch(f.__iter__(), allow_dirty) for f in
446
 
                        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)]
447
363
 
448
364
 
449
365
def difference_index(atext, btext):
469
385
    """Iterate through a series of lines with a patch applied.
470
386
    This handles a single file, and does exact, not fuzzy patching.
471
387
    """
472
 
    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__())
473
392
    get_patch_names(patch_lines)
474
 
    return iter_patched_from_hunks(orig_lines, iter_hunks(patch_lines))
475
 
 
476
 
 
477
 
def iter_patched_from_hunks(orig_lines, hunks):
478
 
    """Iterate through a series of lines with a patch applied.
479
 
    This handles a single file, and does exact, not fuzzy patching.
480
 
 
481
 
    :param orig_lines: The unpatched lines.
482
 
    :param hunks: An iterable of Hunk instances.
483
 
    """
484
 
    seen_patch = []
485
393
    line_no = 1
486
 
    if orig_lines is not None:
487
 
        orig_lines = iter(orig_lines)
488
 
    for hunk in hunks:
 
394
    for hunk in iter_hunks(patch_lines):
489
395
        while line_no < hunk.orig_pos:
490
396
            orig_line = orig_lines.next()
491
397
            yield orig_line
501
407
                if isinstance(hunk_line, ContextLine):
502
408
                    yield orig_line
503
409
                else:
504
 
                    if not isinstance(hunk_line, RemoveLine):
505
 
                        raise AssertionError(hunk_line)
 
410
                    assert isinstance(hunk_line, RemoveLine)
506
411
                line_no += 1
507
412
    if orig_lines is not None:
508
413
        for line in orig_lines: