~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/repository.py

  • Committer: Robert Collins
  • Date: 2010-05-06 23:41:35 UTC
  • mto: This revision was merged to the branch mainline in revision 5223.
  • Revision ID: robertc@robertcollins.net-20100506234135-yivbzczw1sejxnxc
Lock methods on ``Tree``, ``Branch`` and ``Repository`` are now
expected to return an object which can be used to unlock them. This reduces
duplicate code when using cleanups. The previous 'tokens's returned by
``Branch.lock_write`` and ``Repository.lock_write`` are now attributes
on the result of the lock_write. ``repository.RepositoryWriteLockResult``
and ``branch.BranchWriteLockResult`` document this. (Robert Collins)

``log._get_info_for_log_files`` now takes an add_cleanup callable.
(Robert Collins)

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005, 2006, 2007, 2008, 2009 Canonical Ltd
 
1
# Copyright (C) 2005-2010 Canonical Ltd
2
2
#
3
3
# This program is free software; you can redistribute it and/or modify
4
4
# it under the terms of the GNU General Public License as published by
24
24
    bzrdir,
25
25
    check,
26
26
    chk_map,
 
27
    config,
27
28
    debug,
28
29
    errors,
 
30
    fetch as _mod_fetch,
29
31
    fifo_cache,
30
32
    generate_ids,
31
33
    gpg,
32
34
    graph,
33
35
    inventory,
 
36
    inventory_delta,
34
37
    lazy_regex,
35
38
    lockable_files,
36
39
    lockdir,
37
40
    lru_cache,
38
41
    osutils,
39
42
    revision as _mod_revision,
 
43
    static_tuple,
40
44
    symbol_versioning,
 
45
    trace,
41
46
    tsort,
42
47
    ui,
43
48
    versionedfile,
48
53
from bzrlib.testament import Testament
49
54
""")
50
55
 
51
 
from bzrlib.decorators import needs_read_lock, needs_write_lock
 
56
from bzrlib.decorators import needs_read_lock, needs_write_lock, only_raises
52
57
from bzrlib.inter import InterObject
53
58
from bzrlib.inventory import (
54
59
    Inventory,
56
61
    ROOT_ID,
57
62
    entry_factory,
58
63
    )
 
64
from bzrlib.lock import _RelockDebugMixin
59
65
from bzrlib import registry
60
66
from bzrlib.trace import (
61
67
    log_exception_quietly, note, mutter, mutter_callsite, warning)
204
210
            # an inventory delta was accumulated without creating a new
205
211
            # inventory.
206
212
            basis_id = self.basis_delta_revision
207
 
            self.inv_sha1 = self.repository.add_inventory_by_delta(
 
213
            # We ignore the 'inventory' returned by add_inventory_by_delta
 
214
            # because self.new_inventory is used to hint to the rest of the
 
215
            # system what code path was taken
 
216
            self.inv_sha1, _ = self.repository.add_inventory_by_delta(
208
217
                basis_id, self._basis_delta, self._new_revision_id,
209
218
                self.parents)
210
219
        else:
464
473
            if content_summary[2] is None:
465
474
                raise ValueError("Files must not have executable = None")
466
475
            if not store:
467
 
                if (# if the file length changed we have to store:
468
 
                    parent_entry.text_size != content_summary[1] or
469
 
                    # if the exec bit has changed we have to store:
 
476
                # We can't trust a check of the file length because of content
 
477
                # filtering...
 
478
                if (# if the exec bit has changed we have to store:
470
479
                    parent_entry.executable != content_summary[2]):
471
480
                    store = True
472
481
                elif parent_entry.text_sha1 == content_summary[3]:
539
548
                ie.revision = parent_entry.revision
540
549
                return self._get_delta(ie, basis_inv, path), False, None
541
550
            ie.reference_revision = content_summary[3]
 
551
            if ie.reference_revision is None:
 
552
                raise AssertionError("invalid content_summary for nested tree: %r"
 
553
                    % (content_summary,))
542
554
            self._add_text_to_weave(ie.file_id, '', heads, None)
543
555
        else:
544
556
            raise NotImplementedError('unknown kind')
806
818
                seen_root = True
807
819
        self.new_inventory = None
808
820
        if len(inv_delta):
 
821
            # This should perhaps be guarded by a check that the basis we
 
822
            # commit against is the basis for the commit and if not do a delta
 
823
            # against the basis.
809
824
            self._any_changes = True
810
825
        if not seen_root:
811
826
            # housekeeping root entry changes do not affect no-change commits.
845
860
        # versioned roots do not change unless the tree found a change.
846
861
 
847
862
 
 
863
class RepositoryWriteLockResult(object):
 
864
    """The result of write locking a repository.
 
865
 
 
866
    :ivar repository_token: The token obtained from the underlying lock, or
 
867
        None.
 
868
    :ivar unlock: A callable which will unlock the lock.
 
869
    """
 
870
 
 
871
    def __init__(self, unlock, repository_token):
 
872
        self.repository_token = repository_token
 
873
        self.unlock = unlock
 
874
 
 
875
 
848
876
######################################################################
849
877
# Repositories
850
878
 
851
 
class Repository(object):
 
879
 
 
880
class Repository(_RelockDebugMixin, bzrdir.ControlComponent):
852
881
    """Repository holding history for one or more branches.
853
882
 
854
883
    The repository holds and retrieves historical information including
923
952
        """
924
953
        if self._write_group is not self.get_transaction():
925
954
            # has an unlock or relock occured ?
 
955
            if suppress_errors:
 
956
                mutter(
 
957
                '(suppressed) mismatched lock context and write group. %r, %r',
 
958
                self._write_group, self.get_transaction())
 
959
                return
926
960
            raise errors.BzrError(
927
961
                'mismatched lock context and write group. %r, %r' %
928
962
                (self._write_group, self.get_transaction()))
1007
1041
 
1008
1042
        :seealso: add_inventory, for the contract.
1009
1043
        """
1010
 
        inv_lines = self._serialise_inventory_to_lines(inv)
 
1044
        inv_lines = self._serializer.write_inventory_to_lines(inv)
1011
1045
        return self._inventory_add_lines(revision_id, parents,
1012
1046
            inv_lines, check_content=False)
1013
1047
 
1062
1096
        check_content=True):
1063
1097
        """Store lines in inv_vf and return the sha1 of the inventory."""
1064
1098
        parents = [(parent,) for parent in parents]
