/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

Fixed dangling inventory ids in revert

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
import xml.sax.saxutils
 
21
 
 
22
 
 
23
import bzrlib.bzrdir as bzrdir
 
24
from bzrlib.decorators import needs_read_lock, needs_write_lock
 
25
import bzrlib.errors as errors
 
26
from bzrlib.errors import InvalidRevisionId
 
27
from bzrlib.lockable_files import LockableFiles
 
28
from bzrlib.osutils import safe_unicode
 
29
from bzrlib.revision import NULL_REVISION
 
30
from bzrlib.store import copy_all
 
31
from bzrlib.store.weave import WeaveStore
 
32
from bzrlib.store.text import TextStore
 
33
from bzrlib.symbol_versioning import *
 
34
from bzrlib.trace import mutter
 
35
from bzrlib.tree import RevisionTree
 
36
from bzrlib.testament import Testament
 
37
from bzrlib.tree import EmptyTree
 
38
import bzrlib.ui
 
39
import bzrlib.xml5
 
40
 
 
41
 
 
42
class Repository(object):
 
43
    """Repository holding history for one or more branches.
 
44
 
 
45
    The repository holds and retrieves historical information including
 
46
    revisions and file history.  It's normally accessed only by the Branch,
 
47
    which views a particular line of development through that history.
 
48
 
 
49
    The Repository builds on top of Stores and a Transport, which respectively 
 
50
    describe the disk data format and the way of accessing the (possibly 
 
51
    remote) disk.
 
52
    """
 
53
 
 
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, transport, branch_format, _format=None, a_bzrdir=None):
 
88
        object.__init__(self)
 
89
        if transport is not None:
 
90
            warn("Repository.__init__(..., transport=XXX): The transport parameter is "
 
91
                 "deprecated and was never in a supported release. Please use "
 
92
                 "bzrdir.open_repository() or bzrdir.open_branch().repository.",
 
93
                 DeprecationWarning,
 
94
                 stacklevel=2)
 
95
            self.control_files = LockableFiles(transport.clone(bzrlib.BZRDIR), 'README')
 
96
        else: 
 
97
            # TODO: clone into repository if needed
 
98
            self.control_files = LockableFiles(a_bzrdir.get_repository_transport(None), 'README')
 
99
 
 
100
        dir_mode = self.control_files._dir_mode
 
101
        file_mode = self.control_files._file_mode
 
102
        self._format = _format
 
103
        self.bzrdir = a_bzrdir
 
104
 
 
105
        def get_weave(name, prefixed=False):
 
106
            if name:
 
107
                name = safe_unicode(name)
 
108
            else:
 
109
                name = ''
 
110
            relpath = self.control_files._escape(name)
 
111
            weave_transport = self.control_files._transport.clone(relpath)
 
112
            ws = WeaveStore(weave_transport, prefixed=prefixed,
 
113
                            dir_mode=dir_mode,
 
114
                            file_mode=file_mode)
 
115
            if self.control_files._transport.should_cache():
 
116
                ws.enable_cache = True
 
117
            return ws
 
118
 
 
119
 
 
120
        def get_store(name, compressed=True, prefixed=False):
 
121
            # FIXME: This approach of assuming stores are all entirely compressed
 
122
            # or entirely uncompressed is tidy, but breaks upgrade from 
 
123
            # some existing branches where there's a mixture; we probably 
 
124
            # still want the option to look for both.
 
125
            if name:
 
126
                name = safe_unicode(name)
 
127
            else:
 
128
                name = ''
 
129
            relpath = self.control_files._escape(name)
 
130
            store = TextStore(self.control_files._transport.clone(relpath),
 
131
                              prefixed=prefixed, compressed=compressed,
 
132
                              dir_mode=dir_mode,
 
133
                              file_mode=file_mode)
 
134
            #if self._transport.should_cache():
 
135
            #    cache_path = os.path.join(self.cache_root, name)
 
136
            #    os.mkdir(cache_path)
 
137
            #    store = bzrlib.store.CachedStore(store, cache_path)
 
138
            return store
 
139
 
 
140
        if branch_format is not None:
 
141
            # circular dependencies:
 
142
            from bzrlib.branch import (BzrBranchFormat4,
 
143
                                       BzrBranchFormat5,
 
144
                                       BzrBranchFormat6,
 
145
                                       )
 
146
            if isinstance(branch_format, BzrBranchFormat4):
 
147
                self._format = RepositoryFormat4()
 
148
            elif isinstance(branch_format, BzrBranchFormat5):
 
149
                self._format = RepositoryFormat5()
 
