~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: mbp at sourcefrog
  • Date: 2005-04-08 05:39:46 UTC
  • Revision ID: mbp@sourcefrog.net-20050408053946-1cb3415e1f8f58493034a5cf
- import lovely urlgrabber library

Show diffs side-by-side

added added

removed removed

Lines of Context:
26
26
from trace import mutter, note
27
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
73
73
class Branch:
74
74
    """Branch holding a history of revisions.
75
75
 
76
 
    base
77
 
        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.
78
88
    """
79
 
    _lockmode = None
80
 
    
81
 
    def __init__(self, base, init=False, find_root=True, lock_mode='w'):
 
89
    def __init__(self, base, init=False, find_root=True):
82
90
        """Create new branch object at a particular location.
83
91
 
84
 
        base -- Base directory for the branch.
 
92
        :param base: Base directory for the branch.
85
93
        
86
 
        init -- If True, create new control files in a previously
 
94
        :param init: If True, create new control files in a previously
87
95
             unversioned directory.  If False, the branch must already
88
96
             be versioned.
89
97
 
90
 
        find_root -- If true and init is false, find the root of the
 
98
        :param find_root: If true and init is false, find the root of the
91
99
             existing branch containing base.
92
100
 
93
101
        In the test suite, creation of new trees is tested using the
105
113
                        ['use "bzr init" to initialize a new working tree',
106
114
                         'current bzr can only operate from top-of-tree'])
107
115
        self._check_format()
108
 
        self.lock(lock_mode)
109
116
 
110
117
        self.text_store = ImmutableStore(self.controlfilename('text-store'))
111
118
        self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
119
126
    __repr__ = __str__
120
127
 
121
128
 
122
 
 
123
 
    def lock(self, mode='w'):
124
 
        """Lock the on-disk branch, excluding other processes."""
125
 
        try:
126
 
            import fcntl, errno
127
 
 
128
 
            if mode == 'w':
129
 
                lm = fcntl.LOCK_EX
130
 
                om = os.O_WRONLY | os.O_CREAT
131
 
            elif mode == 'r':
132
 
                lm = fcntl.LOCK_SH
133
 
                om = os.O_RDONLY
134
 
            else:
135
 
                raise BzrError("invalid locking mode %r" % mode)
136
 
 
137
 
            try:
138
 
                lockfile = os.open(self.controlfilename('branch-lock'), om)
139
 
            except OSError, e:
140
 
                if e.errno == errno.ENOENT:
141
 
                    # might not exist on branches from <0.0.4
142
 
                    self.controlfile('branch-lock', 'w').close()
143
 
                    lockfile = os.open(self.controlfilename('branch-lock'), om)
144
 
                else:
145
 
                    raise e
146
 
            
147
 
            fcntl.lockf(lockfile, lm)
148
 
            def unlock():
149
 
                fcntl.lockf(lockfile, fcntl.LOCK_UN)
150
 
                os.close(lockfile)
151
 
                self._lockmode = None
152
 
            self.unlock = unlock
153
 
            self._lockmode = mode
154
 
        except ImportError:
155
 
            warning("please write a locking method for platform %r" % sys.platform)
156
 
            def unlock():
157
 
                self._lockmode = None
158
 
            self.unlock = unlock
159
 
            self._lockmode = mode
160
 
 
161
 
 
162
 
    def _need_readlock(self):
163
 
        if self._lockmode not in ['r', 'w']:
164
 
            raise BzrError('need read lock on branch, only have %r' % self._lockmode)
165
 
 
166
 
    def _need_writelock(self):
167
 
        if self._lockmode not in ['w']:
168
 
            raise BzrError('need write lock on branch, only have %r' % self._lockmode)
169
 
 
170
 
 
171
129
    def abspath(self, name):
172
130
        """Return absolute filename for something in the branch"""
173
131
        return os.path.join(self.base, name)
194
152
 
195
153
 
196
154
    def controlfile(self, file_or_path, mode='r'):
197
 
        """Open a control file for this branch.
198
 
 
199
 
        There are two classes of file in the control directory: text
200
 
        and binary.  binary files are untranslated byte streams.  Text
201
 
        control files are stored with Unix newlines and in UTF-8, even
202
 
        if the platform or locale defaults are different.
