1
# Copyright (C) 2005 Canonical Ltd
3
# This program is free software; you can redistribute it and/or modify
4
# it under the terms of the GNU General Public License as published by
5
# the Free Software Foundation; either version 2 of the License, or
6
# (at your option) any later version.
8
# This program is distributed in the hope that it will be useful,
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
# GNU General Public License for more details.
13
# You should have received a copy of the GNU General Public License
14
# along with this program; if not, write to the Free Software
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
17
"""WorkingTree object and friends.
19
A WorkingTree represents the editable working copy of a branch.
20
Operations which represent the WorkingTree are also done here,
21
such as renaming or adding files. The WorkingTree has an inventory
22
which is updated by these operations. A commit produces a
23
new revision based on the workingtree and its inventory.
25
At the moment every WorkingTree has its own branch. Remote
26
WorkingTrees aren't supported.
28
To get a WorkingTree, call WorkingTree(dir[, branch])
32
# FIXME: I don't know if writing out the cache from the destructor is really a
33
# good idea, because destructors are considered poor taste in Python, and it's
34
# not predictable when it will be written out.
36
# TODO: Give the workingtree sole responsibility for the working inventory;
37
# remove the variable and references to it from the branch. This may require
38
# updating the commit code so as to update the inventory within the working
39
# copy, and making sure there's only one WorkingTree for any directory on disk.
40
# At the momenthey may alias the inventory and have old copies of it in memory.
42
from copy import deepcopy
43
from cStringIO import StringIO
50
from bzrlib.atomicfile import AtomicFile
51
from bzrlib.branch import (Branch,
57
from bzrlib.decorators import needs_read_lock, needs_write_lock
58
from bzrlib.errors import (BzrCheckError,
61
WeaveRevisionNotPresent,
65
from bzrlib.inventory import InventoryEntry
66
from bzrlib.lockable_files import LockableFiles
67
from bzrlib.osutils import (appendpath,
82
from bzrlib.symbol_versioning import *
83
from bzrlib.textui import show_status
85
from bzrlib.trace import mutter
86
from bzrlib.transport import get_transport
90
def gen_file_id(name):
91
"""Return new file id.
93
This should probably generate proper UUIDs, but for the moment we
94
cope with just randomness because running uuidgen every time is
97
from binascii import hexlify
101
idx = name.rfind('/')
103
name = name[idx+1 : ]
104
idx = name.rfind('\\')
106
name = name[idx+1 : ]
108
# make it not a hidden file
109
name = name.lstrip('.')
111
# remove any wierd characters; we don't escape them but rather
113
name = re.sub(r'[^\w.]', '', name)
115
s = hexlify(rand_bytes(8))
116
return '-'.join((name, compact_date(time()), s))
120
"""Return a new tree-root file id."""
121
return gen_file_id('TREE_ROOT')
124
class TreeEntry(object):
125
"""An entry that implements the minium interface used by commands.
127
This needs further inspection, it may be better to have
128
InventoryEntries without ids - though that seems wrong. For now,
129
this is a parallel hierarchy to InventoryEntry, and needs to become
130
one of several things: decorates to that hierarchy, children of, or
132
Another note is that these objects are currently only used when there is
133
no InventoryEntry available - i.e. for unversioned objects.
134
Perhaps they should be UnversionedEntry et al. ? - RBC 20051003
137
def __eq__(self, other):
138
# yes, this us ugly, TODO: best practice __eq__ style.
139
return (isinstance(other, TreeEntry)
140
and other.__class__ == self.__class__)
142
def kind_character(self):
146
class TreeDirectory(TreeEntry):
147
"""See TreeEntry. This is a directory in a working tree."""
149
def __eq__(self, other):
150
return (isinstance(other, TreeDirectory)
151
and other.__class__ == self.__class__)
153
def kind_character(self):
157
class TreeFile(TreeEntry):
158
"""See TreeEntry. This is a regular file in a working tree."""
160
def __eq__(self, other):
161
return (isinstance(other, TreeFile)
162
and other.__class__ == self.__class__)
164
def kind_character(self):
168
class TreeLink(TreeEntry):
169
"""See TreeEntry. This is a symlink in a working tree."""
171
def __eq__(self, other):
172
return (isinstance(other, TreeLink)
173
and other.__class__ == self.__class__)
175
def kind_character(self):
179
class WorkingTree(bzrlib.tree.Tree):
180
"""Working copy tree.
182
The inventory is held in the `Branch` working-inventory, and the
183
files are in a directory on disk.
185
It is possible for a `WorkingTree` to have a filename which is
186
not listed in the Inventory and vice versa.
189
def __init__(self, basedir='.', branch=None, _inventory=None, _control_files=None):
190
"""Construct a WorkingTree for basedir.
192
If the branch is not supplied, it is opened automatically.
193
If the branch is supplied, it must be the branch for this basedir.
194
(branch.base is not cross checked, because for remote branches that
195
would be meaningless).
197
from bzrlib.hashcache import HashCache
198
from bzrlib.trace import note, mutter
199
assert isinstance(basedir, basestring), \
200
"base directory %r is not a string" % basedir
201
basedir = safe_unicode(basedir)
202
mutter("openeing working tree %r", basedir)
204
branch = Branch.open(basedir)
205
assert isinstance(branch, Branch), \
206
"branch %r is not a Branch" % branch
208
self.basedir = realpath(basedir)
209
# if branch is at our basedir and is a format 6 or less
210
if (isinstance(self.branch._branch_format,
211
(BzrBranchFormat4, BzrBranchFormat5, BzrBranchFormat6))
212
# might be able to share control object
213
and self.branch.base.split('/')[-2] == self.basedir.split('/')[-1]):
214
self._control_files = self.branch.control_files
215
elif _control_files is not None:
216
assert False, "not done yet"
217
# self._control_files = _control_files
219
self._control_files = LockableFiles(
220
get_transport(self.basedir).clone(bzrlib.BZRDIR), 'branch-lock')
222
# update the whole cache up front and write to disk if anything changed;
223
# in the future we might want to do this more selectively
224
# two possible ways offer themselves : in self._unlock, write the cache
225
# if needed, or, when the cache sees a change, append it to the hash
226
# cache file, and have the parser take the most recent entry for a
228
hc = self._hashcache = HashCache(basedir)
236
if _inventory is None:
237
self._set_inventory(self.read_working_inventory())
239
self._set_inventory(_inventory)
241
def _set_inventory(self, inv):
242
self._inventory = inv
243
self.path2id = self._inventory.path2id
246
def open_containing(path=None):
247
"""Open an existing working tree which has its root about path.
249
This probes for a working tree at path and searches upwards from there.
251
Basically we keep looking up until we find the control directory or
252
run into /. If there isn't one, raises NotBranchError.
253
TODO: give this a new exception.
254
If there is one, it is returned, along with the unused portion of path.
260
if path.find('://') != -1:
261
raise NotBranchError(path=path)
267
return WorkingTree(path), tail
268
except NotBranchError:
271
tail = pathjoin(os.path.basename(path), tail)
273
tail = os.path.basename(path)
275
path = os.path.dirname(path)
277
# reached the root, whatever that may be
278
raise NotBranchError(path=orig_path)
281
"""Iterate through file_ids for this tree.
283
file_ids are in a WorkingTree if they are in the working inventory
284
and the working file exists.
286
inv = self._inventory
287
for path, ie in inv.iter_entries():
288
if bzrlib.osutils.lexists(self.abspath(path)):
292
return "<%s of %s>" % (self.__class__.__name__,
293
getattr(self, 'basedir', None))
295
def abspath(self, filename):
296
return pathjoin(self.basedir, filename)
299
def create(branch, directory):
300
"""Create a workingtree for branch at directory.
302
If existing_directory already exists it must have a .bzr directory.
303
If it does not exist, it will be created.
305
This returns a new WorkingTree object for the new checkout.
307
TODO FIXME RBC 20060124 when we have checkout formats in place this
308
should accept an optional revisionid to checkout [and reject this if
309
checking out into the same dir as a pre-checkout-aware branch format.]
314
if e.errno != errno.EEXIST:
317
os.mkdir(pathjoin(directory, '.bzr'))
319
if e.errno != errno.EEXIST:
321
inv = branch.repository.revision_tree(branch.last_revision()).inventory
322
wt = WorkingTree(directory, branch, inv)
323
wt._write_inventory(inv)
324
if branch.last_revision() is not None:
325
wt.set_last_revision(branch.last_revision())
326
wt.set_pending_merges([])
331
def create_standalone(directory):
332
"""Create a checkout and a branch at directory.
334
Directory must exist and be empty.
336
directory = safe_unicode(directory)
337
b = Branch.create(directory)
338
return WorkingTree.create(b, directory)
340
def relpath(self, abs):
341
"""Return the local path portion from a given absolute path."""
342
return relpath(self.basedir, abs)
344
def has_filename(self, filename):
345
return bzrlib.osutils.lexists(self.abspath(filename))
347
def get_file(self, file_id):
348
return self.get_file_byname(self.id2path(file_id))
350
def get_file_byname(self, filename):
351
return file(self.abspath(filename), 'rb')
353
def get_root_id(self):
354
"""Return the id of this trees root"""
355
inv = self.read_working_inventory()
356
return inv.root.file_id
358
def _get_store_filename(self, file_id):
359
## XXX: badly named; this is not in the store at all
360
return self.abspath(self.id2path(file_id))
363
def commit(self, *args, **kwargs):
364
from bzrlib.commit import Commit
365
# args for wt.commit start at message from the Commit.commit method,
366
# but with branch a kwarg now, passing in args as is results in the
367
#message being used for the branch
368
args = (DEPRECATED_PARAMETER, ) + args
369
Commit().commit(working_tree=self, *args, **kwargs)
370
self._set_inventory(self.read_working_inventory())
372
def id2abspath(self, file_id):
373
return self.abspath(self.id2path(file_id))
375
def has_id(self, file_id):
376
# files that have been deleted are excluded
377
inv = self._inventory
378
if not inv.has_id(file_id):
380
path = inv.id2path(file_id)
381
return bzrlib.osutils.lexists(self.abspath(path))
383
def has_or_had_id(self, file_id):
384
if file_id == self.inventory.root.file_id:
386
return self.inventory.has_id(file_id)
388
__contains__ = has_id
390
def get_file_size(self, file_id):
391
return os.path.getsize(self.id2abspath(file_id))
394
def get_file_sha1(self, file_id):
395
path = self._inventory.id2path(file_id)
396
return self._hashcache.get_sha1(path)
398
def is_executable(self, file_id):
400
return self._inventory[file_id].executable
402
path = self._inventory.id2path(file_id)
403
mode = os.lstat(self.abspath(path)).st_mode
404
return bool(stat.S_ISREG(mode) and stat.S_IEXEC&mode)
407
def add(self, files, ids=None):
408
"""Make files versioned.
410
Note that the command line normally calls smart_add instead,
411
which can automatically recurse.
413
This adds the files to the inventory, so that they will be
414
recorded by the next commit.
417
List of paths to add, relative to the base of the tree.
420
If set, use these instead of automatically generated ids.
421
Must be the same length as the list of files, but may
422
contain None for ids that are to be autogenerated.
424
TODO: Perhaps have an option to add the ids even if the files do
427
TODO: Perhaps callback with the ids and paths as they're added.
429
# TODO: Re-adding a file that is removed in the working copy
430
# should probably put it back with the previous ID.
431
if isinstance(files, basestring):
432
assert(ids is None or isinstance(ids, basestring))
438
ids = [None] * len(files)
440
assert(len(ids) == len(files))
442
inv = self.read_working_inventory()
443
for f,file_id in zip(files, ids):
444
if is_control_file(f):
445
raise BzrError("cannot add control file %s" % quotefn(f))
450
raise BzrError("cannot add top-level %r" % f)
452
fullpath = normpath(self.abspath(f))
455
kind = file_kind(fullpath)
457
# maybe something better?
458
raise BzrError('cannot add: not a regular file, symlink or directory: %s' % quotefn(f))
460
if not InventoryEntry.versionable_kind(kind):
461
raise BzrError('cannot add: not a versionable file ('
462
'i.e. regular file, symlink or directory): %s' % quotefn(f))
465
file_id = gen_file_id(f)
466
inv.add_path(f, kind=kind, file_id=file_id)
468
mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
469
self._write_inventory(inv)
472
def add_pending_merge(self, *revision_ids):
473
# TODO: Perhaps should check at this point that the
474
# history of the revision is actually present?
475
p = self.pending_merges()
477
for rev_id in revision_ids:
483
self.set_pending_merges(p)
486
def pending_merges(self):
487
"""Return a list of pending merges.
489
These are revisions that have been merged into the working
490
directory but not yet committed.
493
merges_file = self._control_files.get_utf8('pending-merges')
495
if e.errno != errno.ENOENT:
499
for l in merges_file.readlines():
500
p.append(l.rstrip('\n'))
504
def set_pending_merges(self, rev_list):
505
self._control_files.put_utf8('pending-merges', '\n'.join(rev_list))
507
def get_symlink_target(self, file_id):
508
return os.readlink(self.id2abspath(file_id))
510
def file_class(self, filename):
511
if self.path2id(filename):
513
elif self.is_ignored(filename):
519
def list_files(self):
520
"""Recursively list all files as (path, class, kind, id).
522
Lists, but does not descend into unversioned directories.
524
This does not include files that have been deleted in this
527
Skips the control directory.
529
inv = self._inventory
531
def descend(from_dir_relpath, from_dir_id, dp):
535
## TODO: If we find a subdirectory with its own .bzr
536
## directory, then that is a separate tree and we
537
## should exclude it.
538
if bzrlib.BZRDIR == f:
542
fp = appendpath(from_dir_relpath, f)
545
fap = appendpath(dp, f)
547
f_ie = inv.get_child(from_dir_id, f)
550
elif self.is_ignored(fp):
559
raise BzrCheckError("file %r entered as kind %r id %r, "
561
% (fap, f_ie.kind, f_ie.file_id, fk))
563
# make a last minute entry
567
if fk == 'directory':
568
entry = TreeDirectory()
571
elif fk == 'symlink':
576
yield fp, c, fk, (f_ie and f_ie.file_id), entry
578
if fk != 'directory':
582
# don't descend unversioned directories
585
for ff in descend(fp, f_ie.file_id, fap):
588
for f in descend(u'', inv.root.file_id, self.basedir):
592
def move(self, from_paths, to_name):
595
to_name must exist in the inventory.
597
If to_name exists and is a directory, the files are moved into
598
it, keeping their old names.
600
Note that to_name is only the last component of the new name;
601
this doesn't change the directory.
603
This returns a list of (from_path, to_path) pairs for each
607
## TODO: Option to move IDs only
608
assert not isinstance(from_paths, basestring)
610
to_abs = self.abspath(to_name)
611
if not isdir(to_abs):
612
raise BzrError("destination %r is not a directory" % to_abs)
613
if not self.has_filename(to_name):
614
raise BzrError("destination %r not in working directory" % to_abs)
615
to_dir_id = inv.path2id(to_name)
616
if to_dir_id == None and to_name != '':
617
raise BzrError("destination %r is not a versioned directory" % to_name)
618
to_dir_ie = inv[to_dir_id]
619
if to_dir_ie.kind not in ('directory', 'root_directory'):
620
raise BzrError("destination %r is not a directory" % to_abs)
622
to_idpath = inv.get_idpath(to_dir_id)
625
if not self.has_filename(f):
626
raise BzrError("%r does not exist in working tree" % f)
627
f_id = inv.path2id(f)
629
raise BzrError("%r is not versioned" % f)
630
name_tail = splitpath(f)[-1]
631
dest_path = appendpath(to_name, name_tail)
632
if self.has_filename(dest_path):
633
raise BzrError("destination %r already exists" % dest_path)
634
if f_id in to_idpath:
635
raise BzrError("can't move %r to a subdirectory of itself" % f)
637
# OK, so there's a race here, it's possible that someone will
638
# create a file in this interval and then the rename might be
639
# left half-done. But we should have caught most problems.
640
orig_inv = deepcopy(self.inventory)
643
name_tail = splitpath(f)[-1]
644
dest_path = appendpath(to_name, name_tail)
645
result.append((f, dest_path))
646
inv.rename(inv.path2id(f), to_dir_id, name_tail)
648
rename(self.abspath(f), self.abspath(dest_path))
650
raise BzrError("failed to rename %r to %r: %s" %
651
(f, dest_path, e[1]),
652
["rename rolled back"])
654
# restore the inventory on error
655
self._set_inventory(orig_inv)
657
self._write_inventory(inv)
661
def rename_one(self, from_rel, to_rel):
664
This can change the directory or the filename or both.
667
if not self.has_filename(from_rel):
668
raise BzrError("can't rename: old working file %r does not exist" % from_rel)
669
if self.has_filename(to_rel):
670
raise BzrError("can't rename: new working file %r already exists" % to_rel)
672
file_id = inv.path2id(from_rel)
674
raise BzrError("can't rename: old name %r is not versioned" % from_rel)
677
from_parent = entry.parent_id
678
from_name = entry.name
680
if inv.path2id(to_rel):
681
raise BzrError("can't rename: new name %r is already versioned" % to_rel)
683
to_dir, to_tail = os.path.split(to_rel)
684
to_dir_id = inv.path2id(to_dir)
685
if to_dir_id == None and to_dir != '':
686
raise BzrError("can't determine destination directory id for %r" % to_dir)
688
mutter("rename_one:")
689
mutter(" file_id {%s}" % file_id)
690
mutter(" from_rel %r" % from_rel)
691
mutter(" to_rel %r" % to_rel)
692
mutter(" to_dir %r" % to_dir)
693
mutter(" to_dir_id {%s}" % to_dir_id)
695
inv.rename(file_id, to_dir_id, to_tail)
697
from_abs = self.abspath(from_rel)
698
to_abs = self.abspath(to_rel)
700
rename(from_abs, to_abs)
702
inv.rename(file_id, from_parent, from_name)
703
raise BzrError("failed to rename %r to %r: %s"
704
% (from_abs, to_abs, e[1]),
705
["rename rolled back"])
706
self._write_inventory(inv)
710
"""Return all unknown files.
712
These are files in the working directory that are not versioned or
713
control files or ignored.
715
>>> from bzrlib.branch import ScratchBranch
716
>>> b = ScratchBranch(files=['foo', 'foo~'])
717
>>> tree = WorkingTree(b.base, b)
718
>>> map(str, tree.unknowns())
721
>>> list(b.unknowns())
723
>>> tree.remove('foo')
724
>>> list(b.unknowns())
727
for subp in self.extras():
728
if not self.is_ignored(subp):
731
def iter_conflicts(self):
733
for path in (s[0] for s in self.list_files()):
734
stem = get_conflicted_stem(path)
737
if stem not in conflicted:
742
def pull(self, source, overwrite=False):
743
from bzrlib.merge import merge_inner
746
old_revision_history = self.branch.revision_history()
747
count = self.branch.pull(source, overwrite)
748
new_revision_history = self.branch.revision_history()
749
if new_revision_history != old_revision_history:
750
if len(old_revision_history):
751
other_revision = old_revision_history[-1]
753
other_revision = None
754
repository = self.branch.repository
755
merge_inner(self.branch,
756
self.branch.basis_tree(),
757
repository.revision_tree(other_revision),
759
self.set_last_revision(self.branch.last_revision())
765
"""Yield all unknown files in this WorkingTree.
767
If there are any unknown directories then only the directory is
768
returned, not all its children. But if there are unknown files
769
under a versioned subdirectory, they are returned.
771
Currently returned depth-first, sorted by name within directories.
773
## TODO: Work from given directory downwards
774
for path, dir_entry in self.inventory.directories():
775
mutter("search for unknowns in %r", path)
776
dirabs = self.abspath(path)
777
if not isdir(dirabs):
778
# e.g. directory deleted
782
for subf in os.listdir(dirabs):
784
and (subf not in dir_entry.children)):
789
subp = appendpath(path, subf)
793
def ignored_files(self):
794
"""Yield list of PATH, IGNORE_PATTERN"""
795
for subp in self.extras():
796
pat = self.is_ignored(subp)
801
def get_ignore_list(self):
802
"""Return list of ignore patterns.
804
Cached in the Tree object after the first call.
806
if hasattr(self, '_ignorelist'):
807
return self._ignorelist
809
l = bzrlib.DEFAULT_IGNORE[:]
810
if self.has_filename(bzrlib.IGNORE_FILENAME):
811
f = self.get_file_byname(bzrlib.IGNORE_FILENAME)
812
l.extend([line.rstrip("\n\r") for line in f.readlines()])
817
def is_ignored(self, filename):
818
r"""Check whether the filename matches an ignore pattern.
820
Patterns containing '/' or '\' need to match the whole path;
821
others match against only the last component.
823
If the file is ignored, returns the pattern which caused it to
824
be ignored, otherwise None. So this can simply be used as a
825
boolean if desired."""
827
# TODO: Use '**' to match directories, and other extended
828
# globbing stuff from cvs/rsync.
830
# XXX: fnmatch is actually not quite what we want: it's only
831
# approximately the same as real Unix fnmatch, and doesn't
832
# treat dotfiles correctly and allows * to match /.
833
# Eventually it should be replaced with something more
836
for pat in self.get_ignore_list():
837
if '/' in pat or '\\' in pat:
839
# as a special case, you can put ./ at the start of a
840
# pattern; this is good to match in the top-level
843
if (pat[:2] == './') or (pat[:2] == '.\\'):
847
if fnmatch.fnmatchcase(filename, newpat):
850
if fnmatch.fnmatchcase(splitpath(filename)[-1], pat):
855
def kind(self, file_id):
856
return file_kind(self.id2abspath(file_id))
859
"""See Branch.lock_read, and WorkingTree.unlock."""
860
return self.branch.lock_read()
862
def lock_write(self):
863
"""See Branch.lock_write, and WorkingTree.unlock."""
864
return self.branch.lock_write()
866
def _basis_inventory_name(self, revision_id):
867
return 'basis-inventory.%s' % revision_id
869
def set_last_revision(self, new_revision, old_revision=None):
870
if old_revision is not None:
872
path = self._basis_inventory_name(old_revision)
873
path = self.branch.control_files._escape(path)
874
self.branch.control_files._transport.delete(path)
878
xml = self.branch.repository.get_inventory_xml(new_revision)
879
path = self._basis_inventory_name(new_revision)
880
self.branch.control_files.put_utf8(path, xml)
881
except WeaveRevisionNotPresent:
884
def read_basis_inventory(self, revision_id):
885
"""Read the cached basis inventory."""
886
path = self._basis_inventory_name(revision_id)
887
return self.branch.control_files.get_utf8(path).read()
890
def read_working_inventory(self):
891
"""Read the working inventory."""
892
# ElementTree does its own conversion from UTF-8, so open in
894
return bzrlib.xml5.serializer_v5.read_inventory(
895
self._control_files.get('inventory'))
898
def remove(self, files, verbose=False):
899
"""Remove nominated files from the working inventory..
901
This does not remove their text. This does not run on XXX on what? RBC
903
TODO: Refuse to remove modified files unless --force is given?
905
TODO: Do something useful with directories.
907
TODO: Should this remove the text or not? Tough call; not
908
removing may be useful and the user can just use use rm, and
909
is the opposite of add. Removing it is consistent with most
910
other tools. Maybe an option.
912
## TODO: Normalize names
913
## TODO: Remove nested loops; better scalability
914
if isinstance(files, basestring):
919
# do this before any modifications
923
# TODO: Perhaps make this just a warning, and continue?
924
# This tends to happen when
925
raise NotVersionedError(path=f)
926
mutter("remove inventory entry %s {%s}", quotefn(f), fid)
928
# having remove it, it must be either ignored or unknown
929
if self.is_ignored(f):
933
show_status(new_status, inv[fid].kind, quotefn(f))
936
self._write_inventory(inv)
939
def revert(self, filenames, old_tree=None, backups=True):
940
from bzrlib.merge import merge_inner
942
old_tree = self.branch.basis_tree()
943
merge_inner(self.branch, old_tree,
944
self, ignore_zero=True,
945
backup_files=backups,
946
interesting_files=filenames,
948
if not len(filenames):
949
self.set_pending_merges([])
952
def set_inventory(self, new_inventory_list):
953
from bzrlib.inventory import (Inventory,
958
inv = Inventory(self.get_root_id())
959
for path, file_id, parent, kind in new_inventory_list:
960
name = os.path.basename(path)
963
# fixme, there should be a factory function inv,add_??
964
if kind == 'directory':
965
inv.add(InventoryDirectory(file_id, name, parent))
967
inv.add(InventoryFile(file_id, name, parent))
968
elif kind == 'symlink':
969
inv.add(InventoryLink(file_id, name, parent))
971
raise BzrError("unknown kind %r" % kind)
972
self._write_inventory(inv)
975
def set_root_id(self, file_id):
976
"""Set the root id for this tree."""
977
inv = self.read_working_inventory()
978
orig_root_id = inv.root.file_id
979
del inv._byid[inv.root.file_id]
980
inv.root.file_id = file_id
981
inv._byid[inv.root.file_id] = inv.root
984
if entry.parent_id in (None, orig_root_id):
985
entry.parent_id = inv.root.file_id
986
self._write_inventory(inv)
989
"""See Branch.unlock.
991
WorkingTree locking just uses the Branch locking facilities.
992
This is current because all working trees have an embedded branch
993
within them. IF in the future, we were to make branch data shareable
994
between multiple working trees, i.e. via shared storage, then we
995
would probably want to lock both the local tree, and the branch.
997
# FIXME: We want to write out the hashcache only when the last lock on
998
# this working copy is released. Peeking at the lock count is a bit
999
# of a nasty hack; probably it's better to have a transaction object,
1000
# which can do some finalization when it's either successfully or
1001
# unsuccessfully completed. (Denys's original patch did that.)
1002
if self._hashcache.needs_write and self.branch.control_files._lock_count==1:
1003
self._hashcache.write()
1004
return self.branch.unlock()
1007
def _write_inventory(self, inv):
1008
"""Write inventory as the current inventory."""
1010
bzrlib.xml5.serializer_v5.write_inventory(inv, sio)
1012
self._control_files.put('inventory', sio)
1013
self._set_inventory(inv)
1014
mutter('wrote working inventory')
1017
CONFLICT_SUFFIXES = ('.THIS', '.BASE', '.OTHER')
1018
def get_conflicted_stem(path):
1019
for suffix in CONFLICT_SUFFIXES:
1020
if path.endswith(suffix):
1021
return path[:-len(suffix)]