150
            elif isinstance(branch_format, BzrBranchFormat6):
 
151
                self._format = RepositoryFormat6()
 
152
            
 
153
 
 
154
        if isinstance(self._format, RepositoryFormat4):
 
155
            self.inventory_store = get_store('inventory-store')
 
156
            self.text_store = get_store('text-store')
 
157
            self.revision_store = get_store('revision-store')
 
158
        elif isinstance(self._format, RepositoryFormat5):
 
159
            self.control_weaves = get_weave('')
 
160
            self.weave_store = get_weave('weaves')
 
161
            self.revision_store = get_store('revision-store', compressed=False)
 
162
        elif isinstance(self._format, RepositoryFormat6):
 
163
            self.control_weaves = get_weave('')
 
164
            self.weave_store = get_weave('weaves', prefixed=True)
 
165
            self.revision_store = get_store('revision-store', compressed=False,
 
166
                                            prefixed=True)
 
167
        elif isinstance(self._format, RepositoryFormat7):
 
168
            self.control_weaves = get_weave('')
 
169
            self.weave_store = get_weave('weaves', prefixed=True)
 
170
            self.revision_store = get_store('revision-store', compressed=False,
 
171
                                            prefixed=True)
 
172
        self.revision_store.register_suffix('sig')
 
173
 
 
174
    def lock_write(self):
 
175
        self.control_files.lock_write()
 
176
 
 
177
    def lock_read(self):
 
178
        self.control_files.lock_read()
 
179
 
 
180
    @needs_read_lock
 
181
    def missing_revision_ids(self, other, revision_id=None):
 
182
        """Return the revision ids that other has that this does not.
 
183
        
 
184
        These are returned in topological order.
 
185
 
 
186
        revision_id: only return revision ids included by revision_id.
 
187
        """
 
188
        if self._compatible_formats(other):
 
189
            # fast path for weave-inventory based stores.
 
190
            # we want all revisions to satisft revision_id in other.
 
191
            # but we dont want to stat every file here and there.
 
192
            # we want then, all revisions other needs to satisfy revision_id 
 
193
            # checked, but not those that we have locally.
 
194
            # so the first thing is to get a subset of the revisions to 
 
195
            # satisfy revision_id in other, and then eliminate those that
 
196
            # we do already have. 
 
197
            # this is slow on high latency connection to self, but as as this
 
198
            # disk format scales terribly for push anyway due to rewriting 
 
199
            # inventory.weave, this is considered acceptable.
 
200
            # - RBC 20060209
 
201
            if revision_id is not None:
 
202
                other_ids = other.get_ancestry(revision_id)
 
203
                assert other_ids.pop(0) == None
 
204
            else:
 
205
                other_ids = other._all_possible_ids()
 
206
            other_ids_set = set(other_ids)
 
207
            # other ids is the worst case to pull now.
 
208
            # now we want to filter other_ids against what we actually
 
209
            # have, but dont try to stat what we know we dont.
 
210
            my_ids = set(self._all_possible_ids())
 
211
            possibly_present_revisions = my_ids.intersection(other_ids_set)
 
212
            actually_present_revisions = set(self._eliminate_revisions_not_present(possibly_present_revisions))
 
213
            required_revisions = other_ids_set.difference(actually_present_revisions)
 
214
            required_topo_revisions = [rev_id for rev_id in other_ids if rev_id in required_revisions]
 
215
            if revision_id is not None:
 
216
                # we used get_ancestry to determine other_ids then we are assured all
 
217
                # revisions referenced are present as they are installed in topological order.
 
218
                return required_topo_revisions
 
219
            else:
 
220
                # we only have an estimate of whats available
 
221
                return other._eliminate_revisions_not_present(required_topo_revisions)
 
222
        # slow code path.
 
223
        my_ids = set(self.all_revision_ids())
 
224
        if revision_id is not None:
 
225
            other_ids = other.get_ancestry(revision_id)
 
226
            assert other_ids.pop(0) == None
 
227
        else:
 
228
            other_ids = other.all_revision_ids()
 
229
        result_set = set(other_ids).difference(my_ids)
 
230
        return [rev_id for rev_id in other_ids if rev_id in result_set]
 
231
 
 
232
    @staticmethod
 
233
    def open(base):
 
234
        """Open the repository rooted at base.
 
235
 
 
236
        For instance, if the repository is at URL/.bzr/repository,
 
237
        Repository.open(URL) -> a Repository instance.
 
238
        """
 
239
        control = bzrdir.BzrDir.open(base)
 
240
        return control.open_repository()
 
241
 
 
242
    def _compatible_formats(self, other):
 
