~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/patches.py

  • Committer: Patch Queue Manager
  • Date: 2011-09-22 14:12:18 UTC
  • mfrom: (6155.3.1 jam)
  • Revision ID: pqm@pqm.ubuntu.com-20110922141218-86s4uu6nqvourw4f
(jameinel) Cleanup comments bzrlib/smart/__init__.py (John A Meinel)

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2004 - 2006 Aaron Bentley
 
1
# Copyright (C) 2005-2010 Aaron Bentley, Canonical Ltd
2
2
# <aaron.bentley@utoronto.ca>
3
3
#
4
 
#    This program is free software; you can redistribute it and/or modify
5
 
#    it under the terms of the GNU General Public License as published by
6
 
#    the Free Software Foundation; either version 2 of the License, or
7
 
#    (at your option) any later version.
8
 
#
9
 
#    This program is distributed in the hope that it will be useful,
10
 
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
 
#    GNU General Public License for more details.
13
 
#
14
 
#    You should have received a copy of the GNU General Public License
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
 
4
# This program is free software; you can redistribute it and/or modify
 
5
# it under the terms of the GNU General Public License as published by
 
6
# the Free Software Foundation; either version 2 of the License, or
 
7
# (at your option) any later version.
 
8
#
 
9
# This program is distributed in the hope that it will be useful,
 
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
12
# GNU General Public License for more details.
 
13
#
 
14
# You should have received a copy of the GNU General Public License
 
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.')
17
29
 
18
30
 
19
31
class PatchSyntax(Exception):
57
69
def get_patch_names(iter_lines):
58
70
    try:
59
71
        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))
60
75
        if not line.startswith("--- "):
61
76
            raise MalformedPatchHeader("No orig name", line)
62
77
        else:
92
107
    range = int(range)
93
108
    return (pos, range)
94
109
 
95
 
 
 
110
 
96
111
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)
 
112
    import re
 
113
    matches = re.match(r'\@\@ ([^@]*) \@\@( (.*))?\n', line)
 
114
    if matches is None:
 
115
        raise MalformedHunkHeader("Does not match format.", line)
100
116
    try:
101
 
        (orig, mod) = line[3:-4].split(" ")
102
 
    except Exception, e:
 
117
        (orig, mod) = matches.group(1).split(" ")
 
118
    except (ValueError, IndexError), e:
103
119
        raise MalformedHunkHeader(str(e), line)
104
120
    if not orig.startswith('-') or not mod.startswith('+'):
105
121
        raise MalformedHunkHeader("Positions don't start with + or -.", line)
106
122
    try:
107
123
        (orig_pos, orig_range) = parse_range(orig[1:])
108
124
        (mod_pos, mod_range) = parse_range(mod[1:])
109
 
    except Exception, e:
 
125
    except (ValueError, IndexError), e:
110
126
        raise MalformedHunkHeader(str(e), line)
111
127
    if mod_range < 0 or orig_range < 0:
112
128
        raise MalformedHunkHeader("Hunk range is negative", line)
113
 
    return Hunk(orig_pos, orig_range, mod_pos, mod_range)
 
129
    tail = matches.group(3)
 
130
    return Hunk(orig_pos, orig_range, mod_pos, mod_range, tail)
114
131
 
115
132
 
116
133
class HunkLine:
162
179
        return InsertLine(line[1:])
163
180
    elif line.startswith("-"):
164
181
        return RemoveLine(line[1:])
165
 
    elif line == NO_NL:
166
 
        return NO_NL
167
182
    else:
168
183
        raise MalformedLine("Unknown line type", line)
169
184
__pychecker__=""
170
185
 
171
186
 
172
187
class Hunk:
173
 
    def __init__(self, orig_pos, orig_range, mod_pos, mod_range):
 
188
    def __init__(self, orig_pos, orig_range, mod_pos, mod_range, tail=None):
174
189
        self.orig_pos = orig_pos
175
190
        self.orig_range = orig_range
176
191
        self.mod_pos = mod_pos
177
192
        self.mod_range = mod_range
 
193
        self.tail = tail
178
194
        self.lines = []
179
195
 
180
196
    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))
 
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)
185
206
 
186
207
    def range_str(self, pos, range):
