~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Robert Collins
  • Date: 2005-10-14 04:29:39 UTC
  • mto: This revision was merged to the branch mainline in revision 1456.
  • Revision ID: robertc@lifelesslap.robertcollins.net-20051014042939-d03fe7c79012279c
Create a default signature checking policy of CHECK_IF_POSSIBLE

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
        
91
93
 
92
94
######################################################################
93
95
# branch objects
125
127
 
126
128
        Basically we keep looking up until we find the control directory or
127
129
        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
130
        """
130
131
        t = get_transport(url)
131
132
        while True:
132
133
            try:
133
 
                return _Branch(t), t.relpath(url)
 
134
                return _Branch(t)
134
135
            except NotBranchError:
135
136
                pass
136
137
            new_t = t.clone('..')
137
138
            if new_t.base == t.base:
138
139
                # reached the root, whatever that may be
139
 
                raise NotBranchError(path=url)
 
140
                raise NotBranchError('%s is not in a branch' % url)
140
141
            t = new_t
141
142
 
142
143
    @staticmethod
259
260
            self.text_store = get_store('text-store')
260
261
            self.revision_store = get_store('revision-store')
261
262
        elif self._branch_format == 5:
262
 
            self.control_weaves = get_weave('')
 
263
            self.control_weaves = get_weave([])
263
264
            self.weave_store = get_weave('weaves')
264
265
            self.revision_store = get_store('revision-store', compressed=False)
265
266
        elif self._branch_format == 6:
266
 
            self.control_weaves = get_weave('')
 
267
            self.control_weaves = get_weave([])
267
268
            self.weave_store = get_weave('weaves', prefixed=True)
268
269
            self.revision_store = get_store('revision-store', compressed=False,
269
270
                                            prefixed=True)
270
 
        self.revision_store.register_suffix('sig')
271
271
        self._transaction = None
272
272
 
273
273
    def __str__(self):
377
377
            self._lock_mode = self._lock_count = None
378
378
 
379
379
    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
 
        """
 
380
        """Return absolute filename for something in the branch"""
385
381
        return self._transport.abspath(name)
386
382
 
 
383
    def relpath(self, path):
 
384
        """Return path relative to this branch of something inside it.
 
385
 
 
386
        Raises an error if path is not in this branch."""
 
387
        return self._transport.relpath(path)
 
388
 
 
389
 
387
390
    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)
 
391
        if isinstance(file_or_path, basestring):
 
392
            file_or_path = [file_or_path]
 
393
        return [bzrlib.BZRDIR] + file_or_path
393
394
 
394
395
    def controlfilename(self, file_or_path):
395
396
        """Return location relative to branch."""
498
499
        try:
499
500
            fmt = self.controlfile('branch-format', 'r').read()
500
501
        except NoSuchFile:
501
 
            raise NotBranchError(path=self.base)
 
502
            raise NotBranchError(self.base)
502
503
        mutter("got branch format %r", fmt)
503
504
        if fmt == BZR_BRANCH_FORMAT_6:
504
505
            self._branch_format = 6
532
533
                entry.parent_id = inv.root.file_id
533
534
        self._write_inventory(inv)
534
535
 
535
 
    @needs_read_lock
536
536
    def read_working_inventory(self):
537
537
        """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)
 
538
        self.lock_read()
 
539
        try:
 
540
            # ElementTree does its own conversion from UTF-8, so open in
 
541
            # binary.
 
542
            f = self.controlfile('inventory', 'rb')
 
543
            return bzrlib.xml5.serializer_v5.read_inventory(f)
 
544
        finally:
 
545
            self.unlock()
 
546
            
542
547
 
543
 
    @needs_write_lock
544
548
    def _write_inventory(self, inv):
545
549
        """Update the working inventory.
546
550
 
548
552
        will be committed to the next revision.
