/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to breezy/bzr/vf_repository.py

  • Committer: Breezy landing bot
  • Author(s): Jelmer Vernooij
  • Date: 2017-06-20 22:54:06 UTC
  • mfrom: (6700.2.5 remove-record-entry-contents)
  • Revision ID: breezy.the.bot@gmail.com-20170620225406-0my0i6wr2ow0o7g7
Remove remaining implementations of CommitBuilder.record_{entry_contents,delete}.

Merged from https://code.launchpad.net/~jelmer/brz/remove-record-entry-contents/+merge/325968

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005-2011 Canonical Ltd
 
2
#
 
3
# This program is free software; you can redistribute it and/or modify
 
4
# it under the terms of the GNU General Public License as published by
 
5
# the Free Software Foundation; either version 2 of the License, or
 
6
# (at your option) any later version.
 
7
#
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
#
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software
 
15
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
16
 
 
17
"""Repository formats built around versioned files."""
 
18
 
 
19
from __future__ import absolute_import
 
20
 
 
21
 
 
22
from ..lazy_import import lazy_import
 
23
lazy_import(globals(), """
 
24
import itertools
 
25
 
 
26
from breezy import (
 
27
    check,
 
28
    config as _mod_config,
 
29
    debug,
 
30
    fetch as _mod_fetch,
 
31
    fifo_cache,
 
32
    gpg,
 
33
    graph,
 
34
    lru_cache,
 
35
    osutils,
 
36
    revision as _mod_revision,
 
37
    serializer as _mod_serializer,
 
38
    static_tuple,
 
39
    tsort,
 
40
    ui,
 
41
    )
 
42
from breezy.bzr import (
 
43
    inventory_delta,
 
44
    inventorytree,
 
45
    versionedfile,
 
46
    vf_search,
 
47
    )
 
48
 
 
49
from breezy.recordcounter import RecordCounter
 
50
from breezy.testament import Testament
 
51
from breezy.i18n import gettext
 
52
""")
 
53
 
 
54
from .. import (
 
55
    errors,
 
56
    )
 
57
from ..decorators import (
 
58
    needs_read_lock,
 
59
    needs_write_lock,
 
60
    only_raises,
 
61
    )
 
62
from .inventory import (
 
63
    Inventory,
 
64
    InventoryDirectory,
 
65
    ROOT_ID,
 
66
    entry_factory,
 
67
    )
 
68
 
 
69
from ..repository import (
 
70
    CommitBuilder,
 
71
    InterRepository,
 
72
    Repository,
 
73
    RepositoryFormat,
 
74
    )
 
75
from .repository import (
 
76
    MetaDirRepository,
 
77
    RepositoryFormatMetaDir,
 
78
    )
 
79
 
 
80
from ..sixish import (
 
81
    range,
 
82
    viewitems,
 
83
    viewvalues,
 
84
    )
 
85
 
 
86
from ..trace import (
 
87
    mutter
 
88
    )
 
89
 
 
90
 
 
91
class VersionedFileRepositoryFormat(RepositoryFormat):
 
92
    """Base class for all repository formats that are VersionedFiles-based."""
 
93
 
 
94
    supports_full_versioned_files = True
 
95
    supports_versioned_directories = True
 
96
    supports_unreferenced_revisions = True
 
97
 
 
98
    # Should commit add an inventory, or an inventory delta to the repository.
 
99
    _commit_inv_deltas = True
 
100
    # What order should fetch operations request streams in?
 
101
    # The default is unordered as that is the cheapest for an origin to
 
102
    # provide.
 
103
    _fetch_order = 'unordered'
 
104
    # Does this repository format use deltas that can be fetched as-deltas ?
 
105
    # (E.g. knits, where the knit deltas can be transplanted intact.
 
106
    # We default to False, which will ensure that enough data to get
 
107
    # a full text out of any fetch stream will be grabbed.
 
108
    _fetch_uses_deltas = False
 
109
 
 
110
 
 
111
class VersionedFileCommitBuilder(CommitBuilder):
 
112
    """Commit builder implementation for versioned files based repositories.
 
113
    """
 
114
 
 
115
    # the default CommitBuilder does not manage trees whose root is versioned.
 
116
    _versioned_root = False
 
117
 
 
118
    def __init__(self, repository, parents, config_stack, timestamp=None,
 
119
                 timezone=None, committer=None, revprops=None,
 
120
                 revision_id=None, lossy=False):
 
121
        super(VersionedFileCommitBuilder, self).__init__(repository,
 
122
            parents, config_stack, timestamp, timezone, committer, revprops,
 
123
            revision_id, lossy)
 
124
        try:
 
125
            basis_id = self.parents[0]
 
126
        except IndexError:
 
127
            basis_id = _mod_revision.NULL_REVISION
 
128
        self.basis_delta_revision = basis_id
 
129
        self._new_inventory = None
 
130
        self._basis_delta = []
 
131
        self.__heads = graph.HeadsCache(repository.get_graph()).heads
 
132
        # memo'd check for no-op commits.
 
133
        self._any_changes = False
 
134
 
 
135
    def any_changes(self):
 
136
        """Return True if any entries were changed.
 
137
 
 
138
        This includes merge-only changes. It is the core for the --unchanged
 
139
        detection in commit.
 
140
 
 
141
        :return: True if any changes have occured.
 
142
        """
 
143
        return self._any_changes
 
144
 
 
145
    def _ensure_fallback_inventories(self):
 
146
        """Ensure that appropriate inventories are available.
 
147
 
 
148
        This only applies to repositories that are stacked, and is about
 
149
        enusring the stacking invariants. Namely, that for any revision that is
 
150
        present, we either have all of the file content, or we have the parent
 
151
        inventory and the delta file content.
 
152
        """
 
153
        if not self.repository._fallback_repositories:
 
154
            return
 
155
        if not self.repository._format.supports_chks:
 
156
            raise errors.BzrError("Cannot commit directly to a stacked branch"
 
157
                " in pre-2a formats. See "
 
158
                "https://bugs.launchpad.net/bzr/+bug/375013 for details.")
 
159
        # This is a stacked repo, we need to make sure we have the parent
 
160
        # inventories for the parents.
 
161
        parent_keys = [(p,) for p in self.parents]
 
162
        parent_map = self.repository.inventories._index.get_parent_map(parent_keys)
 
163
        missing_parent_keys = {pk for pk in parent_keys
 
164
                                       if pk not in parent_map}
 
165
        fallback_repos = list(reversed(self.repository._fallback_repositories))
 
166
        missing_keys = [('inventories', pk[0])
 
167
                        for pk in missing_parent_keys]
 
168
        resume_tokens = []
 
169
        while missing_keys and fallback_repos:
 
170
            fallback_repo = fallback_repos.pop()
 
171
            source = fallback_repo._get_source(self.repository._format)
 
172
            sink = self.repository._get_sink()
 
173
            stream = source.get_stream_for_missing_keys(missing_keys)
 
174
            missing_keys = sink.insert_stream_without_locking(stream,
 
175
                self.repository._format)
 
176
        if missing_keys:
 
177
            raise errors.BzrError('Unable to fill in parent inventories for a'
 
178
                                  ' stacked branch')
 
179
 
 
180
    def commit(self, message):
 
181
        """Make the actual commit.
 
182
 
 
183
        :return: The revision id of the recorded revision.
 
184
        """
 
185
        self._validate_unicode_text(message, 'commit message')
 
186
        rev = _mod_revision.Revision(
 
187
                       timestamp=self._timestamp,
 
188
                       timezone=self._timezone,
 
189
                       committer=self._committer,
 
190
                       message=message,
 
191
                       inventory_sha1=self.inv_sha1,
 
192
                       revision_id=self._new_revision_id,
 
193
                       properties=self._revprops)
 
194
        rev.parent_ids = self.parents
 
195
        if self._config_stack.get('create_signatures') == _mod_config.SIGN_ALWAYS:
 
196
            testament = Testament(rev, self.revision_tree())
 
197
            plaintext = testament.as_short_text()
 
198
            self.repository.store_revision_signature(
 
199
                gpg.GPGStrategy(self._config_stack), plaintext,
 
200
                self._new_revision_id)
 
201
        self.repository._add_revision(rev)
 
202
        self._ensure_fallback_inventories()
 
203
        self.repository.commit_write_group()
 
204
        return self._new_revision_id
 
205
 
 
206
    def abort(self):
 
207
        """Abort the commit that is being built.
 
208
        """
 
209
        self.repository.abort_write_group()
 
210
 
 
211
    def revision_tree(self):
 
212
        """Return the tree that was just committed.
 
213
 
 
214
        After calling commit() this can be called to get a
 
215
        RevisionTree representing the newly committed tree. This is
 
216
        preferred to calling Repository.revision_tree() because that may
 
217
        require deserializing the inventory, while we already have a copy in
 
218
        memory.
 
219
        """
 
220
        if self._new_inventory is None:
 
221
            self._new_inventory = self.repository.get_inventory(
 
222
                self._new_revision_id)
 
223
        return inventorytree.InventoryRevisionTree(self.repository,
 
224
            self._new_inventory, self._new_revision_id)
 
225
 
 
226
    def finish_inventory(self):
 
227
        """Tell the builder that the inventory is finished.
 
228
 
 
229
        :return: The inventory id in the repository, which can be used with
 
230
            repository.get_inventory.
 
231
        """
 
232
        # an inventory delta was accumulated without creating a new
 
233
        # inventory.
 
234
        basis_id = self.basis_delta_revision
 
235
        self.inv_sha1, self._new_inventory = self.repository.add_inventory_by_delta(
 
236
            basis_id, self._basis_delta, self._new_revision_id,
 
237
            self.parents)
 
238
        return self._new_revision_id
 
239
 
 
240
    def _require_root_change(self, tree):
 
241
        """Enforce an appropriate root object change.
 
242
 
 
243
        This is called once when record_iter_changes is called, if and only if
 
244
        the root was not in the delta calculated by record_iter_changes.
 
245
 
 
246
        :param tree: The tree which is being committed.
 
247
        """
 
248
        if len(self.parents) == 0:
 
249
            raise errors.RootMissing()
 
250
        entry = entry_factory['directory'](tree.path2id(''), '',
 
251
            None)
 
252
        entry.revision = self._new_revision_id
 
253
        self._basis_delta.append(('', '', entry.file_id, entry))
 
254
 
 
255
    def _get_delta(self, ie, basis_inv, path):
 
256
        """Get a delta against the basis inventory for ie."""
 
257
        if not basis_inv.has_id(ie.file_id):
 
258
            # add
 
259
            result = (None, path, ie.file_id, ie)
 
260
            self._basis_delta.append(result)
 
261
            return result
 
262
        elif ie != basis_inv[ie.file_id]:
 
263
            # common but altered
 
264
            # TODO: avoid tis id2path call.
 
265
            result = (basis_inv.id2path(ie.file_id), path, ie.file_id, ie)
 
266
            self._basis_delta.append(result)
 
267
            return result
 
268
        else:
 
269
            # common, unaltered
 
270
            return None
 
271
 
 
272
    def _heads(self, file_id, revision_ids):
 
273
        """Calculate the graph heads for revision_ids in the graph of file_id.
 
274
 
 
275
        This can use either a per-file graph or a global revision graph as we
 
276
        have an identity relationship between the two graphs.
 
277
        """
 
278
        return self.__heads(revision_ids)
 
279
 
 
280
    def get_basis_delta(self):
 
281
        """Return the complete inventory delta versus the basis inventory.
 
282
 
 
283
        :return: An inventory delta, suitable for use with apply_delta, or
 
284
            Repository.add_inventory_by_delta, etc.
 
285
        """
 
286
        return self._basis_delta
 
287
 
 
288
    def record_iter_changes(self, tree, basis_revision_id, iter_changes,
 
289
        _entry_factory=entry_factory):
 
290
        """Record a new tree via iter_changes.
 
291
 
 
292
        :param tree: The tree to obtain text contents from for changed objects.
 
293
        :param basis_revision_id: The revision id of the tree the iter_changes
 
294
            has been generated against. Currently assumed to be the same
 
295
            as self.parents[0] - if it is not, errors may occur.
 
296
        :param iter_changes: An iter_changes iterator with the changes to apply
 
297
            to basis_revision_id. The iterator must not include any items with
 
298
            a current kind of None - missing items must be either filtered out
 
299
            or errored-on before record_iter_changes sees the item.
 
300
        :param _entry_factory: Private method to bind entry_factory locally for
 
301
            performance.
 
302
        :return: A generator of (file_id, relpath, fs_hash) tuples for use with
 
303
            tree._observed_sha1.
 
304
        """
 
305
        # Create an inventory delta based on deltas between all the parents and
 
306
        # deltas between all the parent inventories. We use inventory delta's 
 
307
        # between the inventory objects because iter_changes masks
 
308
        # last-changed-field only changes.
 
309
        # Working data:
 
310
        # file_id -> change map, change is fileid, paths, changed, versioneds,
 
311
        # parents, names, kinds, executables
 
312
        merged_ids = {}
 
313
        # {file_id -> revision_id -> inventory entry, for entries in parent
 
314
        # trees that are not parents[0]
 
315
        parent_entries = {}
 
316
        ghost_basis = False
 
317
        try:
 
318
            revtrees = list(self.repository.revision_trees(self.parents))
 
319
        except errors.NoSuchRevision:
 
320
            # one or more ghosts, slow path.
 
321
            revtrees = []
 
322
            for revision_id in self.parents:
 
323
                try:
 
324
                    revtrees.append(self.repository.revision_tree(revision_id))
 
325
                except errors.NoSuchRevision:
 
326
                    if not revtrees:
 
327
                        basis_revision_id = _mod_revision.NULL_REVISION
 
328
                        ghost_basis = True
 
329
                    revtrees.append(self.repository.revision_tree(
 
330
                        _mod_revision.NULL_REVISION))
 
331
        # The basis inventory from a repository 
 
332
        if revtrees:
 
333
            basis_tree = revtrees[0]
 
334
        else:
 
335
            basis_tree = self.repository.revision_tree(
 
336
                _mod_revision.NULL_REVISION)
 
337
        basis_inv = basis_tree.root_inventory
 
338
        if len(self.parents) > 0:
 
339
            if basis_revision_id != self.parents[0] and not ghost_basis:
 
340
                raise Exception(
 
341
                    "arbitrary basis parents not yet supported with merges")
 
342
            for revtree in revtrees[1:]:
 
343
                for change in revtree.root_inventory._make_delta(basis_inv):
 
344
                    if change[1] is None:
 
345
                        # Not present in this parent.
 
346
                        continue
 
347
                    if change[2] not in merged_ids:
 
348
                        if change[0] is not None:
 
349
                            basis_entry = basis_inv[change[2]]
 
350
                            merged_ids[change[2]] = [
 
351
                                # basis revid
 
352
                                basis_entry.revision,
 
353
                                # new tree revid
 
354
                                change[3].revision]
 
355
                            parent_entries[change[2]] = {
 
356
                                # basis parent
 
357
                                basis_entry.revision:basis_entry,
 
358
                                # this parent 
 
359
                                change[3].revision:change[3],
 
360
                                }
 
361
                        else:
 
362
                            merged_ids[change[2]] = [change[3].revision]
 
363
                            parent_entries[change[2]] = {change[3].revision:change[3]}
 
364
                    else:
 
365
                        merged_ids[change[2]].append(change[3].revision)
 
366
                        parent_entries[change[2]][change[3].revision] = change[3]
 
367
        else:
 
368
            merged_ids = {}
 
369
        # Setup the changes from the tree:
 
370
        # changes maps file_id -> (change, [parent revision_ids])
 
371
        changes= {}
 
372
        for change in iter_changes:
 
373
            # This probably looks up in basis_inv way to much.
 
374
            if change[1][0] is not None:
 
375
                head_candidate = [basis_inv[change[0]].revision]
 
376
            else:
 
377
                head_candidate = []
 
378
            changes[change[0]] = change, merged_ids.get(change[0],
 
379
                head_candidate)
 
380
        unchanged_merged = set(merged_ids) - set(changes)
 
381
        # Extend the changes dict with synthetic changes to record merges of
 
382
        # texts.
 
383
        for file_id in unchanged_merged:
 
384
            # Record a merged version of these items that did not change vs the
 
385
            # basis. This can be either identical parallel changes, or a revert
 
386
            # of a specific file after a merge. The recorded content will be
 
387
            # that of the current tree (which is the same as the basis), but
 
388
            # the per-file graph will reflect a merge.
 
389
            # NB:XXX: We are reconstructing path information we had, this
 
390
            # should be preserved instead.
 
391
            # inv delta  change: (file_id, (path_in_source, path_in_target),
 
392
            #   changed_content, versioned, parent, name, kind,
 
393
            #   executable)
 
394
            try:
 
395
                basis_entry = basis_inv[file_id]
 
396
            except errors.NoSuchId:
 
397
                # a change from basis->some_parents but file_id isn't in basis
 
398
                # so was new in the merge, which means it must have changed
 
399
                # from basis -> current, and as it hasn't the add was reverted
 
400
                # by the user. So we discard this change.
 
401
                pass
 
402
            else:
 
403
                change = (file_id,
 
404
                    (basis_inv.id2path(file_id), tree.id2path(file_id)),
 
405
                    False, (True, True),
 
406
                    (basis_entry.parent_id, basis_entry.parent_id),
 
407
                    (basis_entry.name, basis_entry.name),
 
408
                    (basis_entry.kind, basis_entry.kind),
 
409
                    (basis_entry.executable, basis_entry.executable))
 
