~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Martin Pool
  • Date: 2005-05-11 06:14:54 UTC
  • Revision ID: mbp@sourcefrog.net-20050511061454-aa52aad45335cb11
- actually avoid reporting unchanged files if not required

Show diffs side-by-side

added added

removed removed

Lines of Context:
24
24
import bzrlib
25
25
from inventory import Inventory
26
26
from trace import mutter, note
27
 
from tree import Tree, EmptyTree, RevisionTree, WorkingTree
 
27
from tree import Tree, EmptyTree, RevisionTree
28
28
from inventory import InventoryEntry, Inventory
29
29
from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \
30
30
     format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \
40
40
 
41
41
 
42
42
 
 
43
def find_branch(f, **args):
 
44
    if f and (f.startswith('http://') or f.startswith('https://')):
 
45
        import remotebranch 
 
46
        return remotebranch.RemoteBranch(f, **args)
 
47
    else:
 
48
        return Branch(f, **args)
 
49
        
 
50
 
43
51
def find_branch_root(f=None):
44
52
    """Find the branch root enclosing f, or pwd.
45
53
 
 
54
    f may be a filename or a URL.
 
55
 
46
56
    It is not necessary that f exists.
47
57
 
48
58
    Basically we keep looking up until we find the control directory or
53
63
        f = os.path.realpath(f)
54
64
    else:
55
65
        f = os.path.abspath(f)
 
66
    if not os.path.exists(f):
 
67
        raise BzrError('%r does not exist' % f)
 
68
        
56
69
 
57
70
    orig_f = f
58
71
 
73
86
class Branch:
74
87
    """Branch holding a history of revisions.
75
88
 
76
 
    TODO: Perhaps use different stores for different classes of object,
77
 
           so that we can keep track of how much space each one uses,
78
 
           or garbage-collect them.
79
 
 
80
 
    TODO: Add a RemoteBranch subclass.  For the basic case of read-only
81
 
           HTTP access this should be very easy by, 
82
 
           just redirecting controlfile access into HTTP requests.
83
 
           We would need a RemoteStore working similarly.
84
 
 
85
 
    TODO: Keep the on-disk branch locked while the object exists.
86
 
 
87
 
    TODO: mkdir() method.
 
89
    base
 
90
        Base directory of the branch.
88
91
    """
89
 
    def __init__(self, base, init=False, find_root=True):
 
92
    _lockmode = None
 
93
    
 
94
    def __init__(self, base, init=False, find_root=True, lock_mode='w'):
90
95
        """Create new branch object at a particular location.
91
96
 
92
97
        base -- Base directory for the branch.
113
118
                        ['use "bzr init" to initialize a new working tree',
114
119
                         'current bzr can only operate from top-of-tree'])
115
120
        self._check_format()
 
121
        self.lock(lock_mode)
116
122
 
117
123
        self.text_store = ImmutableStore(self.controlfilename('text-store'))
118
124
        self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
126
132
    __repr__ = __str__
127
133
 
128
134
 
 
135
 
 
136
    def lock(self, mode='w'):
 
137
        """Lock the on-disk branch, excluding other processes."""
 
138
        try:
 
139
            import fcntl, errno
 
140
 
 
141
            if mode == 'w':
 
142
                lm = fcntl.LOCK_EX
 
143
                om = os.O_WRONLY | os.O_CREAT
 
144
            elif mode == 'r':
 
145
                lm = fcntl.LOCK_SH
 
146
                om = os.O_RDONLY
 
147
            else:
 
148
                raise BzrError("invalid locking mode %r" % mode)
 
149
 
 
150
            try:
 
151
                lockfile = os.open(self.controlfilename('branch-lock'), om)
 
152
            except OSError, e:
 
153
                if e.errno == errno.ENOENT:
 
154
                    # might not exist on branches from <0.0.4
 
155
                    self.controlfile('branch-lock', 'w').close()
 
156
                    lockfile = os.open(self.controlfilename('branch-lock'), om)
 
157
                else:
 
158
                    raise e
 
