1# Copyright (C) 2005-2011 Canonical Ltd 2# 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. 7# 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. 12# 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 16 17"""Tree classes, representing directory at point in time. 18""" 19 20try: 21 from collections.abc import deque 22except ImportError: # python < 3.7 23 from collections import deque 24 25import os 26import re 27 28 29from .. import ( 30 branch as _mod_branch, 31 debug, 32 errors, 33 lazy_import, 34 osutils, 35 revision, 36 ) 37from ..controldir import ( 38 ControlDir, 39 ) 40from ..mutabletree import ( 41 MutableTree, 42 ) 43from ..repository import ( 44 Repository, 45 ) 46from ..revisiontree import ( 47 RevisionTree, 48 ) 49lazy_import.lazy_import(globals(), """ 50from breezy import ( 51 add, 52 controldir, 53 trace, 54 transport as _mod_transport, 55 ) 56from breezy.bzr import ( 57 inventory as _mod_inventory, 58 ) 59""") 60from ..tree import ( 61 FileTimestampUnavailable, 62 InterTree, 63 MissingNestedTree, 64 Tree, 65 TreeChange, 66 TreeFile, 67 ) 68 69 70class InventoryTreeChange(TreeChange): 71 72 __slots__ = TreeChange.__slots__ + ['file_id', 'parent_id'] 73 74 def __init__(self, file_id, path, changed_content, versioned, parent_id, 75 name, kind, executable, copied=False): 76 self.file_id = file_id 77 self.parent_id = parent_id 78 super(InventoryTreeChange, self).__init__( 79 path=path, changed_content=changed_content, versioned=versioned, 80 name=name, kind=kind, executable=executable, copied=copied) 81 82 def __repr__(self): 83 return "%s%r" % (self.__class__.__name__, self._as_tuple()) 84 85 def _as_tuple(self): 86 return (self.file_id, self.path, self.changed_content, self.versioned, 87 self.parent_id, self.name, self.kind, self.executable, self.copied) 88 89 def __eq__(self, other): 90 if isinstance(other, TreeChange): 91 return self._as_tuple() == other._as_tuple() 92 if isinstance(other, tuple): 93 return self._as_tuple() == other 94 return False 95 96 def __lt__(self, other): 97 return self._as_tuple() < other._as_tuple() 98 99 def meta_modified(self): 100 if self.versioned == (True, True): 101 return (self.executable[0] != self.executable[1]) 102 return False 103 104 def is_reparented(self): 105 return self.parent_id[0] != self.parent_id[1] 106 107 @property 108 def renamed(self): 109 return ( 110 not self.copied and 111 None not in self.name and 112 None not in self.parent_id and 113 (self.name[0] != self.name[1] or self.parent_id[0] != self.parent_id[1])) 114 115 def discard_new(self): 116 return self.__class__( 117 self.file_id, (self.path[0], None), self.changed_content, 118 (self.versioned[0], None), (self.parent_id[0], None), 119 (self.name[0], None), (self.kind[0], None), 120 (self.executable[0], None), 121 copied=False) 122 123 124class InventoryTree(Tree): 125 """A tree that relies on an inventory for its metadata. 126 127 Trees contain an `Inventory` object, and also know how to retrieve 128 file texts mentioned in the inventory, either from a working 129 directory or from a store. 130 131 It is possible for trees to contain files that are not described 132 in their inventory or vice versa; for this use `filenames()`. 133 134 Subclasses should set the _inventory attribute, which is considered 135 private to external API users. 136 """ 137 138 def _get_root_inventory(self): 139 return self._inventory 140 141 root_inventory = property(_get_root_inventory, 142 doc="Root inventory of this tree") 143 144 def _unpack_file_id(self, file_id): 145 """Find the inventory and inventory file id for a tree file id. 146 147 :param file_id: The tree file id, as bytestring or tuple 148 :return: Inventory and inventory file id 149 """ 150 if isinstance(file_id, tuple): 151 if len(file_id) != 1: 152 raise ValueError( 153 "nested trees not yet supported: %r" % file_id) 154 file_id = file_id[0] 155 return self.root_inventory, file_id 156 157 def find_related_paths_across_trees(self, paths, trees=[], 158 require_versioned=True): 159 """Find related paths in tree corresponding to specified filenames in any 160 of `lookup_trees`. 161 162 All matches in all trees will be used, and all children of matched 163 directories will be used. 164 165 :param paths: The filenames to find related paths for (if None, returns 166 None) 167 :param trees: The trees to find file_ids within 168 :param require_versioned: if true, all specified filenames must occur in 169 at least one tree. 170 :return: a set of paths for the specified filenames and their children 171 in `tree` 172 """ 173 if paths is None: 174 return None 175 file_ids = self.paths2ids( 176 paths, trees, require_versioned=require_versioned) 177 ret = set() 178 for file_id in file_ids: 179 try: 180 ret.add(self.id2path(file_id)) 181 except errors.NoSuchId: 182 pass 183 return ret 184 185 def paths2ids(self, paths, trees=[], require_versioned=True): 186 """Return all the ids that can be reached by walking from paths. 187 188 Each path is looked up in this tree and any extras provided in 189 trees, and this is repeated recursively: the children in an extra tree 190 of a directory that has been renamed under a provided path in this tree 191 are all returned, even if none exist under a provided path in this 192 tree, and vice versa. 193 194 :param paths: An iterable of paths to start converting to ids from. 195 Alternatively, if paths is None, no ids should be calculated and None 196 will be returned. This is offered to make calling the api unconditional 197 for code that *might* take a list of files. 198 :param trees: Additional trees to consider. 199 :param require_versioned: If False, do not raise NotVersionedError if 200 an element of paths is not versioned in this tree and all of trees. 201 """ 202 return find_ids_across_trees(paths, [self] + list(trees), require_versioned) 203 204 def path2id(self, path): 205 """Return the id for path in this tree.""" 206 with self.lock_read(): 207 return self._path2inv_file_id(path)[1] 208 209 def _path2ie(self, path): 210 """Lookup an inventory entry by path. 211 212 :param path: Path to look up 213 :return: InventoryEntry 214 """ 215 inv, ie = self._path2inv_ie(path) 216 if ie is None: 217 raise errors.NoSuchFile(path) 218 return ie 219 220 def _path2inv_ie(self, path): 221 inv = self.root_inventory 222 if isinstance(path, list): 223 remaining = path 224 else: 225 remaining = osutils.splitpath(path) 226 ie = inv.root 227 while remaining: 228 ie, base, remaining = inv.get_entry_by_path_partial(remaining) 229 if remaining: 230 inv = self._get_nested_tree('/'.join(base), ie.file_id, ie.reference_revision).root_inventory 231 if ie is None: 232 return None, None 233 return inv, ie 234 235 def _path2inv_file_id(self, path): 236 """Lookup a inventory and inventory file id by path. 237 238 :param path: Path to look up 239 :return: tuple with inventory and inventory file id 240 """ 241 inv, ie = self._path2inv_ie(path) 242 if ie is None: 243 return None, None 244 return inv, ie.file_id 245 246 def id2path(self, file_id, recurse='down'): 247 """Return the path for a file id. 248 249 :raises NoSuchId: 250 """ 251 inventory, file_id = self._unpack_file_id(file_id) 252 try: 253 return inventory.id2path(file_id) 254 except errors.NoSuchId: 255 if recurse == 'down': 256 if 'evil' in debug.debug_flags: 257 trace.mutter_callsite( 258 2, "id2path with nested trees scales with tree size.") 259 for path in self.iter_references(): 260 subtree = self.get_nested_tree(path) 261 try: 262 return osutils.pathjoin(path, subtree.id2path(file_id)) 263 except errors.NoSuchId: 264 pass 265 raise errors.NoSuchId(self, file_id) 266 267 def all_file_ids(self): 268 return {entry.file_id for path, entry in self.iter_entries_by_dir()} 269 270 def all_versioned_paths(self): 271 return {path for path, entry in self.iter_entries_by_dir()} 272 273 def iter_entries_by_dir(self, specific_files=None, 274 recurse_nested=False): 275 """Walk the tree in 'by_dir' order. 276 277 This will yield each entry in the tree as a (path, entry) tuple. 278 The order that they are yielded is: 279 280 See Tree.iter_entries_by_dir for details. 281 """ 282 with self.lock_read(): 283 if specific_files is not None: 284 inventory_file_ids = [] 285 for path in specific_files: 286 inventory, inv_file_id = self._path2inv_file_id(path) 287 if inventory and inventory is not self.root_inventory: 288 raise AssertionError("%r != %r" % ( 289 inventory, self.root_inventory)) 290 inventory_file_ids.append(inv_file_id) 291 else: 292 inventory_file_ids = None 293 def iter_entries(inv): 294 for p, e in inv.iter_entries_by_dir(specific_file_ids=inventory_file_ids): 295 if e.kind == 'tree-reference' and recurse_nested: 296 subinv = self._get_nested_tree(p, e.file_id, e.reference_revision).root_inventory 297 for subp, e in iter_entries(subinv): 298 yield (osutils.pathjoin(p, subp) if subp else p), e 299 else: 300 yield p, e 301 return iter_entries(self.root_inventory) 302 303 def iter_child_entries(self, path): 304 with self.lock_read(): 305 ie = self._path2ie(path) 306 if ie.kind != 'directory': 307 raise errors.NotADirectory(path) 308 return ie.children.values() 309 310 def _get_plan_merge_data(self, path, other, base): 311 from . import versionedfile 312 file_id = self.path2id(path) 313 vf = versionedfile._PlanMergeVersionedFile(file_id) 314 last_revision_a = self._get_file_revision( 315 path, file_id, vf, b'this:') 316 last_revision_b = other._get_file_revision( 317 other.id2path(file_id), file_id, vf, b'other:') 318 if base is None: 319 last_revision_base = None 320 else: 321 last_revision_base = base._get_file_revision( 322 base.id2path(file_id), file_id, vf, b'base:') 323 return vf, last_revision_a, last_revision_b, last_revision_base 324 325 def plan_file_merge(self, path, other, base=None): 326 """Generate a merge plan based on annotations. 327 328 If the file contains uncommitted changes in this tree, they will be 329 attributed to the 'current:' pseudo-revision. If the file contains 330 uncommitted changes in the other tree, they will be assigned to the 331 'other:' pseudo-revision. 332 """ 333 data = self._get_plan_merge_data(path, other, base) 334 vf, last_revision_a, last_revision_b, last_revision_base = data 335 return vf.plan_merge(last_revision_a, last_revision_b, 336 last_revision_base) 337 338 def plan_file_lca_merge(self, path, other, base=None): 339 """Generate a merge plan based lca-newness. 340 341 If the file contains uncommitted changes in this tree, they will be 342 attributed to the 'current:' pseudo-revision. If the file contains 343 uncommitted changes in the other tree, they will be assigned to the 344 'other:' pseudo-revision. 345 """ 346 data = self._get_plan_merge_data(path, other, base) 347 vf, last_revision_a, last_revision_b, last_revision_base = data 348 return vf.plan_lca_merge(last_revision_a, last_revision_b, 349 last_revision_base) 350 351 def _iter_parent_trees(self): 352 """Iterate through parent trees, defaulting to Tree.revision_tree.""" 353 for revision_id in self.get_parent_ids(): 354 try: 355 yield self.revision_tree(revision_id) 356 except errors.NoSuchRevisionInTree: 357 yield self.branch.repository.revision_tree(revision_id) 358 359 def _get_file_revision(self, path, file_id, vf, tree_revision): 360 """Ensure that file_id, tree_revision is in vf to plan the merge.""" 361 from . import versionedfile 362 last_revision = tree_revision 363 parent_keys = [ 364 (file_id, t.get_file_revision(path)) for t in 365 self._iter_parent_trees()] 366 with self.get_file(path) as f: 367 vf.add_content( 368 versionedfile.FileContentFactory( 369 (file_id, last_revision), parent_keys, f, size=osutils.filesize(f))) 370 repo = self.branch.repository 371 base_vf = repo.texts 372 if base_vf not in vf.fallback_versionedfiles: 373 vf.fallback_versionedfiles.append(base_vf) 374 return last_revision 375 376 def preview_transform(self, pb=None): 377 from .transform import TransformPreview 378 return TransformPreview(self, pb=pb) 379 380 381def find_ids_across_trees(filenames, trees, require_versioned=True): 382 """Find the ids corresponding to specified filenames. 383 384 All matches in all trees will be used, and all children of matched 385 directories will be used. 386 387 :param filenames: The filenames to find file_ids for (if None, returns 388 None) 389 :param trees: The trees to find file_ids within 390 :param require_versioned: if true, all specified filenames must occur in 391 at least one tree. 392 :return: a set of file ids for the specified filenames and their children. 393 """ 394 if not filenames: 395 return None 396 specified_path_ids = _find_ids_across_trees(filenames, trees, 397 require_versioned) 398 return _find_children_across_trees(specified_path_ids, trees) 399 400 401def _find_ids_across_trees(filenames, trees, require_versioned): 402 """Find the ids corresponding to specified filenames. 403 404 All matches in all trees will be used, but subdirectories are not scanned. 405 406 :param filenames: The filenames to find file_ids for 407 :param trees: The trees to find file_ids within 408 :param require_versioned: if true, all specified filenames must occur in 409 at least one tree. 410 :return: a set of file ids for the specified filenames 411 """ 412 not_versioned = [] 413 interesting_ids = set() 414 for tree_path in filenames: 415 not_found = True 416 for tree in trees: 417 file_id = tree.path2id(tree_path) 418 if file_id is not None: 419 interesting_ids.add(file_id) 420 not_found = False 421 if not_found: 422 not_versioned.append(tree_path) 423 if len(not_versioned) > 0 and require_versioned: 424 raise errors.PathsNotVersionedError(not_versioned) 425 return interesting_ids 426 427 428def _find_children_across_trees(specified_ids, trees): 429 """Return a set including specified ids and their children. 430 431 All matches in all trees will be used. 432 433 :param trees: The trees to find file_ids within 434 :return: a set containing all specified ids and their children 435 """ 436 interesting_ids = set(specified_ids) 437 pending = interesting_ids 438 # now handle children of interesting ids 439 # we loop so that we handle all children of each id in both trees 440 while len(pending) > 0: 441 new_pending = set() 442 for file_id in pending: 443 for tree in trees: 444 try: 445 path = tree.id2path(file_id) 446 except errors.NoSuchId: 447 continue 448 try: 449 for child in tree.iter_child_entries(path): 450 if child.file_id not in interesting_ids: 451 new_pending.add(child.file_id) 452 except errors.NotADirectory: 453 pass 454 interesting_ids.update(new_pending) 455 pending = new_pending 456 return interesting_ids 457 458 459class MutableInventoryTree(MutableTree, InventoryTree): 460 461 def apply_inventory_delta(self, changes): 462 """Apply changes to the inventory as an atomic operation. 463 464 :param changes: An inventory delta to apply to the working tree's 465 inventory. 466 :return None: 467 :seealso Inventory.apply_delta: For details on the changes parameter. 468 """ 469 with self.lock_tree_write(): 470 self.flush() 471 inv = self.root_inventory 472 inv.apply_delta(changes) 473 self._write_inventory(inv) 474 475 def has_changes(self, _from_tree=None): 476 """Quickly check that the tree contains at least one commitable change. 477 478 :param _from_tree: tree to compare against to find changes (default to 479 the basis tree and is intended to be used by tests). 480 481 :return: True if a change is found. False otherwise 482 """ 483 with self.lock_read(): 484 # Check pending merges 485 if len(self.get_parent_ids()) > 1: 486 return True 487 if _from_tree is None: 488 _from_tree = self.basis_tree() 489 changes = self.iter_changes(_from_tree) 490 if self.supports_symlinks(): 491 # Fast path for has_changes. 492 try: 493 change = next(changes) 494 # Exclude root (talk about black magic... --vila 20090629) 495 if change.parent_id == (None, None): 496 change = next(changes) 497 return True 498 except StopIteration: 499 # No changes 500 return False 501 else: 502 # Slow path for has_changes. 503 # Handle platforms that do not support symlinks in the 504 # conditional below. This is slower than the try/except 505 # approach below that but we don't have a choice as we 506 # need to be sure that all symlinks are removed from the 507 # entire changeset. This is because in platforms that 508 # do not support symlinks, they show up as None in the 509 # working copy as compared to the repository. 510 # Also, exclude root as mention in the above fast path. 511 changes = filter( 512 lambda c: c[6][0] != 'symlink' and c[4] != (None, None), 513 changes) 514 try: 515 next(iter(changes)) 516 except StopIteration: 517 return False 518 return True 519 520 def _fix_case_of_inventory_path(self, path): 521 """If our tree isn't case sensitive, return the canonical path""" 522 if not self.case_sensitive: 523 path = self.get_canonical_path(path) 524 return path 525 526 def smart_add(self, file_list, recurse=True, action=None, save=True): 527 """Version file_list, optionally recursing into directories. 528 529 This is designed more towards DWIM for humans than API clarity. 530 For the specific behaviour see the help for cmd_add(). 531 532 :param file_list: List of zero or more paths. *NB: these are 533 interpreted relative to the process cwd, not relative to the 534 tree.* (Add and most other tree methods use tree-relative 535 paths.) 536 :param action: A reporter to be called with the inventory, parent_ie, 537 path and kind of the path being added. It may return a file_id if 538 a specific one should be used. 539 :param save: Save the inventory after completing the adds. If False 540 this provides dry-run functionality by doing the add and not saving 541 the inventory. 542 :return: A tuple - files_added, ignored_files. files_added is the count 543 of added files, and ignored_files is a dict mapping files that were 544 ignored to the rule that caused them to be ignored. 545 """ 546 with self.lock_tree_write(): 547 # Not all mutable trees can have conflicts 548 if getattr(self, 'conflicts', None) is not None: 549 # Collect all related files without checking whether they exist or 550 # are versioned. It's cheaper to do that once for all conflicts 551 # than trying to find the relevant conflict for each added file. 552 conflicts_related = set() 553 for c in self.conflicts(): 554 conflicts_related.update(c.associated_filenames()) 555 else: 556 conflicts_related = None 557 adder = _SmartAddHelper(self, action, conflicts_related) 558 adder.add(file_list, recurse=recurse) 559 if save: 560 invdelta = adder.get_inventory_delta() 561 self.apply_inventory_delta(invdelta) 562 return adder.added, adder.ignored 563 564 def update_basis_by_delta(self, new_revid, delta): 565 """Update the parents of this tree after a commit. 566 567 This gives the tree one parent, with revision id new_revid. The 568 inventory delta is applied to the current basis tree to generate the 569 inventory for the parent new_revid, and all other parent trees are 570 discarded. 571 572 All the changes in the delta should be changes synchronising the basis 573 tree with some or all of the working tree, with a change to a directory 574 requiring that its contents have been recursively included. That is, 575 this is not a general purpose tree modification routine, but a helper 576 for commit which is not required to handle situations that do not arise 577 outside of commit. 578 579 See the inventory developers documentation for the theory behind 580 inventory deltas. 581 582 :param new_revid: The new revision id for the trees parent. 583 :param delta: An inventory delta (see apply_inventory_delta) describing 584 the changes from the current left most parent revision to new_revid. 585 """ 586 # if the tree is updated by a pull to the branch, as happens in 587 # WorkingTree2, when there was no separation between branch and tree, 588 # then just clear merges, efficiency is not a concern for now as this 589 # is legacy environments only, and they are slow regardless. 590 if self.last_revision() == new_revid: 591 self.set_parent_ids([new_revid]) 592 return 593 # generic implementation based on Inventory manipulation. See 594 # WorkingTree classes for optimised versions for specific format trees. 595 basis = self.basis_tree() 596 with basis.lock_read(): 597 # TODO: Consider re-evaluating the need for this with CHKInventory 598 # we don't strictly need to mutate an inventory for this 599 # it only makes sense when apply_delta is cheaper than get_inventory() 600 inventory = _mod_inventory.mutable_inventory_from_tree(basis) 601 inventory.apply_delta(delta) 602 rev_tree = InventoryRevisionTree(self.branch.repository, 603 inventory, new_revid) 604 self.set_parent_trees([(new_revid, rev_tree)]) 605 606 def transform(self, pb=None): 607 from .transform import InventoryTreeTransform 608 return InventoryTreeTransform(self, pb=pb) 609 610 611class _SmartAddHelper(object): 612 """Helper for MutableTree.smart_add.""" 613 614 def get_inventory_delta(self): 615 # GZ 2016-06-05: Returning view would probably be fine but currently 616 # Inventory.apply_delta is documented as requiring a list of changes. 617 return list(self._invdelta.values()) 618 619 def _get_ie(self, inv_path): 620 """Retrieve the most up to date inventory entry for a path. 621 622 :param inv_path: Normalized inventory path 623 :return: Inventory entry (with possibly invalid .children for 624 directories) 625 """ 626 entry = self._invdelta.get(inv_path) 627 if entry is not None: 628 return entry[3] 629 # Find a 'best fit' match if the filesystem is case-insensitive 630 inv_path = self.tree._fix_case_of_inventory_path(inv_path) 631 try: 632 return next(self.tree.iter_entries_by_dir( 633 specific_files=[inv_path]))[1] 634 except StopIteration: 635 return None 636 637 def _convert_to_directory(self, this_ie, inv_path): 638 """Convert an entry to a directory. 639 640 :param this_ie: Inventory entry 641 :param inv_path: Normalized path for the inventory entry 642 :return: The new inventory entry 643 """ 644 # Same as in _add_one below, if the inventory doesn't 645 # think this is a directory, update the inventory 646 this_ie = _mod_inventory.InventoryDirectory( 647 this_ie.file_id, this_ie.name, this_ie.parent_id) 648 self._invdelta[inv_path] = (inv_path, inv_path, this_ie.file_id, 649 this_ie) 650 return this_ie 651 652 def _add_one_and_parent(self, parent_ie, path, kind, inv_path): 653 """Add a new entry to the inventory and automatically add unversioned parents. 654 655 :param parent_ie: Parent inventory entry if known, or None. If 656 None, the parent is looked up by name and used if present, otherwise it 657 is recursively added. 658 :param path: Filesystem path to add 659 :param kind: Kind of new entry (file, directory, etc) 660 :param inv_path: Inventory path 661 :return: Inventory entry for path and a list of paths which have been added. 662 """ 663 # Nothing to do if path is already versioned. 664 # This is safe from infinite recursion because the tree root is 665 # always versioned. 666 inv_dirname = osutils.dirname(inv_path) 667 dirname, basename = osutils.split(path) 668 if parent_ie is None: 669 # slower but does not need parent_ie 670 this_ie = self._get_ie(inv_path) 671 if this_ie is not None: 672 return this_ie 673 # its really not there : add the parent 674 # note that the dirname use leads to some extra str copying etc but as 675 # there are a limited number of dirs we can be nested under, it should 676 # generally find it very fast and not recurse after that. 677 parent_ie = self._add_one_and_parent(None, 678 dirname, 'directory', 679 inv_dirname) 680 # if the parent exists, but isn't a directory, we have to do the 681 # kind change now -- really the inventory shouldn't pretend to know 682 # the kind of wt files, but it does. 683 if parent_ie.kind != 'directory': 684 # nb: this relies on someone else checking that the path we're using 685 # doesn't contain symlinks. 686 parent_ie = self._convert_to_directory(parent_ie, inv_dirname) 687 file_id = self.action(self.tree, parent_ie, path, kind) 688 entry = _mod_inventory.make_entry(kind, basename, parent_ie.file_id, 689 file_id=file_id) 690 self._invdelta[inv_path] = (None, inv_path, entry.file_id, entry) 691 self.added.append(inv_path) 692 return entry 693 694 def _gather_dirs_to_add(self, user_dirs): 695 # only walk the minimal parents needed: we have user_dirs to override 696 # ignores. 697 prev_dir = None 698 699 is_inside = osutils.is_inside_or_parent_of_any 700 for path in sorted(user_dirs): 701 if (prev_dir is None or not is_inside([prev_dir], path)): 702 inv_path, this_ie = user_dirs[path] 703 yield (path, inv_path, this_ie, None) 704 prev_dir = path 705 706 def __init__(self, tree, action, conflicts_related=None): 707 self.tree = tree 708 if action is None: 709 self.action = add.AddAction() 710 else: 711 self.action = action 712 self._invdelta = {} 713 self.added = [] 714 self.ignored = {} 715 if conflicts_related is None: 716 self.conflicts_related = frozenset() 717 else: 718 self.conflicts_related = conflicts_related 719 720 def add(self, file_list, recurse=True): 721 if not file_list: 722 # no paths supplied: add the entire tree. 723 # FIXME: this assumes we are running in a working tree subdir :-/ 724 # -- vila 20100208 725 file_list = [u'.'] 726 727 # expand any symlinks in the directory part, while leaving the 728 # filename alone 729 # only expanding if symlinks are supported avoids windows path bugs 730 if self.tree.supports_symlinks(): 731 file_list = list(map(osutils.normalizepath, file_list)) 732 733 user_dirs = {} 734 # validate user file paths and convert all paths to tree 735 # relative : it's cheaper to make a tree relative path an abspath 736 # than to convert an abspath to tree relative, and it's cheaper to 737 # perform the canonicalization in bulk. 738 for filepath in osutils.canonical_relpaths(self.tree.basedir, file_list): 739 # validate user parameters. Our recursive code avoids adding new 740 # files that need such validation 741 if self.tree.is_control_filename(filepath): 742 raise errors.ForbiddenControlFileError(filename=filepath) 743 744 abspath = self.tree.abspath(filepath) 745 kind = osutils.file_kind(abspath) 746 # ensure the named path is added, so that ignore rules in the later 747 # directory walk dont skip it. 748 # we dont have a parent ie known yet.: use the relatively slower 749 # inventory probing method 750 inv_path, _ = osutils.normalized_filename(filepath) 751 this_ie = self._get_ie(inv_path) 752 if this_ie is None: 753 this_ie = self._add_one_and_parent( 754 None, filepath, kind, inv_path) 755 if kind == 'directory': 756 # schedule the dir for scanning 757 user_dirs[filepath] = (inv_path, this_ie) 758 759 if not recurse: 760 # no need to walk any directories at all. 761 return 762 763 things_to_add = list(self._gather_dirs_to_add(user_dirs)) 764 765 illegalpath_re = re.compile(r'[\r\n]') 766 for directory, inv_path, this_ie, parent_ie in things_to_add: 767 # directory is tree-relative 768 abspath = self.tree.abspath(directory) 769 770 # get the contents of this directory. 771 772 # find the kind of the path being added, and save stat_value 773 # for reuse 774 stat_value = None 775 if this_ie is None: 776 stat_value = osutils.file_stat(abspath) 777 kind = osutils.file_kind_from_stat_mode(stat_value.st_mode) 778 else: 779 kind = this_ie.kind 780 781 # allow AddAction to skip this file 782 if self.action.skip_file(self.tree, abspath, kind, stat_value): 783 continue 784 if not _mod_inventory.InventoryEntry.versionable_kind(kind): 785 trace.warning("skipping %s (can't add file of kind '%s')", 786 abspath, kind) 787 continue 788 if illegalpath_re.search(directory): 789 trace.warning("skipping %r (contains \\n or \\r)" % abspath) 790 continue 791 if directory in self.conflicts_related: 792 # If the file looks like one generated for a conflict, don't 793 # add it. 794 trace.warning( 795 'skipping %s (generated to help resolve conflicts)', 796 abspath) 797 continue 798 799 if kind == 'directory' and directory != '': 800 try: 801 transport = _mod_transport.get_transport_from_path(abspath) 802 controldir.ControlDirFormat.find_format(transport) 803 sub_tree = True 804 except errors.NotBranchError: 805 sub_tree = False 806 except errors.UnsupportedFormatError: 807 sub_tree = True 808 else: 809 sub_tree = False 810 811 if this_ie is not None: 812 pass 813 elif sub_tree: 814 # XXX: This is wrong; people *might* reasonably be trying to 815 # add subtrees as subtrees. This should probably only be done 816 # in formats which can represent subtrees, and even then 817 # perhaps only when the user asked to add subtrees. At the 818 # moment you can add them specially through 'join --reference', 819 # which is perhaps reasonable: adding a new reference is a 820 # special operation and can have a special behaviour. mbp 821 # 20070306 822 trace.warning("skipping nested tree %r", abspath) 823 else: 824 this_ie = self._add_one_and_parent(parent_ie, directory, kind, 825 inv_path) 826 827 if kind == 'directory' and not sub_tree: 828 if this_ie.kind != 'directory': 829 this_ie = self._convert_to_directory(this_ie, inv_path) 830 831 for subf in sorted(os.listdir(abspath)): 832 inv_f, _ = osutils.normalized_filename(subf) 833 # here we could use TreeDirectory rather than 834 # string concatenation. 835 subp = osutils.pathjoin(directory, subf) 836 # TODO: is_control_filename is very slow. Make it faster. 837 # TreeDirectory.is_control_filename could also make this 838 # faster - its impossible for a non root dir to have a 839 # control file. 840 if self.tree.is_control_filename(subp): 841 trace.mutter("skip control directory %r", subp) 842 continue 843 sub_invp = osutils.pathjoin(inv_path, inv_f) 844 entry = self._invdelta.get(sub_invp) 845 if entry is not None: 846 sub_ie = entry[3] 847 else: 848 sub_ie = this_ie.children.get(inv_f) 849 if sub_ie is not None: 850 # recurse into this already versioned subdir. 851 things_to_add.append((subp, sub_invp, sub_ie, this_ie)) 852 else: 853 # user selection overrides ignores 854 # ignore while selecting files - if we globbed in the 855 # outer loop we would ignore user files. 856 ignore_glob = self.tree.is_ignored(subp) 857 if ignore_glob is not None: 858 self.ignored.setdefault( 859 ignore_glob, []).append(subp) 860 else: 861 things_to_add.append( 862 (subp, sub_invp, None, this_ie)) 863 864 865class InventoryRevisionTree(RevisionTree, InventoryTree): 866 867 def __init__(self, repository, inv, revision_id): 868 RevisionTree.__init__(self, repository, revision_id) 869 self._inventory = inv 870 871 def _get_file_revision(self, path, file_id, vf, tree_revision): 872 """Ensure that file_id, tree_revision is in vf to plan the merge.""" 873 last_revision = self.get_file_revision(path) 874 base_vf = self._repository.texts 875 if base_vf not in vf.fallback_versionedfiles: 876 vf.fallback_versionedfiles.append(base_vf) 877 return last_revision 878 879 def get_file_mtime(self, path): 880 ie = self._path2ie(path) 881 try: 882 revision = self._repository.get_revision(ie.revision) 883 except errors.NoSuchRevision: 884 raise FileTimestampUnavailable(path) 885 return revision.timestamp 886 887 def get_file_size(self, path): 888 return self._path2ie(path).text_size 889 890 def get_file_sha1(self, path, stat_value=None): 891 ie = self._path2ie(path) 892 if ie.kind == "file": 893 return ie.text_sha1 894 return None 895 896 def get_file_revision(self, path): 897 return self._path2ie(path).revision 898 899 def is_executable(self, path): 900 ie = self._path2ie(path) 901 if ie.kind != "file": 902 return False 903 return ie.executable 904 905 def has_filename(self, filename): 906 return bool(self.path2id(filename)) 907 908 def reference_parent(self, path, branch=None, possible_transports=None): 909 if branch is not None: 910 file_id = self.path2id(path) 911 parent_url = branch.get_reference_info(file_id)[0] 912 else: 913 subdir = ControlDir.open_from_transport( 914 self._repository.user_transport.clone(path)) 915 parent_url = subdir.open_branch().get_parent() 916 if parent_url is None: 917 return None 918 return _mod_branch.Branch.open( 919 parent_url, 920 possible_transports=possible_transports) 921 922 def get_reference_info(self, path, branch=None): 923 return branch.get_reference_info(self.path2id(path))[0] 924 925 def list_files(self, include_root=False, from_dir=None, recursive=True, 926 recurse_nested=False): 927 # The only files returned by this are those from the version 928 if from_dir is None: 929 from_dir_id = None 930 inv = self.root_inventory 931 else: 932 inv, from_dir_id = self._path2inv_file_id(from_dir) 933 if from_dir_id is None: 934 # Directory not versioned 935 return 936 entries = inv.iter_entries(from_dir=from_dir_id, recursive=recursive) 937 if inv.root is not None and not include_root and from_dir is None: 938 # skip the root for compatibility with the current apis. 939 next(entries) 940 for path, entry in entries: 941 if entry.kind == 'tree-reference' and recurse_nested: 942 subtree = self._get_nested_tree( 943 path, entry.file_id, entry.reference_revision) 944 for subpath, status, kind, entry in subtree.list_files( 945 include_root=True, recurse_nested=recurse_nested, 946 recursive=recursive): 947 if subpath: 948 full_subpath = osutils.pathjoin(path, subpath) 949 else: 950 full_subpath = path 951 yield full_subpath, status, kind, entry 952 else: 953 yield path, 'V', entry.kind, entry 954 955 def get_symlink_target(self, path): 956 # Inventories store symlink targets in unicode 957 return self._path2ie(path).symlink_target 958 959 def get_reference_revision(self, path): 960 return self._path2ie(path).reference_revision 961 962 def _get_nested_tree(self, path, file_id, reference_revision): 963 # Just a guess.. 964 subdir = ControlDir.open_from_transport( 965 self._repository.user_transport.clone(path)) 966 subrepo = subdir.find_repository() 967 try: 968 revtree = subrepo.revision_tree(reference_revision) 969 except errors.NoSuchRevision: 970 raise MissingNestedTree(path) 971 if file_id is not None and file_id != revtree.path2id(''): 972 raise AssertionError('invalid root id: %r != %r' % ( 973 file_id, revtree.path2id(''))) 974 return revtree 975 976 def get_nested_tree(self, path): 977 nested_revid = self.get_reference_revision(path) 978 return self._get_nested_tree(path, None, nested_revid) 979 980 def kind(self, path): 981 return self._path2ie(path).kind 982 983 def path_content_summary(self, path): 984 """See Tree.path_content_summary.""" 985 try: 986 entry = self._path2ie(path) 987 except errors.NoSuchFile: 988 return ('missing', None, None, None) 989 kind = entry.kind 990 if kind == 'file': 991 return (kind, entry.text_size, entry.executable, entry.text_sha1) 992 elif kind == 'symlink': 993 return (kind, None, None, entry.symlink_target) 994 else: 995 return (kind, None, None, None) 996 997 def _comparison_data(self, entry, path): 998 if entry is None: 999 return None, False, None 1000 return entry.kind, entry.executable, None 1001 1002 def walkdirs(self, prefix=""): 1003 _directory = 'directory' 1004 inv, top_id = self._path2inv_file_id(prefix) 1005 if top_id is None: 1006 pending = [] 1007 else: 1008 pending = [(prefix, top_id)] 1009 while pending: 1010 dirblock = [] 1011 root, file_id = pending.pop() 1012 if root: 1013 relroot = root + '/' 1014 else: 1015 relroot = "" 1016 # FIXME: stash the node in pending 1017 entry = inv.get_entry(file_id) 1018 subdirs = [] 1019 for name, child in entry.sorted_children(): 1020 toppath = relroot + name 1021 dirblock.append((toppath, name, child.kind, None, child.kind)) 1022 if child.kind == _directory: 1023 subdirs.append((toppath, child.file_id)) 1024 yield root, dirblock 1025 # push the user specified dirs from dirblock 1026 pending.extend(reversed(subdirs)) 1027 1028 def iter_files_bytes(self, desired_files): 1029 """See Tree.iter_files_bytes. 1030 1031 This version is implemented on top of Repository.iter_files_bytes""" 1032 repo_desired_files = [(self.path2id(f), self.get_file_revision(f), i) 1033 for f, i in desired_files] 1034 try: 1035 for result in self._repository.iter_files_bytes(repo_desired_files): 1036 yield result 1037 except errors.RevisionNotPresent as e: 1038 raise errors.NoSuchFile(e.file_id) 1039 1040 def annotate_iter(self, path, default_revision=revision.CURRENT_REVISION): 1041 """See Tree.annotate_iter""" 1042 file_id = self.path2id(path) 1043 text_key = (file_id, self.get_file_revision(path)) 1044 annotator = self._repository.texts.get_annotator() 1045 annotations = annotator.annotate_flat(text_key) 1046 return [(key[-1], line) for key, line in annotations] 1047 1048 def __eq__(self, other): 1049 if self is other: 1050 return True 1051 if isinstance(other, InventoryRevisionTree): 1052 return (self.root_inventory == other.root_inventory) 1053 return False 1054 1055 def __ne__(self, other): 1056 return not (self == other) 1057 1058 def __hash__(self): 1059 raise ValueError('not hashable') 1060 1061 1062class InterInventoryTree(InterTree): 1063 """InterTree implementation for InventoryTree objects. 1064 1065 """ 1066 # Formats that will be used to test this InterTree. If both are 1067 # None, this InterTree will not be tested (e.g. because a complex 1068 # setup is required) 1069 _matching_from_tree_format = None 1070 _matching_to_tree_format = None 1071 1072 @classmethod 1073 def is_compatible(kls, source, target): 1074 # The default implementation is naive and uses the public API, so 1075 # it works for all trees. 1076 return (isinstance(source, InventoryTree) and 1077 isinstance(target, InventoryTree)) 1078 1079 def _changes_from_entries(self, source_entry, target_entry, source_path, 1080 target_path): 1081 """Generate a iter_changes tuple between source_entry and target_entry. 1082 1083 :param source_entry: An inventory entry from self.source, or None. 1084 :param target_entry: An inventory entry from self.target, or None. 1085 :param source_path: The path of source_entry. 1086 :param target_path: The path of target_entry. 1087 :return: A tuple, item 0 of which is an iter_changes result tuple, and 1088 item 1 is True if there are any changes in the result tuple. 1089 """ 1090 if source_entry is None: 1091 if target_entry is None: 1092 return None 1093 file_id = target_entry.file_id 1094 else: 1095 file_id = source_entry.file_id 1096 if source_entry is not None: 1097 source_versioned = True 1098 source_name = source_entry.name 1099 source_parent = source_entry.parent_id 1100 source_kind, source_executable, source_stat = \ 1101 self.source._comparison_data(source_entry, source_path) 1102 else: 1103 source_versioned = False 1104 source_name = None 1105 source_parent = None 1106 source_kind = None 1107 source_executable = None 1108 if target_entry is not None: 1109 target_versioned = True 1110 target_name = target_entry.name 1111 target_parent = target_entry.parent_id 1112 target_kind, target_executable, target_stat = \ 1113 self.target._comparison_data(target_entry, target_path) 1114 else: 1115 target_versioned = False 1116 target_name = None 1117 target_parent = None 1118 target_kind = None 1119 target_executable = None 1120 versioned = (source_versioned, target_versioned) 1121 kind = (source_kind, target_kind) 1122 changed_content = False 1123 if source_kind != target_kind: 1124 changed_content = True 1125 elif source_kind == 'file': 1126 if not self.file_content_matches( 1127 source_path, target_path, 1128 source_stat, target_stat): 1129 changed_content = True 1130 elif source_kind == 'symlink': 1131 if (self.source.get_symlink_target(source_path) != 1132 self.target.get_symlink_target(target_path)): 1133 changed_content = True 1134 elif source_kind == 'tree-reference': 1135 if (self.source.get_reference_revision(source_path) 1136 != self.target.get_reference_revision(target_path)): 1137 changed_content = True 1138 parent = (source_parent, target_parent) 1139 name = (source_name, target_name) 1140 executable = (source_executable, target_executable) 1141 if (changed_content is not False or versioned[0] != versioned[1] or 1142 parent[0] != parent[1] or name[0] != name[1] or 1143 executable[0] != executable[1]): 1144 changes = True 1145 else: 1146 changes = False 1147 return InventoryTreeChange( 1148 file_id, (source_path, target_path), changed_content, 1149 versioned, parent, name, kind, executable), changes 1150 1151 def iter_changes(self, include_unchanged=False, 1152 specific_files=None, pb=None, extra_trees=[], 1153 require_versioned=True, want_unversioned=False): 1154 """Generate an iterator of changes between trees. 1155 1156 A tuple is returned: 1157 (file_id, (path_in_source, path_in_target), 1158 changed_content, versioned, parent, name, kind, 1159 executable) 1160 1161 Changed_content is True if the file's content has changed. This 1162 includes changes to its kind, and to a symlink's target. 1163 1164 versioned, parent, name, kind, executable are tuples of (from, to). 1165 If a file is missing in a tree, its kind is None. 1166 1167 Iteration is done in parent-to-child order, relative to the target 1168 tree. 1169 1170 There is no guarantee that all paths are in sorted order: the 1171 requirement to expand the search due to renames may result in children 1172 that should be found early being found late in the search, after 1173 lexically later results have been returned. 1174 :param require_versioned: Raise errors.PathsNotVersionedError if a 1175 path in the specific_files list is not versioned in one of 1176 source, target or extra_trees. 1177 :param specific_files: An optional list of file paths to restrict the 1178 comparison to. When mapping filenames to ids, all matches in all 1179 trees (including optional extra_trees) are used, and all children 1180 of matched directories are included. The parents in the target tree 1181 of the specific files up to and including the root of the tree are 1182 always evaluated for changes too. 1183 :param want_unversioned: Should unversioned files be returned in the 1184 output. An unversioned file is defined as one with (False, False) 1185 for the versioned pair. 1186 """ 1187 if not extra_trees: 1188 extra_trees = [] 1189 else: 1190 extra_trees = list(extra_trees) 1191 # The ids of items we need to examine to insure delta consistency. 1192 precise_file_ids = set() 1193 changed_file_ids = [] 1194 if specific_files == []: 1195 target_specific_files = [] 1196 source_specific_files = [] 1197 else: 1198 target_specific_files = self.target.find_related_paths_across_trees( 1199 specific_files, [self.source] + extra_trees, 1200 require_versioned=require_versioned) 1201 source_specific_files = self.source.find_related_paths_across_trees( 1202 specific_files, [self.target] + extra_trees, 1203 require_versioned=require_versioned) 1204 if specific_files is not None: 1205 # reparented or added entries must have their parents included 1206 # so that valid deltas can be created. The seen_parents set 1207 # tracks the parents that we need to have. 1208 # The seen_dirs set tracks directory entries we've yielded. 1209 # After outputting version object in to_entries we set difference 1210 # the two seen sets and start checking parents. 1211 seen_parents = set() 1212 seen_dirs = set() 1213 if want_unversioned: 1214 all_unversioned = sorted([(p.split('/'), p) for p in 1215 self.target.extras() 1216 if specific_files is None or 1217 osutils.is_inside_any(specific_files, p)]) 1218 all_unversioned = deque(all_unversioned) 1219 else: 1220 all_unversioned = deque() 1221 to_paths = {} 1222 from_entries_by_dir = list(self.source.iter_entries_by_dir( 1223 specific_files=source_specific_files)) 1224 from_data = dict(from_entries_by_dir) 1225 to_entries_by_dir = list(self.target.iter_entries_by_dir( 1226 specific_files=target_specific_files)) 1227 path_equivs = self.find_source_paths([p for p, e in to_entries_by_dir]) 1228 num_entries = len(from_entries_by_dir) + len(to_entries_by_dir) 1229 entry_count = 0 1230 # the unversioned path lookup only occurs on real trees - where there 1231 # can be extras. So the fake_entry is solely used to look up 1232 # executable it values when execute is not supported. 1233 fake_entry = TreeFile() 1234 for target_path, target_entry in to_entries_by_dir: 1235 while (all_unversioned and 1236 all_unversioned[0][0] < target_path.split('/')): 1237 unversioned_path = all_unversioned.popleft() 1238 target_kind, target_executable, target_stat = \ 1239 self.target._comparison_data( 1240 fake_entry, unversioned_path[1]) 1241 yield InventoryTreeChange( 1242 None, (None, unversioned_path[1]), True, (False, False), 1243 (None, None), 1244 (None, unversioned_path[0][-1]), 1245 (None, target_kind), 1246 (None, target_executable)) 1247 source_path = path_equivs[target_path] 1248 if source_path is not None: 1249 source_entry = from_data.get(source_path) 1250 else: 1251 source_entry = None 1252 result, changes = self._changes_from_entries( 1253 source_entry, target_entry, source_path=source_path, target_path=target_path) 1254 to_paths[result.file_id] = result.path[1] 1255 entry_count += 1 1256 if result.versioned[0]: 1257 entry_count += 1 1258 if pb is not None: 1259 pb.update('comparing files', entry_count, num_entries) 1260 if changes or include_unchanged: 1261 if specific_files is not None: 1262 precise_file_ids.add(result.parent_id[1]) 1263 changed_file_ids.append(result.file_id) 1264 yield result 1265 # Ensure correct behaviour for reparented/added specific files. 1266 if specific_files is not None: 1267 # Record output dirs 1268 if result.kind[1] == 'directory': 1269 seen_dirs.add(result.file_id) 1270 # Record parents of reparented/added entries. 1271 if not result.versioned[0] or result.is_reparented(): 1272 seen_parents.add(result.parent_id[1]) 1273 while all_unversioned: 1274 # yield any trailing unversioned paths 1275 unversioned_path = all_unversioned.popleft() 1276 to_kind, to_executable, to_stat = \ 1277 self.target._comparison_data(fake_entry, unversioned_path[1]) 1278 yield InventoryTreeChange( 1279 None, (None, unversioned_path[1]), True, (False, False), 1280 (None, None), 1281 (None, unversioned_path[0][-1]), 1282 (None, to_kind), 1283 (None, to_executable)) 1284 # Yield all remaining source paths 1285 for path, from_entry in from_entries_by_dir: 1286 file_id = from_entry.file_id 1287 if file_id in to_paths: 1288 # already returned 1289 continue 1290 to_path = self.find_target_path(path) 1291 entry_count += 1 1292 if pb is not None: 1293 pb.update('comparing files', entry_count, num_entries) 1294 versioned = (True, False) 1295 parent = (from_entry.parent_id, None) 1296 name = (from_entry.name, None) 1297 from_kind, from_executable, stat_value = \ 1298 self.source._comparison_data(from_entry, path) 1299 kind = (from_kind, None) 1300 executable = (from_executable, None) 1301 changed_content = from_kind is not None 1302 # the parent's path is necessarily known at this point. 1303 changed_file_ids.append(file_id) 1304 yield InventoryTreeChange( 1305 file_id, (path, to_path), changed_content, versioned, parent, 1306 name, kind, executable) 1307 changed_file_ids = set(changed_file_ids) 1308 if specific_files is not None: 1309 for result in self._handle_precise_ids(precise_file_ids, 1310 changed_file_ids): 1311 yield result 1312 1313 @staticmethod 1314 def _get_entry(tree, path): 1315 """Get an inventory entry from a tree, with missing entries as None. 1316 1317 If the tree raises NotImplementedError on accessing .inventory, then 1318 this is worked around using iter_entries_by_dir on just the file id 1319 desired. 1320 1321 :param tree: The tree to lookup the entry in. 1322 :param path: The path to look up 1323 """ 1324 # No inventory available. 1325 try: 1326 iterator = tree.iter_entries_by_dir(specific_files=[path]) 1327 return next(iterator)[1] 1328 except StopIteration: 1329 return None 1330 1331 def _handle_precise_ids(self, precise_file_ids, changed_file_ids, 1332 discarded_changes=None): 1333 """Fill out a partial iter_changes to be consistent. 1334 1335 :param precise_file_ids: The file ids of parents that were seen during 1336 the iter_changes. 1337 :param changed_file_ids: The file ids of already emitted items. 1338 :param discarded_changes: An optional dict of precalculated 1339 iter_changes items which the partial iter_changes had not output 1340 but had calculated. 1341 :return: A generator of iter_changes items to output. 1342 """ 1343 # process parents of things that had changed under the users 1344 # requested paths to prevent incorrect paths or parent ids which 1345 # aren't in the tree. 1346 while precise_file_ids: 1347 precise_file_ids.discard(None) 1348 # Don't emit file_ids twice 1349 precise_file_ids.difference_update(changed_file_ids) 1350 if not precise_file_ids: 1351 break 1352 # If the there was something at a given output path in source, we 1353 # have to include the entry from source in the delta, or we would 1354 # be putting this entry into a used path. 1355 paths = [] 1356 for parent_id in precise_file_ids: 1357 try: 1358 paths.append(self.target.id2path(parent_id)) 1359 except errors.NoSuchId: 1360 # This id has been dragged in from the source by delta 1361 # expansion and isn't present in target at all: we don't 1362 # need to check for path collisions on it. 1363 pass 1364 for path in paths: 1365 old_id = self.source.path2id(path) 1366 precise_file_ids.add(old_id) 1367 precise_file_ids.discard(None) 1368 current_ids = precise_file_ids 1369 precise_file_ids = set() 1370 # We have to emit all of precise_file_ids that have been altered. 1371 # We may have to output the children of some of those ids if any 1372 # directories have stopped being directories. 1373 for file_id in current_ids: 1374 # Examine file_id 1375 if discarded_changes: 1376 result = discarded_changes.get(file_id) 1377 source_entry = None 1378 else: 1379 result = None 1380 if result is None: 1381 try: 1382 source_path = self.source.id2path(file_id) 1383 except errors.NoSuchId: 1384 source_path = None 1385 source_entry = None 1386 else: 1387 source_entry = self._get_entry( 1388 self.source, source_path) 1389 try: 1390 target_path = self.target.id2path(file_id) 1391 except errors.NoSuchId: 1392 target_path = None 1393 target_entry = None 1394 else: 1395 target_entry = self._get_entry( 1396 self.target, target_path) 1397 result, changes = self._changes_from_entries( 1398 source_entry, target_entry, source_path, target_path) 1399 else: 1400 changes = True 1401 # Get this parents parent to examine. 1402 new_parent_id = result.parent_id[1] 1403 precise_file_ids.add(new_parent_id) 1404 if changes: 1405 if (result.kind[0] == 'directory' and 1406 result.kind[1] != 'directory'): 1407 # This stopped being a directory, the old children have 1408 # to be included. 1409 if source_entry is None: 1410 # Reusing a discarded change. 1411 source_entry = self._get_entry( 1412 self.source, result.path[0]) 1413 precise_file_ids.update( 1414 child.file_id 1415 for child in self.source.iter_child_entries(result.path[0])) 1416 changed_file_ids.add(result.file_id) 1417 yield result 1418 1419 def find_target_path(self, path, recurse='none'): 1420 """Find target tree path. 1421 1422 :param path: Path to search for (exists in source) 1423 :return: path in target, or None if there is no equivalent path. 1424 :raise NoSuchFile: If the path doesn't exist in source 1425 """ 1426 file_id = self.source.path2id(path) 1427 if file_id is None: 1428 raise errors.NoSuchFile(path) 1429 try: 1430 return self.target.id2path(file_id, recurse=recurse) 1431 except errors.NoSuchId: 1432 return None 1433 1434 def find_source_path(self, path, recurse='none'): 1435 """Find the source tree path. 1436 1437 :param path: Path to search for (exists in target) 1438 :return: path in source, or None if there is no equivalent path. 1439 :raise NoSuchFile: if the path doesn't exist in target 1440 """ 1441 file_id = self.target.path2id(path) 1442 if file_id is None: 1443 raise errors.NoSuchFile(path) 1444 try: 1445 return self.source.id2path(file_id, recurse=recurse) 1446 except errors.NoSuchId: 1447 return None 1448 1449 1450InterTree.register_optimiser(InterInventoryTree) 1451 1452 1453class InterCHKRevisionTree(InterInventoryTree): 1454 """Fast path optimiser for RevisionTrees with CHK inventories.""" 1455 1456 @staticmethod 1457 def is_compatible(source, target): 1458 if (isinstance(source, RevisionTree) and 1459 isinstance(target, RevisionTree)): 1460 try: 1461 # Only CHK inventories have id_to_entry attribute 1462 source.root_inventory.id_to_entry 1463 target.root_inventory.id_to_entry 1464 return True 1465 except AttributeError: 1466 pass 1467 return False 1468 1469 def iter_changes(self, include_unchanged=False, 1470 specific_files=None, pb=None, extra_trees=[], 1471 require_versioned=True, want_unversioned=False): 1472 lookup_trees = [self.source] 1473 if extra_trees: 1474 lookup_trees.extend(extra_trees) 1475 # The ids of items we need to examine to insure delta consistency. 1476 precise_file_ids = set() 1477 discarded_changes = {} 1478 if specific_files == []: 1479 specific_file_ids = [] 1480 else: 1481 specific_file_ids = self.target.paths2ids(specific_files, 1482 lookup_trees, require_versioned=require_versioned) 1483 # FIXME: It should be possible to delegate include_unchanged handling 1484 # to CHKInventory.iter_changes and do a better job there -- vila 1485 # 20090304 1486 changed_file_ids = set() 1487 # FIXME: nested tree support 1488 for result in self.target.root_inventory.iter_changes( 1489 self.source.root_inventory): 1490 result = InventoryTreeChange(*result) 1491 if specific_file_ids is not None: 1492 if result.file_id not in specific_file_ids: 1493 # A change from the whole tree that we don't want to show yet. 1494 # We may find that we need to show it for delta consistency, so 1495 # stash it. 1496 discarded_changes[result.file_id] = result 1497 continue 1498 precise_file_ids.add(result.parent_id[1]) 1499 yield result 1500 changed_file_ids.add(result.file_id) 1501 if specific_file_ids is not None: 1502 for result in self._handle_precise_ids(precise_file_ids, 1503 changed_file_ids, discarded_changes=discarded_changes): 1504 yield result 1505 if include_unchanged: 1506 # CHKMap avoid being O(tree), so we go to O(tree) only if 1507 # required to. 1508 # Now walk the whole inventory, excluding the already yielded 1509 # file ids 1510 # FIXME: Support nested trees 1511 changed_file_ids = set(changed_file_ids) 1512 for relpath, entry in self.target.root_inventory.iter_entries(): 1513 if (specific_file_ids is not None and 1514 entry.file_id not in specific_file_ids): 1515 continue 1516 if entry.file_id not in changed_file_ids: 1517 yield InventoryTreeChange( 1518 entry.file_id, 1519 (relpath, relpath), # Not renamed 1520 False, # Not modified 1521 (True, True), # Still versioned 1522 (entry.parent_id, entry.parent_id), 1523 (entry.name, entry.name), 1524 (entry.kind, entry.kind), 1525 (entry.executable, entry.executable)) 1526 1527 1528InterTree.register_optimiser(InterCHKRevisionTree) 1529