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_canonical_inventory_paths(self, paths):
73
"""Like get_canonical_inventory_path() but works on multiple items.
75
:param paths: A sequence of paths relative to the root of the tree.
76
:return: A list of paths, with each item the corresponding input path
77
adjusted to account for existing elements that match case
80
with self.lock_read():
81
return list(self._yield_canonical_inventory_paths(paths))
83
def get_canonical_inventory_path(self, path):
84
"""Returns the first inventory item that case-insensitively matches path.
86
If a path matches exactly, it is returned. If no path matches exactly
87
but more than one path matches case-insensitively, it is implementation
88
defined which is returned.
90
If no path matches case-insensitively, the input path is returned, but
91
with as many path entries that do exist changed to their canonical
94
If you need to resolve many names from the same tree, you should
95
use get_canonical_inventory_paths() to avoid O(N) behaviour.
97
:param path: A paths relative to the root of the tree.
98
:return: The input path adjusted to account for existing elements
99
that match case insensitively.
101
with self.lock_read():
102
return next(self._yield_canonical_inventory_paths([path]))
104
def _yield_canonical_inventory_paths(self, paths):
106
# First, if the path as specified exists exactly, just use it.
107
if self.path2id(path) is not None:
111
cur_id = self.get_root_id()
113
bit_iter = iter(path.split("/"))
118
for child in self.iter_child_entries(self.id2path(cur_id), cur_id):
120
# XXX: it seem like if the child is known to be in the
121
# tree, we shouldn't need to go from its id back to
122
# its path -- mbp 2010-02-11
124
# XXX: it seems like we could be more efficient
125
# by just directly looking up the original name and
126
# only then searching all children; also by not
127
# chopping paths so much. -- mbp 2010-02-11
128
child_base = os.path.basename(self.id2path(child.file_id))
129
if (child_base == elt):
130
# if we found an exact match, we can stop now; if
131
# we found an approximate match we need to keep
132
# searching because there might be an exact match
134
cur_id = child.file_id
135
new_path = osutils.pathjoin(cur_path, child_base)
137
elif child_base.lower() == lelt:
138
cur_id = child.file_id
139
new_path = osutils.pathjoin(cur_path, child_base)
140
except errors.NoSuchId:
141
# before a change is committed we can see this error...
143
except errors.NotADirectory:
148
# got to the end of this directory and no entries matched.
149
# Return what matched so far, plus the rest as specified.
150
cur_path = osutils.pathjoin(cur_path, elt, *list(bit_iter))
155
def _get_root_inventory(self):
156
return self._inventory
158
root_inventory = property(_get_root_inventory,
159
doc="Root inventory of this tree")
161
def _unpack_file_id(self, file_id):
162
"""Find the inventory and inventory file id for a tree file id.
164
:param file_id: The tree file id, as bytestring or tuple
165
:return: Inventory and inventory file id
167
if isinstance(file_id, tuple):
168
if len(file_id) != 1:
169
raise ValueError("nested trees not yet supported: %r" % file_id)
171
return self.root_inventory, file_id
173
def find_related_paths_across_trees(self, paths, trees=[],
174
require_versioned=True):
175
"""Find related paths in tree corresponding to specified filenames in any
178
All matches in all trees will be used, and all children of matched
179
directories will be used.
181
:param paths: The filenames to find related paths for (if None, returns
183
:param trees: The trees to find file_ids within
184
:param require_versioned: if true, all specified filenames must occur in
186
:return: a set of paths for the specified filenames and their children
191
file_ids = self.paths2ids(
192
paths, trees, require_versioned=require_versioned)
194
for file_id in file_ids:
196
ret.add(self.id2path(file_id))
197
except errors.NoSuchId:
201
def paths2ids(self, paths, trees=[], require_versioned=True):
202
"""Return all the ids that can be reached by walking from paths.
204
Each path is looked up in this tree and any extras provided in
205
trees, and this is repeated recursively: the children in an extra tree
206
of a directory that has been renamed under a provided path in this tree
207
are all returned, even if none exist under a provided path in this
208
tree, and vice versa.
210
:param paths: An iterable of paths to start converting to ids from.
211
Alternatively, if paths is None, no ids should be calculated and None
212
will be returned. This is offered to make calling the api unconditional
213
for code that *might* take a list of files.
214
:param trees: Additional trees to consider.
215
:param require_versioned: If False, do not raise NotVersionedError if
216
an element of paths is not versioned in this tree and all of trees.
218
return find_ids_across_trees(paths, [self] + list(trees), require_versioned)
220
def path2id(self, path):
221
"""Return the id for path in this tree."""
222
with self.lock_read():
223
return self._path2inv_file_id(path)[1]
225
def _path2ie(self, path):
226
"""Lookup an inventory entry by path.
228
:param path: Path to look up
229
:return: InventoryEntry
231
ie = self.root_inventory.get_entry_by_path(path)
233
raise errors.NoSuchFile(path)
236
def _path2inv_file_id(self, path):
237
"""Lookup a inventory and inventory file id by path.
239
:param path: Path to look up
240
:return: tuple with inventory and inventory file id
242
# FIXME: Support nested trees
243
inv = self.root_inventory
244
inv_file_id = self.root_inventory.path2id(path)
245
return inv, inv_file_id
247
def id2path(self, file_id):
248
"""Return the path for a file id.
252
inventory, file_id = self._unpack_file_id(file_id)
253
return inventory.id2path(file_id)
255
def has_id(self, file_id):
256
inventory, file_id = self._unpack_file_id(file_id)
257
return inventory.has_id(file_id)
259
def has_or_had_id(self, file_id):
260
inventory, file_id = self._unpack_file_id(file_id)
261
return inventory.has_id(file_id)
263
def all_file_ids(self):
264
return {entry.file_id for path, entry in self.iter_entries_by_dir()}
266
def all_versioned_paths(self):
267
return {path for path, entry in self.iter_entries_by_dir()}
269
def iter_entries_by_dir(self, specific_files=None):
270
"""Walk the tree in 'by_dir' order.
272
This will yield each entry in the tree as a (path, entry) tuple.
273
The order that they are yielded is:
275
See Tree.iter_entries_by_dir for details.
277
with self.lock_read():
278
if specific_files is not None:
279
inventory_file_ids = []
280
for path in specific_files:
281
inventory, inv_file_id = self._path2inv_file_id(path)
282
if not inventory is self.root_inventory: # for now
283
raise AssertionError("%r != %r" % (
284
inventory, self.root_inventory))
285
inventory_file_ids.append(inv_file_id)
287
inventory_file_ids = None
288
# FIXME: Handle nested trees
289
return self.root_inventory.iter_entries_by_dir(
290
specific_file_ids=inventory_file_ids)
292
def iter_child_entries(self, path, file_id=None):
293
with self.lock_read():
294
ie = self._path2ie(path)
295
if ie.kind != 'directory':
296
raise errors.NotADirectory(path)
297
return iter(viewvalues(ie.children))
299
def _get_plan_merge_data(self, file_id, other, base):
300
from . import versionedfile
301
vf = versionedfile._PlanMergeVersionedFile(file_id)
302
last_revision_a = self._get_file_revision(
303
self.id2path(file_id), file_id, vf, b'this:')
304
last_revision_b = other._get_file_revision(
305
other.id2path(file_id), file_id, vf, b'other:')
307
last_revision_base = None
309
last_revision_base = base._get_file_revision(
310
base.id2path(file_id), file_id, vf, b'base:')
311
return vf, last_revision_a, last_revision_b, last_revision_base
313
def plan_file_merge(self, file_id, other, base=None):
314
"""Generate a merge plan based on annotations.
316
If the file contains uncommitted changes in this tree, they will be
317
attributed to the 'current:' pseudo-revision. If the file contains
318
uncommitted changes in the other tree, they will be assigned to the
319
'other:' pseudo-revision.
321
data = self._get_plan_merge_data(file_id, other, base)
322
vf, last_revision_a, last_revision_b, last_revision_base = data
323
return vf.plan_merge(last_revision_a, last_revision_b,
326
def plan_file_lca_merge(self, file_id, other, base=None):
327
"""Generate a merge plan based lca-newness.
329
If the file contains uncommitted changes in this tree, they will be
330
attributed to the 'current:' pseudo-revision. If the file contains
331
uncommitted changes in the other tree, they will be assigned to the
332
'other:' pseudo-revision.
334
data = self._get_plan_merge_data(file_id, other, base)
335
vf, last_revision_a, last_revision_b, last_revision_base = data
336
return vf.plan_lca_merge(last_revision_a, last_revision_b,
339
def _get_file_revision(self, path, file_id, vf, tree_revision):
340
"""Ensure that file_id, tree_revision is in vf to plan the merge."""
341
if getattr(self, '_repository', None) is None:
342
last_revision = tree_revision
343
parent_keys = [(file_id, t.get_file_revision(path, file_id)) for t in
344
self._iter_parent_trees()]
345
vf.add_lines((file_id, last_revision), parent_keys,
346
self.get_file_lines(path, file_id))
347
repo = self.branch.repository
350
last_revision = self.get_file_revision(path, file_id)
351
base_vf = self._repository.texts
352
if base_vf not in vf.fallback_versionedfiles:
353
vf.fallback_versionedfiles.append(base_vf)
357
def find_ids_across_trees(filenames, trees, require_versioned=True):
358
"""Find the ids corresponding to specified filenames.
360
All matches in all trees will be used, and all children of matched
361
directories will be used.
363
:param filenames: The filenames to find file_ids for (if None, returns
365
:param trees: The trees to find file_ids within
366
:param require_versioned: if true, all specified filenames must occur in
368
:return: a set of file ids for the specified filenames and their children.
372
specified_path_ids = _find_ids_across_trees(filenames, trees,
374
return _find_children_across_trees(specified_path_ids, trees)
377
def _find_ids_across_trees(filenames, trees, require_versioned):
378
"""Find the ids corresponding to specified filenames.
380
All matches in all trees will be used, but subdirectories are not scanned.
382
:param filenames: The filenames to find file_ids for
383
:param trees: The trees to find file_ids within
384
:param require_versioned: if true, all specified filenames must occur in
386
:return: a set of file ids for the specified filenames
389
interesting_ids = set()
390
for tree_path in filenames:
393
file_id = tree.path2id(tree_path)
394
if file_id is not None:
395
interesting_ids.add(file_id)
398
not_versioned.append(tree_path)
399
if len(not_versioned) > 0 and require_versioned:
400
raise errors.PathsNotVersionedError(not_versioned)
401
return interesting_ids
404
def _find_children_across_trees(specified_ids, trees):
405
"""Return a set including specified ids and their children.
407
All matches in all trees will be used.
409
:param trees: The trees to find file_ids within
410
:return: a set containing all specified ids and their children
412
interesting_ids = set(specified_ids)
413
pending = interesting_ids
414
# now handle children of interesting ids
415
# we loop so that we handle all children of each id in both trees
416
while len(pending) > 0:
418
for file_id in pending:
421
path = tree.id2path(file_id)
422
except errors.NoSuchId:
425
for child in tree.iter_child_entries(path, file_id):
426
if child.file_id not in interesting_ids:
427
new_pending.add(child.file_id)
428
except errors.NotADirectory:
430
interesting_ids.update(new_pending)
431
pending = new_pending
432
return interesting_ids
435
class MutableInventoryTree(MutableTree, InventoryTree):
437
def apply_inventory_delta(self, changes):
438
"""Apply changes to the inventory as an atomic operation.
440
:param changes: An inventory delta to apply to the working tree's
443
:seealso Inventory.apply_delta: For details on the changes parameter.
445
with self.lock_tree_write():
447
inv = self.root_inventory
448
inv.apply_delta(changes)
449
self._write_inventory(inv)
451
def _fix_case_of_inventory_path(self, path):
452
"""If our tree isn't case sensitive, return the canonical path"""
453
if not self.case_sensitive:
454
path = self.get_canonical_inventory_path(path)
457
def smart_add(self, file_list, recurse=True, action=None, save=True):
458
"""Version file_list, optionally recursing into directories.
460
This is designed more towards DWIM for humans than API clarity.
461
For the specific behaviour see the help for cmd_add().
463
:param file_list: List of zero or more paths. *NB: these are
464
interpreted relative to the process cwd, not relative to the
465
tree.* (Add and most other tree methods use tree-relative
467
:param action: A reporter to be called with the inventory, parent_ie,
468
path and kind of the path being added. It may return a file_id if
469
a specific one should be used.
470
:param save: Save the inventory after completing the adds. If False
471
this provides dry-run functionality by doing the add and not saving
473
:return: A tuple - files_added, ignored_files. files_added is the count
474
of added files, and ignored_files is a dict mapping files that were
475
ignored to the rule that caused them to be ignored.
477
with self.lock_tree_write():
478
# Not all mutable trees can have conflicts
479
if getattr(self, 'conflicts', None) is not None:
480
# Collect all related files without checking whether they exist or
481
# are versioned. It's cheaper to do that once for all conflicts
482
# than trying to find the relevant conflict for each added file.
483
conflicts_related = set()
484
for c in self.conflicts():
485
conflicts_related.update(c.associated_filenames())
487
conflicts_related = None
488
adder = _SmartAddHelper(self, action, conflicts_related)
489
adder.add(file_list, recurse=recurse)
491
invdelta = adder.get_inventory_delta()
492
self.apply_inventory_delta(invdelta)
493
return adder.added, adder.ignored
495
def update_basis_by_delta(self, new_revid, delta):
496
"""Update the parents of this tree after a commit.
498
This gives the tree one parent, with revision id new_revid. The
499
inventory delta is applied to the current basis tree to generate the
500
inventory for the parent new_revid, and all other parent trees are
503
All the changes in the delta should be changes synchronising the basis
504
tree with some or all of the working tree, with a change to a directory
505
requiring that its contents have been recursively included. That is,
506
this is not a general purpose tree modification routine, but a helper
507
for commit which is not required to handle situations that do not arise
510
See the inventory developers documentation for the theory behind
513
:param new_revid: The new revision id for the trees parent.
514
:param delta: An inventory delta (see apply_inventory_delta) describing
515
the changes from the current left most parent revision to new_revid.
517
# if the tree is updated by a pull to the branch, as happens in
518
# WorkingTree2, when there was no separation between branch and tree,
519
# then just clear merges, efficiency is not a concern for now as this
520
# is legacy environments only, and they are slow regardless.
521
if self.last_revision() == new_revid:
522
self.set_parent_ids([new_revid])
524
# generic implementation based on Inventory manipulation. See
525
# WorkingTree classes for optimised versions for specific format trees.
526
basis = self.basis_tree()
527
with basis.lock_read():
528
# TODO: Consider re-evaluating the need for this with CHKInventory
529
# we don't strictly need to mutate an inventory for this
530
# it only makes sense when apply_delta is cheaper than get_inventory()
531
inventory = _mod_inventory.mutable_inventory_from_tree(basis)
532
inventory.apply_delta(delta)
533
rev_tree = InventoryRevisionTree(self.branch.repository,
534
inventory, new_revid)
535
self.set_parent_trees([(new_revid, rev_tree)])
538
class _SmartAddHelper(object):
539
"""Helper for MutableTree.smart_add."""
541
def get_inventory_delta(self):
542
# GZ 2016-06-05: Returning view would probably be fine but currently
543
# Inventory.apply_delta is documented as requiring a list of changes.
544
return list(viewvalues(self._invdelta))
546
def _get_ie(self, inv_path):
547
"""Retrieve the most up to date inventory entry for a path.
549
:param inv_path: Normalized inventory path
550
:return: Inventory entry (with possibly invalid .children for
553
entry = self._invdelta.get(inv_path)
554
if entry is not None:
556
# Find a 'best fit' match if the filesystem is case-insensitive
557
inv_path = self.tree._fix_case_of_inventory_path(inv_path)
559
return next(self.tree.iter_entries_by_dir(
560
specific_files=[inv_path]))[1]
561
except StopIteration:
564
def _convert_to_directory(self, this_ie, inv_path):
565
"""Convert an entry to a directory.
567
:param this_ie: Inventory entry
568
:param inv_path: Normalized path for the inventory entry
569
:return: The new inventory entry
571
# Same as in _add_one below, if the inventory doesn't
572
# think this is a directory, update the inventory
573
this_ie = _mod_inventory.InventoryDirectory(
574
this_ie.file_id, this_ie.name, this_ie.parent_id)
575
self._invdelta[inv_path] = (inv_path, inv_path, this_ie.file_id,
579
def _add_one_and_parent(self, parent_ie, path, kind, inv_path):
580
"""Add a new entry to the inventory and automatically add unversioned parents.
582
:param parent_ie: Parent inventory entry if known, or None. If
583
None, the parent is looked up by name and used if present, otherwise it
584
is recursively added.
585
:param path: Filesystem path to add
586
:param kind: Kind of new entry (file, directory, etc)
587
:param inv_path: Inventory path
588
:return: Inventory entry for path and a list of paths which have been added.
590
# Nothing to do if path is already versioned.
591
# This is safe from infinite recursion because the tree root is
593
inv_dirname = osutils.dirname(inv_path)
594
dirname, basename = osutils.split(path)
595
if parent_ie is None:
596
# slower but does not need parent_ie
597
this_ie = self._get_ie(inv_path)
598
if this_ie is not None:
600
# its really not there : add the parent
601
# note that the dirname use leads to some extra str copying etc but as
602
# there are a limited number of dirs we can be nested under, it should
603
# generally find it very fast and not recurse after that.
604
parent_ie = self._add_one_and_parent(None,
605
dirname, 'directory',
607
# if the parent exists, but isn't a directory, we have to do the
608
# kind change now -- really the inventory shouldn't pretend to know
609
# the kind of wt files, but it does.
610
if parent_ie.kind != 'directory':
611
# nb: this relies on someone else checking that the path we're using
612
# doesn't contain symlinks.
613
parent_ie = self._convert_to_directory(parent_ie, inv_dirname)
614
file_id = self.action(self.tree, parent_ie, path, kind)
615
entry = _mod_inventory.make_entry(kind, basename, parent_ie.file_id,
617
self._invdelta[inv_path] = (None, inv_path, entry.file_id, entry)
618
self.added.append(inv_path)
621
def _gather_dirs_to_add(self, user_dirs):
622
# only walk the minimal parents needed: we have user_dirs to override
626
is_inside = osutils.is_inside_or_parent_of_any
627
for path in sorted(user_dirs):
628
if (prev_dir is None or not is_inside([prev_dir], path)):
629
inv_path, this_ie = user_dirs[path]
630
yield (path, inv_path, this_ie, None)
633
def __init__(self, tree, action, conflicts_related=None):
636
self.action = add.AddAction()
642
if conflicts_related is None:
643
self.conflicts_related = frozenset()
645
self.conflicts_related = conflicts_related
647
def add(self, file_list, recurse=True):
649
# no paths supplied: add the entire tree.
650
# FIXME: this assumes we are running in a working tree subdir :-/
654
# expand any symlinks in the directory part, while leaving the
656
# only expanding if symlinks are supported avoids windows path bugs
657
if osutils.has_symlinks():
658
file_list = list(map(osutils.normalizepath, file_list))
661
# validate user file paths and convert all paths to tree
662
# relative : it's cheaper to make a tree relative path an abspath
663
# than to convert an abspath to tree relative, and it's cheaper to
664
# perform the canonicalization in bulk.
665
for filepath in osutils.canonical_relpaths(self.tree.basedir, file_list):
666
# validate user parameters. Our recursive code avoids adding new
667
# files that need such validation
668
if self.tree.is_control_filename(filepath):
669
raise errors.ForbiddenControlFileError(filename=filepath)
671
abspath = self.tree.abspath(filepath)
672
kind = osutils.file_kind(abspath)
673
# ensure the named path is added, so that ignore rules in the later
674
# directory walk dont skip it.
675
# we dont have a parent ie known yet.: use the relatively slower
676
# inventory probing method
677
inv_path, _ = osutils.normalized_filename(filepath)
678
this_ie = self._get_ie(inv_path)
680
this_ie = self._add_one_and_parent(None, filepath, kind, inv_path)
681
if kind == 'directory':
682
# schedule the dir for scanning
683
user_dirs[filepath] = (inv_path, this_ie)
686
# no need to walk any directories at all.
689
things_to_add = list(self._gather_dirs_to_add(user_dirs))
691
illegalpath_re = re.compile(r'[\r\n]')
692
for directory, inv_path, this_ie, parent_ie in things_to_add:
693
# directory is tree-relative
694
abspath = self.tree.abspath(directory)
696
# get the contents of this directory.
698
# find the kind of the path being added, and save stat_value
702
stat_value = osutils.file_stat(abspath)
703
kind = osutils.file_kind_from_stat_mode(stat_value.st_mode)
707
# allow AddAction to skip this file
708
if self.action.skip_file(self.tree, abspath, kind, stat_value):
710
if not _mod_inventory.InventoryEntry.versionable_kind(kind):
711
trace.warning("skipping %s (can't add file of kind '%s')",
714
if illegalpath_re.search(directory):
715
trace.warning("skipping %r (contains \\n or \\r)" % abspath)
717
if directory in self.conflicts_related:
718
# If the file looks like one generated for a conflict, don't
721
'skipping %s (generated to help resolve conflicts)',
725
if kind == 'directory' and directory != '':
727
transport = _mod_transport.get_transport_from_path(abspath)
728
controldir.ControlDirFormat.find_format(transport)
730
except errors.NotBranchError:
732
except errors.UnsupportedFormatError:
737
if this_ie is not None:
740
# XXX: This is wrong; people *might* reasonably be trying to
741
# add subtrees as subtrees. This should probably only be done
742
# in formats which can represent subtrees, and even then
743
# perhaps only when the user asked to add subtrees. At the
744
# moment you can add them specially through 'join --reference',
745
# which is perhaps reasonable: adding a new reference is a
746
# special operation and can have a special behaviour. mbp
748
trace.warning("skipping nested tree %r", abspath)
750
this_ie = self._add_one_and_parent(parent_ie, directory, kind,
753
if kind == 'directory' and not sub_tree:
754
if this_ie.kind != 'directory':
755
this_ie = self._convert_to_directory(this_ie, inv_path)
757
for subf in sorted(os.listdir(abspath)):
758
inv_f, _ = osutils.normalized_filename(subf)
759
# here we could use TreeDirectory rather than
760
# string concatenation.
761
subp = osutils.pathjoin(directory, subf)
762
# TODO: is_control_filename is very slow. Make it faster.
763
# TreeDirectory.is_control_filename could also make this
764
# faster - its impossible for a non root dir to have a
766
if self.tree.is_control_filename(subp):
767
trace.mutter("skip control directory %r", subp)
769
sub_invp = osutils.pathjoin(inv_path, inv_f)
770
entry = self._invdelta.get(sub_invp)
771
if entry is not None:
774
sub_ie = this_ie.children.get(inv_f)
775
if sub_ie is not None:
776
# recurse into this already versioned subdir.
777
things_to_add.append((subp, sub_invp, sub_ie, this_ie))
779
# user selection overrides ignores
780
# ignore while selecting files - if we globbed in the
781
# outer loop we would ignore user files.
782
ignore_glob = self.tree.is_ignored(subp)
783
if ignore_glob is not None:
784
self.ignored.setdefault(ignore_glob, []).append(subp)
786
things_to_add.append((subp, sub_invp, None, this_ie))
789
class InventoryRevisionTree(RevisionTree, InventoryTree):
791
def __init__(self, repository, inv, revision_id):
792
RevisionTree.__init__(self, repository, revision_id)
793
self._inventory = inv
795
def get_file_mtime(self, path, file_id=None):
796
ie = self._path2ie(path)
798
revision = self._repository.get_revision(ie.revision)
799
except errors.NoSuchRevision:
800
raise FileTimestampUnavailable(self.id2path(file_id))
801
return revision.timestamp
803
def get_file_size(self, path, file_id=None):
804
return self._path2ie(path).text_size
806
def get_file_sha1(self, path, file_id=None, stat_value=None):
807
ie = self._path2ie(path)
808
if ie.kind == "file":
812
def get_file_revision(self, path, file_id=None):
813
return self._path2ie(path).revision
815
def is_executable(self, path, file_id=None):
816
ie = self._path2ie(path)
817
if ie.kind != "file":
821
def has_filename(self, filename):
822
return bool(self.path2id(filename))
824
def list_files(self, include_root=False, from_dir=None, recursive=True):
825
# The only files returned by this are those from the version
828
inv = self.root_inventory
830
inv, from_dir_id = self._path2inv_file_id(from_dir)
831
if from_dir_id is None:
832
# Directory not versioned
834
entries = inv.iter_entries(from_dir=from_dir_id, recursive=recursive)
835
if inv.root is not None and not include_root and from_dir is None:
836
# skip the root for compatability with the current apis.
838
for path, entry in entries:
839
yield path, 'V', entry.kind, entry.file_id, entry
841
def get_symlink_target(self, path, file_id=None):
842
# Inventories store symlink targets in unicode
843
return self._path2ie(path).symlink_target
845
def get_reference_revision(self, path, file_id=None):
846
return self._path2ie(path).reference_revision
848
def get_root_id(self):
849
if self.root_inventory.root:
850
return self.root_inventory.root.file_id
852
def kind(self, path, file_id=None):
853
return self._path2ie(path).kind
855
def path_content_summary(self, path):
856
"""See Tree.path_content_summary."""
858
entry = self._path2ie(path)
859
except errors.NoSuchFile:
860
return ('missing', None, None, None)
863
return (kind, entry.text_size, entry.executable, entry.text_sha1)
864
elif kind == 'symlink':
865
return (kind, None, None, entry.symlink_target)
867
return (kind, None, None, None)
869
def _comparison_data(self, entry, path):
871
return None, False, None
872
return entry.kind, entry.executable, None
874
def walkdirs(self, prefix=""):
875
_directory = 'directory'
876
inv, top_id = self._path2inv_file_id(prefix)
880
pending = [(prefix, '', _directory, None, top_id, None)]
883
currentdir = pending.pop()
884
# 0 - relpath, 1- basename, 2- kind, 3- stat, id, v-kind
886
relroot = currentdir[0] + '/'
889
# FIXME: stash the node in pending
890
entry = inv.get_entry(currentdir[4])
891
for name, child in entry.sorted_children():
892
toppath = relroot + name
893
dirblock.append((toppath, name, child.kind, None,
894
child.file_id, child.kind
896
yield (currentdir[0], entry.file_id), dirblock
897
# push the user specified dirs from dirblock
898
for dir in reversed(dirblock):
899
if dir[2] == _directory:
902
def iter_files_bytes(self, desired_files):
903
"""See Tree.iter_files_bytes.
905
This version is implemented on top of Repository.iter_files_bytes"""
906
repo_desired_files = [(self.path2id(f), self.get_file_revision(f), i)
907
for f, i in desired_files]
909
for result in self._repository.iter_files_bytes(repo_desired_files):
911
except errors.RevisionNotPresent as e:
912
raise errors.NoSuchFile(e.file_id)
914
def annotate_iter(self, path, file_id=None,
915
default_revision=revision.CURRENT_REVISION):
916
"""See Tree.annotate_iter"""
918
file_id = self.path2id(path)
919
text_key = (file_id, self.get_file_revision(path, file_id))
920
annotator = self._repository.texts.get_annotator()
921
annotations = annotator.annotate_flat(text_key)
922
return [(key[-1], line) for key, line in annotations]
924
def __eq__(self, other):
927
if isinstance(other, InventoryRevisionTree):
928
return (self.root_inventory == other.root_inventory)
931
def __ne__(self, other):
932
return not (self == other)
935
raise ValueError('not hashable')
938
class InterCHKRevisionTree(InterTree):
939
"""Fast path optimiser for RevisionTrees with CHK inventories."""
942
def is_compatible(source, target):
943
if (isinstance(source, RevisionTree)
944
and isinstance(target, RevisionTree)):
946
# Only CHK inventories have id_to_entry attribute
947
source.root_inventory.id_to_entry
948
target.root_inventory.id_to_entry
950
except AttributeError:
954
def iter_changes(self, include_unchanged=False,
955
specific_files=None, pb=None, extra_trees=[],
956
require_versioned=True, want_unversioned=False):
957
lookup_trees = [self.source]
959
lookup_trees.extend(extra_trees)
960
# The ids of items we need to examine to insure delta consistency.
961
precise_file_ids = set()
962
discarded_changes = {}
963
if specific_files == []:
964
specific_file_ids = []
966
specific_file_ids = self.target.paths2ids(specific_files,
967
lookup_trees, require_versioned=require_versioned)
968
# FIXME: It should be possible to delegate include_unchanged handling
969
# to CHKInventory.iter_changes and do a better job there -- vila
971
changed_file_ids = set()
972
# FIXME: nested tree support
973
for result in self.target.root_inventory.iter_changes(
974
self.source.root_inventory):
975
if specific_file_ids is not None:
977
if file_id not in specific_file_ids:
978
# A change from the whole tree that we don't want to show yet.
979
# We may find that we need to show it for delta consistency, so
981
discarded_changes[result[0]] = result
983
new_parent_id = result[4][1]
984
precise_file_ids.add(new_parent_id)
986
changed_file_ids.add(result[0])
987
if specific_file_ids is not None:
988
for result in self._handle_precise_ids(precise_file_ids,
989
changed_file_ids, discarded_changes=discarded_changes):
991
if include_unchanged:
992
# CHKMap avoid being O(tree), so we go to O(tree) only if
994
# Now walk the whole inventory, excluding the already yielded
996
# FIXME: Support nested trees
997
changed_file_ids = set(changed_file_ids)
998
for relpath, entry in self.target.root_inventory.iter_entries():
999
if (specific_file_ids is not None
1000
and not entry.file_id in specific_file_ids):
1002
if not entry.file_id in changed_file_ids:
1003
yield (entry.file_id,
1004
(relpath, relpath), # Not renamed
1005
False, # Not modified
1006
(True, True), # Still versioned
1007
(entry.parent_id, entry.parent_id),
1008
(entry.name, entry.name),
1009
(entry.kind, entry.kind),
1010
(entry.executable, entry.executable))
1013
InterTree.register_optimiser(InterCHKRevisionTree)