159
            
 
160
            fcntl.lockf(lockfile, lm)
 
161
            def unlock():
 
162
                fcntl.lockf(lockfile, fcntl.LOCK_UN)
 
163
                os.close(lockfile)
 
164
                self._lockmode = None
 
165
            self.unlock = unlock
 
166
            self._lockmode = mode
 
167
        except ImportError:
 
168
            warning("please write a locking method for platform %r" % sys.platform)
 
169
            def unlock():
 
170
                self._lockmode = None
 
171
            self.unlock = unlock
 
172
            self._lockmode = mode
 
173
 
 
174
 
 
175
    def _need_readlock(self):
 
176
        if self._lockmode not in ['r', 'w']:
 
177
            raise BzrError('need read lock on branch, only have %r' % self._lockmode)
 
178
 
 
179
    def _need_writelock(self):
 
180
        if self._lockmode not in ['w']:
 
181
            raise BzrError('need write lock on branch, only have %r' % self._lockmode)
 
182
 
 
183
 
129
184
    def abspath(self, name):
130
185
        """Return absolute filename for something in the branch"""
131
186
        return os.path.join(self.base, name)
158
213
        and binary.  binary files are untranslated byte streams.  Text
159
214
        control files are stored with Unix newlines and in UTF-8, even
160
215
        if the platform or locale defaults are different.
 
216
 
 
217
        Controlfiles should almost never be opened in write mode but
 
218
        rather should be atomically copied and replaced using atomicfile.
161
219
        """
162
220
 
163
221
        fn = self.controlfilename(file_or_path)
184
242
        for d in ('text-store', 'inventory-store', 'revision-store'):
185
243
            os.mkdir(self.controlfilename(d))
186
244
        for f in ('revision-history', 'merged-patches',
187
 
                  'pending-merged-patches', 'branch-name'):
 
245
                  'pending-merged-patches', 'branch-name',
 
246
                  'branch-lock'):
188
247
            self.controlfile(f, 'w').write('')
189
248
        mutter('created control directory in ' + self.base)
190
249
        Inventory().write_xml(self.controlfile('inventory','w'))
211
270
 
212
271
    def read_working_inventory(self):
213
272
        """Read the working inventory."""
 
273
        self._need_readlock()
214
274
        before = time.time()
215
275
        # ElementTree does its own conversion from UTF-8, so open in
216
276
        # binary.
226
286
        That is to say, the inventory describing changes underway, that
227
287
        will be committed to the next revision.
228
288
        """
 
289
        self._need_writelock()
229
290
        ## TODO: factor out to atomicfile?  is rename safe on windows?
230
291
        ## TODO: Maybe some kind of clean/dirty marker on inventory?
231
292
        tmpfname = self.controlfilename('inventory.tmp')
285
346
        Traceback (most recent call last):
286
347
        BzrError: ('cannot add: not a regular file or directory: nothere', [])
287
348
        """
 
349
        self._need_writelock()
288
350
 
289
351
        # TODO: Re-adding a file that is removed in the working copy
290
352
        # should probably put it back with the previous ID.
325
387
 
326
388
    def print_file(self, file, revno):
327
389
        """Print `file` to stdout."""
 
390
        self._need_readlock()
328
391
        tree = self.revision_tree(self.lookup_revision(revno))
329
392
        # use inventory as it was in that revision
330
393
        file_id = tree.inventory.path2id(file)
371
434
        """
372
435
        ## TODO: Normalize names
373
436
        ## TODO: Remove nested loops; better scalability
 
437
        self._need_writelock()
374
438
 
375
439
        if isinstance(files, types.StringTypes):
376
440
            files = [files]
437
501
        timestamp -- if not None, seconds-since-epoch for a
438
502
             postdated/predated commit.
439
503
        """
 
504
        self._need_writelock()
440
505
 
441
506
        ## TODO: Show branch names
442
507
 
606
671
 
607
672
    def get_revision(self, revision_id):
608
673
        """Return the Revision object for a named revision"""
 
674
        self._need_readlock()
609
675
        r = Revision.read_xml(self.revision_store[revision_id])
610
676
        assert r.revision_id == revision_id
611
677
        return r
617
683
        TODO: Perhaps for this and similar methods, take a revision
618
684
               parameter which can be either an integer revno or a
619
685
               string hash."""
 
686
        self._need_readlock()
620
687
        i = Inventory.read_xml(self.inventory_store[inventory_id])
621
688
        return i
622
689
 
623
690
 
624
691
    def get_revision_inventory(self, revision_id):
625
692
        """Return inventory of a past revision."""
 
693
        self._need_readlock()
626
694
        if revision_id == None:
627
695
            return Inventory()
628
696
        else:
635
703
        >>> ScratchBranch().revision_history()
636
704
        []
637
705
        """
 
706
        self._need_readlock()
638
707
        return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()]
639
708
 
640
709
 
 
710
    def enum_history(self, direction):
 
711
        """Return (revno, revision_id) for history of branch.
 
712
 
 
713
        direction
 
714
            'forward' is from earliest to latest
 
715
            'reverse' is from latest to earliest
 
716
        """
 
717
        rh = self.revision_history()
 
718
        if direction == 'forward':
 
719
            i = 1
 
720
            for rid in rh:
 
721
                yield i, rid
 
722
                i += 1
 
723
        elif direction == 'reverse':
 
724
            i = len(rh)
 
725
            while i > 0:
 
726
                yield i, rh[i-1]
 
727
                i -= 1
 
728
        else:
 
729
            raise BzrError('invalid history direction %r' % direction)
 
730
 
 
731
 
641
732
    def revno(self):
642
733
        """Return current revision number for this branch.
643
734
 
684
775
 
685
776
        `revision_id` may be None for the null revision, in which case
686
777
        an `EmptyTree` is returned."""
687
 
 
 
778
        self._need_readlock()
688
779
        if revision_id == None:
689
780
            return EmptyTree()
690
781
        else:
694
785
 
695
786
    def working_tree(self):
696
787
        """Return a `Tree` for the working copy."""
 
788
        from workingtree import WorkingTree
697
789
        return WorkingTree(self.base, self.read_working_inventory())
698
790
 
699
791
 
720
812
 
721
813
 
722
814
 
723
 
    def write_log(self, show_timezone='original', verbose=False):
724
 
        """Write out human-readable log of commits to this branch
725
 
 
726
 
        utc -- If true, show dates in universal time, not local time."""
727
 
        ## TODO: Option to choose either original, utc or local timezone
728
 
        revno = 1
729
 
        precursor = None
730
 
        for p in self.revision_history():
731
 
            print '-' * 40
732
 
            print 'revno:', revno
733
 
            ## TODO: Show hash if --id is given.
734
 
            ##print 'revision-hash:', p
735
 
            rev = self.get_revision(p)
736
 
            print 'committer:', rev.committer
737
 
            print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0,
738
 
                                                 show_timezone))
739
 
 
740
 
            ## opportunistic consistency check, same as check_patch_chaining
741
 
            if rev.precursor != precursor:
742
 
                bailout("mismatched precursor!")
743
 
 
744
 
            print 'message:'
745
 
            if not rev.message:
746
 
                print '  (no message)'
747
 
            else:
748
 
                for l in rev.message.split('\n'):
749
 
                    print '  ' + l
750
 
 
751
 
            if verbose == True and precursor != None:
752
 
                print 'changed files:'
753
 
                tree = self.revision_tree(p)
754
 
                prevtree = self.revision_tree(precursor)
755
 
                
756
 
                for file_state, fid, old_name, new_name, kind in \
757
 
                                        diff_trees(prevtree, tree, ):
758
 
                    if file_state == 'A' or file_state == 'M':
759
 
                        show_status(file_state, kind, new_name)
760
 
                    elif file_state == 'D':
761
 
                        show_status(file_state, kind, old_name)
762
 
                    elif file_state == 'R':
763
 
                        show_status(file_state, kind,
764
 
                            old_name + ' => ' + new_name)
765
 
                
766
 
            revno += 1
767
 
            precursor = p
768
 
 
769
 
 
770
815
    def rename_one(self, from_rel, to_rel):
771
816
        """Rename one file.
772
817
 
773
818
        This can change the directory or the filename or both.
774
 
         """
 
819
        """
 
820
        self._need_writelock()
775
821
        tree = self.working_tree()
776
822
        inv = tree.inventory
777
823
        if not tree.has_filename(from_rel):
826
872
        Note that to_name is only the last component of the new name;
827
873
        this doesn't change the directory.
828
874
        """
 
875
        self._need_writelock()
829
876
        ## TODO: Option to move IDs only
830
877
        assert not isinstance(from_paths, basestring)
831
878
        tree = self.working_tree()
876
923
 
877
924
 
878
925
 
879
 
    def show_status(self, show_all=False):
880
 
        """Display single-line status for non-ignored working files.
881
 
 
882
 
        The list is show sorted in order by file name.
883
 
 
884
 
        >>> b = ScratchBranch(files=['foo', 'foo~'])
885
 
        >>> b.show_status()
886
 
        ?       foo
887
 
        >>> b.add('foo')
888
 
        >>> b.show_status()
889
 
        A       foo
890
 
        >>> b.commit("add foo")
891
 
        >>> b.show_status()
892
 
        >>> os.unlink(b.abspath('foo'))
893
 
        >>> b.show_status()
894
 
        D       foo
895
 
        
896
 
        TODO: Get state for single files.
897
 
        """
898
 
 
899
 
        # We have to build everything into a list first so that it can
900
 
        # sorted by name, incorporating all the different sources.
901
 
 
902
 
        # FIXME: Rather than getting things in random order and then sorting,
903
 
        # just step through in order.
904
 
 
905
 
        # Interesting case: the old ID for a file has been removed,
906
 
        # but a new file has been created under that name.
907
 
 
908
 
        old = self.basis_tree()
909
 
        new = self.working_tree()
910
 
 
911
 
        for fs, fid, oldname, newname, kind in diff_trees(old, new):
912
 
            if fs == 'R':
913
 
                show_status(fs, kind,
914
 
                            oldname + ' => ' + newname)
915
 
            elif fs == 'A' or fs == 'M':
916
 
                show_status(fs, kind, newname)
917
 
            elif fs == 'D':
918
 
                show_status(fs, kind, oldname)
919
 
            elif fs == '.':
920
 
                if show_all:
921
 
                    show_status(fs, kind, newname)
922
 
            elif fs == 'I':
923
 
                if show_all:
924
 
                    show_status(fs, kind, newname)
925
 
            elif fs == '?':
926
 
                show_status(fs, kind, newname)
927
 
            else:
928
 
                bailout("weird file state %r" % ((fs, fid),))
929
 
                
930
 
 
931
926
 
932
927
class ScratchBranch(Branch):
933
928
    """Special test class: a branch that cleans up after itself.
936
931
    >>> isdir(b.base)
937
932
    True
938
933
    >>> bd = b.base
939
 
    >>> del b
 
934
    >>> b.destroy()
940
935
    >>> isdir(bd)
941
936
    False
942
937
    """
956
951
 
957
952
 
958
953
    def __del__(self):
 
954
        self.destroy()
 
955
 
 
956
    def destroy(self):
959
957
        """Destroy the test branch, removing the scratch directory."""
960
958
        try:
 
959
            mutter("delete ScratchBranch %s" % self.base)
961
960
            shutil.rmtree(self.base)
962
 
        except OSError:
 
961
        except OSError, e:
963
962
            # Work around for shutil.rmtree failing on Windows when
964
963
            # readonly files are encountered
 
964
            mutter("hit exception in destroying ScratchBranch: %s" % e)
965
965
            for root, dirs, files in os.walk(self.base, topdown=False):
966
966
                for name in files:
967
967
                    os.chmod(os.path.join(root, name), 0700)
968
968
            shutil.rmtree(self.base)
 
969
        self.base = None
969
970
 
970
971
    
971
972