549
553
        """
550
554
        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)
 
555
        self.lock_write()
 
556
        try:
 
557
            sio = StringIO()
 
558
            bzrlib.xml5.serializer_v5.write_inventory(inv, sio)
 
559
            sio.seek(0)
 
560
            # Transport handles atomicity
 
561
            self.put_controlfile('inventory', sio)
 
562
        finally:
 
563
            self.unlock()
556
564
        
557
565
        mutter('wrote working inventory')
558
566
            
559
567
    inventory = property(read_working_inventory, _write_inventory, None,
560
568
                         """Inventory for the working copy.""")
561
569
 
562
 
    @needs_write_lock
563
570
    def add(self, files, ids=None):
564
571
        """Make files versioned.
565
572
 
595
602
        else:
596
603
            assert(len(ids) == len(files))
597
604
 
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
 
605
        self.lock_write()
 
606
        try:
 
607
            inv = self.read_working_inventory()
 
608
            for f,file_id in zip(files, ids):
 
609
                if is_control_file(f):
 
610
                    raise BzrError("cannot add control file %s" % quotefn(f))
 
611
 
 
612
                fp = splitpath(f)
 
613
 
 
614
                if len(fp) == 0:
 
615
                    raise BzrError("cannot add top-level %r" % f)
 
616
 
 
617
                fullpath = os.path.normpath(self.abspath(f))
 
618
 
 
619
                try:
 
620
                    kind = file_kind(fullpath)
 
621
                except OSError:
 
622
                    # maybe something better?
 
623
                    raise BzrError('cannot add: not a regular file, symlink or directory: %s' % quotefn(f))
 
624
 
 
625
                if not InventoryEntry.versionable_kind(kind):
 
626
                    raise BzrError('cannot add: not a versionable file ('
 
627
                                   'i.e. regular file, symlink or directory): %s' % quotefn(f))
 
628
 
 
629
                if file_id is None:
 
630
                    file_id = gen_file_id(f)
 
631
                inv.add_path(f, kind=kind, file_id=file_id)
 
632
 
 
633
                mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
 
634
 
 
635
            self._write_inventory(inv)
 
636
        finally:
 
637
            self.unlock()
 
638
            
 
639
 
629
640
    def print_file(self, file, revno):
630
641
        """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)
 
642
        self.lock_read()
 
643
        try:
 
644
            tree = self.revision_tree(self.get_rev_id(revno))
 
645
            # use inventory as it was in that revision
 
646
            file_id = tree.inventory.path2id(file)
 
647
            if not file_id:
 
648
                raise BzrError("%r is not present in revision %s" % (file, revno))
 
649
            tree.print_file(file_id)
 
650
        finally:
 
651
            self.unlock()
 
652
 
 
653
 
 
654
    def remove(self, files, verbose=False):
 
655
        """Mark nominated files for removal from the inventory.
 
656
 
 
657
        This does not remove their text.  This does not run on 
 
658
 
 
659
        TODO: Refuse to remove modified files unless --force is given?
 
660
 
 
661
        TODO: Do something useful with directories.
 
662
 
 
663
        TODO: Should this remove the text or not?  Tough call; not
 
664
        removing may be useful and the user can just use use rm, and
 
665
        is the opposite of add.  Removing it is consistent with most
 
666
        other tools.  Maybe an option.
 