243
        """Return True if the stores in self and other are 'compatible'
 
244
        
 
245
        'compatible' means that they are both the same underlying type
 
246
        i.e. both weave stores, or both knits and thus support fast-path
 
247
        operations."""
 
248
        return (isinstance(self._format, (RepositoryFormat5,
 
249
                                          RepositoryFormat6,
 
250
                                          RepositoryFormat7)) and
 
251
                isinstance(other._format, (RepositoryFormat5,
 
252
                                           RepositoryFormat6,
 
253
                                           RepositoryFormat7)))
 
254
 
 
255
    @needs_read_lock
 
256
    def copy_content_into(self, destination, revision_id=None, basis=None):
 
257
        """Make a complete copy of the content in self into destination."""
 
258
        destination.lock_write()
 
259
        try:
 
260
            # optimised paths:
 
261
            # compatible stores
 
262
            if self._compatible_formats(destination):
 
263
                if basis is not None:
 
264
                    # copy the basis in, then fetch remaining data.
 
265
                    basis.copy_content_into(destination, revision_id)
 
266
                    destination.fetch(self, revision_id=revision_id)
 
267
                else:
 
268
                    # FIXME do not peek!
 
269
                    if self.control_files._transport.listable():
 
270
                        pb = bzrlib.ui.ui_factory.progress_bar()
 
271
                        copy_all(self.weave_store,
 
272
                            destination.weave_store, pb=pb)
 
273
                        pb.update('copying inventory', 0, 1)
 
274
                        destination.control_weaves.copy_multi(
 
275
                            self.control_weaves, ['inventory'])
 
276
                        copy_all(self.revision_store,
 
277
                            destination.revision_store, pb=pb)
 
278
                    else:
 
279
                        destination.fetch(self, revision_id=revision_id)
 
280
            # compatible v4 stores
 
281
            elif isinstance(self._format, RepositoryFormat4):
 
282
                if not isinstance(destination._format, RepositoryFormat4):
 
283
                    raise BzrError('cannot copy v4 branches to anything other than v4 branches.')
 
284
                store_pairs = ((self.text_store,      destination.text_store),
 
285
                               (self.inventory_store, destination.inventory_store),
 
286
                               (self.revision_store,  destination.revision_store))
 
287
                try:
 
288
                    for from_store, to_store in store_pairs: 
 
289
                        copy_all(from_store, to_store)
 
290
                except UnlistableStore:
 
291
                    raise UnlistableBranch(from_store)
 
292
            # fallback - 'fetch'
 
293
            else:
 
294
                destination.fetch(self, revision_id=revision_id)
 
295
        finally:
 
296
            destination.unlock()
 
297
 
 
298
    @needs_write_lock
 
299
    def fetch(self, source, revision_id=None):
 
300
        """Fetch the content required to construct revision_id from source.
 
301
 
 
302
        If revision_id is None all content is copied.
 
303
        """
 
304
        from bzrlib.fetch import RepoFetcher
 
305
        mutter("Using fetch logic to copy between %s(%s) and %s(%s)",
 
306
               source, source._format, self, self._format)
 
307
        RepoFetcher(to_repository=self, from_repository=source, last_revision=revision_id)
 
308
 
 
309
    def unlock(self):
 
310
        self.control_files.unlock()
 
311
 
 
312
    @needs_read_lock
 
313
    def clone(self, a_bzrdir, revision_id=None, basis=None):
 
314
        """Clone this repository into a_bzrdir using the current format.
 
315
 
 
316
        Currently no check is made that the format of this repository and
 
317
        the bzrdir format are compatible. FIXME RBC 20060201.
 
318
        """
 
319
        if not isinstance(a_bzrdir._format, self.bzrdir._format.__class__):
 
320
            # use target default format.
 
321
            result = a_bzrdir.create_repository()
 
322
        # FIXME RBC 20060209 split out the repository type to avoid this check ?
 
323
        elif isinstance(a_bzrdir._format,
 
324
                      (bzrdir.BzrDirFormat4,
 
325
                       bzrdir.BzrDirFormat5,
 
326
                       bzrdir.BzrDirFormat6)):
 
327
            result = a_bzrdir.open_repository()
 
328
        else:
 
329
            result = self._format.initialize(a_bzrdir)
 
330
        self.copy_content_into(result, revision_id, basis)
 
331
        return result
 
332
 
 
333
    def has_revision(self, revision_id):
 
334
        """True if this branch has a copy of the revision.
 
335
 
 
336
        This does not necessarily imply the revision is merge
 
337
        or on the mainline."""
 
