/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/workingtree.py

  • Committer: Aaron Bentley
  • Date: 2006-05-20 17:51:13 UTC
  • mfrom: (1718 +trunk)
  • mto: This revision was merged to the branch mainline in revision 1727.
  • Revision ID: aaron.bentley@utoronto.ca-20060520175113-4549e0023f9210bf
Merge from bzr.dev

Show diffs side-by-side

added added

removed removed

Lines of Context:
30
30
"""
31
31
 
32
32
MERGE_MODIFIED_HEADER_1 = "BZR merge-modified list format 1"
 
33
CONFLICT_HEADER_1 = "BZR conflict list format 1"
33
34
 
34
35
# TODO: Give the workingtree sole responsibility for the working inventory;
35
36
# remove the variable and references to it from the branch.  This may require
38
39
# At the moment they may alias the inventory and have old copies of it in
39
40
# memory.  (Now done? -- mbp 20060309)
40
41
 
 
42
from binascii import hexlify
41
43
from copy import deepcopy
42
44
from cStringIO import StringIO
43
45
import errno
44
46
import fnmatch
45
47
import os
 
48
import re
46
49
import stat
47
 
 
 
50
from time import time
48
51
 
49
52
from bzrlib.atomicfile import AtomicFile
50
53
from bzrlib.branch import (Branch,
51
54
                           quotefn)
 
55
from bzrlib.conflicts import Conflict, ConflictList, CONFLICT_SUFFIXES
52
56
import bzrlib.bzrdir as bzrdir
53
57
from bzrlib.decorators import needs_read_lock, needs_write_lock
54
58
import bzrlib.errors as errors
55
59
from bzrlib.errors import (BzrCheckError,
56
60
                           BzrError,
 
61
                           ConflictFormatError,
57
62
                           DivergedBranches,
58
63
                           WeaveRevisionNotPresent,
59
64
                           NotBranchError,
60
65
                           NoSuchFile,
61
66
                           NotVersionedError,
62
 
                           MergeModifiedFormatError)
 
67
                           MergeModifiedFormatError,
 
68
                           UnsupportedOperation,
 
69
                           )
63
70
from bzrlib.inventory import InventoryEntry, Inventory
64
71
from bzrlib.lockable_files import LockableFiles, TransportLock
65
72
from bzrlib.lockdir import LockDir
75
82
                            pumpfile,
76
83
                            safe_unicode,
77
84
                            splitpath,
78
 
                            rand_bytes,
 
85
                            rand_chars,
79
86
                            normpath,
80
87
                            realpath,
81
88
                            relpath,
84
91
                            )
85
92
from bzrlib.progress import DummyProgress, ProgressPhase
86
93
from bzrlib.revision import NULL_REVISION
87
 
from bzrlib.rio import RioReader, RioWriter, Stanza
 
94
from bzrlib.rio import RioReader, rio_file, Stanza
88
95
from bzrlib.symbol_versioning import *
89
96
from bzrlib.textui import show_status
90
97
import bzrlib.tree
91
 
from bzrlib.trace import mutter
92
98
from bzrlib.transform import build_tree
 
99
from bzrlib.trace import mutter, note
93
100
from bzrlib.transport import get_transport
94
101
from bzrlib.transport.local import LocalTransport
95
102
import bzrlib.ui
96
103
import bzrlib.xml5
97
104
 
98
105
 
 
106
# the regex here does the following:
 
107
# 1) remove any weird characters; we don't escape them but rather
 
108
# just pull them out
 
109
 # 2) match leading '.'s to make it not hidden
 
110
_gen_file_id_re = re.compile(r'[^\w.]|(^\.*)')
 
111
_gen_id_suffix = None
 
112
_gen_id_serial = 0
 
113
 
 
114
 
 
115
def _next_id_suffix():
 
116
    """Create a new file id suffix that is reasonably unique.
 
117
    
 
118
    On the first call we combine the current time with 64 bits of randomness
 
119
    to give a highly probably globally unique number. Then each call in the same
 
120
    process adds 1 to a serial number we append to that unique value.
 
