1# Copyright (C) 2006-2011 Canonical Ltd 2# Copyright (C) 2020 Breezy Developers 3# 4# This program is free software; you can redistribute it and/or modify 5# it under the terms of the GNU General Public License as published by 6# the Free Software Foundation; either version 2 of the License, or 7# (at your option) any later version. 8# 9# This program is distributed in the hope that it will be useful, 10# but WITHOUT ANY WARRANTY; without even the implied warranty of 11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12# GNU General Public License for more details. 13# 14# You should have received a copy of the GNU General Public License 15# along with this program; if not, write to the Free Software 16# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 17 18from __future__ import absolute_import 19 20import contextlib 21import errno 22import os 23from stat import S_IEXEC, S_ISREG 24import time 25 26from .. import ( 27 annotate, 28 conflicts, 29 controldir, 30 errors, 31 lock, 32 multiparent, 33 osutils, 34 revision as _mod_revision, 35 trace, 36 tree, 37 ui, 38 urlutils, 39 ) 40 41from ..filters import filtered_output_bytes, ContentFilterContext 42from ..i18n import gettext 43from ..mutabletree import MutableTree 44from ..progress import ProgressPhase 45from ..transform import ( 46 ROOT_PARENT, 47 _FileMover, 48 _TransformResults, 49 joinpath, 50 NoFinalPath, 51 FinalPaths, 52 unique_add, 53 TreeTransform, 54 TransformRenameFailed, 55 ImmortalLimbo, 56 ReusingTransform, 57 MalformedTransform, 58 PreviewTree, 59 new_by_entry, 60 _reparent_children, 61 resolve_conflicts, 62 ) 63from ..tree import find_previous_path 64from .conflicts import Conflict 65 66from . import ( 67 inventory, 68 inventorytree, 69 ) 70 71 72def _content_match(tree, entry, tree_path, kind, target_path): 73 if entry.kind != kind: 74 return False 75 if entry.kind == "directory": 76 return True 77 if entry.kind == "file": 78 with open(target_path, 'rb') as f1, \ 79 tree.get_file(tree_path) as f2: 80 if osutils.compare_files(f1, f2): 81 return True 82 elif entry.kind == "symlink": 83 if tree.get_symlink_target(tree_path) == os.readlink(target_path): 84 return True 85 return False 86 87 88class TreeTransformBase(TreeTransform): 89 """The base class for TreeTransform and its kin.""" 90 91 def __init__(self, tree, pb=None, case_sensitive=True): 92 """Constructor. 93 94 :param tree: The tree that will be transformed, but not necessarily 95 the output tree. 96 :param pb: ignored 97 :param case_sensitive: If True, the target of the transform is 98 case sensitive, not just case preserving. 99 """ 100 super(TreeTransformBase, self).__init__(tree, pb=pb) 101 # mapping of trans_id => (sha1 of content, stat_value) 102 self._observed_sha1s = {} 103 # Mapping of trans_id -> new file_id 104 self._new_id = {} 105 # Mapping of old file-id -> trans_id 106 self._non_present_ids = {} 107 # Mapping of new file_id -> trans_id 108 self._r_new_id = {} 109 # The trans_id that will be used as the tree root 110 if tree.is_versioned(''): 111 self._new_root = self.trans_id_tree_path('') 112 else: 113 self._new_root = None 114 # Whether the target is case sensitive 115 self._case_sensitive_target = case_sensitive 116 117 def finalize(self): 118 """Release the working tree lock, if held. 119 120 This is required if apply has not been invoked, but can be invoked 121 even after apply. 122 """ 123 if self._tree is None: 124 return 125 for hook in MutableTree.hooks['post_transform']: 126 hook(self._tree, self) 127 self._tree.unlock() 128 self._tree = None 129 130 def __get_root(self): 131 return self._new_root 132 133 root = property(__get_root) 134 135 def create_path(self, name, parent): 136 """Assign a transaction id to a new path""" 137 trans_id = self.assign_id() 138 unique_add(self._new_name, trans_id, name) 139 unique_add(self._new_parent, trans_id, parent) 140 return trans_id 141 142 def adjust_root_path(self, name, parent): 143 """Emulate moving the root by moving all children, instead. 144 145 We do this by undoing the association of root's transaction id with the 146 current tree. This allows us to create a new directory with that 147 transaction id. We unversion the root directory and version the 148 physically new directory, and hope someone versions the tree root 149 later. 150 """ 151 old_root = self._new_root 152 old_root_file_id = self.final_file_id(old_root) 153 # force moving all children of root 154 for child_id in self.iter_tree_children(old_root): 155 if child_id != parent: 156 self.adjust_path(self.final_name(child_id), 157 self.final_parent(child_id), child_id) 158 file_id = self.final_file_id(child_id) 159 if file_id is not None: 160 self.unversion_file(child_id) 161 self.version_file(child_id, file_id=file_id) 162 163 # the physical root needs a new transaction id 164 self._tree_path_ids.pop("") 165 self._tree_id_paths.pop(old_root) 166 self._new_root = self.trans_id_tree_path('') 167 if parent == old_root: 168 parent = self._new_root 169 self.adjust_path(name, parent, old_root) 170 self.create_directory(old_root) 171 self.version_file(old_root, file_id=old_root_file_id) 172 self.unversion_file(self._new_root) 173 174 def fixup_new_roots(self): 175 """Reinterpret requests to change the root directory 176 177 Instead of creating a root directory, or moving an existing directory, 178 all the attributes and children of the new root are applied to the 179 existing root directory. 180 181 This means that the old root trans-id becomes obsolete, so it is 182 recommended only to invoke this after the root trans-id has become 183 irrelevant. 184 185 """ 186 new_roots = [k for k, v in self._new_parent.items() 187 if v == ROOT_PARENT] 188 if len(new_roots) < 1: 189 return 190 if len(new_roots) != 1: 191 raise ValueError('A tree cannot have two roots!') 192 if self._new_root is None: 193 self._new_root = new_roots[0] 194 return 195 old_new_root = new_roots[0] 196 # unversion the new root's directory. 197 if self.final_kind(self._new_root) is None: 198 file_id = self.final_file_id(old_new_root) 199 else: 200 file_id = self.final_file_id(self._new_root) 201 if old_new_root in self._new_id: 202 self.cancel_versioning(old_new_root) 203 else: 204 self.unversion_file(old_new_root) 205 # if, at this stage, root still has an old file_id, zap it so we can 206 # stick a new one in. 207 if (self.tree_file_id(self._new_root) is not None 208 and self._new_root not in self._removed_id): 209 self.unversion_file(self._new_root) 210 if file_id is not None: 211 self.version_file(self._new_root, file_id=file_id) 212 213 # Now move children of new root into old root directory. 214 # Ensure all children are registered with the transaction, but don't 215 # use directly-- some tree children have new parents 216 list(self.iter_tree_children(old_new_root)) 217 # Move all children of new root into old root directory. 218 for child in self.by_parent().get(old_new_root, []): 219 self.adjust_path(self.final_name(child), self._new_root, child) 220 221 # Ensure old_new_root has no directory. 222 if old_new_root in self._new_contents: 223 self.cancel_creation(old_new_root) 224 else: 225 self.delete_contents(old_new_root) 226 227 # prevent deletion of root directory. 228 if self._new_root in self._removed_contents: 229 self.cancel_deletion(self._new_root) 230 231 # destroy path info for old_new_root. 232 del self._new_parent[old_new_root] 233 del self._new_name[old_new_root] 234 235 def trans_id_file_id(self, file_id): 236 """Determine or set the transaction id associated with a file ID. 237 A new id is only created for file_ids that were never present. If 238 a transaction has been unversioned, it is deliberately still returned. 239 (this will likely lead to an unversioned parent conflict.) 240 """ 241 if file_id is None: 242 raise ValueError('None is not a valid file id') 243 if file_id in self._r_new_id and self._r_new_id[file_id] is not None: 244 return self._r_new_id[file_id] 245 else: 246 try: 247 path = self._tree.id2path(file_id) 248 except errors.NoSuchId: 249 if file_id in self._non_present_ids: 250 return self._non_present_ids[file_id] 251 else: 252 trans_id = self.assign_id() 253 self._non_present_ids[file_id] = trans_id 254 return trans_id 255 else: 256 return self.trans_id_tree_path(path) 257 258 def version_file(self, trans_id, file_id=None): 259 """Schedule a file to become versioned.""" 260 raise NotImplementedError(self.version_file) 261 262 def cancel_versioning(self, trans_id): 263 """Undo a previous versioning of a file""" 264 raise NotImplementedError(self.cancel_versioning) 265 266 def new_paths(self, filesystem_only=False): 267 """Determine the paths of all new and changed files. 268 269 :param filesystem_only: if True, only calculate values for files 270 that require renames or execute bit changes. 271 """ 272 new_ids = set() 273 if filesystem_only: 274 stale_ids = self._needs_rename.difference(self._new_name) 275 stale_ids.difference_update(self._new_parent) 276 stale_ids.difference_update(self._new_contents) 277 stale_ids.difference_update(self._new_id) 278 needs_rename = self._needs_rename.difference(stale_ids) 279 id_sets = (needs_rename, self._new_executability) 280 else: 281 id_sets = (self._new_name, self._new_parent, self._new_contents, 282 self._new_id, self._new_executability) 283 for id_set in id_sets: 284 new_ids.update(id_set) 285 return sorted(FinalPaths(self).get_paths(new_ids)) 286 287 def tree_file_id(self, trans_id): 288 """Determine the file id associated with the trans_id in the tree""" 289 path = self.tree_path(trans_id) 290 if path is None: 291 return None 292 # the file is old; the old id is still valid 293 if self._new_root == trans_id: 294 return self._tree.path2id('') 295 return self._tree.path2id(path) 296 297 def final_is_versioned(self, trans_id): 298 return self.final_file_id(trans_id) is not None 299 300 def final_file_id(self, trans_id): 301 """Determine the file id after any changes are applied, or None. 302 303 None indicates that the file will not be versioned after changes are 304 applied. 305 """ 306 try: 307 return self._new_id[trans_id] 308 except KeyError: 309 if trans_id in self._removed_id: 310 return None 311 return self.tree_file_id(trans_id) 312 313 def inactive_file_id(self, trans_id): 314 """Return the inactive file_id associated with a transaction id. 315 That is, the one in the tree or in non_present_ids. 316 The file_id may actually be active, too. 317 """ 318 file_id = self.tree_file_id(trans_id) 319 if file_id is not None: 320 return file_id 321 for key, value in self._non_present_ids.items(): 322 if value == trans_id: 323 return key 324 325 def find_raw_conflicts(self): 326 """Find any violations of inventory or filesystem invariants""" 327 if self._done is True: 328 raise ReusingTransform() 329 conflicts = [] 330 # ensure all children of all existent parents are known 331 # all children of non-existent parents are known, by definition. 332 self._add_tree_children() 333 by_parent = self.by_parent() 334 conflicts.extend(self._unversioned_parents(by_parent)) 335 conflicts.extend(self._parent_loops()) 336 conflicts.extend(self._duplicate_entries(by_parent)) 337 conflicts.extend(self._parent_type_conflicts(by_parent)) 338 conflicts.extend(self._improper_versioning()) 339 conflicts.extend(self._executability_conflicts()) 340 conflicts.extend(self._overwrite_conflicts()) 341 return conflicts 342 343 def _check_malformed(self): 344 conflicts = self.find_raw_conflicts() 345 if len(conflicts) != 0: 346 raise MalformedTransform(conflicts=conflicts) 347 348 def _add_tree_children(self): 349 """Add all the children of all active parents to the known paths. 350 351 Active parents are those which gain children, and those which are 352 removed. This is a necessary first step in detecting conflicts. 353 """ 354 parents = list(self.by_parent()) 355 parents.extend([t for t in self._removed_contents if 356 self.tree_kind(t) == 'directory']) 357 for trans_id in self._removed_id: 358 path = self.tree_path(trans_id) 359 if path is not None: 360 if self._tree.stored_kind(path) == 'directory': 361 parents.append(trans_id) 362 elif self.tree_kind(trans_id) == 'directory': 363 parents.append(trans_id) 364 365 for parent_id in parents: 366 # ensure that all children are registered with the transaction 367 list(self.iter_tree_children(parent_id)) 368 369 def _has_named_child(self, name, parent_id, known_children): 370 """Does a parent already have a name child. 371 372 :param name: The searched for name. 373 374 :param parent_id: The parent for which the check is made. 375 376 :param known_children: The already known children. This should have 377 been recently obtained from `self.by_parent.get(parent_id)` 378 (or will be if None is passed). 379 """ 380 if known_children is None: 381 known_children = self.by_parent().get(parent_id, []) 382 for child in known_children: 383 if self.final_name(child) == name: 384 return True 385 parent_path = self._tree_id_paths.get(parent_id, None) 386 if parent_path is None: 387 # No parent... no children 388 return False 389 child_path = joinpath(parent_path, name) 390 child_id = self._tree_path_ids.get(child_path, None) 391 if child_id is None: 392 # Not known by the tree transform yet, check the filesystem 393 return osutils.lexists(self._tree.abspath(child_path)) 394 else: 395 raise AssertionError('child_id is missing: %s, %s, %s' 396 % (name, parent_id, child_id)) 397 398 def _available_backup_name(self, name, target_id): 399 """Find an available backup name. 400 401 :param name: The basename of the file. 402 403 :param target_id: The directory trans_id where the backup should 404 be placed. 405 """ 406 known_children = self.by_parent().get(target_id, []) 407 return osutils.available_backup_name( 408 name, 409 lambda base: self._has_named_child( 410 base, target_id, known_children)) 411 412 def _parent_loops(self): 413 """No entry should be its own ancestor""" 414 for trans_id in self._new_parent: 415 seen = set() 416 parent_id = trans_id 417 while parent_id != ROOT_PARENT: 418 seen.add(parent_id) 419 try: 420 parent_id = self.final_parent(parent_id) 421 except KeyError: 422 break 423 if parent_id == trans_id: 424 yield ('parent loop', trans_id) 425 if parent_id in seen: 426 break 427 428 def _unversioned_parents(self, by_parent): 429 """If parent directories are versioned, children must be versioned.""" 430 for parent_id, children in by_parent.items(): 431 if parent_id == ROOT_PARENT: 432 continue 433 if self.final_is_versioned(parent_id): 434 continue 435 for child_id in children: 436 if self.final_is_versioned(child_id): 437 yield ('unversioned parent', parent_id) 438 break 439 440 def _improper_versioning(self): 441 """Cannot version a file with no contents, or a bad type. 442 443 However, existing entries with no contents are okay. 444 """ 445 for trans_id in self._new_id: 446 kind = self.final_kind(trans_id) 447 if kind == 'symlink' and not self._tree.supports_symlinks(): 448 # Ignore symlinks as they are not supported on this platform 449 continue 450 if kind is None: 451 yield ('versioning no contents', trans_id) 452 continue 453 if not self._tree.versionable_kind(kind): 454 yield ('versioning bad kind', trans_id, kind) 455 456 def _executability_conflicts(self): 457 """Check for bad executability changes. 458 459 Only versioned files may have their executability set, because 460 1. only versioned entries can have executability under windows 461 2. only files can be executable. (The execute bit on a directory 462 does not indicate searchability) 463 """ 464 for trans_id in self._new_executability: 465 if not self.final_is_versioned(trans_id): 466 yield ('unversioned executability', trans_id) 467 else: 468 if self.final_kind(trans_id) != "file": 469 yield ('non-file executability', trans_id) 470 471 def _overwrite_conflicts(self): 472 """Check for overwrites (not permitted on Win32)""" 473 for trans_id in self._new_contents: 474 if self.tree_kind(trans_id) is None: 475 continue 476 if trans_id not in self._removed_contents: 477 yield ('overwrite', trans_id, self.final_name(trans_id)) 478 479 def _duplicate_entries(self, by_parent): 480 """No directory may have two entries with the same name.""" 481 if (self._new_name, self._new_parent) == ({}, {}): 482 return 483 for children in by_parent.values(): 484 name_ids = [] 485 for child_tid in children: 486 name = self.final_name(child_tid) 487 if name is not None: 488 # Keep children only if they still exist in the end 489 if not self._case_sensitive_target: 490 name = name.lower() 491 name_ids.append((name, child_tid)) 492 name_ids.sort() 493 last_name = None 494 last_trans_id = None 495 for name, trans_id in name_ids: 496 kind = self.final_kind(trans_id) 497 if kind is None and not self.final_is_versioned(trans_id): 498 continue 499 if name == last_name: 500 yield ('duplicate', last_trans_id, trans_id, name) 501 last_name = name 502 last_trans_id = trans_id 503 504 def _parent_type_conflicts(self, by_parent): 505 """Children must have a directory parent""" 506 for parent_id, children in by_parent.items(): 507 if parent_id == ROOT_PARENT: 508 continue 509 no_children = True 510 for child_id in children: 511 if self.final_kind(child_id) is not None: 512 no_children = False 513 break 514 if no_children: 515 continue 516 # There is at least a child, so we need an existing directory to 517 # contain it. 518 kind = self.final_kind(parent_id) 519 if kind is None: 520 # The directory will be deleted 521 yield ('missing parent', parent_id) 522 elif kind != "directory": 523 # Meh, we need a *directory* to put something in it 524 yield ('non-directory parent', parent_id) 525 526 def _set_executability(self, path, trans_id): 527 """Set the executability of versioned files """ 528 if self._tree._supports_executable(): 529 new_executability = self._new_executability[trans_id] 530 abspath = self._tree.abspath(path) 531 current_mode = os.stat(abspath).st_mode 532 if new_executability: 533 umask = os.umask(0) 534 os.umask(umask) 535 to_mode = current_mode | (0o100 & ~umask) 536 # Enable x-bit for others only if they can read it. 537 if current_mode & 0o004: 538 to_mode |= 0o001 & ~umask 539 if current_mode & 0o040: 540 to_mode |= 0o010 & ~umask 541 else: 542 to_mode = current_mode & ~0o111 543 osutils.chmod_if_possible(abspath, to_mode) 544 545 def _new_entry(self, name, parent_id, file_id): 546 """Helper function to create a new filesystem entry.""" 547 trans_id = self.create_path(name, parent_id) 548 if file_id is not None: 549 self.version_file(trans_id, file_id=file_id) 550 return trans_id 551 552 def new_file(self, name, parent_id, contents, file_id=None, 553 executable=None, sha1=None): 554 """Convenience method to create files. 555 556 name is the name of the file to create. 557 parent_id is the transaction id of the parent directory of the file. 558 contents is an iterator of bytestrings, which will be used to produce 559 the file. 560 :param file_id: The inventory ID of the file, if it is to be versioned. 561 :param executable: Only valid when a file_id has been supplied. 562 """ 563 trans_id = self._new_entry(name, parent_id, file_id) 564 # TODO: rather than scheduling a set_executable call, 565 # have create_file create the file with the right mode. 566 self.create_file(contents, trans_id, sha1=sha1) 567 if executable is not None: 568 self.set_executability(executable, trans_id) 569 return trans_id 570 571 def new_directory(self, name, parent_id, file_id=None): 572 """Convenience method to create directories. 573 574 name is the name of the directory to create. 575 parent_id is the transaction id of the parent directory of the 576 directory. 577 file_id is the inventory ID of the directory, if it is to be versioned. 578 """ 579 trans_id = self._new_entry(name, parent_id, file_id) 580 self.create_directory(trans_id) 581 return trans_id 582 583 def new_symlink(self, name, parent_id, target, file_id=None): 584 """Convenience method to create symbolic link. 585 586 name is the name of the symlink to create. 587 parent_id is the transaction id of the parent directory of the symlink. 588 target is a bytestring of the target of the symlink. 589 file_id is the inventory ID of the file, if it is to be versioned. 590 """ 591 trans_id = self._new_entry(name, parent_id, file_id) 592 self.create_symlink(target, trans_id) 593 return trans_id 594 595 def new_orphan(self, trans_id, parent_id): 596 """Schedule an item to be orphaned. 597 598 When a directory is about to be removed, its children, if they are not 599 versioned are moved out of the way: they don't have a parent anymore. 600 601 :param trans_id: The trans_id of the existing item. 602 :param parent_id: The parent trans_id of the item. 603 """ 604 raise NotImplementedError(self.new_orphan) 605 606 def _get_potential_orphans(self, dir_id): 607 """Find the potential orphans in a directory. 608 609 A directory can't be safely deleted if there are versioned files in it. 610 If all the contained files are unversioned then they can be orphaned. 611 612 The 'None' return value means that the directory contains at least one 613 versioned file and should not be deleted. 614 615 :param dir_id: The directory trans id. 616 617 :return: A list of the orphan trans ids or None if at least one 618 versioned file is present. 619 """ 620 orphans = [] 621 # Find the potential orphans, stop if one item should be kept 622 for child_tid in self.by_parent()[dir_id]: 623 if child_tid in self._removed_contents: 624 # The child is removed as part of the transform. Since it was 625 # versioned before, it's not an orphan 626 continue 627 if not self.final_is_versioned(child_tid): 628 # The child is not versioned 629 orphans.append(child_tid) 630 else: 631 # We have a versioned file here, searching for orphans is 632 # meaningless. 633 orphans = None 634 break 635 return orphans 636 637 def _affected_ids(self): 638 """Return the set of transform ids affected by the transform""" 639 trans_ids = set(self._removed_id) 640 trans_ids.update(self._new_id) 641 trans_ids.update(self._removed_contents) 642 trans_ids.update(self._new_contents) 643 trans_ids.update(self._new_executability) 644 trans_ids.update(self._new_name) 645 trans_ids.update(self._new_parent) 646 return trans_ids 647 648 def _get_file_id_maps(self): 649 """Return mapping of file_ids to trans_ids in the to and from states""" 650 trans_ids = self._affected_ids() 651 from_trans_ids = {} 652 to_trans_ids = {} 653 # Build up two dicts: trans_ids associated with file ids in the 654 # FROM state, vs the TO state. 655 for trans_id in trans_ids: 656 from_file_id = self.tree_file_id(trans_id) 657 if from_file_id is not None: 658 from_trans_ids[from_file_id] = trans_id 659 to_file_id = self.final_file_id(trans_id) 660 if to_file_id is not None: 661 to_trans_ids[to_file_id] = trans_id 662 return from_trans_ids, to_trans_ids 663 664 def _from_file_data(self, from_trans_id, from_versioned, from_path): 665 """Get data about a file in the from (tree) state 666 667 Return a (name, parent, kind, executable) tuple 668 """ 669 from_path = self._tree_id_paths.get(from_trans_id) 670 if from_versioned: 671 # get data from working tree if versioned 672 from_entry = next(self._tree.iter_entries_by_dir( 673 specific_files=[from_path]))[1] 674 from_name = from_entry.name 675 from_parent = from_entry.parent_id 676 else: 677 from_entry = None 678 if from_path is None: 679 # File does not exist in FROM state 680 from_name = None 681 from_parent = None 682 else: 683 # File exists, but is not versioned. Have to use path- 684 # splitting stuff 685 from_name = os.path.basename(from_path) 686 tree_parent = self.get_tree_parent(from_trans_id) 687 from_parent = self.tree_file_id(tree_parent) 688 if from_path is not None: 689 from_kind, from_executable, from_stats = \ 690 self._tree._comparison_data(from_entry, from_path) 691 else: 692 from_kind = None 693 from_executable = False 694 return from_name, from_parent, from_kind, from_executable 695 696 def _to_file_data(self, to_trans_id, from_trans_id, from_executable): 697 """Get data about a file in the to (target) state 698 699 Return a (name, parent, kind, executable) tuple 700 """ 701 to_name = self.final_name(to_trans_id) 702 to_kind = self.final_kind(to_trans_id) 703 to_parent = self.final_file_id(self.final_parent(to_trans_id)) 704 if to_trans_id in self._new_executability: 705 to_executable = self._new_executability[to_trans_id] 706 elif to_trans_id == from_trans_id: 707 to_executable = from_executable 708 else: 709 to_executable = False 710 return to_name, to_parent, to_kind, to_executable 711 712 def iter_changes(self): 713 """Produce output in the same format as Tree.iter_changes. 714 715 Will produce nonsensical results if invoked while inventory/filesystem 716 conflicts (as reported by TreeTransform.find_raw_conflicts()) are present. 717 718 This reads the Transform, but only reproduces changes involving a 719 file_id. Files that are not versioned in either of the FROM or TO 720 states are not reflected. 721 """ 722 final_paths = FinalPaths(self) 723 from_trans_ids, to_trans_ids = self._get_file_id_maps() 724 results = [] 725 # Now iterate through all active file_ids 726 for file_id in set(from_trans_ids).union(to_trans_ids): 727 modified = False 728 from_trans_id = from_trans_ids.get(file_id) 729 # find file ids, and determine versioning state 730 if from_trans_id is None: 731 from_versioned = False 732 from_trans_id = to_trans_ids[file_id] 733 else: 734 from_versioned = True 735 to_trans_id = to_trans_ids.get(file_id) 736 if to_trans_id is None: 737 to_versioned = False 738 to_trans_id = from_trans_id 739 else: 740 to_versioned = True 741 742 if not from_versioned: 743 from_path = None 744 else: 745 from_path = self._tree_id_paths.get(from_trans_id) 746 if not to_versioned: 747 to_path = None 748 else: 749 to_path = final_paths.get_path(to_trans_id) 750 751 from_name, from_parent, from_kind, from_executable = \ 752 self._from_file_data(from_trans_id, from_versioned, from_path) 753 754 to_name, to_parent, to_kind, to_executable = \ 755 self._to_file_data(to_trans_id, from_trans_id, from_executable) 756 757 if from_kind != to_kind: 758 modified = True 759 elif to_kind in ('file', 'symlink') and ( 760 to_trans_id != from_trans_id 761 or to_trans_id in self._new_contents): 762 modified = True 763 if (not modified and from_versioned == to_versioned 764 and from_parent == to_parent and from_name == to_name 765 and from_executable == to_executable): 766 continue 767 results.append( 768 inventorytree.InventoryTreeChange( 769 file_id, (from_path, to_path), modified, 770 (from_versioned, to_versioned), 771 (from_parent, to_parent), 772 (from_name, to_name), 773 (from_kind, to_kind), 774 (from_executable, to_executable))) 775 776 def path_key(c): 777 return (c.path[0] or '', c.path[1] or '') 778 return iter(sorted(results, key=path_key)) 779 780 def get_preview_tree(self): 781 """Return a tree representing the result of the transform. 782 783 The tree is a snapshot, and altering the TreeTransform will invalidate 784 it. 785 """ 786 raise NotImplementedError(self.get_preview) 787 788 def commit(self, branch, message, merge_parents=None, strict=False, 789 timestamp=None, timezone=None, committer=None, authors=None, 790 revprops=None, revision_id=None): 791 """Commit the result of this TreeTransform to a branch. 792 793 :param branch: The branch to commit to. 794 :param message: The message to attach to the commit. 795 :param merge_parents: Additional parent revision-ids specified by 796 pending merges. 797 :param strict: If True, abort the commit if there are unversioned 798 files. 799 :param timestamp: if not None, seconds-since-epoch for the time and 800 date. (May be a float.) 801 :param timezone: Optional timezone for timestamp, as an offset in 802 seconds. 803 :param committer: Optional committer in email-id format. 804 (e.g. "J Random Hacker <jrandom@example.com>") 805 :param authors: Optional list of authors in email-id format. 806 :param revprops: Optional dictionary of revision properties. 807 :param revision_id: Optional revision id. (Specifying a revision-id 808 may reduce performance for some non-native formats.) 809 :return: The revision_id of the revision committed. 810 """ 811 self._check_malformed() 812 if strict: 813 unversioned = set(self._new_contents).difference(set(self._new_id)) 814 for trans_id in unversioned: 815 if not self.final_is_versioned(trans_id): 816 raise errors.StrictCommitFailed() 817 818 revno, last_rev_id = branch.last_revision_info() 819 if last_rev_id == _mod_revision.NULL_REVISION: 820 if merge_parents is not None: 821 raise ValueError('Cannot supply merge parents for first' 822 ' commit.') 823 parent_ids = [] 824 else: 825 parent_ids = [last_rev_id] 826 if merge_parents is not None: 827 parent_ids.extend(merge_parents) 828 if self._tree.get_revision_id() != last_rev_id: 829 raise ValueError('TreeTransform not based on branch basis: %s' % 830 self._tree.get_revision_id().decode('utf-8')) 831 from .. import commit 832 revprops = commit.Commit.update_revprops(revprops, branch, authors) 833 builder = branch.get_commit_builder(parent_ids, 834 timestamp=timestamp, 835 timezone=timezone, 836 committer=committer, 837 revprops=revprops, 838 revision_id=revision_id) 839 preview = self.get_preview_tree() 840 list(builder.record_iter_changes(preview, last_rev_id, 841 self.iter_changes())) 842 builder.finish_inventory() 843 revision_id = builder.commit(message) 844 branch.set_last_revision_info(revno + 1, revision_id) 845 return revision_id 846 847 def _text_parent(self, trans_id): 848 path = self.tree_path(trans_id) 849 try: 850 if path is None or self._tree.kind(path) != 'file': 851 return None 852 except errors.NoSuchFile: 853 return None 854 return path 855 856 def _get_parents_texts(self, trans_id): 857 """Get texts for compression parents of this file.""" 858 path = self._text_parent(trans_id) 859 if path is None: 860 return () 861 return (self._tree.get_file_text(path),) 862 863 def _get_parents_lines(self, trans_id): 864 """Get lines for compression parents of this file.""" 865 path = self._text_parent(trans_id) 866 if path is None: 867 return () 868 return (self._tree.get_file_lines(path),) 869 870 def serialize(self, serializer): 871 """Serialize this TreeTransform. 872 873 :param serializer: A Serialiser like pack.ContainerSerializer. 874 """ 875 from .. import bencode 876 new_name = {k.encode('utf-8'): v.encode('utf-8') 877 for k, v in self._new_name.items()} 878 new_parent = {k.encode('utf-8'): v.encode('utf-8') 879 for k, v in self._new_parent.items()} 880 new_id = {k.encode('utf-8'): v 881 for k, v in self._new_id.items()} 882 new_executability = {k.encode('utf-8'): int(v) 883 for k, v in self._new_executability.items()} 884 tree_path_ids = {k.encode('utf-8'): v.encode('utf-8') 885 for k, v in self._tree_path_ids.items()} 886 non_present_ids = {k: v.encode('utf-8') 887 for k, v in self._non_present_ids.items()} 888 removed_contents = [trans_id.encode('utf-8') 889 for trans_id in self._removed_contents] 890 removed_id = [trans_id.encode('utf-8') 891 for trans_id in self._removed_id] 892 attribs = { 893 b'_id_number': self._id_number, 894 b'_new_name': new_name, 895 b'_new_parent': new_parent, 896 b'_new_executability': new_executability, 897 b'_new_id': new_id, 898 b'_tree_path_ids': tree_path_ids, 899 b'_removed_id': removed_id, 900 b'_removed_contents': removed_contents, 901 b'_non_present_ids': non_present_ids, 902 } 903 yield serializer.bytes_record(bencode.bencode(attribs), 904 ((b'attribs',),)) 905 for trans_id, kind in sorted(self._new_contents.items()): 906 if kind == 'file': 907 with open(self._limbo_name(trans_id), 'rb') as cur_file: 908 lines = cur_file.readlines() 909 parents = self._get_parents_lines(trans_id) 910 mpdiff = multiparent.MultiParent.from_lines(lines, parents) 911 content = b''.join(mpdiff.to_patch()) 912 if kind == 'directory': 913 content = b'' 914 if kind == 'symlink': 915 content = self._read_symlink_target(trans_id) 916 if not isinstance(content, bytes): 917 content = content.encode('utf-8') 918 yield serializer.bytes_record( 919 content, ((trans_id.encode('utf-8'), kind.encode('ascii')),)) 920 921 def deserialize(self, records): 922 """Deserialize a stored TreeTransform. 923 924 :param records: An iterable of (names, content) tuples, as per 925 pack.ContainerPushParser. 926 """ 927 from .. import bencode 928 names, content = next(records) 929 attribs = bencode.bdecode(content) 930 self._id_number = attribs[b'_id_number'] 931 self._new_name = {k.decode('utf-8'): v.decode('utf-8') 932 for k, v in attribs[b'_new_name'].items()} 933 self._new_parent = {k.decode('utf-8'): v.decode('utf-8') 934 for k, v in attribs[b'_new_parent'].items()} 935 self._new_executability = { 936 k.decode('utf-8'): bool(v) 937 for k, v in attribs[b'_new_executability'].items()} 938 self._new_id = {k.decode('utf-8'): v 939 for k, v in attribs[b'_new_id'].items()} 940 self._r_new_id = {v: k for k, v in self._new_id.items()} 941 self._tree_path_ids = {} 942 self._tree_id_paths = {} 943 for bytepath, trans_id in attribs[b'_tree_path_ids'].items(): 944 path = bytepath.decode('utf-8') 945 trans_id = trans_id.decode('utf-8') 946 self._tree_path_ids[path] = trans_id 947 self._tree_id_paths[trans_id] = path 948 self._removed_id = {trans_id.decode('utf-8') 949 for trans_id in attribs[b'_removed_id']} 950 self._removed_contents = set( 951 trans_id.decode('utf-8') 952 for trans_id in attribs[b'_removed_contents']) 953 self._non_present_ids = { 954 k: v.decode('utf-8') 955 for k, v in attribs[b'_non_present_ids'].items()} 956 for ((trans_id, kind),), content in records: 957 trans_id = trans_id.decode('utf-8') 958 kind = kind.decode('ascii') 959 if kind == 'file': 960 mpdiff = multiparent.MultiParent.from_patch(content) 961 lines = mpdiff.to_lines(self._get_parents_texts(trans_id)) 962 self.create_file(lines, trans_id) 963 if kind == 'directory': 964 self.create_directory(trans_id) 965 if kind == 'symlink': 966 self.create_symlink(content.decode('utf-8'), trans_id) 967 968 def create_file(self, contents, trans_id, mode_id=None, sha1=None): 969 """Schedule creation of a new file. 970 971 :seealso: new_file. 972 973 :param contents: an iterator of strings, all of which will be written 974 to the target destination. 975 :param trans_id: TreeTransform handle 976 :param mode_id: If not None, force the mode of the target file to match 977 the mode of the object referenced by mode_id. 978 Otherwise, we will try to preserve mode bits of an existing file. 979 :param sha1: If the sha1 of this content is already known, pass it in. 980 We can use it to prevent future sha1 computations. 981 """ 982 raise NotImplementedError(self.create_file) 983 984 def create_directory(self, trans_id): 985 """Schedule creation of a new directory. 986 987 See also new_directory. 988 """ 989 raise NotImplementedError(self.create_directory) 990 991 def create_symlink(self, target, trans_id): 992 """Schedule creation of a new symbolic link. 993 994 target is a bytestring. 995 See also new_symlink. 996 """ 997 raise NotImplementedError(self.create_symlink) 998 999 def create_hardlink(self, path, trans_id): 1000 """Schedule creation of a hard link""" 1001 raise NotImplementedError(self.create_hardlink) 1002 1003 def cancel_creation(self, trans_id): 1004 """Cancel the creation of new file contents.""" 1005 raise NotImplementedError(self.cancel_creation) 1006 1007 def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None): 1008 """Apply all changes to the inventory and filesystem. 1009 1010 If filesystem or inventory conflicts are present, MalformedTransform 1011 will be thrown. 1012 1013 If apply succeeds, finalize is not necessary. 1014 1015 :param no_conflicts: if True, the caller guarantees there are no 1016 conflicts, so no check is made. 1017 :param precomputed_delta: An inventory delta to use instead of 1018 calculating one. 1019 :param _mover: Supply an alternate FileMover, for testing 1020 """ 1021 raise NotImplementedError(self.apply) 1022 1023 def cook_conflicts(self, raw_conflicts): 1024 """Generate a list of cooked conflicts, sorted by file path""" 1025 content_conflict_file_ids = set() 1026 cooked_conflicts = list(iter_cook_conflicts(raw_conflicts, self)) 1027 for c in cooked_conflicts: 1028 if c.typestring == 'contents conflict': 1029 content_conflict_file_ids.add(c.file_id) 1030 # We want to get rid of path conflicts when a corresponding contents 1031 # conflict exists. This can occur when one branch deletes a file while 1032 # the other renames *and* modifies it. In this case, the content 1033 # conflict is enough. 1034 cooked_conflicts = [ 1035 c for c in cooked_conflicts 1036 if c.typestring != 'path conflict' or 1037 c.file_id not in content_conflict_file_ids] 1038 return sorted(cooked_conflicts, key=Conflict.sort_key) 1039 1040 1041def cook_path_conflict( 1042 tt, fp, conflict_type, trans_id, file_id, this_parent, this_name, 1043 other_parent, other_name): 1044 if this_parent is None or this_name is None: 1045 this_path = '<deleted>' 1046 else: 1047 parent_path = fp.get_path(tt.trans_id_file_id(this_parent)) 1048 this_path = osutils.pathjoin(parent_path, this_name) 1049 if other_parent is None or other_name is None: 1050 other_path = '<deleted>' 1051 else: 1052 try: 1053 parent_path = fp.get_path(tt.trans_id_file_id(other_parent)) 1054 except NoFinalPath: 1055 # The other entry was in a path that doesn't exist in our tree. 1056 # Put it in the root. 1057 parent_path = '' 1058 other_path = osutils.pathjoin(parent_path, other_name) 1059 return Conflict.factory( 1060 conflict_type, path=this_path, 1061 conflict_path=other_path, 1062 file_id=file_id) 1063 1064 1065def cook_content_conflict(tt, fp, conflict_type, trans_ids): 1066 for trans_id in trans_ids: 1067 file_id = tt.final_file_id(trans_id) 1068 if file_id is not None: 1069 # Ok we found the relevant file-id 1070 break 1071 path = fp.get_path(trans_id) 1072 for suffix in ('.BASE', '.THIS', '.OTHER'): 1073 if path.endswith(suffix): 1074 # Here is the raw path 1075 path = path[:-len(suffix)] 1076 break 1077 return Conflict.factory(conflict_type, path=path, file_id=file_id) 1078 1079 1080def cook_text_conflict(tt, fp, conflict_type, trans_id): 1081 path = fp.get_path(trans_id) 1082 file_id = tt.final_file_id(trans_id) 1083 return Conflict.factory(conflict_type, path=path, file_id=file_id) 1084 1085 1086CONFLICT_COOKERS = { 1087 'path conflict': cook_path_conflict, 1088 'text conflict': cook_text_conflict, 1089 'contents conflict': cook_content_conflict, 1090} 1091 1092def iter_cook_conflicts(raw_conflicts, tt): 1093 fp = FinalPaths(tt) 1094 for conflict in raw_conflicts: 1095 c_type = conflict[0] 1096 try: 1097 cooker = CONFLICT_COOKERS[c_type] 1098 except KeyError: 1099 action = conflict[1] 1100 modified_path = fp.get_path(conflict[2]) 1101 modified_id = tt.final_file_id(conflict[2]) 1102 if len(conflict) == 3: 1103 yield Conflict.factory( 1104 c_type, action=action, path=modified_path, file_id=modified_id) 1105 1106 else: 1107 conflicting_path = fp.get_path(conflict[3]) 1108 conflicting_id = tt.final_file_id(conflict[3]) 1109 yield Conflict.factory( 1110 c_type, action=action, path=modified_path, 1111 file_id=modified_id, 1112 conflict_path=conflicting_path, 1113 conflict_file_id=conflicting_id) 1114 else: 1115 yield cooker(tt, fp, *conflict) 1116 1117 1118class DiskTreeTransform(TreeTransformBase): 1119 """Tree transform storing its contents on disk.""" 1120 1121 def __init__(self, tree, limbodir, pb=None, case_sensitive=True): 1122 """Constructor. 1123 :param tree: The tree that will be transformed, but not necessarily 1124 the output tree. 1125 :param limbodir: A directory where new files can be stored until 1126 they are installed in their proper places 1127 :param pb: ignored 1128 :param case_sensitive: If True, the target of the transform is 1129 case sensitive, not just case preserving. 1130 """ 1131 TreeTransformBase.__init__(self, tree, pb, case_sensitive) 1132 self._limbodir = limbodir 1133 self._deletiondir = None 1134 # A mapping of transform ids to their limbo filename 1135 self._limbo_files = {} 1136 self._possibly_stale_limbo_files = set() 1137 # A mapping of transform ids to a set of the transform ids of children 1138 # that their limbo directory has 1139 self._limbo_children = {} 1140 # Map transform ids to maps of child filename to child transform id 1141 self._limbo_children_names = {} 1142 # List of transform ids that need to be renamed from limbo into place 1143 self._needs_rename = set() 1144 self._creation_mtime = None 1145 self._create_symlinks = osutils.supports_symlinks(self._limbodir) 1146 1147 def finalize(self): 1148 """Release the working tree lock, if held, clean up limbo dir. 1149 1150 This is required if apply has not been invoked, but can be invoked 1151 even after apply. 1152 """ 1153 if self._tree is None: 1154 return 1155 try: 1156 limbo_paths = list(self._limbo_files.values()) 1157 limbo_paths.extend(self._possibly_stale_limbo_files) 1158 limbo_paths.sort(reverse=True) 1159 for path in limbo_paths: 1160 try: 1161 osutils.delete_any(path) 1162 except OSError as e: 1163 if e.errno != errno.ENOENT: 1164 raise 1165 # XXX: warn? perhaps we just got interrupted at an 1166 # inconvenient moment, but perhaps files are disappearing 1167 # from under us? 1168 try: 1169 osutils.delete_any(self._limbodir) 1170 except OSError: 1171 # We don't especially care *why* the dir is immortal. 1172 raise ImmortalLimbo(self._limbodir) 1173 try: 1174 if self._deletiondir is not None: 1175 osutils.delete_any(self._deletiondir) 1176 except OSError: 1177 raise errors.ImmortalPendingDeletion(self._deletiondir) 1178 finally: 1179 TreeTransformBase.finalize(self) 1180 1181 def _limbo_supports_executable(self): 1182 """Check if the limbo path supports the executable bit.""" 1183 return osutils.supports_executable(self._limbodir) 1184 1185 def _limbo_name(self, trans_id): 1186 """Generate the limbo name of a file""" 1187 limbo_name = self._limbo_files.get(trans_id) 1188 if limbo_name is None: 1189 limbo_name = self._generate_limbo_path(trans_id) 1190 self._limbo_files[trans_id] = limbo_name 1191 return limbo_name 1192 1193 def _generate_limbo_path(self, trans_id): 1194 """Generate a limbo path using the trans_id as the relative path. 1195 1196 This is suitable as a fallback, and when the transform should not be 1197 sensitive to the path encoding of the limbo directory. 1198 """ 1199 self._needs_rename.add(trans_id) 1200 return osutils.pathjoin(self._limbodir, trans_id) 1201 1202 def adjust_path(self, name, parent, trans_id): 1203 previous_parent = self._new_parent.get(trans_id) 1204 previous_name = self._new_name.get(trans_id) 1205 super(DiskTreeTransform, self).adjust_path(name, parent, trans_id) 1206 if (trans_id in self._limbo_files 1207 and trans_id not in self._needs_rename): 1208 self._rename_in_limbo([trans_id]) 1209 if previous_parent != parent: 1210 self._limbo_children[previous_parent].remove(trans_id) 1211 if previous_parent != parent or previous_name != name: 1212 del self._limbo_children_names[previous_parent][previous_name] 1213 1214 def _rename_in_limbo(self, trans_ids): 1215 """Fix limbo names so that the right final path is produced. 1216 1217 This means we outsmarted ourselves-- we tried to avoid renaming 1218 these files later by creating them with their final names in their 1219 final parents. But now the previous name or parent is no longer 1220 suitable, so we have to rename them. 1221 1222 Even for trans_ids that have no new contents, we must remove their 1223 entries from _limbo_files, because they are now stale. 1224 """ 1225 for trans_id in trans_ids: 1226 old_path = self._limbo_files[trans_id] 1227 self._possibly_stale_limbo_files.add(old_path) 1228 del self._limbo_files[trans_id] 1229 if trans_id not in self._new_contents: 1230 continue 1231 new_path = self._limbo_name(trans_id) 1232 os.rename(old_path, new_path) 1233 self._possibly_stale_limbo_files.remove(old_path) 1234 for descendant in self._limbo_descendants(trans_id): 1235 desc_path = self._limbo_files[descendant] 1236 desc_path = new_path + desc_path[len(old_path):] 1237 self._limbo_files[descendant] = desc_path 1238 1239 def _limbo_descendants(self, trans_id): 1240 """Return the set of trans_ids whose limbo paths descend from this.""" 1241 descendants = set(self._limbo_children.get(trans_id, [])) 1242 for descendant in list(descendants): 1243 descendants.update(self._limbo_descendants(descendant)) 1244 return descendants 1245 1246 def _set_mode(self, trans_id, mode_id, typefunc): 1247 raise NotImplementedError(self._set_mode) 1248 1249 def create_file(self, contents, trans_id, mode_id=None, sha1=None): 1250 """Schedule creation of a new file. 1251 1252 :seealso: new_file. 1253 1254 :param contents: an iterator of strings, all of which will be written 1255 to the target destination. 1256 :param trans_id: TreeTransform handle 1257 :param mode_id: If not None, force the mode of the target file to match 1258 the mode of the object referenced by mode_id. 1259 Otherwise, we will try to preserve mode bits of an existing file. 1260 :param sha1: If the sha1 of this content is already known, pass it in. 1261 We can use it to prevent future sha1 computations. 1262 """ 1263 name = self._limbo_name(trans_id) 1264 with open(name, 'wb') as f: 1265 unique_add(self._new_contents, trans_id, 'file') 1266 f.writelines(contents) 1267 self._set_mtime(name) 1268 self._set_mode(trans_id, mode_id, S_ISREG) 1269 # It is unfortunate we have to use lstat instead of fstat, but we just 1270 # used utime and chmod on the file, so we need the accurate final 1271 # details. 1272 if sha1 is not None: 1273 self._observed_sha1s[trans_id] = (sha1, osutils.lstat(name)) 1274 1275 def _read_symlink_target(self, trans_id): 1276 return os.readlink(self._limbo_name(trans_id)) 1277 1278 def _set_mtime(self, path): 1279 """All files that are created get the same mtime. 1280 1281 This time is set by the first object to be created. 1282 """ 1283 if self._creation_mtime is None: 1284 self._creation_mtime = time.time() 1285 os.utime(path, (self._creation_mtime, self._creation_mtime)) 1286 1287 def create_hardlink(self, path, trans_id): 1288 """Schedule creation of a hard link""" 1289 name = self._limbo_name(trans_id) 1290 try: 1291 os.link(path, name) 1292 except OSError as e: 1293 if e.errno != errno.EPERM: 1294 raise 1295 raise errors.HardLinkNotSupported(path) 1296 try: 1297 unique_add(self._new_contents, trans_id, 'file') 1298 except BaseException: 1299 # Clean up the file, it never got registered so 1300 # TreeTransform.finalize() won't clean it up. 1301 os.unlink(name) 1302 raise 1303 1304 def create_directory(self, trans_id): 1305 """Schedule creation of a new directory. 1306 1307 See also new_directory. 1308 """ 1309 os.mkdir(self._limbo_name(trans_id)) 1310 unique_add(self._new_contents, trans_id, 'directory') 1311 1312 def create_symlink(self, target, trans_id): 1313 """Schedule creation of a new symbolic link. 1314 1315 target is a bytestring. 1316 See also new_symlink. 1317 """ 1318 if self._create_symlinks: 1319 os.symlink(target, self._limbo_name(trans_id)) 1320 else: 1321 try: 1322 path = FinalPaths(self).get_path(trans_id) 1323 except KeyError: 1324 path = None 1325 trace.warning( 1326 'Unable to create symlink "%s" on this filesystem.' % (path,)) 1327 # We add symlink to _new_contents even if they are unsupported 1328 # and not created. These entries are subsequently used to avoid 1329 # conflicts on platforms that don't support symlink 1330 unique_add(self._new_contents, trans_id, 'symlink') 1331 1332 def cancel_creation(self, trans_id): 1333 """Cancel the creation of new file contents.""" 1334 del self._new_contents[trans_id] 1335 if trans_id in self._observed_sha1s: 1336 del self._observed_sha1s[trans_id] 1337 children = self._limbo_children.get(trans_id) 1338 # if this is a limbo directory with children, move them before removing 1339 # the directory 1340 if children is not None: 1341 self._rename_in_limbo(children) 1342 del self._limbo_children[trans_id] 1343 del self._limbo_children_names[trans_id] 1344 osutils.delete_any(self._limbo_name(trans_id)) 1345 1346 def new_orphan(self, trans_id, parent_id): 1347 conf = self._tree.get_config_stack() 1348 handle_orphan = conf.get('transform.orphan_policy') 1349 handle_orphan(self, trans_id, parent_id) 1350 1351 1352class InventoryTreeTransform(DiskTreeTransform): 1353 """Represent a tree transformation. 1354 1355 This object is designed to support incremental generation of the transform, 1356 in any order. 1357 1358 However, it gives optimum performance when parent directories are created 1359 before their contents. The transform is then able to put child files 1360 directly in their parent directory, avoiding later renames. 1361 1362 It is easy to produce malformed transforms, but they are generally 1363 harmless. Attempting to apply a malformed transform will cause an 1364 exception to be raised before any modifications are made to the tree. 1365 1366 Many kinds of malformed transforms can be corrected with the 1367 resolve_conflicts function. The remaining ones indicate programming error, 1368 such as trying to create a file with no path. 1369 1370 Two sets of file creation methods are supplied. Convenience methods are: 1371 * new_file 1372 * new_directory 1373 * new_symlink 1374 1375 These are composed of the low-level methods: 1376 * create_path 1377 * create_file or create_directory or create_symlink 1378 * version_file 1379 * set_executability 1380 1381 Transform/Transaction ids 1382 ------------------------- 1383 trans_ids are temporary ids assigned to all files involved in a transform. 1384 It's possible, even common, that not all files in the Tree have trans_ids. 1385 1386 trans_ids are used because filenames and file_ids are not good enough 1387 identifiers; filenames change, and not all files have file_ids. File-ids 1388 are also associated with trans-ids, so that moving a file moves its 1389 file-id. 1390 1391 trans_ids are only valid for the TreeTransform that generated them. 1392 1393 Limbo 1394 ----- 1395 Limbo is a temporary directory use to hold new versions of files. 1396 Files are added to limbo by create_file, create_directory, create_symlink, 1397 and their convenience variants (new_*). Files may be removed from limbo 1398 using cancel_creation. Files are renamed from limbo into their final 1399 location as part of TreeTransform.apply 1400 1401 Limbo must be cleaned up, by either calling TreeTransform.apply or 1402 calling TreeTransform.finalize. 1403 1404 Files are placed into limbo inside their parent directories, where 1405 possible. This reduces subsequent renames, and makes operations involving 1406 lots of files faster. This optimization is only possible if the parent 1407 directory is created *before* creating any of its children, so avoid 1408 creating children before parents, where possible. 1409 1410 Pending-deletion 1411 ---------------- 1412 This temporary directory is used by _FileMover for storing files that are 1413 about to be deleted. In case of rollback, the files will be restored. 1414 FileMover does not delete files until it is sure that a rollback will not 1415 happen. 1416 """ 1417 1418 def __init__(self, tree, pb=None): 1419 """Note: a tree_write lock is taken on the tree. 1420 1421 Use TreeTransform.finalize() to release the lock (can be omitted if 1422 TreeTransform.apply() called). 1423 """ 1424 tree.lock_tree_write() 1425 try: 1426 limbodir = urlutils.local_path_from_url( 1427 tree._transport.abspath('limbo')) 1428 osutils.ensure_empty_directory_exists( 1429 limbodir, 1430 errors.ExistingLimbo) 1431 deletiondir = urlutils.local_path_from_url( 1432 tree._transport.abspath('pending-deletion')) 1433 osutils.ensure_empty_directory_exists( 1434 deletiondir, 1435 errors.ExistingPendingDeletion) 1436 except BaseException: 1437 tree.unlock() 1438 raise 1439 1440 # Cache of realpath results, to speed up canonical_path 1441 self._realpaths = {} 1442 # Cache of relpath results, to speed up canonical_path 1443 self._relpaths = {} 1444 DiskTreeTransform.__init__(self, tree, limbodir, pb, 1445 tree.case_sensitive) 1446 self._deletiondir = deletiondir 1447 1448 def canonical_path(self, path): 1449 """Get the canonical tree-relative path""" 1450 # don't follow final symlinks 1451 abs = self._tree.abspath(path) 1452 if abs in self._relpaths: 1453 return self._relpaths[abs] 1454 dirname, basename = os.path.split(abs) 1455 if dirname not in self._realpaths: 1456 self._realpaths[dirname] = os.path.realpath(dirname) 1457 dirname = self._realpaths[dirname] 1458 abs = osutils.pathjoin(dirname, basename) 1459 if dirname in self._relpaths: 1460 relpath = osutils.pathjoin(self._relpaths[dirname], basename) 1461 relpath = relpath.rstrip('/\\') 1462 else: 1463 relpath = self._tree.relpath(abs) 1464 self._relpaths[abs] = relpath 1465 return relpath 1466 1467 def tree_kind(self, trans_id): 1468 """Determine the file kind in the working tree. 1469 1470 :returns: The file kind or None if the file does not exist 1471 """ 1472 path = self._tree_id_paths.get(trans_id) 1473 if path is None: 1474 return None 1475 try: 1476 return osutils.file_kind(self._tree.abspath(path)) 1477 except errors.NoSuchFile: 1478 return None 1479 1480 def _set_mode(self, trans_id, mode_id, typefunc): 1481 """Set the mode of new file contents. 1482 The mode_id is the existing file to get the mode from (often the same 1483 as trans_id). The operation is only performed if there's a mode match 1484 according to typefunc. 1485 """ 1486 if mode_id is None: 1487 mode_id = trans_id 1488 try: 1489 old_path = self._tree_id_paths[mode_id] 1490 except KeyError: 1491 return 1492 try: 1493 mode = os.stat(self._tree.abspath(old_path)).st_mode 1494 except OSError as e: 1495 if e.errno in (errno.ENOENT, errno.ENOTDIR): 1496 # Either old_path doesn't exist, or the parent of the 1497 # target is not a directory (but will be one eventually) 1498 # Either way, we know it doesn't exist *right now* 1499 # See also bug #248448 1500 return 1501 else: 1502 raise 1503 if typefunc(mode): 1504 osutils.chmod_if_possible(self._limbo_name(trans_id), mode) 1505 1506 def iter_tree_children(self, parent_id): 1507 """Iterate through the entry's tree children, if any""" 1508 try: 1509 path = self._tree_id_paths[parent_id] 1510 except KeyError: 1511 return 1512 try: 1513 children = os.listdir(self._tree.abspath(path)) 1514 except OSError as e: 1515 if not (osutils._is_error_enotdir(e) or 1516 e.errno in (errno.ENOENT, errno.ESRCH)): 1517 raise 1518 return 1519 1520 for child in children: 1521 childpath = joinpath(path, child) 1522 if self._tree.is_control_filename(childpath): 1523 continue 1524 yield self.trans_id_tree_path(childpath) 1525 1526 def _generate_limbo_path(self, trans_id): 1527 """Generate a limbo path using the final path if possible. 1528 1529 This optimizes the performance of applying the tree transform by 1530 avoiding renames. These renames can be avoided only when the parent 1531 directory is already scheduled for creation. 1532 1533 If the final path cannot be used, falls back to using the trans_id as 1534 the relpath. 1535 """ 1536 parent = self._new_parent.get(trans_id) 1537 # if the parent directory is already in limbo (e.g. when building a 1538 # tree), choose a limbo name inside the parent, to reduce further 1539 # renames. 1540 use_direct_path = False 1541 if self._new_contents.get(parent) == 'directory': 1542 filename = self._new_name.get(trans_id) 1543 if filename is not None: 1544 if parent not in self._limbo_children: 1545 self._limbo_children[parent] = set() 1546 self._limbo_children_names[parent] = {} 1547 use_direct_path = True 1548 # the direct path can only be used if no other file has 1549 # already taken this pathname, i.e. if the name is unused, or 1550 # if it is already associated with this trans_id. 1551 elif self._case_sensitive_target: 1552 if (self._limbo_children_names[parent].get(filename) 1553 in (trans_id, None)): 1554 use_direct_path = True 1555 else: 1556 for l_filename, l_trans_id in ( 1557 self._limbo_children_names[parent].items()): 1558 if l_trans_id == trans_id: 1559 continue 1560 if l_filename.lower() == filename.lower(): 1561 break 1562 else: 1563 use_direct_path = True 1564 1565 if not use_direct_path: 1566 return DiskTreeTransform._generate_limbo_path(self, trans_id) 1567 1568 limbo_name = osutils.pathjoin(self._limbo_files[parent], filename) 1569 self._limbo_children[parent].add(trans_id) 1570 self._limbo_children_names[parent][filename] = trans_id 1571 return limbo_name 1572 1573 def version_file(self, trans_id, file_id=None): 1574 """Schedule a file to become versioned.""" 1575 if file_id is None: 1576 raise ValueError() 1577 unique_add(self._new_id, trans_id, file_id) 1578 unique_add(self._r_new_id, file_id, trans_id) 1579 1580 def cancel_versioning(self, trans_id): 1581 """Undo a previous versioning of a file""" 1582 file_id = self._new_id[trans_id] 1583 del self._new_id[trans_id] 1584 del self._r_new_id[file_id] 1585 1586 def _duplicate_ids(self): 1587 """Each inventory id may only be used once""" 1588 conflicts = [] 1589 try: 1590 all_ids = self._tree.all_file_ids() 1591 except errors.UnsupportedOperation: 1592 # it's okay for non-file-id trees to raise UnsupportedOperation. 1593 return [] 1594 removed_tree_ids = set((self.tree_file_id(trans_id) for trans_id in 1595 self._removed_id)) 1596 active_tree_ids = all_ids.difference(removed_tree_ids) 1597 for trans_id, file_id in self._new_id.items(): 1598 if file_id in active_tree_ids: 1599 path = self._tree.id2path(file_id) 1600 old_trans_id = self.trans_id_tree_path(path) 1601 conflicts.append(('duplicate id', old_trans_id, trans_id)) 1602 return conflicts 1603 1604 def find_raw_conflicts(self): 1605 conflicts = super(InventoryTreeTransform, self).find_raw_conflicts() 1606 conflicts.extend(self._duplicate_ids()) 1607 return conflicts 1608 1609 def apply(self, no_conflicts=False, precomputed_delta=None, _mover=None): 1610 """Apply all changes to the inventory and filesystem. 1611 1612 If filesystem or inventory conflicts are present, MalformedTransform 1613 will be thrown. 1614 1615 If apply succeeds, finalize is not necessary. 1616 1617 :param no_conflicts: if True, the caller guarantees there are no 1618 conflicts, so no check is made. 1619 :param precomputed_delta: An inventory delta to use instead of 1620 calculating one. 1621 :param _mover: Supply an alternate FileMover, for testing 1622 """ 1623 for hook in MutableTree.hooks['pre_transform']: 1624 hook(self._tree, self) 1625 if not no_conflicts: 1626 self._check_malformed() 1627 self.rename_count = 0 1628 with ui.ui_factory.nested_progress_bar() as child_pb: 1629 if precomputed_delta is None: 1630 child_pb.update(gettext('Apply phase'), 0, 2) 1631 inventory_delta = self._generate_inventory_delta() 1632 offset = 1 1633 else: 1634 inventory_delta = precomputed_delta 1635 offset = 0 1636 if _mover is None: 1637 mover = _FileMover() 1638 else: 1639 mover = _mover 1640 try: 1641 child_pb.update(gettext('Apply phase'), 0 + offset, 2 + offset) 1642 self._apply_removals(mover) 1643 child_pb.update(gettext('Apply phase'), 1 + offset, 2 + offset) 1644 modified_paths = self._apply_insertions(mover) 1645 except BaseException: 1646 mover.rollback() 1647 raise 1648 else: 1649 mover.apply_deletions() 1650 if self.final_file_id(self.root) is None: 1651 inventory_delta = [e for e in inventory_delta if e[0] != ''] 1652 self._tree.apply_inventory_delta(inventory_delta) 1653 self._apply_observed_sha1s() 1654 self._done = True 1655 self.finalize() 1656 return _TransformResults(modified_paths, self.rename_count) 1657 1658 def _apply_removals(self, mover): 1659 """Perform tree operations that remove directory/inventory names. 1660 1661 That is, delete files that are to be deleted, and put any files that 1662 need renaming into limbo. This must be done in strict child-to-parent 1663 order. 1664 1665 If inventory_delta is None, no inventory delta generation is performed. 1666 """ 1667 tree_paths = sorted(self._tree_path_ids.items(), reverse=True) 1668 with ui.ui_factory.nested_progress_bar() as child_pb: 1669 for num, (path, trans_id) in enumerate(tree_paths): 1670 # do not attempt to move root into a subdirectory of itself. 1671 if path == '': 1672 continue 1673 child_pb.update(gettext('removing file'), num, len(tree_paths)) 1674 full_path = self._tree.abspath(path) 1675 if trans_id in self._removed_contents: 1676 delete_path = os.path.join(self._deletiondir, trans_id) 1677 mover.pre_delete(full_path, delete_path) 1678 elif (trans_id in self._new_name or 1679 trans_id in self._new_parent): 1680 try: 1681 mover.rename(full_path, self._limbo_name(trans_id)) 1682 except TransformRenameFailed as e: 1683 if e.errno != errno.ENOENT: 1684 raise 1685 else: 1686 self.rename_count += 1 1687 1688 def _apply_insertions(self, mover): 1689 """Perform tree operations that insert directory/inventory names. 1690 1691 That is, create any files that need to be created, and restore from 1692 limbo any files that needed renaming. This must be done in strict 1693 parent-to-child order. 1694 1695 If inventory_delta is None, no inventory delta is calculated, and 1696 no list of modified paths is returned. 1697 """ 1698 new_paths = self.new_paths(filesystem_only=True) 1699 modified_paths = [] 1700 with ui.ui_factory.nested_progress_bar() as child_pb: 1701 for num, (path, trans_id) in enumerate(new_paths): 1702 if (num % 10) == 0: 1703 child_pb.update(gettext('adding file'), 1704 num, len(new_paths)) 1705 full_path = self._tree.abspath(path) 1706 if trans_id in self._needs_rename: 1707 try: 1708 mover.rename(self._limbo_name(trans_id), full_path) 1709 except TransformRenameFailed as e: 1710 # We may be renaming a dangling inventory id 1711 if e.errno != errno.ENOENT: 1712 raise 1713 else: 1714 self.rename_count += 1 1715 # TODO: if trans_id in self._observed_sha1s, we should 1716 # re-stat the final target, since ctime will be 1717 # updated by the change. 1718 if (trans_id in self._new_contents 1719 or self.path_changed(trans_id)): 1720 if trans_id in self._new_contents: 1721 modified_paths.append(full_path) 1722 if trans_id in self._new_executability: 1723 self._set_executability(path, trans_id) 1724 if trans_id in self._observed_sha1s: 1725 o_sha1, o_st_val = self._observed_sha1s[trans_id] 1726 st = osutils.lstat(full_path) 1727 self._observed_sha1s[trans_id] = (o_sha1, st) 1728 for path, trans_id in new_paths: 1729 # new_paths includes stuff like workingtree conflicts. Only the 1730 # stuff in new_contents actually comes from limbo. 1731 if trans_id in self._limbo_files: 1732 del self._limbo_files[trans_id] 1733 self._new_contents.clear() 1734 return modified_paths 1735 1736 def _apply_observed_sha1s(self): 1737 """After we have finished renaming everything, update observed sha1s 1738 1739 This has to be done after self._tree.apply_inventory_delta, otherwise 1740 it doesn't know anything about the files we are updating. Also, we want 1741 to do this as late as possible, so that most entries end up cached. 1742 """ 1743 # TODO: this doesn't update the stat information for directories. So 1744 # the first 'bzr status' will still need to rewrite 1745 # .bzr/checkout/dirstate. However, we at least don't need to 1746 # re-read all of the files. 1747 # TODO: If the operation took a while, we could do a time.sleep(3) here 1748 # to allow the clock to tick over and ensure we won't have any 1749 # problems. (we could observe start time, and finish time, and if 1750 # it is less than eg 10% overhead, add a sleep call.) 1751 paths = FinalPaths(self) 1752 for trans_id, observed in self._observed_sha1s.items(): 1753 path = paths.get_path(trans_id) 1754 self._tree._observed_sha1(path, observed) 1755 1756 def get_preview_tree(self): 1757 """Return a tree representing the result of the transform. 1758 1759 The tree is a snapshot, and altering the TreeTransform will invalidate 1760 it. 1761 """ 1762 return InventoryPreviewTree(self) 1763 1764 def _inventory_altered(self): 1765 """Determine which trans_ids need new Inventory entries. 1766 1767 An new entry is needed when anything that would be reflected by an 1768 inventory entry changes, including file name, file_id, parent file_id, 1769 file kind, and the execute bit. 1770 1771 Some care is taken to return entries with real changes, not cases 1772 where the value is deleted and then restored to its original value, 1773 but some actually unchanged values may be returned. 1774 1775 :returns: A list of (path, trans_id) for all items requiring an 1776 inventory change. Ordered by path. 1777 """ 1778 changed_ids = set() 1779 # Find entries whose file_ids are new (or changed). 1780 new_file_id = set(t for t in self._new_id 1781 if self._new_id[t] != self.tree_file_id(t)) 1782 for id_set in [self._new_name, self._new_parent, new_file_id, 1783 self._new_executability]: 1784 changed_ids.update(id_set) 1785 # removing implies a kind change 1786 changed_kind = set(self._removed_contents) 1787 # so does adding 1788 changed_kind.intersection_update(self._new_contents) 1789 # Ignore entries that are already known to have changed. 1790 changed_kind.difference_update(changed_ids) 1791 # to keep only the truly changed ones 1792 changed_kind = (t for t in changed_kind 1793 if self.tree_kind(t) != self.final_kind(t)) 1794 # all kind changes will alter the inventory 1795 changed_ids.update(changed_kind) 1796 # To find entries with changed parent_ids, find parents which existed, 1797 # but changed file_id. 1798 # Now add all their children to the set. 1799 for parent_trans_id in new_file_id: 1800 changed_ids.update(self.iter_tree_children(parent_trans_id)) 1801 return sorted(FinalPaths(self).get_paths(changed_ids)) 1802 1803 def _generate_inventory_delta(self): 1804 """Generate an inventory delta for the current transform.""" 1805 inventory_delta = [] 1806 new_paths = self._inventory_altered() 1807 total_entries = len(new_paths) + len(self._removed_id) 1808 with ui.ui_factory.nested_progress_bar() as child_pb: 1809 for num, trans_id in enumerate(self._removed_id): 1810 if (num % 10) == 0: 1811 child_pb.update(gettext('removing file'), 1812 num, total_entries) 1813 if trans_id == self._new_root: 1814 file_id = self._tree.path2id('') 1815 else: 1816 file_id = self.tree_file_id(trans_id) 1817 # File-id isn't really being deleted, just moved 1818 if file_id in self._r_new_id: 1819 continue 1820 path = self._tree_id_paths[trans_id] 1821 inventory_delta.append((path, None, file_id, None)) 1822 new_path_file_ids = dict((t, self.final_file_id(t)) for p, t in 1823 new_paths) 1824 for num, (path, trans_id) in enumerate(new_paths): 1825 if (num % 10) == 0: 1826 child_pb.update(gettext('adding file'), 1827 num + len(self._removed_id), total_entries) 1828 file_id = new_path_file_ids[trans_id] 1829 if file_id is None: 1830 continue 1831 kind = self.final_kind(trans_id) 1832 if kind is None: 1833 kind = self._tree.stored_kind(self._tree.id2path(file_id)) 1834 parent_trans_id = self.final_parent(trans_id) 1835 parent_file_id = new_path_file_ids.get(parent_trans_id) 1836 if parent_file_id is None: 1837 parent_file_id = self.final_file_id(parent_trans_id) 1838 if trans_id in self._new_reference_revision: 1839 new_entry = inventory.TreeReference( 1840 file_id, 1841 self._new_name[trans_id], 1842 self.final_file_id(self._new_parent[trans_id]), 1843 None, self._new_reference_revision[trans_id]) 1844 else: 1845 new_entry = inventory.make_entry(kind, 1846 self.final_name(trans_id), 1847 parent_file_id, file_id) 1848 try: 1849 old_path = self._tree.id2path(new_entry.file_id) 1850 except errors.NoSuchId: 1851 old_path = None 1852 new_executability = self._new_executability.get(trans_id) 1853 if new_executability is not None: 1854 new_entry.executable = new_executability 1855 inventory_delta.append( 1856 (old_path, path, new_entry.file_id, new_entry)) 1857 return inventory_delta 1858 1859 1860class TransformPreview(InventoryTreeTransform): 1861 """A TreeTransform for generating preview trees. 1862 1863 Unlike TreeTransform, this version works when the input tree is a 1864 RevisionTree, rather than a WorkingTree. As a result, it tends to ignore 1865 unversioned files in the input tree. 1866 """ 1867 1868 def __init__(self, tree, pb=None, case_sensitive=True): 1869 tree.lock_read() 1870 limbodir = osutils.mkdtemp(prefix='bzr-limbo-') 1871 DiskTreeTransform.__init__(self, tree, limbodir, pb, case_sensitive) 1872 1873 def canonical_path(self, path): 1874 return path 1875 1876 def tree_kind(self, trans_id): 1877 path = self._tree_id_paths.get(trans_id) 1878 if path is None: 1879 return None 1880 kind = self._tree.path_content_summary(path)[0] 1881 if kind == 'missing': 1882 kind = None 1883 return kind 1884 1885 def _set_mode(self, trans_id, mode_id, typefunc): 1886 """Set the mode of new file contents. 1887 The mode_id is the existing file to get the mode from (often the same 1888 as trans_id). The operation is only performed if there's a mode match 1889 according to typefunc. 1890 """ 1891 # is it ok to ignore this? probably 1892 pass 1893 1894 def iter_tree_children(self, parent_id): 1895 """Iterate through the entry's tree children, if any""" 1896 try: 1897 path = self._tree_id_paths[parent_id] 1898 except KeyError: 1899 return 1900 try: 1901 entry = next(self._tree.iter_entries_by_dir( 1902 specific_files=[path]))[1] 1903 except StopIteration: 1904 return 1905 children = getattr(entry, 'children', {}) 1906 for child in children: 1907 childpath = joinpath(path, child) 1908 yield self.trans_id_tree_path(childpath) 1909 1910 def new_orphan(self, trans_id, parent_id): 1911 raise NotImplementedError(self.new_orphan) 1912 1913 1914class InventoryPreviewTree(PreviewTree, inventorytree.InventoryTree): 1915 """Partial implementation of Tree to support show_diff_trees""" 1916 1917 def __init__(self, transform): 1918 PreviewTree.__init__(self, transform) 1919 self._final_paths = FinalPaths(transform) 1920 self._iter_changes_cache = { 1921 c.file_id: c for c in self._transform.iter_changes()} 1922 1923 def supports_setting_file_ids(self): 1924 return True 1925 1926 def supports_tree_reference(self): 1927 # TODO(jelmer): Support tree references in PreviewTree. 1928 # return self._transform._tree.supports_tree_reference() 1929 return False 1930 1931 def _content_change(self, file_id): 1932 """Return True if the content of this file changed""" 1933 changes = self._iter_changes_cache.get(file_id) 1934 return (changes is not None and changes.changed_content) 1935 1936 def _get_file_revision(self, path, file_id, vf, tree_revision): 1937 parent_keys = [ 1938 (file_id, t.get_file_revision(t.id2path(file_id))) 1939 for t in self._iter_parent_trees()] 1940 vf.add_lines((file_id, tree_revision), parent_keys, 1941 self.get_file_lines(path)) 1942 repo = self._get_repository() 1943 base_vf = repo.texts 1944 if base_vf not in vf.fallback_versionedfiles: 1945 vf.fallback_versionedfiles.append(base_vf) 1946 return tree_revision 1947 1948 def _stat_limbo_file(self, trans_id): 1949 name = self._transform._limbo_name(trans_id) 1950 return os.lstat(name) 1951 1952 def _comparison_data(self, entry, path): 1953 kind, size, executable, link_or_sha1 = self.path_content_summary(path) 1954 if kind == 'missing': 1955 kind = None 1956 executable = False 1957 else: 1958 file_id = self._transform.final_file_id(self._path2trans_id(path)) 1959 executable = self.is_executable(path) 1960 return kind, executable, None 1961 1962 @property 1963 def root_inventory(self): 1964 """This Tree does not use inventory as its backing data.""" 1965 raise NotImplementedError(PreviewTree.root_inventory) 1966 1967 def all_file_ids(self): 1968 tree_ids = set(self._transform._tree.all_file_ids()) 1969 tree_ids.difference_update(self._transform.tree_file_id(t) 1970 for t in self._transform._removed_id) 1971 tree_ids.update(self._transform._new_id.values()) 1972 return tree_ids 1973 1974 def all_versioned_paths(self): 1975 tree_paths = set(self._transform._tree.all_versioned_paths()) 1976 1977 tree_paths.difference_update( 1978 self._transform.trans_id_tree_path(t) 1979 for t in self._transform._removed_id) 1980 1981 tree_paths.update( 1982 self._final_paths._determine_path(t) 1983 for t in self._transform._new_id) 1984 1985 return tree_paths 1986 1987 def path2id(self, path): 1988 if isinstance(path, list): 1989 if path == []: 1990 path = [""] 1991 path = osutils.pathjoin(*path) 1992 return self._transform.final_file_id(self._path2trans_id(path)) 1993 1994 def id2path(self, file_id, recurse='down'): 1995 trans_id = self._transform.trans_id_file_id(file_id) 1996 try: 1997 return self._final_paths._determine_path(trans_id) 1998 except NoFinalPath: 1999 raise errors.NoSuchId(self, file_id) 2000 2001 def extras(self): 2002 possible_extras = set(self._transform.trans_id_tree_path(p) for p 2003 in self._transform._tree.extras()) 2004 possible_extras.update(self._transform._new_contents) 2005 possible_extras.update(self._transform._removed_id) 2006 for trans_id in possible_extras: 2007 if self._transform.final_file_id(trans_id) is None: 2008 yield self._final_paths._determine_path(trans_id) 2009 2010 def _make_inv_entries(self, ordered_entries, specific_files=None): 2011 for trans_id, parent_file_id in ordered_entries: 2012 file_id = self._transform.final_file_id(trans_id) 2013 if file_id is None: 2014 continue 2015 if (specific_files is not None 2016 and self._final_paths.get_path(trans_id) not in specific_files): 2017 continue 2018 kind = self._transform.final_kind(trans_id) 2019 if kind is None: 2020 kind = self._transform._tree.stored_kind( 2021 self._transform._tree.id2path(file_id)) 2022 new_entry = inventory.make_entry( 2023 kind, 2024 self._transform.final_name(trans_id), 2025 parent_file_id, file_id) 2026 yield new_entry, trans_id 2027 2028 def _list_files_by_dir(self): 2029 todo = [ROOT_PARENT] 2030 ordered_ids = [] 2031 while len(todo) > 0: 2032 parent = todo.pop() 2033 parent_file_id = self._transform.final_file_id(parent) 2034 children = list(self._all_children(parent)) 2035 paths = dict(zip(children, self._final_paths.get_paths(children))) 2036 children.sort(key=paths.get) 2037 todo.extend(reversed(children)) 2038 for trans_id in children: 2039 ordered_ids.append((trans_id, parent_file_id)) 2040 return ordered_ids 2041 2042 def iter_child_entries(self, path): 2043 trans_id = self._path2trans_id(path) 2044 if trans_id is None: 2045 raise errors.NoSuchFile(path) 2046 todo = [(child_trans_id, trans_id) for child_trans_id in 2047 self._all_children(trans_id)] 2048 for entry, trans_id in self._make_inv_entries(todo): 2049 yield entry 2050 2051 def iter_entries_by_dir(self, specific_files=None, recurse_nested=False): 2052 if recurse_nested: 2053 raise NotImplementedError( 2054 'follow tree references not yet supported') 2055 2056 # This may not be a maximally efficient implementation, but it is 2057 # reasonably straightforward. An implementation that grafts the 2058 # TreeTransform changes onto the tree's iter_entries_by_dir results 2059 # might be more efficient, but requires tricky inferences about stack 2060 # position. 2061 ordered_ids = self._list_files_by_dir() 2062 for entry, trans_id in self._make_inv_entries(ordered_ids, 2063 specific_files): 2064 yield self._final_paths.get_path(trans_id), entry 2065 2066 def _iter_entries_for_dir(self, dir_path): 2067 """Return path, entry for items in a directory without recursing down.""" 2068 ordered_ids = [] 2069 dir_trans_id = self._path2trans_id(dir_path) 2070 dir_id = self._transform.final_file_id(dir_trans_id) 2071 for child_trans_id in self._all_children(dir_trans_id): 2072 ordered_ids.append((child_trans_id, dir_id)) 2073 path_entries = [] 2074 for entry, trans_id in self._make_inv_entries(ordered_ids): 2075 path_entries.append((self._final_paths.get_path(trans_id), entry)) 2076 path_entries.sort() 2077 return path_entries 2078 2079 def list_files(self, include_root=False, from_dir=None, recursive=True, 2080 recurse_nested=False): 2081 """See WorkingTree.list_files.""" 2082 if recurse_nested: 2083 raise NotImplementedError( 2084 'follow tree references not yet supported') 2085 2086 # XXX This should behave like WorkingTree.list_files, but is really 2087 # more like RevisionTree.list_files. 2088 if from_dir == '.': 2089 from_dir = None 2090 if recursive: 2091 prefix = None 2092 if from_dir: 2093 prefix = from_dir + '/' 2094 entries = self.iter_entries_by_dir() 2095 for path, entry in entries: 2096 if entry.name == '' and not include_root: 2097 continue 2098 if prefix: 2099 if not path.startswith(prefix): 2100 continue 2101 path = path[len(prefix):] 2102 yield path, 'V', entry.kind, entry 2103 else: 2104 if from_dir is None and include_root is True: 2105 root_entry = inventory.make_entry( 2106 'directory', '', ROOT_PARENT, self.path2id('')) 2107 yield '', 'V', 'directory', root_entry 2108 entries = self._iter_entries_for_dir(from_dir or '') 2109 for path, entry in entries: 2110 yield path, 'V', entry.kind, entry 2111 2112 def get_file_mtime(self, path): 2113 """See Tree.get_file_mtime""" 2114 file_id = self.path2id(path) 2115 if file_id is None: 2116 raise errors.NoSuchFile(path) 2117 if not self._content_change(file_id): 2118 return self._transform._tree.get_file_mtime( 2119 self._transform._tree.id2path(file_id)) 2120 trans_id = self._path2trans_id(path) 2121 return self._stat_limbo_file(trans_id).st_mtime 2122 2123 def path_content_summary(self, path): 2124 trans_id = self._path2trans_id(path) 2125 tt = self._transform 2126 tree_path = tt._tree_id_paths.get(trans_id) 2127 kind = tt._new_contents.get(trans_id) 2128 if kind is None: 2129 if tree_path is None or trans_id in tt._removed_contents: 2130 return 'missing', None, None, None 2131 summary = tt._tree.path_content_summary(tree_path) 2132 kind, size, executable, link_or_sha1 = summary 2133 else: 2134 link_or_sha1 = None 2135 limbo_name = tt._limbo_name(trans_id) 2136 if trans_id in tt._new_reference_revision: 2137 kind = 'tree-reference' 2138 link_or_sha1 = tt._new_reference_revision 2139 if kind == 'file': 2140 statval = os.lstat(limbo_name) 2141 size = statval.st_size 2142 if not tt._limbo_supports_executable(): 2143 executable = False 2144 else: 2145 executable = statval.st_mode & S_IEXEC 2146 else: 2147 size = None 2148 executable = None 2149 if kind == 'symlink': 2150 link_or_sha1 = os.readlink(limbo_name) 2151 if not isinstance(link_or_sha1, str): 2152 link_or_sha1 = link_or_sha1.decode(osutils._fs_enc) 2153 executable = tt._new_executability.get(trans_id, executable) 2154 return kind, size, executable, link_or_sha1 2155 2156 def iter_changes(self, from_tree, include_unchanged=False, 2157 specific_files=None, pb=None, extra_trees=None, 2158 require_versioned=True, want_unversioned=False): 2159 """See InterTree.iter_changes. 2160 2161 This has a fast path that is only used when the from_tree matches 2162 the transform tree, and no fancy options are supplied. 2163 """ 2164 if (from_tree is not self._transform._tree or include_unchanged 2165 or specific_files or want_unversioned): 2166 return tree.InterTree.get(from_tree, self).iter_changes( 2167 include_unchanged=include_unchanged, 2168 specific_files=specific_files, 2169 pb=pb, 2170 extra_trees=extra_trees, 2171 require_versioned=require_versioned, 2172 want_unversioned=want_unversioned) 2173 if want_unversioned: 2174 raise ValueError('want_unversioned is not supported') 2175 return self._transform.iter_changes() 2176 2177 def annotate_iter(self, path, 2178 default_revision=_mod_revision.CURRENT_REVISION): 2179 file_id = self.path2id(path) 2180 changes = self._iter_changes_cache.get(file_id) 2181 if changes is None: 2182 if file_id is None: 2183 old_path = None 2184 else: 2185 old_path = self._transform._tree.id2path(file_id) 2186 else: 2187 if changes.kind[1] is None: 2188 return None 2189 if changes.kind[0] == 'file' and changes.versioned[0]: 2190 old_path = changes.path[0] 2191 else: 2192 old_path = None 2193 if old_path is not None: 2194 old_annotation = self._transform._tree.annotate_iter( 2195 old_path, default_revision=default_revision) 2196 else: 2197 old_annotation = [] 2198 if changes is None: 2199 if old_path is None: 2200 return None 2201 else: 2202 return old_annotation 2203 if not changes.changed_content: 2204 return old_annotation 2205 # TODO: This is doing something similar to what WT.annotate_iter is 2206 # doing, however it fails slightly because it doesn't know what 2207 # the *other* revision_id is, so it doesn't know how to give the 2208 # other as the origin for some lines, they all get 2209 # 'default_revision' 2210 # It would be nice to be able to use the new Annotator based 2211 # approach, as well. 2212 return annotate.reannotate([old_annotation], 2213 self.get_file_lines(path), 2214 default_revision) 2215 2216 def walkdirs(self, prefix=''): 2217 pending = [self._transform.root] 2218 while len(pending) > 0: 2219 parent_id = pending.pop() 2220 children = [] 2221 subdirs = [] 2222 prefix = prefix.rstrip('/') 2223 parent_path = self._final_paths.get_path(parent_id) 2224 parent_file_id = self._transform.final_file_id(parent_id) 2225 for child_id in self._all_children(parent_id): 2226 path_from_root = self._final_paths.get_path(child_id) 2227 basename = self._transform.final_name(child_id) 2228 file_id = self._transform.final_file_id(child_id) 2229 kind = self._transform.final_kind(child_id) 2230 if kind is not None: 2231 versioned_kind = kind 2232 else: 2233 kind = 'unknown' 2234 versioned_kind = self._transform._tree.stored_kind( 2235 path_from_root) 2236 if versioned_kind == 'directory': 2237 subdirs.append(child_id) 2238 children.append((path_from_root, basename, kind, None, 2239 versioned_kind)) 2240 children.sort() 2241 if parent_path.startswith(prefix): 2242 yield parent_path, children 2243 pending.extend(sorted(subdirs, key=self._final_paths.get_path, 2244 reverse=True)) 2245 2246 def get_symlink_target(self, path): 2247 """See Tree.get_symlink_target""" 2248 file_id = self.path2id(path) 2249 if not self._content_change(file_id): 2250 return self._transform._tree.get_symlink_target(path) 2251 trans_id = self._path2trans_id(path) 2252 name = self._transform._limbo_name(trans_id) 2253 return osutils.readlink(name) 2254 2255 def get_file(self, path): 2256 """See Tree.get_file""" 2257 file_id = self.path2id(path) 2258 if not self._content_change(file_id): 2259 return self._transform._tree.get_file(path) 2260 trans_id = self._path2trans_id(path) 2261 name = self._transform._limbo_name(trans_id) 2262 return open(name, 'rb') 2263 2264 2265def build_tree(tree, wt, accelerator_tree=None, hardlink=False, 2266 delta_from_tree=False): 2267 """Create working tree for a branch, using a TreeTransform. 2268 2269 This function should be used on empty trees, having a tree root at most. 2270 (see merge and revert functionality for working with existing trees) 2271 2272 Existing files are handled like so: 2273 2274 - Existing bzrdirs take precedence over creating new items. They are 2275 created as '%s.diverted' % name. 2276 - Otherwise, if the content on disk matches the content we are building, 2277 it is silently replaced. 2278 - Otherwise, conflict resolution will move the old file to 'oldname.moved'. 2279 2280 :param tree: The tree to convert wt into a copy of 2281 :param wt: The working tree that files will be placed into 2282 :param accelerator_tree: A tree which can be used for retrieving file 2283 contents more quickly than tree itself, i.e. a workingtree. tree 2284 will be used for cases where accelerator_tree's content is different. 2285 :param hardlink: If true, hard-link files to accelerator_tree, where 2286 possible. accelerator_tree must implement abspath, i.e. be a 2287 working tree. 2288 :param delta_from_tree: If true, build_tree may use the input Tree to 2289 generate the inventory delta. 2290 """ 2291 with contextlib.ExitStack() as exit_stack: 2292 exit_stack.enter_context(wt.lock_tree_write()) 2293 exit_stack.enter_context(tree.lock_read()) 2294 if accelerator_tree is not None: 2295 exit_stack.enter_context(accelerator_tree.lock_read()) 2296 return _build_tree(tree, wt, accelerator_tree, hardlink, 2297 delta_from_tree) 2298 2299 2300def resolve_checkout(tt, conflicts, divert): 2301 new_conflicts = set() 2302 for c_type, conflict in ((c[0], c) for c in conflicts): 2303 # Anything but a 'duplicate' would indicate programmer error 2304 if c_type != 'duplicate': 2305 raise AssertionError(c_type) 2306 # Now figure out which is new and which is old 2307 if tt.new_contents(conflict[1]): 2308 new_file = conflict[1] 2309 old_file = conflict[2] 2310 else: 2311 new_file = conflict[2] 2312 old_file = conflict[1] 2313 2314 # We should only get here if the conflict wasn't completely 2315 # resolved 2316 final_parent = tt.final_parent(old_file) 2317 if new_file in divert: 2318 new_name = tt.final_name(old_file) + '.diverted' 2319 tt.adjust_path(new_name, final_parent, new_file) 2320 new_conflicts.add((c_type, 'Diverted to', 2321 new_file, old_file)) 2322 else: 2323 new_name = tt.final_name(old_file) + '.moved' 2324 tt.adjust_path(new_name, final_parent, old_file) 2325 new_conflicts.add((c_type, 'Moved existing file to', 2326 old_file, new_file)) 2327 return new_conflicts 2328 2329 2330def _build_tree(tree, wt, accelerator_tree, hardlink, delta_from_tree): 2331 """See build_tree.""" 2332 for num, _unused in enumerate(wt.all_versioned_paths()): 2333 if num > 0: # more than just a root 2334 raise errors.WorkingTreeAlreadyPopulated(base=wt.basedir) 2335 file_trans_id = {} 2336 top_pb = ui.ui_factory.nested_progress_bar() 2337 pp = ProgressPhase("Build phase", 2, top_pb) 2338 if tree.path2id('') is not None: 2339 # This is kind of a hack: we should be altering the root 2340 # as part of the regular tree shape diff logic. 2341 # The conditional test here is to avoid doing an 2342 # expensive operation (flush) every time the root id 2343 # is set within the tree, nor setting the root and thus 2344 # marking the tree as dirty, because we use two different 2345 # idioms here: tree interfaces and inventory interfaces. 2346 if wt.path2id('') != tree.path2id(''): 2347 wt.set_root_id(tree.path2id('')) 2348 wt.flush() 2349 tt = wt.transform() 2350 divert = set() 2351 try: 2352 pp.next_phase() 2353 file_trans_id[find_previous_path(wt, tree, '')] = tt.trans_id_tree_path('') 2354 with ui.ui_factory.nested_progress_bar() as pb: 2355 deferred_contents = [] 2356 num = 0 2357 total = len(tree.all_versioned_paths()) 2358 if delta_from_tree: 2359 precomputed_delta = [] 2360 else: 2361 precomputed_delta = None 2362 # Check if tree inventory has content. If so, we populate 2363 # existing_files with the directory content. If there are no 2364 # entries we skip populating existing_files as its not used. 2365 # This improves performance and unncessary work on large 2366 # directory trees. (#501307) 2367 if total > 0: 2368 existing_files = set() 2369 for dir, files in wt.walkdirs(): 2370 existing_files.update(f[0] for f in files) 2371 for num, (tree_path, entry) in \ 2372 enumerate(tree.iter_entries_by_dir()): 2373 pb.update(gettext("Building tree"), num 2374 - len(deferred_contents), total) 2375 if entry.parent_id is None: 2376 continue 2377 reparent = False 2378 file_id = entry.file_id 2379 if delta_from_tree: 2380 precomputed_delta.append((None, tree_path, file_id, entry)) 2381 if tree_path in existing_files: 2382 target_path = wt.abspath(tree_path) 2383 kind = osutils.file_kind(target_path) 2384 if kind == "directory": 2385 try: 2386 controldir.ControlDir.open(target_path) 2387 except errors.NotBranchError: 2388 pass 2389 else: 2390 divert.add(tree_path) 2391 if (tree_path not in divert 2392 and _content_match( 2393 tree, entry, tree_path, kind, target_path)): 2394 tt.delete_contents(tt.trans_id_tree_path(tree_path)) 2395 if kind == 'directory': 2396 reparent = True 2397 parent_id = file_trans_id[osutils.dirname(tree_path)] 2398 if entry.kind == 'file': 2399 # We *almost* replicate new_by_entry, so that we can defer 2400 # getting the file text, and get them all at once. 2401 trans_id = tt.create_path(entry.name, parent_id) 2402 file_trans_id[tree_path] = trans_id 2403 tt.version_file(trans_id, file_id=file_id) 2404 executable = tree.is_executable(tree_path) 2405 if executable: 2406 tt.set_executability(executable, trans_id) 2407 trans_data = (trans_id, tree_path, entry.text_sha1) 2408 deferred_contents.append((tree_path, trans_data)) 2409 else: 2410 file_trans_id[tree_path] = new_by_entry( 2411 tree_path, tt, entry, parent_id, tree) 2412 if reparent: 2413 new_trans_id = file_trans_id[tree_path] 2414 old_parent = tt.trans_id_tree_path(tree_path) 2415 _reparent_children(tt, old_parent, new_trans_id) 2416 offset = num + 1 - len(deferred_contents) 2417 _create_files(tt, tree, deferred_contents, pb, offset, 2418 accelerator_tree, hardlink) 2419 pp.next_phase() 2420 divert_trans = set(file_trans_id[f] for f in divert) 2421 2422 def resolver(t, c): 2423 return resolve_checkout(t, c, divert_trans) 2424 raw_conflicts = resolve_conflicts(tt, pass_func=resolver) 2425 if len(raw_conflicts) > 0: 2426 precomputed_delta = None 2427 conflicts = tt.cook_conflicts(raw_conflicts) 2428 for conflict in conflicts: 2429 trace.warning(str(conflict)) 2430 try: 2431 wt.add_conflicts(conflicts) 2432 except errors.UnsupportedOperation: 2433 pass 2434 result = tt.apply(no_conflicts=True, 2435 precomputed_delta=precomputed_delta) 2436 finally: 2437 tt.finalize() 2438 top_pb.finished() 2439 return result 2440 2441 2442def _create_files(tt, tree, desired_files, pb, offset, accelerator_tree, 2443 hardlink): 2444 total = len(desired_files) + offset 2445 wt = tt._tree 2446 if accelerator_tree is None: 2447 new_desired_files = desired_files 2448 else: 2449 iter = accelerator_tree.iter_changes(tree, include_unchanged=True) 2450 unchanged = [ 2451 change.path for change in iter 2452 if not (change.changed_content or change.executable[0] != change.executable[1])] 2453 if accelerator_tree.supports_content_filtering(): 2454 unchanged = [(tp, ap) for (tp, ap) in unchanged 2455 if not next(accelerator_tree.iter_search_rules([ap]))] 2456 unchanged = dict(unchanged) 2457 new_desired_files = [] 2458 count = 0 2459 for unused_tree_path, (trans_id, tree_path, text_sha1) in desired_files: 2460 accelerator_path = unchanged.get(tree_path) 2461 if accelerator_path is None: 2462 new_desired_files.append((tree_path, 2463 (trans_id, tree_path, text_sha1))) 2464 continue 2465 pb.update(gettext('Adding file contents'), count + offset, total) 2466 if hardlink: 2467 tt.create_hardlink(accelerator_tree.abspath(accelerator_path), 2468 trans_id) 2469 else: 2470 with accelerator_tree.get_file(accelerator_path) as f: 2471 chunks = osutils.file_iterator(f) 2472 if wt.supports_content_filtering(): 2473 filters = wt._content_filter_stack(tree_path) 2474 chunks = filtered_output_bytes(chunks, filters, 2475 ContentFilterContext(tree_path, tree)) 2476 tt.create_file(chunks, trans_id, sha1=text_sha1) 2477 count += 1 2478 offset += count 2479 for count, ((trans_id, tree_path, text_sha1), contents) in enumerate( 2480 tree.iter_files_bytes(new_desired_files)): 2481 if wt.supports_content_filtering(): 2482 filters = wt._content_filter_stack(tree_path) 2483 contents = filtered_output_bytes(contents, filters, 2484 ContentFilterContext(tree_path, tree)) 2485 tt.create_file(contents, trans_id, sha1=text_sha1) 2486 pb.update(gettext('Adding file contents'), count + offset, total) 2487 2488 2489 2490