~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/repository.py

  • Committer: Jelmer Vernooij
  • Date: 2010-12-20 11:57:14 UTC
  • mto: This revision was merged to the branch mainline in revision 5577.
  • Revision ID: jelmer@samba.org-20101220115714-2ru3hfappjweeg7q
Don't use no-plugins.

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,
 
28
    controldir,
27
29
    debug,
28
 
    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,
 
42
    pyutils,
39
43
    revision as _mod_revision,
 
44
    static_tuple,
40
45
    symbol_versioning,
 
46
    trace,
41
47
    tsort,
42
 
    ui,
43
48
    versionedfile,
44
49
    )
45
50
from bzrlib.bundle import serializer
48
53
from bzrlib.testament import Testament
49
54
""")
50
55
 
51
 
from bzrlib.decorators import needs_read_lock, needs_write_lock
 
56
import sys
 
57
from bzrlib import (
 
58
    errors,
 
59
    registry,
 
60
    ui,
 
61
    )
 
62
from bzrlib.decorators import needs_read_lock, needs_write_lock, only_raises
52
63
from bzrlib.inter import InterObject
53
64
from bzrlib.inventory import (
54
65
    Inventory,
56
67
    ROOT_ID,
57
68
    entry_factory,
58
69
    )
59
 
from bzrlib import registry
 
70
from bzrlib.recordcounter import RecordCounter
 
71
from bzrlib.lock import _RelockDebugMixin, LogicalLockResult
60
72
from bzrlib.trace import (
61
73
    log_exception_quietly, note, mutter, mutter_callsite, warning)
62
74
 
65
77
_deprecation_warning_done = False
66
78
 
67
79
 
 
80
class IsInWriteGroupError(errors.InternalBzrError):
 
81
 
 
82
    _fmt = "May not refresh_data of repo %(repo)s while in a write group."
 
83
 
 
84
    def __init__(self, repo):
 
85
        errors.InternalBzrError.__init__(self, repo=repo)
 
86
 
 
87
 
68
88
class CommitBuilder(object):
69
89
    """Provides an interface to build up a commit.
70
90
 
95
115
 
96
116
        if committer is None:
97
117
            self._committer = self._config.username()
 
118
        elif not isinstance(committer, unicode):
 
119
            self._committer = committer.decode() # throw if non-ascii
98
120
        else:
99
121
            self._committer = committer
100
122
 
204
226
            # an inventory delta was accumulated without creating a new
205
227
            # inventory.
206
228
            basis_id = self.basis_delta_revision
207
 
            self.inv_sha1 = self.repository.add_inventory_by_delta(
 
229
            # We ignore the 'inventory' returned by add_inventory_by_delta
 
230
            # because self.new_inventory is used to hint to the rest of the
 
231
            # system what code path was taken
 
232
            self.inv_sha1, _ = self.repository.add_inventory_by_delta(
208
233
                basis_id, self._basis_delta, self._new_revision_id,
209
234
                self.parents)
210
235
        else:
222
247
 
223
248
    def _gen_revision_id(self):
224
249
        """Return new revision-id."""
225
 
        return generate_ids.gen_revision_id(self._config.username(),
226
 
                                            self._timestamp)
 
250
        return generate_ids.gen_revision_id(self._committer, self._timestamp)
227
251
 
228
252
    def _generate_revision_if_needed(self):
229
253
        """Create a revision id if None was supplied.
269
293
 
270
294
        :param tree: The tree which is being committed.
271
295
        """
272
 
        # NB: if there are no parents then this method is not called, so no
273
 
        # need to guard on parents having length.
 
296
        if len(self.parents) == 0:
 
297
            raise errors.RootMissing()
274
298
        entry = entry_factory['directory'](tree.path2id(''), '',
275
299
            None)
276
300
        entry.revision = self._new_revision_id
414
438
            else:
415
439
                # we don't need to commit this, because the caller already
416
440
                # determined that an existing revision of this file is
417
 
                # appropriate. If its not being considered for committing then
 
441
                # appropriate. If it's not being considered for committing then
418
442
                # it and all its parents to the root must be unaltered so
419
443
                # no-change against the basis.
420
444
                if ie.revision == self._new_revision_id:
464
488
            if content_summary[2] is None:
465
489
                raise ValueError("Files must not have executable = None")
466
490
            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:
 
491
                # We can't trust a check of the file length because of content
 
492
                # filtering...
 
493
                if (# if the exec bit has changed we have to store:
470
494
                    parent_entry.executable != content_summary[2]):
471
495
                    store = True
472
496
                elif parent_entry.text_sha1 == content_summary[3]:
539
563
                ie.revision = parent_entry.revision
540
564
                return self._get_delta(ie, basis_inv, path), False, None
541
565
            ie.reference_revision = content_summary[3]
 
566
            if ie.reference_revision is None:
 
567
                raise AssertionError("invalid content_summary for nested tree: %r"
 
568
                    % (content_summary,))
542
569
            self._add_text_to_weave(ie.file_id, '', heads, None)
543
570
        else:
544
571
            raise NotImplementedError('unknown kind')
733
760
                    # after iter_changes examines and decides it has changed,
734
761
                    # we will unconditionally record a new version even if some
735
762
                    # other process reverts it while commit is running (with
736
 
                    # the revert happening after iter_changes did it's
 
763
                    # the revert happening after iter_changes did its
737
764
                    # examination).
738
765
                    if change[7][1]:
739
766
                        entry.executable = True
806
833
                seen_root = True
807
834
        self.new_inventory = None
808
835
        if len(inv_delta):
 
836
            # This should perhaps be guarded by a check that the basis we
 
837
            # commit against is the basis for the commit and if not do a delta
 
838
            # against the basis.
809
839
            self._any_changes = True
810
840
        if not seen_root:
811
841
            # housekeeping root entry changes do not affect no-change commits.
845
875
        # versioned roots do not change unless the tree found a change.
846
876
 
847
877
 
 
878
class RepositoryWriteLockResult(LogicalLockResult):
 
879
    """The result of write locking a repository.
 
880
 
 
881
    :ivar repository_token: The token obtained from the underlying lock, or
 
882
        None.
 
883
    :ivar unlock: A callable which will unlock the lock.
 
884
    """
 
885
 
 
886
    def __init__(self, unlock, repository_token):
 
887
        LogicalLockResult.__init__(self, unlock)
 
888
        self.repository_token = repository_token
 
889
 
 
890
    def __repr__(self):
 
891
        return "RepositoryWriteLockResult(%s, %s)" % (self.repository_token,
 
892
            self.unlock)
 
893
 
 
894
 
848
895
######################################################################
849
896
# Repositories
850
897
 
851
898
 
852
 
class Repository(object):
 
899
class Repository(_RelockDebugMixin, controldir.ControlComponent):
853
900
    """Repository holding history for one or more branches.
854
901
 
855
902
    The repository holds and retrieves historical information including
902
949
        pointing to .bzr/repository.
903
950
    """
904
951
 
905
 
    # What class to use for a CommitBuilder. Often its simpler to change this
 
952
    # What class to use for a CommitBuilder. Often it's simpler to change this
906
953
    # in a Repository class subclass rather than to override
907
954
    # get_commit_builder.
908
955
    _commit_builder_class = CommitBuilder
924
971
        """
925
972
        if self._write_group is not self.get_transaction():
926
973
            # has an unlock or relock occured ?
 
974
            if suppress_errors:
 
975
                mutter(
 
976
                '(suppressed) mismatched lock context and write group. %r, %r',
 
977
                self._write_group, self.get_transaction())
 
978
                return
927
979
            raise errors.BzrError(
928
980
                'mismatched lock context and write group. %r, %r' %
929
981
                (self._write_group, self.get_transaction()))
998
1050
                " id and insertion revid (%r, %r)"
999
1051
                % (inv.revision_id, revision_id))
1000
1052
        if inv.root is None:
1001
 
            raise AssertionError()
 
1053
            raise errors.RootMissing()
1002
1054
        return self._add_inventory_checked(revision_id, inv, parents)
1003
1055
 
1004
1056
    def _add_inventory_checked(self, revision_id, inv, parents):
1008
1060
 
1009
1061
        :seealso: add_inventory, for the contract.
1010
1062
        """
1011
 
        inv_lines = self._serialise_inventory_to_lines(inv)
 
1063
        inv_lines = self._serializer.write_inventory_to_lines(inv)
1012
1064
        return self._inventory_add_lines(revision_id, parents,
1013
1065
            inv_lines, check_content=False)
1014
1066
 
1063
1115
        check_content=True):
1064
1116
        """Store lines in inv_vf and return the sha1 of the inventory."""
1065
1117
        parents = [(parent,) for parent in parents]