667
        """
 
668
        ## TODO: Normalize names
 
669
        ## TODO: Remove nested loops; better scalability
 
670
        if isinstance(files, basestring):
 
671
            files = [files]
 
672
 
 
673
        self.lock_write()
 
674
 
 
675
        try:
 
676
            tree = self.working_tree()
 
677
            inv = tree.inventory
 
678
 
 
679
            # do this before any modifications
 
680
            for f in files:
 
681
                fid = inv.path2id(f)
 
682
                if not fid:
 
683
                    raise BzrError("cannot remove unversioned file %s" % quotefn(f))
 
684
                mutter("remove inventory entry %s {%s}" % (quotefn(f), fid))
 
685
                if verbose:
 
686
                    # having remove it, it must be either ignored or unknown
 
687
                    if tree.is_ignored(f):
 
688
                        new_status = 'I'
 
689
                    else:
 
690
                        new_status = '?'
 
691
                    show_status(new_status, inv[fid].kind, quotefn(f))
 
692
                del inv[fid]
 
693
 
 
694
            self._write_inventory(inv)
 
695
        finally:
 
696
            self.unlock()
637
697
 
638
698
    # FIXME: this doesn't need to be a branch method
639
699
    def set_inventory(self, new_inventory_list):
660
720
        These are files in the working directory that are not versioned or
661
721
        control files or ignored.
662
722
        
663
 
        >>> from bzrlib.workingtree import WorkingTree
664
723
        >>> b = ScratchBranch(files=['foo', 'foo~'])
665
 
        >>> map(str, b.unknowns())
 
724
        >>> list(b.unknowns())
666
725
        ['foo']
667
726
        >>> b.add('foo')
668
727
        >>> list(b.unknowns())
669
728
        []
670
 
        >>> WorkingTree(b.base, b).remove('foo')
 
729
        >>> b.remove('foo')
671
730
        >>> list(b.unknowns())
672
 
        [u'foo']
 
731
        ['foo']
673
732
        """
674
733
        return self.working_tree().unknowns()
675
734
 
676
 
    @needs_write_lock
 
735
 
677
736
    def append_revision(self, *revision_ids):
678
737
        for revision_id in revision_ids:
679
738
            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))
 
739
        self.lock_write()
 
740
        try:
 
741
            rev_history = self.revision_history()
 
742
            rev_history.extend(revision_ids)
 
743
            self.put_controlfile('revision-history', '\n'.join(rev_history))
 
744
        finally:
 
745
            self.unlock()
687
746
 
688
747
    def has_revision(self, revision_id):
689
748
        """True if this branch has a copy of the revision.
691
750
        This does not necessarily imply the revision is merge
692
751
        or on the mainline."""
693
752
        return (revision_id is None
694
 
                or self.revision_store.has_id(revision_id))
 
753
                or revision_id in self.revision_store)
695
754
 
696
 
    @needs_read_lock
697
755
    def get_revision_xml_file(self, revision_id):
698
756
        """Return XML file object for revision object."""
699
757
        if not revision_id or not isinstance(revision_id, basestring):
700
758
            raise InvalidRevisionId(revision_id)
 
759
 
 
760
        self.lock_read()
701
761
        try:
702
 
            return self.revision_store.get(revision_id)
703
 
        except (IndexError, KeyError):
704
 
            raise bzrlib.errors.NoSuchRevision(self, revision_id)
 
762
            try:
 
763
                return self.revision_store[revision_id]
 
764
            except (IndexError, KeyError):
 
765
                raise bzrlib.errors.NoSuchRevision(self, revision_id)
 
766
        finally:
 
767
            self.unlock()
705
768
 
706
769
    #deprecated
707
770
    get_revision_xml = get_revision_xml_file
800
863
        else:
801
864
            return self.get_inventory(revision_id)
802
865
 
803
 
    @needs_read_lock
804
866
    def revision_history(self):
805
867
        """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)
 
868
        self.lock_read()
 
869
        try:
 
870
            transaction = self.get_transaction()
 
871
            history = transaction.map.find_revision_history()
 
872
            if history is not None:
 
873
                mutter("cache hit for revision-history in %s", self)
 
874
                return list(history)
 
875
            history = [l.rstrip('\r\n') for l in
 
876
                    self.controlfile('revision-history', 'r').readlines()]
 
877
            transaction.map.add_revision_history(history)
 
878
            # this call is disabled because revision_history is 
 
879
            # not really an object yet, and the transaction is for objects.
 
880
            # transaction.register_clean(history, precious=True)
810
881
            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)
 
882
        finally:
 
883
            self.unlock()
 
884
 
 
885
    def common_ancestor(self, other, self_revno=None, other_revno=None):
 
886
        """
 
