~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Martin Pool
  • Date: 2005-04-15 01:31:21 UTC
  • Revision ID: mbp@sourcefrog.net-20050415013121-b18f1be12a735066
- Doc cleanups from Magnus Therning

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)
223
165
        if mode == 'rb' or mode == 'wb':
224
166
            return file(fn, mode)
225
167
        elif mode == 'r' or mode == 'w':
226
 
            # open in binary mode anyhow so there's no newline translation;
227
 
            # codecs uses line buffering by default; don't want that.
 
168
            # open in binary mode anyhow so there's no newline translation
228
169
            import codecs
229
 
            return codecs.open(fn, mode + 'b', 'utf-8',
230
 
                               buffering=60000)
 
170
            return codecs.open(fn, mode + 'b', 'utf-8')
231
171
        else:
232
172
            raise BzrError("invalid controlfile mode %r" % mode)
233
173
 
242
182
        for d in ('text-store', 'inventory-store', 'revision-store'):
243
183
            os.mkdir(self.controlfilename(d))
244
184
        for f in ('revision-history', 'merged-patches',
245
 
                  'pending-merged-patches', 'branch-name',
246
 
                  'branch-lock'):
 
185
                  'pending-merged-patches', 'branch-name'):
247
186
            self.controlfile(f, 'w').write('')
248
187
        mutter('created control directory in ' + self.base)
249
188
        Inventory().write_xml(self.controlfile('inventory','w'))
270
209
 
271
210
    def read_working_inventory(self):
272
211
        """Read the working inventory."""
273
 
        self._need_readlock()
274
212
        before = time.time()
275
213
        # ElementTree does its own conversion from UTF-8, so open in
276
214
        # binary.
286
224
        That is to say, the inventory describing changes underway, that
287
225
        will be committed to the next revision.
288
226
        """
289
 
        self._need_writelock()
290
227
        ## TODO: factor out to atomicfile?  is rename safe on windows?
291
228
        ## TODO: Maybe some kind of clean/dirty marker on inventory?
292
229
        tmpfname = self.controlfilename('inventory.tmp')
346
283
        Traceback (most recent call last):
347
284
        BzrError: ('cannot add: not a regular file or directory: nothere', [])
348
285
        """
349
 
        self._need_writelock()
350
286
 
351
287
        # TODO: Re-adding a file that is removed in the working copy
352
288
        # should probably put it back with the previous ID.
387
323
 
388
324
    def print_file(self, file, revno):
389
325
        """Print `file` to stdout."""
390
 
        self._need_readlock()
391
326
        tree = self.revision_tree(self.lookup_revision(revno))
392
327
        # use inventory as it was in that revision
393
328
        file_id = tree.inventory.path2id(file)
434
369
        """
435
370
        ## TODO: Normalize names
436
371
        ## TODO: Remove nested loops; better scalability
437
 
        self._need_writelock()
438
372
 
439
373
        if isinstance(files, types.StringTypes):
440
374
            files = [files]
501
435
        timestamp -- if not None, seconds-since-epoch for a
502
436
             postdated/predated commit.
503
437
        """
504
 
        self._need_writelock()
505
438
 
506
439
        ## TODO: Show branch names
507
440
 
671
604
 
672
605
    def get_revision(self, revision_id):
673
606
        """Return the Revision object for a named revision"""
674
 
        self._need_readlock()
675
607
        r = Revision.read_xml(self.revision_store[revision_id])
676
608
        assert r.revision_id == revision_id
677
609
        return r
683
615
        TODO: Perhaps for this and similar methods, take a revision
684
616
               parameter which can be either an integer revno or a
685
617
               string hash."""
686
 
        self._need_readlock()
687
618
        i = Inventory.read_xml(self.inventory_store[inventory_id])
688
619
        return i
689
620
 
690
621
 
691
622
    def get_revision_inventory(self, revision_id):
692
623
        """Return inventory of a past revision."""
693
 
        self._need_readlock()
694
624
        if revision_id == None:
695
625
            return Inventory()
696
626
        else:
703
633
        >>> ScratchBranch().revision_history()
704
634
        []
705
635
        """
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)
 
636
        return [chomp(l) for l in self.controlfile('revision-history', 'r').readlines()]
730
637
 
731
638
 
732
639
    def revno(self):
775
682
 
776
683
        `revision_id` may be None for the null revision, in which case
777
684
        an `EmptyTree` is returned."""
778
 
        self._need_readlock()
 
685
 
779
686
        if revision_id == None:
780
687
            return EmptyTree()
781
688
        else:
785
692
 
786
693
    def working_tree(self):
787
694
        """Return a `Tree` for the working copy."""
788
 
        from workingtree import WorkingTree
789
695
        return WorkingTree(self.base, self.read_working_inventory())
790
696
 
791
697
 
812
718
 
813
719
 
814
720
 
 
721
    def write_log(self, show_timezone='original', verbose=False):
 
722
        """Write out human-readable log of commits to this branch
 