1066
 
        return self.inventories.add_lines((revision_id,), parents, lines,
 
1118
        result = self.inventories.add_lines((revision_id,), parents, lines,
1067
1119
            check_content=check_content)[0]
 
1120
        self.inventories._access.flush()
 
1121
        return result
1068
1122
 
1069
1123
    def add_revision(self, revision_id, rev, inv=None, config=None):
1070
1124
        """Add rev to the revision store as revision_id.
1146
1200
        # The old API returned a list, should this actually be a set?
1147
1201
        return parent_map.keys()
1148
1202
 
 
1203
    def _check_inventories(self, checker):
 
1204
        """Check the inventories found from the revision scan.
 
1205
        
 
1206
        This is responsible for verifying the sha1 of inventories and
 
1207
        creating a pending_keys set that covers data referenced by inventories.
 
1208
        """
 
1209
        bar = ui.ui_factory.nested_progress_bar()
 
1210
        try:
 
1211
            self._do_check_inventories(checker, bar)
 
1212
        finally:
 
1213
            bar.finished()
 
1214
 
 
1215
    def _do_check_inventories(self, checker, bar):
 
1216
        """Helper for _check_inventories."""
 
1217
        revno = 0
 
1218
        keys = {'chk_bytes':set(), 'inventories':set(), 'texts':set()}
 
1219
        kinds = ['chk_bytes', 'texts']
 
1220
        count = len(checker.pending_keys)
 
1221
        bar.update("inventories", 0, 2)
 
1222
        current_keys = checker.pending_keys
 
1223
        checker.pending_keys = {}
 
1224
        # Accumulate current checks.
 
1225
        for key in current_keys:
 
1226
            if key[0] != 'inventories' and key[0] not in kinds:
 
1227
                checker._report_items.append('unknown key type %r' % (key,))
 
1228
            keys[key[0]].add(key[1:])
 
1229
        if keys['inventories']:
 
1230
            # NB: output order *should* be roughly sorted - topo or
 
1231
            # inverse topo depending on repository - either way decent
 
1232
            # to just delta against. However, pre-CHK formats didn't
 
1233
            # try to optimise inventory layout on disk. As such the
 
1234
            # pre-CHK code path does not use inventory deltas.
 
1235
            last_object = None
 
1236
            for record in self.inventories.check(keys=keys['inventories']):
 
1237
                if record.storage_kind == 'absent':
 
1238
                    checker._report_items.append(
 
1239
                        'Missing inventory {%s}' % (record.key,))
 
1240
                else:
 
1241
                    last_object = self._check_record('inventories', record,
 
1242
                        checker, last_object,
 
1243
                        current_keys[('inventories',) + record.key])
 
1244
            del keys['inventories']
 
1245
        else:
 
1246
            return
 
1247
        bar.update("texts", 1)
 
1248
        while (checker.pending_keys or keys['chk_bytes']
 
1249
            or keys['texts']):
 
1250
            # Something to check.
 
1251
            current_keys = checker.pending_keys
 
1252
            checker.pending_keys = {}
 
1253
            # Accumulate current checks.
 
1254
            for key in current_keys:
 
1255
                if key[0] not in kinds:
 
1256
                    checker._report_items.append('unknown key type %r' % (key,))
 
1257
                keys[key[0]].add(key[1:])
 
1258
            # Check the outermost kind only - inventories || chk_bytes || texts
 
1259
            for kind in kinds:
 
1260
                if keys[kind]:
 
1261
                    last_object = None
 
1262
                    for record in getattr(self, kind).check(keys=keys[kind]):
 
1263
                        if record.storage_kind == 'absent':
 
1264
                            checker._report_items.append(
 
1265
                                'Missing %s {%s}' % (kind, record.key,))
 
1266
                        else:
 
1267
                            last_object = self._check_record(kind, record,
 
1268
                                checker, last_object, current_keys[(kind,) + record.key])
 
1269
                    keys[kind] = set()
 
1270
                    break
 
1271
 
 
1272
    def _check_record(self, kind, record, checker, last_object, item_data):
 
1273
        """Check a single text from this repository."""
 
1274
        if kind == 'inventories':
 
1275
            rev_id = record.key[0]
 
1276
            inv = self._deserialise_inventory(rev_id,
 
1277
                record.get_bytes_as('fulltext'))
 
1278
            if last_object is not None:
 
1279
                delta = inv._make_delta(last_object)
 
1280
                for old_path, path, file_id, ie in delta:
 
1281
                    if ie is None:
 
1282
                        continue
 
1283
                    ie.check(checker, rev_id, inv)
 
1284
            else:
 
1285
                for path, ie in inv.iter_entries():
 
1286
                    ie.check(checker, rev_id, inv)
 
1287
            if self._format.fast_deltas:
 
1288
                return inv
 
1289
        elif kind == 'chk_bytes':
 
1290
            # No code written to check chk_bytes for this repo format.
 
1291
            checker._report_items.append(
 
1292
                'unsupported key type chk_bytes for %s' % (record.key,))
 
1293
        elif kind == 'texts':
 
1294
            self._check_text(record, checker, item_data)
 
1295
        else:
 
1296
            checker._report_items.append(
 
1297
                'unknown key type %s for %s' % (kind, record.key))
 
1298
 
 
1299
    def _check_text(self, record, checker, item_data):
 
1300
        """Check a single text."""
 
1301
        # Check it is extractable.
 
1302
        # TODO: check length.
 
1303
        if record.storage_kind == 'chunked':
 
1304
            chunks = record.get_bytes_as(record.storage_kind)
 
1305
            sha1 = osutils.sha_strings(chunks)
 
1306
            length = sum(map(len, chunks))
 
1307
        else:
 
1308
            content = record.get_bytes_as('fulltext')
 
1309
            sha1 = osutils.sha_string(content)
 
1310
            length = len(content)
 
1311
        if item_data and sha1 != item_data[1]:
 
1312
            checker._report_items.append(
 
1313
                'sha1 mismatch: %s has sha1 %s expected %s referenced by %s' %
 
1314
                (record.key, sha1, item_data[1], item_data[2]))
 
1315
 
1149
1316
    @staticmethod
1150
1317
    def create(a_bzrdir):
1151
1318
        """Construct the current default format repository in a_bzrdir."""
1156
1323
 
1157
1324
        :param _format: The format of the repository on disk.
1158
1325
        :param a_bzrdir: The BzrDir of the repository.
1159
 
 
1160
 
        In the future we will have a single api for all stores for
1161
 
        getting file texts, inventories and revisions, then
1162
 
        this construct will accept instances of those things.
1163
1326
        """
 
1327
        # In the future we will have a single api for all stores for
 
1328
        # getting file texts, inventories and revisions, then
 
1329
        # this construct will accept instances of those things.
1164
1330
        super(Repository, self).__init__()
1165
1331
        self._format = _format
1166
1332
        # the following are part of the public API for Repository:
1172
1338
        self._reconcile_does_inventory_gc = True
1173
1339
        self._reconcile_fixes_text_parents = False
1174
1340
        self._reconcile_backsup_inventory = True
1175
 
        # not right yet - should be more semantically clear ?
1176
 
        #
1177
 
        # TODO: make sure to construct the right store classes, etc, depending
1178
 
        # on whether escaping is required.
1179
 
        self._warn_if_deprecated()
1180
1341
        self._write_group = None
1181
1342
        # Additional places to query for data.
1182
1343
        self._fallback_repositories = []
1183
1344
        # An InventoryEntry cache, used during deserialization
1184
1345
        self._inventory_entry_cache = fifo_cache.FIFOCache(10*1024)
 
1346
        # Is it safe to return inventory entries directly from the entry cache,
 
1347
        # rather copying them?
 
1348
        self._safe_to_return_from_cache = False
 
1349
 
 
1350
    @property
 
1351
    def user_transport(self):
 
1352
        return self.bzrdir.user_transport
 
1353
 
 
1354
    @property
 
1355
    def control_transport(self):
 
1356
        return self._transport
1185
1357
 
1186
1358
    def __repr__(self):
1187
1359
        if self._fallback_repositories:
1236
1408
        data during reads, and allows a 'write_group' to be obtained. Write
1237
1409
        groups must be used for actual data insertion.
1238
1410
 
 
1411
        A token should be passed in if you know that you have locked the object
 
1412
        some other way, and need to synchronise this object's state with that
 
1413
        fact.
 
1414
 
 
1415
        XXX: this docstring is duplicated in many places, e.g. lockable_files.py
 
1416
 
1239
1417
        :param token: if this is already locked, then lock_write will fail
1240
1418
            unless the token matches the existing lock.
1241
1419
        :returns: a token if this instance supports tokens, otherwise None.
1244
1422
        :raises MismatchedToken: if the specified token doesn't match the token
1245
1423
            of the existing lock.
1246
1424
        :seealso: start_write_group.
1247
 
 
1248
 
        A token should be passed in if you know that you have locked the object
1249
 
        some other way, and need to synchronise this object's state with that
1250
 
        fact.
1251
 
 
1252
 
        XXX: this docstring is duplicated in many places, e.g. lockable_files.py
 
1425
        :return: A RepositoryWriteLockResult.
1253
1426
        """
1254
1427
        locked = self.is_locked()
1255
 
        result = self.control_files.lock_write(token=token)
 
1428
        token = self.control_files.lock_write(token=token)
1256
1429
        if not locked:
 
1430
            self._warn_if_deprecated()
 
1431
            self._note_lock('w')
1257
1432
            for repo in self._fallback_repositories:
1258
1433
                # Writes don't affect fallback repos
1259
1434
                repo.lock_read()
1260
1435
            self._refresh_data()
1261
 
        return result
 
1436
        return RepositoryWriteLockResult(self.unlock, token)
1262
1437
 
1263
1438
    def lock_read(self):
 
1439
        """Lock the repository for read operations.
 
1440
 
 
1441
        :return: An object with an unlock method which will release the lock
 
1442
            obtained.
 
1443
        """
1264
1444
        locked = self.is_locked()
1265
1445
        self.control_files.lock_read()
1266
1446
        if not locked:
 
1447
            self._warn_if_deprecated()
 
1448
            self._note_lock('r')
1267
1449
            for repo in self._fallback_repositories:
1268
1450
                repo.lock_read()
1269
1451
            self._refresh_data()
 
1452
        return LogicalLockResult(self.unlock)
1270
1453
 
1271
1454
    def get_physical_lock_status(self):
1272
1455
        return self.control_files.get_physical_lock_status()
1332
1515
 
1333
1516
        # now gather global repository information
1334
1517
        # XXX: This is available for many repos regardless of listability.
1335
 
        if self.bzrdir.root_transport.listable():
 
1518
        if self.user_transport.listable():
1336
1519
            # XXX: do we want to __define len__() ?
1337
1520
            # Maybe the versionedfiles object should provide a different
1338
1521
            # method to get the number of keys.
1348
1531
        :param using: If True, list only branches using this repository.
1349
1532
        """
1350
1533
        if using and not self.is_shared():
1351
 
            try:
1352
 
                return [self.bzrdir.open_branch()]
1353
 
            except errors.NotBranchError:
1354
 
                return []
 
1534
            return self.bzrdir.list_branches()
1355
1535
        class Evaluator(object):
1356
1536
 
1357
1537
            def __init__(self):
1366
1546
                    except errors.NoRepositoryPresent:
1367
1547
                        pass
1368
1548
                    else:
1369
 
                        return False, (None, repository)
 
1549
                        return False, ([], repository)
1370
1550
                self.first_call = False
1371
 
                try:
1372
 
                    value = (bzrdir.open_branch(), None)
1373
 
                except errors.NotBranchError:
1374
 
                    value = (None, None)
 
1551
                value = (bzrdir.list_branches(), None)
1375
1552
                return True, value
1376
1553
 
1377
 
        branches = []
1378
 
        for branch, repository in bzrdir.BzrDir.find_bzrdirs(
1379
 
                self.bzrdir.root_transport, evaluate=Evaluator()):
1380
 
            if branch is not None:
1381
 
                branches.append(branch)
 
1554
        ret = []
 
1555
        for branches, repository in bzrdir.BzrDir.find_bzrdirs(
 
1556
                self.user_transport, evaluate=Evaluator()):
 
1557
            if branches is not None:
 
1558
                ret.extend(branches)
1382
1559
            if not using and repository is not None:
1383
 
                branches.extend(repository.find_branches())
1384
 
        return branches
 
1560
                ret.extend(repository.find_branches())
 
1561
        return ret
1385
1562
 
1386
1563
    @needs_read_lock
1387
1564
    def search_missing_revision_ids(self, other, revision_id=None, find_ghosts=True):
1416
1593
        """Commit the contents accrued within the current write group.
1417
1594
 
1418
1595
        :seealso: start_write_group.
 
1596
        
 
1597
        :return: it may return an opaque hint that can be passed to 'pack'.
1419
1598
        """
1420
1599
        if self._write_group is not self.get_transaction():
1421
1600
            # has an unlock or relock occured ?
1475
1654
        # but at the moment we're only checking for texts referenced by
1476
1655
        # inventories at the graph's edge.
1477
1656
        key_deps = self.revisions._index._key_dependencies
1478
 
        key_deps.add_keys(present_inventories)
 
1657
        key_deps.satisfy_refs_for_keys(present_inventories)
1479
1658
        referrers = frozenset(r[0] for r in key_deps.get_referrers())
1480
1659
        file_ids = self.fileids_altered_by_revision_ids(referrers)
1481
1660
        missing_texts = set()
1494
1673
        return missing_keys
1495
1674
 
1496
1675
    def refresh_data(self):
1497
 
        """Re-read any data needed to to synchronise with disk.
 
1676
        """Re-read any data needed to synchronise with disk.
1498
1677
 
1499
1678
        This method is intended to be called after another repository instance
1500
1679
        (such as one used by a smart server) has inserted data into the
1501
 
        repository. It may not be called during a write group, but may be
1502
 
        called at any other time.
 
1680
        repository. On all repositories this will work outside of write groups.
 
1681
        Some repository formats (pack and newer for bzrlib native formats)
 
1682
        support refresh_data inside write groups. If called inside a write
 
1683
        group on a repository that does not support refreshing in a write group
 
1684
        IsInWriteGroupError will be raised.
1503
1685
        """
1504
 
        if self.is_in_write_group():
1505
 
            raise errors.InternalBzrError(
1506
 
                "May not refresh_data while in a write group.")
1507
1686
        self._refresh_data()
1508
1687
 
1509
1688
    def resume_write_group(self, tokens):
1548
1727
                "May not fetch while in a write group.")
1549
1728
        # fast path same-url fetch operations
1550
1729
        # TODO: lift out to somewhere common with RemoteRepository
1551
 
        # <https://bugs.edge.launchpad.net/bzr/+bug/401646>
 
1730
        # <https://bugs.launchpad.net/bzr/+bug/401646>
1552
1731
        if (self.has_same_location(source)
1553
1732
            and fetch_spec is None
1554
1733
            and self._has_same_fallbacks(source)):
1582
1761
        :param revprops: Optional dictionary of revision properties.
1583
1762
        :param revision_id: Optional revision id.
1584
1763
        """
 
1764
        if self._fallback_repositories:
 
1765
            raise errors.BzrError("Cannot commit from a lightweight checkout "
 
1766
                "to a stacked branch. See "
 
1767
                "https://bugs.launchpad.net/bzr/+bug/375013 for details.")
1585
1768
        result = self._commit_builder_class(self, parents, config,
1586
1769
            timestamp, timezone, committer, revprops, revision_id)
1587
1770
        self.start_write_group()
1588
1771
        return result
1589
1772
 
 
1773
    @only_raises(errors.LockNotHeld, errors.LockBroken)
1590
1774
    def unlock(self):
1591
1775
        if (self.control_files._lock_count == 1 and
1592
1776
            self.control_files._lock_mode == 'w'):
1714
1898
 
1715
1899
    @needs_read_lock
1716
1900
    def get_revisions(self, revision_ids):
1717
 
        """Get many revisions at once."""
 
1901
        """Get many revisions at once.
 
1902
        
 
1903
        Repositories that need to check data on every revision read should 
 
1904
        subclass this method.
 
1905
        """
1718
1906
        return self._get_revisions(revision_ids)
1719
1907
 
1720
1908
    @needs_read_lock
1721
1909
    def _get_revisions(self, revision_ids):
1722
1910
        """Core work logic to get many revisions without sanity checks."""
1723
 
        for rev_id in revision_ids:
1724
 
            if not rev_id or not isinstance(rev_id, basestring):
1725
 
                raise errors.InvalidRevisionId(revision_id=rev_id, branch=self)
 
1911
        revs = {}
 
1912
        for revid, rev in self._iter_revisions(revision_ids):
 
1913
            if rev is None:
 
1914
                raise errors.NoSuchRevision(self, revid)
 
1915
            revs[revid] = rev
 
1916
        return [revs[revid] for revid in revision_ids]
 
1917
 
 
1918
    def _iter_revisions(self, revision_ids):
 
1919
        """Iterate over revision objects.
 
1920
 
 
1921
        :param revision_ids: An iterable of revisions to examine. None may be
 
1922
            passed to request all revisions known to the repository. Note that
 
1923
            not all repositories can find unreferenced revisions; for those
 
1924
            repositories only referenced ones will be returned.
 
1925
        :return: An iterator of (revid, revision) tuples. Absent revisions (
 
1926
            those asked for but not available) are returned as (revid, None).
 
1927
        """
 
1928
        if revision_ids is None:
 
1929
            revision_ids = self.all_revision_ids()
 
1930
        else:
 
1931
            for rev_id in revision_ids:
 
1932
                if not rev_id or not isinstance(rev_id, basestring):
 
1933
                    raise errors.InvalidRevisionId(revision_id=rev_id, branch=self)
1726
1934
        keys = [(key,) for key in revision_ids]
1727
1935
        stream = self.revisions.get_record_stream(keys, 'unordered', True)
1728
 
        revs = {}
1729
1936
        for record in stream:
 
1937
            revid = record.key[0]
1730
1938
            if record.storage_kind == 'absent':
1731
 
                raise errors.NoSuchRevision(self, record.key[0])
1732
 
            text = record.get_bytes_as('fulltext')
1733
 
            rev = self._serializer.read_revision_from_string(text)
1734
 
            revs[record.key[0]] = rev
1735
 
        return [revs[revid] for revid in revision_ids]
1736
 
 
1737
 
    @needs_read_lock
1738
 
    def get_revision_xml(self, revision_id):
1739
 
        # TODO: jam 20070210 This shouldn't be necessary since get_revision
1740
 
        #       would have already do it.
1741
 
        # TODO: jam 20070210 Just use _serializer.write_revision_to_string()
1742
 
        # TODO: this can't just be replaced by:
1743
 
        # return self._serializer.write_revision_to_string(
1744
 
        #     self.get_revision(revision_id))
1745
 
        # as cStringIO preservers the encoding unlike write_revision_to_string
1746
 
        # or some other call down the path.
1747
 
        rev = self.get_revision(revision_id)
1748
 
        rev_tmp = cStringIO.StringIO()
1749
 
        # the current serializer..
1750
 
        self._serializer.write_revision(rev, rev_tmp)
1751
 
        rev_tmp.seek(0)
1752
 
        return rev_tmp.getvalue()
 
1939
                yield (revid, None)
 
1940
            else:
 
1941
                text = record.get_bytes_as('fulltext')
 
1942
                rev = self._serializer.read_revision_from_string(text)
 
1943
                yield (revid, rev)
1753
1944
 
1754
1945
    def get_deltas_for_revisions(self, revisions, specific_fileids=None):
1755
1946
        """Produce a generator of revision deltas.
1998
2189
        """
1999
2190
        selected_keys = set((revid,) for revid in revision_ids)
2000
2191
        w = _inv_weave or self.inventories
2001
 
        pb = ui.ui_factory.nested_progress_bar()
2002
 
        try:
2003
 
            return self._find_file_ids_from_xml_inventory_lines(
2004
 
                w.iter_lines_added_or_present_in_keys(
2005
 
                    selected_keys, pb=pb),
2006
 
                selected_keys)
2007
 
        finally:
2008
 
            pb.finished()
 
2192
        return self._find_file_ids_from_xml_inventory_lines(
 
2193
            w.iter_lines_added_or_present_in_keys(
 
2194
                selected_keys, pb=None),
 
2195
            selected_keys)
2009
2196
 
2010
2197
    def iter_files_bytes(self, desired_files):
2011
2198
        """Iterate through file versions.
2093
2280
                batch_size]
2094
2281
            if not to_query:
2095
2282
                break
2096
 
            for rev_tree in self.revision_trees(to_query):
2097
 
                revision_id = rev_tree.get_revision_id()
 
2283
            for revision_id in to_query:
2098
2284
                parent_ids = ancestors[revision_id]
2099
2285
                for text_key in revision_keys[revision_id]:
2100
2286
                    pb.update("Calculating text parents", processed_texts)
2173
2359
        num_file_ids = len(file_ids)
2174
2360
        for file_id, altered_versions in file_ids.iteritems():
2175
2361
            if pb is not None:
2176
 
                pb.update("fetch texts", count, num_file_ids)
 
2362
                pb.update("Fetch texts", count, num_file_ids)
2177
2363
            count += 1
2178
2364
            yield ("file", file_id, altered_versions)
2179
2365
 
2200
2386
        """Get Inventory object by revision id."""
2201
2387
        return self.iter_inventories([revision_id]).next()
2202
2388
 
2203
 
    def iter_inventories(self, revision_ids):
 
2389
    def iter_inventories(self, revision_ids, ordering=None):
2204
2390
        """Get many inventories by revision_ids.
2205
2391
 
2206
2392
        This will buffer some or all of the texts used in constructing the
2208
2394
        time.
2209
2395
 
2210
2396
        :param revision_ids: The expected revision ids of the inventories.
 
2397
        :param ordering: optional ordering, e.g. 'topological'.  If not
 
2398
            specified, the order of revision_ids will be preserved (by
 
2399
            buffering if necessary).
2211
2400
        :return: An iterator of inventories.
2212
2401
        """
2213
2402
        if ((None in revision_ids)
2214
2403
            or (_mod_revision.NULL_REVISION in revision_ids)):
2215
2404
            raise ValueError('cannot get null revision inventory')
2216
 
        return self._iter_inventories(revision_ids)
 
2405
        return self._iter_inventories(revision_ids, ordering)
2217
2406
 
2218
 
    def _iter_inventories(self, revision_ids):
 
2407
    def _iter_inventories(self, revision_ids, ordering):
2219
2408
        """single-document based inventory iteration."""
2220
 
        for text, revision_id in self._iter_inventory_xmls(revision_ids):
2221
 
            yield self.deserialise_inventory(revision_id, text)
 
2409
        inv_xmls = self._iter_inventory_xmls(revision_ids, ordering)
 
2410
        for text, revision_id in inv_xmls:
 
2411
            yield self._deserialise_inventory(revision_id, text)
2222
2412
 
2223
 
    def _iter_inventory_xmls(self, revision_ids):
 
2413
    def _iter_inventory_xmls(self, revision_ids, ordering):
 
2414
        if ordering is None:
 
2415
            order_as_requested = True
 
2416
            ordering = 'unordered'
 
2417
        else:
 
2418
            order_as_requested = False
2224
2419
        keys = [(revision_id,) for revision_id in revision_ids]
2225
 
        stream = self.inventories.get_record_stream(keys, 'unordered', True)
 
2420
        if not keys:
 
2421
            return
 
2422
        if order_as_requested:
 
2423
            key_iter = iter(keys)
 
2424
            next_key = key_iter.next()
 
2425
        stream = self.inventories.get_record_stream(keys, ordering, True)
2226
2426
        text_chunks = {}
2227
2427
        for record in stream:
2228
2428
            if record.storage_kind != 'absent':
2229
 
                text_chunks[record.key] = record.get_bytes_as('chunked')
 
2429
                chunks = record.get_bytes_as('chunked')
 
2430
                if order_as_requested:
 
2431
                    text_chunks[record.key] = chunks
 
2432
                else:
 
2433
                    yield ''.join(chunks), record.key[-1]
2230
2434
            else:
2231
2435
                raise errors.NoSuchRevision(self, record.key)
2232
 
        for key in keys:
2233
 
            chunks = text_chunks.pop(key)
2234
 
            yield ''.join(chunks), key[-1]
 
2436
            if order_as_requested:
 
2437
                # Yield as many results as we can while preserving order.
 
2438
                while next_key in text_chunks:
 
2439
                    chunks = text_chunks.pop(next_key)
 
2440
                    yield ''.join(chunks), next_key[-1]
 
2441
                    try:
 
2442
                        next_key = key_iter.next()
 
2443
                    except StopIteration:
 
2444
                        # We still want to fully consume the get_record_stream,
 
2445
                        # just in case it is not actually finished at this point
 
2446
                        next_key = None
 
2447
                        break
2235
2448
 
2236
 
    def deserialise_inventory(self, revision_id, xml):
 
2449
    def _deserialise_inventory(self, revision_id, xml):
2237
2450
        """Transform the xml into an inventory object.
2238
2451
 
2239
2452
        :param revision_id: The expected revision id of the inventory.
2240
2453
        :param xml: A serialised inventory.
2241
2454
        """
2242
2455
        result = self._serializer.read_inventory_from_string(xml, revision_id,
2243
 
                    entry_cache=self._inventory_entry_cache)
 
2456
                    entry_cache=self._inventory_entry_cache,
 
2457
                    return_from_cache=self._safe_to_return_from_cache)
2244
2458
        if result.revision_id != revision_id:
2245
2459
            raise AssertionError('revision id mismatch %s != %s' % (
2246
2460
                result.revision_id, revision_id))
2247
2461
        return result
2248
2462
 
2249
 
    def serialise_inventory(self, inv):
2250
 
        return self._serializer.write_inventory_to_string(inv)
2251
 
 
2252
 
    def _serialise_inventory_to_lines(self, inv):
2253
 
        return self._serializer.write_inventory_to_lines(inv)
2254
 
 
2255
2463
    def get_serializer_format(self):
2256
2464
        return self._serializer.format_num
2257
2465
 
2258
2466
    @needs_read_lock
2259
 
    def get_inventory_xml(self, revision_id):
2260
 
        """Get inventory XML as a file object."""
2261
 
        texts = self._iter_inventory_xmls([revision_id])
 
2467
    def _get_inventory_xml(self, revision_id):
 
2468
        """Get serialized inventory as a string."""
 
2469
        texts = self._iter_inventory_xmls([revision_id], 'unordered')
2262
2470
        try:
2263
2471
            text, revision_id = texts.next()
2264
2472
        except StopIteration:
2265
2473
            raise errors.HistoryMissing(self, 'inventory', revision_id)
2266
2474
        return text
2267
2475
 
2268
 
    @needs_read_lock
2269
 
    def get_inventory_sha1(self, revision_id):
2270
 
        """Return the sha1 hash of the inventory entry
2271
 
        """
2272
 
        return self.get_revision(revision_id).inventory_sha1
2273
 
 
2274
2476
    def get_rev_id_for_revno(self, revno, known_pair):
2275
2477
        """Return the revision id of a revno, given a later (revno, revid)
2276
2478
        pair in the same history.
2313
2515
            ancestors will be traversed.
2314
2516
        """
2315
2517
        graph = self.get_graph()
2316
 
        next_id = revision_id
2317
 
        while True:
2318
 
            if next_id in (None, _mod_revision.NULL_REVISION):
2319
 
                return
2320
 
            try:
2321
 
                parents = graph.get_parent_map([next_id])[next_id]
2322
 
            except KeyError:
2323
 
                raise errors.RevisionNotPresent(next_id, self)
2324
 
            yield next_id
2325
 
            if len(parents) == 0:
2326
 
                return
2327
 
            else:
2328
 
                next_id = parents[0]
2329
 
 
2330
 
    @needs_read_lock
2331
 
    def get_revision_inventory(self, revision_id):
2332
 
        """Return inventory of a past revision."""
2333
 
        # TODO: Unify this with get_inventory()
2334
 
        # bzr 0.0.6 and later imposes the constraint that the inventory_id
2335
 
        # must be the same as its revision, so this is trivial.
2336
 
        if revision_id is None:
2337
 
            # This does not make sense: if there is no revision,
2338
 
            # then it is the current tree inventory surely ?!
2339
 
            # and thus get_root_id() is something that looks at the last
2340
 
            # commit on the branch, and the get_root_id is an inventory check.
2341
 
            raise NotImplementedError
2342
 
            # return Inventory(self.get_root_id())
2343
 
        else:
2344
 
            return self.get_inventory(revision_id)
 
2518
        stop_revisions = (None, _mod_revision.NULL_REVISION)
 
2519
        return graph.iter_lefthand_ancestry(revision_id, stop_revisions)
2345
2520
 
2346
2521
    def is_shared(self):
2347
2522
        """Return True if this repository is flagged as a shared repository."""
2382
2557
            return RevisionTree(self, Inventory(root_id=None),
2383
2558
                                _mod_revision.NULL_REVISION)
2384
2559
        else:
2385
 
            inv = self.get_revision_inventory(revision_id)
 
2560
            inv = self.get_inventory(revision_id)
2386
2561
            return RevisionTree(self, inv, revision_id)
2387
2562
 
2388
2563
    def revision_trees(self, revision_ids):
2441
2616
            keys = tsort.topo_sort(parent_map)
2442
2617
        return [None] + list(keys)
2443
2618
 
2444
 
    def pack(self, hint=None):
 
2619
    def pack(self, hint=None, clean_obsolete_packs=False):
2445
2620
        """Compress the data within the repository.
2446
2621
 
2447
2622
        This operation only makes sense for some repository types. For other
2448
2623
        types it should be a no-op that just returns.
2449
2624
 
2450
2625
        This stub method does not require a lock, but subclasses should use
2451
 
        @needs_write_lock as this is a long running call its reasonable to
 
2626
        @needs_write_lock as this is a long running call it's reasonable to
2452
2627
        implicitly lock for the user.
2453
2628
 
2454
2629
        :param hint: If not supplied, the whole repository is packed.
2457
2632
            obtained from the result of commit_write_group(). Out of
2458
2633
            date hints are simply ignored, because concurrent operations
2459
2634
            can obsolete them rapidly.
 
2635
 
 
2636
        :param clean_obsolete_packs: Clean obsolete packs immediately after
 
2637
            the pack operation.
2460
2638
        """
2461
2639
 
2462
2640
    def get_transaction(self):
2478
2656
        for ((revision_id,), parent_keys) in \
2479
2657
                self.revisions.get_parent_map(query_keys).iteritems():
2480
2658
            if parent_keys:
2481
 
                result[revision_id] = tuple(parent_revid
2482
 
                    for (parent_revid,) in parent_keys)
 
2659
                result[revision_id] = tuple([parent_revid
 
2660
                    for (parent_revid,) in parent_keys])
2483
2661
            else:
2484
2662
                result[revision_id] = (_mod_revision.NULL_REVISION,)
2485
2663
        return result
2487
2665
    def _make_parents_provider(self):
2488
2666
        return self
2489
2667
 
 
2668
    @needs_read_lock
 
2669
    def get_known_graph_ancestry(self, revision_ids):
 
2670
        """Return the known graph for a set of revision ids and their ancestors.
 
2671
        """
 
2672
        st = static_tuple.StaticTuple
 
2673
        revision_keys = [st(r_id).intern() for r_id in revision_ids]
 
2674
        known_graph = self.revisions.get_known_graph_ancestry(revision_keys)
 
2675
        return graph.GraphThunkIdsToKeys(known_graph)
 
2676
 
2490
2677
    def get_graph(self, other_repository=None):
2491
2678
        """Return the graph walker for this repository format"""
2492
2679
        parents_provider = self._make_parents_provider()
2496
2683
                [parents_provider, other_repository._make_parents_provider()])
2497
2684
        return graph.Graph(parents_provider)
2498
2685
 
2499
 
    def _get_versioned_file_checker(self, text_key_references=None):
 
2686
    def _get_versioned_file_checker(self, text_key_references=None,
 
2687
        ancestors=None):
2500
2688
        """Return an object suitable for checking versioned files.
2501
2689
        
2502
2690
        :param text_key_references: if non-None, an already built
2504
2692
            to whether they were referred to by the inventory of the
2505
2693
            revision_id that they contain. If None, this will be
2506
2694
            calculated.
 
2695
        :param ancestors: Optional result from
 
2696
            self.get_graph().get_parent_map(self.all_revision_ids()) if already
 
2697
            available.
2507
2698
        """
2508
2699
        return _VersionedFileChecker(self,
2509
 
            text_key_references=text_key_references)
 
2700
            text_key_references=text_key_references, ancestors=ancestors)
2510
2701
 
2511
2702
    def revision_ids_to_search_result(self, result_set):
2512
2703
        """Convert a set of revision ids to a graph SearchResult."""
2562
2753
        return record.get_bytes_as('fulltext')
2563
2754
 
2564
2755
    @needs_read_lock
2565
 
    def check(self, revision_ids=None):
 
2756
    def check(self, revision_ids=None, callback_refs=None, check_repo=True):
2566
2757
        """Check consistency of all history of given revision_ids.
2567
2758
 
2568
2759
        Different repository implementations should override _check().
2569
2760
 
2570
2761
        :param revision_ids: A non-empty list of revision_ids whose ancestry
2571
2762
             will be checked.  Typically the last revision_id of a branch.
 
2763
        :param callback_refs: A dict of check-refs to resolve and callback
 
2764
            the check/_check method on the items listed as wanting the ref.
 
2765
            see bzrlib.check.
 
2766
        :param check_repo: If False do not check the repository contents, just 
 
2767
            calculate the data callback_refs requires and call them back.
2572
2768
        """
2573
 
        return self._check(revision_ids)
 
2769
        return self._check(revision_ids, callback_refs=callback_refs,
 
2770
            check_repo=check_repo)
2574
2771
 
2575
 
    def _check(self, revision_ids):
2576
 
        result = check.Check(self)
2577
 
        result.check()
 
2772
    def _check(self, revision_ids, callback_refs, check_repo):
 
2773
        result = check.Check(self, check_repo=check_repo)
 
2774
        result.check(callback_refs)
2578
2775
        return result
2579
2776
 
2580
 
    def _warn_if_deprecated(self):
 
2777
    def _warn_if_deprecated(self, branch=None):
2581
2778
        global _deprecation_warning_done
2582
2779
        if _deprecation_warning_done:
2583
2780
            return
2584
 
        _deprecation_warning_done = True
2585
 
        warning("Format %s for %s is deprecated - please use 'bzr upgrade' to get better performance"
2586
 
                % (self._format, self.bzrdir.transport.base))
 
2781
        try:
 
2782
            if branch is None:
 
2783
                conf = config.GlobalConfig()
 
2784
            else:
 
2785
                conf = branch.get_config()
 
2786
            if conf.suppress_warning('format_deprecation'):
 
2787
                return
 
2788
            warning("Format %s for %s is deprecated -"
 
2789
                    " please use 'bzr upgrade' to get better performance"
 
2790
                    % (self._format, self.bzrdir.transport.base))
 
2791
        finally:
 
2792
            _deprecation_warning_done = True
2587
2793
 
2588
2794
    def supports_rich_root(self):
2589
2795
        return self._format.rich_root_data
2623
2829
            % (name, from_module),
2624
2830
            DeprecationWarning,
2625
2831
            stacklevel=2)
2626
 
        m = __import__(from_module, globals(), locals(), [name])
2627
2832
        try:
2628
 
            return getattr(m, name)
 
2833
            return pyutils.get_named_object(from_module, name)
2629
2834
        except AttributeError:
2630
2835
            raise AttributeError('module %s has no name %s'
2631
 
                    % (m, name))
 
2836
                    % (sys.modules[from_module], name))
2632
2837
    globals()[name] = _deprecated_repository_forwarder
2633
2838
 
2634
2839
for _name in [
2870
3075
    # help), and for fetching when data won't have come from the same
2871
3076
    # compressor.
2872
3077
    pack_compresses = False
 
3078
    # Does the repository inventory storage understand references to trees?
 
3079
    supports_tree_reference = None
 
3080
    # Is the format experimental ?
 
3081
    experimental = False
2873
3082
 
2874
 
    def __str__(self):
2875
 
        return "<%s>" % self.__class__.__name__
 
3083
    def __repr__(self):
 
3084
        return "%s()" % self.__class__.__name__
2876
3085
 
2877
3086
    def __eq__(self, other):
2878
3087
        # format objects are generally stateless
2891
3100
        """
2892
3101
        try:
2893
3102
            transport = a_bzrdir.get_repository_transport(None)
2894
 
            format_string = transport.get("format").read()
 
3103
            format_string = transport.get_bytes("format")
2895
3104
            return format_registry.get(format_string)
2896
3105
        except errors.NoSuchFile:
2897
3106
            raise errors.NoRepositoryPresent(a_bzrdir)
2979
3188
        raise NotImplementedError(self.network_name)
2980
3189
 
2981
3190
    def check_conversion_target(self, target_format):
2982
 
        raise NotImplementedError(self.check_conversion_target)
 
3191
        if self.rich_root_data and not target_format.rich_root_data:
 
3192
            raise errors.BadConversionTarget(
 
3193
                'Does not support rich root data.', target_format,
 
3194
                from_format=self)
 
3195
        if (self.supports_tree_reference and 
 
3196
            not getattr(target_format, 'supports_tree_reference', False)):
 
3197
            raise errors.BadConversionTarget(
 
3198
                'Does not support nested trees', target_format,
 
3199
                from_format=self)
2983
3200
 
2984
3201
    def open(self, a_bzrdir, _found=False):
2985
3202
        """Return an instance of this format for the bzrdir a_bzrdir.
2988
3205
        """
2989
3206
        raise NotImplementedError(self.open)
2990
3207
 
 
3208
    def _run_post_repo_init_hooks(self, repository, a_bzrdir, shared):
 
3209
        from bzrlib.bzrdir import BzrDir, RepoInitHookParams
 
3210
        hooks = BzrDir.hooks['post_repo_init']
 
3211
        if not hooks:
 
3212
            return
 
3213
        params = RepoInitHookParams(repository, self, a_bzrdir, shared)
 
3214
        for hook in hooks:
 
3215
            hook(params)
 
3216
 
2991
3217
 
2992
3218
class MetaDirRepositoryFormat(RepositoryFormat):
2993
3219
    """Common base class for the new repositories using the metadir layout."""
3126
3352
    'bzrlib.repofmt.pack_repo',
3127
3353
    'RepositoryFormatKnitPack6RichRoot',
3128
3354
    )
 