338
        return (revision_id is None
 
339
                or self.revision_store.has_id(revision_id))
 
340
 
 
341
    @needs_read_lock
 
342
    def get_revision_xml_file(self, revision_id):
 
343
        """Return XML file object for revision object."""
 
344
        if not revision_id or not isinstance(revision_id, basestring):
 
345
            raise InvalidRevisionId(revision_id=revision_id, branch=self)
 
346
        try:
 
347
            return self.revision_store.get(revision_id)
 
348
        except (IndexError, KeyError):
 
349
            raise bzrlib.errors.NoSuchRevision(self, revision_id)
 
350
 
 
351
    @needs_read_lock
 
352
    def get_revision_xml(self, revision_id):
 
353
        return self.get_revision_xml_file(revision_id).read()
 
354
 
 
355
    @needs_read_lock
 
356
    def get_revision(self, revision_id):
 
357
        """Return the Revision object for a named revision"""
 
358
        xml_file = self.get_revision_xml_file(revision_id)
 
359
 
 
360
        try:
 
361
            r = bzrlib.xml5.serializer_v5.read_revision(xml_file)
 
362
        except SyntaxError, e:
 
363
            raise bzrlib.errors.BzrError('failed to unpack revision_xml',
 
364
                                         [revision_id,
 
365
                                          str(e)])
 
366
            
 
367
        assert r.revision_id == revision_id
 
368
        return r
 
369
 
 
370
    @needs_read_lock
 
371
    def get_revision_sha1(self, revision_id):
 
372
        """Hash the stored value of a revision, and return it."""
 
373
        # In the future, revision entries will be signed. At that
 
374
        # point, it is probably best *not* to include the signature
 
375
        # in the revision hash. Because that lets you re-sign
 
376
        # the revision, (add signatures/remove signatures) and still
 
377
        # have all hash pointers stay consistent.
 
378
        # But for now, just hash the contents.
 
379
        return bzrlib.osutils.sha_file(self.get_revision_xml_file(revision_id))
 
380
 
 
381
    @needs_write_lock
 
382
    def store_revision_signature(self, gpg_strategy, plaintext, revision_id):
 
383
        self.revision_store.add(StringIO(gpg_strategy.sign(plaintext)), 
 
384
                                revision_id, "sig")
 
385
 
 
386
    def fileid_involved_between_revs(self, from_revid, to_revid):
 
387
        """Find file_id(s) which are involved in the changes between revisions.
 
388
 
 
389
        This determines the set of revisions which are involved, and then
 
390
        finds all file ids affected by those revisions.
 
391
        """
 
392
        # TODO: jam 20060119 This code assumes that w.inclusions will
 
393
        #       always be correct. But because of the presence of ghosts
 
394
        #       it is possible to be wrong.
 
395
        #       One specific example from Robert Collins:
 
396
        #       Two branches, with revisions ABC, and AD
 
397
        #       C is a ghost merge of D.
 
398
        #       Inclusions doesn't recognize D as an ancestor.
 
399
        #       If D is ever merged in the future, the weave
 
400
        #       won't be fixed, because AD never saw revision C
 
401
        #       to cause a conflict which would force a reweave.
 
402
        w = self.get_inventory_weave()
 
403
        from_set = set(w.inclusions([w.lookup(from_revid)]))
 
404
        to_set = set(w.inclusions([w.lookup(to_revid)]))
 
405
        included = to_set.difference(from_set)
 
406
        changed = map(w.idx_to_name, included)
 
407
        return self._fileid_involved_by_set(changed)
 
408
 
 
409
    def fileid_involved(self, last_revid=None):
 
410
        """Find all file_ids modified in the ancestry of last_revid.
 
411
 
 
412
        :param last_revid: If None, last_revision() will be used.
 
413
        """
 
414
        w = self.get_inventory_weave()
 
415
        if not last_revid:
 
416
            changed = set(w._names)
 
417
        else:
 
418
            included = w.inclusions([w.lookup(last_revid)])
 
419
            changed = map(w.idx_to_name, included)
 
420
        return self._fileid_involved_by_set(changed)
 
421
 
 
422
    def fileid_involved_by_set(self, changes):
 
423
        """Find all file_ids modified by the set of revisions passed in.
 
424
 
 
425
        :param changes: A set() of revision ids
 
426
        """
 
427
        # TODO: jam 20060119 This line does *nothing*, remove it.
 
428
        #       or better yet, change _fileid_involved_by_set so
 
429
        #       that it takes the inventory weave, rather than
 
430
        #       pulling it out by itself.
 
431
        return self._fileid_involved_by_set(changes)
 
432
 
 
433
    def _fileid_involved_by_set(self, changes):
 
434
        """Find the set of file-ids affected by the set of revisions.
 
435
 
 
436
        :param changes: A set() of revision ids.
 
437
        :return: A set() of file ids.
 
438
        
 
439
        This peaks at the Weave, interpreting each line, looking to
 
440
        see if it mentions one of the revisions. And if so, includes
 
441
        the file id mentioned.
 
442
        This expects both the Weave format, and the serialization
 
443
        to have a single line per file/directory, and to have
 
444
        fileid="" and revision="" on that line.
 
445
        """
 
446
        assert isinstance(self._format, (RepositoryFormat5,
 
447
                                         RepositoryFormat6,
 
448
                                         RepositoryFormat7)), \
 
449
            "fileid_involved only supported for branches which store inventory as unnested xml"
 
450
 
 
451
        w = self.get_inventory_weave()
 
452
        file_ids = set()
 
453
        for line in w._weave:
 
454
 
 
455
            # it is ugly, but it is due to the weave structure
 
456
            if not isinstance(line, basestring): continue
 
457
 
 
458
            start = line.find('file_id="')+9
 
459
            if start < 9: continue
 
460
            end = line.find('"', start)
 
461
            assert end>= 0
 
462
            file_id = xml.sax.saxutils.unescape(line[start:end])
 
463
 
 
464
            # check if file_id is already present
 
465
            if file_id in file_ids: continue
 
466
 
 
467
            start = line.find('revision="')+10
 
468
            if start < 10: continue
 
469
            end = line.find('"', start)
 
470
            assert end>= 0
 
471
            revision_id = xml.sax.saxutils.unescape(line[start:end])
 
472
 
 
473
            if revision_id in changes:
 
474
                file_ids.add(file_id)
 
475
        return file_ids
 
476
 
 
477
    @needs_read_lock
 
478
    def get_inventory_weave(self):
 
479
        return self.control_weaves.get_weave('inventory',
 
480
            self.get_transaction())
 
481
 
 
482
    @needs_read_lock
 
483
    def get_inventory(self, revision_id):
 
484
        """Get Inventory object by hash."""
 
485
        xml = self.get_inventory_xml(revision_id)
 
486
        return bzrlib.xml5.serializer_v5.read_inventory_from_string(xml)
 
487
 
 
488
    @needs_read_lock
 
489
    def get_inventory_xml(self, revision_id):
 
490
        """Get inventory XML as a file object."""
 
491
        try:
 
492
            assert isinstance(revision_id, basestring), type(revision_id)
 
493
            iw = self.get_inventory_weave()
 
494
            return iw.get_text(iw.lookup(revision_id))
 
495
        except IndexError:
 
496
            raise bzrlib.errors.HistoryMissing(self, 'inventory', revision_id)
 
497
 
 
498
    @needs_read_lock
 
499
    def get_inventory_sha1(self, revision_id):
 
500
        """Return the sha1 hash of the inventory entry
 
501
        """
 
502
        return self.get_revision(revision_id).inventory_sha1
 
503
 
 
504
    @needs_read_lock
 
505
    def get_revision_inventory(self, revision_id):
 
506
        """Return inventory of a past revision."""
 
507
        # TODO: Unify this with get_inventory()
 
508
        # bzr 0.0.6 and later imposes the constraint that the inventory_id
 
509
        # must be the same as its revision, so this is trivial.
 
510
        if revision_id is None:
 
511
            # This does not make sense: if there is no revision,
 
512
            # then it is the current tree inventory surely ?!
 
513
            # and thus get_root_id() is something that looks at the last
 
514
            # commit on the branch, and the get_root_id is an inventory check.
 
515
            raise NotImplementedError
 
516
            # return Inventory(self.get_root_id())
 
517
        else:
 
518
            return self.get_inventory(revision_id)
 
519
 
 
520
    @needs_read_lock
 
521
    def revision_tree(self, revision_id):
 
522
        """Return Tree for a revision on this branch.
 
523
 
 
524
        `revision_id` may be None for the null revision, in which case
 
525
        an `EmptyTree` is returned."""
 
526
        # TODO: refactor this to use an existing revision object
 
527
        # so we don't need to read it in twice.
 
528
        if revision_id is None or revision_id == NULL_REVISION:
 
529
            return EmptyTree()
 
530
        else:
 
531
            inv = self.get_revision_inventory(revision_id)
 
532
            return RevisionTree(self, inv, revision_id)
 
533
 
 
534
    @needs_read_lock
 
535
    def get_ancestry(self, revision_id):
 
536
        """Return a list of revision-ids integrated by a revision.
 
537
        
 
538
        This is topologically sorted.
 
539
        """
 
540
        if revision_id is None:
 
541
            return [None]
 
542
        if not self.has_revision(revision_id):
 
543
            raise errors.NoSuchRevision(self, revision_id)
 
544
        w = self.get_inventory_weave()
 
545
        return [None] + map(w.idx_to_name,
 
546
                            w.inclusions([w.lookup(revision_id)]))
 
547
 
 
548
    @needs_read_lock
 
549
    def print_file(self, file, revision_id):
 
550
        """Print `file` to stdout.
 
551
        
 
552
        FIXME RBC 20060125 as John Meinel points out this is a bad api
 
553
        - it writes to stdout, it assumes that that is valid etc. Fix
 
554
        by creating a new more flexible convenience function.
 
555
        """
 
556
        tree = self.revision_tree(revision_id)
 
557
        # use inventory as it was in that revision
 
558
        file_id = tree.inventory.path2id(file)
 
559
        if not file_id:
 
560
            raise BzrError("%r is not present in revision %s" % (file, revno))
 
561
            try:
 
562
                revno = self.revision_id_to_revno(revision_id)
 
563
            except errors.NoSuchRevision:
 
564
                # TODO: This should not be BzrError,
 
565
                # but NoSuchFile doesn't fit either
 
566
                raise BzrError('%r is not present in revision %s' 
 
567
                                % (file, revision_id))
 
568
            else:
 
569
                raise BzrError('%r is not present in revision %s'
 
570
                                % (file, revno))
 
571
        tree.print_file(file_id)
 
572
 
 
573
    def get_transaction(self):
 
574
        return self.control_files.get_transaction()
 
575
 
 
576
    @needs_write_lock
 
577
    def sign_revision(self, revision_id, gpg_strategy):
 
578
        plaintext = Testament.from_revision(self, revision_id).as_short_text()
 
579
        self.store_revision_signature(gpg_strategy, plaintext, revision_id)
 
580
 
 
581
 
 
582
class RepositoryFormat(object):
 
583
    """A repository format.
 
584
 
 
585
    Formats provide three things:
 
586
     * An initialization routine to construct repository data on disk.
 
587
     * a format string which is used when the BzrDir supports versioned
 
588
       children.
 
589
     * an open routine which returns a Repository instance.
 
590
 
 
591
    Formats are placed in an dict by their format string for reference 
 
592
    during opening. These should be subclasses of RepositoryFormat
 
593
    for consistency.
 
594
 
 
595
    Once a format is deprecated, just deprecate the initialize and open
 
596
    methods on the format class. Do not deprecate the object, as the 
 
597
    object will be created every system load.
 
598
 
 
599
    Common instance attributes:
 
600
    _matchingbzrdir - the bzrdir format that the repository format was
 
601
    originally written to work with. This can be used if manually
 
602
    constructing a bzrdir and repository, or more commonly for test suite
 
603
    parameterisation.
 
604
    """
 
605
 
 
606
    _default_format = None
 
607
    """The default format used for new repositories."""
 
608
 
 
609
    _formats = {}
 
610
    """The known formats."""
 
611
 
 
612
    @classmethod
 
613
    def find_format(klass, a_bzrdir):
 
614
        """Return the format for the repository object in a_bzrdir."""
 
615
        try:
 
616
            transport = a_bzrdir.get_repository_transport(None)
 
617
            format_string = transport.get("format").read()
 
618
            return klass._formats[format_string]
 
619
        except errors.NoSuchFile:
 
620
            raise errors.NoRepositoryPresent(a_bzrdir)
 
621
        except KeyError:
 
622
            raise errors.UnknownFormatError(format_string)
 
623
 
 
624
    @classmethod
 
625
    def get_default_format(klass):
 
626
        """Return the current default format."""
 
627
        return klass._default_format
 
628
 
 
629
    def get_format_string(self):
 
630
        """Return the ASCII format string that identifies this format.
 
631
        
 
632
        Note that in pre format ?? repositories the format string is 
 
633
        not permitted nor written to disk.
 
634
        """
 
635
        raise NotImplementedError(self.get_format_string)
 
636
 
 
637
    def initialize(self, a_bzrdir, _internal=False):
 
638
        """Create a weave repository.
 
639
        
 
640
        TODO: when creating split out bzr branch formats, move this to a common
 
641
        base for Format5, Format6. or something like that.
 
642
        """
 
643
        from bzrlib.weavefile import write_weave_v5
 
644
        from bzrlib.weave import Weave
 
645
 
 
646
        if not _internal:
 
