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
38
from collections.abc import deque
39
except ImportError: # python < 3.7
40
from collections import deque
48
# Explicitly import breezy.bzrdir so that the BzrProber
49
# is guaranteed to be registered.
52
from .. import lazy_import
53
lazy_import.lazy_import(globals(), """
56
conflicts as _mod_conflicts,
59
revision as _mod_revision,
62
from breezy.bzr import (
73
from ..lock import LogicalLockResult
74
from .inventorytree import InventoryRevisionTree, MutableInventoryTree
75
from ..sixish import (
79
from ..trace import mutter, note
87
from ..workingtree import (
94
MERGE_MODIFIED_HEADER_1 = b"BZR merge-modified list format 1"
95
# TODO: Modifying the conflict objects or their type is currently nearly
96
# impossible as there is no clear relationship between the working tree format
97
# and the conflict list file format.
98
CONFLICT_HEADER_1 = b"BZR conflict list format 1"
101
class InventoryWorkingTree(WorkingTree, MutableInventoryTree):
102
"""Base class for working trees that are inventory-oriented.
104
The inventory is held in the `Branch` working-inventory, and the
105
files are in a directory on disk.
107
It is possible for a `WorkingTree` to have a filename which is
108
not listed in the Inventory and vice versa.
111
def __init__(self, basedir='.',
118
"""Construct a InventoryWorkingTree instance. This is not a public API.
120
:param branch: A branch to override probing for the branch.
122
super(InventoryWorkingTree, self).__init__(
123
basedir=basedir, branch=branch,
124
_transport=_control_files._transport, _internal=_internal,
125
_format=_format, _controldir=_controldir)
127
self._control_files = _control_files
128
self._detect_case_handling()
130
if _inventory is None:
131
# This will be acquired on lock_read() or lock_write()
132
self._inventory_is_modified = False
133
self._inventory = None
135
# the caller of __init__ has provided an inventory,
136
# we assume they know what they are doing - as its only
137
# the Format factory and creation methods that are
138
# permitted to do this.
139
self._set_inventory(_inventory, dirty=False)
141
def _set_inventory(self, inv, dirty):
142
"""Set the internal cached inventory.
144
:param inv: The inventory to set.
145
:param dirty: A boolean indicating whether the inventory is the same
146
logical inventory as whats on disk. If True the inventory is not
147
the same and should be written to disk or data will be lost, if
148
False then the inventory is the same as that on disk and any
149
serialisation would be unneeded overhead.
151
self._inventory = inv
152
self._inventory_is_modified = dirty
154
def _detect_case_handling(self):
155
wt_trans = self.controldir.get_workingtree_transport(None)
157
wt_trans.stat(self._format.case_sensitive_filename)
158
except errors.NoSuchFile:
159
self.case_sensitive = True
161
self.case_sensitive = False
163
self._setup_directory_is_tree_reference()
165
def _serialize(self, inventory, out_file):
166
xml5.serializer_v5.write_inventory(
167
self._inventory, out_file, working=True)
169
def _deserialize(selt, in_file):
170
return xml5.serializer_v5.read_inventory(in_file)
172
def break_lock(self):
173
"""Break a lock if one is present from another instance.
175
Uses the ui factory to ask for confirmation if the lock may be from
178
This will probe the repository for its lock as well.
180
self._control_files.break_lock()
181
self.branch.break_lock()
184
return self._control_files.is_locked()
186
def _must_be_locked(self):
187
if not self.is_locked():
188
raise errors.ObjectNotLocked(self)
191
"""Lock the tree for reading.
193
This also locks the branch, and can be unlocked via self.unlock().
195
:return: A breezy.lock.LogicalLockResult.
197
if not self.is_locked():
199
self.branch.lock_read()
201
self._control_files.lock_read()
202
return LogicalLockResult(self.unlock)
203
except BaseException:
207
def lock_tree_write(self):
208
"""See MutableTree.lock_tree_write, and WorkingTree.unlock.
210
:return: A breezy.lock.LogicalLockResult.
212
if not self.is_locked():
214
self.branch.lock_read()
216
self._control_files.lock_write()
217
return LogicalLockResult(self.unlock)
218
except BaseException:
222
def lock_write(self):
223
"""See MutableTree.lock_write, and WorkingTree.unlock.
225
:return: A breezy.lock.LogicalLockResult.
227
if not self.is_locked():
229
self.branch.lock_write()
231
self._control_files.lock_write()
232
return LogicalLockResult(self.unlock)
233
except BaseException:
237
def get_physical_lock_status(self):
238
return self._control_files.get_physical_lock_status()
240
def _write_inventory(self, inv):
241
"""Write inventory as the current inventory."""
242
with self.lock_tree_write():
243
self._set_inventory(inv, dirty=True)
246
# XXX: This method should be deprecated in favour of taking in a proper
247
# new Inventory object.
248
def set_inventory(self, new_inventory_list):
249
from .inventory import (
254
with self.lock_tree_write():
255
inv = Inventory(self.get_root_id())
256
for path, file_id, parent, kind in new_inventory_list:
257
name = os.path.basename(path)
260
# fixme, there should be a factory function inv,add_??
261
if kind == 'directory':
262
inv.add(InventoryDirectory(file_id, name, parent))
264
inv.add(InventoryFile(file_id, name, parent))
265
elif kind == 'symlink':
266
inv.add(InventoryLink(file_id, name, parent))
268
raise errors.BzrError("unknown kind %r" % kind)
269
self._write_inventory(inv)
271
def _write_basis_inventory(self, xml):
272
"""Write the basis inventory XML to the basis-inventory file"""
273
path = self._basis_inventory_name()
275
self._transport.put_file(path, sio,
276
mode=self.controldir._get_file_mode())
278
def _reset_data(self):
279
"""Reset transient data that cannot be revalidated."""
280
self._inventory_is_modified = False
281
with self._transport.get('inventory') as f:
282
result = self._deserialize(f)
283
self._set_inventory(result, dirty=False)
285
def store_uncommitted(self):
286
"""Store uncommitted changes from the tree in the branch."""
287
with self.lock_write():
288
target_tree = self.basis_tree()
289
from ..shelf import ShelfCreator
290
shelf_creator = ShelfCreator(self, target_tree)
292
if not shelf_creator.shelve_all():
294
self.branch.store_uncommitted(shelf_creator)
295
shelf_creator.transform()
297
shelf_creator.finalize()
298
note('Uncommitted changes stored in branch "%s".',
301
def restore_uncommitted(self):
302
"""Restore uncommitted changes from the branch into the tree."""
303
with self.lock_write():
304
unshelver = self.branch.get_unshelver(self)
305
if unshelver is None:
308
merger = unshelver.make_merger()
309
merger.ignore_zero = True
311
self.branch.store_uncommitted(None)
315
def get_shelf_manager(self):
316
"""Return the ShelfManager for this WorkingTree."""
317
from ..shelf import ShelfManager
318
return ShelfManager(self, self._transport)
320
def _set_root_id(self, file_id):
321
"""Set the root id for this tree, in a format specific manner.
323
:param file_id: The file id to assign to the root. It must not be
324
present in the current inventory or an error will occur. It must
325
not be None, but rather a valid file id.
327
inv = self._inventory
328
orig_root_id = inv.root.file_id
329
# TODO: it might be nice to exit early if there was nothing
330
# to do, saving us from trigger a sync on unlock.
331
self._inventory_is_modified = True
332
# we preserve the root inventory entry object, but
333
# unlinkit from the byid index
334
inv.delete(inv.root.file_id)
335
inv.root.file_id = file_id
336
# and link it into the index with the new changed id.
337
inv._byid[inv.root.file_id] = inv.root
338
# and finally update all children to reference the new id.
339
# XXX: this should be safe to just look at the root.children
340
# list, not the WHOLE INVENTORY.
341
for fid in inv.iter_all_ids():
342
entry = inv.get_entry(fid)
343
if entry.parent_id == orig_root_id:
344
entry.parent_id = inv.root.file_id
346
def remove(self, files, verbose=False, to_file=None, keep_files=True,
348
"""Remove nominated files from the working tree metadata.
350
:files: File paths relative to the basedir.
351
:keep_files: If true, the files will also be kept.
352
:force: Delete files and directories, even if they are changed and
353
even if the directories are not empty.
355
if isinstance(files, (str, text_type)):
360
all_files = set() # specified and nested files
366
def recurse_directory_to_add_files(directory):
367
# Recurse directory and add all files
368
# so we can check if they have changed.
369
for parent_info, file_infos in self.walkdirs(directory):
370
for relpath, basename, kind, lstat, fileid, kind in file_infos:
371
# Is it versioned or ignored?
372
if self.is_versioned(relpath):
373
# Add nested content for deletion.
374
all_files.add(relpath)
376
# Files which are not versioned
377
# should be treated as unknown.
378
files_to_backup.append(relpath)
380
with self.lock_tree_write():
382
for filename in files:
383
# Get file name into canonical form.
384
abspath = self.abspath(filename)
385
filename = self.relpath(abspath)
386
if len(filename) > 0:
387
all_files.add(filename)
388
recurse_directory_to_add_files(filename)
390
files = list(all_files)
393
return # nothing to do
395
# Sort needed to first handle directory content before the
397
files.sort(reverse=True)
399
# Bail out if we are going to delete files we shouldn't
400
if not keep_files and not force:
401
for (file_id, path, content_change, versioned, parent_id, name,
402
kind, executable) in self.iter_changes(
403
self.basis_tree(), include_unchanged=True,
404
require_versioned=False, want_unversioned=True,
405
specific_files=files):
406
if versioned[0] is False:
407
# The record is unknown or newly added
408
files_to_backup.append(path[1])
409
elif (content_change and (kind[1] is not None)
410
and osutils.is_inside_any(files, path[1])):
411
# Versioned and changed, but not deleted, and still
412
# in one of the dirs to be deleted.
413
files_to_backup.append(path[1])
415
def backup(file_to_backup):
416
backup_name = self.controldir._available_backup_name(
418
osutils.rename(abs_path, self.abspath(backup_name))
419
return "removed %s (but kept a copy: %s)" % (file_to_backup,
422
# Build inv_delta and delete files where applicable,
423
# do this before any modifications to meta data.
425
fid = self.path2id(f)
428
message = "%s is not versioned." % (f,)
431
# having removed it, it must be either ignored or
433
if self.is_ignored(f):
437
# XXX: Really should be a more abstract reporter
439
kind_ch = osutils.kind_marker(self.kind(f))
441
new_status + ' ' + f + kind_ch + '\n')
443
inv_delta.append((f, None, fid, None))
444
message = "removed %s" % (f,)
447
abs_path = self.abspath(f)
448
if osutils.lexists(abs_path):
449
if (osutils.isdir(abs_path)
450
and len(os.listdir(abs_path)) > 0):
452
osutils.rmtree(abs_path)
453
message = "deleted %s" % (f,)
457
if f in files_to_backup:
460
osutils.delete_any(abs_path)
461
message = "deleted %s" % (f,)
462
elif message is not None:
463
# Only care if we haven't done anything yet.
464
message = "%s does not exist." % (f,)
466
# Print only one message (if any) per file.
467
if message is not None:
469
self.apply_inventory_delta(inv_delta)
471
def set_parent_trees(self, parents_list, allow_leftmost_as_ghost=False):
472
"""See MutableTree.set_parent_trees."""
473
parent_ids = [rev for (rev, tree) in parents_list]
474
for revision_id in parent_ids:
475
_mod_revision.check_not_reserved_id(revision_id)
477
with self.lock_tree_write():
478
self._check_parents_for_ghosts(parent_ids,
479
allow_leftmost_as_ghost=allow_leftmost_as_ghost)
481
parent_ids = self._filter_parent_ids_by_ancestry(parent_ids)
483
if len(parent_ids) == 0:
484
leftmost_parent_id = _mod_revision.NULL_REVISION
485
leftmost_parent_tree = None
487
leftmost_parent_id, leftmost_parent_tree = parents_list[0]
489
if self._change_last_revision(leftmost_parent_id):
490
if leftmost_parent_tree is None:
491
# If we don't have a tree, fall back to reading the
492
# parent tree from the repository.
493
self._cache_basis_inventory(leftmost_parent_id)
495
inv = leftmost_parent_tree.root_inventory
496
xml = self._create_basis_xml_from_inventory(
497
leftmost_parent_id, inv)
498
self._write_basis_inventory(xml)
499
self._set_merges_from_parent_ids(parent_ids)
501
def _cache_basis_inventory(self, new_revision):
502
"""Cache new_revision as the basis inventory."""
503
# TODO: this should allow the ready-to-use inventory to be passed in,
504
# as commit already has that ready-to-use [while the format is the
507
# this double handles the inventory - unpack and repack -
508
# but is easier to understand. We can/should put a conditional
509
# in here based on whether the inventory is in the latest format
510
# - perhaps we should repack all inventories on a repository
512
# the fast path is to copy the raw xml from the repository. If the
513
# xml contains 'revision_id="', then we assume the right
514
# revision_id is set. We must check for this full string, because a
515
# root node id can legitimately look like 'revision_id' but cannot
517
xml = self.branch.repository._get_inventory_xml(new_revision)
518
firstline = xml.split(b'\n', 1)[0]
519
if (b'revision_id="' not in firstline
520
or b'format="7"' not in firstline):
521
inv = self.branch.repository._serializer.read_inventory_from_string(
523
xml = self._create_basis_xml_from_inventory(new_revision, inv)
524
self._write_basis_inventory(xml)
525
except (errors.NoSuchRevision, errors.RevisionNotPresent):
528
def _basis_inventory_name(self):
529
return 'basis-inventory-cache'
531
def _create_basis_xml_from_inventory(self, revision_id, inventory):
532
"""Create the text that will be saved in basis-inventory"""
533
inventory.revision_id = revision_id
534
return xml7.serializer_v7.write_inventory_to_string(inventory)
536
def set_conflicts(self, conflicts):
537
with self.lock_tree_write():
538
self._put_rio('conflicts', conflicts.to_stanzas(),
541
def add_conflicts(self, new_conflicts):
542
with self.lock_tree_write():
543
conflict_set = set(self.conflicts())
544
conflict_set.update(set(list(new_conflicts)))
545
self.set_conflicts(_mod_conflicts.ConflictList(
546
sorted(conflict_set, key=_mod_conflicts.Conflict.sort_key)))
549
with self.lock_read():
551
confile = self._transport.get('conflicts')
552
except errors.NoSuchFile:
553
return _mod_conflicts.ConflictList()
556
if next(confile) != CONFLICT_HEADER_1 + b'\n':
557
raise errors.ConflictFormatError()
558
except StopIteration:
559
raise errors.ConflictFormatError()
560
reader = _mod_rio.RioReader(confile)
561
return _mod_conflicts.ConflictList.from_stanzas(reader)
565
def get_ignore_list(self):
566
"""Return list of ignore patterns.
568
Cached in the Tree object after the first call.
570
ignoreset = getattr(self, '_ignoreset', None)
571
if ignoreset is not None:
575
ignore_globs.update(ignores.get_runtime_ignores())
576
ignore_globs.update(ignores.get_user_ignores())
577
if self.has_filename(self._format.ignore_filename):
578
with self.get_file(self._format.ignore_filename) as f:
579
ignore_globs.update(ignores.parse_ignore_file(f))
580
self._ignoreset = ignore_globs
584
self._flush_ignore_list_cache()
586
def _flush_ignore_list_cache(self):
587
"""Resets the cached ignore list to force a cache rebuild."""
588
self._ignoreset = None
589
self._ignoreglobster = None
591
def is_ignored(self, filename):
592
r"""Check whether the filename matches an ignore pattern.
594
Patterns containing '/' or '\' need to match the whole path;
595
others match against only the last component. Patterns starting
596
with '!' are ignore exceptions. Exceptions take precedence
597
over regular patterns and cause the filename to not be ignored.
599
If the file is ignored, returns the pattern which caused it to
600
be ignored, otherwise None. So this can simply be used as a
601
boolean if desired."""
602
if getattr(self, '_ignoreglobster', None) is None:
603
self._ignoreglobster = globbing.ExceptionGlobster(
604
self.get_ignore_list())
605
return self._ignoreglobster.match(filename)
607
def read_basis_inventory(self):
608
"""Read the cached basis inventory."""
609
path = self._basis_inventory_name()
610
return self._transport.get_bytes(path)
612
def read_working_inventory(self):
613
"""Read the working inventory.
615
:raises errors.InventoryModified: read_working_inventory will fail
616
when the current in memory inventory has been modified.
618
# conceptually this should be an implementation detail of the tree.
619
# XXX: Deprecate this.
620
# ElementTree does its own conversion from UTF-8, so open in
622
with self.lock_read():
623
if self._inventory_is_modified:
624
raise errors.InventoryModified(self)
625
with self._transport.get('inventory') as f:
626
result = self._deserialize(f)
627
self._set_inventory(result, dirty=False)
630
def get_root_id(self):
631
"""Return the id of this trees root"""
632
with self.lock_read():
633
return self._inventory.root.file_id
635
def has_id(self, file_id):
636
# files that have been deleted are excluded
637
inv, inv_file_id = self._unpack_file_id(file_id)
638
if not inv.has_id(inv_file_id):
640
path = inv.id2path(inv_file_id)
641
return osutils.lexists(self.abspath(path))
643
def has_or_had_id(self, file_id):
644
if file_id == self.get_root_id():
646
inv, inv_file_id = self._unpack_file_id(file_id)
647
return inv.has_id(inv_file_id)
649
def all_file_ids(self):
650
"""Iterate through file_ids for this tree.
652
file_ids are in a WorkingTree if they are in the working inventory
653
and the working file exists.
655
return {ie.file_id for path, ie in self.iter_entries_by_dir()}
657
def all_versioned_paths(self):
658
return {path for path, ie in self.iter_entries_by_dir()}
660
def set_last_revision(self, new_revision):
661
"""Change the last revision in the working tree."""
662
with self.lock_tree_write():
663
if self._change_last_revision(new_revision):
664
self._cache_basis_inventory(new_revision)
666
def _get_check_refs(self):
667
"""Return the references needed to perform a check of this tree.
669
The default implementation returns no refs, and is only suitable for
670
trees that have no local caching and can commit on ghosts at any time.
672
:seealso: breezy.check for details about check_refs.
676
def _check(self, references):
677
"""Check the tree for consistency.
679
:param references: A dict with keys matching the items returned by
680
self._get_check_refs(), and values from looking those keys up in
683
with self.lock_read():
684
tree_basis = self.basis_tree()
685
with tree_basis.lock_read():
686
repo_basis = references[('trees', self.last_revision())]
687
if len(list(repo_basis.iter_changes(tree_basis))) > 0:
688
raise errors.BzrCheckError(
689
"Mismatched basis inventory content.")
692
def check_state(self):
693
"""Check that the working state is/isn't valid."""
694
with self.lock_read():
695
check_refs = self._get_check_refs()
697
for ref in check_refs:
700
refs[ref] = self.branch.repository.revision_tree(value)
703
def reset_state(self, revision_ids=None):
704
"""Reset the state of the working tree.
706
This does a hard-reset to a last-known-good state. This is a way to
707
fix if something got corrupted (like the .bzr/checkout/dirstate file)
709
with self.lock_tree_write():
710
if revision_ids is None:
711
revision_ids = self.get_parent_ids()
713
rt = self.branch.repository.revision_tree(
714
_mod_revision.NULL_REVISION)
716
rt = self.branch.repository.revision_tree(revision_ids[0])
717
self._write_inventory(rt.root_inventory)
718
self.set_parent_ids(revision_ids)
721
"""Write the in memory inventory to disk."""
722
# TODO: Maybe this should only write on dirty ?
723
if self._control_files._lock_mode != 'w':
724
raise errors.NotWriteLocked(self)
726
self._serialize(self._inventory, sio)
728
self._transport.put_file('inventory', sio,
729
mode=self.controldir._get_file_mode())
730
self._inventory_is_modified = False
732
def get_file_mtime(self, path):
733
"""See Tree.get_file_mtime."""
735
return os.lstat(self.abspath(path)).st_mtime
737
if e.errno == errno.ENOENT:
738
raise errors.NoSuchFile(path)
741
def _is_executable_from_path_and_stat_from_basis(self, path, stat_result):
743
return self._path2ie(path).executable
744
except errors.NoSuchFile:
745
# For unversioned files on win32, we just assume they are not
749
def _is_executable_from_path_and_stat_from_stat(self, path, stat_result):
750
mode = stat_result.st_mode
751
return bool(stat.S_ISREG(mode) and stat.S_IEXEC & mode)
753
def is_executable(self, path):
754
if not self._supports_executable():
755
ie = self._path2ie(path)
758
mode = os.lstat(self.abspath(path)).st_mode
759
return bool(stat.S_ISREG(mode) and stat.S_IEXEC & mode)
761
def _is_executable_from_path_and_stat(self, path, stat_result):
762
if not self._supports_executable():
763
return self._is_executable_from_path_and_stat_from_basis(
766
return self._is_executable_from_path_and_stat_from_stat(
769
def _add(self, files, ids, kinds):
770
"""See MutableTree._add."""
771
with self.lock_tree_write():
772
# TODO: Re-adding a file that is removed in the working copy
773
# should probably put it back with the previous ID.
774
# the read and write working inventory should not occur in this
775
# function - they should be part of lock_write and unlock.
776
# FIXME: nested trees
777
inv = self.root_inventory
778
for f, file_id, kind in zip(files, ids, kinds):
780
inv.add_path(f, kind=kind)
782
inv.add_path(f, kind=kind, file_id=file_id)
783
self._inventory_is_modified = True
785
def revision_tree(self, revision_id):
786
"""See WorkingTree.revision_id."""
787
if revision_id == self.last_revision():
789
xml = self.read_basis_inventory()
790
except errors.NoSuchFile:
794
inv = xml7.serializer_v7.read_inventory_from_string(xml)
795
# dont use the repository revision_tree api because we want
796
# to supply the inventory.
797
if inv.revision_id == revision_id:
798
return InventoryRevisionTree(
799
self.branch.repository, inv, revision_id)
800
except errors.BadInventoryFormat:
802
# raise if there was no inventory, or if we read the wrong inventory.
803
raise errors.NoSuchRevisionInTree(self, revision_id)
805
def annotate_iter(self, path,
806
default_revision=_mod_revision.CURRENT_REVISION):
807
"""See Tree.annotate_iter
809
This implementation will use the basis tree implementation if possible.
810
Lines not in the basis are attributed to CURRENT_REVISION
812
If there are pending merges, lines added by those merges will be
813
incorrectly attributed to CURRENT_REVISION (but after committing, the
814
attribution will be correct).
816
with self.lock_read():
817
file_id = self.path2id(path)
819
raise errors.NoSuchFile(path)
820
maybe_file_parent_keys = []
821
for parent_id in self.get_parent_ids():
823
parent_tree = self.revision_tree(parent_id)
824
except errors.NoSuchRevisionInTree:
825
parent_tree = self.branch.repository.revision_tree(
827
with parent_tree.lock_read():
830
kind = parent_tree.kind(path)
831
except errors.NoSuchFile:
834
# Note: this is slightly unnecessary, because symlinks
835
# and directories have a "text" which is the empty
836
# text, and we know that won't mess up annotations. But
839
parent_path = parent_tree.id2path(file_id)
842
parent_tree.get_file_revision(parent_path))
843
if parent_text_key not in maybe_file_parent_keys:
844
maybe_file_parent_keys.append(parent_text_key)
845
graph = self.branch.repository.get_file_graph()
846
heads = graph.heads(maybe_file_parent_keys)
847
file_parent_keys = []
848
for key in maybe_file_parent_keys:
850
file_parent_keys.append(key)
852
# Now we have the parents of this content
853
annotator = self.branch.repository.texts.get_annotator()
854
text = self.get_file_text(path)
855
this_key = (file_id, default_revision)
856
annotator.add_special_text(this_key, file_parent_keys, text)
857
annotations = [(key[-1], line)
858
for key, line in annotator.annotate_flat(this_key)]
861
def _put_rio(self, filename, stanzas, header):
862
self._must_be_locked()
863
my_file = _mod_rio.rio_file(stanzas, header)
864
self._transport.put_file(filename, my_file,
865
mode=self.controldir._get_file_mode())
867
def set_merge_modified(self, modified_hashes):
869
for file_id in modified_hashes:
870
yield _mod_rio.Stanza(file_id=file_id.decode('utf8'),
871
hash=modified_hashes[file_id])
872
with self.lock_tree_write():
873
self._put_rio('merge-hashes', iter_stanzas(),
874
MERGE_MODIFIED_HEADER_1)
876
def merge_modified(self):
877
"""Return a dictionary of files modified by a merge.
879
The list is initialized by WorkingTree.set_merge_modified, which is
880
typically called after we make some automatic updates to the tree
883
This returns a map of file_id->sha1, containing only files which are
884
still in the working inventory and have that text hash.
886
with self.lock_read():
888
hashfile = self._transport.get('merge-hashes')
889
except errors.NoSuchFile:
894
if next(hashfile) != MERGE_MODIFIED_HEADER_1 + b'\n':
895
raise errors.MergeModifiedFormatError()
896
except StopIteration:
897
raise errors.MergeModifiedFormatError()
898
for s in _mod_rio.RioReader(hashfile):
899
# RioReader reads in Unicode, so convert file_ids back to
901
file_id = cache_utf8.encode(s.get("file_id"))
902
if not self.has_id(file_id):
904
text_hash = s.get("hash").encode('ascii')
905
path = self.id2path(file_id)
906
if text_hash == self.get_file_sha1(path):
907
merge_hashes[file_id] = text_hash
912
def subsume(self, other_tree):
913
def add_children(inventory, entry):
914
for child_entry in entry.children.values():
915
inventory._byid[child_entry.file_id] = child_entry
916
if child_entry.kind == 'directory':
917
add_children(inventory, child_entry)
918
with self.lock_write():
919
if other_tree.get_root_id() == self.get_root_id():
920
raise errors.BadSubsumeSource(self, other_tree,
921
'Trees have the same root')
923
other_tree_path = self.relpath(other_tree.basedir)
924
except errors.PathNotChild:
925
raise errors.BadSubsumeSource(
926
self, other_tree, 'Tree is not contained by the other')
927
new_root_parent = self.path2id(osutils.dirname(other_tree_path))
928
if new_root_parent is None:
929
raise errors.BadSubsumeSource(
930
self, other_tree, 'Parent directory is not versioned.')
931
# We need to ensure that the result of a fetch will have a
932
# versionedfile for the other_tree root, and only fetching into
933
# RepositoryKnit2 guarantees that.
934
if not self.branch.repository.supports_rich_root():
935
raise errors.SubsumeTargetNeedsUpgrade(other_tree)
936
with other_tree.lock_tree_write():
937
other_root = other_tree.root_inventory.root
938
other_root.parent_id = new_root_parent
939
other_root.name = osutils.basename(other_tree_path)
940
self.root_inventory.add(other_root)
941
add_children(self.root_inventory, other_root)
942
self._write_inventory(self.root_inventory)
943
# normally we don't want to fetch whole repositories, but i
944
# think here we really do want to consolidate the whole thing.
945
for parent_id in other_tree.get_parent_ids():
946
self.branch.fetch(other_tree.branch, parent_id)
947
self.add_parent_tree_id(parent_id)
948
other_tree.controldir.retire_bzrdir()
950
def extract(self, sub_path, format=None):
951
"""Extract a subtree from this tree.
953
A new branch will be created, relative to the path for this tree.
956
segments = osutils.splitpath(path)
957
transport = self.branch.controldir.root_transport
958
for name in segments:
959
transport = transport.clone(name)
960
transport.ensure_base()
963
with self.lock_tree_write():
965
branch_transport = mkdirs(sub_path)
967
format = self.controldir.cloning_metadir()
968
branch_transport.ensure_base()
969
branch_bzrdir = format.initialize_on_transport(branch_transport)
971
repo = branch_bzrdir.find_repository()
972
except errors.NoRepositoryPresent:
973
repo = branch_bzrdir.create_repository()
974
if not repo.supports_rich_root():
975
raise errors.RootNotRich()
976
new_branch = branch_bzrdir.create_branch()
977
new_branch.pull(self.branch)
978
for parent_id in self.get_parent_ids():
979
new_branch.fetch(self.branch, parent_id)
980
tree_transport = self.controldir.root_transport.clone(sub_path)
981
if tree_transport.base != branch_transport.base:
982
tree_bzrdir = format.initialize_on_transport(tree_transport)
983
tree_bzrdir.set_branch_reference(new_branch)
985
tree_bzrdir = branch_bzrdir
986
wt = tree_bzrdir.create_workingtree(_mod_revision.NULL_REVISION)
987
wt.set_parent_ids(self.get_parent_ids())
988
# FIXME: Support nested trees
989
my_inv = self.root_inventory
990
child_inv = inventory.Inventory(root_id=None)
991
file_id = self.path2id(sub_path)
992
new_root = my_inv.get_entry(file_id)
993
my_inv.remove_recursive_id(file_id)
994
new_root.parent_id = None
995
child_inv.add(new_root)
996
self._write_inventory(my_inv)
997
wt._write_inventory(child_inv)
1000
def list_files(self, include_root=False, from_dir=None, recursive=True):
1001
"""List all files as (path, class, kind, id, entry).
1003
Lists, but does not descend into unversioned directories.
1004
This does not include files that have been deleted in this
1005
tree. Skips the control directory.
1007
:param include_root: if True, return an entry for the root
1008
:param from_dir: start from this directory or None for the root
1009
:param recursive: whether to recurse into subdirectories or not
1011
with self.lock_read():
1012
if from_dir is None and include_root is True:
1013
yield ('', 'V', 'directory', self.root_inventory.root)
1014
# Convert these into local objects to save lookup times
1015
pathjoin = osutils.pathjoin
1017
# transport.base ends in a slash, we want the piece
1018
# between the last two slashes
1019
transport_base_dir = self.controldir.transport.base.rsplit(
1023
'directory': TreeDirectory,
1028
# directory file_id, relative path, absolute path, reverse sorted
1030
if from_dir is not None:
1031
inv, from_dir_id = self._path2inv_file_id(from_dir)
1032
if from_dir_id is None:
1033
# Directory not versioned
1035
from_dir_abspath = pathjoin(self.basedir, from_dir)
1037
inv = self.root_inventory
1038
from_dir_id = inv.root.file_id
1039
from_dir_abspath = self.basedir
1040
children = sorted(os.listdir(from_dir_abspath))
1041
# jam 20060527 The kernel sized tree seems equivalent whether we
1042
# use a deque and popleft to keep them sorted, or if we use a plain
1043
# list and just reverse() them.
1044
children = deque(children)
1045
stack = [(from_dir_id, u'', from_dir_abspath, children)]
1047
(from_dir_id, from_dir_relpath, from_dir_abspath,
1048
children) = stack[-1]
1051
f = children.popleft()
1052
# TODO: If we find a subdirectory with its own .bzr
1053
# directory, then that is a separate tree and we
1054
# should exclude it.
1056
# the bzrdir for this tree
1057
if transport_base_dir == f:
1060
# we know that from_dir_relpath and from_dir_abspath never
1061
# end in a slash and 'f' doesn't begin with one, we can do
1062
# a string op, rather than the checks of pathjoin(), all
1063
# relative paths will have an extra slash at the beginning
1064
fp = from_dir_relpath + '/' + f
1067
fap = from_dir_abspath + '/' + f
1069
dir_ie = inv.get_entry(from_dir_id)
1070
if dir_ie.kind == 'directory':
1071
f_ie = dir_ie.children.get(f)
1076
elif self.is_ignored(fp[1:]):
1079
# we may not have found this file, because of a unicode
1080
# issue, or because the directory was actually a
1082
f_norm, can_access = osutils.normalized_filename(f)
1083
if f == f_norm or not can_access:
1084
# No change, so treat this file normally
1087
# this file can be accessed by a normalized path
1088
# check again if it is versioned
1089
# these lines are repeated here for performance
1091
fp = from_dir_relpath + '/' + f
1092
fap = from_dir_abspath + '/' + f
1093
f_ie = inv.get_child(from_dir_id, f)
1096
elif self.is_ignored(fp[1:]):
1101
fk = osutils.file_kind(fap)
1103
# make a last minute entry
1105
yield fp[1:], c, fk, f_ie
1108
yield fp[1:], c, fk, fk_entries[fk]()
1110
yield fp[1:], c, fk, TreeEntry()
1113
if fk != 'directory':
1116
# But do this child first if recursing down
1118
new_children = sorted(os.listdir(fap))
1119
new_children = deque(new_children)
1120
stack.append((f_ie.file_id, fp, fap, new_children))
1121
# Break out of inner loop,
1122
# so that we start outer loop with child
1125
# if we finished all children, pop it off the stack
1128
def move(self, from_paths, to_dir=None, after=False):
1131
to_dir must exist in the inventory.
1133
If to_dir exists and is a directory, the files are moved into
1134
it, keeping their old names.
1136
Note that to_dir is only the last component of the new name;
1137
this doesn't change the directory.
1139
For each entry in from_paths the move mode will be determined
1142
The first mode moves the file in the filesystem and updates the
1143
inventory. The second mode only updates the inventory without
1144
touching the file on the filesystem.
1146
move uses the second mode if 'after == True' and the target is
1147
either not versioned or newly added, and present in the working tree.
1149
move uses the second mode if 'after == False' and the source is
1150
versioned but no longer in the working tree, and the target is not
1151
versioned but present in the working tree.
1153
move uses the first mode if 'after == False' and the source is
1154
versioned and present in the working tree, and the target is not
1155
versioned and not present in the working tree.
1157
Everything else results in an error.
1159
This returns a list of (from_path, to_path) pairs for each
1160
entry that is moved.
1165
# check for deprecated use of signature
1167
raise TypeError('You must supply a target directory')
1168
# check destination directory
1169
if isinstance(from_paths, (str, text_type)):
1171
with self.lock_tree_write():
1172
to_abs = self.abspath(to_dir)
1173
if not osutils.isdir(to_abs):
1174
raise errors.BzrMoveFailedError(
1175
'', to_dir, errors.NotADirectory(to_abs))
1176
if not self.has_filename(to_dir):
1177
raise errors.BzrMoveFailedError(
1178
'', to_dir, errors.NotInWorkingDirectory(to_dir))
1179
to_inv, to_dir_id = self._path2inv_file_id(to_dir)
1180
if to_dir_id is None:
1181
raise errors.BzrMoveFailedError(
1182
'', to_dir, errors.NotVersionedError(path=to_dir))
1184
to_dir_ie = to_inv.get_entry(to_dir_id)
1185
if to_dir_ie.kind != 'directory':
1186
raise errors.BzrMoveFailedError(
1187
'', to_dir, errors.NotADirectory(to_abs))
1189
# create rename entries and tuples
1190
for from_rel in from_paths:
1191
from_tail = osutils.splitpath(from_rel)[-1]
1192
from_inv, from_id = self._path2inv_file_id(from_rel)
1194
raise errors.BzrMoveFailedError(from_rel, to_dir,
1195
errors.NotVersionedError(path=from_rel))
1197
from_entry = from_inv.get_entry(from_id)
1198
from_parent_id = from_entry.parent_id
1199
to_rel = osutils.pathjoin(to_dir, from_tail)
1200
rename_entry = InventoryWorkingTree._RenameEntry(
1203
from_tail=from_tail,
1204
from_parent_id=from_parent_id,
1205
to_rel=to_rel, to_tail=from_tail,
1206
to_parent_id=to_dir_id)
1207
rename_entries.append(rename_entry)
1208
rename_tuples.append((from_rel, to_rel))
1210
# determine which move mode to use. checks also for movability
1211
rename_entries = self._determine_mv_mode(rename_entries, after)
1213
original_modified = self._inventory_is_modified
1216
self._inventory_is_modified = True
1217
self._move(rename_entries)
1218
except BaseException:
1219
# restore the inventory on error
1220
self._inventory_is_modified = original_modified
1222
# FIXME: Should potentially also write the from_invs
1223
self._write_inventory(to_inv)
1224
return rename_tuples
1226
def rename_one(self, from_rel, to_rel, after=False):
1229
This can change the directory or the filename or both.
1231
rename_one has several 'modes' to work. First, it can rename a physical
1232
file and change the file_id. That is the normal mode. Second, it can
1233
only change the file_id without touching any physical file.
1235
rename_one uses the second mode if 'after == True' and 'to_rel' is not
1236
versioned but present in the working tree.
1238
rename_one uses the second mode if 'after == False' and 'from_rel' is
1239
versioned but no longer in the working tree, and 'to_rel' is not
1240
versioned but present in the working tree.
1242
rename_one uses the first mode if 'after == False' and 'from_rel' is
1243
versioned and present in the working tree, and 'to_rel' is not
1244
versioned and not present in the working tree.
1246
Everything else results in an error.
1248
with self.lock_tree_write():
1251
# create rename entries and tuples
1252
from_tail = osutils.splitpath(from_rel)[-1]
1253
from_inv, from_id = self._path2inv_file_id(from_rel)
1255
# if file is missing in the inventory maybe it's in the
1257
basis_tree = self.branch.basis_tree()
1258
from_id = basis_tree.path2id(from_rel)
1260
raise errors.BzrRenameFailedError(
1262
errors.NotVersionedError(path=from_rel))
1264
from_entry = from_inv.get_entry(from_id)
1265
except errors.NoSuchId:
1266
# put entry back in the inventory so we can rename it
1267
from_entry = basis_tree.root_inventory.get_entry(
1269
from_inv.add(from_entry)
1271
from_inv, from_inv_id = self._unpack_file_id(from_id)
1272
from_entry = from_inv.get_entry(from_inv_id)
1273
from_parent_id = from_entry.parent_id
1274
to_dir, to_tail = os.path.split(to_rel)
1275
to_inv, to_dir_id = self._path2inv_file_id(to_dir)
1276
rename_entry = InventoryWorkingTree._RenameEntry(
1279
from_tail=from_tail,
1280
from_parent_id=from_parent_id,
1281
to_rel=to_rel, to_tail=to_tail,
1282
to_parent_id=to_dir_id)
1283
rename_entries.append(rename_entry)
1285
# determine which move mode to use. checks also for movability
1286
rename_entries = self._determine_mv_mode(rename_entries, after)
1288
# check if the target changed directory and if the target directory
1290
if to_dir_id is None:
1291
raise errors.BzrMoveFailedError(
1292
from_rel, to_rel, errors.NotVersionedError(path=to_dir))
1294
# all checks done. now we can continue with our actual work
1295
mutter('rename_one:\n'
1300
' to_dir_id {%s}\n',
1301
from_id, from_rel, to_rel, to_dir, to_dir_id)
1303
self._move(rename_entries)
1304
self._write_inventory(to_inv)
1306
class _RenameEntry(object):
1307
def __init__(self, from_rel, from_id, from_tail, from_parent_id,
1308
to_rel, to_tail, to_parent_id, only_change_inv=False,
1310
self.from_rel = from_rel
1311
self.from_id = from_id
1312
self.from_tail = from_tail
1313
self.from_parent_id = from_parent_id
1314
self.to_rel = to_rel
1315
self.to_tail = to_tail
1316
self.to_parent_id = to_parent_id
1317
self.change_id = change_id
1318
self.only_change_inv = only_change_inv
1320
def _determine_mv_mode(self, rename_entries, after=False):
1321
"""Determines for each from-to pair if both inventory and working tree
1322
or only the inventory has to be changed.
1324
Also does basic plausability tests.
1326
# FIXME: Handling of nested trees
1327
inv = self.root_inventory
1329
for rename_entry in rename_entries:
1330
# store to local variables for easier reference
1331
from_rel = rename_entry.from_rel
1332
from_id = rename_entry.from_id
1333
to_rel = rename_entry.to_rel
1334
to_id = inv.path2id(to_rel)
1335
only_change_inv = False
1337
# check the inventory for source and destination
1339
raise errors.BzrMoveFailedError(
1340
from_rel, to_rel, errors.NotVersionedError(path=from_rel))
1341
if to_id is not None:
1343
# allow it with --after but only if dest is newly added
1345
basis = self.basis_tree()
1346
with basis.lock_read():
1347
if not basis.has_id(to_id):
1348
rename_entry.change_id = True
1351
raise errors.BzrMoveFailedError(
1353
errors.AlreadyVersionedError(path=to_rel))
1355
# try to determine the mode for rename (only change inv or change
1356
# inv and file system)
1358
if not self.has_filename(to_rel):
1359
raise errors.BzrMoveFailedError(
1363
extra="New file has not been created yet"))
1364
only_change_inv = True
1365
elif not self.has_filename(from_rel) and self.has_filename(to_rel):
1366
only_change_inv = True
1367
elif self.has_filename(from_rel) and not self.has_filename(to_rel):
1368
only_change_inv = False
1369
elif (not self.case_sensitive and
1370
from_rel.lower() == to_rel.lower() and
1371
self.has_filename(from_rel)):
1372
only_change_inv = False
1374
# something is wrong, so lets determine what exactly
1375
if not self.has_filename(from_rel) and \
1376
not self.has_filename(to_rel):
1377
raise errors.BzrRenameFailedError(
1379
errors.PathsDoNotExist(paths=(from_rel, to_rel)))
1381
raise errors.RenameFailedFilesExist(from_rel, to_rel)
1382
rename_entry.only_change_inv = only_change_inv
1383
return rename_entries
1385
def _move(self, rename_entries):
1386
"""Moves a list of files.
1388
Depending on the value of the flag 'only_change_inv', the
1389
file will be moved on the file system or not.
1393
for entry in rename_entries:
1395
self._move_entry(entry)
1396
except BaseException:
1397
self._rollback_move(moved)
1401
def _rollback_move(self, moved):
1402
"""Try to rollback a previous move in case of an filesystem error."""
1405
self._move_entry(WorkingTree._RenameEntry(
1406
entry.to_rel, entry.from_id,
1407
entry.to_tail, entry.to_parent_id, entry.from_rel,
1408
entry.from_tail, entry.from_parent_id,
1409
entry.only_change_inv))
1410
except errors.BzrMoveFailedError as e:
1411
raise errors.BzrMoveFailedError(
1412
'', '', "Rollback failed."
1413
" The working tree is in an inconsistent state."
1414
" Please consider doing a 'bzr revert'."
1415
" Error message is: %s" % e)
1417
def _move_entry(self, entry):
1418
inv = self.root_inventory
1419
from_rel_abs = self.abspath(entry.from_rel)
1420
to_rel_abs = self.abspath(entry.to_rel)
1421
if from_rel_abs == to_rel_abs:
1422
raise errors.BzrMoveFailedError(entry.from_rel, entry.to_rel,
1423
"Source and target are identical.")
1425
if not entry.only_change_inv:
1427
osutils.rename(from_rel_abs, to_rel_abs)
1428
except OSError as e:
1429
raise errors.BzrMoveFailedError(
1430
entry.from_rel, entry.to_rel, e[1])
1432
to_id = inv.path2id(entry.to_rel)
1433
inv.remove_recursive_id(to_id)
1434
inv.rename(entry.from_id, entry.to_parent_id, entry.to_tail)
1436
def unversion(self, paths):
1437
"""Remove the paths in paths from the current versioned set.
1439
When a path is unversioned, all of its children are automatically
1442
:param paths: The paths to stop versioning.
1443
:raises NoSuchFile: if any path is not currently versioned.
1445
with self.lock_tree_write():
1448
file_id = self._inventory.path2id(path)
1450
raise errors.NoSuchFile(path, self)
1451
file_ids.add(file_id)
1452
for file_id in file_ids:
1453
if self._inventory.has_id(file_id):
1454
self._inventory.remove_recursive_id(file_id)
1456
# in the future this should just set a dirty bit to wait for
1457
# the final unlock. However, until all methods of workingtree
1458
# start with the current in -memory inventory rather than
1459
# triggering a read, it is more complex - we need to teach
1460
# read_inventory to know when to read, and when to not read
1461
# first... and possibly to save first when the in memory one
1462
# may be corrupted. so for now, we just only write it if it is
1463
# indeed dirty. - RBC 20060907
1464
self._write_inventory(self._inventory)
1466
def stored_kind(self, path):
1467
"""See Tree.stored_kind"""
1468
return self._path2ie(path).kind
1471
"""Yield all unversioned files in this WorkingTree.
1473
If there are any unversioned directories then only the directory is
1474
returned, not all its children. But if there are unversioned files
1475
under a versioned subdirectory, they are returned.
1477
Currently returned depth-first, sorted by name within directories.
1478
This is the same order used by 'osutils.walkdirs'.
1480
# TODO: Work from given directory downwards
1481
for path, dir_entry in self.iter_entries_by_dir():
1482
if dir_entry.kind != 'directory':
1484
# mutter("search for unknowns in %r", path)
1485
dirabs = self.abspath(path)
1486
if not osutils.isdir(dirabs):
1487
# e.g. directory deleted
1491
for subf in os.listdir(dirabs.encode(osutils._fs_enc)):
1493
subf = subf.decode(osutils._fs_enc)
1494
except UnicodeDecodeError:
1495
path_os_enc = path.encode(osutils._fs_enc)
1496
relpath = path_os_enc + b'/' + subf
1497
raise errors.BadFilenameEncoding(relpath,
1500
if self.controldir.is_control_filename(subf):
1502
if subf not in dir_entry.children:
1505
can_access) = osutils.normalized_filename(subf)
1506
except UnicodeDecodeError:
1507
path_os_enc = path.encode(osutils._fs_enc)
1508
relpath = path_os_enc + '/' + subf
1509
raise errors.BadFilenameEncoding(relpath,
1511
if subf_norm != subf and can_access:
1512
if subf_norm not in dir_entry.children:
1513
fl.append(subf_norm)
1519
subp = osutils.pathjoin(path, subf)
1522
def walkdirs(self, prefix=""):
1523
"""Walk the directories of this tree.
1525
returns a generator which yields items in the form:
1526
((curren_directory_path, fileid),
1527
[(file1_path, file1_name, file1_kind, (lstat), file1_id,
1530
This API returns a generator, which is only valid during the current
1531
tree transaction - within a single lock_read or lock_write duration.
1533
If the tree is not locked, it may cause an error to be raised,
1534
depending on the tree implementation.
1536
disk_top = self.abspath(prefix)
1537
if disk_top.endswith('/'):
1538
disk_top = disk_top[:-1]
1539
top_strip_len = len(disk_top) + 1
1540
inventory_iterator = self._walkdirs(prefix)
1541
disk_iterator = osutils.walkdirs(disk_top, prefix)
1543
current_disk = next(disk_iterator)
1544
disk_finished = False
1545
except OSError as e:
1546
if not (e.errno == errno.ENOENT
1547
or (sys.platform == 'win32' and e.errno == ERROR_PATH_NOT_FOUND)):
1550
disk_finished = True
1552
current_inv = next(inventory_iterator)
1553
inv_finished = False
1554
except StopIteration:
1557
while not inv_finished or not disk_finished:
1559
((cur_disk_dir_relpath, cur_disk_dir_path_from_top),
1560
cur_disk_dir_content) = current_disk
1562
((cur_disk_dir_relpath, cur_disk_dir_path_from_top),
1563
cur_disk_dir_content) = ((None, None), None)
1564
if not disk_finished:
1565
# strip out .bzr dirs
1566
if (cur_disk_dir_path_from_top[top_strip_len:] == ''
1567
and len(cur_disk_dir_content) > 0):
1568
# osutils.walkdirs can be made nicer -
1569
# yield the path-from-prefix rather than the pathjoined
1571
bzrdir_loc = bisect_left(cur_disk_dir_content,
1573
if (bzrdir_loc < len(cur_disk_dir_content) and
1574
self.controldir.is_control_filename(
1575
cur_disk_dir_content[bzrdir_loc][0])):
1576
# we dont yield the contents of, or, .bzr itself.
1577
del cur_disk_dir_content[bzrdir_loc]
1579
# everything is unknown
1582
# everything is missing
1585
direction = ((current_inv[0][0] > cur_disk_dir_relpath)
1586
- (current_inv[0][0] < cur_disk_dir_relpath))
1589
# disk is before inventory - unknown
1590
dirblock = [(relpath, basename, kind, stat, None, None) for
1591
relpath, basename, kind, stat, top_path in
1592
cur_disk_dir_content]
1593
yield (cur_disk_dir_relpath, None), dirblock
1595
current_disk = next(disk_iterator)
1596
except StopIteration:
1597
disk_finished = True
1599
# inventory is before disk - missing.
1600
dirblock = [(relpath, basename, 'unknown', None, fileid, kind)
1601
for relpath, basename, dkind, stat, fileid, kind in
1603
yield (current_inv[0][0], current_inv[0][1]), dirblock
1605
current_inv = next(inventory_iterator)
1606
except StopIteration:
1609
# versioned present directory
1610
# merge the inventory and disk data together
1612
for relpath, subiterator in itertools.groupby(sorted(
1613
current_inv[1] + cur_disk_dir_content,
1614
key=operator.itemgetter(0)), operator.itemgetter(1)):
1615
path_elements = list(subiterator)
1616
if len(path_elements) == 2:
1617
inv_row, disk_row = path_elements
1618
# versioned, present file
1619
dirblock.append((inv_row[0],
1620
inv_row[1], disk_row[2],
1621
disk_row[3], inv_row[4],
1623
elif len(path_elements[0]) == 5:
1626
(path_elements[0][0], path_elements[0][1],
1627
path_elements[0][2], path_elements[0][3], None,
1629
elif len(path_elements[0]) == 6:
1630
# versioned, absent file.
1632
(path_elements[0][0], path_elements[0][1],
1633
'unknown', None, path_elements[0][4],
1634
path_elements[0][5]))
1636
raise NotImplementedError('unreachable code')
1637
yield current_inv[0], dirblock
1639
current_inv = next(inventory_iterator)
1640
except StopIteration:
1643
current_disk = next(disk_iterator)
1644
except StopIteration:
1645
disk_finished = True
1647
def _walkdirs(self, prefix=""):
1648
"""Walk the directories of this tree.
1650
:param prefix: is used as the directrory to start with.
1651
:returns: a generator which yields items in the form::
1653
((curren_directory_path, fileid),
1654
[(file1_path, file1_name, file1_kind, None, file1_id,
1657
_directory = 'directory'
1658
# get the root in the inventory
1659
inv, top_id = self._path2inv_file_id(prefix)
1663
pending = [(prefix, '', _directory, None, top_id, None)]
1666
currentdir = pending.pop()
1667
# 0 - relpath, 1- basename, 2- kind, 3- stat, 4-id, 5-kind
1668
top_id = currentdir[4]
1670
relroot = currentdir[0] + '/'
1673
# FIXME: stash the node in pending
1674
entry = inv.get_entry(top_id)
1675
if entry.kind == 'directory':
1676
for name, child in entry.sorted_children():
1677
dirblock.append((relroot + name, name, child.kind, None,
1678
child.file_id, child.kind
1680
yield (currentdir[0], entry.file_id), dirblock
1681
# push the user specified dirs from dirblock
1682
for dir in reversed(dirblock):
1683
if dir[2] == _directory:
1686
def update_feature_flags(self, updated_flags):
1687
"""Update the feature flags for this branch.
1689
:param updated_flags: Dictionary mapping feature names to necessities
1690
A necessity can be None to indicate the feature should be removed
1692
with self.lock_write():
1693
self._format._update_feature_flags(updated_flags)
1694
self.control_transport.put_bytes(
1695
'format', self._format.as_string())
1697
def _check_for_tree_references(self, iterator):
1698
"""See if directories have become tree-references."""
1699
blocked_parent_ids = set()
1700
for path, ie in iterator:
1701
if ie.parent_id in blocked_parent_ids:
1702
# This entry was pruned because one of its parents became a
1703
# TreeReference. If this is a directory, mark it as blocked.
1704
if ie.kind == 'directory':
1705
blocked_parent_ids.add(ie.file_id)
1707
if (ie.kind == 'directory' and
1708
self._directory_is_tree_reference(path)):
1709
# This InventoryDirectory needs to be a TreeReference
1710
ie = inventory.TreeReference(ie.file_id, ie.name, ie.parent_id)
1711
blocked_parent_ids.add(ie.file_id)
1714
def iter_entries_by_dir(self, specific_files=None):
1715
"""See Tree.iter_entries_by_dir()"""
1716
# The only trick here is that if we supports_tree_reference then we
1717
# need to detect if a directory becomes a tree-reference.
1718
iterator = super(WorkingTree, self).iter_entries_by_dir(
1719
specific_files=specific_files)
1720
if not self.supports_tree_reference():
1723
return self._check_for_tree_references(iterator)
1725
def get_canonical_paths(self, paths):
1726
"""Look up canonical paths for multiple items.
1728
:param paths: A sequence of paths relative to the root of the tree.
1729
:return: A iterator over paths, with each item the corresponding input
1730
path adjusted to account for existing elements that match case
1733
with self.lock_read():
1734
if not self.case_sensitive:
1737
elif sys.platform == 'darwin':
1741
return unicodedata.normalize('NFC', x)
1745
if normalize is None or self.is_versioned(path):
1748
yield get_canonical_path(self, path, normalize)
1751
class WorkingTreeFormatMetaDir(bzrdir.BzrFormat, WorkingTreeFormat):
1752
"""Base class for working trees that live in bzr meta directories."""
1754
ignore_filename = '.bzrignore'
1757
WorkingTreeFormat.__init__(self)
1758
bzrdir.BzrFormat.__init__(self)
1761
def find_format_string(klass, controldir):
1762
"""Return format name for the working tree object in controldir."""
1764
transport = controldir.get_workingtree_transport(None)
1765
return transport.get_bytes("format")
1766
except errors.NoSuchFile:
1767
raise errors.NoWorkingTree(base=transport.base)
1770
def find_format(klass, controldir):
1771
"""Return the format for the working tree object in controldir."""
1772
format_string = klass.find_format_string(controldir)
1773
return klass._find_format(format_registry, 'working tree',
1776
def check_support_status(self, allow_unsupported, recommend_upgrade=True,
1778
WorkingTreeFormat.check_support_status(
1779
self, allow_unsupported=allow_unsupported,
1780
recommend_upgrade=recommend_upgrade, basedir=basedir)
1781
bzrdir.BzrFormat.check_support_status(
1782
self, allow_unsupported=allow_unsupported,
1783
recommend_upgrade=recommend_upgrade, basedir=basedir)