/brz/remove-bazaar

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

« back to all changes in this revision

Viewing changes to bzrlib/repository.py

Add RepositoryFormats and allow bzrdir.open or create _repository to be used.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright (C) 2005 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 
16
 
 
17
from copy import deepcopy
 
18
from cStringIO import StringIO
 
19
from unittest import TestSuite
 
20
 
 
21
import bzrlib.bzrdir as bzrdir
 
22
from bzrlib.decorators import needs_read_lock, needs_write_lock
 
23
from bzrlib.errors import InvalidRevisionId
 
24
from bzrlib.lockable_files import LockableFiles
 
25
from bzrlib.osutils import safe_unicode
 
26
from bzrlib.revision import NULL_REVISION
 
27
from bzrlib.store import copy_all
 
28
from bzrlib.store.weave import WeaveStore
 
29
from bzrlib.store.text import TextStore
 
30
from bzrlib.trace import mutter
 
31
from bzrlib.tree import RevisionTree
 
32
from bzrlib.testament import Testament
 
33
from bzrlib.tree import EmptyTree
 
34
import bzrlib.xml5
 
35
 
 
36
 
 
37
 
 
38
class Repository(object):
 
39
    """Repository holding history for one or more branches.
 
40
 
 
41
    The repository holds and retrieves historical information including
 
42
    revisions and file history.  It's normally accessed only by the Branch,
 
43
    which views a particular line of development through that history.
 
44
 
 
45
    The Repository builds on top of Stores and a Transport, which respectively 
 
46
    describe the disk data format and the way of accessing the (possibly 
 
47
    remote) disk.
 
48
    """
 
49
 
 
50
    @staticmethod
 
51
    def create(a_bzrdir):
 
52
        """Construct the current default format repository in a_bzrdir."""
 
53
        return RepositoryFormat.get_default_format().initialize(a_bzrdir)
 
54
 
 
55
    def __init__(self, transport, branch_format, _format=None, a_bzrdir=None):
 
56
        object.__init__(self)
 
57
        if transport is not None:
 
58
            self.control_files = LockableFiles(transport.clone(bzrlib.BZRDIR), 'README')
 
59
        else: 
 
60
            # TODO: clone into repository if needed
 
61
            self.control_files = LockableFiles(a_bzrdir.transport, 'README')
 
62
 
 
63
        dir_mode = self.control_files._dir_mode
 
64
        file_mode = self.control_files._file_mode
 
65
        self._format = _format
 
66
        self.bzrdir = a_bzrdir
 
67
 
 
68
        def get_weave(name, prefixed=False):
 
69
            if name:
 
70
                name = safe_unicode(name)
 
71
            else:
 
72
                name = ''
 
73
            relpath = self.control_files._escape(name)
 
74
            weave_transport = self.control_files._transport.clone(relpath)
 
75
            ws = WeaveStore(weave_transport, prefixed=prefixed,
 
76
                            dir_mode=dir_mode,
 
77
                            file_mode=file_mode)
 
78
            if self.control_files._transport.should_cache():
 
79
                ws.enable_cache = True
 
80
            return ws
 
81
 
 
82
 
 
83
        def get_store(name, compressed=True, prefixed=False):
 
84
            # FIXME: This approach of assuming stores are all entirely compressed
 
85
            # or entirely uncompressed is tidy, but breaks upgrade from 
 
86
            # some existing branches where there's a mixture; we probably 
 
87
            # still want the option to look for both.
 
88
            if name:
 
89
                name = safe_unicode(name)
 
90
            else:
 
91
                name = ''
 
92
            relpath = self.control_files._escape(name)
 
93
            store = TextStore(self.control_files._transport.clone(relpath),
 
94
                              prefixed=prefixed, compressed=compressed,
 
95
                              dir_mode=dir_mode,
 
96
                              file_mode=file_mode)
 
97
            #if self._transport.should_cache():
 
98
            #    cache_path = os.path.join(self.cache_root, name)
 
99
            #    os.mkdir(cache_path)
 
100
            #    store = bzrlib.store.CachedStore(store, cache_path)
 
101
            return store
 
102
 
 
103
        if branch_format is not None:
 
104
            # circular dependencies:
 
105
            from bzrlib.branch import (BzrBranchFormat4,
 
106
                                       BzrBranchFormat5,
 
107
                                       BzrBranchFormat6,
 
108
                                       )
 
109
            if isinstance(branch_format, BzrBranchFormat4):
 