647
            # always initialized when the bzrdir is.
 
648
            return Repository(None, branch_format=None, _format=self, a_bzrdir=a_bzrdir)
 
649
        
 
650
        # Create an empty weave
 
651
        sio = StringIO()
 
652
        bzrlib.weavefile.write_weave_v5(Weave(), sio)
 
653
        empty_weave = sio.getvalue()
 
654
 
 
655
        mutter('creating repository in %s.', a_bzrdir.transport.base)
 
656
        dirs = ['revision-store', 'weaves']
 
657
        lock_file = 'branch-lock'
 
658
        files = [('inventory.weave', StringIO(empty_weave)), 
 
659
                 ]
 
660
        
 
661
        # FIXME: RBC 20060125 dont peek under the covers
 
662
        # NB: no need to escape relative paths that are url safe.
 
663
        control_files = LockableFiles(a_bzrdir.transport, 'branch-lock')
 
664
        control_files.lock_write()
 
665
        control_files._transport.mkdir_multi(dirs,
 
666
                mode=control_files._dir_mode)
 
667
        try:
 
668
            for file, content in files:
 
669
                control_files.put(file, content)
 
670
        finally:
 
671
            control_files.unlock()
 
672
        return Repository(None, branch_format=None, _format=self, a_bzrdir=a_bzrdir)
 
673
 
 
674
    def is_supported(self):
 
675
        """Is this format supported?
 
676
 
 
677
        Supported formats must be initializable and openable.
 
678
        Unsupported formats may not support initialization or committing or 
 
679
        some other features depending on the reason for not being supported.
 
680
        """
 
681
        return True
 
682
 
 
683
    def open(self, a_bzrdir, _found=False):
 
684
        """Return an instance of this format for the bzrdir a_bzrdir.
 
685
        
 
686
        _found is a private parameter, do not use it.
 
687
        """
 
688
        if not _found:
 
689
            # we are being called directly and must probe.
 
690
            raise NotImplementedError
 
691
        return Repository(None, branch_format=None, _format=self, a_bzrdir=a_bzrdir)
 
692
 
 
693
    @classmethod
 
694
    def register_format(klass, format):
 
695
        klass._formats[format.get_format_string()] = format
 
696
 
 
697
    @classmethod
 
698
    def set_default_format(klass, format):
 
699
        klass._default_format = format
 
700
 
 
701
    @classmethod
 
702
    def unregister_format(klass, format):
 
703
        assert klass._formats[format.get_format_string()] is format
 
704
        del klass._formats[format.get_format_string()]
 
705
 
 
706
 
 
707
class RepositoryFormat4(RepositoryFormat):
 
708
    """Bzr repository format 4.
 
709
 
 
710
    This repository format has:
 
711
     - flat stores
 
712
     - TextStores for texts, inventories,revisions.
 
713
 
 
714
    This format is deprecated: it indexes texts using a text id which is
 
715
    removed in format 5; initializationa and write support for this format
 
716
    has been removed.
 
717
    """
 
718
 
 
719
    def __init__(self):
 
720
        super(RepositoryFormat4, self).__init__()
 
721
        self._matchingbzrdir = bzrdir.BzrDirFormat4()
 
722
 
 
723
    def initialize(self, url, _internal=False):
 
724
        """Format 4 branches cannot be created."""
 
725
        raise errors.UninitializableFormat(self)
 
726
 
 
727
    def is_supported(self):
 
728
        """Format 4 is not supported.
 
729
 
 
730
        It is not supported because the model changed from 4 to 5 and the
 
731
        conversion logic is expensive - so doing it on the fly was not 
 
732
        feasible.
 
733
        """
 
734
        return False
 
735
 
 
736
 
 
737
class RepositoryFormat5(RepositoryFormat):
 
738
    """Bzr control format 5.
 
739
 
 
740
    This repository format has:
 
741
     - weaves for file texts and inventory
 
742
     - flat stores
 
743
     - TextStores for revisions and signatures.
 
744
    """
 
745
 
 
746
    def __init__(self):
 
747
        super(RepositoryFormat5, self).__init__()
 
748
        self._matchingbzrdir = bzrdir.BzrDirFormat5()
 
749
 
 
750
 
 
751
class RepositoryFormat6(RepositoryFormat):
 
752
    """Bzr control format 6.
 
753
 
 
754
    This repository format has:
 
755
     - weaves for file texts and inventory
 
756
     - hash subdirectory based stores.
 
757
     - TextStores for revisions and signatures.
 
758
    """
 
759
 
 
760
    def __init__(self):
 
761
        super(RepositoryFormat6, self).__init__()
 
762
        self._matchingbzrdir = bzrdir.BzrDirFormat6()
 
763
 
 
764
 
 
765
class RepositoryFormat7(RepositoryFormat):
 
766
    """Bzr repository 7.
 
767
 
 
768
    This repository format has:
 
769
     - weaves for file texts and inventory
 
770
     - hash subdirectory based stores.
 
771
     - TextStores for revisions and signatures.
 
772
     - a format marker of its own
 
773
    """
 
774
 
 
775
    def get_format_string(self):
 
776
        """See RepositoryFormat.get_format_string()."""
 
777
        return "Bazaar-NG Repository format 7"
 
778
 
 
779
    def initialize(self, a_bzrdir):
 
780
        """Create a weave repository.
 
781
        """
 
782
        from bzrlib.weavefile import write_weave_v5
 
783
        from bzrlib.weave import Weave
 
784
 
 
785
        # Create an empty weave
 
786
        sio = StringIO()
 
787
        bzrlib.weavefile.write_weave_v5(Weave(), sio)
 
788
        empty_weave = sio.getvalue()
 
789
 
 
790
        mutter('creating repository in %s.', a_bzrdir.transport.base)
 
791
        dirs = ['revision-store', 'weaves']
 
792
        files = [('inventory.weave', StringIO(empty_weave)), 
 
793
                 ]
 
794
        utf8_files = [('format', self.get_format_string())]
 
795
        
 
796
        # FIXME: RBC 20060125 dont peek under the covers
 
797
        # NB: no need to escape relative paths that are url safe.
 
798
        lock_file = 'lock'
 
799
        repository_transport = a_bzrdir.get_repository_transport(self)
 
800
        repository_transport.put(lock_file, StringIO()) # TODO get the file mode from the bzrdir lock files., mode=file_mode)
 
801
        control_files = LockableFiles(repository_transport, 'lock')
 
802
        control_files.lock_write()
 
803
        control_files._transport.mkdir_multi(dirs,
 
804
                mode=control_files._dir_mode)
 
805
        try:
 
806
            for file, content in files:
 
807
                control_files.put(file, content)
 
808
            for file, content in utf8_files:
 
809
                control_files.put_utf8(file, content)
 
810
        finally:
 
811
            control_files.unlock()
 
812
        return Repository(None, branch_format=None, _format=self, a_bzrdir=a_bzrdir)
 
813
 
 
814
    def __init__(self):
 
815
        super(RepositoryFormat7, self).__init__()
 
816
        self._matchingbzrdir = bzrdir.BzrDirMetaFormat1()
 
817
 
 
818
 
 
819
# formats which have no format string are not discoverable
 
820
# and not independently creatable, so are not registered.
 
821
__default_format = RepositoryFormat7()
 
822
RepositoryFormat.register_format(__default_format)
 
823
RepositoryFormat.set_default_format(__default_format)
 
824
_legacy_formats = [RepositoryFormat4(),
 
825
                   RepositoryFormat5(),
 
826
                   RepositoryFormat6()]
 
827
 
 
828
 
 
829
# TODO: jam 20060108 Create a new branch format, and as part of upgrade
 
830
#       make sure that ancestry.weave is deleted (it is never used, but
 
831
#       used to be created)
 
832
 
 
833
class RepositoryTestProviderAdapter(object):
 
834
    """A tool to generate a suite testing multiple repository formats at once.
 
835
 
 
836
    This is done by copying the test once for each transport and injecting
 
837
    the transport_server, transport_readonly_server, and bzrdir_format and
 
838
    repository_format classes into each copy. Each copy is also given a new id()
 
839
    to make it easy to identify.
 
840
    """
 
841
 
 
842
    def __init__(self, transport_server, transport_readonly_server, formats):
 
843
        self._transport_server = transport_server
 
844
        self._transport_readonly_server = transport_readonly_server
 
845
        self._formats = formats
 
846
    
 
847
    def adapt(self, test):
 
848
        result = TestSuite()
 
849
        for repository_format, bzrdir_format in self._formats:
 
850
            new_test = deepcopy(test)
 
851
            new_test.transport_server = self._transport_server
 
852
            new_test.transport_readonly_server = self._transport_readonly_server
 
853
            new_test.bzrdir_format = bzrdir_format
 
854
            new_test.repository_format = repository_format
 
855
            def make_new_test_id():
 
856
                new_id = "%s(%s)" % (new_test.id(), repository_format.__class__.__name__)
 
857
                return lambda: new_id
 
858
            new_test.id = make_new_test_id()
 
859
            result.addTest(new_test)
 
860
        return result