14
14
# along with this program; if not, write to the Free Software
15
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 Branch.working_tree():
32
# TODO: Don't allow WorkingTrees to be constructed for remote branches if
17
# TODO: Don't allow WorkingTrees to be constructed for remote branches.
35
19
# FIXME: I don't know if writing out the cache from the destructor is really a
36
# good idea, because destructors are considered poor taste in Python, and it's
37
# not predictable when it will be written out.
39
# TODO: Give the workingtree sole responsibility for the working inventory;
40
# remove the variable and references to it from the branch. This may require
41
# updating the commit code so as to update the inventory within the working
42
# copy, and making sure there's only one WorkingTree for any directory on disk.
43
# At the momenthey may alias the inventory and have old copies of it in memory.
45
from copy import deepcopy
20
# good idea, because destructors are considered poor taste in Python, and
21
# it's not predictable when it will be written out.
50
from bzrlib.branch import (Branch,
55
from bzrlib.errors import (BzrCheckError,
60
from bzrlib.inventory import InventoryEntry
61
from bzrlib.osutils import (appendpath,
28
from bzrlib.osutils import appendpath, file_kind, isdir, splitpath
29
from bzrlib.errors import BzrCheckError
72
30
from bzrlib.trace import mutter
76
def gen_file_id(name):
77
"""Return new file id.
79
This should probably generate proper UUIDs, but for the moment we
80
cope with just randomness because running uuidgen every time is
83
from binascii import hexlify
90
idx = name.rfind('\\')
94
# make it not a hidden file
95
name = name.lstrip('.')
97
# remove any wierd characters; we don't escape them but rather
99
name = re.sub(r'[^\w.]', '', name)
101
s = hexlify(rand_bytes(8))
102
return '-'.join((name, compact_date(time()), s))
106
"""Return a new tree-root file id."""
107
return gen_file_id('TREE_ROOT')
110
32
class TreeEntry(object):
111
33
"""An entry that implements the minium interface used by commands.
171
93
It is possible for a `WorkingTree` to have a filename which is
172
94
not listed in the Inventory and vice versa.
175
def __init__(self, basedir='.', branch=None):
176
"""Construct a WorkingTree for basedir.
178
If the branch is not supplied, it is opened automatically.
179
If the branch is supplied, it must be the branch for this basedir.
180
(branch.base is not cross checked, because for remote branches that
181
would be meaningless).
96
def __init__(self, basedir, inv):
183
97
from bzrlib.hashcache import HashCache
184
98
from bzrlib.trace import note, mutter
185
assert isinstance(basedir, basestring), \
186
"base directory %r is not a string" % basedir
188
branch = Branch.open(basedir)
189
assert isinstance(branch, Branch), \
190
"branch %r is not a Branch" % branch
192
self.basedir = realpath(basedir)
194
self._set_inventory(self.read_working_inventory())
100
self._inventory = inv
101
self.basedir = basedir
102
self.path2id = inv.path2id
196
104
# update the whole cache up front and write to disk if anything changed;
197
105
# in the future we might want to do this more selectively
198
# two possible ways offer themselves : in self._unlock, write the cache
199
# if needed, or, when the cache sees a change, append it to the hash
200
# cache file, and have the parser take the most recent entry for a
202
106
hc = self._hashcache = HashCache(basedir)
206
110
if hc.needs_write:
207
111
mutter("write hc")
210
def _set_inventory(self, inv):
211
self._inventory = inv
212
self.path2id = self._inventory.path2id
215
def open_containing(path=None):
216
"""Open an existing working tree which has its root about path.
218
This probes for a working tree at path and searches upwards from there.
220
Basically we keep looking up until we find the control directory or
221
run into /. If there isn't one, raises NotBranchError.
222
TODO: give this a new exception.
223
If there is one, it is returned, along with the unused portion of path.
229
if path.find('://') != -1:
230
raise NotBranchError(path=path)
231
path = os.path.abspath(path)
235
return WorkingTree(path), tail
236
except NotBranchError:
239
tail = os.path.join(os.path.basename(path), tail)
241
tail = os.path.basename(path)
242
path = os.path.dirname(path)
243
# FIXME: top in windows is indicated how ???
244
if path == os.path.sep:
245
# reached the root, whatever that may be
246
raise NotBranchError(path=path)
116
if self._hashcache.needs_write:
117
self._hashcache.write()
248
120
def __iter__(self):
249
121
"""Iterate through file_ids for this tree.
276
147
def get_file_byname(self, filename):
277
148
return file(self.abspath(filename), 'rb')
279
def get_root_id(self):
280
"""Return the id of this trees root"""
281
inv = self.read_working_inventory()
282
return inv.root.file_id
284
150
def _get_store_filename(self, file_id):
285
## XXX: badly named; this is not in the store at all
151
## XXX: badly named; this isn't in the store at all
286
152
return self.abspath(self.id2path(file_id))
289
def commit(self, *args, **kw):
290
from bzrlib.commit import Commit
291
Commit().commit(self.branch, *args, **kw)
292
self._set_inventory(self.read_working_inventory())
294
155
def id2abspath(self, file_id):
295
156
return self.abspath(self.id2path(file_id))
297
159
def has_id(self, file_id):
298
160
# files that have been deleted are excluded
299
161
inv = self._inventory
324
184
mode = os.lstat(self.abspath(path)).st_mode
325
185
return bool(stat.S_ISREG(mode) and stat.S_IEXEC&mode)
328
def add(self, files, ids=None):
329
"""Make files versioned.
331
Note that the command line normally calls smart_add instead,
332
which can automatically recurse.
334
This adds the files to the inventory, so that they will be
335
recorded by the next commit.
338
List of paths to add, relative to the base of the tree.
341
If set, use these instead of automatically generated ids.
342
Must be the same length as the list of files, but may
343
contain None for ids that are to be autogenerated.
345
TODO: Perhaps have an option to add the ids even if the files do
348
TODO: Perhaps callback with the ids and paths as they're added.
350
# TODO: Re-adding a file that is removed in the working copy
351
# should probably put it back with the previous ID.
352
if isinstance(files, basestring):
353
assert(ids is None or isinstance(ids, basestring))
359
ids = [None] * len(files)
361
assert(len(ids) == len(files))
363
inv = self.read_working_inventory()
364
for f,file_id in zip(files, ids):
365
if is_control_file(f):
366
raise BzrError("cannot add control file %s" % quotefn(f))
371
raise BzrError("cannot add top-level %r" % f)
373
fullpath = os.path.normpath(self.abspath(f))
376
kind = file_kind(fullpath)
378
# maybe something better?
379
raise BzrError('cannot add: not a regular file, symlink or directory: %s' % quotefn(f))
381
if not InventoryEntry.versionable_kind(kind):
382
raise BzrError('cannot add: not a versionable file ('
383
'i.e. regular file, symlink or directory): %s' % quotefn(f))
386
file_id = gen_file_id(f)
387
inv.add_path(f, kind=kind, file_id=file_id)
389
mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
390
self._write_inventory(inv)
393
def add_pending_merge(self, *revision_ids):
394
# TODO: Perhaps should check at this point that the
395
# history of the revision is actually present?
396
p = self.pending_merges()
398
for rev_id in revision_ids:
404
self.set_pending_merges(p)
406
def pending_merges(self):
407
"""Return a list of pending merges.
409
These are revisions that have been merged into the working
410
directory but not yet committed.
412
cfn = self.branch._rel_controlfilename('pending-merges')
413
if not self.branch._transport.has(cfn):
416
for l in self.branch.controlfile('pending-merges', 'r').readlines():
417
p.append(l.rstrip('\n'))
421
def set_pending_merges(self, rev_list):
422
self.branch.put_controlfile('pending-merges', '\n'.join(rev_list))
424
187
def get_symlink_target(self, file_id):
425
188
return os.readlink(self.id2abspath(file_id))
505
268
for f in descend('', inv.root.file_id, self.basedir):
509
def move(self, from_paths, to_name):
512
to_name must exist in the inventory.
514
If to_name exists and is a directory, the files are moved into
515
it, keeping their old names.
517
Note that to_name is only the last component of the new name;
518
this doesn't change the directory.
520
This returns a list of (from_path, to_path) pairs for each
524
## TODO: Option to move IDs only
525
assert not isinstance(from_paths, basestring)
527
to_abs = self.abspath(to_name)
528
if not isdir(to_abs):
529
raise BzrError("destination %r is not a directory" % to_abs)
530
if not self.has_filename(to_name):
531
raise BzrError("destination %r not in working directory" % to_abs)
532
to_dir_id = inv.path2id(to_name)
533
if to_dir_id == None and to_name != '':
534
raise BzrError("destination %r is not a versioned directory" % to_name)
535
to_dir_ie = inv[to_dir_id]
536
if to_dir_ie.kind not in ('directory', 'root_directory'):
537
raise BzrError("destination %r is not a directory" % to_abs)
539
to_idpath = inv.get_idpath(to_dir_id)
542
if not self.has_filename(f):
543
raise BzrError("%r does not exist in working tree" % f)
544
f_id = inv.path2id(f)
546
raise BzrError("%r is not versioned" % f)
547
name_tail = splitpath(f)[-1]
548
dest_path = appendpath(to_name, name_tail)
549
if self.has_filename(dest_path):
550
raise BzrError("destination %r already exists" % dest_path)
551
if f_id in to_idpath:
552
raise BzrError("can't move %r to a subdirectory of itself" % f)
554
# OK, so there's a race here, it's possible that someone will
555
# create a file in this interval and then the rename might be
556
# left half-done. But we should have caught most problems.
557
orig_inv = deepcopy(self.inventory)
560
name_tail = splitpath(f)[-1]
561
dest_path = appendpath(to_name, name_tail)
562
result.append((f, dest_path))
563
inv.rename(inv.path2id(f), to_dir_id, name_tail)
565
rename(self.abspath(f), self.abspath(dest_path))
567
raise BzrError("failed to rename %r to %r: %s" %
568
(f, dest_path, e[1]),
569
["rename rolled back"])
571
# restore the inventory on error
572
self._set_inventory(orig_inv)
574
self._write_inventory(inv)
578
def rename_one(self, from_rel, to_rel):
581
This can change the directory or the filename or both.
584
if not self.has_filename(from_rel):
585
raise BzrError("can't rename: old working file %r does not exist" % from_rel)
586
if self.has_filename(to_rel):
587
raise BzrError("can't rename: new working file %r already exists" % to_rel)
589
file_id = inv.path2id(from_rel)
591
raise BzrError("can't rename: old name %r is not versioned" % from_rel)
594
from_parent = entry.parent_id
595
from_name = entry.name
597
if inv.path2id(to_rel):
598
raise BzrError("can't rename: new name %r is already versioned" % to_rel)
600
to_dir, to_tail = os.path.split(to_rel)
601
to_dir_id = inv.path2id(to_dir)
602
if to_dir_id == None and to_dir != '':
603
raise BzrError("can't determine destination directory id for %r" % to_dir)
605
mutter("rename_one:")
606
mutter(" file_id {%s}" % file_id)
607
mutter(" from_rel %r" % from_rel)
608
mutter(" to_rel %r" % to_rel)
609
mutter(" to_dir %r" % to_dir)
610
mutter(" to_dir_id {%s}" % to_dir_id)
612
inv.rename(file_id, to_dir_id, to_tail)
614
from_abs = self.abspath(from_rel)
615
to_abs = self.abspath(to_rel)
617
rename(from_abs, to_abs)
619
inv.rename(file_id, from_parent, from_name)
620
raise BzrError("failed to rename %r to %r: %s"
621
% (from_abs, to_abs, e[1]),
622
["rename rolled back"])
623
self._write_inventory(inv)
626
273
def unknowns(self):
627
"""Return all unknown files.
629
These are files in the working directory that are not versioned or
630
control files or ignored.
632
>>> from bzrlib.branch import ScratchBranch
633
>>> b = ScratchBranch(files=['foo', 'foo~'])
634
>>> tree = WorkingTree(b.base, b)
635
>>> map(str, tree.unknowns())
638
>>> list(b.unknowns())
640
>>> tree.remove('foo')
641
>>> list(b.unknowns())
644
274
for subp in self.extras():
645
275
if not self.is_ignored(subp):
769
def kind(self, file_id):
770
return file_kind(self.id2abspath(file_id))
773
"""See Branch.lock_read, and WorkingTree.unlock."""
774
return self.branch.lock_read()
776
def lock_write(self):
777
"""See Branch.lock_write, and WorkingTree.unlock."""
778
return self.branch.lock_write()
781
def read_working_inventory(self):
782
"""Read the working inventory."""
783
# ElementTree does its own conversion from UTF-8, so open in
785
f = self.branch.controlfile('inventory', 'rb')
786
return bzrlib.xml5.serializer_v5.read_inventory(f)
789
def remove(self, files, verbose=False):
790
"""Remove nominated files from the working inventory..
792
This does not remove their text. This does not run on XXX on what? RBC
794
TODO: Refuse to remove modified files unless --force is given?
796
TODO: Do something useful with directories.
798
TODO: Should this remove the text or not? Tough call; not
799
removing may be useful and the user can just use use rm, and
800
is the opposite of add. Removing it is consistent with most
801
other tools. Maybe an option.
803
## TODO: Normalize names
804
## TODO: Remove nested loops; better scalability
805
if isinstance(files, basestring):
810
# do this before any modifications
814
# TODO: Perhaps make this just a warning, and continue?
815
# This tends to happen when
816
raise NotVersionedError(path=f)
817
mutter("remove inventory entry %s {%s}", quotefn(f), fid)
819
# having remove it, it must be either ignored or unknown
820
if self.is_ignored(f):
824
show_status(new_status, inv[fid].kind, quotefn(f))
827
self._write_inventory(inv)
830
def revert(self, filenames, old_tree=None, backups=True):
831
from bzrlib.merge import merge_inner
833
old_tree = self.branch.basis_tree()
834
merge_inner(self.branch, old_tree,
835
self, ignore_zero=True,
836
backup_files=backups,
837
interesting_files=filenames)
838
if not len(filenames):
839
self.set_pending_merges([])
842
def set_inventory(self, new_inventory_list):
843
from bzrlib.inventory import (Inventory,
848
inv = Inventory(self.get_root_id())
849
for path, file_id, parent, kind in new_inventory_list:
850
name = os.path.basename(path)
853
# fixme, there should be a factory function inv,add_??
854
if kind == 'directory':
855
inv.add(InventoryDirectory(file_id, name, parent))
857
inv.add(InventoryFile(file_id, name, parent))
858
elif kind == 'symlink':
859
inv.add(InventoryLink(file_id, name, parent))
861
raise BzrError("unknown kind %r" % kind)
862
self._write_inventory(inv)
865
def set_root_id(self, file_id):
866
"""Set the root id for this tree."""
867
inv = self.read_working_inventory()
868
orig_root_id = inv.root.file_id
869
del inv._byid[inv.root.file_id]
870
inv.root.file_id = file_id
871
inv._byid[inv.root.file_id] = inv.root
874
if entry.parent_id in (None, orig_root_id):
875
entry.parent_id = inv.root.file_id
876
self._write_inventory(inv)
879
"""See Branch.unlock.
881
WorkingTree locking just uses the Branch locking facilities.
882
This is current because all working trees have an embedded branch
883
within them. IF in the future, we were to make branch data shareable
884
between multiple working trees, i.e. via shared storage, then we
885
would probably want to lock both the local tree, and the branch.
887
return self.branch.unlock()
890
def _write_inventory(self, inv):
891
"""Write inventory as the current inventory."""
892
from cStringIO import StringIO
893
from bzrlib.atomicfile import AtomicFile
895
bzrlib.xml5.serializer_v5.write_inventory(inv, sio)
897
f = AtomicFile(self.branch.controlfilename('inventory'))
903
self._set_inventory(inv)
904
mutter('wrote working inventory')
907
379
CONFLICT_SUFFIXES = ('.THIS', '.BASE', '.OTHER')
908
380
def get_conflicted_stem(path):
909
381
for suffix in CONFLICT_SUFFIXES: