/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 breezy/patches.py

  • Committer: Gustav Hartvigsson
  • Date: 2021-01-09 21:36:27 UTC
  • Revision ID: gustav.hartvigsson@gmail.com-20210109213627-h1xwcutzy9m7a99b
Added 'Case Preserving Working Tree Use Cases' from Canonical Wiki

* Addod a page from the Canonical Bazaar wiki
  with information on the scmeatics of case
  perserving filesystems an a case insensitive
  filesystem works.
  
  * Needs re-work, but this will do as it is the
    same inforamoton as what was on the linked
    page in the currint documentation.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005-2010 Aaron Bentley, Canonical Ltd
 
2
# <aaron.bentley@utoronto.ca>
 
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
 
 
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
 
40
 
 
41
 
 
42
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
 
49
 
 
50
 
 
51
class MalformedLine(PatchSyntax):
 
52
 
 
53
    _fmt = "Malformed line.  %(desc)s\n%(line)r"
 
54
 
 
55
    def __init__(self, desc, line):
 
56
        self.desc = desc
 
57
        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
 
 
65
    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
 
78
 
 
79
 
 
80
def get_patch_names(iter_lines):
 
81
    line = next(iter_lines)
 
82
    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"--- "):
 
87
            raise MalformedPatchHeader("No orig name", line)
 
88
        else:
 
89
            orig_name = line[4:].rstrip(b"\n")
 
90
    except StopIteration:
 
91
        raise MalformedPatchHeader("No orig line", "")
 
92
    try:
 
93
        line = next(iter_lines)
 
94
        if not line.startswith(b"+++ "):
 
95
            raise PatchSyntax("No mod name")
 
96
        else:
 
97
            mod_name = line[4:].rstrip(b"\n")
 
98
    except StopIteration:
 
99
        raise MalformedPatchHeader("No mod line", "")
 
100
    return (orig_name, mod_name)
 
101
 
 
102
 
 
103
def parse_range(textrange):
 
104
    """Parse a patch range, handling the "1" special-case
 
105
 
 
106
    :param textrange: The text to parse
 
107
    :type textrange: str
 
108
    :return: the position and range, as a tuple
 
109
    :rtype: (int, int)
 
110
    """
 
111
    tmp = textrange.split(b',')
 
112
    if len(tmp) == 1:
 
113
        pos = tmp[0]
 
114
        range = b"1"
 
115
    else:
 
116
        (pos, range) = tmp
 
117
    pos = int(pos)
 
118
    range = int(range)
 
119
    return (pos, range)
 
120
 
 
121
 
 
122
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)
 
127
    try:
 
128
        (orig, mod) = matches.group(1).split(b" ")
 
129
    except (ValueError, IndexError) as e:
 
130
        raise MalformedHunkHeader(str(e), line)
 
131
    if not orig.startswith(b'-') or not mod.startswith(b'+'):
 
132
        raise MalformedHunkHeader("Positions don't start with + or -.", line)
 
133
    try:
 
134
        (orig_pos, orig_range) = parse_range(orig[1:])
 
135
        (mod_pos, mod_range) = parse_range(mod[1:])
 
136
    except (ValueError, IndexError) as e:
 
137
        raise MalformedHunkHeader(str(e), line)
 
138
    if mod_range < 0 or orig_range < 0:
 
139
        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
 
 
146
    def __init__(self, contents):
 
147
        self.contents = contents
 
148
 
 
149
    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
 
154
        else:
 
155
            terminator = b''
 
156
        return leadchar + self.contents + terminator
 
157
 
 
158
    def as_bytes(self):
 
159
        raise NotImplementedError
 
160
 
 
161
 
 
162
class ContextLine(HunkLine):
 
163
 
 
164
    def __init__(self, contents):
 
165
        HunkLine.__init__(self, contents)
 
166
 
 
167
    def as_bytes(self):
 
168
        return self.get_str(b" ")
 
169
 
 
170
 
 
171
class InsertLine(HunkLine):
 
172
    def __init__(self, contents):
 
173
        HunkLine.__init__(self, contents)
 
174
 
 
175
    def as_bytes(self):
 
176
        return self.get_str(b"+")
 
177
 
 
178
 
 
179
class RemoveLine(HunkLine):
 
180
    def __init__(self, contents):
 
181
        HunkLine.__init__(self, contents)
 
182
 
 
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
 
 
190
 
 
191
def parse_line(line):
 
192
    if line.startswith(b"\n"):
 
193
        return ContextLine(line)
 
194
    elif line.startswith(b" "):
 
195
        return ContextLine(line[1:])
 
196
    elif line.startswith(b"+"):
 
