1# Copyright (C) 2007-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 17import bz2 18from io import ( 19 BytesIO, 20 ) 21import re 22 23from .... import ( 24 bencode, 25 errors, 26 iterablefile, 27 lru_cache, 28 multiparent, 29 osutils, 30 repository as _mod_repository, 31 revision as _mod_revision, 32 trace, 33 ui, 34 ) 35from ... import ( 36 pack, 37 serializer, 38 versionedfile as _mod_versionedfile, 39 ) 40from .. import bundle_data, serializer as bundle_serializer 41from ....i18n import ngettext 42 43 44class _MPDiffInventoryGenerator(_mod_versionedfile._MPDiffGenerator): 45 """Generate Inventory diffs serialized inventories.""" 46 47 def __init__(self, repo, inventory_keys): 48 super(_MPDiffInventoryGenerator, self).__init__(repo.inventories, 49 inventory_keys) 50 self.repo = repo 51 self.sha1s = {} 52 53 def iter_diffs(self): 54 """Compute the diffs one at a time.""" 55 # This is instead of compute_diffs() since we guarantee our ordering of 56 # inventories, we don't have to do any buffering 57 self._find_needed_keys() 58 # We actually use a slightly different ordering. We grab all of the 59 # parents first, and then grab the ordered requests. 60 needed_ids = [k[-1] for k in self.present_parents] 61 needed_ids.extend([k[-1] for k in self.ordered_keys]) 62 inv_to_lines = self.repo._serializer.write_inventory_to_chunks 63 for inv in self.repo.iter_inventories(needed_ids): 64 revision_id = inv.revision_id 65 key = (revision_id,) 66 if key in self.present_parents: 67 # Not a key we will transmit, which is a shame, since because 68 # of that bundles don't work with stacked branches 69 parent_ids = None 70 else: 71 parent_ids = [k[-1] for k in self.parent_map[key]] 72 as_chunks = inv_to_lines(inv) 73 self._process_one_record(key, as_chunks) 74 if parent_ids is None: 75 continue 76 diff = self.diffs.pop(key) 77 sha1 = osutils.sha_strings(as_chunks) 78 yield revision_id, parent_ids, sha1, diff 79 80 81class BundleWriter(object): 82 """Writer for bundle-format files. 83 84 This serves roughly the same purpose as ContainerReader, but acts as a 85 layer on top of it. 86 87 Provides ways of writing the specific record types supported this bundle 88 format. 89 """ 90 91 def __init__(self, fileobj): 92 self._container = pack.ContainerWriter(self._write_encoded) 93 self._fileobj = fileobj 94 self._compressor = bz2.BZ2Compressor() 95 96 def _write_encoded(self, bytes): 97 """Write bzip2-encoded bytes to the file""" 98 self._fileobj.write(self._compressor.compress(bytes)) 99 100 def begin(self): 101 """Start writing the bundle""" 102 self._fileobj.write(bundle_serializer._get_bundle_header('4')) 103 self._fileobj.write(b'#\n') 104 self._container.begin() 105 106 def end(self): 107 """Finish writing the bundle""" 108 self._container.end() 109 self._fileobj.write(self._compressor.flush()) 110 111 def add_multiparent_record(self, mp_bytes, sha1, parents, repo_kind, 112 revision_id, file_id): 113 """Add a record for a multi-parent diff 114 115 :mp_bytes: A multi-parent diff, as a bytestring 116 :sha1: The sha1 hash of the fulltext 117 :parents: a list of revision-ids of the parents 118 :repo_kind: The kind of object in the repository. May be 'file' or 119 'inventory' 120 :revision_id: The revision id of the mpdiff being added. 121 :file_id: The file-id of the file, or None for inventories. 122 """ 123 metadata = {b'parents': parents, 124 b'storage_kind': b'mpdiff', 125 b'sha1': sha1} 126 self._add_record(mp_bytes, metadata, repo_kind, revision_id, file_id) 127 128 def add_fulltext_record(self, bytes, parents, repo_kind, revision_id): 129 """Add a record for a fulltext 130 131 :bytes: The fulltext, as a bytestring 132 :parents: a list of revision-ids of the parents 133 :repo_kind: The kind of object in the repository. May be 'revision' or 134 'signature' 135 :revision_id: The revision id of the fulltext being added. 136 """ 137 metadata = {b'parents': parents, 138 b'storage_kind': b'mpdiff'} 139 self._add_record(bytes, {b'parents': parents, 140 b'storage_kind': b'fulltext'}, repo_kind, revision_id, None) 141 142 def add_info_record(self, kwargs): 143 """Add an info record to the bundle 144 145 Any parameters may be supplied, except 'self' and 'storage_kind'. 146 Values must be lists, strings, integers, dicts, or a combination. 147 """ 148 kwargs[b'storage_kind'] = b'header' 149 self._add_record(None, kwargs, 'info', None, None) 150 151 @staticmethod 152 def encode_name(content_kind, revision_id, file_id=None): 153 """Encode semantic ids as a container name""" 154 if content_kind not in ('revision', 'file', 'inventory', 'signature', 155 'info'): 156 raise ValueError(content_kind) 157 if content_kind == 'file': 158 if file_id is None: 159 raise AssertionError() 160 else: 161 if file_id is not None: 162 raise AssertionError() 163 if content_kind == 'info': 164 if revision_id is not None: 165 raise AssertionError() 166 elif revision_id is None: 167 raise AssertionError() 168 names = [n.replace(b'/', b'//') for n in 169 (content_kind.encode('ascii'), revision_id, file_id) if n is not None] 170 return b'/'.join(names) 171 172 def _add_record(self, bytes, metadata, repo_kind, revision_id, file_id): 173 """Add a bundle record to the container. 174 175 Most bundle records are recorded as header/body pairs, with the 176 body being nameless. Records with storage_kind 'header' have no 177 body. 178 """ 179 name = self.encode_name(repo_kind, revision_id, file_id) 180 encoded_metadata = bencode.bencode(metadata) 181 self._container.add_bytes_record([encoded_metadata], len(encoded_metadata), [(name, )]) 182 if metadata[b'storage_kind'] != b'header': 183 self._container.add_bytes_record([bytes], len(bytes), []) 184 185 186class BundleReader(object): 187 """Reader for bundle-format files. 188 189 This serves roughly the same purpose as ContainerReader, but acts as a 190 layer on top of it, providing metadata, a semantic name, and a record 191 body 192 """ 193 194 def __init__(self, fileobj, stream_input=True): 195 """Constructor 196 197 :param fileobj: a file containing a bzip-encoded container 198 :param stream_input: If True, the BundleReader stream input rather than 199 reading it all into memory at once. Reading it into memory all at 200 once is (currently) faster. 201 """ 202 line = fileobj.readline() 203 if line != '\n': 204 fileobj.readline() 205 self.patch_lines = [] 206 if stream_input: 207 source_file = iterablefile.IterableFile(self.iter_decode(fileobj)) 208 else: 209 source_file = BytesIO(bz2.decompress(fileobj.read())) 210 self._container_file = source_file 211 212 @staticmethod 213 def iter_decode(fileobj): 214 """Iterate through decoded fragments of the file""" 215 decompressor = bz2.BZ2Decompressor() 216 for line in fileobj: 217 try: 218 yield decompressor.decompress(line) 219 except EOFError: 220 return 221 222 @staticmethod 223 def decode_name(name): 224 """Decode a name from its container form into a semantic form 225 226 :retval: content_kind, revision_id, file_id 227 """ 228 segments = re.split(b'(//?)', name) 229 names = [b''] 230 for segment in segments: 231 if segment == b'//': 232 names[-1] += b'/' 233 elif segment == b'/': 234 names.append(b'') 235 else: 236 names[-1] += segment 237 content_kind = names[0] 238 revision_id = None 239 file_id = None 240 if len(names) > 1: 241 revision_id = names[1] 242 if len(names) > 2: 243 file_id = names[2] 244 return content_kind.decode('ascii'), revision_id, file_id 245 246 def iter_records(self): 247 """Iterate through bundle records 248 249 :return: a generator of (bytes, metadata, content_kind, revision_id, 250 file_id) 251 """ 252 iterator = pack.iter_records_from_file(self._container_file) 253 for names, bytes in iterator: 254 if len(names) != 1: 255 raise errors.BadBundle('Record has %d names instead of 1' 256 % len(names)) 257 metadata = bencode.bdecode(bytes) 258 if metadata[b'storage_kind'] == b'header': 259 bytes = None 260 else: 261 _unused, bytes = next(iterator) 262 yield (bytes, metadata) + self.decode_name(names[0][0]) 263 264 265class BundleSerializerV4(bundle_serializer.BundleSerializer): 266 """Implement the high-level bundle interface""" 267 268 def write_bundle(self, repository, target, base, fileobj): 269 """Write a bundle to a file object 270 271 :param repository: The repository to retrieve revision data from 272 :param target: The head revision to include ancestors of 273 :param base: The ancestor of the target to stop including acestors 274 at. 275 :param fileobj: The file-like object to write to 276 """ 277 write_op = BundleWriteOperation(base, target, repository, fileobj) 278 return write_op.do_write() 279 280 def read(self, file): 281 """return a reader object for a given file""" 282 bundle = BundleInfoV4(file, self) 283 return bundle 284 285 @staticmethod 286 def get_source_serializer(info): 287 """Retrieve the serializer for a given info object""" 288 return serializer.format_registry.get(info[b'serializer'].decode('ascii')) 289 290 291class BundleWriteOperation(object): 292 """Perform the operation of writing revisions to a bundle""" 293 294 def __init__(self, base, target, repository, fileobj, revision_ids=None): 295 self.base = base 296 self.target = target 297 self.repository = repository 298 bundle = BundleWriter(fileobj) 299 self.bundle = bundle 300 if revision_ids is not None: 301 self.revision_ids = revision_ids 302 else: 303 graph = repository.get_graph() 304 revision_ids = graph.find_unique_ancestors(target, [base]) 305 # Strip ghosts 306 parents = graph.get_parent_map(revision_ids) 307 self.revision_ids = [r for r in revision_ids if r in parents] 308 self.revision_keys = {(revid,) for revid in self.revision_ids} 309 310 def do_write(self): 311 """Write all data to the bundle""" 312 trace.note(ngettext('Bundling %d revision.', 'Bundling %d revisions.', 313 len(self.revision_ids)), len(self.revision_ids)) 314 with self.repository.lock_read(): 315 self.bundle.begin() 316 self.write_info() 317 self.write_files() 318 self.write_revisions() 319 self.bundle.end() 320 return self.revision_ids 321 322 def write_info(self): 323 """Write format info""" 324 serializer_format = self.repository.get_serializer_format() 325 supports_rich_root = {True: 1, False: 0}[ 326 self.repository.supports_rich_root()] 327 self.bundle.add_info_record({b'serializer': serializer_format, 328 b'supports_rich_root': supports_rich_root}) 329 330 def write_files(self): 331 """Write bundle records for all revisions of all files""" 332 text_keys = [] 333 altered_fileids = self.repository.fileids_altered_by_revision_ids( 334 self.revision_ids) 335 for file_id, revision_ids in altered_fileids.items(): 336 for revision_id in revision_ids: 337 text_keys.append((file_id, revision_id)) 338 self._add_mp_records_keys('file', self.repository.texts, text_keys) 339 340 def write_revisions(self): 341 """Write bundle records for all revisions and signatures""" 342 inv_vf = self.repository.inventories 343 topological_order = [key[-1] for key in multiparent.topo_iter_keys( 344 inv_vf, self.revision_keys)] 345 revision_order = topological_order 346 if self.target is not None and self.target in self.revision_ids: 347 # Make sure the target revision is always the last entry 348 revision_order = list(topological_order) 349 revision_order.remove(self.target) 350 revision_order.append(self.target) 351 if self.repository._serializer.support_altered_by_hack: 352 # Repositories that support_altered_by_hack means that 353 # inventories.make_mpdiffs() contains all the data about the tree 354 # shape. Formats without support_altered_by_hack require 355 # chk_bytes/etc, so we use a different code path. 356 self._add_mp_records_keys('inventory', inv_vf, 357 [(revid,) for revid in topological_order]) 358 else: 359 # Inventories should always be added in pure-topological order, so 360 # that we can apply the mpdiff for the child to the parent texts. 361 self._add_inventory_mpdiffs_from_serializer(topological_order) 362 self._add_revision_texts(revision_order) 363 364 def _add_inventory_mpdiffs_from_serializer(self, revision_order): 365 """Generate mpdiffs by serializing inventories. 366 367 The current repository only has part of the tree shape information in 368 the 'inventories' vf. So we use serializer.write_inventory_to_lines to 369 get a 'full' representation of the tree shape, and then generate 370 mpdiffs on that data stream. This stream can then be reconstructed on 371 the other side. 372 """ 373 inventory_key_order = [(r,) for r in revision_order] 374 generator = _MPDiffInventoryGenerator(self.repository, 375 inventory_key_order) 376 for revision_id, parent_ids, sha1, diff in generator.iter_diffs(): 377 text = b''.join(diff.to_patch()) 378 self.bundle.add_multiparent_record(text, sha1, parent_ids, 379 'inventory', revision_id, None) 380 381 def _add_revision_texts(self, revision_order): 382 parent_map = self.repository.get_parent_map(revision_order) 383 revision_to_bytes = self.repository._serializer.write_revision_to_string 384 revisions = self.repository.get_revisions(revision_order) 385 for revision in revisions: 386 revision_id = revision.revision_id 387 parents = parent_map.get(revision_id, None) 388 revision_text = revision_to_bytes(revision) 389 self.bundle.add_fulltext_record(revision_text, parents, 390 'revision', revision_id) 391 try: 392 self.bundle.add_fulltext_record( 393 self.repository.get_signature_text( 394 revision_id), parents, 'signature', revision_id) 395 except errors.NoSuchRevision: 396 pass 397 398 @staticmethod 399 def get_base_target(revision_ids, forced_bases, repository): 400 """Determine the base and target from old-style revision ids""" 401 if len(revision_ids) == 0: 402 return None, None 403 target = revision_ids[0] 404 base = forced_bases.get(target) 405 if base is None: 406 parents = repository.get_revision(target).parent_ids 407 if len(parents) == 0: 408 base = _mod_revision.NULL_REVISION 409 else: 410 base = parents[0] 411 return base, target 412 413 def _add_mp_records_keys(self, repo_kind, vf, keys): 414 """Add multi-parent diff records to a bundle""" 415 ordered_keys = list(multiparent.topo_iter_keys(vf, keys)) 416 mpdiffs = vf.make_mpdiffs(ordered_keys) 417 sha1s = vf.get_sha1s(ordered_keys) 418 parent_map = vf.get_parent_map(ordered_keys) 419 for mpdiff, item_key, in zip(mpdiffs, ordered_keys): 420 sha1 = sha1s[item_key] 421 parents = [key[-1] for key in parent_map[item_key]] 422 text = b''.join(mpdiff.to_patch()) 423 # Infer file id records as appropriate. 424 if len(item_key) == 2: 425 file_id = item_key[0] 426 else: 427 file_id = None 428 self.bundle.add_multiparent_record(text, sha1, parents, repo_kind, 429 item_key[-1], file_id) 430 431 432class BundleInfoV4(object): 433 434 """Provide (most of) the BundleInfo interface""" 435 436 def __init__(self, fileobj, serializer): 437 self._fileobj = fileobj 438 self._serializer = serializer 439 self.__real_revisions = None 440 self.__revisions = None 441 442 def install(self, repository): 443 return self.install_revisions(repository) 444 445 def install_revisions(self, repository, stream_input=True): 446 """Install this bundle's revisions into the specified repository 447 448 :param target_repo: The repository to install into 449 :param stream_input: If True, will stream input rather than reading it 450 all into memory at once. Reading it into memory all at once is 451 (currently) faster. 452 """ 453 with repository.lock_write(): 454 ri = RevisionInstaller(self.get_bundle_reader(stream_input), 455 self._serializer, repository) 456 return ri.install() 457 458 def get_merge_request(self, target_repo): 459 """Provide data for performing a merge 460 461 Returns suggested base, suggested target, and patch verification status 462 """ 463 return None, self.target, 'inapplicable' 464 465 def get_bundle_reader(self, stream_input=True): 466 """Return a new BundleReader for the associated bundle 467 468 :param stream_input: If True, the BundleReader stream input rather than 469 reading it all into memory at once. Reading it into memory all at 470 once is (currently) faster. 471 """ 472 self._fileobj.seek(0) 473 return BundleReader(self._fileobj, stream_input) 474 475 def _get_real_revisions(self): 476 if self.__real_revisions is None: 477 self.__real_revisions = [] 478 bundle_reader = self.get_bundle_reader() 479 for bytes, metadata, repo_kind, revision_id, file_id in \ 480 bundle_reader.iter_records(): 481 if repo_kind == 'info': 482 serializer =\ 483 self._serializer.get_source_serializer(metadata) 484 if repo_kind == 'revision': 485 rev = serializer.read_revision_from_string(bytes) 486 self.__real_revisions.append(rev) 487 return self.__real_revisions 488 real_revisions = property(_get_real_revisions) 489 490 def _get_revisions(self): 491 if self.__revisions is None: 492 self.__revisions = [] 493 for revision in self.real_revisions: 494 self.__revisions.append( 495 bundle_data.RevisionInfo.from_revision(revision)) 496 return self.__revisions 497 498 revisions = property(_get_revisions) 499 500 def _get_target(self): 501 return self.revisions[-1].revision_id 502 503 target = property(_get_target) 504 505 506class RevisionInstaller(object): 507 """Installs revisions into a repository""" 508 509 def __init__(self, container, serializer, repository): 510 self._container = container 511 self._serializer = serializer 512 self._repository = repository 513 self._info = None 514 515 def install(self): 516 """Perform the installation. 517 518 Must be called with the Repository locked. 519 """ 520 with _mod_repository.WriteGroup(self._repository): 521 return self._install_in_write_group() 522 523 def _install_in_write_group(self): 524 current_file = None 525 current_versionedfile = None 526 pending_file_records = [] 527 inventory_vf = None 528 pending_inventory_records = [] 529 added_inv = set() 530 target_revision = None 531 for bytes, metadata, repo_kind, revision_id, file_id in\ 532 self._container.iter_records(): 533 if repo_kind == 'info': 534 if self._info is not None: 535 raise AssertionError() 536 self._handle_info(metadata) 537 if (pending_file_records and 538 (repo_kind, file_id) != ('file', current_file)): 539 # Flush the data for a single file - prevents memory 540 # spiking due to buffering all files in memory. 541 self._install_mp_records_keys(self._repository.texts, 542 pending_file_records) 543 current_file = None 544 del pending_file_records[:] 545 if len(pending_inventory_records) > 0 and repo_kind != 'inventory': 546 self._install_inventory_records(pending_inventory_records) 547 pending_inventory_records = [] 548 if repo_kind == 'inventory': 549 pending_inventory_records.append( 550 ((revision_id,), metadata, bytes)) 551 if repo_kind == 'revision': 552 target_revision = revision_id 553 self._install_revision(revision_id, metadata, bytes) 554 if repo_kind == 'signature': 555 self._install_signature(revision_id, metadata, bytes) 556 if repo_kind == 'file': 557 current_file = file_id 558 pending_file_records.append( 559 ((file_id, revision_id), metadata, bytes)) 560 self._install_mp_records_keys( 561 self._repository.texts, pending_file_records) 562 return target_revision 563 564 def _handle_info(self, info): 565 """Extract data from an info record""" 566 self._info = info 567 self._source_serializer = self._serializer.get_source_serializer(info) 568 if (info[b'supports_rich_root'] == 0 and 569 self._repository.supports_rich_root()): 570 self.update_root = True 571 else: 572 self.update_root = False 573 574 def _install_mp_records(self, versionedfile, records): 575 if len(records) == 0: 576 return 577 d_func = multiparent.MultiParent.from_patch 578 vf_records = [(r, m['parents'], m['sha1'], d_func(t)) for r, m, t in 579 records if r not in versionedfile] 580 versionedfile.add_mpdiffs(vf_records) 581 582 def _install_mp_records_keys(self, versionedfile, records): 583 d_func = multiparent.MultiParent.from_patch 584 vf_records = [] 585 for key, meta, text in records: 586 # Adapt to tuple interface: A length two key is a file_id, 587 # revision_id pair, a length 1 key is a 588 # revision/signature/inventory. We need to do this because 589 # the metadata extraction from the bundle has not yet been updated 590 # to use the consistent tuple interface itself. 591 if len(key) == 2: 592 prefix = key[:1] 593 else: 594 prefix = () 595 parents = [prefix + (parent,) for parent in meta[b'parents']] 596 vf_records.append((key, parents, meta[b'sha1'], d_func(text))) 597 versionedfile.add_mpdiffs(vf_records) 598 599 def _get_parent_inventory_texts(self, inventory_text_cache, 600 inventory_cache, parent_ids): 601 cached_parent_texts = {} 602 remaining_parent_ids = [] 603 for parent_id in parent_ids: 604 p_text = inventory_text_cache.get(parent_id, None) 605 if p_text is None: 606 remaining_parent_ids.append(parent_id) 607 else: 608 cached_parent_texts[parent_id] = p_text 609 ghosts = () 610 # TODO: Use inventory_cache to grab inventories we already have in 611 # memory 612 if remaining_parent_ids: 613 # first determine what keys are actually present in the local 614 # inventories object (don't use revisions as they haven't been 615 # installed yet.) 616 parent_keys = [(r,) for r in remaining_parent_ids] 617 present_parent_map = self._repository.inventories.get_parent_map( 618 parent_keys) 619 present_parent_ids = [] 620 ghosts = set() 621 for p_id in remaining_parent_ids: 622 if (p_id,) in present_parent_map: 623 present_parent_ids.append(p_id) 624 else: 625 ghosts.add(p_id) 626 to_lines = self._source_serializer.write_inventory_to_chunks 627 for parent_inv in self._repository.iter_inventories( 628 present_parent_ids): 629 p_text = b''.join(to_lines(parent_inv)) 630 inventory_cache[parent_inv.revision_id] = parent_inv 631 cached_parent_texts[parent_inv.revision_id] = p_text 632 inventory_text_cache[parent_inv.revision_id] = p_text 633 634 parent_texts = [cached_parent_texts[parent_id] 635 for parent_id in parent_ids 636 if parent_id not in ghosts] 637 return parent_texts 638 639 def _install_inventory_records(self, records): 640 if (self._info[b'serializer'] == self._repository._serializer.format_num 641 and self._repository._serializer.support_altered_by_hack): 642 return self._install_mp_records_keys(self._repository.inventories, 643 records) 644 # Use a 10MB text cache, since these are string xml inventories. Note 645 # that 10MB is fairly small for large projects (a single inventory can 646 # be >5MB). Another possibility is to cache 10-20 inventory texts 647 # instead 648 inventory_text_cache = lru_cache.LRUSizeCache(10 * 1024 * 1024) 649 # Also cache the in-memory representation. This allows us to create 650 # inventory deltas to apply rather than calling add_inventory from 651 # scratch each time. 652 inventory_cache = lru_cache.LRUCache(10) 653 with ui.ui_factory.nested_progress_bar() as pb: 654 num_records = len(records) 655 for idx, (key, metadata, bytes) in enumerate(records): 656 pb.update('installing inventory', idx, num_records) 657 revision_id = key[-1] 658 parent_ids = metadata[b'parents'] 659 # Note: This assumes the local ghosts are identical to the 660 # ghosts in the source, as the Bundle serialization 661 # format doesn't record ghosts. 662 p_texts = self._get_parent_inventory_texts(inventory_text_cache, 663 inventory_cache, 664 parent_ids) 665 # Why does to_lines() take strings as the source, it seems that 666 # it would have to cast to a list of lines, which we get back 667 # as lines and then cast back to a string. 668 target_lines = multiparent.MultiParent.from_patch(bytes 669 ).to_lines(p_texts) 670 sha1 = osutils.sha_strings(target_lines) 671 if sha1 != metadata[b'sha1']: 672 raise errors.BadBundle("Can't convert to target format") 673 # Add this to the cache so we don't have to extract it again. 674 inventory_text_cache[revision_id] = b''.join(target_lines) 675 target_inv = self._source_serializer.read_inventory_from_lines( 676 target_lines) 677 del target_lines 678 self._handle_root(target_inv, parent_ids) 679 parent_inv = None 680 if parent_ids: 681 parent_inv = inventory_cache.get(parent_ids[0], None) 682 try: 683 if parent_inv is None: 684 self._repository.add_inventory(revision_id, target_inv, 685 parent_ids) 686 else: 687 delta = target_inv._make_delta(parent_inv) 688 self._repository.add_inventory_by_delta(parent_ids[0], 689 delta, revision_id, parent_ids) 690 except serializer.UnsupportedInventoryKind: 691 raise errors.IncompatibleRevision(repr(self._repository)) 692 inventory_cache[revision_id] = target_inv 693 694 def _handle_root(self, target_inv, parent_ids): 695 revision_id = target_inv.revision_id 696 if self.update_root: 697 text_key = (target_inv.root.file_id, revision_id) 698 parent_keys = [(target_inv.root.file_id, parent) for 699 parent in parent_ids] 700 self._repository.texts.add_lines(text_key, parent_keys, []) 701 elif not self._repository.supports_rich_root(): 702 if target_inv.root.revision != revision_id: 703 raise errors.IncompatibleRevision(repr(self._repository)) 704 705 def _install_revision(self, revision_id, metadata, text): 706 if self._repository.has_revision(revision_id): 707 return 708 revision = self._source_serializer.read_revision_from_string(text) 709 self._repository.add_revision(revision.revision_id, revision) 710 711 def _install_signature(self, revision_id, metadata, text): 712 transaction = self._repository.get_transaction() 713 if self._repository.has_signature_for_revision_id(revision_id): 714 return 715 self._repository.add_signature_text(revision_id, text) 716