121
    """
 
122
    # XXX TODO: change bzrlib.add.smart_add to call workingtree.add() rather 
 
123
    # than having to move the id randomness out of the inner loop like this.
 
124
    # XXX TODO: for the global randomness this uses we should add the thread-id
 
125
    # before the serial #.
 
126
    global _gen_id_suffix, _gen_id_serial
 
127
    if _gen_id_suffix is None:
 
128
        _gen_id_suffix = "-%s-%s-" % (compact_date(time()), rand_chars(16))
 
129
    _gen_id_serial += 1
 
130
    return _gen_id_suffix + str(_gen_id_serial)
 
131
 
 
132
 
99
133
def gen_file_id(name):
100
 
    """Return new file id.
101
 
 
102
 
    This should probably generate proper UUIDs, but for the moment we
103
 
    cope with just randomness because running uuidgen every time is
104
 
    slow."""
105
 
    import re
106
 
    from binascii import hexlify
107
 
    from time import time
108
 
 
109
 
    # get last component
110
 
    idx = name.rfind('/')
111
 
    if idx != -1:
112
 
        name = name[idx+1 : ]
113
 
    idx = name.rfind('\\')
114
 
    if idx != -1:
115
 
        name = name[idx+1 : ]
116
 
 
117
 
    # make it not a hidden file
118
 
    name = name.lstrip('.')
119
 
 
120
 
    # remove any wierd characters; we don't escape them but rather
121
 
    # just pull them out
122
 
    name = re.sub(r'[^\w.]', '', name)
123
 
 
124
 
    s = hexlify(rand_bytes(8))
125
 
    return '-'.join((name, compact_date(time()), s))
 
134
    """Return new file id for the basename 'name'.
 
135
 
 
136
    The uniqueness is supplied from _next_id_suffix.
 
137
    """
 
138
    # XXX TODO: squash the filename to lowercase.
 
139
    # XXX TODO: truncate the filename to something like 20 or 30 chars.
 
140
    # XXX TODO: consider what to do with ids that look like illegal filepaths
 
141
    # on platforms we support.
 
142
    return _gen_file_id_re.sub('', name) + _next_id_suffix()
126
143
 
127
144
 
128
145
def gen_root_id():
218
235
                 DeprecationWarning,
219
236
                 stacklevel=2)
220
237
            wt = WorkingTree.open(basedir)
221
 
            self.branch = wt.branch
 
238
            self._branch = wt.branch
222
239
            self.basedir = wt.basedir
223
240
            self._control_files = wt._control_files
224
241
            self._hashcache = wt._hashcache
234
251
        if deprecated_passed(branch):
235
252
            if not _internal:
236
253
                warn("WorkingTree(..., branch=XXX) is deprecated as of bzr 0.8."
237
 
                     " Please use bzrdir.open_workingtree() or WorkingTree.open().",
 
254
                     " Please use bzrdir.open_workingtree() or"
 
255
                     " WorkingTree.open().",
238
256
                     DeprecationWarning,
239
257
                     stacklevel=2
240
258
                     )
241
 
            self.branch = branch
 
259
            self._branch = branch
242
260
        else:
243
 
            self.branch = self.bzrdir.open_branch()
 
261
            self._branch = self.bzrdir.open_branch()
244
262
        assert isinstance(self.branch, Branch), \
245
263
            "branch %r is not a Branch" % self.branch
246
264
        self.basedir = realpath(basedir)
276
294
        else:
277
295
            self._set_inventory(_inventory)
278
296
 
 
297
    branch = property(
 
298
        fget=lambda self: self._branch,
 
299
        doc="""The branch this WorkingTree is connected to.
 
300
 
 
301
            This cannot be set - it is reflective of the actual disk structure
 
302
            the working tree has been constructed from.
 
303
            """)
 
304
 
 
305
    def break_lock(self):
 
306
        """Break a lock if one is present from another instance.
 
307
 
 
308
        Uses the ui factory to ask for confirmation if the lock may be from
 
309
        an active process.
 
310
 
 
311
        This will probe the repository for its lock as well.
 
312
        """
 
313
        self._control_files.break_lock()
 
314
        self.branch.break_lock()
 
315
 
279
316
    def _set_inventory(self, inv):
280
317
        self._inventory = inv
281
318
        self.path2id = self._inventory.path2id
283
320
    def is_control_filename(self, filename):
284
321
        """True if filename is the name of a control file in this tree.
285
322
        
 
323
        :param filename: A filename within the tree. This is a relative path
 
324
        from the root of this tree.
 
325
 
286
326
        This is true IF and ONLY IF the filename is part of the meta data
287
327
        that bzr controls in this tree. I.E. a random .bzr directory placed
288
328
        on disk will not be a control file for this tree.