410
                changes[file_id] = (change, merged_ids[file_id])
 
411
        # changes contains tuples with the change and a set of inventory
 
412
        # candidates for the file.
 
413
        # inv delta is:
 
414
        # old_path, new_path, file_id, new_inventory_entry
 
415
        seen_root = False # Is the root in the basis delta?
 
416
        inv_delta = self._basis_delta
 
417
        modified_rev = self._new_revision_id
 
418
        for change, head_candidates in viewvalues(changes):
 
419
            if change[3][1]: # versioned in target.
 
420
                # Several things may be happening here:
 
421
                # We may have a fork in the per-file graph
 
422
                #  - record a change with the content from tree
 
423
                # We may have a change against < all trees
 
424
                #  - carry over the tree that hasn't changed
 
425
                # We may have a change against all trees
 
426
                #  - record the change with the content from tree
 
427
                kind = change[6][1]
 
428
                file_id = change[0]
 
429
                entry = _entry_factory[kind](file_id, change[5][1],
 
430
                    change[4][1])
 
431
                head_set = self._heads(change[0], set(head_candidates))
 
432
                heads = []
 
433
                # Preserve ordering.
 
434
                for head_candidate in head_candidates:
 
435
                    if head_candidate in head_set:
 
436
                        heads.append(head_candidate)
 
437
                        head_set.remove(head_candidate)
 
438
                carried_over = False
 
439
                if len(heads) == 1:
 
440
                    # Could be a carry-over situation:
 
441
                    parent_entry_revs = parent_entries.get(file_id, None)
 
442
                    if parent_entry_revs:
 
443
                        parent_entry = parent_entry_revs.get(heads[0], None)
 
444
                    else:
 
445
                        parent_entry = None
 
446
                    if parent_entry is None:
 
447
                        # The parent iter_changes was called against is the one
 
448
                        # that is the per-file head, so any change is relevant
 
449
                        # iter_changes is valid.
 
450
                        carry_over_possible = False
 
451
                    else:
 
452
                        # could be a carry over situation
 
453
                        # A change against the basis may just indicate a merge,
 
454
                        # we need to check the content against the source of the
 
455
                        # merge to determine if it was changed after the merge
 
456
                        # or carried over.
 
457
                        if (parent_entry.kind != entry.kind or
 
458
                            parent_entry.parent_id != entry.parent_id or
 
459
                            parent_entry.name != entry.name):
 
460
                            # Metadata common to all entries has changed
 
461
                            # against per-file parent
 
462
                            carry_over_possible = False
 
463
                        else:
 
464
                            carry_over_possible = True
 
465
                        # per-type checks for changes against the parent_entry
 
466
                        # are done below.
 
467
                else:
 
468
                    # Cannot be a carry-over situation
 
469
                    carry_over_possible = False
 
470
                # Populate the entry in the delta
 
471
                if kind == 'file':
 
472
                    # XXX: There is still a small race here: If someone reverts the content of a file
 
473
                    # after iter_changes examines and decides it has changed,
 
474
                    # we will unconditionally record a new version even if some
 
475
                    # other process reverts it while commit is running (with
 
476
                    # the revert happening after iter_changes did its
 
477
                    # examination).
 
478
                    if change[7][1]:
 
479
                        entry.executable = True
 
480
                    else:
 
481
                        entry.executable = False
 
482
                    if (carry_over_possible and
 
483
                        parent_entry.executable == entry.executable):
 
484
                            # Check the file length, content hash after reading
 
485
                            # the file.
 
486
                            nostore_sha = parent_entry.text_sha1
 
487
                    else:
 
488
                        nostore_sha = None
 
489
                    file_obj, stat_value = tree.get_file_with_stat(file_id, change[1][1])
 
490
                    try:
 
491
                        text = file_obj.read()
 
492
                    finally:
 
493
                        file_obj.close()
 
494
                    try:
 
495
                        entry.text_sha1, entry.text_size = self._add_text_to_weave(
 
496
                            file_id, text, heads, nostore_sha)
 
497
                        yield file_id, change[1][1], (entry.text_sha1, stat_value)
 
498
                    except errors.ExistingContent:
 
499
                        # No content change against a carry_over parent
 
500
                        # Perhaps this should also yield a fs hash update?
 
501
                        carried_over = True
 
502
                        entry.text_size = parent_entry.text_size
 
503
                        entry.text_sha1 = parent_entry.text_sha1
 
504
                elif kind == 'symlink':
 
505
                    # Wants a path hint?
 
506
                    entry.symlink_target = tree.get_symlink_target(file_id)
 
507
                    if (carry_over_possible and
 
508
                        parent_entry.symlink_target == entry.symlink_target):
 
509
                        carried_over = True
 
510
                    else:
 
511
                        self._add_text_to_weave(change[0], '', heads, None)
 
512
                elif kind == 'directory':
 
513
                    if carry_over_possible:
 
514
                        carried_over = True
 
515
                    else:
 
516
                        # Nothing to set on the entry.
 
517
                        # XXX: split into the Root and nonRoot versions.
 
518
                        if change[1][1] != '' or self.repository.supports_rich_root():
 
519
                            self._add_text_to_weave(change[0], '', heads, None)
 
520
                elif kind == 'tree-reference':
 
521
                    if not self.repository._format.supports_tree_reference:
 
522
                        # This isn't quite sane as an error, but we shouldn't
 
523
                        # ever see this code path in practice: tree's don't
 
524
                        # permit references when the repo doesn't support tree
 
525
                        # references.
 
526
                        raise errors.UnsupportedOperation(tree.add_reference,
 
527
                            self.repository)
 
528
                    reference_revision = tree.get_reference_revision(change[0])
 
529
                    entry.reference_revision = reference_revision
 
530
                    if (carry_over_possible and
 
531
                        parent_entry.reference_revision == reference_revision):
 
532
                        carried_over = True
 
533
                    else:
 
534
                        self._add_text_to_weave(change[0], '', heads, None)
 
535
                else:
 
536
                    raise AssertionError('unknown kind %r' % kind)
 
537
                if not carried_over:
 
538
                    entry.revision = modified_rev
 
539
                else:
 
540
                    entry.revision = parent_entry.revision
 
541
            else:
 
542
                entry = None
 
543
            new_path = change[1][1]
 
544
            inv_delta.append((change[1][0], new_path, change[0], entry))
 
545
            if new_path == '':
 
546
                seen_root = True
 
547
        # The initial commit adds a root directory, but this in itself is not
 
548
        # a worthwhile commit.
 
549
        if ((len(inv_delta) > 0 and basis_revision_id != _mod_revision.NULL_REVISION) or
 
550
            (len(inv_delta) > 1 and basis_revision_id == _mod_revision.NULL_REVISION)):
 
551
            # This should perhaps be guarded by a check that the basis we
 
552
            # commit against is the basis for the commit and if not do a delta
 
553
            # against the basis.
 
554
            self._any_changes = True
 
555
        if not seen_root:
 
556
            # housekeeping root entry changes do not affect no-change commits.
 
557
            self._require_root_change(tree)
 
558
        self.basis_delta_revision = basis_revision_id
 
559
 
 
560
    def _add_text_to_weave(self, file_id, new_text, parents, nostore_sha):
 
561
        parent_keys = tuple([(file_id, parent) for parent in parents])
 
562
        return self.repository.texts._add_text(
 
563
            (file_id, self._new_revision_id), parent_keys, new_text,
 
564
            nostore_sha=nostore_sha, random_id=self.random_revid)[0:2]
 
565
 
 
566
 
 
567
class VersionedFileRootCommitBuilder(VersionedFileCommitBuilder):
 
568
    """This commitbuilder actually records the root id"""
 
569
 
 
570
    # the root entry gets versioned properly by this builder.
 
571
    _versioned_root = True
 
572
 
 
573
    def _require_root_change(self, tree):
 
574
        """Enforce an appropriate root object change.
 
575
 
 
576
        This is called once when record_iter_changes is called, if and only if
 
577
        the root was not in the delta calculated by record_iter_changes.
 
578
 
 
579
        :param tree: The tree which is being committed.
 
580
        """
 
581
        # versioned roots do not change unless the tree found a change.
 
582
 
 
583
 
 
584
class VersionedFileRepository(Repository):
 
585
    """Repository holding history for one or more branches.
 
586
 
 
587
    The repository holds and retrieves historical information including
 
588
    revisions and file history.  It's normally accessed only by the Branch,
 
589
    which views a particular line of development through that history.
 
590
 
 
591
    The Repository builds on top of some byte storage facilies (the revisions,
 
592
    signatures, inventories, texts and chk_bytes attributes) and a Transport,
 
593
    which respectively provide byte storage and a means to access the (possibly
 
594
    remote) disk.
 
595
 
 
596
    The byte storage facilities are addressed via tuples, which we refer to
 
597
    as 'keys' throughout the code base. Revision_keys, inventory_keys and
 
598
    signature_keys are all 1-tuples: (revision_id,). text_keys are two-tuples:
 
599
    (file_id, revision_id). chk_bytes uses CHK keys - a 1-tuple with a single
 
600
    byte string made up of a hash identifier and a hash value.
 
601
    We use this interface because it allows low friction with the underlying
 
602
    code that implements disk indices, network encoding and other parts of
 
603
    breezy.
 
604
 
 
605
    :ivar revisions: A breezy.versionedfile.VersionedFiles instance containing
 
606
        the serialised revisions for the repository. This can be used to obtain
 
607
        revision graph information or to access raw serialised revisions.
 
608
        The result of trying to insert data into the repository via this store
 
609
        is undefined: it should be considered read-only except for implementors
 
610
        of repositories.
 
611
    :ivar signatures: A breezy.versionedfile.VersionedFiles instance containing
 
612
        the serialised signatures for the repository. This can be used to
 
613
        obtain access to raw serialised signatures.  The result of trying to
 
614
        insert data into the repository via this store is undefined: it should
 
615
        be considered read-only except for implementors of repositories.
 
616
    :ivar inventories: A breezy.versionedfile.VersionedFiles instance containing
 
617
        the serialised inventories for the repository. This can be used to
 
618
        obtain unserialised inventories.  The result of trying to insert data
 
619
        into the repository via this store is undefined: it should be
 
620
        considered read-only except for implementors of repositories.
 
621
    :ivar texts: A breezy.versionedfile.VersionedFiles instance containing the
 
622
        texts of files and directories for the repository. This can be used to
 
623
        obtain file texts or file graphs. Note that Repository.iter_file_bytes
 
624
        is usually a better interface for accessing file texts.
 
625
        The result of trying to insert data into the repository via this store
 
626
        is undefined: it should be considered read-only except for implementors
 
627
        of repositories.
 
628
    :ivar chk_bytes: A breezy.versionedfile.VersionedFiles instance containing
 
629
        any data the repository chooses to store or have indexed by its hash.
 
630
        The result of trying to insert data into the repository via this store
 
631
        is undefined: it should be considered read-only except for implementors
 
632
        of repositories.
 
633
    :ivar _transport: Transport for file access to repository, typically
 
634
        pointing to .bzr/repository.
 
635
    """
 
636
 
 
637
    # What class to use for a CommitBuilder. Often it's simpler to change this
 
638
    # in a Repository class subclass rather than to override
 
639
    # get_commit_builder.
 
640
    _commit_builder_class = VersionedFileCommitBuilder
 
641
 
 
642
    def add_fallback_repository(self, repository):
 
643
        """Add a repository to use for looking up data not held locally.
 
644
 
 
645
        :param repository: A repository.
 
646
        """
 
647
        if not self._format.supports_external_lookups:
 
648
            raise errors.UnstackableRepositoryFormat(self._format, self.base)
 
649
        # This can raise an exception, so should be done before we lock the
 
650
        # fallback repository.
 
651
        self._check_fallback_repository(repository)
 
652
        if self.is_locked():
 
653
            # This repository will call fallback.unlock() when we transition to
 
654
            # the unlocked state, so we make sure to increment the lock count
 
655
            repository.lock_read()
 
656
        self._fallback_repositories.append(repository)
 
657
        self.texts.add_fallback_versioned_files(repository.texts)
 
658
        self.inventories.add_fallback_versioned_files(repository.inventories)
 
659
        self.revisions.add_fallback_versioned_files(repository.revisions)
 
660
        self.signatures.add_fallback_versioned_files(repository.signatures)
 
661
        if self.chk_bytes is not None:
 
662
            self.chk_bytes.add_fallback_versioned_files(repository.chk_bytes)
 
663
 
 
664
    @only_raises(errors.LockNotHeld, errors.LockBroken)
 
665
    def unlock(self):
 
666
        super(VersionedFileRepository, self).unlock()
 
667
        if self.control_files._lock_count == 0:
 
668
            self._inventory_entry_cache.clear()
 
669
 
 
670
    def add_inventory(self, revision_id, inv, parents):
 
671
        """Add the inventory inv to the repository as revision_id.
 
672
 
 
673
        :param parents: The revision ids of the parents that revision_id
 
674
                        is known to have and are in the repository already.
 
675
 
 
676
        :returns: The validator(which is a sha1 digest, though what is sha'd is
 
677
            repository format specific) of the serialized inventory.
 
678
        """
 
679
        if not self.is_in_write_group():
 
680
            raise AssertionError("%r not in write group" % (self,))
 
681
        _mod_revision.check_not_reserved_id(revision_id)
 
682
        if not (inv.revision_id is None or inv.revision_id == revision_id):
 
683
            raise AssertionError(
 
684
                "Mismatch between inventory revision"
 
685
                " id and insertion revid (%r, %r)"
 
686
                % (inv.revision_id, revision_id))
 
687
        if inv.root is None:
 
688
            raise errors.RootMissing()
 
689
        return self._add_inventory_checked(revision_id, inv, parents)
 
690
 
 
691
    def _add_inventory_checked(self, revision_id, inv, parents):
 
692
        """Add inv to the repository after checking the inputs.
 
693
 
 
694
        This function can be overridden to allow different inventory styles.
 
695
 
 
696
        :seealso: add_inventory, for the contract.
 
697
        """
 
698
        inv_lines = self._serializer.write_inventory_to_lines(inv)
 
699
        return self._inventory_add_lines(revision_id, parents,
 
700
            inv_lines, check_content=False)
 
701
 
 
702
    def add_inventory_by_delta(self, basis_revision_id, delta, new_revision_id,
 
703
                               parents, basis_inv=None, propagate_caches=False):
 
704
        """Add a new inventory expressed as a delta against another revision.
 
705
 
 
706
        See the inventory developers documentation for the theory behind
 
707
        inventory deltas.
 
708
 
 
709
        :param basis_revision_id: The inventory id the delta was created
 
710
            against. (This does not have to be a direct parent.)
 
711
        :param delta: The inventory delta (see Inventory.apply_delta for
 
712
            details).
 
713
        :param new_revision_id: The revision id that the inventory is being
 
714
            added for.
 
715
        :param parents: The revision ids of the parents that revision_id is
 
716
            known to have and are in the repository already. These are supplied
 
717
            for repositories that depend on the inventory graph for revision
 
718
            graph access, as well as for those that pun ancestry with delta
 
719
            compression.
 
720
        :param basis_inv: The basis inventory if it is already known,
 
721
            otherwise None.
 
722
        :param propagate_caches: If True, the caches for this inventory are
 
723
          copied to and updated for the result if possible.
 
724
 
 
725
        :returns: (validator, new_inv)
 
726
            The validator(which is a sha1 digest, though what is sha'd is
 
727
            repository format specific) of the serialized inventory, and the
 
728
            resulting inventory.
 
729
        """
 
730
        if not self.is_in_write_group():
 
731
            raise AssertionError("%r not in write group" % (self,))
 
732
        _mod_revision.check_not_reserved_id(new_revision_id)
 
733
        basis_tree = self.revision_tree(basis_revision_id)
 
734
        basis_tree.lock_read()
 
735
        try:
 
736
            # Note that this mutates the inventory of basis_tree, which not all
 
737
            # inventory implementations may support: A better idiom would be to
 
738
            # return a new inventory, but as there is no revision tree cache in
 
739
            # repository this is safe for now - RBC 20081013
 
740
            if basis_inv is None:
 
741
                basis_inv = basis_tree.root_inventory
 
742
            basis_inv.apply_delta(delta)
 
743
            basis_inv.revision_id = new_revision_id
 
744
            return (self.add_inventory(new_revision_id, basis_inv, parents),
 
745
                    basis_inv)
 
746
        finally:
 
747
            basis_tree.unlock()
 
748
 
 
749
    def _inventory_add_lines(self, revision_id, parents, lines,
 
750
        check_content=True):
 
751
        """Store lines in inv_vf and return the sha1 of the inventory."""
 
752
        parents = [(parent,) for parent in parents]
 
753
        result = self.inventories.add_lines((revision_id,), parents, lines,
 
754
            check_content=check_content)[0]
 
755
        self.inventories._access.flush()
 
756
        return result
 
757
 
 
758
    def add_revision(self, revision_id, rev, inv=None):
 
759
        """Add rev to the revision store as revision_id.
 
760
 
 
761
        :param revision_id: the revision id to use.
 
762
        :param rev: The revision object.
 
763
        :param inv: The inventory for the revision. if None, it will be looked
 
764
                    up in the inventory storer
 
765
        """
 
766
        # TODO: jam 20070210 Shouldn't we check rev.revision_id and
 
767
        #       rev.parent_ids?
 
768
        _mod_revision.check_not_reserved_id(revision_id)
 
769
        # check inventory present
 