887
        >>> from bzrlib.commit import commit
 
888
        >>> sb = ScratchBranch(files=['foo', 'foo~'])
 
889
        >>> sb.common_ancestor(sb) == (None, None)
 
890
        True
 
891
        >>> commit(sb, "Committing first revision", verbose=False)
 
892
        >>> sb.common_ancestor(sb)[0]
 
893
        1
 
894
        >>> clone = sb.clone()
 
895
        >>> commit(sb, "Committing second revision", verbose=False)
 
896
        >>> sb.common_ancestor(sb)[0]
 
897
        2
 
898
        >>> sb.common_ancestor(clone)[0]
 
899
        1
 
900
        >>> commit(clone, "Committing divergent second revision", 
 
901
        ...               verbose=False)
 
902
        >>> sb.common_ancestor(clone)[0]
 
903
        1
 
904
        >>> sb.common_ancestor(clone) == clone.common_ancestor(sb)
 
905
        True
 
906
        >>> sb.common_ancestor(sb) != clone.common_ancestor(clone)
 
907
        True
 
908
        >>> clone2 = sb.clone()
 
909
        >>> sb.common_ancestor(clone2)[0]
 
910
        2
 
911
        >>> sb.common_ancestor(clone2, self_revno=1)[0]
 
912
        1
 
913
        >>> sb.common_ancestor(clone2, other_revno=1)[0]
 
914
        1
 
915
        """
 
916
        my_history = self.revision_history()
 
917
        other_history = other.revision_history()
 
918
        if self_revno is None:
 
919
            self_revno = len(my_history)
 
920
        if other_revno is None:
 
921
            other_revno = len(other_history)
 
922
        indices = range(min((self_revno, other_revno)))
 
923
        indices.reverse()
 
924
        for r in indices:
 
925
            if my_history[r] == other_history[r]:
 
926
                return r+1, my_history[r]
 
927
        return None, None
 
928
 
818
929
 
819
930
    def revno(self):
820
931
        """Return current revision number for this branch.
824
935
        """
825
936
        return len(self.revision_history())
826
937
 
 
938
 
827
939
    def last_revision(self):
828
940
        """Return last patch hash, or None if no history.
829
941
        """
833
945
        else:
834
946
            return None
835
947
 
 
948
 
836
949
    def missing_revisions(self, other, stop_revision=None, diverged_ok=False):
837
950
        """Return a list of new revisions that would perfectly fit.
838
951
        
861
974
        Traceback (most recent call last):
862
975
        DivergedBranches: These branches have diverged.
863
976
        """
 
977
        # FIXME: If the branches have diverged, but the latest
 
978
        # revision in this branch is completely merged into the other,
 
979
        # then we should still be able to pull.
864
980
        self_history = self.revision_history()
865
981
        self_len = len(self_history)
866
982
        other_history = other.revision_history()
880
996
 
881
997
    def update_revisions(self, other, stop_revision=None):
882
998
        """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
999
        from bzrlib.fetch import greedy_fetch
 
1000
        from bzrlib.revision import get_intervening_revisions
887
1001
        if stop_revision is None:
888
1002
            stop_revision = other.last_revision()
889
 
        ### Should this be checking is_ancestor instead of revision_history?
890
1003
        if (stop_revision is not None and 
891
1004
            stop_revision in self.revision_history()):
892
1005
            return
893
1006
        greedy_fetch(to_branch=self, from_branch=other,
894
1007
                     revision=stop_revision)
895
 
        pullable_revs = self.pullable_revisions(other, stop_revision)
896
 
        if len(pullable_revs) > 0:
 
1008
        pullable_revs = self.missing_revisions(
 
1009
            other, other.revision_id_to_revno(stop_revision))
 
1010
        if pullable_revs:
 