1065
 
        return self.inventories.add_lines((revision_id,), parents, lines,
 
1099
        result = self.inventories.add_lines((revision_id,), parents, lines,
1066
1100
            check_content=check_content)[0]
 
1101
        self.inventories._access.flush()
 
1102
        return result
1067
1103
 
1068
1104
    def add_revision(self, revision_id, rev, inv=None, config=None):
1069
1105
        """Add rev to the revision store as revision_id.
1145
1181
        # The old API returned a list, should this actually be a set?
1146
1182
        return parent_map.keys()
1147
1183
 
 
1184
    def _check_inventories(self, checker):
 
1185
        """Check the inventories found from the revision scan.
 
1186
        
 
1187
        This is responsible for verifying the sha1 of inventories and
 
1188
        creating a pending_keys set that covers data referenced by inventories.
 
1189
        """
 
1190
        bar = ui.ui_factory.nested_progress_bar()
 
1191
        try:
 
1192
            self._do_check_inventories(checker, bar)
 
1193
        finally:
 
1194
            bar.finished()
 
1195
 
 
1196
    def _do_check_inventories(self, checker, bar):
 
1197
        """Helper for _check_inventories."""
 
1198
        revno = 0
 
1199
        keys = {'chk_bytes':set(), 'inventories':set(), 'texts':set()}
 
1200
        kinds = ['chk_bytes', 'texts']
 
1201
        count = len(checker.pending_keys)
 
1202
        bar.update("inventories", 0, 2)
 
1203
        current_keys = checker.pending_keys
 
1204
        checker.pending_keys = {}
 
1205
        # Accumulate current checks.
 
1206
        for key in current_keys:
 
1207
            if key[0] != 'inventories' and key[0] not in kinds:
 
1208
                checker._report_items.append('unknown key type %r' % (key,))
 
1209
            keys[key[0]].add(key[1:])
 
1210
        if keys['inventories']:
 
1211
            # NB: output order *should* be roughly sorted - topo or
 
1212
            # inverse topo depending on repository - either way decent
 
1213
            # to just delta against. However, pre-CHK formats didn't
 
1214
            # try to optimise inventory layout on disk. As such the
 
1215
            # pre-CHK code path does not use inventory deltas.
 
1216
            last_object = None
 
1217
            for record in self.inventories.check(keys=keys['inventories']):
 
1218
                if record.storage_kind == 'absent':
 
1219
                    checker._report_items.append(
 
1220
                        'Missing inventory {%s}' % (record.key,))
 
1221
                else:
 
1222
                    last_object = self._check_record('inventories', record,
 
1223
                        checker, last_object,
 
1224
                        current_keys[('inventories',) + record.key])
 
1225
            del keys['inventories']
 
1226
        else:
 
1227
            return
 
1228
        bar.update("texts", 1)
 
1229
        while (checker.pending_keys or keys['chk_bytes']
 
1230
            or keys['texts']):
 
1231
            # Something to check.
 
1232
            current_keys = checker.pending_keys
 
1233
            checker.pending_keys = {}
 
1234
            # Accumulate current checks.
 
1235
            for key in current_keys:
 
1236
                if key[0] not in kinds:
 
1237
                    checker._report_items.append('unknown key type %r' % (key,))
 
1238
                keys[key[0]].add(key[1:])
 
1239
            # Check the outermost kind only - inventories || chk_bytes || texts
 
1240
            for kind in kinds:
 
1241
                if keys[kind]:
 
1242
                    last_object = None
 
1243
                    for record in getattr(self, kind).check(keys=keys[kind]):
 
1244
                        if record.storage_kind == 'absent':
 
1245
                            checker._report_items.append(
 
1246
                                'Missing %s {%s}' % (kind, record.key,))
 
1247
                        else:
 
1248
                            last_object = self._check_record(kind, record,
 
1249
                                checker, last_object, current_keys[(kind,) + record.key])
 
1250
                    keys[kind] = set()
 
1251
                    break
 
1252
 
 
1253
    def _check_record(self, kind, record, checker, last_object, item_data):
 
1254
        """Check a single text from this repository."""
 
1255
        if kind == 'inventories':
 
1256
            rev_id = record.key[0]
 
1257
            inv = self._deserialise_inventory(rev_id,
 
1258
                record.get_bytes_as('fulltext'))
 
1259
            if last_object is not None:
 
1260
                delta = inv._make_delta(last_object)
 
1261
                for old_path, path, file_id, ie in delta:
 
1262
                    if ie is None:
 
1263
                        continue
 
1264
                    ie.check(checker, rev_id, inv)
 
1265
            else:
 
1266
                for path, ie in inv.iter_entries():
 
1267
                    ie.check(checker, rev_id, inv)
 
1268
            if self._format.fast_deltas:
 
1269
                return inv
 
1270
        elif kind == 'chk_bytes':
 
1271
            # No code written to check chk_bytes for this repo format.
 
1272
            checker._report_items.append(
 
1273
                'unsupported key type chk_bytes for %s' % (record.key,))
 
1274
        elif kind == 'texts':
 
1275
            self._check_text(record, checker, item_data)
 
1276
        else:
 
1277
            checker._report_items.append(
 
1278
                'unknown key type %s for %s' % (kind, record.key))
 
1279
 
 
1280
    def _check_text(self, record, checker, item_data):
 
1281
        """Check a single text."""
 
1282
        # Check it is extractable.
 
1283
        # TODO: check length.
 
1284
        if record.storage_kind == 'chunked':
 
1285
            chunks = record.get_bytes_as(record.storage_kind)
 
1286
            sha1 = osutils.sha_strings(chunks)
 
1287
            length = sum(map(len, chunks))
 
1288
        else:
 
1289
            content = record.get_bytes_as('fulltext')
 
1290
            sha1 = osutils.sha_string(content)
 
1291
            length = len(content)
 
1292
        if item_data and sha1 != item_data[1]:
 
1293
            checker._report_items.append(
 
1294
                'sha1 mismatch: %s has sha1 %s expected %s referenced by %s' %
 
1295
                (record.key, sha1, item_data[1], item_data[2]))
 
1296
 
1148
1297
    @staticmethod
1149
1298
    def create(a_bzrdir):
1150
1299
        """Construct the current default format repository in a_bzrdir."""
1155
1304
 
1156
1305
        :param _format: The format of the repository on disk.
1157
1306
        :param a_bzrdir: The BzrDir of the repository.
1158
 
 
1159
 
        In the future we will have a single api for all stores for
1160
 
        getting file texts, inventories and revisions, then
1161
 
        this construct will accept instances of those things.
1162
1307
        """
 
1308
        # In the future we will have a single api for all stores for
 
1309
        # getting file texts, inventories and revisions, then
 
1310
        # this construct will accept instances of those things.
1163
1311
        super(Repository, self).__init__()
1164
1312
        self._format = _format
1165
1313
        # the following are part of the public API for Repository:
1171
1319
        self._reconcile_does_inventory_gc = True
1172
1320
        self._reconcile_fixes_text_parents = False
1173
1321
        self._reconcile_backsup_inventory = True
1174
 
        # not right yet - should be more semantically clear ?
1175
 
        #
1176
 
        # TODO: make sure to construct the right store classes, etc, depending
1177
 
        # on whether escaping is required.
1178
 
        self._warn_if_deprecated()
1179
1322
        self._write_group = None
1180
1323
        # Additional places to query for data.
1181
1324
        self._fallback_repositories = []
1182
1325
        # An InventoryEntry cache, used during deserialization
1183
1326
        self._inventory_entry_cache = fifo_cache.FIFOCache(10*1024)
 
1327
        # Is it safe to return inventory entries directly from the entry cache,
 
1328
        # rather copying them?
 
1329
        self._safe_to_return_from_cache = False
 
1330
 
 
1331
    @property
 
1332
    def user_transport(self):
 
1333
        return self.bzrdir.user_transport
 
1334
 
 
1335
    @property
 
1336
    def control_transport(self):
 
1337
        return self._transport
1184
1338
 
1185
1339
    def __repr__(self):
1186
 
        return '%s(%r)' % (self.__class__.__name__,
1187
 
                           self.base)
 
1340
        if self._fallback_repositories:
 
1341
            return '%s(%r, fallback_repositories=%r)' % (
 
1342
                self.__class__.__name__,
 
1343
                self.base,
 
1344
                self._fallback_repositories)
 
1345
        else:
 
1346
            return '%s(%r)' % (self.__class__.__name__,
 
1347
                               self.base)
 
1348
 
 
1349
    def _has_same_fallbacks(self, other_repo):
 
1350
        """Returns true if the repositories have the same fallbacks."""
 
1351
        my_fb = self._fallback_repositories
 
1352
        other_fb = other_repo._fallback_repositories
 
1353
        if len(my_fb) != len(other_fb):
 
1354
            return False
 
1355
        for f, g in zip(my_fb, other_fb):
 
1356
            if not f.has_same_location(g):
 
1357
                return False
 
1358
        return True
1188
1359
 
1189
1360
    def has_same_location(self, other):
1190
1361
        """Returns a boolean indicating if this repository is at the same
1218
1389
        data during reads, and allows a 'write_group' to be obtained. Write
1219
1390
        groups must be used for actual data insertion.
1220
1391
 
 
1392
        A token should be passed in if you know that you have locked the object
 
1393
        some other way, and need to synchronise this object's state with that
 
1394
        fact.
 
1395
 
 
1396
        XXX: this docstring is duplicated in many places, e.g. lockable_files.py
 
1397
 
1221
1398
        :param token: if this is already locked, then lock_write will fail
1222
1399
            unless the token matches the existing lock.
1223
1400
        :returns: a token if this instance supports tokens, otherwise None.
1226
1403
        :raises MismatchedToken: if the specified token doesn't match the token
1227
1404
            of the existing lock.
1228
1405
        :seealso: start_write_group.
1229
 
 
1230
 
        A token should be passed in if you know that you have locked the object
1231
 
        some other way, and need to synchronise this object's state with that
1232
 
        fact.
1233
 
 
1234
 
        XXX: this docstring is duplicated in many places, e.g. lockable_files.py
 
1406
        :return: A RepositoryWriteLockResult.
1235
1407
        """
1236
1408
        locked = self.is_locked()
1237
 
        result = self.control_files.lock_write(token=token)
 
1409
        token = self.control_files.lock_write(token=token)
1238
1410
        if not locked:
 
1411
            self._warn_if_deprecated()
 
1412
            self._note_lock('w')
1239
1413
            for repo in self._fallback_repositories:
1240
1414
                # Writes don't affect fallback repos
1241
1415
                repo.lock_read()
1242
1416
            self._refresh_data()
1243
 
        return result
 
1417
        return RepositoryWriteLockResult(self.unlock, token)
1244
1418
 
1245
1419
    def lock_read(self):
 
1420
        """Lock the repository for read operations.
 
1421
 
 
1422
        :return: An object with an unlock method which will release the lock
 
1423
            obtained.
 
1424
        """
1246
1425
        locked = self.is_locked()
1247
1426
        self.control_files.lock_read()
1248
1427
        if not locked:
 
1428
            self._warn_if_deprecated()
 
1429
            self._note_lock('r')
1249
1430
            for repo in self._fallback_repositories:
1250
1431
                repo.lock_read()
1251
1432
            self._refresh_data()
 
1433
        return self
1252
1434
 
1253
1435
    def get_physical_lock_status(self):
1254
1436
        return self.control_files.get_physical_lock_status()
1314
1496
 
1315
1497
        # now gather global repository information
1316
1498
        # XXX: This is available for many repos regardless of listability.
1317
 
        if self.bzrdir.root_transport.listable():
 
1499
        if self.user_transport.listable():
1318
1500
            # XXX: do we want to __define len__() ?
1319
1501
            # Maybe the versionedfiles object should provide a different
1320
1502
            # method to get the number of keys.
1330
1512
        :param using: If True, list only branches using this repository.
1331
1513
        """
1332
1514
        if using and not self.is_shared():
1333
 
            try:
1334
 
                return [self.bzrdir.open_branch()]
1335
 
            except errors.NotBranchError:
1336
 
                return []
 
1515
            return self.bzrdir.list_branches()
1337
1516
        class Evaluator(object):
1338
1517
 
1339
1518
            def __init__(self):
1348
1527
                    except errors.NoRepositoryPresent:
1349
1528
                        pass
1350
1529
                    else:
1351
 
                        return False, (None, repository)
 
1530
                        return False, ([], repository)
1352
1531
                self.first_call = False
1353
 
                try:
1354
 
                    value = (bzrdir.open_branch(), None)
1355
 
                except errors.NotBranchError:
1356
 
                    value = (None, None)
 
1532
                value = (bzrdir.list_branches(), None)
1357
1533
                return True, value
1358
1534
 
1359
 
        branches = []
1360
 
        for branch, repository in bzrdir.BzrDir.find_bzrdirs(
1361
 
                self.bzrdir.root_transport, evaluate=Evaluator()):
1362
 
            if branch is not None:
1363
 
                branches.append(branch)
 
1535
        ret = []
 
1536
        for branches, repository in bzrdir.BzrDir.find_bzrdirs(
 
1537
                self.user_transport, evaluate=Evaluator()):
 
1538
            if branches is not None:
 
1539
                ret.extend(branches)
1364
1540
            if not using and repository is not None:
1365
 
                branches.extend(repository.find_branches())
1366
 
        return branches
 
1541
                ret.extend(repository.find_branches())
 
1542
        return ret
1367
1543
 
1368
1544
    @needs_read_lock
1369
1545
    def search_missing_revision_ids(self, other, revision_id=None, find_ghosts=True):
1398
1574
        """Commit the contents accrued within the current write group.
1399
1575
 
1400
1576
        :seealso: start_write_group.
 
1577
        
 
1578
        :return: it may return an opaque hint that can be passed to 'pack'.
1401
1579
        """
1402
1580
        if self._write_group is not self.get_transaction():
1403
1581
            # has an unlock or relock occured ?
1457
1635
        # but at the moment we're only checking for texts referenced by
1458
1636
        # inventories at the graph's edge.
1459
1637
        key_deps = self.revisions._index._key_dependencies
1460
 
        key_deps.add_keys(present_inventories)
 
1638
        key_deps.satisfy_refs_for_keys(present_inventories)
1461
1639
        referrers = frozenset(r[0] for r in key_deps.get_referrers())
1462
1640
        file_ids = self.fileids_altered_by_revision_ids(referrers)
1463
1641
        missing_texts = set()
1529
1707
            raise errors.InternalBzrError(
1530
1708
                "May not fetch while in a write group.")
1531
1709
        # fast path same-url fetch operations
1532
 
        if self.has_same_location(source) and fetch_spec is None:
 
1710
        # TODO: lift out to somewhere common with RemoteRepository
 
1711
        # <https://bugs.edge.launchpad.net/bzr/+bug/401646>
 
1712
        if (self.has_same_location(source)
 
1713
            and fetch_spec is None
 
1714
            and self._has_same_fallbacks(source)):
1533
1715
            # check that last_revision is in 'from' and then return a
1534
1716
            # no-operation.
1535
1717
            if (revision_id is not None and
1560
1742
        :param revprops: Optional dictionary of revision properties.
1561
1743
        :param revision_id: Optional revision id.
1562
1744
        """
 
1745
        if self._fallback_repositories:
 
1746
            raise errors.BzrError("Cannot commit from a lightweight checkout "
 
1747
                "to a stacked branch. See "
 
1748
                "https://bugs.launchpad.net/bzr/+bug/375013 for details.")
1563
1749
        result = self._commit_builder_class(self, parents, config,
1564
1750
            timestamp, timezone, committer, revprops, revision_id)
1565
1751
        self.start_write_group()
1566
1752
        return result
1567
1753
 
 
1754
    @only_raises(errors.LockNotHeld, errors.LockBroken)
1568
1755
    def unlock(self):
1569
1756
        if (self.control_files._lock_count == 1 and
1570
1757
            self.control_files._lock_mode == 'w'):
1692
1879
 
1693
1880
    @needs_read_lock
1694
1881
    def get_revisions(self, revision_ids):
1695
 
        """Get many revisions at once."""
 
1882
        """Get many revisions at once.
 
1883
        
 
1884
        Repositories that need to check data on every revision read should 
 
1885
        subclass this method.
 
1886
        """
1696
1887
        return self._get_revisions(revision_ids)
1697
1888
 
1698
1889
    @needs_read_lock
1699
1890
    def _get_revisions(self, revision_ids):
1700
1891
        """Core work logic to get many revisions without sanity checks."""
1701
 
        for rev_id in revision_ids:
1702
 
            if not rev_id or not isinstance(rev_id, basestring):
1703
 
                raise errors.InvalidRevisionId(revision_id=rev_id, branch=self)
 
1892
        revs = {}
 
1893
        for revid, rev in self._iter_revisions(revision_ids):
 
1894
            if rev is None:
 
1895
                raise errors.NoSuchRevision(self, revid)
 
1896
            revs[revid] = rev
 
1897
        return [revs[revid] for revid in revision_ids]
 
1898
 
 
1899
    def _iter_revisions(self, revision_ids):
 
1900
        """Iterate over revision objects.
 
1901
 
 
1902
        :param revision_ids: An iterable of revisions to examine. None may be
 
1903
            passed to request all revisions known to the repository. Note that
 
1904
            not all repositories can find unreferenced revisions; for those
 
1905
            repositories only referenced ones will be returned.
 
1906
        :return: An iterator of (revid, revision) tuples. Absent revisions (
 
1907
            those asked for but not available) are returned as (revid, None).
 
1908
        """
 
1909
        if revision_ids is None:
 
1910
            revision_ids = self.all_revision_ids()
 
1911
        else:
 
1912
            for rev_id in revision_ids:
 
1913
                if not rev_id or not isinstance(rev_id, basestring):
 
1914
                    raise errors.InvalidRevisionId(revision_id=rev_id, branch=self)
1704
1915
        keys = [(key,) for key in revision_ids]
1705
1916
        stream = self.revisions.get_record_stream(keys, 'unordered', True)
1706
 
        revs = {}
1707
1917
        for record in stream:
 
1918
            revid = record.key[0]
1708
1919
            if record.storage_kind == 'absent':
1709
 
                raise errors.NoSuchRevision(self, record.key[0])
1710
 
            text = record.get_bytes_as('fulltext')
1711
 
            rev = self._serializer.read_revision_from_string(text)
1712
 
            revs[record.key[0]] = rev
1713
 
        return [revs[revid] for revid in revision_ids]
1714
 
 
1715
 
    @needs_read_lock
1716
 
    def get_revision_xml(self, revision_id):
1717
 
        # TODO: jam 20070210 This shouldn't be necessary since get_revision
1718
 
        #       would have already do it.
1719
 
        # TODO: jam 20070210 Just use _serializer.write_revision_to_string()
1720
 
        # TODO: this can't just be replaced by:
1721
 
        # return self._serializer.write_revision_to_string(
1722
 
        #     self.get_revision(revision_id))
1723
 
        # as cStringIO preservers the encoding unlike write_revision_to_string
1724
 
        # or some other call down the path.
1725
 
        rev = self.get_revision(revision_id)
1726
 
        rev_tmp = cStringIO.StringIO()
1727
 
        # the current serializer..
1728
 
        self._serializer.write_revision(rev, rev_tmp)
1729
 
        rev_tmp.seek(0)
1730
 
        return rev_tmp.getvalue()
 
1920
                yield (revid, None)
 
1921
            else:
 
1922
                text = record.get_bytes_as('fulltext')
 
1923
                rev = self._serializer.read_revision_from_string(text)
 
1924
                yield (revid, rev)
1731
1925
 
1732
1926
    def get_deltas_for_revisions(self, revisions, specific_fileids=None):
1733
1927
        """Produce a generator of revision deltas.
1976
2170
        """
1977
2171
        selected_keys = set((revid,) for revid in revision_ids)
1978
2172
        w = _inv_weave or self.inventories
1979
 
        pb = ui.ui_factory.nested_progress_bar()
1980
 
        try:
1981
 
            return self._find_file_ids_from_xml_inventory_lines(
1982
 
                w.iter_lines_added_or_present_in_keys(
1983
 
                    selected_keys, pb=pb),
1984
 
                selected_keys)
1985
 
        finally:
1986
 
            pb.finished()
 
2173
        return self._find_file_ids_from_xml_inventory_lines(
 
2174
            w.iter_lines_added_or_present_in_keys(
 
2175
                selected_keys, pb=None),
 
2176
            selected_keys)
1987
2177
 
1988
2178
    def iter_files_bytes(self, desired_files):
1989
2179
        """Iterate through file versions.
2071
2261
                batch_size]
2072
2262
            if not to_query:
2073
2263
                break
2074
 
            for rev_tree in self.revision_trees(to_query):
2075
 
                revision_id = rev_tree.get_revision_id()
 
2264
            for revision_id in to_query:
2076
2265
                parent_ids = ancestors[revision_id]
2077
2266
                for text_key in revision_keys[revision_id]:
2078
2267
                    pb.update("Calculating text parents", processed_texts)
2151
2340
        num_file_ids = len(file_ids)
2152
2341
        for file_id, altered_versions in file_ids.iteritems():
2153
2342
            if pb is not None:
2154
 
                pb.update("fetch texts", count, num_file_ids)
 
2343
                pb.update("Fetch texts", count, num_file_ids)
2155
2344
            count += 1
2156
2345
            yield ("file", file_id, altered_versions)
2157
2346
 
2178
2367
        """Get Inventory object by revision id."""
2179
2368
        return self.iter_inventories([revision_id]).next()
2180
2369
 
2181
 
    def iter_inventories(self, revision_ids):
 
2370
    def iter_inventories(self, revision_ids, ordering=None):
2182
2371
        """Get many inventories by revision_ids.
2183
2372
 
2184
2373
        This will buffer some or all of the texts used in constructing the
2186
2375
        time.
2187
2376
 
2188
2377
        :param revision_ids: The expected revision ids of the inventories.
 
2378
        :param ordering: optional ordering, e.g. 'topological'.  If not
 
2379
            specified, the order of revision_ids will be preserved (by
 
2380
            buffering if necessary).
2189
2381
        :return: An iterator of inventories.
2190
2382
        """
2191
2383
        if ((None in revision_ids)
2192
2384
            or (_mod_revision.NULL_REVISION in revision_ids)):
2193
2385
            raise ValueError('cannot get null revision inventory')
2194
 
        return self._iter_inventories(revision_ids)
 
2386
        return self._iter_inventories(revision_ids, ordering)
2195
2387
 
2196
 
    def _iter_inventories(self, revision_ids):
 
2388
    def _iter_inventories(self, revision_ids, ordering):
2197
2389
        """single-document based inventory iteration."""
2198
 
        for text, revision_id in self._iter_inventory_xmls(revision_ids):
2199
 
            yield self.deserialise_inventory(revision_id, text)
 
2390
        inv_xmls = self._iter_inventory_xmls(revision_ids, ordering)
 
2391
        for text, revision_id in inv_xmls:
 
2392
            yield self._deserialise_inventory(revision_id, text)
2200
2393
 
2201
 
    def _iter_inventory_xmls(self, revision_ids):
 
2394
    def _iter_inventory_xmls(self, revision_ids, ordering):
 
2395
        if ordering is None:
 
2396
            order_as_requested = True
 
2397
            ordering = 'unordered'
 
2398
        else:
 
2399
            order_as_requested = False
2202
2400
        keys = [(revision_id,) for revision_id in revision_ids]
2203
 
        stream = self.inventories.get_record_stream(keys, 'unordered', True)
 
2401
        if not keys:
 
2402
            return
 
2403
        if order_as_requested:
 
2404
            key_iter = iter(keys)
 
2405
            next_key = key_iter.next()
 
2406
        stream = self.inventories.get_record_stream(keys, ordering, True)
2204
2407
        text_chunks = {}
2205
2408
        for record in stream:
2206
2409
            if record.storage_kind != 'absent':
2207
 
                text_chunks[record.key] = record.get_bytes_as('chunked')
 
2410
                chunks = record.get_bytes_as('chunked')
 
2411
                if order_as_requested:
 
2412
                    text_chunks[record.key] = chunks
 
2413
                else:
 
2414
                    yield ''.join(chunks), record.key[-1]
2208
2415
            else:
2209
2416
                raise errors.NoSuchRevision(self, record.key)
2210
 
        for key in keys:
2211
 
            chunks = text_chunks.pop(key)
2212
 
            yield ''.join(chunks), key[-1]
 
2417
            if order_as_requested:
 
2418
                # Yield as many results as we can while preserving order.
 
2419
                while next_key in text_chunks:
 
2420
                    chunks = text_chunks.pop(next_key)
 
2421
                    yield ''.join(chunks), next_key[-1]
 
2422
                    try:
 
2423
                        next_key = key_iter.next()
 
2424
                    except StopIteration:
 
2425
                        # We still want to fully consume the get_record_stream,
 
2426
                        # just in case it is not actually finished at this point
 
2427
                        next_key = None
 
2428
                        break
2213
2429
 
2214
 
    def deserialise_inventory(self, revision_id, xml):
 
2430
    def _deserialise_inventory(self, revision_id, xml):
2215
2431
        """Transform the xml into an inventory object.
2216
2432
 
2217
2433
        :param revision_id: The expected revision id of the inventory.
2218
2434
        :param xml: A serialised inventory.
2219
2435
        """
2220
2436
        result = self._serializer.read_inventory_from_string(xml, revision_id,
2221
 
                    entry_cache=self._inventory_entry_cache)
 
2437
                    entry_cache=self._inventory_entry_cache,
 
2438
                    return_from_cache=self._safe_to_return_from_cache)
2222
2439
        if result.revision_id != revision_id:
2223
2440
            raise AssertionError('revision id mismatch %s != %s' % (
2224
2441
                result.revision_id, revision_id))
2225
2442
        return result
2226
2443
 
2227
 
    def serialise_inventory(self, inv):
2228
 
        return self._serializer.write_inventory_to_string(inv)
2229
 
 
2230
 
    def _serialise_inventory_to_lines(self, inv):
2231
 
        return self._serializer.write_inventory_to_lines(inv)
2232
 
 
2233
2444
    def get_serializer_format(self):
2234
2445
        return self._serializer.format_num
2235
2446
 
2236
2447
    @needs_read_lock
2237
 
    def get_inventory_xml(self, revision_id):
2238
 
        """Get inventory XML as a file object."""
2239
 
        texts = self._iter_inventory_xmls([revision_id])
 
2448
    def _get_inventory_xml(self, revision_id):
 
2449
        """Get serialized inventory as a string."""
 
2450
        texts = self._iter_inventory_xmls([revision_id], 'unordered')
2240
2451
        try:
2241
2452
            text, revision_id = texts.next()
2242
2453
        except StopIteration:
2243
2454
            raise errors.HistoryMissing(self, 'inventory', revision_id)
2244
2455
        return text
2245
2456
 
2246
 
    @needs_read_lock
2247
 
    def get_inventory_sha1(self, revision_id):
2248
 
        """Return the sha1 hash of the inventory entry
2249
 
        """
2250
 
        return self.get_revision(revision_id).inventory_sha1
2251
 
 
2252
2457
    def get_rev_id_for_revno(self, revno, known_pair):
2253
2458
        """Return the revision id of a revno, given a later (revno, revid)
2254
2459
        pair in the same history.
2305
2510
            else:
2306
2511
                next_id = parents[0]
2307
2512
 
2308
 
    @needs_read_lock
2309
 
    def get_revision_inventory(self, revision_id):
2310
 
        """Return inventory of a past revision."""
2311
 
        # TODO: Unify this with get_inventory()
2312
 
        # bzr 0.0.6 and later imposes the constraint that the inventory_id
2313
 
        # must be the same as its revision, so this is trivial.
2314
 
        if revision_id is None:
2315
 
            # This does not make sense: if there is no revision,
2316
 
            # then it is the current tree inventory surely ?!
2317
 
            # and thus get_root_id() is something that looks at the last
2318
 
            # commit on the branch, and the get_root_id is an inventory check.
2319
 
            raise NotImplementedError
2320
 
            # return Inventory(self.get_root_id())
2321
 
        else:
2322
 
            return self.get_inventory(revision_id)
2323
 
 
2324
2513
    def is_shared(self):
2325
2514
        """Return True if this repository is flagged as a shared repository."""
2326
2515
        raise NotImplementedError(self.is_shared)
2360
2549
            return RevisionTree(self, Inventory(root_id=None),
2361
2550
                                _mod_revision.NULL_REVISION)
2362
2551
        else:
2363
 
            inv = self.get_revision_inventory(revision_id)
 
2552
            inv = self.get_inventory(revision_id)
2364
2553
            return RevisionTree(self, inv, revision_id)
2365
2554
 
2366
2555
    def revision_trees(self, revision_ids):
2419
2608
            keys = tsort.topo_sort(parent_map)
2420
2609
        return [None] + list(keys)
2421
2610
 
2422
 
    def pack(self, hint=None):
 
2611
    def pack(self, hint=None, clean_obsolete_packs=False):
2423
2612
        """Compress the data within the repository.
2424
2613
 
2425
2614
        This operation only makes sense for some repository types. For other
2435
2624
            obtained from the result of commit_write_group(). Out of
2436
2625
            date hints are simply ignored, because concurrent operations
2437
2626
            can obsolete them rapidly.
 
2627
 
 
2628
        :param clean_obsolete_packs: Clean obsolete packs immediately after
 
2629
            the pack operation.
2438
2630
        """
2439
2631
 
2440
2632
    def get_transaction(self):
2456
2648
        for ((revision_id,), parent_keys) in \
2457
2649
                self.revisions.get_parent_map(query_keys).iteritems():
2458
2650
            if parent_keys:
2459
 
                result[revision_id] = tuple(parent_revid
2460
 
                    for (parent_revid,) in parent_keys)
 
2651
                result[revision_id] = tuple([parent_revid
 
2652
                    for (parent_revid,) in parent_keys])
2461
2653
            else:
2462
2654
                result[revision_id] = (_mod_revision.NULL_REVISION,)
2463
2655
        return result
2465
2657
    def _make_parents_provider(self):
2466
2658
        return self
2467
2659
 
 
2660
    @needs_read_lock
 
2661
    def get_known_graph_ancestry(self, revision_ids):
 
2662
        """Return the known graph for a set of revision ids and their ancestors.
 
2663
        """
 
2664
        st = static_tuple.StaticTuple
 
2665
        revision_keys = [st(r_id).intern() for r_id in revision_ids]
 
2666
        known_graph = self.revisions.get_known_graph_ancestry(revision_keys)
 
2667
        return graph.GraphThunkIdsToKeys(known_graph)
 
2668
 
2468
2669
    def get_graph(self, other_repository=None):
2469
2670
        """Return the graph walker for this repository format"""
2470
2671
        parents_provider = self._make_parents_provider()
2474
2675
                [parents_provider, other_repository._make_parents_provider()])
