/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/branch.py

Move branch implementations tests into a package.

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
 
 
18
from copy import deepcopy
 
19
from cStringIO import StringIO
 
20
import errno
 
21
import os
 
22
import shutil
 
23
import sys
 
24
from unittest import TestSuite
 
25
from warnings import warn
 
26
import xml.sax.saxutils
 
27
 
 
28
 
 
29
import bzrlib
 
30
from bzrlib.config import TreeConfig
 
31
from bzrlib.delta import compare_trees
 
32
import bzrlib.errors as errors
 
33
from bzrlib.errors import (BzrError, InvalidRevisionNumber, InvalidRevisionId,
 
34
                           NoSuchRevision, HistoryMissing, NotBranchError,
 
35
                           DivergedBranches, LockError, 
 
36
                           UninitializableFormat,
 
37
                           UnlistableStore,
 
38
                           UnlistableBranch, NoSuchFile, NotVersionedError,
 
39
                           NoWorkingTree)
 
40
import bzrlib.inventory as inventory
 
41
from bzrlib.inventory import Inventory
 
42
from bzrlib.osutils import (isdir, quotefn,
 
43
                            rename, splitpath, sha_file,
 
44
                            file_kind, abspath, normpath, pathjoin,
 
45
                            safe_unicode,
 
46
                            )
 
47
from bzrlib.textui import show_status
 
48
from bzrlib.trace import mutter, note
 
49
from bzrlib.tree import EmptyTree, RevisionTree
 
50
from bzrlib.revision import (Revision, is_ancestor, get_intervening_revisions,
 
51
                             NULL_REVISION)
 
52
from bzrlib.store import copy_all
 
53
from bzrlib.store.text import TextStore
 
54
from bzrlib.store.weave import WeaveStore
 
55
from bzrlib.symbol_versioning import deprecated_nonce, deprecated_passed
 
56
from bzrlib.testament import Testament
 
57
import bzrlib.transactions as transactions
 
58
from bzrlib.transport import Transport, get_transport
 
59
import bzrlib.xml5
 
60
import bzrlib.ui
 
61
 
 
62
 
 
63
BZR_BRANCH_FORMAT_4 = "Bazaar-NG branch, format 0.0.4\n"
 
64
BZR_BRANCH_FORMAT_5 = "Bazaar-NG branch, format 5\n"
 
65
BZR_BRANCH_FORMAT_6 = "Bazaar-NG branch, format 6\n"
 
66
## TODO: Maybe include checks for common corruption of newlines, etc?
 
67
 
 
68
 
 
69
# TODO: Some operations like log might retrieve the same revisions
 
70
# repeatedly to calculate deltas.  We could perhaps have a weakref
 
71
# cache in memory to make this faster.  In general anything can be
 
72
# cached in memory between lock and unlock operations.
 
73
 
 
74
def find_branch(*ignored, **ignored_too):
 
75
    # XXX: leave this here for about one release, then remove it
 
76
    raise NotImplementedError('find_branch() is not supported anymore, '
 
77
                              'please use one of the new branch constructors')
 
78
 
 
79
 
 
80
def needs_read_lock(unbound):
 
81
    """Decorate unbound to take out and release a read lock."""
 
82
    def decorated(self, *args, **kwargs):
 
83
        self.lock_read()
 
84
        try:
 
85
            return unbound(self, *args, **kwargs)
 
86
        finally:
 
87
            self.unlock()
 
88
    return decorated
 
89
 
 
90
 
 
91
def needs_write_lock(unbound):
 
92
    """Decorate unbound to take out and release a write lock."""
 
93
    def decorated(self, *args, **kwargs):
 
94
        self.lock_write()
 
95
        try:
 
96
            return unbound(self, *args, **kwargs)
 
97
        finally:
 
98
            self.unlock()
 
99
    return decorated
 
100
 
 
101
######################################################################
 
102
# branch objects
 
103
 
 
104
class Branch(object):
 
105
    """Branch holding a history of revisions.
 
106
 
 
107
    base
 
108
        Base directory/url of the branch.
 
109
    """
 
110
    # this is really an instance variable - FIXME move it there
 
111
    # - RBC 20060112
 
112
    base = None
 
113
 
 
114
    _default_initializer = None
 
115
    """The default initializer for making new branches."""
 
116
 
 
117
    def __init__(self, *ignored, **ignored_too):
 
118
        raise NotImplementedError('The Branch class is abstract')
 
119
 
 
120
    @staticmethod
 
121
    def open_downlevel(base):
 
122
        """Open a branch which may be of an old format."""
 
123
        return Branch.open(base, _unsupported=True)
 
124
        
 
125
    @staticmethod
 
126
    def open(base, _unsupported=False):
 
127
        """Open an existing branch, rooted at 'base' (url)
 
128
        
 
129
        _unsupported is a private parameter to the Branch class.
 
130
        """
 
131
        t = get_transport(base)
 
132
        mutter("trying to open %r with transport %r", base, t)
 
133
        format = BzrBranchFormat.find_format(t)
 
134
        if not _unsupported and not format.is_supported():
 
135
            # see open_downlevel to open legacy branches.
 
136
            raise errors.UnsupportedFormatError(
 
137
                    'sorry, branch format %s not supported' % format,
 
138
                    ['use a different bzr version',
 
139
                     'or remove the .bzr directory'
 
140
                     ' and "bzr init" again'])
 
141
        return format.open(t)
 
142
 
 
143
    @staticmethod
 
144
    def open_containing(url):
 
145
        """Open an existing branch which contains url.
 
146
        
 
147
        This probes for a branch at url, and searches upwards from there.
 
148
 
 
149
        Basically we keep looking up until we find the control directory or
 
150
        run into the root.  If there isn't one, raises NotBranchError.
 
151
        If there is one and it is either an unrecognised format or an unsupported 
 
152
        format, UnknownFormatError or UnsupportedFormatError are raised.
 
153
        If there is one, it is returned, along with the unused portion of url.
 
154
        """
 
155
        t = get_transport(url)
 
156
        # this gets the normalised url back. I.e. '.' -> the full path.
 
157
        url = t.base
 
158
        while True:
 
159
            try:
 
160
                format = BzrBranchFormat.find_format(t)
 
161
                return format.open(t), t.relpath(url)
 
162
            except NotBranchError, e:
 
163
                mutter('not a branch in: %r %s', t.base, e)
 
164
            new_t = t.clone('..')
 
165
            if new_t.base == t.base:
 
166
                # reached the root, whatever that may be
 
167
                raise NotBranchError(path=url)
 
168
            t = new_t
 
169
 
 
170
    @staticmethod
 
171
    def initialize(base):
 
172
        """Create a new branch, rooted at 'base' (url)
 
173
        
 
174
        This will call the current default initializer with base
 
175
        as the only parameter.
 
176
        """
 
177
        return Branch._default_initializer(safe_unicode(base))
 
178
 
 
179
    @staticmethod
 
180
    def get_default_initializer():
 
181
        """Return the initializer being used for new branches."""
 
182
        return Branch._default_initializer
 
183
 
 
184
    @staticmethod
 
185
    def set_default_initializer(initializer):
 
186
        """Set the initializer to be used for new branches."""
 
187
        Branch._default_initializer = staticmethod(initializer)
 
188
 
 
189
    def setup_caching(self, cache_root):
 
190
        """Subclasses that care about caching should override this, and set
 
191
        up cached stores located under cache_root.
 
192
        """
 
193
        self.cache_root = cache_root
 
194
 
 
195
    def _get_nick(self):
 
196
        cfg = self.tree_config()
 
197
        return cfg.get_option(u"nickname", default=self.base.split('/')[-2])
 
198
 
 
199
    def _set_nick(self, nick):
 
200
        cfg = self.tree_config()
 
201
        cfg.set_option(nick, "nickname")
 
202
        assert cfg.get_option("nickname") == nick
 
203
 
 
204
    nick = property(_get_nick, _set_nick)
 
205
        
 
206
    def push_stores(self, branch_to):
 
207
        """Copy the content of this branches store to branch_to."""
 
208
        raise NotImplementedError('push_stores is abstract')
 
209
 
 
210
    def get_transaction(self):
 
211
        """Return the current active transaction.
 
212
 
 
213
        If no transaction is active, this returns a passthrough object
 
214
        for which all data is immediately flushed and no caching happens.
 
215
        """
 
216
        raise NotImplementedError('get_transaction is abstract')
 
217
 
 
218
    def lock_write(self):
 