203
 
        """
204
 
 
205
 
        fn = self.controlfilename(file_or_path)
206
 
 
207
 
        if mode == 'rb' or mode == 'wb':
208
 
            return file(fn, mode)
209
 
        elif mode == 'r' or mode == 'w':
210
 
            # open in binary mode anyhow so there's no newline translation;
211
 
            # codecs uses line buffering by default; don't want that.
212
 
            import codecs
213
 
            return codecs.open(fn, mode + 'b', 'utf-8',
214
 
                               buffering=60000)
215
 
        else:
216
 
            raise BzrError("invalid controlfile mode %r" % mode)
217
 
 
 
155
        """Open a control file for this branch"""
 
156
        return file(self.controlfilename(file_or_path), mode)
218
157
 
219
158
 
220
159
    def _make_control(self):
222
161
        self.controlfile('README', 'w').write(
223
162
            "This is a Bazaar-NG control directory.\n"
224
163
            "Do not change any files in this directory.")
225
 
        self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT)
 
164
        self.controlfile('branch-format', 'wb').write(BZR_BRANCH_FORMAT)
226
165
        for d in ('text-store', 'inventory-store', 'revision-store'):
227
166
            os.mkdir(self.controlfilename(d))
228
167
        for f in ('revision-history', 'merged-patches',
229
 
                  'pending-merged-patches', 'branch-name',
230
 
                  'branch-lock'):
 
168
                  'pending-merged-patches', 'branch-name'):
231
169
            self.controlfile(f, 'w').write('')
232
170
        mutter('created control directory in ' + self.base)
233
171
        Inventory().write_xml(self.controlfile('inventory','w'))
244
182
        # This ignores newlines so that we can open branches created
245
183
        # on Windows from Linux and so on.  I think it might be better
246
184
        # to always make all internal files in unix format.
247
 
        fmt = self.controlfile('branch-format', 'r').read()
 
185
        fmt = self.controlfile('branch-format', 'rb').read()
248
186
        fmt.replace('\r\n', '')
249
187
        if fmt != BZR_BRANCH_FORMAT:
250
188
            bailout('sorry, branch format %r not supported' % fmt,
254
192
 
255
193
    def read_working_inventory(self):
256
194
        """Read the working inventory."""
257
 
        self._need_readlock()
258
195
        before = time.time()
259
 
        # ElementTree does its own conversion from UTF-8, so open in
260
 
        # binary.
261
 
        inv = Inventory.read_xml(self.controlfile('inventory', 'rb'))
 
196
        inv = Inventory.read_xml(self.controlfile('inventory', 'r'))
262
197
        mutter("loaded inventory of %d items in %f"
263
198
               % (len(inv), time.time() - before))
264
199
        return inv
270
205
        That is to say, the inventory describing changes underway, that
271
206
        will be committed to the next revision.
272
207
        """
273
 
        self._need_writelock()
274
208
        ## TODO: factor out to atomicfile?  is rename safe on windows?
275
209
        ## TODO: Maybe some kind of clean/dirty marker on inventory?
276
210
        tmpfname = self.controlfilename('inventory.tmp')
277
 
        tmpf = file(tmpfname, 'wb')
 
211
        tmpf = file(tmpfname, 'w')
278
212
        inv.write_xml(tmpf)
279
213
        tmpf.close()
280
214
        inv_fname = self.controlfilename('inventory')
291
225
    def add(self, files, verbose=False):
292
226
        """Make files versioned.
293
227
 
294
 
        Note that the command line normally calls smart_add instead.
295
 
 
296
228
        This puts the files in the Added state, so that they will be
297
229
        recorded by the next commit.
298
230
 
299
 
        TODO: Perhaps have an option to add the ids even if the files do
 
231
        :todo: Perhaps have an option to add the ids even if the files do
300
232
               not (yet) exist.
301
233
 
302
 
        TODO: Perhaps return the ids of the files?  But then again it
 
234
        :todo: Perhaps return the ids of the files?  But then again it
303
235
               is easy to retrieve them if they're needed.
304
236
 
305
 
        TODO: Option to specify file id.
 
237
        :todo: Option to specify file id.
306
238
 
307
 
        TODO: Adding a directory should optionally recurse down and
 
239
        :todo: Adding a directory should optionally recurse down and
308
240
               add all non-ignored children.  Perhaps do that in a
309
241
               higher-level method.
310
242
 
330
262
        Traceback (most recent call last):
331
263
        BzrError: ('cannot add: not a regular file or directory: nothere', [])
332
264
        """
333
 
        self._need_writelock()
334
265
 
335
266
        # TODO: Re-adding a file that is removed in the working copy
336
267
        # should probably put it back with the previous ID.
371
302
 
372
303
    def print_file(self, file, revno):
373
304
        """Print `file` to stdout."""
374
 
        self._need_readlock()
375
305
        tree = self.revision_tree(self.lookup_revision(revno))
376
306
        # use inventory as it was in that revision
377
307
        file_id = tree.inventory.path2id(file)
385
315
 