2475
2676
        return graph.Graph(parents_provider)
2476
2677
 
2477
 
    def _get_versioned_file_checker(self, text_key_references=None):
 
2678
    def _get_versioned_file_checker(self, text_key_references=None,
 
2679
        ancestors=None):
2478
2680
        """Return an object suitable for checking versioned files.
2479
2681
        
2480
2682
        :param text_key_references: if non-None, an already built
2482
2684
            to whether they were referred to by the inventory of the
2483
2685
            revision_id that they contain. If None, this will be
2484
2686
            calculated.
 
2687
        :param ancestors: Optional result from
 
2688
            self.get_graph().get_parent_map(self.all_revision_ids()) if already
 
2689
            available.
2485
2690
        """
2486
2691
        return _VersionedFileChecker(self,
2487
 
            text_key_references=text_key_references)
 
2692
            text_key_references=text_key_references, ancestors=ancestors)
2488
2693
 
2489
2694
    def revision_ids_to_search_result(self, result_set):
2490
2695
        """Convert a set of revision ids to a graph SearchResult."""
2540
2745
        return record.get_bytes_as('fulltext')
2541
2746
 
2542
2747
    @needs_read_lock
2543
 
    def check(self, revision_ids=None):
 
2748
    def check(self, revision_ids=None, callback_refs=None, check_repo=True):
