~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Robert Collins
  • Date: 2005-10-10 08:29:42 UTC
  • mfrom: (1185.15.10)
  • Revision ID: robertc@robertcollins.net-20051010082942-4d1442a783ba926d
bugfix from scott for symlink support when not in the tree root.

Show diffs side-by-side

added added

removed removed

Lines of Context:
28
28
from bzrlib.trace import mutter, note
29
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)
 
36
                           UnlistableBranch, NoSuchFile)
37
37
from bzrlib.textui import show_status
38
 
from bzrlib.revision import Revision, is_ancestor, get_intervening_revisions
39
 
 
 
38
from bzrlib.revision import Revision
40
39
from bzrlib.delta import compare_trees
41
40
from bzrlib.tree import EmptyTree, RevisionTree
42
41
from bzrlib.inventory import Inventory
44
43
from bzrlib.store.compressed_text import CompressedTextStore
45
44
from bzrlib.store.text import TextStore
46
45
from bzrlib.store.weave import WeaveStore
47
 
from bzrlib.testament import Testament
48
46
import bzrlib.transactions as transactions
49
47
from bzrlib.transport import Transport, get_transport
50
48
import bzrlib.xml5
66
64
    # XXX: leave this here for about one release, then remove it
67
65
    raise NotImplementedError('find_branch() is not supported anymore, '
68
66
                              'please use one of the new branch constructors')
69
 
 
70
 
 
71
 
def needs_read_lock(unbound):
72
 
    """Decorate unbound to take out and release a read lock."""
73
 
    def decorated(self, *args, **kwargs):
74
 
        self.lock_read()
75
 
        try:
76
 
            return unbound(self, *args, **kwargs)
77
 
        finally:
78
 
            self.unlock()
79
 
    return decorated
80
 
 
81
 
 
82
 
def needs_write_lock(unbound):
83
 
    """Decorate unbound to take out and release a write lock."""
84
 
    def decorated(self, *args, **kwargs):
85
 
        self.lock_write()
86
 
        try:
87
 
            return unbound(self, *args, **kwargs)
88
 
        finally:
89
 
            self.unlock()
90
 
    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
 
 
94
def find_branch_root(t):
 
95
    """Find the branch root enclosing the transport's base.
 
96
 
 
97
    t is a Transport object.
 
98
 
 
99
    It is not necessary that the base of t exists.
 
100
 
 
101
    Basically we keep looking up until we find the control directory or
 
102
    run into the root.  If there isn't one, raises NotBranchError.
 
103
    """
 
104
    orig_base = t.base
 
105
    while True:
 
106
        if t.has(bzrlib.BZRDIR):
 
107
            return t
 
108
        new_t = t.clone('..')
 
109
        if new_t.base == t.base:
 
110
            # reached the root, whatever that may be
 
111
            raise NotBranchError('%s is not in a branch' % orig_base)
 
112
        t = new_t
 
113
 
91
114
 
92
115
######################################################################
93
116
# branch objects
122
145
        """Open an existing branch which contains url.
123
146
        
124
147
        This probes for a branch at url, and searches upwards from there.
125
 
 
126
 
        Basically we keep looking up until we find the control directory or
127
 
        run into the root.  If there isn't one, raises NotBranchError.
128
 
        If there is one, it is returned, along with the unused portion of url.
129
148
        """
130
149
        t = get_transport(url)
131
 
        while True:
132
 
            try:
133
 
                return _Branch(t), t.relpath(url)
134
 
            except NotBranchError:
135
 
                pass
136
 
            new_t = t.clone('..')
137
 
            if new_t.base == t.base:
138
 
                # reached the root, whatever that may be
139
 
                raise NotBranchError(path=url)
140
 
            t = new_t
 
150
        t = find_branch_root(t)
 
151
        return _Branch(t)
141
152
 
142
153
    @staticmethod
143
154
    def initialize(base):
259
270
            self.text_store = get_store('text-store')
260
271
            self.revision_store = get_store('revision-store')
261
272
        elif self._branch_format == 5:
262
 
            self.control_weaves = get_weave('')
 
273
            self.control_weaves = get_weave([])
263
274
            self.weave_store = get_weave('weaves')
264
275
            self.revision_store = get_store('revision-store', compressed=False)
265
276
        elif self._branch_format == 6:
266
 
            self.control_weaves = get_weave('')
 
277
            self.control_weaves = get_weave([])
267
278
            self.weave_store = get_weave('weaves', prefixed=True)
268
279
            self.revision_store = get_store('revision-store', compressed=False,
269
280
                                            prefixed=True)
270
 
        self.revision_store.register_suffix('sig')
271
281
        self._transaction = None
272
282
 
273
283
    def __str__(self):
303
313
            return self._transport.base
304
314
        return None
305
315
 
306
 
    base = property(_get_base, doc="The URL for the root of this branch.")
 
316
    base = property(_get_base)
307
317
 
308
318
    def _finish_transaction(self):
309
319
        """Exit the current transaction."""
333
343
        self._transaction = new_transaction
334
344
 
335
345
    def lock_write(self):
336
 
        mutter("lock write: %s (%s)", self, self._lock_count)