3355
format_registry.register_lazy(
 
3356
    'Bazaar repository format 2a (needs bzr 1.16 or later)\n',
 
3357
    'bzrlib.repofmt.groupcompress_repo',
 
3358
    'RepositoryFormat2a',
 
3359
    )
3129
3360
 
3130
3361
# Development formats.
3131
 
# Obsolete but kept pending a CHK based subtree format.
 
3362
# Check their docstrings to see if/when they are obsolete.
3132
3363
format_registry.register_lazy(
3133
3364
    ("Bazaar development format 2 with subtree support "
3134
3365
        "(needs bzr.dev from before 1.8)\n"),
3135
3366
    'bzrlib.repofmt.pack_repo',
3136
3367
    'RepositoryFormatPackDevelopment2Subtree',
3137
3368
    )
3138
 
 
3139
 
# 1.14->1.16 go below here
3140
 
format_registry.register_lazy(
3141
 
    'Bazaar development format - group compression and chk inventory'
3142
 
        ' (needs bzr.dev from 1.14)\n',
3143
 
    'bzrlib.repofmt.groupcompress_repo',
3144
 
    'RepositoryFormatCHK1',
3145
 
    )
3146
 
 
3147
 
format_registry.register_lazy(
3148
 
    'Bazaar development format - chk repository with bencode revision '
3149
 
        'serialization (needs bzr.dev from 1.16)\n',
3150
 
    'bzrlib.repofmt.groupcompress_repo',
3151
 
    'RepositoryFormatCHK2',
3152
 
    )
