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(b'\n', 1)[0]
511
if (not b'revision_id="' in firstline or
512
b'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)
809
raise errors.NoSuchFile(path)
810
maybe_file_parent_keys = []
811
for parent_id in self.get_parent_ids():
813
parent_tree = self.revision_tree(parent_id)
814
except errors.NoSuchRevisionInTree:
815
parent_tree = self.branch.repository.revision_tree(
817
with parent_tree.lock_read():
820
kind = parent_tree.kind(path, file_id)
821
except errors.NoSuchFile:
824
# Note: this is slightly unnecessary, because symlinks and
825
# directories have a "text" which is the empty text, and we
826
# know that won't mess up annotations. But it seems cleaner
828
parent_path = parent_tree.id2path(file_id)
831
parent_tree.get_file_revision(parent_path, file_id))
832
if parent_text_key not in maybe_file_parent_keys:
833
maybe_file_parent_keys.append(parent_text_key)
834
graph = self.branch.repository.get_file_graph()
835
heads = graph.heads(maybe_file_parent_keys)
836
file_parent_keys = []
837
for key in maybe_file_parent_keys:
839
file_parent_keys.append(key)
841
# Now we have the parents of this content
842
annotator = self.branch.repository.texts.get_annotator()
843
text = self.get_file_text(path, file_id)
844
this_key = (file_id, default_revision)
845
annotator.add_special_text(this_key, file_parent_keys, text)
846
annotations = [(key[-1], line)
847
for key, line in annotator.annotate_flat(this_key)]
850
def _put_rio(self, filename, stanzas, header):
851
self._must_be_locked()
852
my_file = _mod_rio.rio_file(stanzas, header)
853
self._transport.put_file(filename, my_file,
854
mode=self.controldir._get_file_mode())
856
def set_merge_modified(self, modified_hashes):
858
for file_id in modified_hashes:
859
yield _mod_rio.Stanza(file_id=file_id.decode('utf8'),
860
hash=modified_hashes[file_id])
861
with self.lock_tree_write():
862
self._put_rio('merge-hashes', iter_stanzas(), MERGE_MODIFIED_HEADER_1)
864
def merge_modified(self):
865
"""Return a dictionary of files modified by a merge.
867
The list is initialized by WorkingTree.set_merge_modified, which is
868
typically called after we make some automatic updates to the tree
871
This returns a map of file_id->sha1, containing only files which are
872
still in the working inventory and have that text hash.
874
with self.lock_read():
876
hashfile = self._transport.get('merge-hashes')
877
except errors.NoSuchFile:
882
if next(hashfile) != MERGE_MODIFIED_HEADER_1 + b'\n':
883
raise errors.MergeModifiedFormatError()
884
except StopIteration:
885
raise errors.MergeModifiedFormatError()
886
for s in _mod_rio.RioReader(hashfile):
887
# RioReader reads in Unicode, so convert file_ids back to utf8
888
file_id = cache_utf8.encode(s.get("file_id"))
889
if not self.has_id(file_id):
891
text_hash = s.get("hash").encode('ascii')
892
path = self.id2path(file_id)
893
if text_hash == self.get_file_sha1(path, file_id):
894
merge_hashes[file_id] = text_hash
899
def subsume(self, other_tree):
900
def add_children(inventory, entry):
901
for child_entry in entry.children.values():
902
inventory._byid[child_entry.file_id] = child_entry
903
if child_entry.kind == 'directory':
904
add_children(inventory, child_entry)
905
with self.lock_write():
906
if other_tree.get_root_id() == self.get_root_id():
907
raise errors.BadSubsumeSource(self, other_tree,
908
'Trees have the same root')
910
other_tree_path = self.relpath(other_tree.basedir)
911
except errors.PathNotChild:
912
raise errors.BadSubsumeSource(self, other_tree,
913
'Tree is not contained by the other')
914
new_root_parent = self.path2id(osutils.dirname(other_tree_path))
915
if new_root_parent is None:
916
raise errors.BadSubsumeSource(self, other_tree,
917
'Parent directory is not versioned.')
918
# We need to ensure that the result of a fetch will have a
919
# versionedfile for the other_tree root, and only fetching into
920
# RepositoryKnit2 guarantees that.
921
if not self.branch.repository.supports_rich_root():
922
raise errors.SubsumeTargetNeedsUpgrade(other_tree)
923
with other_tree.lock_tree_write():
924
new_parents = other_tree.get_parent_ids()
925
other_root = other_tree.root_inventory.root
926
other_root.parent_id = new_root_parent
927
other_root.name = osutils.basename(other_tree_path)
928
self.root_inventory.add(other_root)
929
add_children(self.root_inventory, other_root)
930
self._write_inventory(self.root_inventory)
931
# normally we don't want to fetch whole repositories, but i think
932
# here we really do want to consolidate the whole thing.
933
for parent_id in other_tree.get_parent_ids():
934
self.branch.fetch(other_tree.branch, parent_id)
935
self.add_parent_tree_id(parent_id)
936
other_tree.controldir.retire_bzrdir()
938
def extract(self, sub_path, file_id=None, format=None):
939
"""Extract a subtree from this tree.
941
A new branch will be created, relative to the path for this tree.
944
segments = osutils.splitpath(path)
945
transport = self.branch.controldir.root_transport
946
for name in segments:
947
transport = transport.clone(name)
948
transport.ensure_base()
951
with self.lock_tree_write():
953
branch_transport = mkdirs(sub_path)
955
format = self.controldir.cloning_metadir()
956
branch_transport.ensure_base()
957
branch_bzrdir = format.initialize_on_transport(branch_transport)
959
repo = branch_bzrdir.find_repository()
960
except errors.NoRepositoryPresent:
961
repo = branch_bzrdir.create_repository()
962
if not repo.supports_rich_root():
963
raise errors.RootNotRich()
964
new_branch = branch_bzrdir.create_branch()
965
new_branch.pull(self.branch)
966
for parent_id in self.get_parent_ids():
967
new_branch.fetch(self.branch, parent_id)
968
tree_transport = self.controldir.root_transport.clone(sub_path)
969
if tree_transport.base != branch_transport.base:
970
tree_bzrdir = format.initialize_on_transport(tree_transport)
971
tree_bzrdir.set_branch_reference(new_branch)
973
tree_bzrdir = branch_bzrdir
974
wt = tree_bzrdir.create_workingtree(_mod_revision.NULL_REVISION)
975
wt.set_parent_ids(self.get_parent_ids())
976
# FIXME: Support nested trees
977
my_inv = self.root_inventory
978
child_inv = inventory.Inventory(root_id=None)
980
file_id = self.path2id(sub_path)
981
new_root = my_inv.get_entry(file_id)
982
my_inv.remove_recursive_id(file_id)
983
new_root.parent_id = None
984
child_inv.add(new_root)
985
self._write_inventory(my_inv)
986
wt._write_inventory(child_inv)
989
def list_files(self, include_root=False, from_dir=None, recursive=True):
990
"""List all files as (path, class, kind, id, entry).
992
Lists, but does not descend into unversioned directories.
993
This does not include files that have been deleted in this
994
tree. Skips the control directory.
996
:param include_root: if True, return an entry for the root
997
:param from_dir: start from this directory or None for the root
998
:param recursive: whether to recurse into subdirectories or not
1000
with self.lock_read():
1001
if from_dir is None and include_root is True:
1002
yield ('', 'V', 'directory', self.get_root_id(),
1003
self.root_inventory.root)
1004
# Convert these into local objects to save lookup times
1005
pathjoin = osutils.pathjoin
1007
# transport.base ends in a slash, we want the piece
1008
# between the last two slashes
1009
transport_base_dir = self.controldir.transport.base.rsplit('/', 2)[1]
1012
'directory': TreeDirectory,
1017
# directory file_id, relative path, absolute path, reverse sorted
1019
if from_dir is not None:
1020
inv, from_dir_id = self._path2inv_file_id(from_dir)
1021
if from_dir_id is None:
1022
# Directory not versioned
1024
from_dir_abspath = pathjoin(self.basedir, from_dir)
1026
inv = self.root_inventory
1027
from_dir_id = inv.root.file_id
1028
from_dir_abspath = self.basedir
1029
children = sorted(os.listdir(from_dir_abspath))
1030
# jam 20060527 The kernel sized tree seems equivalent whether we
1031
# use a deque and popleft to keep them sorted, or if we use a plain
1032
# list and just reverse() them.
1033
children = collections.deque(children)
1034
stack = [(from_dir_id, u'', from_dir_abspath, children)]
1036
from_dir_id, from_dir_relpath, from_dir_abspath, children = stack[-1]
1039
f = children.popleft()
1040
# TODO: If we find a subdirectory with its own .bzr
1041
# directory, then that is a separate tree and we
1042
# should exclude it.
1044
# the bzrdir for this tree
1045
if transport_base_dir == f:
1048
# we know that from_dir_relpath and from_dir_abspath never
1049
# end in a slash and 'f' doesn't begin with one, we can do
1050
# a string op, rather than the checks of pathjoin(), all
1051
# relative paths will have an extra slash at the beginning
1052
fp = from_dir_relpath + '/' + f
1055
fap = from_dir_abspath + '/' + f
1057
dir_ie = inv.get_entry(from_dir_id)
1058
if dir_ie.kind == 'directory':
1059
f_ie = dir_ie.children.get(f)
1064
elif self.is_ignored(fp[1:]):
1067
# we may not have found this file, because of a unicode
1068
# issue, or because the directory was actually a
1070
f_norm, can_access = osutils.normalized_filename(f)
1071
if f == f_norm or not can_access:
1072
# No change, so treat this file normally
1075
# this file can be accessed by a normalized path
1076
# check again if it is versioned
1077
# these lines are repeated here for performance
1079
fp = from_dir_relpath + '/' + f
1080
fap = from_dir_abspath + '/' + f
1081
f_ie = inv.get_child(from_dir_id, f)
1084
elif self.is_ignored(fp[1:]):
1089
fk = osutils.file_kind(fap)
1091
# make a last minute entry
1093
yield fp[1:], c, fk, f_ie.file_id, f_ie
1096
yield fp[1:], c, fk, None, fk_entries[fk]()
1098
yield fp[1:], c, fk, None, TreeEntry()
1101
if fk != 'directory':
1104
# But do this child first if recursing down
1106
new_children = sorted(os.listdir(fap))
1107
new_children = collections.deque(new_children)
1108
stack.append((f_ie.file_id, fp, fap, new_children))
1109
# Break out of inner loop,
1110
# so that we start outer loop with child
1113
# if we finished all children, pop it off the stack
1116
def move(self, from_paths, to_dir=None, after=False):
1119
to_dir must exist in the inventory.
1121
If to_dir exists and is a directory, the files are moved into
1122
it, keeping their old names.
1124
Note that to_dir is only the last component of the new name;
1125
this doesn't change the directory.
1127
For each entry in from_paths the move mode will be determined
1130
The first mode moves the file in the filesystem and updates the
1131
inventory. The second mode only updates the inventory without
1132
touching the file on the filesystem.
1134
move uses the second mode if 'after == True' and the target is
1135
either not versioned or newly added, and present in the working tree.
1137
move uses the second mode if 'after == False' and the source is
1138
versioned but no longer in the working tree, and the target is not
1139
versioned but present in the working tree.
1141
move uses the first mode if 'after == False' and the source is
1142
versioned and present in the working tree, and the target is not
1143
versioned and not present in the working tree.
1145
Everything else results in an error.
1147
This returns a list of (from_path, to_path) pairs for each
1148
entry that is moved.
1153
invs_to_write = set()
1155
# check for deprecated use of signature
1157
raise TypeError('You must supply a target directory')
1158
# check destination directory
1159
if isinstance(from_paths, (str, text_type)):
1161
with self.lock_tree_write():
1162
to_abs = self.abspath(to_dir)
1163
if not osutils.isdir(to_abs):
1164
raise errors.BzrMoveFailedError(
1165
'', to_dir, errors.NotADirectory(to_abs))
1166
if not self.has_filename(to_dir):
1167
raise errors.BzrMoveFailedError(
1168
'', to_dir, errors.NotInWorkingDirectory(to_dir))
1169
to_inv, to_dir_id = self._path2inv_file_id(to_dir)
1170
if to_dir_id is None:
1171
raise errors.BzrMoveFailedError(
1172
'', to_dir, errors.NotVersionedError(path=to_dir))
1174
to_dir_ie = to_inv.get_entry(to_dir_id)
1175
if to_dir_ie.kind != 'directory':
1176
raise errors.BzrMoveFailedError(
1177
'', to_dir, errors.NotADirectory(to_abs))
1179
# create rename entries and tuples
1180
for from_rel in from_paths:
1181
from_tail = osutils.splitpath(from_rel)[-1]
1182
from_inv, from_id = self._path2inv_file_id(from_rel)
1184
raise errors.BzrMoveFailedError(from_rel, to_dir,
1185
errors.NotVersionedError(path=from_rel))
1187
from_entry = from_inv.get_entry(from_id)
1188
from_parent_id = from_entry.parent_id
1189
to_rel = osutils.pathjoin(to_dir, from_tail)
1190
rename_entry = InventoryWorkingTree._RenameEntry(
1193
from_tail=from_tail,
1194
from_parent_id=from_parent_id,
1195
to_rel=to_rel, to_tail=from_tail,
1196
to_parent_id=to_dir_id)
1197
rename_entries.append(rename_entry)
1198
rename_tuples.append((from_rel, to_rel))
1200
# determine which move mode to use. checks also for movability
1201
rename_entries = self._determine_mv_mode(rename_entries, after)
1203
original_modified = self._inventory_is_modified
1206
self._inventory_is_modified = True
1207
self._move(rename_entries)
1209
# restore the inventory on error
1210
self._inventory_is_modified = original_modified
1212
#FIXME: Should potentially also write the from_invs
1213
self._write_inventory(to_inv)
1214
return rename_tuples
1216
def rename_one(self, from_rel, to_rel, after=False):
1219
This can change the directory or the filename or both.
1221
rename_one has several 'modes' to work. First, it can rename a physical
1222
file and change the file_id. That is the normal mode. Second, it can
1223
only change the file_id without touching any physical file.
1225
rename_one uses the second mode if 'after == True' and 'to_rel' is not
1226
versioned but present in the working tree.
1228
rename_one uses the second mode if 'after == False' and 'from_rel' is
1229
versioned but no longer in the working tree, and 'to_rel' is not
1230
versioned but present in the working tree.
1232
rename_one uses the first mode if 'after == False' and 'from_rel' is
1233
versioned and present in the working tree, and 'to_rel' is not
1234
versioned and not present in the working tree.
1236
Everything else results in an error.
1238
with self.lock_tree_write():
1241
# create rename entries and tuples
1242
from_tail = osutils.splitpath(from_rel)[-1]
1243
from_inv, from_id = self._path2inv_file_id(from_rel)
1245
# if file is missing in the inventory maybe it's in the
1247
basis_tree = self.branch.basis_tree()
1248
from_id = basis_tree.path2id(from_rel)
1250
raise errors.BzrRenameFailedError(
1252
errors.NotVersionedError(path=from_rel))
1253
# put entry back in the inventory so we can rename it
1254
from_entry = basis_tree.root_inventory.get_entry(from_id).copy()
1255
from_inv.add(from_entry)
1257
from_inv, from_inv_id = self._unpack_file_id(from_id)
1258
from_entry = from_inv.get_entry(from_inv_id)
1259
from_parent_id = from_entry.parent_id
1260
to_dir, to_tail = os.path.split(to_rel)
1261
to_inv, to_dir_id = self._path2inv_file_id(to_dir)
1262
rename_entry = InventoryWorkingTree._RenameEntry(
1265
from_tail=from_tail,
1266
from_parent_id=from_parent_id,
1267
to_rel=to_rel, to_tail=to_tail,
1268
to_parent_id=to_dir_id)
1269
rename_entries.append(rename_entry)
1271
# determine which move mode to use. checks also for movability
1272
rename_entries = self._determine_mv_mode(rename_entries, after)
1274
# check if the target changed directory and if the target directory is
1276
if to_dir_id is None:
1277
raise errors.BzrMoveFailedError(from_rel, to_rel,
1278
errors.NotVersionedError(path=to_dir))
1280
# all checks done. now we can continue with our actual work
1281
mutter('rename_one:\n'
1286
' to_dir_id {%s}\n',
1287
from_id, from_rel, to_rel, to_dir, to_dir_id)
1289
self._move(rename_entries)
1290
self._write_inventory(to_inv)
1292
class _RenameEntry(object):
1293
def __init__(self, from_rel, from_id, from_tail, from_parent_id,
1294
to_rel, to_tail, to_parent_id, only_change_inv=False,
1296
self.from_rel = from_rel
1297
self.from_id = from_id
1298
self.from_tail = from_tail
1299
self.from_parent_id = from_parent_id
1300
self.to_rel = to_rel
1301
self.to_tail = to_tail
1302
self.to_parent_id = to_parent_id
1303
self.change_id = change_id
1304
self.only_change_inv = only_change_inv
1306
def _determine_mv_mode(self, rename_entries, after=False):
1307
"""Determines for each from-to pair if both inventory and working tree
1308
or only the inventory has to be changed.
1310
Also does basic plausability tests.
1312
# FIXME: Handling of nested trees
1313
inv = self.root_inventory
1315
for rename_entry in rename_entries:
1316
# store to local variables for easier reference
1317
from_rel = rename_entry.from_rel
1318
from_id = rename_entry.from_id
1319
to_rel = rename_entry.to_rel
1320
to_id = inv.path2id(to_rel)
1321
only_change_inv = False
1324
# check the inventory for source and destination
1326
raise errors.BzrMoveFailedError(from_rel, to_rel,
1327
errors.NotVersionedError(path=from_rel))
1328
if to_id is not None:
1330
# allow it with --after but only if dest is newly added
1332
basis = self.basis_tree()
1333
with basis.lock_read():
1334
if not basis.has_id(to_id):
1335
rename_entry.change_id = True
1338
raise errors.BzrMoveFailedError(from_rel, to_rel,
1339
errors.AlreadyVersionedError(path=to_rel))
1341
# try to determine the mode for rename (only change inv or change
1342
# inv and file system)
1344
if not self.has_filename(to_rel):
1345
raise errors.BzrMoveFailedError(from_rel, to_rel,
1346
errors.NoSuchFile(path=to_rel,
1347
extra="New file has not been created yet"))
1348
only_change_inv = True
1349
elif not self.has_filename(from_rel) and self.has_filename(to_rel):
1350
only_change_inv = True
1351
elif self.has_filename(from_rel) and not self.has_filename(to_rel):
1352
only_change_inv = False
1353
elif (not self.case_sensitive
1354
and from_rel.lower() == to_rel.lower()
1355
and self.has_filename(from_rel)):
1356
only_change_inv = False
1358
# something is wrong, so lets determine what exactly
1359
if not self.has_filename(from_rel) and \
1360
not self.has_filename(to_rel):
1361
raise errors.BzrRenameFailedError(from_rel, to_rel,
1362
errors.PathsDoNotExist(paths=(from_rel, to_rel)))
1364
raise errors.RenameFailedFilesExist(from_rel, to_rel)
1365
rename_entry.only_change_inv = only_change_inv
1366
return rename_entries
1368
def _move(self, rename_entries):
1369
"""Moves a list of files.
1371
Depending on the value of the flag 'only_change_inv', the
1372
file will be moved on the file system or not.
1376
for entry in rename_entries:
1378
self._move_entry(entry)
1380
self._rollback_move(moved)
1384
def _rollback_move(self, moved):
1385
"""Try to rollback a previous move in case of an filesystem error."""
1388
self._move_entry(WorkingTree._RenameEntry(
1389
entry.to_rel, entry.from_id,
1390
entry.to_tail, entry.to_parent_id, entry.from_rel,
1391
entry.from_tail, entry.from_parent_id,
1392
entry.only_change_inv))
1393
except errors.BzrMoveFailedError as e:
1394
raise errors.BzrMoveFailedError('', '', "Rollback failed."
1395
" The working tree is in an inconsistent state."
1396
" Please consider doing a 'bzr revert'."
1397
" Error message is: %s" % e)
1399
def _move_entry(self, entry):
1400
inv = self.root_inventory
1401
from_rel_abs = self.abspath(entry.from_rel)
1402
to_rel_abs = self.abspath(entry.to_rel)
1403
if from_rel_abs == to_rel_abs:
1404
raise errors.BzrMoveFailedError(entry.from_rel, entry.to_rel,
1405
"Source and target are identical.")
1407
if not entry.only_change_inv:
1409
osutils.rename(from_rel_abs, to_rel_abs)
1410
except OSError as e:
1411
raise errors.BzrMoveFailedError(
1412
entry.from_rel, entry.to_rel, e[1])
1414
to_id = inv.path2id(entry.to_rel)
1415
inv.remove_recursive_id(to_id)
1416
inv.rename(entry.from_id, entry.to_parent_id, entry.to_tail)
1418
def unversion(self, paths, file_ids=None):
1419
"""Remove the paths in paths from the current versioned set.
1421
When a path is unversioned, all of its children are automatically
1424
:param paths: The paths to stop versioning.
1425
:param file_ids: Optional file_ids for the paths
1426
:raises NoSuchFile: if any path is not currently versioned.
1427
:raises NoSuchId: if any fileid is not currently versioned.
1429
with self.lock_tree_write():
1430
if file_ids is not None:
1431
for file_id in file_ids:
1432
if not self._inventory.has_id(file_id):
1433
raise errors.NoSuchId(self, file_id)
1437
file_id = self._inventory.path2id(path)
1439
raise errors.NoSuchFile(self, path)
1440
file_ids.add(file_id)
1441
for file_id in file_ids:
1442
if self._inventory.has_id(file_id):
1443
self._inventory.remove_recursive_id(file_id)
1445
# in the future this should just set a dirty bit to wait for the
1446
# final unlock. However, until all methods of workingtree start
1447
# with the current in -memory inventory rather than triggering
1448
# a read, it is more complex - we need to teach read_inventory
1449
# to know when to read, and when to not read first... and possibly
1450
# to save first when the in memory one may be corrupted.
1451
# so for now, we just only write it if it is indeed dirty.
1453
self._write_inventory(self._inventory)
1455
def stored_kind(self, path, file_id=None):
1456
"""See Tree.stored_kind"""
1457
inv, inv_file_id = self._path2inv_file_id(path)
1458
if inv_file_id is None:
1459
raise errors.NoSuchFile(self, path)
1460
return inv.get_entry(inv_file_id).kind
1463
"""Yield all unversioned files in this WorkingTree.
1465
If there are any unversioned directories then only the directory is
1466
returned, not all its children. But if there are unversioned files
1467
under a versioned subdirectory, they are returned.
1469
Currently returned depth-first, sorted by name within directories.
1470
This is the same order used by 'osutils.walkdirs'.
1472
## TODO: Work from given directory downwards
1473
for path, dir_entry in self.iter_entries_by_dir():
1474
if dir_entry.kind != 'directory':
1476
# mutter("search for unknowns in %r", path)
1477
dirabs = self.abspath(path)
1478
if not osutils.isdir(dirabs):
1479
# e.g. directory deleted
1483
for subf in os.listdir(dirabs):
1484
if self.controldir.is_control_filename(subf):
1486
if subf not in dir_entry.children:
1489
can_access) = osutils.normalized_filename(subf)
1490
except UnicodeDecodeError:
1491
path_os_enc = path.encode(osutils._fs_enc)
1492
relpath = path_os_enc + '/' + subf
1493
raise errors.BadFilenameEncoding(relpath,
1495
if subf_norm != subf and can_access:
1496
if subf_norm not in dir_entry.children:
1497
fl.append(subf_norm)
1503
subp = osutils.pathjoin(path, subf)
1506
def walkdirs(self, prefix=""):
1507
"""Walk the directories of this tree.
1509
returns a generator which yields items in the form:
1510
((curren_directory_path, fileid),
1511
[(file1_path, file1_name, file1_kind, (lstat), file1_id,
1514
This API returns a generator, which is only valid during the current
1515
tree transaction - within a single lock_read or lock_write duration.
1517
If the tree is not locked, it may cause an error to be raised,
1518
depending on the tree implementation.
1520
disk_top = self.abspath(prefix)
1521
if disk_top.endswith('/'):
1522
disk_top = disk_top[:-1]
1523
top_strip_len = len(disk_top) + 1
1524
inventory_iterator = self._walkdirs(prefix)
1525
disk_iterator = osutils.walkdirs(disk_top, prefix)
1527
current_disk = next(disk_iterator)
1528
disk_finished = False
1529
except OSError as e:
1530
if not (e.errno == errno.ENOENT or
1531
(sys.platform == 'win32' and e.errno == ERROR_PATH_NOT_FOUND)):
1534
disk_finished = True
1536
current_inv = next(inventory_iterator)
1537
inv_finished = False
1538
except StopIteration:
1541
while not inv_finished or not disk_finished:
1543
((cur_disk_dir_relpath, cur_disk_dir_path_from_top),
1544
cur_disk_dir_content) = current_disk
1546
((cur_disk_dir_relpath, cur_disk_dir_path_from_top),
1547
cur_disk_dir_content) = ((None, None), None)
1548
if not disk_finished:
1549
# strip out .bzr dirs
1550
if (cur_disk_dir_path_from_top[top_strip_len:] == '' and
1551
len(cur_disk_dir_content) > 0):
1552
# osutils.walkdirs can be made nicer -
1553
# yield the path-from-prefix rather than the pathjoined
1555
bzrdir_loc = bisect_left(cur_disk_dir_content,
1557
if (bzrdir_loc < len(cur_disk_dir_content)
1558
and self.controldir.is_control_filename(
1559
cur_disk_dir_content[bzrdir_loc][0])):
1560
# we dont yield the contents of, or, .bzr itself.
1561
del cur_disk_dir_content[bzrdir_loc]
1563
# everything is unknown
1566
# everything is missing
1569
direction = ((current_inv[0][0] > cur_disk_dir_relpath) -
1570
(current_inv[0][0] < cur_disk_dir_relpath))
1573
# disk is before inventory - unknown
1574
dirblock = [(relpath, basename, kind, stat, None, None) for
1575
relpath, basename, kind, stat, top_path in
1576
cur_disk_dir_content]
1577
yield (cur_disk_dir_relpath, None), dirblock
1579
current_disk = next(disk_iterator)
1580
except StopIteration:
1581
disk_finished = True
1583
# inventory is before disk - missing.
1584
dirblock = [(relpath, basename, 'unknown', None, fileid, kind)
1585
for relpath, basename, dkind, stat, fileid, kind in
1587
yield (current_inv[0][0], current_inv[0][1]), dirblock
1589
current_inv = next(inventory_iterator)
1590
except StopIteration:
1593
# versioned present directory
1594
# merge the inventory and disk data together
1596
for relpath, subiterator in itertools.groupby(sorted(
1597
current_inv[1] + cur_disk_dir_content,
1598
key=operator.itemgetter(0)), operator.itemgetter(1)):
1599
path_elements = list(subiterator)
1600
if len(path_elements) == 2:
1601
inv_row, disk_row = path_elements
1602
# versioned, present file
1603
dirblock.append((inv_row[0],
1604
inv_row[1], disk_row[2],
1605
disk_row[3], inv_row[4],
1607
elif len(path_elements[0]) == 5:
1609
dirblock.append((path_elements[0][0],
1610
path_elements[0][1], path_elements[0][2],
1611
path_elements[0][3], None, None))
1612
elif len(path_elements[0]) == 6:
1613
# versioned, absent file.
1614
dirblock.append((path_elements[0][0],
1615
path_elements[0][1], 'unknown', None,
1616
path_elements[0][4], path_elements[0][5]))
1618
raise NotImplementedError('unreachable code')
1619
yield current_inv[0], dirblock
1621
current_inv = next(inventory_iterator)
1622
except StopIteration:
1625
current_disk = next(disk_iterator)
1626
except StopIteration:
1627
disk_finished = True
1629
def _walkdirs(self, prefix=""):
1630
"""Walk the directories of this tree.
1632
:param prefix: is used as the directrory to start with.
1633
:returns: a generator which yields items in the form::
1635
((curren_directory_path, fileid),
1636
[(file1_path, file1_name, file1_kind, None, file1_id,
1639
_directory = 'directory'
1640
# get the root in the inventory
1641
inv, top_id = self._path2inv_file_id(prefix)
1645
pending = [(prefix, '', _directory, None, top_id, None)]
1648
currentdir = pending.pop()
1649
# 0 - relpath, 1- basename, 2- kind, 3- stat, 4-id, 5-kind
1650
top_id = currentdir[4]
1652
relroot = currentdir[0] + '/'
1655
# FIXME: stash the node in pending
1656
entry = inv.get_entry(top_id)
1657
if entry.kind == 'directory':
1658
for name, child in entry.sorted_children():
1659
dirblock.append((relroot + name, name, child.kind, None,
1660
child.file_id, child.kind
1662
yield (currentdir[0], entry.file_id), dirblock
1663
# push the user specified dirs from dirblock
1664
for dir in reversed(dirblock):
1665
if dir[2] == _directory:
1668
def update_feature_flags(self, updated_flags):
1669
"""Update the feature flags for this branch.
1671
:param updated_flags: Dictionary mapping feature names to necessities
1672
A necessity can be None to indicate the feature should be removed
1674
with self.lock_write():
1675
self._format._update_feature_flags(updated_flags)
1676
self.control_transport.put_bytes('format', self._format.as_string())
1678
def _check_for_tree_references(self, iterator):
1679
"""See if directories have become tree-references."""
1680
blocked_parent_ids = set()
1681
for path, ie in iterator:
1682
if ie.parent_id in blocked_parent_ids:
1683
# This entry was pruned because one of its parents became a
1684
# TreeReference. If this is a directory, mark it as blocked.
1685
if ie.kind == 'directory':
1686
blocked_parent_ids.add(ie.file_id)
1688
if ie.kind == 'directory' and self._directory_is_tree_reference(path):
1689
# This InventoryDirectory needs to be a TreeReference
1690
ie = inventory.TreeReference(ie.file_id, ie.name, ie.parent_id)
1691
blocked_parent_ids.add(ie.file_id)
1694
def iter_entries_by_dir(self, specific_files=None):
1695
"""See Tree.iter_entries_by_dir()"""
1696
# The only trick here is that if we supports_tree_reference then we
1697
# need to detect if a directory becomes a tree-reference.
1698
iterator = super(WorkingTree, self).iter_entries_by_dir(
1699
specific_files=specific_files)
1700
if not self.supports_tree_reference():
1703
return self._check_for_tree_references(iterator)
1706
class WorkingTreeFormatMetaDir(bzrdir.BzrFormat, WorkingTreeFormat):
1707
"""Base class for working trees that live in bzr meta directories."""
1709
ignore_filename = '.bzrignore'
1712
WorkingTreeFormat.__init__(self)
1713
bzrdir.BzrFormat.__init__(self)
1716
def find_format_string(klass, controldir):
1717
"""Return format name for the working tree object in controldir."""
1719
transport = controldir.get_workingtree_transport(None)
1720
return transport.get_bytes("format")
1721
except errors.NoSuchFile:
1722
raise errors.NoWorkingTree(base=transport.base)
1725
def find_format(klass, controldir):
1726
"""Return the format for the working tree object in controldir."""
1727
format_string = klass.find_format_string(controldir)
1728
return klass._find_format(format_registry, 'working tree',
1731
def check_support_status(self, allow_unsupported, recommend_upgrade=True,
1733
WorkingTreeFormat.check_support_status(self,
1734
allow_unsupported=allow_unsupported, recommend_upgrade=recommend_upgrade,
1736
bzrdir.BzrFormat.check_support_status(self, allow_unsupported=allow_unsupported,
1737
recommend_upgrade=recommend_upgrade, basedir=basedir)