337
346
        # TODO: Upgrade locking to support using a Transport,
338
347
        # and potentially a remote locking protocol
339
348
        if self._lock_mode:
348
357
            self._lock_count = 1
349
358
            self._set_transaction(transactions.PassThroughTransaction())
350
359
 
 
360
 
351
361
    def lock_read(self):
352
 
        mutter("lock read: %s (%s)", self, self._lock_count)
353
362
        if self._lock_mode:
354
363
            assert self._lock_mode in ('r', 'w'), \
355
364
                   "invalid lock mode %r" % self._lock_mode
360
369
            self._lock_mode = 'r'
361
370
            self._lock_count = 1
362
371
            self._set_transaction(transactions.ReadOnlyTransaction())
363
 
            # 5K may be excessive, but hey, its a knob.
364
 
            self.get_transaction().set_cache_size(5000)
365
372
                        
366
373
    def unlock(self):
367
 
        mutter("unlock: %s (%s)", self, self._lock_count)
368
374
        if not self._lock_mode:
369
375
            raise LockError('branch %r is not locked' % (self))
370
376
 
377
383
            self._lock_mode = self._lock_count = None
378
384
 
379
385
    def abspath(self, name):
380
 
        """Return absolute filename for something in the branch
381
 
        
382
 
        XXX: Robert Collins 20051017 what is this used for? why is it a branch
383
 
        method and not a tree method.
384
 
        """
 
386
        """Return absolute filename for something in the branch"""
385
387
        return self._transport.abspath(name)
386
388
 
 
389
    def relpath(self, path):
 
390
        """Return path relative to this branch of something inside it.
 
391
 
 
392
        Raises an error if path is not in this branch."""
 
393
        return self._transport.relpath(path)
 
394
 
 
395
 
387
396
    def _rel_controlfilename(self, file_or_path):
388
 
        if not isinstance(file_or_path, basestring):
389
 
            file_or_path = '/'.join(file_or_path)
390
 
        if file_or_path == '':
391
 
            return bzrlib.BZRDIR
392
 
        return bzrlib.transport.urlescape(bzrlib.BZRDIR + '/' + file_or_path)
 
397
        if isinstance(file_or_path, basestring):
 
398
            file_or_path = [file_or_path]
 
399
        return [bzrlib.BZRDIR] + file_or_path
393
400
 
394
401
    def controlfilename(self, file_or_path):
395
402
        """Return location relative to branch."""
498
505
        try:
499
506
            fmt = self.controlfile('branch-format', 'r').read()
500
507
        except NoSuchFile:
501
 
            raise NotBranchError(path=self.base)
 
508
            raise NotBranchError(self.base)
502
509
        mutter("got branch format %r", fmt)
503
510
        if fmt == BZR_BRANCH_FORMAT_6:
504
511
            self._branch_format = 6
532
539
                entry.parent_id = inv.root.file_id
533
540
        self._write_inventory(inv)
534
541
 
535
 
    @needs_read_lock
536
542
    def read_working_inventory(self):
537
543
        """Read the working inventory."""
538
 
        # ElementTree does its own conversion from UTF-8, so open in
539
 
        # binary.
540
 
        f = self.controlfile('inventory', 'rb')
541
 
        return bzrlib.xml5.serializer_v5.read_inventory(f)
 
544
        self.lock_read()
 
545
        try:
 
546
            # ElementTree does its own conversion from UTF-8, so open in
 
547
            # binary.
 
548
            f = self.controlfile('inventory', 'rb')
 
549
            return bzrlib.xml5.serializer_v5.read_inventory(f)
 
550
        finally:
 
551
            self.unlock()
 
552
            
542
553
 
543
 
    @needs_write_lock
544
554
    def _write_inventory(self, inv):
545
555
        """Update the working inventory.
546
556
 
548
558
        will be committed to the next revision.
549
559
        """
550
560
        from cStringIO import StringIO
551
 
        sio = StringIO()
552
 
        bzrlib.xml5.serializer_v5.write_inventory(inv, sio)
553
 
        sio.seek(0)
554
 
        # Transport handles atomicity
555
 
        self.put_controlfile('inventory', sio)
 
561
        self.lock_write()
 
562
        try:
 
563
            sio = StringIO()
 
564
            bzrlib.xml5.serializer_v5.write_inventory(inv, sio)
 
565
            sio.seek(0)
 
566
            # Transport handles atomicity
 
567
            self.put_controlfile('inventory', sio)
 
568
        finally:
 
569
            self.unlock()
556
570
        
557
571
        mutter('wrote working inventory')
558
572
            
559
573
    inventory = property(read_working_inventory, _write_inventory, None,
560
574
                         """Inventory for the working copy.""")
561
575
 
562
 
    @needs_write_lock
563
576
    def add(self, files, ids=None):