219
        raise NotImplementedError('lock_write is abstract')
 
220
        
 
221
    def lock_read(self):
 
222
        raise NotImplementedError('lock_read is abstract')
 
223
 
 
224
    def unlock(self):
 
225
        raise NotImplementedError('unlock is abstract')
 
226
 
 
227
    def abspath(self, name):
 
228
        """Return absolute filename for something in the branch
 
229
        
 
230
        XXX: Robert Collins 20051017 what is this used for? why is it a branch
 
231
        method and not a tree method.
 
232
        """
 
233
        raise NotImplementedError('abspath is abstract')
 
234
 
 
235
    def controlfilename(self, file_or_path):
 
236
        """Return location relative to branch."""
 
237
        raise NotImplementedError('controlfilename is abstract')
 
238
 
 
239
    def controlfile(self, file_or_path, mode='r'):
 
240
        """Open a control file for this branch.
 
241
 
 
242
        There are two classes of file in the control directory: text
 
243
        and binary.  binary files are untranslated byte streams.  Text
 
244
        control files are stored with Unix newlines and in UTF-8, even
 
245
        if the platform or locale defaults are different.
 
246
 
 
247
        Controlfiles should almost never be opened in write mode but
 
248
        rather should be atomically copied and replaced using atomicfile.
 
249
        """
 
250
        raise NotImplementedError('controlfile is abstract')
 
251
 
 
252
    def put_controlfile(self, path, f, encode=True):
 
253
        """Write an entry as a controlfile.
 
254
 
 
255
        :param path: The path to put the file, relative to the .bzr control
 
256
                     directory
 
257
        :param f: A file-like or string object whose contents should be copied.
 
258
        :param encode:  If true, encode the contents as utf-8
 
259
        """
 
260
        raise NotImplementedError('put_controlfile is abstract')
 
261
 
 
262
    def put_controlfiles(self, files, encode=True):
 
263
        """Write several entries as controlfiles.
 
264
 
 
265
        :param files: A list of [(path, file)] pairs, where the path is the directory
 
266
                      underneath the bzr control directory
 
267
        :param encode:  If true, encode the contents as utf-8
 
268
        """
 
269
        raise NotImplementedError('put_controlfiles is abstract')
 
270
 
 
271
    def get_root_id(self):
 
272
        """Return the id of this branches root"""
 
273
        raise NotImplementedError('get_root_id is abstract')
 
274
 
 
275
    def set_root_id(self, file_id):
 
276
        raise NotImplementedError('set_root_id is abstract')
 
277
 
 
278
    def print_file(self, file, revision_id):
 
279
        """Print `file` to stdout."""
 
280
        raise NotImplementedError('print_file is abstract')
 
281
 
 
282
    def append_revision(self, *revision_ids):
 
283
        raise NotImplementedError('append_revision is abstract')
 
284
 
 
285
    def set_revision_history(self, rev_history):
 
286
        raise NotImplementedError('set_revision_history is abstract')
 
287
 
 
288
    def has_revision(self, revision_id):
 
289
        """True if this branch has a copy of the revision.
 
290
 
 
291
        This does not necessarily imply the revision is merge
 
292
        or on the mainline."""
 
293
        raise NotImplementedError('has_revision is abstract')
 
294
 
 
295
    def get_revision_xml(self, revision_id):
 
296
        raise NotImplementedError('get_revision_xml is abstract')
 
297
 
 
298
    def get_revision(self, revision_id):
 
299
        """Return the Revision object for a named revision"""
 
300
        raise NotImplementedError('get_revision is abstract')
 
301
 
 
302
    def get_revision_delta(self, revno):
 
303
        """Return the delta for one revision.
 
304
 
 
305
        The delta is relative to its mainline predecessor, or the
 
306
        empty tree for revision 1.
 
307
        """
 
308
        assert isinstance(revno, int)
 
309
        rh = self.revision_history()
 
310
        if not (1 <= revno <= len(rh)):
 
311
            raise InvalidRevisionNumber(revno)
 
312
 
 
313
        # revno is 1-based; list is 0-based
 
314
 
 
315
        new_tree = self.revision_tree(rh[revno-1])
 
316
        if revno == 1:
 
317
            old_tree = EmptyTree()
 
318
        else:
 
319
            old_tree = self.revision_tree(rh[revno-2])
 
320
 
 
321
        return compare_trees(old_tree, new_tree)
 
322
 
 
323
    def get_revision_sha1(self, revision_id):
 
324
        """Hash the stored value of a revision, and return it."""
 
325
        raise NotImplementedError('get_revision_sha1 is abstract')
 
326
 
 
327
    def get_ancestry(self, revision_id):
 
328
        """Return a list of revision-ids integrated by a revision.
 
329
        
 
330
        This currently returns a list, but the ordering is not guaranteed:
 
331
        treat it as a set.
 
332
        """
 
333
        raise NotImplementedError('get_ancestry is abstract')
 
334
 
 
335
    def get_inventory(self, revision_id):
 
336
        """Get Inventory object by hash."""
 
337
        raise NotImplementedError('get_inventory is abstract')
 
338
 
 
339
    def get_inventory_xml(self, revision_id):
 
340
        """Get inventory XML as a file object."""
 
341
        raise NotImplementedError('get_inventory_xml is abstract')
 
342
 
 
343
    def get_inventory_sha1(self, revision_id):
 
344
        """Return the sha1 hash of the inventory entry."""
 
345
        raise NotImplementedError('get_inventory_sha1 is abstract')
 
346
 
 
347
    def get_revision_inventory(self, revision_id):
 
348
        """Return inventory of a past revision."""
 
349
        raise NotImplementedError('get_revision_inventory is abstract')
 
350
 
 
351
    def revision_history(self):
 
352
        """Return sequence of revision hashes on to this branch."""
 
353
        raise NotImplementedError('revision_history is abstract')
 
354
 
 
355
    def revno(self):
 
356
        """Return current revision number for this branch.
 
357
 
 
358
        That is equivalent to the number of revisions committed to
 
359
        this branch.
 
360
        """
 
361
        return len(self.revision_history())
 
362
 
 
363
    def last_revision(self):
 
364
        """Return last patch hash, or None if no history."""
 
365
        ph = self.revision_history()
 
366
        if ph:
 
367
            return ph[-1]
 
368
        else:
 
369
            return None
 
370
 
 
371
    def missing_revisions(self, other, stop_revision=None, diverged_ok=False):
 
372
        """Return a list of new revisions that would perfectly fit.
 
373
        
 
374
        If self and other have not diverged, return a list of the revisions
 
375
        present in other, but missing from self.
 
376
 
 
377
        >>> from bzrlib.commit import commit
 
378
        >>> bzrlib.trace.silent = True
 
379
        >>> br1 = ScratchBranch()
 
380
        >>> br2 = ScratchBranch()
 
381
        >>> br1.missing_revisions(br2)
 
382
        []
 
383
        >>> commit(br2, "lala!", rev_id="REVISION-ID-1")
 
384
        >>> br1.missing_revisions(br2)
 
385
        [u'REVISION-ID-1']
 
386
        >>> br2.missing_revisions(br1)
 
387
        []
 
388
        >>> commit(br1, "lala!", rev_id="REVISION-ID-1")
 
389
        >>> br1.missing_revisions(br2)
 
390
        []
 
391
        >>> commit(br2, "lala!", rev_id="REVISION-ID-2A")
 
392
        >>> br1.missing_revisions(br2)
 
393
        [u'REVISION-ID-2A']
 
394
        >>> commit(br1, "lala!", rev_id="REVISION-ID-2B")
 
395
        >>> br1.missing_revisions(br2)
 
396
        Traceback (most recent call last):
 
397
        DivergedBranches: These branches have diverged.  Try merge.
 
398
        """
 
399
        self_history = self.revision_history()
 
400
        self_len = len(self_history)
 
401
        other_history = other.revision_history()
 
402
        other_len = len(other_history)
 
403
        common_index = min(self_len, other_len) -1
 
404
        if common_index >= 0 and \
 
405
            self_history[common_index] != other_history[common_index]:
 
406
            raise DivergedBranches(self, other)
 
407
 
 
408
        if stop_revision is None:
 
409
            stop_revision = other_len
 
410
        else:
 
411
            assert isinstance(stop_revision, int)
 
412
            if stop_revision > other_len:
 
413
                raise bzrlib.errors.NoSuchRevision(self, stop_revision)
 