197
        return InsertLine(line[1:])
 
198
    elif line.startswith(b"-"):
 
199
        return RemoveLine(line[1:])
 
200
    else:
 
201
        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):
 
210
        self.orig_pos = orig_pos
 
211
        self.orig_range = orig_range
 
212
        self.mod_pos = mod_pos
 
213
        self.mod_range = mod_range
 
214
        self.tail = tail
 
215
        self.lines = []
 
216
 
 
217
    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)
 
227
 
 
228
    def range_str(self, pos, range):
 
229
        """Return a file range, special-casing for 1-line files.
 
230
 
 
231
        :param pos: The position in the file
 
232
        :type pos: int
 
233
        :range: The range in the file
 
234
        :type range: int
 
235
        :return: a string in the format 1,4 except when range == pos == 1
 
236
        """
 
237
        if range == 1:
 
238
            return b"%i" % pos
 
239
        else:
 
240
            return b"%i,%i" % (pos, range)
 
241
 
 
242
    def as_bytes(self):
 
243
        lines = [self.get_header()]
 
244
        for line in self.lines:
 
245
            lines.append(line.as_bytes())
 
246
        return b"".join(lines)
 
247
 
 
248
    __bytes__ = as_bytes
 
249
 
 
250
    def shift_to_mod(self, pos):
 
251
        if pos < self.orig_pos - 1:
 
252
            return 0
 
253
        elif pos > self.orig_pos + self.orig_range:
 
254
            return self.mod_range - self.orig_range
 
255
        else:
 
256
            return self.shift_to_mod_lines(pos)
 
257
 
 
258
    def shift_to_mod_lines(self, pos):
 
259
        position = self.orig_pos - 1
 
260
        shift = 0
 
261
        for line in self.lines:
 
262
            if isinstance(line, InsertLine):
 
263
                shift += 1
 
264
            elif isinstance(line, RemoveLine):
 
265
                if position == pos:
 
266
                    return None
 
267
                shift -= 1
 
268
                position += 1
 
269
            elif isinstance(line, ContextLine):
 
270
                position += 1
 
271
            if position > pos:
 
272
                break
 
273
        return shift
 
