~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Aaron Bentley
  • Date: 2005-10-04 04:32:32 UTC
  • mfrom: (1185.12.6)
  • mto: (1185.12.13)
  • mto: This revision was merged to the branch mainline in revision 1419.
  • Revision ID: aaron.bentley@utoronto.ca-20051004043231-40302a149769263b
merged my own changes

Show diffs side-by-side

added added

removed removed

Lines of Context:
23
23
 
24
24
 
25
25
import bzrlib
26
 
from bzrlib.inventory import InventoryEntry
27
 
import bzrlib.inventory as inventory
28
26
from bzrlib.trace import mutter, note
29
27
from bzrlib.osutils import (isdir, quotefn, compact_date, rand_bytes, 
30
28
                            rename, splitpath, sha_file, appendpath, 
31
 
                            file_kind, abspath)
32
 
import bzrlib.errors as errors
 
29
                            file_kind)
33
30
from bzrlib.errors import (BzrError, InvalidRevisionNumber, InvalidRevisionId,
34
31
                           NoSuchRevision, HistoryMissing, NotBranchError,
35
32
                           DivergedBranches, LockError, UnlistableStore,
36
 
                           UnlistableBranch, NoSuchFile, NotVersionedError)
 
33
                           UnlistableBranch, NoSuchFile)
37
34
from bzrlib.textui import show_status
38
 
from bzrlib.revision import (Revision, is_ancestor, get_intervening_revisions,
39
 
                             NULL_REVISION)
40
 
 
 
35
from bzrlib.revision import Revision, validate_revision_id, is_ancestor
41
36
from bzrlib.delta import compare_trees
42
37
from bzrlib.tree import EmptyTree, RevisionTree
43
38
from bzrlib.inventory import Inventory
45
40
from bzrlib.store.compressed_text import CompressedTextStore
46
41
from bzrlib.store.text import TextStore
47
42
from bzrlib.store.weave import WeaveStore
48
 
from bzrlib.testament import Testament
49
 
import bzrlib.transactions as transactions
50
43
from bzrlib.transport import Transport, get_transport
51
44
import bzrlib.xml5
52
45
import bzrlib.ui
54
47
 
55
48
BZR_BRANCH_FORMAT_4 = "Bazaar-NG branch, format 0.0.4\n"
56
49
BZR_BRANCH_FORMAT_5 = "Bazaar-NG branch, format 5\n"
57
 
BZR_BRANCH_FORMAT_6 = "Bazaar-NG branch, format 6\n"
58
50
## TODO: Maybe include checks for common corruption of newlines, etc?
59
51
 
60
52
 
67
59
    # XXX: leave this here for about one release, then remove it
68
60
    raise NotImplementedError('find_branch() is not supported anymore, '
69
61
                              'please use one of the new branch constructors')
70
 
 
71
 
 
72
 
def needs_read_lock(unbound):
73
 
    """Decorate unbound to take out and release a read lock."""
74
 
    def decorated(self, *args, **kwargs):
75
 
        self.lock_read()
76
 
        try:
77
 
            return unbound(self, *args, **kwargs)
78
 
        finally:
79
 
            self.unlock()
80
 
    return decorated
81
 
 
82
 
 
83
 
def needs_write_lock(unbound):
84
 
    """Decorate unbound to take out and release a write lock."""
85
 
    def decorated(self, *args, **kwargs):
86
 
        self.lock_write()
87
 
        try:
88
 
            return unbound(self, *args, **kwargs)
89
 
        finally:
90
 
            self.unlock()
91
 
    return decorated
 
62
def _relpath(base, path):
 
63
    """Return path relative to base, or raise exception.
 
64
 
 
65
    The path may be either an absolute path or a path relative to the
 
66
    current working directory.
 
67
 
 
68
    Lifted out of Branch.relpath for ease of testing.
 
69
 
 
70
    os.path.commonprefix (python2.4) has a bad bug that it works just
 
71
    on string prefixes, assuming that '/u' is a prefix of '/u2'.  This
 
72
    avoids that problem."""
 
73
    rp = os.path.abspath(path)
 
74
 
 
75
    s = []
 
76
    head = rp
 
77
    while len(head) >= len(base):
 
78
        if head == base:
 
79
            break
 
80
        head, tail = os.path.split(head)
 
81
        if tail:
 
82
            s.insert(0, tail)
 
83
    else:
 
84
        raise NotBranchError("path %r is not within branch %r" % (rp, base))
 
85
 
 
86
    return os.sep.join(s)
 
87
        
 
88
 
 
89
def find_branch_root(t):
 
90
    """Find the branch root enclosing the transport's base.
 
91
 
 
92
    t is a Transport object.
 
93
 
 
94
    It is not necessary that the base of t exists.
 
95
 
 
96
    Basically we keep looking up until we find the control directory or
 
97
    run into the root.  If there isn't one, raises NotBranchError.
 
98
    """
 
99
    orig_base = t.base
 
100
    while True:
 
101
        if t.has(bzrlib.BZRDIR):
 
102
            return t
 
103
        new_t = t.clone('..')
 
104
        if new_t.base == t.base:
 
105
            # reached the root, whatever that may be
 
106
            raise NotBranchError('%s is not in a branch' % orig_base)
 
107
        t = new_t
 
108
 
92
109
 
93
110
######################################################################
94
111
# branch objects
115
132
    def open(base):
116
133
        """Open an existing branch, rooted at 'base' (url)"""
117
134
        t = get_transport(base)
118
 
        mutter("trying to open %r with transport %r", base, t)
119
135
        return _Branch(t)
120
136
 
121
137
    @staticmethod
123
139
        """Open an existing branch which contains url.
124
140
        
125
141
        This probes for a branch at url, and searches upwards from there.
126
 
 
127
 
        Basically we keep looking up until we find the control directory or
128
 
        run into the root.  If there isn't one, raises NotBranchError.
129
 
        If there is one, it is returned, along with the unused portion of url.
130
142
        """
131
143
        t = get_transport(url)
132
 
        while True:
133
 
            try:
134
 
                return _Branch(t), t.relpath(url)
135
 
            except NotBranchError:
136
 
                pass
137
 
            new_t = t.clone('..')
138
 
            if new_t.base == t.base:
139
 
                # reached the root, whatever that may be
140
 
                raise NotBranchError(path=url)
141
 
            t = new_t
 
144
        t = find_branch_root(t)
 
145
        return _Branch(t)
142
146
 
143
147
    @staticmethod
144
148
    def initialize(base):
