/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

  • Committer: Robert Collins
  • Date: 2006-02-13 10:54:50 UTC
  • mto: (1534.5.2 bzr-dir)
  • mto: This revision was merged to the branch mainline in revision 1554.
  • Revision ID: robertc@robertcollins.net-20060213105450-ecb3a07f7774a697
Cloning of repos preserves shared and make-working-tree attributes.

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