289
329
        """
290
 
        try:
291
 
            self.bzrdir.transport.relpath(self.abspath(filename))
292
 
            return True
293
 
        except errors.PathNotChild:
294
 
            return False
 
330
        return self.bzrdir.is_control_filename(filename)
295
331
 
296
332
    @staticmethod
297
333
    def open(path=None, _unsupported=False):
398
434
        """
399
435
        return bzrdir.BzrDir.create_standalone_workingtree(directory)
400
436
 
401
 
    def relpath(self, abs):
402
 
        """Return the local path portion from a given absolute path."""
403
 
        return relpath(self.basedir, abs)
 
437
    def relpath(self, path):
 
438
        """Return the local path portion from a given path.
 
439
        
 
440
        The path may be absolute or relative. If its a relative path it is 
 
441
        interpreted relative to the python current working directory.
 
442
        """
 
443
        return relpath(self.basedir, path)
404
444
 
405
445
    def has_filename(self, filename):
406
446
        return bzrlib.osutils.lexists(self.abspath(filename))
563
603
                               'i.e. regular file, symlink or directory): %s' % quotefn(f))
564
604
 
565
605
            if file_id is None:
566
 
                file_id = gen_file_id(f)
567
 
            inv.add_path(f, kind=kind, file_id=file_id)
 
606
                inv.add_path(f, kind=kind)
 
607
            else:
 
608
                inv.add_path(f, kind=kind, file_id=file_id)
568
609
 
569
 
            mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
570
610
        self._write_inventory(inv)
571
611
 
572
612
    @needs_write_lock
607
647
 
608
648
    @needs_write_lock
609
649
    def set_merge_modified(self, modified_hashes):
610
 
        my_file = StringIO()
611
 
        my_file.write(MERGE_MODIFIED_HEADER_1 + '\n')
612
 
        writer = RioWriter(my_file)
613
 
        for file_id, hash in modified_hashes.iteritems():
614
 
            s = Stanza(file_id=file_id, hash=hash)
615
 
            writer.write_stanza(s)
616
 
        my_file.seek(0)
617
 
        self._control_files.put('merge-hashes', my_file)
 
650
        def iter_stanzas():
 
651
            for file_id, hash in modified_hashes.iteritems():
 
652
                yield Stanza(file_id=file_id, hash=hash)
 
653
        self._put_rio('merge-hashes', iter_stanzas(), MERGE_MODIFIED_HEADER_1)
 
654
 
 
655
    @needs_write_lock
 
656
    def _put_rio(self, filename, stanzas, header):
 
657
        my_file = rio_file(stanzas, header)
 
658
        self._control_files.put(filename, my_file)
618
659
 
619
660
    @needs_read_lock
620
661
    def merge_modified(self):
630
671
            raise MergeModifiedFormatError()
631
672
        for s in RioReader(hashfile):
632
673
            file_id = s.get("file_id")
 
674
            if file_id not in self.inventory:
 
675
                continue
633
676
            hash = s.get("hash")
634
677
            if hash == self.get_file_sha1(file_id):
635
678
                merge_hashes[file_id] = hash
861
904
            if not self.is_ignored(subp):
862
905
                yield subp
863
906
 
 
907
    @deprecated_method(zero_eight)
864
908
    def iter_conflicts(self):
 
909
        """List all files in the tree that have text or content conflicts.
 
910
        DEPRECATED.  Use conflicts instead."""
 
911
        return self._iter_conflicts()
 
912
 
 
913
    def _iter_conflicts(self):
865
914
        conflicted = set()
866
915
        for path in (s[0] for s in self.list_files()):
867
916
            stem = get_conflicted_stem(path)
932
981
                subp = appendpath(path, subf)
933
982
                yield subp
934
983
 
 
984
    def _translate_ignore_rule(self, rule):
 
985
        """Translate a single ignore rule to a regex.
 
986
 
 
987
        There are two types of ignore rules.  Those that do not contain a / are
 
988
        matched against the tail of the filename (that is, they do not care
 
989
        what directory the file is in.)  Rules which do contain a slash must
 
990
        match the entire path.  As a special case, './' at the start of the
 
991
        string counts as a slash in the string but is removed before matching
 
992
        (e.g. ./foo.c, ./src/foo.c)
 
993
 
 
994
        :return: The translated regex.
 
995
        """
 
996
        if rule[:2] in ('./', '.\\'):
 
997
            # rootdir rule
 