770
        if not self.inventories.get_parent_map([(revision_id,)]):
 
771
            if inv is None:
 
772
                raise errors.WeaveRevisionNotPresent(revision_id,
 
773
                                                     self.inventories)
 
774
            else:
 
775
                # yes, this is not suitable for adding with ghosts.
 
776
                rev.inventory_sha1 = self.add_inventory(revision_id, inv,
 
777
                                                        rev.parent_ids)
 
778
        else:
 
779
            key = (revision_id,)
 
780
            rev.inventory_sha1 = self.inventories.get_sha1s([key])[key]
 
781
        self._add_revision(rev)
 
782
 
 
783
    def _add_revision(self, revision):
 
784
        text = self._serializer.write_revision_to_string(revision)
 
785
        key = (revision.revision_id,)
 
786
        parents = tuple((parent,) for parent in revision.parent_ids)
 
787
        self.revisions.add_lines(key, parents, osutils.split_lines(text))
 
788
 
 
789
    def _check_inventories(self, checker):
 
790
        """Check the inventories found from the revision scan.
 
791
        
 
792
        This is responsible for verifying the sha1 of inventories and
 
793
        creating a pending_keys set that covers data referenced by inventories.
 
794
        """
 
795
        bar = ui.ui_factory.nested_progress_bar()
 
796
        try:
 
797
            self._do_check_inventories(checker, bar)
 
798
        finally:
 
799
            bar.finished()
 
800
 
 
801
    def _do_check_inventories(self, checker, bar):
 
802
        """Helper for _check_inventories."""
 
803
        revno = 0
 
804
        keys = {'chk_bytes':set(), 'inventories':set(), 'texts':set()}
 
805
        kinds = ['chk_bytes', 'texts']
 
806
        count = len(checker.pending_keys)
 
807
        bar.update(gettext("inventories"), 0, 2)
 
808
        current_keys = checker.pending_keys
 
809
        checker.pending_keys = {}
 
810
        # Accumulate current checks.
 
811
        for key in current_keys:
 
812
            if key[0] != 'inventories' and key[0] not in kinds:
 
813
                checker._report_items.append('unknown key type %r' % (key,))
 
814
            keys[key[0]].add(key[1:])
 
815
        if keys['inventories']:
 
816
            # NB: output order *should* be roughly sorted - topo or
 
817
            # inverse topo depending on repository - either way decent
 
818
            # to just delta against. However, pre-CHK formats didn't
 
819
            # try to optimise inventory layout on disk. As such the
 
820
            # pre-CHK code path does not use inventory deltas.
 
821
            last_object = None
 
822
            for record in self.inventories.check(keys=keys['inventories']):
 
823
                if record.storage_kind == 'absent':
 
824
                    checker._report_items.append(
 
825
                        'Missing inventory {%s}' % (record.key,))
 
826
                else:
 
827
                    last_object = self._check_record('inventories', record,
 
828
                        checker, last_object,
 
829
                        current_keys[('inventories',) + record.key])
 
830
            del keys['inventories']
 
831
        else:
 
832
            return
 
833
        bar.update(gettext("texts"), 1)
 
834
        while (checker.pending_keys or keys['chk_bytes']
 
835
            or keys['texts']):
 
836
            # Something to check.
 
837
            current_keys = checker.pending_keys
 
838
            checker.pending_keys = {}
 
839
            # Accumulate current checks.
 
840
            for key in current_keys:
 
841
                if key[0] not in kinds:
 
842
                    checker._report_items.append('unknown key type %r' % (key,))
 
843
                keys[key[0]].add(key[1:])
 
844
            # Check the outermost kind only - inventories || chk_bytes || texts
 
845
            for kind in kinds:
 
846
                if keys[kind]:
 
847
                    last_object = None
 
848
                    for record in getattr(self, kind).check(keys=keys[kind]):
 
849
                        if record.storage_kind == 'absent':
 
850
                            checker._report_items.append(
 
851
                                'Missing %s {%s}' % (kind, record.key,))
 
852
                        else:
 
853
                            last_object = self._check_record(kind, record,
 
854
                                checker, last_object, current_keys[(kind,) + record.key])
 
855
                    keys[kind] = set()
 
856
                    break
 
857
 
 
858
    def _check_record(self, kind, record, checker, last_object, item_data):
 
859
        """Check a single text from this repository."""
 
860
        if kind == 'inventories':
 
861
            rev_id = record.key[0]
 
862
            inv = self._deserialise_inventory(rev_id,
 
863
                record.get_bytes_as('fulltext'))
 
864
            if last_object is not None:
 
865
                delta = inv._make_delta(last_object)
 
866
                for old_path, path, file_id, ie in delta:
 
867
                    if ie is None:
 
868
                        continue
 
869
                    ie.check(checker, rev_id, inv)
 
870
            else:
 
871
                for path, ie in inv.iter_entries():
 
872
                    ie.check(checker, rev_id, inv)
 
873
            if self._format.fast_deltas:
 
874
                return inv
 
875
        elif kind == 'chk_bytes':
 
876
            # No code written to check chk_bytes for this repo format.
 
877
            checker._report_items.append(
 
878
                'unsupported key type chk_bytes for %s' % (record.key,))
 
879
        elif kind == 'texts':
 
880
            self._check_text(record, checker, item_data)
 
881
        else:
 
882
            checker._report_items.append(
 
883
                'unknown key type %s for %s' % (kind, record.key))
 
884
 
 
885
    def _check_text(self, record, checker, item_data):
 
886
        """Check a single text."""
 
887
        # Check it is extractable.
 
888
        # TODO: check length.
 
889
        if record.storage_kind == 'chunked':
 
890
            chunks = record.get_bytes_as(record.storage_kind)
 
891
            sha1 = osutils.sha_strings(chunks)
 
892
            length = sum(map(len, chunks))
 
893
        else:
 
894
            content = record.get_bytes_as('fulltext')
 
895
            sha1 = osutils.sha_string(content)
 
896
            length = len(content)
 
897
        if item_data and sha1 != item_data[1]:
 
898
            checker._report_items.append(
 
899
                'sha1 mismatch: %s has sha1 %s expected %s referenced by %s' %
 
900
                (record.key, sha1, item_data[1], item_data[2]))
 
901
 
 
902
    @needs_read_lock
 
903
    def _eliminate_revisions_not_present(self, revision_ids):
 
904
        """Check every revision id in revision_ids to see if we have it.
 
905
 
 
906
        Returns a set of the present revisions.
 
907
        """
 
908
        result = []
 
909
        graph = self.get_graph()
 
910
        parent_map = graph.get_parent_map(revision_ids)
 
911
        # The old API returned a list, should this actually be a set?
 
912
        return list(parent_map)
 
913
 
 
914
    def __init__(self, _format, a_controldir, control_files):
 
915
        """Instantiate a VersionedFileRepository.
 
916
 
 
917
        :param _format: The format of the repository on disk.
 
918
        :param controldir: The ControlDir of the repository.
 
919
        :param control_files: Control files to use for locking, etc.
 
920
        """
 
921
        # In the future we will have a single api for all stores for
 
922
        # getting file texts, inventories and revisions, then
 
923
        # this construct will accept instances of those things.
 
924
        super(VersionedFileRepository, self).__init__(_format, a_controldir,
 
925
            control_files)
 
926
        self._transport = control_files._transport
 
927
        self.base = self._transport.base
 
928
        # for tests
 
929
        self._reconcile_does_inventory_gc = True
 
930
        self._reconcile_fixes_text_parents = False
 
931
        self._reconcile_backsup_inventory = True
 
932
        # An InventoryEntry cache, used during deserialization
 
933
        self._inventory_entry_cache = fifo_cache.FIFOCache(10*1024)
 
934
        # Is it safe to return inventory entries directly from the entry cache,
 
935
        # rather copying them?
 
936
        self._safe_to_return_from_cache = False
 
937
 
 
938
    def fetch(self, source, revision_id=None, find_ghosts=False,
 
939
            fetch_spec=None):
 
940
        """Fetch the content required to construct revision_id from source.
 
941
 
 
942
        If revision_id is None and fetch_spec is None, then all content is
 
943
        copied.
 
944
 
 
945
        fetch() may not be used when the repository is in a write group -
 
946
        either finish the current write group before using fetch, or use
 
947
        fetch before starting the write group.
 
948
 
 
949
        :param find_ghosts: Find and copy revisions in the source that are
 
950
            ghosts in the target (and not reachable directly by walking out to
 
951
            the first-present revision in target from revision_id).
 
952
        :param revision_id: If specified, all the content needed for this
 
953
            revision ID will be copied to the target.  Fetch will determine for
 
954
            itself which content needs to be copied.
 
955
        :param fetch_spec: If specified, a SearchResult or
 
956
            PendingAncestryResult that describes which revisions to copy.  This
 
957
            allows copying multiple heads at once.  Mutually exclusive with
 
958
            revision_id.
 
959
        """
 
960
        if fetch_spec is not None and revision_id is not None:
 
961
            raise AssertionError(
 
962
                "fetch_spec and revision_id are mutually exclusive.")
 
963
        if self.is_in_write_group():
 
964
            raise errors.InternalBzrError(
 
965
                "May not fetch while in a write group.")
 
966
        # fast path same-url fetch operations
 
967
        # TODO: lift out to somewhere common with RemoteRepository
 
968
        # <https://bugs.launchpad.net/bzr/+bug/401646>
 
969
        if (self.has_same_location(source)
 
970
            and fetch_spec is None
 
971
            and self._has_same_fallbacks(source)):
 
972
            # check that last_revision is in 'from' and then return a
 
973
            # no-operation.
 
974
            if (revision_id is not None and
 
975
                not _mod_revision.is_null(revision_id)):
 
976
                self.get_revision(revision_id)
 
977
            return 0, []
 
978
        inter = InterRepository.get(source, self)
 
979
        if (fetch_spec is not None and
 
980
            not getattr(inter, "supports_fetch_spec", False)):
 
981
            raise errors.UnsupportedOperation(
 
982
                "fetch_spec not supported for %r" % inter)
 
983
        return inter.fetch(revision_id=revision_id,
 
984
            find_ghosts=find_ghosts, fetch_spec=fetch_spec)
 
985
 
 
986
    @needs_read_lock
 
987
    def gather_stats(self, revid=None, committers=None):
 
988
        """See Repository.gather_stats()."""
 
989
        result = super(VersionedFileRepository, self).gather_stats(revid, committers)
 
990
        # now gather global repository information
 
991
        # XXX: This is available for many repos regardless of listability.
 
992
        if self.user_transport.listable():
 
993
            # XXX: do we want to __define len__() ?
 
994
            # Maybe the versionedfiles object should provide a different
 
995
            # method to get the number of keys.
 
996
            result['revisions'] = len(self.revisions.keys())
 
997
            # result['size'] = t
 
998
        return result
 
999
 
 
1000
    def get_commit_builder(self, branch, parents, config_stack, timestamp=None,
 
1001
                           timezone=None, committer=None, revprops=None,
 
1002
                           revision_id=None, lossy=False):
 
1003
        """Obtain a CommitBuilder for this repository.
 
1004
 
 
1005
        :param branch: Branch to commit to.
 
1006
        :param parents: Revision ids of the parents of the new revision.
 
1007
        :param config_stack: Configuration stack to use.
 
1008
        :param timestamp: Optional timestamp recorded for commit.
 
1009
        :param timezone: Optional timezone for timestamp.
 
1010
        :param committer: Optional committer to set for commit.
 
1011
        :param revprops: Optional dictionary of revision properties.
 
1012
        :param revision_id: Optional revision id.
 
1013
        :param lossy: Whether to discard data that can not be natively
 
1014
            represented, when pushing to a foreign VCS
 
1015
        """
 
1016
        if self._fallback_repositories and not self._format.supports_chks:
 
1017
            raise errors.BzrError("Cannot commit directly to a stacked branch"
 
1018
                " in pre-2a formats. See "
 
1019
                "https://bugs.launchpad.net/bzr/+bug/375013 for details.")
 
1020
        result = self._commit_builder_class(self, parents, config_stack,
 
1021
            timestamp, timezone, committer, revprops, revision_id,
 
1022
            lossy)
 
1023
        self.start_write_group()
 
1024
        return result
 
1025
 
 
1026
    def get_missing_parent_inventories(self, check_for_missing_texts=True):
 
1027
        """Return the keys of missing inventory parents for revisions added in
 
1028
        this write group.
 
1029
 
 
1030
        A revision is not complete if the inventory delta for that revision
 
1031
        cannot be calculated.  Therefore if the parent inventories of a
 
1032
        revision are not present, the revision is incomplete, and e.g. cannot
 
1033
        be streamed by a smart server.  This method finds missing inventory
 
1034
        parents for revisions added in this write group.
 
1035
        """
 
1036
        if not self._format.supports_external_lookups:
 
1037
            # This is only an issue for stacked repositories
 
1038
            return set()
 
1039
        if not self.is_in_write_group():
 
1040
            raise AssertionError('not in a write group')
 
1041
 
 
1042
        # XXX: We assume that every added revision already has its
 
1043
        # corresponding inventory, so we only check for parent inventories that
 
1044
        # might be missing, rather than all inventories.
 
1045
        parents = set(self.revisions._index.get_missing_parents())
 
1046
        parents.discard(_mod_revision.NULL_REVISION)
 
1047
        unstacked_inventories = self.inventories._index
 
1048
        present_inventories = unstacked_inventories.get_parent_map(
 
1049
            key[-1:] for key in parents)
 
1050
        parents.difference_update(present_inventories)
 
1051
        if len(parents) == 0:
 
1052
            # No missing parent inventories.
 
1053
            return set()
 
1054
        if not check_for_missing_texts:
 
1055
            return set(('inventories', rev_id) for (rev_id,) in parents)
 
1056
        # Ok, now we have a list of missing inventories.  But these only matter
 
1057
        # if the inventories that reference them are missing some texts they
 
1058
        # appear to introduce.
 
1059
        # XXX: Texts referenced by all added inventories need to be present,
 
1060
        # but at the moment we're only checking for texts referenced by
 
1061
        # inventories at the graph's edge.
 
1062
        key_deps = self.revisions._index._key_dependencies
 
1063
        key_deps.satisfy_refs_for_keys(present_inventories)
 
1064
        referrers = frozenset(r[0] for r in key_deps.get_referrers())
 
1065
        file_ids = self.fileids_altered_by_revision_ids(referrers)
 
1066
        missing_texts = set()
 
1067
        for file_id, version_ids in viewitems(file_ids):
 
1068
            missing_texts.update(
 
1069
                (file_id, version_id) for version_id in version_ids)
 
1070
        present_texts = self.texts.get_parent_map(missing_texts)
 
1071
        missing_texts.difference_update(present_texts)
 
1072
        if not missing_texts:
 
1073
            # No texts are missing, so all revisions and their deltas are
 
1074
            # reconstructable.
 
1075
            return set()
 
1076
        # Alternatively the text versions could be returned as the missing
 
1077
        # keys, but this is likely to be less data.
 
1078
        missing_keys = set(('inventories', rev_id) for (rev_id,) in parents)
 
1079
        return missing_keys
 
1080
 
 
1081
    @needs_read_lock
 
1082
    def has_revisions(self, revision_ids):
 
1083
        """Probe to find out the presence of multiple revisions.
 
1084
 
 
1085
        :param revision_ids: An iterable of revision_ids.
 
1086
        :return: A set of the revision_ids that were present.
 
1087
        """
 
1088
        parent_map = self.revisions.get_parent_map(
 
1089
            [(rev_id,) for rev_id in revision_ids])
 
1090
        result = set()
 
1091
        if _mod_revision.NULL_REVISION in revision_ids:
 
1092
            result.add(_mod_revision.NULL_REVISION)
 
1093
        result.update([key[0] for key in parent_map])
 
1094
        return result
 
1095
 
 
1096
    @needs_read_lock
 
1097
    def get_revision_reconcile(self, revision_id):
 
1098
        """'reconcile' helper routine that allows access to a revision always.
 
1099
 
 
1100
        This variant of get_revision does not cross check the weave graph
 
1101
        against the revision one as get_revision does: but it should only
 
1102
        be used by reconcile, or reconcile-alike commands that are correcting
 
1103
        or testing the revision graph.
 
1104
        """
 
1105
        return self._get_revisions([revision_id])[0]
 
1106
 
 
1107
    @needs_read_lock
 
1108
    def get_revisions(self, revision_ids):
 
1109
        """Get many revisions at once.
 
1110
        
 
1111
        Repositories that need to check data on every revision read should 
 
1112
        subclass this method.
 
1113
        """
 
1114
        return self._get_revisions(revision_ids)
 
1115
 
 
1116
    @needs_read_lock
 
1117
    def _get_revisions(self, revision_ids):
 
1118
        """Core work logic to get many revisions without sanity checks."""
 
1119
        revs = {}
 
1120
        for revid, rev in self._iter_revisions(revision_ids):
 
1121
            if rev is None:
 
1122
                raise errors.NoSuchRevision(self, revid)
 
1123
            revs[revid] = rev
 
1124
        return [revs[revid] for revid in revision_ids]
 
1125
 
 
1126
    def _iter_revisions(self, revision_ids):
 
1127
        """Iterate over revision objects.
 
1128
 
 
1129
        :param revision_ids: An iterable of revisions to examine. None may be
 
1130
            passed to request all revisions known to the repository. Note that
 
1131
            not all repositories can find unreferenced revisions; for those
 
1132
            repositories only referenced ones will be returned.
 
1133
        :return: An iterator of (revid, revision) tuples. Absent revisions (
 
1134
            those asked for but not available) are returned as (revid, None).
 
1135
        """
 
