1# Copyright (C) 2005-2010 Canonical Ltd 2# 3# This program is free software; you can redistribute it and/or modify 4# it under the terms of the GNU General Public License as published by 5# the Free Software Foundation; either version 2 of the License, or 6# (at your option) any later version. 7# 8# This program is distributed in the hope that it will be useful, 9# but WITHOUT ANY WARRANTY; without even the implied warranty of 10# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11# GNU General Public License for more details. 12# 13# You should have received a copy of the GNU General Public License 14# along with this program; if not, write to the Free Software 15# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 16 17"""Read in a bundle stream, and process it into a BundleReader object.""" 18 19import base64 20from io import BytesIO 21import os 22import pprint 23 24from ... import ( 25 cache_utf8, 26 osutils, 27 timestamp, 28 ) 29from . import apply_bundle 30from ...errors import ( 31 TestamentMismatch, 32 BzrError, 33 NoSuchId, 34 ) 35from ..inventory import ( 36 Inventory, 37 InventoryDirectory, 38 InventoryFile, 39 InventoryLink, 40 ) 41from ..inventorytree import InventoryTree 42from ...osutils import sha_string, sha_strings, pathjoin 43from ...revision import Revision, NULL_REVISION 44from ..testament import StrictTestament 45from ...trace import mutter, warning 46from ...tree import ( 47 InterTree, 48 Tree, 49 ) 50from ..xml5 import serializer_v5 51 52 53class RevisionInfo(object): 54 """Gets filled out for each revision object that is read. 55 """ 56 57 def __init__(self, revision_id): 58 self.revision_id = revision_id 59 self.sha1 = None 60 self.committer = None 61 self.date = None 62 self.timestamp = None 63 self.timezone = None 64 self.inventory_sha1 = None 65 66 self.parent_ids = None 67 self.base_id = None 68 self.message = None 69 self.properties = None 70 self.tree_actions = None 71 72 def __str__(self): 73 return pprint.pformat(self.__dict__) 74 75 def as_revision(self): 76 rev = Revision(revision_id=self.revision_id, 77 committer=self.committer, 78 timestamp=float(self.timestamp), 79 timezone=int(self.timezone), 80 inventory_sha1=self.inventory_sha1, 81 message='\n'.join(self.message)) 82 83 if self.parent_ids: 84 rev.parent_ids.extend(self.parent_ids) 85 86 if self.properties: 87 for property in self.properties: 88 key_end = property.find(': ') 89 if key_end == -1: 90 if not property.endswith(':'): 91 raise ValueError(property) 92 key = str(property[:-1]) 93 value = '' 94 else: 95 key = str(property[:key_end]) 96 value = property[key_end + 2:] 97 rev.properties[key] = value 98 99 return rev 100 101 @staticmethod 102 def from_revision(revision): 103 revision_info = RevisionInfo(revision.revision_id) 104 date = timestamp.format_highres_date(revision.timestamp, 105 revision.timezone) 106 revision_info.date = date 107 revision_info.timezone = revision.timezone 108 revision_info.timestamp = revision.timestamp 109 revision_info.message = revision.message.split('\n') 110 revision_info.properties = [': '.join(p) for p in 111 revision.properties.items()] 112 return revision_info 113 114 115class BundleInfo(object): 116 """This contains the meta information. Stuff that allows you to 117 recreate the revision or inventory XML. 118 """ 119 120 def __init__(self, bundle_format=None): 121 self.bundle_format = None 122 self.committer = None 123 self.date = None 124 self.message = None 125 126 # A list of RevisionInfo objects 127 self.revisions = [] 128 129 # The next entries are created during complete_info() and 130 # other post-read functions. 131 132 # A list of real Revision objects 133 self.real_revisions = [] 134 135 self.timestamp = None 136 self.timezone = None 137 138 # Have we checked the repository yet? 139 self._validated_revisions_against_repo = False 140 141 def __str__(self): 142 return pprint.pformat(self.__dict__) 143 144 def complete_info(self): 145 """This makes sure that all information is properly 146 split up, based on the assumptions that can be made 147 when information is missing. 148 """ 149 from breezy.timestamp import unpack_highres_date 150 # Put in all of the guessable information. 151 if not self.timestamp and self.date: 152 self.timestamp, self.timezone = unpack_highres_date(self.date) 153 154 self.real_revisions = [] 155 for rev in self.revisions: 156 if rev.timestamp is None: 157 if rev.date is not None: 158 rev.timestamp, rev.timezone = \ 159 unpack_highres_date(rev.date) 160 else: 161 rev.timestamp = self.timestamp 162 rev.timezone = self.timezone 163 if rev.message is None and self.message: 164 rev.message = self.message 165 if rev.committer is None and self.committer: 166 rev.committer = self.committer 167 self.real_revisions.append(rev.as_revision()) 168 169 def get_base(self, revision): 170 revision_info = self.get_revision_info(revision.revision_id) 171 if revision_info.base_id is not None: 172 return revision_info.base_id 173 if len(revision.parent_ids) == 0: 174 # There is no base listed, and 175 # the lowest revision doesn't have a parent 176 # so this is probably against the empty tree 177 # and thus base truly is NULL_REVISION 178 return NULL_REVISION 179 else: 180 return revision.parent_ids[-1] 181 182 def _get_target(self): 183 """Return the target revision.""" 184 if len(self.real_revisions) > 0: 185 return self.real_revisions[0].revision_id 186 elif len(self.revisions) > 0: 187 return self.revisions[0].revision_id 188 return None 189 190 target = property(_get_target, doc='The target revision id') 191 192 def get_revision(self, revision_id): 193 for r in self.real_revisions: 194 if r.revision_id == revision_id: 195 return r 196 raise KeyError(revision_id) 197 198 def get_revision_info(self, revision_id): 199 for r in self.revisions: 200 if r.revision_id == revision_id: 201 return r 202 raise KeyError(revision_id) 203 204 def revision_tree(self, repository, revision_id, base=None): 205 revision = self.get_revision(revision_id) 206 base = self.get_base(revision) 207 if base == revision_id: 208 raise AssertionError() 209 if not self._validated_revisions_against_repo: 210 self._validate_references_from_repository(repository) 211 revision_info = self.get_revision_info(revision_id) 212 inventory_revision_id = revision_id 213 bundle_tree = BundleTree(repository.revision_tree(base), 214 inventory_revision_id) 215 self._update_tree(bundle_tree, revision_id) 216 217 inv = bundle_tree.inventory 218 self._validate_inventory(inv, revision_id) 219 self._validate_revision(bundle_tree, revision_id) 220 221 return bundle_tree 222 223 def _validate_references_from_repository(self, repository): 224 """Now that we have a repository which should have some of the 225 revisions we care about, go through and validate all of them 226 that we can. 227 """ 228 rev_to_sha = {} 229 inv_to_sha = {} 230 231 def add_sha(d, revision_id, sha1): 232 if revision_id is None: 233 if sha1 is not None: 234 raise BzrError('A Null revision should always' 235 'have a null sha1 hash') 236 return 237 if revision_id in d: 238 # This really should have been validated as part 239 # of _validate_revisions but lets do it again 240 if sha1 != d[revision_id]: 241 raise BzrError('** Revision %r referenced with 2 different' 242 ' sha hashes %s != %s' % (revision_id, 243 sha1, d[revision_id])) 244 else: 245 d[revision_id] = sha1 246 247 # All of the contained revisions were checked 248 # in _validate_revisions 249 checked = {} 250 for rev_info in self.revisions: 251 checked[rev_info.revision_id] = True 252 add_sha(rev_to_sha, rev_info.revision_id, rev_info.sha1) 253 254 for (rev, rev_info) in zip(self.real_revisions, self.revisions): 255 add_sha(inv_to_sha, rev_info.revision_id, rev_info.inventory_sha1) 256 257 count = 0 258 missing = {} 259 for revision_id, sha1 in rev_to_sha.items(): 260 if repository.has_revision(revision_id): 261 testament = StrictTestament.from_revision(repository, 262 revision_id) 263 local_sha1 = self._testament_sha1_from_revision(repository, 264 revision_id) 265 if sha1 != local_sha1: 266 raise BzrError('sha1 mismatch. For revision id {%s}' 267 'local: %s, bundle: %s' % (revision_id, local_sha1, sha1)) 268 else: 269 count += 1 270 elif revision_id not in checked: 271 missing[revision_id] = sha1 272 273 if len(missing) > 0: 274 # I don't know if this is an error yet 275 warning('Not all revision hashes could be validated.' 276 ' Unable validate %d hashes' % len(missing)) 277 mutter('Verified %d sha hashes for the bundle.' % count) 278 self._validated_revisions_against_repo = True 279 280 def _validate_inventory(self, inv, revision_id): 281 """At this point we should have generated the BundleTree, 282 so build up an inventory, and make sure the hashes match. 283 """ 284 # Now we should have a complete inventory entry. 285 cs = serializer_v5.write_inventory_to_chunks(inv) 286 sha1 = sha_strings(cs) 287 # Target revision is the last entry in the real_revisions list 288 rev = self.get_revision(revision_id) 289 if rev.revision_id != revision_id: 290 raise AssertionError() 291 if sha1 != rev.inventory_sha1: 292 with open(',,bogus-inv', 'wb') as f: 293 f.writelines(cs) 294 warning('Inventory sha hash mismatch for revision %s. %s' 295 ' != %s' % (revision_id, sha1, rev.inventory_sha1)) 296 297 def _testament(self, revision, tree): 298 raise NotImplementedError(self._testament) 299 300 def _validate_revision(self, tree, revision_id): 301 """Make sure all revision entries match their checksum.""" 302 303 # This is a mapping from each revision id to its sha hash 304 rev_to_sha1 = {} 305 306 rev = self.get_revision(revision_id) 307 rev_info = self.get_revision_info(revision_id) 308 if not (rev.revision_id == rev_info.revision_id): 309 raise AssertionError() 310 if not (rev.revision_id == revision_id): 311 raise AssertionError() 312 testament = self._testament(rev, tree) 313 sha1 = testament.as_sha1() 314 if sha1 != rev_info.sha1: 315 raise TestamentMismatch(rev.revision_id, rev_info.sha1, sha1) 316 if rev.revision_id in rev_to_sha1: 317 raise BzrError('Revision {%s} given twice in the list' 318 % (rev.revision_id)) 319 rev_to_sha1[rev.revision_id] = sha1 320 321 def _update_tree(self, bundle_tree, revision_id): 322 """This fills out a BundleTree based on the information 323 that was read in. 324 325 :param bundle_tree: A BundleTree to update with the new information. 326 """ 327 328 def get_rev_id(last_changed, path, kind): 329 if last_changed is not None: 330 # last_changed will be a Unicode string because of how it was 331 # read. Convert it back to utf8. 332 changed_revision_id = cache_utf8.encode(last_changed) 333 else: 334 changed_revision_id = revision_id 335 bundle_tree.note_last_changed(path, changed_revision_id) 336 return changed_revision_id 337 338 def extra_info(info, new_path): 339 last_changed = None 340 encoding = None 341 for info_item in info: 342 try: 343 name, value = info_item.split(':', 1) 344 except ValueError: 345 raise ValueError('Value %r has no colon' % info_item) 346 if name == 'last-changed': 347 last_changed = value 348 elif name == 'executable': 349 val = (value == 'yes') 350 bundle_tree.note_executable(new_path, val) 351 elif name == 'target': 352 bundle_tree.note_target(new_path, value) 353 elif name == 'encoding': 354 encoding = value 355 return last_changed, encoding 356 357 def do_patch(path, lines, encoding): 358 if encoding == 'base64': 359 patch = base64.b64decode(b''.join(lines)) 360 elif encoding is None: 361 patch = b''.join(lines) 362 else: 363 raise ValueError(encoding) 364 bundle_tree.note_patch(path, patch) 365 366 def renamed(kind, extra, lines): 367 info = extra.split(' // ') 368 if len(info) < 2: 369 raise BzrError('renamed action lines need both a from and to' 370 ': %r' % extra) 371 old_path = info[0] 372 if info[1].startswith('=> '): 373 new_path = info[1][3:] 374 else: 375 new_path = info[1] 376 377 bundle_tree.note_rename(old_path, new_path) 378 last_modified, encoding = extra_info(info[2:], new_path) 379 revision = get_rev_id(last_modified, new_path, kind) 380 if lines: 381 do_patch(new_path, lines, encoding) 382 383 def removed(kind, extra, lines): 384 info = extra.split(' // ') 385 if len(info) > 1: 386 # TODO: in the future we might allow file ids to be 387 # given for removed entries 388 raise BzrError('removed action lines should only have the path' 389 ': %r' % extra) 390 path = info[0] 391 bundle_tree.note_deletion(path) 392 393 def added(kind, extra, lines): 394 info = extra.split(' // ') 395 if len(info) <= 1: 396 raise BzrError('add action lines require the path and file id' 397 ': %r' % extra) 398 elif len(info) > 5: 399 raise BzrError('add action lines have fewer than 5 entries.' 400 ': %r' % extra) 401 path = info[0] 402 if not info[1].startswith('file-id:'): 403 raise BzrError('The file-id should follow the path for an add' 404 ': %r' % extra) 405 # This will be Unicode because of how the stream is read. Turn it 406 # back into a utf8 file_id 407 file_id = cache_utf8.encode(info[1][8:]) 408 409 bundle_tree.note_id(file_id, path, kind) 410 # this will be overridden in extra_info if executable is specified. 411 bundle_tree.note_executable(path, False) 412 last_changed, encoding = extra_info(info[2:], path) 413 revision = get_rev_id(last_changed, path, kind) 414 if kind == 'directory': 415 return 416 do_patch(path, lines, encoding) 417 418 def modified(kind, extra, lines): 419 info = extra.split(' // ') 420 if len(info) < 1: 421 raise BzrError('modified action lines have at least' 422 'the path in them: %r' % extra) 423 path = info[0] 424 425 last_modified, encoding = extra_info(info[1:], path) 426 revision = get_rev_id(last_modified, path, kind) 427 if lines: 428 do_patch(path, lines, encoding) 429 430 valid_actions = { 431 'renamed': renamed, 432 'removed': removed, 433 'added': added, 434 'modified': modified 435 } 436 for action_line, lines in \ 437 self.get_revision_info(revision_id).tree_actions: 438 first = action_line.find(' ') 439 if first == -1: 440 raise BzrError('Bogus action line' 441 ' (no opening space): %r' % action_line) 442 second = action_line.find(' ', first + 1) 443 if second == -1: 444 raise BzrError('Bogus action line' 445 ' (missing second space): %r' % action_line) 446 action = action_line[:first] 447 kind = action_line[first + 1:second] 448 if kind not in ('file', 'directory', 'symlink'): 449 raise BzrError('Bogus action line' 450 ' (invalid object kind %r): %r' % (kind, action_line)) 451 extra = action_line[second + 1:] 452 453 if action not in valid_actions: 454 raise BzrError('Bogus action line' 455 ' (unrecognized action): %r' % action_line) 456 valid_actions[action](kind, extra, lines) 457 458 def install_revisions(self, target_repo, stream_input=True): 459 """Install revisions and return the target revision 460 461 :param target_repo: The repository to install into 462 :param stream_input: Ignored by this implementation. 463 """ 464 apply_bundle.install_bundle(target_repo, self) 465 return self.target 466 467 def get_merge_request(self, target_repo): 468 """Provide data for performing a merge 469 470 Returns suggested base, suggested target, and patch verification status 471 """ 472 return None, self.target, 'inapplicable' 473 474 475class BundleTree(InventoryTree): 476 477 def __init__(self, base_tree, revision_id): 478 self.base_tree = base_tree 479 self._renamed = {} # Mapping from old_path => new_path 480 self._renamed_r = {} # new_path => old_path 481 self._new_id = {} # new_path => new_id 482 self._new_id_r = {} # new_id => new_path 483 self._kinds = {} # new_path => kind 484 self._last_changed = {} # new_id => revision_id 485 self._executable = {} # new_id => executable value 486 self.patches = {} 487 self._targets = {} # new path => new symlink target 488 self.deleted = [] 489 self.revision_id = revision_id 490 self._inventory = None 491 self._base_inter = InterTree.get(self.base_tree, self) 492 493 def __str__(self): 494 return pprint.pformat(self.__dict__) 495 496 def note_rename(self, old_path, new_path): 497 """A file/directory has been renamed from old_path => new_path""" 498 if new_path in self._renamed: 499 raise AssertionError(new_path) 500 if old_path in self._renamed_r: 501 raise AssertionError(old_path) 502 self._renamed[new_path] = old_path 503 self._renamed_r[old_path] = new_path 504 505 def note_id(self, new_id, new_path, kind='file'): 506 """Files that don't exist in base need a new id.""" 507 self._new_id[new_path] = new_id 508 self._new_id_r[new_id] = new_path 509 self._kinds[new_path] = kind 510 511 def note_last_changed(self, file_id, revision_id): 512 if (file_id in self._last_changed 513 and self._last_changed[file_id] != revision_id): 514 raise BzrError('Mismatched last-changed revision for file_id {%s}' 515 ': %s != %s' % (file_id, 516 self._last_changed[file_id], 517 revision_id)) 518 self._last_changed[file_id] = revision_id 519 520 def note_patch(self, new_path, patch): 521 """There is a patch for a given filename.""" 522 self.patches[new_path] = patch 523 524 def note_target(self, new_path, target): 525 """The symlink at the new path has the given target""" 526 self._targets[new_path] = target 527 528 def note_deletion(self, old_path): 529 """The file at old_path has been deleted.""" 530 self.deleted.append(old_path) 531 532 def note_executable(self, new_path, executable): 533 self._executable[new_path] = executable 534 535 def old_path(self, new_path): 536 """Get the old_path (path in the base_tree) for the file at new_path""" 537 if new_path[:1] in ('\\', '/'): 538 raise ValueError(new_path) 539 old_path = self._renamed.get(new_path) 540 if old_path is not None: 541 return old_path 542 dirname, basename = os.path.split(new_path) 543 # dirname is not '' doesn't work, because 544 # dirname may be a unicode entry, and is 545 # requires the objects to be identical 546 if dirname != '': 547 old_dir = self.old_path(dirname) 548 if old_dir is None: 549 old_path = None 550 else: 551 old_path = pathjoin(old_dir, basename) 552 else: 553 old_path = new_path 554 # If the new path wasn't in renamed, the old one shouldn't be in 555 # renamed_r 556 if old_path in self._renamed_r: 557 return None 558 return old_path 559 560 def new_path(self, old_path): 561 """Get the new_path (path in the target_tree) for the file at old_path 562 in the base tree. 563 """ 564 if old_path[:1] in ('\\', '/'): 565 raise ValueError(old_path) 566 new_path = self._renamed_r.get(old_path) 567 if new_path is not None: 568 return new_path 569 if new_path in self._renamed: 570 return None 571 dirname, basename = os.path.split(old_path) 572 if dirname != '': 573 new_dir = self.new_path(dirname) 574 if new_dir is None: 575 new_path = None 576 else: 577 new_path = pathjoin(new_dir, basename) 578 else: 579 new_path = old_path 580 # If the old path wasn't in renamed, the new one shouldn't be in 581 # renamed_r 582 if new_path in self._renamed: 583 return None 584 return new_path 585 586 def path2id(self, path): 587 """Return the id of the file present at path in the target tree.""" 588 file_id = self._new_id.get(path) 589 if file_id is not None: 590 return file_id 591 old_path = self.old_path(path) 592 if old_path is None: 593 return None 594 if old_path in self.deleted: 595 return None 596 return self.base_tree.path2id(old_path) 597 598 def id2path(self, file_id, recurse='down'): 599 """Return the new path in the target tree of the file with id file_id""" 600 path = self._new_id_r.get(file_id) 601 if path is not None: 602 return path 603 old_path = self.base_tree.id2path(file_id, recurse) 604 if old_path is None: 605 raise NoSuchId(file_id, self) 606 if old_path in self.deleted: 607 raise NoSuchId(file_id, self) 608 new_path = self.new_path(old_path) 609 if new_path is None: 610 raise NoSuchId(file_id, self) 611 return new_path 612 613 def get_file(self, path): 614 """Return a file-like object containing the new contents of the 615 file given by file_id. 616 617 TODO: It might be nice if this actually generated an entry 618 in the text-store, so that the file contents would 619 then be cached. 620 """ 621 old_path = self._base_inter.find_source_path(path) 622 if old_path is None: 623 patch_original = None 624 else: 625 patch_original = self.base_tree.get_file(old_path) 626 file_patch = self.patches.get(path) 627 if file_patch is None: 628 if (patch_original is None and 629 self.kind(path) == 'directory'): 630 return BytesIO() 631 if patch_original is None: 632 raise AssertionError("None: %s" % file_id) 633 return patch_original 634 635 if file_patch.startswith(b'\\'): 636 raise ValueError( 637 'Malformed patch for %s, %r' % (file_id, file_patch)) 638 return patched_file(file_patch, patch_original) 639 640 def get_symlink_target(self, path): 641 try: 642 return self._targets[path] 643 except KeyError: 644 old_path = self.old_path(path) 645 return self.base_tree.get_symlink_target(old_path) 646 647 def kind(self, path): 648 try: 649 return self._kinds[path] 650 except KeyError: 651 old_path = self.old_path(path) 652 return self.base_tree.kind(old_path) 653 654 def get_file_revision(self, path): 655 if path in self._last_changed: 656 return self._last_changed[path] 657 else: 658 old_path = self.old_path(path) 659 return self.base_tree.get_file_revision(old_path) 660 661 def is_executable(self, path): 662 if path in self._executable: 663 return self._executable[path] 664 else: 665 old_path = self.old_path(path) 666 return self.base_tree.is_executable(old_path) 667 668 def get_last_changed(self, path): 669 if path in self._last_changed: 670 return self._last_changed[path] 671 old_path = self.old_path(path) 672 return self.base_tree.get_file_revision(old_path) 673 674 def get_size_and_sha1(self, new_path): 675 """Return the size and sha1 hash of the given file id. 676 If the file was not locally modified, this is extracted 677 from the base_tree. Rather than re-reading the file. 678 """ 679 if new_path is None: 680 return None, None 681 if new_path not in self.patches: 682 # If the entry does not have a patch, then the 683 # contents must be the same as in the base_tree 684 base_path = self.old_path(new_path) 685 text_size = self.base_tree.get_file_size(base_path) 686 text_sha1 = self.base_tree.get_file_sha1(base_path) 687 return text_size, text_sha1 688 fileobj = self.get_file(new_path) 689 content = fileobj.read() 690 return len(content), sha_string(content) 691 692 def _get_inventory(self): 693 """Build up the inventory entry for the BundleTree. 694 695 This need to be called before ever accessing self.inventory 696 """ 697 from os.path import dirname, basename 698 inv = Inventory(None, self.revision_id) 699 700 def add_entry(path, file_id): 701 if path == '': 702 parent_id = None 703 else: 704 parent_path = dirname(path) 705 parent_id = self.path2id(parent_path) 706 707 kind = self.kind(path) 708 revision_id = self.get_last_changed(path) 709 710 name = basename(path) 711 if kind == 'directory': 712 ie = InventoryDirectory(file_id, name, parent_id) 713 elif kind == 'file': 714 ie = InventoryFile(file_id, name, parent_id) 715 ie.executable = self.is_executable(path) 716 elif kind == 'symlink': 717 ie = InventoryLink(file_id, name, parent_id) 718 ie.symlink_target = self.get_symlink_target(path) 719 ie.revision = revision_id 720 721 if kind == 'file': 722 ie.text_size, ie.text_sha1 = self.get_size_and_sha1(path) 723 if ie.text_size is None: 724 raise BzrError( 725 'Got a text_size of None for file_id %r' % file_id) 726 inv.add(ie) 727 728 sorted_entries = self.sorted_path_id() 729 for path, file_id in sorted_entries: 730 add_entry(path, file_id) 731 732 return inv 733 734 # Have to overload the inherited inventory property 735 # because _get_inventory is only called in the parent. 736 # Reading the docs, property() objects do not use 737 # overloading, they use the function as it was defined 738 # at that instant 739 inventory = property(_get_inventory) 740 741 root_inventory = property(_get_inventory) 742 743 def all_file_ids(self): 744 return {entry.file_id for path, entry in self.inventory.iter_entries()} 745 746 def all_versioned_paths(self): 747 return {path for path, entry in self.inventory.iter_entries()} 748 749 def list_files(self, include_root=False, from_dir=None, recursive=True): 750 # The only files returned by this are those from the version 751 inv = self.inventory 752 if from_dir is None: 753 from_dir_id = None 754 else: 755 from_dir_id = inv.path2id(from_dir) 756 if from_dir_id is None: 757 # Directory not versioned 758 return 759 entries = inv.iter_entries(from_dir=from_dir_id, recursive=recursive) 760 if inv.root is not None and not include_root and from_dir is None: 761 # skip the root for compatibility with the current apis. 762 next(entries) 763 for path, entry in entries: 764 yield path, 'V', entry.kind, entry 765 766 def sorted_path_id(self): 767 paths = [] 768 for result in self._new_id.items(): 769 paths.append(result) 770 for id in self.base_tree.all_file_ids(): 771 try: 772 path = self.id2path(id, recurse='none') 773 except NoSuchId: 774 continue 775 paths.append((path, id)) 776 paths.sort() 777 return paths 778 779 780def patched_file(file_patch, original): 781 """Produce a file-like object with the patched version of a text""" 782 from breezy.patches import iter_patched 783 from breezy.iterablefile import IterableFile 784 if file_patch == b"": 785 return IterableFile(()) 786 # string.splitlines(True) also splits on '\r', but the iter_patched code 787 # only expects to iterate over '\n' style lines 788 return IterableFile(iter_patched(original, 789 BytesIO(file_patch).readlines())) 790