/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/patches.py

  • Committer: Robert Collins
  • Date: 2007-04-19 02:27:44 UTC
  • mto: This revision was merged to the branch mainline in revision 2426.
  • Revision ID: robertc@robertcollins.net-20070419022744-pfdqz42kp1wizh43
``make docs`` now creates a man page at ``man1/bzr.1`` fixing bug 107388.
(Robert Collins)

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 .errors import (
19
 
    BzrError,
20
 
    )
21
 
 
22
 
import os
23
 
import re
24
 
 
25
 
 
26
 
binary_files_re = b'Binary files (.*) and (.*) differ\n'
27
 
 
28
 
 
29
 
class PatchSyntax(BzrError):
30
 
    """Base class for patch syntax errors."""
31
 
 
32
 
 
33
 
class BinaryFiles(BzrError):
34
 
 
35
 
    _fmt = 'Binary files section encountered.'
36
 
 
37
 
    def __init__(self, orig_name, mod_name):
38
 
        self.orig_name = orig_name
39
 
        self.mod_name = mod_name
 
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)
40
22
 
41
23
 
42
24
class MalformedPatchHeader(PatchSyntax):
43
 
 
44
 
    _fmt = "Malformed patch header.  %(desc)s\n%(line)r"
45
 
 
46
 
    def __init__(self, desc, line):
47
 
        self.desc = desc
48
 
        self.line = line
 
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)
49
38
 
50
39
 
51
40
class MalformedLine(PatchSyntax):
52
 
 
53
 
    _fmt = "Malformed line.  %(desc)s\n%(line)r"
54
 
 
55
41
    def __init__(self, desc, line):
56
42
        self.desc = desc
57
43
        self.line = line
58
 
 
59
 
 
60
 
class PatchConflict(BzrError):
61
 
 
62
 
    _fmt = ('Text contents mismatch at line %(line_no)d.  Original has '
63
 
            '"%(orig_line)s", but patch says it should be "%(patch_line)s"')
64
 
 
 
44
        msg = "Malformed line.  %s\n%s" % (self.desc, self.line)
 
45
        PatchSyntax.__init__(self, msg)
 
46
 
 
47
 
 
48
class PatchConflict(Exception):
65
49
    def __init__(self, line_no, orig_line, patch_line):
66
 
        self.line_no = line_no
67
 
        self.orig_line = orig_line.rstrip('\n')
68
 
        self.patch_line = patch_line.rstrip('\n')
69
 
 
70
 
 
71
 
class MalformedHunkHeader(PatchSyntax):
72
 
 
73
 
    _fmt = "Malformed hunk header.  %(desc)s\n%(line)r"
74
 
 
75
 
    def __init__(self, desc, line):
76
 
        self.desc = desc
77
 
        self.line = 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)
78
55
 
79
56
 
80
57
def get_patch_names(iter_lines):
81
 
    line = next(iter_lines)
82
58
    try:
83
 
        match = re.match(binary_files_re, line)
84
 
        if match is not None:
85
 
            raise BinaryFiles(match.group(1), match.group(2))
86
 
        if not line.startswith(b"--- "):
 
59
        line = iter_lines.next()
 
60
        if not line.startswith("--- "):
87
61
            raise MalformedPatchHeader("No orig name", line)
88
62
        else:
89
 
            orig_name = line[4:].rstrip(b"\n")
 
63
            orig_name = line[4:].rstrip("\n")
90
64
    except StopIteration:
91
65
        raise MalformedPatchHeader("No orig line", "")
92
66
    try:
93
 
        line = next(iter_lines)
94
 
        if not line.startswith(b"+++ "):
 
67
        line = iter_lines.next()
 
68
        if not line.startswith("+++ "):
95
69
            raise PatchSyntax("No mod name")
96
70
        else:
97
 
            mod_name = line[4:].rstrip(b"\n")
 
71
            mod_name = line[4:].rstrip("\n")
98
72
    except StopIteration:
99
73
        raise MalformedPatchHeader("No mod line", "")
100
74
    return (orig_name, mod_name)
108
82
    :return: the position and range, as a tuple
109
83
    :rtype: (int, int)
