1# Copyright (C) 2009-2018 Jelmer Vernooij <jelmer@jelmer.uk> 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 18"""Git Trees.""" 19 20from collections import deque 21import errno 22from io import BytesIO 23import os 24 25from dulwich.config import ( 26 parse_submodules, 27 ConfigFile as GitConfigFile, 28 ) 29from dulwich.diff_tree import tree_changes, RenameDetector 30from dulwich.errors import NotTreeError 31from dulwich.index import ( 32 blob_from_path_and_stat, 33 cleanup_mode, 34 commit_tree, 35 index_entry_from_stat, 36 Index, 37 ) 38from dulwich.object_store import ( 39 tree_lookup_path, 40 OverlayObjectStore, 41 ) 42from dulwich.objects import ( 43 Blob, 44 Tree, 45 ZERO_SHA, 46 S_IFGITLINK, 47 S_ISGITLINK, 48 ) 49import stat 50import posixpath 51 52from .. import ( 53 controldir as _mod_controldir, 54 delta, 55 errors, 56 mutabletree, 57 osutils, 58 revisiontree, 59 trace, 60 tree as _mod_tree, 61 workingtree, 62 ) 63from ..revision import ( 64 CURRENT_REVISION, 65 NULL_REVISION, 66 ) 67 68from .mapping import ( 69 encode_git_path, 70 decode_git_path, 71 mode_is_executable, 72 mode_kind, 73 default_mapping, 74 ) 75from .transportgit import ( 76 TransportObjectStore, 77 TransportRepo, 78 ) 79from ..bzr.inventorytree import InventoryTreeChange 80 81 82class GitTreeDirectory(_mod_tree.TreeDirectory): 83 84 __slots__ = ['file_id', 'name', 'parent_id'] 85 86 def __init__(self, file_id, name, parent_id): 87 self.file_id = file_id 88 self.name = name 89 self.parent_id = parent_id 90 91 @property 92 def kind(self): 93 return 'directory' 94 95 @property 96 def executable(self): 97 return False 98 99 def copy(self): 100 return self.__class__( 101 self.file_id, self.name, self.parent_id) 102 103 def __repr__(self): 104 return "%s(file_id=%r, name=%r, parent_id=%r)" % ( 105 self.__class__.__name__, self.file_id, self.name, 106 self.parent_id) 107 108 def __eq__(self, other): 109 return (self.kind == other.kind and 110 self.file_id == other.file_id and 111 self.name == other.name and 112 self.parent_id == other.parent_id) 113 114 115class GitTreeFile(_mod_tree.TreeFile): 116 117 __slots__ = ['file_id', 'name', 'parent_id', 'text_size', 118 'executable', 'git_sha1'] 119 120 def __init__(self, file_id, name, parent_id, text_size=None, 121 git_sha1=None, executable=None): 122 self.file_id = file_id 123 self.name = name 124 self.parent_id = parent_id 125 self.text_size = text_size 126 self.git_sha1 = git_sha1 127 self.executable = executable 128 129 @property 130 def kind(self): 131 return 'file' 132 133 def __eq__(self, other): 134 return (self.kind == other.kind and 135 self.file_id == other.file_id and 136 self.name == other.name and 137 self.parent_id == other.parent_id and 138 self.git_sha1 == other.git_sha1 and 139 self.text_size == other.text_size and 140 self.executable == other.executable) 141 142 def __repr__(self): 143 return ("%s(file_id=%r, name=%r, parent_id=%r, text_size=%r, " 144 "git_sha1=%r, executable=%r)") % ( 145 type(self).__name__, self.file_id, self.name, self.parent_id, 146 self.text_size, self.git_sha1, self.executable) 147 148 def copy(self): 149 ret = self.__class__( 150 self.file_id, self.name, self.parent_id) 151 ret.git_sha1 = self.git_sha1 152 ret.text_size = self.text_size 153 ret.executable = self.executable 154 return ret 155 156 157class GitTreeSymlink(_mod_tree.TreeLink): 158 159 __slots__ = ['file_id', 'name', 'parent_id', 'symlink_target'] 160 161 def __init__(self, file_id, name, parent_id, 162 symlink_target=None): 163 self.file_id = file_id 164 self.name = name 165 self.parent_id = parent_id 166 self.symlink_target = symlink_target 167 168 @property 169 def kind(self): 170 return 'symlink' 171 172 @property 173 def executable(self): 174 return False 175 176 @property 177 def text_size(self): 178 return None 179 180 def __repr__(self): 181 return "%s(file_id=%r, name=%r, parent_id=%r, symlink_target=%r)" % ( 182 type(self).__name__, self.file_id, self.name, self.parent_id, 183 self.symlink_target) 184 185 def __eq__(self, other): 186 return (self.kind == other.kind and 187 self.file_id == other.file_id and 188 self.name == other.name and 189 self.parent_id == other.parent_id and 190 self.symlink_target == other.symlink_target) 191 192 def copy(self): 193 return self.__class__( 194 self.file_id, self.name, self.parent_id, 195 self.symlink_target) 196 197 198class GitTreeSubmodule(_mod_tree.TreeReference): 199 200 __slots__ = ['file_id', 'name', 'parent_id', 'reference_revision'] 201 202 def __init__(self, file_id, name, parent_id, reference_revision=None): 203 self.file_id = file_id 204 self.name = name 205 self.parent_id = parent_id 206 self.reference_revision = reference_revision 207 208 @property 209 def executable(self): 210 return False 211 212 @property 213 def kind(self): 214 return 'tree-reference' 215 216 def __repr__(self): 217 return ("%s(file_id=%r, name=%r, parent_id=%r, " 218 "reference_revision=%r)") % ( 219 type(self).__name__, self.file_id, self.name, self.parent_id, 220 self.reference_revision) 221 222 def __eq__(self, other): 223 return (self.kind == other.kind and 224 self.file_id == other.file_id and 225 self.name == other.name and 226 self.parent_id == other.parent_id and 227 self.reference_revision == other.reference_revision) 228 229 def copy(self): 230 return self.__class__( 231 self.file_id, self.name, self.parent_id, 232 self.reference_revision) 233 234 235entry_factory = { 236 'directory': GitTreeDirectory, 237 'file': GitTreeFile, 238 'symlink': GitTreeSymlink, 239 'tree-reference': GitTreeSubmodule, 240 } 241 242 243def ensure_normalized_path(path): 244 """Check whether path is normalized. 245 246 :raises InvalidNormalization: When path is not normalized, and cannot be 247 accessed on this platform by the normalized path. 248 :return: The NFC normalised version of path. 249 """ 250 norm_path, can_access = osutils.normalized_filename(path) 251 if norm_path != path: 252 if can_access: 253 return norm_path 254 else: 255 raise errors.InvalidNormalization(path) 256 return path 257 258 259class GitTree(_mod_tree.Tree): 260 261 def iter_git_objects(self): 262 """Iterate over all the objects in the tree. 263 264 :return :Yields tuples with (path, sha, mode) 265 """ 266 raise NotImplementedError(self.iter_git_objects) 267 268 def git_snapshot(self, want_unversioned=False): 269 """Snapshot a tree, and return tree object. 270 271 :return: Tree sha and set of extras 272 """ 273 raise NotImplementedError(self.snapshot) 274 275 def preview_transform(self, pb=None): 276 from .transform import GitTransformPreview 277 return GitTransformPreview(self, pb=pb) 278 279 def find_related_paths_across_trees(self, paths, trees=[], 280 require_versioned=True): 281 if paths is None: 282 return None 283 if require_versioned: 284 trees = [self] + (trees if trees is not None else []) 285 unversioned = set() 286 for p in paths: 287 for t in trees: 288 if t.is_versioned(p): 289 break 290 else: 291 unversioned.add(p) 292 if unversioned: 293 raise errors.PathsNotVersionedError(unversioned) 294 return filter(self.is_versioned, paths) 295 296 def _submodule_info(self): 297 if self._submodules is None: 298 try: 299 with self.get_file('.gitmodules') as f: 300 config = GitConfigFile.from_file(f) 301 self._submodules = { 302 path: (url, section) 303 for path, url, section in parse_submodules(config)} 304 except errors.NoSuchFile: 305 self._submodules = {} 306 return self._submodules 307 308 309class GitRevisionTree(revisiontree.RevisionTree, GitTree): 310 """Revision tree implementation based on Git objects.""" 311 312 def __init__(self, repository, revision_id): 313 self._revision_id = revision_id 314 self._repository = repository 315 self._submodules = None 316 self.store = repository._git.object_store 317 if not isinstance(revision_id, bytes): 318 raise TypeError(revision_id) 319 self.commit_id, self.mapping = repository.lookup_bzr_revision_id( 320 revision_id) 321 if revision_id == NULL_REVISION: 322 self.tree = None 323 self.mapping = default_mapping 324 else: 325 try: 326 commit = self.store[self.commit_id] 327 except KeyError: 328 raise errors.NoSuchRevision(repository, revision_id) 329 self.tree = commit.tree 330 331 def git_snapshot(self, want_unversioned=False): 332 return self.tree, set() 333 334 def _get_submodule_repository(self, relpath): 335 if not isinstance(relpath, bytes): 336 raise TypeError(relpath) 337 try: 338 info = self._submodule_info()[relpath] 339 except KeyError: 340 nested_repo_transport = self._repository.controldir.user_transport.clone( 341 decode_git_path(relpath)) 342 else: 343 nested_repo_transport = self._repository.controldir.control_transport.clone( 344 posixpath.join('modules', decode_git_path(info[1]))) 345 nested_controldir = _mod_controldir.ControlDir.open_from_transport( 346 nested_repo_transport) 347 return nested_controldir.find_repository() 348 349 def _get_submodule_store(self, relpath): 350 return self._get_submodule_repository(relpath)._git.object_store 351 352 def get_nested_tree(self, path): 353 encoded_path = encode_git_path(path) 354 nested_repo = self._get_submodule_repository(encoded_path) 355 ref_rev = self.get_reference_revision(path) 356 return nested_repo.revision_tree(ref_rev) 357 358 def supports_rename_tracking(self): 359 return False 360 361 def get_file_revision(self, path): 362 change_scanner = self._repository._file_change_scanner 363 if self.commit_id == ZERO_SHA: 364 return NULL_REVISION 365 (unused_path, commit_id) = change_scanner.find_last_change_revision( 366 encode_git_path(path), self.commit_id) 367 return self._repository.lookup_foreign_revision_id( 368 commit_id, self.mapping) 369 370 def get_file_mtime(self, path): 371 try: 372 revid = self.get_file_revision(path) 373 except KeyError: 374 raise errors.NoSuchFile(path) 375 try: 376 rev = self._repository.get_revision(revid) 377 except errors.NoSuchRevision: 378 raise _mod_tree.FileTimestampUnavailable(path) 379 return rev.timestamp 380 381 def id2path(self, file_id, recurse='down'): 382 try: 383 path = self.mapping.parse_file_id(file_id) 384 except ValueError: 385 raise errors.NoSuchId(self, file_id) 386 if self.is_versioned(path): 387 return path 388 raise errors.NoSuchId(self, file_id) 389 390 def is_versioned(self, path): 391 return self.has_filename(path) 392 393 def path2id(self, path): 394 if self.mapping.is_special_file(path): 395 return None 396 if not self.is_versioned(path): 397 return None 398 return self.mapping.generate_file_id(osutils.safe_unicode(path)) 399 400 def all_file_ids(self): 401 raise errors.UnsupportedOperation(self.all_file_ids, self) 402 403 def all_versioned_paths(self): 404 ret = {u''} 405 todo = [(self.store, b'', self.tree)] 406 while todo: 407 (store, path, tree_id) = todo.pop() 408 if tree_id is None: 409 continue 410 tree = store[tree_id] 411 for name, mode, hexsha in tree.items(): 412 subpath = posixpath.join(path, name) 413 ret.add(decode_git_path(subpath)) 414 if stat.S_ISDIR(mode): 415 todo.append((store, subpath, hexsha)) 416 return ret 417 418 def _lookup_path(self, path): 419 if self.tree is None: 420 raise errors.NoSuchFile(path) 421 422 encoded_path = encode_git_path(path) 423 parts = encoded_path.split(b'/') 424 hexsha = self.tree 425 store = self.store 426 mode = None 427 for i, p in enumerate(parts): 428 if not p: 429 continue 430 obj = store[hexsha] 431 if not isinstance(obj, Tree): 432 raise NotTreeError(hexsha) 433 try: 434 mode, hexsha = obj[p] 435 except KeyError: 436 raise errors.NoSuchFile(path) 437 if S_ISGITLINK(mode) and i != len(parts) - 1: 438 store = self._get_submodule_store(b'/'.join(parts[:i + 1])) 439 hexsha = store[hexsha].tree 440 return (store, mode, hexsha) 441 442 def is_executable(self, path): 443 (store, mode, hexsha) = self._lookup_path(path) 444 if mode is None: 445 # the tree root is a directory 446 return False 447 return mode_is_executable(mode) 448 449 def kind(self, path): 450 (store, mode, hexsha) = self._lookup_path(path) 451 if mode is None: 452 # the tree root is a directory 453 return "directory" 454 return mode_kind(mode) 455 456 def has_filename(self, path): 457 try: 458 self._lookup_path(path) 459 except errors.NoSuchFile: 460 return False 461 else: 462 return True 463 464 def list_files(self, include_root=False, from_dir=None, recursive=True, 465 recurse_nested=False): 466 if self.tree is None: 467 return 468 if from_dir is None or from_dir == '.': 469 from_dir = u"" 470 (store, mode, hexsha) = self._lookup_path(from_dir) 471 if mode is None: # Root 472 root_ie = self._get_dir_ie(b"", None) 473 else: 474 parent_path = posixpath.dirname(from_dir) 475 parent_id = self.mapping.generate_file_id(parent_path) 476 if mode_kind(mode) == 'directory': 477 root_ie = self._get_dir_ie(encode_git_path(from_dir), parent_id) 478 else: 479 root_ie = self._get_file_ie( 480 store, encode_git_path(from_dir), 481 posixpath.basename(from_dir), mode, hexsha) 482 if include_root: 483 yield (from_dir, "V", root_ie.kind, root_ie) 484 todo = [] 485 if root_ie.kind == 'directory': 486 todo.append((store, encode_git_path(from_dir), 487 b"", hexsha, root_ie.file_id)) 488 while todo: 489 (store, path, relpath, hexsha, parent_id) = todo.pop() 490 tree = store[hexsha] 491 for name, mode, hexsha in tree.iteritems(): 492 if self.mapping.is_special_file(name): 493 continue 494 child_path = posixpath.join(path, name) 495 child_relpath = posixpath.join(relpath, name) 496 if S_ISGITLINK(mode) and recurse_nested: 497 mode = stat.S_IFDIR 498 store = self._get_submodule_store(child_relpath) 499 hexsha = store[hexsha].tree 500 if stat.S_ISDIR(mode): 501 ie = self._get_dir_ie(child_path, parent_id) 502 if recursive: 503 todo.append( 504 (store, child_path, child_relpath, hexsha, 505 ie.file_id)) 506 else: 507 ie = self._get_file_ie( 508 store, child_path, name, mode, hexsha, parent_id) 509 yield (decode_git_path(child_relpath), "V", ie.kind, ie) 510 511 def _get_file_ie(self, store, path, name, mode, hexsha, parent_id): 512 if not isinstance(path, bytes): 513 raise TypeError(path) 514 if not isinstance(name, bytes): 515 raise TypeError(name) 516 kind = mode_kind(mode) 517 path = decode_git_path(path) 518 name = decode_git_path(name) 519 file_id = self.mapping.generate_file_id(path) 520 ie = entry_factory[kind](file_id, name, parent_id) 521 if kind == 'symlink': 522 ie.symlink_target = decode_git_path(store[hexsha].data) 523 elif kind == 'tree-reference': 524 ie.reference_revision = self.mapping.revision_id_foreign_to_bzr( 525 hexsha) 526 else: 527 ie.git_sha1 = hexsha 528 ie.text_size = None 529 ie.executable = mode_is_executable(mode) 530 return ie 531 532 def _get_dir_ie(self, path, parent_id): 533 path = decode_git_path(path) 534 file_id = self.mapping.generate_file_id(path) 535 return GitTreeDirectory(file_id, posixpath.basename(path), parent_id) 536 537 def iter_child_entries(self, path): 538 (store, mode, tree_sha) = self._lookup_path(path) 539 540 if mode is not None and not stat.S_ISDIR(mode): 541 return 542 543 encoded_path = encode_git_path(path) 544 file_id = self.path2id(path) 545 tree = store[tree_sha] 546 for name, mode, hexsha in tree.iteritems(): 547 if self.mapping.is_special_file(name): 548 continue 549 child_path = posixpath.join(encoded_path, name) 550 if stat.S_ISDIR(mode): 551 yield self._get_dir_ie(child_path, file_id) 552 else: 553 yield self._get_file_ie(store, child_path, name, mode, hexsha, 554 file_id) 555 556 def iter_entries_by_dir(self, specific_files=None, 557 recurse_nested=False): 558 if self.tree is None: 559 return 560 if specific_files is not None: 561 if specific_files in ([""], []): 562 specific_files = None 563 else: 564 specific_files = set([encode_git_path(p) 565 for p in specific_files]) 566 todo = deque([(self.store, b"", self.tree, self.path2id(''))]) 567 if specific_files is None or u"" in specific_files: 568 yield u"", self._get_dir_ie(b"", None) 569 while todo: 570 store, path, tree_sha, parent_id = todo.popleft() 571 tree = store[tree_sha] 572 extradirs = [] 573 for name, mode, hexsha in tree.iteritems(): 574 if self.mapping.is_special_file(name): 575 continue 576 child_path = posixpath.join(path, name) 577 child_path_decoded = decode_git_path(child_path) 578 if recurse_nested and S_ISGITLINK(mode): 579 mode = stat.S_IFDIR 580 store = self._get_submodule_store(child_path) 581 hexsha = store[hexsha].tree 582 if stat.S_ISDIR(mode): 583 if (specific_files is None or 584 any([p for p in specific_files if p.startswith( 585 child_path)])): 586 extradirs.append( 587 (store, child_path, hexsha, 588 self.path2id(child_path_decoded))) 589 if specific_files is None or child_path in specific_files: 590 if stat.S_ISDIR(mode): 591 yield (child_path_decoded, 592 self._get_dir_ie(child_path, parent_id)) 593 else: 594 yield (child_path_decoded, 595 self._get_file_ie(store, child_path, name, mode, 596 hexsha, parent_id)) 597 todo.extendleft(reversed(extradirs)) 598 599 def iter_references(self): 600 if self.supports_tree_reference(): 601 for path, entry in self.iter_entries_by_dir(): 602 if entry.kind == 'tree-reference': 603 yield path 604 605 def get_revision_id(self): 606 """See RevisionTree.get_revision_id.""" 607 return self._revision_id 608 609 def get_file_sha1(self, path, stat_value=None): 610 if self.tree is None: 611 raise errors.NoSuchFile(path) 612 return osutils.sha_string(self.get_file_text(path)) 613 614 def get_file_verifier(self, path, stat_value=None): 615 (store, mode, hexsha) = self._lookup_path(path) 616 return ("GIT", hexsha) 617 618 def get_file_size(self, path): 619 (store, mode, hexsha) = self._lookup_path(path) 620 if stat.S_ISREG(mode): 621 return len(store[hexsha].data) 622 return None 623 624 def get_file_text(self, path): 625 """See RevisionTree.get_file_text.""" 626 (store, mode, hexsha) = self._lookup_path(path) 627 if stat.S_ISREG(mode): 628 return store[hexsha].data 629 else: 630 return b"" 631 632 def get_symlink_target(self, path): 633 """See RevisionTree.get_symlink_target.""" 634 (store, mode, hexsha) = self._lookup_path(path) 635 if stat.S_ISLNK(mode): 636 return decode_git_path(store[hexsha].data) 637 else: 638 return None 639 640 def get_reference_revision(self, path): 641 """See RevisionTree.get_symlink_target.""" 642 (store, mode, hexsha) = self._lookup_path(path) 643 if S_ISGITLINK(mode): 644 try: 645 nested_repo = self._get_submodule_repository(encode_git_path(path)) 646 except errors.NotBranchError: 647 return self.mapping.revision_id_foreign_to_bzr(hexsha) 648 else: 649 return nested_repo.lookup_foreign_revision_id(hexsha) 650 else: 651 return None 652 653 def _comparison_data(self, entry, path): 654 if entry is None: 655 return None, False, None 656 return entry.kind, entry.executable, None 657 658 def path_content_summary(self, path): 659 """See Tree.path_content_summary.""" 660 try: 661 (store, mode, hexsha) = self._lookup_path(path) 662 except errors.NoSuchFile: 663 return ('missing', None, None, None) 664 kind = mode_kind(mode) 665 if kind == 'file': 666 executable = mode_is_executable(mode) 667 contents = store[hexsha].data 668 return (kind, len(contents), executable, 669 osutils.sha_string(contents)) 670 elif kind == 'symlink': 671 return (kind, None, None, decode_git_path(store[hexsha].data)) 672 elif kind == 'tree-reference': 673 nested_repo = self._get_submodule_repository(encode_git_path(path)) 674 return (kind, None, None, 675 nested_repo.lookup_foreign_revision_id(hexsha)) 676 else: 677 return (kind, None, None, None) 678 679 def _iter_tree_contents(self, include_trees=False): 680 if self.tree is None: 681 return iter([]) 682 return self.store.iter_tree_contents( 683 self.tree, include_trees=include_trees) 684 685 def annotate_iter(self, path, default_revision=CURRENT_REVISION): 686 """Return an iterator of revision_id, line tuples. 687 688 For working trees (and mutable trees in general), the special 689 revision_id 'current:' will be used for lines that are new in this 690 tree, e.g. uncommitted changes. 691 :param default_revision: For lines that don't match a basis, mark them 692 with this revision id. Not all implementations will make use of 693 this value. 694 """ 695 with self.lock_read(): 696 # Now we have the parents of this content 697 from breezy.annotate import Annotator 698 from .annotate import AnnotateProvider 699 annotator = Annotator(AnnotateProvider( 700 self._repository._file_change_scanner)) 701 this_key = (path, self.get_file_revision(path)) 702 annotations = [(key[-1], line) 703 for key, line in annotator.annotate_flat(this_key)] 704 return annotations 705 706 def _get_rules_searcher(self, default_searcher): 707 return default_searcher 708 709 def walkdirs(self, prefix=u""): 710 (store, mode, hexsha) = self._lookup_path(prefix) 711 todo = deque( 712 [(store, encode_git_path(prefix), hexsha)]) 713 while todo: 714 store, path, tree_sha = todo.popleft() 715 path_decoded = decode_git_path(path) 716 tree = store[tree_sha] 717 children = [] 718 for name, mode, hexsha in tree.iteritems(): 719 if self.mapping.is_special_file(name): 720 continue 721 child_path = posixpath.join(path, name) 722 if stat.S_ISDIR(mode): 723 todo.append((store, child_path, hexsha)) 724 children.append( 725 (decode_git_path(child_path), decode_git_path(name), 726 mode_kind(mode), None, 727 mode_kind(mode))) 728 yield path_decoded, children 729 730 731def tree_delta_from_git_changes(changes, mappings, 732 specific_files=None, 733 require_versioned=False, include_root=False, 734 source_extras=None, target_extras=None): 735 """Create a TreeDelta from two git trees. 736 737 source and target are iterators over tuples with: 738 (filename, sha, mode) 739 """ 740 (old_mapping, new_mapping) = mappings 741 if target_extras is None: 742 target_extras = set() 743 if source_extras is None: 744 source_extras = set() 745 ret = delta.TreeDelta() 746 added = [] 747 for (change_type, old, new) in changes: 748 (oldpath, oldmode, oldsha) = old 749 (newpath, newmode, newsha) = new 750 if newpath == b'' and not include_root: 751 continue 752 copied = (change_type == 'copy') 753 if oldpath is not None: 754 oldpath_decoded = decode_git_path(oldpath) 755 else: 756 oldpath_decoded = None 757 if newpath is not None: 758 newpath_decoded = decode_git_path(newpath) 759 else: 760 newpath_decoded = None 761 if not (specific_files is None or 762 (oldpath is not None and 763 osutils.is_inside_or_parent_of_any( 764 specific_files, oldpath_decoded)) or 765 (newpath is not None and 766 osutils.is_inside_or_parent_of_any( 767 specific_files, newpath_decoded))): 768 continue 769 770 if oldpath is None: 771 oldexe = None 772 oldkind = None 773 oldname = None 774 oldparent = None 775 oldversioned = False 776 else: 777 oldversioned = (oldpath not in source_extras) 778 if oldmode: 779 oldexe = mode_is_executable(oldmode) 780 oldkind = mode_kind(oldmode) 781 else: 782 oldexe = False 783 oldkind = None 784 if oldpath == b'': 785 oldparent = None 786 oldname = u'' 787 else: 788 (oldparentpath, oldname) = osutils.split(oldpath_decoded) 789 oldparent = old_mapping.generate_file_id(oldparentpath) 790 if newpath is None: 791 newexe = None 792 newkind = None 793 newname = None 794 newparent = None 795 newversioned = False 796 else: 797 newversioned = (newpath not in target_extras) 798 if newmode: 799 newexe = mode_is_executable(newmode) 800 newkind = mode_kind(newmode) 801 else: 802 newexe = False 803 newkind = None 804 if newpath_decoded == u'': 805 newparent = None 806 newname = u'' 807 else: 808 newparentpath, newname = osutils.split(newpath_decoded) 809 newparent = new_mapping.generate_file_id(newparentpath) 810 if oldversioned and not copied: 811 fileid = old_mapping.generate_file_id(oldpath_decoded) 812 elif newversioned: 813 fileid = new_mapping.generate_file_id(newpath_decoded) 814 else: 815 fileid = None 816 if old_mapping.is_special_file(oldpath): 817 oldpath = None 818 if new_mapping.is_special_file(newpath): 819 newpath = None 820 if oldpath is None and newpath is None: 821 continue 822 change = InventoryTreeChange( 823 fileid, (oldpath_decoded, newpath_decoded), (oldsha != newsha), 824 (oldversioned, newversioned), 825 (oldparent, newparent), (oldname, newname), 826 (oldkind, newkind), (oldexe, newexe), 827 copied=copied) 828 if newpath is not None and not newversioned and newkind != 'directory': 829 change.file_id = None 830 ret.unversioned.append(change) 831 elif change_type == 'add': 832 added.append((newpath, newkind)) 833 elif newpath is None or newmode == 0: 834 ret.removed.append(change) 835 elif change_type == 'delete': 836 ret.removed.append(change) 837 elif change_type == 'copy': 838 if stat.S_ISDIR(oldmode) and stat.S_ISDIR(newmode): 839 continue 840 ret.copied.append(change) 841 elif change_type == 'rename': 842 if stat.S_ISDIR(oldmode) and stat.S_ISDIR(newmode): 843 continue 844 ret.renamed.append(change) 845 elif mode_kind(oldmode) != mode_kind(newmode): 846 ret.kind_changed.append(change) 847 elif oldsha != newsha or oldmode != newmode: 848 if stat.S_ISDIR(oldmode) and stat.S_ISDIR(newmode): 849 continue 850 ret.modified.append(change) 851 else: 852 ret.unchanged.append(change) 853 854 implicit_dirs = {b''} 855 for path, kind in added: 856 if kind == 'directory' or path in target_extras: 857 continue 858 implicit_dirs.update(osutils.parent_directories(path)) 859 860 for path, kind in added: 861 if kind == 'directory' and path not in implicit_dirs: 862 continue 863 path_decoded = decode_git_path(path) 864 parent_path, basename = osutils.split(path_decoded) 865 parent_id = new_mapping.generate_file_id(parent_path) 866 file_id = new_mapping.generate_file_id(path_decoded) 867 ret.added.append( 868 InventoryTreeChange( 869 file_id, (None, path_decoded), True, 870 (False, True), 871 (None, parent_id), 872 (None, basename), (None, kind), (None, False))) 873 874 return ret 875 876 877def changes_from_git_changes(changes, mapping, specific_files=None, 878 include_unchanged=False, source_extras=None, 879 target_extras=None): 880 """Create a iter_changes-like generator from a git stream. 881 882 source and target are iterators over tuples with: 883 (filename, sha, mode) 884 """ 885 if target_extras is None: 886 target_extras = set() 887 if source_extras is None: 888 source_extras = set() 889 for (change_type, old, new) in changes: 890 if change_type == 'unchanged' and not include_unchanged: 891 continue 892 (oldpath, oldmode, oldsha) = old 893 (newpath, newmode, newsha) = new 894 if oldpath is not None: 895 oldpath_decoded = decode_git_path(oldpath) 896 else: 897 oldpath_decoded = None 898 if newpath is not None: 899 newpath_decoded = decode_git_path(newpath) 900 else: 901 newpath_decoded = None 902 if not (specific_files is None or 903 (oldpath_decoded is not None and 904 osutils.is_inside_or_parent_of_any( 905 specific_files, oldpath_decoded)) or 906 (newpath_decoded is not None and 907 osutils.is_inside_or_parent_of_any( 908 specific_files, newpath_decoded))): 909 continue 910 if oldpath is not None and mapping.is_special_file(oldpath): 911 continue 912 if newpath is not None and mapping.is_special_file(newpath): 913 continue 914 if oldpath is None: 915 oldexe = None 916 oldkind = None 917 oldname = None 918 oldparent = None 919 oldversioned = False 920 else: 921 oldversioned = (oldpath not in source_extras) 922 if oldmode: 923 oldexe = mode_is_executable(oldmode) 924 oldkind = mode_kind(oldmode) 925 else: 926 oldexe = False 927 oldkind = None 928 if oldpath_decoded == u'': 929 oldparent = None 930 oldname = u'' 931 else: 932 (oldparentpath, oldname) = osutils.split(oldpath_decoded) 933 oldparent = mapping.generate_file_id(oldparentpath) 934 if newpath is None: 935 newexe = None 936 newkind = None 937 newname = None 938 newparent = None 939 newversioned = False 940 else: 941 newversioned = (newpath not in target_extras) 942 if newmode: 943 newexe = mode_is_executable(newmode) 944 newkind = mode_kind(newmode) 945 else: 946 newexe = False 947 newkind = None 948 if newpath_decoded == u'': 949 newparent = None 950 newname = u'' 951 else: 952 newparentpath, newname = osutils.split(newpath_decoded) 953 newparent = mapping.generate_file_id(newparentpath) 954 if (not include_unchanged and 955 oldkind == 'directory' and newkind == 'directory' and 956 oldpath_decoded == newpath_decoded): 957 continue 958 if oldversioned and change_type != 'copy': 959 fileid = mapping.generate_file_id(oldpath_decoded) 960 elif newversioned: 961 fileid = mapping.generate_file_id(newpath_decoded) 962 else: 963 fileid = None 964 if oldkind == 'directory' and newkind == 'directory': 965 modified = False 966 else: 967 modified = (oldsha != newsha) or (oldmode != newmode) 968 yield InventoryTreeChange( 969 fileid, (oldpath_decoded, newpath_decoded), 970 modified, 971 (oldversioned, newversioned), 972 (oldparent, newparent), (oldname, newname), 973 (oldkind, newkind), (oldexe, newexe), 974 copied=(change_type == 'copy')) 975 976 977class InterGitTrees(_mod_tree.InterTree): 978 """InterTree that works between two git trees.""" 979 980 _matching_from_tree_format = None 981 _matching_to_tree_format = None 982 _test_mutable_trees_to_test_trees = None 983 984 def __init__(self, source, target): 985 super(InterGitTrees, self).__init__(source, target) 986 if self.source.store == self.target.store: 987 self.store = self.source.store 988 else: 989 self.store = OverlayObjectStore( 990 [self.source.store, self.target.store]) 991 self.rename_detector = RenameDetector(self.store) 992 993 @classmethod 994 def is_compatible(cls, source, target): 995 return isinstance(source, GitTree) and isinstance(target, GitTree) 996 997 def compare(self, want_unchanged=False, specific_files=None, 998 extra_trees=None, require_versioned=False, include_root=False, 999 want_unversioned=False): 1000 with self.lock_read(): 1001 changes, source_extras, target_extras = self._iter_git_changes( 1002 want_unchanged=want_unchanged, 1003 require_versioned=require_versioned, 1004 specific_files=specific_files, 1005 extra_trees=extra_trees, 1006 want_unversioned=want_unversioned) 1007 return tree_delta_from_git_changes( 1008 changes, (self.source.mapping, self.target.mapping), 1009 specific_files=specific_files, 1010 include_root=include_root, 1011 source_extras=source_extras, target_extras=target_extras) 1012 1013 def iter_changes(self, include_unchanged=False, specific_files=None, 1014 pb=None, extra_trees=[], require_versioned=True, 1015 want_unversioned=False): 1016 with self.lock_read(): 1017 changes, source_extras, target_extras = self._iter_git_changes( 1018 want_unchanged=include_unchanged, 1019 require_versioned=require_versioned, 1020 specific_files=specific_files, 1021 extra_trees=extra_trees, 1022 want_unversioned=want_unversioned) 1023 return changes_from_git_changes( 1024 changes, self.target.mapping, 1025 specific_files=specific_files, 1026 include_unchanged=include_unchanged, 1027 source_extras=source_extras, 1028 target_extras=target_extras) 1029 1030 def _iter_git_changes(self, want_unchanged=False, specific_files=None, 1031 require_versioned=False, extra_trees=None, 1032 want_unversioned=False, include_trees=True): 1033 trees = [self.source] 1034 if extra_trees is not None: 1035 trees.extend(extra_trees) 1036 if specific_files is not None: 1037 specific_files = self.target.find_related_paths_across_trees( 1038 specific_files, trees, 1039 require_versioned=require_versioned) 1040 # TODO(jelmer): Restrict to specific_files, for performance reasons. 1041 with self.lock_read(): 1042 from_tree_sha, from_extras = self.source.git_snapshot( 1043 want_unversioned=want_unversioned) 1044 to_tree_sha, to_extras = self.target.git_snapshot( 1045 want_unversioned=want_unversioned) 1046 changes = tree_changes( 1047 self.store, from_tree_sha, to_tree_sha, 1048 include_trees=include_trees, 1049 rename_detector=self.rename_detector, 1050 want_unchanged=want_unchanged, change_type_same=True) 1051 return changes, from_extras, to_extras 1052 1053 def find_target_path(self, path, recurse='none'): 1054 ret = self.find_target_paths([path], recurse=recurse) 1055 return ret[path] 1056 1057 def find_source_path(self, path, recurse='none'): 1058 ret = self.find_source_paths([path], recurse=recurse) 1059 return ret[path] 1060 1061 def find_target_paths(self, paths, recurse='none'): 1062 paths = set(paths) 1063 ret = {} 1064 changes = self._iter_git_changes( 1065 specific_files=paths, include_trees=False)[0] 1066 for (change_type, old, new) in changes: 1067 if old[0] is None: 1068 continue 1069 oldpath = decode_git_path(old[0]) 1070 if oldpath in paths: 1071 ret[oldpath] = decode_git_path(new[0]) if new[0] else None 1072 for path in paths: 1073 if path not in ret: 1074 if self.source.has_filename(path): 1075 if self.target.has_filename(path): 1076 ret[path] = path 1077 else: 1078 ret[path] = None 1079 else: 1080 raise errors.NoSuchFile(path) 1081 return ret 1082 1083 def find_source_paths(self, paths, recurse='none'): 1084 paths = set(paths) 1085 ret = {} 1086 changes = self._iter_git_changes( 1087 specific_files=paths, include_trees=False)[0] 1088 for (change_type, old, new) in changes: 1089 if new[0] is None: 1090 continue 1091 newpath = decode_git_path(new[0]) 1092 if newpath in paths: 1093 ret[newpath] = decode_git_path(old[0]) if old[0] else None 1094 for path in paths: 1095 if path not in ret: 1096 if self.target.has_filename(path): 1097 if self.source.has_filename(path): 1098 ret[path] = path 1099 else: 1100 ret[path] = None 1101 else: 1102 raise errors.NoSuchFile(path) 1103 return ret 1104 1105 1106_mod_tree.InterTree.register_optimiser(InterGitTrees) 1107 1108 1109class MutableGitIndexTree(mutabletree.MutableTree, GitTree): 1110 1111 def __init__(self): 1112 self._lock_mode = None 1113 self._lock_count = 0 1114 self._versioned_dirs = None 1115 self._index_dirty = False 1116 self._submodules = None 1117 1118 def git_snapshot(self, want_unversioned=False): 1119 return snapshot_workingtree(self, want_unversioned=want_unversioned) 1120 1121 def is_versioned(self, path): 1122 with self.lock_read(): 1123 path = encode_git_path(path.rstrip('/')) 1124 (index, subpath) = self._lookup_index(path) 1125 return (subpath in index or self._has_dir(path)) 1126 1127 def _has_dir(self, path): 1128 if not isinstance(path, bytes): 1129 raise TypeError(path) 1130 if path == b"": 1131 return True 1132 if self._versioned_dirs is None: 1133 self._load_dirs() 1134 return path in self._versioned_dirs 1135 1136 def _load_dirs(self): 1137 if self._lock_mode is None: 1138 raise errors.ObjectNotLocked(self) 1139 self._versioned_dirs = set() 1140 for p, sha, mode in self.iter_git_objects(): 1141 self._ensure_versioned_dir(posixpath.dirname(p)) 1142 1143 def _ensure_versioned_dir(self, dirname): 1144 if not isinstance(dirname, bytes): 1145 raise TypeError(dirname) 1146 if dirname in self._versioned_dirs: 1147 return 1148 if dirname != b"": 1149 self._ensure_versioned_dir(posixpath.dirname(dirname)) 1150 self._versioned_dirs.add(dirname) 1151 1152 def path2id(self, path): 1153 with self.lock_read(): 1154 path = path.rstrip('/') 1155 if self.is_versioned(path.rstrip('/')): 1156 return self.mapping.generate_file_id( 1157 osutils.safe_unicode(path)) 1158 return None 1159 1160 def id2path(self, file_id, recurse='down'): 1161 if file_id is None: 1162 return '' 1163 if type(file_id) is not bytes: 1164 raise TypeError(file_id) 1165 with self.lock_read(): 1166 try: 1167 path = self.mapping.parse_file_id(file_id) 1168 except ValueError: 1169 raise errors.NoSuchId(self, file_id) 1170 if self.is_versioned(path): 1171 return path 1172 raise errors.NoSuchId(self, file_id) 1173 1174 def _set_root_id(self, file_id): 1175 raise errors.UnsupportedOperation(self._set_root_id, self) 1176 1177 def _add(self, files, ids, kinds): 1178 for (path, file_id, kind) in zip(files, ids, kinds): 1179 if file_id is not None: 1180 raise workingtree.SettingFileIdUnsupported() 1181 path, can_access = osutils.normalized_filename(path) 1182 if not can_access: 1183 raise errors.InvalidNormalization(path) 1184 self._index_add_entry(path, kind) 1185 1186 def _read_submodule_head(self, path): 1187 raise NotImplementedError(self._read_submodule_head) 1188 1189 def _lookup_index(self, encoded_path): 1190 if not isinstance(encoded_path, bytes): 1191 raise TypeError(encoded_path) 1192 # Common case: 1193 if encoded_path in self.index: 1194 return self.index, encoded_path 1195 # TODO(jelmer): Perhaps have a cache with paths under which some 1196 # submodules exist? 1197 index = self.index 1198 remaining_path = encoded_path 1199 while True: 1200 parts = remaining_path.split(b'/') 1201 for i in range(1, len(parts)): 1202 basepath = b'/'.join(parts[:i]) 1203 try: 1204 (ctime, mtime, dev, ino, mode, uid, gid, size, sha, 1205 flags) = index[basepath] 1206 except KeyError: 1207 continue 1208 else: 1209 if S_ISGITLINK(mode): 1210 index = self._get_submodule_index(basepath) 1211 remaining_path = b'/'.join(parts[i:]) 1212 break 1213 else: 1214 return index, remaining_path 1215 else: 1216 return index, remaining_path 1217 return index, remaining_path 1218 1219 def _index_del_entry(self, index, path): 1220 del index[path] 1221 # TODO(jelmer): Keep track of dirty per index 1222 self._index_dirty = True 1223 1224 def _apply_index_changes(self, changes): 1225 for (path, kind, executability, reference_revision, 1226 symlink_target) in changes: 1227 if kind is None or kind == 'directory': 1228 (index, subpath) = self._lookup_index( 1229 encode_git_path(path)) 1230 try: 1231 self._index_del_entry(index, subpath) 1232 except KeyError: 1233 pass 1234 else: 1235 self._versioned_dirs = None 1236 else: 1237 self._index_add_entry( 1238 path, kind, 1239 reference_revision=reference_revision, 1240 symlink_target=symlink_target) 1241 self.flush() 1242 1243 def _index_add_entry( 1244 self, path, kind, flags=0, reference_revision=None, 1245 symlink_target=None): 1246 if kind == "directory": 1247 # Git indexes don't contain directories 1248 return 1249 elif kind == "file": 1250 blob = Blob() 1251 try: 1252 file, stat_val = self.get_file_with_stat(path) 1253 except (errors.NoSuchFile, IOError): 1254 # TODO: Rather than come up with something here, use the old 1255 # index 1256 file = BytesIO() 1257 stat_val = os.stat_result( 1258 (stat.S_IFREG | 0o644, 0, 0, 0, 0, 0, 0, 0, 0, 0)) 1259 with file: 1260 blob.set_raw_string(file.read()) 1261 # Add object to the repository if it didn't exist yet 1262 if blob.id not in self.store: 1263 self.store.add_object(blob) 1264 hexsha = blob.id 1265 elif kind == "symlink": 1266 blob = Blob() 1267 try: 1268 stat_val = self._lstat(path) 1269 except EnvironmentError: 1270 # TODO: Rather than come up with something here, use the 1271 # old index 1272 stat_val = os.stat_result( 1273 (stat.S_IFLNK, 0, 0, 0, 0, 0, 0, 0, 0, 0)) 1274 if symlink_target is None: 1275 symlink_target = self.get_symlink_target(path) 1276 blob.set_raw_string(encode_git_path(symlink_target)) 1277 # Add object to the repository if it didn't exist yet 1278 if blob.id not in self.store: 1279 self.store.add_object(blob) 1280 hexsha = blob.id 1281 elif kind == "tree-reference": 1282 if reference_revision is not None: 1283 hexsha = self.branch.lookup_bzr_revision_id( 1284 reference_revision)[0] 1285 else: 1286 hexsha = self._read_submodule_head(path) 1287 if hexsha is None: 1288 raise errors.NoCommits(path) 1289 try: 1290 stat_val = self._lstat(path) 1291 except EnvironmentError: 1292 stat_val = os.stat_result( 1293 (S_IFGITLINK, 0, 0, 0, 0, 0, 0, 0, 0, 0)) 1294 stat_val = os.stat_result((S_IFGITLINK, ) + stat_val[1:]) 1295 else: 1296 raise AssertionError("unknown kind '%s'" % kind) 1297 # Add an entry to the index or update the existing entry 1298 ensure_normalized_path(path) 1299 encoded_path = encode_git_path(path) 1300 if b'\r' in encoded_path or b'\n' in encoded_path: 1301 # TODO(jelmer): Why do we need to do this? 1302 trace.mutter('ignoring path with invalid newline in it: %r', path) 1303 return 1304 (index, index_path) = self._lookup_index(encoded_path) 1305 index[index_path] = index_entry_from_stat(stat_val, hexsha, flags) 1306 self._index_dirty = True 1307 if self._versioned_dirs is not None: 1308 self._ensure_versioned_dir(index_path) 1309 1310 def iter_git_objects(self): 1311 for p, entry in self._recurse_index_entries(): 1312 yield p, entry.sha, entry.mode 1313 1314 def _recurse_index_entries(self, index=None, basepath=b"", 1315 recurse_nested=False): 1316 # Iterate over all index entries 1317 with self.lock_read(): 1318 if index is None: 1319 index = self.index 1320 for path, value in index.items(): 1321 (ctime, mtime, dev, ino, mode, uid, gid, size, sha, 1322 flags) = value 1323 if S_ISGITLINK(mode) and recurse_nested: 1324 subindex = self._get_submodule_index(path) 1325 for entry in self._recurse_index_entries( 1326 index=subindex, basepath=path, 1327 recurse_nested=recurse_nested): 1328 yield entry 1329 else: 1330 yield (posixpath.join(basepath, path), value) 1331 1332 def iter_entries_by_dir(self, specific_files=None, 1333 recurse_nested=False): 1334 with self.lock_read(): 1335 if specific_files is not None: 1336 specific_files = set(specific_files) 1337 else: 1338 specific_files = None 1339 root_ie = self._get_dir_ie(u"", None) 1340 ret = {} 1341 if specific_files is None or u"" in specific_files: 1342 ret[(u"", u"")] = root_ie 1343 dir_ids = {u"": root_ie.file_id} 1344 for path, value in self._recurse_index_entries( 1345 recurse_nested=recurse_nested): 1346 if self.mapping.is_special_file(path): 1347 continue 1348 path = decode_git_path(path) 1349 if specific_files is not None and path not in specific_files: 1350 continue 1351 (parent, name) = posixpath.split(path) 1352 try: 1353 file_ie = self._get_file_ie(name, path, value, None) 1354 except errors.NoSuchFile: 1355 continue 1356 if specific_files is None: 1357 for (dir_path, dir_ie) in self._add_missing_parent_ids( 1358 parent, dir_ids): 1359 ret[(posixpath.dirname(dir_path), dir_path)] = dir_ie 1360 file_ie.parent_id = self.path2id(parent) 1361 ret[(posixpath.dirname(path), path)] = file_ie 1362 # Special casing for directories 1363 if specific_files: 1364 for path in specific_files: 1365 key = (posixpath.dirname(path), path) 1366 if key not in ret and self.is_versioned(path): 1367 ret[key] = self._get_dir_ie(path, self.path2id(key[0])) 1368 return ((path, ie) for ((_, path), ie) in sorted(ret.items())) 1369 1370 def iter_references(self): 1371 if self.supports_tree_reference(): 1372 # TODO(jelmer): Implement a more efficient version of this 1373 for path, entry in self.iter_entries_by_dir(): 1374 if entry.kind == 'tree-reference': 1375 yield path 1376 1377 def _get_dir_ie(self, path, parent_id): 1378 file_id = self.path2id(path) 1379 return GitTreeDirectory(file_id, 1380 posixpath.basename(path).strip("/"), parent_id) 1381 1382 def _get_file_ie(self, name, path, value, parent_id): 1383 if not isinstance(name, str): 1384 raise TypeError(name) 1385 if not isinstance(path, str): 1386 raise TypeError(path) 1387 if not isinstance(value, tuple) or len(value) != 10: 1388 raise TypeError(value) 1389 (ctime, mtime, dev, ino, mode, uid, gid, size, sha, flags) = value 1390 file_id = self.path2id(path) 1391 if not isinstance(file_id, bytes): 1392 raise TypeError(file_id) 1393 kind = mode_kind(mode) 1394 ie = entry_factory[kind](file_id, name, parent_id) 1395 if kind == 'symlink': 1396 ie.symlink_target = self.get_symlink_target(path) 1397 elif kind == 'tree-reference': 1398 ie.reference_revision = self.get_reference_revision(path) 1399 else: 1400 ie.git_sha1 = sha 1401 ie.text_size = size 1402 ie.executable = bool(stat.S_ISREG(mode) and stat.S_IEXEC & mode) 1403 return ie 1404 1405 def _add_missing_parent_ids(self, path, dir_ids): 1406 if path in dir_ids: 1407 return [] 1408 parent = posixpath.dirname(path).strip("/") 1409 ret = self._add_missing_parent_ids(parent, dir_ids) 1410 parent_id = dir_ids[parent] 1411 ie = self._get_dir_ie(path, parent_id) 1412 dir_ids[path] = ie.file_id 1413 ret.append((path, ie)) 1414 return ret 1415 1416 def _comparison_data(self, entry, path): 1417 if entry is None: 1418 return None, False, None 1419 return entry.kind, entry.executable, None 1420 1421 def _unversion_path(self, path): 1422 if self._lock_mode is None: 1423 raise errors.ObjectNotLocked(self) 1424 encoded_path = encode_git_path(path) 1425 count = 0 1426 (index, subpath) = self._lookup_index(encoded_path) 1427 try: 1428 self._index_del_entry(index, encoded_path) 1429 except KeyError: 1430 # A directory, perhaps? 1431 # TODO(jelmer): Deletes that involve submodules? 1432 for p in list(index): 1433 if p.startswith(subpath + b"/"): 1434 count += 1 1435 self._index_del_entry(index, p) 1436 else: 1437 count = 1 1438 self._versioned_dirs = None 1439 return count 1440 1441 def unversion(self, paths): 1442 with self.lock_tree_write(): 1443 for path in paths: 1444 if self._unversion_path(path) == 0: 1445 raise errors.NoSuchFile(path) 1446 self._versioned_dirs = None 1447 self.flush() 1448 1449 def flush(self): 1450 pass 1451 1452 def update_basis_by_delta(self, revid, delta): 1453 # TODO(jelmer): This shouldn't be called, it's inventory specific. 1454 for (old_path, new_path, file_id, ie) in delta: 1455 if old_path is not None: 1456 (index, old_subpath) = self._lookup_index( 1457 encode_git_path(old_path)) 1458 if old_subpath in index: 1459 self._index_del_entry(index, old_subpath) 1460 self._versioned_dirs = None 1461 if new_path is not None and ie.kind != 'directory': 1462 self._index_add_entry(new_path, ie.kind) 1463 self.flush() 1464 self._set_merges_from_parent_ids([]) 1465 1466 def move(self, from_paths, to_dir=None, after=None): 1467 rename_tuples = [] 1468 with self.lock_tree_write(): 1469 to_abs = self.abspath(to_dir) 1470 if not os.path.isdir(to_abs): 1471 raise errors.BzrMoveFailedError('', to_dir, 1472 errors.NotADirectory(to_abs)) 1473 1474 for from_rel in from_paths: 1475 from_tail = os.path.split(from_rel)[-1] 1476 to_rel = os.path.join(to_dir, from_tail) 1477 self.rename_one(from_rel, to_rel, after=after) 1478 rename_tuples.append((from_rel, to_rel)) 1479 self.flush() 1480 return rename_tuples 1481 1482 def rename_one(self, from_rel, to_rel, after=None): 1483 from_path = encode_git_path(from_rel) 1484 to_rel, can_access = osutils.normalized_filename(to_rel) 1485 if not can_access: 1486 raise errors.InvalidNormalization(to_rel) 1487 to_path = encode_git_path(to_rel) 1488 with self.lock_tree_write(): 1489 if not after: 1490 # Perhaps it's already moved? 1491 after = ( 1492 not self.has_filename(from_rel) and 1493 self.has_filename(to_rel) and 1494 not self.is_versioned(to_rel)) 1495 if after: 1496 if not self.has_filename(to_rel): 1497 raise errors.BzrMoveFailedError( 1498 from_rel, to_rel, errors.NoSuchFile(to_rel)) 1499 if self.basis_tree().is_versioned(to_rel): 1500 raise errors.BzrMoveFailedError( 1501 from_rel, to_rel, errors.AlreadyVersionedError(to_rel)) 1502 1503 kind = self.kind(to_rel) 1504 else: 1505 try: 1506 to_kind = self.kind(to_rel) 1507 except errors.NoSuchFile: 1508 exc_type = errors.BzrRenameFailedError 1509 to_kind = None 1510 else: 1511 exc_type = errors.BzrMoveFailedError 1512 if self.is_versioned(to_rel): 1513 raise exc_type(from_rel, to_rel, 1514 errors.AlreadyVersionedError(to_rel)) 1515 if not self.has_filename(from_rel): 1516 raise errors.BzrMoveFailedError( 1517 from_rel, to_rel, errors.NoSuchFile(from_rel)) 1518 kind = self.kind(from_rel) 1519 if not self.is_versioned(from_rel) and kind != 'directory': 1520 raise exc_type(from_rel, to_rel, 1521 errors.NotVersionedError(from_rel)) 1522 if self.has_filename(to_rel): 1523 raise errors.RenameFailedFilesExist( 1524 from_rel, to_rel, errors.FileExists(to_rel)) 1525 1526 kind = self.kind(from_rel) 1527 1528 if not after and kind != 'directory': 1529 (index, from_subpath) = self._lookup_index(from_path) 1530 if from_subpath not in index: 1531 # It's not a file 1532 raise errors.BzrMoveFailedError( 1533 from_rel, to_rel, 1534 errors.NotVersionedError(path=from_rel)) 1535 1536 if not after: 1537 try: 1538 self._rename_one(from_rel, to_rel) 1539 except OSError as e: 1540 if e.errno == errno.ENOENT: 1541 raise errors.BzrMoveFailedError( 1542 from_rel, to_rel, errors.NoSuchFile(to_rel)) 1543 raise 1544 if kind != 'directory': 1545 (index, from_index_path) = self._lookup_index(from_path) 1546 try: 1547 self._index_del_entry(index, from_path) 1548 except KeyError: 1549 pass 1550 self._index_add_entry(to_rel, kind) 1551 else: 1552 todo = [(p, i) for (p, i) in self._recurse_index_entries() 1553 if p.startswith(from_path + b'/')] 1554 for child_path, child_value in todo: 1555 (child_to_index, child_to_index_path) = self._lookup_index( 1556 posixpath.join(to_path, posixpath.relpath(child_path, from_path))) 1557 child_to_index[child_to_index_path] = child_value 1558 # TODO(jelmer): Mark individual index as dirty 1559 self._index_dirty = True 1560 (child_from_index, child_from_index_path) = self._lookup_index( 1561 child_path) 1562 self._index_del_entry( 1563 child_from_index, child_from_index_path) 1564 1565 self._versioned_dirs = None 1566 self.flush() 1567 1568 def path_content_summary(self, path): 1569 """See Tree.path_content_summary.""" 1570 try: 1571 stat_result = self._lstat(path) 1572 except OSError as e: 1573 if getattr(e, 'errno', None) == errno.ENOENT: 1574 # no file. 1575 return ('missing', None, None, None) 1576 # propagate other errors 1577 raise 1578 kind = mode_kind(stat_result.st_mode) 1579 if kind == 'file': 1580 return self._file_content_summary(path, stat_result) 1581 elif kind == 'directory': 1582 # perhaps it looks like a plain directory, but it's really a 1583 # reference. 1584 if self._directory_is_tree_reference(path): 1585 kind = 'tree-reference' 1586 return kind, None, None, None 1587 elif kind == 'symlink': 1588 target = osutils.readlink(self.abspath(path)) 1589 return ('symlink', None, None, target) 1590 else: 1591 return (kind, None, None, None) 1592 1593 def stored_kind(self, relpath): 1594 if relpath == '': 1595 return 'directory' 1596 (index, index_path) = self._lookup_index(encode_git_path(relpath)) 1597 if index is None: 1598 return None 1599 try: 1600 mode = index[index_path].mode 1601 except KeyError: 1602 for p in index: 1603 if osutils.is_inside( 1604 decode_git_path(index_path), decode_git_path(p)): 1605 return 'directory' 1606 return None 1607 else: 1608 return mode_kind(mode) 1609 1610 def kind(self, relpath): 1611 kind = osutils.file_kind(self.abspath(relpath)) 1612 if kind == 'directory': 1613 if self._directory_is_tree_reference(relpath): 1614 return 'tree-reference' 1615 return 'directory' 1616 else: 1617 return kind 1618 1619 def _live_entry(self, relpath): 1620 raise NotImplementedError(self._live_entry) 1621 1622 def transform(self, pb=None): 1623 from .transform import GitTreeTransform 1624 return GitTreeTransform(self, pb=pb) 1625 1626 def has_changes(self, _from_tree=None): 1627 """Quickly check that the tree contains at least one commitable change. 1628 1629 :param _from_tree: tree to compare against to find changes (default to 1630 the basis tree and is intended to be used by tests). 1631 1632 :return: True if a change is found. False otherwise 1633 """ 1634 with self.lock_read(): 1635 # Check pending merges 1636 if len(self.get_parent_ids()) > 1: 1637 return True 1638 if _from_tree is None: 1639 _from_tree = self.basis_tree() 1640 changes = self.iter_changes(_from_tree) 1641 if self.supports_symlinks(): 1642 # Fast path for has_changes. 1643 try: 1644 change = next(changes) 1645 if change.path[1] == '': 1646 next(changes) 1647 return True 1648 except StopIteration: 1649 # No changes 1650 return False 1651 else: 1652 # Slow path for has_changes. 1653 # Handle platforms that do not support symlinks in the 1654 # conditional below. This is slower than the try/except 1655 # approach below that but we don't have a choice as we 1656 # need to be sure that all symlinks are removed from the 1657 # entire changeset. This is because in platforms that 1658 # do not support symlinks, they show up as None in the 1659 # working copy as compared to the repository. 1660 # Also, exclude root as mention in the above fast path. 1661 changes = filter( 1662 lambda c: c[6][0] != 'symlink' and c[4] != (None, None), 1663 changes) 1664 try: 1665 next(iter(changes)) 1666 except StopIteration: 1667 return False 1668 return True 1669 1670 1671def snapshot_workingtree(target, want_unversioned=False): 1672 extras = set() 1673 blobs = {} 1674 # Report dirified directories to commit_tree first, so that they can be 1675 # replaced with non-empty directories if they have contents. 1676 dirified = [] 1677 trust_executable = target._supports_executable() 1678 for path, index_entry in target._recurse_index_entries(): 1679 try: 1680 live_entry = target._live_entry(path) 1681 except EnvironmentError as e: 1682 if e.errno == errno.ENOENT: 1683 # Entry was removed; keep it listed, but mark it as gone. 1684 blobs[path] = (ZERO_SHA, 0) 1685 else: 1686 raise 1687 else: 1688 if live_entry is None: 1689 # Entry was turned into a directory. 1690 # Maybe it's just a submodule that's not checked out? 1691 if S_ISGITLINK(index_entry.mode): 1692 blobs[path] = (index_entry.sha, index_entry.mode) 1693 else: 1694 dirified.append((path, Tree().id, stat.S_IFDIR)) 1695 target.store.add_object(Tree()) 1696 else: 1697 mode = live_entry.mode 1698 if not trust_executable: 1699 if mode_is_executable(index_entry.mode): 1700 mode |= 0o111 1701 else: 1702 mode &= ~0o111 1703 if live_entry.sha != index_entry.sha: 1704 rp = decode_git_path(path) 1705 if stat.S_ISREG(live_entry.mode): 1706 blob = Blob() 1707 with target.get_file(rp) as f: 1708 blob.data = f.read() 1709 elif stat.S_ISLNK(live_entry.mode): 1710 blob = Blob() 1711 blob.data = target.get_symlink_target(rp).encode(osutils._fs_enc) 1712 else: 1713 blob = None 1714 if blob is not None: 1715 target.store.add_object(blob) 1716 blobs[path] = (live_entry.sha, cleanup_mode(live_entry.mode)) 1717 if want_unversioned: 1718 for extra in target._iter_files_recursive(include_dirs=False): 1719 try: 1720 extra, accessible = osutils.normalized_filename(extra) 1721 except UnicodeDecodeError: 1722 raise errors.BadFilenameEncoding( 1723 extra, osutils._fs_enc) 1724 np = encode_git_path(extra) 1725 if np in blobs: 1726 continue 1727 st = target._lstat(extra) 1728 if stat.S_ISDIR(st.st_mode): 1729 blob = Tree() 1730 elif stat.S_ISREG(st.st_mode) or stat.S_ISLNK(st.st_mode): 1731 blob = blob_from_path_and_stat( 1732 target.abspath(extra).encode(osutils._fs_enc), st) 1733 else: 1734 continue 1735 target.store.add_object(blob) 1736 blobs[np] = (blob.id, cleanup_mode(st.st_mode)) 1737 extras.add(np) 1738 return commit_tree( 1739 target.store, dirified + [(p, s, m) for (p, (s, m)) in blobs.items()]), extras 1740