564
577
        """Make files versioned.
565
578
 
595
608
        else:
596
609
            assert(len(ids) == len(files))
597
610
 
598
 
        inv = self.read_working_inventory()
599
 
        for f,file_id in zip(files, ids):
600
 
            if is_control_file(f):
601
 
                raise BzrError("cannot add control file %s" % quotefn(f))
602
 
 
603
 
            fp = splitpath(f)
604
 
 
605
 
            if len(fp) == 0:
606
 
                raise BzrError("cannot add top-level %r" % f)
607
 
 
608
 
            fullpath = os.path.normpath(self.abspath(f))
609
 
 
610
 
            try:
611
 
                kind = file_kind(fullpath)
612
 
            except OSError:
613
 
                # maybe something better?
614
 
                raise BzrError('cannot add: not a regular file, symlink or directory: %s' % quotefn(f))
615
 
 
616
 
            if not InventoryEntry.versionable_kind(kind):
617
 
                raise BzrError('cannot add: not a versionable file ('
618
 
                               'i.e. regular file, symlink or directory): %s' % quotefn(f))
619
 
 
620
 
            if file_id is None:
621
 
                file_id = gen_file_id(f)
622
 
            inv.add_path(f, kind=kind, file_id=file_id)
623
 
 
624
 
            mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
625
 
 
626
 
        self._write_inventory(inv)
627
 
 
628
 
    @needs_read_lock
 
611
        self.lock_write()
 
612
        try:
 
613
            inv = self.read_working_inventory()
 
614
            for f,file_id in zip(files, ids):
 
615
                if is_control_file(f):
 
616
                    raise BzrError("cannot add control file %s" % quotefn(f))
 
617
 
 
618
                fp = splitpath(f)
 
619
 
 
620
                if len(fp) == 0:
 
621
                    raise BzrError("cannot add top-level %r" % f)
 
622
 
 
623
                fullpath = os.path.normpath(self.abspath(f))
 
624
 
 
625
                try:
 
626
                    kind = file_kind(fullpath)
 
627
                except OSError:
 
628
                    # maybe something better?
 
629
                    raise BzrError('cannot add: not a regular file, symlink or directory: %s' % quotefn(f))
 
630
 
 
631
                if not InventoryEntry.versionable_kind(kind):
 
632
                    raise BzrError('cannot add: not a versionable file ('
 
633
                                   'i.e. regular file, symlink or directory): %s' % quotefn(f))
 
634
 
 
635
                if file_id is None:
 
636
                    file_id = gen_file_id(f)
 
637
                inv.add_path(f, kind=kind, file_id=file_id)
 
638
 
 
639
                mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
 
640
 
 
641
            self._write_inventory(inv)
 
642
        finally:
 
643
            self.unlock()
 
644
            
 
645
 
629
646
    def print_file(self, file, revno):
630
647
        """Print `file` to stdout."""
631
 
        tree = self.revision_tree(self.get_rev_id(revno))
632
 
        # use inventory as it was in that revision
633
 
        file_id = tree.inventory.path2id(file)
634
 
        if not file_id:
635
 
            raise BzrError("%r is not present in revision %s" % (file, revno))
636
 
        tree.print_file(file_id)
 
648
        self.lock_read()
 
649
        try:
 
650
            tree = self.revision_tree(self.get_rev_id(revno))
 
651
            # use inventory as it was in that revision
 
652
            file_id = tree.inventory.path2id(file)
 
653
            if not file_id:
 
654
                raise BzrError("%r is not present in revision %s" % (file, revno))
 
655
            tree.print_file(file_id)
 
656
        finally:
 
657
            self.unlock()
 
658
 
 
659
 
 
660
    def remove(self, files, verbose=False):
 
661
        """Mark nominated files for removal from the inventory.
 
662
 
 
663
        This does not remove their text.  This does not run on 
 
664
 
 
665
        TODO: Refuse to remove modified files unless --force is given?
 
666
 
 
667
        TODO: Do something useful with directories.
 
668
 
 
669
        TODO: Should this remove the text or not?  Tough call; not
 
670
        removing may be useful and the user can just use use rm, and
 
671
        is the opposite of add.  Removing it is consistent with most
 
672
        other tools.  Maybe an option.
 
673
        """
 
674
        ## TODO: Normalize names
 
675
        ## TODO: Remove nested loops; better scalability
 
676
        if isinstance(files, basestring):
 
677
            files = [files]
 
678
 
 
679
        self.lock_write()
 
680
 
 
681
        try:
 
682
            tree = self.working_tree()
 
683
            inv = tree.inventory
 
684
 
 
685
            # do this before any modifications
 
686
            for f in files:
 
687
                fid = inv.path2id(f)
 
688
                if not fid:
 
689
                    raise BzrError("cannot remove unversioned file %s" % quotefn(f))
 
690
                mutter("remove inventory entry %s {%s}" % (quotefn(f), fid))
 
691
                if verbose:
 
692
                    # having remove it, it must be either ignored or unknown
 
693
                    if tree.is_ignored(f):
 
694
                        new_status = 'I'
 
695
                    else:
 
696
                        new_status = '?'
 
697
                    show_status(new_status, inv[fid].kind, quotefn(f))
 
698
                del inv[fid]
 
699
 
 
700
            self._write_inventory(inv)
 
701
        finally:
 
702
            self.unlock()
637
703
 
638
704
    # FIXME: this doesn't need to be a branch method
639
705
    def set_inventory(self, new_inventory_list):
660
726
        These are files in the working directory that are not versioned or
661
727
        control files or ignored.
662
728
        
663
 
        >>> from bzrlib.workingtree import WorkingTree
664
729
        >>> b = ScratchBranch(files=['foo', 'foo~'])
665
 
        >>> map(str, b.unknowns())
 
730
        >>> list(b.unknowns())
666
731
        ['foo']
667
732
        >>> b.add('foo')
668
733
        >>> list(b.unknowns())
669
734
        []
670
 
        >>> WorkingTree(b.base, b).remove('foo')
 
735
        >>> b.remove('foo')
671
736
        >>> list(b.unknowns())
672
 
        [u'foo']
 
737
        ['foo']
673
738
        """
