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 cStringIO import StringIO
19
from bzrlib.lockable_files import LockableFiles
20
from bzrlib.tree import EmptyTree
21
from bzrlib.revision import NULL_REVISION
22
from bzrlib.store import copy_all
23
from bzrlib.store.weave import WeaveStore
24
from bzrlib.store.text import TextStore
26
from bzrlib.tree import RevisionTree
27
from bzrlib.errors import InvalidRevisionId
28
from bzrlib.testament import Testament
29
from bzrlib.decorators import needs_read_lock, needs_write_lock
33
class Repository(object):
34
"""Repository holding history for one or more branches.
36
The repository holds and retrieves historical information including
37
revisions and file history. It's normally accessed only by the Branch,
38
which views a particular line of development through that history.
40
The Repository builds on top of Stores and a Transport, which respectively
41
describe the disk data format and the way of accessing the (possibly
45
def __init__(self, transport, branch_format):
47
self.control_files = LockableFiles(transport.clone(bzrlib.BZRDIR), 'README')
49
dir_mode = self.control_files._dir_mode
50
file_mode = self.control_files._file_mode
52
def get_weave(name, prefixed=False):
54
name = bzrlib.BZRDIR + '/' + unicode(name)
57
relpath = self.control_files._escape(name)
58
weave_transport = transport.clone(relpath)
59
ws = WeaveStore(weave_transport, prefixed=prefixed,
62
if self.control_files._transport.should_cache():
63
ws.enable_cache = True
66
def get_store(name, compressed=True, prefixed=False):
67
# FIXME: This approach of assuming stores are all entirely compressed
68
# or entirely uncompressed is tidy, but breaks upgrade from
69
# some existing branches where there's a mixture; we probably
70
# still want the option to look for both.
72
name = bzrlib.BZRDIR + '/' + unicode(name)
75
relpath = self.control_files._escape(name)
76
store = TextStore(transport.clone(relpath),
77
prefixed=prefixed, compressed=compressed,
80
#if self._transport.should_cache():
81
# cache_path = os.path.join(self.cache_root, name)
82
# os.mkdir(cache_path)
83
# store = bzrlib.store.CachedStore(store, cache_path)
86
if branch_format == 4:
87
self.inventory_store = get_store('inventory-store')
88
self.text_store = get_store('text-store')
89
self.revision_store = get_store('revision-store')
90
elif branch_format == 5:
91
self.control_weaves = get_weave('')
92
self.weave_store = get_weave('weaves')
93
self.revision_store = get_store('revision-store', compressed=False)
94
elif branch_format == 6:
95
self.control_weaves = get_weave('')
96
self.weave_store = get_weave('weaves', prefixed=True)
97
self.revision_store = get_store('revision-store', compressed=False,
99
self.revision_store.register_suffix('sig')
101
def lock_write(self):
102
self.control_files.lock_write()
105
self.control_files.lock_read()
108
self.control_files.unlock()
110
def copy(self, destination):
111
destination.control_weaves.copy_multi(self.control_weaves,
113
copy_all(self.weave_store, destination.weave_store)
114
copy_all(self.revision_store, destination.revision_store)
116
def has_revision(self, revision_id):
117
"""True if this branch has a copy of the revision.
119
This does not necessarily imply the revision is merge
120
or on the mainline."""
121
return (revision_id is None
122
or self.revision_store.has_id(revision_id))
125
def get_revision_xml_file(self, revision_id):
126
"""Return XML file object for revision object."""
127
if not revision_id or not isinstance(revision_id, basestring):
128
raise InvalidRevisionId(revision_id=revision_id, branch=self)
130
return self.revision_store.get(revision_id)
131
except (IndexError, KeyError):
132
raise bzrlib.errors.NoSuchRevision(self, revision_id)
134
def get_revision_xml(self, revision_id):
135
return self.get_revision_xml_file(revision_id).read()
137
def get_revision(self, revision_id):
138
"""Return the Revision object for a named revision"""
139
xml_file = self.get_revision_xml_file(revision_id)
142
r = bzrlib.xml5.serializer_v5.read_revision(xml_file)
143
except SyntaxError, e:
144
raise bzrlib.errors.BzrError('failed to unpack revision_xml',
148
assert r.revision_id == revision_id
151
def get_revision_sha1(self, revision_id):
152
"""Hash the stored value of a revision, and return it."""
153
# In the future, revision entries will be signed. At that
154
# point, it is probably best *not* to include the signature
155
# in the revision hash. Because that lets you re-sign
156
# the revision, (add signatures/remove signatures) and still
157
# have all hash pointers stay consistent.
158
# But for now, just hash the contents.
159
return bzrlib.osutils.sha_file(self.get_revision_xml_file(revision_id))
162
def store_revision_signature(self, gpg_strategy, plaintext, revision_id):
163
self.revision_store.add(StringIO(gpg_strategy.sign(plaintext)),
166
def get_inventory_weave(self):
167
return self.control_weaves.get_weave('inventory',
168
self.get_transaction())
170
def get_inventory(self, revision_id):
171
"""Get Inventory object by hash."""
172
xml = self.get_inventory_xml(revision_id)
173
return bzrlib.xml5.serializer_v5.read_inventory_from_string(xml)
175
def get_inventory_xml(self, revision_id):
176
"""Get inventory XML as a file object."""
178
assert isinstance(revision_id, basestring), type(revision_id)
179
iw = self.get_inventory_weave()
180
return iw.get_text(iw.lookup(revision_id))
182
raise bzrlib.errors.HistoryMissing(self, 'inventory', revision_id)
184
def get_inventory_sha1(self, revision_id):
185
"""Return the sha1 hash of the inventory entry
187
return self.get_revision(revision_id).inventory_sha1
189
def get_revision_inventory(self, revision_id):
190
"""Return inventory of a past revision."""
191
# TODO: Unify this with get_inventory()
192
# bzr 0.0.6 and later imposes the constraint that the inventory_id
193
# must be the same as its revision, so this is trivial.
194
if revision_id == None:
195
# This does not make sense: if there is no revision,
196
# then it is the current tree inventory surely ?!
197
# and thus get_root_id() is something that looks at the last
198
# commit on the branch, and the get_root_id is an inventory check.
199
raise NotImplementedError
200
# return Inventory(self.get_root_id())
202
return self.get_inventory(revision_id)
204
def revision_tree(self, revision_id):
205
"""Return Tree for a revision on this branch.
207
`revision_id` may be None for the null revision, in which case
208
an `EmptyTree` is returned."""
209
# TODO: refactor this to use an existing revision object
210
# so we don't need to read it in twice.
211
if revision_id == None or revision_id == NULL_REVISION:
214
inv = self.get_revision_inventory(revision_id)
215
return RevisionTree(self, inv, revision_id)
217
def get_ancestry(self, revision_id):
218
"""Return a list of revision-ids integrated by a revision.
220
This is topologically sorted.
222
if revision_id is None:
224
w = self.get_inventory_weave()
225
return [None] + map(w.idx_to_name,
226
w.inclusions([w.lookup(revision_id)]))
229
def print_file(self, file, revision_id):
230
"""Print `file` to stdout."""
231
tree = self.revision_tree(revision_id)
232
# use inventory as it was in that revision
233
file_id = tree.inventory.path2id(file)
235
raise BzrError("%r is not present in revision %s" % (file, revno))
237
revno = self.revision_id_to_revno(revision_id)
238
except errors.NoSuchRevision:
239
# TODO: This should not be BzrError,
240
# but NoSuchFile doesn't fit either
241
raise BzrError('%r is not present in revision %s'
242
% (file, revision_id))
244
raise BzrError('%r is not present in revision %s'
246
tree.print_file(file_id)
248
def get_transaction(self):
249
return self.control_files.get_transaction()
251
def sign_revision(self, revision_id, gpg_strategy):
252
plaintext = Testament.from_revision(self, revision_id).as_short_text()
253
self.store_revision_signature(gpg_strategy, plaintext, revision_id)