1136
        if revision_ids is None:
 
1137
            revision_ids = self.all_revision_ids()
 
1138
        else:
 
1139
            for rev_id in revision_ids:
 
1140
                if not rev_id or not isinstance(rev_id, bytes):
 
1141
                    raise errors.InvalidRevisionId(revision_id=rev_id, branch=self)
 
1142
        keys = [(key,) for key in revision_ids]
 
1143
        stream = self.revisions.get_record_stream(keys, 'unordered', True)
 
1144
        for record in stream:
 
1145
            revid = record.key[0]
 
1146
            if record.storage_kind == 'absent':
 
1147
                yield (revid, None)
 
1148
            else:
 
1149
                text = record.get_bytes_as('fulltext')
 
1150
                rev = self._serializer.read_revision_from_string(text)
 
1151
                yield (revid, rev)
 
1152
 
 
1153
    @needs_write_lock
 
1154
    def add_signature_text(self, revision_id, signature):
 
1155
        """Store a signature text for a revision.
 
1156
 
 
1157
        :param revision_id: Revision id of the revision
 
1158
        :param signature: Signature text.
 
1159
        """
 
1160
        self.signatures.add_lines((revision_id,), (),
 
1161
            osutils.split_lines(signature))
 
1162
 
 
1163
    def find_text_key_references(self):
 
1164
        """Find the text key references within the repository.
 
1165
 
 
1166
        :return: A dictionary mapping text keys ((fileid, revision_id) tuples)
 
1167
            to whether they were referred to by the inventory of the
 
1168
            revision_id that they contain. The inventory texts from all present
 
1169
            revision ids are assessed to generate this report.
 
1170
        """
 
1171
        revision_keys = self.revisions.keys()
 
1172
        w = self.inventories
 
1173
        pb = ui.ui_factory.nested_progress_bar()
 
1174
        try:
 
1175
            return self._serializer._find_text_key_references(
 
1176
                w.iter_lines_added_or_present_in_keys(revision_keys, pb=pb))
 
1177
        finally:
 
1178
            pb.finished()
 
1179
 
 
1180
    def _inventory_xml_lines_for_keys(self, keys):
 
1181
        """Get a line iterator of the sort needed for findind references.
 
1182
 
 
1183
        Not relevant for non-xml inventory repositories.
 
1184
 
 
1185
        Ghosts in revision_keys are ignored.
 
1186
 
 
1187
        :param revision_keys: The revision keys for the inventories to inspect.
 
1188
        :return: An iterator over (inventory line, revid) for the fulltexts of
 
1189
            all of the xml inventories specified by revision_keys.
 
1190
        """
 
1191
        stream = self.inventories.get_record_stream(keys, 'unordered', True)
 
1192
        for record in stream:
 
1193
            if record.storage_kind != 'absent':
 
1194
                chunks = record.get_bytes_as('chunked')
 
1195
                revid = record.key[-1]
 
1196
                lines = osutils.chunks_to_lines(chunks)
 
1197
                for line in lines:
 
1198
                    yield line, revid
 
1199
 
 
1200
    def _find_file_ids_from_xml_inventory_lines(self, line_iterator,
 
1201
        revision_keys):
 
1202
        """Helper routine for fileids_altered_by_revision_ids.
 
1203
 
 
1204
        This performs the translation of xml lines to revision ids.
 
1205
 
 
1206
        :param line_iterator: An iterator of lines, origin_version_id
 
1207
        :param revision_keys: The revision ids to filter for. This should be a
 
1208
            set or other type which supports efficient __contains__ lookups, as
 
1209
            the revision key from each parsed line will be looked up in the
 
1210
            revision_keys filter.
 
1211
        :return: a dictionary mapping altered file-ids to an iterable of
 
1212
            revision_ids. Each altered file-ids has the exact revision_ids that
 
1213
            altered it listed explicitly.
 
1214
        """
 
1215
        seen = set(self._serializer._find_text_key_references(line_iterator))
 
1216
        parent_keys = self._find_parent_keys_of_revisions(revision_keys)
 
1217
        parent_seen = set(self._serializer._find_text_key_references(
 
1218
            self._inventory_xml_lines_for_keys(parent_keys)))
 
1219
        new_keys = seen - parent_seen
 
1220
        result = {}
 
1221
        setdefault = result.setdefault
 
1222
        for key in new_keys:
 
1223
            setdefault(key[0], set()).add(key[-1])
 
1224
        return result
 
1225
 
 
1226
    def _find_parent_keys_of_revisions(self, revision_keys):
 
1227
        """Similar to _find_parent_ids_of_revisions, but used with keys.
 
1228
 
 
1229
        :param revision_keys: An iterable of revision_keys.
 
1230
        :return: The parents of all revision_keys that are not already in
 
1231
            revision_keys
 
1232
        """
 
1233
        parent_map = self.revisions.get_parent_map(revision_keys)
 
1234
        parent_keys = set(itertools.chain.from_iterable(
 
1235
            viewvalues(parent_map)))
 
1236
        parent_keys.difference_update(revision_keys)
 
1237
        parent_keys.discard(_mod_revision.NULL_REVISION)
 
1238
        return parent_keys
 
1239
 
 
1240
    def fileids_altered_by_revision_ids(self, revision_ids, _inv_weave=None):
 
1241
        """Find the file ids and versions affected by revisions.
 
1242
 
 
1243
        :param revisions: an iterable containing revision ids.
 
1244
        :param _inv_weave: The inventory weave from this repository or None.
 
1245
            If None, the inventory weave will be opened automatically.
 
1246
        :return: a dictionary mapping altered file-ids to an iterable of
 
1247
            revision_ids. Each altered file-ids has the exact revision_ids that
 
1248
            altered it listed explicitly.
 
1249
        """
 
1250
        selected_keys = set((revid,) for revid in revision_ids)
 
1251
        w = _inv_weave or self.inventories
 
1252
        return self._find_file_ids_from_xml_inventory_lines(
 
1253
            w.iter_lines_added_or_present_in_keys(
 
1254
                selected_keys, pb=None),
 
1255
            selected_keys)
 
1256
 
 
1257
    def iter_files_bytes(self, desired_files):
 
1258
        """Iterate through file versions.
 
1259
 
 
1260
        Files will not necessarily be returned in the order they occur in
 
1261
        desired_files.  No specific order is guaranteed.
 
1262
 
 
1263
        Yields pairs of identifier, bytes_iterator.  identifier is an opaque
 
1264
        value supplied by the caller as part of desired_files.  It should
 
1265
        uniquely identify the file version in the caller's context.  (Examples:
 
1266
        an index number or a TreeTransform trans_id.)
 
1267
 
 
1268
        bytes_iterator is an iterable of bytestrings for the file.  The
 
1269
        kind of iterable and length of the bytestrings are unspecified, but for
 
1270
        this implementation, it is a list of bytes produced by
 
1271
        VersionedFile.get_record_stream().
 
1272
 
 
1273
        :param desired_files: a list of (file_id, revision_id, identifier)
 
1274
            triples
 
1275
        """
 
1276
        text_keys = {}
 
1277
        for file_id, revision_id, callable_data in desired_files:
 
1278
            text_keys[(file_id, revision_id)] = callable_data
 
1279
        for record in self.texts.get_record_stream(text_keys, 'unordered', True):
 
1280
            if record.storage_kind == 'absent':
 
1281
                raise errors.RevisionNotPresent(record.key[1], record.key[0])
 
1282
            yield text_keys[record.key], record.get_bytes_as('chunked')
 
1283
 
 
1284
    def _generate_text_key_index(self, text_key_references=None,
 
1285
        ancestors=None):
 
1286
        """Generate a new text key index for the repository.
 
1287
 
 
1288
        This is an expensive function that will take considerable time to run.
 
1289
 
 
1290
        :return: A dict mapping text keys ((file_id, revision_id) tuples) to a
 
1291
            list of parents, also text keys. When a given key has no parents,
 
1292
            the parents list will be [NULL_REVISION].
 
1293
        """
 
1294
        # All revisions, to find inventory parents.
 
1295
        if ancestors is None:
 
1296
            graph = self.get_graph()
 
1297
            ancestors = graph.get_parent_map(self.all_revision_ids())
 
1298
        if text_key_references is None:
 
1299
            text_key_references = self.find_text_key_references()
 
1300
        pb = ui.ui_factory.nested_progress_bar()
 
1301
        try:
 
1302
            return self._do_generate_text_key_index(ancestors,
 
1303
                text_key_references, pb)
 
1304
        finally:
 
1305
            pb.finished()
 
1306
 
 
1307
    def _do_generate_text_key_index(self, ancestors, text_key_references, pb):
 
1308
        """Helper for _generate_text_key_index to avoid deep nesting."""
 
1309
        revision_order = tsort.topo_sort(ancestors)
 
1310
        invalid_keys = set()
 
1311
        revision_keys = {}
 
1312
        for revision_id in revision_order:
 
1313
            revision_keys[revision_id] = set()
 
1314
        text_count = len(text_key_references)
 
1315
        # a cache of the text keys to allow reuse; costs a dict of all the
 
1316
        # keys, but saves a 2-tuple for every child of a given key.
 
1317
        text_key_cache = {}
 
1318
        for text_key, valid in viewitems(text_key_references):
 
1319
            if not valid:
 
1320
                invalid_keys.add(text_key)
 
1321
            else:
 
1322
                revision_keys[text_key[1]].add(text_key)
 
1323
            text_key_cache[text_key] = text_key
 
1324
        del text_key_references
 
1325
        text_index = {}
 
1326
        text_graph = graph.Graph(graph.DictParentsProvider(text_index))
 
1327
        NULL_REVISION = _mod_revision.NULL_REVISION
 
1328
        # Set a cache with a size of 10 - this suffices for bzr.dev but may be
 
1329
        # too small for large or very branchy trees. However, for 55K path
 
1330
        # trees, it would be easy to use too much memory trivially. Ideally we
 
1331
        # could gauge this by looking at available real memory etc, but this is
 
1332
        # always a tricky proposition.
 
1333
        inventory_cache = lru_cache.LRUCache(10)
 
1334
        batch_size = 10 # should be ~150MB on a 55K path tree
 
1335
        batch_count = len(revision_order) / batch_size + 1
 
1336
        processed_texts = 0
 
1337
        pb.update(gettext("Calculating text parents"), processed_texts, text_count)
 
1338
        for offset in range(batch_count):
 
1339
            to_query = revision_order[offset * batch_size:(offset + 1) *
 
1340
                batch_size]
 
1341
            if not to_query:
 
1342
                break
 
1343
            for revision_id in to_query:
 
1344
                parent_ids = ancestors[revision_id]
 
1345
                for text_key in revision_keys[revision_id]:
 
1346
                    pb.update(gettext("Calculating text parents"), processed_texts)
 
1347
                    processed_texts += 1
 
1348
                    candidate_parents = []
 
1349
                    for parent_id in parent_ids:
 
1350
                        parent_text_key = (text_key[0], parent_id)
 
1351
                        try:
 
1352
                            check_parent = parent_text_key not in \
 
1353
                                revision_keys[parent_id]
 
1354
                        except KeyError:
 
1355
                            # the parent parent_id is a ghost:
 
1356
                            check_parent = False
 
1357
                            # truncate the derived graph against this ghost.
 
1358
                            parent_text_key = None
 
1359
                        if check_parent:
 
1360
                            # look at the parent commit details inventories to
 
1361
                            # determine possible candidates in the per file graph.
 
1362
                            # TODO: cache here.
 
1363
                            try:
 
1364
                                inv = inventory_cache[parent_id]
 
1365
                            except KeyError:
 
1366
                                inv = self.revision_tree(parent_id).root_inventory
 
1367
                                inventory_cache[parent_id] = inv
 
1368
                            try:
 
1369
                                parent_entry = inv[text_key[0]]
 
1370
                            except (KeyError, errors.NoSuchId):
 
1371
                                parent_entry = None
 
1372
                            if parent_entry is not None:
 
1373
                                parent_text_key = (
 
1374
                                    text_key[0], parent_entry.revision)
 
1375
                            else:
 
1376
                                parent_text_key = None
 
1377
                        if parent_text_key is not None:
 
1378
                            candidate_parents.append(
 
1379
                                text_key_cache[parent_text_key])
 
1380
                    parent_heads = text_graph.heads(candidate_parents)
 
1381
                    new_parents = list(parent_heads)
 
1382
                    new_parents.sort(key=lambda x:candidate_parents.index(x))
 
1383
                    if new_parents == []:
 
1384
                        new_parents = [NULL_REVISION]
 
1385
                    text_index[text_key] = new_parents
 
1386
 
 
1387
        for text_key in invalid_keys:
 
1388
            text_index[text_key] = [NULL_REVISION]
 
1389
        return text_index
 
1390
 
 
1391
    def item_keys_introduced_by(self, revision_ids, _files_pb=None):
 
1392
        """Get an iterable listing the keys of all the data introduced by a set
 
1393
        of revision IDs.
 
1394
 
 
1395
        The keys will be ordered so that the corresponding items can be safely
 
1396
        fetched and inserted in that order.
 
1397
 
 
1398
        :returns: An iterable producing tuples of (knit-kind, file-id,
 
1399
            versions).  knit-kind is one of 'file', 'inventory', 'signatures',
 
1400
            'revisions'.  file-id is None unless knit-kind is 'file'.
 
1401
        """
 
1402
        for result in self._find_file_keys_to_fetch(revision_ids, _files_pb):
 
1403
            yield result
 
1404
        del _files_pb
 
1405
        for result in self._find_non_file_keys_to_fetch(revision_ids):
 
1406
            yield result
 
1407
 
 
1408
    def _find_file_keys_to_fetch(self, revision_ids, pb):
 
1409
        # XXX: it's a bit weird to control the inventory weave caching in this
 
1410
        # generator.  Ideally the caching would be done in fetch.py I think.  Or
 
1411
        # maybe this generator should explicitly have the contract that it
 
1412
        # should not be iterated until the previously yielded item has been
 
1413
        # processed?
 
1414
        inv_w = self.inventories
 
1415
 
 
1416
        # file ids that changed
 
1417
        file_ids = self.fileids_altered_by_revision_ids(revision_ids, inv_w)
 
1418
        count = 0
 
1419
        num_file_ids = len(file_ids)
 
1420
        for file_id, altered_versions in viewitems(file_ids):
 
1421
            if pb is not None:
 
1422
                pb.update(gettext("Fetch texts"), count, num_file_ids)
 
1423
            count += 1
 
1424
            yield ("file", file_id, altered_versions)
 
1425
 
 
1426
    def _find_non_file_keys_to_fetch(self, revision_ids):
 
1427
        # inventory
 
1428
        yield ("inventory", None, revision_ids)
 
1429
 
 
1430
        # signatures
 
1431
        # XXX: Note ATM no callers actually pay attention to this return
 
1432
        #      instead they just use the list of revision ids and ignore
 
1433
        #      missing sigs. Consider removing this work entirely
 
1434
        revisions_with_signatures = set(self.signatures.get_parent_map(
 
1435
            [(r,) for r in revision_ids]))
 
1436
        revisions_with_signatures = {r for (r,) in revisions_with_signatures}
 
1437
        revisions_with_signatures.intersection_update(revision_ids)
 
1438
        yield ("signatures", None, revisions_with_signatures)
 
1439
 
 
1440
        # revisions
 
1441
        yield ("revisions", None, revision_ids)
 
1442
 
 
1443
    @needs_read_lock
 
1444
    def get_inventory(self, revision_id):
 
1445
        """Get Inventory object by revision id."""
 
1446
        return next(self.iter_inventories([revision_id]))
 
1447
 
 
1448
    def iter_inventories(self, revision_ids, ordering=None):
 
1449
        """Get many inventories by revision_ids.
 
1450
 
 
1451
        This will buffer some or all of the texts used in constructing the
 
1452
        inventories in memory, but will only parse a single inventory at a
 
1453
        time.
 
1454
 
 
1455
        :param revision_ids: The expected revision ids of the inventories.
 
1456
        :param ordering: optional ordering, e.g. 'topological'.  If not
 
1457
            specified, the order of revision_ids will be preserved (by
 
1458
            buffering if necessary).
 
1459
        :return: An iterator of inventories.
 
1460
        """
 
1461
        if ((None in revision_ids)
 
1462
            or (_mod_revision.NULL_REVISION in revision_ids)):
 
1463
            raise ValueError('cannot get null revision inventory')
 
1464
        for inv, revid in self._iter_inventories(revision_ids, ordering):
 
1465
            if inv is None:
 
1466
                raise errors.NoSuchRevision(self, revid)
 
1467
            yield inv
 
1468
 
 
1469
    def _iter_inventories(self, revision_ids, ordering):
 
1470
        """single-document based inventory iteration."""
 
1471
        inv_xmls = self._iter_inventory_xmls(revision_ids, ordering)
 
1472
        for text, revision_id in inv_xmls:
 
1473
            if text is None:
 
1474
                yield None, revision_id
 
1475
            else:
 
1476
                yield self._deserialise_inventory(revision_id, text), revision_id
 
1477
 
 
1478
    def _iter_inventory_xmls(self, revision_ids, ordering):
 
1479
        if ordering is None:
 
1480
            order_as_requested = True
 
1481
            ordering = 'unordered'
 
1482
        else:
 
1483
            order_as_requested = False
 
1484
        keys = [(revision_id,) for revision_id in revision_ids]
 