3153
 
format_registry.register_lazy(
3154
 
    'Bazaar repository format 2a (needs bzr 1.16 or later)\n',
3155
 
    'bzrlib.repofmt.groupcompress_repo',
3156
 
    'RepositoryFormat2a',
 
3369
format_registry.register_lazy(
 
3370
    'Bazaar development format 8\n',
 
3371
    'bzrlib.repofmt.groupcompress_repo',
 
3372
    'RepositoryFormat2aSubtree',
3157
3373
    )
3158
3374
 
3159
3375
 
3198
3414
 
3199
3415
        :param revision_id: if None all content is copied, if NULL_REVISION no
3200
3416
                            content is copied.
3201
 
        :param pb: optional progress bar to use for progress reports. If not
3202
 
                   provided a default one will be created.
 
3417
        :param pb: ignored.
3203
3418
        :return: None.
3204
3419
        """
 
3420
        ui.ui_factory.warn_experimental_format_fetch(self)
3205
3421
        from bzrlib.fetch import RepoFetcher
 
3422
        # See <https://launchpad.net/bugs/456077> asking for a warning here
 
3423
        if self.source._format.network_name() != self.target._format.network_name():
 
3424
            ui.ui_factory.show_user_warning('cross_format_fetch',
 
3425
                from_format=self.source._format,
 
3426
                to_format=self.target._format)
3206
3427
        f = RepoFetcher(to_repository=self.target,
3207
3428
                               from_repository=self.source,
3208
3429
                               last_revision=revision_id,
3209
3430
                               fetch_spec=fetch_spec,
3210
 
                               pb=pb, find_ghosts=find_ghosts)
 
3431
                               find_ghosts=find_ghosts)
3211
3432
 
3212
3433
    def _walk_to_common_revisions(self, revision_ids):
3213
3434
        """Walk out from revision_ids in source to revisions target has.
3332
3553
        return InterRepository._same_model(source, target)
3333
3554
 
3334
3555
 
3335
 
class InterWeaveRepo(InterSameDataRepository):
3336
 
    """Optimised code paths between Weave based repositories.
3337
 
 
3338
 
    This should be in bzrlib/repofmt/weaverepo.py but we have not yet
3339
 
    implemented lazy inter-object optimisation.
3340
 
    """
3341
 
 
3342
 
    @classmethod
3343
 
    def _get_repo_format_to_test(self):
3344
 
        from bzrlib.repofmt import weaverepo
3345
 
        return weaverepo.RepositoryFormat7()
3346
 
 
3347
 
    @staticmethod
3348
 
    def is_compatible(source, target):
3349
 
        """Be compatible with known Weave formats.
3350
 
 
3351
 
        We don't test for the stores being of specific types because that
3352
 
        could lead to confusing results, and there is no need to be
3353
 
        overly general.
3354
 
        """
3355
 
        from bzrlib.repofmt.weaverepo import (
3356
 
                RepositoryFormat5,
3357
 
                RepositoryFormat6,
3358
 
                RepositoryFormat7,
3359
 
                )
3360
 
        try:
3361
 
            return (isinstance(source._format, (RepositoryFormat5,
3362
 
                                                RepositoryFormat6,
3363
 
                                                RepositoryFormat7)) and
3364
 
                    isinstance(target._format, (RepositoryFormat5,
3365
 
                                                RepositoryFormat6,
3366
 
                                                RepositoryFormat7)))
3367
 
        except AttributeError:
3368
 
            return False
3369
 
 
3370
 
    @needs_write_lock
3371
 
    def copy_content(self, revision_id=None):
3372
 
        """See InterRepository.copy_content()."""
3373
 
        # weave specific optimised path:
3374
 
        try:
3375
 
            self.target.set_make_working_trees(self.source.make_working_trees())
3376
 
        except (errors.RepositoryUpgradeRequired, NotImplemented):
3377
 
            pass
3378
 
        # FIXME do not peek!
3379
 
        if self.source._transport.listable():
3380
 
            pb = ui.ui_factory.nested_progress_bar()
3381
 
            try:
3382
 
                self.target.texts.insert_record_stream(
3383
 
                    self.source.texts.get_record_stream(
3384
 
                        self.source.texts.keys(), 'topological', False))
3385
 
                pb.update('copying inventory', 0, 1)
3386
 
                self.target.inventories.insert_record_stream(
3387
 
                    self.source.inventories.get_record_stream(
3388
 
                        self.source.inventories.keys(), 'topological', False))
3389
 
                self.target.signatures.insert_record_stream(
3390
 
                    self.source.signatures.get_record_stream(
3391
 
                        self.source.signatures.keys(),
3392
 
                        'unordered', True))
3393
 
                self.target.revisions.insert_record_stream(
3394
 
                    self.source.revisions.get_record_stream(
3395
 
                        self.source.revisions.keys(),
3396
 
                        'topological', True))
3397
 
            finally:
3398
 
                pb.finished()
3399
 
        else:
3400
 
            self.target.fetch(self.source, revision_id=revision_id)
3401
 
 
3402
 
    @needs_read_lock
3403
 
    def search_missing_revision_ids(self, revision_id=None, find_ghosts=True):
3404
 
        """See InterRepository.missing_revision_ids()."""
3405
 
        # we want all revisions to satisfy revision_id in source.
3406
 
        # but we don't want to stat every file here and there.
3407
 
        # we want then, all revisions other needs to satisfy revision_id
3408
 
        # checked, but not those that we have locally.
3409
 
        # so the first thing is to get a subset of the revisions to
3410
 
        # satisfy revision_id in source, and then eliminate those that
3411
 
        # we do already have.
3412
 
        # this is slow on high latency connection to self, but as this
3413
 
        # disk format scales terribly for push anyway due to rewriting
3414
 
        # inventory.weave, this is considered acceptable.
3415
 
        # - RBC 20060209
3416
 
        if revision_id is not None:
3417
 
            source_ids = self.source.get_ancestry(revision_id)
3418
 
            if source_ids[0] is not None:
3419
 
                raise AssertionError()
3420
 
            source_ids.pop(0)
3421
 
        else:
3422
 
            source_ids = self.source._all_possible_ids()
3423
 
        source_ids_set = set(source_ids)
3424
 
        # source_ids is the worst possible case we may need to pull.
3425
 
        # now we want to filter source_ids against what we actually
3426
 
        # have in target, but don't try to check for existence where we know
3427
 
        # we do not have a revision as that would be pointless.
3428
 
        target_ids = set(self.target._all_possible_ids())
3429
 
        possibly_present_revisions = target_ids.intersection(source_ids_set)
3430
 
        actually_present_revisions = set(
3431
 
            self.target._eliminate_revisions_not_present(possibly_present_revisions))
3432
 
        required_revisions = source_ids_set.difference(actually_present_revisions)
3433
 
        if revision_id is not None:
3434
 
            # we used get_ancestry to determine source_ids then we are assured all
3435
 
            # revisions referenced are present as they are installed in topological order.
3436
 
            # and the tip revision was validated by get_ancestry.
3437
 
            result_set = required_revisions
3438
 
        else:
3439
 
            # if we just grabbed the possibly available ids, then
3440
 
            # we only have an estimate of whats available and need to validate
3441
 
            # that against the revision records.
3442
 
            result_set = set(
3443
 
                self.source._eliminate_revisions_not_present(required_revisions))
3444
 
        return self.source.revision_ids_to_search_result(result_set)
3445
 
 
3446
 
 
3447
 
class InterKnitRepo(InterSameDataRepository):
3448
 
    """Optimised code paths between Knit based repositories."""
3449
 
 
3450
 
    @classmethod
3451
 
    def _get_repo_format_to_test(self):
3452
 
        from bzrlib.repofmt import knitrepo
3453
 
        return knitrepo.RepositoryFormatKnit1()
3454
 
 
3455
 
    @staticmethod
3456
 
    def is_compatible(source, target):
3457
 
        """Be compatible with known Knit formats.
3458
 
 
3459
 
        We don't test for the stores being of specific types because that
3460
 
        could lead to confusing results, and there is no need to be
3461
 
        overly general.
3462
 
        """
3463
 
        from bzrlib.repofmt.knitrepo import RepositoryFormatKnit
3464
 
        try:
3465
 
            are_knits = (isinstance(source._format, RepositoryFormatKnit) and
3466
 
                isinstance(target._format, RepositoryFormatKnit))
3467
 
        except AttributeError:
3468
 
            return False
3469
 
        return are_knits and InterRepository._same_model(source, target)
3470
 
 
3471
 
    @needs_read_lock
3472
 
    def search_missing_revision_ids(self, revision_id=None, find_ghosts=True):
3473
 
        """See InterRepository.missing_revision_ids()."""
3474
 
        if revision_id is not None:
3475
 
            source_ids = self.source.get_ancestry(revision_id)
3476
 
            if source_ids[0] is not None:
3477
 
                raise AssertionError()
3478
 
            source_ids.pop(0)
3479
 
        else:
3480
 
            source_ids = self.source.all_revision_ids()
3481
 
        source_ids_set = set(source_ids)
3482
 
        # source_ids is the worst possible case we may need to pull.
3483
 
        # now we want to filter source_ids against what we actually
3484
 
        # have in target, but don't try to check for existence where we know
3485
 
        # we do not have a revision as that would be pointless.
3486
 
        target_ids = set(self.target.all_revision_ids())
3487
 
        possibly_present_revisions = target_ids.intersection(source_ids_set)
3488
 
        actually_present_revisions = set(
3489
 
            self.target._eliminate_revisions_not_present(possibly_present_revisions))
3490
 
        required_revisions = source_ids_set.difference(actually_present_revisions)
3491
 
        if revision_id is not None:
3492
 
            # we used get_ancestry to determine source_ids then we are assured all
3493
 
            # revisions referenced are present as they are installed in topological order.
3494
 
            # and the tip revision was validated by get_ancestry.
3495
 
            result_set = required_revisions
3496
 
        else:
3497
 
            # if we just grabbed the possibly available ids, then
3498
 
            # we only have an estimate of whats available and need to validate
3499
 
            # that against the revision records.
3500
 
            result_set = set(
3501
 
                self.source._eliminate_revisions_not_present(required_revisions))
3502
 
        return self.source.revision_ids_to_search_result(result_set)
3503
 
 
3504
 
 
3505
3556
class InterDifferingSerializer(InterRepository):
3506
3557
 
3507
3558
    @classmethod
3514
3565
        # This is redundant with format.check_conversion_target(), however that
3515
3566
        # raises an exception, and we just want to say "False" as in we won't
3516
3567
        # support converting between these formats.
 
3568
        if 'IDS_never' in debug.debug_flags:
 
3569
            return False
3517
3570
        if source.supports_rich_root() and not target.supports_rich_root():
3518
3571
            return False
3519
3572
        if (source._format.supports_tree_reference
3520
3573
            and not target._format.supports_tree_reference):
3521
3574
            return False
 
3575
        if target._fallback_repositories and target._format.supports_chks:
 
3576
            # IDS doesn't know how to copy CHKs for the parent inventories it
 
3577
            # adds to stacked repos.
 
3578
            return False
 
3579
        if 'IDS_always' in debug.debug_flags:
 
3580
            return True
 
3581
        # Only use this code path for local source and target.  IDS does far
 
3582
        # too much IO (both bandwidth and roundtrips) over a network.
 
3583
        if not source.bzrdir.transport.base.startswith('file:///'):
 
3584
            return False
 
3585
        if not target.bzrdir.transport.base.startswith('file:///'):
 
3586
            return False
3522
3587
        return True
3523
3588
 
3524
 
    def _get_delta_for_revision(self, tree, parent_ids, basis_id, cache):
 
3589
    def _get_trees(self, revision_ids, cache):
 
3590
        possible_trees = []
 
3591
        for rev_id in revision_ids:
 
3592
            if rev_id in cache:
 
3593
                possible_trees.append((rev_id, cache[rev_id]))
 
3594
            else:
 
3595
                # Not cached, but inventory might be present anyway.
 
3596
                try:
 
3597
                    tree = self.source.revision_tree(rev_id)
 
3598
                except errors.NoSuchRevision:
 
3599
                    # Nope, parent is ghost.
 
3600
                    pass
 
3601
                else:
 
3602
                    cache[rev_id] = tree
 
3603
                    possible_trees.append((rev_id, tree))
 
3604
        return possible_trees
 
3605
 
 
3606
    def _get_delta_for_revision(self, tree, parent_ids, possible_trees):
3525
3607
        """Get the best delta and base for this revision.
3526
3608
 
3527
3609
        :return: (basis_id, delta)
3528
3610
        """
3529
 
        possible_trees = [(parent_id, cache[parent_id])
3530
 
                          for parent_id in parent_ids
3531
 
                           if parent_id in cache]
3532
 
        if len(possible_trees) == 0:
3533
 
            # There either aren't any parents, or the parents aren't in the
3534
 
            # cache, so just use the last converted tree
3535
 
            possible_trees.append((basis_id, cache[basis_id]))
3536
3611
        deltas = []
 
3612
        # Generate deltas against each tree, to find the shortest.
 
3613
        texts_possibly_new_in_tree = set()
3537
3614
        for basis_id, basis_tree in possible_trees:
3538
3615
            delta = tree.inventory._make_delta(basis_tree.inventory)
 
3616
            for old_path, new_path, file_id, new_entry in delta:
 
3617
                if new_path is None:
 
3618
                    # This file_id isn't present in the new rev, so we don't
 
3619
                    # care about it.
 
3620
                    continue
 
3621
                if not new_path:
 
3622
                    # Rich roots are handled elsewhere...
 
3623
                    continue
 
3624
                kind = new_entry.kind
 
3625
                if kind != 'directory' and kind != 'file':
 
3626
                    # No text record associated with this inventory entry.
 
3627
                    continue
 
3628
                # This is a directory or file that has changed somehow.
 
3629
                texts_possibly_new_in_tree.add((file_id, new_entry.revision))
3539
3630
            deltas.append((len(delta), basis_id, delta))
3540
3631
        deltas.sort()
3541
3632
        return deltas[0][1:]
3542
3633
 
3543
 
    def _get_parent_keys(self, root_key, parent_map):
3544
 
        """Get the parent keys for a given root id."""
3545
 
        root_id, rev_id = root_key
3546
 
        # Include direct parents of the revision, but only if they used
3547
 
        # the same root_id and are heads.
3548
 
        parent_keys = []
3549
 
        for parent_id in parent_map[rev_id]:
3550
 
            if parent_id == _mod_revision.NULL_REVISION:
3551
 
                continue
3552
 
            if parent_id not in self._revision_id_to_root_id:
3553
 
                # We probably didn't read this revision, go spend the
3554
 
                # extra effort to actually check
3555
 
                try:
3556
 
                    tree = self.source.revision_tree(parent_id)
3557
 
                except errors.NoSuchRevision:
3558
 
                    # Ghost, fill out _revision_id_to_root_id in case we
3559
 
                    # encounter this again.
3560
 
                    # But set parent_root_id to None since we don't really know
3561
 
                    parent_root_id = None
3562
 
                else:
3563
 
                    parent_root_id = tree.get_root_id()
3564
 
                self._revision_id_to_root_id[parent_id] = None
3565
 
            else:
3566
 
                parent_root_id = self._revision_id_to_root_id[parent_id]
3567
 
            if root_id == parent_root_id:
3568
 
                # With stacking we _might_ want to refer to a non-local
3569
 
                # revision, but this code path only applies when we have the
3570
 
                # full content available, so ghosts really are ghosts, not just
3571
 
                # the edge of local data.
3572
 
                parent_keys.append((parent_id,))
3573
 
            else:
3574
 
                # root_id may be in the parent anyway.
3575
 
                try:
3576
 
                    tree = self.source.revision_tree(parent_id)
3577
 
                except errors.NoSuchRevision:
3578
 
                    # ghost, can't refer to it.
3579
 
                    pass
3580
 
                else:
3581
 
                    try:
3582
 
                        parent_keys.append((tree.inventory[root_id].revision,))
3583
 
                    except errors.NoSuchId:
3584
 
                        # not in the tree
3585
 
                        pass
3586
 
        g = graph.Graph(self.source.revisions)
3587
 
        heads = g.heads(parent_keys)
3588
 
        selected_keys = []
3589
 
        for key in parent_keys:
3590
 
            if key in heads and key not in selected_keys:
3591
 
                selected_keys.append(key)
3592
 
        return tuple([(root_id,)+ key for key in selected_keys])
 
3634
    def _fetch_parent_invs_for_stacking(self, parent_map, cache):
 
3635
        """Find all parent revisions that are absent, but for which the
 
3636
        inventory is present, and copy those inventories.
3593
3637
 
3594
 
    def _new_root_data_stream(self, root_keys_to_create, parent_map):
3595
 
        for root_key in root_keys_to_create:
3596
 
            parent_keys = self._get_parent_keys(root_key, parent_map)
3597
 
            yield versionedfile.FulltextContentFactory(root_key,
3598
 
                parent_keys, None, '')
 
3638
        This is necessary to preserve correctness when the source is stacked
 
3639
        without fallbacks configured.  (Note that in cases like upgrade the
 
3640
        source may be not have _fallback_repositories even though it is
 
3641
        stacked.)
 
3642
        """
 
3643
        parent_revs = set()
 
3644
        for parents in parent_map.values():
 
3645
            parent_revs.update(parents)
 
3646
        present_parents = self.source.get_parent_map(parent_revs)
 
3647
        absent_parents = set(parent_revs).difference(present_parents)
 
3648
        parent_invs_keys_for_stacking = self.source.inventories.get_parent_map(
 
3649
            (rev_id,) for rev_id in absent_parents)
 
3650
        parent_inv_ids = [key[-1] for key in parent_invs_keys_for_stacking]
 
3651
        for parent_tree in self.source.revision_trees(parent_inv_ids):
 
3652
            current_revision_id = parent_tree.get_revision_id()
 
3653
            parents_parents_keys = parent_invs_keys_for_stacking[
 
3654
                (current_revision_id,)]
 
3655
            parents_parents = [key[-1] for key in parents_parents_keys]
 
3656
            basis_id = _mod_revision.NULL_REVISION
 
3657
            basis_tree = self.source.revision_tree(basis_id)
 
3658
            delta = parent_tree.inventory._make_delta(basis_tree.inventory)
 
3659
            self.target.add_inventory_by_delta(
 
3660
                basis_id, delta, current_revision_id, parents_parents)
 
3661
            cache[current_revision_id] = parent_tree
3599
3662
 
3600
3663
    def _fetch_batch(self, revision_ids, basis_id, cache):
3601
3664
        """Fetch across a few revisions.
3615
3678
        pending_deltas = []
3616
3679
        pending_revisions = []
3617
3680
        parent_map = self.source.get_parent_map(revision_ids)
 
3681
        self._fetch_parent_invs_for_stacking(parent_map, cache)
 
3682
        self.source._safe_to_return_from_cache = True
3618
3683
        for tree in self.source.revision_trees(revision_ids):
 
3684
            # Find a inventory delta for this revision.
 
3685
            # Find text entries that need to be copied, too.
3619
3686
            current_revision_id = tree.get_revision_id()
3620
3687
            parent_ids = parent_map.get(current_revision_id, ())
 
3688
            parent_trees = self._get_trees(parent_ids, cache)
 
3689
            possible_trees = list(parent_trees)
 
3690
            if len(possible_trees) == 0:
 
3691
                # There either aren't any parents, or the parents are ghosts,
 
3692
                # so just use the last converted tree.
 
3693
                possible_trees.append((basis_id, cache[basis_id]))
3621
3694
            basis_id, delta = self._get_delta_for_revision(tree, parent_ids,
3622
 
                                                           basis_id, cache)
 
3695
                                                           possible_trees)
 
3696
            revision = self.source.get_revision(current_revision_id)
 
3697
            pending_deltas.append((basis_id, delta,
 
3698
                current_revision_id, revision.parent_ids))
3623
3699
            if self._converting_to_rich_root:
3624
3700
                self._revision_id_to_root_id[current_revision_id] = \
3625
3701
                    tree.get_root_id()
3626
 
            # Find text entries that need to be copied
 
3702
            # Determine which texts are in present in this revision but not in
 
3703
            # any of the available parents.
 
3704
            texts_possibly_new_in_tree = set()
3627
3705
            for old_path, new_path, file_id, entry in delta:
3628
 
                if new_path is not None:
3629
 
                    if not new_path:
3630
 
                        # This is the root
3631
 
                        if not self.target.supports_rich_root():
3632
 
                            # The target doesn't support rich root, so we don't
3633
 
                            # copy
3634
 
                            continue
3635
 
                        if self._converting_to_rich_root:
3636
 
                            # This can't be copied normally, we have to insert
3637
 
                            # it specially
3638
 
                            root_keys_to_create.add((file_id, entry.revision))
3639
 
                            continue
3640
 
                    text_keys.add((file_id, entry.revision))
3641
 
            revision = self.source.get_revision(current_revision_id)
3642
 
            pending_deltas.append((basis_id, delta,
3643
 
                current_revision_id, revision.parent_ids))
 
3706
                if new_path is None:
 
3707
                    # This file_id isn't present in the new rev
 
3708
                    continue
 
3709
                if not new_path:
 
3710
                    # This is the root
 
3711
                    if not self.target.supports_rich_root():
 
3712
                        # The target doesn't support rich root, so we don't
 
3713
                        # copy
 
3714
                        continue
 
3715
                    if self._converting_to_rich_root:
 
3716
                        # This can't be copied normally, we have to insert
 
3717
                        # it specially
 
3718
                        root_keys_to_create.add((file_id, entry.revision))
 
3719
                        continue
 
3720
                kind = entry.kind
 
3721
                texts_possibly_new_in_tree.add((file_id, entry.revision))
 
3722
            for basis_id, basis_tree in possible_trees:
 
3723
                basis_inv = basis_tree.inventory
 
3724
                for file_key in list(texts_possibly_new_in_tree):
 
3725
                    file_id, file_revision = file_key
 
3726
                    try:
 
3727
                        entry = basis_inv[file_id]
 
3728
                    except errors.NoSuchId:
 
3729
                        continue
 
3730
                    if entry.revision == file_revision:
 
3731
                        texts_possibly_new_in_tree.remove(file_key)
 
3732
            text_keys.update(texts_possibly_new_in_tree)
3644
3733
            pending_revisions.append(revision)
3645
3734
            cache[current_revision_id] = tree
3646
3735
            basis_id = current_revision_id
 
3736
        self.source._safe_to_return_from_cache = False
3647
3737
        # Copy file texts
3648
3738
        from_texts = self.source.texts
3649
3739
        to_texts = self.target.texts
3650
3740
        if root_keys_to_create:
3651
 
            root_stream = self._new_root_data_stream(root_keys_to_create,
3652
 
                                                     parent_map)
 
3741
            root_stream = _mod_fetch._new_root_data_stream(
 
3742
                root_keys_to_create, self._revision_id_to_root_id, parent_map,
 
3743
                self.source)
3653
3744
            to_texts.insert_record_stream(root_stream)
3654
3745
        to_texts.insert_record_stream(from_texts.get_record_stream(
3655
3746
            text_keys, self.target._format._fetch_order,
3662
3753
            # for the new revisions that we are about to insert.  We do this
3663
3754
            # before adding the revisions so that no revision is added until
3664
3755
            # all the inventories it may depend on are added.
 
3756
            # Note that this is overzealous, as we may have fetched these in an
 
3757
            # earlier batch.
3665
3758
            parent_ids = set()
3666
3759
            revision_ids = set()
3667
3760
            for revision in pending_revisions:
3670
3763
            parent_ids.difference_update(revision_ids)
3671
3764
            parent_ids.discard(_mod_revision.NULL_REVISION)
3672
3765
            parent_map = self.source.get_parent_map(parent_ids)
3673
 
            for parent_tree in self.source.revision_trees(parent_ids):
3674
 
                basis_id, delta = self._get_delta_for_revision(tree, parent_ids, basis_id, cache)
 
3766
            # we iterate over parent_map and not parent_ids because we don't
 
3767
            # want to try copying any revision which is a ghost
 
3768
            for parent_tree in self.source.revision_trees(parent_map):
3675
3769
                current_revision_id = parent_tree.get_revision_id()
3676
3770
                parents_parents = parent_map[current_revision_id]
 
3771
                possible_trees = self._get_trees(parents_parents, cache)
 
3772
                if len(possible_trees) == 0:
 
3773
                    # There either aren't any parents, or the parents are
 
3774
                    # ghosts, so just use the last converted tree.
 
3775
                    possible_trees.append((basis_id, cache[basis_id]))
 
3776
                basis_id, delta = self._get_delta_for_revision(parent_tree,
 
3777
                    parents_parents, possible_trees)
3677
3778
                self.target.add_inventory_by_delta(
3678
3779
                    basis_id, delta, current_revision_id, parents_parents)
3679
3780
        # insert signatures and revisions
3693
3794
 
3694
3795
        :param revision_ids: The list of revisions to fetch. Must be in
3695
3796
            topological order.
3696
 
        :param pb: A ProgressBar
 
3797
        :param pb: A ProgressTask
3697
3798
        :return: None
3698
3799
        """
3699
3800
        basis_id, basis_tree = self._get_basis(revision_ids[0])
3702
3803
        cache[basis_id] = basis_tree
3703
3804
        del basis_tree # We don't want to hang on to it here
3704
3805
        hints = []
 
3806
        a_graph = None
 
3807
 
3705
3808
        for offset in range(0, len(revision_ids), batch_size):
3706
3809
            self.target.start_write_group()
3707
3810
            try:
3710
3813
                batch = revision_ids[offset:offset+batch_size]
3711
3814
                basis_id = self._fetch_batch(batch, basis_id, cache)
3712
3815
            except:
 
3816
                self.source._safe_to_return_from_cache = False
3713
3817
                self.target.abort_write_group()
3714
3818
                raise
3715
3819
            else:
3727
3831
        """See InterRepository.fetch()."""
3728
3832
        if fetch_spec is not None:
3729
3833
            raise AssertionError("Not implemented yet...")
 
3834
        ui.ui_factory.warn_experimental_format_fetch(self)
3730
3835
        if (not self.source.supports_rich_root()
3731
3836
            and self.target.supports_rich_root()):
3732
3837
            self._converting_to_rich_root = True
3733
3838
            self._revision_id_to_root_id = {}
3734
3839
        else:
3735
3840
            self._converting_to_rich_root = False
 
3841
        # See <https://launchpad.net/bugs/456077> asking for a warning here
 
3842
        if self.source._format.network_name() != self.target._format.network_name():
 
3843
            ui.ui_factory.show_user_warning('cross_format_fetch',
 
3844
                from_format=self.source._format,
 
3845
                to_format=self.target._format)
3736
3846
        revision_ids = self.target.search_missing_revision_ids(self.source,
3737
3847
            revision_id, find_ghosts=find_ghosts).get_keys()
3738
3848
        if not revision_ids:
3744
3854
        # Walk though all revisions; get inventory deltas, copy referenced
3745
3855
        # texts that delta references, insert the delta, revision and
3746
3856
        # signature.
3747
 
        first_rev = self.source.get_revision(revision_ids[0])
3748
3857
        if pb is None:
3749
3858
            my_pb = ui.ui_factory.nested_progress_bar()
3750
3859
            pb = my_pb
3774
3883
            basis_id = first_rev.parent_ids[0]
3775
3884
            # only valid as a basis if the target has it
3776
3885
            self.target.get_revision(basis_id)
3777
 
            # Try to get a basis tree - if its a ghost it will hit the
 
3886
            # Try to get a basis tree - if it's a ghost it will hit the
3778
3887
            # NoSuchRevision case.
3779
3888
            basis_tree = self.source.revision_tree(basis_id)
3780
3889
        except (IndexError, errors.NoSuchRevision):
3785
3894
 
3786
3895
InterRepository.register_optimiser(InterDifferingSerializer)
3787
3896
InterRepository.register_optimiser(InterSameDataRepository)
3788
 
InterRepository.register_optimiser(InterWeaveRepo)
3789
 
InterRepository.register_optimiser(InterKnitRepo)
3790
3897
 
3791
3898
 
3792
3899
class CopyConverter(object):
3808
3915
        :param to_convert: The disk object to convert.
3809
3916
        :param pb: a progress bar to use for progress information.
3810
3917
        """
3811
 
        self.pb = pb
 
3918
        pb = ui.ui_factory.nested_progress_bar()
3812
3919
        self.count = 0
3813
3920
        self.total = 4
3814
3921
        # this is only useful with metadir layouts - separated repo content.
3815
3922
        # trigger an assertion if not such
3816
3923
        repo._format.get_format_string()
3817
3924
        self.repo_dir = repo.bzrdir
3818
 
        self.step('Moving repository to repository.backup')
 
3925
        pb.update('Moving repository to repository.backup')
3819
3926
        self.repo_dir.transport.move('repository', 'repository.backup')
3820
3927
        backup_transport =  self.repo_dir.transport.clone('repository.backup')
3821
3928
        repo._format.check_conversion_target(self.target_format)
3822
3929
        self.source_repo = repo._format.open(self.repo_dir,
3823
3930
            _found=True,
3824
3931
            _override_transport=backup_transport)
3825
 
        self.step('Creating new repository')
 
3932
        pb.update('Creating new repository')
3826
3933
        converted = self.target_format.initialize(self.repo_dir,
3827
3934
                                                  self.source_repo.is_shared())
3828
3935
        converted.lock_write()
3829
3936
        try:
3830
 
            self.step('Copying content into repository.')
 
3937
            pb.update('Copying content')
3831
3938
            self.source_repo.copy_content_into(converted)
3832
3939
        finally:
3833
3940
            converted.unlock()
3834
 
        self.step('Deleting old repository content.')
 
3941
        pb.update('Deleting old repository content')
3835
3942
        self.repo_dir.transport.delete_tree('repository.backup')
3836
 
        self.pb.note('repository converted')
3837
 
 
3838
 
    def step(self, message):
3839
 
        """Update the pb by a step."""
3840
 
        self.count +=1
3841
 
        self.pb.update(message, self.count, self.total)
 
3943
        ui.ui_factory.note('repository converted')
 
3944
        pb.finished()
3842
3945
 
3843
3946
 
3844
3947
_unescape_map = {
3873
3976
 
3874
3977
class _VersionedFileChecker(object):
3875
3978
 
3876
 
    def __init__(self, repository, text_key_references=None):
 
3979
    def __init__(self, repository, text_key_references=None, ancestors=None):
3877
3980
        self.repository = repository
3878
3981
        self.text_index = self.repository._generate_text_key_index(
3879
 
            text_key_references=text_key_references)
 
3982
            text_key_references=text_key_references, ancestors=ancestors)
3880
3983
 
3881
3984
    def calculate_file_version_parents(self, text_key):
3882
3985
        """Calculate the correct parents for a file version according to
3900
4003
            revision_id) tuples for versions that are present in this versioned
