~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: abentley
  • Date: 2005-10-14 03:50:50 UTC
  • mto: (1185.25.1)
  • mto: This revision was merged to the branch mainline in revision 1460.
  • Revision ID: abentley@lappy-20051014035050-d779472ccb599a51
semi-broke merge

Show diffs side-by-side

added added

removed removed

Lines of Context:
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
16
 
17
17
 
18
 
import shutil
19
18
import sys
20
19
import os
21
20
import errno
24
23
 
25
24
 
26
25
import bzrlib
 
26
from bzrlib.inventory import InventoryEntry
27
27
import bzrlib.inventory as inventory
28
28
from bzrlib.trace import mutter, note
29
 
from bzrlib.osutils import (isdir, quotefn,
 
29
from bzrlib.osutils import (isdir, quotefn, compact_date, rand_bytes, 
30
30
                            rename, splitpath, sha_file, appendpath, 
31
 
                            file_kind, abspath)
 
31
                            file_kind)
32
32
import bzrlib.errors as errors
33
33
from bzrlib.errors import (BzrError, InvalidRevisionNumber, InvalidRevisionId,
34
34
                           NoSuchRevision, HistoryMissing, NotBranchError,
35
35
                           DivergedBranches, LockError, UnlistableStore,
36
 
                           UnlistableBranch, NoSuchFile, NotVersionedError,
37
 
                           NoWorkingTree)
 
36
                           UnlistableBranch, NoSuchFile)
38
37
from bzrlib.textui import show_status
39
 
from bzrlib.revision import (Revision, is_ancestor, get_intervening_revisions,
40
 
                             NULL_REVISION)
41
 
 
 
38
from bzrlib.revision import Revision
42
39
from bzrlib.delta import compare_trees
43
40
from bzrlib.tree import EmptyTree, RevisionTree
44
41
from bzrlib.inventory import Inventory
45
42
from bzrlib.store import copy_all
 
43
from bzrlib.store.compressed_text import CompressedTextStore
46
44
from bzrlib.store.text import TextStore
47
45
from bzrlib.store.weave import WeaveStore
48
 
from bzrlib.testament import Testament
49
46
import bzrlib.transactions as transactions
50
47
from bzrlib.transport import Transport, get_transport
51
48
import bzrlib.xml5
52
49
import bzrlib.ui
53
 
from config import TreeConfig
54
50
 
55
51
 
56
52
BZR_BRANCH_FORMAT_4 = "Bazaar-NG branch, format 0.0.4\n"
68
64
    # XXX: leave this here for about one release, then remove it
69
65
    raise NotImplementedError('find_branch() is not supported anymore, '
70
66
                              'please use one of the new branch constructors')
71
 
 
72
 
 
73
 
def needs_read_lock(unbound):
74
 
    """Decorate unbound to take out and release a read lock."""
75
 
    def decorated(self, *args, **kwargs):
76
 
        self.lock_read()
77
 
        try:
78
 
            return unbound(self, *args, **kwargs)
79
 
        finally:
80
 
            self.unlock()
81
 
    return decorated
82
 
 
83
 
 
84
 
def needs_write_lock(unbound):
85
 
    """Decorate unbound to take out and release a write lock."""
86
 
    def decorated(self, *args, **kwargs):
87
 
        self.lock_write()
88
 
        try:
89
 
            return unbound(self, *args, **kwargs)
90
 
        finally:
91
 
            self.unlock()
92
 
    return decorated
 
67
def _relpath(base, path):
 
68
    """Return path relative to base, or raise exception.
 
69
 
 
70
    The path may be either an absolute path or a path relative to the
 
71
    current working directory.
 
72
 
 
73
    Lifted out of Branch.relpath for ease of testing.
 
74
 
 
75
    os.path.commonprefix (python2.4) has a bad bug that it works just
 
76
    on string prefixes, assuming that '/u' is a prefix of '/u2'.  This
 
77
    avoids that problem."""
 
78
    rp = os.path.abspath(path)
 
79
 
 
80
    s = []
 
81
    head = rp
 
82
    while len(head) >= len(base):
 
83
        if head == base:
 
84
            break
 
85
        head, tail = os.path.split(head)
 
86
        if tail:
 
87
            s.insert(0, tail)
 
88
    else:
 
89
        raise NotBranchError("path %r is not within branch %r" % (rp, base))
 
90
 
 
91
    return os.sep.join(s)
 
92
        
93
93
 
94
94
######################################################################
95
95
# branch objects
110
110
        """Open a branch which may be of an old format.
111
111
        
112
112
        Only local branches are supported."""
113
 
        return BzrBranch(get_transport(base), relax_version_check=True)
 
113
        return _Branch(get_transport(base), relax_version_check=True)
114
114
        
115
115
    @staticmethod
116
116
    def open(base):
117
117
        """Open an existing branch, rooted at 'base' (url)"""
118
118
        t = get_transport(base)
119
119
        mutter("trying to open %r with transport %r", base, t)
120
 
        return BzrBranch(t)
 
120
        return _Branch(t)
121
121
 
122
122
    @staticmethod
123
123
    def open_containing(url):
127
127
 
128
128
        Basically we keep looking up until we find the control directory or
129
129
        run into the root.  If there isn't one, raises NotBranchError.
130
 
        If there is one, it is returned, along with the unused portion of url.
131
130
        """
132
131
        t = get_transport(url)
133
132
        while True:
134
133
            try:
135
 
                return BzrBranch(t), t.relpath(url)
 
134
                return _Branch(t)
136
135
            except NotBranchError:
137
136
                pass
138
137
            new_t = t.clone('..')
139
138
            if new_t.base == t.base:
140
139
                # reached the root, whatever that may be
141
 
                raise NotBranchError(path=url)
 
140
                raise NotBranchError('%s is not in a branch' % url)
142
141
            t = new_t
143
142
 
144
143
    @staticmethod
145
144
    def initialize(base):
146
145
        """Create a new branch, rooted at 'base' (url)"""
147
146
        t = get_transport(base)
148
 
        return BzrBranch(t, init=True)
 
147
        return _Branch(t, init=True)
149
148
 
150
149
    def setup_caching(self, cache_root):
151
150
        """Subclasses that care about caching should override this, and set
153
152
        """
154
153
        self.cache_root = cache_root
155
154
 
156
 
    def _get_nick(self):
157
 
        cfg = self.tree_config()
158
 
        return cfg.get_option(u"nickname", default=self.base.split('/')[-1])
159
 
 
160
 
    def _set_nick(self, nick):
161
 
        cfg = self.tree_config()
162
 
        cfg.set_option(nick, "nickname")
163
 
        assert cfg.get_option("nickname") == nick
164
 
 
165
 
    nick = property(_get_nick, _set_nick)
166
 
        
167
 
    def push_stores(self, branch_to):
168
 
        """Copy the content of this branches store to branch_to."""
169
 
        raise NotImplementedError('push_stores is abstract')
170
 
 
171
 
    def get_transaction(self):
172
 
        """Return the current active transaction.
173
 
 
174
 
        If no transaction is active, this returns a passthrough object
175
 
        for which all data is immediately flushed and no caching happens.
176
 
        """
177
 
        raise NotImplementedError('get_transaction is abstract')
178
 
 
179
 
    def lock_write(self):
180
 
        raise NotImplementedError('lock_write is abstract')
181
 
        
182
 
    def lock_read(self):
183
 
        raise NotImplementedError('lock_read is abstract')
184
 
 
185
 
    def unlock(self):
186
 
        raise NotImplementedError('unlock is abstract')
187
 
 
188
 
    def abspath(self, name):
189
 
        """Return absolute filename for something in the branch
190
 
        
191
 
        XXX: Robert Collins 20051017 what is this used for? why is it a branch
192
 
        method and not a tree method.
193
 
        """
194
 
        raise NotImplementedError('abspath is abstract')
195
 
 
196
 
    def controlfilename(self, file_or_path):
197
 
        """Return location relative to branch."""
198
 
        raise NotImplementedError('controlfilename is abstract')
199
 
 
200
 
    def controlfile(self, file_or_path, mode='r'):
201
 
        """Open a control file for this branch.
202
 
 
203
 
        There are two classes of file in the control directory: text
204
 
        and binary.  binary files are untranslated byte streams.  Text
205
 
        control files are stored with Unix newlines and in UTF-8, even
206
 
        if the platform or locale defaults are different.
207
 
 
208
 
        Controlfiles should almost never be opened in write mode but
209
 
        rather should be atomically copied and replaced using atomicfile.
210
 
        """
211
 
        raise NotImplementedError('controlfile is abstract')
212
 
 
213
 
    def put_controlfile(self, path, f, encode=True):
214
 
        """Write an entry as a controlfile.
215
 
 
216
 
        :param path: The path to put the file, relative to the .bzr control