674
739
        return self.working_tree().unknowns()
675
740
 
676
 
    @needs_write_lock
 
741
 
677
742
    def append_revision(self, *revision_ids):
678
743
        for revision_id in revision_ids:
679
744
            mutter("add {%s} to revision-history" % revision_id)
680
 
        rev_history = self.revision_history()
681
 
        rev_history.extend(revision_ids)
682
 
        self.set_revision_history(rev_history)
683
 
 
684
 
    @needs_write_lock
685
 
    def set_revision_history(self, rev_history):
686
 
        self.put_controlfile('revision-history', '\n'.join(rev_history))
 
745
        self.lock_write()
 
746
        try:
 
747
            rev_history = self.revision_history()
 
748
            rev_history.extend(revision_ids)
 
749
            self.put_controlfile('revision-history', '\n'.join(rev_history))
 
750
        finally:
 
751
            self.unlock()
687
752
 
688
753
    def has_revision(self, revision_id):
689
754
        """True if this branch has a copy of the revision.
691
756
        This does not necessarily imply the revision is merge
692
757
        or on the mainline."""
693
758
        return (revision_id is None
694
 
                or self.revision_store.has_id(revision_id))
 
759
                or revision_id in self.revision_store)
695
760
 
696
 
    @needs_read_lock
697
761
    def get_revision_xml_file(self, revision_id):
698
762
        """Return XML file object for revision object."""
699
763
        if not revision_id or not isinstance(revision_id, basestring):
700
764
            raise InvalidRevisionId(revision_id)
 
765
 
 
766
        self.lock_read()
701
767
        try:
702
 
            return self.revision_store.get(revision_id)
703
 
        except (IndexError, KeyError):
704
 
            raise bzrlib.errors.NoSuchRevision(self, revision_id)
 
768
            try:
 
769
                return self.revision_store[revision_id]
 
770
            except (IndexError, KeyError):
 
771
                raise bzrlib.errors.NoSuchRevision(self, revision_id)
 
772
        finally:
 
773
            self.unlock()
705
774
 
706
775
    #deprecated
707
776
    get_revision_xml = get_revision_xml_file
800
869
        else:
801
870
            return self.get_inventory(revision_id)
802
871
 
803
 
    @needs_read_lock
804
872
    def revision_history(self):
805
873
        """Return sequence of revision hashes on to this branch."""
806
 
        transaction = self.get_transaction()
807
 
        history = transaction.map.find_revision_history()
808
 
        if history is not None:
809
 
            mutter("cache hit for revision-history in %s", self)
810
 
            return list(history)
811
 
        history = [l.rstrip('\r\n') for l in
812
 
                self.controlfile('revision-history', 'r').readlines()]
813
 
        transaction.map.add_revision_history(history)
814
 
        # this call is disabled because revision_history is 
815
 
        # not really an object yet, and the transaction is for objects.
816
 
        # transaction.register_clean(history, precious=True)
817
 
        return list(history)
 
874
        self.lock_read()
 
875
        try:
 
876
            return [l.rstrip('\r\n') for l in
 
877
                    self.controlfile('revision-history', 'r').readlines()]
 
878
        finally:
 
879
            self.unlock()
 
880
 
 
881
    def common_ancestor(self, other, self_revno=None, other_revno=None):
 
882
        """
 
883
        >>> from bzrlib.commit import commit
 
884
        >>> sb = ScratchBranch(files=['foo', 'foo~'])
 
885
        >>> sb.common_ancestor(sb) == (None, None)
 
886
        True
 
887
        >>> commit(sb, "Committing first revision", verbose=False)
 
888
        >>> sb.common_ancestor(sb)[0]
 
889
        1
 
890
        >>> clone = sb.clone()
 
891
        >>> commit(sb, "Committing second revision", verbose=False)
 
892
        >>> sb.common_ancestor(sb)[0]
 
893
        2
 
894
        >>> sb.common_ancestor(clone)[0]
 
895
        1
 
896
        >>> commit(clone, "Committing divergent second revision", 
 
897
        ...               verbose=False)
 
898
        >>> sb.common_ancestor(clone)[0]
 
899
        1
 
900
        >>> sb.common_ancestor(clone) == clone.common_ancestor(sb)
 
901
        True
 
902
        >>> sb.common_ancestor(sb) != clone.common_ancestor(clone)
 
903
        True
 
904
        >>> clone2 = sb.clone()
 
905
        >>> sb.common_ancestor(clone2)[0]
 
906
        2
 
907
        >>> sb.common_ancestor(clone2, self_revno=1)[0]
 
908
        1
 
909
        >>> sb.common_ancestor(clone2, other_revno=1)[0]
 
910
        1
 
911
        """
 
