1# Copyright (C) 2007-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"""Deprecated weave-based repository formats.
18
19Weave based formats scaled linearly with history size and could not represent
20ghosts.
21"""
22
23import gzip
24from io import BytesIO
25import os
26
27from ...lazy_import import lazy_import
28lazy_import(globals(), """
29import itertools
30
31from breezy import (
32    graph as _mod_graph,
33    ui,
34    )
35from breezy.bzr import (
36    xml5,
37    )
38""")
39from ... import (
40    debug,
41    errors,
42    lockable_files,
43    lockdir,
44    osutils,
45    trace,
46    tuned_gzip,
47    urlutils,
48    )
49from ...bzr import (
50    versionedfile,
51    weave,
52    weavefile,
53    )
54from ...repository import (
55    InterRepository,
56    )
57from ...bzr.repository import (
58    RepositoryFormatMetaDir,
59    )
60from .store.text import TextStore
61from ...bzr.versionedfile import (
62    AbsentContentFactory,
63    FulltextContentFactory,
64    VersionedFiles,
65    )
66from ...bzr.vf_repository import (
67    InterSameDataRepository,
68    VersionedFileCommitBuilder,
69    VersionedFileRepository,
70    VersionedFileRepositoryFormat,
71    MetaDirVersionedFileRepository,
72    MetaDirVersionedFileRepositoryFormat,
73    )
74
75from . import bzrdir as weave_bzrdir
76
77
78class AllInOneRepository(VersionedFileRepository):
79    """Legacy support - the repository behaviour for all-in-one branches."""
80
81    @property
82    def _serializer(self):
83        return xml5.serializer_v5
84
85    def _escape(self, file_or_path):
86        if not isinstance(file_or_path, str):
87            file_or_path = '/'.join(file_or_path)
88        if file_or_path == '':
89            return u''
90        return urlutils.escape(osutils.safe_unicode(file_or_path))
91
92    def __init__(self, _format, a_controldir):
93        # we reuse one control files instance.
94        dir_mode = a_controldir._get_dir_mode()
95        file_mode = a_controldir._get_file_mode()
96
97        def get_store(name, compressed=True, prefixed=False):
98            # FIXME: This approach of assuming stores are all entirely compressed
99            # or entirely uncompressed is tidy, but breaks upgrade from
100            # some existing branches where there's a mixture; we probably
101            # still want the option to look for both.
102            relpath = self._escape(name)
103            store = TextStore(a_controldir.transport.clone(relpath),
104                              prefixed=prefixed, compressed=compressed,
105                              dir_mode=dir_mode,
106                              file_mode=file_mode)
107            return store
108
109        # not broken out yet because the controlweaves|inventory_store
110        # and texts bits are still different.
111        if isinstance(_format, RepositoryFormat4):
112            # cannot remove these - there is still no consistent api
113            # which allows access to this old info.
114            self.inventory_store = get_store('inventory-store')
115            self._text_store = get_store('text-store')
116        super(AllInOneRepository, self).__init__(
117            _format, a_controldir, a_controldir._control_files)
118
119    def _all_possible_ids(self):
120        """Return all the possible revisions that we could find."""
121        if 'evil' in debug.debug_flags:
122            trace.mutter_callsite(
123                3, "_all_possible_ids scales with size of history.")
124        with self.lock_read():
125            return [key[-1] for key in self.inventories.keys()]
126
127    def _all_revision_ids(self):
128        """Returns a list of all the revision ids in the repository.
129
130        These are in as much topological order as the underlying store can
131        present: for weaves ghosts may lead to a lack of correctness until
132        the reweave updates the parents list.
133        """
134        with self.lock_read():
135            return [key[-1] for key in self.revisions.keys()]
136
137    def _activate_new_inventory(self):
138        """Put a replacement inventory.new into use as inventories."""
139        # Copy the content across
140        t = self.controldir._control_files._transport
141        t.copy('inventory.new.weave', 'inventory.weave')
142        # delete the temp inventory
143        t.delete('inventory.new.weave')
144        # Check we can parse the new weave properly as a sanity check
145        self.inventories.keys()
146
147    def _backup_inventory(self):
148        t = self.controldir._control_files._transport
149        t.copy('inventory.weave', 'inventory.backup.weave')
150
151    def _temp_inventories(self):
152        t = self.controldir._control_files._transport
153        return self._format._get_inventories(t, self, 'inventory.new')
154
155    def get_commit_builder(self, branch, parents, config, timestamp=None,
156                           timezone=None, committer=None, revprops=None,
157                           revision_id=None, lossy=False):
158        self._check_ascii_revisionid(revision_id, self.get_commit_builder)
159        result = VersionedFileCommitBuilder(self, parents, config, timestamp,
160                                            timezone, committer, revprops, revision_id, lossy=lossy)
161        self.start_write_group()
162        return result
163
164    def _inventory_add_lines(self, revision_id, parents, lines,
165                             check_content=True):
166        """Store lines in inv_vf and return the sha1 of the inventory."""
167        present_parents = self.get_graph().get_parent_map(parents)
168        final_parents = []
169        for parent in parents:
170            if parent in present_parents:
171                final_parents.append((parent,))
172        return self.inventories.add_lines((revision_id,), final_parents, lines,
173                                          check_content=check_content)[0]
174
175    def is_shared(self):
176        """AllInOne repositories cannot be shared."""
177        return False
178
179    def set_make_working_trees(self, new_value):
180        """Set the policy flag for making working trees when creating branches.
181
182        This only applies to branches that use this repository.
183
184        The default is 'True'.
185        :param new_value: True to restore the default, False to disable making
186                          working trees.
187        """
188        raise errors.RepositoryUpgradeRequired(self.user_url)
189
190    def make_working_trees(self):
191        """Returns the policy for making working trees on new branches."""
192        return True
193
194
195class WeaveMetaDirRepository(MetaDirVersionedFileRepository):
196    """A subclass of MetaDirRepository to set weave specific policy."""
197
198    def __init__(self, _format, a_controldir, control_files):
199        super(WeaveMetaDirRepository, self).__init__(
200            _format, a_controldir, control_files)
201        self._serializer = _format._serializer
202
203    def _all_possible_ids(self):
204        """Return all the possible revisions that we could find."""
205        if 'evil' in debug.debug_flags:
206            trace.mutter_callsite(
207                3, "_all_possible_ids scales with size of history.")
208        with self.lock_read():
209            return [key[-1] for key in self.inventories.keys()]
210
211    def _all_revision_ids(self):
212        """Returns a list of all the revision ids in the repository.
213
214        These are in as much topological order as the underlying store can
215        present: for weaves ghosts may lead to a lack of correctness until
216        the reweave updates the parents list.
217        """
218        with self.lock_read():
219            return [key[-1] for key in self.revisions.keys()]
220
221    def _activate_new_inventory(self):
222        """Put a replacement inventory.new into use as inventories."""
223        # Copy the content across
224        t = self._transport
225        t.copy('inventory.new.weave', 'inventory.weave')
226        # delete the temp inventory
227        t.delete('inventory.new.weave')
228        # Check we can parse the new weave properly as a sanity check
229        self.inventories.keys()
230
231    def _backup_inventory(self):
232        t = self._transport
233        t.copy('inventory.weave', 'inventory.backup.weave')
234
235    def _temp_inventories(self):
236        t = self._transport
237        return self._format._get_inventories(t, self, 'inventory.new')
238
239    def get_commit_builder(self, branch, parents, config, timestamp=None,
240                           timezone=None, committer=None, revprops=None,
241                           revision_id=None, lossy=False):
242        self._check_ascii_revisionid(revision_id, self.get_commit_builder)
243        result = VersionedFileCommitBuilder(self, parents, config, timestamp,
244                                            timezone, committer, revprops, revision_id, lossy=lossy)
245        self.start_write_group()
246        return result
247
248    def get_revision(self, revision_id):
249        """Return the Revision object for a named revision"""
250        with self.lock_read():
251            return self.get_revision_reconcile(revision_id)
252
253    def _inventory_add_lines(self, revision_id, parents, lines,
254                             check_content=True):
255        """Store lines in inv_vf and return the sha1 of the inventory."""
256        present_parents = self.get_graph().get_parent_map(parents)
257        final_parents = []
258        for parent in parents:
259            if parent in present_parents:
260                final_parents.append((parent,))
261        return self.inventories.add_lines((revision_id,), final_parents, lines,
262                                          check_content=check_content)[0]
263
264
265class PreSplitOutRepositoryFormat(VersionedFileRepositoryFormat):
266    """Base class for the pre split out repository formats."""
267
268    rich_root_data = False
269    supports_tree_reference = False
270    supports_ghosts = False
271    supports_external_lookups = False
272    supports_chks = False
273    supports_nesting_repositories = True
274    _fetch_order = 'topological'
275    _fetch_reconcile = True
276    fast_deltas = False
277    supports_leaving_lock = False
278    supports_overriding_transport = False
279    # XXX: This is an old format that we don't support full checking on, so
280    # just claim that checking for this inconsistency is not required.
281    revision_graph_can_have_wrong_parents = False
282
283    def initialize(self, a_controldir, shared=False, _internal=False):
284        """Create a weave repository."""
285        if shared:
286            raise errors.IncompatibleFormat(self, a_controldir._format)
287
288        if not _internal:
289            # always initialized when the bzrdir is.
290            return self.open(a_controldir, _found=True)
291
292        # Create an empty weave
293        sio = BytesIO()
294        weavefile.write_weave_v5(weave.Weave(), sio)
295        empty_weave = sio.getvalue()
296
297        trace.mutter('creating repository in %s.', a_controldir.transport.base)
298
299        # FIXME: RBC 20060125 don't peek under the covers
300        # NB: no need to escape relative paths that are url safe.
301        control_files = lockable_files.LockableFiles(a_controldir.transport,
302                                                     'branch-lock', lockable_files.TransportLock)
303        control_files.create_lock()
304        control_files.lock_write()
305        transport = a_controldir.transport
306        try:
307            transport.mkdir('revision-store',
308                            mode=a_controldir._get_dir_mode())
309            transport.mkdir('weaves', mode=a_controldir._get_dir_mode())
310            transport.put_bytes_non_atomic('inventory.weave', empty_weave,
311                                           mode=a_controldir._get_file_mode())
312        finally:
313            control_files.unlock()
314        repository = self.open(a_controldir, _found=True)
315        self._run_post_repo_init_hooks(repository, a_controldir, shared)
316        return repository
317
318    def open(self, a_controldir, _found=False):
319        """See RepositoryFormat.open()."""
320        if not _found:
321            # we are being called directly and must probe.
322            raise NotImplementedError
323
324        repo_transport = a_controldir.get_repository_transport(None)
325        result = AllInOneRepository(_format=self, a_controldir=a_controldir)
326        result.revisions = self._get_revisions(repo_transport, result)
327        result.signatures = self._get_signatures(repo_transport, result)
328        result.inventories = self._get_inventories(repo_transport, result)
329        result.texts = self._get_texts(repo_transport, result)
330        result.chk_bytes = None
331        return result
332
333    def is_deprecated(self):
334        return True
335
336
337class RepositoryFormat4(PreSplitOutRepositoryFormat):
338    """Bzr repository format 4.
339
340    This repository format has:
341     - flat stores
342     - TextStores for texts, inventories,revisions.
343
344    This format is deprecated: it indexes texts using a text id which is
345    removed in format 5; initialization and write support for this format
346    has been removed.
347    """
348
349    supports_funky_characters = False
350
351    _matchingcontroldir = weave_bzrdir.BzrDirFormat4()
352
353    def get_format_description(self):
354        """See RepositoryFormat.get_format_description()."""
355        return "Repository format 4"
356
357    def initialize(self, url, shared=False, _internal=False):
358        """Format 4 branches cannot be created."""
359        raise errors.UninitializableFormat(self)
360
361    def is_supported(self):
362        """Format 4 is not supported.
363
364        It is not supported because the model changed from 4 to 5 and the
365        conversion logic is expensive - so doing it on the fly was not
366        feasible.
367        """
368        return False
369
370    def _get_inventories(self, repo_transport, repo, name='inventory'):
371        # No inventories store written so far.
372        return None
373
374    def _get_revisions(self, repo_transport, repo):
375        from .xml4 import serializer_v4
376        return RevisionTextStore(repo_transport.clone('revision-store'),
377                                 serializer_v4, True, versionedfile.PrefixMapper(),
378                                 repo.is_locked, repo.is_write_locked)
379
380    def _get_signatures(self, repo_transport, repo):
381        return SignatureTextStore(repo_transport.clone('revision-store'),
382                                  False, versionedfile.PrefixMapper(),
383                                  repo.is_locked, repo.is_write_locked)
384
385    def _get_texts(self, repo_transport, repo):
386        return None
387
388
389class RepositoryFormat5(PreSplitOutRepositoryFormat):
390    """Bzr control format 5.
391
392    This repository format has:
393     - weaves for file texts and inventory
394     - flat stores
395     - TextStores for revisions and signatures.
396    """
397
398    _versionedfile_class = weave.WeaveFile
399    _matchingcontroldir = weave_bzrdir.BzrDirFormat5()
400    supports_funky_characters = False
401
402    @property
403    def _serializer(self):
404        return xml5.serializer_v5
405
406    def get_format_description(self):
407        """See RepositoryFormat.get_format_description()."""
408        return "Weave repository format 5"
409
410    def network_name(self):
411        """The network name for this format is the control dirs disk label."""
412        return self._matchingcontroldir.get_format_string()
413
414    def _get_inventories(self, repo_transport, repo, name='inventory'):
415        mapper = versionedfile.ConstantMapper(name)
416        return versionedfile.ThunkedVersionedFiles(repo_transport,
417                                                   weave.WeaveFile, mapper, repo.is_locked)
418
419    def _get_revisions(self, repo_transport, repo):
420        return RevisionTextStore(repo_transport.clone('revision-store'),
421                                 xml5.serializer_v5, False, versionedfile.PrefixMapper(),
422                                 repo.is_locked, repo.is_write_locked)
423
424    def _get_signatures(self, repo_transport, repo):
425        return SignatureTextStore(repo_transport.clone('revision-store'),
426                                  False, versionedfile.PrefixMapper(),
427                                  repo.is_locked, repo.is_write_locked)
428
429    def _get_texts(self, repo_transport, repo):
430        mapper = versionedfile.PrefixMapper()
431        base_transport = repo_transport.clone('weaves')
432        return versionedfile.ThunkedVersionedFiles(base_transport,
433                                                   weave.WeaveFile, mapper, repo.is_locked)
434
435
436class RepositoryFormat6(PreSplitOutRepositoryFormat):
437    """Bzr control format 6.
438
439    This repository format has:
440     - weaves for file texts and inventory
441     - hash subdirectory based stores.
442     - TextStores for revisions and signatures.
443    """
444
445    _versionedfile_class = weave.WeaveFile
446    _matchingcontroldir = weave_bzrdir.BzrDirFormat6()
447    supports_funky_characters = False
448
449    @property
450    def _serializer(self):
451        return xml5.serializer_v5
452
453    def get_format_description(self):
454        """See RepositoryFormat.get_format_description()."""
455        return "Weave repository format 6"
456
457    def network_name(self):
458        """The network name for this format is the control dirs disk label."""
459        return self._matchingcontroldir.get_format_string()
460
461    def _get_inventories(self, repo_transport, repo, name='inventory'):
462        mapper = versionedfile.ConstantMapper(name)
463        return versionedfile.ThunkedVersionedFiles(repo_transport,
464                                                   weave.WeaveFile, mapper, repo.is_locked)
465
466    def _get_revisions(self, repo_transport, repo):
467        return RevisionTextStore(repo_transport.clone('revision-store'),
468                                 xml5.serializer_v5, False, versionedfile.HashPrefixMapper(),
469                                 repo.is_locked, repo.is_write_locked)
470
471    def _get_signatures(self, repo_transport, repo):
472        return SignatureTextStore(repo_transport.clone('revision-store'),
473                                  False, versionedfile.HashPrefixMapper(),
474                                  repo.is_locked, repo.is_write_locked)
475
476    def _get_texts(self, repo_transport, repo):
477        mapper = versionedfile.HashPrefixMapper()
478        base_transport = repo_transport.clone('weaves')
479        return versionedfile.ThunkedVersionedFiles(base_transport,
480                                                   weave.WeaveFile, mapper, repo.is_locked)
481
482
483class RepositoryFormat7(MetaDirVersionedFileRepositoryFormat):
484    """Bzr repository 7.
485
486    This repository format has:
487     - weaves for file texts and inventory
488     - hash subdirectory based stores.
489     - TextStores for revisions and signatures.
490     - a format marker of its own
491     - an optional 'shared-storage' flag
492     - an optional 'no-working-trees' flag
493    """
494
495    _versionedfile_class = weave.WeaveFile
496    supports_ghosts = False
497    supports_chks = False
498    supports_funky_characters = False
499    revision_graph_can_have_wrong_parents = False
500
501    _fetch_order = 'topological'
502    _fetch_reconcile = True
503    fast_deltas = False
504
505    @property
506    def _serializer(self):
507        return xml5.serializer_v5
508
509    @classmethod
510    def get_format_string(cls):
511        """See RepositoryFormat.get_format_string()."""
512        return b"Bazaar-NG Repository format 7"
513
514    def get_format_description(self):
515        """See RepositoryFormat.get_format_description()."""
516        return "Weave repository format 7"
517
518    def _get_inventories(self, repo_transport, repo, name='inventory'):
519        mapper = versionedfile.ConstantMapper(name)
520        return versionedfile.ThunkedVersionedFiles(repo_transport,
521                                                   weave.WeaveFile, mapper, repo.is_locked)
522
523    def _get_revisions(self, repo_transport, repo):
524        return RevisionTextStore(repo_transport.clone('revision-store'),
525                                 xml5.serializer_v5, True, versionedfile.HashPrefixMapper(),
526                                 repo.is_locked, repo.is_write_locked)
527
528    def _get_signatures(self, repo_transport, repo):
529        return SignatureTextStore(repo_transport.clone('revision-store'),
530                                  True, versionedfile.HashPrefixMapper(),
531                                  repo.is_locked, repo.is_write_locked)
532
533    def _get_texts(self, repo_transport, repo):
534        mapper = versionedfile.HashPrefixMapper()
535        base_transport = repo_transport.clone('weaves')
536        return versionedfile.ThunkedVersionedFiles(base_transport,
537                                                   weave.WeaveFile, mapper, repo.is_locked)
538
539    def initialize(self, a_controldir, shared=False):
540        """Create a weave repository.
541
542        :param shared: If true the repository will be initialized as a shared
543                       repository.
544        """
545        # Create an empty weave
546        sio = BytesIO()
547        weavefile.write_weave_v5(weave.Weave(), sio)
548        empty_weave = sio.getvalue()
549
550        trace.mutter('creating repository in %s.', a_controldir.transport.base)
551        dirs = ['revision-store', 'weaves']
552        files = [('inventory.weave', BytesIO(empty_weave)),
553                 ]
554        utf8_files = [('format', self.get_format_string())]
555
556        self._upload_blank_content(
557            a_controldir, dirs, files, utf8_files, shared)
558        return self.open(a_controldir=a_controldir, _found=True)
559
560    def open(self, a_controldir, _found=False, _override_transport=None):
561        """See RepositoryFormat.open().
562
563        :param _override_transport: INTERNAL USE ONLY. Allows opening the
564                                    repository at a slightly different url
565                                    than normal. I.e. during 'upgrade'.
566        """
567        if not _found:
568            format = RepositoryFormatMetaDir.find_format(a_controldir)
569        if _override_transport is not None:
570            repo_transport = _override_transport
571        else:
572            repo_transport = a_controldir.get_repository_transport(None)
573        control_files = lockable_files.LockableFiles(repo_transport,
574                                                     'lock', lockdir.LockDir)
575        result = WeaveMetaDirRepository(_format=self, a_controldir=a_controldir,
576                                        control_files=control_files)
577        result.revisions = self._get_revisions(repo_transport, result)
578        result.signatures = self._get_signatures(repo_transport, result)
579        result.inventories = self._get_inventories(repo_transport, result)
580        result.texts = self._get_texts(repo_transport, result)
581        result.chk_bytes = None
582        result._transport = repo_transport
583        return result
584
585    def is_deprecated(self):
586        return True
587
588
589class TextVersionedFiles(VersionedFiles):
590    """Just-a-bunch-of-files based VersionedFile stores."""
591
592    def __init__(self, transport, compressed, mapper, is_locked, can_write):
593        self._compressed = compressed
594        self._transport = transport
595        self._mapper = mapper
596        if self._compressed:
597            self._ext = '.gz'
598        else:
599            self._ext = ''
600        self._is_locked = is_locked
601        self._can_write = can_write
602
603    def add_lines(self, key, parents, lines):
604        """Add a revision to the store."""
605        if not self._is_locked():
606            raise errors.ObjectNotLocked(self)
607        if not self._can_write():
608            raise errors.ReadOnlyError(self)
609        if b'/' in key[-1]:
610            raise ValueError('bad idea to put / in %r' % (key,))
611        chunks = lines
612        if self._compressed:
613            chunks = tuned_gzip.chunks_to_gzip(chunks)
614        path = self._map(key)
615        self._transport.put_file_non_atomic(
616            path, BytesIO(b''.join(chunks)),
617            create_parent_dir=True)
618
619    def insert_record_stream(self, stream):
620        adapters = {}
621        for record in stream:
622            # Raise an error when a record is missing.
623            if record.storage_kind == 'absent':
624                raise errors.RevisionNotPresent([record.key[0]], self)
625            # adapt to non-tuple interface
626            if record.storage_kind in ('fulltext', 'chunks', 'lines'):
627                self.add_lines(record.key, None,
628                               record.get_bytes_as('lines'))
629            else:
630                adapter_key = record.storage_kind, 'lines'
631                try:
632                    adapter = adapters[adapter_key]
633                except KeyError:
634                    adapter_factory = adapter_registry.get(adapter_key)
635                    adapter = adapter_factory(self)
636                    adapters[adapter_key] = adapter
637                lines = adapter.get_bytes(
638                    record, record.get_bytes_as(record.storage_kind))
639                try:
640                    self.add_lines(record.key, None, lines)
641                except errors.RevisionAlreadyPresent:
642                    pass
643
644    def _load_text(self, key):
645        if not self._is_locked():
646            raise errors.ObjectNotLocked(self)
647        path = self._map(key)
648        try:
649            text = self._transport.get_bytes(path)
650            compressed = self._compressed
651        except errors.NoSuchFile:
652            if self._compressed:
653                # try without the .gz
654                path = path[:-3]
655                try:
656                    text = self._transport.get_bytes(path)
657                    compressed = False
658                except errors.NoSuchFile:
659                    return None
660            else:
661                return None
662        if compressed:
663            text = gzip.GzipFile(mode='rb', fileobj=BytesIO(text)).read()
664        return text
665
666    def _map(self, key):
667        return self._mapper.map(key) + self._ext
668
669
670class RevisionTextStore(TextVersionedFiles):
671    """Legacy thunk for format 4 repositories."""
672
673    def __init__(self, transport, serializer, compressed, mapper, is_locked,
674                 can_write):
675        """Create a RevisionTextStore at transport with serializer."""
676        TextVersionedFiles.__init__(self, transport, compressed, mapper,
677                                    is_locked, can_write)
678        self._serializer = serializer
679
680    def _load_text_parents(self, key):
681        text = self._load_text(key)
682        if text is None:
683            return None, None
684        parents = self._serializer.read_revision_from_string(text).parent_ids
685        return text, tuple((parent,) for parent in parents)
686
687    def get_parent_map(self, keys):
688        result = {}
689        for key in keys:
690            parents = self._load_text_parents(key)[1]
691            if parents is None:
692                continue
693            result[key] = parents
694        return result
695
696    def get_known_graph_ancestry(self, keys):
697        """Get a KnownGraph instance with the ancestry of keys."""
698        keys = self.keys()
699        parent_map = self.get_parent_map(keys)
700        kg = _mod_graph.KnownGraph(parent_map)
701        return kg
702
703    def get_record_stream(self, keys, sort_order, include_delta_closure):
704        for key in keys:
705            text, parents = self._load_text_parents(key)
706            if text is None:
707                yield AbsentContentFactory(key)
708            else:
709                yield FulltextContentFactory(key, parents, None, text)
710
711    def keys(self):
712        if not self._is_locked():
713            raise errors.ObjectNotLocked(self)
714        relpaths = set()
715        for quoted_relpath in self._transport.iter_files_recursive():
716            relpath = urlutils.unquote(quoted_relpath)
717            path, ext = os.path.splitext(relpath)
718            if ext == '.gz':
719                relpath = path
720            if not relpath.endswith('.sig'):
721                relpaths.add(relpath)
722        paths = list(relpaths)
723        return {self._mapper.unmap(path) for path in paths}
724
725
726class SignatureTextStore(TextVersionedFiles):
727    """Legacy thunk for format 4-7 repositories."""
728
729    def __init__(self, transport, compressed, mapper, is_locked, can_write):
730        TextVersionedFiles.__init__(self, transport, compressed, mapper,
731                                    is_locked, can_write)
732        self._ext = '.sig' + self._ext
733
734    def get_parent_map(self, keys):
735        result = {}
736        for key in keys:
737            text = self._load_text(key)
738            if text is None:
739                continue
740            result[key] = None
741        return result
742
743    def get_record_stream(self, keys, sort_order, include_delta_closure):
744        for key in keys:
745            text = self._load_text(key)
746            if text is None:
747                yield AbsentContentFactory(key)
748            else:
749                yield FulltextContentFactory(key, None, None, text)
750
751    def keys(self):
752        if not self._is_locked():
753            raise errors.ObjectNotLocked(self)
754        relpaths = set()
755        for quoted_relpath in self._transport.iter_files_recursive():
756            relpath = urlutils.unquote(quoted_relpath)
757            path, ext = os.path.splitext(relpath)
758            if ext == '.gz':
759                relpath = path
760            if not relpath.endswith('.sig'):
761                continue
762            relpaths.add(relpath[:-4])
763        paths = list(relpaths)
764        return {self._mapper.unmap(path) for path in paths}
765
766
767class InterWeaveRepo(InterSameDataRepository):
768    """Optimised code paths between Weave based repositories.
769    """
770
771    @classmethod
772    def _get_repo_format_to_test(self):
773        return RepositoryFormat7()
774
775    @staticmethod
776    def is_compatible(source, target):
777        """Be compatible with known Weave formats.
778
779        We don't test for the stores being of specific types because that
780        could lead to confusing results, and there is no need to be
781        overly general.
782        """
783        try:
784            return (isinstance(source._format, (RepositoryFormat5,
785                                                RepositoryFormat6,
786                                                RepositoryFormat7))
787                    and isinstance(target._format, (RepositoryFormat5,
788                                                    RepositoryFormat6,
789                                                    RepositoryFormat7)))
790        except AttributeError:
791            return False
792
793    def copy_content(self, revision_id=None):
794        """See InterRepository.copy_content()."""
795        with self.lock_write():
796            # weave specific optimised path:
797            try:
798                self.target.set_make_working_trees(
799                    self.source.make_working_trees())
800            except (errors.RepositoryUpgradeRequired, NotImplementedError):
801                pass
802            # FIXME do not peek!
803            if self.source._transport.listable():
804                with ui.ui_factory.nested_progress_bar() as pb:
805                    self.target.texts.insert_record_stream(
806                        self.source.texts.get_record_stream(
807                            self.source.texts.keys(), 'topological', False))
808                    pb.update('Copying inventory', 0, 1)
809                    self.target.inventories.insert_record_stream(
810                        self.source.inventories.get_record_stream(
811                            self.source.inventories.keys(), 'topological', False))
812                    self.target.signatures.insert_record_stream(
813                        self.source.signatures.get_record_stream(
814                            self.source.signatures.keys(),
815                            'unordered', True))
816                    self.target.revisions.insert_record_stream(
817                        self.source.revisions.get_record_stream(
818                            self.source.revisions.keys(),
819                            'topological', True))
820            else:
821                self.target.fetch(self.source, revision_id=revision_id)
822
823    def search_missing_revision_ids(self, find_ghosts=True, revision_ids=None,
824                                    if_present_ids=None, limit=None):
825        """See InterRepository.search_missing_revision_ids()."""
826        with self.lock_read():
827            # we want all revisions to satisfy revision_id in source.
828            # but we don't want to stat every file here and there.
829            # we want then, all revisions other needs to satisfy revision_id
830            # checked, but not those that we have locally.
831            # so the first thing is to get a subset of the revisions to
832            # satisfy revision_id in source, and then eliminate those that
833            # we do already have.
834            # this is slow on high latency connection to self, but as this
835            # disk format scales terribly for push anyway due to rewriting
836            # inventory.weave, this is considered acceptable.
837            # - RBC 20060209
838            source_ids_set = self._present_source_revisions_for(
839                revision_ids, if_present_ids)
840            # source_ids is the worst possible case we may need to pull.
841            # now we want to filter source_ids against what we actually
842            # have in target, but don't try to check for existence where we
843            # know we do not have a revision as that would be pointless.
844            target_ids = set(self.target._all_possible_ids())
845            possibly_present_revisions = target_ids.intersection(
846                source_ids_set)
847            actually_present_revisions = set(
848                self.target._eliminate_revisions_not_present(
849                    possibly_present_revisions))
850            required_revisions = source_ids_set.difference(
851                actually_present_revisions)
852            if revision_ids is not None:
853                # we used get_ancestry to determine source_ids then we are
854                # assured all revisions referenced are present as they are
855                # installed in topological order. and the tip revision was
856                # validated by get_ancestry.
857                result_set = required_revisions
858            else:
859                # if we just grabbed the possibly available ids, then
860                # we only have an estimate of whats available and need to
861                # validate that against the revision records.
862                result_set = set(
863                    self.source._eliminate_revisions_not_present(
864                        required_revisions))
865            if limit is not None:
866                topo_ordered = self.source.get_graph().iter_topo_order(result_set)
867                result_set = set(itertools.islice(topo_ordered, limit))
868            return self.source.revision_ids_to_search_result(result_set)
869
870
871InterRepository.register_optimiser(InterWeaveRepo)
872
873
874def get_extra_interrepo_test_combinations():
875    from ...bzr import knitrepo
876    return [(InterRepository, RepositoryFormat5(),
877             knitrepo.RepositoryFormatKnit3())]
878