2544
2749
        """Check consistency of all history of given revision_ids.
2545
2750
 
2546
2751
        Different repository implementations should override _check().
2547
2752
 
2548
2753
        :param revision_ids: A non-empty list of revision_ids whose ancestry
2549
2754
             will be checked.  Typically the last revision_id of a branch.
 
2755
        :param callback_refs: A dict of check-refs to resolve and callback
 
2756
            the check/_check method on the items listed as wanting the ref.
 
2757
            see bzrlib.check.
 
2758
        :param check_repo: If False do not check the repository contents, just 
 
2759
            calculate the data callback_refs requires and call them back.
2550
2760
        """
2551
 
        return self._check(revision_ids)
 
2761
        return self._check(revision_ids, callback_refs=callback_refs,
 
2762
            check_repo=check_repo)
2552
2763
 
2553
 
    def _check(self, revision_ids):
2554
 
        result = check.Check(self)
2555
 
        result.check()
 
2764
    def _check(self, revision_ids, callback_refs, check_repo):
 
2765
        result = check.Check(self, check_repo=check_repo)
 
2766
        result.check(callback_refs)
2556
2767
        return result
2557
2768
 
2558
 
    def _warn_if_deprecated(self):
 
2769
    def _warn_if_deprecated(self, branch=None):
2559
2770
        global _deprecation_warning_done
2560
2771
        if _deprecation_warning_done:
2561
2772
            return
2562
 
        _deprecation_warning_done = True
2563
 
        warning("Format %s for %s is deprecated - please use 'bzr upgrade' to get better performance"
2564
 
                % (self._format, self.bzrdir.transport.base))
 
2773
        try:
 
2774
            if branch is None:
 
2775
                conf = config.GlobalConfig()
 
2776
            else:
 
2777
                conf = branch.get_config()
 
2778
            if conf.suppress_warning('format_deprecation'):
 
2779
                return
 
2780
            warning("Format %s for %s is deprecated -"
 
2781
                    " please use 'bzr upgrade' to get better performance"
 
2782
                    % (self._format, self.bzrdir.transport.base))
 
2783
        finally:
 
2784
            _deprecation_warning_done = True
2565
2785
 
2566
2786
    def supports_rich_root(self):
2567
2787
        return self._format.rich_root_data
2848
3068
    # help), and for fetching when data won't have come from the same
2849
3069
    # compressor.
2850
3070
    pack_compresses = False
 
3071
    # Does the repository inventory storage understand references to trees?
 
3072
    supports_tree_reference = None
 
3073
    # Is the format experimental ?
 
3074
    experimental = False
2851
3075
 
2852
 
    def __str__(self):
2853
 
        return "<%s>" % self.__class__.__name__
 
3076
    def __repr__(self):
 
3077
        return "%s()" % self.__class__.__name__
2854
3078
 
2855
3079
    def __eq__(self, other):
2856
3080
        # format objects are generally stateless
2869
3093
        """
2870
3094
        try:
2871
3095
            transport = a_bzrdir.get_repository_transport(None)
2872
 
            format_string = transport.get("format").read()
 
3096
            format_string = transport.get_bytes("format")
2873
3097
            return format_registry.get(format_string)
2874
3098
        except errors.NoSuchFile:
2875
3099
            raise errors.NoRepositoryPresent(a_bzrdir)
2957
3181
        raise NotImplementedError(self.network_name)
2958
3182
 
2959
3183
    def check_conversion_target(self, target_format):
2960
 
        raise NotImplementedError(self.check_conversion_target)
 
3184
        if self.rich_root_data and not target_format.rich_root_data:
 
3185
            raise errors.BadConversionTarget(
 
3186
                'Does not support rich root data.', target_format,
 
3187
                from_format=self)
 
3188
        if (self.supports_tree_reference and 
 
3189
            not getattr(target_format, 'supports_tree_reference', False)):
 
3190
            raise errors.BadConversionTarget(
 
3191
                'Does not support nested trees', target_format,
 
3192
                from_format=self)
2961
3193
 
2962
3194
    def open(self, a_bzrdir, _found=False):
2963
3195
        """Return an instance of this format for the bzrdir a_bzrdir.
2966
3198
        """
2967
3199
        raise NotImplementedError(self.open)
2968
3200
 
 
3201
    def _run_post_repo_init_hooks(self, repository, a_bzrdir, shared):
 
3202
        from bzrlib.bzrdir import BzrDir, RepoInitHookParams
 
3203
        hooks = BzrDir.hooks['post_repo_init']
 
3204
        if not hooks:
 
3205
            return
 
3206
        params = RepoInitHookParams(repository, self, a_bzrdir, shared)
 
3207
        for hook in hooks:
 
3208
            hook(params)
 
3209
 
2969
3210
 
2970
3211
class MetaDirRepositoryFormat(RepositoryFormat):
2971
3212
    """Common base class for the new repositories using the metadir layout."""
3176
3417
 
3177
3418
        :param revision_id: if None all content is copied, if NULL_REVISION no
3178
3419
                            content is copied.
3179
 
        :param pb: optional progress bar to use for progress reports. If not
3180
 
                   provided a default one will be created.
 
3420
        :param pb: ignored.
3181
3421
        :return: None.
3182
3422
        """
 
3423
        ui.ui_factory.warn_experimental_format_fetch(self)
3183
3424
        from bzrlib.fetch import RepoFetcher
 
3425
        # See <https://launchpad.net/bugs/456077> asking for a warning here
 
3426
        if self.source._format.network_name() != self.target._format.network_name():
 
3427
            ui.ui_factory.show_user_warning('cross_format_fetch',
 
3428
                from_format=self.source._format,
 
3429
                to_format=self.target._format)
3184
3430
        f = RepoFetcher(to_repository=self.target,
3185
3431
                               from_repository=self.source,
3186
3432
                               last_revision=revision_id,
3187
3433
                               fetch_spec=fetch_spec,
3188
 
                               pb=pb, find_ghosts=find_ghosts)
 
3434
                               find_ghosts=find_ghosts)
3189
3435
 
3190
3436
    def _walk_to_common_revisions(self, revision_ids):
3191
3437
        """Walk out from revision_ids in source to revisions target has.
3360
3606
                self.target.texts.insert_record_stream(
3361
3607
                    self.source.texts.get_record_stream(
3362
3608
                        self.source.texts.keys(), 'topological', False))
3363
 
                pb.update('copying inventory', 0, 1)
 
3609
                pb.update('Copying inventory', 0, 1)
3364
3610
                self.target.inventories.insert_record_stream(
3365
3611
                    self.source.inventories.get_record_stream(
3366
3612
                        self.source.inventories.keys(), 'topological', False))
3492
3738
        # This is redundant with format.check_conversion_target(), however that
3493
3739
        # raises an exception, and we just want to say "False" as in we won't
3494
3740
        # support converting between these formats.
 
3741
        if 'IDS_never' in debug.debug_flags:
 
3742
            return False
3495
3743
        if source.supports_rich_root() and not target.supports_rich_root():
3496
3744
            return False
3497
3745
        if (source._format.supports_tree_reference
3498
3746
            and not target._format.supports_tree_reference):
3499
3747
            return False
 
3748
        if target._fallback_repositories and target._format.supports_chks:
 
3749
            # IDS doesn't know how to copy CHKs for the parent inventories it
 
3750
            # adds to stacked repos.
 
3751
            return False
 
3752
        if 'IDS_always' in debug.debug_flags:
 
3753
            return True
 
3754
        # Only use this code path for local source and target.  IDS does far
 
3755
        # too much IO (both bandwidth and roundtrips) over a network.
 
3756
        if not source.bzrdir.transport.base.startswith('file:///'):
 
3757
            return False
 
3758
        if not target.bzrdir.transport.base.startswith('file:///'):
 
3759
            return False
3500
3760
        return True
3501
3761
 
3502
 
    def _get_delta_for_revision(self, tree, parent_ids, basis_id, cache):
 
3762
    def _get_trees(self, revision_ids, cache):
 
3763
        possible_trees = []
 
3764
        for rev_id in revision_ids:
 
3765
            if rev_id in cache:
 
3766
                possible_trees.append((rev_id, cache[rev_id]))
 
3767
            else:
 
3768
                # Not cached, but inventory might be present anyway.
 
3769
                try:
 
3770
                    tree = self.source.revision_tree(rev_id)
 
3771
                except errors.NoSuchRevision:
 
3772
                    # Nope, parent is ghost.
 
3773
                    pass
 
3774
                else:
 
3775
                    cache[rev_id] = tree
 
3776
                    possible_trees.append((rev_id, tree))
 
3777
        return possible_trees
 
3778
 
 
3779
    def _get_delta_for_revision(self, tree, parent_ids, possible_trees):
3503
3780
        """Get the best delta and base for this revision.
3504
3781
 
3505
3782
        :return: (basis_id, delta)
3506
3783
        """
3507
 
        possible_trees = [(parent_id, cache[parent_id])
3508
 
                          for parent_id in parent_ids
3509
 
                           if parent_id in cache]
3510
 
        if len(possible_trees) == 0:
3511
 
            # There either aren't any parents, or the parents aren't in the
3512
 
            # cache, so just use the last converted tree
3513
 
            possible_trees.append((basis_id, cache[basis_id]))
3514
3784
        deltas = []
 
3785
        # Generate deltas against each tree, to find the shortest.
 
3786
        texts_possibly_new_in_tree = set()
3515
3787
        for basis_id, basis_tree in possible_trees:
3516
3788
            delta = tree.inventory._make_delta(basis_tree.inventory)
 
3789
            for old_path, new_path, file_id, new_entry in delta:
 
3790
                if new_path is None:
 
3791
                    # This file_id isn't present in the new rev, so we don't
 
3792
                    # care about it.
 
3793
                    continue
 
3794
                if not new_path:
 
3795
                    # Rich roots are handled elsewhere...
 
3796
                    continue
 
3797
                kind = new_entry.kind
 
3798
                if kind != 'directory' and kind != 'file':
 
3799
                    # No text record associated with this inventory entry.
 
3800
                    continue
 
3801
                # This is a directory or file that has changed somehow.
 
3802
                texts_possibly_new_in_tree.add((file_id, new_entry.revision))
3517
3803
            deltas.append((len(delta), basis_id, delta))
3518
3804
        deltas.sort()
3519
3805
        return deltas[0][1:]
3520
3806
 
3521
 
    def _get_parent_keys(self, root_key, parent_map):
3522
 
        """Get the parent keys for a given root id."""
3523
 
        root_id, rev_id = root_key
3524
 
        # Include direct parents of the revision, but only if they used
3525
 
        # the same root_id and are heads.
3526
 
        parent_keys = []
3527
 
        for parent_id in parent_map[rev_id]:
3528
 
            if parent_id == _mod_revision.NULL_REVISION:
3529
 
                continue
3530
 
            if parent_id not in self._revision_id_to_root_id:
3531
 
                # We probably didn't read this revision, go spend the
3532
 
                # extra effort to actually check
3533
 
                try:
3534
 
                    tree = self.source.revision_tree(parent_id)
3535
 
                except errors.NoSuchRevision:
3536
 
                    # Ghost, fill out _revision_id_to_root_id in case we
3537
 
                    # encounter this again.
3538
 
                    # But set parent_root_id to None since we don't really know
3539
 
                    parent_root_id = None
3540
 
                else:
3541
 
                    parent_root_id = tree.get_root_id()
3542
 
                self._revision_id_to_root_id[parent_id] = None
3543
 
            else:
3544
 
                parent_root_id = self._revision_id_to_root_id[parent_id]
3545
 
            if root_id == parent_root_id:
3546
 
                # With stacking we _might_ want to refer to a non-local
3547
 
                # revision, but this code path only applies when we have the
3548
 
                # full content available, so ghosts really are ghosts, not just
3549
 
                # the edge of local data.
3550
 
                parent_keys.append((parent_id,))
3551
 
            else:
3552
 
                # root_id may be in the parent anyway.
3553
 
                try:
3554
 
                    tree = self.source.revision_tree(parent_id)
3555
 
                except errors.NoSuchRevision:
3556
 
                    # ghost, can't refer to it.
3557
 
                    pass
3558
 
                else:
3559
 
                    try:
3560
 
                        parent_keys.append((tree.inventory[root_id].revision,))
3561
 
                    except errors.NoSuchId:
3562
 
                        # not in the tree
3563
 
                        pass
3564
 
        g = graph.Graph(self.source.revisions)
3565
 
        heads = g.heads(parent_keys)
3566
 
        selected_keys = []
3567
 
        for key in parent_keys:
3568
 
            if key in heads and key not in selected_keys:
3569
 
                selected_keys.append(key)
3570
 
        return tuple([(root_id,)+ key for key in selected_keys])
3571
 
 
3572
 
    def _new_root_data_stream(self, root_keys_to_create, parent_map):
3573
 
        for root_key in root_keys_to_create:
3574
 
            parent_keys = self._get_parent_keys(root_key, parent_map)
3575
 
            yield versionedfile.FulltextContentFactory(root_key,
3576
 
                parent_keys, None, '')
3577
 
 
3578
 
    def _fetch_batch(self, revision_ids, basis_id, cache):
 
3807
    def _fetch_parent_invs_for_stacking(self, parent_map, cache):
 
3808
        """Find all parent revisions that are absent, but for which the
 
3809
        inventory is present, and copy those inventories.
 
3810
 
 
3811
        This is necessary to preserve correctness when the source is stacked
 
3812
        without fallbacks configured.  (Note that in cases like upgrade the
 
3813
        source may be not have _fallback_repositories even though it is
 
3814
        stacked.)
 
3815
        """
 
3816
        parent_revs = set()
 
3817
        for parents in parent_map.values():
 
3818
            parent_revs.update(parents)
 
3819
        present_parents = self.source.get_parent_map(parent_revs)
 
3820
        absent_parents = set(parent_revs).difference(present_parents)
 
3821
        parent_invs_keys_for_stacking = self.source.inventories.get_parent_map(
 
3822
            (rev_id,) for rev_id in absent_parents)
 
3823
        parent_inv_ids = [key[-1] for key in parent_invs_keys_for_stacking]
 
3824
        for parent_tree in self.source.revision_trees(parent_inv_ids):
 
3825
            current_revision_id = parent_tree.get_revision_id()
 
3826
            parents_parents_keys = parent_invs_keys_for_stacking[
 
3827
                (current_revision_id,)]
 
3828
            parents_parents = [key[-1] for key in parents_parents_keys]
 
3829
            basis_id = _mod_revision.NULL_REVISION
 
3830
            basis_tree = self.source.revision_tree(basis_id)
 
3831
            delta = parent_tree.inventory._make_delta(basis_tree.inventory)
 
3832
            self.target.add_inventory_by_delta(
 
3833
                basis_id, delta, current_revision_id, parents_parents)
 
3834
            cache[current_revision_id] = parent_tree
 
3835
 
 
3836
    def _fetch_batch(self, revision_ids, basis_id, cache, a_graph=None):
3579
3837
        """Fetch across a few revisions.
3580
3838
 
3581
3839
        :param revision_ids: The revisions to copy
3582
3840
        :param basis_id: The revision_id of a tree that must be in cache, used
3583
3841
            as a basis for delta when no other base is available
3584
3842
        :param cache: A cache of RevisionTrees that we can use.
 
3843
        :param a_graph: A Graph object to determine the heads() of the
 
3844
            rich-root data stream.
3585
3845
        :return: The revision_id of the last converted tree. The RevisionTree
3586
3846
            for it will be in cache
3587
3847
        """
3593
3853
        pending_deltas = []
3594
3854
        pending_revisions = []
3595
3855
        parent_map = self.source.get_parent_map(revision_ids)
 
3856
        self._fetch_parent_invs_for_stacking(parent_map, cache)
 
3857
        self.source._safe_to_return_from_cache = True
3596
3858
        for tree in self.source.revision_trees(revision_ids):
 
3859
            # Find a inventory delta for this revision.
 
3860
            # Find text entries that need to be copied, too.
3597
3861
            current_revision_id = tree.get_revision_id()
3598
3862
            parent_ids = parent_map.get(current_revision_id, ())
 
3863
            parent_trees = self._get_trees(parent_ids, cache)
 
3864
            possible_trees = list(parent_trees)
 
3865
            if len(possible_trees) == 0:
 
3866
                # There either aren't any parents, or the parents are ghosts,
 
3867
                # so just use the last converted tree.
 
3868
                possible_trees.append((basis_id, cache[basis_id]))
3599
3869
            basis_id, delta = self._get_delta_for_revision(tree, parent_ids,
3600
 
                                                           basis_id, cache)
 
3870
                                                           possible_trees)
 
3871
            revision = self.source.get_revision(current_revision_id)
 
3872
            pending_deltas.append((basis_id, delta,
 
3873
                current_revision_id, revision.parent_ids))
3601
3874
            if self._converting_to_rich_root:
3602
3875
                self._revision_id_to_root_id[current_revision_id] = \
3603
3876
                    tree.get_root_id()
3604
 
            # Find text entries that need to be copied
 
3877
            # Determine which texts are in present in this revision but not in
 
3878
            # any of the available parents.
 
3879
            texts_possibly_new_in_tree = set()
3605
3880
            for old_path, new_path, file_id, entry in delta:
3606
 
                if new_path is not None:
3607
 
                    if not new_path:
3608
 
                        # This is the root
3609
 
                        if not self.target.supports_rich_root():
3610
 
                            # The target doesn't support rich root, so we don't
3611
 
                            # copy
3612
 
                            continue
3613
 
                        if self._converting_to_rich_root:
3614
 
                            # This can't be copied normally, we have to insert
3615
 
                            # it specially
3616
 
                            root_keys_to_create.add((file_id, entry.revision))
3617
 
                            continue
3618
 
                    text_keys.add((file_id, entry.revision))
3619
 
            revision = self.source.get_revision(current_revision_id)
3620
 
            pending_deltas.append((basis_id, delta,
3621
 
                current_revision_id, revision.parent_ids))
 
3881
                if new_path is None:
 
3882
                    # This file_id isn't present in the new rev
 
3883
                    continue
 
3884
                if not new_path:
 
3885
                    # This is the root
 
3886
                    if not self.target.supports_rich_root():
 
3887
                        # The target doesn't support rich root, so we don't
 
3888
                        # copy
 
3889
                        continue
 
3890
                    if self._converting_to_rich_root:
 
3891
                        # This can't be copied normally, we have to insert
 
3892
                        # it specially
 
3893
                        root_keys_to_create.add((file_id, entry.revision))
 
3894
                        continue
 
3895
                kind = entry.kind
 
3896
                texts_possibly_new_in_tree.add((file_id, entry.revision))
 
3897
            for basis_id, basis_tree in possible_trees:
 
3898
                basis_inv = basis_tree.inventory
 
3899
                for file_key in list(texts_possibly_new_in_tree):
 
3900
                    file_id, file_revision = file_key
 
3901
                    try:
 
3902
                        entry = basis_inv[file_id]
 
3903
                    except errors.NoSuchId:
 
3904
                        continue
 
3905
                    if entry.revision == file_revision:
 
3906
                        texts_possibly_new_in_tree.remove(file_key)
 
3907
            text_keys.update(texts_possibly_new_in_tree)
3622
3908
            pending_revisions.append(revision)
3623
3909
            cache[current_revision_id] = tree
3624
3910
            basis_id = current_revision_id
 
3911
        self.source._safe_to_return_from_cache = False
3625
3912
        # Copy file texts
3626
3913
        from_texts = self.source.texts
3627
3914
        to_texts = self.target.texts
3628
3915
        if root_keys_to_create:
3629
 
            root_stream = self._new_root_data_stream(root_keys_to_create,
3630
 
                                                     parent_map)
 
3916
            root_stream = _mod_fetch._new_root_data_stream(
 
3917
                root_keys_to_create, self._revision_id_to_root_id, parent_map,
 
3918
                self.source, graph=a_graph)
3631
3919
            to_texts.insert_record_stream(root_stream)
3632
3920
        to_texts.insert_record_stream(from_texts.get_record_stream(
3633
3921
            text_keys, self.target._format._fetch_order,
3640
3928
            # for the new revisions that we are about to insert.  We do this
3641
3929
            # before adding the revisions so that no revision is added until
3642
3930
            # all the inventories it may depend on are added.
 
3931
            # Note that this is overzealous, as we may have fetched these in an
 
3932
            # earlier batch.
3643
3933
            parent_ids = set()
3644
3934
            revision_ids = set()
3645
3935
            for revision in pending_revisions:
3648
3938
            parent_ids.difference_update(revision_ids)
3649
3939
            parent_ids.discard(_mod_revision.NULL_REVISION)
3650
3940
            parent_map = self.source.get_parent_map(parent_ids)
3651
 
            for parent_tree in self.source.revision_trees(parent_ids):
3652
 
                basis_id, delta = self._get_delta_for_revision(tree, parent_ids, basis_id, cache)
 
3941
            # we iterate over parent_map and not parent_ids because we don't
 
3942
            # want to try copying any revision which is a ghost
 
3943
            for parent_tree in self.source.revision_trees(parent_map):
3653
3944
                current_revision_id = parent_tree.get_revision_id()
3654
3945
                parents_parents = parent_map[current_revision_id]
 
3946
                possible_trees = self._get_trees(parents_parents, cache)
 
3947
                if len(possible_trees) == 0:
 
3948
                    # There either aren't any parents, or the parents are
 
3949
                    # ghosts, so just use the last converted tree.
 
3950
                    possible_trees.append((basis_id, cache[basis_id]))
 
3951
                basis_id, delta = self._get_delta_for_revision(parent_tree,
 
3952
                    parents_parents, possible_trees)
3655
3953
                self.target.add_inventory_by_delta(
3656
3954
                    basis_id, delta, current_revision_id, parents_parents)
3657
3955
        # insert signatures and revisions
3671
3969
 
3672
3970
        :param revision_ids: The list of revisions to fetch. Must be in
3673
3971
            topological order.
3674
 
        :param pb: A ProgressBar
 
3972
        :param pb: A ProgressTask
3675
3973
        :return: None
3676
3974
        """
3677
3975
        basis_id, basis_tree = self._get_basis(revision_ids[0])
3680
3978
        cache[basis_id] = basis_tree
3681
3979
        del basis_tree # We don't want to hang on to it here
3682
3980
        hints = []
 
3981
        if self._converting_to_rich_root and len(revision_ids) > 100:
 
3982
            a_graph = _mod_fetch._get_rich_root_heads_graph(self.source,
 
3983
                                                            revision_ids)
 
3984
        else:
 
3985
            a_graph = None
 
3986
 
3683
3987
        for offset in range(0, len(revision_ids), batch_size):
3684
3988
            self.target.start_write_group()
3685
3989
            try:
3686
3990
                pb.update('Transferring revisions', offset,
3687
3991
                          len(revision_ids))
3688
3992
                batch = revision_ids[offset:offset+batch_size]
3689
 
                basis_id = self._fetch_batch(batch, basis_id, cache)
 
3993
                basis_id = self._fetch_batch(batch, basis_id, cache,
 
3994
                                             a_graph=a_graph)
3690
3995
            except:
 
3996
                self.source._safe_to_return_from_cache = False
3691
3997
                self.target.abort_write_group()
3692
3998
                raise
3693
3999
            else:
3705
4011
        """See InterRepository.fetch()."""
3706
4012
        if fetch_spec is not None:
3707
4013
            raise AssertionError("Not implemented yet...")
 
4014
        ui.ui_factory.warn_experimental_format_fetch(self)
3708
4015
        if (not self.source.supports_rich_root()
3709
4016
            and self.target.supports_rich_root()):
3710
4017
            self._converting_to_rich_root = True
3711
4018
            self._revision_id_to_root_id = {}
3712
4019
        else:
3713
4020
            self._converting_to_rich_root = False
 
4021
        # See <https://launchpad.net/bugs/456077> asking for a warning here
 
4022
        if self.source._format.network_name() != self.target._format.network_name():
 
4023
            ui.ui_factory.show_user_warning('cross_format_fetch',
 
4024
                from_format=self.source._format,
 
4025
                to_format=self.target._format)
3714
4026
        revision_ids = self.target.search_missing_revision_ids(self.source,
3715
4027
            revision_id, find_ghosts=find_ghosts).get_keys()
3716
4028
        if not revision_ids:
3722
4034
        # Walk though all revisions; get inventory deltas, copy referenced
3723
4035
        # texts that delta references, insert the delta, revision and
3724
4036
        # signature.
3725
 
        first_rev = self.source.get_revision(revision_ids[0])
3726
4037
        if pb is None:
3727
4038
            my_pb = ui.ui_factory.nested_progress_bar()
3728
4039
            pb = my_pb
3786
4097
        :param to_convert: The disk object to convert.
3787
4098
        :param pb: a progress bar to use for progress information.
3788
4099
        """
3789
 
        self.pb = pb
 
4100
        pb = ui.ui_factory.nested_progress_bar()
3790
4101
        self.count = 0
3791
4102
        self.total = 4
3792
4103
        # this is only useful with metadir layouts - separated repo content.
3793
4104
        # trigger an assertion if not such
3794
4105
        repo._format.get_format_string()
3795
4106
        self.repo_dir = repo.bzrdir
3796
 
        self.step('Moving repository to repository.backup')
 
4107
        pb.update('Moving repository to repository.backup')
3797
4108
        self.repo_dir.transport.move('repository', 'repository.backup')
3798
4109
        backup_transport =  self.repo_dir.transport.clone('repository.backup')
3799
4110
        repo._format.check_conversion_target(self.target_format)
3800
4111
        self.source_repo = repo._format.open(self.repo_dir,
3801
4112
            _found=True,
3802
4113
            _override_transport=backup_transport)
3803
 
        self.step('Creating new repository')
 
4114
        pb.update('Creating new repository')
3804
4115
        converted = self.target_format.initialize(self.repo_dir,
3805
4116
                                                  self.source_repo.is_shared())
3806
4117
        converted.lock_write()
3807
4118
        try:
3808
 
            self.step('Copying content into repository.')
 
4119
            pb.update('Copying content')
3809
4120
            self.source_repo.copy_content_into(converted)
3810
4121
        finally:
3811
4122
            converted.unlock()
3812
 
        self.step('Deleting old repository content.')
 
4123
        pb.update('Deleting old repository content')
3813
4124
        self.repo_dir.transport.delete_tree('repository.backup')
3814
 
        self.pb.note('repository converted')
3815
 
 
3816
 
    def step(self, message):
3817
 
        """Update the pb by a step."""
3818
 
        self.count +=1
3819
 
        self.pb.update(message, self.count, self.total)
 
4125
        ui.ui_factory.note('repository converted')
 
4126
        pb.finished()
3820
4127
 
3821
4128
 
3822
4129
_unescape_map = {
3851
4158
 
3852
4159
class _VersionedFileChecker(object):
3853
4160
 
3854
 
    def __init__(self, repository, text_key_references=None):
 
4161
    def __init__(self, repository, text_key_references=None, ancestors=None):
3855
4162
        self.repository = repository
3856
4163
        self.text_index = self.repository._generate_text_key_index(
3857
 
            text_key_references=text_key_references)
 
4164
            text_key_references=text_key_references, ancestors=ancestors)
3858
4165
 
3859
4166
    def calculate_file_version_parents(self, text_key):
3860
4167
        """Calculate the correct parents for a file version according to
3878
4185
            revision_id) tuples for versions that are present in this versioned
3879
4186
            file, but not used by the corresponding inventory.
3880
4187
        """
 
4188
        local_progress = None
 
4189
        if progress_bar is None:
 
4190
            local_progress = ui.ui_factory.nested_progress_bar()
 
4191
            progress_bar = local_progress
 
4192
        try:
 
4193
            return self._check_file_version_parents(texts, progress_bar)
 
4194
        finally:
 
4195
            if local_progress:
 
4196
                local_progress.finished()
 
4197
 
 
4198
    def _check_file_version_parents(self, texts, progress_bar):
 
4199
        """See check_file_version_parents."""
3881
4200
        wrong_parents = {}
3882
4201
        self.file_ids = set([file_id for file_id, _ in
3883
4202
            self.text_index.iterkeys()])
3884
4203
        # text keys is now grouped by file_id
3885
 
        n_weaves = len(self.file_ids)
3886
 
        files_in_revisions = {}
3887
 
        revisions_of_files = {}
3888
4204
        n_versions = len(self.text_index)
3889
4205
        progress_bar.update('loading text store', 0, n_versions)
3890
4206
        parent_map = self.repository.texts.get_parent_map(self.text_index)
3892
4208
        text_keys = self.repository.texts.keys()
3893
4209
        unused_keys = frozenset(text_keys) - set(self.text_index)
3894
4210
        for num, key in enumerate(self.text_index.iterkeys()):
3895
 
            if progress_bar is not None:
3896
 
                progress_bar.update('checking text graph', num, n_versions)
 
4211
            progress_bar.update('checking text graph', num, n_versions)
3897
4212
            correct_parents = self.calculate_file_version_parents(key)
3898
4213
            try:
3899
4214
                knit_parents = parent_map[key]
3984
4299
            else:
3985
4300
                new_pack.set_write_cache_size(1024*1024)
3986
4301
        for substream_type, substream in stream:
 
4302
            if 'stream' in debug.debug_flags:
 
4303
                mutter('inserting substream: %s', substream_type)
3987
4304
            if substream_type == 'texts':
3988
4305
                self.target_repo.texts.insert_record_stream(substream)
3989
4306
            elif substream_type == 'inventories':
3993
4310
                else:
3994
4311
                    self._extract_and_insert_inventories(
3995
4312
                        substream, src_serializer)
 
4313
            elif substream_type == 'inventory-deltas':
 
4314
                self._extract_and_insert_inventory_deltas(
 
4315
                    substream, src_serializer)
3996
4316
            elif substream_type == 'chk_bytes':
3997
4317
                # XXX: This doesn't support conversions, as it assumes the
3998
4318
                #      conversion was done in the fetch code.
4029
4349
                ):
4030
4350
                if versioned_file is None:
4031
4351
                    continue
 
4352
                # TODO: key is often going to be a StaticTuple object
 
4353
                #       I don't believe we can define a method by which
 
4354
                #       (prefix,) + StaticTuple will work, though we could
 
4355
                #       define a StaticTuple.sq_concat that would allow you to
 
4356
                #       pass in either a tuple or a StaticTuple as the second
 
4357
                #       object, so instead we could have:
 
4358
                #       StaticTuple(prefix) + key here...
4032
4359
                missing_keys.update((prefix,) + key for key in
4033
4360
                    versioned_file.get_missing_compression_parent_keys())
4034
4361
        except NotImplementedError:
4049
4376
            self.target_repo.pack(hint=hint)
4050
4377
        return [], set()
4051
4378
 
4052
 
    def _extract_and_insert_inventories(self, substream, serializer):
 
4379
    def _extract_and_insert_inventory_deltas(self, substream, serializer):
 
4380
        target_rich_root = self.target_repo._format.rich_root_data
 
4381
        target_tree_refs = self.target_repo._format.supports_tree_reference
 
4382
        for record in substream:
 
4383
            # Insert the delta directly
 
4384
            inventory_delta_bytes = record.get_bytes_as('fulltext')
 
4385
            deserialiser = inventory_delta.InventoryDeltaDeserializer()
 
4386
            try:
 
4387
                parse_result = deserialiser.parse_text_bytes(
 
4388
                    inventory_delta_bytes)
 
4389
            except inventory_delta.IncompatibleInventoryDelta, err:
 
4390
                trace.mutter("Incompatible delta: %s", err.msg)
 
4391
                raise errors.IncompatibleRevision(self.target_repo._format)
 
4392
            basis_id, new_id, rich_root, tree_refs, inv_delta = parse_result
 
4393
            revision_id = new_id
 
4394
            parents = [key[0] for key in record.parents]
 
4395
            self.target_repo.add_inventory_by_delta(
 
4396
                basis_id, inv_delta, revision_id, parents)
 
4397
 
 
4398
    def _extract_and_insert_inventories(self, substream, serializer,
 
4399
            parse_delta=None):
4053
4400
        """Generate a new inventory versionedfile in target, converting data.
4054
4401
 
4055
4402
        The inventory is retrieved from the source, (deserializing it), and
4056
4403
        stored in the target (reserializing it in a different format).
4057
4404
        """
 
4405
        target_rich_root = self.target_repo._format.rich_root_data
 
4406
        target_tree_refs = self.target_repo._format.supports_tree_reference
4058
4407
        for record in substream:
 
4408
            # It's not a delta, so it must be a fulltext in the source
 
4409
            # serializer's format.
4059
4410
            bytes = record.get_bytes_as('fulltext')
4060
4411
            revision_id = record.key[0]
4061
4412
            inv = serializer.read_inventory_from_string(bytes, revision_id)
4062
4413
            parents = [key[0] for key in record.parents]
4063
4414
            self.target_repo.add_inventory(revision_id, inv, parents)
 
4415
            # No need to keep holding this full inv in memory when the rest of
 
4416
            # the substream is likely to be all deltas.
 
4417
            del inv
4064
4418
 
4065
4419
    def _extract_and_insert_revisions(self, substream, serializer):
4066
4420
        for record in substream:
4115
4469
        return [('signatures', signatures), ('revisions', revisions)]
4116
4470
 
4117
4471
    def _generate_root_texts(self, revs):
4118
 
        """This will be called by __fetch between fetching weave texts and
 
4472
        """This will be called by get_stream between fetching weave texts and
4119
4473
        fetching the inventory weave.
4120
 
 
4121
 
        Subclasses should override this if they need to generate root texts
4122
 
        after fetching weave texts.
4123
4474
        """
4124
4475
        if self._rich_root_upgrade():
4125
 
            import bzrlib.fetch