912
        my_history = self.revision_history()
 
913
        other_history = other.revision_history()
 
914
        if self_revno is None:
 
915
            self_revno = len(my_history)
 
916
        if other_revno is None:
 
917
            other_revno = len(other_history)
 
918
        indices = range(min((self_revno, other_revno)))
 
919
        indices.reverse()
 
920
        for r in indices:
 
921
            if my_history[r] == other_history[r]:
 
922
                return r+1, my_history[r]
 
923
        return None, None
 
924
 
818
925
 
819
926
    def revno(self):
820
927
        """Return current revision number for this branch.
824
931
        """
825
932
        return len(self.revision_history())
826
933
 
 
934
 
827
935
    def last_revision(self):
828
936
        """Return last patch hash, or None if no history.
829
937
        """
833
941
        else:
834
942
            return None
835
943
 
 
944
 
836
945
    def missing_revisions(self, other, stop_revision=None, diverged_ok=False):
837
946
        """Return a list of new revisions that would perfectly fit.
838
947
        
861
970
        Traceback (most recent call last):
862
971
        DivergedBranches: These branches have diverged.
863
972
        """
 
973
        # FIXME: If the branches have diverged, but the latest
 
974
        # revision in this branch is completely merged into the other,
 
975
        # then we should still be able to pull.
864
976
        self_history = self.revision_history()
865
977
        self_len = len(self_history)
866
978
        other_history = other.revision_history()
880
992
 
881
993
    def update_revisions(self, other, stop_revision=None):
882
994
        """Pull in new perfect-fit revisions."""
883
 
        # FIXME: If the branches have diverged, but the latest
884
 
        # revision in this branch is completely merged into the other,
885
 
        # then we should still be able to pull.
886
995
        from bzrlib.fetch import greedy_fetch
 
996
        from bzrlib.revision import get_intervening_revisions
887
997
        if stop_revision is None:
888
998
            stop_revision = other.last_revision()
889
 
        ### Should this be checking is_ancestor instead of revision_history?
890
 
        if (stop_revision is not None and 
891
 
            stop_revision in self.revision_history()):
892
 
            return
893
999
        greedy_fetch(to_branch=self, from_branch=other,
894
1000
                     revision=stop_revision)
895
 
        pullable_revs = self.pullable_revisions(other, stop_revision)
896
 
        if len(pullable_revs) > 0:
 
1001
        pullable_revs = self.missing_revisions(
 
1002
            other, other.revision_id_to_revno(stop_revision))
 
1003
        if pullable_revs:
 
1004
            greedy_fetch(to_branch=self,
 
1005
                         from_branch=other,
 
1006
                         revision=pullable_revs[-1])
897
1007
            self.append_revision(*pullable_revs)
 
1008
    
898
1009
 
899
 
    def pullable_revisions(self, other, stop_revision):
900
 
        other_revno = other.revision_id_to_revno(stop_revision)
901
 
        try:
902
 
            return self.missing_revisions(other, other_revno)
903
 
        except DivergedBranches, e:
904
 
            try:
905
 
                pullable_revs = get_intervening_revisions(self.last_revision(),
906
 
                                                          stop_revision, self)
907
 
                assert self.last_revision() not in pullable_revs
908
 
                return pullable_revs
909
 
            except bzrlib.errors.NotAncestor:
910
 
                if is_ancestor(self.last_revision(), stop_revision, self):
911
 
                    return []
912
 
                else:
913
 
                    raise e
914
 
        
915
1010
    def commit(self, *args, **kw):
916
1011
        from bzrlib.commit import Commit
917
1012
        Commit().commit(self, *args, **kw)
949
1044
            inv = self.get_revision_inventory(revision_id)
950
1045
            return RevisionTree(self.weave_store, inv, revision_id)
951
1046
 
 
1047
 
952
1048
    def working_tree(self):
953
1049
        """Return a `Tree` for the working copy."""
954
1050
        from bzrlib.workingtree import WorkingTree
955
 
        # TODO: In the future, perhaps WorkingTree should utilize Transport
 
1051
        # TODO: In the future, WorkingTree should utilize Transport
956
1052
        # RobertCollins 20051003 - I don't think it should - working trees are
957
1053
        # much more complex to keep consistent than our careful .bzr subset.
958
1054
        # instead, we should say that working trees are local only, and optimise
959
1055
        # for that.
960
 
        return WorkingTree(self.base, branch=self)
 
1056
        return WorkingTree(self._transport.base, self.read_working_inventory())
961
1057
 
962
1058
 
963
1059
    def basis_tree(self):
967
1063
        """
968
1064
        return self.revision_tree(self.last_revision())
969
1065
 
970
 
    @needs_write_lock
 
1066
 
971
1067
    def rename_one(self, from_rel, to_rel):
972
1068
        """Rename one file.
973
1069
 
974
1070
        This can change the directory or the filename or both.
975
1071
        """
976
 
        tree = self.working_tree()
977
 
        inv = tree.inventory
978
 
        if not tree.has_filename(from_rel):
979
 
            raise BzrError("can't rename: old working file %r does not exist" % from_rel)
980
 
        if tree.has_filename(to_rel):
981
 
            raise BzrError("can't rename: new working file %r already exists" % to_rel)
982
 
 
983
 
        file_id = inv.path2id(from_rel)
984
 
        if file_id == None:
985
 
            raise BzrError("can't rename: old name %r is not versioned" % from_rel)
986
 
 
987
 
        if inv.path2id(to_rel):
988
 
            raise BzrError("can't rename: new name %r is already versioned" % to_rel)
989
 
 
990
 
        to_dir, to_tail = os.path.split(to_rel)
991
 
        to_dir_id = inv.path2id(to_dir)
992
 
        if to_dir_id == None and to_dir != '':
993
 
            raise BzrError("can't determine destination directory id for %r" % to_dir)
994
 
 
995
 
        mutter("rename_one:")
996
 
        mutter("  file_id    {%s}" % file_id)
997
 
        mutter("  from_rel   %r" % from_rel)
998
 
        mutter("  to_rel     %r" % to_rel)
999
 
        mutter("  to_dir     %r" % to_dir)
1000
 
        mutter("  to_dir_id  {%s}" % to_dir_id)
1001
 
 
1002
 
        inv.rename(file_id, to_dir_id, to_tail)
1003
 
 
1004
 
        from_abs = self.abspath(from_rel)
1005
 
        to_abs = self.abspath(to_rel)
 
1072
        self.lock_write()
1006
1073
        try:
1007
 
            rename(from_abs, to_abs)
1008
 
        except OSError, e:
1009
 
            raise BzrError("failed to rename %r to %r: %s"
1010
 
                    % (from_abs, to_abs, e[1]),
1011
 
                    ["rename rolled back"])
1012
 
 
1013
 
        self._write_inventory(inv)
1014
 
 
1015
 
    @needs_write_lock
 
1074
            tree = self.working_tree()
 
1075
            inv = tree.inventory
 
1076
            if not tree.has_filename(from_rel):
 
1077
                raise BzrError("can't rename: old working file %r does not exist" % from_rel)
 
1078
            if tree.has_filename(to_rel):
 
1079
                raise BzrError("can't rename: new working file %r already exists" % to_rel)
 
1080
 
 
1081
            file_id = inv.path2id(from_rel)
 
1082
            if file_id == None:
 
1083
                raise BzrError("can't rename: old name %r is not versioned" % from_rel)
 
1084
 
 
1085
            if inv.path2id(to_rel):
 
1086
                raise BzrError("can't rename: new name %r is already versioned" % to_rel)
 
1087
 
 
1088
            to_dir, to_tail = os.path.split(to_rel)
 
1089
            to_dir_id = inv.path2id(to_dir)
 
1090
            if to_dir_id == None and to_dir != '':
 
1091
                raise BzrError("can't determine destination directory id for %r" % to_dir)
 
1092
 
 
1093
            mutter("rename_one:")
 
1094
            mutter("  file_id    {%s}" % file_id)
 
1095
            mutter("  from_rel   %r" % from_rel)
 
1096
            mutter("  to_rel     %r" % to_rel)
 
1097
            mutter("  to_dir     %r" % to_dir)
 
1098
            mutter("  to_dir_id  {%s}" % to_dir_id)
 
1099
 
 
1100
            inv.rename(file_id, to_dir_id, to_tail)
 
1101
 
 
1102
            from_abs = self.abspath(from_rel)
 
1103
            to_abs = self.abspath(to_rel)
 
1104
            try:
 
1105
                rename(from_abs, to_abs)
 
1106
            except OSError, e:
 
1107
                raise BzrError("failed to rename %r to %r: %s"
 
1108
                        % (from_abs, to_abs, e[1]),
 
1109
                        ["rename rolled back"])
 
1110
 
 
1111
            self._write_inventory(inv)
 
1112
        finally:
 
1113
            self.unlock()
 
1114
 
 
1115
 
1016
1116
    def move(self, from_paths, to_name):
1017
1117
        """Rename files.
1018
1118
 
1028
1128
        entry that is moved.