3901
4004
            file, but not used by the corresponding inventory.
3902
4005
        """
 
4006
        local_progress = None
 
4007
        if progress_bar is None:
 
4008
            local_progress = ui.ui_factory.nested_progress_bar()
 
4009
            progress_bar = local_progress
 
4010
        try:
 
4011
            return self._check_file_version_parents(texts, progress_bar)
 
4012
        finally:
 
4013
            if local_progress:
 
4014
                local_progress.finished()
 
4015
 
 
4016
    def _check_file_version_parents(self, texts, progress_bar):
 
4017
        """See check_file_version_parents."""
3903
4018
        wrong_parents = {}
3904
4019
        self.file_ids = set([file_id for file_id, _ in
3905
4020
            self.text_index.iterkeys()])
3906
4021
        # text keys is now grouped by file_id
3907
 
        n_weaves = len(self.file_ids)
3908
 
        files_in_revisions = {}
3909
 
        revisions_of_files = {}
3910
4022
        n_versions = len(self.text_index)
3911
4023
        progress_bar.update('loading text store', 0, n_versions)
3912
4024
        parent_map = self.repository.texts.get_parent_map(self.text_index)
3914
4026
        text_keys = self.repository.texts.keys()
3915
4027
        unused_keys = frozenset(text_keys) - set(self.text_index)
3916
4028
        for num, key in enumerate(self.text_index.iterkeys()):
3917
 
            if progress_bar is not None:
3918
 
                progress_bar.update('checking text graph', num, n_versions)
 
4029
            progress_bar.update('checking text graph', num, n_versions)
3919
4030
            correct_parents = self.calculate_file_version_parents(key)
3920
4031
            try:
3921
4032
                knit_parents = parent_map[key]
3976
4087
                is_resume = False
3977
4088
            try:
3978
4089
                # locked_insert_stream performs a commit|suspend.
3979
 
                return self._locked_insert_stream(stream, src_format, is_resume)
 
4090
                return self._locked_insert_stream(stream, src_format,
 
4091
                    is_resume)
3980
4092
            except:
3981
4093
                self.target_repo.abort_write_group(suppress_errors=True)
3982
4094
                raise
4006
4118
            else:
4007
4119
                new_pack.set_write_cache_size(1024*1024)
4008
4120
        for substream_type, substream in stream:
 
4121
            if 'stream' in debug.debug_flags:
 
4122
                mutter('inserting substream: %s', substream_type)
4009
4123
            if substream_type == 'texts':
4010
4124
                self.target_repo.texts.insert_record_stream(substream)
4011
4125
            elif substream_type == 'inventories':
4015
4129
                else:
4016
4130
                    self._extract_and_insert_inventories(
4017
4131
                        substream, src_serializer)
 
4132
            elif substream_type == 'inventory-deltas':
 
4133
                self._extract_and_insert_inventory_deltas(
 
4134
                    substream, src_serializer)
4018
4135
            elif substream_type == 'chk_bytes':
4019
4136
                # XXX: This doesn't support conversions, as it assumes the
4020
4137
                #      conversion was done in the fetch code.
4024
4141
                # required if the serializers are different only in terms of
4025
4142
                # the inventory.
4026
4143
                if src_serializer == to_serializer:
4027
 
                    self.target_repo.revisions.insert_record_stream(
4028
 
                        substream)
 
4144
                    self.target_repo.revisions.insert_record_stream(substream)
4029
4145
                else:
4030
4146
                    self._extract_and_insert_revisions(substream,
4031
4147
                        src_serializer)
4051
4167
                ):
4052
4168
                if versioned_file is None:
4053
4169
                    continue
 
4170
                # TODO: key is often going to be a StaticTuple object
 
4171
                #       I don't believe we can define a method by which
 
4172
                #       (prefix,) + StaticTuple will work, though we could
 
4173
                #       define a StaticTuple.sq_concat that would allow you to
 
4174
                #       pass in either a tuple or a StaticTuple as the second
 
4175
                #       object, so instead we could have:
 
4176
                #       StaticTuple(prefix) + key here...
4054
4177
                missing_keys.update((prefix,) + key for key in
4055
4178
                    versioned_file.get_missing_compression_parent_keys())
4056
4179
        except NotImplementedError:
4071
4194
            self.target_repo.pack(hint=hint)
4072
4195
        return [], set()
4073
4196
 
4074
 
    def _extract_and_insert_inventories(self, substream, serializer):
 
4197
    def _extract_and_insert_inventory_deltas(self, substream, serializer):
 
4198
        target_rich_root = self.target_repo._format.rich_root_data
 
4199
        target_tree_refs = self.target_repo._format.supports_tree_reference
 
4200
        for record in substream:
 
4201
            # Insert the delta directly
 
4202
            inventory_delta_bytes = record.get_bytes_as('fulltext')
 
4203
            deserialiser = inventory_delta.InventoryDeltaDeserializer()
 
4204
            try:
 
4205
                parse_result = deserialiser.parse_text_bytes(
 
4206
                    inventory_delta_bytes)
 
4207
            except inventory_delta.IncompatibleInventoryDelta, err:
 
4208
                trace.mutter("Incompatible delta: %s", err.msg)
 
4209
                raise errors.IncompatibleRevision(self.target_repo._format)
 
4210
            basis_id, new_id, rich_root, tree_refs, inv_delta = parse_result
 
4211
            revision_id = new_id
 
4212
            parents = [key[0] for key in record.parents]
 
4213
            self.target_repo.add_inventory_by_delta(
 
4214
                basis_id, inv_delta, revision_id, parents)
 
4215
 
 
4216
    def _extract_and_insert_inventories(self, substream, serializer,
 
4217
            parse_delta=None):
4075
4218
        """Generate a new inventory versionedfile in target, converting data.
