1
# Copyright (C) 2005-2011 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
17
"""InventoryWorkingTree 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 bzrdir.open_workingtree() or
29
WorkingTree.open(dir).
33
from __future__ import absolute_import
35
from bisect import bisect_left
45
# Explicitly import breezy.bzrdir so that the BzrProber
46
# is guaranteed to be registered.
49
from .. import lazy_import
50
lazy_import.lazy_import(globals(), """
53
conflicts as _mod_conflicts,
57
revision as _mod_revision,
62
from breezy.bzr import (
73
from ..lock import _RelockDebugMixin, LogicalLockResult
74
from .inventorytree import InventoryRevisionTree, MutableInventoryTree
75
from ..sixish import (
79
from ..trace import mutter, note
81
FileTimestampUnavailable,
86
from ..workingtree import (
93
MERGE_MODIFIED_HEADER_1 = b"BZR merge-modified list format 1"
94
# TODO: Modifying the conflict objects or their type is currently nearly
95
# impossible as there is no clear relationship between the working tree format
96
# and the conflict list file format.
97
CONFLICT_HEADER_1 = b"BZR conflict list format 1"
100
class InventoryWorkingTree(WorkingTree, MutableInventoryTree):
101
"""Base class for working trees that are inventory-oriented.
103
The inventory is held in the `Branch` working-inventory, and the
104
files are in a directory on disk.
106
It is possible for a `WorkingTree` to have a filename which is
107
not listed in the Inventory and vice versa.
110
def __init__(self, basedir='.',
117
"""Construct a InventoryWorkingTree instance. This is not a public API.
119
:param branch: A branch to override probing for the branch.
121
super(InventoryWorkingTree, self).__init__(basedir=basedir,
122
branch=branch, _transport=_control_files._transport,
123
_internal=_internal, _format=_format, _controldir=_controldir)
125
self._control_files = _control_files
126
self._detect_case_handling()
128
if _inventory is None:
129
# This will be acquired on lock_read() or lock_write()
130
self._inventory_is_modified = False
131
self._inventory = None
133
# the caller of __init__ has provided an inventory,
134
# we assume they know what they are doing - as its only
135
# the Format factory and creation methods that are
136
# permitted to do this.
137
self._set_inventory(_inventory, dirty=False)
139
def _set_inventory(self, inv, dirty):
140
"""Set the internal cached inventory.
142
:param inv: The inventory to set.
143
:param dirty: A boolean indicating whether the inventory is the same
144
logical inventory as whats on disk. If True the inventory is not
145
the same and should be written to disk or data will be lost, if
146
False then the inventory is the same as that on disk and any
147
serialisation would be unneeded overhead.
149
self._inventory = inv
150
self._inventory_is_modified = dirty
152
def _detect_case_handling(self):
153
wt_trans = self.controldir.get_workingtree_transport(None)
155
wt_trans.stat(self._format.case_sensitive_filename)
156
except errors.NoSuchFile:
157
self.case_sensitive = True
159
self.case_sensitive = False
161
self._setup_directory_is_tree_reference()
163
def _serialize(self, inventory, out_file):
164
xml5.serializer_v5.write_inventory(
165
self._inventory, out_file, working=True)
167
def _deserialize(selt, in_file):
168
return xml5.serializer_v5.read_inventory(in_file)
170
def break_lock(self):
171
"""Break a lock if one is present from another instance.
173
Uses the ui factory to ask for confirmation if the lock may be from
176
This will probe the repository for its lock as well.
178
self._control_files.break_lock()
179
self.branch.break_lock()
182
return self._control_files.is_locked()
184
def _must_be_locked(self):
185
if not self.is_locked():
186
raise errors.ObjectNotLocked(self)
189
"""Lock the tree for reading.
191
This also locks the branch, and can be unlocked via self.unlock().
193
:return: A breezy.lock.LogicalLockResult.
195
if not self.is_locked():
197
self.branch.lock_read()
199
self._control_files.lock_read()
200
return LogicalLockResult(self.unlock)
205
def lock_tree_write(self):
206
"""See MutableTree.lock_tree_write, and WorkingTree.unlock.
208
:return: A breezy.lock.LogicalLockResult.
210
if not self.is_locked():
212
self.branch.lock_read()
214
self._control_files.lock_write()
215
return LogicalLockResult(self.unlock)
220
def lock_write(self):
221
"""See MutableTree.lock_write, and WorkingTree.unlock.
223
:return: A breezy.lock.LogicalLockResult.
225
if not self.is_locked():
227
self.branch.lock_write()
229
self._control_files.lock_write()
230
return LogicalLockResult(self.unlock)
235
def get_physical_lock_status(self):
236
return self._control_files.get_physical_lock_status()
238
def _write_inventory(self, inv):
239
"""Write inventory as the current inventory."""
240
with self.lock_tree_write():
241
self._set_inventory(inv, dirty=True)
244
# XXX: This method should be deprecated in favour of taking in a proper
245
# new Inventory object.
246
def set_inventory(self, new_inventory_list):
247
from .inventory import (
252
with self.lock_tree_write():
253
inv = Inventory(self.get_root_id())
254
for path, file_id, parent, kind in new_inventory_list:
255
name = os.path.basename(path)
258
# fixme, there should be a factory function inv,add_??
259
if kind == 'directory':
260
inv.add(InventoryDirectory(file_id, name, parent))
262
inv.add(InventoryFile(file_id, name, parent))
263
elif kind == 'symlink':
264
inv.add(InventoryLink(file_id, name, parent))
266
raise errors.BzrError("unknown kind %r" % kind)
267
self._write_inventory(inv)
269
def _write_basis_inventory(self, xml):
270
"""Write the basis inventory XML to the basis-inventory file"""
271
path = self._basis_inventory_name()
273
self._transport.put_file(path, sio,
274
mode=self.controldir._get_file_mode())
276
def _reset_data(self):
277
"""Reset transient data that cannot be revalidated."""
278
self._inventory_is_modified = False
279
with self._transport.get('inventory') as f:
280
result = self._deserialize(f)
281
self._set_inventory(result, dirty=False)
283
def store_uncommitted(self):
284
"""Store uncommitted changes from the tree in the branch."""
285
with self.lock_write():
286
target_tree = self.basis_tree()
287
from ..shelf import ShelfCreator
288
shelf_creator = ShelfCreator(self, target_tree)
290
if not shelf_creator.shelve_all():
292
self.branch.store_uncommitted(shelf_creator)
293
shelf_creator.transform()
295
shelf_creator.finalize()
296
note('Uncommitted changes stored in branch "%s".', self.branch.nick)
298
def restore_uncommitted(self):
299
"""Restore uncommitted changes from the branch into the tree."""
300
with self.lock_write():
301
unshelver = self.branch.get_unshelver(self)
302
if unshelver is None:
305
merger = unshelver.make_merger()
306
merger.ignore_zero = True
308
self.branch.store_uncommitted(None)
312
def get_shelf_manager(self):
313
"""Return the ShelfManager for this WorkingTree."""
314
from ..shelf import ShelfManager
315
return ShelfManager(self, self._transport)
317
def _set_root_id(self, file_id):
318
"""Set the root id for this tree, in a format specific manner.
320
:param file_id: The file id to assign to the root. It must not be
321
present in the current inventory or an error will occur. It must
322
not be None, but rather a valid file id.
324
inv = self._inventory
325
orig_root_id = inv.root.file_id
326
# TODO: it might be nice to exit early if there was nothing
327
# to do, saving us from trigger a sync on unlock.
328
self._inventory_is_modified = True
329
# we preserve the root inventory entry object, but
330
# unlinkit from the byid index
331
inv.delete(inv.root.file_id)
332
inv.root.file_id = file_id
333
# and link it into the index with the new changed id.
334
inv._byid[inv.root.file_id] = inv.root
335
# and finally update all children to reference the new id.
336
# XXX: this should be safe to just look at the root.children
337
# list, not the WHOLE INVENTORY.
338
for fid in inv.iter_all_ids():
339
entry = inv.get_entry(fid)
340
if entry.parent_id == orig_root_id:
341
entry.parent_id = inv.root.file_id
343
def remove(self, files, verbose=False, to_file=None, keep_files=True,
345
"""Remove nominated files from the working tree metadata.
347
:files: File paths relative to the basedir.
348
:keep_files: If true, the files will also be kept.
349
:force: Delete files and directories, even if they are changed and
350
even if the directories are not empty.
352
if isinstance(files, (str, text_type)):
357
all_files = set() # specified and nested files
358
unknown_nested_files=set()
364
def recurse_directory_to_add_files(directory):
365
# Recurse directory and add all files
366
# so we can check if they have changed.
367
for parent_info, file_infos in self.walkdirs(directory):
368
for relpath, basename, kind, lstat, fileid, kind in file_infos:
369
# Is it versioned or ignored?
370
if self.is_versioned(relpath):
371
# Add nested content for deletion.
372
all_files.add(relpath)
374
# Files which are not versioned
375
# should be treated as unknown.
376
files_to_backup.append(relpath)
378
with self.lock_tree_write():
380
for filename in files:
381
# Get file name into canonical form.
382
abspath = self.abspath(filename)
383
filename = self.relpath(abspath)
384
if len(filename) > 0:
385
all_files.add(filename)
386
recurse_directory_to_add_files(filename)
388
files = list(all_files)
391
return # nothing to do
393
# Sort needed to first handle directory content before the directory
394
files.sort(reverse=True)
396
# Bail out if we are going to delete files we shouldn't
397
if not keep_files and not force:
398
for (file_id, path, content_change, versioned, parent_id, name,
399
kind, executable) in self.iter_changes(self.basis_tree(),
400
include_unchanged=True, require_versioned=False,
401
want_unversioned=True, specific_files=files):
402
if versioned[0] == False:
403
# The record is unknown or newly added
404
files_to_backup.append(path[1])
405
elif (content_change and (kind[1] is not None) and
406
osutils.is_inside_any(files, path[1])):
407
# Versioned and changed, but not deleted, and still
408
# in one of the dirs to be deleted.
409
files_to_backup.append(path[1])
411
def backup(file_to_backup):
412
backup_name = self.controldir._available_backup_name(file_to_backup)
413
osutils.rename(abs_path, self.abspath(backup_name))
414
return "removed %s (but kept a copy: %s)" % (file_to_backup,
417
# Build inv_delta and delete files where applicable,
418
# do this before any modifications to meta data.
420
fid = self.path2id(f)
423
message = "%s is not versioned." % (f,)
426
# having removed it, it must be either ignored or unknown
427
if self.is_ignored(f):
431
# XXX: Really should be a more abstract reporter interface
432
kind_ch = osutils.kind_marker(self.kind(f, fid))
433
to_file.write(new_status + ' ' + f + kind_ch + '\n')
435
inv_delta.append((f, None, fid, None))
436
message = "removed %s" % (f,)
439
abs_path = self.abspath(f)
440
if osutils.lexists(abs_path):
441
if (osutils.isdir(abs_path) and
442
len(os.listdir(abs_path)) > 0):
444
osutils.rmtree(abs_path)
445
message = "deleted %s" % (f,)
449
if f in files_to_backup:
452
osutils.delete_any(abs_path)
453
message = "deleted %s" % (f,)
454
elif message is not None:
455
# Only care if we haven't done anything yet.
456
message = "%s does not exist." % (f,)
458
# Print only one message (if any) per file.
459
if message is not None:
461
self.apply_inventory_delta(inv_delta)
463
def set_parent_trees(self, parents_list, allow_leftmost_as_ghost=False):
464
"""See MutableTree.set_parent_trees."""
465
parent_ids = [rev for (rev, tree) in parents_list]
466
for revision_id in parent_ids:
467
_mod_revision.check_not_reserved_id(revision_id)
469
with self.lock_tree_write():
470
self._check_parents_for_ghosts(parent_ids,
471
allow_leftmost_as_ghost=allow_leftmost_as_ghost)
473
parent_ids = self._filter_parent_ids_by_ancestry(parent_ids)
475
if len(parent_ids) == 0:
476
leftmost_parent_id = _mod_revision.NULL_REVISION
477
leftmost_parent_tree = None
479
leftmost_parent_id, leftmost_parent_tree = parents_list[0]
481
if self._change_last_revision(leftmost_parent_id):
482
if leftmost_parent_tree is None:
483
# If we don't have a tree, fall back to reading the
484
# parent tree from the repository.
485
self._cache_basis_inventory(leftmost_parent_id)
487
inv = leftmost_parent_tree.root_inventory
488
xml = self._create_basis_xml_from_inventory(
489
leftmost_parent_id, inv)
490
self._write_basis_inventory(xml)
491
self._set_merges_from_parent_ids(parent_ids)
493
def _cache_basis_inventory(self, new_revision):
494
"""Cache new_revision as the basis inventory."""
495
# TODO: this should allow the ready-to-use inventory to be passed in,
496
# as commit already has that ready-to-use [while the format is the
499
# this double handles the inventory - unpack and repack -
500
# but is easier to understand. We can/should put a conditional
501
# in here based on whether the inventory is in the latest format
502
# - perhaps we should repack all inventories on a repository
504
# the fast path is to copy the raw xml from the repository. If the
505
# xml contains 'revision_id="', then we assume the right
506
# revision_id is set. We must check for this full string, because a
507
# root node id can legitimately look like 'revision_id' but cannot
509
xml = self.branch.repository._get_inventory_xml(new_revision)
510
firstline = xml.split('\n', 1)[0]
511
if (not 'revision_id="' in firstline or
512
'format="7"' not in firstline):
513
inv = self.branch.repository._serializer.read_inventory_from_string(
515
xml = self._create_basis_xml_from_inventory(new_revision, inv)
516
self._write_basis_inventory(xml)
517
except (errors.NoSuchRevision, errors.RevisionNotPresent):
520
def _basis_inventory_name(self):
521
return 'basis-inventory-cache'
523
def _create_basis_xml_from_inventory(self, revision_id, inventory):
524
"""Create the text that will be saved in basis-inventory"""
525
inventory.revision_id = revision_id
526
return xml7.serializer_v7.write_inventory_to_string(inventory)
528
def set_conflicts(self, conflicts):
529
with self.lock_tree_write():
530
self._put_rio('conflicts', conflicts.to_stanzas(),
533
def add_conflicts(self, new_conflicts):
534
with self.lock_tree_write():
535
conflict_set = set(self.conflicts())
536
conflict_set.update(set(list(new_conflicts)))
537
self.set_conflicts(_mod_conflicts.ConflictList(
538
sorted(conflict_set, key=_mod_conflicts.Conflict.sort_key)))
541
with self.lock_read():
543
confile = self._transport.get('conflicts')
544
except errors.NoSuchFile:
545
return _mod_conflicts.ConflictList()
548
if next(confile) != CONFLICT_HEADER_1 + b'\n':
549
raise errors.ConflictFormatError()
550
except StopIteration:
551
raise errors.ConflictFormatError()
552
reader = _mod_rio.RioReader(confile)
553
return _mod_conflicts.ConflictList.from_stanzas(reader)
557
def get_ignore_list(self):
558
"""Return list of ignore patterns.
560
Cached in the Tree object after the first call.
562
ignoreset = getattr(self, '_ignoreset', None)
563
if ignoreset is not None:
567
ignore_globs.update(ignores.get_runtime_ignores())
568
ignore_globs.update(ignores.get_user_ignores())
569
if self.has_filename(self._format.ignore_filename):
570
with self.get_file(self._format.ignore_filename) as f:
571
ignore_globs.update(ignores.parse_ignore_file(f))
572
self._ignoreset = ignore_globs
576
self._flush_ignore_list_cache()
578
def _flush_ignore_list_cache(self):
579
"""Resets the cached ignore list to force a cache rebuild."""
580
self._ignoreset = None
581
self._ignoreglobster = None
583
def is_ignored(self, filename):
584
r"""Check whether the filename matches an ignore pattern.
586
Patterns containing '/' or '\' need to match the whole path;
587
others match against only the last component. Patterns starting
588
with '!' are ignore exceptions. Exceptions take precedence
589
over regular patterns and cause the filename to not be ignored.
591
If the file is ignored, returns the pattern which caused it to
592
be ignored, otherwise None. So this can simply be used as a
593
boolean if desired."""
594
if getattr(self, '_ignoreglobster', None) is None:
595
self._ignoreglobster = globbing.ExceptionGlobster(self.get_ignore_list())
596
return self._ignoreglobster.match(filename)
598
def read_basis_inventory(self):
599
"""Read the cached basis inventory."""
600
path = self._basis_inventory_name()
601
return self._transport.get_bytes(path)
603
def read_working_inventory(self):
604
"""Read the working inventory.
606
:raises errors.InventoryModified: read_working_inventory will fail
607
when the current in memory inventory has been modified.
609
# conceptually this should be an implementation detail of the tree.
610
# XXX: Deprecate this.
611
# ElementTree does its own conversion from UTF-8, so open in
613
with self.lock_read():
614
if self._inventory_is_modified:
615
raise errors.InventoryModified(self)
616
with self._transport.get('inventory') as f:
617
result = self._deserialize(f)
618
self._set_inventory(result, dirty=False)
621
def get_root_id(self):
622
"""Return the id of this trees root"""
623
with self.lock_read():
624
return self._inventory.root.file_id
626
def has_id(self, file_id):
627
# files that have been deleted are excluded
628
inv, inv_file_id = self._unpack_file_id(file_id)
629
if not inv.has_id(inv_file_id):
631
path = inv.id2path(inv_file_id)
632
return osutils.lexists(self.abspath(path))
634
def has_or_had_id(self, file_id):
635
if file_id == self.get_root_id():
637
inv, inv_file_id = self._unpack_file_id(file_id)
638
return inv.has_id(inv_file_id)
640
def all_file_ids(self):
641
"""Iterate through file_ids for this tree.
643
file_ids are in a WorkingTree if they are in the working inventory
644
and the working file exists.
646
return {ie.file_id for path, ie in self.iter_entries_by_dir()}
648
def all_versioned_paths(self):
649
return {path for path, ie in self.iter_entries_by_dir()}
651
def set_last_revision(self, new_revision):
652
"""Change the last revision in the working tree."""
653
with self.lock_tree_write():
654
if self._change_last_revision(new_revision):
655
self._cache_basis_inventory(new_revision)
657
def _get_check_refs(self):
658
"""Return the references needed to perform a check of this tree.
660
The default implementation returns no refs, and is only suitable for
661
trees that have no local caching and can commit on ghosts at any time.
663
:seealso: breezy.check for details about check_refs.
667
def _check(self, references):
668
"""Check the tree for consistency.
670
:param references: A dict with keys matching the items returned by
671
self._get_check_refs(), and values from looking those keys up in
674
with self.lock_read():
675
tree_basis = self.basis_tree()
676
with tree_basis.lock_read():
677
repo_basis = references[('trees', self.last_revision())]
678
if len(list(repo_basis.iter_changes(tree_basis))) > 0:
679
raise errors.BzrCheckError(
680
"Mismatched basis inventory content.")
683
def check_state(self):
684
"""Check that the working state is/isn't valid."""
685
with self.lock_read():
686
check_refs = self._get_check_refs()
688
for ref in check_refs:
691
refs[ref] = self.branch.repository.revision_tree(value)
694
def reset_state(self, revision_ids=None):
695
"""Reset the state of the working tree.
697
This does a hard-reset to a last-known-good state. This is a way to
698
fix if something got corrupted (like the .bzr/checkout/dirstate file)
700
with self.lock_tree_write():
701
if revision_ids is None:
702
revision_ids = self.get_parent_ids()
704
rt = self.branch.repository.revision_tree(
705
_mod_revision.NULL_REVISION)
707
rt = self.branch.repository.revision_tree(revision_ids[0])
708
self._write_inventory(rt.root_inventory)
709
self.set_parent_ids(revision_ids)
712
"""Write the in memory inventory to disk."""
713
# TODO: Maybe this should only write on dirty ?
714
if self._control_files._lock_mode != 'w':
715
raise errors.NotWriteLocked(self)
717
self._serialize(self._inventory, sio)
719
self._transport.put_file('inventory', sio,
720
mode=self.controldir._get_file_mode())
721
self._inventory_is_modified = False
723
def get_file_mtime(self, path, file_id=None):
724
"""See Tree.get_file_mtime."""
726
return os.lstat(self.abspath(path)).st_mtime
728
if e.errno == errno.ENOENT:
729
raise errors.NoSuchFile(path)
732
def _is_executable_from_path_and_stat_from_basis(self, path, stat_result):
733
inv, file_id = self._path2inv_file_id(path)
735
# For unversioned files on win32, we just assume they are not
738
return inv.get_entry(file_id).executable
740
def _is_executable_from_path_and_stat_from_stat(self, path, stat_result):
741
mode = stat_result.st_mode
742
return bool(stat.S_ISREG(mode) and stat.S_IEXEC & mode)
744
def is_executable(self, path, file_id=None):
745
if not self._supports_executable():
746
inv, inv_file_id = self._path2inv_file_id(path)
747
return inv.get_entry(inv_file_id).executable
749
mode = os.lstat(self.abspath(path)).st_mode
750
return bool(stat.S_ISREG(mode) and stat.S_IEXEC & mode)
752
def _is_executable_from_path_and_stat(self, path, stat_result):
753
if not self._supports_executable():
754
return self._is_executable_from_path_and_stat_from_basis(path, stat_result)
756
return self._is_executable_from_path_and_stat_from_stat(path, stat_result)
758
def _add(self, files, ids, kinds):
759
"""See MutableTree._add."""
760
with self.lock_tree_write():
761
# TODO: Re-adding a file that is removed in the working copy
762
# should probably put it back with the previous ID.
763
# the read and write working inventory should not occur in this
764
# function - they should be part of lock_write and unlock.
765
# FIXME: nested trees
766
inv = self.root_inventory
767
for f, file_id, kind in zip(files, ids, kinds):
769
inv.add_path(f, kind=kind)
771
inv.add_path(f, kind=kind, file_id=file_id)
772
self._inventory_is_modified = True
774
def revision_tree(self, revision_id):
775
"""See WorkingTree.revision_id."""
776
if revision_id == self.last_revision():
778
xml = self.read_basis_inventory()
779
except errors.NoSuchFile:
783
inv = xml7.serializer_v7.read_inventory_from_string(xml)
784
# dont use the repository revision_tree api because we want
785
# to supply the inventory.
786
if inv.revision_id == revision_id:
787
return InventoryRevisionTree(
788
self.branch.repository, inv, revision_id)
789
except errors.BadInventoryFormat:
791
# raise if there was no inventory, or if we read the wrong inventory.
792
raise errors.NoSuchRevisionInTree(self, revision_id)
794
def annotate_iter(self, path, file_id=None,
795
default_revision=_mod_revision.CURRENT_REVISION):
796
"""See Tree.annotate_iter
798
This implementation will use the basis tree implementation if possible.
799
Lines not in the basis are attributed to CURRENT_REVISION
801
If there are pending merges, lines added by those merges will be
802
incorrectly attributed to CURRENT_REVISION (but after committing, the
803
attribution will be correct).
805
with self.lock_read():
807
file_id = self.path2id(path)
808
maybe_file_parent_keys = []
809
for parent_id in self.get_parent_ids():
811
parent_tree = self.revision_tree(parent_id)
812
except errors.NoSuchRevisionInTree:
813
parent_tree = self.branch.repository.revision_tree(
815
with parent_tree.lock_read():
818
kind = parent_tree.kind(path, file_id)
819
except errors.NoSuchFile:
822
# Note: this is slightly unnecessary, because symlinks and
823
# directories have a "text" which is the empty text, and we
824
# know that won't mess up annotations. But it seems cleaner
826
parent_path = parent_tree.id2path(file_id)
829
parent_tree.get_file_revision(parent_path, file_id))
830
if parent_text_key not in maybe_file_parent_keys:
831
maybe_file_parent_keys.append(parent_text_key)
832
graph = self.branch.repository.get_file_graph()
833
heads = graph.heads(maybe_file_parent_keys)
834
file_parent_keys = []
835
for key in maybe_file_parent_keys:
837
file_parent_keys.append(key)
839
# Now we have the parents of this content
840
annotator = self.branch.repository.texts.get_annotator()
841
text = self.get_file_text(path, file_id)
842
this_key = (file_id, default_revision)
843
annotator.add_special_text(this_key, file_parent_keys, text)
844
annotations = [(key[-1], line)
845
for key, line in annotator.annotate_flat(this_key)]
848
def _put_rio(self, filename, stanzas, header):
849
self._must_be_locked()
850
my_file = _mod_rio.rio_file(stanzas, header)
851
self._transport.put_file(filename, my_file,
852
mode=self.controldir._get_file_mode())
854
def set_merge_modified(self, modified_hashes):
856
for file_id in modified_hashes:
857
yield _mod_rio.Stanza(file_id=file_id.decode('utf8'),
858
hash=modified_hashes[file_id])
859
with self.lock_tree_write():
860
self._put_rio('merge-hashes', iter_stanzas(), MERGE_MODIFIED_HEADER_1)
862
def merge_modified(self):
863
"""Return a dictionary of files modified by a merge.
865
The list is initialized by WorkingTree.set_merge_modified, which is
866
typically called after we make some automatic updates to the tree
869
This returns a map of file_id->sha1, containing only files which are
870
still in the working inventory and have that text hash.
872
with self.lock_read():
874
hashfile = self._transport.get('merge-hashes')
875
except errors.NoSuchFile:
880
if next(hashfile) != MERGE_MODIFIED_HEADER_1 + b'\n':
881
raise errors.MergeModifiedFormatError()
882
except StopIteration:
883
raise errors.MergeModifiedFormatError()
884
for s in _mod_rio.RioReader(hashfile):
885
# RioReader reads in Unicode, so convert file_ids back to utf8
886
file_id = cache_utf8.encode(s.get("file_id"))
887
if not self.has_id(file_id):
889
text_hash = s.get("hash")
890
path = self.id2path(file_id)
891
if text_hash == self.get_file_sha1(path, file_id):
892
merge_hashes[file_id] = text_hash
897
def subsume(self, other_tree):
898
def add_children(inventory, entry):
899
for child_entry in entry.children.values():
900
inventory._byid[child_entry.file_id] = child_entry
901
if child_entry.kind == 'directory':
902
add_children(inventory, child_entry)
903
with self.lock_write():
904
if other_tree.get_root_id() == self.get_root_id():
905
raise errors.BadSubsumeSource(self, other_tree,
906
'Trees have the same root')
908
other_tree_path = self.relpath(other_tree.basedir)
909
except errors.PathNotChild:
910
raise errors.BadSubsumeSource(self, other_tree,
911
'Tree is not contained by the other')
912
new_root_parent = self.path2id(osutils.dirname(other_tree_path))
913
if new_root_parent is None:
914
raise errors.BadSubsumeSource(self, other_tree,
915
'Parent directory is not versioned.')
916
# We need to ensure that the result of a fetch will have a
917
# versionedfile for the other_tree root, and only fetching into
918
# RepositoryKnit2 guarantees that.
919
if not self.branch.repository.supports_rich_root():
920
raise errors.SubsumeTargetNeedsUpgrade(other_tree)
921
with other_tree.lock_tree_write():
922
new_parents = other_tree.get_parent_ids()
923
other_root = other_tree.root_inventory.root
924
other_root.parent_id = new_root_parent
925
other_root.name = osutils.basename(other_tree_path)
926
self.root_inventory.add(other_root)
927
add_children(self.root_inventory, other_root)
928
self._write_inventory(self.root_inventory)
929
# normally we don't want to fetch whole repositories, but i think
930
# here we really do want to consolidate the whole thing.
931
for parent_id in other_tree.get_parent_ids():
932
self.branch.fetch(other_tree.branch, parent_id)
933
self.add_parent_tree_id(parent_id)
934
other_tree.controldir.retire_bzrdir()
936
def extract(self, sub_path, file_id=None, format=None):
937
"""Extract a subtree from this tree.
939
A new branch will be created, relative to the path for this tree.
942
segments = osutils.splitpath(path)
943
transport = self.branch.controldir.root_transport
944
for name in segments:
945
transport = transport.clone(name)
946
transport.ensure_base()
949
with self.lock_tree_write():
951
branch_transport = mkdirs(sub_path)
953
format = self.controldir.cloning_metadir()
954
branch_transport.ensure_base()
955
branch_bzrdir = format.initialize_on_transport(branch_transport)
957
repo = branch_bzrdir.find_repository()
958
except errors.NoRepositoryPresent:
959
repo = branch_bzrdir.create_repository()
960
if not repo.supports_rich_root():
961
raise errors.RootNotRich()
962
new_branch = branch_bzrdir.create_branch()
963
new_branch.pull(self.branch)
964
for parent_id in self.get_parent_ids():
965
new_branch.fetch(self.branch, parent_id)
966
tree_transport = self.controldir.root_transport.clone(sub_path)
967
if tree_transport.base != branch_transport.base:
968
tree_bzrdir = format.initialize_on_transport(tree_transport)
969
tree_bzrdir.set_branch_reference(new_branch)
971
tree_bzrdir = branch_bzrdir
972
wt = tree_bzrdir.create_workingtree(_mod_revision.NULL_REVISION)
973
wt.set_parent_ids(self.get_parent_ids())
974
# FIXME: Support nested trees
975
my_inv = self.root_inventory
976
child_inv = inventory.Inventory(root_id=None)
978
file_id = self.path2id(sub_path)
979
new_root = my_inv.get_entry(file_id)
980
my_inv.remove_recursive_id(file_id)
981
new_root.parent_id = None
982
child_inv.add(new_root)
983
self._write_inventory(my_inv)
984
wt._write_inventory(child_inv)
987
def list_files(self, include_root=False, from_dir=None, recursive=True):
988
"""List all files as (path, class, kind, id, entry).
990
Lists, but does not descend into unversioned directories.
991
This does not include files that have been deleted in this
992
tree. Skips the control directory.
994
:param include_root: if True, return an entry for the root
995
:param from_dir: start from this directory or None for the root
996
:param recursive: whether to recurse into subdirectories or not
998
with self.lock_read():
999
if from_dir is None and include_root is True:
1000
yield ('', 'V', 'directory', self.get_root_id(),
1001
self.root_inventory.root)
1002
# Convert these into local objects to save lookup times
1003
pathjoin = osutils.pathjoin
1005
# transport.base ends in a slash, we want the piece
1006
# between the last two slashes
1007
transport_base_dir = self.controldir.transport.base.rsplit('/', 2)[1]
1010
'directory': TreeDirectory,
1015
# directory file_id, relative path, absolute path, reverse sorted
1017
if from_dir is not None:
1018
inv, from_dir_id = self._path2inv_file_id(from_dir)
1019
if from_dir_id is None:
1020
# Directory not versioned
1022
from_dir_abspath = pathjoin(self.basedir, from_dir)
1024
inv = self.root_inventory
1025
from_dir_id = inv.root.file_id
1026
from_dir_abspath = self.basedir
1027
children = sorted(os.listdir(from_dir_abspath))
1028
# jam 20060527 The kernel sized tree seems equivalent whether we
1029
# use a deque and popleft to keep them sorted, or if we use a plain
1030
# list and just reverse() them.
1031
children = collections.deque(children)
1032
stack = [(from_dir_id, u'', from_dir_abspath, children)]
1034
from_dir_id, from_dir_relpath, from_dir_abspath, children = stack[-1]
1037
f = children.popleft()
1038
# TODO: If we find a subdirectory with its own .bzr
1039
# directory, then that is a separate tree and we
1040
# should exclude it.
1042
# the bzrdir for this tree
1043
if transport_base_dir == f:
1046
# we know that from_dir_relpath and from_dir_abspath never
1047
# end in a slash and 'f' doesn't begin with one, we can do
1048
# a string op, rather than the checks of pathjoin(), all
1049
# relative paths will have an extra slash at the beginning
1050
fp = from_dir_relpath + '/' + f
1053
fap = from_dir_abspath + '/' + f
1055
dir_ie = inv.get_entry(from_dir_id)
1056
if dir_ie.kind == 'directory':
1057
f_ie = dir_ie.children.get(f)
1062
elif self.is_ignored(fp[1:]):
1065
# we may not have found this file, because of a unicode
1066
# issue, or because the directory was actually a
1068
f_norm, can_access = osutils.normalized_filename(f)
1069
if f == f_norm or not can_access:
1070
# No change, so treat this file normally
1073
# this file can be accessed by a normalized path
1074
# check again if it is versioned
1075
# these lines are repeated here for performance
1077
fp = from_dir_relpath + '/' + f
1078
fap = from_dir_abspath + '/' + f
1079
f_ie = inv.get_child(from_dir_id, f)
1082
elif self.is_ignored(fp[1:]):
1087
fk = osutils.file_kind(fap)
1089
# make a last minute entry
1091
yield fp[1:], c, fk, f_ie.file_id, f_ie
1094
yield fp[1:], c, fk, None, fk_entries[fk]()
1096
yield fp[1:], c, fk, None, TreeEntry()
1099
if fk != 'directory':
1102
# But do this child first if recursing down
1104
new_children = sorted(os.listdir(fap))
1105
new_children = collections.deque(new_children)
1106
stack.append((f_ie.file_id, fp, fap, new_children))
1107
# Break out of inner loop,
1108
# so that we start outer loop with child
1111
# if we finished all children, pop it off the stack
1114
def move(self, from_paths, to_dir=None, after=False):
1117
to_dir must exist in the inventory.
1119
If to_dir exists and is a directory, the files are moved into
1120
it, keeping their old names.
1122
Note that to_dir is only the last component of the new name;
1123
this doesn't change the directory.
1125
For each entry in from_paths the move mode will be determined
1128
The first mode moves the file in the filesystem and updates the
1129
inventory. The second mode only updates the inventory without
1130
touching the file on the filesystem.
1132
move uses the second mode if 'after == True' and the target is
1133
either not versioned or newly added, and present in the working tree.
1135
move uses the second mode if 'after == False' and the source is
1136
versioned but no longer in the working tree, and the target is not
1137
versioned but present in the working tree.
1139
move uses the first mode if 'after == False' and the source is
1140
versioned and present in the working tree, and the target is not
1141
versioned and not present in the working tree.
1143
Everything else results in an error.
1145
This returns a list of (from_path, to_path) pairs for each
1146
entry that is moved.
1151
invs_to_write = set()
1153
# check for deprecated use of signature
1155
raise TypeError('You must supply a target directory')
1156
# check destination directory
1157
if isinstance(from_paths, (str, text_type)):
1159
with self.lock_tree_write():
1160
to_abs = self.abspath(to_dir)
1161
if not osutils.isdir(to_abs):
1162
raise errors.BzrMoveFailedError(
1163
'', to_dir, errors.NotADirectory(to_abs))
1164
if not self.has_filename(to_dir):
1165
raise errors.BzrMoveFailedError(
1166
'', to_dir, errors.NotInWorkingDirectory(to_dir))
1167
to_inv, to_dir_id = self._path2inv_file_id(to_dir)
1168
if to_dir_id is None:
1169
raise errors.BzrMoveFailedError(
1170
'', to_dir, errors.NotVersionedError(path=to_dir))
1172
to_dir_ie = to_inv.get_entry(to_dir_id)
1173
if to_dir_ie.kind != 'directory':
1174
raise errors.BzrMoveFailedError(
1175
'', to_dir, errors.NotADirectory(to_abs))
1177
# create rename entries and tuples
1178
for from_rel in from_paths:
1179
from_tail = osutils.splitpath(from_rel)[-1]
1180
from_inv, from_id = self._path2inv_file_id(from_rel)
1182
raise errors.BzrMoveFailedError(from_rel, to_dir,
1183
errors.NotVersionedError(path=from_rel))
1185
from_entry = from_inv.get_entry(from_id)
1186
from_parent_id = from_entry.parent_id
1187
to_rel = osutils.pathjoin(to_dir, from_tail)
1188
rename_entry = InventoryWorkingTree._RenameEntry(
1191
from_tail=from_tail,
1192
from_parent_id=from_parent_id,
1193
to_rel=to_rel, to_tail=from_tail,
1194
to_parent_id=to_dir_id)
1195
rename_entries.append(rename_entry)
1196
rename_tuples.append((from_rel, to_rel))
1198
# determine which move mode to use. checks also for movability
1199
rename_entries = self._determine_mv_mode(rename_entries, after)
1201
original_modified = self._inventory_is_modified
1204
self._inventory_is_modified = True
1205
self._move(rename_entries)
1207
# restore the inventory on error
1208
self._inventory_is_modified = original_modified
1210
#FIXME: Should potentially also write the from_invs
1211
self._write_inventory(to_inv)
1212
return rename_tuples
1214
def rename_one(self, from_rel, to_rel, after=False):
1217
This can change the directory or the filename or both.
1219
rename_one has several 'modes' to work. First, it can rename a physical
1220
file and change the file_id. That is the normal mode. Second, it can
1221
only change the file_id without touching any physical file.
1223
rename_one uses the second mode if 'after == True' and 'to_rel' is not
1224
versioned but present in the working tree.
1226
rename_one uses the second mode if 'after == False' and 'from_rel' is
1227
versioned but no longer in the working tree, and 'to_rel' is not
1228
versioned but present in the working tree.
1230
rename_one uses the first mode if 'after == False' and 'from_rel' is
1231
versioned and present in the working tree, and 'to_rel' is not
1232
versioned and not present in the working tree.
1234
Everything else results in an error.
1236
with self.lock_tree_write():
1239
# create rename entries and tuples
1240
from_tail = osutils.splitpath(from_rel)[-1]
1241
from_inv, from_id = self._path2inv_file_id(from_rel)
1243
# if file is missing in the inventory maybe it's in the
1245
basis_tree = self.branch.basis_tree()
1246
from_id = basis_tree.path2id(from_rel)
1248
raise errors.BzrRenameFailedError(
1250
errors.NotVersionedError(path=from_rel))
1251
# put entry back in the inventory so we can rename it
1252
from_entry = basis_tree.root_inventory.get_entry(from_id).copy()
1253
from_inv.add(from_entry)
1255
from_inv, from_inv_id = self._unpack_file_id(from_id)
1256
from_entry = from_inv.get_entry(from_inv_id)
1257
from_parent_id = from_entry.parent_id
1258
to_dir, to_tail = os.path.split(to_rel)
1259
to_inv, to_dir_id = self._path2inv_file_id(to_dir)
1260
rename_entry = InventoryWorkingTree._RenameEntry(
1263
from_tail=from_tail,
1264
from_parent_id=from_parent_id,
1265
to_rel=to_rel, to_tail=to_tail,
1266
to_parent_id=to_dir_id)
1267
rename_entries.append(rename_entry)
1269
# determine which move mode to use. checks also for movability
1270
rename_entries = self._determine_mv_mode(rename_entries, after)
1272
# check if the target changed directory and if the target directory is
1274
if to_dir_id is None:
1275
raise errors.BzrMoveFailedError(from_rel, to_rel,
1276
errors.NotVersionedError(path=to_dir))
1278
# all checks done. now we can continue with our actual work
1279
mutter('rename_one:\n'
1284
' to_dir_id {%s}\n',
1285
from_id, from_rel, to_rel, to_dir, to_dir_id)
1287
self._move(rename_entries)
1288
self._write_inventory(to_inv)
1290
class _RenameEntry(object):
1291
def __init__(self, from_rel, from_id, from_tail, from_parent_id,
1292
to_rel, to_tail, to_parent_id, only_change_inv=False,
1294
self.from_rel = from_rel
1295
self.from_id = from_id
1296
self.from_tail = from_tail
1297
self.from_parent_id = from_parent_id
1298
self.to_rel = to_rel
1299
self.to_tail = to_tail
1300
self.to_parent_id = to_parent_id
1301
self.change_id = change_id
1302
self.only_change_inv = only_change_inv
1304
def _determine_mv_mode(self, rename_entries, after=False):
1305
"""Determines for each from-to pair if both inventory and working tree
1306
or only the inventory has to be changed.
1308
Also does basic plausability tests.
1310
# FIXME: Handling of nested trees
1311
inv = self.root_inventory
1313
for rename_entry in rename_entries:
1314
# store to local variables for easier reference
1315
from_rel = rename_entry.from_rel
1316
from_id = rename_entry.from_id
1317
to_rel = rename_entry.to_rel
1318
to_id = inv.path2id(to_rel)
1319
only_change_inv = False
1322
# check the inventory for source and destination
1324
raise errors.BzrMoveFailedError(from_rel, to_rel,
1325
errors.NotVersionedError(path=from_rel))
1326
if to_id is not None:
1328
# allow it with --after but only if dest is newly added
1330
basis = self.basis_tree()
1331
with basis.lock_read():
1332
if not basis.has_id(to_id):
1333
rename_entry.change_id = True
1336
raise errors.BzrMoveFailedError(from_rel, to_rel,
1337
errors.AlreadyVersionedError(path=to_rel))
1339
# try to determine the mode for rename (only change inv or change
1340
# inv and file system)
1342
if not self.has_filename(to_rel):
1343
raise errors.BzrMoveFailedError(from_id, to_rel,
1344
errors.NoSuchFile(path=to_rel,
1345
extra="New file has not been created yet"))
1346
only_change_inv = True
1347
elif not self.has_filename(from_rel) and self.has_filename(to_rel):
1348
only_change_inv = True
1349
elif self.has_filename(from_rel) and not self.has_filename(to_rel):
1350
only_change_inv = False
1351
elif (not self.case_sensitive
1352
and from_rel.lower() == to_rel.lower()
1353
and self.has_filename(from_rel)):
1354
only_change_inv = False
1356
# something is wrong, so lets determine what exactly
1357
if not self.has_filename(from_rel) and \
1358
not self.has_filename(to_rel):
1359
raise errors.BzrRenameFailedError(from_rel, to_rel,
1360
errors.PathsDoNotExist(paths=(from_rel, to_rel)))
1362
raise errors.RenameFailedFilesExist(from_rel, to_rel)
1363
rename_entry.only_change_inv = only_change_inv
1364
return rename_entries
1366
def _move(self, rename_entries):
1367
"""Moves a list of files.
1369
Depending on the value of the flag 'only_change_inv', the
1370
file will be moved on the file system or not.
1374
for entry in rename_entries:
1376
self._move_entry(entry)
1378
self._rollback_move(moved)
1382
def _rollback_move(self, moved):
1383
"""Try to rollback a previous move in case of an filesystem error."""
1386
self._move_entry(WorkingTree._RenameEntry(
1387
entry.to_rel, entry.from_id,
1388
entry.to_tail, entry.to_parent_id, entry.from_rel,
1389
entry.from_tail, entry.from_parent_id,
1390
entry.only_change_inv))
1391
except errors.BzrMoveFailedError as e:
1392
raise errors.BzrMoveFailedError( '', '', "Rollback failed."
1393
" The working tree is in an inconsistent state."
1394
" Please consider doing a 'bzr revert'."
1395
" Error message is: %s" % e)
1397
def _move_entry(self, entry):
1398
inv = self.root_inventory
1399
from_rel_abs = self.abspath(entry.from_rel)
1400
to_rel_abs = self.abspath(entry.to_rel)
1401
if from_rel_abs == to_rel_abs:
1402
raise errors.BzrMoveFailedError(entry.from_rel, entry.to_rel,
1403
"Source and target are identical.")
1405
if not entry.only_change_inv:
1407
osutils.rename(from_rel_abs, to_rel_abs)
1408
except OSError as e:
1409
raise errors.BzrMoveFailedError(
1410
entry.from_rel, entry.to_rel, e[1])
1412
to_id = inv.path2id(entry.to_rel)
1413
inv.remove_recursive_id(to_id)
1414
inv.rename(entry.from_id, entry.to_parent_id, entry.to_tail)
1416
def unversion(self, paths, file_ids=None):
1417
"""Remove the paths in paths from the current versioned set.
1419
When a path is unversioned, all of its children are automatically
1422
:param paths: The paths to stop versioning.
1423
:param file_ids: Optional file_ids for the paths
1424
:raises NoSuchFile: if any path is not currently versioned.
1425
:raises NoSuchId: if any fileid is not currently versioned.
1427
with self.lock_tree_write():
1428
if file_ids is not None:
1429
for file_id in file_ids:
1430
if not self._inventory.has_id(file_id):
1431
raise errors.NoSuchId(self, file_id)
1435
file_id = self._inventory.path2id(path)
1437
raise errors.NoSuchFile(self, path)
1438
file_ids.add(file_id)
1439
for file_id in file_ids:
1440
if self._inventory.has_id(file_id):
1441
self._inventory.remove_recursive_id(file_id)
1443
# in the future this should just set a dirty bit to wait for the
1444
# final unlock. However, until all methods of workingtree start
1445
# with the current in -memory inventory rather than triggering
1446
# a read, it is more complex - we need to teach read_inventory
1447
# to know when to read, and when to not read first... and possibly
1448
# to save first when the in memory one may be corrupted.
1449
# so for now, we just only write it if it is indeed dirty.
1451
self._write_inventory(self._inventory)
1453
def stored_kind(self, path, file_id=None):
1454
"""See Tree.stored_kind"""
1455
inv, inv_file_id = self._path2inv_file_id(path)
1456
if inv_file_id is None:
1457
raise errors.NoSuchFile(self, path)
1458
return inv.get_entry(inv_file_id).kind
1461
"""Yield all unversioned files in this WorkingTree.
1463
If there are any unversioned directories then only the directory is
1464
returned, not all its children. But if there are unversioned files
1465
under a versioned subdirectory, they are returned.
1467
Currently returned depth-first, sorted by name within directories.
1468
This is the same order used by 'osutils.walkdirs'.
1470
## TODO: Work from given directory downwards
1471
for path, dir_entry in self.iter_entries_by_dir():
1472
if dir_entry.kind != 'directory':
1474
# mutter("search for unknowns in %r", path)
1475
dirabs = self.abspath(path)
1476
if not osutils.isdir(dirabs):
1477
# e.g. directory deleted
1481
for subf in os.listdir(dirabs):
1482
if self.controldir.is_control_filename(subf):
1484
if subf not in dir_entry.children:
1487
can_access) = osutils.normalized_filename(subf)
1488
except UnicodeDecodeError:
1489
path_os_enc = path.encode(osutils._fs_enc)
1490
relpath = path_os_enc + '/' + subf
1491
raise errors.BadFilenameEncoding(relpath,
1493
if subf_norm != subf and can_access:
1494
if subf_norm not in dir_entry.children:
1495
fl.append(subf_norm)
1501
subp = osutils.pathjoin(path, subf)
1504
def walkdirs(self, prefix=""):
1505
"""Walk the directories of this tree.
1507
returns a generator which yields items in the form:
1508
((curren_directory_path, fileid),
1509
[(file1_path, file1_name, file1_kind, (lstat), file1_id,
1512
This API returns a generator, which is only valid during the current
1513
tree transaction - within a single lock_read or lock_write duration.
1515
If the tree is not locked, it may cause an error to be raised,
1516
depending on the tree implementation.
1518
disk_top = self.abspath(prefix)
1519
if disk_top.endswith('/'):
1520
disk_top = disk_top[:-1]
1521
top_strip_len = len(disk_top) + 1
1522
inventory_iterator = self._walkdirs(prefix)
1523
disk_iterator = osutils.walkdirs(disk_top, prefix)
1525
current_disk = next(disk_iterator)
1526
disk_finished = False
1527
except OSError as e:
1528
if not (e.errno == errno.ENOENT or
1529
(sys.platform == 'win32' and e.errno == ERROR_PATH_NOT_FOUND)):
1532
disk_finished = True
1534
current_inv = next(inventory_iterator)
1535
inv_finished = False
1536
except StopIteration:
1539
while not inv_finished or not disk_finished:
1541
((cur_disk_dir_relpath, cur_disk_dir_path_from_top),
1542
cur_disk_dir_content) = current_disk
1544
((cur_disk_dir_relpath, cur_disk_dir_path_from_top),
1545
cur_disk_dir_content) = ((None, None), None)
1546
if not disk_finished:
1547
# strip out .bzr dirs
1548
if (cur_disk_dir_path_from_top[top_strip_len:] == '' and
1549
len(cur_disk_dir_content) > 0):
1550
# osutils.walkdirs can be made nicer -
1551
# yield the path-from-prefix rather than the pathjoined
1553
bzrdir_loc = bisect_left(cur_disk_dir_content,
1555
if (bzrdir_loc < len(cur_disk_dir_content)
1556
and self.controldir.is_control_filename(
1557
cur_disk_dir_content[bzrdir_loc][0])):
1558
# we dont yield the contents of, or, .bzr itself.
1559
del cur_disk_dir_content[bzrdir_loc]
1561
# everything is unknown
1564
# everything is missing
1567
direction = cmp(current_inv[0][0], cur_disk_dir_relpath)
1569
# disk is before inventory - unknown
1570
dirblock = [(relpath, basename, kind, stat, None, None) for
1571
relpath, basename, kind, stat, top_path in
1572
cur_disk_dir_content]
1573
yield (cur_disk_dir_relpath, None), dirblock
1575
current_disk = next(disk_iterator)
1576
except StopIteration:
1577
disk_finished = True
1579
# inventory is before disk - missing.
1580
dirblock = [(relpath, basename, 'unknown', None, fileid, kind)
1581
for relpath, basename, dkind, stat, fileid, kind in
1583
yield (current_inv[0][0], current_inv[0][1]), dirblock
1585
current_inv = next(inventory_iterator)
1586
except StopIteration:
1589
# versioned present directory
1590
# merge the inventory and disk data together
1592
for relpath, subiterator in itertools.groupby(sorted(
1593
current_inv[1] + cur_disk_dir_content,
1594
key=operator.itemgetter(0)), operator.itemgetter(1)):
1595
path_elements = list(subiterator)
1596
if len(path_elements) == 2:
1597
inv_row, disk_row = path_elements
1598
# versioned, present file
1599
dirblock.append((inv_row[0],
1600
inv_row[1], disk_row[2],
1601
disk_row[3], inv_row[4],
1603
elif len(path_elements[0]) == 5:
1605
dirblock.append((path_elements[0][0],
1606
path_elements[0][1], path_elements[0][2],
1607
path_elements[0][3], None, None))
1608
elif len(path_elements[0]) == 6:
1609
# versioned, absent file.
1610
dirblock.append((path_elements[0][0],
1611
path_elements[0][1], 'unknown', None,
1612
path_elements[0][4], path_elements[0][5]))
1614
raise NotImplementedError('unreachable code')
1615
yield current_inv[0], dirblock
1617
current_inv = next(inventory_iterator)
1618
except StopIteration:
1621
current_disk = next(disk_iterator)
1622
except StopIteration:
1623
disk_finished = True
1625
def _walkdirs(self, prefix=""):
1626
"""Walk the directories of this tree.
1628
:param prefix: is used as the directrory to start with.
1629
:returns: a generator which yields items in the form::
1631
((curren_directory_path, fileid),
1632
[(file1_path, file1_name, file1_kind, None, file1_id,
1635
_directory = 'directory'
1636
# get the root in the inventory
1637
inv, top_id = self._path2inv_file_id(prefix)
1641
pending = [(prefix, '', _directory, None, top_id, None)]
1644
currentdir = pending.pop()
1645
# 0 - relpath, 1- basename, 2- kind, 3- stat, 4-id, 5-kind
1646
top_id = currentdir[4]
1648
relroot = currentdir[0] + '/'
1651
# FIXME: stash the node in pending
1652
entry = inv.get_entry(top_id)
1653
if entry.kind == 'directory':
1654
for name, child in entry.sorted_children():
1655
dirblock.append((relroot + name, name, child.kind, None,
1656
child.file_id, child.kind
1658
yield (currentdir[0], entry.file_id), dirblock
1659
# push the user specified dirs from dirblock
1660
for dir in reversed(dirblock):
1661
if dir[2] == _directory:
1664
def update_feature_flags(self, updated_flags):
1665
"""Update the feature flags for this branch.
1667
:param updated_flags: Dictionary mapping feature names to necessities
1668
A necessity can be None to indicate the feature should be removed
1670
with self.lock_write():
1671
self._format._update_feature_flags(updated_flags)
1672
self.control_transport.put_bytes('format', self._format.as_string())
1674
def _check_for_tree_references(self, iterator):
1675
"""See if directories have become tree-references."""
1676
blocked_parent_ids = set()
1677
for path, ie in iterator:
1678
if ie.parent_id in blocked_parent_ids:
1679
# This entry was pruned because one of its parents became a
1680
# TreeReference. If this is a directory, mark it as blocked.
1681
if ie.kind == 'directory':
1682
blocked_parent_ids.add(ie.file_id)
1684
if ie.kind == 'directory' and self._directory_is_tree_reference(path):
1685
# This InventoryDirectory needs to be a TreeReference
1686
ie = inventory.TreeReference(ie.file_id, ie.name, ie.parent_id)
1687
blocked_parent_ids.add(ie.file_id)
1690
def iter_entries_by_dir(self, specific_files=None):
1691
"""See Tree.iter_entries_by_dir()"""
1692
# The only trick here is that if we supports_tree_reference then we
1693
# need to detect if a directory becomes a tree-reference.
1694
iterator = super(WorkingTree, self).iter_entries_by_dir(
1695
specific_files=specific_files)
1696
if not self.supports_tree_reference():
1699
return self._check_for_tree_references(iterator)
1702
class WorkingTreeFormatMetaDir(bzrdir.BzrFormat, WorkingTreeFormat):
1703
"""Base class for working trees that live in bzr meta directories."""
1705
ignore_filename = '.bzrignore'
1708
WorkingTreeFormat.__init__(self)
1709
bzrdir.BzrFormat.__init__(self)
1712
def find_format_string(klass, controldir):
1713
"""Return format name for the working tree object in controldir."""
1715
transport = controldir.get_workingtree_transport(None)
1716
return transport.get_bytes("format")
1717
except errors.NoSuchFile:
1718
raise errors.NoWorkingTree(base=transport.base)
1721
def find_format(klass, controldir):
1722
"""Return the format for the working tree object in controldir."""
1723
format_string = klass.find_format_string(controldir)
1724
return klass._find_format(format_registry, 'working tree',
1727
def check_support_status(self, allow_unsupported, recommend_upgrade=True,
1729
WorkingTreeFormat.check_support_status(self,
1730
allow_unsupported=allow_unsupported, recommend_upgrade=recommend_upgrade,
1732
bzrdir.BzrFormat.check_support_status(self, allow_unsupported=allow_unsupported,
1733
recommend_upgrade=recommend_upgrade, basedir=basedir)