217
 
                     directory
218
 
        :param f: A file-like or string object whose contents should be copied.
219
 
        :param encode:  If true, encode the contents as utf-8
220
 
        """
221
 
        raise NotImplementedError('put_controlfile is abstract')
222
 
 
223
 
    def put_controlfiles(self, files, encode=True):
224
 
        """Write several entries as controlfiles.
225
 
 
226
 
        :param files: A list of [(path, file)] pairs, where the path is the directory
227
 
                      underneath the bzr control directory
228
 
        :param encode:  If true, encode the contents as utf-8
229
 
        """
230
 
        raise NotImplementedError('put_controlfiles is abstract')
231
 
 
232
 
    def get_root_id(self):
233
 
        """Return the id of this branches root"""
234
 
        raise NotImplementedError('get_root_id is abstract')
235
 
 
236
 
    def set_root_id(self, file_id):
237
 
        raise NotImplementedError('set_root_id is abstract')
238
 
 
239
 
    def print_file(self, file, revno):
240
 
        """Print `file` to stdout."""
241
 
        raise NotImplementedError('print_file is abstract')
242
 
 
243
 
    def append_revision(self, *revision_ids):
244
 
        raise NotImplementedError('append_revision is abstract')
245
 
 
246
 
    def set_revision_history(self, rev_history):
247
 
        raise NotImplementedError('set_revision_history is abstract')
248
 
 
249
 
    def has_revision(self, revision_id):
250
 
        """True if this branch has a copy of the revision.
251
 
 
252
 
        This does not necessarily imply the revision is merge
253
 
        or on the mainline."""
254
 
        raise NotImplementedError('has_revision is abstract')
255
 
 
256
 
    def get_revision_xml(self, revision_id):
257
 
        raise NotImplementedError('get_revision_xml is abstract')
258
 
 
259
 
    def get_revision(self, revision_id):
260
 
        """Return the Revision object for a named revision"""
261
 
        raise NotImplementedError('get_revision is abstract')
262
 
 
263
 
    def get_revision_delta(self, revno):
264
 
        """Return the delta for one revision.
265
 
 
266
 
        The delta is relative to its mainline predecessor, or the
267
 
        empty tree for revision 1.
268
 
        """
269
 
        assert isinstance(revno, int)
270
 
        rh = self.revision_history()
271
 
        if not (1 <= revno <= len(rh)):
272
 
            raise InvalidRevisionNumber(revno)
273
 
 
274
 
        # revno is 1-based; list is 0-based
275
 
 
276
 
        new_tree = self.revision_tree(rh[revno-1])
277
 
        if revno == 1:
278
 
            old_tree = EmptyTree()
279
 
        else:
280
 
            old_tree = self.revision_tree(rh[revno-2])
281
 
 
282
 
        return compare_trees(old_tree, new_tree)
283
 
 
284
 
    def get_revision_sha1(self, revision_id):
285
 
        """Hash the stored value of a revision, and return it."""
286
 
        raise NotImplementedError('get_revision_sha1 is abstract')
287
 
 
288
 
    def get_ancestry(self, revision_id):
289
 
        """Return a list of revision-ids integrated by a revision.
290
 
        
291
 
        This currently returns a list, but the ordering is not guaranteed:
292
 
        treat it as a set.
293
 
        """
294
 
        raise NotImplementedError('get_ancestry is abstract')
295
 
 
296
 
    def get_inventory(self, revision_id):
297
 
        """Get Inventory object by hash."""
298
 
        raise NotImplementedError('get_inventory is abstract')
299
 
 
300
 
    def get_inventory_xml(self, revision_id):
301
 
        """Get inventory XML as a file object."""
302
 
        raise NotImplementedError('get_inventory_xml is abstract')
303
 
 
304
 
    def get_inventory_sha1(self, revision_id):
305
 
        """Return the sha1 hash of the inventory entry."""
306
 
        raise NotImplementedError('get_inventory_sha1 is abstract')
307
 
 
308
 
    def get_revision_inventory(self, revision_id):
309
 
        """Return inventory of a past revision."""
310
 
        raise NotImplementedError('get_revision_inventory is abstract')
311
 
 
312
 
    def revision_history(self):
313
 
        """Return sequence of revision hashes on to this branch."""
314
 
        raise NotImplementedError('revision_history is abstract')
315
 
 
316
 
    def revno(self):
317
 
        """Return current revision number for this branch.
318
 
 
319
 
        That is equivalent to the number of revisions committed to
320
 
        this branch.
321
 
        """
322
 
        return len(self.revision_history())
323
 
 
324
 
    def last_revision(self):
325
 
        """Return last patch hash, or None if no history."""
326
 
        ph = self.revision_history()
327
 
        if ph:
328
 
            return ph[-1]
329
 
        else:
330
 
            return None
331
 
 
332
 
    def missing_revisions(self, other, stop_revision=None, diverged_ok=False):
333
 
        """Return a list of new revisions that would perfectly fit.
334
 
        
335
 
        If self and other have not diverged, return a list of the revisions
336
 
        present in other, but missing from self.
337
 
 
338
 
        >>> from bzrlib.commit import commit
339
 
        >>> bzrlib.trace.silent = True
340
 
        >>> br1 = ScratchBranch()
341
 
        >>> br2 = ScratchBranch()
342
 
        >>> br1.missing_revisions(br2)
343
 
        []
344
 
        >>> commit(br2, "lala!", rev_id="REVISION-ID-1")
345
 
        >>> br1.missing_revisions(br2)
346
 
        [u'REVISION-ID-1']
347
 
        >>> br2.missing_revisions(br1)
348
 
        []
349
 
        >>> commit(br1, "lala!", rev_id="REVISION-ID-1")
350
 
        >>> br1.missing_revisions(br2)
351
 
        []
352
 
        >>> commit(br2, "lala!", rev_id="REVISION-ID-2A")
353
 
        >>> br1.missing_revisions(br2)
354
 
        [u'REVISION-ID-2A']
355
 
        >>> commit(br1, "lala!", rev_id="REVISION-ID-2B")
356
 
        >>> br1.missing_revisions(br2)
357
 
        Traceback (most recent call last):
358
 
        DivergedBranches: These branches have diverged.
359
 
        """
360
 
        self_history = self.revision_history()
361
 
        self_len = len(self_history)
362
 
        other_history = other.revision_history()
363
 
        other_len = len(other_history)
364
 
        common_index = min(self_len, other_len) -1
365
 
        if common_index >= 0 and \
366
 
            self_history[common_index] != other_history[common_index]:
367
 
            raise DivergedBranches(self, other)
368
 
 
369
 
        if stop_revision is None:
370
 
            stop_revision = other_len
371
 
        else:
372
 
            assert isinstance(stop_revision, int)
373
 
            if stop_revision > other_len:
374
 
                raise bzrlib.errors.NoSuchRevision(self, stop_revision)
375
 
        return other_history[self_len:stop_revision]
376
 
    
377
 
    def update_revisions(self, other, stop_revision=None):
378
 
        """Pull in new perfect-fit revisions."""
379
 
        raise NotImplementedError('update_revisions is abstract')
380
 
 
381
 
    def pullable_revisions(self, other, stop_revision):
382
 
        raise NotImplementedError('pullable_revisions is abstract')
383
 
        
384
 
    def revision_id_to_revno(self, revision_id):
385
 
        """Given a revision id, return its revno"""
386
 
        if revision_id is None:
387
 
            return 0
388
 
        history = self.revision_history()
389
 
        try:
390
 
            return history.index(revision_id) + 1
391
 
        except ValueError:
392
 
            raise bzrlib.errors.NoSuchRevision(self, revision_id)
393
 
 
394
 
    def get_rev_id(self, revno, history=None):
395
 
        """Find the revision id of the specified revno."""
396
 
        if revno == 0:
397
 
            return None
398
 
        if history is None:
399
 
            history = self.revision_history()
400
 
        elif revno <= 0 or revno > len(history):
401
 
            raise bzrlib.errors.NoSuchRevision(self, revno)
402
 
        return history[revno - 1]
403
 
 
404
 
    def revision_tree(self, revision_id):
405
 
        """Return Tree for a revision on this branch.
406
 
 
407
 
        `revision_id` may be None for the null revision, in which case
408
 
        an `EmptyTree` is returned."""
409
 
        raise NotImplementedError('revision_tree is abstract')
410
 
 
411
 
    def working_tree(self):
412
 
        """Return a `Tree` for the working copy if this is a local branch."""
413
 
        raise NotImplementedError('working_tree is abstract')
414
 
 
415
 
    def pull(self, source, overwrite=False):
416
 
        raise NotImplementedError('pull is abstract')
417
 
 
418
 
    def basis_tree(self):
419
 
        """Return `Tree` object for last revision.
420
 
 
421
 
        If there are no revisions yet, return an `EmptyTree`.
422
 
        """
423
 
        return self.revision_tree(self.last_revision())
424
 
 
425
 
    def rename_one(self, from_rel, to_rel):
426
 
        """Rename one file.
427
 
 
428
 
        This can change the directory or the filename or both.
429
 
        """
430
 
        raise NotImplementedError('rename_one is abstract')
431
 
 
432
 
    def move(self, from_paths, to_name):
433
 
        """Rename files.
434
 
 
435
 
        to_name must exist as a versioned directory.
436
 
 
437
 
        If to_name exists and is a directory, the files are moved into
438
 
        it, keeping their old names.  If it is a directory, 
439
 
 
440
 
        Note that to_name is only the last component of the new name;
441
 
        this doesn't change the directory.
442
 
 
443
 
        This returns a list of (from_path, to_path) pairs for each
444
 
        entry that is moved.
445
 
        """
446
 
        raise NotImplementedError('move is abstract')
447
 
 
448
 
    def get_parent(self):
449
 
        """Return the parent location of the branch.
450
 
 
451
 
        This is the default location for push/pull/missing.  The usual
452
 
        pattern is that the user can override it by specifying a
453
 
        location.
454
 
        """
455
 
        raise NotImplementedError('get_parent is abstract')
456
 
 
457
 
    def get_push_location(self):
458
 
        """Return the None or the location to push this branch to."""
459
 
        raise NotImplementedError('get_push_location is abstract')
460
 
 
461
 
    def set_push_location(self, location):
462
 
        """Set a new push location for this branch."""
463
 
        raise NotImplementedError('set_push_location is abstract')
464
 
 
465
 
    def set_parent(self, url):
466
 
        raise NotImplementedError('set_parent is abstract')
467
 
 
468
 
    def check_revno(self, revno):
469
 
        """\
470
 
        Check whether a revno corresponds to any revision.
471
 
        Zero (the NULL revision) is considered valid.
472
 
        """
473
 
        if revno != 0:
474
 
            self.check_real_revno(revno)
475
 
            
476
 
    def check_real_revno(self, revno):
477
 
        """\
478
 
        Check whether a revno corresponds to a real revision.
479
 
        Zero (the NULL revision) is considered invalid
480
 
        """
481
 
        if revno < 1 or revno > self.revno():
482
 
            raise InvalidRevisionNumber(revno)
483
 
        
484
 
    def sign_revision(self, revision_id, gpg_strategy):
485
 
        raise NotImplementedError('sign_revision is abstract')
486
 
 
487
 
    def store_revision_signature(self, gpg_strategy, plaintext, revision_id):
488
 
        raise NotImplementedError('store_revision_signature is abstract')
489
 
 
490
 
class BzrBranch(Branch):
 
155
 
 
156
class _Branch(Branch):
491
157
    """A branch stored in the actual filesystem.
492
158
 
493
159
    Note that it's "local" in the context of the filesystem; it doesn't
519
185
    REVISION_NAMESPACES = {}
520
186
 
521
187
    def push_stores(self, branch_to):
522
 
        """See Branch.push_stores."""
 
188
        """Copy the content of this branches store to branch_to."""
523
189
        if (self._branch_format != branch_to._branch_format
524
190
            or self._branch_format != 4):
525
191
            from bzrlib.fetch import greedy_fetch
543
209
        """Create new branch object at a particular location.
544
210
 
545
211
        transport -- A Transport object, defining how to access files.
 
212
                (If a string, transport.transport() will be used to
 
213
                create a Transport object)
546
214
        
547
215
        init -- If True, create new control files in a previously
548
216
             unversioned directory.  If False, the branch must already
569
237
            # some existing branches where there's a mixture; we probably 
570
238
            # still want the option to look for both.
571
239
            relpath = self._rel_controlfilename(name)
572
 
            store = TextStore(self._transport.clone(relpath),
573
 
                              prefixed=prefixed,
574
 
                              compressed=compressed)
 
240
            if compressed:
 
241
                store = CompressedTextStore(self._transport.clone(relpath),
 
242
                                            prefixed=prefixed)
 
243
            else:
 
244
                store = TextStore(self._transport.clone(relpath),
 
245
                                  prefixed=prefixed)
575
246
            #if self._transport.should_cache():
576
247
            #    cache_path = os.path.join(self.cache_root, name)
577
248
            #    os.mkdir(cache_path)
589
260
            self.text_store = get_store('text-store')
590
261
            self.revision_store = get_store('revision-store')
591
262
        elif self._branch_format == 5:
592
 
            self.control_weaves = get_weave('')
 
263
            self.control_weaves = get_weave([])
593
264
            self.weave_store = get_weave('weaves')
594
265
            self.revision_store = get_store('revision-store', compressed=False)
595
266
        elif self._branch_format == 6:
596
 
            self.control_weaves = get_weave('')
 
267
            self.control_weaves = get_weave([])
597
268
            self.weave_store = get_weave('weaves', prefixed=True)
598
269
            self.revision_store = get_store('revision-store', compressed=False,
599
270
                                            prefixed=True)
600
 
        self.revision_store.register_suffix('sig')
601
271
        self._transaction = None
602
272
 
603
273
    def __str__(self):
604
274
        return '%s(%r)' % (self.__class__.__name__, self._transport.base)
605
275
 
 
276
 
606
277
    __repr__ = __str__
607
278
 
 
279
 
608
280
    def __del__(self):
609
281
        if self._lock_mode or self._lock:
610
282
            # XXX: This should show something every time, and be suitable for
620
292
        # should never expect their __del__ function to run.
621
293
        if hasattr(self, 'cache_root') and self.cache_root is not None:
622
294
            try:
 
295
                import shutil
623
296
                shutil.rmtree(self.cache_root)
624
297
            except:
625
298
                pass
630
303
            return self._transport.base
631
304
        return None
632
305
 
633
 
    base = property(_get_base, doc="The URL for the root of this branch.")
 
306
    base = property(_get_base)
634
307
 
635
308
    def _finish_transaction(self):
636
309
        """Exit the current transaction."""
642
315
        transaction.finish()
643
316
 
644
317
    def get_transaction(self):
645
 
        """See Branch.get_transaction."""
 
318
        """Return the current active transaction.
 
319
 
 
320
        If no transaction is active, this returns a passthrough object
 
321
        for which all data is immedaitely flushed and no caching happens.
 
322
        """
646
323
        if self._transaction is None:
647
324
            return transactions.PassThroughTransaction()
648
325
        else:
700
377
            self._lock_mode = self._lock_count = None
701
378
 
702
379
    def abspath(self, name):
703
 
        """See Branch.abspath."""
 
380
        """Return absolute filename for something in the branch"""
704
381
        return self._transport.abspath(name)
705
382
 
 
383
    def relpath(self, path):
 
384
        """Return path relative to this branch of something inside it.
 
385
 
 
386
        Raises an error if path is not in this branch."""
 
387
        return self._transport.relpath(path)
 
388
 
 
389
 
706
390
    def _rel_controlfilename(self, file_or_path):
707
 
        if not isinstance(file_or_path, basestring):
708
 
            file_or_path = '/'.join(file_or_path)
709
 
        if file_or_path == '':
710
 
            return bzrlib.BZRDIR
711
 
        return bzrlib.transport.urlescape(bzrlib.BZRDIR + '/' + file_or_path)
 
391
        if isinstance(file_or_path, basestring):
 
392
            file_or_path = [file_or_path]
 
393
        return [bzrlib.BZRDIR] + file_or_path
712
394
 
713
395
    def controlfilename(self, file_or_path):
714
 
        """See Branch.controlfilename."""
 
396
        """Return location relative to branch."""
715
397
        return self._transport.abspath(self._rel_controlfilename(file_or_path))
716
398
 
 
399
 
717
400
    def controlfile(self, file_or_path, mode='r'):
718
 
        """See Branch.controlfile."""
 
401
        """Open a control file for this branch.
 
402
 
 
403
        There are two classes of file in the control directory: text
 
404
        and binary.  binary files are untranslated byte streams.  Text
 
405
        control files are stored with Unix newlines and in UTF-8, even
 
406
        if the platform or locale defaults are different.
 
407
 
 
408
        Controlfiles should almost never be opened in write mode but
 
409
        rather should be atomically copied and replaced using atomicfile.
 
410
        """
719
411
        import codecs
720
412
 
721
413
        relpath = self._rel_controlfilename(file_or_path)
726
418
        elif mode == 'wb':
727
419
            raise BzrError("Branch.controlfile(mode='wb') is not supported, use put_controlfiles")
728
420
        elif mode == 'r':
729
 
            # XXX: Do we really want errors='replace'?   Perhaps it should be
730
 
            # an error, or at least reported, if there's incorrectly-encoded
731
 
            # data inside a file.
732
 
            # <https://launchpad.net/products/bzr/+bug/3823>
733
421
            return codecs.getreader('utf-8')(self._transport.get(relpath), errors='replace')
734
422
        elif mode == 'w':
735
423
            raise BzrError("Branch.controlfile(mode='w') is not supported, use put_controlfiles")
737
425
            raise BzrError("invalid controlfile mode %r" % mode)
738
426
 
739
427
    def put_controlfile(self, path, f, encode=True):
740
 
        """See Branch.put_controlfile."""
 
428
        """Write an entry as a controlfile.
 
429
 
 
430
        :param path: The path to put the file, relative to the .bzr control
 
431
                     directory
 
432
        :param f: A file-like or string object whose contents should be copied.
 
433
        :param encode:  If true, encode the contents as utf-8
 
434
        """
741
435
        self.put_controlfiles([(path, f)], encode=encode)
742
436
 
743
437
    def put_controlfiles(self, files, encode=True):
744
 
        """See Branch.put_controlfiles."""
 
438
        """Write several entries as controlfiles.
 
439
 
 
440
        :param files: A list of [(path, file)] pairs, where the path is the directory
 
441
                      underneath the bzr control directory
 
442
        :param encode:  If true, encode the contents as utf-8
 
443
        """
745
444
        import codecs
746
445
        ctrl_files = []
747
446
        for path, f in files:
800
499
        try:
801
500
            fmt = self.controlfile('branch-format', 'r').read()
802
501
        except NoSuchFile:
803
 
            raise NotBranchError(path=self.base)
 
502
            raise NotBranchError(self.base)
804
503
        mutter("got branch format %r", fmt)
805
504
        if fmt == BZR_BRANCH_FORMAT_6:
806
505
            self._branch_format = 6
817
516
                            'or remove the .bzr directory'
818
517
                            ' and "bzr init" again'])
819
518
 
820
 
    @needs_read_lock
821
519
    def get_root_id(self):
822
 
        """See Branch.get_root_id."""
823
 
        inv = self.get_inventory(self.last_revision())
 
520
        """Return the id of this branches root"""
 
521
        inv = self.read_working_inventory()
824
522
        return inv.root.file_id
825
523
 
826
 
    @needs_read_lock
 
524
    def set_root_id(self, file_id):
 
525
        inv = self.read_working_inventory()
 
526
        orig_root_id = inv.root.file_id
 
527
        del inv._byid[inv.root.file_id]
 
528
        inv.root.file_id = file_id
 
529
        inv._byid[inv.root.file_id] = inv.root
 
530
        for fid in inv:
 
531
            entry = inv[fid]
 
532
            if entry.parent_id in (None, orig_root_id):
 
533
                entry.parent_id = inv.root.file_id
 
534
        self._write_inventory(inv)
 
535
 
 
536
    def read_working_inventory(self):
 
537
        """Read the working inventory."""
 
538
        self.lock_read()
 
539
        try:
 
540
            # ElementTree does its own conversion from UTF-8, so open in
 
541
            # binary.
 
542
            f = self.controlfile('inventory', 'rb')
 
543
            return bzrlib.xml5.serializer_v5.read_inventory(f)
 
544
        finally:
 
545
            self.unlock()
 
546
            
 
547
 
 
548
    def _write_inventory(self, inv):
 
549
        """Update the working inventory.
 
550
 
 
551
        That is to say, the inventory describing changes underway, that
 
552
        will be committed to the next revision.
 
553
        """
 
554
        from cStringIO import StringIO
 
555
        self.lock_write()
 
556
        try:
 
557
            sio = StringIO()
 
558
            bzrlib.xml5.serializer_v5.write_inventory(inv, sio)
 
559
            sio.seek(0)
 
560
            # Transport handles atomicity
 
561
            self.put_controlfile('inventory', sio)
 
562
        finally:
 
563
            self.unlock()
 
564
        
 
565
        mutter('wrote working inventory')
 
566
            
 
567
    inventory = property(read_working_inventory, _write_inventory, None,
 
568
                         """Inventory for the working copy.""")
 
569
 
 
570
    def add(self, files, ids=None):
 
571
        """Make files versioned.
 
572
 
 
573
        Note that the command line normally calls smart_add instead,
 
574
        which can automatically recurse.
 
575
 
 
576
        This puts the files in the Added state, so that they will be
 
577
        recorded by the next commit.
 
578
 
 
579
        files
 
580
            List of paths to add, relative to the base of the tree.
 
581
 
 
582
        ids
 
583
            If set, use these instead of automatically generated ids.
 
584
            Must be the same length as the list of files, but may
 
585
            contain None for ids that are to be autogenerated.
 
586
 
 
587
        TODO: Perhaps have an option to add the ids even if the files do
 
588
              not (yet) exist.
 
589
 
 
590
        TODO: Perhaps yield the ids and paths as they're added.
 
591
        """
 
592
        # TODO: Re-adding a file that is removed in the working copy
 
593
        # should probably put it back with the previous ID.
 
594
        if isinstance(files, basestring):
 
595
            assert(ids is None or isinstance(ids, basestring))
 
596
            files = [files]
 
597
            if ids is not None:
 
598
                ids = [ids]
 
599
 
 
600
        if ids is None:
 
601
            ids = [None] * len(files)
 
602
        else:
 
603
            assert(len(ids) == len(files))
 
604
 
 
605
        self.lock_write()
 
606
        try:
 
607
            inv = self.read_working_inventory()
 
608
            for f,file_id in zip(files, ids):
 
609
                if is_control_file(f):
 
610
                    raise BzrError("cannot add control file %s" % quotefn(f))
 
611
 
 
612
                fp = splitpath(f)
 
613
 
 
614
                if len(fp) == 0:
 
615
                    raise BzrError("cannot add top-level %r" % f)
 
616
 
 
617
                fullpath = os.path.normpath(self.abspath(f))
 
618
 
 
619
                try:
 
620
                    kind = file_kind(fullpath)
 
621
                except OSError:
 
622
                    # maybe something better?
 
623
                    raise BzrError('cannot add: not a regular file, symlink or directory: %s' % quotefn(f))
 
624
 
 
625
                if not InventoryEntry.versionable_kind(kind):
 
626
                    raise BzrError('cannot add: not a versionable file ('
 
627
                                   'i.e. regular file, symlink or directory): %s' % quotefn(f))
 
628
 
 
629
                if file_id is None:
 
630
                    file_id = gen_file_id(f)
 
631
                inv.add_path(f, kind=kind, file_id=file_id)
 
632
 
 
633
                mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
 
634
 
 
635
            self._write_inventory(inv)
 
636
        finally:
 
637
            self.unlock()
 
638
            
 
639
 
827
640
    def print_file(self, file, revno):
828
 
        """See Branch.print_file."""
829
 
        tree = self.revision_tree(self.get_rev_id(revno))
830
 
        # use inventory as it was in that revision
831
 
        file_id = tree.inventory.path2id(file)
832
 
        if not file_id:
833
 
            raise BzrError("%r is not present in revision %s" % (file, revno))
834
 
        tree.print_file(file_id)
835
 
 
836
 
    @needs_write_lock
 
641
        """Print `file` to stdout."""
 
642
        self.lock_read()
 
643
        try:
 
644
            tree = self.revision_tree(self.get_rev_id(revno))
 
645
            # use inventory as it was in that revision
 
646
            file_id = tree.inventory.path2id(file)
 
647
            if not file_id:
 
648
                raise BzrError("%r is not present in revision %s" % (file, revno))
 
649
            tree.print_file(file_id)
 
650
        finally:
 
651
            self.unlock()
 
652
 
 
653
 
 
654
    def remove(self, files, verbose=False):
 
655
        """Mark nominated files for removal from the inventory.
 
656
 
 
657
        This does not remove their text.  This does not run on 
 
658
 
 
659
        TODO: Refuse to remove modified files unless --force is given?
 
660
 
 
661
        TODO: Do something useful with directories.
 
662
 
 
663
        TODO: Should this remove the text or not?  Tough call; not
 
664
        removing may be useful and the user can just use use rm, and
 
665
        is the opposite of add.  Removing it is consistent with most
 
666
        other tools.  Maybe an option.
 