414
        return other_history[self_len:stop_revision]
 
415
    
 
416
    def update_revisions(self, other, stop_revision=None):
 
417
        """Pull in new perfect-fit revisions."""
 
418
        raise NotImplementedError('update_revisions is abstract')
 
419
 
 
420
    def pullable_revisions(self, other, stop_revision):
 
421
        raise NotImplementedError('pullable_revisions is abstract')
 
422
        
 
423
    def revision_id_to_revno(self, revision_id):
 
424
        """Given a revision id, return its revno"""
 
425
        if revision_id is None:
 
426
            return 0
 
427
        history = self.revision_history()
 
428
        try:
 
429
            return history.index(revision_id) + 1
 
430
        except ValueError:
 
431
            raise bzrlib.errors.NoSuchRevision(self, revision_id)
 
432
 
 
433
    def get_rev_id(self, revno, history=None):
 
434
        """Find the revision id of the specified revno."""
 
435
        if revno == 0:
 
436
            return None
 
437
        if history is None:
 
438
            history = self.revision_history()
 
439
        elif revno <= 0 or revno > len(history):
 
440
            raise bzrlib.errors.NoSuchRevision(self, revno)
 
441
        return history[revno - 1]
 
442
 
 
443
    def revision_tree(self, revision_id):
 
444
        """Return Tree for a revision on this branch.
 
445
 
 
446
        `revision_id` may be None for the null revision, in which case
 
447
        an `EmptyTree` is returned."""
 
448
        raise NotImplementedError('revision_tree is abstract')
 
449
 
 
450
    def working_tree(self):
 
451
        """Return a `Tree` for the working copy if this is a local branch."""
 
452
        raise NotImplementedError('working_tree is abstract')
 
453
 
 
454
    def pull(self, source, overwrite=False):
 
455
        raise NotImplementedError('pull is abstract')
 
456
 
 
457
    def basis_tree(self):
 
458
        """Return `Tree` object for last revision.
 
459
 
 
460
        If there are no revisions yet, return an `EmptyTree`.
 
461
        """
 
462
        return self.revision_tree(self.last_revision())
 
463
 
 
464
    def rename_one(self, from_rel, to_rel):
 
465
        """Rename one file.
 
466
 
 
467
        This can change the directory or the filename or both.
 
468
        """
 
469
        raise NotImplementedError('rename_one is abstract')
 
470
 
 
471
    def move(self, from_paths, to_name):
 
472
        """Rename files.
 
473
 
 
474
        to_name must exist as a versioned directory.
 
475
 
 
476
        If to_name exists and is a directory, the files are moved into
 
477
        it, keeping their old names.  If it is a directory, 
 
478
 
 
479
        Note that to_name is only the last component of the new name;
 
480
        this doesn't change the directory.
 
481
 
 
482
        This returns a list of (from_path, to_path) pairs for each
 
483
        entry that is moved.
 
484
        """
 
485
        raise NotImplementedError('move is abstract')
 
486
 
 
487
    def get_parent(self):
 
488
        """Return the parent location of the branch.
 
489
 
 
490
        This is the default location for push/pull/missing.  The usual
 
491
        pattern is that the user can override it by specifying a
 
492
        location.
 
493
        """
 
494
        raise NotImplementedError('get_parent is abstract')
 
495
 
 
496
    def get_push_location(self):
 
497
        """Return the None or the location to push this branch to."""
 
498
        raise NotImplementedError('get_push_location is abstract')
 
499
 
 
500
    def set_push_location(self, location):
 
501
        """Set a new push location for this branch."""
 
502
        raise NotImplementedError('set_push_location is abstract')
 
503
 
 
504
    def set_parent(self, url):
 
505
        raise NotImplementedError('set_parent is abstract')
 
506
 
 
507
    def check_revno(self, revno):
 
508
        """\
 
509
        Check whether a revno corresponds to any revision.
 
510
        Zero (the NULL revision) is considered valid.
 
511
        """
 
512
        if revno != 0:
 
513
            self.check_real_revno(revno)
 
514
            
 
515
    def check_real_revno(self, revno):
 
516
        """\
 
517
        Check whether a revno corresponds to a real revision.
 
518
        Zero (the NULL revision) is considered invalid
 
519
        """
 
520
        if revno < 1 or revno > self.revno():
 
521
            raise InvalidRevisionNumber(revno)
 
522
        
 
523
    def sign_revision(self, revision_id, gpg_strategy):
 
524
        raise NotImplementedError('sign_revision is abstract')
 
525
 
 
526
    def store_revision_signature(self, gpg_strategy, plaintext, revision_id):
 
527
        raise NotImplementedError('store_revision_signature is abstract')
 
528
 
 
529
    def fileid_involved_between_revs(self, from_revid, to_revid):
 
530
        """ This function returns the file_id(s) involved in the
 
531
            changes between the from_revid revision and the to_revid
 
532
            revision
 
533
        """
 
534
        raise NotImplementedError('fileid_involved_between_revs is abstract')
 
535
 
 
536
    def fileid_involved(self, last_revid=None):
 
537
        """ This function returns the file_id(s) involved in the
 
538
            changes up to the revision last_revid
 
539
            If no parametr is passed, then all file_id[s] present in the
 
540
            repository are returned
 
541
        """
 
542
        raise NotImplementedError('fileid_involved is abstract')
 
543
 
 
544
    def fileid_involved_by_set(self, changes):
 
545
        """ This function returns the file_id(s) involved in the
 
546
            changes present in the set 'changes'
 
547
        """
 
548
        raise NotImplementedError('fileid_involved_by_set is abstract')
 
549
 
 
550
    def fileid_involved_between_revs(self, from_revid, to_revid):
 
551
        """ This function returns the file_id(s) involved in the
 
552
            changes between the from_revid revision and the to_revid
 
553
            revision
 
554
        """
 
555
        raise NotImplementedError('fileid_involved_between_revs is abstract')
 
556
 
 
557
    def fileid_involved(self, last_revid=None):
 
558
        """ This function returns the file_id(s) involved in the
 
559
            changes up to the revision last_revid
 
560
            If no parametr is passed, then all file_id[s] present in the
 
561
            repository are returned
 
562
        """
 
563
        raise NotImplementedError('fileid_involved is abstract')
 
564
 
 
565
    def fileid_involved_by_set(self, changes):
 
566
        """ This function returns the file_id(s) involved in the
 
567
            changes present in the set 'changes'
 
568
        """
 
569
        raise NotImplementedError('fileid_involved_by_set is abstract')
 
570
 
 
571
class BzrBranchFormat(object):
 
572
    """An encapsulation of the initialization and open routines for a format.
 
573
 
 
574
    Formats provide three things:
 
575
     * An initialization routine,
 
576
     * a format string,
 
577
     * an open routine.
 
578
 
 
579
    Formats are placed in an dict by their format string for reference 
 
580
    during branch opening. Its not required that these be instances, they
 
581
    can be classes themselves with class methods - it simply depends on 
 
582
    whether state is needed for a given format or not.
 
583
 
 
584
    Once a format is deprecated, just deprecate the initialize and open
 
585
    methods on the format class. Do not deprecate the object, as the 
 
586
    object will be created every time regardless.
 
587
    """
 
588
 
 
589
    _formats = {}
 
590
    """The known formats."""
 
591
 
 
592
    @classmethod
 
593
    def find_format(klass, transport):
 
594
        """Return the format registered for URL."""
 
595
        try:
 
596
            format_string = transport.get(".bzr/branch-format").read()
 
597
            return klass._formats[format_string]
 
598
        except NoSuchFile:
 
599
            raise NotBranchError(path=transport.base)
 
600
        except KeyError:
 
601
            raise errors.UnknownFormatError(format_string)
 
602
 
 
603
    def get_format_string(self):
 
604
        """Return the ASCII format string that identifies this format."""
 
605
        raise NotImplementedError(self.get_format_string)
 
606
 
 
607
    def _find_modes(self, t):
 
608
        """Determine the appropriate modes for files and directories.
 
609
        
 
610
        FIXME: When this merges into, or from storage,
 
611
        this code becomes delgatable to a LockableFiles instance.
 
612
 
 
613
        For now its cribbed and returns (dir_mode, file_mode)
 
614
        """
 
615
        try:
 
616
            st = t.stat('.')
 
617
        except errors.TransportNotPossible:
 
618
            dir_mode = 0755
 
619
            file_mode = 0644
 
620
        else:
 
621
            dir_mode = st.st_mode & 07777
 
622
            # Remove the sticky and execute bits for files
 
623
            file_mode = dir_mode & ~07111
 
624
        if not BzrBranch._set_dir_mode:
 
625
            dir_mode = None
 
626
        if not BzrBranch._set_file_mode:
 
627
            file_mode = None
 
628
        return dir_mode, file_mode
 
629
 
 
630
    def initialize(self, url):
 
631
        """Create a branch of this format at url and return an open branch."""
 
632
        t = get_transport(url)
 
633
        from bzrlib.inventory import Inventory
 
634
        from bzrlib.weavefile import write_weave_v5
 
635
        from bzrlib.weave import Weave
 
636
        
 
637
        # Create an empty inventory
 
638
        sio = StringIO()
 
639
        # if we want per-tree root ids then this is the place to set
 
640
        # them; they're not needed for now and so ommitted for
 
641
        # simplicity.
 
642
        bzrlib.xml5.serializer_v5.write_inventory(Inventory(), sio)
 
643
        empty_inv = sio.getvalue()
 
644
        sio = StringIO()
 
645
        bzrlib.weavefile.write_weave_v5(Weave(), sio)
 
646
        empty_weave = sio.getvalue()
 
647
 
 
648
        # Since we don't have a .bzr directory, inherit the
 
649
        # mode from the root directory
 
650
        dir_mode, file_mode = self._find_modes(t)
 
651
 
 
652
        t.mkdir('.bzr', mode=dir_mode)
 
653
        control = t.clone('.bzr')
 
654
        dirs = ['revision-store', 'weaves']
 
655
        files = [('README', 
 
656
            StringIO("This is a Bazaar-NG control directory.\n"
 
657
            "Do not change any files in this directory.\n")),
 
658
            ('branch-format', StringIO(self.get_format_string())),
 
659
            ('revision-history', StringIO('')),
 
660
            ('branch-name', StringIO('')),
 
661
            ('branch-lock', StringIO('')),
 
662
            ('pending-merges', StringIO('')),
 
663
            ('inventory', StringIO(empty_inv)),
 
664
            ('inventory.weave', StringIO(empty_weave)),
 
665
        ]
 
666
        control.mkdir_multi(dirs, mode=dir_mode)
 
667
        control.put_multi(files, mode=file_mode)
 
668
        mutter('created control directory in ' + t.base)
 
669
        return BzrBranch(t, format=self)
 
670
 
 
671
    def is_supported(self):
 
672
        """Is this format supported?
 
673
 
 
674
        Supported formats can be initialized and opened.
 
675
        Unsupported formats may not support initialization or committing or 
 
676
        some other features depending on the reason for not being supported.
 
677
        """
 
678
        return True
 
679
 
 
680
    def open(self, transport):
 
681
        """Fill out the data in branch for the branch at url."""
 
682
        return BzrBranch(transport, format=self)
 
683
 
 
684
    @classmethod
 
685
    def register_format(klass, format):
 
686
        klass._formats[format.get_format_string()] = format
 
687
 
 
688
    @classmethod
 
689
    def unregister_format(klass, format):
 
690
        assert klass._formats[format.get_format_string()] is format
 
691
        del klass._formats[format.get_format_string()]
 
692
 
 
693
 
 
694
class BzrBranchFormat4(BzrBranchFormat):
 
695
    """Bzr branch format 4.
 
696
 
 
697
    This format has:
 
698
     - flat stores
 
699
     - TextStores for texts, inventories,revisions.
 
700
 
 
701
    This format is deprecated: it indexes texts using a text it which is
 
702
    removed in format 5; write support for this format has been removed.
 
703
    """
 
704
 
 
705
    def get_format_string(self):
 
706
        """See BzrBranchFormat.get_format_string()."""
 
707
        return BZR_BRANCH_FORMAT_4
 
708
 
 
709
    def initialize(self, url):
 
710
        """Format 4 branches cannot be created."""
 
711
        raise UninitializableFormat(self)
 
712
 
 
713
    def is_supported(self):
 
714
        """Format 4 is not supported.
 
715
 
 
716
        It is not supported because the model changed from 4 to 5 and the
 
717
        conversion logic is expensive - so doing it on the fly was not 
 
718
        feasible.
 
719
        """
 
720
        return False
 
721
 
 
722
 
 
723
class BzrBranchFormat5(BzrBranchFormat):
 
724
    """Bzr branch format 5.
 
725
 
 
726
    This format has:
 
727
     - weaves for file texts and inventory
 
728
     - flat stores
 
729
     - TextStores for revisions and signatures.
 
730
    """
 
731
 
 
732
    def get_format_string(self):
 
733
        """See BzrBranchFormat.get_format_string()."""
 
734
        return BZR_BRANCH_FORMAT_5
 
735
 
 
736
 
 
737
class BzrBranchFormat6(BzrBranchFormat):
 
738
    """Bzr branch format 6.
 
739
 
 
740
    This format has:
 
741
     - weaves for file texts and inventory
 
742
     - hash subdirectory based stores.
 
743
     - TextStores for revisions and signatures.
 
744
    """
 
745
 
 
746
    def get_format_string(self):
 
747
        """See BzrBranchFormat.get_format_string()."""
 
748
        return BZR_BRANCH_FORMAT_6
 
749
 
 
750
 
 
751
BzrBranchFormat.register_format(BzrBranchFormat4())
 
752
BzrBranchFormat.register_format(BzrBranchFormat5())
 
753
BzrBranchFormat.register_format(BzrBranchFormat6())
 
754
 
 
755
# TODO: jam 20060108 Create a new branch format, and as part of upgrade
 
756
#       make sure that ancestry.weave is deleted (it is never used, but
 
757
#       used to be created)
 
758
 
 
759
 
 
760
class BzrBranch(Branch):
 
761
    """A branch stored in the actual filesystem.
 
762
 
 
763
    Note that it's "local" in the context of the filesystem; it doesn't
 
764
    really matter if it's on an nfs/smb/afs/coda/... share, as long as
 
765
    it's writable, and can be accessed via the normal filesystem API.
 
766
 
 
767
    _lock_mode
 
768
        None, or 'r' or 'w'
 
769
 
 
770
    _lock_count
 
771
        If _lock_mode is true, a positive count of the number of times the
 
772
        lock has been taken.
 
773
 
 
774
    _lock
 
775
        Lock object from bzrlib.lock.
 
776
    """
 
777
    # We actually expect this class to be somewhat short-lived; part of its
 
778
    # purpose is to try to isolate what bits of the branch logic are tied to
 
779
    # filesystem access, so that in a later step, we can extricate them to
 
780
    # a separarte ("storage") class.
 
781
    _lock_mode = None
 
782
    _lock_count = None
 
783
    _lock = None
 
784
    _inventory_weave = None
 
785
    # If set to False (by a plugin, etc) BzrBranch will not set the
 
786
    # mode on created files or directories
 
787
    _set_file_mode = True
 
788
    _set_dir_mode = True
 
789
    
 
790
    # Map some sort of prefix into a namespace
 
791
    # stuff like "revno:10", "revid:", etc.
 
792
    # This should match a prefix with a function which accepts
 
793
    REVISION_NAMESPACES = {}
 
794
 
 
795
    def push_stores(self, branch_to):
 
796
        """See Branch.push_stores."""
 
797
        if (not isinstance(self._branch_format, BzrBranchFormat4) or
 
798
            self._branch_format != branch_to._branch_format):
 
799
            from bzrlib.fetch import greedy_fetch
 
800
            mutter("Using fetch logic to push between %s(%s) and %s(%s)",
 
801
                   self, self._branch_format, branch_to, branch_to._branch_format)
 
802
            greedy_fetch(to_branch=branch_to, from_branch=self,
 
803
                         revision=self.last_revision())
 
804
            return
 
805
 
 
806
        # format 4 to format 4 logic only.
 
807
        store_pairs = ((self.text_store,      branch_to.text_store),
 
808
                       (self.inventory_store, branch_to.inventory_store),
 
809
                       (self.revision_store,  branch_to.revision_store))
 
810
        try:
 
811
            for from_store, to_store in store_pairs: 
 
812
                copy_all(from_store, to_store)
 
813
        except UnlistableStore:
 
814
            raise UnlistableBranch(from_store)
 
815
 
 
816
    def __init__(self, transport, init=deprecated_nonce,
 
817
                 relax_version_check=deprecated_nonce, format=None):
 