150
154
        """Subclasses that care about caching should override this, and set
151
155
        up cached stores located under cache_root.
152
156
        """
153
 
        self.cache_root = cache_root
154
157
 
155
158
 
156
159
class _Branch(Branch):
209
212
        """Create new branch object at a particular location.
210
213
 
211
214
        transport -- A Transport object, defining how to access files.
 
215
                (If a string, transport.transport() will be used to
 
216
                create a Transport object)
212
217
        
213
218
        init -- If True, create new control files in a previously
214
219
             unversioned directory.  If False, the branch must already
229
234
            self._make_control()
230
235
        self._check_format(relax_version_check)
231
236
 
232
 
        def get_store(name, compressed=True, prefixed=False):
233
 
            # FIXME: This approach of assuming stores are all entirely compressed
234
 
            # or entirely uncompressed is tidy, but breaks upgrade from 
235
 
            # some existing branches where there's a mixture; we probably 
236
 
            # still want the option to look for both.
 
237
        def get_store(name, compressed=True):
237
238
            relpath = self._rel_controlfilename(name)
238
239
            if compressed:
239
 
                store = CompressedTextStore(self._transport.clone(relpath),
240
 
                                            prefixed=prefixed)
 
240
                store = CompressedTextStore(self._transport.clone(relpath))
241
241
            else:
242
 
                store = TextStore(self._transport.clone(relpath),
243
 
                                  prefixed=prefixed)
244
 
            #if self._transport.should_cache():
245
 
            #    cache_path = os.path.join(self.cache_root, name)
246
 
            #    os.mkdir(cache_path)
247
 
            #    store = bzrlib.store.CachedStore(store, cache_path)
 
242
                store = TextStore(self._transport.clone(relpath))
 
243
            if self._transport.should_cache():
 
244
                from meta_store import CachedStore
 
245
                cache_path = os.path.join(self.cache_root, name)
 
246
                os.mkdir(cache_path)
 
247
                store = CachedStore(store, cache_path)
248
248
            return store
249
 
        def get_weave(name, prefixed=False):
 
249
        def get_weave(name):
250
250
            relpath = self._rel_controlfilename(name)
251
 
            ws = WeaveStore(self._transport.clone(relpath), prefixed=prefixed)
 
251
            ws = WeaveStore(self._transport.clone(relpath))
252
252
            if self._transport.should_cache():
253
253
                ws.enable_cache = True
254
254
            return ws
258
258
            self.text_store = get_store('text-store')
259
259
            self.revision_store = get_store('revision-store')
260
260
        elif self._branch_format == 5:
261
 
            self.control_weaves = get_weave('')
 
261
            self.control_weaves = get_weave([])
262
262
            self.weave_store = get_weave('weaves')
263
263
            self.revision_store = get_store('revision-store', compressed=False)
264
 
        elif self._branch_format == 6:
265
 
            self.control_weaves = get_weave('')
266
 
            self.weave_store = get_weave('weaves', prefixed=True)
267
 
            self.revision_store = get_store('revision-store', compressed=False,
268
 
                                            prefixed=True)
269
 
        self.revision_store.register_suffix('sig')
270
 
        self._transaction = None
271
264
 
272
265
    def __str__(self):
273
266
        return '%s(%r)' % (self.__class__.__name__, self._transport.base)
302
295
            return self._transport.base
303
296
        return None
304
297
 
305
 
    base = property(_get_base, doc="The URL for the root of this branch.")
306
 
 
307
 
    def _finish_transaction(self):
308
 
        """Exit the current transaction."""
309
 
        if self._transaction is None:
310
 
            raise errors.LockError('Branch %s is not in a transaction' %
311
 
                                   self)
312
 
        transaction = self._transaction
313
 
        self._transaction = None
314
 
        transaction.finish()
315
 
 
316
 
    def get_transaction(self):
317
 
        """Return the current active transaction.
318
 
 
319
 
        If no transaction is active, this returns a passthrough object
320
 
        for which all data is immediately flushed and no caching happens.
321
 
        """
322
 
        if self._transaction is None:
323
 
            return transactions.PassThroughTransaction()
324
 
        else:
325
 
            return self._transaction
326
 
 
327
 
    def _set_transaction(self, new_transaction):
328
 
        """Set a new active transaction."""
329
 
        if self._transaction is not None:
330
 
            raise errors.LockError('Branch %s is in a transaction already.' %
331
 
                                   self)
332
 
        self._transaction = new_transaction
 
298
    base = property(_get_base)
 
299
 
333
300
 
334
301
    def lock_write(self):
335
 
        mutter("lock write: %s (%s)", self, self._lock_count)
336
302
        # TODO: Upgrade locking to support using a Transport,
337
303
        # and potentially a remote locking protocol
338
304
        if self._lock_mode:
345
311
                    self._rel_controlfilename('branch-lock'))
346
312
            self._lock_mode = 'w'
347
313
            self._lock_count = 1
348
 
            self._set_transaction(transactions.PassThroughTransaction())
 
314
 
349
315
 
350
316
    def lock_read(self):
351
 
        mutter("lock read: %s (%s)", self, self._lock_count)
352
317
        if self._lock_mode:
353
318
            assert self._lock_mode in ('r', 'w'), \
354
319
                   "invalid lock mode %r" % self._lock_mode
358
323
                    self._rel_controlfilename('branch-lock'))
359
324
            self._lock_mode = 'r'
360
325
            self._lock_count = 1
361
 
            self._set_transaction(transactions.ReadOnlyTransaction())
362
 
            # 5K may be excessive, but hey, its a knob.
363
 
            self.get_transaction().set_cache_size(5000)
364
326
                        
365
327
    def unlock(self):
366
 
        mutter("unlock: %s (%s)", self, self._lock_count)
367
328
        if not self._lock_mode:
368
329
            raise LockError('branch %r is not locked' % (self))
369
330
 
370
331
        if self._lock_count > 1:
371
332
            self._lock_count -= 1
372
333
        else:
373
 
            self._finish_transaction()
374
334
            self._lock.unlock()
375
335
            self._lock = None
376
336
            self._lock_mode = self._lock_count = None
377
337
 
378
338
    def abspath(self, name):
379
 
        """Return absolute filename for something in the branch
380
 
        
381
 
        XXX: Robert Collins 20051017 what is this used for? why is it a branch
382
 
        method and not a tree method.
383
 
        """
 
339
        """Return absolute filename for something in the branch"""
384
340
        return self._transport.abspath(name)
385
341
 
 
342
    def relpath(self, path):
 
343
        """Return path relative to this branch of something inside it.
 
