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
# TODO: Don't allow WorkingTrees to be constructed for remote branches.
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
19
35
# FIXME: I don't know if writing out the cache from the destructor is really a
20
# good idea, because destructors are considered poor taste in Python, and
21
# it's not predictable when it will be written out.
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,
76
from bzrlib.textui import show_status
26
from errors import BzrCheckError
27
from trace import mutter
78
from bzrlib.trace import mutter
82
def gen_file_id(name):
83
"""Return new file id.
85
This should probably generate proper UUIDs, but for the moment we
86
cope with just randomness because running uuidgen every time is
89
from binascii import hexlify
96
idx = name.rfind('\\')
100
# make it not a hidden file
101
name = name.lstrip('.')
103
# remove any wierd characters; we don't escape them but rather
105
name = re.sub(r'[^\w.]', '', name)
107
s = hexlify(rand_bytes(8))
108
return '-'.join((name, compact_date(time()), s))
112
"""Return a new tree-root file id."""
113
return gen_file_id('TREE_ROOT')
116
class TreeEntry(object):
117
"""An entry that implements the minium interface used by commands.
119
This needs further inspection, it may be better to have
120
InventoryEntries without ids - though that seems wrong. For now,
121
this is a parallel hierarchy to InventoryEntry, and needs to become
122
one of several things: decorates to that hierarchy, children of, or
124
Another note is that these objects are currently only used when there is
125
no InventoryEntry available - i.e. for unversioned objects.
126
Perhaps they should be UnversionedEntry et al. ? - RBC 20051003
129
def __eq__(self, other):
130
# yes, this us ugly, TODO: best practice __eq__ style.
131
return (isinstance(other, TreeEntry)
132
and other.__class__ == self.__class__)
134
def kind_character(self):
138
class TreeDirectory(TreeEntry):
139
"""See TreeEntry. This is a directory in a working tree."""
141
def __eq__(self, other):
142
return (isinstance(other, TreeDirectory)
143
and other.__class__ == self.__class__)
145
def kind_character(self):
149
class TreeFile(TreeEntry):
150
"""See TreeEntry. This is a regular file in a working tree."""
152
def __eq__(self, other):
153
return (isinstance(other, TreeFile)
154
and other.__class__ == self.__class__)
156
def kind_character(self):
160
class TreeLink(TreeEntry):
161
"""See TreeEntry. This is a symlink in a working tree."""
163
def __eq__(self, other):
164
return (isinstance(other, TreeLink)
165
and other.__class__ == self.__class__)
167
def kind_character(self):
29
171
class WorkingTree(bzrlib.tree.Tree):
30
172
"""Working copy tree.
35
177
It is possible for a `WorkingTree` to have a filename which is
36
178
not listed in the Inventory and vice versa.
38
def __init__(self, basedir, inv):
181
def __init__(self, basedir=u'.', branch=None):
182
"""Construct a WorkingTree for basedir.
184
If the branch is not supplied, it is opened automatically.
185
If the branch is supplied, it must be the branch for this basedir.
186
(branch.base is not cross checked, because for remote branches that
187
would be meaningless).
39
189
from bzrlib.hashcache import HashCache
40
190
from bzrlib.trace import note, mutter
43
self.basedir = basedir
44
self.path2id = inv.path2id
191
assert isinstance(basedir, basestring), \
192
"base directory %r is not a string" % basedir
194
branch = Branch.open(basedir)
195
assert isinstance(branch, Branch), \
196
"branch %r is not a Branch" % branch
198
self.basedir = realpath(basedir)
46
200
# update the whole cache up front and write to disk if anything changed;
47
201
# in the future we might want to do this more selectively
202
# two possible ways offer themselves : in self._unlock, write the cache
203
# if needed, or, when the cache sees a change, append it to the hash
204
# cache file, and have the parser take the most recent entry for a
48
206
hc = self._hashcache = HashCache(basedir)
52
210
if hc.needs_write:
53
211
mutter("write hc")
58
if self._hashcache.needs_write:
59
self._hashcache.write()
214
self._set_inventory(self.read_working_inventory())
216
def _set_inventory(self, inv):
217
self._inventory = inv
218
self.path2id = self._inventory.path2id
221
def open_containing(path=None):
222
"""Open an existing working tree which has its root about path.
224
This probes for a working tree at path and searches upwards from there.
226
Basically we keep looking up until we find the control directory or
227
run into /. If there isn't one, raises NotBranchError.
228
TODO: give this a new exception.
229
If there is one, it is returned, along with the unused portion of path.
235
if path.find('://') != -1:
236
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=orig_path)
62
255
def __iter__(self):
63
256
"""Iterate through file_ids for this tree.
68
261
inv = self._inventory
69
262
for path, ie in inv.iter_entries():
70
if os.path.exists(self.abspath(path)):
263
if bzrlib.osutils.lexists(self.abspath(path)):
74
266
def __repr__(self):
75
267
return "<%s of %s>" % (self.__class__.__name__,
76
268
getattr(self, 'basedir', None))
80
270
def abspath(self, filename):
81
return os.path.join(self.basedir, 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
277
def has_filename(self, filename):
84
return os.path.exists(self.abspath(filename))
278
return bzrlib.osutils.lexists(self.abspath(filename))
86
280
def get_file(self, file_id):
87
281
return self.get_file_byname(self.id2path(file_id))
89
283
def get_file_byname(self, filename):
90
284
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
92
291
def _get_store_filename(self, file_id):
93
## XXX: badly named; this isn't in the store at all
94
return self.abspath(self.id2path(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))
97
304
def has_id(self, file_id):
98
305
# files that have been deleted are excluded
99
306
inv = self._inventory
100
307
if not inv.has_id(file_id):
102
309
path = inv.id2path(file_id)
103
return os.path.exists(self.abspath(path))
310
return bzrlib.osutils.lexists(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)
106
317
__contains__ = has_id
109
319
def get_file_size(self, file_id):
110
# is this still called?
111
raise NotImplementedError()
320
return os.path.getsize(self.id2abspath(file_id))
114
323
def get_file_sha1(self, file_id):
115
324
path = self._inventory.id2path(file_id)
116
325
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)
414
def pending_merges(self):
415
"""Return a list of pending merges.
417
These are revisions that have been merged into the working
418
directory but not yet committed.
420
cfn = self.branch._rel_controlfilename('pending-merges')
421
if not self.branch._transport.has(cfn):
424
for l in self.branch.controlfile('pending-merges', 'r').readlines():
425
p.append(l.rstrip('\n'))
429
def set_pending_merges(self, rev_list):
430
self.branch.put_controlfile('pending-merges', '\n'.join(rev_list))
432
def get_symlink_target(self, file_id):
433
return os.readlink(self.id2abspath(file_id))
119
435
def file_class(self, filename):
120
436
if self.path2id(filename):
184
510
for ff in descend(fp, f_ie.file_id, fap):
187
for f in descend('', inv.root.file_id, self.basedir):
513
for f in descend(u'', inv.root.file_id, self.basedir):
517
def move(self, from_paths, to_name):
520
to_name must exist in the inventory.
522
If to_name exists and is a directory, the files are moved into
523
it, keeping their old names.
525
Note that to_name is only the last component of the new name;
526
this doesn't change the directory.
528
This returns a list of (from_path, to_path) pairs for each
532
## TODO: Option to move IDs only
533
assert not isinstance(from_paths, basestring)
535
to_abs = self.abspath(to_name)
536
if not isdir(to_abs):
537
raise BzrError("destination %r is not a directory" % to_abs)
538
if not self.has_filename(to_name):
539
raise BzrError("destination %r not in working directory" % to_abs)
540
to_dir_id = inv.path2id(to_name)
541
if to_dir_id == None and to_name != '':
542
raise BzrError("destination %r is not a versioned directory" % to_name)
543
to_dir_ie = inv[to_dir_id]
544
if to_dir_ie.kind not in ('directory', 'root_directory'):
545
raise BzrError("destination %r is not a directory" % to_abs)
547
to_idpath = inv.get_idpath(to_dir_id)
550
if not self.has_filename(f):
551
raise BzrError("%r does not exist in working tree" % f)
552
f_id = inv.path2id(f)
554
raise BzrError("%r is not versioned" % f)
555
name_tail = splitpath(f)[-1]
556
dest_path = appendpath(to_name, name_tail)
557
if self.has_filename(dest_path):
558
raise BzrError("destination %r already exists" % dest_path)
559
if f_id in to_idpath:
560
raise BzrError("can't move %r to a subdirectory of itself" % f)
562
# OK, so there's a race here, it's possible that someone will
563
# create a file in this interval and then the rename might be
564
# left half-done. But we should have caught most problems.
565
orig_inv = deepcopy(self.inventory)
568
name_tail = splitpath(f)[-1]
569
dest_path = appendpath(to_name, name_tail)
570
result.append((f, dest_path))
571
inv.rename(inv.path2id(f), to_dir_id, name_tail)
573
rename(self.abspath(f), self.abspath(dest_path))
575
raise BzrError("failed to rename %r to %r: %s" %
576
(f, dest_path, e[1]),
577
["rename rolled back"])
579
# restore the inventory on error
580
self._set_inventory(orig_inv)
582
self._write_inventory(inv)
586
def rename_one(self, from_rel, to_rel):
589
This can change the directory or the filename or both.
592
if not self.has_filename(from_rel):
593
raise BzrError("can't rename: old working file %r does not exist" % from_rel)
594
if self.has_filename(to_rel):
595
raise BzrError("can't rename: new working file %r already exists" % to_rel)
597
file_id = inv.path2id(from_rel)
599
raise BzrError("can't rename: old name %r is not versioned" % from_rel)
602
from_parent = entry.parent_id
603
from_name = entry.name
605
if inv.path2id(to_rel):
606
raise BzrError("can't rename: new name %r is already versioned" % to_rel)
608
to_dir, to_tail = os.path.split(to_rel)
609
to_dir_id = inv.path2id(to_dir)
610
if to_dir_id == None and to_dir != '':
611
raise BzrError("can't determine destination directory id for %r" % to_dir)
613
mutter("rename_one:")
614
mutter(" file_id {%s}" % file_id)
615
mutter(" from_rel %r" % from_rel)
616
mutter(" to_rel %r" % to_rel)
617
mutter(" to_dir %r" % to_dir)
618
mutter(" to_dir_id {%s}" % to_dir_id)
620
inv.rename(file_id, to_dir_id, to_tail)
622
from_abs = self.abspath(from_rel)
623
to_abs = self.abspath(to_rel)
625
rename(from_abs, to_abs)
627
inv.rename(file_id, from_parent, from_name)
628
raise BzrError("failed to rename %r to %r: %s"
629
% (from_abs, to_abs, e[1]),
630
["rename rolled back"])
631
self._write_inventory(inv)
192
634
def unknowns(self):
635
"""Return all unknown files.
637
These are files in the working directory that are not versioned or
638
control files or ignored.
640
>>> from bzrlib.branch import ScratchBranch
641
>>> b = ScratchBranch(files=['foo', 'foo~'])
642
>>> tree = WorkingTree(b.base, b)
643
>>> map(str, tree.unknowns())
646
>>> list(b.unknowns())
648
>>> tree.remove('foo')
649
>>> list(b.unknowns())
193
652
for subp in self.extras():
194
653
if not self.is_ignored(subp):
656
def iter_conflicts(self):
658
for path in (s[0] for s in self.list_files()):
659
stem = get_conflicted_stem(path)
662
if stem not in conflicted:
667
def pull(self, source, overwrite=False):
668
from bzrlib.merge import merge_inner
671
old_revision_history = self.branch.revision_history()
672
count = self.branch.pull(source, overwrite)
673
new_revision_history = self.branch.revision_history()
674
if new_revision_history != old_revision_history:
675
if len(old_revision_history):
676
other_revision = old_revision_history[-1]
678
other_revision = None
679
merge_inner(self.branch,
680
self.branch.basis_tree(),
681
self.branch.revision_tree(other_revision))
198
686
def extras(self):
199
687
"""Yield all unknown files in this WorkingTree.
b'\\ No newline at end of file'
777
def kind(self, file_id):
778
return file_kind(self.id2abspath(file_id))
781
"""See Branch.lock_read, and WorkingTree.unlock."""
782
return self.branch.lock_read()
784
def lock_write(self):
785
"""See Branch.lock_write, and WorkingTree.unlock."""
786
return self.branch.lock_write()
788
def _basis_inventory_name(self, revision_id):
789
return 'basis-inventory.%s' % revision_id
791
def set_last_revision(self, new_revision, old_revision=None):
794
path = self._basis_inventory_name(old_revision)
795
path = self.branch._rel_controlfilename(path)
796
self.branch._transport.delete(path)
800
xml = self.branch.get_inventory_xml(new_revision)
801
path = self._basis_inventory_name(new_revision)
802
self.branch.put_controlfile(path, xml)
803
except WeaveRevisionNotPresent:
806
def read_basis_inventory(self, revision_id):
807
"""Read the cached basis inventory."""
808
path = self._basis_inventory_name(revision_id)
809
return self.branch.controlfile(path, 'r').read()
812
def read_working_inventory(self):
813
"""Read the working inventory."""
814
# ElementTree does its own conversion from UTF-8, so open in
816
f = self.branch.controlfile('inventory', 'rb')
817
return bzrlib.xml5.serializer_v5.read_inventory(f)
820
def remove(self, files, verbose=False):
821
"""Remove nominated files from the working inventory..
823
This does not remove their text. This does not run on XXX on what? RBC
825
TODO: Refuse to remove modified files unless --force is given?
827
TODO: Do something useful with directories.
829
TODO: Should this remove the text or not? Tough call; not
830
removing may be useful and the user can just use use rm, and
831
is the opposite of add. Removing it is consistent with most
832
other tools. Maybe an option.
834
## TODO: Normalize names
835
## TODO: Remove nested loops; better scalability
836
if isinstance(files, basestring):
841
# do this before any modifications
845
# TODO: Perhaps make this just a warning, and continue?
846
# This tends to happen when
847
raise NotVersionedError(path=f)
848
mutter("remove inventory entry %s {%s}", quotefn(f), fid)
850
# having remove it, it must be either ignored or unknown
851
if self.is_ignored(f):
855
show_status(new_status, inv[fid].kind, quotefn(f))
858
self._write_inventory(inv)
861
def revert(self, filenames, old_tree=None, backups=True):
862
from bzrlib.merge import merge_inner
864
old_tree = self.branch.basis_tree()
865
merge_inner(self.branch, old_tree,
866
self, ignore_zero=True,
867
backup_files=backups,
868
interesting_files=filenames)
869
if not len(filenames):
870
self.set_pending_merges([])
873
def set_inventory(self, new_inventory_list):
874
from bzrlib.inventory import (Inventory,
879
inv = Inventory(self.get_root_id())
880
for path, file_id, parent, kind in new_inventory_list:
881
name = os.path.basename(path)
884
# fixme, there should be a factory function inv,add_??
885
if kind == 'directory':
886
inv.add(InventoryDirectory(file_id, name, parent))
888
inv.add(InventoryFile(file_id, name, parent))
889
elif kind == 'symlink':
890
inv.add(InventoryLink(file_id, name, parent))
892
raise BzrError("unknown kind %r" % kind)
893
self._write_inventory(inv)
896
def set_root_id(self, file_id):
897
"""Set the root id for this tree."""
898
inv = self.read_working_inventory()
899
orig_root_id = inv.root.file_id
900
del inv._byid[inv.root.file_id]
901
inv.root.file_id = file_id
902
inv._byid[inv.root.file_id] = inv.root
905
if entry.parent_id in (None, orig_root_id):
906
entry.parent_id = inv.root.file_id
907
self._write_inventory(inv)
910
"""See Branch.unlock.
912
WorkingTree locking just uses the Branch locking facilities.
913
This is current because all working trees have an embedded branch
914
within them. IF in the future, we were to make branch data shareable
915
between multiple working trees, i.e. via shared storage, then we
916
would probably want to lock both the local tree, and the branch.
918
if self._hashcache.needs_write and self.branch._lock_count==1:
919
self._hashcache.write()
920
return self.branch.unlock()
923
def _write_inventory(self, inv):
924
"""Write inventory as the current inventory."""
925
from cStringIO import StringIO
926
from bzrlib.atomicfile import AtomicFile
928
bzrlib.xml5.serializer_v5.write_inventory(inv, sio)
930
f = AtomicFile(self.branch.controlfilename('inventory'))
936
self._set_inventory(inv)
937
mutter('wrote working inventory')
940
CONFLICT_SUFFIXES = ('.THIS', '.BASE', '.OTHER')
941
def get_conflicted_stem(path):
942
for suffix in CONFLICT_SUFFIXES:
943
if path.endswith(suffix):
944
return path[:-len(suffix)]