1485
        if not keys:
 
1486
            return
 
1487
        if order_as_requested:
 
1488
            key_iter = iter(keys)
 
1489
            next_key = next(key_iter)
 
1490
        stream = self.inventories.get_record_stream(keys, ordering, True)
 
1491
        text_chunks = {}
 
1492
        for record in stream:
 
1493
            if record.storage_kind != 'absent':
 
1494
                chunks = record.get_bytes_as('chunked')
 
1495
                if order_as_requested:
 
1496
                    text_chunks[record.key] = chunks
 
1497
                else:
 
1498
                    yield ''.join(chunks), record.key[-1]
 
1499
            else:
 
1500
                yield None, record.key[-1]
 
1501
            if order_as_requested:
 
1502
                # Yield as many results as we can while preserving order.
 
1503
                while next_key in text_chunks:
 
1504
                    chunks = text_chunks.pop(next_key)
 
1505
                    yield ''.join(chunks), next_key[-1]
 
1506
                    try:
 
1507
                        next_key = next(key_iter)
 
1508
                    except StopIteration:
 
1509
                        # We still want to fully consume the get_record_stream,
 
1510
                        # just in case it is not actually finished at this point
 
1511
                        next_key = None
 
1512
                        break
 
1513
 
 
1514
    def _deserialise_inventory(self, revision_id, xml):
 
1515
        """Transform the xml into an inventory object.
 
1516
 
 
1517
        :param revision_id: The expected revision id of the inventory.
 
1518
        :param xml: A serialised inventory.
 
1519
        """
 
1520
        result = self._serializer.read_inventory_from_string(xml, revision_id,
 
1521
                    entry_cache=self._inventory_entry_cache,
 
1522
                    return_from_cache=self._safe_to_return_from_cache)
 
1523
        if result.revision_id != revision_id:
 
1524
            raise AssertionError('revision id mismatch %s != %s' % (
 
1525
                result.revision_id, revision_id))
 
1526
        return result
 
1527
 
 
1528
    def get_serializer_format(self):
 
1529
        return self._serializer.format_num
 
1530
 
 
1531
    @needs_read_lock
 
1532
    def _get_inventory_xml(self, revision_id):
 
1533
        """Get serialized inventory as a string."""
 
1534
        texts = self._iter_inventory_xmls([revision_id], 'unordered')
 
1535
        text, revision_id = next(texts)
 
1536
        if text is None:
 
1537
            raise errors.NoSuchRevision(self, revision_id)
 
1538
        return text
 
1539
 
 
1540
    @needs_read_lock
 
1541
    def revision_tree(self, revision_id):
 
1542
        """Return Tree for a revision on this branch.
 
1543
 
 
1544
        `revision_id` may be NULL_REVISION for the empty tree revision.
 
1545
        """
 
1546
        revision_id = _mod_revision.ensure_null(revision_id)
 
1547
        # TODO: refactor this to use an existing revision object
 
1548
        # so we don't need to read it in twice.
 
1549
        if revision_id == _mod_revision.NULL_REVISION:
 
1550
            return inventorytree.InventoryRevisionTree(self,
 
1551
                Inventory(root_id=None), _mod_revision.NULL_REVISION)
 
1552
        else:
 
1553
            inv = self.get_inventory(revision_id)
 
1554
            return inventorytree.InventoryRevisionTree(self, inv, revision_id)
 
1555
 
 
1556
    def revision_trees(self, revision_ids):
 
1557
        """Return Trees for revisions in this repository.
 
1558
 
 
1559
        :param revision_ids: a sequence of revision-ids;
 
1560
          a revision-id may not be None or 'null:'
 
1561
        """
 
1562
        inventories = self.iter_inventories(revision_ids)
 
1563
        for inv in inventories:
 
1564
            yield inventorytree.InventoryRevisionTree(self, inv, inv.revision_id)
 
1565
 
 
1566
    def _filtered_revision_trees(self, revision_ids, file_ids):
 
1567
        """Return Tree for a revision on this branch with only some files.
 
1568
 
 
1569
        :param revision_ids: a sequence of revision-ids;
 
1570
          a revision-id may not be None or 'null:'
 
1571
        :param file_ids: if not None, the result is filtered
 
1572
          so that only those file-ids, their parents and their
 
1573
          children are included.
 
1574
        """
 
1575
        inventories = self.iter_inventories(revision_ids)
 
1576
        for inv in inventories:
 
1577
            # Should we introduce a FilteredRevisionTree class rather
 
1578
            # than pre-filter the inventory here?
 
1579
            filtered_inv = inv.filter(file_ids)
 
1580
            yield inventorytree.InventoryRevisionTree(self, filtered_inv, filtered_inv.revision_id)
 
1581
 
 
1582
    def get_parent_map(self, revision_ids):
 
1583
        """See graph.StackedParentsProvider.get_parent_map"""
 
1584
        # revisions index works in keys; this just works in revisions
 
1585
        # therefore wrap and unwrap
 
1586
        query_keys = []
 
1587
        result = {}
 
1588
        for revision_id in revision_ids:
 
1589
            if revision_id == _mod_revision.NULL_REVISION:
 
1590
                result[revision_id] = ()
 
1591
            elif revision_id is None:
 
1592
                raise ValueError('get_parent_map(None) is not valid')
 
1593
            else:
 
1594
                query_keys.append((revision_id ,))
 
1595
        for (revision_id,), parent_keys in viewitems(
 
1596
                self.revisions.get_parent_map(query_keys)):
 
1597
            if parent_keys:
 
1598
                result[revision_id] = tuple([parent_revid
 
1599
                    for (parent_revid,) in parent_keys])
 
1600
            else:
 
1601
                result[revision_id] = (_mod_revision.NULL_REVISION,)
 
1602
        return result
 
1603
 
 
1604
    @needs_read_lock
 
1605
    def get_known_graph_ancestry(self, revision_ids):
 
1606
        """Return the known graph for a set of revision ids and their ancestors.
 
1607
        """
 
1608
        st = static_tuple.StaticTuple
 
1609
        revision_keys = [st(r_id).intern() for r_id in revision_ids]
 
1610
        known_graph = self.revisions.get_known_graph_ancestry(revision_keys)
 
1611
        return graph.GraphThunkIdsToKeys(known_graph)
 
1612
 
 
1613
    @needs_read_lock
 
1614
    def get_file_graph(self):
 
1615
        """Return the graph walker for text revisions."""
 
1616
        return graph.Graph(self.texts)
 
1617
 
 
1618
    def revision_ids_to_search_result(self, result_set):
 
1619
        """Convert a set of revision ids to a graph SearchResult."""
 
1620
        result_parents = set(itertools.chain.from_iterable(viewvalues(
 
1621
            self.get_graph().get_parent_map(result_set))))
 
1622
        included_keys = result_set.intersection(result_parents)
 
1623
        start_keys = result_set.difference(included_keys)
 
1624
        exclude_keys = result_parents.difference(result_set)
 
1625
        result = vf_search.SearchResult(start_keys, exclude_keys,
 
1626
            len(result_set), result_set)
 
1627
        return result
 
1628
 
 
1629
    def _get_versioned_file_checker(self, text_key_references=None,
 
1630
        ancestors=None):
 
1631
        """Return an object suitable for checking versioned files.
 
1632
        
 
1633
        :param text_key_references: if non-None, an already built
 
1634
            dictionary mapping text keys ((fileid, revision_id) tuples)
 
1635
            to whether they were referred to by the inventory of the
 
1636
            revision_id that they contain. If None, this will be
 
1637
            calculated.
 
1638
        :param ancestors: Optional result from
 
1639
            self.get_graph().get_parent_map(self.all_revision_ids()) if already
 
1640
            available.
 
1641
        """
 
1642
        return _VersionedFileChecker(self,
 
1643
            text_key_references=text_key_references, ancestors=ancestors)
 
1644
 
 
1645
    @needs_read_lock
 
1646
    def has_signature_for_revision_id(self, revision_id):
 
1647
        """Query for a revision signature for revision_id in the repository."""
 
1648
        if not self.has_revision(revision_id):
 
1649
            raise errors.NoSuchRevision(self, revision_id)
 
1650
        sig_present = (1 == len(
 
1651
            self.signatures.get_parent_map([(revision_id,)])))
 
1652
        return sig_present
 
1653
 
 
1654
    @needs_read_lock
 
1655
    def get_signature_text(self, revision_id):
 
1656
        """Return the text for a signature."""
 
1657
        stream = self.signatures.get_record_stream([(revision_id,)],
 
1658
            'unordered', True)
 
1659
        record = next(stream)
 
1660
        if record.storage_kind == 'absent':
 
1661
            raise errors.NoSuchRevision(self, revision_id)
 
1662
        return record.get_bytes_as('fulltext')
 
1663
 
 
1664
    @needs_read_lock
 
1665
    def _check(self, revision_ids, callback_refs, check_repo):
 
1666
        result = check.VersionedFileCheck(self, check_repo=check_repo)
 
1667
        result.check(callback_refs)
 
1668
        return result
 
1669
 
 
1670
    def _find_inconsistent_revision_parents(self, revisions_iterator=None):
 
1671
        """Find revisions with different parent lists in the revision object
 
1672
        and in the index graph.
 
1673
 
 
1674
        :param revisions_iterator: None, or an iterator of (revid,
 
1675
            Revision-or-None). This iterator controls the revisions checked.
 
1676
        :returns: an iterator yielding tuples of (revison-id, parents-in-index,
 
1677
            parents-in-revision).
 
1678
        """
 
1679
        if not self.is_locked():
 
1680
            raise AssertionError()
 
1681
        vf = self.revisions
 
1682
        if revisions_iterator is None:
 
1683
            revisions_iterator = self._iter_revisions(None)
 
1684
        for revid, revision in revisions_iterator:
 
1685
            if revision is None:
 
1686
                pass
 
1687
            parent_map = vf.get_parent_map([(revid,)])
 
1688
            parents_according_to_index = tuple(parent[-1] for parent in
 
1689
                parent_map[(revid,)])
 
1690
            parents_according_to_revision = tuple(revision.parent_ids)
 
1691
            if parents_according_to_index != parents_according_to_revision:
 
1692
                yield (revid, parents_according_to_index,
 
1693
                    parents_according_to_revision)
 
1694
 
 
1695
    def _check_for_inconsistent_revision_parents(self):
 
1696
        inconsistencies = list(self._find_inconsistent_revision_parents())
 
1697
        if inconsistencies:
 
1698
            raise errors.BzrCheckError(
 
1699
                "Revision knit has inconsistent parents.")
 
1700
 
 
1701
    def _get_sink(self):
 
1702
        """Return a sink for streaming into this repository."""
 
1703
        return StreamSink(self)
 
1704
 
 
1705
    def _get_source(self, to_format):
 
1706
        """Return a source for streaming from this repository."""
 
1707
        return StreamSource(self, to_format)
 
1708
 
 
1709
 
 
1710
class MetaDirVersionedFileRepository(MetaDirRepository,
 
1711
                                     VersionedFileRepository):
 
1712
    """Repositories in a meta-dir, that work via versioned file objects."""
 
1713
 
 
1714
    def __init__(self, _format, a_controldir, control_files):
 
1715
        super(MetaDirVersionedFileRepository, self).__init__(_format, a_controldir,
 
1716
            control_files)
 
1717
 
 
1718
 
 
1719
class MetaDirVersionedFileRepositoryFormat(RepositoryFormatMetaDir,
 
1720
        VersionedFileRepositoryFormat):
 
1721
    """Base class for repository formats using versioned files in metadirs."""
 
1722
 
 
1723
 
 
1724
class StreamSink(object):
 
1725
    """An object that can insert a stream into a repository.
 
1726
 
 
1727
    This interface handles the complexity of reserialising inventories and
 
1728
    revisions from different formats, and allows unidirectional insertion into
 
1729
    stacked repositories without looking for the missing basis parents
 
1730
    beforehand.
 
1731
    """
 
1732
 
 
1733
    def __init__(self, target_repo):
 
1734
        self.target_repo = target_repo
 
1735
 
 
1736
    def insert_stream(self, stream, src_format, resume_tokens):
 
1737
        """Insert a stream's content into the target repository.
 
1738
 
 
1739
        :param src_format: a bzr repository format.
 
1740
 
 
1741
        :return: a list of resume tokens and an  iterable of keys additional
 
1742
            items required before the insertion can be completed.
 
1743
        """
 
1744
        self.target_repo.lock_write()
 
1745
        try:
 
1746
            if resume_tokens:
 
1747
                self.target_repo.resume_write_group(resume_tokens)
 
1748
                is_resume = True
 
1749
            else:
 
1750
                self.target_repo.start_write_group()
 
1751
                is_resume = False
 
1752
            try:
 
1753
                # locked_insert_stream performs a commit|suspend.
 
1754
                missing_keys = self.insert_stream_without_locking(stream,
 
1755
                                    src_format, is_resume)
 
1756
                if missing_keys:
 
1757
                    # suspend the write group and tell the caller what we is
 
1758
                    # missing. We know we can suspend or else we would not have
 
1759
                    # entered this code path. (All repositories that can handle
 
1760
                    # missing keys can handle suspending a write group).
 
1761
                    write_group_tokens = self.target_repo.suspend_write_group()
 
1762
                    return write_group_tokens, missing_keys
 
1763
                hint = self.target_repo.commit_write_group()
 
1764
                to_serializer = self.target_repo._format._serializer
 
1765
                src_serializer = src_format._serializer
 
1766
                if (to_serializer != src_serializer and
 
1767
                    self.target_repo._format.pack_compresses):
 
1768
                    self.target_repo.pack(hint=hint)
 
1769
                return [], set()
 
1770
            except:
 
1771
                self.target_repo.abort_write_group(suppress_errors=True)
 
1772
                raise
 
1773
        finally:
 
1774
            self.target_repo.unlock()
 
1775
 
 
1776
    def insert_stream_without_locking(self, stream, src_format,
 
1777
                                      is_resume=False):
 
1778
        """Insert a stream's content into the target repository.
 
1779
 
 
1780
        This assumes that you already have a locked repository and an active
 
1781
        write group.
 
1782
 
 
1783
        :param src_format: a bzr repository format.
 
1784
        :param is_resume: Passed down to get_missing_parent_inventories to
 
1785
            indicate if we should be checking for missing texts at the same
 
1786
            time.
 
1787
 
 
1788
        :return: A set of keys that are missing.
 
1789
        """
 
1790
        if not self.target_repo.is_write_locked():
 
1791
            raise errors.ObjectNotLocked(self)
 
1792
        if not self.target_repo.is_in_write_group():
 
1793
            raise errors.BzrError('you must already be in a write group')
 
1794
        to_serializer = self.target_repo._format._serializer
 
1795
        src_serializer = src_format._serializer
 
1796
        new_pack = None
 
1797
        if to_serializer == src_serializer:
 
1798
            # If serializers match and the target is a pack repository, set the
 
1799
            # write cache size on the new pack.  This avoids poor performance
 
1800
            # on transports where append is unbuffered (such as
 
1801
            # RemoteTransport).  This is safe to do because nothing should read
 
1802
            # back from the target repository while a stream with matching
 
1803
            # serialization is being inserted.
 
1804
            # The exception is that a delta record from the source that should
 
1805
            # be a fulltext may need to be expanded by the target (see
 
1806
            # test_fetch_revisions_with_deltas_into_pack); but we take care to
 
1807
            # explicitly flush any buffered writes first in that rare case.
 
1808
            try:
 
1809
                new_pack = self.target_repo._pack_collection._new_pack
 
1810
            except AttributeError:
 
1811
                # Not a pack repository
 
1812
                pass
 
1813
            else:
 
1814
                new_pack.set_write_cache_size(1024*1024)
 
1815
        for substream_type, substream in stream:
 
1816
            if 'stream' in debug.debug_flags:
 
1817
                mutter('inserting substream: %s', substream_type)
 
1818
            if substream_type == 'texts':
 
1819
                self.target_repo.texts.insert_record_stream(substream)
 
1820
            elif substream_type == 'inventories':
 
1821
                if src_serializer == to_serializer:
 
1822
                    self.target_repo.inventories.insert_record_stream(
 
1823
                        substream)
 
1824
                else:
 
1825
                    self._extract_and_insert_inventories(
 
1826
                        substream, src_serializer)
 
1827
            elif substream_type == 'inventory-deltas':
 
1828
                self._extract_and_insert_inventory_deltas(
 
1829
                    substream, src_serializer)
 
1830
            elif substream_type == 'chk_bytes':
 
1831
                # XXX: This doesn't support conversions, as it assumes the
 
1832
                #      conversion was done in the fetch code.
 
1833
                self.target_repo.chk_bytes.insert_record_stream(substream)
 
1834
            elif substream_type == 'revisions':
 
1835
                # This may fallback to extract-and-insert more often than
 
1836
                # required if the serializers are different only in terms of
 
1837
                # the inventory.
 
1838
                if src_serializer == to_serializer:
 
1839
                    self.target_repo.revisions.insert_record_stream(substream)
 
1840
                else:
 
1841
                    self._extract_and_insert_revisions(substream,
 
1842
                        src_serializer)
 
1843
            elif substream_type == 'signatures':
 
1844
                self.target_repo.signatures.insert_record_stream(substream)
 
1845
            else:
 
1846
                raise AssertionError('kaboom! %s' % (substream_type,))
 
1847
        # Done inserting data, and the missing_keys calculations will try to
 
1848
        # read back from the inserted data, so flush the writes to the new pack
 
1849
        # (if this is pack format).
 
1850
        if new_pack is not None:
 
1851
            new_pack._write_data('', flush=True)
 
1852
        # Find all the new revisions (including ones from resume_tokens)
 
1853
        missing_keys = self.target_repo.get_missing_parent_inventories(
 
1854
            check_for_missing_texts=is_resume)
 
1855
        try:
 
1856
            for prefix, versioned_file in (
 
1857
                ('texts', self.target_repo.texts),
 
1858
                ('inventories', self.target_repo.inventories),
 
1859
                ('revisions', self.target_repo.revisions),
 
1860
                ('signatures', self.target_repo.signatures),
 
1861
                ('chk_bytes', self.target_repo.chk_bytes),
 
1862
                ):
 
1863
                if versioned_file is None:
 
1864
                    continue
 
1865
                # TODO: key is often going to be a StaticTuple object
 
1866
                #       I don't believe we can define a method by which
 
1867
                #       (prefix,) + StaticTuple will work, though we could
 
1868
                #       define a StaticTuple.sq_concat that would allow you to
 
1869
                #       pass in either a tuple or a StaticTuple as the second
 
1870
                #       object, so instead we could have:
 
1871
                #       StaticTuple(prefix) + key here...
 
1872
                missing_keys.update((prefix,) + key for key in
 
1873
                    versioned_file.get_missing_compression_parent_keys())
 
1874
        except NotImplementedError:
 
1875
            # cannot even attempt suspending, and missing would have failed
 
1876
            # during stream insertion.
 
1877
            missing_keys = set()
 
1878
        return missing_keys
 
1879
 
 
1880
    def _extract_and_insert_inventory_deltas(self, substream, serializer):
 
1881
        target_rich_root = self.target_repo._format.rich_root_data
 
1882
        target_tree_refs = self.target_repo._format.supports_tree_reference
 
1883
        for record in substream:
 
1884
            # Insert the delta directly
 
1885
            inventory_delta_bytes = record.get_bytes_as('fulltext')
 
1886
            deserialiser = inventory_delta.InventoryDeltaDeserializer()
 
1887
            try:
 
1888
                parse_result = deserialiser.parse_text_bytes(
 
1889
                    inventory_delta_bytes)
 
1890
            except inventory_delta.IncompatibleInventoryDelta as err:
 
1891
                mutter("Incompatible delta: %s", err.msg)
 
1892
                raise errors.IncompatibleRevision(self.target_repo._format)
 
1893
            basis_id, new_id, rich_root, tree_refs, inv_delta = parse_result
 
1894
            revision_id = new_id
 
1895
            parents = [key[0] for key in record.parents]
 
1896
            self.target_repo.add_inventory_by_delta(
 
1897
                basis_id, inv_delta, revision_id, parents)
 
1898
 
 
1899
    def _extract_and_insert_inventories(self, substream, serializer,
 
1900
            parse_delta=None):
 
1901
        """Generate a new inventory versionedfile in target, converting data.
 
1902
 
 
1903
        The inventory is retrieved from the source, (deserializing it), and
 
1904
        stored in the target (reserializing it in a different format).
 
1905
        """
 
1906
        target_rich_root = self.target_repo._format.rich_root_data
 
1907
        target_tree_refs = self.target_repo._format.supports_tree_reference
 
1908
        for record in substream:
 
1909
            # It's not a delta, so it must be a fulltext in the source
 
1910
            # serializer's format.
 
1911
            bytes = record.get_bytes_as('fulltext')
 
1912
            revision_id = record.key[0]
 
1913
            inv = serializer.read_inventory_from_string(bytes, revision_id)
 
1914
            parents = [key[0] for key in record.parents]
 
1915
            self.target_repo.add_inventory(revision_id, inv, parents)
 
1916
            # No need to keep holding this full inv in memory when the rest of
 
1917
            # the substream is likely to be all deltas.
 
1918
            del inv
 
1919
 
 
1920
    def _extract_and_insert_revisions(self, substream, serializer):
 
1921
        for record in substream:
 
1922
            bytes = record.get_bytes_as('fulltext')
 
1923
            revision_id = record.key[0]
 
1924
            rev = serializer.read_revision_from_string(bytes)
 
1925
            if rev.revision_id != revision_id:
 
1926
                raise AssertionError('wtf: %s != %s' % (rev, revision_id))
 
1927
            self.target_repo.add_revision(revision_id, rev)
 
1928
 
 
1929
    def finished(self):
 
1930
        if self.target_repo._format._fetch_reconcile:
 
1931
            self.target_repo.reconcile()
 
1932
 
 
1933
 
 
1934
class StreamSource(object):
 
1935
    """A source of a stream for fetching between repositories."""
 
1936
 
 
1937
    def __init__(self, from_repository, to_format):
 
1938
        """Create a StreamSource streaming from from_repository."""
 
1939
        self.from_repository = from_repository
 
1940
        self.to_format = to_format
 
1941
        self._record_counter = RecordCounter()
 
1942
 
 
1943
    def delta_on_metadata(self):
 
1944
        """Return True if delta's are permitted on metadata streams.
 
1945
 
 
1946
        That is on revisions and signatures.
 
1947
        """
 
1948
        src_serializer = self.from_repository._format._serializer
 
1949
        target_serializer = self.to_format._serializer
 
1950
        return (self.to_format._fetch_uses_deltas and
 
1951
            src_serializer == target_serializer)
 
1952
 
 
1953
    def _fetch_revision_texts(self, revs):
 
1954
        # fetch signatures first and then the revision texts
 
1955
        # may need to be a InterRevisionStore call here.
 
1956
        from_sf = self.from_repository.signatures
 
1957
        # A missing signature is just skipped.
 
1958
        keys = [(rev_id,) for rev_id in revs]
 
1959
        signatures = versionedfile.filter_absent(from_sf.get_record_stream(
 
1960
            keys,
 
1961
            self.to_format._fetch_order,
 
1962
            not self.to_format._fetch_uses_deltas))
 
1963
        # If a revision has a delta, this is actually expanded inside the
 
1964
        # insert_record_stream code now, which is an alternate fix for
 
1965
        # bug #261339
 
1966
        from_rf = self.from_repository.revisions
 
1967
        revisions = from_rf.get_record_stream(
 
1968
            keys,
 
1969
            self.to_format._fetch_order,
 
1970
            not self.delta_on_metadata())
 
1971
        return [('signatures', signatures), ('revisions', revisions)]
 
1972
 
 
1973
    def _generate_root_texts(self, revs):
 
1974
        """This will be called by get_stream between fetching weave texts and
 
1975
        fetching the inventory weave.
 
1976
        """
 
1977
        if self._rich_root_upgrade():
 
1978
            return _mod_fetch.Inter1and2Helper(
 
1979
                self.from_repository).generate_root_texts(revs)
 
1980
        else:
 
1981
            return []
 
1982
 
 
1983
    def get_stream(self, search):
 
1984
        phase = 'file'
 
1985
        revs = search.get_keys()
 
1986
        graph = self.from_repository.get_graph()
 
1987
        revs = tsort.topo_sort(graph.get_parent_map(revs))
 
1988
        data_to_fetch = self.from_repository.item_keys_introduced_by(revs)
 
1989
        text_keys = []
 
1990
        for knit_kind, file_id, revisions in data_to_fetch:
 
1991
            if knit_kind != phase:
 
1992
                phase = knit_kind
 
1993
                # Make a new progress bar for this phase
 
1994
            if knit_kind == "file":
 
1995
                # Accumulate file texts
 
1996
                text_keys.extend([(file_id, revision) for revision in
 
1997
                    revisions])
 
1998
            elif knit_kind == "inventory":
 
1999
                # Now copy the file texts.
 
2000
                from_texts = self.from_repository.texts
 
2001
                yield ('texts', from_texts.get_record_stream(
 
2002
                    text_keys, self.to_format._fetch_order,
 
2003
                    not self.to_format._fetch_uses_deltas))
 
2004
                # Cause an error if a text occurs after we have done the
 
2005
                # copy.
 
2006
                text_keys = None
 
2007
                # Before we process the inventory we generate the root
 
2008
                # texts (if necessary) so that the inventories references
 
2009
                # will be valid.
 
2010
                for _ in self._generate_root_texts(revs):
 
2011
                    yield _
 
2012
                # we fetch only the referenced inventories because we do not
 
2013
                # know for unselected inventories whether all their required
 
2014
                # texts are present in the other repository - it could be
 
2015
                # corrupt.
 
2016
                for info in self._get_inventory_stream(revs):
 
2017
                    yield info
 
2018
            elif knit_kind == "signatures":
 
2019
                # Nothing to do here; this will be taken care of when
 
2020
                # _fetch_revision_texts happens.
 
2021
                pass
 
2022
            elif knit_kind == "revisions":
 
2023
                for record in self._fetch_revision_texts(revs):
 
2024
                    yield record
 
2025
            else:
 
2026
                raise AssertionError("Unknown knit kind %r" % knit_kind)
 
2027
 
 
2028
    def get_stream_for_missing_keys(self, missing_keys):
 
2029
        # missing keys can only occur when we are byte copying and not
 
2030
        # translating (because translation means we don't send
 
2031
        # unreconstructable deltas ever).
 
2032
        keys = {}
 
2033
        keys['texts'] = set()
 
2034
        keys['revisions'] = set()
 
2035
        keys['inventories'] = set()
 
2036
        keys['chk_bytes'] = set()
 
2037
        keys['signatures'] = set()
 
2038
        for key in missing_keys:
 
2039
            keys[key[0]].add(key[1:])
 
2040
        if len(keys['revisions']):
 
2041
            # If we allowed copying revisions at this point, we could end up
 
2042
            # copying a revision without copying its required texts: a
 
2043
            # violation of the requirements for repository integrity.
 
2044
            raise AssertionError(
 
2045
                'cannot copy revisions to fill in missing deltas %s' % (
 
2046
                    keys['revisions'],))
 
2047
        for substream_kind, keys in viewitems(keys):
 
2048
            vf = getattr(self.from_repository, substream_kind)
 
2049
            if vf is None and keys:
 
2050
                    raise AssertionError(
 
2051
                        "cannot fill in keys for a versioned file we don't"
 
2052
                        " have: %s needs %s" % (substream_kind, keys))
 
2053
            if not keys:
 
2054
                # No need to stream something we don't have
 
2055
                continue
 
2056
            if substream_kind == 'inventories':
 
2057
                # Some missing keys are genuinely ghosts, filter those out.
 
2058
                present = self.from_repository.inventories.get_parent_map(keys)
 
2059
                revs = [key[0] for key in present]
 
2060
                # Get the inventory stream more-or-less as we do for the
 
2061
                # original stream; there's no reason to assume that records
 
2062
                # direct from the source will be suitable for the sink.  (Think
 
2063
                # e.g. 2a -> 1.9-rich-root).
 
2064
                for info in self._get_inventory_stream(revs, missing=True):
 
2065
                    yield info
 
2066
                continue
 
2067
 
 
2068
            # Ask for full texts always so that we don't need more round trips
 
2069
            # after this stream.
 
2070
            # Some of the missing keys are genuinely ghosts, so filter absent
 
2071
            # records. The Sink is responsible for doing another check to
 
2072
            # ensure that ghosts don't introduce missing data for future
 
2073
            # fetches.
 
2074
            stream = versionedfile.filter_absent(vf.get_record_stream(keys,
 
2075
                self.to_format._fetch_order, True))
 
2076
            yield substream_kind, stream
 
2077
 
 
2078
    def inventory_fetch_order(self):
 
2079
        if self._rich_root_upgrade():
 
2080
            return 'topological'
 
2081
        else:
 
2082
            return self.to_format._fetch_order
 
2083
 
 
2084
    def _rich_root_upgrade(self):
 
2085
        return (not self.from_repository._format.rich_root_data and
 
2086
            self.to_format.rich_root_data)
 
2087
 
 
2088
    def _get_inventory_stream(self, revision_ids, missing=False):
 
2089
        from_format = self.from_repository._format
 
2090
        if (from_format.supports_chks and self.to_format.supports_chks and
 
2091
            from_format.network_name() == self.to_format.network_name()):
 
2092
            raise AssertionError(
 
2093
                "this case should be handled by GroupCHKStreamSource")
 
2094
        elif 'forceinvdeltas' in debug.debug_flags:
 
2095
            return self._get_convertable_inventory_stream(revision_ids,
 
2096
                    delta_versus_null=missing)
 
2097
        elif from_format.network_name() == self.to_format.network_name():
 
2098
            # Same format.
 
2099
            return self._get_simple_inventory_stream(revision_ids,
 
2100
                    missing=missing)
 
2101
        elif (not from_format.supports_chks and not self.to_format.supports_chks
 
2102
                and from_format._serializer == self.to_format._serializer):
 
2103
            # Essentially the same format.
 
2104
            return self._get_simple_inventory_stream(revision_ids,
 
2105
                    missing=missing)
 
2106
        else:
 
2107
            # Any time we switch serializations, we want to use an
 
2108
            # inventory-delta based approach.
 
2109
            return self._get_convertable_inventory_stream(revision_ids,
 
2110
                    delta_versus_null=missing)
 
2111
 
 
2112
    def _get_simple_inventory_stream(self, revision_ids, missing=False):
 
2113
        # NB: This currently reopens the inventory weave in source;
 
2114
        # using a single stream interface instead would avoid this.
 
2115
        from_weave = self.from_repository.inventories
 
2116
        if missing:
 
2117
            delta_closure = True
 
2118
        else:
 
2119
            delta_closure = not self.delta_on_metadata()
 
2120
        yield ('inventories', from_weave.get_record_stream(
 
2121
            [(rev_id,) for rev_id in revision_ids],
 
2122
            self.inventory_fetch_order(), delta_closure))
 
2123
 
 
2124
    def _get_convertable_inventory_stream(self, revision_ids,
 
2125
                                          delta_versus_null=False):
 
2126
        # The two formats are sufficiently different that there is no fast
 
2127
        # path, so we need to send just inventorydeltas, which any
 
2128
        # sufficiently modern client can insert into any repository.
 
2129
        # The StreamSink code expects to be able to
 
2130
        # convert on the target, so we need to put bytes-on-the-wire that can
 
2131
        # be converted.  That means inventory deltas (if the remote is <1.19,
 
2132
        # RemoteStreamSink will fallback to VFS to insert the deltas).
 
2133
        yield ('inventory-deltas',
 
2134
           self._stream_invs_as_deltas(revision_ids,
 
2135
                                       delta_versus_null=delta_versus_null))
 
2136
 
 
2137
    def _stream_invs_as_deltas(self, revision_ids, delta_versus_null=False):
 
2138
        """Return a stream of inventory-deltas for the given rev ids.
 
2139
 
 
2140
        :param revision_ids: The list of inventories to transmit
 
2141
        :param delta_versus_null: Don't try to find a minimal delta for this
 
2142
            entry, instead compute the delta versus the NULL_REVISION. This
 
2143
            effectively streams a complete inventory. Used for stuff like
 
2144
            filling in missing parents, etc.
 
2145
        """
 
2146
        from_repo = self.from_repository
 
2147
        revision_keys = [(rev_id,) for rev_id in revision_ids]
 
2148
        parent_map = from_repo.inventories.get_parent_map(revision_keys)
 
2149
        # XXX: possibly repos could implement a more efficient iter_inv_deltas
 
2150
        # method...
 
2151
        inventories = self.from_repository.iter_inventories(
 
2152
            revision_ids, 'topological')
 
2153
        format = from_repo._format
 
2154
        invs_sent_so_far = {_mod_revision.NULL_REVISION}
 
2155
        inventory_cache = lru_cache.LRUCache(50)
 
2156
        null_inventory = from_repo.revision_tree(
 
2157
            _mod_revision.NULL_REVISION).root_inventory
 
2158
        # XXX: ideally the rich-root/tree-refs flags would be per-revision, not
 
2159
        # per-repo (e.g.  streaming a non-rich-root revision out of a rich-root
 
2160
        # repo back into a non-rich-root repo ought to be allowed)
 
2161
        serializer = inventory_delta.InventoryDeltaSerializer(
 
2162
            versioned_root=format.rich_root_data,
 
2163
            tree_references=format.supports_tree_reference)
 
2164
        for inv in inventories:
 
2165
            key = (inv.revision_id,)
 
2166
            parent_keys = parent_map.get(key, ())
 
2167
            delta = None
 
2168
            if not delta_versus_null and parent_keys:
 
2169
                # The caller did not ask for complete inventories and we have
 
2170
                # some parents that we can delta against.  Make a delta against
 
2171
                # each parent so that we can find the smallest.
 
2172
                parent_ids = [parent_key[0] for parent_key in parent_keys]
 
2173
                for parent_id in parent_ids:
 
2174
                    if parent_id not in invs_sent_so_far:
 
2175
                        # We don't know that the remote side has this basis, so
 
2176
                        # we can't use it.
 
2177
                        continue
 
2178
                    if parent_id == _mod_revision.NULL_REVISION:
 
2179
                        parent_inv = null_inventory
 
2180
                    else:
 
2181
                        parent_inv = inventory_cache.get(parent_id, None)
 
2182
                        if parent_inv is None:
 
2183
                            parent_inv = from_repo.get_inventory(parent_id)
 
2184
                    candidate_delta = inv._make_delta(parent_inv)
 
2185
                    if (delta is None or
 
2186
                        len(delta) > len(candidate_delta)):
 
2187
                        delta = candidate_delta
 
2188
                        basis_id = parent_id
 
2189
            if delta is None:
 