344
 
 
345
        Raises an error if path is not in this branch."""
 
346
        return self._transport.relpath(path)
 
347
 
 
348
 
386
349
    def _rel_controlfilename(self, file_or_path):
387
 
        if not isinstance(file_or_path, basestring):
388
 
            file_or_path = '/'.join(file_or_path)
389
 
        if file_or_path == '':
390
 
            return bzrlib.BZRDIR
391
 
        return bzrlib.transport.urlescape(bzrlib.BZRDIR + '/' + file_or_path)
 
350
        if isinstance(file_or_path, basestring):
 
351
            file_or_path = [file_or_path]
 
352
        return [bzrlib.BZRDIR] + file_or_path
392
353
 
393
354
    def controlfilename(self, file_or_path):
394
355
        """Return location relative to branch."""
395
356
        return self._transport.abspath(self._rel_controlfilename(file_or_path))
396
357
 
 
358
 
397
359
    def controlfile(self, file_or_path, mode='r'):
398
360
        """Open a control file for this branch.
399
361
 
470
432
        files = [('README', 
471
433
            "This is a Bazaar-NG control directory.\n"
472
434
            "Do not change any files in this directory.\n"),
473
 
            ('branch-format', BZR_BRANCH_FORMAT_6),
 
435
            ('branch-format', BZR_BRANCH_FORMAT_5),
474
436
            ('revision-history', ''),
475
437
            ('branch-name', ''),
476
438
            ('branch-lock', ''),
496
458
        try:
497
459
            fmt = self.controlfile('branch-format', 'r').read()
498
460
        except NoSuchFile:
499
 
            raise NotBranchError(path=self.base)
500
 
        mutter("got branch format %r", fmt)
501
 
        if fmt == BZR_BRANCH_FORMAT_6:
502
 
            self._branch_format = 6
503
 
        elif fmt == BZR_BRANCH_FORMAT_5:
 
461
            raise NotBranchError(self.base)
 
462
 
 
463
        if fmt == BZR_BRANCH_FORMAT_5:
504
464
            self._branch_format = 5
505
465
        elif fmt == BZR_BRANCH_FORMAT_4:
506
466
            self._branch_format = 4
507
467
 
508
468
        if (not relax_version_check
509
 
            and self._branch_format not in (5, 6)):
510
 
            raise errors.UnsupportedFormatError(
511
 
                           'sorry, branch format %r not supported' % fmt,
 
469
            and self._branch_format != 5):
 
470
            raise BzrError('sorry, branch format %r not supported' % fmt,
512
471
                           ['use a different bzr version',
513
472
                            'or remove the .bzr directory'
514
473
                            ' and "bzr init" again'])
530
489
                entry.parent_id = inv.root.file_id
531
490
        self._write_inventory(inv)
532
491
 
533
 
    @needs_read_lock
534
492
    def read_working_inventory(self):
535
493
        """Read the working inventory."""
536
 
        # ElementTree does its own conversion from UTF-8, so open in
537
 
        # binary.
538
 
        f = self.controlfile('inventory', 'rb')
539
 
        return bzrlib.xml5.serializer_v5.read_inventory(f)
 
494
        self.lock_read()
 
495
        try:
 
496
            # ElementTree does its own conversion from UTF-8, so open in
 
497
            # binary.
 
498
            f = self.controlfile('inventory', 'rb')
 
499
            return bzrlib.xml5.serializer_v5.read_inventory(f)
 
500
        finally:
 
501
            self.unlock()
 
502
            
540
503
 
541
 
    @needs_write_lock
542
504
    def _write_inventory(self, inv):
543
505
        """Update the working inventory.
544
506
 
546
508
        will be committed to the next revision.
547
509
        """
548
510
        from cStringIO import StringIO
549
 
        sio = StringIO()
550
 
        bzrlib.xml5.serializer_v5.write_inventory(inv, sio)
551
 
        sio.seek(0)
552
 
        # Transport handles atomicity
553
 
        self.put_controlfile('inventory', sio)
 
511
        self.lock_write()
 
512
        try:
 
513
            sio = StringIO()
 
514
            bzrlib.xml5.serializer_v5.write_inventory(inv, sio)
 
515
            sio.seek(0)
 
516
            # Transport handles atomicity
 
517
            self.put_controlfile('inventory', sio)
 
518
        finally:
 
519
            self.unlock()
554
520
        
555
521
        mutter('wrote working inventory')
556
522
            
557
523
    inventory = property(read_working_inventory, _write_inventory, None,
558
524
                         """Inventory for the working copy.""")
559
525
 
560
 
    @needs_write_lock
561
526
    def add(self, files, ids=None):
562
527
        """Make files versioned.
563
528
 
593
558
        else:
594
559
            assert(len(ids) == len(files))
595
560
 
596
 
        inv = self.read_working_inventory()
597
 
        for f,file_id in zip(files, ids):
598
 
            if is_control_file(f):
599
 
                raise BzrError("cannot add control file %s" % quotefn(f))
600
 
 
601
 
            fp = splitpath(f)
602
 
 
603
 
            if len(fp) == 0:
604
 
                raise BzrError("cannot add top-level %r" % f)
605
 
 
606
 
            fullpath = os.path.normpath(self.abspath(f))
607
 
 
608
 
            try:
609
 
                kind = file_kind(fullpath)
610
 
            except OSError:
611
 
                # maybe something better?
612
 
                raise BzrError('cannot add: not a regular file, symlink or directory: %s' % quotefn(f))
613
 
 
614
 
            if not InventoryEntry.versionable_kind(kind):
615
 
                raise BzrError('cannot add: not a versionable file ('
616
 
                               'i.e. regular file, symlink or directory): %s' % quotefn(f))
617
 
 
618
 
            if file_id is None:
619
 
                file_id = gen_file_id(f)
620
 
            inv.add_path(f, kind=kind, file_id=file_id)
621
 
 
622
 
            mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
623
 
 
624
 
        self._write_inventory(inv)
625
 
 
626
 
    @needs_read_lock
 
561
        self.lock_write()
 
562
        try:
 
563
            inv = self.read_working_inventory()
 
564
            for f,file_id in zip(files, ids):
 
565
                if is_control_file(f):
 
566
                    raise BzrError("cannot add control file %s" % quotefn(f))
 
567
 
 
568
                fp = splitpath(f)
 
569
 
 
570
                if len(fp) == 0:
 
571
                    raise BzrError("cannot add top-level %r" % f)
 
572
 
 
573
                fullpath = os.path.normpath(self.abspath(f))
 
574
 
 
575
                try:
 
576
                    kind = file_kind(fullpath)
 
577
                except OSError:
 
578
                    # maybe something better?
 
579
                    raise BzrError('cannot add: not a regular file, symlink or directory: %s' % quotefn(f))
 
580
 
 
581
                if kind not in ('file', 'directory', 'symlink'):
 
582
                    raise BzrError('cannot add: not a regular file, symlink or directory: %s' % quotefn(f))
 
583
 
 
584
                if file_id is None:
 
585
                    file_id = gen_file_id(f)
 
586
                inv.add_path(f, kind=kind, file_id=file_id)
 
587
 
 
588
                mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
 
589
 
 
590
            self._write_inventory(inv)
 
591
        finally:
 
592
            self.unlock()
 
593
            
 
594
 
627
595
    def print_file(self, file, revno):
628
596
        """Print `file` to stdout."""
629
 
        tree = self.revision_tree(self.get_rev_id(revno))
630
 
        # use inventory as it was in that revision
631
 
        file_id = tree.inventory.path2id(file)
632
 
        if not file_id:
633
 
            raise BzrError("%r is not present in revision %s" % (file, revno))
634
 
        tree.print_file(file_id)
 
597
        self.lock_read()
 
598
        try:
 
599
            tree = self.revision_tree(self.get_rev_id(revno))
 
600
            # use inventory as it was in that revision
 
601
            file_id = tree.inventory.path2id(file)
 
602
            if not file_id:
 
603
                raise BzrError("%r is not present in revision %s" % (file, revno))
 
604
            tree.print_file(file_id)
 
605
        finally:
 
606
            self.unlock()
 
607
 
 
608
 
 
609
    def remove(self, files, verbose=False):
 
610
        """Mark nominated files for removal from the inventory.
 
611
 
 
612
        This does not remove their text.  This does not run on 
 
613
 
 
614
        TODO: Refuse to remove modified files unless --force is given?
 
615
 
 
616
        TODO: Do something useful with directories.
 
617
 
 
618
        TODO: Should this remove the text or not?  Tough call; not
 
619
        removing may be useful and the user can just use use rm, and
 
620
        is the opposite of add.  Removing it is consistent with most
 
621
        other tools.  Maybe an option.
 
622
        """
 
623
        ## TODO: Normalize names
 
624
        ## TODO: Remove nested loops; better scalability
 
625
        if isinstance(files, basestring):
 
626
            files = [files]
 
627
 
 
628
        self.lock_write()
 
629
 
 
630
        try:
 
631
            tree = self.working_tree()
 
632
            inv = tree.inventory
 
633
 
 
634
            # do this before any modifications
 
635
            for f in files:
 
636
                fid = inv.path2id(f)
 
637
                if not fid:
 
638
                    raise BzrError("cannot remove unversioned file %s" % quotefn(f))
 
639
                mutter("remove inventory entry %s {%s}" % (quotefn(f), fid))
 
640
                if verbose:
 
641
                    # having remove it, it must be either ignored or unknown
 
642
                    if tree.is_ignored(f):
 
643
                        new_status = 'I'
 
644
                    else:
 
645
                        new_status = '?'
 
646
                    show_status(new_status, inv[fid].kind, quotefn(f))
 
647
                del inv[fid]
 
648
 
 
649
            self._write_inventory(inv)
 
650
        finally:
 
651
            self.unlock()
635
652
 
636
653
    # FIXME: this doesn't need to be a branch method
637
654
    def set_inventory(self, new_inventory_list):
641
658
            name = os.path.basename(path)
642
659
            if name == "":
643
660
                continue
644
 
            # fixme, there should be a factory function inv,add_?? 
645
 
            if kind == 'directory':
646
 
                inv.add(inventory.InventoryDirectory(file_id, name, parent))
647
 
            elif kind == 'file':
648
 
                inv.add(inventory.InventoryFile(file_id, name, parent))
649
 
            elif kind == 'symlink':
650
 
                inv.add(inventory.InventoryLink(file_id, name, parent))
651
 
            else:
652
 
                raise BzrError("unknown kind %r" % kind)
 
661
            inv.add(InventoryEntry(file_id, name, kind, parent))
653
662
        self._write_inventory(inv)
654
663
 
655
664
    def unknowns(self):
658
667
        These are files in the working directory that are not versioned or
659
668
        control files or ignored.
660
669
        
661
 
        >>> from bzrlib.workingtree import WorkingTree
662
670
        >>> b = ScratchBranch(files=['foo', 'foo~'])
663
 
        >>> map(str, b.unknowns())
 
671
        >>> list(b.unknowns())
664
672
        ['foo']
665
673
        >>> b.add('foo')
666
674
        >>> list(b.unknowns())
667
675
        []
668
 
        >>> WorkingTree(b.base, b).remove('foo')
 
676
        >>> b.remove('foo')
669
677
        >>> list(b.unknowns())
670
 
        [u'foo']
 
678
        ['foo']
671
679
        """
672
680
        return self.working_tree().unknowns()
673
681
 
674
 
    @needs_write_lock
 
682
 
675
683
    def append_revision(self, *revision_ids):
676
684
        for revision_id in revision_ids:
677
685
            mutter("add {%s} to revision-history" % revision_id)
678
 
        rev_history = self.revision_history()
679
 
        rev_history.extend(revision_ids)
680
 
        self.set_revision_history(rev_history)
681
 
 
682
 
    @needs_write_lock
683
 
    def set_revision_history(self, rev_history):
684
 
        self.put_controlfile('revision-history', '\n'.join(rev_history))
 
686
        self.lock_write()
 
687
        try:
 
688
            rev_history = self.revision_history()
 
689
            rev_history.extend(revision_ids)
 
690
            self.put_controlfile('revision-history', '\n'.join(rev_history))
 
691
        finally:
 
692
            self.unlock()
685
693
 
686
694
    def has_revision(self, revision_id):
687
695
        """True if this branch has a copy of the revision.
689
697
        This does not necessarily imply the revision is merge
690
698
        or on the mainline."""
691
699
        return (revision_id is None
692
 
                or self.revision_store.has_id(revision_id))
 
700
                or revision_id in self.revision_store)
