~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/repository.py

Dirty merge of the mainline

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 cStringIO import StringIO
 
18
 
 
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
 
25
import bzrlib.xml5
 
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
 
30
 
 
31
 
 
32
 
 
33
class Repository(object):
 
34
    """Repository holding history for one or more branches.
 
35
 
 
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.
 
39
 
 
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 
 
42
    remote) disk.
 
43
    """
 
44
 
 
45
    def __init__(self, transport, branch_format):
 
46
        object.__init__(self)
 
47
        self.control_files = LockableFiles(transport.clone(bzrlib.BZRDIR), 'README')
 
48
 
 
49
        dir_mode = self.control_files._dir_mode
 
50
        file_mode = self.control_files._file_mode
 
51
 
 
52
        def get_weave(name, prefixed=False):
 
53
            if name:
 
54
                name = bzrlib.BZRDIR + '/' + unicode(name)
 
55
            else:
 
56
                name = bzrlib.BZRDIR
 
57
            relpath = self.control_files._escape(name)
 
58
            weave_transport = transport.clone(relpath)
 
59
            ws = WeaveStore(weave_transport, prefixed=prefixed,
 
60
                            dir_mode=dir_mode,
 
61
                            file_mode=file_mode)
 
62
            if self.control_files._transport.should_cache():
 
63
                ws.enable_cache = True
 
64
            return ws
 
65
 
 
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.
 
71
            if name:
 
72
                name = bzrlib.BZRDIR + '/' + unicode(name)
 
73
            else:
 
74
                name = bzrlib.BZRDIR
 
75
            relpath = self.control_files._escape(name)
 
76
            store = TextStore(transport.clone(relpath),
 
77
                              prefixed=prefixed, compressed=compressed,
 
78
                              dir_mode=dir_mode,
 
79
                              file_mode=file_mode)
 
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)
 
84
            return store
 
85
 
 
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,
 
98
                                            prefixed=True)
 
99
        self.revision_store.register_suffix('sig')
 
100
 
 
101
    def lock_write(self):
 
102
        self.control_files.lock_write()
 
103
 
 
104
    def lock_read(self):
 
105
        self.control_files.lock_read()
 
106
 
 
107
    def unlock(self):
 
108
        self.control_files.unlock()
 
109
 
 
110
    @needs_read_lock
 
111
    def copy(self, destination):
 
112
        destination.lock_write()
 
113
        try:
 
114
            destination.control_weaves.copy_multi(self.control_weaves, 
 
115
                ['inventory'])
 
116
            copy_all(self.weave_store, destination.weave_store)
 
117
            copy_all(self.revision_store, destination.revision_store)
 
118
        finally:
 
119
            destination.unlock()
 
120
 
 
121
    def has_revision(self, revision_id):
 
122
        """True if this branch has a copy of the revision.
 
123
 
 
124
        This does not necessarily imply the revision is merge
 
125
        or on the mainline."""
 
126
        return (revision_id is None
 
127
                or self.revision_store.has_id(revision_id))
 
128
 
 
129
    @needs_read_lock
 
130
    def get_revision_xml_file(self, revision_id):
 
131
        """Return XML file object for revision object."""
 
132
        if not revision_id or not isinstance(revision_id, basestring):
 
133
            raise InvalidRevisionId(revision_id=revision_id, branch=self)
 
134
        try:
 
135
            return self.revision_store.get(revision_id)
 
136
        except (IndexError, KeyError):
 
137
            raise bzrlib.errors.NoSuchRevision(self, revision_id)
 
138
 
 
139
    @needs_read_lock
 
140
    def get_revision_xml(self, revision_id):
 
141
        return self.get_revision_xml_file(revision_id).read()
 
142
 
 
143
    @needs_read_lock
 
144
    def get_revision(self, revision_id):
 
145
        """Return the Revision object for a named revision"""
 
146
        xml_file = self.get_revision_xml_file(revision_id)
 
147
 
 
148
        try:
 
149
            r = bzrlib.xml5.serializer_v5.read_revision(xml_file)
 
150
        except SyntaxError, e:
 
151
            raise bzrlib.errors.BzrError('failed to unpack revision_xml',
 
152
                                         [revision_id,
 
153
                                          str(e)])
 
154
            
 
155
        assert r.revision_id == revision_id
 
156
        return r
 
157
 
 
158
    @needs_read_lock
 
159
    def get_revision_sha1(self, revision_id):
 
160
        """Hash the stored value of a revision, and return it."""
 
161
        # In the future, revision entries will be signed. At that
 
162
        # point, it is probably best *not* to include the signature
 
163
        # in the revision hash. Because that lets you re-sign
 
164
        # the revision, (add signatures/remove signatures) and still
 
165
        # have all hash pointers stay consistent.
 
166
        # But for now, just hash the contents.
 
167
        return bzrlib.osutils.sha_file(self.get_revision_xml_file(revision_id))
 