1011
            greedy_fetch(to_branch=self,
 
1012
                         from_branch=other,
 
1013
                         revision=pullable_revs[-1])
897
1014
            self.append_revision(*pullable_revs)
 
1015
    
898
1016
 
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
1017
    def commit(self, *args, **kw):
916
1018
        from bzrlib.commit import Commit
917
1019
        Commit().commit(self, *args, **kw)
949
1051
            inv = self.get_revision_inventory(revision_id)
950
1052
            return RevisionTree(self.weave_store, inv, revision_id)
951
1053
 
 
1054
 
952
1055
    def working_tree(self):
953
1056
        """Return a `Tree` for the working copy."""
954
1057
        from bzrlib.workingtree import WorkingTree
955
 
        # TODO: In the future, perhaps WorkingTree should utilize Transport
 
1058
        # TODO: In the future, WorkingTree should utilize Transport
956
1059
        # RobertCollins 20051003 - I don't think it should - working trees are
957
1060
        # much more complex to keep consistent than our careful .bzr subset.
958
1061
        # instead, we should say that working trees are local only, and optimise
959
1062
        # for that.
960
 
        return WorkingTree(self.base, branch=self)
 
1063
        return WorkingTree(self._transport.base, self.read_working_inventory())
961
1064
 
962
1065
 
963
1066
    def basis_tree(self):
967
1070
        """
968
1071
        return self.revision_tree(self.last_revision())
969
1072
 
970
 
    @needs_write_lock
 
1073
 
971
1074
    def rename_one(self, from_rel, to_rel):
972
1075
        """Rename one file.
973
1076
 
974
1077
        This can change the directory or the filename or both.
975
1078
        """
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)
 
1079
        self.lock_write()
1006
1080
        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
 
1081
            tree = self.working_tree()
 
1082
            inv = tree.inventory
 
1083
            if not tree.has_filename(from_rel):
 
1084
                raise BzrError("can't rename: old working file %r does not exist" % from_rel)
 
1085
            if tree.has_filename(to_rel):
 
1086
                raise BzrError("can't rename: new working file %r already exists" % to_rel)
 
1087
 
 
1088
            file_id = inv.path2id(from_rel)
 
1089
            if file_id == None:
 
1090
                raise BzrError("can't rename: old name %r is not versioned" % from_rel)
 
1091
 
 
1092
            if inv.path2id(to_rel):
 
1093
                raise BzrError("can't rename: new name %r is already versioned" % to_rel)
 
1094
 
 
1095
            to_dir, to_tail = os.path.split(to_rel)
 
1096
            to_dir_id = inv.path2id(to_dir)
 
1097
            if to_dir_id == None and to_dir != '':
 
1098
                raise BzrError("can't determine destination directory id for %r" % to_dir)
 
1099
 
 
1100
            mutter("rename_one:")
 
1101
            mutter("  file_id    {%s}" % file_id)
 
1102
            mutter("  from_rel   %r" % from_rel)
 
1103
            mutter("  to_rel     %r" % to_rel)
 
1104
            mutter("  to_dir     %r" % to_dir)
 
1105
            mutter("  to_dir_id  {%s}" % to_dir_id)
 
1106
 
 
1107
            inv.rename(file_id, to_dir_id, to_tail)
 
1108
 
 
1109
            from_abs = self.abspath(from_rel)
 
1110
            to_abs = self.abspath(to_rel)
 
1111
            try:
 
1112
                rename(from_abs, to_abs)
 
1113
            except OSError, e:
 
1114
                raise BzrError("failed to rename %r to %r: %s"
 
1115
                        % (from_abs, to_abs, e[1]),
 
1116
                        ["rename rolled back"])
 
1117
 
 
1118
            self._write_inventory(inv)
 
1119
        finally:
 
1120
            self.unlock()
 
1121
 
 
1122
 
1016
1123
    def move(self, from_paths, to_name):
1017
1124
        """Rename files.
1018
1125
 
1028
1135
        entry that is moved.
1029
1136
        """
1030
1137
        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)
 
1138
        self.lock_write()
 
1139
        try:
 
1140
            ## TODO: Option to move IDs only
 
1141
            assert not isinstance(from_paths, basestring)
 
1142
            tree = self.working_tree()
 
1143
            inv = tree.inventory
 
1144
            to_abs = self.abspath(to_name)
 
1145
            if not isdir(to_abs):
 
1146
                raise BzrError("destination %r is not a directory" % to_abs)
 
1147
            if not tree.has_filename(to_name):
 
1148
                raise BzrError("destination %r not in working directory" % to_abs)
 
1149
            to_dir_id = inv.path2id(to_name)
 
1150
            if to_dir_id == None and to_name != '':
 
1151
                raise BzrError("destination %r is not a versioned directory" % to_name)
 
1152
            to_dir_ie = inv[to_dir_id]
 
1153
            if to_dir_ie.kind not in ('directory', 'root_directory'):
 
1154
                raise BzrError("destination %r is not a directory" % to_abs)
 
1155
 
 
1156
            to_idpath = inv.get_idpath(to_dir_id)
 
1157
 
 
1158
            for f in from_paths:
 
1159
                if not tree.has_filename(f):
 
1160
                    raise BzrError("%r does not exist in working tree" % f)
 
1161
                f_id = inv.path2id(f)
 
1162
                if f_id == None:
 
1163
                    raise BzrError("%r is not versioned" % f)
 
1164
                name_tail = splitpath(f)[-1]
 
1165
                dest_path = appendpath(to_name, name_tail)
 
1166
                if tree.has_filename(dest_path):
 
1167
                    raise BzrError("destination %r already exists" % dest_path)
 
1168
                if f_id in to_idpath:
 
1169
                    raise BzrError("can't move %r to a subdirectory of itself" % f)
 
1170
 
 
1171
            # OK, so there's a race here, it's possible that someone will
 
1172
            # create a file in this interval and then the rename might be
 
1173
            # left half-done.  But we should have caught most problems.
 
1174
 
 
1175
            for f in from_paths:
 
1176
                name_tail = splitpath(f)[-1]
 
1177
                dest_path = appendpath(to_name, name_tail)
 
1178
                result.append((f, dest_path))
 
1179
                inv.rename(inv.path2id(f), to_dir_id, name_tail)
 
1180
                try:
 
1181
                    rename(self.abspath(f), self.abspath(dest_path))
 
1182
                except OSError, e:
 
1183
                    raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
 
1184
                            ["rename rolled back"])
 
1185
 
 
1186
            self._write_inventory(inv)
 
1187
        finally:
 
1188
            self.unlock()
 
1189
 
1078
1190
        return result
1079
1191
 
1080
1192
 
1085
1197
            If true (default) backups are made of files before
1086
1198
            they're renamed.
1087
1199
        """
 
1200
        from bzrlib.errors import NotVersionedError, BzrError
1088
1201
        from bzrlib.atomicfile import AtomicFile
1089
1202
        from bzrlib.osutils import backup_file
1090
1203
        
1097
1210
        for fn in filenames:
1098
1211
            file_id = inv.path2id(fn)
1099
1212
            if not file_id:
1100
 
                raise NotVersionedError(path=fn)
 
1213
                raise NotVersionedError("not a versioned file", fn)
1101
1214
            if not old_inv.has_id(file_id):
1102
1215
                raise BzrError("file not present in old tree", fn, file_id)
1103
1216
            nids.append((fn, file_id))
1149
1262
        if updated:
1150
1263
            self.set_pending_merges(p)
1151
1264
 
1152
 
    @needs_write_lock
1153
1265
    def set_pending_merges(self, rev_list):
1154
 
        self.put_controlfile('pending-merges', '\n'.join(rev_list))
 
1266
        self.lock_write()
 
1267
        try:
 
1268
            self.put_controlfile('pending-merges', '\n'.join(rev_list))
 
1269
        finally:
 
1270
            self.unlock()
 
1271
 
1155
1272
 
1156
1273
    def get_parent(self):
1157
1274
        """Return the parent location of the branch.
1170
1287
                    raise
1171
1288
        return None
1172
1289
 
1173
 
    @needs_write_lock
 
1290
 
1174
1291
    def set_parent(self, url):
1175
1292
        # TODO: Maybe delete old location files?
1176
1293
        from bzrlib.atomicfile import AtomicFile
1177
 
        f = AtomicFile(self.controlfilename('parent'))
 
1294
        self.lock_write()
1178
1295
        try:
1179
 
            f.write(url + '\n')
1180
 
            f.commit()
 
1296
            f = AtomicFile(self.controlfilename('parent'))
 
1297
            try:
 
1298
                f.write(url + '\n')
 
1299
                f.commit()
 
1300
            finally:
 
1301
                f.close()
1181
1302
        finally:
1182
 
            f.close()
 
1303
            self.unlock()
1183
1304
 
1184
1305
    def check_revno(self, revno):
1185
1306
        """\
1197
1318
        if revno < 1 or revno > self.revno():
1198
1319
            raise InvalidRevisionNumber(revno)
1199
1320
        
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")
 
1321
        
 
1322
        
1208
1323
 
1209
1324
 
1210
1325
class ScratchBranch(_Branch):
1214
1329
    >>> isdir(b.base)
1215
1330
    True
1216
1331
    >>> bd = b.base
1217
 
    >>> b._transport.__del__()
 
1332
    >>> b.destroy()
1218
1333
    >>> isdir(bd)
1219
1334
    False
1220
1335
    """
1221
 
 
1222
 
    def __init__(self, files=[], dirs=[], transport=None):
 
1336
    def __init__(self, files=[], dirs=[], base=None):
1223
1337
        """Make a test branch.
1224
1338
 
1225
1339
        This creates a temporary directory and runs init-tree in it.
1226
1340
 
1227
1341
        If any files are listed, they are created in the working copy.
1228
1342
        """
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
 
 
 
1343
        from tempfile import mkdtemp
 
1344
        init = False
 
1345
        if base is None:
 
1346
            base = mkdtemp()
 
1347
            init = True
 
1348
        if isinstance(base, basestring):
 
1349
            base = get_transport(base)
 
1350
        _Branch.__init__(self, base, init=init)
1235
1351
        for d in dirs:
1236
1352
            self._transport.mkdir(d)
1237
1353
            
1257
1373
        base = mkdtemp()
1258
1374
        os.rmdir(base)
1259
1375
        copytree(self.base, base, symlinks=True)
1260
 
        return ScratchBranch(
1261
 
            transport=bzrlib.transport.local.ScratchTransport(base))
 
1376
        return ScratchBranch(base=base)
 
1377
 
 
1378
    def __del__(self):
 
1379
        self.destroy()
 
1380
 
 
1381
    def destroy(self):
 
1382
        """Destroy the test branch, removing the scratch directory."""
 
1383
        from shutil import rmtree
 
1384
        try:
 
1385
            if self.base:
 
1386
                mutter("delete ScratchBranch %s" % self.base)
 
1387
                rmtree(self.base)
 
1388
        except OSError, e:
 
1389
            # Work around for shutil.rmtree failing on Windows when
 
1390
            # readonly files are encountered
 
1391
            mutter("hit exception in destroying ScratchBranch: %s" % e)
 
1392
            for root, dirs, files in os.walk(self.base, topdown=False):
 
1393
                for name in files:
 
1394
                    os.chmod(os.path.join(root, name), 0700)
 
1395
            rmtree(self.base)
 
1396
        self._transport = None
 
1397
 
1262
1398
    
1263
1399
 
1264
1400
######################################################################