110
84
    """
111
 
    tmp = textrange.split(b',')
 
85
    tmp = textrange.split(',')
112
86
    if len(tmp) == 1:
113
87
        pos = tmp[0]
114
 
        range = b"1"
 
88
        range = "1"
115
89
    else:
116
90
        (pos, range) = tmp
117
91
    pos = int(pos)
118
92
    range = int(range)
119
93
    return (pos, range)
120
94
 
121
 
 
 
95
 
122
96
def hunk_from_header(line):
123
 
    import re
124
 
    matches = re.match(br'\@\@ ([^@]*) \@\@( (.*))?\n', line)
125
 
    if matches is None:
126
 
        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)
127
100
    try:
128
 
        (orig, mod) = matches.group(1).split(b" ")
129
 
    except (ValueError, IndexError) as e:
 
101
        (orig, mod) = line[3:-4].split(" ")
 
102
    except Exception, e:
130
103
        raise MalformedHunkHeader(str(e), line)
131
 
    if not orig.startswith(b'-') or not mod.startswith(b'+'):
 
104
    if not orig.startswith('-') or not mod.startswith('+'):
132
105
        raise MalformedHunkHeader("Positions don't start with + or -.", line)
133
106
    try:
134
107
        (orig_pos, orig_range) = parse_range(orig[1:])
135
108
        (mod_pos, mod_range) = parse_range(mod[1:])
136
 
    except (ValueError, IndexError) as e:
 
109
    except Exception, e:
137
110
        raise MalformedHunkHeader(str(e), line)
138
111
    if mod_range < 0 or orig_range < 0:
139
112
        raise MalformedHunkHeader("Hunk range is negative", line)
140
 
    tail = matches.group(3)
141
 
    return Hunk(orig_pos, orig_range, mod_pos, mod_range, tail)
142
 
 
143
 
 
144
 
class HunkLine(object):
145
 
 
 
113
    return Hunk(orig_pos, orig_range, mod_pos, mod_range)
 
114
 
 
115
 
 
116
class HunkLine:
146
117
    def __init__(self, contents):
147
118
        self.contents = contents
148
119
 
149
120
    def get_str(self, leadchar):
150
 
        if self.contents == b"\n" and leadchar == b" " and False:
151
 
            return b"\n"
152
 
        if not self.contents.endswith(b'\n'):
153
 
            terminator = b'\n' + NO_NL
 
121
        if self.contents == "\n" and leadchar == " " and False:
 
122
            return "\n"
 
123
        if not self.contents.endswith('\n'):
 
124
            terminator = '\n' + NO_NL
154
125
        else:
155
 
            terminator = b''
 
126
            terminator = ''
156
127
        return leadchar + self.contents + terminator
157
128
 
158
 
    def as_bytes(self):
159
 
        raise NotImplementedError
160
 
 
161
129
 
162
130
class ContextLine(HunkLine):
163
 
 
164
131
    def __init__(self, contents):
165
132
        HunkLine.__init__(self, contents)
166
133
 
167
 
    def as_bytes(self):
168
 
        return self.get_str(b" ")
 
134
    def __str__(self):
 
135
        return self.get_str(" ")
169
136
 
170
137
 
171
138
class InsertLine(HunkLine):
172
139
    def __init__(self, contents):
173
140
        HunkLine.__init__(self, contents)
174
141
 
175
 
    def as_bytes(self):
176
 
        return self.get_str(b"+")
 
142
    def __str__(self):
 
143
        return self.get_str("+")
177
144
 
178
145
 
179
146
class RemoveLine(HunkLine):
180
147
    def __init__(self, contents):
181
148
        HunkLine.__init__(self, contents)
182
149
 
183
 
    def as_bytes(self):
184
 
        return self.get_str(b"-")
185
 
 
186
 
 
187
 
NO_NL = b'\\ No newline at end of file\n'
188
 
__pychecker__ = "no-returnvalues"
189
 
 
 
150
    def __str__(self):
 
151
        return self.get_str("-")
 
152
 
 
153
NO_NL = '\\ No newline at end of file\n'
 
154
__pychecker__="no-returnvalues"
190
155
 
191
156
def parse_line(line):
192
 
    if line.startswith(b"\n"):
 
157
    if line.startswith("\n"):
193
158
        return ContextLine(line)
194
 
    elif line.startswith(b" "):
 
159
    elif line.startswith(" "):
195
160
        return ContextLine(line[1:])
196
 
    elif line.startswith(b"+"):
 
161
    elif line.startswith("+"):
197
162
        return InsertLine(line[1:])
198
 
    elif line.startswith(b"-"):
 
163
    elif line.startswith("-"):
199
164
        return RemoveLine(line[1:])
 
165
    elif line == NO_NL:
 
166
        return NO_NL
200
167
    else:
201
168
        raise MalformedLine("Unknown line type", line)
202
 
 
203
 
 
204
 
__pychecker__ = ""
205
 
 
206
 
 
207
 
class Hunk(object):
208
 
 
209
 
    def __init__(self, orig_pos, orig_range, mod_pos, mod_range, tail=None):
 
169
__pychecker__=""
 
170
 
 
171
 
 
172
class Hunk:
 
173
    def __init__(self, orig_pos, orig_range, mod_pos, mod_range):
210
174
        self.orig_pos = orig_pos
211
175
        self.orig_range = orig_range
212
176
        self.mod_pos = mod_pos
213
177
        self.mod_range = mod_range
214
 
        self.tail = tail
215
178
        self.lines = []
216
179
 
217
180
    def get_header(self):
218
 
        if self.tail is None:
219
 
            tail_str = b''
220
 
        else:
221
 
            tail_str = b' ' + self.tail
222
 
        return b"@@ -%s +%s @@%s\n" % (self.range_str(self.orig_pos,
223
 
                                                      self.orig_range),
224
 
                                       self.range_str(self.mod_pos,
225
 
                                                      self.mod_range),
226
 
                                       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))
227
185
 
228
186
    def range_str(self, pos, range):
229
187
        """Return a file range, special-casing for 1-line files.