168
 
 
169
    @needs_write_lock
 
170
    def store_revision_signature(self, gpg_strategy, plaintext, revision_id):
 
171
        self.revision_store.add(StringIO(gpg_strategy.sign(plaintext)), 
 
172
                                revision_id, "sig")
 
173
 
 
174
    @needs_read_lock
 
175
    def get_inventory_weave(self):
 
176
        return self.control_weaves.get_weave('inventory',
 
177
            self.get_transaction())
 
178
 
 
179
    @needs_read_lock
 
180
    def get_inventory(self, revision_id):
 
181
        """Get Inventory object by hash."""
 
182
        xml = self.get_inventory_xml(revision_id)
 
183
        return bzrlib.xml5.serializer_v5.read_inventory_from_string(xml)
 
184
 
 
185
    @needs_read_lock
 
186
    def get_inventory_xml(self, revision_id):
 
187
        """Get inventory XML as a file object."""
 
188
        try:
 
189
            assert isinstance(revision_id, basestring), type(revision_id)
 
190
            iw = self.get_inventory_weave()
 
191
            return iw.get_text(iw.lookup(revision_id))
 
192
        except IndexError:
 
193
            raise bzrlib.errors.HistoryMissing(self, 'inventory', revision_id)
 
194
 
 
195
    @needs_read_lock
 
196
    def get_inventory_sha1(self, revision_id):
 
197
        """Return the sha1 hash of the inventory entry
 
198
        """
 
199
        return self.get_revision(revision_id).inventory_sha1
 
200
 
 
201
    @needs_read_lock
 
202
    def get_revision_inventory(self, revision_id):
 
203
        """Return inventory of a past revision."""
 
204
        # TODO: Unify this with get_inventory()
 
205
        # bzr 0.0.6 and later imposes the constraint that the inventory_id
 
206
        # must be the same as its revision, so this is trivial.
 
207
        if revision_id == None:
 
208
            # This does not make sense: if there is no revision,
 
209
            # then it is the current tree inventory surely ?!
 
210
            # and thus get_root_id() is something that looks at the last
 
211
            # commit on the branch, and the get_root_id is an inventory check.
 
212
            raise NotImplementedError
 
213
            # return Inventory(self.get_root_id())
 
214
        else:
 
215
            return self.get_inventory(revision_id)
 
216
 
 
217
    @needs_read_lock
 
218
    def revision_tree(self, revision_id):
 
219
        """Return Tree for a revision on this branch.
 
220
 
 
221
        `revision_id` may be None for the null revision, in which case
 
222
        an `EmptyTree` is returned."""
 
223
        # TODO: refactor this to use an existing revision object
 
224
        # so we don't need to read it in twice.
 
225
        if revision_id == None or revision_id == NULL_REVISION:
 
226
            return EmptyTree()
 
227
        else:
 
228
            inv = self.get_revision_inventory(revision_id)
 
229
            return RevisionTree(self, inv, revision_id)
 
230
 
 
231
    @needs_read_lock
 
232
    def get_ancestry(self, revision_id):
 
233
        """Return a list of revision-ids integrated by a revision.
 
234
        
 
235
        This is topologically sorted.
 
236
        """
 
237
        if revision_id is None:
 
238
            return [None]
 
239
        w = self.get_inventory_weave()
 
240
        return [None] + map(w.idx_to_name,
 
241
                            w.inclusions([w.lookup(revision_id)]))
 
242
 
 
243
    @needs_read_lock
 
244
    def print_file(self, file, revision_id):
 
245
        """Print `file` to stdout.
 
246
        
 
247
        FIXME RBC 20060125 as John Meinel points out this is a bad api
 
248
        - it writes to stdout, it assumes that that is valid etc. Fix
 
249
        by creating a new more flexible convenience function.
 
250
        """
 
251
        tree = self.revision_tree(revision_id)
 
252
        # use inventory as it was in that revision
 
253
        file_id = tree.inventory.path2id(file)
 
254
        if not file_id:
 
255
            raise BzrError("%r is not present in revision %s" % (file, revno))
 
256
            try:
 
257
                revno = self.revision_id_to_revno(revision_id)
 
258
            except errors.NoSuchRevision:
 
259
                # TODO: This should not be BzrError,
 
260
                # but NoSuchFile doesn't fit either
 
261
                raise BzrError('%r is not present in revision %s' 
 
262
                                % (file, revision_id))
 
263
            else:
 
264
                raise BzrError('%r is not present in revision %s'
 
265
                                % (file, revno))
 
266
        tree.print_file(file_id)
 
267
 
 
268
    def get_transaction(self):
 
269
        return self.control_files.get_transaction()
 
270
 
 
271
    @needs_write_lock
 
272
    def sign_revision(self, revision_id, gpg_strategy):
 
273
        plaintext = Testament.from_revision(self, revision_id).as_short_text()
 
274
        self.store_revision_signature(gpg_strategy, plaintext, revision_id)