667
        """
 
668
        ## TODO: Normalize names
 
669
        ## TODO: Remove nested loops; better scalability
 
670
        if isinstance(files, basestring):
 
671
            files = [files]
 
672
 
 
673
        self.lock_write()
 
674
 
 
675
        try:
 
676
            tree = self.working_tree()
 
677
            inv = tree.inventory
 
678
 
 
679
            # do this before any modifications
 
680
            for f in files:
 
681
                fid = inv.path2id(f)
 
682
                if not fid:
 
683
                    raise BzrError("cannot remove unversioned file %s" % quotefn(f))
 
684
                mutter("remove inventory entry %s {%s}" % (quotefn(f), fid))
 
685
                if verbose:
 
686
                    # having remove it, it must be either ignored or unknown
 
687
                    if tree.is_ignored(f):
 
688
                        new_status = 'I'
 
689
                    else:
 
690
                        new_status = '?'
 
691
                    show_status(new_status, inv[fid].kind, quotefn(f))
 
692
                del inv[fid]
 
693
 
 
694
            self._write_inventory(inv)
 
695
        finally:
 
696
            self.unlock()
 
697
 
 
698
    # FIXME: this doesn't need to be a branch method
 
699
    def set_inventory(self, new_inventory_list):
 
700
        from bzrlib.inventory import Inventory, InventoryEntry
 
701
        inv = Inventory(self.get_root_id())
 
702
        for path, file_id, parent, kind in new_inventory_list:
 
703
            name = os.path.basename(path)
 
704
            if name == "":
 
705
                continue
 
706
            # fixme, there should be a factory function inv,add_?? 
 
707
            if kind == 'directory':
 
708
                inv.add(inventory.InventoryDirectory(file_id, name, parent))
 
709
            elif kind == 'file':
 
710
                inv.add(inventory.InventoryFile(file_id, name, parent))
 
711
            elif kind == 'symlink':
 
712
                inv.add(inventory.InventoryLink(file_id, name, parent))
 
713
            else:
 
714
                raise BzrError("unknown kind %r" % kind)
 
715
        self._write_inventory(inv)
 
716
 
 
717
    def unknowns(self):
 
718
        """Return all unknown files.
 
719
 
 
720
        These are files in the working directory that are not versioned or
 
721
        control files or ignored.
 
722
        
 
723
        >>> b = ScratchBranch(files=['foo', 'foo~'])
 
724
        >>> list(b.unknowns())
 
725
        ['foo']
 
726
        >>> b.add('foo')
 
727
        >>> list(b.unknowns())
 
728
        []
 
729
        >>> b.remove('foo')
 
730
        >>> list(b.unknowns())
 
731
        ['foo']
 
732
        """
 
733
        return self.working_tree().unknowns()
 
734
 
 
735
 
837
736
    def append_revision(self, *revision_ids):
838
 
        """See Branch.append_revision."""
839
737
        for revision_id in revision_ids:
840
738
            mutter("add {%s} to revision-history" % revision_id)
841
 
        rev_history = self.revision_history()
842
 
        rev_history.extend(revision_ids)
843
 
        self.set_revision_history(rev_history)
844
 
 
845
 
    @needs_write_lock
846
 
    def set_revision_history(self, rev_history):
847
 
        """See Branch.set_revision_history."""
848
 
        self.put_controlfile('revision-history', '\n'.join(rev_history))
 
739
        self.lock_write()
 
740
        try:
 
741
            rev_history = self.revision_history()
 
742
            rev_history.extend(revision_ids)
 
743
            self.put_controlfile('revision-history', '\n'.join(rev_history))
 
744
        finally:
 
745
            self.unlock()
849
746
 
850
747
    def has_revision(self, revision_id):
851
 
        """See Branch.has_revision."""
 
748
        """True if this branch has a copy of the revision.
 
749
 
 
750
        This does not necessarily imply the revision is merge
 
751
        or on the mainline."""
852
752
        return (revision_id is None
853
 
                or self.revision_store.has_id(revision_id))
 
753
                or revision_id in self.revision_store)
854
754
 
855
 
    @needs_read_lock
856
 
    def _get_revision_xml_file(self, revision_id):
 
755
    def get_revision_xml_file(self, revision_id):
 
756
        """Return XML file object for revision object."""
857
757
        if not revision_id or not isinstance(revision_id, basestring):
858
 
            raise InvalidRevisionId(revision_id=revision_id, branch=self)
 
758
            raise InvalidRevisionId(revision_id)
 
759
 
 
760
        self.lock_read()
859
761
        try:
860
 
            return self.revision_store.get(revision_id)
861
 
        except (IndexError, KeyError):
862
 
            raise bzrlib.errors.NoSuchRevision(self, revision_id)
 
762
            try:
 
763
                return self.revision_store[revision_id]
 
764
            except (IndexError, KeyError):
 
765
                raise bzrlib.errors.NoSuchRevision(self, revision_id)
 
766
        finally:
 
767
            self.unlock()
 
768
 
 
769
    #deprecated
 
770
    get_revision_xml = get_revision_xml_file
863
771
 
864
772
    def get_revision_xml(self, revision_id):
865
 
        """See Branch.get_revision_xml."""
866
 
        return self._get_revision_xml_file(revision_id).read()
 
773
        return self.get_revision_xml_file(revision_id).read()
 
774
 
867
775
 
868
776
    def get_revision(self, revision_id):
869
 
        """See Branch.get_revision."""
870
 
        xml_file = self._get_revision_xml_file(revision_id)
 
777
        """Return the Revision object for a named revision"""
 
778
        xml_file = self.get_revision_xml_file(revision_id)
871
779
 
872
780
        try:
873
781
            r = bzrlib.xml5.serializer_v5.read_revision(xml_file)
879
787
        assert r.revision_id == revision_id
880
788
        return r
881
789
 
 
790
    def get_revision_delta(self, revno):
 
791
        """Return the delta for one revision.
 
792
 
 
793
        The delta is relative to its mainline predecessor, or the
 
794
        empty tree for revision 1.
 
795
        """
 
796
        assert isinstance(revno, int)
 
797
        rh = self.revision_history()
 
798
        if not (1 <= revno <= len(rh)):
 
799
            raise InvalidRevisionNumber(revno)
 
800
 
 
801
        # revno is 1-based; list is 0-based
 
802
 
 
803
        new_tree = self.revision_tree(rh[revno-1])
 
804
        if revno == 1:
 
805
            old_tree = EmptyTree()
 
806
        else:
 
807
            old_tree = self.revision_tree(rh[revno-2])
 
808
 
 
809
        return compare_trees(old_tree, new_tree)
 
810
 
882
811
    def get_revision_sha1(self, revision_id):
883
 
        """See Branch.get_revision_sha1."""
 
812
        """Hash the stored value of a revision, and return it."""
884
813
        # In the future, revision entries will be signed. At that
885
814
        # point, it is probably best *not* to include the signature
886
815
        # in the revision hash. Because that lets you re-sign
890
819
        return bzrlib.osutils.sha_file(self.get_revision_xml_file(revision_id))
891
820
 
892
821
    def get_ancestry(self, revision_id):
893
 
        """See Branch.get_ancestry."""
 
822
        """Return a list of revision-ids integrated by a revision.
 
823
        
 
824
        This currently returns a list, but the ordering is not guaranteed:
 
825
        treat it as a set.
 
826
        """
894
827
        if revision_id is None:
895
828
            return [None]
896
 
        w = self._get_inventory_weave()
 
829
        w = self.get_inventory_weave()
897
830
        return [None] + map(w.idx_to_name,
898
831
                            w.inclusions([w.lookup(revision_id)]))
899
832
 
900
 
    def _get_inventory_weave(self):
 
833
    def get_inventory_weave(self):
901
834
        return self.control_weaves.get_weave('inventory',
902
835
                                             self.get_transaction())
903
836
 
904
837
    def get_inventory(self, revision_id):
905
 
        """See Branch.get_inventory."""
 
838
        """Get Inventory object by hash."""
906
839
        xml = self.get_inventory_xml(revision_id)
907
840
        return bzrlib.xml5.serializer_v5.read_inventory_from_string(xml)
908
841
 
909
842
    def get_inventory_xml(self, revision_id):
910
 
        """See Branch.get_inventory_xml."""
 
843
        """Get inventory XML as a file object."""
911
844
        try:
912
845
            assert isinstance(revision_id, basestring), type(revision_id)
913
 
            iw = self._get_inventory_weave()
 
846
            iw = self.get_inventory_weave()
914
847
            return iw.get_text(iw.lookup(revision_id))
915
848
        except IndexError:
916
849
            raise bzrlib.errors.HistoryMissing(self, 'inventory', revision_id)
917
850
 
918
851
    def get_inventory_sha1(self, revision_id):
919
 
        """See Branch.get_inventory_sha1."""
 
852
        """Return the sha1 hash of the inventory entry
 
853
        """
920
854
        return self.get_revision(revision_id).inventory_sha1
921
855
 
922
856
    def get_revision_inventory(self, revision_id):
923
 
        """See Branch.get_revision_inventory."""
 
857
        """Return inventory of a past revision."""
924
858
        # TODO: Unify this with get_inventory()
925
859
        # bzr 0.0.6 and later imposes the constraint that the inventory_id
926
860
        # must be the same as its revision, so this is trivial.
927
861
        if revision_id == None:
928
 
            # This does not make sense: if there is no revision,
929
 
            # then it is the current tree inventory surely ?!
930
 
            # and thus get_root_id() is something that looks at the last
931
 
            # commit on the branch, and the get_root_id is an inventory check.
932
 
            raise NotImplementedError
933
 
            # return Inventory(self.get_root_id())
 
862
            return Inventory(self.get_root_id())
934
863
        else:
935
864
            return self.get_inventory(revision_id)
936
865
 
937
 
    @needs_read_lock
938
866
    def revision_history(self):
939
 
        """See Branch.revision_history."""
940
 
        transaction = self.get_transaction()
941
 
        history = transaction.map.find_revision_history()
942
 
        if history is not None:
943
 
            mutter("cache hit for revision-history in %s", self)
 
867
        """Return sequence of revision hashes on to this branch."""
 
868
        self.lock_read()
 
869
        try:
 
870
            transaction = self.get_transaction()
 
871
            history = transaction.map.find_revision_history()
 
872
            if history is not None:
 
873
                mutter("cache hit for revision-history in %s", self)
 
874
                return list(history)
 
875
            history = [l.rstrip('\r\n') for l in
 
876
                    self.controlfile('revision-history', 'r').readlines()]
 
877
            transaction.map.add_revision_history(history)
 
878
            # this call is disabled because revision_history is 
 
879
            # not really an object yet, and the transaction is for objects.
 
880
            # transaction.register_clean(history, precious=True)
944
881
            return list(history)
945
 
        history = [l.rstrip('\r\n') for l in
946
 
                self.controlfile('revision-history', 'r').readlines()]
947
 
        transaction.map.add_revision_history(history)
948
 
        # this call is disabled because revision_history is 
949
 
        # not really an object yet, and the transaction is for objects.
950
 
        # transaction.register_clean(history, precious=True)
951
 
        return list(history)
 
882
        finally:
 
883
            self.unlock()
 
884
 
 
885
    def revno(self):
 
886
        """Return current revision number for this branch.
 
887
 
 
888
        That is equivalent to the number of revisions committed to
 
889
        this branch.
 
890
        """
 
891
        return len(self.revision_history())
 
892
 
 
893
 
 
894
    def last_revision(self):
 
895
        """Return last patch hash, or None if no history.
 
896
        """
 
897
        ph = self.revision_history()
 
898
        if ph:
 
899
            return ph[-1]
 
900
        else:
 
901
            return None
 
902
 
 
903
 
 
904
    def missing_revisions(self, other, stop_revision=None, diverged_ok=False):
 
905
        """Return a list of new revisions that would perfectly fit.
 
906
        
 
907
        If self and other have not diverged, return a list of the revisions
 
908
        present in other, but missing from self.
 
909
 
 
910
        >>> from bzrlib.commit import commit
 
911
        >>> bzrlib.trace.silent = True
 
912
        >>> br1 = ScratchBranch()
 
913
        >>> br2 = ScratchBranch()
 
914
        >>> br1.missing_revisions(br2)
 
915
        []
 
916
        >>> commit(br2, "lala!", rev_id="REVISION-ID-1")
 
917
        >>> br1.missing_revisions(br2)
 
918
        [u'REVISION-ID-1']
 
919
        >>> br2.missing_revisions(br1)
 
920
        []
 
921
        >>> commit(br1, "lala!", rev_id="REVISION-ID-1")
 
922
        >>> br1.missing_revisions(br2)
 
923
        []
 
924
        >>> commit(br2, "lala!", rev_id="REVISION-ID-2A")
 
925
        >>> br1.missing_revisions(br2)
 
926
        [u'REVISION-ID-2A']
 
927
        >>> commit(br1, "lala!", rev_id="REVISION-ID-2B")
 
928
        >>> br1.missing_revisions(br2)
 
929
        Traceback (most recent call last):
 
930
        DivergedBranches: These branches have diverged.
 
931
        """
 
932
        self_history = self.revision_history()
 
933
        self_len = len(self_history)
 
934
        other_history = other.revision_history()
 
935
        other_len = len(other_history)
 
936
        common_index = min(self_len, other_len) -1
 
937
        if common_index >= 0 and \
 
938
            self_history[common_index] != other_history[common_index]:
 
939
            raise DivergedBranches(self, other)
 
940
 
 
941
        if stop_revision is None:
 
942
            stop_revision = other_len
 
943
        else:
 
944
            assert isinstance(stop_revision, int)
 
945
            if stop_revision > other_len:
 
946
                raise bzrlib.errors.NoSuchRevision(self, stop_revision)
 
947
        return other_history[self_len:stop_revision]
952
948
 
953
949
    def update_revisions(self, other, stop_revision=None):
954
 
        """See Branch.update_revisions."""
 
950
        """Pull in new perfect-fit revisions."""
 
951
        # FIXME: If the branches have diverged, but the latest
 
952
        # revision in this branch is completely merged into the other,
 
953
        # then we should still be able to pull.
955
954
        from bzrlib.fetch import greedy_fetch
 
955
        from bzrlib.revision import get_intervening_revisions
956
956
        if stop_revision is None:
957
957
            stop_revision = other.last_revision()
958
 
        ### Should this be checking is_ancestor instead of revision_history?
959
958
        if (stop_revision is not None and 
960
959
            stop_revision in self.revision_history()):
961
960
            return
962
961
        greedy_fetch(to_branch=self, from_branch=other,
963
962
                     revision=stop_revision)
964
 
        pullable_revs = self.pullable_revisions(other, stop_revision)
965
 
        if len(pullable_revs) > 0:
 
963
        pullable_revs = self.missing_revisions(
 
964
            other, other.revision_id_to_revno(stop_revision))
 
965
        if pullable_revs:
 
966
            greedy_fetch(to_branch=self,
 
967
                         from_branch=other,
 
968
                         revision=pullable_revs[-1])
966
969
            self.append_revision(*pullable_revs)
 
970
    
967
971
 
968
 
    def pullable_revisions(self, other, stop_revision):
969
 
        """See Branch.pullable_revisions."""
970
 
        other_revno = other.revision_id_to_revno(stop_revision)
 
972
    def commit(self, *args, **kw):
 
973
        from bzrlib.commit import Commit
 
974
        Commit().commit(self, *args, **kw)
 
975
    
 
976
    def revision_id_to_revno(self, revision_id):
 
977
        """Given a revision id, return its revno"""
 
978
        if revision_id is None:
 
979
            return 0
 
980
        history = self.revision_history()
971
981
        try:
972
 
            return self.missing_revisions(other, other_revno)
973
 
        except DivergedBranches, e:
974
 
            try:
975
 
                pullable_revs = get_intervening_revisions(self.last_revision(),
976
 
                                                          stop_revision, self)
977
 
                assert self.last_revision() not in pullable_revs
978
 
                return pullable_revs
979
 
            except bzrlib.errors.NotAncestor:
980
 
                if is_ancestor(self.last_revision(), stop_revision, self):
981
 
                    return []
982
 
                else:
983
 
                    raise e
984
 
        
 
982
            return history.index(revision_id) + 1
 
983
        except ValueError:
 
984
            raise bzrlib.errors.NoSuchRevision(self, revision_id)
 
985
 
 
986
    def get_rev_id(self, revno, history=None):
 
987
        """Find the revision id of the specified revno."""
 
988
        if revno == 0:
 
989
            return None
 
990
        if history is None:
 
991
            history = self.revision_history()
 
992
        elif revno <= 0 or revno > len(history):
 
993
            raise bzrlib.errors.NoSuchRevision(self, revno)
 
994
        return history[revno - 1]
 
995
 
985
996
    def revision_tree(self, revision_id):
986
 
        """See Branch.revision_tree."""
 
997
        """Return Tree for a revision on this branch.
 
998
 
 
999
        `revision_id` may be None for the null revision, in which case
 
1000
        an `EmptyTree` is returned."""
987
1001
        # TODO: refactor this to use an existing revision object
988
1002
        # so we don't need to read it in twice.
989
 
        if revision_id == None or revision_id == NULL_REVISION:
 
1003
        if revision_id == None:
990
1004
            return EmptyTree()
991
1005
        else:
992
1006
            inv = self.get_revision_inventory(revision_id)
993
1007
            return RevisionTree(self.weave_store, inv, revision_id)
994
1008
 
 
1009
 
995
1010
    def working_tree(self):
996
 
        """See Branch.working_tree."""
 
1011
        """Return a `Tree` for the working copy."""
997
1012
        from bzrlib.workingtree import WorkingTree
998
 
        if self._transport.base.find('://') != -1:
999
 
            raise NoWorkingTree(self.base)
1000
 
        return WorkingTree(self.base, branch=self)
1001
 
 
1002
 
    @needs_write_lock
1003
 
    def pull(self, source, overwrite=False):
1004
 
        """See Branch.pull."""
1005
 
        source.lock_read()
1006
 
        try:
1007
 
            old_count = len(self.revision_history())
1008
 
            try:
1009
 
                self.update_revisions(source)
1010
 
            except DivergedBranches:
1011
 
                if not overwrite:
1012
 
                    raise
1013
 
                self.set_revision_history(source.revision_history())
1014
 
            new_count = len(self.revision_history())
1015
 
            return new_count - old_count
1016
 
        finally:
1017
 
            source.unlock()
 
1013
        # TODO: In the future, WorkingTree should utilize Transport
 
1014
        # RobertCollins 20051003 - I don't think it should - working trees are
 
1015
        # much more complex to keep consistent than our careful .bzr subset.
 
1016
        # instead, we should say that working trees are local only, and optimise
 
1017
        # for that.
 
1018
        return WorkingTree(self._transport.base, self.read_working_inventory())
 
1019
 
 
1020
 
 
1021
    def basis_tree(self):
 
1022
        """Return `Tree` object for last revision.
 
1023
 
 
1024
        If there are no revisions yet, return an `EmptyTree`.
 
1025
        """
 
1026
        return self.revision_tree(self.last_revision())
 
1027
 
 
1028
 
 
1029
    def rename_one(self, from_rel, to_rel):
 
1030
        """Rename one file.
 
1031
 
 
1032
        This can change the directory or the filename or both.
 
1033
        """
 
1034
        self.lock_write()
 
1035
        try:
 
1036
            tree = self.working_tree()
 
1037
            inv = tree.inventory
 
1038
            if not tree.has_filename(from_rel):
 
1039
                raise BzrError("can't rename: old working file %r does not exist" % from_rel)
 
1040
            if tree.has_filename(to_rel):
 
1041
                raise BzrError("can't rename: new working file %r already exists" % to_rel)
 
1042
 
 
1043
            file_id = inv.path2id(from_rel)
 
1044
            if file_id == None:
 
1045
                raise BzrError("can't rename: old name %r is not versioned" % from_rel)
 
1046
 
 
1047
            if inv.path2id(to_rel):
 
1048
                raise BzrError("can't rename: new name %r is already versioned" % to_rel)
 
1049
 
 
1050
            to_dir, to_tail = os.path.split(to_rel)
 
1051
            to_dir_id = inv.path2id(to_dir)
 
1052
            if to_dir_id == None and to_dir != '':
 
1053
                raise BzrError("can't determine destination directory id for %r" % to_dir)
 
1054
 
 
1055
            mutter("rename_one:")
 
1056
            mutter("  file_id    {%s}" % file_id)
 
1057
            mutter("  from_rel   %r" % from_rel)
 
1058
            mutter("  to_rel     %r" % to_rel)
 
1059
            mutter("  to_dir     %r" % to_dir)
 
1060
            mutter("  to_dir_id  {%s}" % to_dir_id)
 
1061
 
 
1062
            inv.rename(file_id, to_dir_id, to_tail)
 
1063
 
 
1064
            from_abs = self.abspath(from_rel)
 
1065
            to_abs = self.abspath(to_rel)
 
1066
            try:
 
1067
                rename(from_abs, to_abs)
 
1068
            except OSError, e:
 
1069
                raise BzrError("failed to rename %r to %r: %s"
 
1070
                        % (from_abs, to_abs, e[1]),
 
1071
                        ["rename rolled back"])
 
1072
 
 
1073
            self._write_inventory(inv)
 
1074
        finally:
 
1075
            self.unlock()
 
1076
 
 
1077
 
 
1078
    def move(self, from_paths, to_name):
 
1079
        """Rename files.
 
1080
 
 
1081
        to_name must exist as a versioned directory.
 
1082
 
 
1083
        If to_name exists and is a directory, the files are moved into
 
1084
        it, keeping their old names.  If it is a directory, 
 
1085
 
 
1086
        Note that to_name is only the last component of the new name;
 
1087
        this doesn't change the directory.
 
1088
 
 
1089
        This returns a list of (from_path, to_path) pairs for each
 
1090
        entry that is moved.
 
1091
        """
 
1092
        result = []
 
1093
        self.lock_write()
 
1094
        try:
 
1095
            ## TODO: Option to move IDs only
 
1096
            assert not isinstance(from_paths, basestring)
 
1097
            tree = self.working_tree()
 
1098
            inv = tree.inventory
 
1099
            to_abs = self.abspath(to_name)
 
1100
            if not isdir(to_abs):
 
1101
                raise BzrError("destination %r is not a directory" % to_abs)
 
1102
            if not tree.has_filename(to_name):
 
1103
                raise BzrError("destination %r not in working directory" % to_abs)
 
1104
            to_dir_id = inv.path2id(to_name)
 
1105
            if to_dir_id == None and to_name != '':
 
1106
                raise BzrError("destination %r is not a versioned directory" % to_name)
 
1107
            to_dir_ie = inv[to_dir_id]
 
1108
            if to_dir_ie.kind not in ('directory', 'root_directory'):
 
1109
                raise BzrError("destination %r is not a directory" % to_abs)
 
1110
 
 
1111
            to_idpath = inv.get_idpath(to_dir_id)
 
1112
 
 
1113
            for f in from_paths:
 
1114
                if not tree.has_filename(f):
 
1115
                    raise BzrError("%r does not exist in working tree" % f)
 
1116
                f_id = inv.path2id(f)
 
1117
                if f_id == None:
 
1118
                    raise BzrError("%r is not versioned" % f)
 
1119
                name_tail = splitpath(f)[-1]
 
1120
                dest_path = appendpath(to_name, name_tail)
 
1121
                if tree.has_filename(dest_path):
 
1122
                    raise BzrError("destination %r already exists" % dest_path)
 
1123
                if f_id in to_idpath:
 
1124
                    raise BzrError("can't move %r to a subdirectory of itself" % f)
 
1125
 
 
1126
            # OK, so there's a race here, it's possible that someone will
 
1127
            # create a file in this interval and then the rename might be
 
1128
            # left half-done.  But we should have caught most problems.
 
1129
 
 
1130
            for f in from_paths:
 
1131
                name_tail = splitpath(f)[-1]
 
1132
                dest_path = appendpath(to_name, name_tail)
 
1133
                result.append((f, dest_path))
 
1134
                inv.rename(inv.path2id(f), to_dir_id, name_tail)
 
1135
                try:
 
1136
                    rename(self.abspath(f), self.abspath(dest_path))
 
1137
                except OSError, e:
 
1138
                    raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
 
1139
                            ["rename rolled back"])
 
1140
 
 
1141
            self._write_inventory(inv)
 
1142
        finally:
 
1143
            self.unlock()
 
1144
 
 
1145
        return result
 
1146
 
 
1147
 
 
1148
    def revert(self, filenames, old_tree=None, backups=True):
 
1149
        """Restore selected files to the versions from a previous tree.
 
1150
 
 
1151
        backups
 
1152
            If true (default) backups are made of files before
 
1153
            they're renamed.
 
1154
        """
 
1155
        from bzrlib.errors import NotVersionedError, BzrError
 
1156
        from bzrlib.atomicfile import AtomicFile
 
1157
        from bzrlib.osutils import backup_file
 
1158
        
 
1159
        inv = self.read_working_inventory()
 
1160
        if old_tree is None:
 
1161
            old_tree = self.basis_tree()
 
1162
        old_inv = old_tree.inventory
 
1163
 
 
1164
        nids = []
 
1165
        for fn in filenames:
 
1166
            file_id = inv.path2id(fn)
 
1167
            if not file_id:
 
1168
                raise NotVersionedError("not a versioned file", fn)
 
1169
            if not old_inv.has_id(file_id):
 
1170
                raise BzrError("file not present in old tree", fn, file_id)
 
1171
            nids.append((fn, file_id))
 
1172
            
 
1173
        # TODO: Rename back if it was previously at a different location
 
1174
 
 
1175
        # TODO: If given a directory, restore the entire contents from
 
1176
        # the previous version.
 
1177
 
 
1178
        # TODO: Make a backup to a temporary file.
 
1179
 
 
1180
        # TODO: If the file previously didn't exist, delete it?
 
1181
        for fn, file_id in nids:
 
1182
            backup_file(fn)
 
1183
            
 
1184
            f = AtomicFile(fn, 'wb')
 
1185
            try:
 
1186
                f.write(old_tree.get_file(file_id).read())
 
1187
                f.commit()
 
1188
            finally:
 
1189
                f.close()
 
1190
 
 
1191
 
 
1192
    def pending_merges(self):
 
1193
        """Return a list of pending merges.
 
1194
 
 
1195
        These are revisions that have been merged into the working
 
1196
        directory but not yet committed.
 
1197
        """
 
1198
        cfn = self._rel_controlfilename('pending-merges')
 
1199
        if not self._transport.has(cfn):
 
1200
            return []
 
1201
        p = []
 
1202
        for l in self.controlfile('pending-merges', 'r').readlines():
 
1203
            p.append(l.rstrip('\n'))
 
1204
        return p
 
1205
 
 
1206
 
 
1207
    def add_pending_merge(self, *revision_ids):
 
1208
        # TODO: Perhaps should check at this point that the
 
1209
        # history of the revision is actually present?
 
1210
        p = self.pending_merges()
 
1211
        updated = False
 
1212
        for rev_id in revision_ids:
 
1213
            if rev_id in p:
 
1214
                continue
 
1215
            p.append(rev_id)
 
1216
            updated = True
 
1217
        if updated:
 
1218
            self.set_pending_merges(p)
 
1219
 
 
1220
    def set_pending_merges(self, rev_list):
 
1221
        self.lock_write()
 
1222
        try:
 
1223
            self.put_controlfile('pending-merges', '\n'.join(rev_list))
 
1224
        finally:
 
1225
            self.unlock()
 
1226
 
1018
1227
 
1019
1228
    def get_parent(self):
1020
 
        """See Branch.get_parent."""
 
1229
        """Return the parent location of the branch.
 
1230
 
 
1231
        This is the default location for push/pull/missing.  The usual
 
1232
        pattern is that the user can override it by specifying a
 
1233
        location.
 
1234
        """
1021
1235
        import errno
1022
1236
        _locs = ['parent', 'pull', 'x-pull']
1023
1237
        for l in _locs:
1028
1242
                    raise
1029
1243
        return None
1030
1244
 
1031
 
    def get_push_location(self):
1032
 
        """See Branch.get_push_location."""
1033
 
        config = bzrlib.config.BranchConfig(self)
1034
 
        push_loc = config.get_user_option('push_location')
1035
 
        return push_loc
1036
 
 
1037
 
    def set_push_location(self, location):
1038
 
        """See Branch.set_push_location."""
1039
 
        config = bzrlib.config.LocationConfig(self.base)
1040
 
        config.set_user_option('push_location', location)
1041
 
 
1042
 
    @needs_write_lock
 
1245
 
1043
1246
    def set_parent(self, url):
1044
 
        """See Branch.set_parent."""
1045
1247
        # TODO: Maybe delete old location files?
1046
1248
        from bzrlib.atomicfile import AtomicFile
1047
 
        f = AtomicFile(self.controlfilename('parent'))
 
1249
        self.lock_write()
1048
1250
        try:
1049
 
            f.write(url + '\n')
1050
 
            f.commit()
 
1251
            f = AtomicFile(self.controlfilename('parent'))
 
1252
            try:
 
1253
                f.write(url + '\n')
 
1254
                f.commit()
 
1255
            finally:
 
1256
                f.close()
1051
1257
        finally:
1052
 
            f.close()
1053
 
 
1054
 
    def tree_config(self):
1055
 
        return TreeConfig(self)
1056
 
 
1057
 
    def sign_revision(self, revision_id, gpg_strategy):
1058
 
        """See Branch.sign_revision."""
1059
 
        plaintext = Testament.from_revision(self, revision_id).as_short_text()
1060
 
        self.store_revision_signature(gpg_strategy, plaintext, revision_id)
1061
 
 
1062
 
    @needs_write_lock
1063
 
    def store_revision_signature(self, gpg_strategy, plaintext, revision_id):
1064
 
        """See Branch.store_revision_signature."""
1065
 
        self.revision_store.add(StringIO(gpg_strategy.sign(plaintext)), 
1066
 
                                revision_id, "sig")
1067
 
 
1068
 
 
1069
 
class ScratchBranch(BzrBranch):
 
1258
            self.unlock()
 
1259
 
 
1260
    def check_revno(self, revno):
 
1261
        """\
 
1262
        Check whether a revno corresponds to any revision.
 
1263
        Zero (the NULL revision) is considered valid.
 
1264
        """
 
1265
        if revno != 0:
 
1266
            self.check_real_revno(revno)
 
1267
            
 
1268
    def check_real_revno(self, revno):
 
1269
        """\
 
1270
        Check whether a revno corresponds to a real revision.
 
1271
        Zero (the NULL revision) is considered invalid
 
1272
        """
 
1273
        if revno < 1 or revno > self.revno():
 
1274
            raise InvalidRevisionNumber(revno)
 
1275
        
 
1276
        
 
1277
        
 
1278
 
 
1279
 
 
1280
class ScratchBranch(_Branch):
1070
1281
    """Special test class: a branch that cleans up after itself.
1071
1282
 
1072
1283
    >>> b = ScratchBranch()
1073
1284
    >>> isdir(b.base)
1074
1285
    True
1075
1286
    >>> bd = b.base
1076
 
    >>> b._transport.__del__()
 
1287
    >>> b.destroy()
1077
1288
    >>> isdir(bd)
1078
1289
    False
1079
1290
    """
1080
 
 
1081
 
    def __init__(self, files=[], dirs=[], transport=None):
 
1291
    def __init__(self, files=[], dirs=[], base=None):
1082
1292
        """Make a test branch.
1083
1293
 
1084
1294
        This creates a temporary directory and runs init-tree in it.
1085
1295
 
1086
1296
        If any files are listed, they are created in the working copy.
1087
1297
        """
1088
 
        if transport is None:
1089
 
            transport = bzrlib.transport.local.ScratchTransport()
1090
 
            super(ScratchBranch, self).__init__(transport, init=True)
1091
 
        else:
1092
 
            super(ScratchBranch, self).__init__(transport)
1093
 
 
 
1298
        from tempfile import mkdtemp
 
1299
        init = False
 
1300
        if base is None:
 
1301
            base = mkdtemp()
 
1302
            init = True
 
1303
        if isinstance(base, basestring):
 
1304
            base = get_transport(base)
 
1305
        _Branch.__init__(self, base, init=init)
1094
1306
        for d in dirs:
1095
1307
            self._transport.mkdir(d)
1096
1308
            
1116
1328
        base = mkdtemp()
1117
1329
        os.rmdir(base)
1118
1330
        copytree(self.base, base, symlinks=True)
1119
 
        return ScratchBranch(
1120
 
            transport=bzrlib.transport.local.ScratchTransport(base))
 
1331
        return ScratchBranch(base=base)
 
1332
 
 
1333
    def __del__(self):
 
1334
        self.destroy()
 
1335
 
 
1336
    def destroy(self):
 
1337
        """Destroy the test branch, removing the scratch directory."""
 
1338
        from shutil import rmtree
 
1339
        try:
 
1340
            if self.base:
 
1341
                mutter("delete ScratchBranch %s" % self.base)
 
1342
                rmtree(self.base)
 
1343
        except OSError, e:
 
1344
            # Work around for shutil.rmtree failing on Windows when
 
1345
            # readonly files are encountered
 
1346
            mutter("hit exception in destroying ScratchBranch: %s" % e)
 
1347
            for root, dirs, files in os.walk(self.base, topdown=False):
 
1348
                for name in files:
 
1349
                    os.chmod(os.path.join(root, name), 0700)
 
1350
            rmtree(self.base)
 
1351
        self._transport = None
 
1352
 
1121
1353
    
1122
1354
 
1123
1355
######################################################################
1136
1368
            break
1137
1369
        filename = head
1138
1370
    return False
 
1371
 
 
1372
 
 
1373
 
 
1374
def gen_file_id(name):
 
1375
    """Return new file id.
 
1376
 
 
1377
    This should probably generate proper UUIDs, but for the moment we
 
1378
    cope with just randomness because running uuidgen every time is
 
1379
    slow."""
 
1380
    import re
 
1381
    from binascii import hexlify
 
1382
    from time import time
 
1383
 
 
1384
    # get last component
 
1385
    idx = name.rfind('/')
 
1386
    if idx != -1:
 
1387
        name = name[idx+1 : ]
 
1388
    idx = name.rfind('\\')
 
1389
    if idx != -1:
 
1390
        name = name[idx+1 : ]
 
1391
 
 
1392
    # make it not a hidden file
 
1393
    name = name.lstrip('.')
 
1394
 
 
1395
    # remove any wierd characters; we don't escape them but rather
 
1396
    # just pull them out
 
1397
    name = re.sub(r'[^\w.]', '', name)
 
1398
 
 
1399
    s = hexlify(rand_bytes(8))
 
1400
    return '-'.join((name, compact_date(time()), s))
 
1401
 
 
1402
 
 
1403
def gen_root_id():
 
1404
    """Return a new tree-root file id."""
 
1405
    return gen_file_id('TREE_ROOT')
 
1406
 
 
1407