235
193
        :return: a string in the format 1,4 except when range == pos == 1
236
194
        """
237
195
        if range == 1:
238
 
            return b"%i" % pos
 
196
            return "%i" % pos
239
197
        else:
240
 
            return b"%i,%i" % (pos, range)
 
198
            return "%i,%i" % (pos, range)
241
199
 
242
 
    def as_bytes(self):
 
200
    def __str__(self):
243
201
        lines = [self.get_header()]
244
202
        for line in self.lines:
245
 
            lines.append(line.as_bytes())
246
 
        return b"".join(lines)
247
 
 
248
 
    __bytes__ = as_bytes
 
203
            lines.append(str(line))
 
204
        return "".join(lines)
249
205
 
250
206
    def shift_to_mod(self, pos):
251
 
        if pos < self.orig_pos - 1:
 
207
        if pos < self.orig_pos-1:
252
208
            return 0
253
 
        elif pos > self.orig_pos + self.orig_range:
 
209
        elif pos > self.orig_pos+self.orig_range:
254
210
            return self.mod_range - self.orig_range
255
211
        else:
256
212
            return self.shift_to_mod_lines(pos)
257
213
 
258
214
    def shift_to_mod_lines(self, pos):
259
 
        position = self.orig_pos - 1
 
215
        assert (pos >= self.orig_pos-1 and pos <= self.orig_pos+self.orig_range)
 
216
        position = self.orig_pos-1
260
217
        shift = 0
261
218
        for line in self.lines:
262
219
            if isinstance(line, InsertLine):
273
230
        return shift
274
231
 
275
232
 
276
 
def iter_hunks(iter_lines, allow_dirty=False):
277
 
    '''
278
 
    :arg iter_lines: iterable of lines to parse for hunks
279
 
    :kwarg allow_dirty: If True, when we encounter something that is not
280
 
        a hunk header when we're looking for one, assume the rest of the lines
281
 
        are not part of the patch (comments or other junk).  Default False
282
 
    '''
 
233
def iter_hunks(iter_lines):
283
234
    hunk = None
284
235
    for line in iter_lines:
285
 
        if line == b"\n":
 
236
        if line == "\n":
286
237
            if hunk is not None:
287
238
                yield hunk
288
239
                hunk = None
289
240
            continue
290
241
        if hunk is not None:
291
242
            yield hunk
292
 
        try:
293
 
            hunk = hunk_from_header(line)
294
 
        except MalformedHunkHeader:
295
 
            if allow_dirty:
296
 
                # If the line isn't a hunk header, then we've reached the end
297
 
                # of this patch and there's "junk" at the end.  Ignore the
298
 
                # rest of this patch.
299
 
                return
300
 
            raise
 
243
        hunk = hunk_from_header(line)
301
244
        orig_size = 0
302
245
        mod_size = 0
303
246
        while orig_size < hunk.orig_range or mod_size < hunk.mod_range:
304
 
            hunk_line = parse_line(next(iter_lines))
 
247
            hunk_line = parse_line(iter_lines.next())
305
248
            hunk.lines.append(hunk_line)
306
249
            if isinstance(hunk_line, (RemoveLine, ContextLine)):
307
250
                orig_size += 1
311
254
        yield hunk
312
255
 
313
256
 
314
 
class BinaryPatch(object):
315
 
 
 
257
class Patch:
316
258
    def __init__(self, oldname, newname):
317
259
        self.oldname = oldname
318
260
        self.newname = newname
319
 
 
320
 
    def as_bytes(self):
321
 
        return b'Binary files %s and %s differ\n' % (self.oldname, self.newname)
322
 
 
323
 
 
324
 
class Patch(BinaryPatch):
325
 
 
326
 
    def __init__(self, oldname, newname):
327
 
        BinaryPatch.__init__(self, oldname, newname)
328
261
        self.hunks = []
329
262
 
330
 
    def as_bytes(self):
331
 
        ret = self.get_header()
332
 
        ret += b"".join([h.as_bytes() for h in self.hunks])
 
263
    def __str__(self):
 
264
        ret = self.get_header() 
 
265
        ret += "".join([str(h) for h in self.hunks])
333
266
        return ret
334
267
 
335
268
    def get_header(self):
336
 
        return b"--- %s\n+++ %s\n" % (self.oldname, self.newname)
 
269
        return "--- %s\n+++ %s\n" % (self.oldname, self.newname)
337
270
 
338
 
    def stats_values(self):
339
 
        """Calculate the number of inserts and removes."""
 
271
    def stats_str(self):
 
272
        """Return a string of patch statistics"""
340
273
        removes = 0
341
274
        inserts = 0
342
275
        for hunk in self.hunks:
343
276
            for line in hunk.lines:
344
277
                if isinstance(line, InsertLine):
345
 
                    inserts += 1
 
278
                     inserts+=1;
346
279
                elif isinstance(line, RemoveLine):
347
 
                    removes += 1
348
 
        return (inserts, removes, len(self.hunks))
349
 
 
350
 
    def stats_str(self):
351
 
        """Return a string of patch statistics"""
 
280
                     removes+=1;
352
281
        return "%i inserts, %i removes in %i hunks" % \
353
 
            self.stats_values()
 
282
            (inserts, removes, len(self.hunks))
354
283
 
355
284
    def pos_in_mod(self, position):
356
285
        newpos = position
360
289
                return None
361
290
            newpos += shift
362
291
        return newpos
363
 
 
 
292
            
364
293
    def iter_inserted(self):
365
294
        """Iteraties through inserted lines
366
 
 
 
295
        
367
296
        :return: Pair of line number, line
368
297
        :rtype: iterator of (int, InsertLine)
369
298
        """
370
299
        for hunk in self.hunks:
371
 
            pos = hunk.mod_pos - 1
 
300
            pos = hunk.mod_pos - 1;
372
301
            for line in hunk.lines:
373
302
                if isinstance(line, InsertLine):
374
303
                    yield (pos, line)
377
306
                    pos += 1
378
307
 
379
308
 
380
 
def parse_patch(iter_lines, allow_dirty=False):
381
 
    '''
382
 
    :arg iter_lines: iterable of lines to parse
383
 
    :kwarg allow_dirty: If True, allow the patch to have trailing junk.
384
 
        Default False
385
 
    '''
386
 
    iter_lines = iter_lines_handle_nl(iter_lines)
387
 
    try:
388
 
        (orig_name, mod_name) = get_patch_names(iter_lines)
389
 
    except BinaryFiles as e:
390
 
        return BinaryPatch(e.orig_name, e.mod_name)
391
 
    else:
392
 
        patch = Patch(orig_name, mod_name)
393
 
        for hunk in iter_hunks(iter_lines, allow_dirty):
394
 
            patch.hunks.append(hunk)
395
 
        return patch
396
 
 
397
 
 
398
 
def iter_file_patch(iter_lines, allow_dirty=False, keep_dirty=False):
399
 
    '''
400
 
    :arg iter_lines: iterable of lines to parse for patches
401
 
    :kwarg allow_dirty: If True, allow comments and other non-patch text
402
 
        before the first patch.  Note that the algorithm here can only find
403
 
        such text before any patches have been found.  Comments after the
404
 
        first patch are stripped away in iter_hunks() if it is also passed
405
 
        allow_dirty=True.  Default False.
406
 
    '''
407
 
    # FIXME: Docstring is not quite true.  We allow certain comments no
408
 
    # matter what, If they startwith '===', '***', or '#' Someone should
409
 
    # reexamine this logic and decide if we should include those in
410
 
    # allow_dirty or restrict those to only being before the patch is found
411
 
    # (as allow_dirty does).
412
 
    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):
413
318
    saved_lines = []
414
 
    dirty_head = []
415
319
    orig_range = 0
416
 
    beginning = True
417
 
 
418
320
    for line in iter_lines:
419
 
        if line.startswith(b'=== '):
420
 
            if len(saved_lines) > 0:
421
 
                if keep_dirty and len(dirty_head) > 0:
422
 
                    yield {'saved_lines': saved_lines,
423
 
                           'dirty_head': dirty_head}
424
 
                    dirty_head = []
425
 
                else:
426
 
                    yield saved_lines
427
 
                saved_lines = []
428
 
            dirty_head.append(line)
429
 
            continue
430
 
        if line.startswith(b'*** '):
431
 
            continue
432
 
        if line.startswith(b'#'):
 
321
        if line.startswith('=== ') or line.startswith('*** '):
 
322
            continue
 
323
        if line.startswith('#'):
433
324
            continue
434
325
        elif orig_range > 0:
435
 
            if line.startswith(b'-') or line.startswith(b' '):
 
326
            if line.startswith('-') or line.startswith(' '):
436
327
                orig_range -= 1
437
 
        elif line.startswith(b'--- ') or regex.match(line):
438
 
            if allow_dirty and beginning:
439
 
                # Patches can have "junk" at the beginning
440
 
                # Stripping junk from the end of patches is handled when we
441
 
                # parse the patch
442
 
                beginning = False
443
 
            elif len(saved_lines) > 0:
444
 
                if keep_dirty and len(dirty_head) > 0:
445
 
                    yield {'saved_lines': saved_lines,
446
 
                           'dirty_head': dirty_head}
447
 
                    dirty_head = []
448
 
                else:
449
 
                    yield saved_lines
 
328
        elif line.startswith('--- '):
 
329
            if len(saved_lines) > 0:
 
330
                yield saved_lines
450
331
            saved_lines = []
451
 
        elif line.startswith(b'@@'):
 
332
        elif line.startswith('@@'):
452
333
            hunk = hunk_from_header(line)
453
334
            orig_range = hunk.orig_range
454
335
        saved_lines.append(line)
455
336
    if len(saved_lines) > 0:
456
 
        if keep_dirty and len(dirty_head) > 0:
457
 
            yield {'saved_lines': saved_lines,
458
 
                   'dirty_head': dirty_head}
459
 
        else:
460
 
            yield saved_lines
 
337
        yield saved_lines
461
338
 
462
339
 
463
340
def iter_lines_handle_nl(iter_lines):
470
347
    last_line = None
471
348
    for line in iter_lines:
472
349
        if line == NO_NL:
473
 
            if not last_line.endswith(b'\n'):
474
 
                raise AssertionError()
 
350
            assert last_line.endswith('\n')
475
351
            last_line = last_line[:-1]
476
352
            line = None
477
353
        if last_line is not None:
481
357
        yield last_line
482
358
 
483
359
 
484
 
def parse_patches(iter_lines, allow_dirty=False, keep_dirty=False):
485
 
    '''
486
 
    :arg iter_lines: iterable of lines to parse for patches
487
 
    :kwarg allow_dirty: If True, allow text that's not part of the patch at
488
 
        selected places.  This includes comments before and after a patch
489
 
        for instance.  Default False.
490
 
    :kwarg keep_dirty: If True, returns a dict of patches with dirty headers.
491
 
        Default False.
492
 
    '''
493
 
    for patch_lines in iter_file_patch(iter_lines, allow_dirty, keep_dirty):
494
 
        if 'dirty_head' in patch_lines:
495
 
            yield ({'patch': parse_patch(patch_lines['saved_lines'], allow_dirty),
496
 
                    'dirty_head': patch_lines['dirty_head']})
497
 
        else:
498
 
            yield parse_patch(patch_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)]
499
363
 
500
364
 
501
365
def difference_index(atext, btext):
513
377
        length = len(btext)
514
378
    for i in range(length):
515
379
        if atext[i] != btext[i]:
516
 
            return i
 
380
            return i;
517
381
    return None
518
382
 
519
383
 
521
385
    """Iterate through a series of lines with a patch applied.