998
            result = fnmatch.translate(rule[2:])
 
999
        elif '/' in rule or '\\' in rule:
 
1000
            # path prefix 
 
1001
            result = fnmatch.translate(rule)
 
1002
        else:
 
1003
            # default rule style.
 
1004
            result = "(?:.*/)?(?!.*/)" + fnmatch.translate(rule)
 
1005
        assert result[-1] == '$', "fnmatch.translate did not add the expected $"
 
1006
        return "(" + result + ")"
 
1007
 
 
1008
    def _combine_ignore_rules(self, rules):
 
1009
        """Combine a list of ignore rules into a single regex object.
 
1010
 
 
1011
        Each individual rule is combined with | to form a big regex, which then
 
1012
        has $ added to it to form something like ()|()|()$. The group index for
 
1013
        each subregex's outermost group is placed in a dictionary mapping back 
 
1014
        to the rule. This allows quick identification of the matching rule that
 
1015
        triggered a match.
 
1016
        :return: a list of the compiled regex and the matching-group index 
 
1017
        dictionaries. We return a list because python complains if you try to 
 
1018
        combine more than 100 regexes.
 
1019
        """
 
1020
        result = []
 
1021
        groups = {}
 
1022
        next_group = 0
 
1023
        translated_rules = []
 
1024
        for rule in rules:
 
1025
            translated_rule = self._translate_ignore_rule(rule)
 
1026
            compiled_rule = re.compile(translated_rule)
 
1027
            groups[next_group] = rule
 
1028
            next_group += compiled_rule.groups
 
1029
            translated_rules.append(translated_rule)
 
1030
            if next_group == 99:
 
1031
                result.append((re.compile("|".join(translated_rules)), groups))
 
1032
                groups = {}
 
1033
                next_group = 0
 
1034
                translated_rules = []
 
1035
        if len(translated_rules):
 
1036
            result.append((re.compile("|".join(translated_rules)), groups))
 
1037
        return result
935
1038
 
936
1039
    def ignored_files(self):
937
1040
        """Yield list of PATH, IGNORE_PATTERN"""
940
1043
            if pat != None:
941
1044
                yield subp, pat
942
1045
 
943
 
 
944
1046
    def get_ignore_list(self):
945
1047
        """Return list of ignore patterns.
946
1048
 
954
1056
            f = self.get_file_byname(bzrlib.IGNORE_FILENAME)
955
1057
            l.extend([line.rstrip("\n\r") for line in f.readlines()])
956
1058
        self._ignorelist = l
 
1059
        self._ignore_regex = self._combine_ignore_rules(l)
957
1060
        return l
958
1061
 
 
1062
    def _get_ignore_rules_as_regex(self):
 
1063
        """Return a regex of the ignore rules and a mapping dict.
 
1064
 
 
1065
        :return: (ignore rules compiled regex, dictionary mapping rule group 
 
1066
        indices to original rule.)
 
1067
        """
 
1068
        if getattr(self, '_ignorelist', None) is None:
 
1069
            self.get_ignore_list()
 
1070
        return self._ignore_regex
959
1071
 
960
1072
    def is_ignored(self, filename):
961
1073
        r"""Check whether the filename matches an ignore pattern.
975
1087
        # treat dotfiles correctly and allows * to match /.
976
1088
        # Eventually it should be replaced with something more
977
1089
        # accurate.
978
 
        
979
 
        for pat in self.get_ignore_list():
980
 
            if '/' in pat or '\\' in pat:
981
 
                
982
 
                # as a special case, you can put ./ at the start of a
983
 
                # pattern; this is good to match in the top-level
984
 
                # only;
985
 
                
986
 
                if (pat[:2] == './') or (pat[:2] == '.\\'):
987
 
                    newpat = pat[2:]
988
 
                else:
989
 
                    newpat = pat
990
 
                if fnmatch.fnmatchcase(filename, newpat):
991
 
                    return pat
992
 
            else:
993
 
                if fnmatch.fnmatchcase(splitpath(filename)[-1], pat):
994
 
                    return pat
995
 
        else:
996
 
            return None
 
1090
    
 
1091
        rules = self._get_ignore_rules_as_regex()
 
1092
        for regex, mapping in rules:
 
1093
            match = regex.match(filename)
 
1094
            if match is not None:
 
1095
                # one or more of the groups in mapping will have a non-None group 
 
1096
                # match.
 
