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,
58
WeaveRevisionNotPresent,
62
from bzrlib.inventory import InventoryEntry
63
from bzrlib.osutils import (appendpath,
77
from bzrlib.textui import show_status
27
from bzrlib.osutils import appendpath, file_kind, isdir, splitpath
28
from bzrlib.errors import BzrCheckError
79
29
from bzrlib.trace import mutter
83
def gen_file_id(name):
84
"""Return new file id.
86
This should probably generate proper UUIDs, but for the moment we
87
cope with just randomness because running uuidgen every time is
90
from binascii import hexlify
97
idx = name.rfind('\\')
101
# make it not a hidden file
102
name = name.lstrip('.')
104
# remove any wierd characters; we don't escape them but rather
106
name = re.sub(r'[^\w.]', '', name)
108
s = hexlify(rand_bytes(8))
109
return '-'.join((name, compact_date(time()), s))
113
"""Return a new tree-root file id."""
114
return gen_file_id('TREE_ROOT')
117
class TreeEntry(object):
118
"""An entry that implements the minium interface used by commands.
120
This needs further inspection, it may be better to have
121
InventoryEntries without ids - though that seems wrong. For now,
122
this is a parallel hierarchy to InventoryEntry, and needs to become
123
one of several things: decorates to that hierarchy, children of, or
125
Another note is that these objects are currently only used when there is
126
no InventoryEntry available - i.e. for unversioned objects.
127
Perhaps they should be UnversionedEntry et al. ? - RBC 20051003
130
def __eq__(self, other):
131
# yes, this us ugly, TODO: best practice __eq__ style.
132
return (isinstance(other, TreeEntry)
133
and other.__class__ == self.__class__)
135
def kind_character(self):
139
class TreeDirectory(TreeEntry):
140
"""See TreeEntry. This is a directory in a working tree."""
142
def __eq__(self, other):
143
return (isinstance(other, TreeDirectory)
144
and other.__class__ == self.__class__)
146
def kind_character(self):
150
class TreeFile(TreeEntry):
151
"""See TreeEntry. This is a regular file in a working tree."""
153
def __eq__(self, other):
154
return (isinstance(other, TreeFile)
155
and other.__class__ == self.__class__)
157
def kind_character(self):
161
class TreeLink(TreeEntry):
162
"""See TreeEntry. This is a symlink in a working tree."""
164
def __eq__(self, other):
165
return (isinstance(other, TreeLink)
166
and other.__class__ == self.__class__)
168
def kind_character(self):
172
31
class WorkingTree(bzrlib.tree.Tree):
173
32
"""Working copy tree.
178
37
It is possible for a `WorkingTree` to have a filename which is
179
38
not listed in the Inventory and vice versa.
182
def __init__(self, basedir=u'.', branch=None):
183
"""Construct a WorkingTree for basedir.
185
If the branch is not supplied, it is opened automatically.
186
If the branch is supplied, it must be the branch for this basedir.
187
(branch.base is not cross checked, because for remote branches that
188
would be meaningless).
40
def __init__(self, basedir, inv):
190
41
from bzrlib.hashcache import HashCache
191
42
from bzrlib.trace import note, mutter
192
assert isinstance(basedir, basestring), \
193
"base directory %r is not a string" % basedir
195
branch = Branch.open(basedir)
196
assert isinstance(branch, Branch), \
197
"branch %r is not a Branch" % branch
199
self.basedir = realpath(basedir)
45
self.basedir = basedir
46
self.path2id = inv.path2id
201
48
# update the whole cache up front and write to disk if anything changed;
202
49
# in the future we might want to do this more selectively
203
# two possible ways offer themselves : in self._unlock, write the cache
204
# if needed, or, when the cache sees a change, append it to the hash
205
# cache file, and have the parser take the most recent entry for a
207
50
hc = self._hashcache = HashCache(basedir)
211
54
if hc.needs_write:
212
55
mutter("write hc")
215
self._set_inventory(self.read_working_inventory())
217
def _set_inventory(self, inv):
218
self._inventory = inv
219
self.path2id = self._inventory.path2id
222
def open_containing(path=None):
223
"""Open an existing working tree which has its root about path.
225
This probes for a working tree at path and searches upwards from there.
227
Basically we keep looking up until we find the control directory or
228
run into /. If there isn't one, raises NotBranchError.
229
TODO: give this a new exception.
230
If there is one, it is returned, along with the unused portion of path.
236
if path.find('://') != -1:
237
raise NotBranchError(path=path)
242
return WorkingTree(path), tail
243
except NotBranchError:
246
tail = pathjoin(os.path.basename(path), tail)
248
tail = os.path.basename(path)
250
path = os.path.dirname(path)
252
# reached the root, whatever that may be
253
raise NotBranchError(path=path)
60
if self._hashcache.needs_write:
61
self._hashcache.write()
255
64
def __iter__(self):
256
65
"""Iterate through file_ids for this tree.
261
70
inv = self._inventory
262
71
for path, ie in inv.iter_entries():
263
if bzrlib.osutils.lexists(self.abspath(path)):
72
if os.path.exists(self.abspath(path)):
266
76
def __repr__(self):
267
77
return "<%s of %s>" % (self.__class__.__name__,
268
78
getattr(self, 'basedir', None))
270
82
def abspath(self, filename):
271
return pathjoin(self.basedir, filename)
273
def relpath(self, abs):
274
"""Return the local path portion from a given absolute path."""
275
return relpath(self.basedir, abs)
83
return os.path.join(self.basedir, filename)
277
85
def has_filename(self, filename):
278
return bzrlib.osutils.lexists(self.abspath(filename))
86
return os.path.exists(self.abspath(filename))
280
88
def get_file(self, file_id):
281
89
return self.get_file_byname(self.id2path(file_id))
283
91
def get_file_byname(self, filename):
284
92
return file(self.abspath(filename), 'rb')
286
def get_root_id(self):
287
"""Return the id of this trees root"""
288
inv = self.read_working_inventory()
289
return inv.root.file_id
291
94
def _get_store_filename(self, file_id):
292
## XXX: badly named; this is not in the store at all
293
return self.abspath(self.id2path(file_id))
296
def commit(self, *args, **kw):
297
from bzrlib.commit import Commit
298
Commit().commit(self.branch, *args, **kw)
299
self._set_inventory(self.read_working_inventory())
301
def id2abspath(self, file_id):
302
return self.abspath(self.id2path(file_id))
95
## XXX: badly named; this isn't in the store at all
96
return self.abspath(self.id2path(file_id))
304
99
def has_id(self, file_id):
305
100
# files that have been deleted are excluded
306
101
inv = self._inventory
307
102
if not inv.has_id(file_id):
309
104
path = inv.id2path(file_id)
310
return bzrlib.osutils.lexists(self.abspath(path))
105
return os.path.exists(self.abspath(path))
312
def has_or_had_id(self, file_id):
313
if file_id == self.inventory.root.file_id:
315
return self.inventory.has_id(file_id)
317
108
__contains__ = has_id
319
111
def get_file_size(self, file_id):
320
return os.path.getsize(self.id2abspath(file_id))
112
# is this still called?
113
raise NotImplementedError()
323
116
def get_file_sha1(self, file_id):
324
117
path = self._inventory.id2path(file_id)
325
118
return self._hashcache.get_sha1(path)
327
def is_executable(self, file_id):
329
return self._inventory[file_id].executable
331
path = self._inventory.id2path(file_id)
332
mode = os.lstat(self.abspath(path)).st_mode
333
return bool(stat.S_ISREG(mode) and stat.S_IEXEC&mode)
336
def add(self, files, ids=None):
337
"""Make files versioned.
339
Note that the command line normally calls smart_add instead,
340
which can automatically recurse.
342
This adds the files to the inventory, so that they will be
343
recorded by the next commit.
346
List of paths to add, relative to the base of the tree.
349
If set, use these instead of automatically generated ids.
350
Must be the same length as the list of files, but may
351
contain None for ids that are to be autogenerated.
353
TODO: Perhaps have an option to add the ids even if the files do
356
TODO: Perhaps callback with the ids and paths as they're added.
358
# TODO: Re-adding a file that is removed in the working copy
359
# should probably put it back with the previous ID.
360
if isinstance(files, basestring):
361
assert(ids is None or isinstance(ids, basestring))
367
ids = [None] * len(files)
369
assert(len(ids) == len(files))
371
inv = self.read_working_inventory()
372
for f,file_id in zip(files, ids):
373
if is_control_file(f):
374
raise BzrError("cannot add control file %s" % quotefn(f))
379
raise BzrError("cannot add top-level %r" % f)
381
fullpath = normpath(self.abspath(f))
384
kind = file_kind(fullpath)
386
# maybe something better?
387
raise BzrError('cannot add: not a regular file, symlink or directory: %s' % quotefn(f))
389
if not InventoryEntry.versionable_kind(kind):
390
raise BzrError('cannot add: not a versionable file ('
391
'i.e. regular file, symlink or directory): %s' % quotefn(f))
394
file_id = gen_file_id(f)
395
inv.add_path(f, kind=kind, file_id=file_id)
397
mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
398
self._write_inventory(inv)
401
def add_pending_merge(self, *revision_ids):
402
# TODO: Perhaps should check at this point that the
403
# history of the revision is actually present?
404
p = self.pending_merges()
406
for rev_id in revision_ids:
412
self.set_pending_merges(p)
415
def pending_merges(self):
416
"""Return a list of pending merges.
418
These are revisions that have been merged into the working
419
directory but not yet committed.
422
f = self.branch.control_files.controlfile('pending-merges', 'r')
426
for l in f.readlines():
427
p.append(l.rstrip('\n'))
431
def set_pending_merges(self, rev_list):
432
self.branch.control_files.put_utf8('pending-merges', '\n'.join(rev_list))
434
def get_symlink_target(self, file_id):
435
return os.readlink(self.id2abspath(file_id))
437
121
def file_class(self, filename):
438
122
if self.path2id(filename):
512
183
for ff in descend(fp, f_ie.file_id, fap):
515
for f in descend(u'', inv.root.file_id, self.basedir):
186
for f in descend('', inv.root.file_id, self.basedir):
519
def move(self, from_paths, to_name):
522
to_name must exist in the inventory.
524
If to_name exists and is a directory, the files are moved into
525
it, keeping their old names.
527
Note that to_name is only the last component of the new name;
528
this doesn't change the directory.
530
This returns a list of (from_path, to_path) pairs for each
534
## TODO: Option to move IDs only
535
assert not isinstance(from_paths, basestring)
537
to_abs = self.abspath(to_name)
538
if not isdir(to_abs):
539
raise BzrError("destination %r is not a directory" % to_abs)
540
if not self.has_filename(to_name):
541
raise BzrError("destination %r not in working directory" % to_abs)
542
to_dir_id = inv.path2id(to_name)
543
if to_dir_id == None and to_name != '':
544
raise BzrError("destination %r is not a versioned directory" % to_name)
545
to_dir_ie = inv[to_dir_id]
546
if to_dir_ie.kind not in ('directory', 'root_directory'):
547
raise BzrError("destination %r is not a directory" % to_abs)
549
to_idpath = inv.get_idpath(to_dir_id)
552
if not self.has_filename(f):
553
raise BzrError("%r does not exist in working tree" % f)
554
f_id = inv.path2id(f)
556
raise BzrError("%r is not versioned" % f)
557
name_tail = splitpath(f)[-1]
558
dest_path = appendpath(to_name, name_tail)
559
if self.has_filename(dest_path):
560
raise BzrError("destination %r already exists" % dest_path)
561
if f_id in to_idpath:
562
raise BzrError("can't move %r to a subdirectory of itself" % f)
564
# OK, so there's a race here, it's possible that someone will
565
# create a file in this interval and then the rename might be
566
# left half-done. But we should have caught most problems.
567
orig_inv = deepcopy(self.inventory)
570
name_tail = splitpath(f)[-1]
571
dest_path = appendpath(to_name, name_tail)
572
result.append((f, dest_path))
573
inv.rename(inv.path2id(f), to_dir_id, name_tail)
575
rename(self.abspath(f), self.abspath(dest_path))
577
raise BzrError("failed to rename %r to %r: %s" %
578
(f, dest_path, e[1]),
579
["rename rolled back"])
581
# restore the inventory on error
582
self._set_inventory(orig_inv)
584
self._write_inventory(inv)
588
def rename_one(self, from_rel, to_rel):
591
This can change the directory or the filename or both.
594
if not self.has_filename(from_rel):
595
raise BzrError("can't rename: old working file %r does not exist" % from_rel)
596
if self.has_filename(to_rel):
597
raise BzrError("can't rename: new working file %r already exists" % to_rel)
599
file_id = inv.path2id(from_rel)
601
raise BzrError("can't rename: old name %r is not versioned" % from_rel)
604
from_parent = entry.parent_id
605
from_name = entry.name
607
if inv.path2id(to_rel):
608
raise BzrError("can't rename: new name %r is already versioned" % to_rel)
610
to_dir, to_tail = os.path.split(to_rel)
611
to_dir_id = inv.path2id(to_dir)
612
if to_dir_id == None and to_dir != '':
613
raise BzrError("can't determine destination directory id for %r" % to_dir)
615
mutter("rename_one:")
616
mutter(" file_id {%s}" % file_id)
617
mutter(" from_rel %r" % from_rel)
618
mutter(" to_rel %r" % to_rel)
619
mutter(" to_dir %r" % to_dir)
620
mutter(" to_dir_id {%s}" % to_dir_id)
622
inv.rename(file_id, to_dir_id, to_tail)
624
from_abs = self.abspath(from_rel)
625
to_abs = self.abspath(to_rel)
627
rename(from_abs, to_abs)
629
inv.rename(file_id, from_parent, from_name)
630
raise BzrError("failed to rename %r to %r: %s"
631
% (from_abs, to_abs, e[1]),
632
["rename rolled back"])
633
self._write_inventory(inv)
636
191
def unknowns(self):
637
"""Return all unknown files.
639
These are files in the working directory that are not versioned or
640
control files or ignored.
642
>>> from bzrlib.branch import ScratchBranch
643
>>> b = ScratchBranch(files=['foo', 'foo~'])
644
>>> tree = WorkingTree(b.base, b)
645
>>> map(str, tree.unknowns())
648
>>> list(b.unknowns())
650
>>> tree.remove('foo')
651
>>> list(b.unknowns())
654
192
for subp in self.extras():
655
193
if not self.is_ignored(subp):
658
def iter_conflicts(self):
660
for path in (s[0] for s in self.list_files()):
661
stem = get_conflicted_stem(path)
664
if stem not in conflicted:
669
def pull(self, source, overwrite=False):
670
from bzrlib.merge import merge_inner
673
old_revision_history = self.branch.revision_history()
674
count = self.branch.pull(source, overwrite)
675
new_revision_history = self.branch.revision_history()
676
if new_revision_history != old_revision_history:
677
if len(old_revision_history):
678
other_revision = old_revision_history[-1]
680
other_revision = None
681
repository = self.branch.repository
682
merge_inner(self.branch,
683
self.branch.basis_tree(),
684
repository.revision_tree(other_revision))
689
197
def extras(self):
690
198
"""Yield all unknown files in this WorkingTree.
780
def kind(self, file_id):
781
return file_kind(self.id2abspath(file_id))
784
"""See Branch.lock_read, and WorkingTree.unlock."""
785
return self.branch.lock_read()
787
def lock_write(self):
788
"""See Branch.lock_write, and WorkingTree.unlock."""
789
return self.branch.lock_write()
791
def _basis_inventory_name(self, revision_id):
792
return 'basis-inventory.%s' % revision_id
794
def set_last_revision(self, new_revision, old_revision=None):
795
if old_revision is not None:
797
path = self._basis_inventory_name(old_revision)
798
path = self.branch.control_files._escape(path)
799
self.branch.control_files._transport.delete(path)
803
xml = self.branch.repository.get_inventory_xml(new_revision)
804
path = self._basis_inventory_name(new_revision)
805
self.branch.control_files.put_utf8(path, xml)
806
except WeaveRevisionNotPresent:
809
def read_basis_inventory(self, revision_id):
810
"""Read the cached basis inventory."""
811
path = self._basis_inventory_name(revision_id)
812
return self.branch.control_files.controlfile(path, 'r').read()
815
def read_working_inventory(self):
816
"""Read the working inventory."""
817
# ElementTree does its own conversion from UTF-8, so open in
819
f = self.branch.control_files.controlfile('inventory', 'rb')
820
return bzrlib.xml5.serializer_v5.read_inventory(f)
823
def remove(self, files, verbose=False):
824
"""Remove nominated files from the working inventory..
826
This does not remove their text. This does not run on XXX on what? RBC
828
TODO: Refuse to remove modified files unless --force is given?
830
TODO: Do something useful with directories.
832
TODO: Should this remove the text or not? Tough call; not
833
removing may be useful and the user can just use use rm, and
834
is the opposite of add. Removing it is consistent with most
835
other tools. Maybe an option.
837
## TODO: Normalize names
838
## TODO: Remove nested loops; better scalability
839
if isinstance(files, basestring):
844
# do this before any modifications
848
# TODO: Perhaps make this just a warning, and continue?
849
# This tends to happen when
850
raise NotVersionedError(path=f)
851
mutter("remove inventory entry %s {%s}", quotefn(f), fid)
853
# having remove it, it must be either ignored or unknown
854
if self.is_ignored(f):
858
show_status(new_status, inv[fid].kind, quotefn(f))
861
self._write_inventory(inv)
864
def revert(self, filenames, old_tree=None, backups=True):
865
from bzrlib.merge import merge_inner
867
old_tree = self.branch.basis_tree()
868
merge_inner(self.branch, old_tree,
869
self, ignore_zero=True,
870
backup_files=backups,
871
interesting_files=filenames)
872
if not len(filenames):
873
self.set_pending_merges([])
876
def set_inventory(self, new_inventory_list):
877
from bzrlib.inventory import (Inventory,
882
inv = Inventory(self.get_root_id())
883
for path, file_id, parent, kind in new_inventory_list:
884
name = os.path.basename(path)
887
# fixme, there should be a factory function inv,add_??
888
if kind == 'directory':
889
inv.add(InventoryDirectory(file_id, name, parent))
891
inv.add(InventoryFile(file_id, name, parent))
892
elif kind == 'symlink':
893
inv.add(InventoryLink(file_id, name, parent))
895
raise BzrError("unknown kind %r" % kind)
896
self._write_inventory(inv)
899
def set_root_id(self, file_id):
900
"""Set the root id for this tree."""
901
inv = self.read_working_inventory()
902
orig_root_id = inv.root.file_id
903
del inv._byid[inv.root.file_id]
904
inv.root.file_id = file_id
905
inv._byid[inv.root.file_id] = inv.root
908
if entry.parent_id in (None, orig_root_id):
909
entry.parent_id = inv.root.file_id
910
self._write_inventory(inv)
913
"""See Branch.unlock.
915
WorkingTree locking just uses the Branch locking facilities.
916
This is current because all working trees have an embedded branch
917
within them. IF in the future, we were to make branch data shareable
918
between multiple working trees, i.e. via shared storage, then we
919
would probably want to lock both the local tree, and the branch.
921
if self._hashcache.needs_write:
922
self._hashcache.write()
923
return self.branch.unlock()
926
def _write_inventory(self, inv):
927
"""Write inventory as the current inventory."""
928
from cStringIO import StringIO
929
from bzrlib.atomicfile import AtomicFile
931
bzrlib.xml5.serializer_v5.write_inventory(inv, sio)
933
f = AtomicFile(self.branch.control_files.controlfilename('inventory'))
939
self._set_inventory(inv)
940
mutter('wrote working inventory')
943
CONFLICT_SUFFIXES = ('.THIS', '.BASE', '.OTHER')
944
def get_conflicted_stem(path):
945
for suffix in CONFLICT_SUFFIXES:
946
if path.endswith(suffix):
947
return path[:-len(suffix)]