522
386
    This handles a single file, and does exact, not fuzzy patching.
523
387
    """
524
 
    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__())
525
392
    get_patch_names(patch_lines)
526
 
    return iter_patched_from_hunks(orig_lines, iter_hunks(patch_lines))
527
 
 
528
 
 
529
 
def iter_patched_from_hunks(orig_lines, hunks):
530
 
    """Iterate through a series of lines with a patch applied.
531
 
    This handles a single file, and does exact, not fuzzy patching.
532
 
 
533
 
    :param orig_lines: The unpatched lines.
534
 
    :param hunks: An iterable of Hunk instances.
535
 
    """
536
 
    seen_patch = []
537
393
    line_no = 1
538
 
    if orig_lines is not None:
539
 
        orig_lines = iter(orig_lines)
540
 
    for hunk in hunks:
 
394
    for hunk in iter_hunks(patch_lines):
541
395
        while line_no < hunk.orig_pos:
542
 
            orig_line = next(orig_lines)
 
396
            orig_line = orig_lines.next()
543
397
            yield orig_line
544
398
            line_no += 1
545
399
        for hunk_line in hunk.lines:
546
 
            seen_patch.append(hunk_line.contents)
 
400
            seen_patch.append(str(hunk_line))
547
401
            if isinstance(hunk_line, InsertLine):
548
402
                yield hunk_line.contents
549
403
            elif isinstance(hunk_line, (ContextLine, RemoveLine)):
550
 
                orig_line = next(orig_lines)
 
404
                orig_line = orig_lines.next()
551
405
                if orig_line != hunk_line.contents:
552
 
                    raise PatchConflict(line_no, orig_line,
553
 
                                        b''.join(seen_patch))
 
406
                    raise PatchConflict(line_no, orig_line, "".join(seen_patch))
554
407
                if isinstance(hunk_line, ContextLine):
555
408
                    yield orig_line
556
409
                else:
557
 
                    if not isinstance(hunk_line, RemoveLine):
558
 
                        raise AssertionError(hunk_line)
 
410
                    assert isinstance(hunk_line, RemoveLine)
559
411
                line_no += 1
560
412
    if orig_lines is not None:
561
413
        for line in orig_lines:
562
414
            yield line
563
 
 
564
 
 
565
 
def apply_patches(tt, patches, prefix=1):
566
 
    """Apply patches to a TreeTransform.