4076
4219
 
4077
4220
        The inventory is retrieved from the source, (deserializing it), and
4078
4221
        stored in the target (reserializing it in a different format).
4079
4222
        """
 
4223
        target_rich_root = self.target_repo._format.rich_root_data
 
4224
        target_tree_refs = self.target_repo._format.supports_tree_reference
4080
4225
        for record in substream:
 
4226
            # It's not a delta, so it must be a fulltext in the source
 
4227
            # serializer's format.
4081
4228
            bytes = record.get_bytes_as('fulltext')
4082
4229
            revision_id = record.key[0]
4083
4230
            inv = serializer.read_inventory_from_string(bytes, revision_id)
4084
4231
            parents = [key[0] for key in record.parents]
4085
4232
            self.target_repo.add_inventory(revision_id, inv, parents)
 
4233
            # No need to keep holding this full inv in memory when the rest of
 
4234
            # the substream is likely to be all deltas.
 
4235
            del inv
4086
4236
 
4087
4237
    def _extract_and_insert_revisions(self, substream, serializer):
4088
4238
        for record in substream:
4105
4255
        """Create a StreamSource streaming from from_repository."""
4106
4256
        self.from_repository = from_repository
4107
4257
        self.to_format = to_format
 
4258
        self._record_counter = RecordCounter()
4108
4259
 
4109
4260
    def delta_on_metadata(self):
4110
4261
        """Return True if delta's are permitted on metadata streams.