1097
                groups = match.groups()
 
1098
                rules = [mapping[group] for group in 
 
1099
                    mapping if groups[group] is not None]
 
1100
                return rules[0]
 
1101
        return None
997
1102
 
998
1103
    def kind(self, file_id):
999
1104
        return file_kind(self.id2abspath(file_id))
1008
1113
        """
1009
1114
        return self.branch.last_revision()
1010
1115
 
 
1116
    def is_locked(self):
 
1117
        return self._control_files.is_locked()
 
1118
 
1011
1119
    def lock_read(self):
1012
1120
        """See Branch.lock_read, and WorkingTree.unlock."""
1013
1121
        self.branch.lock_read()
1026
1134
            self.branch.unlock()
1027
1135
            raise
1028
1136
 
 
1137
    def get_physical_lock_status(self):
 
1138
        return self._control_files.get_physical_lock_status()
 
1139
 
1029
1140
    def _basis_inventory_name(self):
1030
1141
        return 'basis-inventory'
1031
1142
 
1130
1241
    def revert(self, filenames, old_tree=None, backups=True, 
1131
1242
               pb=DummyProgress()):
1132
1243
        from transform import revert
 
1244
        from conflicts import resolve
1133
1245
        if old_tree is None:
1134
1246
            old_tree = self.basis_tree()
1135
 
        revert(self, old_tree, filenames, backups, pb)
 
1247
        conflicts = revert(self, old_tree, filenames, backups, pb)
1136
1248
        if not len(filenames):
1137
1249
            self.set_pending_merges([])
 
1250
            resolve(self)
 
1251
        else:
 
1252
            resolve(self, filenames, ignore_misses=True)
 
1253
        return conflicts
1138
1254
 
 
1255
    # XXX: This method should be deprecated in favour of taking in a proper
 
1256
    # new Inventory object.
1139
1257
    @needs_write_lock
1140
1258
    def set_inventory(self, new_inventory_list):
1141
1259
        from bzrlib.inventory import (Inventory,
1199
1317
             self._control_files._lock_count==3)):
1200
1318
            self._hashcache.write()
1201
1319
        # reverse order of locking.
1202
 
        result = self._control_files.unlock()
1203
1320
        try:
 
1321
            return self._control_files.unlock()
 
1322
        finally:
1204
1323
            self.branch.unlock()
1205
 
        finally:
1206
 
            return result
1207
1324
 
1208
1325
    @needs_write_lock
1209
1326
    def update(self):
1272
1389
        self._set_inventory(inv)
1273
1390
        mutter('wrote working inventory')
1274
1391
 
 
1392
    def set_conflicts(self, arg):
 
1393
        raise UnsupportedOperation(self.set_conflicts, self)
 
1394
 
 
1395
    @needs_read_lock
 
1396
    def conflicts(self):
 
1397
        conflicts = ConflictList()
 
1398
        for conflicted in self._iter_conflicts():
 
1399
            text = True
 
1400
            try:
 
1401
                if file_kind(self.abspath(conflicted)) != "file":
 
1402
                    text = False
 
1403
            except OSError, e:
 
1404
                if e.errno == errno.ENOENT:
 
1405
                    text = False
 
1406
                else:
 
1407
                    raise
 
1408
            if text is True:
 
1409
                for suffix in ('.THIS', '.OTHER'):
 
1410
                    try:
 
1411
                        kind = file_kind(self.abspath(conflicted+suffix))
 
1412
                    except OSError, e:
 
1413
                        if e.errno == errno.ENOENT:
 
1414
                            text = False
 
1415
                            break
 
1416
                        else:
 
1417
                            raise
 
1418
                    if kind != "file":
 
1419
                        text = False
 
1420
                        break
 
1421
            ctype = {True: 'text conflict', False: 'contents conflict'}[text]
 
1422
            conflicts.append(Conflict.factory(ctype, path=conflicted,
 
1423
                             file_id=self.path2id(conflicted)))
 
1424
        return conflicts
 
1425
 
1275
1426
 
1276
1427
class WorkingTree3(WorkingTree):
1277
1428
    """This is the Format 3 working tree.
1307
1458
            self._control_files.put_utf8('last-revision', revision_id)
1308
1459
            return True
1309
1460
 
1310
 
 
1311
 
CONFLICT_SUFFIXES = ('.THIS', '.BASE', '.OTHER')
 
1461
    @needs_write_lock
 
