~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/patches.py

  • Committer: Alexander Belchenko
  • Date: 2006-07-30 16:43:12 UTC
  • mto: (1711.2.111 jam-integration)
  • mto: This revision was merged to the branch mainline in revision 1906.
  • Revision ID: bialix@ukr.net-20060730164312-b025fd3ff0cee59e
rename  gpl.txt => COPYING.txt

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
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., 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.')
 
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
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
 
    orig_range = 0
391
 
    beginning = True
392
319
    for line in iter_lines:
393
320
        if line.startswith('=== ') or line.startswith('*** '):
394
321
            continue
395
322
        if line.startswith('#'):
396
323
            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
        elif line.startswith('--- '):
 
325
            if len(saved_lines) > 0:
407
326
                yield saved_lines
408
327
            saved_lines = []
409
 
        elif line.startswith('@@'):
410
 
            hunk = hunk_from_header(line)
411
 
            orig_range = hunk.orig_range
412
328
        saved_lines.append(line)
413
329
    if len(saved_lines) > 0:
414
330
        yield saved_lines
424
340
    last_line = None
425
341
    for line in iter_lines:
426
342
        if line == NO_NL:
427
 
            if not last_line.endswith('\n'):
428
 
                raise AssertionError()
 
343
            assert last_line.endswith('\n')
429
344
            last_line = last_line[:-1]
430
345
            line = None
431
346
        if last_line is not None:
435
350
        yield last_line
436
351
 
437
352
 
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)]
 
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)]
447
356
 
448
357
 
449
358
def difference_index(atext, btext):
469
378
    """Iterate through a series of lines with a patch applied.
470
379
    This handles a single file, and does exact, not fuzzy patching.
471
380
    """
472
 
    patch_lines = iter_lines_handle_nl(iter(patch_lines))
 
381
    if orig_lines is not None:
 
382
        orig_lines = orig_lines.__iter__()
 
383
    seen_patch = []
 
384
    patch_lines = iter_lines_handle_nl(patch_lines.__iter__())
473
385
    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
386
    line_no = 1
486
 
    if orig_lines is not None:
487
 
        orig_lines = iter(orig_lines)
488
 
    for hunk in hunks:
 
387
    for hunk in iter_hunks(patch_lines):
489
388
        while line_no < hunk.orig_pos:
490
389
            orig_line = orig_lines.next()
491
390
            yield orig_line
501
400
                if isinstance(hunk_line, ContextLine):
502
401
                    yield orig_line
503
402
                else:
504
 
                    if not isinstance(hunk_line, RemoveLine):
505
 
                        raise AssertionError(hunk_line)
 
403
                    assert isinstance(hunk_line, RemoveLine)
506
404
                line_no += 1
507
405
    if orig_lines is not None:
508
406
        for line in orig_lines: