/breezy/unstable

To get this branch, use:
bzr branch https://code.breezy-vcs.org/breezy/unstable

« back to all changes in this revision

Viewing changes to breezy/plugins/fastimport/revision_store.py

  • Committer: Jelmer Vernooij
  • Date: 2017-05-24 01:39:33 UTC
  • mfrom: (3815.3776.6)
  • Revision ID: jelmer@jelmer.uk-20170524013933-ir4y4tqtrsiz2ka2
New upstream snapshot.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2008, 2009 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, see <http://www.gnu.org/licenses/>.
 
15
 
 
16
"""An abstraction of a repository providing just the bits importing needs."""
 
17
 
 
18
from __future__ import absolute_import
 
19
 
 
20
import cStringIO
 
21
 
 
22
from ... import (
 
23
    errors,
 
24
    graph as _mod_graph,
 
25
    inventory,
 
26
    knit,
 
27
    lru_cache,
 
28
    osutils,
 
29
    revision as _mod_revision,
 
30
    trace,
 
31
    )
 
32
 
 
33
 
 
34
class _TreeShim(object):
 
35
    """Fake a Tree implementation.
 
36
 
 
37
    This implements just enough of the tree api to make commit builder happy.
 
38
    """
 
39
 
 
40
    def __init__(self, repo, basis_inv, inv_delta, content_provider):
 
41
        self._repo = repo
 
42
        self._content_provider = content_provider
 
43
        self._basis_inv = basis_inv
 
44
        self._inv_delta = inv_delta
 
45
        self._new_info_by_id = dict([(file_id, (new_path, ie))
 
46
                                    for _, new_path, file_id, ie in inv_delta])
 
47
 
 
48
    def id2path(self, file_id):
 
49
        if file_id in self._new_info_by_id:
 
50
            new_path = self._new_info_by_id[file_id][0]
 
51
            if new_path is None:
 
52
                raise errors.NoSuchId(self, file_id)
 
53
            return new_path
 
54
        return self._basis_inv.id2path(file_id)
 
55
 
 
56
    def path2id(self, path):
 
57
        # CommitBuilder currently only requires access to the root id. We don't
 
58
        # build a map of renamed files, etc. One possibility if we ever *do*
 
59
        # need more than just root, is to defer to basis_inv.path2id() and then
 
60
        # check if the file_id is in our _new_info_by_id dict. And in that
 
61
        # case, return _new_info_by_id[file_id][0]
 
62
        if path != '':
 
63
            raise NotImplementedError(_TreeShim.path2id)
 
64
        # TODO: Handle root renames?
 
65
        return self._basis_inv.root.file_id
 
66
 
 
67
    def get_file_with_stat(self, file_id, path=None):
 
68
        content = self.get_file_text(file_id, path)
 
69
        sio = cStringIO.StringIO(content)
 
70
        return sio, None
 
71
 
 
72
    def get_file_text(self, file_id, path=None):
 
73
        try:
 
74
            return self._content_provider(file_id)
 
75
        except KeyError:
 
76
            # The content wasn't shown as 'new'. Just validate this fact
 
77
            assert file_id not in self._new_info_by_id
 
78
            old_ie = self._basis_inv[file_id]
 
79
            old_text_key = (file_id, old_ie.revision)
 
80
            stream = self._repo.texts.get_record_stream([old_text_key],
 
81
                                                        'unordered', True)
 
82
            return stream.next().get_bytes_as('fulltext')
 
83
 
 
84
    def get_symlink_target(self, file_id):
 
85
        if file_id in self._new_info_by_id:
 
86
            ie = self._new_info_by_id[file_id][1]
 
87
            return ie.symlink_target
 
88
        return self._basis_inv[file_id].symlink_target
 
89
 
 
90
    def get_reference_revision(self, file_id, path=None):
 
91
        raise NotImplementedError(_TreeShim.get_reference_revision)
 
92
 
 
93
    def _delta_to_iter_changes(self):
 
94
        """Convert the inv_delta into an iter_changes repr."""
 
95
        # iter_changes is:
 
96
        #   (file_id,
 
97
        #    (old_path, new_path),
 
98
        #    content_changed,
 
99
        #    (old_versioned, new_versioned),
 
100
        #    (old_parent_id, new_parent_id),
 
101
        #    (old_name, new_name),
 
102
        #    (old_kind, new_kind),
 
103
        #    (old_exec, new_exec),
 
104
        #   )
 
105
        basis_inv = self._basis_inv
 
106
        for old_path, new_path, file_id, ie in self._inv_delta:
 
107
            # Perf: Would this be faster if we did 'if file_id in basis_inv'?
 
108
            # Since the *very* common case is that the file already exists, it
 
109
            # probably is better to optimize for that
 
110
            try:
 
111
                old_ie = basis_inv[file_id]
 
112
            except errors.NoSuchId:
 
113
                old_ie = None
 
114
                if ie is None:
 
115
                    raise AssertionError('How is both old and new None?')
 
116
                    change = (file_id,
 
117
                        (old_path, new_path),
 
118
                        False,
 
119
                        (False, False),
 
120
                        (None, None),
 
121
                        (None, None),
 
122
                        (None, None),
 
123
                        (None, None),
 
124
                        )
 
125
                change = (file_id,
 
126
                    (old_path, new_path),
 
127
                    True,
 
128
                    (False, True),
 
129
                    (None, ie.parent_id),
 
130
                    (None, ie.name),
 
131
                    (None, ie.kind),
 
132
                    (None, ie.executable),
 
133
                    )
 
134
            else:
 
135
                if ie is None:
 
136
                    change = (file_id,
 
137
                        (old_path, new_path),
 
138
                        True,
 
139
                        (True, False),
 
140
                        (old_ie.parent_id, None),
 
141
                        (old_ie.name, None),
 
142
                        (old_ie.kind, None),
 
143
                        (old_ie.executable, None),
 
144
                        )
 
145
                else:
 
146
                    content_modified = (ie.text_sha1 != old_ie.text_sha1
 
147
                                        or ie.text_size != old_ie.text_size)
 
148
                    # TODO: ie.kind != old_ie.kind
 
149
                    # TODO: symlinks changing targets, content_modified?
 
150
                    change = (file_id,
 
151
                        (old_path, new_path),
 
152
                        content_modified,
 
153
                        (True, True),
 
154
                        (old_ie.parent_id, ie.parent_id),
 
155
                        (old_ie.name, ie.name),
 
156
                        (old_ie.kind, ie.kind),
 
157
                        (old_ie.executable, ie.executable),
 
158
                        )
 
159
            yield change
 
160
 
 
161
 
 
162
class AbstractRevisionStore(object):
 
163
 
 
164
    def __init__(self, repo):
 
165
        """An object responsible for loading revisions into a repository.
 
166
 
 
167
        NOTE: Repository locking is not managed by this class. Clients
 
168
        should take a write lock, call load() multiple times, then release
 
169
        the lock.
 
170
 
 
171
        :param repository: the target repository
 
172
        """
 
173
        self.repo = repo
 
174
        self._graph = None
 
175
        self._use_known_graph = True
 
176
        self._supports_chks = getattr(repo._format, 'supports_chks', False)
 
177
 
 
178
    def expects_rich_root(self):
 
179
        """Does this store expect inventories with rich roots?"""
 
180
        return self.repo.supports_rich_root()
 
181
 
 
182
    def init_inventory(self, revision_id):
 
183
        """Generate an inventory for a parentless revision."""
 
184
        if self._supports_chks:
 
185
            inv = self._init_chk_inventory(revision_id, inventory.ROOT_ID)
 
186
        else:
 
187
            inv = inventory.Inventory(revision_id=revision_id)
 
188
            if self.expects_rich_root():
 
189
                # The very first root needs to have the right revision
 
190
                inv.root.revision = revision_id
 
191
        return inv
 
192
 
 
193
    def _init_chk_inventory(self, revision_id, root_id):
 
194
        """Generate a CHKInventory for a parentless revision."""
 
195
        from ... import chk_map
 
196
        # Get the creation parameters
 
197
        chk_store = self.repo.chk_bytes
 
198
        serializer = self.repo._format._serializer
 
199
        search_key_name = serializer.search_key_name
 
200
        maximum_size = serializer.maximum_size
 
201
 
 
202
        # Maybe the rest of this ought to be part of the CHKInventory API?
 
203
        inv = inventory.CHKInventory(search_key_name)
 
204
        inv.revision_id = revision_id
 
205
        inv.root_id = root_id
 
206
        search_key_func = chk_map.search_key_registry.get(search_key_name)
 
207
        inv.id_to_entry = chk_map.CHKMap(chk_store, None, search_key_func)
 
208
        inv.id_to_entry._root_node.set_maximum_size(maximum_size)
 
209
        inv.parent_id_basename_to_file_id = chk_map.CHKMap(chk_store,
 
210
            None, search_key_func)
 
211
        inv.parent_id_basename_to_file_id._root_node.set_maximum_size(
 
212
            maximum_size)
 
213
        inv.parent_id_basename_to_file_id._root_node._key_width = 2
 
214
        return inv
 
215
 
 
216
    def get_inventory(self, revision_id):
 
217
        """Get a stored inventory."""
 
218
        return self.repo.get_inventory(revision_id)
 
219
 
 
220
    def get_file_text(self, revision_id, file_id):
 
221
        """Get the text stored for a file in a given revision."""
 
222
        revtree = self.repo.revision_tree(revision_id)
 
223
        return revtree.get_file_text(file_id)
 
224
 
 
225
    def get_file_lines(self, revision_id, file_id):
 
226
        """Get the lines stored for a file in a given revision."""
 
227
        revtree = self.repo.revision_tree(revision_id)
 
228
        return osutils.split_lines(revtree.get_file_text(file_id))
 
229
 
 
230
    def start_new_revision(self, revision, parents, parent_invs):
 
231
        """Init the metadata needed for get_parents_and_revision_for_entry().
 
232
 
 
233
        :param revision: a Revision object
 
234
        """
 
235
        self._current_rev_id = revision.revision_id
 
236
        self._rev_parents = parents
 
237
        self._rev_parent_invs = parent_invs
 
238
        # We don't know what the branch will be so there's no real BranchConfig.
 
239
        # That means we won't be triggering any hooks and that's a good thing.
 
240
        # Without a config though, we must pass in the committer below so that
 
241
        # the commit builder doesn't try to look up the config.
 
242
        config = None
 
243
        # We can't use self.repo.get_commit_builder() here because it starts a
 
244
        # new write group. We want one write group around a batch of imports
 
245
        # where the default batch size is currently 10000. IGC 20090312
 
246
        self._commit_builder = self.repo._commit_builder_class(self.repo,
 
247
            parents, config, timestamp=revision.timestamp,
 
248
            timezone=revision.timezone, committer=revision.committer,
 
249
            revprops=revision.properties, revision_id=revision.revision_id)
 
250
 
 
251
    def get_parents_and_revision_for_entry(self, ie):
 
252
        """Get the parents and revision for an inventory entry.
 
253
 
 
254
        :param ie: the inventory entry
 
255
        :return parents, revision_id where
 
256
            parents is the tuple of parent revision_ids for the per-file graph
 
257
            revision_id is the revision_id to use for this entry
 
258
        """
 
259
        # Check for correct API usage
 
260
        if self._current_rev_id is None:
 
261
            raise AssertionError("start_new_revision() must be called"
 
262
                " before get_parents_and_revision_for_entry()")
 
263
        if ie.revision != self._current_rev_id:
 
264
            raise AssertionError("start_new_revision() registered a different"
 
265
                " revision (%s) to that in the inventory entry (%s)" %
 
266
                (self._current_rev_id, ie.revision))
 
267
 
 
268
        # Find the heads. This code is lifted from
 
269
        # repository.CommitBuilder.record_entry_contents().
 
270
        parent_candidate_entries = ie.parent_candidates(self._rev_parent_invs)
 
271
        head_set = self._commit_builder._heads(ie.file_id,
 
272
            parent_candidate_entries.keys())
 
273
        heads = []
 
274
        for inv in self._rev_parent_invs:
 
275
            if inv.has_id(ie.file_id):
 
276
                old_rev = inv[ie.file_id].revision
 
277
                if old_rev in head_set:
 
278
                    rev_id = inv[ie.file_id].revision
 
279
                    heads.append(rev_id)
 
280
                    head_set.remove(rev_id)
 
281
 
 
282
        # Find the revision to use. If the content has not changed
 
283
        # since the parent, record the parent's revision.
 
284
        if len(heads) == 0:
 
285
            return (), ie.revision
 
286
        parent_entry = parent_candidate_entries[heads[0]]
 
287
        changed = False
 
288
        if len(heads) > 1:
 
289
            changed = True
 
290
        elif (parent_entry.name != ie.name or parent_entry.kind != ie.kind or
 
291
            parent_entry.parent_id != ie.parent_id): 
 
292
            changed = True
 
293
        elif ie.kind == 'file':
 
294
            if (parent_entry.text_sha1 != ie.text_sha1 or
 
295
                parent_entry.executable != ie.executable):
 
296
                changed = True
 
297
        elif ie.kind == 'symlink':
 
298
            if parent_entry.symlink_target != ie.symlink_target:
 
299
                changed = True
 
300
        if changed:
 
301
            rev_id = ie.revision
 
302
        else:
 
303
            rev_id = parent_entry.revision
 
304
        return tuple(heads), rev_id
 
305
 
 
306
    def load(self, rev, inv, signature, text_provider, parents_provider,
 
307
        inventories_provider=None):
 
308
        """Load a revision.
 
309
 
 
310
        :param rev: the Revision
 
311
        :param inv: the inventory
 
312
        :param signature: signing information
 
313
        :param text_provider: a callable expecting a file_id parameter
 
314
            that returns the text for that file-id
 
315
        :param parents_provider: a callable expecting a file_id parameter
 
316
            that return the list of parent-ids for that file-id
 
317
        :param inventories_provider: a callable expecting a repository and
 
318
            a list of revision-ids, that returns:
 
319
              * the list of revision-ids present in the repository
 
320
              * the list of inventories for the revision-id's,
 
321
                including an empty inventory for the missing revisions
 
322
            If None, a default implementation is provided.
 
323
        """
 
324
        # NOTE: This is breezy.repository._install_revision refactored to
 
325
        # to provide more flexibility in how previous revisions are cached,
 
326
        # data is feed in, etc.
 
327
 
 
328
        # Get the non-ghost parents and their inventories
 
329
        if inventories_provider is None:
 
330
            inventories_provider = self._default_inventories_provider
 
331
        present_parents, parent_invs = inventories_provider(rev.parent_ids)
 
332
 
 
333
        # Load the inventory
 
334
        try:
 
335
            rev.inventory_sha1 = self._add_inventory(rev.revision_id,
 
336
                inv, present_parents, parent_invs)
 
337
        except errors.RevisionAlreadyPresent:
 
338
            pass
 
339
 
 
340
        # Load the texts, signature and revision
 
341
        entries = self._non_root_entries_iter(inv, rev.revision_id)
 
342
        self._load_texts(rev.revision_id, entries, text_provider,
 
343
            parents_provider)
 
344
        if signature is not None:
 
345
            self.repo.add_signature_text(rev.revision_id, signature)
 
346
        self._add_revision(rev, inv)
 
347
 
 
348
    def load_using_delta(self, rev, basis_inv, inv_delta, signature,
 
349
        text_provider, parents_provider, inventories_provider=None):
 
350
        """Load a revision by applying a delta to a (CHK)Inventory.
 
351
 
 
352
        :param rev: the Revision
 
353
        :param basis_inv: the basis Inventory or CHKInventory
 
354
        :param inv_delta: the inventory delta
 
355
        :param signature: signing information
 
356
        :param text_provider: a callable expecting a file_id parameter
 
357
            that returns the text for that file-id
 
358
        :param parents_provider: a callable expecting a file_id parameter
 
359
            that return the list of parent-ids for that file-id
 
360
        :param inventories_provider: a callable expecting a repository and
 
361
            a list of revision-ids, that returns:
 
362
              * the list of revision-ids present in the repository
 
363
              * the list of inventories for the revision-id's,
 
364
                including an empty inventory for the missing revisions
 
365
            If None, a default implementation is provided.
 
366
        """
 
367
        # TODO: set revision_id = rev.revision_id
 
368
        builder = self.repo._commit_builder_class(self.repo,
 
369
            parents=rev.parent_ids, config=None, timestamp=rev.timestamp,
 
370
            timezone=rev.timezone, committer=rev.committer,
 
371
            revprops=rev.properties, revision_id=rev.revision_id)
 
372
        if self._graph is None and self._use_known_graph:
 
373
            if (getattr(_mod_graph, 'GraphThunkIdsToKeys', None) and
 
374
                getattr(_mod_graph.GraphThunkIdsToKeys, "add_node", None) and
 
375
                getattr(self.repo, "get_known_graph_ancestry", None)):
 
376
                self._graph = self.repo.get_known_graph_ancestry(
 
377
                    rev.parent_ids)
 
378
            else:
 
379
                self._use_known_graph = False
 
380
        if self._graph is not None:
 
381
            orig_heads = builder._heads
 
382
            def thunked_heads(file_id, revision_ids):
 
383
                # self._graph thinks in terms of keys, not ids, so translate
 
384
                # them
 
385
                # old_res = orig_heads(file_id, revision_ids)
 
386
                if len(revision_ids) < 2:
 
387
                    res = set(revision_ids)
 
388
                else:
 
389
                    res = set(self._graph.heads(revision_ids))
 
390
                # if old_res != res:
 
391
                #     import pdb; pdb.set_trace()
 
392
                return res
 
393
            builder._heads = thunked_heads
 
394
 
 
395
        if rev.parent_ids:
 
396
            basis_rev_id = rev.parent_ids[0]
 
397
        else:
 
398
            basis_rev_id = _mod_revision.NULL_REVISION
 
399
        tree = _TreeShim(self.repo, basis_inv, inv_delta, text_provider)
 
400
        changes = tree._delta_to_iter_changes()
 
401
        for (file_id, path, fs_hash) in builder.record_iter_changes(
 
402
                tree, basis_rev_id, changes):
 
403
            # So far, we don't *do* anything with the result
 
404
            pass
 
405
        builder.finish_inventory()
 
406
        # TODO: This is working around a bug in the breezy code base.
 
407
        # 'builder.finish_inventory()' ends up doing:
 
408
        # self.inv_sha1 = self.repository.add_inventory_by_delta(...)
 
409
        # However, add_inventory_by_delta returns (sha1, inv)
 
410
        # And we *want* to keep a handle on both of those objects
 
411
        if isinstance(builder.inv_sha1, tuple):
 
412
            builder.inv_sha1, builder.new_inventory = builder.inv_sha1
 
413
        # This is a duplicate of Builder.commit() since we already have the
 
414
        # Revision object, and we *don't* want to call commit_write_group()
 
415
        rev.inv_sha1 = builder.inv_sha1
 
416
        try:
 
417
            config = builder._config_stack
 
418
        except AttributeError: # bzr < 2.5
 
419
            config = builder._config
 
420
        builder.repository.add_revision(builder._new_revision_id, rev,
 
421
            builder.new_inventory)
 
422
        if self._graph is not None:
 
423
            # TODO: Use StaticTuple and .intern() for these things
 
424
            self._graph.add_node(builder._new_revision_id, rev.parent_ids)
 
425
 
 
426
        if signature is not None:
 
427
            raise AssertionError('signatures not guaranteed yet')
 
428
            self.repo.add_signature_text(rev.revision_id, signature)
 
429
        # self._add_revision(rev, inv)
 
430
        return builder.revision_tree().inventory
 
431
 
 
432
    def _non_root_entries_iter(self, inv, revision_id):
 
433
        if hasattr(inv, 'iter_non_root_entries'):
 
434
            entries = inv.iter_non_root_entries()
 
435
        else:
 
436
            path_entries = inv.iter_entries()
 
437
            # Backwards compatibility hack: skip the root id.
 
438
            if not self.repo.supports_rich_root():
 
439
                path, root = path_entries.next()
 
440
                if root.revision != revision_id:
 
441
                    raise errors.IncompatibleRevision(repr(self.repo))
 
442
            entries = iter([ie for path, ie in path_entries])
 
443
        return entries
 
444
 
 
445
    def _load_texts(self, revision_id, entries, text_provider,
 
446
        parents_provider):
 
447
        """Load texts to a repository for inventory entries.
 
448
        
 
449
        This method is provided for subclasses to use or override.
 
450
 
 
451
        :param revision_id: the revision identifier
 
452
        :param entries: iterator over the inventory entries
 
453
        :param text_provider: a callable expecting a file_id parameter
 
454
            that returns the text for that file-id
 
455
        :param parents_provider: a callable expecting a file_id parameter
 
456
            that return the list of parent-ids for that file-id
 
457
        """
 
458
        raise NotImplementedError(self._load_texts)
 
459
 
 
460
    def _add_inventory(self, revision_id, inv, parents, parent_invs):
 
461
        """Add the inventory inv to the repository as revision_id.
 
462
        
 
463
        :param parents: The revision ids of the parents that revision_id
 
464
                        is known to have and are in the repository already.
 
465
        :param parent_invs: the parent inventories
 
466
 
 
467
        :returns: The validator(which is a sha1 digest, though what is sha'd is
 
468
            repository format specific) of the serialized inventory.
 
469
        """
 
470
        return self.repo.add_inventory(revision_id, inv, parents)
 
471
 
 
472
    def _add_inventory_by_delta(self, revision_id, basis_inv, inv_delta,
 
473
        parents, parent_invs):
 
474
        """Add the inventory to the repository as revision_id.
 
475
        
 
476
        :param basis_inv: the basis Inventory or CHKInventory
 
477
        :param inv_delta: the inventory delta
 
478
        :param parents: The revision ids of the parents that revision_id
 
479
                        is known to have and are in the repository already.
 
480
        :param parent_invs: the parent inventories
 
481
 
 
482
        :returns: (validator, inv) where validator is the validator
 
483
          (which is a sha1 digest, though what is sha'd is repository format
 
484
          specific) of the serialized inventory;
 
485
          inv is the generated inventory
 
486
        """
 
487
        if len(parents):
 
488
            if self._supports_chks:
 
489
                try:
 
490
                    validator, new_inv = self.repo.add_inventory_by_delta(parents[0],
 
491
                        inv_delta, revision_id, parents, basis_inv=basis_inv,
 
492
                        propagate_caches=False)
 
493
                except errors.InconsistentDelta:
 
494
                    #print "BASIS INV IS\n%s\n" % "\n".join([str(i) for i in basis_inv.iter_entries_by_dir()])
 
495
                    trace.mutter("INCONSISTENT DELTA IS:\n%s\n" % "\n".join([str(i) for i in inv_delta]))
 
496
                    raise
 
497
            else:
 
498
                validator, new_inv = self.repo.add_inventory_by_delta(parents[0],
 
499
                    inv_delta, revision_id, parents)
 
500
        else:
 
501
            if isinstance(basis_inv, inventory.CHKInventory):
 
502
                new_inv = basis_inv.create_by_apply_delta(inv_delta, revision_id)
 
503
            else:
 
504
                new_inv = inventory.Inventory(revision_id=revision_id)
 
505
                # This is set in the delta so remove it to prevent a duplicate
 
506
                del new_inv[inventory.ROOT_ID]
 
507
                new_inv.apply_delta(inv_delta)
 
508
            validator = self.repo.add_inventory(revision_id, new_inv, parents)
 
509
        return validator, new_inv
 
510
 
 
511
    def _add_revision(self, rev, inv):
 
512
        """Add a revision and its inventory to a repository.
 
513
 
 
514
        :param rev: the Revision
 
515
        :param inv: the inventory
 
516
        """
 
517
        self.repo.add_revision(rev.revision_id, rev, inv)
 
518
 
 
519
    def _default_inventories_provider(self, revision_ids):
 
520
        """An inventories provider that queries the repository."""
 
521
        present = []
 
522
        inventories = []
 
523
        for revision_id in revision_ids:
 
524
            if self.repo.has_revision(revision_id):
 
525
                present.append(revision_id)
 
526
                rev_tree = self.repo.revision_tree(revision_id)
 
527
            else:
 
528
                rev_tree = self.repo.revision_tree(None)
 
529
            inventories.append(rev_tree.inventory)
 
530
        return present, inventories
 
531
 
 
532
 
 
533
class RevisionStore1(AbstractRevisionStore):
 
534
    """A RevisionStore that uses the old breezy Repository API.
 
535
    
 
536
    The old API was present until bzr.dev rev 3510.
 
537
    """
 
538
 
 
539
    def _load_texts(self, revision_id, entries, text_provider, parents_provider):
 
540
        """See RevisionStore._load_texts()."""
 
541
        # Add the texts that are not already present
 
542
        tx = self.repo.get_transaction()
 
543
        for ie in entries:
 
544
            # This test is *really* slow: over 50% of import time
 
545
            #w = self.repo.weave_store.get_weave_or_empty(ie.file_id, tx)
 
546
            #if ie.revision in w:
 
547
            #    continue
 
548
            # Try another way, realising that this assumes that the
 
549
            # version is not already there. In the general case,
 
550
            # a shared repository might already have the revision but
 
551
            # we arguably don't need that check when importing from
 
552
            # a foreign system.
 
553
            if ie.revision != revision_id:
 
554
                continue
 
555
            file_id = ie.file_id
 
556
            text_parents = [(file_id, p) for p in parents_provider(file_id)]
 
557
            lines = text_provider(file_id)
 
558
            vfile = self.repo.weave_store.get_weave_or_empty(file_id,  tx)
 
559
            vfile.add_lines(revision_id, text_parents, lines)
 
560
 
 
561
    def get_file_lines(self, revision_id, file_id):
 
562
        tx = self.repo.get_transaction()
 
563
        w = self.repo.weave_store.get_weave(file_id, tx)
 
564
        return w.get_lines(revision_id)
 
565
 
 
566
    def _add_revision(self, rev, inv):
 
567
        # There's no need to do everything repo.add_revision does and
 
568
        # doing so (since bzr.dev 3392) can be pretty slow for long
 
569
        # delta chains on inventories. Just do the essentials here ...
 
570
        _mod_revision.check_not_reserved_id(rev.revision_id)
 
571
        self.repo._revision_store.add_revision(rev, self.repo.get_transaction())
 
572
 
 
573
 
 
574
class RevisionStore2(AbstractRevisionStore):
 
575
    """A RevisionStore that uses the new breezy Repository API."""
 
576
 
 
577
    def _load_texts(self, revision_id, entries, text_provider, parents_provider):
 
578
        """See RevisionStore._load_texts()."""
 
579
        text_keys = {}
 
580
        for ie in entries:
 
581
            text_keys[(ie.file_id, ie.revision)] = ie
 
582
        text_parent_map = self.repo.texts.get_parent_map(text_keys)
 
583
        missing_texts = set(text_keys) - set(text_parent_map)
 
584
        self._load_texts_for_file_rev_ids(missing_texts, text_provider,
 
585
            parents_provider)
 
586
 
 
587
    def _load_texts_for_file_rev_ids(self, file_rev_ids, text_provider,
 
588
        parents_provider):
 
589
        """Load texts to a repository for file-ids, revision-id tuples.
 
590
        
 
591
        :param file_rev_ids: iterator over the (file_id, revision_id) tuples
 
592
        :param text_provider: a callable expecting a file_id parameter
 
593
            that returns the text for that file-id
 
594
        :param parents_provider: a callable expecting a file_id parameter
 
595
            that return the list of parent-ids for that file-id
 
596
        """
 
597
        for file_id, revision_id in file_rev_ids:
 
598
            text_key = (file_id, revision_id)
 
599
            text_parents = [(file_id, p) for p in parents_provider(file_id)]
 
600
            lines = text_provider(file_id)
 
601
            #print "adding text for %s\n\tparents:%s" % (text_key,text_parents)
 
602
            self.repo.texts.add_lines(text_key, text_parents, lines)
 
603
 
 
604
    def get_file_lines(self, revision_id, file_id):
 
605
        record = self.repo.texts.get_record_stream([(file_id, revision_id)],
 
606
            'unordered', True).next()
 
607
        if record.storage_kind == 'absent':
 
608
            raise errors.RevisionNotPresent(record.key, self.repo)
 
609
        return osutils.split_lines(record.get_bytes_as('fulltext'))
 
610
 
 
611
    # This is breaking imports into brisbane-core currently
 
612
    #def _add_revision(self, rev, inv):
 
613
    #    # There's no need to do everything repo.add_revision does and
 
614
    #    # doing so (since bzr.dev 3392) can be pretty slow for long
 
615
    #    # delta chains on inventories. Just do the essentials here ...
 
616
    #    _mod_revision.check_not_reserved_id(rev.revision_id)
 
617
    #    self.repo._add_revision(rev)
 
618
 
 
619
 
 
620
class ImportRevisionStore1(RevisionStore1):
 
621
    """A RevisionStore (old Repository API) optimised for importing.
 
622
 
 
623
    This implementation caches serialised inventory texts and provides
 
624
    fine-grained control over when inventories are stored as fulltexts.
 
625
    """
 
626
 
 
627
    def __init__(self, repo, parent_texts_to_cache=1, fulltext_when=None,
 
628
        random_ids=True):
 
629
        """See AbstractRevisionStore.__init__.
 
630
 
 
631
        :param repository: the target repository
 
632
        :param parent_text_to_cache: the number of parent texts to cache
 
633
        :para fulltext_when: if non None, a function to call to decide
 
634
          whether to fulltext the inventory or not. The revision count
 
635
          is passed as a parameter and the result is treated as a boolean.
 
636
        """
 
637
        RevisionStore1.__init__(self, repo)
 
638
        self.inv_parent_texts = lru_cache.LRUCache(parent_texts_to_cache)
 
639
        self.fulltext_when = fulltext_when
 
640
        self.random_ids = random_ids
 
641
        self.revision_count = 0
 
642
 
 
643
    def _add_inventory(self, revision_id, inv, parents, parent_invs):
 
644
        """See RevisionStore._add_inventory."""
 
645
        # Code taken from breezy.repository.add_inventory
 
646
        assert self.repo.is_in_write_group()
 
647
        _mod_revision.check_not_reserved_id(revision_id)
 
648
        assert inv.revision_id is None or inv.revision_id == revision_id, \
 
649
            "Mismatch between inventory revision" \
 
650
            " id and insertion revid (%r, %r)" % (inv.revision_id, revision_id)
 
651
        assert inv.root is not None
 
652
        inv_lines = self.repo._serialise_inventory_to_lines(inv)
 
653
        inv_vf = self.repo.get_inventory_weave()
 
654
        sha1, num_bytes, parent_text = self._inventory_add_lines(inv_vf,
 
655
            revision_id, parents, inv_lines, self.inv_parent_texts)
 
656
        self.inv_parent_texts[revision_id] = parent_text
 
657
        return sha1
 
658
 
 
659
    def _inventory_add_lines(self, inv_vf, version_id, parents, lines,
 
660
            parent_texts):
 
661
        """See Repository._inventory_add_lines()."""
 
662
        # setup parameters used in original code but not this API
 
663
        self.revision_count += 1
 
664
        if self.fulltext_when is not None:
 
665
            delta = not self.fulltext_when(self.revision_count)
 
666
        else:
 
667
            delta = inv_vf.delta
 
668
        left_matching_blocks = None
 
669
        random_id = self.random_ids
 
670
        check_content = False
 
671
 
 
672
        # breezy.knit.add_lines() but error checking optimised
 
673
        inv_vf._check_add(version_id, lines, random_id, check_content)
 
674
 
 
675
        ####################################################################
 
676
        # breezy.knit._add() but skip checking if fulltext better than delta
 
677
        ####################################################################
 
678
 
 
679
        line_bytes = ''.join(lines)
 
680
        digest = osutils.sha_string(line_bytes)
 
681
        present_parents = []
 
682
        for parent in parents:
 
683
            if inv_vf.has_version(parent):
 
684
                present_parents.append(parent)
 
685
        if parent_texts is None:
 
686
            parent_texts = {}
 
687
 
 
688
        # can only compress against the left most present parent.
 
689
        if (delta and
 
690
            (len(present_parents) == 0 or
 
691
             present_parents[0] != parents[0])):
 
692
            delta = False
 
693
 
 
694
        text_length = len(line_bytes)
 
695
        options = []
 
696
        if lines:
 
697
            if lines[-1][-1] != '\n':
 
698
                # copy the contents of lines.
 
699
                lines = lines[:]
 
700
                options.append('no-eol')
 
701
                lines[-1] = lines[-1] + '\n'
 
702
                line_bytes += '\n'
 
703
 
 
704
        #if delta:
 
705
        #    # To speed the extract of texts the delta chain is limited
 
706
        #    # to a fixed number of deltas.  This should minimize both
 
707
        #    # I/O and the time spend applying deltas.
 
708
        #    delta = inv_vf._check_should_delta(present_parents)
 
709
 
 
710
        assert isinstance(version_id, str)
 
711
        content = inv_vf.factory.make(lines, version_id)
 
712
        if delta or (inv_vf.factory.annotated and len(present_parents) > 0):
 
713
            # Merge annotations from parent texts if needed.
 
714
            delta_hunks = inv_vf._merge_annotations(content, present_parents,
 
715
                parent_texts, delta, inv_vf.factory.annotated,
 
716
                left_matching_blocks)
 
717
 
 
718
        if delta:
 
719
            options.append('line-delta')
 
720
            store_lines = inv_vf.factory.lower_line_delta(delta_hunks)
 
721
            size, bytes = inv_vf._data._record_to_data(version_id, digest,
 
722
                store_lines)
 
723
        else:
 
724
            options.append('fulltext')
 
725
            # isinstance is slower and we have no hierarchy.
 
726
            if inv_vf.factory.__class__ == knit.KnitPlainFactory:
 
727
                # Use the already joined bytes saving iteration time in
 
728
                # _record_to_data.
 
729
                size, bytes = inv_vf._data._record_to_data(version_id, digest,
 
730
                    lines, [line_bytes])
 
731
            else:
 
732
                # get mixed annotation + content and feed it into the
 
733
                # serialiser.
 
734
                store_lines = inv_vf.factory.lower_fulltext(content)
 
735
                size, bytes = inv_vf._data._record_to_data(version_id, digest,
 
736
                    store_lines)
 
737
 
 
738
        access_memo = inv_vf._data.add_raw_records([size], bytes)[0]
 
739
        inv_vf._index.add_versions(
 
740
            ((version_id, options, access_memo, parents),),
 
741
            random_id=random_id)
 
742
        return digest, text_length, content