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 Branch.working_tree():
32
# TODO: Don't allow WorkingTrees to be constructed for remote branches if
35
# 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
50
from bzrlib.branch import (Branch,
55
from bzrlib.errors import (BzrCheckError,
58
WeaveRevisionNotPresent,
61
from bzrlib.inventory import InventoryEntry
62
from bzrlib.osutils import (appendpath,
77
from bzrlib.trace import mutter
81
def gen_file_id(name):
82
"""Return new file id.
84
This should probably generate proper UUIDs, but for the moment we
85
cope with just randomness because running uuidgen every time is
88
from binascii import hexlify
95
idx = name.rfind('\\')
99
# make it not a hidden file
100
name = name.lstrip('.')
102
# remove any wierd characters; we don't escape them but rather
104
name = re.sub(r'[^\w.]', '', name)
106
s = hexlify(rand_bytes(8))
107
return '-'.join((name, compact_date(time()), s))
111
"""Return a new tree-root file id."""
112
return gen_file_id('TREE_ROOT')
115
class TreeEntry(object):
116
"""An entry that implements the minium interface used by commands.
118
This needs further inspection, it may be better to have
119
InventoryEntries without ids - though that seems wrong. For now,
120
this is a parallel hierarchy to InventoryEntry, and needs to become
121
one of several things: decorates to that hierarchy, children of, or
123
Another note is that these objects are currently only used when there is
124
no InventoryEntry available - i.e. for unversioned objects.
125
Perhaps they should be UnversionedEntry et al. ? - RBC 20051003
128
def __eq__(self, other):
129
# yes, this us ugly, TODO: best practice __eq__ style.
130
return (isinstance(other, TreeEntry)
131
and other.__class__ == self.__class__)
133
def kind_character(self):
137
class TreeDirectory(TreeEntry):
138
"""See TreeEntry. This is a directory in a working tree."""
140
def __eq__(self, other):
141
return (isinstance(other, TreeDirectory)
142
and other.__class__ == self.__class__)
144
def kind_character(self):
148
class TreeFile(TreeEntry):
149
"""See TreeEntry. This is a regular file in a working tree."""
151
def __eq__(self, other):
152
return (isinstance(other, TreeFile)
153
and other.__class__ == self.__class__)
155
def kind_character(self):
159
class TreeLink(TreeEntry):
160
"""See TreeEntry. This is a symlink in a working tree."""
162
def __eq__(self, other):
163
return (isinstance(other, TreeLink)
164
and other.__class__ == self.__class__)
166
def kind_character(self):
170
class WorkingTree(bzrlib.tree.Tree):
171
"""Working copy tree.
173
The inventory is held in the `Branch` working-inventory, and the
174
files are in a directory on disk.
176
It is possible for a `WorkingTree` to have a filename which is
177
not listed in the Inventory and vice versa.
180
def __init__(self, basedir=u'.', branch=None):
181
"""Construct a WorkingTree for basedir.
183
If the branch is not supplied, it is opened automatically.
184
If the branch is supplied, it must be the branch for this basedir.
185
(branch.base is not cross checked, because for remote branches that
186
would be meaningless).
188
from bzrlib.hashcache import HashCache
189
from bzrlib.trace import note, mutter
190
assert isinstance(basedir, basestring), \
191
"base directory %r is not a string" % basedir
193
branch = Branch.open(basedir)
194
assert isinstance(branch, Branch), \
195
"branch %r is not a Branch" % branch
197
self.basedir = realpath(basedir)
199
self._set_inventory(self.read_working_inventory())
201
# update the whole cache up front and write to disk if anything changed;
202
# 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
hc = self._hashcache = HashCache(basedir)
215
def _set_inventory(self, inv):
216
self._inventory = inv
217
self.path2id = self._inventory.path2id
220
def open_containing(path=None):
221
"""Open an existing working tree which has its root about path.
223
This probes for a working tree at path and searches upwards from there.
225
Basically we keep looking up until we find the control directory or
226
run into /. If there isn't one, raises NotBranchError.
227
TODO: give this a new exception.
228
If there is one, it is returned, along with the unused portion of path.
234
if path.find('://') != -1:
235
raise NotBranchError(path=path)
240
return WorkingTree(path), tail
241
except NotBranchError:
244
tail = pathjoin(os.path.basename(path), tail)
246
tail = os.path.basename(path)
248
path = os.path.dirname(path)
250
# reached the root, whatever that may be
251
raise NotBranchError(path=path)
254
"""Iterate through file_ids for this tree.
256
file_ids are in a WorkingTree if they are in the working inventory
257
and the working file exists.
259
inv = self._inventory
260
for path, ie in inv.iter_entries():
261
if bzrlib.osutils.lexists(self.abspath(path)):
265
return "<%s of %s>" % (self.__class__.__name__,
266
getattr(self, 'basedir', None))
268
def abspath(self, filename):
269
return pathjoin(self.basedir, filename)
271
def relpath(self, abs):
272
"""Return the local path portion from a given absolute path."""
273
return relpath(self.basedir, abs)
275
def has_filename(self, filename):
276
return bzrlib.osutils.lexists(self.abspath(filename))
278
def get_file(self, file_id):
279
return self.get_file_byname(self.id2path(file_id))
281
def get_file_byname(self, filename):
282
return file(self.abspath(filename), 'rb')
284
def get_root_id(self):
285
"""Return the id of this trees root"""
286
inv = self.read_working_inventory()
287
return inv.root.file_id
289
def _get_store_filename(self, file_id):
290
## XXX: badly named; this is not in the store at all
291
return self.abspath(self.id2path(file_id))
294
def commit(self, *args, **kw):
295
from bzrlib.commit import Commit
296
Commit().commit(self.branch, *args, **kw)
297
self._set_inventory(self.read_working_inventory())
299
def id2abspath(self, file_id):
300
return self.abspath(self.id2path(file_id))
302
def has_id(self, file_id):
303
# files that have been deleted are excluded
304
inv = self._inventory
305
if not inv.has_id(file_id):
307
path = inv.id2path(file_id)
308
return bzrlib.osutils.lexists(self.abspath(path))
310
def has_or_had_id(self, file_id):
311
if file_id == self.inventory.root.file_id:
313
return self.inventory.has_id(file_id)
315
__contains__ = has_id
317
def get_file_size(self, file_id):
318
return os.path.getsize(self.id2abspath(file_id))
320
def get_file_sha1(self, file_id):
321
path = self._inventory.id2path(file_id)
322
return self._hashcache.get_sha1(path)
324
def is_executable(self, file_id):
326
return self._inventory[file_id].executable
328
path = self._inventory.id2path(file_id)
329
mode = os.lstat(self.abspath(path)).st_mode
330
return bool(stat.S_ISREG(mode) and stat.S_IEXEC&mode)
333
def add(self, files, ids=None):
334
"""Make files versioned.
336
Note that the command line normally calls smart_add instead,
337
which can automatically recurse.
339
This adds the files to the inventory, so that they will be
340
recorded by the next commit.
343
List of paths to add, relative to the base of the tree.
346
If set, use these instead of automatically generated ids.
347
Must be the same length as the list of files, but may
348
contain None for ids that are to be autogenerated.
350
TODO: Perhaps have an option to add the ids even if the files do
353
TODO: Perhaps callback with the ids and paths as they're added.
355
# TODO: Re-adding a file that is removed in the working copy
356
# should probably put it back with the previous ID.
357
if isinstance(files, basestring):
358
assert(ids is None or isinstance(ids, basestring))
364
ids = [None] * len(files)
366
assert(len(ids) == len(files))
368
inv = self.read_working_inventory()
369
for f,file_id in zip(files, ids):
370
if is_control_file(f):
371
raise BzrError("cannot add control file %s" % quotefn(f))
376
raise BzrError("cannot add top-level %r" % f)
378
fullpath = normpath(self.abspath(f))
381
kind = file_kind(fullpath)
383
# maybe something better?
384
raise BzrError('cannot add: not a regular file, symlink or directory: %s' % quotefn(f))
386
if not InventoryEntry.versionable_kind(kind):
387
raise BzrError('cannot add: not a versionable file ('
388
'i.e. regular file, symlink or directory): %s' % quotefn(f))
391
file_id = gen_file_id(f)
392
inv.add_path(f, kind=kind, file_id=file_id)
394
mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
395
self._write_inventory(inv)
398
def add_pending_merge(self, *revision_ids):
399
# TODO: Perhaps should check at this point that the
400
# history of the revision is actually present?
401
p = self.pending_merges()
403
for rev_id in revision_ids:
409
self.set_pending_merges(p)
411
def pending_merges(self):
412
"""Return a list of pending merges.
414
These are revisions that have been merged into the working
415
directory but not yet committed.
417
cfn = self.branch._rel_controlfilename('pending-merges')
418
if not self.branch._transport.has(cfn):
421
for l in self.branch.controlfile('pending-merges', 'r').readlines():
422
p.append(l.rstrip('\n'))
426
def set_pending_merges(self, rev_list):
427
self.branch.put_controlfile('pending-merges', '\n'.join(rev_list))
429
def get_symlink_target(self, file_id):
430
return os.readlink(self.id2abspath(file_id))
432
def file_class(self, filename):
433
if self.path2id(filename):
435
elif self.is_ignored(filename):
441
def list_files(self):
442
"""Recursively list all files as (path, class, kind, id).
444
Lists, but does not descend into unversioned directories.
446
This does not include files that have been deleted in this
449
Skips the control directory.
451
inv = self._inventory
453
def descend(from_dir_relpath, from_dir_id, dp):
457
## TODO: If we find a subdirectory with its own .bzr
458
## directory, then that is a separate tree and we
459
## should exclude it.
460
if bzrlib.BZRDIR == f:
464
fp = appendpath(from_dir_relpath, f)
467
fap = appendpath(dp, f)
469
f_ie = inv.get_child(from_dir_id, f)
472
elif self.is_ignored(fp):
481
raise BzrCheckError("file %r entered as kind %r id %r, "
483
% (fap, f_ie.kind, f_ie.file_id, fk))
485
# make a last minute entry
489
if fk == 'directory':
490
entry = TreeDirectory()
493
elif fk == 'symlink':
498
yield fp, c, fk, (f_ie and f_ie.file_id), entry
500
if fk != 'directory':
504
# don't descend unversioned directories
507
for ff in descend(fp, f_ie.file_id, fap):
510
for f in descend(u'', inv.root.file_id, self.basedir):
514
def move(self, from_paths, to_name):
517
to_name must exist in the inventory.
519
If to_name exists and is a directory, the files are moved into
520
it, keeping their old names.
522
Note that to_name is only the last component of the new name;
523
this doesn't change the directory.
525
This returns a list of (from_path, to_path) pairs for each
529
## TODO: Option to move IDs only
530
assert not isinstance(from_paths, basestring)
532
to_abs = self.abspath(to_name)
533
if not isdir(to_abs):
534
raise BzrError("destination %r is not a directory" % to_abs)
535
if not self.has_filename(to_name):
536
raise BzrError("destination %r not in working directory" % to_abs)
537
to_dir_id = inv.path2id(to_name)
538
if to_dir_id == None and to_name != '':
539
raise BzrError("destination %r is not a versioned directory" % to_name)
540
to_dir_ie = inv[to_dir_id]
541
if to_dir_ie.kind not in ('directory', 'root_directory'):
542
raise BzrError("destination %r is not a directory" % to_abs)
544
to_idpath = inv.get_idpath(to_dir_id)
547
if not self.has_filename(f):
548
raise BzrError("%r does not exist in working tree" % f)
549
f_id = inv.path2id(f)
551
raise BzrError("%r is not versioned" % f)
552
name_tail = splitpath(f)[-1]
553
dest_path = appendpath(to_name, name_tail)
554
if self.has_filename(dest_path):
555
raise BzrError("destination %r already exists" % dest_path)
556
if f_id in to_idpath:
557
raise BzrError("can't move %r to a subdirectory of itself" % f)
559
# OK, so there's a race here, it's possible that someone will
560
# create a file in this interval and then the rename might be
561
# left half-done. But we should have caught most problems.
562
orig_inv = deepcopy(self.inventory)
565
name_tail = splitpath(f)[-1]
566
dest_path = appendpath(to_name, name_tail)
567
result.append((f, dest_path))
568
inv.rename(inv.path2id(f), to_dir_id, name_tail)
570
rename(self.abspath(f), self.abspath(dest_path))
572
raise BzrError("failed to rename %r to %r: %s" %
573
(f, dest_path, e[1]),
574
["rename rolled back"])
576
# restore the inventory on error
577
self._set_inventory(orig_inv)
579
self._write_inventory(inv)
583
def rename_one(self, from_rel, to_rel):
586
This can change the directory or the filename or both.
589
if not self.has_filename(from_rel):
590
raise BzrError("can't rename: old working file %r does not exist" % from_rel)
591
if self.has_filename(to_rel):
592
raise BzrError("can't rename: new working file %r already exists" % to_rel)
594
file_id = inv.path2id(from_rel)
596
raise BzrError("can't rename: old name %r is not versioned" % from_rel)
599
from_parent = entry.parent_id
600
from_name = entry.name
602
if inv.path2id(to_rel):
603
raise BzrError("can't rename: new name %r is already versioned" % to_rel)
605
to_dir, to_tail = os.path.split(to_rel)
606
to_dir_id = inv.path2id(to_dir)
607
if to_dir_id == None and to_dir != '':
608
raise BzrError("can't determine destination directory id for %r" % to_dir)
610
mutter("rename_one:")
611
mutter(" file_id {%s}" % file_id)
612
mutter(" from_rel %r" % from_rel)
613
mutter(" to_rel %r" % to_rel)
614
mutter(" to_dir %r" % to_dir)
615
mutter(" to_dir_id {%s}" % to_dir_id)
617
inv.rename(file_id, to_dir_id, to_tail)
619
from_abs = self.abspath(from_rel)
620
to_abs = self.abspath(to_rel)
622
rename(from_abs, to_abs)
624
inv.rename(file_id, from_parent, from_name)
625
raise BzrError("failed to rename %r to %r: %s"
626
% (from_abs, to_abs, e[1]),
627
["rename rolled back"])
628
self._write_inventory(inv)
632
"""Return all unknown files.
634
These are files in the working directory that are not versioned or
635
control files or ignored.
637
>>> from bzrlib.branch import ScratchBranch
638
>>> b = ScratchBranch(files=['foo', 'foo~'])
639
>>> tree = WorkingTree(b.base, b)
640
>>> map(str, tree.unknowns())
643
>>> list(b.unknowns())
645
>>> tree.remove('foo')
646
>>> list(b.unknowns())
649
for subp in self.extras():
650
if not self.is_ignored(subp):
653
def iter_conflicts(self):
655
for path in (s[0] for s in self.list_files()):
656
stem = get_conflicted_stem(path)
659
if stem not in conflicted:
664
def pull(self, source, overwrite=False):
665
from bzrlib.merge import merge_inner
668
old_revision_history = self.branch.revision_history()
669
count = self.branch.pull(source, overwrite)
670
new_revision_history = self.branch.revision_history()
671
if new_revision_history != old_revision_history:
672
if len(old_revision_history):
673
other_revision = old_revision_history[-1]
675
other_revision = None
676
merge_inner(self.branch,
677
self.branch.basis_tree(),
678
self.branch.revision_tree(other_revision))
684
"""Yield all unknown files in this WorkingTree.
686
If there are any unknown directories then only the directory is
687
returned, not all its children. But if there are unknown files
688
under a versioned subdirectory, they are returned.
690
Currently returned depth-first, sorted by name within directories.
692
## TODO: Work from given directory downwards
693
for path, dir_entry in self.inventory.directories():
694
mutter("search for unknowns in %r", path)
695
dirabs = self.abspath(path)
696
if not isdir(dirabs):
697
# e.g. directory deleted
701
for subf in os.listdir(dirabs):
703
and (subf not in dir_entry.children)):
708
subp = appendpath(path, subf)
712
def ignored_files(self):
713
"""Yield list of PATH, IGNORE_PATTERN"""
714
for subp in self.extras():
715
pat = self.is_ignored(subp)
720
def get_ignore_list(self):
721
"""Return list of ignore patterns.
723
Cached in the Tree object after the first call.
725
if hasattr(self, '_ignorelist'):
726
return self._ignorelist
728
l = bzrlib.DEFAULT_IGNORE[:]
729
if self.has_filename(bzrlib.IGNORE_FILENAME):
730
f = self.get_file_byname(bzrlib.IGNORE_FILENAME)
731
l.extend([line.rstrip("\n\r") for line in f.readlines()])
736
def is_ignored(self, filename):
737
r"""Check whether the filename matches an ignore pattern.
739
Patterns containing '/' or '\' need to match the whole path;
740
others match against only the last component.
742
If the file is ignored, returns the pattern which caused it to
743
be ignored, otherwise None. So this can simply be used as a
744
boolean if desired."""
746
# TODO: Use '**' to match directories, and other extended
747
# globbing stuff from cvs/rsync.
749
# XXX: fnmatch is actually not quite what we want: it's only
750
# approximately the same as real Unix fnmatch, and doesn't
751
# treat dotfiles correctly and allows * to match /.
752
# Eventually it should be replaced with something more
755
for pat in self.get_ignore_list():
756
if '/' in pat or '\\' in pat:
758
# as a special case, you can put ./ at the start of a
759
# pattern; this is good to match in the top-level
762
if (pat[:2] == './') or (pat[:2] == '.\\'):
766
if fnmatch.fnmatchcase(filename, newpat):
769
if fnmatch.fnmatchcase(splitpath(filename)[-1], pat):
774
def kind(self, file_id):
775
return file_kind(self.id2abspath(file_id))
778
"""See Branch.lock_read, and WorkingTree.unlock."""
779
return self.branch.lock_read()
781
def lock_write(self):
782
"""See Branch.lock_write, and WorkingTree.unlock."""
783
return self.branch.lock_write()
785
def _basis_inventory_name(self, revision_id):
786
return 'basis-inventory.%s' % revision_id
788
def set_last_revision(self, new_revision, old_revision=None):
791
path = self._basis_inventory_name(old_revision)
792
path = self.branch._rel_controlfilename(path)
793
self.branch._transport.delete(path)
797
xml = self.branch.get_inventory_xml(new_revision)
798
path = self._basis_inventory_name(new_revision)
799
self.branch.put_controlfile(path, xml)
800
except WeaveRevisionNotPresent:
803
def read_basis_inventory(self, revision_id):
804
"""Read the cached basis inventory."""
805
path = self._basis_inventory_name(revision_id)
806
return self.branch.controlfile(path, 'r').read()
809
def read_working_inventory(self):
810
"""Read the working inventory."""
811
# ElementTree does its own conversion from UTF-8, so open in
813
f = self.branch.controlfile('inventory', 'rb')
814
return bzrlib.xml5.serializer_v5.read_inventory(f)
817
def remove(self, files, verbose=False):
818
"""Remove nominated files from the working inventory..
820
This does not remove their text. This does not run on XXX on what? RBC
822
TODO: Refuse to remove modified files unless --force is given?
824
TODO: Do something useful with directories.
826
TODO: Should this remove the text or not? Tough call; not
827
removing may be useful and the user can just use use rm, and
828
is the opposite of add. Removing it is consistent with most
829
other tools. Maybe an option.
831
## TODO: Normalize names
832
## TODO: Remove nested loops; better scalability
833
if isinstance(files, basestring):
838
# do this before any modifications
842
# TODO: Perhaps make this just a warning, and continue?
843
# This tends to happen when
844
raise NotVersionedError(path=f)
845
mutter("remove inventory entry %s {%s}", quotefn(f), fid)
847
# having remove it, it must be either ignored or unknown
848
if self.is_ignored(f):
852
show_status(new_status, inv[fid].kind, quotefn(f))
855
self._write_inventory(inv)
858
def revert(self, filenames, old_tree=None, backups=True):
859
from bzrlib.merge import merge_inner
861
old_tree = self.branch.basis_tree()
862
merge_inner(self.branch, old_tree,
863
self, ignore_zero=True,
864
backup_files=backups,
865
interesting_files=filenames)
866
if not len(filenames):
867
self.set_pending_merges([])
870
def set_inventory(self, new_inventory_list):
871
from bzrlib.inventory import (Inventory,
876
inv = Inventory(self.get_root_id())
877
for path, file_id, parent, kind in new_inventory_list:
878
name = os.path.basename(path)
881
# fixme, there should be a factory function inv,add_??
882
if kind == 'directory':
883
inv.add(InventoryDirectory(file_id, name, parent))
885
inv.add(InventoryFile(file_id, name, parent))
886
elif kind == 'symlink':
887
inv.add(InventoryLink(file_id, name, parent))
889
raise BzrError("unknown kind %r" % kind)
890
self._write_inventory(inv)
893
def set_root_id(self, file_id):
894
"""Set the root id for this tree."""
895
inv = self.read_working_inventory()
896
orig_root_id = inv.root.file_id
897
del inv._byid[inv.root.file_id]
898
inv.root.file_id = file_id
899
inv._byid[inv.root.file_id] = inv.root
902
if entry.parent_id in (None, orig_root_id):
903
entry.parent_id = inv.root.file_id
904
self._write_inventory(inv)
907
"""See Branch.unlock.
909
WorkingTree locking just uses the Branch locking facilities.
910
This is current because all working trees have an embedded branch
911
within them. IF in the future, we were to make branch data shareable
912
between multiple working trees, i.e. via shared storage, then we
913
would probably want to lock both the local tree, and the branch.
915
return self.branch.unlock()
918
def _write_inventory(self, inv):
919
"""Write inventory as the current inventory."""
920
from cStringIO import StringIO
921
from bzrlib.atomicfile import AtomicFile
923
bzrlib.xml5.serializer_v5.write_inventory(inv, sio)
925
f = AtomicFile(self.branch.controlfilename('inventory'))
931
self._set_inventory(inv)
932
mutter('wrote working inventory')
935
CONFLICT_SUFFIXES = ('.THIS', '.BASE', '.OTHER')
936
def get_conflicted_stem(path):
937
for suffix in CONFLICT_SUFFIXES:
938
if path.endswith(suffix):
939
return path[:-len(suffix)]