818
        """Create new branch object at a particular location.
 
819
 
 
820
        transport -- A Transport object, defining how to access files.
 
821
        
 
822
        init -- If True, create new control files in a previously
 
823
             unversioned directory.  If False, the branch must already
 
824
             be versioned.
 
825
 
 
826
        relax_version_check -- If true, the usual check for the branch
 
827
            version is not applied.  This is intended only for
 
828
            upgrade/recovery type use; it's not guaranteed that
 
829
            all operations will work on old format branches.
 
830
 
 
831
        In the test suite, creation of new trees is tested using the
 
832
        `ScratchBranch` class.
 
833
        """
 
834
        assert isinstance(transport, Transport), \
 
835
            "%r is not a Transport" % transport
 
836
        self._transport = transport
 
837
        if deprecated_passed(init):
 
838
            warn("BzrBranch.__init__(..., init=XXX): The init parameter is "
 
839
                 "deprecated as of bzr 0.8. Please use Branch.initialize().",
 
840
                 DeprecationWarning)
 
841
            if init:
 
842
                # this is slower than before deprecation, oh well never mind.
 
843
                # -> its deprecated.
 
844
                self._initialize(transport.base)
 
845
        self._find_modes()
 
846
        self._check_format(format)
 
847
        if deprecated_passed(relax_version_check):
 
848
            warn("BzrBranch.__init__(..., relax_version_check=XXX_: The "
 
849
                 "relax_version_check parameter is deprecated as of bzr 0.8. "
 
850
                 "Please use Branch.open_downlevel, or a BzrBranchFormat's "
 
851
                 "open() method.", DeprecationWarning)
 
852
            if (not relax_version_check
 
853
                and not self._branch_format.is_supported()):
 
854
                raise errors.UnsupportedFormatError(
 
855
                        'sorry, branch format %r not supported' % fmt,
 
856
                        ['use a different bzr version',
 
857
                         'or remove the .bzr directory'
 
858
                         ' and "bzr init" again'])
 
859
 
 
860
        def get_store(name, compressed=True, prefixed=False):
 
861
            relpath = self._rel_controlfilename(safe_unicode(name))
 
862
            store = TextStore(self._transport.clone(relpath),
 
863
                              dir_mode=self._dir_mode,
 
864
                              file_mode=self._file_mode,
 
865
                              prefixed=prefixed,
 
866
                              compressed=compressed)
 
867
            return store
 
868
 
 
869
        def get_weave(name, prefixed=False):
 
870
            relpath = self._rel_controlfilename(unicode(name))
 
871
            ws = WeaveStore(self._transport.clone(relpath),
 
872
                            prefixed=prefixed,
 
873
                            dir_mode=self._dir_mode,
 
874
                            file_mode=self._file_mode)
 
875
            if self._transport.should_cache():
 
876
                ws.enable_cache = True
 
877
            return ws
 
878
 
 
879
        if isinstance(self._branch_format, BzrBranchFormat4):
 
880
            self.inventory_store = get_store('inventory-store')
 
881
            self.text_store = get_store('text-store')
 
882
            self.revision_store = get_store('revision-store')
 
883
        elif isinstance(self._branch_format, BzrBranchFormat5):
 
884
            self.control_weaves = get_weave(u'')
 
885
            self.weave_store = get_weave(u'weaves')
 
886
            self.revision_store = get_store(u'revision-store', compressed=False)
 
887
        elif isinstance(self._branch_format, BzrBranchFormat6):
 
888
            self.control_weaves = get_weave(u'')
 
889
            self.weave_store = get_weave(u'weaves', prefixed=True)
 
890
            self.revision_store = get_store(u'revision-store', compressed=False,
 
891
                                            prefixed=True)
 
892
        self.revision_store.register_suffix('sig')
 
893
        self._transaction = None
 
894
 
 
895
    @staticmethod
 
896
    def _initialize(base):
 
897
        """Create a bzr branch in the latest format."""
 
898
        return BzrBranchFormat6().initialize(base)
 
899
 
 
900
    def __str__(self):
 
901
        return '%s(%r)' % (self.__class__.__name__, self._transport.base)
 
902
 
 
903
    __repr__ = __str__
 
904
 
 
905
    def __del__(self):
 
906
        if self._lock_mode or self._lock:
 
907
            # XXX: This should show something every time, and be suitable for
 
908
            # headless operation and embedding
 
909
            warn("branch %r was not explicitly unlocked" % self)
 
910
            self._lock.unlock()
 
911
 
 
912
        # TODO: It might be best to do this somewhere else,
 
913
        # but it is nice for a Branch object to automatically
 
914
        # cache it's information.
 
915
        # Alternatively, we could have the Transport objects cache requests
 
916
        # See the earlier discussion about how major objects (like Branch)
 
917
        # should never expect their __del__ function to run.
 
918
        if hasattr(self, 'cache_root') and self.cache_root is not None:
 
919
            try:
 
920
                shutil.rmtree(self.cache_root)
 
921
            except:
 
922
                pass
 
923
            self.cache_root = None
 
924
 
 
925
    def _get_base(self):
 
926
        if self._transport:
 
927
            return self._transport.base
 
928
        return None
 
929
 
 
930
    base = property(_get_base, doc="The URL for the root of this branch.")
 
931
 
 
932
    def _finish_transaction(self):
 
933
        """Exit the current transaction."""
 
934
        if self._transaction is None:
 
935
            raise errors.LockError('Branch %s is not in a transaction' %
 
936
                                   self)
 
937
        transaction = self._transaction
 
938
        self._transaction = None
 
939
        transaction.finish()
 
940
 
 
941
    def get_transaction(self):
 
942
        """See Branch.get_transaction."""
 
943
        if self._transaction is None:
 
944
            return transactions.PassThroughTransaction()
 
945
        else:
 
946
            return self._transaction
 
947
 
 
948
    def _set_transaction(self, new_transaction):
 
949
        """Set a new active transaction."""
 
950
        if self._transaction is not None:
 
951
            raise errors.LockError('Branch %s is in a transaction already.' %
 
952
                                   self)
 
953
        self._transaction = new_transaction
 
954
 
 
955
    def lock_write(self):
 
956
        #mutter("lock write: %s (%s)", self, self._lock_count)
 
957
        # TODO: Upgrade locking to support using a Transport,
 
958
        # and potentially a remote locking protocol
 
959
        if self._lock_mode:
 
960
            if self._lock_mode != 'w':
 
961
                raise LockError("can't upgrade to a write lock from %r" %
 
962
                                self._lock_mode)
 
963
            self._lock_count += 1
 
964
        else:
 
965
            self._lock = self._transport.lock_write(
 
966
                    self._rel_controlfilename('branch-lock'))
 
967
            self._lock_mode = 'w'
 
968
            self._lock_count = 1
 
969
            self._set_transaction(transactions.PassThroughTransaction())
 
970
 
 
971
    def lock_read(self):
 
972
        #mutter("lock read: %s (%s)", self, self._lock_count)
 
973
        if self._lock_mode:
 
974
            assert self._lock_mode in ('r', 'w'), \
 
975
                   "invalid lock mode %r" % self._lock_mode
 
976
            self._lock_count += 1
 
977
        else:
 
978
            self._lock = self._transport.lock_read(
 
979
                    self._rel_controlfilename('branch-lock'))
 
980
            self._lock_mode = 'r'
 
981
            self._lock_count = 1
 
982
            self._set_transaction(transactions.ReadOnlyTransaction())
 
983
            # 5K may be excessive, but hey, its a knob.
 
984
            self.get_transaction().set_cache_size(5000)
 
985
                        
 
986
    def unlock(self):
 
987
        #mutter("unlock: %s (%s)", self, self._lock_count)
 
988
        if not self._lock_mode:
 
989
            raise LockError('branch %r is not locked' % (self))
 
990
 
 
991
        if self._lock_count > 1:
 
992
            self._lock_count -= 1
 
993
        else:
 
994
            self._finish_transaction()
 
995
            self._lock.unlock()
 
996
            self._lock = None
 
997
            self._lock_mode = self._lock_count = None
 
998
 
 
999
    def abspath(self, name):
 
1000
        """See Branch.abspath."""
 
1001
        return self._transport.abspath(name)
 
1002
 
 
1003
    def _rel_controlfilename(self, file_or_path):
 
1004
        if not isinstance(file_or_path, basestring):
 
1005
            file_or_path = u'/'.join(file_or_path)
 
1006
        if file_or_path == '':
 
1007
            return bzrlib.BZRDIR
 
1008
        return bzrlib.transport.urlescape(bzrlib.BZRDIR + u'/' + file_or_path)
 
1009
 
 
1010
    def controlfilename(self, file_or_path):
 
1011
        """See Branch.controlfilename."""
 
1012
        return self._transport.abspath(self._rel_controlfilename(file_or_path))
 
1013
 
 
1014
    def controlfile(self, file_or_path, mode='r'):
 
1015
        """See Branch.controlfile."""
 
1016
        import codecs
 
1017
 
 
1018
        relpath = self._rel_controlfilename(file_or_path)
 
1019
        #TODO: codecs.open() buffers linewise, so it was overloaded with
 
1020
        # a much larger buffer, do we need to do the same for getreader/getwriter?
 
1021
        if mode == 'rb': 
 
1022
            return self._transport.get(relpath)
 
1023
        elif mode == 'wb':
 
1024
            raise BzrError("Branch.controlfile(mode='wb') is not supported, use put_controlfiles")
 
1025
        elif mode == 'r':
 
1026
            # XXX: Do we really want errors='replace'?   Perhaps it should be
 
1027
            # an error, or at least reported, if there's incorrectly-encoded
 
1028
            # data inside a file.
 
1029
            # <https://launchpad.net/products/bzr/+bug/3823>
 
1030
            return codecs.getreader('utf-8')(self._transport.get(relpath), errors='replace')
 
1031
        elif mode == 'w':
 
1032
            raise BzrError("Branch.controlfile(mode='w') is not supported, use put_controlfiles")
 
1033
        else:
 
1034
            raise BzrError("invalid controlfile mode %r" % mode)
 
1035
 
 
1036
    def put_controlfile(self, path, f, encode=True):
 
1037
        """See Branch.put_controlfile."""
 
1038
        self.put_controlfiles([(path, f)], encode=encode)
 
1039
 
 
1040
    def put_controlfiles(self, files, encode=True):
 
1041
        """See Branch.put_controlfiles."""
 
1042
        import codecs
 
1043
        ctrl_files = []
 
1044
        for path, f in files:
 
1045
            if encode:
 
1046
                if isinstance(f, basestring):
 
1047
                    f = f.encode('utf-8', 'replace')
 
1048
                else:
 
1049
                    f = codecs.getwriter('utf-8')(f, errors='replace')
 
1050
            path = self._rel_controlfilename(path)
 
1051
            ctrl_files.append((path, f))
 
1052
        self._transport.put_multi(ctrl_files, mode=self._file_mode)
 
1053
 
 
1054
    def _find_modes(self, path=None):
 
1055
        """Determine the appropriate modes for files and directories."""
 
1056
        try:
 
1057
            if path is None:
 
1058
                path = self._rel_controlfilename('')
 
1059
            st = self._transport.stat(path)
 
1060
        except errors.TransportNotPossible:
 
1061
            self._dir_mode = 0755
 
1062
            self._file_mode = 0644
 
1063
        else:
 
1064
            self._dir_mode = st.st_mode & 07777
 
1065
            # Remove the sticky and execute bits for files
 
1066
            self._file_mode = self._dir_mode & ~07111
 
1067
        if not self._set_dir_mode:
 
1068
            self._dir_mode = None
 
1069
        if not self._set_file_mode:
 
1070
            self._file_mode = None
 
1071
 
 
1072
    def _check_format(self, format):
 
1073
        """Identify the branch format if needed.
 
1074
 
 
1075
        The format is stored as a reference to the format object in
 
1076
        self._branch_format for code that needs to check it later.
 
1077
 
 
1078
        The format parameter is either None or the branch format class
 
1079
        used to open this branch.
 
1080
        """
 
1081
        if format is None:
 
1082
            format = BzrBranchFormat.find_format(self._transport)
 
1083
        self._branch_format = format
 
1084
        mutter("got branch format %s", self._branch_format)
 
1085
 
 
1086
    @needs_read_lock
 
1087
    def get_root_id(self):
 
1088
        """See Branch.get_root_id."""
 
1089
        inv = self.get_inventory(self.last_revision())
 
1090
        return inv.root.file_id
 
1091
 
 
1092
    @needs_read_lock
 
1093
    def print_file(self, file, revision_id):
 
1094
        """See Branch.print_file."""
 
1095
        tree = self.revision_tree(revision_id)
 
1096
        # use inventory as it was in that revision
 
1097
        file_id = tree.inventory.path2id(file)
 
1098
        if not file_id:
 
1099
            try:
 
1100
                revno = self.revision_id_to_revno(revision_id)
 
1101
            except errors.NoSuchRevision:
 
1102
                # TODO: This should not be BzrError,
 
1103
                # but NoSuchFile doesn't fit either
 
1104
                raise BzrError('%r is not present in revision %s' 
 
1105
                                % (file, revision_id))
 
1106
            else:
 
1107
                raise BzrError('%r is not present in revision %s'
 
1108
                                % (file, revno))
 
1109
        tree.print_file(file_id)
 
1110
 
 
1111
    @needs_write_lock
 
1112
    def append_revision(self, *revision_ids):
 
1113
        """See Branch.append_revision."""
 
1114
        for revision_id in revision_ids:
 
1115
            mutter("add {%s} to revision-history" % revision_id)
 
1116
        rev_history = self.revision_history()
 
1117
        rev_history.extend(revision_ids)
 
1118
        self.set_revision_history(rev_history)
 
1119
 
 
1120
    @needs_write_lock
 
1121
    def set_revision_history(self, rev_history):
 
1122
        """See Branch.set_revision_history."""
 
1123
        old_revision = self.last_revision()
 
1124
        new_revision = rev_history[-1]
 
1125
        self.put_controlfile('revision-history', '\n'.join(rev_history))
 
1126
        try:
 
1127
            self.working_tree().set_last_revision(new_revision, old_revision)
 
1128
        except NoWorkingTree:
 
1129
            mutter('Unable to set_last_revision without a working tree.')
 
1130
 
 
1131
    def has_revision(self, revision_id):
 
1132
        """See Branch.has_revision."""
 
1133
        return (revision_id is None
 
1134
                or self.revision_store.has_id(revision_id))
 
1135
 
 
1136
    @needs_read_lock
 
1137
    def _get_revision_xml_file(self, revision_id):
 
1138
        if not revision_id or not isinstance(revision_id, basestring):
 
1139
            raise InvalidRevisionId(revision_id=revision_id, branch=self)
 
1140
        try:
 
1141
            return self.revision_store.get(revision_id)
 
1142
        except (IndexError, KeyError):
 
1143
            raise bzrlib.errors.NoSuchRevision(self, revision_id)
 
1144
 
 
1145
    def get_revision_xml(self, revision_id):
 
1146
        """See Branch.get_revision_xml."""
 
1147
        return self._get_revision_xml_file(revision_id).read()
 
1148
 
 
1149
    def get_revision(self, revision_id):
 
1150
        """See Branch.get_revision."""
 
1151
        xml_file = self._get_revision_xml_file(revision_id)
 
1152
 
 
1153
        try:
 
1154
            r = bzrlib.xml5.serializer_v5.read_revision(xml_file)
 
1155
        except SyntaxError, e:
 
1156
            raise bzrlib.errors.BzrError('failed to unpack revision_xml',
 
1157
                                         [revision_id,
 
1158
                                          str(e)])
 
1159
            
 
1160
        assert r.revision_id == revision_id
 
1161
        return r
 
1162
 
 
1163
    def get_revision_sha1(self, revision_id):
 
1164
        """See Branch.get_revision_sha1."""
 
1165
        # In the future, revision entries will be signed. At that
 
1166
        # point, it is probably best *not* to include the signature
 
1167
        # in the revision hash. Because that lets you re-sign
 
1168
        # the revision, (add signatures/remove signatures) and still
 
1169
        # have all hash pointers stay consistent.
 
1170
        # But for now, just hash the contents.
 
1171
        return bzrlib.osutils.sha_file(self.get_revision_xml_file(revision_id))
 
1172
 
 
1173
    def get_ancestry(self, revision_id):
 
1174
        """See Branch.get_ancestry."""
 
1175
        if revision_id is None:
 
1176
            return [None]
 
1177
        w = self._get_inventory_weave()
 
1178
        return [None] + map(w.idx_to_name,
 
1179
                            w.inclusions([w.lookup(revision_id)]))
 
1180
 
 
1181
    def _get_inventory_weave(self):
 
1182
        return self.control_weaves.get_weave('inventory',
 
1183
                                             self.get_transaction())
 
1184
 
 
1185
    def get_inventory(self, revision_id):
 
1186
        """See Branch.get_inventory."""
 
1187
        xml = self.get_inventory_xml(revision_id)
 
1188
        return bzrlib.xml5.serializer_v5.read_inventory_from_string(xml)
 
1189
 
 
1190
    def get_inventory_xml(self, revision_id):
 
1191
        """See Branch.get_inventory_xml."""
 
1192
        try:
 
1193
            assert isinstance(revision_id, basestring), type(revision_id)
 
1194
            iw = self._get_inventory_weave()
 
1195
            return iw.get_text(iw.lookup(revision_id))
 
1196
        except IndexError:
 
1197
            raise bzrlib.errors.HistoryMissing(self, 'inventory', revision_id)
 
1198
 
 
1199
    def get_inventory_sha1(self, revision_id):
 
1200
        """See Branch.get_inventory_sha1."""
 
1201
        return self.get_revision(revision_id).inventory_sha1
 
1202
 
 
1203
    def get_revision_inventory(self, revision_id):
 
1204
        """See Branch.get_revision_inventory."""
 
1205
        # TODO: Unify this with get_inventory()
 
1206
        # bzr 0.0.6 and later imposes the constraint that the inventory_id
 
1207
        # must be the same as its revision, so this is trivial.
 
1208
        if revision_id == None:
 
1209
            # This does not make sense: if there is no revision,
 
1210
            # then it is the current tree inventory surely ?!
 
1211
            # and thus get_root_id() is something that looks at the last
 
1212
            # commit on the branch, and the get_root_id is an inventory check.
 
1213
            raise NotImplementedError
 
1214
            # return Inventory(self.get_root_id())
 
1215
        else:
 
1216
            return self.get_inventory(revision_id)
 
1217
 
 
1218
    @needs_read_lock
 
1219
    def revision_history(self):
 
1220
        """See Branch.revision_history."""
 
1221
        transaction = self.get_transaction()
 
1222
        history = transaction.map.find_revision_history()
 
1223
        if history is not None:
 
1224
            mutter("cache hit for revision-history in %s", self)
 
1225
            return list(history)
 
1226
        history = [l.rstrip('\r\n') for l in
 
1227
                self.controlfile('revision-history', 'r').readlines()]
 
1228
        transaction.map.add_revision_history(history)
 
1229
        # this call is disabled because revision_history is 
 
1230
        # not really an object yet, and the transaction is for objects.
 
1231
        # transaction.register_clean(history, precious=True)
 
1232
        return list(history)
 
1233
 
 
1234
    def update_revisions(self, other, stop_revision=None):
 
1235
        """See Branch.update_revisions."""
 
1236
        from bzrlib.fetch import greedy_fetch
 
1237
        if stop_revision is None:
 
1238
            stop_revision = other.last_revision()
 
1239
        ### Should this be checking is_ancestor instead of revision_history?
 
1240
        if (stop_revision is not None and 
 
1241
            stop_revision in self.revision_history()):
 
1242
            return
 
1243
        greedy_fetch(to_branch=self, from_branch=other,
 
1244
                     revision=stop_revision)
 
1245
        pullable_revs = self.pullable_revisions(other, stop_revision)
 
1246
        if len(pullable_revs) > 0:
 
1247
            self.append_revision(*pullable_revs)
 
1248
 
 
1249
    def pullable_revisions(self, other, stop_revision):
 
1250
        """See Branch.pullable_revisions."""
 
1251
        other_revno = other.revision_id_to_revno(stop_revision)
 
1252
        try:
 
1253
            return self.missing_revisions(other, other_revno)
 
1254
        except DivergedBranches, e:
 
1255
            try:
 
1256
                pullable_revs = get_intervening_revisions(self.last_revision(),
 
1257
                                                          stop_revision, self)
 
1258
                assert self.last_revision() not in pullable_revs
 
1259
                return pullable_revs
 
1260
            except bzrlib.errors.NotAncestor:
 
1261
                if is_ancestor(self.last_revision(), stop_revision, self):
 
1262
                    return []
 
1263
                else:
 
1264
                    raise e
 
1265
        
 
1266
    def revision_tree(self, revision_id):
 
1267
        """See Branch.revision_tree."""
 
1268
        # TODO: refactor this to use an existing revision object
 
1269
        # so we don't need to read it in twice.
 
1270
        if revision_id == None or revision_id == NULL_REVISION:
 
1271
            return EmptyTree()
 
1272
        else:
 
1273
            inv = self.get_revision_inventory(revision_id)
 
1274
            return RevisionTree(self, inv, revision_id)
 
1275
 
 
1276
    def basis_tree(self):
 
1277
        """See Branch.basis_tree."""
 
1278
        try:
 
1279
            revision_id = self.revision_history()[-1]
 
1280
            xml = self.working_tree().read_basis_inventory(revision_id)
 
1281
            inv = bzrlib.xml5.serializer_v5.read_inventory_from_string(xml)
 
1282
            return RevisionTree(self, inv, revision_id)
 
1283
        except (IndexError, NoSuchFile, NoWorkingTree), e:
 
1284
            return self.revision_tree(self.last_revision())
 
1285
 
 
1286
    def working_tree(self):
 
1287
        """See Branch.working_tree."""
 
1288
        from bzrlib.workingtree import WorkingTree
 
1289
        if self._transport.base.find('://') != -1:
 
1290
            raise NoWorkingTree(self.base)
 
1291
        return WorkingTree(self.base, branch=self)
 
1292
 
 
1293
    @needs_write_lock
 
1294
    def pull(self, source, overwrite=False):
 
1295
        """See Branch.pull."""
 
1296
        source.lock_read()
 
1297
        try:
 
1298
            old_count = len(self.revision_history())
 
1299
            try:
 
1300
                self.update_revisions(source)
 
1301
            except DivergedBranches:
 
1302
                if not overwrite:
 
1303
                    raise
 
1304
            if overwrite:
 
1305
                self.set_revision_history(source.revision_history())
 
1306
            new_count = len(self.revision_history())
 
1307
            return new_count - old_count
 
1308
        finally:
 
1309
            source.unlock()
 
1310
 
 
1311
    def get_parent(self):
 
1312
        """See Branch.get_parent."""
 
1313
        import errno
 
1314
        _locs = ['parent', 'pull', 'x-pull']
 
1315
        for l in _locs:
 
1316
            try:
 
1317
                return self.controlfile(l, 'r').read().strip('\n')
 
1318
            except NoSuchFile:
 
1319
                pass
 
1320
        return None
 
1321
 
 
1322
    def get_push_location(self):
 
1323
        """See Branch.get_push_location."""
 
1324
        config = bzrlib.config.BranchConfig(self)
 
1325
        push_loc = config.get_user_option('push_location')
 
1326
        return push_loc
 
1327
 
 
1328
    def set_push_location(self, location):
 
1329
        """See Branch.set_push_location."""
 
1330
        config = bzrlib.config.LocationConfig(self.base)
 
1331
        config.set_user_option('push_location', location)
 
1332
 
 
1333
    @needs_write_lock
 
1334
    def set_parent(self, url):
 
1335
        """See Branch.set_parent."""
 
1336
        # TODO: Maybe delete old location files?
 
1337
        from bzrlib.atomicfile import AtomicFile
 
1338
        f = AtomicFile(self.controlfilename('parent'))
 
1339
        try:
 
1340
            f.write(url + '\n')
 
1341
            f.commit()
 
1342
        finally:
 
1343
            f.close()
 
1344
 
 
1345
    def tree_config(self):
 
1346
        return TreeConfig(self)
 
1347
 
 
1348
    def sign_revision(self, revision_id, gpg_strategy):
 
1349
        """See Branch.sign_revision."""
 
1350
        plaintext = Testament.from_revision(self, revision_id).as_short_text()
 
1351
        self.store_revision_signature(gpg_strategy, plaintext, revision_id)
 
1352
 
 
1353
    @needs_write_lock
 
1354
    def store_revision_signature(self, gpg_strategy, plaintext, revision_id):
 
1355
        """See Branch.store_revision_signature."""
 
1356
        self.revision_store.add(StringIO(gpg_strategy.sign(plaintext)), 
 
1357
                                revision_id, "sig")
 
1358
 
 
1359
    def fileid_involved_between_revs(self, from_revid, to_revid):
 