274
 
 
275
 
 
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
    '''
 
283
    hunk = None
 
284
    for line in iter_lines:
 
285
        if line == b"\n":
 
286
            if hunk is not None:
 
287
                yield hunk
 
288
                hunk = None
 
289
            continue
 
290
        if hunk is not None:
 
291
            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
 
301
        orig_size = 0
 
302
        mod_size = 0
 
303
        while orig_size < hunk.orig_range or mod_size < hunk.mod_range:
 
304
            hunk_line = parse_line(next(iter_lines))
 
305
            hunk.lines.append(hunk_line)
 
306
            if isinstance(hunk_line, (RemoveLine, ContextLine)):
 
307
                orig_size += 1
 
308
            if isinstance(hunk_line, (InsertLine, ContextLine)):
 
309
                mod_size += 1
 
310
    if hunk is not None:
 
311
        yield hunk
 
312
 
 
313
 
 
314
class BinaryPatch(object):
 
315
 
 
316
    def __init__(self, oldname, newname):
 
317
        self.oldname = oldname
 
318
        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
        self.hunks = []
 
329
 
 
330
    def as_bytes(self):
 
331
        ret = self.get_header()
 
332
        ret += b"".join([h.as_bytes() for h in self.hunks])
 
333
        return ret
 
334
 
 
335
    def get_header(self):
 
336
        return b"--- %s\n+++ %s\n" % (self.oldname, self.newname)
 
337
 
 
338
    def stats_values(self):
 
339
        """Calculate the number of inserts and removes."""
 
340
        removes = 0
 
341
        inserts = 0
 
342
        for hunk in self.hunks:
 
343
            for line in hunk.lines:
 
344
                if isinstance(line, InsertLine):
 
345
                    inserts += 1
 
346
                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"""
 
352
        return "%i inserts, %i removes in %i hunks" % \
 
353
            self.stats_values()
 
354
 
 
355
    def pos_in_mod(self, position):
 
356
        newpos = position
 
357
        for hunk in self.hunks:
 
358
            shift = hunk.shift_to_mod(position)
 
359
            if shift is None:
 
360
                return None
 
361
            newpos += shift
 
362
        return newpos
 
363
 
 
364
    def iter_inserted(self):
 
365
        """Iteraties through inserted lines
 
366
 
 
367
        :return: Pair of line number, line
 
368
        :rtype: iterator of (int, InsertLine)
 
369
        """
 
370
        for hunk in self.hunks:
 
371
            pos = hunk.mod_pos - 1
 
372
            for line in hunk.lines:
 
373
                if isinstance(line, InsertLine):
 
374
                    yield (pos, line)
 
375
                    pos += 1
 
376
                if isinstance(line, ContextLine):
 
377
                    pos += 1
 
378
 
 
379
 
 
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)
 
413
    saved_lines = []
 
414
    dirty_head = []
 
415
    orig_range = 0
 
416
    beginning = True
 
417
 
 
418
    for line in iter_lines:
 
419
        if line.startswith(b'=== '):
 
420
            if allow_dirty and beginning:
 
421
                # Patches can have "junk" at the beginning
 
422
                # Stripping junk from the end of patches is handled when we
 
423
                # parse the patch
 
424
                pass
 
425
            elif len(saved_lines) > 0:
 
426
                if keep_dirty and len(dirty_head) > 0:
 
427
                    yield {'saved_lines': saved_lines,
 
428
                           'dirty_head': dirty_head}
 
429
                    dirty_head = []
 
430
                else:
 
431
                    yield saved_lines
 
432
                saved_lines = []
 
433
            dirty_head.append(line)
 
434
            continue
 
435
        if line.startswith(b'*** '):
 
436
            continue
 
437
        if line.startswith(b'#'):
 
438
            continue
 
439
        elif orig_range > 0:
 
440
            if line.startswith(b'-') or line.startswith(b' '):
 
441
                orig_range -= 1
 
442
        elif line.startswith(b'--- ') or regex.match(line):
 
443
            if allow_dirty and beginning:
 
444
                # Patches can have "junk" at the beginning
 
445
                # Stripping junk from the end of patches is handled when we
 
446
                # parse the patch
 
447
                beginning = False
 
448
            elif len(saved_lines) > 0:
 
449
                if keep_dirty and len(dirty_head) > 0:
 
450
                    yield {'saved_lines': saved_lines,
 
451
                           'dirty_head': dirty_head}
 
452
                    dirty_head = []
 
453
                else:
 
454
                    yield saved_lines
 
455
            saved_lines = []
 
456
        elif line.startswith(b'@@'):
 
457
            hunk = hunk_from_header(line)
 
458
            orig_range = hunk.orig_range
 
459
        saved_lines.append(line)
 
460
    if len(saved_lines) > 0:
 
461
        if keep_dirty and len(dirty_head) > 0:
 
462
            yield {'saved_lines': saved_lines,
 
463
                   'dirty_head': dirty_head}
 
464
        else:
 
465
            yield saved_lines
 
466
 
 
467
 
 
468
def iter_lines_handle_nl(iter_lines):
 
469
    """
 
470
    Iterates through lines, ensuring that lines that originally had no
 
471
    terminating \n are produced without one.  This transformation may be
 
472
    applied at any point up until hunk line parsing, and is safe to apply
 
473
    repeatedly.
 
474
    """
 
475
    last_line = None
 
476
    for line in iter_lines:
 
477
        if line == NO_NL:
 
478
            if not last_line.endswith(b'\n'):
 
479
                raise AssertionError()
 
480
            last_line = last_line[:-1]
 
481
            line = None
 
482
        if last_line is not None:
 
483
            yield last_line
 
484
        last_line = line
 
485
    if last_line is not None:
 
486
        yield last_line
 
487
 
 
488
 
 
489
def parse_patches(iter_lines, allow_dirty=False, keep_dirty=False):
 
490
    '''
 
491
    :arg iter_lines: iterable of lines to parse for patches
 
492
    :kwarg allow_dirty: If True, allow text that's not part of the patch at
 
493
        selected places.  This includes comments before and after a patch
 
494
        for instance.  Default False.
 
495
    :kwarg keep_dirty: If True, returns a dict of patches with dirty headers.
 
496
        Default False.
 