110
                self._format = RepositoryFormat4()
 
111
            elif isinstance(branch_format, BzrBranchFormat5):
 
112
                self._format = RepositoryFormat5()
 
113
            elif isinstance(branch_format, BzrBranchFormat6):
 
114
                self._format = RepositoryFormat6()
 
115
            
 
116
 
 
117
        if isinstance(self._format, RepositoryFormat4):
 
118
            self.inventory_store = get_store('inventory-store')
 
119
            self.text_store = get_store('text-store')
 
120
            self.revision_store = get_store('revision-store')
 
121
        elif isinstance(self._format, RepositoryFormat5):
 
122
            self.control_weaves = get_weave('')
 
123
            self.weave_store = get_weave('weaves')
 
124
            self.revision_store = get_store('revision-store', compressed=False)
 
125
        elif isinstance(self._format, RepositoryFormat6):
 
126
            self.control_weaves = get_weave('')
 
127
            self.weave_store = get_weave('weaves', prefixed=True)
 
128
            self.revision_store = get_store('revision-store', compressed=False,
 
129
                                            prefixed=True)
 
130
        self.revision_store.register_suffix('sig')
 
131
 
 
132
    def lock_write(self):
 
133
        self.control_files.lock_write()
 
134
 
 
135
    def lock_read(self):
 
136
        self.control_files.lock_read()
 
137
 
 
138
    @staticmethod
 
139
    def open(base):
 
140
        """Open the repository rooted at base.
 
141
 
 
142
        For instance, if the repository is at URL/.bzr/repository,
 
143
        Repository.open(URL) -> a Repository instance.
 
144
        """
 
145
        control = bzrdir.BzrDir.open(base)
 
146
        return control.open_repository()
 
147
 
 
148
    def unlock(self):
 
149
        self.control_files.unlock()
 
150
 
 
151
    @needs_read_lock
 
152
    def copy(self, destination):
 
153
        destination.lock_write()
 
154
        try:
 
155
            destination.control_weaves.copy_multi(self.control_weaves, 
 
156
                ['inventory'])
 
157
            copy_all(self.weave_store, destination.weave_store)
 
158
            copy_all(self.revision_store, destination.revision_store)
 
159
        finally:
 
160
            destination.unlock()
 
161
 
 
162
    def has_revision(self, revision_id):
 
163
        """True if this branch has a copy of the revision.
 
164
 
 
165
        This does not necessarily imply the revision is merge
 
166
        or on the mainline."""
 
167
        return (revision_id is None
 
168
                or self.revision_store.has_id(revision_id))
 
169
 
 
170
    @needs_read_lock
 
171
    def get_revision_xml_file(self, revision_id):
 
172
        """Return XML file object for revision object."""
 
173
        if not revision_id or not isinstance(revision_id, basestring):
 
174
            raise InvalidRevisionId(revision_id=revision_id, branch=self)
 
175
        try:
 
176
            return self.revision_store.get(revision_id)
 
177
        except (IndexError, KeyError):
 
178
            raise bzrlib.errors.NoSuchRevision(self, revision_id)
 
179
 
 
180
    @needs_read_lock
 
181
    def get_revision_xml(self, revision_id):
 
182
        return self.get_revision_xml_file(revision_id).read()
 
183
 
 
184
    @needs_read_lock
 
185
    def get_revision(self, revision_id):
 
186
        """Return the Revision object for a named revision"""
 
187
        xml_file = self.get_revision_xml_file(revision_id)
 
188
 
 
189
        try:
 
190
            r = bzrlib.xml5.serializer_v5.read_revision(xml_file)
 
191
        except SyntaxError, e:
 
192
            raise bzrlib.errors.BzrError('failed to unpack revision_xml',
 
193
                                         [revision_id,
 
194
                                          str(e)])
 
195
            
 
196
        assert r.revision_id == revision_id
 
197
        return r
 
198
 
 
199
    @needs_read_lock
 
200
    def get_revision_sha1(self, revision_id):
 
201
        """Hash the stored value of a revision, and return it."""
 
202
        # In the future, revision entries will be signed. At that
 
203
        # point, it is probably best *not* to include the signature
 
204
        # in the revision hash. Because that lets you re-sign
 
205
        # the revision, (add signatures/remove signatures) and still
 
206
        # have all hash pointers stay consistent.
 
207
        # But for now, just hash the contents.
 
208
        return bzrlib.osutils.sha_file(self.get_revision_xml_file(revision_id))
 
209
 
 
210
    @needs_write_lock
 
211
    def store_revision_signature(self, gpg_strategy, plaintext, revision_id):
 
212
        self.revision_store.add(StringIO(gpg_strategy.sign(plaintext)), 
 
213
                                revision_id, "sig")
 
214
 
 
215
    @needs_read_lock
 
216
    def get_inventory_weave(self):
 
217
        return self.control_weaves.get_weave('inventory',
 
218
            self.get_transaction())
 
219
 
 
220
    @needs_read_lock
 
221
    def get_inventory(self, revision_id):
 
222
        """Get Inventory object by hash."""
 
223
        xml = self.get_inventory_xml(revision_id)
 
224
        return bzrlib.xml5.serializer_v5.read_inventory_from_string(xml)
 
225
 
 
226
    @needs_read_lock
 
227
    def get_inventory_xml(self, revision_id):
 
228
        """Get inventory XML as a file object."""
 
229
        try:
 
230
            assert isinstance(revision_id, basestring), type(revision_id)
 
231
            iw = self.get_inventory_weave()
 
232
            return iw.get_text(iw.lookup(revision_id))
 
233
        except IndexError:
 
234
            raise bzrlib.errors.HistoryMissing(self, 'inventory', revision_id)
 
235
 
 
236
    @needs_read_lock
 
237
    def get_inventory_sha1(self, revision_id):
 
238
        """Return the sha1 hash of the inventory entry
 
239
        """
 
240
        return self.get_revision(revision_id).inventory_sha1
 
241
 
 
242
    @needs_read_lock
 
243
    def get_revision_inventory(self, revision_id):
 
244
        """Return inventory of a past revision."""
 
245
        # TODO: Unify this with get_inventory()
 
246
        # bzr 0.0.6 and later imposes the constraint that the inventory_id
 
247
        # must be the same as its revision, so this is trivial.
 
248
        if revision_id is None:
 
249
            # This does not make sense: if there is no revision,
 
250
            # then it is the current tree inventory surely ?!
 
251
            # and thus get_root_id() is something that looks at the last
 
252
            # commit on the branch, and the get_root_id is an inventory check.
 
253
            raise NotImplementedError
 
254
            # return Inventory(self.get_root_id())
 
255
        else:
 
256
            return self.get_inventory(revision_id)
 
257
 
 
258
    @needs_read_lock
 
259
    def revision_tree(self, revision_id):
 
260
        """Return Tree for a revision on this branch.
 
261
 
 
262
        `revision_id` may be None for the null revision, in which case
 
263
        an `EmptyTree` is returned."""
 
264
        # TODO: refactor this to use an existing revision object
 
265
        # so we don't need to read it in twice.
 
266
        if revision_id is None or revision_id == NULL_REVISION:
 
267
            return EmptyTree()
 
268
        else:
 
269
            inv = self.get_revision_inventory(revision_id)
 
270
            return RevisionTree(self, inv, revision_id)
 
271
 
 
272
    @needs_read_lock
 
273
    def get_ancestry(self, revision_id):
 
274
        """Return a list of revision-ids integrated by a revision.
 
275
        
 
276
        This is topologically sorted.
 
277
        """
 
278
        if revision_id is None:
 
279
            return [None]
 
280
        w = self.get_inventory_weave()
 
281
        return [None] + map(w.idx_to_name,
 
282
                            w.inclusions([w.lookup(revision_id)]))
 
283
 
 
284
    @needs_read_lock
 
285
    def print_file(self, file, revision_id):
 
286
        """Print `file` to stdout.
 
287
        
 
288
        FIXME RBC 20060125 as John Meinel points out this is a bad api
 
289
        - it writes to stdout, it assumes that that is valid etc. Fix
 
290
        by creating a new more flexible convenience function.
 
291
        """
 
292
        tree = self.revision_tree(revision_id)
 
293
        # use inventory as it was in that revision
 
294
        file_id = tree.inventory.path2id(file)
 
295
        if not file_id:
 
296
            raise BzrError("%r is not present in revision %s" % (file, revno))
 
297
            try:
 
298
                revno = self.revision_id_to_revno(revision_id)
 
299
            except errors.NoSuchRevision:
 
300
                # TODO: This should not be BzrError,
 
301
                # but NoSuchFile doesn't fit either
 
302
                raise BzrError('%r is not present in revision %s' 
 
303
                                % (file, revision_id))
 
304
            else:
 
305
                raise BzrError('%r is not present in revision %s'
 
306
                                % (file, revno))
 
307
        tree.print_file(file_id)
 
308
 
 
309
    def get_transaction(self):
 
310
        return self.control_files.get_transaction()
 
311
 
 
312
    @needs_write_lock
 
313
    def sign_revision(self, revision_id, gpg_strategy):
 
314
        plaintext = Testament.from_revision(self, revision_id).as_short_text()
 
315
        self.store_revision_signature(gpg_strategy, plaintext, revision_id)
 
316
 
 
317
 
 
318
class RepositoryFormat(object):
 
319
    """A repository format.
 
320
 
 
321
    Formats provide three things:
 
322
     * An initialization routine to construct repository data on disk.
 
323
     * a format string which is used when the BzrDir supports versioned
 
324
       children.
 
325
     * an open routine which returns a Repository instance.
 
326
 
 
327
    Formats are placed in an dict by their format string for reference 
 
328
    during opening. These should be subclasses of RepositoryFormat
 
329
    for consistency.
 
330
 
 
331
    Once a format is deprecated, just deprecate the initialize and open
 
332
    methods on the format class. Do not deprecate the object, as the 
 
333
    object will be created every system load.
 
334
 
 
335
    Common instance attributes:
 
336
    _matchingbzrdir - the bzrdir format that the repository format was
 
337
    originally written to work with. This can be used if manually
 
338
    constructing a bzrdir and repository, or more commonly for test suite
 
339
    parameterisation.
 
340
    """
 
341
 
 
342
    _default_format = None
 
343
    """The default format used for new branches."""
 
344
 
 
345
    _formats = {}
 
346
    """The known formats."""
 
347
 
 
348
    @classmethod
 
349
    def get_default_format(klass):
 
350
        """Return the current default format."""
 
351
        return klass._default_format
 
352
 
 
353
    def get_format_string(self):
 
354
        """Return the ASCII format string that identifies this format.
 
355
        
 
356
        Note that in pre format ?? repositories the format string is 
 
357
        not permitted nor written to disk.
 
358
        """
 
359
        raise NotImplementedError(self.get_format_string)
 
360
 
 
361
    def initialize(self, a_bzrdir):
 
362
        """Create a weave repository.
 
363
        
 
364
        TODO: when creating split out bzr branch formats, move this to a common
 
365
        base for Format5, Format6. or something like that.
 
366
        """
 
367
        from bzrlib.weavefile import write_weave_v5
 
368
        from bzrlib.weave import Weave
 
369
        
 
370
        # Create an empty weave
 
371
        sio = StringIO()
 
372
        bzrlib.weavefile.write_weave_v5(Weave(), sio)
 
373
        empty_weave = sio.getvalue()
 
374
 
 
375
        mutter('creating repository in %s.', a_bzrdir.transport.base)
 
376
        dirs = ['revision-store', 'weaves']
 
377
        lock_file = 'branch-lock'
 
378
        files = [('inventory.weave', StringIO(empty_weave)), 
 
379
                 ]
 
380
        
 
381
        # FIXME: RBC 20060125 dont peek under the covers
 
382
        # NB: no need to escape relative paths that are url safe.
 
383
        control_files = LockableFiles(a_bzrdir.transport, 'branch-lock')
 
384
        control_files.lock_write()
 
385
        control_files._transport.mkdir_multi(dirs,
 
386
                mode=control_files._dir_mode)
 
387
        try:
 
388
            for file, content in files:
 
389
                control_files.put(file, content)
 
390
        finally:
 
391
            control_files.unlock()
 
392
        return Repository(None, branch_format=None, _format=self, a_bzrdir=a_bzrdir)
 
393
 
 
394
    def is_supported(self):
 
395
        """Is this format supported?
 
396
 
 
397
        Supported formats must be initializable and openable.
 
398
        Unsupported formats may not support initialization or committing or 
 
399
        some other features depending on the reason for not being supported.
 
400
        """
 
401
        return True
 
402
 
 
403
    def open(self, a_bzrdir, _found=False):
 
404
        """Return an instance of this format for the bzrdir a_bzrdir.
 
405
        
 
406
        _found is a private parameter, do not use it.
 
407
        """
 
408
        if not _found:
 
409
            raise NotImplementedError
 
410
        return Repository(None, branch_format=None, _format=self, a_bzrdir=a_bzrdir)
 
411
 
 
412
    @classmethod
 
413
    def register_format(klass, format):
 
414
        klass._formats[format.get_format_string()] = format
 
415
 
 
416
    @classmethod
 
417
    def set_default_format(klass, format):
 
418
        klass._default_format = format
 
419
 
 
420
    @classmethod
 
421
    def unregister_format(klass, format):
 
422
        assert klass._formats[format.get_format_string()] is format
 
423
        del klass._formats[format.get_format_string()]
 
424
 
 
425
 
 
426
class RepositoryFormat4(RepositoryFormat):
 
427
    """Bzr repository format 4.
 
428
 
 
429
    This repository format has:
 
430
     - flat stores
 
431
     - TextStores for texts, inventories,revisions.
 
432
 
 
433
    This format is deprecated: it indexes texts using a text id which is
 
434
    removed in format 5; initializationa and write support for this format
 
435
    has been removed.
 
436
    """
 
437
 
 
438
    def __init__(self):
 
439
        super(RepositoryFormat4, self).__init__()
 
440
        self._matchingbzrdir = bzrdir.BzrDirFormat4()
 
441
 
 
442
    def initialize(self, url):
 
443
        """Format 4 branches cannot be created."""
 
444
        raise errors.UninitializableFormat(self)
 
445
 
 
446
    def is_supported(self):
 
447
        """Format 4 is not supported.
 
448
 
 
449
        It is not supported because the model changed from 4 to 5 and the
 
450
        conversion logic is expensive - so doing it on the fly was not 
 
451
        feasible.
 
452
        """
 
453
        return False
 
454
 
 
455
 
 
456
class RepositoryFormat5(RepositoryFormat):
 
457
    """Bzr control format 5.
 
458
 
 
459
    This repository format has:
 
460
     - weaves for file texts and inventory
 
461
     - flat stores
 
462
     - TextStores for revisions and signatures.
 
463
    """
 
464
 
 
465
    def __init__(self):
 
466
        super(RepositoryFormat5, self).__init__()
 
467
        self._matchingbzrdir = bzrdir.BzrDirFormat5()
 
468
 
 
469
 
 
470
class RepositoryFormat6(RepositoryFormat):
 
471
    """Bzr control format 6.
 
472
 
 
473
    This repository format has:
 
474
     - weaves for file texts and inventory
 
475
     - hash subdirectory based stores.
 
476
     - TextStores for revisions and signatures.
 
477
    """
 
478
 
 
479
    def __init__(self):
 
480
        super(RepositoryFormat6, self).__init__()
 
481
        self._matchingbzrdir = bzrdir.BzrDirFormat6()
 
482
 
 
483
# formats which have no format string are not discoverable
 
484
# and not independently creatable, so are not registered.
 
485
# __default_format = RepositoryFormatXXX()
 
486
# RepositoryFormat.register_format(__default_format)
 
487
# RepositoryFormat.set_default_format(__default_format)
 
488
_legacy_formats = [RepositoryFormat4(),
 
489
                   RepositoryFormat5(),
 
490
                   RepositoryFormat6()]
 
491
 
 
492
 
 
493
class RepositoryTestProviderAdapter(object):
 
494
    """A tool to generate a suite testing multiple repository formats at once.
 
495
 
 
496
    This is done by copying the test once for each transport and injecting
 
497
    the transport_server, transport_readonly_server, and bzrdir_format and
 
498
    repository_format classes into each copy. Each copy is also given a new id()
 
499
    to make it easy to identify.
 
500
    """
 
501
 
 
502
    def __init__(self, transport_server, transport_readonly_server, formats):
 
503
        self._transport_server = transport_server
 
504
        self._transport_readonly_server = transport_readonly_server
 
505
        self._formats = formats
 
506
    
 
507
    def adapt(self, test):
 
508
        result = TestSuite()
 
509
        for repository_format, bzrdir_format in self._formats:
 
510
            new_test = deepcopy(test)
 
511
            new_test.transport_server = self._transport_server
 
512
            new_test.transport_readonly_server = self._transport_readonly_server
 
513
            new_test.bzrdir_format = bzrdir_format
 
514
            new_test.repository_format = repository_format
 
515
            def make_new_test_id():
 
516
                new_id = "%s(%s)" % (new_test.id(), repository_format.__class__.__name__)
 
517
                return lambda: new_id
 
518
            new_test.id = make_new_test_id()
 
519
            result.addTest(new_test)
 
520
        return result