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
"""Tree classes, representing directory at point in time.
20
from __future__ import absolute_import
31
from ..mutabletree import (
34
from ..revisiontree import (
37
lazy_import.lazy_import(globals(), """
42
transport as _mod_transport,
44
from breezy.bzr import (
45
inventory as _mod_inventory,
48
from ..sixish import (
52
FileTimestampUnavailable,
58
class InventoryTree(Tree):
59
"""A tree that relies on an inventory for its metadata.
61
Trees contain an `Inventory` object, and also know how to retrieve
62
file texts mentioned in the inventory, either from a working
63
directory or from a store.
65
It is possible for trees to contain files that are not described
66
in their inventory or vice versa; for this use `filenames()`.
68
Subclasses should set the _inventory attribute, which is considered
69
private to external API users.
72
def _get_root_inventory(self):
73
return self._inventory
75
root_inventory = property(_get_root_inventory,
76
doc="Root inventory of this tree")
78
def _unpack_file_id(self, file_id):
79
"""Find the inventory and inventory file id for a tree file id.
81
:param file_id: The tree file id, as bytestring or tuple
82
:return: Inventory and inventory file id
84
if isinstance(file_id, tuple):
87
"nested trees not yet supported: %r" % file_id)
89
return self.root_inventory, file_id
91
def find_related_paths_across_trees(self, paths, trees=[],
92
require_versioned=True):
93
"""Find related paths in tree corresponding to specified filenames in any
96
All matches in all trees will be used, and all children of matched
97
directories will be used.
99
:param paths: The filenames to find related paths for (if None, returns
101
:param trees: The trees to find file_ids within
102
:param require_versioned: if true, all specified filenames must occur in
104
:return: a set of paths for the specified filenames and their children
109
file_ids = self.paths2ids(
110
paths, trees, require_versioned=require_versioned)
112
for file_id in file_ids:
114
ret.add(self.id2path(file_id))
115
except errors.NoSuchId:
119
def paths2ids(self, paths, trees=[], require_versioned=True):
120
"""Return all the ids that can be reached by walking from paths.
122
Each path is looked up in this tree and any extras provided in
123
trees, and this is repeated recursively: the children in an extra tree
124
of a directory that has been renamed under a provided path in this tree
125
are all returned, even if none exist under a provided path in this
126
tree, and vice versa.
128
:param paths: An iterable of paths to start converting to ids from.
129
Alternatively, if paths is None, no ids should be calculated and None
130
will be returned. This is offered to make calling the api unconditional
131
for code that *might* take a list of files.
132
:param trees: Additional trees to consider.
133
:param require_versioned: If False, do not raise NotVersionedError if
134
an element of paths is not versioned in this tree and all of trees.
136
return find_ids_across_trees(paths, [self] + list(trees), require_versioned)
138
def path2id(self, path):
139
"""Return the id for path in this tree."""
140
with self.lock_read():
141
return self._path2inv_file_id(path)[1]
143
def _path2ie(self, path):
144
"""Lookup an inventory entry by path.
146
:param path: Path to look up
147
:return: InventoryEntry
149
ie = self.root_inventory.get_entry_by_path(path)
151
raise errors.NoSuchFile(path)
154
def _path2inv_file_id(self, path):
155
"""Lookup a inventory and inventory file id by path.
157
:param path: Path to look up
158
:return: tuple with inventory and inventory file id
160
# FIXME: Support nested trees
161
inv = self.root_inventory
162
inv_file_id = self.root_inventory.path2id(path)
163
return inv, inv_file_id
165
def id2path(self, file_id):
166
"""Return the path for a file id.
170
inventory, file_id = self._unpack_file_id(file_id)
171
return inventory.id2path(file_id)
173
def has_id(self, file_id):
174
inventory, file_id = self._unpack_file_id(file_id)
175
return inventory.has_id(file_id)
177
def has_or_had_id(self, file_id):
178
inventory, file_id = self._unpack_file_id(file_id)
179
return inventory.has_id(file_id)
181
def all_file_ids(self):
182
return {entry.file_id for path, entry in self.iter_entries_by_dir()}
184
def all_versioned_paths(self):
185
return {path for path, entry in self.iter_entries_by_dir()}
187
def iter_entries_by_dir(self, specific_files=None):
188
"""Walk the tree in 'by_dir' order.
190
This will yield each entry in the tree as a (path, entry) tuple.
191
The order that they are yielded is:
193
See Tree.iter_entries_by_dir for details.
195
with self.lock_read():
196
if specific_files is not None:
197
inventory_file_ids = []
198
for path in specific_files:
199
inventory, inv_file_id = self._path2inv_file_id(path)
200
if inventory is not self.root_inventory: # for now
201
raise AssertionError("%r != %r" % (
202
inventory, self.root_inventory))
203
inventory_file_ids.append(inv_file_id)
205
inventory_file_ids = None
206
# FIXME: Handle nested trees
207
return self.root_inventory.iter_entries_by_dir(
208
specific_file_ids=inventory_file_ids)
210
def iter_child_entries(self, path, file_id=None):
211
with self.lock_read():
212
ie = self._path2ie(path)
213
if ie.kind != 'directory':
214
raise errors.NotADirectory(path)
215
return iter(viewvalues(ie.children))
217
def _get_plan_merge_data(self, file_id, other, base):
218
from . import versionedfile
219
vf = versionedfile._PlanMergeVersionedFile(file_id)
220
last_revision_a = self._get_file_revision(
221
self.id2path(file_id), file_id, vf, b'this:')
222
last_revision_b = other._get_file_revision(
223
other.id2path(file_id), file_id, vf, b'other:')
225
last_revision_base = None
227
last_revision_base = base._get_file_revision(
228
base.id2path(file_id), file_id, vf, b'base:')
229
return vf, last_revision_a, last_revision_b, last_revision_base
231
def plan_file_merge(self, file_id, other, base=None):
232
"""Generate a merge plan based on annotations.
234
If the file contains uncommitted changes in this tree, they will be
235
attributed to the 'current:' pseudo-revision. If the file contains
236
uncommitted changes in the other tree, they will be assigned to the
237
'other:' pseudo-revision.
239
data = self._get_plan_merge_data(file_id, other, base)
240
vf, last_revision_a, last_revision_b, last_revision_base = data
241
return vf.plan_merge(last_revision_a, last_revision_b,
244
def plan_file_lca_merge(self, file_id, other, base=None):
245
"""Generate a merge plan based lca-newness.
247
If the file contains uncommitted changes in this tree, they will be
248
attributed to the 'current:' pseudo-revision. If the file contains
249
uncommitted changes in the other tree, they will be assigned to the
250
'other:' pseudo-revision.
252
data = self._get_plan_merge_data(file_id, other, base)
253
vf, last_revision_a, last_revision_b, last_revision_base = data
254
return vf.plan_lca_merge(last_revision_a, last_revision_b,
257
def _get_file_revision(self, path, file_id, vf, tree_revision):
258
"""Ensure that file_id, tree_revision is in vf to plan the merge."""
259
if getattr(self, '_repository', None) is None:
260
last_revision = tree_revision
262
(file_id, t.get_file_revision(path)) for t in
263
self._iter_parent_trees()]
264
vf.add_lines((file_id, last_revision), parent_keys,
265
self.get_file_lines(path))
266
repo = self.branch.repository
269
last_revision = self.get_file_revision(path)
270
base_vf = self._repository.texts
271
if base_vf not in vf.fallback_versionedfiles:
272
vf.fallback_versionedfiles.append(base_vf)
276
def find_ids_across_trees(filenames, trees, require_versioned=True):
277
"""Find the ids corresponding to specified filenames.
279
All matches in all trees will be used, and all children of matched
280
directories will be used.
282
:param filenames: The filenames to find file_ids for (if None, returns
284
:param trees: The trees to find file_ids within
285
:param require_versioned: if true, all specified filenames must occur in
287
:return: a set of file ids for the specified filenames and their children.
291
specified_path_ids = _find_ids_across_trees(filenames, trees,
293
return _find_children_across_trees(specified_path_ids, trees)
296
def _find_ids_across_trees(filenames, trees, require_versioned):
297
"""Find the ids corresponding to specified filenames.
299
All matches in all trees will be used, but subdirectories are not scanned.
301
:param filenames: The filenames to find file_ids for
302
:param trees: The trees to find file_ids within
303
:param require_versioned: if true, all specified filenames must occur in
305
:return: a set of file ids for the specified filenames
308
interesting_ids = set()
309
for tree_path in filenames:
312
file_id = tree.path2id(tree_path)
313
if file_id is not None:
314
interesting_ids.add(file_id)
317
not_versioned.append(tree_path)
318
if len(not_versioned) > 0 and require_versioned:
319
raise errors.PathsNotVersionedError(not_versioned)
320
return interesting_ids
323
def _find_children_across_trees(specified_ids, trees):
324
"""Return a set including specified ids and their children.
326
All matches in all trees will be used.
328
:param trees: The trees to find file_ids within
329
:return: a set containing all specified ids and their children
331
interesting_ids = set(specified_ids)
332
pending = interesting_ids
333
# now handle children of interesting ids
334
# we loop so that we handle all children of each id in both trees
335
while len(pending) > 0:
337
for file_id in pending:
340
path = tree.id2path(file_id)
341
except errors.NoSuchId:
344
for child in tree.iter_child_entries(path):
345
if child.file_id not in interesting_ids:
346
new_pending.add(child.file_id)
347
except errors.NotADirectory:
349
interesting_ids.update(new_pending)
350
pending = new_pending
351
return interesting_ids
354
class MutableInventoryTree(MutableTree, InventoryTree):
356
def apply_inventory_delta(self, changes):
357
"""Apply changes to the inventory as an atomic operation.
359
:param changes: An inventory delta to apply to the working tree's
362
:seealso Inventory.apply_delta: For details on the changes parameter.
364
with self.lock_tree_write():
366
inv = self.root_inventory
367
inv.apply_delta(changes)
368
self._write_inventory(inv)
370
def _fix_case_of_inventory_path(self, path):
371
"""If our tree isn't case sensitive, return the canonical path"""
372
if not self.case_sensitive:
373
path = self.get_canonical_path(path)
376
def smart_add(self, file_list, recurse=True, action=None, save=True):
377
"""Version file_list, optionally recursing into directories.
379
This is designed more towards DWIM for humans than API clarity.
380
For the specific behaviour see the help for cmd_add().
382
:param file_list: List of zero or more paths. *NB: these are
383
interpreted relative to the process cwd, not relative to the
384
tree.* (Add and most other tree methods use tree-relative
386
:param action: A reporter to be called with the inventory, parent_ie,
387
path and kind of the path being added. It may return a file_id if
388
a specific one should be used.
389
:param save: Save the inventory after completing the adds. If False
390
this provides dry-run functionality by doing the add and not saving
392
:return: A tuple - files_added, ignored_files. files_added is the count
393
of added files, and ignored_files is a dict mapping files that were
394
ignored to the rule that caused them to be ignored.
396
with self.lock_tree_write():
397
# Not all mutable trees can have conflicts
398
if getattr(self, 'conflicts', None) is not None:
399
# Collect all related files without checking whether they exist or
400
# are versioned. It's cheaper to do that once for all conflicts
401
# than trying to find the relevant conflict for each added file.
402
conflicts_related = set()
403
for c in self.conflicts():
404
conflicts_related.update(c.associated_filenames())
406
conflicts_related = None
407
adder = _SmartAddHelper(self, action, conflicts_related)
408
adder.add(file_list, recurse=recurse)
410
invdelta = adder.get_inventory_delta()
411
self.apply_inventory_delta(invdelta)
412
return adder.added, adder.ignored
414
def update_basis_by_delta(self, new_revid, delta):
415
"""Update the parents of this tree after a commit.
417
This gives the tree one parent, with revision id new_revid. The
418
inventory delta is applied to the current basis tree to generate the
419
inventory for the parent new_revid, and all other parent trees are
422
All the changes in the delta should be changes synchronising the basis
423
tree with some or all of the working tree, with a change to a directory
424
requiring that its contents have been recursively included. That is,
425
this is not a general purpose tree modification routine, but a helper
426
for commit which is not required to handle situations that do not arise
429
See the inventory developers documentation for the theory behind
432
:param new_revid: The new revision id for the trees parent.
433
:param delta: An inventory delta (see apply_inventory_delta) describing
434
the changes from the current left most parent revision to new_revid.
436
# if the tree is updated by a pull to the branch, as happens in
437
# WorkingTree2, when there was no separation between branch and tree,
438
# then just clear merges, efficiency is not a concern for now as this
439
# is legacy environments only, and they are slow regardless.
440
if self.last_revision() == new_revid:
441
self.set_parent_ids([new_revid])
443
# generic implementation based on Inventory manipulation. See
444
# WorkingTree classes for optimised versions for specific format trees.
445
basis = self.basis_tree()
446
with basis.lock_read():
447
# TODO: Consider re-evaluating the need for this with CHKInventory
448
# we don't strictly need to mutate an inventory for this
449
# it only makes sense when apply_delta is cheaper than get_inventory()
450
inventory = _mod_inventory.mutable_inventory_from_tree(basis)
451
inventory.apply_delta(delta)
452
rev_tree = InventoryRevisionTree(self.branch.repository,
453
inventory, new_revid)
454
self.set_parent_trees([(new_revid, rev_tree)])
457
class _SmartAddHelper(object):
458
"""Helper for MutableTree.smart_add."""
460
def get_inventory_delta(self):
461
# GZ 2016-06-05: Returning view would probably be fine but currently
462
# Inventory.apply_delta is documented as requiring a list of changes.
463
return list(viewvalues(self._invdelta))
465
def _get_ie(self, inv_path):
466
"""Retrieve the most up to date inventory entry for a path.
468
:param inv_path: Normalized inventory path
469
:return: Inventory entry (with possibly invalid .children for
472
entry = self._invdelta.get(inv_path)
473
if entry is not None:
475
# Find a 'best fit' match if the filesystem is case-insensitive
476
inv_path = self.tree._fix_case_of_inventory_path(inv_path)
478
return next(self.tree.iter_entries_by_dir(
479
specific_files=[inv_path]))[1]
480
except StopIteration:
483
def _convert_to_directory(self, this_ie, inv_path):
484
"""Convert an entry to a directory.
486
:param this_ie: Inventory entry
487
:param inv_path: Normalized path for the inventory entry
488
:return: The new inventory entry
490
# Same as in _add_one below, if the inventory doesn't
491
# think this is a directory, update the inventory
492
this_ie = _mod_inventory.InventoryDirectory(
493
this_ie.file_id, this_ie.name, this_ie.parent_id)
494
self._invdelta[inv_path] = (inv_path, inv_path, this_ie.file_id,
498
def _add_one_and_parent(self, parent_ie, path, kind, inv_path):
499
"""Add a new entry to the inventory and automatically add unversioned parents.
501
:param parent_ie: Parent inventory entry if known, or None. If
502
None, the parent is looked up by name and used if present, otherwise it
503
is recursively added.
504
:param path: Filesystem path to add
505
:param kind: Kind of new entry (file, directory, etc)
506
:param inv_path: Inventory path
507
:return: Inventory entry for path and a list of paths which have been added.
509
# Nothing to do if path is already versioned.
510
# This is safe from infinite recursion because the tree root is
512
inv_dirname = osutils.dirname(inv_path)
513
dirname, basename = osutils.split(path)
514
if parent_ie is None:
515
# slower but does not need parent_ie
516
this_ie = self._get_ie(inv_path)
517
if this_ie is not None:
519
# its really not there : add the parent
520
# note that the dirname use leads to some extra str copying etc but as
521
# there are a limited number of dirs we can be nested under, it should
522
# generally find it very fast and not recurse after that.
523
parent_ie = self._add_one_and_parent(None,
524
dirname, 'directory',
526
# if the parent exists, but isn't a directory, we have to do the
527
# kind change now -- really the inventory shouldn't pretend to know
528
# the kind of wt files, but it does.
529
if parent_ie.kind != 'directory':
530
# nb: this relies on someone else checking that the path we're using
531
# doesn't contain symlinks.
532
parent_ie = self._convert_to_directory(parent_ie, inv_dirname)
533
file_id = self.action(self.tree, parent_ie, path, kind)
534
entry = _mod_inventory.make_entry(kind, basename, parent_ie.file_id,
536
self._invdelta[inv_path] = (None, inv_path, entry.file_id, entry)
537
self.added.append(inv_path)
540
def _gather_dirs_to_add(self, user_dirs):
541
# only walk the minimal parents needed: we have user_dirs to override
545
is_inside = osutils.is_inside_or_parent_of_any
546
for path in sorted(user_dirs):
547
if (prev_dir is None or not is_inside([prev_dir], path)):
548
inv_path, this_ie = user_dirs[path]
549
yield (path, inv_path, this_ie, None)
552
def __init__(self, tree, action, conflicts_related=None):
555
self.action = add.AddAction()
561
if conflicts_related is None:
562
self.conflicts_related = frozenset()
564
self.conflicts_related = conflicts_related
566
def add(self, file_list, recurse=True):
568
# no paths supplied: add the entire tree.
569
# FIXME: this assumes we are running in a working tree subdir :-/
573
# expand any symlinks in the directory part, while leaving the
575
# only expanding if symlinks are supported avoids windows path bugs
576
if osutils.has_symlinks():
577
file_list = list(map(osutils.normalizepath, file_list))
580
# validate user file paths and convert all paths to tree
581
# relative : it's cheaper to make a tree relative path an abspath
582
# than to convert an abspath to tree relative, and it's cheaper to
583
# perform the canonicalization in bulk.
584
for filepath in osutils.canonical_relpaths(self.tree.basedir, file_list):
585
# validate user parameters. Our recursive code avoids adding new
586
# files that need such validation
587
if self.tree.is_control_filename(filepath):
588
raise errors.ForbiddenControlFileError(filename=filepath)
590
abspath = self.tree.abspath(filepath)
591
kind = osutils.file_kind(abspath)
592
# ensure the named path is added, so that ignore rules in the later
593
# directory walk dont skip it.
594
# we dont have a parent ie known yet.: use the relatively slower
595
# inventory probing method
596
inv_path, _ = osutils.normalized_filename(filepath)
597
this_ie = self._get_ie(inv_path)
599
this_ie = self._add_one_and_parent(
600
None, filepath, kind, inv_path)
601
if kind == 'directory':
602
# schedule the dir for scanning
603
user_dirs[filepath] = (inv_path, this_ie)
606
# no need to walk any directories at all.
609
things_to_add = list(self._gather_dirs_to_add(user_dirs))
611
illegalpath_re = re.compile(r'[\r\n]')
612
for directory, inv_path, this_ie, parent_ie in things_to_add:
613
# directory is tree-relative
614
abspath = self.tree.abspath(directory)
616
# get the contents of this directory.
618
# find the kind of the path being added, and save stat_value
622
stat_value = osutils.file_stat(abspath)
623
kind = osutils.file_kind_from_stat_mode(stat_value.st_mode)
627
# allow AddAction to skip this file
628
if self.action.skip_file(self.tree, abspath, kind, stat_value):
630
if not _mod_inventory.InventoryEntry.versionable_kind(kind):
631
trace.warning("skipping %s (can't add file of kind '%s')",
634
if illegalpath_re.search(directory):
635
trace.warning("skipping %r (contains \\n or \\r)" % abspath)
637
if directory in self.conflicts_related:
638
# If the file looks like one generated for a conflict, don't
641
'skipping %s (generated to help resolve conflicts)',
645
if kind == 'directory' and directory != '':
647
transport = _mod_transport.get_transport_from_path(abspath)
648
controldir.ControlDirFormat.find_format(transport)
650
except errors.NotBranchError:
652
except errors.UnsupportedFormatError:
657
if this_ie is not None:
660
# XXX: This is wrong; people *might* reasonably be trying to
661
# add subtrees as subtrees. This should probably only be done
662
# in formats which can represent subtrees, and even then
663
# perhaps only when the user asked to add subtrees. At the
664
# moment you can add them specially through 'join --reference',
665
# which is perhaps reasonable: adding a new reference is a
666
# special operation and can have a special behaviour. mbp
668
trace.warning("skipping nested tree %r", abspath)
670
this_ie = self._add_one_and_parent(parent_ie, directory, kind,
673
if kind == 'directory' and not sub_tree:
674
if this_ie.kind != 'directory':
675
this_ie = self._convert_to_directory(this_ie, inv_path)
677
for subf in sorted(os.listdir(abspath)):
678
inv_f, _ = osutils.normalized_filename(subf)
679
# here we could use TreeDirectory rather than
680
# string concatenation.
681
subp = osutils.pathjoin(directory, subf)
682
# TODO: is_control_filename is very slow. Make it faster.
683
# TreeDirectory.is_control_filename could also make this
684
# faster - its impossible for a non root dir to have a
686
if self.tree.is_control_filename(subp):
687
trace.mutter("skip control directory %r", subp)
689
sub_invp = osutils.pathjoin(inv_path, inv_f)
690
entry = self._invdelta.get(sub_invp)
691
if entry is not None:
694
sub_ie = this_ie.children.get(inv_f)
695
if sub_ie is not None:
696
# recurse into this already versioned subdir.
697
things_to_add.append((subp, sub_invp, sub_ie, this_ie))
699
# user selection overrides ignores
700
# ignore while selecting files - if we globbed in the
701
# outer loop we would ignore user files.
702
ignore_glob = self.tree.is_ignored(subp)
703
if ignore_glob is not None:
704
self.ignored.setdefault(
705
ignore_glob, []).append(subp)
707
things_to_add.append(
708
(subp, sub_invp, None, this_ie))
711
class InventoryRevisionTree(RevisionTree, InventoryTree):
713
def __init__(self, repository, inv, revision_id):
714
RevisionTree.__init__(self, repository, revision_id)
715
self._inventory = inv
717
def get_file_mtime(self, path):
718
ie = self._path2ie(path)
720
revision = self._repository.get_revision(ie.revision)
721
except errors.NoSuchRevision:
722
raise FileTimestampUnavailable(path)
723
return revision.timestamp
725
def get_file_size(self, path):
726
return self._path2ie(path).text_size
728
def get_file_sha1(self, path, stat_value=None):
729
ie = self._path2ie(path)
730
if ie.kind == "file":
734
def get_file_revision(self, path):
735
return self._path2ie(path).revision
737
def is_executable(self, path):
738
ie = self._path2ie(path)
739
if ie.kind != "file":
743
def has_filename(self, filename):
744
return bool(self.path2id(filename))
746
def list_files(self, include_root=False, from_dir=None, recursive=True):
747
# The only files returned by this are those from the version
750
inv = self.root_inventory
752
inv, from_dir_id = self._path2inv_file_id(from_dir)
753
if from_dir_id is None:
754
# Directory not versioned
756
entries = inv.iter_entries(from_dir=from_dir_id, recursive=recursive)
757
if inv.root is not None and not include_root and from_dir is None:
758
# skip the root for compatability with the current apis.
760
for path, entry in entries:
761
yield path, 'V', entry.kind, entry.file_id, entry
763
def get_symlink_target(self, path):
764
# Inventories store symlink targets in unicode
765
return self._path2ie(path).symlink_target
767
def get_reference_revision(self, path):
768
return self._path2ie(path).reference_revision
770
def get_root_id(self):
771
if self.root_inventory.root:
772
return self.root_inventory.root.file_id
774
def kind(self, path):
775
return self._path2ie(path).kind
777
def path_content_summary(self, path):
778
"""See Tree.path_content_summary."""
780
entry = self._path2ie(path)
781
except errors.NoSuchFile:
782
return ('missing', None, None, None)
785
return (kind, entry.text_size, entry.executable, entry.text_sha1)
786
elif kind == 'symlink':
787
return (kind, None, None, entry.symlink_target)
789
return (kind, None, None, None)
791
def _comparison_data(self, entry, path):
793
return None, False, None
794
return entry.kind, entry.executable, None
796
def walkdirs(self, prefix=""):
797
_directory = 'directory'
798
inv, top_id = self._path2inv_file_id(prefix)
802
pending = [(prefix, '', _directory, None, top_id, None)]
805
currentdir = pending.pop()
806
# 0 - relpath, 1- basename, 2- kind, 3- stat, id, v-kind
808
relroot = currentdir[0] + '/'
811
# FIXME: stash the node in pending
812
entry = inv.get_entry(currentdir[4])
813
for name, child in entry.sorted_children():
814
toppath = relroot + name
815
dirblock.append((toppath, name, child.kind, None,
816
child.file_id, child.kind
818
yield (currentdir[0], entry.file_id), dirblock
819
# push the user specified dirs from dirblock
820
for dir in reversed(dirblock):
821
if dir[2] == _directory:
824
def iter_files_bytes(self, desired_files):
825
"""See Tree.iter_files_bytes.
827
This version is implemented on top of Repository.iter_files_bytes"""
828
repo_desired_files = [(self.path2id(f), self.get_file_revision(f), i)
829
for f, i in desired_files]
831
for result in self._repository.iter_files_bytes(repo_desired_files):
833
except errors.RevisionNotPresent as e:
834
raise errors.NoSuchFile(e.file_id)
836
def annotate_iter(self, path, default_revision=revision.CURRENT_REVISION):
837
"""See Tree.annotate_iter"""
838
file_id = self.path2id(path)
839
text_key = (file_id, self.get_file_revision(path))
840
annotator = self._repository.texts.get_annotator()
841
annotations = annotator.annotate_flat(text_key)
842
return [(key[-1], line) for key, line in annotations]
844
def __eq__(self, other):
847
if isinstance(other, InventoryRevisionTree):
848
return (self.root_inventory == other.root_inventory)
851
def __ne__(self, other):
852
return not (self == other)
855
raise ValueError('not hashable')
858
class InterCHKRevisionTree(InterTree):
859
"""Fast path optimiser for RevisionTrees with CHK inventories."""
862
def is_compatible(source, target):
863
if (isinstance(source, RevisionTree) and
864
isinstance(target, RevisionTree)):
866
# Only CHK inventories have id_to_entry attribute
867
source.root_inventory.id_to_entry
868
target.root_inventory.id_to_entry
870
except AttributeError:
874
def iter_changes(self, include_unchanged=False,
875
specific_files=None, pb=None, extra_trees=[],
876
require_versioned=True, want_unversioned=False):
877
lookup_trees = [self.source]
879
lookup_trees.extend(extra_trees)
880
# The ids of items we need to examine to insure delta consistency.
881
precise_file_ids = set()
882
discarded_changes = {}
883
if specific_files == []:
884
specific_file_ids = []
886
specific_file_ids = self.target.paths2ids(specific_files,
887
lookup_trees, require_versioned=require_versioned)
888
# FIXME: It should be possible to delegate include_unchanged handling
889
# to CHKInventory.iter_changes and do a better job there -- vila
891
changed_file_ids = set()
892
# FIXME: nested tree support
893
for result in self.target.root_inventory.iter_changes(
894
self.source.root_inventory):
895
if specific_file_ids is not None:
897
if file_id not in specific_file_ids:
898
# A change from the whole tree that we don't want to show yet.
899
# We may find that we need to show it for delta consistency, so
901
discarded_changes[result[0]] = result
903
new_parent_id = result[4][1]
904
precise_file_ids.add(new_parent_id)
906
changed_file_ids.add(result[0])
907
if specific_file_ids is not None:
908
for result in self._handle_precise_ids(precise_file_ids,
909
changed_file_ids, discarded_changes=discarded_changes):
911
if include_unchanged:
912
# CHKMap avoid being O(tree), so we go to O(tree) only if
914
# Now walk the whole inventory, excluding the already yielded
916
# FIXME: Support nested trees
917
changed_file_ids = set(changed_file_ids)
918
for relpath, entry in self.target.root_inventory.iter_entries():
919
if (specific_file_ids is not None and
920
entry.file_id not in specific_file_ids):
922
if entry.file_id not in changed_file_ids:
923
yield (entry.file_id,
924
(relpath, relpath), # Not renamed
925
False, # Not modified
926
(True, True), # Still versioned
927
(entry.parent_id, entry.parent_id),
928
(entry.name, entry.name),
929
(entry.kind, entry.kind),
930
(entry.executable, entry.executable))
933
InterTree.register_optimiser(InterCHKRevisionTree)