497
    '''
 
498
    for patch_lines in iter_file_patch(iter_lines, allow_dirty, keep_dirty):
 
499
        if 'dirty_head' in patch_lines:
 
500
            yield ({'patch': parse_patch(patch_lines['saved_lines'], allow_dirty),
 
501
                    'dirty_head': patch_lines['dirty_head']})
 
502
        else:
 
503
            yield parse_patch(patch_lines, allow_dirty)
 
504
 
 
505
 
 
506
def difference_index(atext, btext):
 
507
    """Find the indext of the first character that differs between two texts
 
508
 
 
509
    :param atext: The first text
 
510
    :type atext: str
 
511
    :param btext: The second text
 
512
    :type str: str
 
513
    :return: The index, or None if there are no differences within the range
 
514
    :rtype: int or NoneType
 
515
    """
 
516
    length = len(atext)
 
517
    if len(btext) < length:
 
518
        length = len(btext)
 
519
    for i in range(length):
 
520
        if atext[i] != btext[i]:
 
521
            return i
 
522
    return None
 
523
 
 
524
 
 
525
def iter_patched(orig_lines, patch_lines):
 
526
    """Iterate through a series of lines with a patch applied.
 
527
    This handles a single file, and does exact, not fuzzy patching.
 
528
    """
 
529
    patch_lines = iter_lines_handle_nl(iter(patch_lines))
 
530
    get_patch_names(patch_lines)
 
531
    return iter_patched_from_hunks(orig_lines, iter_hunks(patch_lines))
 
532
 
 
533
 
 
534
def iter_patched_from_hunks(orig_lines, hunks):
 
535
    """Iterate through a series of lines with a patch applied.
 
536
    This handles a single file, and does exact, not fuzzy patching.
 
537
 
 
538
    :param orig_lines: The unpatched lines.
 
539
    :param hunks: An iterable of Hunk instances.
 
540
    """
 
541
    seen_patch = []
 
542
    line_no = 1
 
543
    if orig_lines is not None:
 
544
        orig_lines = iter(orig_lines)
 
545
    for hunk in hunks:
 
546
        while line_no < hunk.orig_pos:
 
547
            orig_line = next(orig_lines)
 
548
            yield orig_line
 
549
            line_no += 1
 
550
        for hunk_line in hunk.lines:
 
551
            seen_patch.append(hunk_line.contents)
 
552
            if isinstance(hunk_line, InsertLine):
 
553
                yield hunk_line.contents
 
554
            elif isinstance(hunk_line, (ContextLine, RemoveLine)):
 
555
                orig_line = next(orig_lines)
 
556
                if orig_line != hunk_line.contents:
 
557
                    raise PatchConflict(line_no, orig_line,
 
558
                                        b''.join(seen_patch))
 
559
                if isinstance(hunk_line, ContextLine):
 
560
                    yield orig_line
 
561
                else:
 
562
                    if not isinstance(hunk_line, RemoveLine):
 
563
                        raise AssertionError(hunk_line)
 
564
                line_no += 1
 
565
    if orig_lines is not None:
 
566
        for line in orig_lines:
 
567
            yield line
 
568
 
 
569
 
 
570
def apply_patches(tt, patches, prefix=1):
 
571
    """Apply patches to a TreeTransform.
 
572
 
 
573
    :param tt: TreeTransform instance
 
574
    :param patches: List of patches
 
575
    :param prefix: Number leading path segments to strip
 
576
    """
 
577
    def strip_prefix(p):
 
578
        return '/'.join(p.split('/')[1:])
 
579
 
 
580
    from breezy.bzr.generate_ids import gen_file_id
 
581
    # TODO(jelmer): Extract and set mode
 
582
    for patch in patches:
 
583
        if patch.oldname == b'/dev/null':
 
584
            trans_id = None
 
585
            orig_contents = b''
 
586
        else:
 
587
            oldname = strip_prefix(patch.oldname.decode())
 
588
            trans_id = tt.trans_id_tree_path(oldname)
 
589
            orig_contents = tt._tree.get_file_text(oldname)
 
590
            tt.delete_contents(trans_id)
 
591
 
 
592
        if patch.newname != b'/dev/null':
 
593
            newname = strip_prefix(patch.newname.decode())
 
594
            new_contents = iter_patched_from_hunks(
 
595
                orig_contents.splitlines(True), patch.hunks)
 
596
            if trans_id is None:
 
597
                parts = os.path.split(newname)
 
598
                trans_id = tt.root
 
599
                for part in parts[1:-1]:
 
600
                    trans_id = tt.new_directory(part, trans_id)
 
601
                tt.new_file(
 
602
                    parts[-1], trans_id, new_contents,
 
603
                    file_id=gen_file_id(newname))
 
604
            else:
 
605
                tt.create_file(new_contents, trans_id)
 
606
 
 
607
 
 
608
class AppliedPatches(object):
 
609
    """Context that provides access to a tree with patches applied.
 
610
    """
 
611
 
 
612
    def __init__(self, tree, patches, prefix=1):
 
613
        self.tree = tree
 
614
        self.patches = patches
 
615
        self.prefix = prefix
 
616
 
 
617
    def __enter__(self):
 
618
        self._tt = self.tree.preview_transform()
 
619
        apply_patches(self._tt, self.patches, prefix=self.prefix)
 
620
        return self._tt.get_preview_tree()
 
621
 
 
622
    def __exit__(self, exc_type, exc_value, exc_tb):
 
623
        self._tt.finalize()
 
624
        return False