693
701
 
694
 
    @needs_read_lock
695
702
    def get_revision_xml_file(self, revision_id):
696
703
        """Return XML file object for revision object."""
697
704
        if not revision_id or not isinstance(revision_id, basestring):
698
 
            raise InvalidRevisionId(revision_id=revision_id, branch=self)
 
705
            raise InvalidRevisionId(revision_id)
 
706
 
 
707
        self.lock_read()
699
708
        try:
700
 
            return self.revision_store.get(revision_id)
701
 
        except (IndexError, KeyError):
702
 
            raise bzrlib.errors.NoSuchRevision(self, revision_id)
 
709
            try:
 
710
                return self.revision_store[revision_id]
 
711
            except (IndexError, KeyError):
 
712
                raise bzrlib.errors.NoSuchRevision(self, revision_id)
 
713
        finally:
 
714
            self.unlock()
703
715
 
704
716
    #deprecated
705
717
    get_revision_xml = get_revision_xml_file
753
765
        # But for now, just hash the contents.
754
766
        return bzrlib.osutils.sha_file(self.get_revision_xml_file(revision_id))
755
767
 
 
768
    def _get_ancestry_weave(self):
 
769
        return self.control_weaves.get_weave('ancestry')
 
770
 
756
771
    def get_ancestry(self, revision_id):
757
772
        """Return a list of revision-ids integrated by a revision.
758
 
        
759
 
        This currently returns a list, but the ordering is not guaranteed:
760
 
        treat it as a set.
761
773
        """
 
774
        # strip newlines
762
775
        if revision_id is None:
763
776
            return [None]
764
 
        w = self.get_inventory_weave()
765
 
        return [None] + map(w.idx_to_name,
766
 
                            w.inclusions([w.lookup(revision_id)]))
 
777
        w = self._get_ancestry_weave()
 
778
        return [None] + [l[:-1] for l in w.get_iter(w.lookup(revision_id))]
767
779
 
768
780
    def get_inventory_weave(self):
769
 
        return self.control_weaves.get_weave('inventory',
770
 
                                             self.get_transaction())
 
781
        return self.control_weaves.get_weave('inventory')
771
782
 
772
783
    def get_inventory(self, revision_id):
773
784
        """Get Inventory object by hash."""
798
809
        else:
799
810
            return self.get_inventory(revision_id)
800
811
 
801
 
    @needs_read_lock
802
812
    def revision_history(self):
803
813
        """Return sequence of revision hashes on to this branch."""
804
 
        transaction = self.get_transaction()
805
 
        history = transaction.map.find_revision_history()
806
 
        if history is not None:
807
 
            mutter("cache hit for revision-history in %s", self)
808
 
            return list(history)
809
 
        history = [l.rstrip('\r\n') for l in
810
 
                self.controlfile('revision-history', 'r').readlines()]
811
 
        transaction.map.add_revision_history(history)
812
 
        # this call is disabled because revision_history is 
813
 
        # not really an object yet, and the transaction is for objects.
814
 
        # transaction.register_clean(history, precious=True)
815
 
        return list(history)
 
814
        self.lock_read()
 
815
        try:
 
816
            return [l.rstrip('\r\n') for l in
 
817
                    self.controlfile('revision-history', 'r').readlines()]
 
818
        finally:
 
819
            self.unlock()
 
820
 
 
821
    def common_ancestor(self, other, self_revno=None, other_revno=None):
 
822
        """
 
823
        >>> from bzrlib.commit import commit
 
824
        >>> sb = ScratchBranch(files=['foo', 'foo~'])
 
825
        >>> sb.common_ancestor(sb) == (None, None)
 
826
        True
 
827
        >>> commit(sb, "Committing first revision", verbose=False)
 
828
        >>> sb.common_ancestor(sb)[0]
 
829
        1
 
830
        >>> clone = sb.clone()
 
831
        >>> commit(sb, "Committing second revision", verbose=False)
 
832
        >>> sb.common_ancestor(sb)[0]
 
833
        2
 
834
        >>> sb.common_ancestor(clone)[0]
 
835
        1
 
836
        >>> commit(clone, "Committing divergent second revision", 
 
837
        ...               verbose=False)
 
838
        >>> sb.common_ancestor(clone)[0]
 
839
        1
 
840
        >>> sb.common_ancestor(clone) == clone.common_ancestor(sb)
 
841
        True
 
842
        >>> sb.common_ancestor(sb) != clone.common_ancestor(clone)
 
843
        True
 
844
        >>> clone2 = sb.clone()
 
845
        >>> sb.common_ancestor(clone2)[0]
 
846
        2
 
847
        >>> sb.common_ancestor(clone2, self_revno=1)[0]
 
848
        1
 
849
        >>> sb.common_ancestor(clone2, other_revno=1)[0]
 
850
        1
 
851
        """
 
852
        my_history = self.revision_history()
 
853
        other_history = other.revision_history()
 
854
        if self_revno is None:
 
855
            self_revno = len(my_history)
 
856
        if other_revno is None:
 
857
            other_revno = len(other_history)
 
858
        indices = range(min((self_revno, other_revno)))
 
859
        indices.reverse()
 
860
        for r in indices:
 
861
            if my_history[r] == other_history[r]:
 
862
                return r+1, my_history[r]
 
863
        return None, None
 
864
 
816
865
 
817
866
    def revno(self):
818
867
        """Return current revision number for this branch.
822
871
        """
823
872
        return len(self.revision_history())
824
873
 
 
874
 
825
875
    def last_revision(self):
826
876
        """Return last patch hash, or None if no history.
827
877
        """
831
881
        else:
832
882
            return None
833
883
 
 
884
 
834
885
    def missing_revisions(self, other, stop_revision=None, diverged_ok=False):
835
886
        """Return a list of new revisions that would perfectly fit.
836
887
        
859
910
        Traceback (most recent call last):
860
911
        DivergedBranches: These branches have diverged.
861
912
        """
 
913
        # FIXME: If the branches have diverged, but the latest
 
914
        # revision in this branch is completely merged into the other,
 
915
        # then we should still be able to pull.
862
916
        self_history = self.revision_history()
863
917
        self_len = len(self_history)
864
918
        other_history = other.revision_history()
879
933
    def update_revisions(self, other, stop_revision=None):
880
934
        """Pull in new perfect-fit revisions."""
881
935
        from bzrlib.fetch import greedy_fetch
 
936
        from bzrlib.revision import get_intervening_revisions
882
937
        if stop_revision is None:
883
938
            stop_revision = other.last_revision()
884
 
        ### Should this be checking is_ancestor instead of revision_history?
885
 
        if (stop_revision is not None and 
886
 
            stop_revision in self.revision_history()):
887
 
            return
888
939
        greedy_fetch(to_branch=self, from_branch=other,
889
940
                     revision=stop_revision)
890
 
        pullable_revs = self.pullable_revisions(other, stop_revision)
891
 
        if len(pullable_revs) > 0:
 
941
        pullable_revs = self.missing_revisions(
 
942
            other, other.revision_id_to_revno(stop_revision))
 
943
        if pullable_revs:
 
944
            greedy_fetch(to_branch=self,
 
945
                         from_branch=other,
 
946
                         revision=pullable_revs[-1])
892
947
            self.append_revision(*pullable_revs)
 
948
    
893
949
 
894
 
    def pullable_revisions(self, other, stop_revision):
895
 
        other_revno = other.revision_id_to_revno(stop_revision)
896
 
        try:
897
 
            return self.missing_revisions(other, other_revno)
898
 
        except DivergedBranches, e:
899
 
            try:
900
 
                pullable_revs = get_intervening_revisions(self.last_revision(),
901
 
                                                          stop_revision, self)
902
 
                assert self.last_revision() not in pullable_revs
903
 
                return pullable_revs
904
 
            except bzrlib.errors.NotAncestor:
905
 
                if is_ancestor(self.last_revision(), stop_revision, self):
906
 
                    return []
907
 
                else:
908
 
                    raise e
909
 
        
910
950
    def commit(self, *args, **kw):
911
951
        from bzrlib.commit import Commit
912
952
        Commit().commit(self, *args, **kw)
938
978
        an `EmptyTree` is returned."""
939
979
        # TODO: refactor this to use an existing revision object
940
980
        # so we don't need to read it in twice.
941
 
        if revision_id == None or revision_id == NULL_REVISION:
 
981
        if revision_id == None:
942
982
            return EmptyTree()
943
983
        else:
944
984
            inv = self.get_revision_inventory(revision_id)
945
985
            return RevisionTree(self.weave_store, inv, revision_id)
946
986
 
 
987
 
947
988
    def working_tree(self):
948
989
        """Return a `Tree` for the working copy."""
949
990
        from bzrlib.workingtree import WorkingTree
950
 
        # TODO: In the future, perhaps WorkingTree should utilize Transport
951
 
        # RobertCollins 20051003 - I don't think it should - working trees are
952
 
        # much more complex to keep consistent than our careful .bzr subset.
953
 
        # instead, we should say that working trees are local only, and optimise
954
 
        # for that.
955
 
        return WorkingTree(self.base, branch=self)
 
991
        # TODO: In the future, WorkingTree should utilize Transport
 
992
        return WorkingTree(self._transport.base, self.read_working_inventory())
956
993
 
957
 
    @needs_write_lock
958
 
    def pull(self, source, overwrite=False):
959
 
        source.lock_read()
960
 
        try:
961
 
            try:
962
 
                self.update_revisions(source)
963
 
            except DivergedBranches:
964
 
                if not overwrite:
965
 
                    raise
966
 
                self.set_revision_history(source.revision_history())
967
 
        finally:
968
 
            source.unlock()
969
994
 
970
995
    def basis_tree(self):
971
996
        """Return `Tree` object for last revision.
974
999
        """
975
1000
        return self.revision_tree(self.last_revision())
976
1001
 
977
 
    @needs_write_lock
 
1002
 
978
1003
    def rename_one(self, from_rel, to_rel):
979
1004
        """Rename one file.
980
1005
 
981
1006
        This can change the directory or the filename or both.
982
1007
        """
983
 
        tree = self.working_tree()
984
 
        inv = tree.inventory
985
 
        if not tree.has_filename(from_rel):
986
 
            raise BzrError("can't rename: old working file %r does not exist" % from_rel)
987
 
        if tree.has_filename(to_rel):
988
 
            raise BzrError("can't rename: new working file %r already exists" % to_rel)
989
 
 
990
 
        file_id = inv.path2id(from_rel)
991
 
        if file_id == None:
992
 
            raise BzrError("can't rename: old name %r is not versioned" % from_rel)
993
 
 
994
 
        if inv.path2id(to_rel):
995
 
            raise BzrError("can't rename: new name %r is already versioned" % to_rel)
996
 
 
997
 
        to_dir, to_tail = os.path.split(to_rel)
998
 
        to_dir_id = inv.path2id(to_dir)
999
 
        if to_dir_id == None and to_dir != '':
1000
 
            raise BzrError("can't determine destination directory id for %r" % to_dir)
1001
 
 
1002
 
        mutter("rename_one:")
1003
 
        mutter("  file_id    {%s}" % file_id)
1004
 
        mutter("  from_rel   %r" % from_rel)
1005
 
        mutter("  to_rel     %r" % to_rel)
1006
 
        mutter("  to_dir     %r" % to_dir)
1007
 
        mutter("  to_dir_id  {%s}" % to_dir_id)
1008
 
 
1009
 
        inv.rename(file_id, to_dir_id, to_tail)
1010
 
 
1011
 
        from_abs = self.abspath(from_rel)
1012
 
        to_abs = self.abspath(to_rel)
 
1008
        self.lock_write()
1013
1009
        try:
1014
 
            rename(from_abs, to_abs)
1015
 
        except OSError, e:
1016
 
            raise BzrError("failed to rename %r to %r: %s"
1017
 
                    % (from_abs, to_abs, e[1]),
1018
 
                    ["rename rolled back"])
1019
 
 
1020
 
        self._write_inventory(inv)
1021
 
 
1022
 
    @needs_write_lock
 
1010
            tree = self.working_tree()
 
1011
            inv = tree.inventory
 
1012
            if not tree.has_filename(from_rel):
 
1013
                raise BzrError("can't rename: old working file %r does not exist" % from_rel)
 
1014
            if tree.has_filename(to_rel):
 
1015
                raise BzrError("can't rename: new working file %r already exists" % to_rel)
 
1016
 
 
1017
            file_id = inv.path2id(from_rel)
 
1018
            if file_id == None:
 
1019
                raise BzrError("can't rename: old name %r is not versioned" % from_rel)
 
1020
 
 
1021
            if inv.path2id(to_rel):
 
1022
                raise BzrError("can't rename: new name %r is already versioned" % to_rel)
 
1023
 
 
1024
            to_dir, to_tail = os.path.split(to_rel)
 
1025
            to_dir_id = inv.path2id(to_dir)
 
1026
            if to_dir_id == None and to_dir != '':
 
1027
                raise BzrError("can't determine destination directory id for %r" % to_dir)
 
1028
 
 
1029
            mutter("rename_one:")
 
1030
            mutter("  file_id    {%s}" % file_id)
 
1031
            mutter("  from_rel   %r" % from_rel)
 
1032
            mutter("  to_rel     %r" % to_rel)
 
1033
            mutter("  to_dir     %r" % to_dir)
 
1034
            mutter("  to_dir_id  {%s}" % to_dir_id)
 
1035
 
 
1036
            inv.rename(file_id, to_dir_id, to_tail)
 
1037
 
 
1038
            from_abs = self.abspath(from_rel)
 
1039
            to_abs = self.abspath(to_rel)
 
1040
            try:
 
1041
                rename(from_abs, to_abs)
 
1042
            except OSError, e:
 
1043
                raise BzrError("failed to rename %r to %r: %s"
 
1044
                        % (from_abs, to_abs, e[1]),
 
1045
                        ["rename rolled back"])
 
1046
 
 
1047
            self._write_inventory(inv)
 
1048
        finally:
 
1049
            self.unlock()
 
1050
 
 
1051
 
1023
1052
    def move(self, from_paths, to_name):
1024
1053
        """Rename files.
1025
1054
 
1035
1064
        entry that is moved.
1036
1065
        """
1037
1066
        result = []
1038
 
        ## TODO: Option to move IDs only
1039
 
        assert not isinstance(from_paths, basestring)
1040
 
        tree = self.working_tree()
1041
 
        inv = tree.inventory
1042
 
        to_abs = self.abspath(to_name)
1043
 
        if not isdir(to_abs):
1044
 
            raise BzrError("destination %r is not a directory" % to_abs)
1045
 
        if not tree.has_filename(to_name):
1046
 
            raise BzrError("destination %r not in working directory" % to_abs)
1047
 
        to_dir_id = inv.path2id(to_name)
1048
 
        if to_dir_id == None and to_name != '':
1049
 
            raise BzrError("destination %r is not a versioned directory" % to_name)
1050
 
        to_dir_ie = inv[to_dir_id]
1051
 
        if to_dir_ie.kind not in ('directory', 'root_directory'):
1052
 
            raise BzrError("destination %r is not a directory" % to_abs)
1053
 
 
1054
 
        to_idpath = inv.get_idpath(to_dir_id)
1055
 
 
1056
 
        for f in from_paths:
1057
 
            if not tree.has_filename(f):
1058
 
                raise BzrError("%r does not exist in working tree" % f)
1059
 
            f_id = inv.path2id(f)
1060
 
            if f_id == None:
1061
 
                raise BzrError("%r is not versioned" % f)
1062
 
            name_tail = splitpath(f)[-1]
1063
 
            dest_path = appendpath(to_name, name_tail)
1064
 
            if tree.has_filename(dest_path):
1065
 
                raise BzrError("destination %r already exists" % dest_path)
1066
 
            if f_id in to_idpath:
1067
 
                raise BzrError("can't move %r to a subdirectory of itself" % f)
1068
 
 
1069
 
        # OK, so there's a race here, it's possible that someone will
1070
 
        # create a file in this interval and then the rename might be
1071
 
        # left half-done.  But we should have caught most problems.
1072
 
 
1073
 
        for f in from_paths:
1074
 
            name_tail = splitpath(f)[-1]
1075
 
            dest_path = appendpath(to_name, name_tail)
1076
 
            result.append((f, dest_path))
1077
 
            inv.rename(inv.path2id(f), to_dir_id, name_tail)
1078
 
            try:
1079
 
                rename(self.abspath(f), self.abspath(dest_path))
1080
 
            except OSError, e:
1081
 
                raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
1082
 
                        ["rename rolled back"])
1083
 
 
1084
 
        self._write_inventory(inv)
 
1067
        self.lock_write()
 
1068
        try:
 
1069
            ## TODO: Option to move IDs only
 
1070
            assert not isinstance(from_paths, basestring)
 
1071
            tree = self.working_tree()
 
1072
            inv = tree.inventory
 
1073
            to_abs = self.abspath(to_name)
 
1074
            if not isdir(to_abs):
 
1075
                raise BzrError("destination %r is not a directory" % to_abs)
 
1076
            if not tree.has_filename(to_name):
 
1077
                raise BzrError("destination %r not in working directory" % to_abs)
 
1078
            to_dir_id = inv.path2id(to_name)
 
1079
            if to_dir_id == None and to_name != '':
 
1080
                raise BzrError("destination %r is not a versioned directory" % to_name)
 
1081
            to_dir_ie = inv[to_dir_id]
 
1082
            if to_dir_ie.kind not in ('directory', 'root_directory'):
 
1083
                raise BzrError("destination %r is not a directory" % to_abs)
 
1084
 
 
1085
            to_idpath = inv.get_idpath(to_dir_id)
 
1086
 
 
1087
            for f in from_paths:
 
1088
                if not tree.has_filename(f):
 
1089
                    raise BzrError("%r does not exist in working tree" % f)
 
1090
                f_id = inv.path2id(f)
 
1091
                if f_id == None:
 
1092
                    raise BzrError("%r is not versioned" % f)
 
1093
                name_tail = splitpath(f)[-1]
 
1094
                dest_path = appendpath(to_name, name_tail)
 
1095
                if tree.has_filename(dest_path):
 
1096
                    raise BzrError("destination %r already exists" % dest_path)
 
1097
                if f_id in to_idpath:
 
1098
                    raise BzrError("can't move %r to a subdirectory of itself" % f)
 
1099
 
 
1100
            # OK, so there's a race here, it's possible that someone will
 
1101
            # create a file in this interval and then the rename might be
 
1102
            # left half-done.  But we should have caught most problems.
 
1103
 
 
1104
            for f in from_paths:
 
1105
                name_tail = splitpath(f)[-1]
 
1106
                dest_path = appendpath(to_name, name_tail)
 
1107
                result.append((f, dest_path))
 
1108
                inv.rename(inv.path2id(f), to_dir_id, name_tail)
 
1109
                try:
 
1110
                    rename(self.abspath(f), self.abspath(dest_path))
 
1111
                except OSError, e:
 
1112
                    raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
 
1113
                            ["rename rolled back"])
 
1114
 
 
1115
            self._write_inventory(inv)
 
1116
        finally:
 
1117
            self.unlock()
 
1118
 
1085
1119
        return result
1086
1120
 
1087
1121
 
1092
1126
            If true (default) backups are made of files before
1093
1127
            they're renamed.
1094
1128
        """
 