2190
                # Either none of the parents ended up being suitable, or we
 
2191
                # were asked to delta against NULL
 
2192
                basis_id = _mod_revision.NULL_REVISION
 
2193
                delta = inv._make_delta(null_inventory)
 
2194
            invs_sent_so_far.add(inv.revision_id)
 
2195
            inventory_cache[inv.revision_id] = inv
 
2196
            delta_serialized = ''.join(
 
2197
                serializer.delta_to_lines(basis_id, key[-1], delta))
 
2198
            yield versionedfile.FulltextContentFactory(
 
2199
                key, parent_keys, None, delta_serialized)
 
2200
 
 
2201
 
 
2202
class _VersionedFileChecker(object):
 
2203
 
 
2204
    def __init__(self, repository, text_key_references=None, ancestors=None):
 
2205
        self.repository = repository
 
2206
        self.text_index = self.repository._generate_text_key_index(
 
2207
            text_key_references=text_key_references, ancestors=ancestors)
 
2208
 
 
2209
    def calculate_file_version_parents(self, text_key):
 
2210
        """Calculate the correct parents for a file version according to
 
2211
        the inventories.
 
2212
        """
 
2213
        parent_keys = self.text_index[text_key]
 
2214
        if parent_keys == [_mod_revision.NULL_REVISION]:
 
2215
            return ()
 
2216
        return tuple(parent_keys)
 
2217
 
 
2218
    def check_file_version_parents(self, texts, progress_bar=None):
 
2219
        """Check the parents stored in a versioned file are correct.
 
2220
 
 
2221
        It also detects file versions that are not referenced by their
 
2222
        corresponding revision's inventory.
 
2223
 
 
2224
        :returns: A tuple of (wrong_parents, dangling_file_versions).
 
2225
            wrong_parents is a dict mapping {revision_id: (stored_parents,
 
2226
            correct_parents)} for each revision_id where the stored parents
 
2227
            are not correct.  dangling_file_versions is a set of (file_id,
 
2228
            revision_id) tuples for versions that are present in this versioned
 
2229
            file, but not used by the corresponding inventory.
 
2230
        """
 
2231
        local_progress = None
 
2232
        if progress_bar is None:
 
2233
            local_progress = ui.ui_factory.nested_progress_bar()
 
2234
            progress_bar = local_progress
 
2235
        try:
 
2236
            return self._check_file_version_parents(texts, progress_bar)
 
2237
        finally:
 
2238
            if local_progress:
 
2239
                local_progress.finished()
 
2240
 
 
2241
    def _check_file_version_parents(self, texts, progress_bar):
 
2242
        """See check_file_version_parents."""
 
2243
        wrong_parents = {}
 
2244
        self.file_ids = {file_id for file_id, _ in self.text_index}
 
2245
        # text keys is now grouped by file_id
 
2246
        n_versions = len(self.text_index)
 
2247
        progress_bar.update(gettext('loading text store'), 0, n_versions)
 
2248
        parent_map = self.repository.texts.get_parent_map(self.text_index)
 
2249
        # On unlistable transports this could well be empty/error...
 
2250
        text_keys = self.repository.texts.keys()
 
2251
        unused_keys = frozenset(text_keys) - set(self.text_index)
 
2252
        for num, key in enumerate(self.text_index):
 
2253
            progress_bar.update(gettext('checking text graph'), num, n_versions)
 
2254
            correct_parents = self.calculate_file_version_parents(key)
 
2255
            try:
 
2256
                knit_parents = parent_map[key]
 
2257
            except errors.RevisionNotPresent:
 
2258
                # Missing text!
 
2259
                knit_parents = None
 
2260
            if correct_parents != knit_parents:
 
2261
                wrong_parents[key] = (knit_parents, correct_parents)
 
2262
        return wrong_parents, unused_keys
 
2263
 
 
2264
 
 
2265
class InterVersionedFileRepository(InterRepository):
 
2266
 
 
2267
    _walk_to_common_revisions_batch_size = 50
 
2268
 
 
2269
    supports_fetch_spec = True
 
2270
 
 
2271
    @needs_write_lock
 
2272
    def fetch(self, revision_id=None, find_ghosts=False,
 
2273
            fetch_spec=None):
 
2274
        """Fetch the content required to construct revision_id.
 
2275
 
 
2276
        The content is copied from self.source to self.target.
 
2277
 
 
2278
        :param revision_id: if None all content is copied, if NULL_REVISION no
 
2279
                            content is copied.
 
2280
        :return: None.
 
2281
        """
 
2282
        if self.target._format.experimental:
 
2283
            ui.ui_factory.show_user_warning('experimental_format_fetch',
 
2284
                from_format=self.source._format,
 
2285
                to_format=self.target._format)
 
2286
        from breezy.fetch import RepoFetcher
 
2287
        # See <https://launchpad.net/bugs/456077> asking for a warning here
 
2288
        if self.source._format.network_name() != self.target._format.network_name():
 
2289
            ui.ui_factory.show_user_warning('cross_format_fetch',
 
2290
                from_format=self.source._format,
 
2291
                to_format=self.target._format)
 
2292
        f = RepoFetcher(to_repository=self.target,
 
2293
                               from_repository=self.source,
 
2294
                               last_revision=revision_id,
 
2295
                               fetch_spec=fetch_spec,
 
2296
                               find_ghosts=find_ghosts)
 
2297
 
 
2298
    def _walk_to_common_revisions(self, revision_ids, if_present_ids=None):
 
2299
        """Walk out from revision_ids in source to revisions target has.
 
2300
 
 
2301
        :param revision_ids: The start point for the search.
 
2302
        :return: A set of revision ids.
 
2303
        """
 
2304
        target_graph = self.target.get_graph()
 
2305
        revision_ids = frozenset(revision_ids)
 
2306
        if if_present_ids:
 
2307
            all_wanted_revs = revision_ids.union(if_present_ids)
 
2308
        else:
 
2309
            all_wanted_revs = revision_ids
 
2310
        missing_revs = set()
 
2311
        source_graph = self.source.get_graph()
 
2312
        # ensure we don't pay silly lookup costs.
 
2313
        searcher = source_graph._make_breadth_first_searcher(all_wanted_revs)
 
2314
        null_set = frozenset([_mod_revision.NULL_REVISION])
 
2315
        searcher_exhausted = False
 
2316
        while True:
 
2317
            next_revs = set()
 
2318
            ghosts = set()
 
2319
            # Iterate the searcher until we have enough next_revs
 
2320
            while len(next_revs) < self._walk_to_common_revisions_batch_size:
 
2321
                try:
 
2322
                    next_revs_part, ghosts_part = searcher.next_with_ghosts()
 
2323
                    next_revs.update(next_revs_part)
 
2324
                    ghosts.update(ghosts_part)
 
2325
                except StopIteration:
 
2326
                    searcher_exhausted = True
 
2327
                    break
 
2328
            # If there are ghosts in the source graph, and the caller asked for
 
2329
            # them, make sure that they are present in the target.
 
2330
            # We don't care about other ghosts as we can't fetch them and
 
2331
            # haven't been asked to.
 
2332
            ghosts_to_check = set(revision_ids.intersection(ghosts))
 
2333
            revs_to_get = set(next_revs).union(ghosts_to_check)
 
2334
            if revs_to_get:
 
2335
                have_revs = set(target_graph.get_parent_map(revs_to_get))
 
2336
                # we always have NULL_REVISION present.
 
2337
                have_revs = have_revs.union(null_set)
 
2338
                # Check if the target is missing any ghosts we need.
 
2339
                ghosts_to_check.difference_update(have_revs)
 
2340
                if ghosts_to_check:
 
2341
                    # One of the caller's revision_ids is a ghost in both the
 
2342
                    # source and the target.
 
2343
                    raise errors.NoSuchRevision(
 
2344
                        self.source, ghosts_to_check.pop())
 
2345
                missing_revs.update(next_revs - have_revs)
 
2346
                # Because we may have walked past the original stop point, make
 
2347
                # sure everything is stopped
 
2348
                stop_revs = searcher.find_seen_ancestors(have_revs)
 
2349
                searcher.stop_searching_any(stop_revs)
 
2350
            if searcher_exhausted:
 
2351
                break
 
2352
        (started_keys, excludes, included_keys) = searcher.get_state()
 
2353
        return vf_search.SearchResult(started_keys, excludes,
 
2354
            len(included_keys), included_keys)
 
2355
 
 
2356
    @needs_read_lock
 
2357
    def search_missing_revision_ids(self,
 
2358
            find_ghosts=True, revision_ids=None, if_present_ids=None,
 
2359
            limit=None):
 
2360
        """Return the revision ids that source has that target does not.
 
2361
 
 
2362
        :param revision_ids: return revision ids included by these
 
2363
            revision_ids.  NoSuchRevision will be raised if any of these
 
2364
            revisions are not present.
 
2365
        :param if_present_ids: like revision_ids, but will not cause
 
2366
            NoSuchRevision if any of these are absent, instead they will simply
 
2367
            not be in the result.  This is useful for e.g. finding revisions
 
2368
            to fetch for tags, which may reference absent revisions.
 
2369
        :param find_ghosts: If True find missing revisions in deep history
 
2370
            rather than just finding the surface difference.
 
2371
        :return: A breezy.graph.SearchResult.
 
2372
        """
 
2373
        # stop searching at found target revisions.
 
2374
        if not find_ghosts and (revision_ids is not None or if_present_ids is
 
2375
                not None):
 
2376
            result = self._walk_to_common_revisions(revision_ids,
 
2377
                    if_present_ids=if_present_ids)
 
2378
            if limit is None:
 
2379
                return result
 
2380
            result_set = result.get_keys()
 
2381
        else:
 
2382
            # generic, possibly worst case, slow code path.
 
2383
            target_ids = set(self.target.all_revision_ids())
 
2384
            source_ids = self._present_source_revisions_for(
 
2385
                revision_ids, if_present_ids)
 
2386
            result_set = set(source_ids).difference(target_ids)
 
2387
        if limit is not None:
 
2388
            topo_ordered = self.source.get_graph().iter_topo_order(result_set)
 
2389
            result_set = set(itertools.islice(topo_ordered, limit))
 
2390
        return self.source.revision_ids_to_search_result(result_set)
 
2391
 
 
2392
    def _present_source_revisions_for(self, revision_ids, if_present_ids=None):
 
2393
        """Returns set of all revisions in ancestry of revision_ids present in
 
2394
        the source repo.
 
2395
 
 
2396
        :param revision_ids: if None, all revisions in source are returned.
 
2397
        :param if_present_ids: like revision_ids, but if any/all of these are
 
2398
            absent no error is raised.
 
2399
        """
 
2400
        if revision_ids is not None or if_present_ids is not None:
 
2401
            # First, ensure all specified revisions exist.  Callers expect
 
2402
            # NoSuchRevision when they pass absent revision_ids here.
 
2403
            if revision_ids is None:
 
2404
                revision_ids = set()
 
2405
            if if_present_ids is None:
 
2406
                if_present_ids = set()
 
2407
            revision_ids = set(revision_ids)
 
2408
            if_present_ids = set(if_present_ids)
 
2409
            all_wanted_ids = revision_ids.union(if_present_ids)
 
2410
            graph = self.source.get_graph()
 
2411
            present_revs = set(graph.get_parent_map(all_wanted_ids))
 
2412
            missing = revision_ids.difference(present_revs)
 
2413
            if missing:
 
2414
                raise errors.NoSuchRevision(self.source, missing.pop())
 
2415
            found_ids = all_wanted_ids.intersection(present_revs)
 
2416
            source_ids = [rev_id for (rev_id, parents) in
 
2417
                          graph.iter_ancestry(found_ids)
 
2418
                          if rev_id != _mod_revision.NULL_REVISION
 
2419
                          and parents is not None]
 
2420
        else:
 
2421
            source_ids = self.source.all_revision_ids()
 
2422
        return set(source_ids)
 
2423
 
 
2424
    @classmethod
 
2425
    def _get_repo_format_to_test(self):
 
2426
        return None
 
2427
 
 
2428
    @classmethod
 
2429
    def is_compatible(cls, source, target):
 
2430
        # The default implementation is compatible with everything
 
2431
        return (source._format.supports_full_versioned_files and
 
2432
                target._format.supports_full_versioned_files)
 
2433
 
 
2434
 
 
2435
class InterDifferingSerializer(InterVersionedFileRepository):
 
2436
 
 
2437
    @classmethod
 
2438
    def _get_repo_format_to_test(self):
 
2439
        return None
 
2440
 
 
2441
    @staticmethod
 
2442
    def is_compatible(source, target):
 
2443
        if not source._format.supports_full_versioned_files:
 
2444
            return False
 
2445
        if not target._format.supports_full_versioned_files:
 
2446
            return False
 
2447
        # This is redundant with format.check_conversion_target(), however that
 
2448
        # raises an exception, and we just want to say "False" as in we won't
 
2449
        # support converting between these formats.
 
2450
        if 'IDS_never' in debug.debug_flags:
 
2451
            return False
 
2452
        if source.supports_rich_root() and not target.supports_rich_root():
 
2453
            return False
 
2454
        if (source._format.supports_tree_reference
 
2455
            and not target._format.supports_tree_reference):
 
2456
            return False
 
2457
        if target._fallback_repositories and target._format.supports_chks:
 
2458
            # IDS doesn't know how to copy CHKs for the parent inventories it
 
2459
            # adds to stacked repos.
 
2460
            return False
 
2461
        if 'IDS_always' in debug.debug_flags:
 
2462
            return True
 
2463
        # Only use this code path for local source and target.  IDS does far
 
2464
        # too much IO (both bandwidth and roundtrips) over a network.
 
2465
        if not source.controldir.transport.base.startswith('file:///'):
 
2466
            return False
 
2467
        if not target.controldir.transport.base.startswith('file:///'):
 
2468
            return False
 
2469
        return True
 
2470
 
 
2471
    def _get_trees(self, revision_ids, cache):
 
2472
        possible_trees = []
 
2473
        for rev_id in revision_ids:
 
2474
            if rev_id in cache:
 
2475
                possible_trees.append((rev_id, cache[rev_id]))
 
2476
            else:
 
2477
                # Not cached, but inventory might be present anyway.
 
2478
                try:
 
2479
                    tree = self.source.revision_tree(rev_id)
 
2480
                except errors.NoSuchRevision:
 
2481
                    # Nope, parent is ghost.
 
2482
                    pass
 
2483
                else:
 
2484
                    cache[rev_id] = tree
 
2485
                    possible_trees.append((rev_id, tree))
 
2486
        return possible_trees
 
2487
 
 
2488
    def _get_delta_for_revision(self, tree, parent_ids, possible_trees):
 
2489
        """Get the best delta and base for this revision.
 
2490
 
 
2491
        :return: (basis_id, delta)
 
2492
        """
 
2493
        deltas = []
 
2494
        # Generate deltas against each tree, to find the shortest.
 
2495
        # FIXME: Support nested trees
 
2496
        texts_possibly_new_in_tree = set()
 
2497
        for basis_id, basis_tree in possible_trees:
 
2498
            delta = tree.root_inventory._make_delta(basis_tree.root_inventory)
 
2499
            for old_path, new_path, file_id, new_entry in delta:
 
2500
                if new_path is None:
 
2501
                    # This file_id isn't present in the new rev, so we don't
 
2502
                    # care about it.
 
2503
                    continue
 
2504
                if not new_path:
 
2505
                    # Rich roots are handled elsewhere...
 
2506
                    continue
 
2507
                kind = new_entry.kind
 
2508
                if kind != 'directory' and kind != 'file':
 
2509
                    # No text record associated with this inventory entry.
 
2510
                    continue
 
2511
                # This is a directory or file that has changed somehow.
 
2512
                texts_possibly_new_in_tree.add((file_id, new_entry.revision))
 
2513
            deltas.append((len(delta), basis_id, delta))
 
2514
        deltas.sort()
 
2515
        return deltas[0][1:]
 
2516
 
 
2517
    def _fetch_parent_invs_for_stacking(self, parent_map, cache):
 
2518
        """Find all parent revisions that are absent, but for which the
 
2519
        inventory is present, and copy those inventories.
 
2520
 
 
2521
        This is necessary to preserve correctness when the source is stacked
 
2522
        without fallbacks configured.  (Note that in cases like upgrade the
 
2523
        source may be not have _fallback_repositories even though it is
 
2524
        stacked.)
 
2525
        """
 
2526
        parent_revs = set(itertools.chain.from_iterable(viewvalues(
 
2527
            parent_map)))
 
2528
        present_parents = self.source.get_parent_map(parent_revs)
 
2529
        absent_parents = parent_revs.difference(present_parents)
 
2530
        parent_invs_keys_for_stacking = self.source.inventories.get_parent_map(
 
2531
            (rev_id,) for rev_id in absent_parents)
 
2532
        parent_inv_ids = [key[-1] for key in parent_invs_keys_for_stacking]
 
2533
        for parent_tree in self.source.revision_trees(parent_inv_ids):
 
2534
            current_revision_id = parent_tree.get_revision_id()
 
2535
            parents_parents_keys = parent_invs_keys_for_stacking[
 
2536
                (current_revision_id,)]
 
2537
            parents_parents = [key[-1] for key in parents_parents_keys]
 
2538
            basis_id = _mod_revision.NULL_REVISION
 
2539
            basis_tree = self.source.revision_tree(basis_id)
 
2540
            delta = parent_tree.root_inventory._make_delta(
 
2541
                basis_tree.root_inventory)
 