1029
1129
        """
1030
1130
        result = []
1031
 
        ## TODO: Option to move IDs only
1032
 
        assert not isinstance(from_paths, basestring)
1033
 
        tree = self.working_tree()
1034
 
        inv = tree.inventory
1035
 
        to_abs = self.abspath(to_name)
1036
 
        if not isdir(to_abs):
1037
 
            raise BzrError("destination %r is not a directory" % to_abs)
1038
 
        if not tree.has_filename(to_name):
1039
 
            raise BzrError("destination %r not in working directory" % to_abs)
1040
 
        to_dir_id = inv.path2id(to_name)
1041
 
        if to_dir_id == None and to_name != '':
1042
 
            raise BzrError("destination %r is not a versioned directory" % to_name)
1043
 
        to_dir_ie = inv[to_dir_id]
1044
 
        if to_dir_ie.kind not in ('directory', 'root_directory'):
1045
 
            raise BzrError("destination %r is not a directory" % to_abs)
1046
 
 
1047
 
        to_idpath = inv.get_idpath(to_dir_id)
1048
 
 
1049
 
        for f in from_paths:
1050
 
            if not tree.has_filename(f):
1051
 
                raise BzrError("%r does not exist in working tree" % f)
1052
 
            f_id = inv.path2id(f)
1053
 
            if f_id == None:
1054
 
                raise BzrError("%r is not versioned" % f)
1055
 
            name_tail = splitpath(f)[-1]
1056
 
            dest_path = appendpath(to_name, name_tail)
1057
 
            if tree.has_filename(dest_path):
1058
 
                raise BzrError("destination %r already exists" % dest_path)
1059
 
            if f_id in to_idpath:
1060
 
                raise BzrError("can't move %r to a subdirectory of itself" % f)
1061
 
 
1062
 
        # OK, so there's a race here, it's possible that someone will
1063
 
        # create a file in this interval and then the rename might be
1064
 
        # left half-done.  But we should have caught most problems.
1065
 
 
1066
 
        for f in from_paths:
1067
 
            name_tail = splitpath(f)[-1]
1068
 
            dest_path = appendpath(to_name, name_tail)
1069
 
            result.append((f, dest_path))
1070
 
            inv.rename(inv.path2id(f), to_dir_id, name_tail)
1071
 
            try:
1072
 
                rename(self.abspath(f), self.abspath(dest_path))
1073
 
            except OSError, e:
1074
 
                raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
1075
 
                        ["rename rolled back"])
1076
 
 
1077
 
        self._write_inventory(inv)
 
1131
        self.lock_write()
 
1132
        try:
 
1133
            ## TODO: Option to move IDs only
 
1134
            assert not isinstance(from_paths, basestring)
 
1135
            tree = self.working_tree()
 
1136
            inv = tree.inventory
 
1137
            to_abs = self.abspath(to_name)
 
1138
            if not isdir(to_abs):
 
1139
                raise BzrError("destination %r is not a directory" % to_abs)
 
1140
            if not tree.has_filename(to_name):
 
1141
                raise BzrError("destination %r not in working directory" % to_abs)
 
1142
            to_dir_id = inv.path2id(to_name)
 
1143
            if to_dir_id == None and to_name != '':
 
1144
                raise BzrError("destination %r is not a versioned directory" % to_name)
 
1145
            to_dir_ie = inv[to_dir_id]
 
1146
            if to_dir_ie.kind not in ('directory', 'root_directory'):
 
1147
                raise BzrError("destination %r is not a directory" % to_abs)
 
1148
 
 
1149
            to_idpath = inv.get_idpath(to_dir_id)
 
1150
 
 
1151
            for f in from_paths:
 
1152
                if not tree.has_filename(f):
 
1153
                    raise BzrError("%r does not exist in working tree" % f)
 
1154
                f_id = inv.path2id(f)
 
1155
                if f_id == None:
 
1156
                    raise BzrError("%r is not versioned" % f)
 
1157
                name_tail = splitpath(f)[-1]
 
1158
                dest_path = appendpath(to_name, name_tail)
 
1159
                if tree.has_filename(dest_path):
 
1160
                    raise BzrError("destination %r already exists" % dest_path)
 
1161
                if f_id in to_idpath:
 
1162
                    raise BzrError("can't move %r to a subdirectory of itself" % f)
 
1163
 
 
1164
            # OK, so there's a race here, it's possible that someone will
 
1165
            # create a file in this interval and then the rename might be
 
1166
            # left half-done.  But we should have caught most problems.
 
1167
 
 
1168
            for f in from_paths:
 
1169
                name_tail = splitpath(f)[-1]
 
1170
                dest_path = appendpath(to_name, name_tail)
 
1171
                result.append((f, dest_path))
 
1172
                inv.rename(inv.path2id(f), to_dir_id, name_tail)
 
1173
                try:
 
1174
                    rename(self.abspath(f), self.abspath(dest_path))
 
1175
                except OSError, e:
 
1176
                    raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
 
1177
                            ["rename rolled back"])
 
1178
 
 
1179
            self._write_inventory(inv)
 
1180
        finally:
 
1181
            self.unlock()
 
1182
 
1078
1183
        return result
1079
1184
 
1080
1185
 
1085
1190
            If true (default) backups are made of files before
1086
1191
            they're renamed.
1087
1192
        """
 
1193
        from bzrlib.errors import NotVersionedError, BzrError
1088
1194
        from bzrlib.atomicfile import AtomicFile
1089
1195
        from bzrlib.osutils import backup_file
1090
1196
        
1097
1203
        for fn in filenames:
1098
1204
            file_id = inv.path2id(fn)
1099
1205
            if not file_id:
1100
 
                raise NotVersionedError(path=fn)
 
1206
                raise NotVersionedError("not a versioned file", fn)
1101
1207
            if not old_inv.has_id(file_id):
1102
1208
                raise BzrError("file not present in old tree", fn, file_id)
1103
1209
            nids.append((fn, file_id))
1149
1255
        if updated:
1150
1256
            self.set_pending_merges(p)
1151
1257
 
1152
 
    @needs_write_lock
1153
1258
    def set_pending_merges(self, rev_list):
1154
 
        self.put_controlfile('pending-merges', '\n'.join(rev_list))
 
1259
        self.lock_write()
 
1260
        try:
 
1261
            self.put_controlfile('pending-merges', '\n'.join(rev_list))
 
1262
        finally:
 
1263
            self.unlock()
 
1264
 
1155
1265
 
1156
1266
    def get_parent(self):
1157
1267
        """Return the parent location of the branch.
1170
1280
                    raise
1171
1281
        return None
1172
1282
 
1173
 
    @needs_write_lock
 
1283
 
1174
1284
    def set_parent(self, url):
1175
1285
        # TODO: Maybe delete old location files?
1176
1286
        from bzrlib.atomicfile import AtomicFile
1177
 
        f = AtomicFile(self.controlfilename('parent'))
 
1287
        self.lock_write()
1178
1288
        try:
1179
 
            f.write(url + '\n')
1180
 
            f.commit()
 
1289
            f = AtomicFile(self.controlfilename('parent'))
 
1290
            try:
 
1291
                f.write(url + '\n')
 
1292
                f.commit()
 
1293
            finally:
 
1294
                f.close()
1181
1295
        finally:
1182
 
            f.close()
 
1296
            self.unlock()
1183
1297
 
1184
1298
    def check_revno(self, revno):
1185
1299
        """\
1197
1311
        if revno < 1 or revno > self.revno():
1198
1312
            raise InvalidRevisionNumber(revno)
1199
1313
        
1200
 
    def sign_revision(self, revision_id, gpg_strategy):
1201
 
        plaintext = Testament.from_revision(self, revision_id).as_short_text()
1202
 
        self.store_revision_signature(gpg_strategy, plaintext, revision_id)
1203
 
 
1204
 
    @needs_write_lock
1205
 
    def store_revision_signature(self, gpg_strategy, plaintext, revision_id):
1206
 
        self.revision_store.add(StringIO(gpg_strategy.sign(plaintext)), 
1207
 
                                revision_id, "sig")
 
1314
        
 
1315
        
1208
1316
 
1209
1317
 
1210
1318
class ScratchBranch(_Branch):
1214
1322
    >>> isdir(b.base)
1215
1323
    True
1216
1324
    >>> bd = b.base
1217
 
    >>> b._transport.__del__()
 
1325
    >>> b.destroy()
1218
1326
    >>> isdir(bd)
1219
1327
    False
1220
1328
    """
1221
 
 
1222
 
    def __init__(self, files=[], dirs=[], transport=None):
 
1329
    def __init__(self, files=[], dirs=[], base=None):
1223
1330
        """Make a test branch.
1224
1331
 
1225
1332
        This creates a temporary directory and runs init-tree in it.
1226
1333
 
1227
1334
        If any files are listed, they are created in the working copy.
1228
1335
        """
1229
 
        if transport is None:
1230
 
            transport = bzrlib.transport.local.ScratchTransport()
1231
 
            super(ScratchBranch, self).__init__(transport, init=True)
1232
 
        else:
1233
 
            super(ScratchBranch, self).__init__(transport)
1234
 
 
 
1336
        from tempfile import mkdtemp
 
1337
        init = False
 
1338
        if base is None:
 
1339
            base = mkdtemp()
 
1340
            init = True
 
1341
        if isinstance(base, basestring):
 
1342
            base = get_transport(base)
 
1343
        _Branch.__init__(self, base, init=init)
1235
1344
        for d in dirs:
1236
1345
            self._transport.mkdir(d)
1237
1346
            
1257
1366
        base = mkdtemp()
1258
1367
        os.rmdir(base)
1259
1368
        copytree(self.base, base, symlinks=True)
1260
 
        return ScratchBranch(
1261
 
            transport=bzrlib.transport.local.ScratchTransport(base))
 
1369
        return ScratchBranch(base=base)
 
1370
 
 
1371
    def __del__(self):
 
1372
        self.destroy()
 
1373
 
 
1374
    def destroy(self):
 
1375
        """Destroy the test branch, removing the scratch directory."""
 
1376
        from shutil import rmtree
 
1377
        try:
 
1378
            if self.base:
 
1379
                mutter("delete ScratchBranch %s" % self.base)
 
1380
                rmtree(self.base)
 
1381
        except OSError, e:
 
1382
            # Work around for shutil.rmtree failing on Windows when
 
1383
            # readonly files are encountered
 
1384
            mutter("hit exception in destroying ScratchBranch: %s" % e)
 
1385
            for root, dirs, files in os.walk(self.base, topdown=False):
 
1386
                for name in files:
 
1387
                    os.chmod(os.path.join(root, name), 0700)
 
1388
            rmtree(self.base)
 
1389
        self._transport = None
 
1390
 
1262
1391
    
1263
1392
 
1264
1393
######################################################################