~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/repository.py

  • Committer: Martin Pool
  • Date: 2006-02-22 04:29:54 UTC
  • mfrom: (1566 +trunk)
  • mto: This revision was merged to the branch mainline in revision 1569.
  • Revision ID: mbp@sourcefrog.net-20060222042954-60333f08dd56a646
[merge] from bzr.dev before integration
Fix undefined ordering in sign_my_revisions breaking tests

Show diffs side-by-side

added added

removed removed

Lines of Context:
14
14
# along with this program; if not, write to the Free Software
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
16
 
 
17
from copy import deepcopy
17
18
from cStringIO import StringIO
18
 
 
 
19
from unittest import TestSuite
 
20
import xml.sax.saxutils
 
21
 
 
22
 
 
23
import bzrlib.bzrdir as bzrdir
19
24
from bzrlib.decorators import needs_read_lock, needs_write_lock
 
25
import bzrlib.errors as errors
20
26
from bzrlib.errors import InvalidRevisionId
21
27
from bzrlib.lockable_files import LockableFiles
22
28
from bzrlib.osutils import safe_unicode
24
30
from bzrlib.store import copy_all
25
31
from bzrlib.store.weave import WeaveStore
26
32
from bzrlib.store.text import TextStore
 
33
from bzrlib.symbol_versioning import *
 
34
from bzrlib.trace import mutter
27
35
from bzrlib.tree import RevisionTree
28
36
from bzrlib.testament import Testament
29
37
from bzrlib.tree import EmptyTree
 
38
import bzrlib.ui
30
39
import bzrlib.xml5
31
40
 
32
41
 
33
 
 
34
42
class Repository(object):
35
43
    """Repository holding history for one or more branches.
36
44
 
43
51
    remote) disk.
44
52
    """
45
53
 
46
 
    def __init__(self, transport, branch_format):
47
 
        # circular dependencies:
48
 
        from bzrlib.branch import (BzrBranchFormat4,
49
 
                                   BzrBranchFormat5,
50
 
                                   BzrBranchFormat6,
51
 
                                   )
 
54
    @needs_read_lock
 
55
    def _all_possible_ids(self):
 
56
        """Return all the possible revisions that we could find."""
 
57
        return self.get_inventory_weave().names()
 
58
 
 
59
    @needs_read_lock
 
60
    def all_revision_ids(self):
 
61
        """Returns a list of all the revision ids in the repository. 
 
62
 
 
63
        These are in as much topological order as the underlying store can 
 
64
        present: for weaves ghosts may lead to a lack of correctness until
 
65
        the reweave updates the parents list.
 
66
        """
 
67
        result = self._all_possible_ids()
 
68
        return self._eliminate_revisions_not_present(result)
 
69
 
 
70
    @needs_read_lock
 
71
    def _eliminate_revisions_not_present(self, revision_ids):
 
72
        """Check every revision id in revision_ids to see if we have it.
 
73
 
 
74
        Returns a set of the present revisions.
 
75
        """
 
76
        result = []
 
77
        for id in revision_ids:
 
78
            if self.has_revision(id):
 
79
               result.append(id)
 
80
        return result
 
81
 
 
82
    @staticmethod
 
83
    def create(a_bzrdir):
 
84
        """Construct the current default format repository in a_bzrdir."""
 
85
        return RepositoryFormat.get_default_format().initialize(a_bzrdir)
 
86
 
 
87
    def __init__(self, _format, a_bzrdir):
52
88
        object.__init__(self)
53
 
        self.control_files = LockableFiles(transport.clone(bzrlib.BZRDIR), 'README')
 
89
        if isinstance(_format, (RepositoryFormat4,
 
90
                                RepositoryFormat5,
 
91
                                RepositoryFormat6)):
 
92
            # legacy: use a common control files.
 
93
            self.control_files = a_bzrdir._control_files
 
94
        else:
 
95
            self.control_files = LockableFiles(a_bzrdir.get_repository_transport(None),
 
96
                                               'lock')
54
97
 
55
98
        dir_mode = self.control_files._dir_mode
56
99
        file_mode = self.control_files._file_mode
 
100
        self._format = _format
 
101
        self.bzrdir = a_bzrdir
57
102
 
58
103
        def get_weave(name, prefixed=False):
59
104
            if name:
60
 
                name = bzrlib.BZRDIR + '/' + safe_unicode(name)
 
105
                name = safe_unicode(name)
61
106
            else:
62
 
                name = bzrlib.BZRDIR
 
107
                name = ''
63
108
            relpath = self.control_files._escape(name)
64
 
            weave_transport = transport.clone(relpath)
 
109
            weave_transport = self.control_files._transport.clone(relpath)
65
110
            ws = WeaveStore(weave_transport, prefixed=prefixed,
66
111
                            dir_mode=dir_mode,
67
112
                            file_mode=file_mode)
69
114
                ws.enable_cache = True
70
115
            return ws
71
116
 
72
 
 
73
117
        def get_store(name, compressed=True, prefixed=False):
74
118
            # FIXME: This approach of assuming stores are all entirely compressed
75
119
            # or entirely uncompressed is tidy, but breaks upgrade from 
76
120
            # some existing branches where there's a mixture; we probably 
77
121
            # still want the option to look for both.
78
122
            if name:
79
 
                name = bzrlib.BZRDIR + '/' + safe_unicode(name)
 
123
                name = safe_unicode(name)
80
124
            else:
81
 
                name = bzrlib.BZRDIR
 
125
                name = ''
82
126
            relpath = self.control_files._escape(name)
83
 
            store = TextStore(transport.clone(relpath),
 
127
            store = TextStore(self.control_files._transport.clone(relpath),
84
128
                              prefixed=prefixed, compressed=compressed,
85
129
                              dir_mode=dir_mode,
86
130
                              file_mode=file_mode)
90
134
            #    store = bzrlib.store.CachedStore(store, cache_path)
91
135
            return store
92
136
 
93
 
 
94
 
        if isinstance(branch_format, BzrBranchFormat4):
 
137
        if isinstance(self._format, RepositoryFormat4):
95
138
            self.inventory_store = get_store('inventory-store')
96
139
            self.text_store = get_store('text-store')
97
140
            self.revision_store = get_store('revision-store')
98
 
        elif isinstance(branch_format, BzrBranchFormat5):
 
141
        elif isinstance(self._format, RepositoryFormat5):
99
142
            self.control_weaves = get_weave('')
100
143
            self.weave_store = get_weave('weaves')
101
144
            self.revision_store = get_store('revision-store', compressed=False)
102
 
        elif isinstance(branch_format, BzrBranchFormat6):
 
145
        elif isinstance(self._format, RepositoryFormat6):
 
146
            self.control_weaves = get_weave('')
 
147
            self.weave_store = get_weave('weaves', prefixed=True)
 
148
            self.revision_store = get_store('revision-store', compressed=False,
 
149
                                            prefixed=True)
 
150
        elif isinstance(self._format, RepositoryFormat7):
103
151
            self.control_weaves = get_weave('')
104
152
            self.weave_store = get_weave('weaves', prefixed=True)
105
153
            self.revision_store = get_store('revision-store', compressed=False,
112
160
    def lock_read(self):
113
161
        self.control_files.lock_read()
114
162
 
 
163
    @needs_read_lock
 
164
    def missing_revision_ids(self, other, revision_id=None):
 
165
        """Return the revision ids that other has that this does not.
 
166
        
 
167
        These are returned in topological order.
 
168
 
 
169
        revision_id: only return revision ids included by revision_id.
 
170
        """
 
171
        return InterRepository.get(other, self).missing_revision_ids(revision_id)
 
172
 
 
173
    @staticmethod
 
174
    def open(base):
 
175
        """Open the repository rooted at base.
 
176
 
 
177
        For instance, if the repository is at URL/.bzr/repository,
 
178
        Repository.open(URL) -> a Repository instance.
 
179
        """
 
180
        control = bzrdir.BzrDir.open(base)
 
181
        return control.open_repository()
 
182
 
 
183
    def copy_content_into(self, destination, revision_id=None, basis=None):
 
184
        """Make a complete copy of the content in self into destination.
 
185
        
 
186
        This is a destructive operation! Do not use it on existing 
 
187
        repositories.
 
188
        """
 
189
        return InterRepository.get(self, destination).copy_content(revision_id, basis)
 
190
 
 
191
    def fetch(self, source, revision_id=None, pb=None):
 
192
        """Fetch the content required to construct revision_id from source.
 
193
 
 
194
        If revision_id is None all content is copied.
 
195
        """
 
196
        return InterRepository.get(source, self).fetch(revision_id=revision_id,
 
197
                                                       pb=pb)
 
198
 
115
199
    def unlock(self):
116
200
        self.control_files.unlock()
117
201
 
118
202
    @needs_read_lock
119
 
    def copy(self, destination):
120
 
        destination.lock_write()
121
 
        try:
122
 
            destination.control_weaves.copy_multi(self.control_weaves, 
123
 
                ['inventory'])
124
 
            copy_all(self.weave_store, destination.weave_store)
125
 
            copy_all(self.revision_store, destination.revision_store)
126
 
        finally:
127
 
            destination.unlock()
 
203
    def clone(self, a_bzrdir, revision_id=None, basis=None):
 
204
        """Clone this repository into a_bzrdir using the current format.
 
205
 
 
206
        Currently no check is made that the format of this repository and
 
207
        the bzrdir format are compatible. FIXME RBC 20060201.
 
208
        """
 
209
        if not isinstance(a_bzrdir._format, self.bzrdir._format.__class__):
 
210
            # use target default format.
 
211
            result = a_bzrdir.create_repository()
 
212
        # FIXME RBC 20060209 split out the repository type to avoid this check ?
 
213
        elif isinstance(a_bzrdir._format,
 
214
                      (bzrdir.BzrDirFormat4,
 
215
                       bzrdir.BzrDirFormat5,
 
216
                       bzrdir.BzrDirFormat6)):
 
217
            result = a_bzrdir.open_repository()
 
218
        else:
 
219
            result = self._format.initialize(a_bzrdir, shared=self.is_shared())
 
220
        self.copy_content_into(result, revision_id, basis)
 
221
        return result
128
222
 
129
223
    def has_revision(self, revision_id):
130
224
        """True if this branch has a copy of the revision.
179
273
        self.revision_store.add(StringIO(gpg_strategy.sign(plaintext)), 
180
274
                                revision_id, "sig")
181
275
 
 
276
    def fileid_involved_between_revs(self, from_revid, to_revid):
 
277
        """Find file_id(s) which are involved in the changes between revisions.
 
278
 
 
279
        This determines the set of revisions which are involved, and then
 
280
        finds all file ids affected by those revisions.
 
281
        """
 
282
        # TODO: jam 20060119 This code assumes that w.inclusions will
 
283
        #       always be correct. But because of the presence of ghosts
 
284
        #       it is possible to be wrong.
 
285
        #       One specific example from Robert Collins:
 
286
        #       Two branches, with revisions ABC, and AD
 
287
        #       C is a ghost merge of D.
 
288
        #       Inclusions doesn't recognize D as an ancestor.
 
289
        #       If D is ever merged in the future, the weave
 
290
        #       won't be fixed, because AD never saw revision C
 
291
        #       to cause a conflict which would force a reweave.
 
292
        w = self.get_inventory_weave()
 
293
        from_set = set(w.inclusions([w.lookup(from_revid)]))
 
294
        to_set = set(w.inclusions([w.lookup(to_revid)]))
 
295
        included = to_set.difference(from_set)
 
296
        changed = map(w.idx_to_name, included)
 
297
        return self._fileid_involved_by_set(changed)
 
298
 
 
299
    def fileid_involved(self, last_revid=None):
 
300
        """Find all file_ids modified in the ancestry of last_revid.
 
301
 
 
302
        :param last_revid: If None, last_revision() will be used.
 
303
        """
 
304
        w = self.get_inventory_weave()
 
305
        if not last_revid:
 
306
            changed = set(w._names)
 
307
        else:
 
308
            included = w.inclusions([w.lookup(last_revid)])
 
309
            changed = map(w.idx_to_name, included)
 
310
        return self._fileid_involved_by_set(changed)
 
311
 
 
312
    def fileid_involved_by_set(self, changes):
 
313
        """Find all file_ids modified by the set of revisions passed in.
 
314
 
 
315
        :param changes: A set() of revision ids
 
316
        """
 
317
        # TODO: jam 20060119 This line does *nothing*, remove it.
 
318
        #       or better yet, change _fileid_involved_by_set so
 
319
        #       that it takes the inventory weave, rather than
 
320
        #       pulling it out by itself.
 
321
        return self._fileid_involved_by_set(changes)
 
322
 
 
323
    def _fileid_involved_by_set(self, changes):
 
324
        """Find the set of file-ids affected by the set of revisions.
 
325
 
 
326
        :param changes: A set() of revision ids.
 
327
        :return: A set() of file ids.
 
328
        
 
329
        This peaks at the Weave, interpreting each line, looking to
 
330
        see if it mentions one of the revisions. And if so, includes
 
331
        the file id mentioned.
 
332
        This expects both the Weave format, and the serialization
 
333
        to have a single line per file/directory, and to have
 
334
        fileid="" and revision="" on that line.
 
335
        """
 
336
        assert isinstance(self._format, (RepositoryFormat5,
 
337
                                         RepositoryFormat6,
 
338
                                         RepositoryFormat7)), \
 
339
            "fileid_involved only supported for branches which store inventory as unnested xml"
 
340
 
 
341
        w = self.get_inventory_weave()
 
342
        file_ids = set()
 
343
        for line in w._weave:
 
344
 
 
345
            # it is ugly, but it is due to the weave structure
 
346
            if not isinstance(line, basestring): continue
 
347
 
 
348
            start = line.find('file_id="')+9
 
349
            if start < 9: continue
 
350
            end = line.find('"', start)
 
351
            assert end>= 0
 
352
            file_id = xml.sax.saxutils.unescape(line[start:end])
 
353
 
 
354
            # check if file_id is already present
 
355
            if file_id in file_ids: continue
 
356
 
 
357
            start = line.find('revision="')+10
 
358
            if start < 10: continue
 
359
            end = line.find('"', start)
 
360
            assert end>= 0
 
361
            revision_id = xml.sax.saxutils.unescape(line[start:end])
 
362
 
 
363
            if revision_id in changes:
 
364
                file_ids.add(file_id)
 
365
        return file_ids
 
366
 
182
367
    @needs_read_lock
183
368
    def get_inventory_weave(self):
184
369
        return self.control_weaves.get_weave('inventory',
223
408
            return self.get_inventory(revision_id)
224
409
 
225
410
    @needs_read_lock
 
411
    def is_shared(self):
 
412
        """Return True if this repository is flagged as a shared repository."""
 
413
        # FIXME format 4-6 cannot be shared, this is technically faulty.
 
414
        return self.control_files._transport.has('shared-storage')
 
415
 
 
416
    @needs_read_lock
226
417
    def revision_tree(self, revision_id):
227
418
        """Return Tree for a revision on this branch.
228
419
 
244
435
        """
245
436
        if revision_id is None:
246
437
            return [None]
 
438
        if not self.has_revision(revision_id):
 
439
            raise errors.NoSuchRevision(self, revision_id)
247
440
        w = self.get_inventory_weave()
248
441
        return [None] + map(w.idx_to_name,
249
442
                            w.inclusions([w.lookup(revision_id)]))
277
470
        return self.control_files.get_transaction()
278
471
 
279
472
    @needs_write_lock
 
473
    def set_make_working_trees(self, new_value):
 
474
        """Set the policy flag for making working trees when creating branches.
 
475
 
 
476
        This only applies to branches that use this repository.
 
477
 
 
478
        The default is 'True'.
 
479
        :param new_value: True to restore the default, False to disable making
 
480
                          working trees.
 
481
        """
 
482
        # FIXME: split out into a new class/strategy ?
 
483
        if isinstance(self._format, (RepositoryFormat4,
 
484
                                     RepositoryFormat5,
 
485
                                     RepositoryFormat6)):
 
486
            raise NotImplementedError(self.set_make_working_trees)
 
487
        if new_value:
 
488
            try:
 
489
                self.control_files._transport.delete('no-working-trees')
 
490
            except errors.NoSuchFile:
 
491
                pass
 
492
        else:
 
493
            self.control_files.put_utf8('no-working-trees', '')
 
494
    
 
495
    def make_working_trees(self):
 
496
        """Returns the policy for making working trees on new branches."""
 
497
        # FIXME: split out into a new class/strategy ?
 
498
        if isinstance(self._format, (RepositoryFormat4,
 
499
                                     RepositoryFormat5,
 
500
                                     RepositoryFormat6)):
 
501
            return True
 
502
        return not self.control_files._transport.has('no-working-trees')
 
503
 
 
504
    @needs_write_lock
280
505
    def sign_revision(self, revision_id, gpg_strategy):
281
506
        plaintext = Testament.from_revision(self, revision_id).as_short_text()
282
507
        self.store_revision_signature(gpg_strategy, plaintext, revision_id)
 
508
 
 
509
 
 
510
class RepositoryFormat(object):
 
511
    """A repository format.
 
512
 
 
513
    Formats provide three things:
 
514
     * An initialization routine to construct repository data on disk.
 
515
     * a format string which is used when the BzrDir supports versioned
 
516
       children.
 
517
     * an open routine which returns a Repository instance.
 
518
 
 
519
    Formats are placed in an dict by their format string for reference 
 
520
    during opening. These should be subclasses of RepositoryFormat
 
521
    for consistency.
 
522
 
 
523
    Once a format is deprecated, just deprecate the initialize and open
 
524
    methods on the format class. Do not deprecate the object, as the 
 
525
    object will be created every system load.
 
526
 
 
527
    Common instance attributes:
 
528
    _matchingbzrdir - the bzrdir format that the repository format was
 
529
    originally written to work with. This can be used if manually
 
530
    constructing a bzrdir and repository, or more commonly for test suite
 
531
    parameterisation.
 
532
    """
 
533
 
 
534
    _default_format = None
 
535
    """The default format used for new repositories."""
 
536
 
 
537
    _formats = {}
 
538
    """The known formats."""
 
539
 
 
540
    @classmethod
 
541
    def find_format(klass, a_bzrdir):
 
542
        """Return the format for the repository object in a_bzrdir."""
 
543
        try:
 
544
            transport = a_bzrdir.get_repository_transport(None)
 
545
            format_string = transport.get("format").read()
 
546
            return klass._formats[format_string]
 
547
        except errors.NoSuchFile:
 
548
            raise errors.NoRepositoryPresent(a_bzrdir)
 
549
        except KeyError:
 
550
            raise errors.UnknownFormatError(format_string)
 
551
 
 
552
    @classmethod
 
553
    def get_default_format(klass):
 
554
        """Return the current default format."""
 
555
        return klass._default_format
 
556
 
 
557
    def get_format_string(self):
 
558
        """Return the ASCII format string that identifies this format.
 
559
        
 
560
        Note that in pre format ?? repositories the format string is 
 
561
        not permitted nor written to disk.
 
562
        """
 
563
        raise NotImplementedError(self.get_format_string)
 
564
 
 
565
    def initialize(self, a_bzrdir, shared=False):
 
566
        """Initialize a repository of this format in a_bzrdir.
 
567
 
 
568
        :param a_bzrdir: The bzrdir to put the new repository in it.
 
569
        :param shared: The repository should be initialized as a sharable one.
 
570
 
 
571
        This may raise UninitializableFormat if shared repository are not
 
572
        compatible the a_bzrdir.
 
573
        """
 
574
 
 
575
    def is_supported(self):
 
576
        """Is this format supported?
 
577
 
 
578
        Supported formats must be initializable and openable.
 
579
        Unsupported formats may not support initialization or committing or 
 
580
        some other features depending on the reason for not being supported.
 
581
        """
 
582
        return True
 
583
 
 
584
    def open(self, a_bzrdir, _found=False):
 
585
        """Return an instance of this format for the bzrdir a_bzrdir.
 
586
        
 
587
        _found is a private parameter, do not use it.
 
588
        """
 
589
        if not _found:
 
590
            # we are being called directly and must probe.
 
591
            raise NotImplementedError
 
592
        return Repository(_format=self, a_bzrdir=a_bzrdir)
 
593
 
 
594
    @classmethod
 
595
    def register_format(klass, format):
 
596
        klass._formats[format.get_format_string()] = format
 
597
 
 
598
    @classmethod
 
599
    def set_default_format(klass, format):
 
600
        klass._default_format = format
 
601
 
 
602
    @classmethod
 
603
    def unregister_format(klass, format):
 
604
        assert klass._formats[format.get_format_string()] is format
 
605
        del klass._formats[format.get_format_string()]
 
606
 
 
607
 
 
608
class PreSplitOutRepositoryFormat(RepositoryFormat):
 
609
    """Base class for the pre split out repository formats."""
 
610
 
 
611
    def initialize(self, a_bzrdir, shared=False, _internal=False):
 
612
        """Create a weave repository.
 
613
        
 
614
        TODO: when creating split out bzr branch formats, move this to a common
 
615
        base for Format5, Format6. or something like that.
 
616
        """
 
617
        from bzrlib.weavefile import write_weave_v5
 
618
        from bzrlib.weave import Weave
 
619
 
 
620
        if shared:
 
621
            raise errors.IncompatibleFormat(self, a_bzrdir._format)
 
622
 
 
623
        if not _internal:
 
624
            # always initialized when the bzrdir is.
 
625
            return Repository(_format=self, a_bzrdir=a_bzrdir)
 
626
        
 
627
        # Create an empty weave
 
628
        sio = StringIO()
 
629
        bzrlib.weavefile.write_weave_v5(Weave(), sio)
 
630
        empty_weave = sio.getvalue()
 
631
 
 
632
        mutter('creating repository in %s.', a_bzrdir.transport.base)
 
633
        dirs = ['revision-store', 'weaves']
 
634
        lock_file = 'branch-lock'
 
635
        files = [('inventory.weave', StringIO(empty_weave)), 
 
636
                 ]
 
637
        
 
638
        # FIXME: RBC 20060125 dont peek under the covers
 
639
        # NB: no need to escape relative paths that are url safe.
 
640
        control_files = LockableFiles(a_bzrdir.transport, 'branch-lock')
 
641
        control_files.lock_write()
 
642
        control_files._transport.mkdir_multi(dirs,
 
643
                mode=control_files._dir_mode)
 
644
        try:
 
645
            for file, content in files:
 
646
                control_files.put(file, content)
 
647
        finally:
 
648
            control_files.unlock()
 
649
        return Repository(_format=self, a_bzrdir=a_bzrdir)
 
650
 
 
651
 
 
652
class RepositoryFormat4(PreSplitOutRepositoryFormat):
 
653
    """Bzr repository format 4.
 
654
 
 
655
    This repository format has:
 
656
     - flat stores
 
657
     - TextStores for texts, inventories,revisions.
 
658
 
 
659
    This format is deprecated: it indexes texts using a text id which is
 
660
    removed in format 5; initializationa and write support for this format
 
661
    has been removed.
 
662
    """
 
663
 
 
664
    def __init__(self):
 
665
        super(RepositoryFormat4, self).__init__()
 
666
        self._matchingbzrdir = bzrdir.BzrDirFormat4()
 
667
 
 
668
    def initialize(self, url, shared=False, _internal=False):
 
669
        """Format 4 branches cannot be created."""
 
670
        raise errors.UninitializableFormat(self)
 
671
 
 
672
    def is_supported(self):
 
673
        """Format 4 is not supported.
 
674
 
 
675
        It is not supported because the model changed from 4 to 5 and the
 
676
        conversion logic is expensive - so doing it on the fly was not 
 
677
        feasible.
 
678
        """
 
679
        return False
 
680
 
 
681
 
 
682
class RepositoryFormat5(PreSplitOutRepositoryFormat):
 
683
    """Bzr control format 5.
 
684
 
 
685
    This repository format has:
 
686
     - weaves for file texts and inventory
 
687
     - flat stores
 
688
     - TextStores for revisions and signatures.
 
689
    """
 
690
 
 
691
    def __init__(self):
 
692
        super(RepositoryFormat5, self).__init__()
 
693
        self._matchingbzrdir = bzrdir.BzrDirFormat5()
 
694
 
 
695
 
 
696
class RepositoryFormat6(PreSplitOutRepositoryFormat):
 
697
    """Bzr control format 6.
 
698
 
 
699
    This repository format has:
 
700
     - weaves for file texts and inventory
 
701
     - hash subdirectory based stores.
 
702
     - TextStores for revisions and signatures.
 
703
    """
 
704
 
 
705
    def __init__(self):
 
706
        super(RepositoryFormat6, self).__init__()
 
707
        self._matchingbzrdir = bzrdir.BzrDirFormat6()
 
708
 
 
709
 
 
710
class RepositoryFormat7(RepositoryFormat):
 
711
    """Bzr repository 7.
 
712
 
 
713
    This repository format has:
 
714
     - weaves for file texts and inventory
 
715
     - hash subdirectory based stores.
 
716
     - TextStores for revisions and signatures.
 
717
     - a format marker of its own
 
718
     - an optional 'shared-storage' flag
 
719
    """
 
720
 
 
721
    def get_format_string(self):
 
722
        """See RepositoryFormat.get_format_string()."""
 
723
        return "Bazaar-NG Repository format 7"
 
724
 
 
725
    def initialize(self, a_bzrdir, shared=False):
 
726
        """Create a weave repository.
 
727
 
 
728
        :param shared: If true the repository will be initialized as a shared
 
729
                       repository.
 
730
        """
 
731
        from bzrlib.weavefile import write_weave_v5
 
732
        from bzrlib.weave import Weave
 
733
 
 
734
        # Create an empty weave
 
735
        sio = StringIO()
 
736
        bzrlib.weavefile.write_weave_v5(Weave(), sio)
 
737
        empty_weave = sio.getvalue()
 
738
 
 
739
        mutter('creating repository in %s.', a_bzrdir.transport.base)
 
740
        dirs = ['revision-store', 'weaves']
 
741
        files = [('inventory.weave', StringIO(empty_weave)), 
 
742
                 ]
 
743
        utf8_files = [('format', self.get_format_string())]
 
744
        
 
745
        # FIXME: RBC 20060125 dont peek under the covers
 
746
        # NB: no need to escape relative paths that are url safe.
 
747
        lock_file = 'lock'
 
748
        repository_transport = a_bzrdir.get_repository_transport(self)
 
749
        repository_transport.put(lock_file, StringIO()) # TODO get the file mode from the bzrdir lock files., mode=file_mode)
 
750
        control_files = LockableFiles(repository_transport, 'lock')
 
751
        control_files.lock_write()
 
752
        control_files._transport.mkdir_multi(dirs,
 
753
                mode=control_files._dir_mode)
 
754
        try:
 
755
            for file, content in files:
 
756
                control_files.put(file, content)
 
757
            for file, content in utf8_files:
 
758
                control_files.put_utf8(file, content)
 
759
            if shared == True:
 
760
                control_files.put_utf8('shared-storage', '')
 
761
        finally:
 
762
            control_files.unlock()
 
763
        return Repository(_format=self, a_bzrdir=a_bzrdir)
 
764
 
 
765
    def __init__(self):
 
766
        super(RepositoryFormat7, self).__init__()
 
767
        self._matchingbzrdir = bzrdir.BzrDirMetaFormat1()
 
768
 
 
769
 
 
770
# formats which have no format string are not discoverable
 
771
# and not independently creatable, so are not registered.
 
772
_default_format = RepositoryFormat7()
 
773
RepositoryFormat.register_format(_default_format)
 
774
RepositoryFormat.set_default_format(_default_format)
 
775
_legacy_formats = [RepositoryFormat4(),
 
776
                   RepositoryFormat5(),
 
777
                   RepositoryFormat6()]
 
778
 
 
779
 
 
780
class InterRepository(object):
 
781
    """This class represents operations taking place between two repositories.
 
782
 
 
783
    Its instances have methods like copy_content and fetch, and contain
 
784
    references to the source and target repositories these operations can be 
 
785
    carried out on.
 
786
 
 
787
    Often we will provide convenience methods on 'repository' which carry out
 
788
    operations with another repository - they will always forward to
 
789
    InterRepository.get(other).method_name(parameters).
 
790
    """
 
791
    # XXX: FIXME: FUTURE: robertc
 
792
    # testing of these probably requires a factory in optimiser type, and 
 
793
    # then a test adapter to test each type thoroughly.
 
794
    #
 
795
 
 
796
    _optimisers = set()
 
797
    """The available optimised InterRepository types."""
 
798
 
 
799
    def __init__(self, source, target):
 
800
        """Construct a default InterRepository instance. Please use 'get'.
 
801
        
 
802
        Only subclasses of InterRepository should call 
 
803
        InterRepository.__init__ - clients should call InterRepository.get
 
804
        instead which will create an optimised InterRepository if possible.
 
805
        """
 
806
        self.source = source
 
807
        self.target = target
 
808
 
 
809
    @needs_write_lock
 
810
    def copy_content(self, revision_id=None, basis=None):
 
811
        """Make a complete copy of the content in self into destination.
 
812
        
 
813
        This is a destructive operation! Do not use it on existing 
 
814
        repositories.
 
815
 
 
816
        :param revision_id: Only copy the content needed to construct
 
817
                            revision_id and its parents.
 
818
        :param basis: Copy the needed data preferentially from basis.
 
819
        """
 
820
        try:
 
821
            self.target.set_make_working_trees(self.source.make_working_trees())
 
822
        except NotImplementedError:
 
823
            pass
 
824
        # grab the basis available data
 
825
        if basis is not None:
 
826
            self.target.fetch(basis, revision_id=revision_id)
 
827
        # but dont both fetching if we have the needed data now.
 
828
        if (revision_id not in (None, NULL_REVISION) and 
 
829
            self.target.has_revision(revision_id)):
 
830
            return
 
831
        self.target.fetch(self.source, revision_id=revision_id)
 
832
 
 
833
    def _double_lock(self, lock_source, lock_target):
 
834
        """Take out too locks, rolling back the first if the second throws."""
 
835
        lock_source()
 
836
        try:
 
837
            lock_target()
 
838
        except Exception:
 
839
            # we want to ensure that we don't leave source locked by mistake.
 
840
            # and any error on target should not confuse source.
 
841
            self.source.unlock()
 
842
            raise
 
843
 
 
844
    @needs_write_lock
 
845
    def fetch(self, revision_id=None, pb=None):
 
846
        """Fetch the content required to construct revision_id.
 
847
 
 
848
        The content is copied from source to target.
 
849
 
 
850
        :param revision_id: if None all content is copied, if NULL_REVISION no
 
851
                            content is copied.
 
852
        :param pb: optional progress bar to use for progress reports. If not
 
853
                   provided a default one will be created.
 
854
 
 
855
        Returns the copied revision count and the failed revisions in a tuple:
 
856
        (copied, failures).
 
857
        """
 
858
        from bzrlib.fetch import RepoFetcher
 
859
        mutter("Using fetch logic to copy between %s(%s) and %s(%s)",
 
860
               self.source, self.source._format, self.target, self.target._format)
 
861
        f = RepoFetcher(to_repository=self.target,
 
862
                        from_repository=self.source,
 
863
                        last_revision=revision_id,
 
864
                        pb=pb)
 
865
        return f.count_copied, f.failed_revisions
 
866
 
 
867
    @classmethod
 
868
    def get(klass, repository_source, repository_target):
 
869
        """Retrieve a InterRepository worker object for these repositories.
 
870
 
 
871
        :param repository_source: the repository to be the 'source' member of
 
872
                                  the InterRepository instance.
 
873
        :param repository_target: the repository to be the 'target' member of
 
874
                                the InterRepository instance.
 
875
        If an optimised InterRepository worker exists it will be used otherwise
 
876
        a default InterRepository instance will be created.
 
877
        """
 
878
        for provider in klass._optimisers:
 
879
            if provider.is_compatible(repository_source, repository_target):
 
880
                return provider(repository_source, repository_target)
 
881
        return InterRepository(repository_source, repository_target)
 
882
 
 
883
    def lock_read(self):
 
884
        """Take out a logical read lock.
 
885
 
 
886
        This will lock the source branch and the target branch. The source gets
 
887
        a read lock and the target a read lock.
 
888
        """
 
889
        self._double_lock(self.source.lock_read, self.target.lock_read)
 
890
 
 
891
    def lock_write(self):
 
892
        """Take out a logical write lock.
 
893
 
 
894
        This will lock the source branch and the target branch. The source gets
 
895
        a read lock and the target a write lock.
 
896
        """
 
897
        self._double_lock(self.source.lock_read, self.target.lock_write)
 
898
 
 
899
    @needs_read_lock
 
900
    def missing_revision_ids(self, revision_id=None):
 
901
        """Return the revision ids that source has that target does not.
 
902
        
 
903
        These are returned in topological order.
 
904
 
 
905
        :param revision_id: only return revision ids included by this
 
906
                            revision_id.
 
907
        """
 
908
        # generic, possibly worst case, slow code path.
 
909
        target_ids = set(self.source.all_revision_ids())
 
910
        if revision_id is not None:
 
911
            source_ids = self.target.get_ancestry(revision_id)
 
912
            assert source_ids.pop(0) == None
 
913
        else:
 
914
            source_ids = self.target.all_revision_ids()
 
915
        result_set = set(source_ids).difference(target_ids)
 
916
        # this may look like a no-op: its not. It preserves the ordering
 
917
        # other_ids had while only returning the members from other_ids
 
918
        # that we've decided we need.
 
919
        return [rev_id for rev_id in other_ids if rev_id in result_set]
 
920
 
 
921
    @classmethod
 
922
    def register_optimiser(klass, optimiser):
 
923
        """Register an InterRepository optimiser."""
 
924
        klass._optimisers.add(optimiser)
 
925
 
 
926
    def unlock(self):
 
927
        """Release the locks on source and target."""
 
928
        try:
 
929
            self.target.unlock()
 
930
        finally:
 
931
            self.source.unlock()
 
932
 
 
933
    @classmethod
 
934
    def unregister_optimiser(klass, optimiser):
 
935
        """Unregister an InterRepository optimiser."""
 
936
        klass._optimisers.remove(optimiser)
 
937
 
 
938
 
 
939
class InterWeaveRepo(InterRepository):
 
940
    """Optimised code paths between Weave based repositories."""
 
941
 
 
942
    _matching_repo_format = _default_format
 
943
    """Repository format for testing with."""
 
944
 
 
945
    @staticmethod
 
946
    def is_compatible(source, target):
 
947
        """Be compatible with known Weave formats.
 
948
        
 
949
        We dont test for the stores being of specific types becase that
 
950
        could lead to confusing results, and there is no need to be 
 
951
        overly general.
 
952
        """
 
953
        try:
 
954
            return (isinstance(source._format, (RepositoryFormat5,
 
955
                                                RepositoryFormat6,
 
956
                                                RepositoryFormat7)) and
 
957
                    isinstance(target._format, (RepositoryFormat5,
 
958
                                                RepositoryFormat6,
 
959
                                                RepositoryFormat7)))
 
960
        except AttributeError:
 
961
            return False
 
962
    
 
963
    @needs_write_lock
 
964
    def copy_content(self, revision_id=None, basis=None):
 
965
        """See InterRepository.copy_content()."""
 
966
        # weave specific optimised path:
 
967
        if basis is not None:
 
968
            # copy the basis in, then fetch remaining data.
 
969
            basis.copy_content_into(self.target, revision_id)
 
970
            # the basis copy_content_into could misset this.
 
971
            try:
 
972
                self.target.set_make_working_trees(self.source.make_working_trees())
 
973
            except NotImplementedError:
 
974
                pass
 
975
            self.target.fetch(self.source, revision_id=revision_id)
 
976
        else:
 
977
            try:
 
978
                self.target.set_make_working_trees(self.source.make_working_trees())
 
979
            except NotImplementedError:
 
980
                pass
 
981
            # FIXME do not peek!
 
982
            if self.source.control_files._transport.listable():
 
983
                pb = bzrlib.ui.ui_factory.progress_bar()
 
984
                copy_all(self.source.weave_store,
 
985
                    self.target.weave_store, pb=pb)
 
986
                pb.update('copying inventory', 0, 1)
 
987
                self.target.control_weaves.copy_multi(
 
988
                    self.source.control_weaves, ['inventory'])
 
989
                copy_all(self.source.revision_store,
 
990
                    self.target.revision_store, pb=pb)
 
991
            else:
 
992
                self.target.fetch(self.source, revision_id=revision_id)
 
993
 
 
994
    @needs_write_lock
 
995
    def fetch(self, revision_id=None, pb=None):
 
996
        """See InterRepository.fetch()."""
 
997
        from bzrlib.fetch import RepoFetcher
 
998
        mutter("Using fetch logic to copy between %s(%s) and %s(%s)",
 
999
               self.source, self.source._format, self.target, self.target._format)
 
1000
        f = RepoFetcher(to_repository=self.target,
 
1001
                        from_repository=self.source,
 
1002
                        last_revision=revision_id,
 
1003
                        pb=pb)
 
1004
        return f.count_copied, f.failed_revisions
 
1005
 
 
1006
    @needs_read_lock
 
1007
    def missing_revision_ids(self, revision_id=None):
 
1008
        """See InterRepository.missing_revision_ids()."""
 
1009
        # we want all revisions to satisfy revision_id in source.
 
1010
        # but we dont want to stat every file here and there.
 
1011
        # we want then, all revisions other needs to satisfy revision_id 
 
1012
        # checked, but not those that we have locally.
 
1013
        # so the first thing is to get a subset of the revisions to 
 
1014
        # satisfy revision_id in source, and then eliminate those that
 
1015
        # we do already have. 
 
1016
        # this is slow on high latency connection to self, but as as this
 
1017
        # disk format scales terribly for push anyway due to rewriting 
 
1018
        # inventory.weave, this is considered acceptable.
 
1019
        # - RBC 20060209
 
1020
        if revision_id is not None:
 
1021
            source_ids = self.source.get_ancestry(revision_id)
 
1022
            assert source_ids.pop(0) == None
 
1023
        else:
 
1024
            source_ids = self.source._all_possible_ids()
 
1025
        source_ids_set = set(source_ids)
 
1026
        # source_ids is the worst possible case we may need to pull.
 
1027
        # now we want to filter source_ids against what we actually
 
1028
        # have in target, but dont try to check for existence where we know
 
1029
        # we do not have a revision as that would be pointless.
 
1030
        target_ids = set(self.target._all_possible_ids())
 
1031
        possibly_present_revisions = target_ids.intersection(source_ids_set)
 
1032
        actually_present_revisions = set(self.target._eliminate_revisions_not_present(possibly_present_revisions))
 
1033
        required_revisions = source_ids_set.difference(actually_present_revisions)
 
1034
        required_topo_revisions = [rev_id for rev_id in source_ids if rev_id in required_revisions]
 
1035
        if revision_id is not None:
 
1036
            # we used get_ancestry to determine source_ids then we are assured all
 
1037
            # revisions referenced are present as they are installed in topological order.
 
1038
            # and the tip revision was validated by get_ancestry.
 
1039
            return required_topo_revisions
 
1040
        else:
 
1041
            # if we just grabbed the possibly available ids, then 
 
1042
            # we only have an estimate of whats available and need to validate
 
1043
            # that against the revision records.
 
1044
            return self.source._eliminate_revisions_not_present(required_topo_revisions)
 
1045
 
 
1046
 
 
1047
InterRepository.register_optimiser(InterWeaveRepo)
 
1048
 
 
1049
 
 
1050
class RepositoryTestProviderAdapter(object):
 
1051
    """A tool to generate a suite testing multiple repository formats at once.
 
1052
 
 
1053
    This is done by copying the test once for each transport and injecting
 
1054
    the transport_server, transport_readonly_server, and bzrdir_format and
 
1055
    repository_format classes into each copy. Each copy is also given a new id()
 
1056
    to make it easy to identify.
 
1057
    """
 
1058
 
 
1059
    def __init__(self, transport_server, transport_readonly_server, formats):
 
1060
        self._transport_server = transport_server
 
1061
        self._transport_readonly_server = transport_readonly_server
 
1062
        self._formats = formats
 
1063
    
 
1064
    def adapt(self, test):
 
1065
        result = TestSuite()
 
1066
        for repository_format, bzrdir_format in self._formats:
 
1067
            new_test = deepcopy(test)
 
1068
            new_test.transport_server = self._transport_server
 
1069
            new_test.transport_readonly_server = self._transport_readonly_server
 
1070
            new_test.bzrdir_format = bzrdir_format
 
1071
            new_test.repository_format = repository_format
 
1072
            def make_new_test_id():
 
1073
                new_id = "%s(%s)" % (new_test.id(), repository_format.__class__.__name__)
 
1074
                return lambda: new_id
 
1075
            new_test.id = make_new_test_id()
 
1076
            result.addTest(new_test)
 
1077
        return result
 
1078
 
 
1079
 
 
1080
class InterRepositoryTestProviderAdapter(object):
 
1081
    """A tool to generate a suite testing multiple inter repository formats.
 
1082
 
 
1083
    This is done by copying the test once for each interrepo provider and injecting
 
1084
    the transport_server, transport_readonly_server, repository_format and 
 
1085
    repository_to_format classes into each copy.
 
1086
    Each copy is also given a new id() to make it easy to identify.
 
1087
    """
 
1088
 
 
1089
    def __init__(self, transport_server, transport_readonly_server, formats):
 
1090
        self._transport_server = transport_server
 
1091
        self._transport_readonly_server = transport_readonly_server
 
1092
        self._formats = formats
 
1093
    
 
1094
    def adapt(self, test):
 
1095
        result = TestSuite()
 
1096
        for interrepo_class, repository_format, repository_format_to in self._formats:
 
1097
            new_test = deepcopy(test)
 
1098
            new_test.transport_server = self._transport_server
 
1099
            new_test.transport_readonly_server = self._transport_readonly_server
 
1100
            new_test.interrepo_class = interrepo_class
 
1101
            new_test.repository_format = repository_format
 
1102
            new_test.repository_format_to = repository_format_to
 
1103
            def make_new_test_id():
 
1104
                new_id = "%s(%s)" % (new_test.id(), interrepo_class.__name__)
 
1105
                return lambda: new_id
 
1106
            new_test.id = make_new_test_id()
 
1107
            result.addTest(new_test)
 
1108
        return result
 
1109
 
 
1110
    @staticmethod
 
1111
    def default_test_list():
 
1112
        """Generate the default list of interrepo permutations to test."""
 
1113
        result = []
 
1114
        # test the default InterRepository between format 6 and the current 
 
1115
        # default format.
 
1116
        # XXX: robertc 20060220 reinstate this when there are two supported
 
1117
        # formats which do not have an optimal code path between them.
 
1118
        #result.append((InterRepository, RepositoryFormat6(),
 
1119
        #              RepositoryFormat.get_default_format()))
 
1120
        for optimiser in InterRepository._optimisers:
 
1121
            result.append((optimiser,
 
1122
                           optimiser._matching_repo_format,
 
1123
                           optimiser._matching_repo_format
 
1124
                           ))
 
1125
        # if there are specific combinations we want to use, we can add them 
 
1126
        # here.
 
1127
        return result