2542
            self.target.add_inventory_by_delta(
 
2543
                basis_id, delta, current_revision_id, parents_parents)
 
2544
            cache[current_revision_id] = parent_tree
 
2545
 
 
2546
    def _fetch_batch(self, revision_ids, basis_id, cache):
 
2547
        """Fetch across a few revisions.
 
2548
 
 
2549
        :param revision_ids: The revisions to copy
 
2550
        :param basis_id: The revision_id of a tree that must be in cache, used
 
2551
            as a basis for delta when no other base is available
 
2552
        :param cache: A cache of RevisionTrees that we can use.
 
2553
        :return: The revision_id of the last converted tree. The RevisionTree
 
2554
            for it will be in cache
 
2555
        """
 
2556
        # Walk though all revisions; get inventory deltas, copy referenced
 
2557
        # texts that delta references, insert the delta, revision and
 
2558
        # signature.
 
2559
        root_keys_to_create = set()
 
2560
        text_keys = set()
 
2561
        pending_deltas = []
 
2562
        pending_revisions = []
 
2563
        parent_map = self.source.get_parent_map(revision_ids)
 
2564
        self._fetch_parent_invs_for_stacking(parent_map, cache)
 
2565
        self.source._safe_to_return_from_cache = True
 
2566
        for tree in self.source.revision_trees(revision_ids):
 
2567
            # Find a inventory delta for this revision.
 
2568
            # Find text entries that need to be copied, too.
 
2569
            current_revision_id = tree.get_revision_id()
 
2570
            parent_ids = parent_map.get(current_revision_id, ())
 
2571
            parent_trees = self._get_trees(parent_ids, cache)
 
2572
            possible_trees = list(parent_trees)
 
2573
            if len(possible_trees) == 0:
 
2574
                # There either aren't any parents, or the parents are ghosts,
 
2575
                # so just use the last converted tree.
 
2576
                possible_trees.append((basis_id, cache[basis_id]))
 
2577
            basis_id, delta = self._get_delta_for_revision(tree, parent_ids,
 
2578
                                                           possible_trees)
 
2579
            revision = self.source.get_revision(current_revision_id)
 
2580
            pending_deltas.append((basis_id, delta,
 
2581
                current_revision_id, revision.parent_ids))
 
2582
            if self._converting_to_rich_root:
 
2583
                self._revision_id_to_root_id[current_revision_id] = \
 
2584
                    tree.get_root_id()
 
2585
            # Determine which texts are in present in this revision but not in
 
2586
            # any of the available parents.
 
2587
            texts_possibly_new_in_tree = set()
 
2588
            for old_path, new_path, file_id, entry in delta:
 
2589
                if new_path is None:
 
2590
                    # This file_id isn't present in the new rev
 
2591
                    continue
 
2592
                if not new_path:
 
2593
                    # This is the root
 
2594
                    if not self.target.supports_rich_root():
 
2595
                        # The target doesn't support rich root, so we don't
 
2596
                        # copy
 
2597
                        continue
 
2598
                    if self._converting_to_rich_root:
 
2599
                        # This can't be copied normally, we have to insert
 
2600
                        # it specially
 
2601
                        root_keys_to_create.add((file_id, entry.revision))
 
2602
                        continue
 
2603
                kind = entry.kind
 
2604
                texts_possibly_new_in_tree.add((file_id, entry.revision))
 
2605
            for basis_id, basis_tree in possible_trees:
 
2606
                basis_inv = basis_tree.root_inventory
 
2607
                for file_key in list(texts_possibly_new_in_tree):
 
2608
                    file_id, file_revision = file_key
 
2609
                    try:
 
2610
                        entry = basis_inv[file_id]
 
2611
                    except errors.NoSuchId:
 
2612
                        continue
 
2613
                    if entry.revision == file_revision:
 
2614
                        texts_possibly_new_in_tree.remove(file_key)
 
2615
            text_keys.update(texts_possibly_new_in_tree)
 
2616
            pending_revisions.append(revision)
 
2617
            cache[current_revision_id] = tree
 
2618
            basis_id = current_revision_id
 
2619
        self.source._safe_to_return_from_cache = False
 
2620
        # Copy file texts
 
2621
        from_texts = self.source.texts
 
2622
        to_texts = self.target.texts
 
2623
        if root_keys_to_create:
 
2624
            root_stream = _mod_fetch._new_root_data_stream(
 
2625
                root_keys_to_create, self._revision_id_to_root_id, parent_map,
 
2626
                self.source)
 
2627
            to_texts.insert_record_stream(root_stream)
 
2628
        to_texts.insert_record_stream(from_texts.get_record_stream(
 
2629
            text_keys, self.target._format._fetch_order,
 
2630
            not self.target._format._fetch_uses_deltas))
 
2631
        # insert inventory deltas
 
2632
        for delta in pending_deltas:
 
2633
            self.target.add_inventory_by_delta(*delta)
 
2634
        if self.target._fallback_repositories:
 
2635
            # Make sure this stacked repository has all the parent inventories
 
2636
            # for the new revisions that we are about to insert.  We do this
 
2637
            # before adding the revisions so that no revision is added until
 
2638
            # all the inventories it may depend on are added.
 
2639
            # Note that this is overzealous, as we may have fetched these in an
 
2640
            # earlier batch.
 
2641
            parent_ids = set()
 
2642
            revision_ids = set()
 
2643
            for revision in pending_revisions:
 
2644
                revision_ids.add(revision.revision_id)
 
2645
                parent_ids.update(revision.parent_ids)
 
2646
            parent_ids.difference_update(revision_ids)
 
2647
            parent_ids.discard(_mod_revision.NULL_REVISION)
 
2648
            parent_map = self.source.get_parent_map(parent_ids)
 
2649
            # we iterate over parent_map and not parent_ids because we don't
 
2650
            # want to try copying any revision which is a ghost
 
2651
            for parent_tree in self.source.revision_trees(parent_map):
 
2652
                current_revision_id = parent_tree.get_revision_id()
 
2653
                parents_parents = parent_map[current_revision_id]
 
2654
                possible_trees = self._get_trees(parents_parents, cache)
 
2655
                if len(possible_trees) == 0:
 
2656
                    # There either aren't any parents, or the parents are
 
2657
                    # ghosts, so just use the last converted tree.
 
2658
                    possible_trees.append((basis_id, cache[basis_id]))
 
2659
                basis_id, delta = self._get_delta_for_revision(parent_tree,
 
2660
                    parents_parents, possible_trees)
 
2661
                self.target.add_inventory_by_delta(
 
2662
                    basis_id, delta, current_revision_id, parents_parents)
 
2663
        # insert signatures and revisions
 
2664
        for revision in pending_revisions:
 
2665
            try:
 
2666
                signature = self.source.get_signature_text(
 
2667
                    revision.revision_id)
 
2668
                self.target.add_signature_text(revision.revision_id,
 
2669
                    signature)
 
2670
            except errors.NoSuchRevision:
 
2671
                pass
 
2672
            self.target.add_revision(revision.revision_id, revision)
 
2673
        return basis_id
 
2674
 
 
2675
    def _fetch_all_revisions(self, revision_ids, pb):
 
2676
        """Fetch everything for the list of revisions.
 
2677
 
 
2678
        :param revision_ids: The list of revisions to fetch. Must be in
 
2679
            topological order.
 
2680
        :param pb: A ProgressTask
 
2681
        :return: None
 
2682
        """
 
2683
        basis_id, basis_tree = self._get_basis(revision_ids[0])
 
2684
        batch_size = 100
 
2685
        cache = lru_cache.LRUCache(100)
 
2686
        cache[basis_id] = basis_tree
 
2687
        del basis_tree # We don't want to hang on to it here
 
2688
        hints = []
 
2689
        a_graph = None
 
2690
 
 
2691
        for offset in range(0, len(revision_ids), batch_size):
 
2692
            self.target.start_write_group()
 
2693
            try:
 
2694
                pb.update(gettext('Transferring revisions'), offset,
 
2695
                          len(revision_ids))
 
2696
                batch = revision_ids[offset:offset+batch_size]
 
2697
                basis_id = self._fetch_batch(batch, basis_id, cache)
 
2698
            except:
 
2699
                self.source._safe_to_return_from_cache = False
 
2700
                self.target.abort_write_group()
 
2701
                raise
 
2702
            else:
 
2703
                hint = self.target.commit_write_group()
 
2704
                if hint:
 
2705
                    hints.extend(hint)
 
2706
        if hints and self.target._format.pack_compresses:
 
2707
            self.target.pack(hint=hints)
 
2708
        pb.update(gettext('Transferring revisions'), len(revision_ids),
 
2709
                  len(revision_ids))
 
2710
 
 
2711
    @needs_write_lock
 
2712
    def fetch(self, revision_id=None, find_ghosts=False,
 
2713
            fetch_spec=None):
 
2714
        """See InterRepository.fetch()."""
 
2715
        if fetch_spec is not None:
 
2716
            revision_ids = fetch_spec.get_keys()
 
2717
        else:
 
2718
            revision_ids = None
 
2719
        if self.source._format.experimental:
 
2720
            ui.ui_factory.show_user_warning('experimental_format_fetch',
 
2721
                from_format=self.source._format,
 
2722
                to_format=self.target._format)
 
2723
        if (not self.source.supports_rich_root()
 
2724
            and self.target.supports_rich_root()):
 
2725
            self._converting_to_rich_root = True
 
2726
            self._revision_id_to_root_id = {}
 
2727
        else:
 
2728
            self._converting_to_rich_root = False
 
2729
        # See <https://launchpad.net/bugs/456077> asking for a warning here
 
2730
        if self.source._format.network_name() != self.target._format.network_name():
 
2731
            ui.ui_factory.show_user_warning('cross_format_fetch',
 
2732
                from_format=self.source._format,
 
2733
                to_format=self.target._format)
 
2734
        if revision_ids is None:
 
2735
            if revision_id:
 
2736
                search_revision_ids = [revision_id]
 
2737
            else:
 
2738
                search_revision_ids = None
 
2739
            revision_ids = self.target.search_missing_revision_ids(self.source,
 
2740
                revision_ids=search_revision_ids,
 
2741
                find_ghosts=find_ghosts).get_keys()
 
2742
        if not revision_ids:
 
2743
            return 0, 0
 
2744
        revision_ids = tsort.topo_sort(
 
2745
            self.source.get_graph().get_parent_map(revision_ids))
 
2746
        if not revision_ids:
 
2747
            return 0, 0
 
2748
        # Walk though all revisions; get inventory deltas, copy referenced
 
2749
        # texts that delta references, insert the delta, revision and
 
2750
        # signature.
 
2751
        pb = ui.ui_factory.nested_progress_bar()
 
2752
        try:
 
2753
            self._fetch_all_revisions(revision_ids, pb)
 
2754
        finally:
 
2755
            pb.finished()
 
2756
        return len(revision_ids), 0
 
2757
 
 
2758
    def _get_basis(self, first_revision_id):
 
2759
        """Get a revision and tree which exists in the target.
 
2760
 
 
2761
        This assumes that first_revision_id is selected for transmission
 
2762
        because all other ancestors are already present. If we can't find an
 
2763
        ancestor we fall back to NULL_REVISION since we know that is safe.
 
2764
 
 
2765
        :return: (basis_id, basis_tree)
 
2766
        """
 
2767
        first_rev = self.source.get_revision(first_revision_id)
 
2768
        try:
 
2769
            basis_id = first_rev.parent_ids[0]
 
2770
            # only valid as a basis if the target has it
 
2771
            self.target.get_revision(basis_id)
 
2772
            # Try to get a basis tree - if it's a ghost it will hit the
 
2773
            # NoSuchRevision case.
 
2774
            basis_tree = self.source.revision_tree(basis_id)
 
2775
        except (IndexError, errors.NoSuchRevision):
 
2776
            basis_id = _mod_revision.NULL_REVISION
 
2777
            basis_tree = self.source.revision_tree(basis_id)
 
2778
        return basis_id, basis_tree
 
2779
 
 
2780
 
 
2781
class InterSameDataRepository(InterVersionedFileRepository):
 
2782
    """Code for converting between repositories that represent the same data.
 
2783
 
 
2784
    Data format and model must match for this to work.
 
2785
    """
 
2786
 
 
2787
    @classmethod
 
2788
    def _get_repo_format_to_test(self):
 
2789
        """Repository format for testing with.
 
2790
 
 
2791
        InterSameData can pull from subtree to subtree and from non-subtree to
 
2792
        non-subtree, so we test this with the richest repository format.
 
2793
        """
 
2794
        from breezy.bzr import knitrepo
 
2795
        return knitrepo.RepositoryFormatKnit3()
 
2796
 
 
2797
    @staticmethod
 
2798
    def is_compatible(source, target):
 
2799
        return (
 
2800
            InterRepository._same_model(source, target) and
 
2801
            source._format.supports_full_versioned_files and
 
2802
            target._format.supports_full_versioned_files)
 
2803
 
 
2804
 
 
2805
InterRepository.register_optimiser(InterVersionedFileRepository)
 
2806
InterRepository.register_optimiser(InterDifferingSerializer)
 
2807
InterRepository.register_optimiser(InterSameDataRepository)
 
2808
 
 
2809
 
 
2810
def install_revisions(repository, iterable, num_revisions=None, pb=None):
 
2811
    """Install all revision data into a repository.
 
2812
 
 
2813
    Accepts an iterable of revision, tree, signature tuples.  The signature
 
2814
    may be None.
 
2815
    """
 
2816
    repository.start_write_group()
 
2817
    try:
 
2818
        inventory_cache = lru_cache.LRUCache(10)
 
2819
        for n, (revision, revision_tree, signature) in enumerate(iterable):
 
2820
            _install_revision(repository, revision, revision_tree, signature,
 
2821
                inventory_cache)
 
2822
            if pb is not None:
 
2823
                pb.update(gettext('Transferring revisions'), n + 1, num_revisions)
 
2824
    except:
 
2825
        repository.abort_write_group()
 
2826
        raise
 
2827
    else:
 
2828
        repository.commit_write_group()
 
2829
 
 
2830
 
 
2831
def _install_revision(repository, rev, revision_tree, signature,
 
2832
    inventory_cache):
 
2833
    """Install all revision data into a repository."""
 
2834
    present_parents = []
 
2835
    parent_trees = {}
 
2836
    for p_id in rev.parent_ids:
 
2837
        if repository.has_revision(p_id):
 
2838
            present_parents.append(p_id)
 
2839
            parent_trees[p_id] = repository.revision_tree(p_id)
 
2840
        else:
 
2841
            parent_trees[p_id] = repository.revision_tree(
 
2842
                                     _mod_revision.NULL_REVISION)
 
2843
 
 
2844
    # FIXME: Support nested trees
 
2845
    inv = revision_tree.root_inventory
 
2846
    entries = inv.iter_entries()
 
2847
    # backwards compatibility hack: skip the root id.
 
2848
    if not repository.supports_rich_root():
 
2849
        path, root = next(entries)
 
2850
        if root.revision != rev.revision_id:
 
2851
            raise errors.IncompatibleRevision(repr(repository))
 
2852
    text_keys = {}
 
2853
    for path, ie in entries:
 
2854
        text_keys[(ie.file_id, ie.revision)] = ie
 
2855
    text_parent_map = repository.texts.get_parent_map(text_keys)
 
2856
    missing_texts = set(text_keys) - set(text_parent_map)
 
2857
    # Add the texts that are not already present
 
2858
    for text_key in missing_texts:
 
2859
        ie = text_keys[text_key]
 
2860
        text_parents = []
 
2861
        # FIXME: TODO: The following loop overlaps/duplicates that done by
 
2862
        # commit to determine parents. There is a latent/real bug here where
 
2863
        # the parents inserted are not those commit would do - in particular
 
2864
        # they are not filtered by heads(). RBC, AB
 
2865
        for revision, tree in viewitems(parent_trees):
 
2866
            if not tree.has_id(ie.file_id):
 
2867
                continue
 
2868
            parent_id = tree.get_file_revision(ie.file_id)
 
2869
            if parent_id in text_parents:
 
2870
                continue
 
2871
            text_parents.append((ie.file_id, parent_id))
 
2872
        lines = revision_tree.get_file(ie.file_id).readlines()
 
2873
        repository.texts.add_lines(text_key, text_parents, lines)
 
2874
    try:
 
2875
        # install the inventory
 
2876
        if repository._format._commit_inv_deltas and len(rev.parent_ids):
 
2877
            # Cache this inventory
 
2878
            inventory_cache[rev.revision_id] = inv
 
2879
            try:
 
2880
                basis_inv = inventory_cache[rev.parent_ids[0]]
 
2881
            except KeyError:
 
2882
                repository.add_inventory(rev.revision_id, inv, present_parents)
 
2883
            else:
 
2884
                delta = inv._make_delta(basis_inv)
 
2885
                repository.add_inventory_by_delta(rev.parent_ids[0], delta,
 
2886
                    rev.revision_id, present_parents)
 
2887
        else:
 
2888
            repository.add_inventory(rev.revision_id, inv, present_parents)
 
2889
    except errors.RevisionAlreadyPresent:
 
2890
        pass
 
2891
    if signature is not None:
 
2892
        repository.add_signature_text(rev.revision_id, signature)
 
2893
    repository.add_revision(rev.revision_id, rev, inv)
 
2894
 
 
2895
 
 
2896
def install_revision(repository, rev, revision_tree):
 
2897
    """Install all revision data into a repository."""
 
2898
    install_revisions(repository, [(rev, revision_tree, None)])