1129
        from bzrlib.errors import NotVersionedError, BzrError
1095
1130
        from bzrlib.atomicfile import AtomicFile
1096
1131
        from bzrlib.osutils import backup_file
1097
1132
        
1104
1139
        for fn in filenames:
1105
1140
            file_id = inv.path2id(fn)
1106
1141
            if not file_id:
1107
 
                raise NotVersionedError(path=fn)
 
1142
                raise NotVersionedError("not a versioned file", fn)
1108
1143
            if not old_inv.has_id(file_id):
1109
1144
                raise BzrError("file not present in old tree", fn, file_id)
1110
1145
            nids.append((fn, file_id))
1146
1181
    def add_pending_merge(self, *revision_ids):
1147
1182
        # TODO: Perhaps should check at this point that the
1148
1183
        # history of the revision is actually present?
 
1184
        for rev_id in revision_ids:
 
1185
            validate_revision_id(rev_id)
 
1186
 
1149
1187
        p = self.pending_merges()
1150
1188
        updated = False
1151
1189
        for rev_id in revision_ids:
1156
1194
        if updated:
1157
1195
            self.set_pending_merges(p)
1158
1196
 
1159
 
    @needs_write_lock
1160
1197
    def set_pending_merges(self, rev_list):
1161
 
        self.put_controlfile('pending-merges', '\n'.join(rev_list))
 
1198
        self.lock_write()
 
1199
        try:
 
1200
            self.put_controlfile('pending-merges', '\n'.join(rev_list))
 
1201
        finally:
 
1202
            self.unlock()
 
1203
 
1162
1204
 
1163
1205
    def get_parent(self):
1164
1206
        """Return the parent location of the branch.
1177
1219
                    raise
1178
1220
        return None
1179
1221
 
1180
 
    def get_push_location(self):
1181
 
        """Return the None or the location to push this branch to."""
1182
 
        config = bzrlib.config.BranchConfig(self)
1183
 
        push_loc = config.get_user_option('push_location')
1184
 
        return push_loc
1185
 
 
1186
 
    def set_push_location(self, location):
1187
 
        """Set a new push location for this branch."""
1188
 
        config = bzrlib.config.LocationConfig(self.base)
1189
 
        config.set_user_option('push_location', location)
1190
 
 
1191
 
    @needs_write_lock
 
1222
 
1192
1223
    def set_parent(self, url):
1193
1224
        # TODO: Maybe delete old location files?
1194
1225
        from bzrlib.atomicfile import AtomicFile
1195
 
        f = AtomicFile(self.controlfilename('parent'))
 
1226
        self.lock_write()
1196
1227
        try:
1197
 
            f.write(url + '\n')
1198
 
            f.commit()
 
1228
            f = AtomicFile(self.controlfilename('parent'))
 
1229
            try:
 
1230
                f.write(url + '\n')
 
1231
                f.commit()
 
1232
            finally:
 
1233
                f.close()
1199
1234
        finally:
1200
 
            f.close()
 
1235
            self.unlock()
1201
1236
 
1202
1237
    def check_revno(self, revno):
1203
1238
        """\
1215
1250
        if revno < 1 or revno > self.revno():
1216
1251
            raise InvalidRevisionNumber(revno)
1217
1252
        
1218
 
    def sign_revision(self, revision_id, gpg_strategy):
1219
 
        plaintext = Testament.from_revision(self, revision_id).as_short_text()
1220
 
        self.store_revision_signature(gpg_strategy, plaintext, revision_id)
1221
 
 
1222
 
    @needs_write_lock
1223
 
    def store_revision_signature(self, gpg_strategy, plaintext, revision_id):
1224
 
        self.revision_store.add(StringIO(gpg_strategy.sign(plaintext)), 
1225
 
                                revision_id, "sig")
 
1253
        
 
1254
        
1226
1255
 
1227
1256
 
1228
1257
class ScratchBranch(_Branch):
1232
1261
    >>> isdir(b.base)
1233
1262
    True
1234
1263
    >>> bd = b.base
1235
 
    >>> b._transport.__del__()
 
1264
    >>> b.destroy()
1236
1265
    >>> isdir(bd)
1237
1266
    False
1238
1267
    """
1239
 
 
1240
 
    def __init__(self, files=[], dirs=[], transport=None):
 
1268
    def __init__(self, files=[], dirs=[], base=None):
1241
1269
        """Make a test branch.
1242
1270
 
1243
1271
        This creates a temporary directory and runs init-tree in it.
1244
1272
 
1245
1273
        If any files are listed, they are created in the working copy.
1246
1274
        """
1247
 
        if transport is None:
1248
 
            transport = bzrlib.transport.local.ScratchTransport()
1249
 
            super(ScratchBranch, self).__init__(transport, init=True)
1250
 
        else:
1251
 
            super(ScratchBranch, self).__init__(transport)
1252
 
 
 
1275
        from tempfile import mkdtemp
 
1276
        init = False
 
1277
        if base is None:
 
1278
            base = mkdtemp()
 
1279
            init = True
 
1280
        if isinstance(base, basestring):
 
1281
            base = get_transport(base)
 
1282
        _Branch.__init__(self, base, init=init)
1253
1283
        for d in dirs:
1254
1284
            self._transport.mkdir(d)
1255
1285
            
1275
1305
        base = mkdtemp()
1276
1306
        os.rmdir(base)
1277
1307
        copytree(self.base, base, symlinks=True)
1278
 
        return ScratchBranch(
1279
 
            transport=bzrlib.transport.local.ScratchTransport(base))
 
1308
        return ScratchBranch(base=base)
 
1309
 
 
1310
    def __del__(self):
 
1311
        self.destroy()
 
1312
 
 
1313
    def destroy(self):
 
1314
        """Destroy the test branch, removing the scratch directory."""
 
1315
        from shutil import rmtree
 
1316
        try:
 
1317
            if self.base:
 
1318
                mutter("delete ScratchBranch %s" % self.base)
 
1319
                rmtree(self.base)
 
1320
        except OSError, e:
 
1321
            # Work around for shutil.rmtree failing on Windows when
 
1322
            # readonly files are encountered
 
1323
            mutter("hit exception in destroying ScratchBranch: %s" % e)
 
1324
            for root, dirs, files in os.walk(self.base, topdown=False):
 
1325
                for name in files:
 
1326
                    os.chmod(os.path.join(root, name), 0700)
 
1327
            rmtree(self.base)
 
1328
        self._transport = None
 
1329
 
1280
1330
    
1281
1331
 
1282
1332
######################################################################