4137
4288
        return [('signatures', signatures), ('revisions', revisions)]
4138
4289
 
4139
4290
    def _generate_root_texts(self, revs):
4140
 
        """This will be called by __fetch between fetching weave texts and
 
4291
        """This will be called by get_stream between fetching weave texts and
4141
4292
        fetching the inventory weave.
4142
 
 
4143
 
        Subclasses should override this if they need to generate root texts
4144
 
        after fetching weave texts.
4145
4293
        """
4146
4294
        if self._rich_root_upgrade():
4147
 
            import bzrlib.fetch
4148
 
            return bzrlib.fetch.Inter1and2Helper(
 
4295
            return _mod_fetch.Inter1and2Helper(
4149
4296
                self.from_repository).generate_root_texts(revs)
4150
4297
        else:
4151
4298
            return []
4154
4301
        phase = 'file'
4155
4302
        revs = search.get_keys()
4156
4303
        graph = self.from_repository.get_graph()
4157
 
        revs = list(graph.iter_topo_order(revs))
 
4304
        revs = tsort.topo_sort(graph.get_parent_map(revs))
4158
4305
        data_to_fetch = self.from_repository.item_keys_introduced_by(revs)
4159
4306
        text_keys = []
4160
4307
        for knit_kind, file_id, revisions in data_to_fetch:
4179
4326
                # will be valid.
4180
4327
                for _ in self._generate_root_texts(revs):
4181
4328
                    yield _
4182
 
                # NB: This currently reopens the inventory weave in source;
4183
 
                # using a single stream interface instead would avoid this.
4184
 
                from_weave = self.from_repository.inventories
4185
4329
                # we fetch only the referenced inventories because we do not
4186
4330
                # know for unselected inventories whether all their required
4187
4331
                # texts are present in the other repository - it could be
4226
4370
            if not keys:
4227
4371
                # No need to stream something we don't have
4228
4372
                continue
 
4373
            if substream_kind == 'inventories':
 
4374
                # Some missing keys are genuinely ghosts, filter those out.
 
4375
                present = self.from_repository.inventories.get_parent_map(keys)
 
4376
                revs = [key[0] for key in present]
 
4377
                # Get the inventory stream more-or-less as we do for the
 
4378
                # original stream; there's no reason to assume that records
 
4379
                # direct from the source will be suitable for the sink.  (Think
 
4380
                # e.g. 2a -> 1.9-rich-root).
 
4381
                for info in self._get_inventory_stream(revs, missing=True):
 
4382
                    yield info
 
4383
                continue
 
4384
 
4229
4385
            # Ask for full texts always so that we don't need more round trips
4230
4386
            # after this stream.
4231
4387
            # Some of the missing keys are genuinely ghosts, so filter absent
4246
4402
        return (not self.from_repository._format.rich_root_data and
4247
4403
            self.to_format.rich_root_data)
4248
4404
 
4249
 
    def _get_inventory_stream(self, revision_ids):
 
4405
    def _get_inventory_stream(self, revision_ids, missing=False):
4250
4406
        from_format = self.from_repository._format
4251
 
        if (from_format.supports_chks and self.to_format.supports_chks
4252
 
            and (from_format._serializer == self.to_format._serializer)):
4253
 
            # Both sides support chks, and they use the same serializer, so it
4254
 
            # is safe to transmit the chk pages and inventory pages across
4255
 
            # as-is.
4256
 
            return self._get_chk_inventory_stream(revision_ids)
4257
 
        elif (not from_format.supports_chks):
4258
 
            # Source repository doesn't support chks. So we can transmit the
4259
 
            # inventories 'as-is' and either they are just accepted on the
4260
 
            # target, or the Sink will properly convert it.
4261
 
            return self._get_simple_inventory_stream(revision_ids)
 
4407
        if (from_format.supports_chks and self.to_format.supports_chks and
 
4408
            from_format.network_name() == self.to_format.network_name()):
 
4409
            raise AssertionError(
 
4410
                "this case should be handled by GroupCHKStreamSource")
 
4411
        elif 'forceinvdeltas' in debug.debug_flags:
 
4412
            return self._get_convertable_inventory_stream(revision_ids,
 
4413
                    delta_versus_null=missing)
 
4414
        elif from_format.network_name() == self.to_format.network_name():
 
4415
            # Same format.
 
4416
            return self._get_simple_inventory_stream(revision_ids,
 
4417
                    missing=missing)
 
4418
        elif (not from_format.supports_chks and not self.to_format.supports_chks
 
4419
                and from_format._serializer == self.to_format._serializer):
 
4420
            # Essentially the same format.
 
4421
            return self._get_simple_inventory_stream(revision_ids,
 
4422
                    missing=missing)
4262
4423
        else:
4263
 
            # XXX: Hack to make not-chk->chk fetch: copy the inventories as
4264
 
            #      inventories. Note that this should probably be done somehow
4265
 
            #      as part of bzrlib.repository.StreamSink. Except JAM couldn't
4266
 
            #      figure out how a non-chk repository could possibly handle
4267
 
            #      deserializing an inventory stream from a chk repo, as it
4268
 
            #      doesn't have a way to understand individual pages.
4269
 
            return self._get_convertable_inventory_stream(revision_ids)
 
4424
            # Any time we switch serializations, we want to use an
 
4425
            # inventory-delta based approach.
 
4426
            return self._get_convertable_inventory_stream(revision_ids,
 
4427
                    delta_versus_null=missing)
4270
4428
 
4271
 
    def _get_simple_inventory_stream(self, revision_ids):
 
4429
    def _get_simple_inventory_stream(self, revision_ids, missing=False):
 
4430
        # NB: This currently reopens the inventory weave in source;
 
4431
        # using a single stream interface instead would avoid this.
4272
4432
        from_weave = self.from_repository.inventories
 
4433
        if missing:
 
4434
            delta_closure = True
 
4435
        else:
 
4436
            delta_closure = not self.delta_on_metadata()
4273
4437
        yield ('inventories', from_weave.get_record_stream(
4274
4438
            [(rev_id,) for rev_id in revision_ids],
4275
 
            self.inventory_fetch_order(),
4276
 
            not self.delta_on_metadata()))
4277
 
 
4278
 
    def _get_chk_inventory_stream(self, revision_ids):
4279
 
        """Fetch the inventory texts, along with the associated chk maps."""
4280
 
        # We want an inventory outside of the search set, so that we can filter
4281
 
        # out uninteresting chk pages. For now we use
4282
 
        # _find_revision_outside_set, but if we had a Search with cut_revs, we
4283
 
        # could use that instead.
4284
 
        start_rev_id = self.from_repository._find_revision_outside_set(
4285
 
                            revision_ids)
4286
 
        start_rev_key = (start_rev_id,)
4287
 
        inv_keys_to_fetch = [(rev_id,) for rev_id in revision_ids]
4288
 
        if start_rev_id != _mod_revision.NULL_REVISION:
4289
 
            inv_keys_to_fetch.append((start_rev_id,))
4290
 
        # Any repo that supports chk_bytes must also support out-of-order
4291
 
        # insertion. At least, that is how we expect it to work
4292
 
        # We use get_record_stream instead of iter_inventories because we want
4293
 
        # to be able to insert the stream as well. We could instead fetch
4294
 
        # allowing deltas, and then iter_inventories, but we don't know whether
4295
 
        # source or target is more 'local' anway.
4296
 
        inv_stream = self.from_repository.inventories.get_record_stream(
4297
 
            inv_keys_to_fetch, 'unordered',
4298
 
            True) # We need them as full-texts so we can find their references
4299
 
        uninteresting_chk_roots = set()
4300
 
        interesting_chk_roots = set()
4301
 
        def filter_inv_stream(inv_stream):
4302
 
            for idx, record in enumerate(inv_stream):
4303
 
                ### child_pb.update('fetch inv', idx, len(inv_keys_to_fetch))
4304
 
                bytes = record.get_bytes_as('fulltext')
4305
 
                chk_inv = inventory.CHKInventory.deserialise(
4306
 
                    self.from_repository.chk_bytes, bytes, record.key)
4307
 
                if record.key == start_rev_key:
4308
 
                    uninteresting_chk_roots.add(chk_inv.id_to_entry.key())
4309
 
                    p_id_map = chk_inv.parent_id_basename_to_file_id
4310
 
                    if p_id_map is not None:
4311
 
                        uninteresting_chk_roots.add(p_id_map.key())
4312
 
                else:
4313
 
                    yield record
4314
 
                    interesting_chk_roots.add(chk_inv.id_to_entry.key())
4315
 
                    p_id_map = chk_inv.parent_id_basename_to_file_id
4316
 
                    if p_id_map is not None:
4317
 
                        interesting_chk_roots.add(p_id_map.key())
4318
 
        ### pb.update('fetch inventory', 0, 2)
4319
 
        yield ('inventories', filter_inv_stream(inv_stream))
4320
 
        # Now that we have worked out all of the interesting root nodes, grab
4321
 
        # all of the interesting pages and insert them
4322
 
        ### pb.update('fetch inventory', 1, 2)
4323
 
        interesting = chk_map.iter_interesting_nodes(
4324
 
            self.from_repository.chk_bytes, interesting_chk_roots,
4325
 
            uninteresting_chk_roots)
4326
 
        def to_stream_adapter():
4327
 
            """Adapt the iter_interesting_nodes result to a single stream.