1462
    def set_conflicts(self, conflicts):
 
1463
        self._put_rio('conflicts', conflicts.to_stanzas(), 
 
1464
                      CONFLICT_HEADER_1)
 
1465
 
 
1466
    @needs_read_lock
 
1467
    def conflicts(self):
 
1468
        try:
 
1469
            confile = self._control_files.get('conflicts')
 
1470
        except NoSuchFile:
 
1471
            return ConflictList()
 
1472
        try:
 
1473
            if confile.next() != CONFLICT_HEADER_1 + '\n':
 
1474
                raise ConflictFormatError()
 
1475
        except StopIteration:
 
1476
            raise ConflictFormatError()
 
1477
        return ConflictList.from_stanzas(RioReader(confile))
 
1478
 
 
1479
 
1312
1480
def get_conflicted_stem(path):
1313
1481
    for suffix in CONFLICT_SUFFIXES:
1314
1482
        if path.endswith(suffix):
1375
1543
        """Return the ASCII format string that identifies this format."""
1376
1544
        raise NotImplementedError(self.get_format_string)
1377
1545
 
 
1546
    def get_format_description(self):
 
1547
        """Return the short description for this format."""
 
1548
        raise NotImplementedError(self.get_format_description)
 
1549
 
1378
1550
    def is_supported(self):
1379
1551
        """Is this format supported?
1380
1552
 
1405
1577
    This format modified the hash cache from the format 1 hash cache.
1406
1578
    """
1407
1579
 
 
1580
    def get_format_description(self):
 
1581
        """See WorkingTreeFormat.get_format_description()."""
 
1582
        return "Working tree format 2"
 
1583
 
 
1584
    def stub_initialize_remote(self, control_files):
 
1585
        """As a special workaround create critical control files for a remote working tree
 
1586
        
 
1587
        This ensures that it can later be updated and dealt with locally,
 
1588
        since BzrDirFormat6 and BzrDirFormat5 cannot represent dirs with 
 
1589
        no working tree.  (See bug #43064).
 
1590
        """
 
1591
        sio = StringIO()
 
1592
        inv = Inventory()
 
1593
        bzrlib.xml5.serializer_v5.write_inventory(inv, sio)
 
1594
        sio.seek(0)
 
1595
        control_files.put('inventory', sio)
 
1596
 
 
1597
        control_files.put_utf8('pending-merges', '')
 
1598
        
 
1599
 
1408
1600
    def initialize(self, a_bzrdir, revision_id=None):
1409
1601
        """See WorkingTreeFormat.initialize()."""
1410
1602
        if not isinstance(a_bzrdir.transport, LocalTransport):
1473
1665
        """See WorkingTreeFormat.get_format_string()."""
1474
1666
        return "Bazaar-NG Working Tree format 3"
1475
1667
 
 
1668
    def get_format_description(self):
 
1669
        """See WorkingTreeFormat.get_format_description()."""
 
1670
        return "Working tree format 3"
 
1671
 
1476
1672
    _lock_file_name = 'lock'
1477
1673
    _lock_class = LockDir
1478
1674
 
1492
1688
        transport = a_bzrdir.get_workingtree_transport(self)
1493
1689
        control_files = self._open_control_files(a_bzrdir)
1494
1690
        control_files.create_lock()
 
1691
        control_files.lock_write()
1495
1692
        control_files.put_utf8('format', self.get_format_string())
1496
1693
        branch = a_bzrdir.open_branch()
1497
1694
        if revision_id is None:
1504
1701
                         _format=self,
1505
1702
                         _bzrdir=a_bzrdir,
1506
1703
                         _control_files=control_files)
1507
 
        wt._write_inventory(inv)
1508
 
        wt.set_root_id(inv.root.file_id)
1509
 
        wt.set_last_revision(revision_id)
1510
 
        wt.set_pending_merges([])
1511
 
        build_tree(wt.basis_tree(), wt)
 
1704
        wt.lock_write()
 
1705
        try:
 
1706
            wt._write_inventory(inv)
 
1707
            wt.set_root_id(inv.root.file_id)
 
1708
            wt.set_last_revision(revision_id)
 
1709
            wt.set_pending_merges([])
 
1710
            build_tree(wt.basis_tree(), wt)
 
1711
        finally:
 
1712
            wt.unlock()
 
1713
            control_files.unlock()
1512
1714
        return wt
1513
1715
 
1514
1716
    def __init__(self):