1360
        """Find file_id(s) which are involved in the changes between revisions.
 
1361
 
 
1362
        This determines the set of revisions which are involved, and then
 
1363
        finds all file ids affected by those revisions.
 
1364
        """
 
1365
        # TODO: jam 20060119 This code assumes that w.inclusions will
 
1366
        #       always be correct. But because of the presence of ghosts
 
1367
        #       it is possible to be wrong.
 
1368
        #       One specific example from Robert Collins:
 
1369
        #       Two branches, with revisions ABC, and AD
 
1370
        #       C is a ghost merge of D.
 
1371
        #       Inclusions doesn't recognize D as an ancestor.
 
1372
        #       If D is ever merged in the future, the weave
 
1373
        #       won't be fixed, because AD never saw revision C
 
1374
        #       to cause a conflict which would force a reweave.
 
1375
        w = self._get_inventory_weave()
 
1376
        from_set = set(w.inclusions([w.lookup(from_revid)]))
 
1377
        to_set = set(w.inclusions([w.lookup(to_revid)]))
 
1378
        included = to_set.difference(from_set)
 
1379
        changed = map(w.idx_to_name, included)
 
1380
        return self._fileid_involved_by_set(changed)
 
1381
 
 
1382
    def fileid_involved(self, last_revid=None):
 
1383
        """Find all file_ids modified in the ancestry of last_revid.
 
1384
 
 
1385
        :param last_revid: If None, last_revision() will be used.
 
1386
        """
 
1387
        w = self._get_inventory_weave()
 
1388
        if not last_revid:
 
1389
            changed = set(w._names)
 
1390
        else:
 
1391
            included = w.inclusions([w.lookup(last_revid)])
 
1392
            changed = map(w.idx_to_name, included)
 
1393
        return self._fileid_involved_by_set(changed)
 
1394
 
 
1395
    def fileid_involved_by_set(self, changes):
 
1396
        """Find all file_ids modified by the set of revisions passed in.
 
1397
 
 
1398
        :param changes: A set() of revision ids
 
1399
        """
 
1400
        # TODO: jam 20060119 This line does *nothing*, remove it.
 
1401
        #       or better yet, change _fileid_involved_by_set so
 
1402
        #       that it takes the inventory weave, rather than
 
1403
        #       pulling it out by itself.
 
1404
        w = self._get_inventory_weave()
 
1405
        return self._fileid_involved_by_set(changes)
 
1406
 
 
1407
    def _fileid_involved_by_set(self, changes):
 
1408
        """Find the set of file-ids affected by the set of revisions.
 
1409
 
 
1410
        :param changes: A set() of revision ids.
 
1411
        :return: A set() of file ids.
 
1412
        
 
1413
        This peaks at the Weave, interpreting each line, looking to
 
1414
        see if it mentions one of the revisions. And if so, includes
 
1415
        the file id mentioned.
 
1416
        This expects both the Weave format, and the serialization
 
1417
        to have a single line per file/directory, and to have
 
1418
        fileid="" and revision="" on that line.
 
1419
        """
 
1420
        assert (isinstance(self._branch_format, BzrBranchFormat5) or
 
1421
                isinstance(self._branch_format, BzrBranchFormat6)), \
 
1422
            "fileid_involved only supported for branches which store inventory as xml"
 
1423
 
 
1424
        w = self._get_inventory_weave()
 
1425
        file_ids = set()
 
1426
        for line in w._weave:
 
1427
 
 
1428
            # it is ugly, but it is due to the weave structure
 
1429
            if not isinstance(line, basestring): continue
 
1430
 
 
1431
            start = line.find('file_id="')+9
 
1432
            if start < 9: continue
 
1433
            end = line.find('"', start)
 
1434
            assert end>= 0
 
1435
            file_id = xml.sax.saxutils.unescape(line[start:end])
 
1436
 
 
1437
            # check if file_id is already present
 
1438
            if file_id in file_ids: continue
 
1439
 
 
1440
            start = line.find('revision="')+10
 
1441
            if start < 10: continue
 
1442
            end = line.find('"', start)
 
1443
            assert end>= 0
 
1444
            revision_id = xml.sax.saxutils.unescape(line[start:end])
 
1445
 
 
1446
            if revision_id in changes:
 
1447
                file_ids.add(file_id)
 
1448
 
 
1449
        return file_ids
 
1450
 
 
1451
 
 
1452
Branch.set_default_initializer(BzrBranch._initialize)
 
1453
 
 
1454
 
 
1455
class BranchTestProviderAdapter(object):
 
1456
    """A tool to generate a suite testing multiple branch formats at once.
 
1457
 
 
1458
    This is done by copying the test once for each transport and injecting
 
1459
    the transport_server, transport_readonly_server, and branch_format
 
1460
    classes into each copy. Each copy is also given a new id() to make it
 
1461
    easy to identify.
 
1462
    """
 
1463
 
 
1464
    def __init__(self, transport_server, transport_readonly_server, formats):
 
1465
        self._transport_server = transport_server
 
1466
        self._transport_readonly_server = transport_readonly_server
 
1467
        self._formats = formats
 
1468
    
 
1469
    def adapt(self, test):
 
1470
        result = TestSuite()
 
1471
        for format in self._formats:
 
1472
            new_test = deepcopy(test)
 
1473
            new_test.transport_server = self._transport_server
 
1474
            new_test.transport_readonly_server = self._transport_readonly_server
 
1475
            new_test.branch_format = format
 
1476
            def make_new_test_id():
 
1477
                new_id = "%s(%s)" % (new_test.id(), format.__class__.__name__)
 
1478
                return lambda: new_id
 
1479
            new_test.id = make_new_test_id()
 
1480
            result.addTest(new_test)
 
1481
        return result
 
1482
 
 
1483
 
 
1484
class ScratchBranch(BzrBranch):
 
1485
    """Special test class: a branch that cleans up after itself.
 
1486
 
 
1487
    >>> b = ScratchBranch()
 
1488
    >>> isdir(b.base)
 
1489
    True
 
1490
    >>> bd = b.base
 
1491
    >>> b._transport.__del__()
 
1492
    >>> isdir(bd)
 
1493
    False
 
1494
    """
 
1495
 
 
1496
    def __init__(self, files=[], dirs=[], transport=None):
 
1497
        """Make a test branch.
 
1498
 
 
1499
        This creates a temporary directory and runs init-tree in it.
 
1500
 
 
1501
        If any files are listed, they are created in the working copy.
 
1502
        """
 
1503
        if transport is None:
 
1504
            transport = bzrlib.transport.local.ScratchTransport()
 
1505
            Branch.initialize(transport.base)
 
1506
            super(ScratchBranch, self).__init__(transport)
 
1507
        else:
 
1508
            super(ScratchBranch, self).__init__(transport)
 
1509
 
 
1510
        for d in dirs:
 
1511
            self._transport.mkdir(d)
 
1512
            
 
1513
        for f in files:
 
1514
            self._transport.put(f, 'content of %s' % f)
 
1515
 
 
1516
 
 
1517
    def clone(self):
 
1518
        """
 
1519
        >>> orig = ScratchBranch(files=["file1", "file2"])
 
1520
        >>> clone = orig.clone()
 
1521
        >>> if os.name != 'nt':
 
1522
        ...   os.path.samefile(orig.base, clone.base)
 
1523
        ... else:
 
1524
        ...   orig.base == clone.base
 
1525
        ...
 
1526
        False
 
1527
        >>> os.path.isfile(pathjoin(clone.base, "file1"))
 
1528
        True
 
1529
        """
 
1530
        from shutil import copytree
 
1531
        from bzrlib.osutils import mkdtemp
 
1532
        base = mkdtemp()
 
1533
        os.rmdir(base)
 
1534
        copytree(self.base, base, symlinks=True)
 
1535
        return ScratchBranch(
 
1536
            transport=bzrlib.transport.local.ScratchTransport(base))
 
1537
    
 
1538
 
 
1539
######################################################################
 
1540
# predicates
 
1541
 
 
1542
 
 
1543
def is_control_file(filename):
 
1544
    ## FIXME: better check
 
1545
    filename = normpath(filename)
 
1546
    while filename != '':
 
1547
        head, tail = os.path.split(filename)
 
1548
        ## mutter('check %r for control file' % ((head, tail),))
 
1549
        if tail == bzrlib.BZRDIR:
 
1550
            return True
 
1551
        if filename == head:
 
1552
            break
 
1553
        filename = head
 
1554
    return False