723
 
 
724
        utc -- If true, show dates in universal time, not local time."""
 
725
        ## TODO: Option to choose either original, utc or local timezone
 
726
        revno = 1
 
727
        precursor = None
 
728
        for p in self.revision_history():
 
729
            print '-' * 40
 
730
            print 'revno:', revno
 
731
            ## TODO: Show hash if --id is given.
 
732
            ##print 'revision-hash:', p
 
733
            rev = self.get_revision(p)
 
734
            print 'committer:', rev.committer
 
735
            print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0,
 
736
                                                 show_timezone))
 
737
 
 
738
            ## opportunistic consistency check, same as check_patch_chaining
 
739
            if rev.precursor != precursor:
 
740
                bailout("mismatched precursor!")
 
741
 
 
742
            print 'message:'
 
743
            if not rev.message:
 
744
                print '  (no message)'
 
745
            else:
 
746
                for l in rev.message.split('\n'):
 
747
                    print '  ' + l
 
748
 
 
749
            if verbose == True and precursor != None:
 
750
                print 'changed files:'
 
751
                tree = self.revision_tree(p)
 
752
                prevtree = self.revision_tree(precursor)
 
753
                
 
754
                for file_state, fid, old_name, new_name, kind in \
 
755
                                        diff_trees(prevtree, tree, ):
 
756
                    if file_state == 'A' or file_state == 'M':
 
757
                        show_status(file_state, kind, new_name)
 
758
                    elif file_state == 'D':
 
759
                        show_status(file_state, kind, old_name)
 
760
                    elif file_state == 'R':
 
761
                        show_status(file_state, kind,
 
762
                            old_name + ' => ' + new_name)
 
763
                
 
764
            revno += 1
 
765
            precursor = p
 
766
 
 
767
 
815
768
    def rename_one(self, from_rel, to_rel):
816
 
        """Rename one file.
817
 
 
818
 
        This can change the directory or the filename or both.
819
 
        """
820
 
        self._need_writelock()
821
769
        tree = self.working_tree()
822
770
        inv = tree.inventory
823
771
        if not tree.has_filename(from_rel):
872
820
        Note that to_name is only the last component of the new name;
873
821
        this doesn't change the directory.
874
822
        """
875
 
        self._need_writelock()
876
823
        ## TODO: Option to move IDs only
877
824
        assert not isinstance(from_paths, basestring)
878
825
        tree = self.working_tree()
923
870
 
924
871
 
925
872
 
 
873
    def show_status(self, show_all=False):
 
874
        """Display single-line status for non-ignored working files.
 
875
 
 
876
        The list is show sorted in order by file name.
 
877
 
 
878
        >>> b = ScratchBranch(files=['foo', 'foo~'])
 
879
        >>> b.show_status()
 
880
        ?       foo
 
881
        >>> b.add('foo')
 
882
        >>> b.show_status()
 
883
        A       foo
 
884
        >>> b.commit("add foo")
 
885
        >>> b.show_status()
 
886
        >>> os.unlink(b.abspath('foo'))
 
887
        >>> b.show_status()
 
888
        D       foo
 
889
        
 
890
 
 
891
        TODO: Get state for single files.
 
892
 
 
893
        TODO: Perhaps show a slash at the end of directory names.        
 
894
 
 
895
        """
 
896
 
 
897
        # We have to build everything into a list first so that it can
 
898
        # sorted by name, incorporating all the different sources.
 
899
 
 
900
        # FIXME: Rather than getting things in random order and then sorting,
 
901
        # just step through in order.
 
902
 
 
903
        # Interesting case: the old ID for a file has been removed,
 
904
        # but a new file has been created under that name.
 
905
 
 
906
        old = self.basis_tree()
 
907
        new = self.working_tree()
 
908
 
 
909
        for fs, fid, oldname, newname, kind in diff_trees(old, new):
 
910
            if fs == 'R':
 
911
                show_status(fs, kind,
 
912
                            oldname + ' => ' + newname)
 
913
            elif fs == 'A' or fs == 'M':
 
914
                show_status(fs, kind, newname)
 
915
            elif fs == 'D':
 
916
                show_status(fs, kind, oldname)
 
917
            elif fs == '.':
 
918
                if show_all:
 
919
                    show_status(fs, kind, newname)
 
920
            elif fs == 'I':
 
921
                if show_all:
 
922
                    show_status(fs, kind, newname)
 
923
            elif fs == '?':
 
924
                show_status(fs, kind, newname)
 
925
            else:
 
926
                bailout("weird file state %r" % ((fs, fid),))
 
927
                
 
928
 
926
929
 
927
930
class ScratchBranch(Branch):
928
931
    """Special test class: a branch that cleans up after itself.
931
934
    >>> isdir(b.base)
932
935
    True
933
936
    >>> bd = b.base
934
 
    >>> b.destroy()
 
937
    >>> del b
935
938
    >>> isdir(bd)
936
939
    False
937
940
    """
951
954
 
952
955
 
953
956
    def __del__(self):
954
 
        self.destroy()
955
 
 
956
 
    def destroy(self):
957
957
        """Destroy the test branch, removing the scratch directory."""
958
958
        try:
959
 
            mutter("delete ScratchBranch %s" % self.base)
960
959
            shutil.rmtree(self.base)
961
 
        except OSError, e:
 
960
        except OSError:
962
961
            # Work around for shutil.rmtree failing on Windows when
963
962
            # readonly files are encountered
964
 
            mutter("hit exception in destroying ScratchBranch: %s" % e)
965
963
            for root, dirs, files in os.walk(self.base, topdown=False):
966
964
                for name in files:
967
965
                    os.chmod(os.path.join(root, name), 0700)
968
966
            shutil.rmtree(self.base)
969
 
        self.base = None
970
967
 
971
968
    
972
969
 
1005
1002
    idx = name.rfind('/')
1006
1003
    if idx != -1:
1007
1004
        name = name[idx+1 : ]
1008
 
    idx = name.rfind('\\')
1009
 
    if idx != -1:
1010
 
        name = name[idx+1 : ]
1011
1005
 
1012
1006
    name = name.lstrip('.')
1013
1007