567
 
 
568
 
    :param tt: TreeTransform instance
569
 
    :param patches: List of patches
570
 
    :param prefix: Number leading path segments to strip
571
 
    """
572
 
    def strip_prefix(p):
573
 
        return '/'.join(p.split('/')[1:])
574
 
 
575
 
    from breezy.bzr.generate_ids import gen_file_id
576
 
    # TODO(jelmer): Extract and set mode
577
 
    for patch in patches:
578
 
        if patch.oldname == b'/dev/null':
579
 
            trans_id = None
580
 
            orig_contents = b''
581
 
        else:
582
 
            oldname = strip_prefix(patch.oldname.decode())
583
 
            trans_id = tt.trans_id_tree_path(oldname)
584
 
            orig_contents = tt._tree.get_file_text(oldname)
585
 
            tt.delete_contents(trans_id)
586
 
 
587
 
        if patch.newname != b'/dev/null':
588
 
            newname = strip_prefix(patch.newname.decode())
589
 
            new_contents = iter_patched_from_hunks(
590
 
                orig_contents.splitlines(True), patch.hunks)
591
 
            if trans_id is None:
592
 
                parts = os.path.split(newname)
593
 
                trans_id = tt.root
594
 
                for part in parts[1:-1]:
595
 
                    trans_id = tt.new_directory(part, trans_id)
596
 
                tt.new_file(
597
 
                    parts[-1], trans_id, new_contents,
598
 
                    file_id=gen_file_id(newname))
599
 
            else:
600
 
                tt.create_file(new_contents, trans_id)
601
 
 
602
 
 
603
 
class AppliedPatches(object):
604
 
    """Context that provides access to a tree with patches applied.
605
 
    """
606
 
 
607
 
    def __init__(self, tree, patches, prefix=1):
608
 
        self.tree = tree
609
 
        self.patches = patches
610
 
        self.prefix = prefix
611
 
 
612
 
    def __enter__(self):
613
 
        self._tt = self.tree.preview_transform()
614
 
        apply_patches(self._tt, self.patches, prefix=self.prefix)
615
 
        return self._tt.get_preview_tree()
616
 
 
617
 
    def __exit__(self, exc_type, exc_value, exc_tb):
618
 
        self._tt.finalize()
619
 
        return False