386
316
        This does not remove their text.  This does not run on 
387
317
 
388
 
        TODO: Refuse to remove modified files unless --force is given?
 
318
        :todo: Refuse to remove modified files unless --force is given?
389
319
 
390
320
        >>> b = ScratchBranch(files=['foo'])
391
321
        >>> b.add('foo')
409
339
        >>> b.working_tree().has_filename('foo') 
410
340
        True
411
341
 
412
 
        TODO: Do something useful with directories.
 
342
        :todo: Do something useful with directories.
413
343
 
414
 
        TODO: Should this remove the text or not?  Tough call; not
 
344
        :todo: Should this remove the text or not?  Tough call; not
415
345
        removing may be useful and the user can just use use rm, and
416
346
        is the opposite of add.  Removing it is consistent with most
417
347
        other tools.  Maybe an option.
418
348
        """
419
349
        ## TODO: Normalize names
420
350
        ## TODO: Remove nested loops; better scalability
421
 
        self._need_writelock()
422
351
 
423
352
        if isinstance(files, types.StringTypes):
424
353
            files = [files]
482
411
        be robust against files disappearing, moving, etc.  So the
483
412
        whole thing is a bit hard.
484
413
 
485
 
        timestamp -- if not None, seconds-since-epoch for a
 
414
        :param timestamp: if not None, seconds-since-epoch for a
486
415
             postdated/predated commit.
487
416
        """
488
 
        self._need_writelock()
489
417
 
490
418
        ## TODO: Show branch names
491
419
 
627
555
        ## TODO: Also calculate and store the inventory SHA1
628
556
        mutter("committing patch r%d" % (self.revno() + 1))
629
557
 
 
558
        mutter("append to revision-history")
 
559
        f = self.controlfile('revision-history', 'at')
 
560
        f.write(rev_id + '\n')
 
561
        f.close()
630
562
 
631
 
        self.append_revision(rev_id)
632
 
        
633
563
        if verbose:
634
564
            note("commited r%d" % self.revno())
635
565
 
636
566
 
637
 
    def append_revision(self, revision_id):
638
 
        mutter("add {%s} to revision-history" % revision_id)
639
 
        rev_history = self.revision_history()
640
 
 
641
 
        tmprhname = self.controlfilename('revision-history.tmp')
642
 
        rhname = self.controlfilename('revision-history')
643
 
        
644
 
        f = file(tmprhname, 'wt')
645
 
        rev_history.append(revision_id)
646
 
        f.write('\n'.join(rev_history))
647
 
        f.write('\n')
648
 
        f.close()
649
 
 
650
 
        if sys.platform == 'win32':
651
 
            os.remove(rhname)
652
 
        os.rename(tmprhname, rhname)
653
 
        
654
 
 
655
 
 
656
567
    def get_revision(self, revision_id):
657
568
        """Return the Revision object for a named revision"""
658
 
        self._need_readlock()
659
569
        r = Revision.read_xml(self.revision_store[revision_id])
660
570
        assert r.revision_id == revision_id
661
571
        return r
664
574
    def get_inventory(self, inventory_id):
665
575
        """Get Inventory object by hash.
666
576
 
667
 
        TODO: Perhaps for this and similar methods, take a revision
 
577
        :todo: Perhaps for this and similar methods, take a revision
668
578
               parameter which can be either an integer revno or a
669
579
               string hash."""
670
 
        self._need_readlock()
671
580
        i = Inventory.read_xml(self.inventory_store[inventory_id])
672
581
        return i
673
582
 
674
583
 
675
584
    def get_revision_inventory(self, revision_id):
676
585
        """Return inventory of a past revision."""
677
 
        self._need_readlock()
678
586
        if revision_id == None:
679
587
            return Inventory()
680
588
        else:
687
595
        >>> ScratchBranch().revision_history()
688
596
        []
689
597
        """
690
 
        self._need_readlock()
691
 
        return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()]
692
 
 
693
 
 
694
 
    def enum_history(self, direction):
695
 
        """Return (revno, revision_id) for history of branch.
696
 
 
697
 
        direction
698
 
            'forward' is from earliest to latest
699
 
            'reverse' is from latest to earliest
700
 
        """
701
 
        rh = self.revision_history()
702
 
        if direction == 'forward':
703
 
            i = 1
704
 
            for rid in rh:
705
 
                yield i, rid
706
 
                i += 1
707
 
        elif direction == 'reverse':
708
 
            i = len(rh)
709
 
            while i > 0:
710
 
                yield i, rh[i-1]
711
 
                i -= 1
712
 
        else:
713
 
            raise BzrError('invalid history direction %r' % direction)
 
598
        return [chomp(l) for l in self.controlfile('revision-history').readlines()]
714
599
 
715
600
 
716
601
    def revno(self):
759
644
 
760
645
        `revision_id` may be None for the null revision, in which case
761
646
        an `EmptyTree` is returned."""
