~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Martin Pool
  • Date: 2005-05-02 04:24:33 UTC
  • Revision ID: mbp@sourcefrog.net-20050502042433-c825a7f7235f6b15
doc: notes on merge

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
 
27
from tree import Tree, EmptyTree, RevisionTree, WorkingTree
28
28
from inventory import InventoryEntry, Inventory
29
 
from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \
 
29
from osutils import isdir, quotefn, isfile, uuid, sha_file, username, chomp, \
30
30
     format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \
31
31
     joinpath, sha_string, file_kind, local_time_offset, appendpath
32
32
from store import ImmutableStore
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
 
 
51
43
def find_branch_root(f=None):
52
44
    """Find the branch root enclosing f, or pwd.
53
45
 
54
 
    f may be a filename or a URL.
55
 
 
56
46
    It is not necessary that f exists.
57
47
 
58
48
    Basically we keep looking up until we find the control directory or
63
53
        f = os.path.realpath(f)
64
54
    else:
65
55
        f = os.path.abspath(f)
66
 
    if not os.path.exists(f):
67
 
        raise BzrError('%r does not exist' % f)
68
 
        
69
56
 
70
57
    orig_f = f
71
58
 
86
73
class Branch:
87
74
    """Branch holding a history of revisions.
88
75
 
89
 
    base
90
 
        Base directory of the branch.
 
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.
91
88
    """
92
 
    _lockmode = None
93
 
    
94
 
    def __init__(self, base, init=False, find_root=True, lock_mode='w'):
 
89
    def __init__(self, base, init=False, find_root=True):
95
90
        """Create new branch object at a particular location.
96
91
 
97
92
        base -- Base directory for the branch.
118
113
                        ['use "bzr init" to initialize a new working tree',
119
114
                         'current bzr can only operate from top-of-tree'])
120
115
        self._check_format()
121
 
        self.lock(lock_mode)
122
116
 
123
117
        self.text_store = ImmutableStore(self.controlfilename('text-store'))
124
118
        self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
132
126
    __repr__ = __str__
133
127
 
134
128
 
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
 
 
184
129
    def abspath(self, name):
185
130
        """Return absolute filename for something in the branch"""
186
131
        return os.path.join(self.base, name)
213
158
        and binary.  binary files are untranslated byte streams.  Text
214
159
        control files are stored with Unix newlines and in UTF-8, even
215
160
        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.
219
161
        """
220
162
 
221
163
        fn = self.controlfilename(file_or_path)
242
184
        for d in ('text-store', 'inventory-store', 'revision-store'):
243
185
            os.mkdir(self.controlfilename(d))
244
186
        for f in ('revision-history', 'merged-patches',
245
 
                  'pending-merged-patches', 'branch-name',
246
 
                  'branch-lock'):
 
187
                  'pending-merged-patches', 'branch-name'):
247
188
            self.controlfile(f, 'w').write('')
248
189
        mutter('created control directory in ' + self.base)
249
190
        Inventory().write_xml(self.controlfile('inventory','w'))
270
211
 
271
212
    def read_working_inventory(self):
272
213
        """Read the working inventory."""
273
 
        self._need_readlock()
274
214
        before = time.time()
275
215
        # ElementTree does its own conversion from UTF-8, so open in
276
216
        # binary.
286
226
        That is to say, the inventory describing changes underway, that
287
227
        will be committed to the next revision.
288
228
        """
289
 
        self._need_writelock()
290
229
        ## TODO: factor out to atomicfile?  is rename safe on windows?
291
230
        ## TODO: Maybe some kind of clean/dirty marker on inventory?
292
231
        tmpfname = self.controlfilename('inventory.tmp')
346
285
        Traceback (most recent call last):
347
286
        BzrError: ('cannot add: not a regular file or directory: nothere', [])
348
287
        """
349
 
        self._need_writelock()
350
288
 
351
289
        # TODO: Re-adding a file that is removed in the working copy
352
290
        # should probably put it back with the previous ID.
387
325
 
388
326
    def print_file(self, file, revno):
389
327
        """Print `file` to stdout."""
390
 
        self._need_readlock()
391
328
        tree = self.revision_tree(self.lookup_revision(revno))
392
329
        # use inventory as it was in that revision
393
330
        file_id = tree.inventory.path2id(file)
434
371
        """