4328
 
 
4329
 
            iter_interesting_nodes returns records as it processes them, along
4330
 
            with keys. However, we only want to return the records themselves.
4331
 
            """
4332
 
            for record, items in interesting:
4333
 
                if record is not None:
4334
 
                    yield record
4335
 
        # XXX: We could instead call get_record_stream(records.keys())
4336
 
        #      ATM, this will always insert the records as fulltexts, and
4337
 
        #      requires that you can hang on to records once you have gone
4338
 
        #      on to the next one. Further, it causes the target to
4339
 
        #      recompress the data. Testing shows it to be faster than
4340
 
        #      requesting the records again, though.
4341
 
        yield ('chk_bytes', to_stream_adapter())
4342
 
        ### pb.update('fetch inventory', 2, 2)
4343
 
 
4344
 
    def _get_convertable_inventory_stream(self, revision_ids):
4345
 
        # XXX: One of source or target is using chks, and they don't have
4346
 
        #      compatible serializations. The StreamSink code expects to be
4347
 
        #      able to convert on the target, so we need to put
4348
 
        #      bytes-on-the-wire that can be converted
4349
 
        yield ('inventories', self._stream_invs_as_fulltexts(revision_ids))
4350
 
 
4351
 
    def _stream_invs_as_fulltexts(self, revision_ids):
 
4439
            self.inventory_fetch_order(), delta_closure))
 
4440
 
 
4441
    def _get_convertable_inventory_stream(self, revision_ids,
 
4442
                                          delta_versus_null=False):
 
4443
        # The two formats are sufficiently different that there is no fast
 
4444
        # path, so we need to send just inventorydeltas, which any
 
4445
        # sufficiently modern client can insert into any repository.
 
4446
        # The StreamSink code expects to be able to
 
4447
        # convert on the target, so we need to put bytes-on-the-wire that can
 
4448
        # be converted.  That means inventory deltas (if the remote is <1.19,
 
4449
        # RemoteStreamSink will fallback to VFS to insert the deltas).
 
4450
        yield ('inventory-deltas',
 
4451
           self._stream_invs_as_deltas(revision_ids,
 
4452
                                       delta_versus_null=delta_versus_null))
 
4453
 
 
4454
    def _stream_invs_as_deltas(self, revision_ids, delta_versus_null=False):
 
4455
        """Return a stream of inventory-deltas for the given rev ids.
 
4456
 
 
4457
        :param revision_ids: The list of inventories to transmit
 
4458
        :param delta_versus_null: Don't try to find a minimal delta for this
 
4459
            entry, instead compute the delta versus the NULL_REVISION. This
 
4460
            effectively streams a complete inventory. Used for stuff like
 
4461
            filling in missing parents, etc.
 
4462
        """
4352
4463
        from_repo = self.from_repository
4353
 
        from_serializer = from_repo._format._serializer
4354
4464
        revision_keys = [(rev_id,) for rev_id in revision_ids]
4355
4465
        parent_map = from_repo.inventories.get_parent_map(revision_keys)
4356
 
        for inv in self.from_repository.iter_inventories(revision_ids):
4357
 
            # XXX: This is a bit hackish, but it works. Basically,
4358
 
            #      CHKSerializer 'accidentally' supports
4359
 
            #      read/write_inventory_to_string, even though that is never
4360
 
            #      the format that is stored on disk. It *does* give us a
4361
 
            #      single string representation for an inventory, so live with
4362
 
            #      it for now.
4363
 
            #      This would be far better if we had a 'serialized inventory
4364
 
            #      delta' form. Then we could use 'inventory._make_delta', and
4365
 
            #      transmit that. This would both be faster to generate, and
4366
 
            #      result in fewer bytes-on-the-wire.
4367
 
            as_bytes = from_serializer.write_inventory_to_string(inv)
 
4466
        # XXX: possibly repos could implement a more efficient iter_inv_deltas
 
4467
        # method...
 
4468
        inventories = self.from_repository.iter_inventories(
 
4469
            revision_ids, 'topological')
 
4470
        format = from_repo._format
 
4471
        invs_sent_so_far = set([_mod_revision.NULL_REVISION])
 
4472
        inventory_cache = lru_cache.LRUCache(50)
 
4473
        null_inventory = from_repo.revision_tree(
 
4474
            _mod_revision.NULL_REVISION).inventory
 
4475
        # XXX: ideally the rich-root/tree-refs flags would be per-revision, not
 
4476
        # per-repo (e.g.  streaming a non-rich-root revision out of a rich-root
 
4477
        # repo back into a non-rich-root repo ought to be allowed)
 
4478
        serializer = inventory_delta.InventoryDeltaSerializer(
 
4479
            versioned_root=format.rich_root_data,
 
4480
            tree_references=format.supports_tree_reference)
 
4481
        for inv in inventories:
4368
4482
            key = (inv.revision_id,)
4369
4483
            parent_keys = parent_map.get(key, ())
 
4484
            delta = None
 
4485
            if not delta_versus_null and parent_keys:
 
4486
                # The caller did not ask for complete inventories and we have
 
4487
                # some parents that we can delta against.  Make a delta against
 
4488
                # each parent so that we can find the smallest.
 
4489
                parent_ids = [parent_key[0] for parent_key in parent_keys]
 
4490
                for parent_id in parent_ids:
 
4491
                    if parent_id not in invs_sent_so_far:
 
4492
                        # We don't know that the remote side has this basis, so
 
4493
                        # we can't use it.
 
4494
                        continue
 
4495
                    if parent_id == _mod_revision.NULL_REVISION:
 
4496
                        parent_inv = null_inventory
 
4497
                    else:
 
4498
                        parent_inv = inventory_cache.get(parent_id, None)
 
4499
                        if parent_inv is None:
 
4500
                            parent_inv = from_repo.get_inventory(parent_id)
 
4501
                    candidate_delta = inv._make_delta(parent_inv)
 
4502
                    if (delta is None or
 
4503
                        len(delta) > len(candidate_delta)):
 
4504
                        delta = candidate_delta
 
4505
                        basis_id = parent_id
 
4506
            if delta is None:
 
4507
                # Either none of the parents ended up being suitable, or we
 
4508
                # were asked to delta against NULL
 
4509
                basis_id = _mod_revision.NULL_REVISION
 
4510
                delta = inv._make_delta(null_inventory)
 
4511
            invs_sent_so_far.add(inv.revision_id)
 
4512
            inventory_cache[inv.revision_id] = inv
 
4513
            delta_serialized = ''.join(
 
4514
                serializer.delta_to_lines(basis_id, key[-1], delta))
4370
4515
            yield versionedfile.FulltextContentFactory(
4371
 
                key, parent_keys, None, as_bytes)
 
4516
                key, parent_keys, None, delta_serialized)
4372
4517
 
4373
4518
 
4374
4519
def _iter_for_revno(repo, partial_history_cache, stop_index=None,