187
208
        """Return a file range, special-casing for 1-line files.
212
233
            return self.shift_to_mod_lines(pos)
213
234
 
214
235
    def shift_to_mod_lines(self, pos):
215
 
        assert (pos >= self.orig_pos-1 and pos <= self.orig_pos+self.orig_range)
216
236
        position = self.orig_pos-1
217
237
        shift = 0
218
238
        for line in self.lines:
230
250
        return shift
231
251
 
232
252
 
233
 
def iter_hunks(iter_lines):
 
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
    '''
234
260
    hunk = None
235
261
    for line in iter_lines:
236
262
        if line == "\n":
240
266
            continue
241
267
        if hunk is not None:
242
268
            yield hunk
243
 
        hunk = hunk_from_header(line)
 
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
244
278
        orig_size = 0
245
279
        mod_size = 0
246
280
        while orig_size < hunk.orig_range or mod_size < hunk.mod_range:
254
288
        yield hunk
255
289
 
256
290
 
257
 
class Patch:
 
291
class BinaryPatch(object):
258
292
    def __init__(self, oldname, newname):
259
293
        self.oldname = oldname
260
294
        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)
261
304
        self.hunks = []
262
305
 
263
306
    def __str__(self):
264
 
        ret = self.get_header() 
 
307
        ret = self.get_header()
265
308
        ret += "".join([str(h) for h in self.hunks])
266
309
        return ret
267
310
 
268
311
    def get_header(self):
269
312
        return "--- %s\n+++ %s\n" % (self.oldname, self.newname)
270
313
 
271
 
    def stats_str(self):
272
 
        """Return a string of patch statistics"""
 
314
    def stats_values(self):
 
315
        """Calculate the number of inserts and removes."""
273
316
        removes = 0
274
317
        inserts = 0
275
318
        for hunk in self.hunks:
278
321
                     inserts+=1;
279
322
                elif isinstance(line, RemoveLine):
280
323
                     removes+=1;
 
324
        return (inserts, removes, len(self.hunks))
 
325
 
 
326
    def stats_str(self):
 
327
        """Return a string of patch statistics"""
281
328
        return "%i inserts, %i removes in %i hunks" % \
282
 
            (inserts, removes, len(self.hunks))
 
329
            self.stats_values()
283
330
 
284
331
    def pos_in_mod(self, position):
285
332
        newpos = position
289
336
                return None
290
337
            newpos += shift
291
338
        return newpos
292
 
            
 
339
 
293
340
    def iter_inserted(self):
294
341
        """Iteraties through inserted lines
295
 
        
 
342
 
296
343
        :return: Pair of line number, line
297
344
        :rtype: iterator of (int, InsertLine)
298
345
        """
306
353
                    pos += 1
307
354
 
308
355
 
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):
 
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)
318
389
    saved_lines = []
 
390
    orig_range = 0
 
391
    beginning = True
319
392
    for line in iter_lines:
320
393
        if line.startswith('=== ') or line.startswith('*** '):
321
394
            continue
322
 
        elif line.startswith('--- '):
323
 
            if len(saved_lines) > 0:
 
395
        if line.startswith('#'):
 
396
            continue
 
397
        elif orig_range > 0:
 
398
            if line.startswith('-') or line.startswith(' '):
 
399
                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:
324
407
                yield saved_lines
325
408
            saved_lines = []
 
409
        elif line.startswith('@@'):
 
410
            hunk = hunk_from_header(line)
 
411
            orig_range = hunk.orig_range
326
412
        saved_lines.append(line)
327
413
    if len(saved_lines) > 0:
328
414
        yield saved_lines
338
424
    last_line = None
339
425
    for line in iter_lines:
340
426
        if line == NO_NL:
341
 
            assert last_line.endswith('\n')
 
427
            if not last_line.endswith('\n'):
 
428
                raise AssertionError()
342
429
            last_line = last_line[:-1]
343
430
            line = None
344
431
        if last_line is not None:
348
435
        yield last_line
349
436
 
350
437
 
351
 
def parse_patches(iter_lines):
352
 
    iter_lines = iter_lines_handle_nl(iter_lines)
353
 
    return [parse_patch(f.__iter__()) for f in iter_file_patch(iter_lines)]
 
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)]
354
447
 
355
448
 
356
449
def difference_index(atext, btext):
357
 
    """Find the indext of the first character that differs betweeen two texts
 
450
    """Find the indext of the first character that differs between two texts
358
451
 
359
452
    :param atext: The first text
360
453
    :type atext: str
376
469
    """Iterate through a series of lines with a patch applied.
377
470
    This handles a single file, and does exact, not fuzzy patching.
378
471
    """
379
 
    if orig_lines is not None:
380
 
        orig_lines = orig_lines.__iter__()
 
472
    patch_lines = iter_lines_handle_nl(iter(patch_lines))
 
473
    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
    """
381
484
    seen_patch = []
382
 
    patch_lines = iter_lines_handle_nl(patch_lines.__iter__())
383
 
    get_patch_names(patch_lines)
384
485
    line_no = 1
385
 
    for hunk in iter_hunks(patch_lines):
 
486
    if orig_lines is not None:
 
487
        orig_lines = iter(orig_lines)
 
488
    for hunk in hunks:
386
489
        while line_no < hunk.orig_pos:
387
490
            orig_line = orig_lines.next()
388
491
            yield orig_line
398
501
                if isinstance(hunk_line, ContextLine):
399
502
                    yield orig_line
400
503
                else:
401
 
                    assert isinstance(hunk_line, RemoveLine)
 
504
                    if not isinstance(hunk_line, RemoveLine):
 
505
                        raise AssertionError(hunk_line)
402
506
                line_no += 1
403
507
    if orig_lines is not None:
404
508
        for line in orig_lines: