1
# Copyright (C) 2005 Canonical Ltd
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.
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.
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
17
from copy import deepcopy
18
from cStringIO import StringIO
19
from unittest import TestSuite
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
38
class Repository(object):
39
"""Repository holding history for one or more branches.
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.
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
52
"""Construct the current default format repository in a_bzrdir."""
53
return RepositoryFormat.get_default_format().initialize(a_bzrdir)
55
def __init__(self, transport, branch_format, _format=None, a_bzrdir=None):
57
if transport is not None:
58
self.control_files = LockableFiles(transport.clone(bzrlib.BZRDIR), 'README')
60
# TODO: clone into repository if needed
61
self.control_files = LockableFiles(a_bzrdir.transport, 'README')
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
68
def get_weave(name, prefixed=False):
70
name = safe_unicode(name)
73
relpath = self.control_files._escape(name)
74
weave_transport = self.control_files._transport.clone(relpath)
75
ws = WeaveStore(weave_transport, prefixed=prefixed,
78
if self.control_files._transport.should_cache():
79
ws.enable_cache = True
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.
89
name = safe_unicode(name)
92
relpath = self.control_files._escape(name)
93
store = TextStore(self.control_files._transport.clone(relpath),
94
prefixed=prefixed, compressed=compressed,
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)
103
if branch_format is not None:
104
# circular dependencies:
105
from bzrlib.branch import (BzrBranchFormat4,
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()
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,
130
self.revision_store.register_suffix('sig')
132
def lock_write(self):
133
self.control_files.lock_write()
136
self.control_files.lock_read()
140
"""Open the repository rooted at base.
142
For instance, if the repository is at URL/.bzr/repository,
143
Repository.open(URL) -> a Repository instance.
145
control = bzrdir.BzrDir.open(base)
146
return control.open_repository()
149
self.control_files.unlock()
152
def copy(self, destination):
153
destination.lock_write()
155
destination.control_weaves.copy_multi(self.control_weaves,
157
copy_all(self.weave_store, destination.weave_store)
158
copy_all(self.revision_store, destination.revision_store)
162
def has_revision(self, revision_id):
163
"""True if this branch has a copy of the revision.
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))
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)
176
return self.revision_store.get(revision_id)
177
except (IndexError, KeyError):
178
raise bzrlib.errors.NoSuchRevision(self, revision_id)
181
def get_revision_xml(self, revision_id):
182
return self.get_revision_xml_file(revision_id).read()
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)
190
r = bzrlib.xml5.serializer_v5.read_revision(xml_file)
191
except SyntaxError, e:
192
raise bzrlib.errors.BzrError('failed to unpack revision_xml',
196
assert r.revision_id == revision_id
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))
211
def store_revision_signature(self, gpg_strategy, plaintext, revision_id):
212
self.revision_store.add(StringIO(gpg_strategy.sign(plaintext)),
216
def get_inventory_weave(self):
217
return self.control_weaves.get_weave('inventory',
218
self.get_transaction())
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)
227
def get_inventory_xml(self, revision_id):
228
"""Get inventory XML as a file object."""
230
assert isinstance(revision_id, basestring), type(revision_id)
231
iw = self.get_inventory_weave()
232
return iw.get_text(iw.lookup(revision_id))
234
raise bzrlib.errors.HistoryMissing(self, 'inventory', revision_id)
237
def get_inventory_sha1(self, revision_id):
238
"""Return the sha1 hash of the inventory entry
240
return self.get_revision(revision_id).inventory_sha1
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())
256
return self.get_inventory(revision_id)
259
def revision_tree(self, revision_id):
260
"""Return Tree for a revision on this branch.
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:
269
inv = self.get_revision_inventory(revision_id)
270
return RevisionTree(self, inv, revision_id)
273
def get_ancestry(self, revision_id):
274
"""Return a list of revision-ids integrated by a revision.
276
This is topologically sorted.
278
if revision_id is None:
280
w = self.get_inventory_weave()
281
return [None] + map(w.idx_to_name,
282
w.inclusions([w.lookup(revision_id)]))
285
def print_file(self, file, revision_id):
286
"""Print `file` to stdout.
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.
292
tree = self.revision_tree(revision_id)
293
# use inventory as it was in that revision
294
file_id = tree.inventory.path2id(file)
296
raise BzrError("%r is not present in revision %s" % (file, revno))
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))
305
raise BzrError('%r is not present in revision %s'
307
tree.print_file(file_id)
309
def get_transaction(self):
310
return self.control_files.get_transaction()
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)
318
class RepositoryFormat(object):
319
"""A repository format.
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
325
* an open routine which returns a Repository instance.
327
Formats are placed in an dict by their format string for reference
328
during opening. These should be subclasses of RepositoryFormat
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.
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
342
_default_format = None
343
"""The default format used for new branches."""
346
"""The known formats."""
349
def get_default_format(klass):
350
"""Return the current default format."""
351
return klass._default_format
353
def get_format_string(self):
354
"""Return the ASCII format string that identifies this format.
356
Note that in pre format ?? repositories the format string is
357
not permitted nor written to disk.
359
raise NotImplementedError(self.get_format_string)
361
def initialize(self, a_bzrdir):
362
"""Create a weave repository.
364
TODO: when creating split out bzr branch formats, move this to a common
365
base for Format5, Format6. or something like that.
367
from bzrlib.weavefile import write_weave_v5
368
from bzrlib.weave import Weave
370
# Create an empty weave
372
bzrlib.weavefile.write_weave_v5(Weave(), sio)
373
empty_weave = sio.getvalue()
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)),
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)
388
for file, content in files:
389
control_files.put(file, content)
391
control_files.unlock()
392
return Repository(None, branch_format=None, _format=self, a_bzrdir=a_bzrdir)
394
def is_supported(self):
395
"""Is this format supported?
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.
403
def open(self, a_bzrdir, _found=False):
404
"""Return an instance of this format for the bzrdir a_bzrdir.
406
_found is a private parameter, do not use it.
409
raise NotImplementedError
410
return Repository(None, branch_format=None, _format=self, a_bzrdir=a_bzrdir)
413
def register_format(klass, format):
414
klass._formats[format.get_format_string()] = format
417
def set_default_format(klass, format):
418
klass._default_format = format
421
def unregister_format(klass, format):
422
assert klass._formats[format.get_format_string()] is format
423
del klass._formats[format.get_format_string()]
426
class RepositoryFormat4(RepositoryFormat):
427
"""Bzr repository format 4.
429
This repository format has:
431
- TextStores for texts, inventories,revisions.
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
439
super(RepositoryFormat4, self).__init__()
440
self._matchingbzrdir = bzrdir.BzrDirFormat4()
442
def initialize(self, url):
443
"""Format 4 branches cannot be created."""
444
raise errors.UninitializableFormat(self)
446
def is_supported(self):
447
"""Format 4 is not supported.
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
456
class RepositoryFormat5(RepositoryFormat):
457
"""Bzr control format 5.
459
This repository format has:
460
- weaves for file texts and inventory
462
- TextStores for revisions and signatures.
466
super(RepositoryFormat5, self).__init__()
467
self._matchingbzrdir = bzrdir.BzrDirFormat5()
470
class RepositoryFormat6(RepositoryFormat):
471
"""Bzr control format 6.
473
This repository format has:
474
- weaves for file texts and inventory
475
- hash subdirectory based stores.
476
- TextStores for revisions and signatures.
480
super(RepositoryFormat6, self).__init__()
481
self._matchingbzrdir = bzrdir.BzrDirFormat6()
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(),
493
class RepositoryTestProviderAdapter(object):
494
"""A tool to generate a suite testing multiple repository formats at once.
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.
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
507
def adapt(self, test):
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)