435
372
        ## TODO: Normalize names
436
373
        ## TODO: Remove nested loops; better scalability
437
 
        self._need_writelock()
438
374
 
439
375
        if isinstance(files, types.StringTypes):
440
376
            files = [files]
501
437
        timestamp -- if not None, seconds-since-epoch for a
502
438
             postdated/predated commit.
503
439
        """
504
 
        self._need_writelock()
505
440
 
506
441
        ## TODO: Show branch names
507
442
 
671
606
 
672
607
    def get_revision(self, revision_id):
673
608
        """Return the Revision object for a named revision"""
674
 
        self._need_readlock()
675
609
        r = Revision.read_xml(self.revision_store[revision_id])
676
610
        assert r.revision_id == revision_id
677
611
        return r
683
617
        TODO: Perhaps for this and similar methods, take a revision
684
618
               parameter which can be either an integer revno or a
685
619
               string hash."""
686
 
        self._need_readlock()
687
620
        i = Inventory.read_xml(self.inventory_store[inventory_id])
688
621
        return i
689
622
 
690
623
 
691
624
    def get_revision_inventory(self, revision_id):
692
625
        """Return inventory of a past revision."""
693
 
        self._need_readlock()
694
626
        if revision_id == None:
695
627
            return Inventory()
696
628
        else:
703
635
        >>> ScratchBranch().revision_history()
704
636
        []
705
637
        """
706
 
        self._need_readlock()
707
 
        return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()]
708
 
 
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)
 
638
        return [chomp(l) for l in self.controlfile('revision-history', 'r').readlines()]
730
639
 
731
640
 
732
641
    def revno(self):
775
684
 
776
685
        `revision_id` may be None for the null revision, in which case
777
686
        an `EmptyTree` is returned."""
778
 
        self._need_readlock()
 
687
 
779
688
        if revision_id == None:
780
689
            return EmptyTree()
781
690
        else:
785
694
 
786
695
    def working_tree(self):
787
696
        """Return a `Tree` for the working copy."""
788
 
        from workingtree import WorkingTree
789
697
        return WorkingTree(self.base, self.read_working_inventory())
790
698
 
791
699
 
812
720
 
813
721
 
814
722
 
 
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
 
815
770
    def rename_one(self, from_rel, to_rel):
816
771
        """Rename one file.
817
772
 
818
773
        This can change the directory or the filename or both.
819
 
        """
820
 
        self._need_writelock()
 
774
         """
821
775
        tree = self.working_tree()
822
776
        inv = tree.inventory
823
777
        if not tree.has_filename(from_rel):
872
826
        Note that to_name is only the last component of the new name;
873
827
        this doesn't change the directory.
874
828
        """
875
 
        self._need_writelock()
876
829
        ## TODO: Option to move IDs only
877
830
        assert not isinstance(from_paths, basestring)
878
831
        tree = self.working_tree()
923
876
 
924
877
 
925
878
 
 
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
 
926
931
 
927
932
class ScratchBranch(Branch):
928
933
    """Special test class: a branch that cleans up after itself.
931
936
    >>> isdir(b.base)
932
937
    True
933
938
    >>> bd = b.base
934
 
    >>> b.destroy()
 
939
    >>> del b
935
940
    >>> isdir(bd)
936
941
    False
937
942
    """
951
956
 
952
957
 
953
958
    def __del__(self):
954
 
        self.destroy()
955
 
 
956
 
    def destroy(self):
957
959
        """Destroy the test branch, removing the scratch directory."""
958
960
        try:
959
 
            mutter("delete ScratchBranch %s" % self.base)
960
961
            shutil.rmtree(self.base)
961
 
        except OSError, e:
 
962
        except OSError:
962
963
            # Work around for shutil.rmtree failing on Windows when
963
964
            # 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
970
969
 
971
970
    
972
971