~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Martin Pool
  • Date: 2005-05-05 06:38:18 UTC
  • Revision ID: mbp@sourcefrog.net-20050505063818-3eb3260343878325
- do upload CHANGELOG to web server, even though it's autogenerated

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, chomp, \
 
29
from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \
30
30
     format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \
31
 
     joinpath, sha_string, file_kind, local_time_offset
 
31
     joinpath, sha_string, file_kind, local_time_offset, appendpath
32
32
from store import ImmutableStore
33
33
from revision import Revision
34
 
from errors import bailout
 
34
from errors import bailout, BzrError
35
35
from textui import show_status
36
36
from diff import diff_trees
37
37
 
47
47
 
48
48
    Basically we keep looking up until we find the control directory or
49
49
    run into the root."""
50
 
    if f is None:
 
50
    if f == None:
51
51
        f = os.getcwd()
52
52
    elif hasattr(os.path, 'realpath'):
53
53
        f = os.path.realpath(f)
56
56
 
57
57
    orig_f = f
58
58
 
59
 
    last_f = f
60
59
    while True:
61
60
        if os.path.exists(os.path.join(f, bzrlib.BZRDIR)):
62
61
            return f
63
62
        head, tail = os.path.split(f)
64
63
        if head == f:
65
64
            # reached the root, whatever that may be
66
 
            bailout('%r is not in a branch' % orig_f)
 
65
            raise BzrError('%r is not in a branch' % orig_f)
67
66
        f = head
68
67
    
69
68
 
74
73
class Branch:
75
74
    """Branch holding a history of revisions.
76
75
 
77
 
    :todo: Perhaps use different stores for different classes of object,
78
 
           so that we can keep track of how much space each one uses,
79
 
           or garbage-collect them.
80
 
 
81
 
    :todo: Add a RemoteBranch subclass.  For the basic case of read-only
82
 
           HTTP access this should be very easy by, 
83
 
           just redirecting controlfile access into HTTP requests.
84
 
           We would need a RemoteStore working similarly.
85
 
 
86
 
    :todo: Keep the on-disk branch locked while the object exists.
87
 
 
88
 
    :todo: mkdir() method.
 
76
    base
 
77
        Base directory of the branch.
89
78
    """
90
 
    def __init__(self, base, init=False, find_root=True):
 
79
    _lockmode = None
 
80
    
 
81
    def __init__(self, base, init=False, find_root=True, lock_mode='w'):
91
82
        """Create new branch object at a particular location.
92
83
 
93
 
        :param base: Base directory for the branch.
 
84
        base -- Base directory for the branch.
94
85
        
95
 
        :param init: If True, create new control files in a previously
 
86
        init -- If True, create new control files in a previously
96
87
             unversioned directory.  If False, the branch must already
97
88
             be versioned.
98
89
 
99
 
        :param find_root: If true and init is false, find the root of the
 
90
        find_root -- If true and init is false, find the root of the
100
91
             existing branch containing base.
101
92
 
102
93
        In the test suite, creation of new trees is tested using the
114
105
                        ['use "bzr init" to initialize a new working tree',
115
106
                         'current bzr can only operate from top-of-tree'])
116
107
        self._check_format()
 
108
        self.lock(lock_mode)
117
109
 
118
110
        self.text_store = ImmutableStore(self.controlfilename('text-store'))
119
111
        self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
127
119
    __repr__ = __str__
128
120
 
129
121
 
 
122
 
 
123
    def lock(self, mode='w'):
 
124
        """Lock the on-disk branch, excluding other processes."""
 
125
        try:
 
126
            import fcntl
 
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
            # XXX: Old branches might not have the lock file, and
 
138
            # won't get one until someone does a write-mode command on
 
139
            # them or creates it by hand.
 
140
 
 
141
            lockfile = os.open(self.controlfilename('branch-lock'), om)
 
142
            fcntl.lockf(lockfile, lm)
 
143
            def unlock():
 
144
                fcntl.lockf(lockfile, fcntl.LOCK_UN)
 
145
                os.close(lockfile)
 
146
                self._lockmode = None
 
147
            self.unlock = unlock
 
148
            self._lockmode = mode
 
149
        except ImportError:
 
150
            warning("please write a locking method for platform %r" % sys.platform)
 
151
            def unlock():
 
152
                self._lockmode = None
 
153
            self.unlock = unlock
 
154
            self._lockmode = mode
 
155
 
 
156
 
 
157
    def _need_readlock(self):
 
158
        if self._lockmode not in ['r', 'w']:
 
159
            raise BzrError('need read lock on branch, only have %r' % self._lockmode)
 
160
 
 
161
    def _need_writelock(self):
 
162
        if self._lockmode not in ['w']:
 
163
            raise BzrError('need write lock on branch, only have %r' % self._lockmode)
 
164
 
 
165
 
130
166
    def abspath(self, name):
131
167
        """Return absolute filename for something in the branch"""
132
168
        return os.path.join(self.base, name)
153
189
 
154
190
 
155
191
    def controlfile(self, file_or_path, mode='r'):
156
 
        """Open a control file for this branch"""
157
 
        return file(self.controlfilename(file_or_path), mode)
 
192
        """Open a control file for this branch.
 
193
 
 
194
        There are two classes of file in the control directory: text
 
195
        and binary.  binary files are untranslated byte streams.  Text
 
196
        control files are stored with Unix newlines and in UTF-8, even
 
197
        if the platform or locale defaults are different.
 
198
        """
 
199
 
 
200
        fn = self.controlfilename(file_or_path)
 
201
 
 
202
        if mode == 'rb' or mode == 'wb':
 
203
            return file(fn, mode)
 
204
        elif mode == 'r' or mode == 'w':
 
205
            # open in binary mode anyhow so there's no newline translation;
 
206
            # codecs uses line buffering by default; don't want that.
 
207
            import codecs
 
208
            return codecs.open(fn, mode + 'b', 'utf-8',
 
209
                               buffering=60000)
 
210
        else:
 
211
            raise BzrError("invalid controlfile mode %r" % mode)
 
212
 
158
213
 
159
214
 
160
215
    def _make_control(self):
166
221
        for d in ('text-store', 'inventory-store', 'revision-store'):
167
222
            os.mkdir(self.controlfilename(d))
168
223
        for f in ('revision-history', 'merged-patches',
169
 
                  'pending-merged-patches', 'branch-name'):
 
224
                  'pending-merged-patches', 'branch-name',
 
225
                  'branch-lock'):
170
226
            self.controlfile(f, 'w').write('')
171
227
        mutter('created control directory in ' + self.base)
172
228
        Inventory().write_xml(self.controlfile('inventory','w'))
179
235
 
180
236
        In the future, we might need different in-memory Branch
181
237
        classes to support downlevel branches.  But not yet.
182
 
        """        
183
 
        # read in binary mode to detect newline wierdness.
184
 
        fmt = self.controlfile('branch-format', 'rb').read()
 
238
        """
 
239
        # This ignores newlines so that we can open branches created
 
240
        # on Windows from Linux and so on.  I think it might be better
 
241
        # to always make all internal files in unix format.
 
242
        fmt = self.controlfile('branch-format', 'r').read()
 
243
        fmt.replace('\r\n', '')
185
244
        if fmt != BZR_BRANCH_FORMAT:
186
245
            bailout('sorry, branch format %r not supported' % fmt,
187
246
                    ['use a different bzr version',
190
249
 
191
250
    def read_working_inventory(self):
192
251
        """Read the working inventory."""
 
252
        self._need_readlock()
193
253
        before = time.time()
194
 
        inv = Inventory.read_xml(self.controlfile('inventory', 'r'))
 
254
        # ElementTree does its own conversion from UTF-8, so open in
 
255
        # binary.
 
256
        inv = Inventory.read_xml(self.controlfile('inventory', 'rb'))
195
257
        mutter("loaded inventory of %d items in %f"
196
258
               % (len(inv), time.time() - before))
197
259
        return inv
203
265
        That is to say, the inventory describing changes underway, that
204
266
        will be committed to the next revision.
205
267
        """
 
268
        self._need_writelock()
206
269
        ## TODO: factor out to atomicfile?  is rename safe on windows?
207
270
        ## TODO: Maybe some kind of clean/dirty marker on inventory?
208
271
        tmpfname = self.controlfilename('inventory.tmp')
209
 
        tmpf = file(tmpfname, 'w')
 
272
        tmpf = file(tmpfname, 'wb')
210
273
        inv.write_xml(tmpf)
211
274
        tmpf.close()
212
 
        os.rename(tmpfname, self.controlfilename('inventory'))
 
275
        inv_fname = self.controlfilename('inventory')
 
276
        if sys.platform == 'win32':
 
277
            os.remove(inv_fname)
 
278
        os.rename(tmpfname, inv_fname)
213
279
        mutter('wrote working inventory')
214
280
 
215
281
 
220
286
    def add(self, files, verbose=False):
221
287
        """Make files versioned.
222
288
 
 
289
        Note that the command line normally calls smart_add instead.
 
290
 
223
291
        This puts the files in the Added state, so that they will be
224
292
        recorded by the next commit.
225
293
 
226
 
        :todo: Perhaps have an option to add the ids even if the files do
 
294
        TODO: Perhaps have an option to add the ids even if the files do
227
295
               not (yet) exist.
228
296
 
229
 
        :todo: Perhaps return the ids of the files?  But then again it
 
297
        TODO: Perhaps return the ids of the files?  But then again it
230
298
               is easy to retrieve them if they're needed.
231
299
 
232
 
        :todo: Option to specify file id.
 
300
        TODO: Option to specify file id.
233
301
 
234
 
        :todo: Adding a directory should optionally recurse down and
 
302
        TODO: Adding a directory should optionally recurse down and
235
303
               add all non-ignored children.  Perhaps do that in a
236
304
               higher-level method.
237
305
 
257
325
        Traceback (most recent call last):
258
326
        BzrError: ('cannot add: not a regular file or directory: nothere', [])
259
327
        """
 
328
        self._need_writelock()
260
329
 
261
330
        # TODO: Re-adding a file that is removed in the working copy
262
331
        # should probably put it back with the previous ID.
295
364
        self._write_inventory(inv)
296
365
 
297
366
 
 
367
    def print_file(self, file, revno):
 
368
        """Print `file` to stdout."""
 
369
        self._need_readlock()
 
370
        tree = self.revision_tree(self.lookup_revision(revno))
 
371
        # use inventory as it was in that revision
 
372
        file_id = tree.inventory.path2id(file)
 
373
        if not file_id:
 
374
            bailout("%r is not present in revision %d" % (file, revno))
 
375
        tree.print_file(file_id)
 
376
        
298
377
 
299
378
    def remove(self, files, verbose=False):
300
379
        """Mark nominated files for removal from the inventory.
301
380
 
302
381
        This does not remove their text.  This does not run on 
303
382
 
304
 
        :todo: Refuse to remove modified files unless --force is given?
 
383
        TODO: Refuse to remove modified files unless --force is given?
305
384
 
306
385
        >>> b = ScratchBranch(files=['foo'])
307
386
        >>> b.add('foo')
325
404
        >>> b.working_tree().has_filename('foo') 
326
405
        True
327
406
 
328
 
        :todo: Do something useful with directories.
 
407
        TODO: Do something useful with directories.
329
408
 
330
 
        :todo: Should this remove the text or not?  Tough call; not
 
409
        TODO: Should this remove the text or not?  Tough call; not
331
410
        removing may be useful and the user can just use use rm, and
332
411
        is the opposite of add.  Removing it is consistent with most
333
412
        other tools.  Maybe an option.
334
413
        """
335
414
        ## TODO: Normalize names
336
415
        ## TODO: Remove nested loops; better scalability
 
416
        self._need_writelock()
337
417
 
338
418
        if isinstance(files, types.StringTypes):
339
419
            files = [files]
397
477
        be robust against files disappearing, moving, etc.  So the
398
478
        whole thing is a bit hard.
399
479
 
400
 
        :param timestamp: if not None, seconds-since-epoch for a
 
480
        timestamp -- if not None, seconds-since-epoch for a
401
481
             postdated/predated commit.
402
482
        """
 
483
        self._need_writelock()
403
484
 
404
485
        ## TODO: Show branch names
405
486
 
480
561
                            state = 'A'
481
562
                        elif (old_ie.name == entry.name
482
563
                              and old_ie.parent_id == entry.parent_id):
 
564
                            state = 'M'
 
565
                        else:
483
566
                            state = 'R'
484
 
                        else:
485
 
                            state = 'M'
486
567
 
487
568
                        show_status(state, entry.kind, quotefn(path))
488
569
 
541
622
        ## TODO: Also calculate and store the inventory SHA1
542
623
        mutter("committing patch r%d" % (self.revno() + 1))
543
624
 
544
 
        mutter("append to revision-history")
545
 
        self.controlfile('revision-history', 'at').write(rev_id + '\n')
546
 
 
547
 
        mutter("done!")
 
625
 
 
626
        self.append_revision(rev_id)
 
627
        
 
628
        if verbose:
 
629
            note("commited r%d" % self.revno())
 
630
 
 
631
 
 
632
    def append_revision(self, revision_id):
 
633
        mutter("add {%s} to revision-history" % revision_id)
 
634
        rev_history = self.revision_history()
 
635
 
 
636
        tmprhname = self.controlfilename('revision-history.tmp')
 
637
        rhname = self.controlfilename('revision-history')
 
638
        
 
639
        f = file(tmprhname, 'wt')
 
640
        rev_history.append(revision_id)
 
641
        f.write('\n'.join(rev_history))
 
642
        f.write('\n')
 
643
        f.close()
 
644
 
 
645
        if sys.platform == 'win32':
 
646
            os.remove(rhname)
 
647
        os.rename(tmprhname, rhname)
 
648
        
548
649
 
549
650
 
550
651
    def get_revision(self, revision_id):
551
652
        """Return the Revision object for a named revision"""
 
653
        self._need_readlock()
552
654
        r = Revision.read_xml(self.revision_store[revision_id])
553
655
        assert r.revision_id == revision_id
554
656
        return r
557
659
    def get_inventory(self, inventory_id):
558
660
        """Get Inventory object by hash.
559
661
 
560
 
        :todo: Perhaps for this and similar methods, take a revision
 
662
        TODO: Perhaps for this and similar methods, take a revision
561
663
               parameter which can be either an integer revno or a
562
664
               string hash."""
 
665
        self._need_readlock()
563
666
        i = Inventory.read_xml(self.inventory_store[inventory_id])
564
667
        return i
565
668
 
566
669
 
567
670
    def get_revision_inventory(self, revision_id):
568
671
        """Return inventory of a past revision."""
 
672
        self._need_readlock()
569
673
        if revision_id == None:
570
674
            return Inventory()
571
675
        else:
578
682
        >>> ScratchBranch().revision_history()
579
683
        []
580
684
        """
581
 
        return [chomp(l) for l in self.controlfile('revision-history').readlines()]
 
685
        self._need_readlock()
 
686
        return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()]
582
687
 
583
688
 
584
689
    def revno(self):
606
711
        ph = self.revision_history()
607
712
        if ph:
608
713
            return ph[-1]
609
 
 
 
714
        else:
 
715
            return None
 
716
        
610
717
 
611
718
    def lookup_revision(self, revno):
612
719
        """Return revision hash for revision number."""
617
724
            # list is 0-based; revisions are 1-based
618
725
            return self.revision_history()[revno-1]
619
726
        except IndexError:
620
 
            bailout("no such revision %s" % revno)
 
727
            raise BzrError("no such revision %s" % revno)
621
728
 
622
729
 
623
730
    def revision_tree(self, revision_id):
625
732
 
626
733
        `revision_id` may be None for the null revision, in which case
627
734
        an `EmptyTree` is returned."""
628
 
 
 
735
        self._need_readlock()
629
736
        if revision_id == None:
630
737
            return EmptyTree()
631
738
        else:
661
768
 
662
769
 
663
770
 
664
 
    def write_log(self, show_timezone='original'):
 
771
    def write_log(self, show_timezone='original', verbose=False):
665
772
        """Write out human-readable log of commits to this branch
666
773
 
667
 
        :param utc: If true, show dates in universal time, not local time."""
 
774
        utc -- If true, show dates in universal time, not local time."""
 
775
        self._need_readlock()
668
776
        ## TODO: Option to choose either original, utc or local timezone
669
777
        revno = 1
670
778
        precursor = None
689
797
                for l in rev.message.split('\n'):
690
798
                    print '  ' + l
691
799
 
 
800
            if verbose == True and precursor != None:
 
801
                print 'changed files:'
 
802
                tree = self.revision_tree(p)
 
803
                prevtree = self.revision_tree(precursor)
 
804
                
 
805
                for file_state, fid, old_name, new_name, kind in \
 
806
                                        diff_trees(prevtree, tree, ):
 
807
                    if file_state == 'A' or file_state == 'M':
 
808
                        show_status(file_state, kind, new_name)
 
809
                    elif file_state == 'D':
 
810
                        show_status(file_state, kind, old_name)
 
811
                    elif file_state == 'R':
 
812
                        show_status(file_state, kind,
 
813
                            old_name + ' => ' + new_name)
 
814
                
692
815
            revno += 1
693
816
            precursor = p
694
817
 
695
818
 
696
 
 
697
 
    def show_status(branch, show_all=False):
 
819
    def rename_one(self, from_rel, to_rel):
 
820
        """Rename one file.
 
821
 
 
822
        This can change the directory or the filename or both.
 
823
        """
 
824
        self._need_writelock()
 
825
        tree = self.working_tree()
 
826
        inv = tree.inventory
 
827
        if not tree.has_filename(from_rel):
 
828
            bailout("can't rename: old working file %r does not exist" % from_rel)
 
829
        if tree.has_filename(to_rel):
 
830
            bailout("can't rename: new working file %r already exists" % to_rel)
 
831
            
 
832
        file_id = inv.path2id(from_rel)
 
833
        if file_id == None:
 
834
            bailout("can't rename: old name %r is not versioned" % from_rel)
 
835
 
 
836
        if inv.path2id(to_rel):
 
837
            bailout("can't rename: new name %r is already versioned" % to_rel)
 
838
 
 
839
        to_dir, to_tail = os.path.split(to_rel)
 
840
        to_dir_id = inv.path2id(to_dir)
 
841
        if to_dir_id == None and to_dir != '':
 
842
            bailout("can't determine destination directory id for %r" % to_dir)
 
843
 
 
844
        mutter("rename_one:")
 
845
        mutter("  file_id    {%s}" % file_id)
 
846
        mutter("  from_rel   %r" % from_rel)
 
847
        mutter("  to_rel     %r" % to_rel)
 
848
        mutter("  to_dir     %r" % to_dir)
 
849
        mutter("  to_dir_id  {%s}" % to_dir_id)
 
850
            
 
851
        inv.rename(file_id, to_dir_id, to_tail)
 
852
 
 
853
        print "%s => %s" % (from_rel, to_rel)
 
854
        
 
855
        from_abs = self.abspath(from_rel)
 
856
        to_abs = self.abspath(to_rel)
 
857
        try:
 
858
            os.rename(from_abs, to_abs)
 
859
        except OSError, e:
 
860
            bailout("failed to rename %r to %r: %s"
 
861
                    % (from_abs, to_abs, e[1]),
 
862
                    ["rename rolled back"])
 
863
 
 
864
        self._write_inventory(inv)
 
865
            
 
866
 
 
867
 
 
868
    def move(self, from_paths, to_name):
 
869
        """Rename files.
 
870
 
 
871
        to_name must exist as a versioned directory.
 
872
 
 
873
        If to_name exists and is a directory, the files are moved into
 
874
        it, keeping their old names.  If it is a directory, 
 
875
 
 
876
        Note that to_name is only the last component of the new name;
 
877
        this doesn't change the directory.
 
878
        """
 
879
        self._need_writelock()
 
880
        ## TODO: Option to move IDs only
 
881
        assert not isinstance(from_paths, basestring)
 
882
        tree = self.working_tree()
 
883
        inv = tree.inventory
 
884
        to_abs = self.abspath(to_name)
 
885
        if not isdir(to_abs):
 
886
            bailout("destination %r is not a directory" % to_abs)
 
887
        if not tree.has_filename(to_name):
 
888
            bailout("destination %r not in working directory" % to_abs)
 
889
        to_dir_id = inv.path2id(to_name)
 
890
        if to_dir_id == None and to_name != '':
 
891
            bailout("destination %r is not a versioned directory" % to_name)
 
892
        to_dir_ie = inv[to_dir_id]
 
893
        if to_dir_ie.kind not in ('directory', 'root_directory'):
 
894
            bailout("destination %r is not a directory" % to_abs)
 
895
 
 
896
        to_idpath = Set(inv.get_idpath(to_dir_id))
 
897
 
 
898
        for f in from_paths:
 
899
            if not tree.has_filename(f):
 
900
                bailout("%r does not exist in working tree" % f)
 
901
            f_id = inv.path2id(f)
 
902
            if f_id == None:
 
903
                bailout("%r is not versioned" % f)
 
904
            name_tail = splitpath(f)[-1]
 
905
            dest_path = appendpath(to_name, name_tail)
 
906
            if tree.has_filename(dest_path):
 
907
                bailout("destination %r already exists" % dest_path)
 
908
            if f_id in to_idpath:
 
909
                bailout("can't move %r to a subdirectory of itself" % f)
 
910
 
 
911
        # OK, so there's a race here, it's possible that someone will
 
912
        # create a file in this interval and then the rename might be
 
913
        # left half-done.  But we should have caught most problems.
 
914
 
 
915
        for f in from_paths:
 
916
            name_tail = splitpath(f)[-1]
 
917
            dest_path = appendpath(to_name, name_tail)
 
918
            print "%s => %s" % (f, dest_path)
 
919
            inv.rename(inv.path2id(f), to_dir_id, name_tail)
 
920
            try:
 
921
                os.rename(self.abspath(f), self.abspath(dest_path))
 
922
            except OSError, e:
 
923
                bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
 
924
                        ["rename rolled back"])
 
925
 
 
926
        self._write_inventory(inv)
 
927
 
 
928
 
 
929
 
 
930
    def show_status(self, show_all=False):
698
931
        """Display single-line status for non-ignored working files.
699
932
 
700
933
        The list is show sorted in order by file name.
711
944
        >>> b.show_status()
712
945
        D       foo
713
946
        
714
 
 
715
 
        :todo: Get state for single files.
716
 
 
717
 
        :todo: Perhaps show a slash at the end of directory names.        
718
 
 
 
947
        TODO: Get state for single files.
719
948
        """
 
949
        self._need_readlock()
720
950
 
721
951
        # We have to build everything into a list first so that it can
722
952
        # sorted by name, incorporating all the different sources.
727
957
        # Interesting case: the old ID for a file has been removed,
728
958
        # but a new file has been created under that name.
729
959
 
730
 
        old = branch.basis_tree()
731
 
        old_inv = old.inventory
732
 
        new = branch.working_tree()
733
 
        new_inv = new.inventory
 
960
        old = self.basis_tree()
 
961
        new = self.working_tree()
734
962
 
735
963
        for fs, fid, oldname, newname, kind in diff_trees(old, new):
736
964
            if fs == 'R':
749
977
            elif fs == '?':
750
978
                show_status(fs, kind, newname)
751
979
            else:
752
 
                bailout("wierd file state %r" % ((fs, fid),))
 
980
                bailout("weird file state %r" % ((fs, fid),))
753
981
                
754
982
 
755
983
 
764
992
    >>> isdir(bd)
765
993
    False
766
994
    """
767
 
    def __init__(self, files = []):
 
995
    def __init__(self, files=[], dirs=[]):
768
996
        """Make a test branch.
769
997
 
770
998
        This creates a temporary directory and runs init-tree in it.
772
1000
        If any files are listed, they are created in the working copy.
773
1001
        """
774
1002
        Branch.__init__(self, tempfile.mkdtemp(), init=True)
 
1003
        for d in dirs:
 
1004
            os.mkdir(self.abspath(d))
 
1005
            
775
1006
        for f in files:
776
1007
            file(os.path.join(self.base, f), 'w').write('content of %s' % f)
777
1008
 
778
1009
 
779
1010
    def __del__(self):
780
1011
        """Destroy the test branch, removing the scratch directory."""
781
 
        shutil.rmtree(self.base)
 
1012
        try:
 
1013
            shutil.rmtree(self.base)
 
1014
        except OSError:
 
1015
            # Work around for shutil.rmtree failing on Windows when
 
1016
            # readonly files are encountered
 
1017
            for root, dirs, files in os.walk(self.base, topdown=False):
 
1018
                for name in files:
 
1019
                    os.chmod(os.path.join(root, name), 0700)
 
1020
            shutil.rmtree(self.base)
782
1021
 
783
1022
    
784
1023
 
817
1056
    idx = name.rfind('/')
818
1057
    if idx != -1:
819
1058
        name = name[idx+1 : ]
 
1059
    idx = name.rfind('\\')
 
1060
    if idx != -1:
 
1061
        name = name[idx+1 : ]
820
1062
 
821
1063
    name = name.lstrip('.')
822
1064
 
823
1065
    s = hexlify(rand_bytes(8))
824
1066
    return '-'.join((name, compact_date(time.time()), s))
825
 
 
826