4126
 
            return bzrlib.fetch.Inter1and2Helper(
 
4476
            return _mod_fetch.Inter1and2Helper(
4127
4477
                self.from_repository).generate_root_texts(revs)
4128
4478
        else:
4129
4479
            return []
4132
4482
        phase = 'file'
4133
4483
        revs = search.get_keys()
4134
4484
        graph = self.from_repository.get_graph()
4135
 
        revs = list(graph.iter_topo_order(revs))
 
4485
        revs = tsort.topo_sort(graph.get_parent_map(revs))
4136
4486
        data_to_fetch = self.from_repository.item_keys_introduced_by(revs)
4137
4487
        text_keys = []
4138
4488
        for knit_kind, file_id, revisions in data_to_fetch:
4157
4507
                # will be valid.
4158
4508
                for _ in self._generate_root_texts(revs):
4159
4509
                    yield _
4160
 
                # NB: This currently reopens the inventory weave in source;
4161
 
                # using a single stream interface instead would avoid this.
4162
 
                from_weave = self.from_repository.inventories
4163
4510
                # we fetch only the referenced inventories because we do not
4164
4511
                # know for unselected inventories whether all their required
4165
4512
                # texts are present in the other repository - it could be
4204
4551
            if not keys:
4205
4552
                # No need to stream something we don't have
4206
4553
                continue
 
4554
            if substream_kind == 'inventories':
 
4555
                # Some missing keys are genuinely ghosts, filter those out.
 
4556
                present = self.from_repository.inventories.get_parent_map(keys)
 
4557
                revs = [key[0] for key in present]
 
4558
                # Get the inventory stream more-or-less as we do for the
 
4559
                # original stream; there's no reason to assume that records
 
4560
                # direct from the source will be suitable for the sink.  (Think
 
4561
                # e.g. 2a -> 1.9-rich-root).
 
4562
                for info in self._get_inventory_stream(revs, missing=True):
 
4563
                    yield info
 
4564
                continue
 
4565
 
4207
4566
            # Ask for full texts always so that we don't need more round trips
4208
4567
            # after this stream.
4209
4568
            # Some of the missing keys are genuinely ghosts, so filter absent
4224
4583
        return (not self.from_repository._format.rich_root_data and
4225
4584
            self.to_format.rich_root_data)
4226
4585
 
4227
 
    def _get_inventory_stream(self, revision_ids):
 
4586
    def _get_inventory_stream(self, revision_ids, missing=False):
4228
4587
        from_format = self.from_repository._format
4229
 
        if (from_format.supports_chks and self.to_format.supports_chks
4230
 
            and (from_format._serializer == self.to_format._serializer)):
4231
 
            # Both sides support chks, and they use the same serializer, so it
4232
 
            # is safe to transmit the chk pages and inventory pages across
4233
 
            # as-is.
4234
 
            return self._get_chk_inventory_stream(revision_ids)
4235
 
        elif (not from_format.supports_chks):
4236
 
            # Source repository doesn't support chks. So we can transmit the
4237
 
            # inventories 'as-is' and either they are just accepted on the
4238
 
            # target, or the Sink will properly convert it.
4239
 
            return self._get_simple_inventory_stream(revision_ids)
 
4588
        if (from_format.supports_chks and self.to_format.supports_chks and
 
4589
            from_format.network_name() == self.to_format.network_name()):
 
4590
            raise AssertionError(
 
4591
                "this case should be handled by GroupCHKStreamSource")
 
4592
        elif 'forceinvdeltas' in debug.debug_flags:
 
4593
            return self._get_convertable_inventory_stream(revision_ids,
 
4594
                    delta_versus_null=missing)
 
4595
        elif from_format.network_name() == self.to_format.network_name():
 
4596
            # Same format.
 
4597
            return self._get_simple_inventory_stream(revision_ids,
 
4598
                    missing=missing)
 
4599
        elif (not from_format.supports_chks and not self.to_format.supports_chks
 
4600
                and from_format._serializer == self.to_format._serializer):
 
4601
            # Essentially the same format.
 
4602
            return self._get_simple_inventory_stream(revision_ids,
 
4603
                    missing=missing)
4240
4604
        else:
4241
 
            # XXX: Hack to make not-chk->chk fetch: copy the inventories as
4242
 
            #      inventories. Note that this should probably be done somehow
4243
 
            #      as part of bzrlib.repository.StreamSink. Except JAM couldn't
4244
 
            #      figure out how a non-chk repository could possibly handle
4245
 
            #      deserializing an inventory stream from a chk repo, as it
4246
 
            #      doesn't have a way to understand individual pages.
4247
 
            return self._get_convertable_inventory_stream(revision_ids)
 
4605
            # Any time we switch serializations, we want to use an
 
4606
            # inventory-delta based approach.
 
4607
            return self._get_convertable_inventory_stream(revision_ids,
 
4608
                    delta_versus_null=missing)
4248
4609
 
4249
 
    def _get_simple_inventory_stream(self, revision_ids):
 
4610
    def _get_simple_inventory_stream(self, revision_ids, missing=False):
 
4611
        # NB: This currently reopens the inventory weave in source;
 
4612
        # using a single stream interface instead would avoid this.
4250
4613
        from_weave = self.from_repository.inventories
 
4614
        if missing:
 
4615
            delta_closure = True
 
4616
        else:
 
4617
            delta_closure = not self.delta_on_metadata()
4251
4618
        yield ('inventories', from_weave.get_record_stream(
4252
4619
            [(rev_id,) for rev_id in revision_ids],
4253
 
            self.inventory_fetch_order(),
4254
 
            not self.delta_on_metadata()))
4255
 
 
4256
 
    def _get_chk_inventory_stream(self, revision_ids):
4257
 
        """Fetch the inventory texts, along with the associated chk maps."""
4258
 
        # We want an inventory outside of the search set, so that we can filter
4259
 
        # out uninteresting chk pages. For now we use
4260
 
        # _find_revision_outside_set, but if we had a Search with cut_revs, we
4261
 
        # could use that instead.
4262
 
        start_rev_id = self.from_repository._find_revision_outside_set(
4263
 
                            revision_ids)
4264
 
        start_rev_key = (start_rev_id,)
4265
 
        inv_keys_to_fetch = [(rev_id,) for rev_id in revision_ids]
4266
 
        if start_rev_id != _mod_revision.NULL_REVISION:
4267
 
            inv_keys_to_fetch.append((start_rev_id,))
4268
 
        # Any repo that supports chk_bytes must also support out-of-order
4269
 
        # insertion. At least, that is how we expect it to work
4270
 
        # We use get_record_stream instead of iter_inventories because we want
4271
 
        # to be able to insert the stream as well. We could instead fetch
4272
 
        # allowing deltas, and then iter_inventories, but we don't know whether
4273
 
        # source or target is more 'local' anway.
4274
 
        inv_stream = self.from_repository.inventories.get_record_stream(
4275
 
            inv_keys_to_fetch, 'unordered',
4276
 
            True) # We need them as full-texts so we can find their references
4277
 
        uninteresting_chk_roots = set()
4278
 
        interesting_chk_roots = set()
4279
 
        def filter_inv_stream(inv_stream):
4280
 
            for idx, record in enumerate(inv_stream):
4281
 
                ### child_pb.update('fetch inv', idx, len(inv_keys_to_fetch))
4282
 
                bytes = record.get_bytes_as('fulltext')
4283
 
                chk_inv = inventory.CHKInventory.deserialise(
4284
 
                    self.from_repository.chk_bytes, bytes, record.key)
4285
 
                if record.key == start_rev_key:
4286
 
                    uninteresting_chk_roots.add(chk_inv.id_to_entry.key())
4287
 
                    p_id_map = chk_inv.parent_id_basename_to_file_id
4288
 
                    if p_id_map is not None:
4289
 
                        uninteresting_chk_roots.add(p_id_map.key())
4290
 
                else:
4291
 
                    yield record
4292
 
                    interesting_chk_roots.add(chk_inv.id_to_entry.key())
4293
 
                    p_id_map = chk_inv.parent_id_basename_to_file_id
4294
 
                    if p_id_map is not None:
4295
 
                        interesting_chk_roots.add(p_id_map.key())
4296
 
        ### pb.update('fetch inventory', 0, 2)
4297
 
        yield ('inventories', filter_inv_stream(inv_stream))
4298
 
        # Now that we have worked out all of the interesting root nodes, grab
4299
 
        # all of the interesting pages and insert them
4300
 
        ### pb.update('fetch inventory', 1, 2)
4301
 
        interesting = chk_map.iter_interesting_nodes(
4302
 
            self.from_repository.chk_bytes, interesting_chk_roots,
4303
 
            uninteresting_chk_roots)
4304
 
        def to_stream_adapter():
4305
 
            """Adapt the iter_interesting_nodes result to a single stream.
4306
 
 
4307
 
            iter_interesting_nodes returns records as it processes them, along
4308
 
            with keys. However, we only want to return the records themselves.
4309
 
            """
4310
 
            for record, items in interesting:
4311
 
                if record is not None:
4312
 
                    yield record
4313
 
        # XXX: We could instead call get_record_stream(records.keys())
4314
 
        #      ATM, this will always insert the records as fulltexts, and
4315
 
        #      requires that you can hang on to records once you have gone
4316
 
        #      on to the next one. Further, it causes the target to
4317
 
        #      recompress the data. Testing shows it to be faster than
4318
 
        #      requesting the records again, though.
4319
 
        yield ('chk_bytes', to_stream_adapter())
4320
 
        ### pb.update('fetch inventory', 2, 2)
4321
 
 
4322
 
    def _get_convertable_inventory_stream(self, revision_ids):
4323
 
        # XXX: One of source or target is using chks, and they don't have
4324
 
        #      compatible serializations. The StreamSink code expects to be
4325
 
        #      able to convert on the target, so we need to put
4326
 
        #      bytes-on-the-wire that can be converted
4327
 
        yield ('inventories', self._stream_invs_as_fulltexts(revision_ids))
4328
 
 
4329
 
    def _stream_invs_as_fulltexts(self, revision_ids):
 
4620
            self.inventory_fetch_order(), delta_closure))
 
4621
 
 
4622
    def _get_convertable_inventory_stream(self, revision_ids,
 
4623
                                          delta_versus_null=False):
 
4624
        # The two formats are sufficiently different that there is no fast
 
4625
        # path, so we need to send just inventorydeltas, which any
 
4626
        # sufficiently modern client can insert into any repository.
 
4627
        # The StreamSink code expects to be able to
 
4628
        # convert on the target, so we need to put bytes-on-the-wire that can
 
4629
        # be converted.  That means inventory deltas (if the remote is <1.19,
 
4630
        # RemoteStreamSink will fallback to VFS to insert the deltas).
 
4631
        yield ('inventory-deltas',
 
4632
           self._stream_invs_as_deltas(revision_ids,
 
4633
                                       delta_versus_null=delta_versus_null))
 
4634
 
 
4635
    def _stream_invs_as_deltas(self, revision_ids, delta_versus_null=False):
 
4636
        """Return a stream of inventory-deltas for the given rev ids.
 
4637
 
 
4638
        :param revision_ids: The list of inventories to transmit
 
4639
        :param delta_versus_null: Don't try to find a minimal delta for this
 
4640
            entry, instead compute the delta versus the NULL_REVISION. This
 
4641
            effectively streams a complete inventory. Used for stuff like
 
4642
            filling in missing parents, etc.
 
4643
        """
4330
4644
        from_repo = self.from_repository
4331
 
        from_serializer = from_repo._format._serializer
4332
4645
        revision_keys = [(rev_id,) for rev_id in revision_ids]
4333
4646
        parent_map = from_repo.inventories.get_parent_map(revision_keys)
4334
 
        for inv in self.from_repository.iter_inventories(revision_ids):
4335
 
            # XXX: This is a bit hackish, but it works. Basically,
4336
 
            #      CHKSerializer 'accidentally' supports
4337
 
            #      read/write_inventory_to_string, even though that is never
4338
 
            #      the format that is stored on disk. It *does* give us a
4339
 
            #      single string representation for an inventory, so live with
4340
 
            #      it for now.
4341
 
            #      This would be far better if we had a 'serialized inventory
4342
 
            #      delta' form. Then we could use 'inventory._make_delta', and
4343
 
            #      transmit that. This would both be faster to generate, and
4344
 
            #      result in fewer bytes-on-the-wire.
4345
 
            as_bytes = from_serializer.write_inventory_to_string(inv)
 
4647
        # XXX: possibly repos could implement a more efficient iter_inv_deltas
 
4648
        # method...
 
4649
        inventories = self.from_repository.iter_inventories(
 
4650
            revision_ids, 'topological')
 
4651
        format = from_repo._format
 
4652
        invs_sent_so_far = set([_mod_revision.NULL_REVISION])
 
4653
        inventory_cache = lru_cache.LRUCache(50)
 
4654
        null_inventory = from_repo.revision_tree(
 
4655
            _mod_revision.NULL_REVISION).inventory
 
4656
        # XXX: ideally the rich-root/tree-refs flags would be per-revision, not
 
4657
        # per-repo (e.g.  streaming a non-rich-root revision out of a rich-root
 
4658
        # repo back into a non-rich-root repo ought to be allowed)
 
4659
        serializer = inventory_delta.InventoryDeltaSerializer(
 
4660
            versioned_root=format.rich_root_data,
 
4661
            tree_references=format.supports_tree_reference)
 
4662
        for inv in inventories:
4346
4663
            key = (inv.revision_id,)
4347
4664
            parent_keys = parent_map.get(key, ())
 
4665
            delta = None
 
4666
            if not delta_versus_null and parent_keys:
 
4667
                # The caller did not ask for complete inventories and we have
 
4668
                # some parents that we can delta against.  Make a delta against
 
4669
                # each parent so that we can find the smallest.
 
4670
                parent_ids = [parent_key[0] for parent_key in parent_keys]
 
4671
                for parent_id in parent_ids:
 
4672
                    if parent_id not in invs_sent_so_far:
 
4673
                        # We don't know that the remote side has this basis, so
 
4674
                        # we can't use it.
 
4675
                        continue
 
4676
                    if parent_id == _mod_revision.NULL_REVISION:
 
4677
                        parent_inv = null_inventory
 
4678
                    else:
 
4679
                        parent_inv = inventory_cache.get(parent_id, None)
 
4680
                        if parent_inv is None:
 
4681
                            parent_inv = from_repo.get_inventory(parent_id)
 
4682
                    candidate_delta = inv._make_delta(parent_inv)
 
4683
                    if (delta is None or
 
4684
                        len(delta) > len(candidate_delta)):
 
4685
                        delta = candidate_delta
 
4686
                        basis_id = parent_id
 
4687
            if delta is None:
 
4688
                # Either none of the parents ended up being suitable, or we
 
4689
                # were asked to delta against NULL
 
4690
                basis_id = _mod_revision.NULL_REVISION
 
4691
                delta = inv._make_delta(null_inventory)
 
4692
            invs_sent_so_far.add(inv.revision_id)
 
4693
            inventory_cache[inv.revision_id] = inv
 
4694
            delta_serialized = ''.join(
 
4695
                serializer.delta_to_lines(basis_id, key[-1], delta))
4348
4696
            yield versionedfile.FulltextContentFactory(
4349
 
                key, parent_keys, None, as_bytes)
 
4697
                key, parent_keys, None, delta_serialized)
4350
4698
 
4351
4699
 
4352
4700
def _iter_for_revno(repo, partial_history_cache, stop_index=None,