762
 
        self._need_readlock()
 
647
 
763
648
        if revision_id == None:
764
649
            return EmptyTree()
765
650
        else:
795
680
 
796
681
 
797
682
 
 
683
    def write_log(self, show_timezone='original'):
 
684
        """Write out human-readable log of commits to this branch
 
685
 
 
686
        :param utc: If true, show dates in universal time, not local time."""
 
687
        ## TODO: Option to choose either original, utc or local timezone
 
688
        revno = 1
 
689
        precursor = None
 
690
        for p in self.revision_history():
 
691
            print '-' * 40
 
692
            print 'revno:', revno
 
693
            ## TODO: Show hash if --id is given.
 
694
            ##print 'revision-hash:', p
 
695
            rev = self.get_revision(p)
 
696
            print 'committer:', rev.committer
 
697
            print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0,
 
698
                                                 show_timezone))
 
699
 
 
700
            ## opportunistic consistency check, same as check_patch_chaining
 
701
            if rev.precursor != precursor:
 
702
                bailout("mismatched precursor!")
 
703
 
 
704
            print 'message:'
 
705
            if not rev.message:
 
706
                print '  (no message)'
 
707
            else:
 
708
                for l in rev.message.split('\n'):
 
709
                    print '  ' + l
 
710
 
 
711
            revno += 1
 
712
            precursor = p
 
713
 
 
714
 
798
715
    def rename_one(self, from_rel, to_rel):
799
 
        """Rename one file.
800
 
 
801
 
        This can change the directory or the filename or both.
802
 
        """
803
 
        self._need_writelock()
804
716
        tree = self.working_tree()
805
717
        inv = tree.inventory
806
718
        if not tree.has_filename(from_rel):
855
767
        Note that to_name is only the last component of the new name;
856
768
        this doesn't change the directory.
857
769
        """
858
 
        self._need_writelock()
859
770
        ## TODO: Option to move IDs only
860
771
        assert not isinstance(from_paths, basestring)
861
772
        tree = self.working_tree()
923
834
        >>> b.show_status()
924
835
        D       foo
925
836
        
926
 
        TODO: Get state for single files.
 
837
 
 
838
        :todo: Get state for single files.
 
839
 
 
840
        :todo: Perhaps show a slash at the end of directory names.        
 
841
 
927
842
        """
928
 
        self._need_readlock()
929
843
 
930
844
        # We have to build everything into a list first so that it can
931
845
        # sorted by name, incorporating all the different sources.
956
870
            elif fs == '?':
957
871
                show_status(fs, kind, newname)
958
872
            else:
959
 
                bailout("weird file state %r" % ((fs, fid),))
 
873
                bailout("wierd file state %r" % ((fs, fid),))
960
874
                
961
875
 
962
876
 
967
881
    >>> isdir(b.base)
968
882
    True
969
883
    >>> bd = b.base
970
 
    >>> b.destroy()
 
884
    >>> del b
971
885
    >>> isdir(bd)
972
886
    False
973
887
    """
987
901
 
988
902
 
989
903
    def __del__(self):
990
 
        self.destroy()
991
 
 
992
 
    def destroy(self):
993
904
        """Destroy the test branch, removing the scratch directory."""
994
905
        try:
995
 
            mutter("delete ScratchBranch %s" % self.base)
996
906
            shutil.rmtree(self.base)
997
 
        except OSError, e:
 
907
        except OSError:
998
908
            # Work around for shutil.rmtree failing on Windows when
999
909
            # readonly files are encountered
1000
 
            mutter("hit exception in destroying ScratchBranch: %s" % e)
1001
910
            for root, dirs, files in os.walk(self.base, topdown=False):
1002
911
                for name in files:
1003
912
                    os.chmod(os.path.join(root, name), 0700)
1004
913
            shutil.rmtree(self.base)
1005
 
        self.base = None
1006
914
 
1007
915
    
1008
916
 
1041
949
    idx = name.rfind('/')
1042
950
    if idx != -1:
1043
951
        name = name[idx+1 : ]
1044
 
    idx = name.rfind('\\')
1045
 
    if idx != -1:
1046
 
        name = name[idx+1 : ]
1047
952
 
1048
953
    name = name.lstrip('.')
1049
954
 
1050
955
    s = hexlify(rand_bytes(8))
1051
956
    return '-'.join((name, compact_date(time.time()), s))
 
957
 
 
958