~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Martin Pool
  • Date: 2005-05-10 03:55:34 UTC
  • Revision ID: mbp@sourcefrog.net-20050510035534-643062e821052ac5
- Add fortune-cookie external plugin demonstration

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
 
40
40
 
41
41
 
42
42
 
 
43
def find_branch(f, **args):
 
44
    if 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
49
59
    run into the root."""
50
 
    if f is None:
 
60
    if f == None:
51
61
        f = os.getcwd()
52
62
    elif hasattr(os.path, 'realpath'):
53
63
        f = os.path.realpath(f)
56
66
 
57
67
    orig_f = f
58
68
 
59
 
    last_f = f
60
69
    while True:
61
70
        if os.path.exists(os.path.join(f, bzrlib.BZRDIR)):
62
71
            return f
63
72
        head, tail = os.path.split(f)
64
73
        if head == f:
65
74
            # reached the root, whatever that may be
66
 
            bailout('%r is not in a branch' % orig_f)
 
75
            raise BzrError('%r is not in a branch' % orig_f)
67
76
        f = head
68
77
    
69
78
 
74
83
class Branch:
75
84
    """Branch holding a history of revisions.
76
85
 
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.
 
86
    base
 
87
        Base directory of the branch.
89
88
    """
90
 
    def __init__(self, base, init=False, find_root=True):
 
89
    _lockmode = None
 
90
    
 
91
    def __init__(self, base, init=False, find_root=True, lock_mode='w'):
91
92
        """Create new branch object at a particular location.
92
93
 
93
 
        :param base: Base directory for the branch.
 
94
        base -- Base directory for the branch.
94
95
        
95
 
        :param init: If True, create new control files in a previously
 
96
        init -- If True, create new control files in a previously
96
97
             unversioned directory.  If False, the branch must already
97
98
             be versioned.
98
99
 
99
 
        :param find_root: If true and init is false, find the root of the
 
100
        find_root -- If true and init is false, find the root of the
100
101
             existing branch containing base.
101
102
 
102
103
        In the test suite, creation of new trees is tested using the
114
115
                        ['use "bzr init" to initialize a new working tree',
115
116
                         'current bzr can only operate from top-of-tree'])
116
117
        self._check_format()
 
118
        self.lock(lock_mode)
117
119
 
118
120
        self.text_store = ImmutableStore(self.controlfilename('text-store'))
119
121
        self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
127
129
    __repr__ = __str__
128
130
 
129
131
 
 
132
 
 
133
    def lock(self, mode='w'):
 
134
        """Lock the on-disk branch, excluding other processes."""
 
135
        try:
 
136
            import fcntl, errno
 
137
 
 
138
            if mode == 'w':
 
139
                lm = fcntl.LOCK_EX
 
140
                om = os.O_WRONLY | os.O_CREAT
 
141
            elif mode == 'r':
 
142
                lm = fcntl.LOCK_SH
 
143
                om = os.O_RDONLY
 
144
            else:
 
145
                raise BzrError("invalid locking mode %r" % mode)
 
146
 
 
147
            try:
 
148
                lockfile = os.open(self.controlfilename('branch-lock'), om)
 
149
            except OSError, e:
 
150
                if e.errno == errno.ENOENT:
 
151
                    # might not exist on branches from <0.0.4
 
152
                    self.controlfile('branch-lock', 'w').close()
 
153
                    lockfile = os.open(self.controlfilename('branch-lock'), om)
 
154
                else:
 
155
                    raise e
 
156
            
 
157
            fcntl.lockf(lockfile, lm)
 
158
            def unlock():
 
159
                fcntl.lockf(lockfile, fcntl.LOCK_UN)
 
160
                os.close(lockfile)
 
161
                self._lockmode = None
 
162
            self.unlock = unlock
 
163
            self._lockmode = mode
 
164
        except ImportError:
 
165
            warning("please write a locking method for platform %r" % sys.platform)
 
166
            def unlock():
 
167
                self._lockmode = None
 
168
            self.unlock = unlock
 
169
            self._lockmode = mode
 
170
 
 
171
 
 
172
    def _need_readlock(self):
 
173
        if self._lockmode not in ['r', 'w']:
 
174
            raise BzrError('need read lock on branch, only have %r' % self._lockmode)
 
175
 
 
176
    def _need_writelock(self):
 
177
        if self._lockmode not in ['w']:
 
178
            raise BzrError('need write lock on branch, only have %r' % self._lockmode)
 
179
 
 
180
 
130
181
    def abspath(self, name):
131
182
        """Return absolute filename for something in the branch"""
132
183
        return os.path.join(self.base, name)
153
204
 
154
205
 
155
206
    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)
 
207
        """Open a control file for this branch.
 
208
 
 
209
        There are two classes of file in the control directory: text
 
210
        and binary.  binary files are untranslated byte streams.  Text
 
211
        control files are stored with Unix newlines and in UTF-8, even
 
212
        if the platform or locale defaults are different.
 
213
        """
 
214
 
 
215
        fn = self.controlfilename(file_or_path)
 
216
 
 
217
        if mode == 'rb' or mode == 'wb':
 
218
            return file(fn, mode)
 
219
        elif mode == 'r' or mode == 'w':
 
220
            # open in binary mode anyhow so there's no newline translation;
 
221
            # codecs uses line buffering by default; don't want that.
 
222
            import codecs
 
223
            return codecs.open(fn, mode + 'b', 'utf-8',
 
224
                               buffering=60000)
 
225
        else:
 
226
            raise BzrError("invalid controlfile mode %r" % mode)
 
227
 
158
228
 
159
229
 
160
230
    def _make_control(self):
166
236
        for d in ('text-store', 'inventory-store', 'revision-store'):
167
237
            os.mkdir(self.controlfilename(d))
168
238
        for f in ('revision-history', 'merged-patches',
169
 
                  'pending-merged-patches', 'branch-name'):
 
239
                  'pending-merged-patches', 'branch-name',
 
240
                  'branch-lock'):
170
241
            self.controlfile(f, 'w').write('')
171
242
        mutter('created control directory in ' + self.base)
172
243
        Inventory().write_xml(self.controlfile('inventory','w'))
179
250
 
180
251
        In the future, we might need different in-memory Branch
181
252
        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()
 
253
        """
 
254
        # This ignores newlines so that we can open branches created
 
255
        # on Windows from Linux and so on.  I think it might be better
 
256
        # to always make all internal files in unix format.
 
257
        fmt = self.controlfile('branch-format', 'r').read()
 
258
        fmt.replace('\r\n', '')
185
259
        if fmt != BZR_BRANCH_FORMAT:
186
260
            bailout('sorry, branch format %r not supported' % fmt,
187
261
                    ['use a different bzr version',
190
264
 
191
265
    def read_working_inventory(self):
192
266
        """Read the working inventory."""
 
267
        self._need_readlock()
193
268
        before = time.time()
194
 
        inv = Inventory.read_xml(self.controlfile('inventory', 'r'))
 
269
        # ElementTree does its own conversion from UTF-8, so open in
 
270
        # binary.
 
271
        inv = Inventory.read_xml(self.controlfile('inventory', 'rb'))
195
272
        mutter("loaded inventory of %d items in %f"
196
273
               % (len(inv), time.time() - before))
197
274
        return inv
203
280
        That is to say, the inventory describing changes underway, that
204
281
        will be committed to the next revision.
205
282
        """
 
283
        self._need_writelock()
206
284
        ## TODO: factor out to atomicfile?  is rename safe on windows?
207
285
        ## TODO: Maybe some kind of clean/dirty marker on inventory?
208
286
        tmpfname = self.controlfilename('inventory.tmp')
209
 
        tmpf = file(tmpfname, 'w')
 
287
        tmpf = file(tmpfname, 'wb')
210
288
        inv.write_xml(tmpf)
211
289
        tmpf.close()
212
 
        os.rename(tmpfname, self.controlfilename('inventory'))
 
290
        inv_fname = self.controlfilename('inventory')
 
291
        if sys.platform == 'win32':
 
292
            os.remove(inv_fname)
 
293
        os.rename(tmpfname, inv_fname)
213
294
        mutter('wrote working inventory')
214
295
 
215
296
 
220
301
    def add(self, files, verbose=False):
221
302
        """Make files versioned.
222
303
 
 
304
        Note that the command line normally calls smart_add instead.
 
305
 
223
306
        This puts the files in the Added state, so that they will be
224
307
        recorded by the next commit.
225
308
 
226
 
        :todo: Perhaps have an option to add the ids even if the files do
 
309
        TODO: Perhaps have an option to add the ids even if the files do
227
310
               not (yet) exist.
228
311
 
229
 
        :todo: Perhaps return the ids of the files?  But then again it
 
312
        TODO: Perhaps return the ids of the files?  But then again it
230
313
               is easy to retrieve them if they're needed.
231
314
 
232
 
        :todo: Option to specify file id.
 
315
        TODO: Option to specify file id.
233
316
 
234
 
        :todo: Adding a directory should optionally recurse down and
 
317
        TODO: Adding a directory should optionally recurse down and
235
318
               add all non-ignored children.  Perhaps do that in a
236
319
               higher-level method.
237
320
 
257
340
        Traceback (most recent call last):
258
341
        BzrError: ('cannot add: not a regular file or directory: nothere', [])
259
342
        """
 
343
        self._need_writelock()
260
344
 
261
345
        # TODO: Re-adding a file that is removed in the working copy
262
346
        # should probably put it back with the previous ID.
295
379
        self._write_inventory(inv)
296
380
 
297
381
 
 
382
    def print_file(self, file, revno):
 
383
        """Print `file` to stdout."""
 
384
        self._need_readlock()
 
385
        tree = self.revision_tree(self.lookup_revision(revno))
 
386
        # use inventory as it was in that revision
 
387
        file_id = tree.inventory.path2id(file)
 
388
        if not file_id:
 
389
            bailout("%r is not present in revision %d" % (file, revno))
 
390
        tree.print_file(file_id)
 
391
        
298
392
 
299
393
    def remove(self, files, verbose=False):
300
394
        """Mark nominated files for removal from the inventory.
301
395
 
302
396
        This does not remove their text.  This does not run on 
303
397
 
304
 
        :todo: Refuse to remove modified files unless --force is given?
 
398
        TODO: Refuse to remove modified files unless --force is given?
305
399
 
306
400
        >>> b = ScratchBranch(files=['foo'])
307
401
        >>> b.add('foo')
325
419
        >>> b.working_tree().has_filename('foo') 
326
420
        True
327
421
 
328
 
        :todo: Do something useful with directories.
 
422
        TODO: Do something useful with directories.
329
423
 
330
 
        :todo: Should this remove the text or not?  Tough call; not
 
424
        TODO: Should this remove the text or not?  Tough call; not
331
425
        removing may be useful and the user can just use use rm, and
332
426
        is the opposite of add.  Removing it is consistent with most
333
427
        other tools.  Maybe an option.
334
428
        """
335
429
        ## TODO: Normalize names
336
430
        ## TODO: Remove nested loops; better scalability
 
431
        self._need_writelock()
337
432
 
338
433
        if isinstance(files, types.StringTypes):
339
434
            files = [files]
397
492
        be robust against files disappearing, moving, etc.  So the
398
493
        whole thing is a bit hard.
399
494
 
400
 
        :param timestamp: if not None, seconds-since-epoch for a
 
495
        timestamp -- if not None, seconds-since-epoch for a
401
496
             postdated/predated commit.
402
497
        """
 
498
        self._need_writelock()
403
499
 
404
500
        ## TODO: Show branch names
405
501
 
541
637
        ## TODO: Also calculate and store the inventory SHA1
542
638
        mutter("committing patch r%d" % (self.revno() + 1))
543
639
 
544
 
        mutter("append to revision-history")
545
 
        f = self.controlfile('revision-history', 'at')
546
 
        f.write(rev_id + '\n')
547
 
        f.close()
548
640
 
 
641
        self.append_revision(rev_id)
 
642
        
549
643
        if verbose:
550
644
            note("commited r%d" % self.revno())
551
645
 
552
646
 
 
647
    def append_revision(self, revision_id):
 
648
        mutter("add {%s} to revision-history" % revision_id)
 
649
        rev_history = self.revision_history()
 
650
 
 
651
        tmprhname = self.controlfilename('revision-history.tmp')
 
652
        rhname = self.controlfilename('revision-history')
 
653
        
 
654
        f = file(tmprhname, 'wt')
 
655
        rev_history.append(revision_id)
 
656
        f.write('\n'.join(rev_history))
 
657
        f.write('\n')
 
658
        f.close()
 
659
 
 
660
        if sys.platform == 'win32':
 
661
            os.remove(rhname)
 
662
        os.rename(tmprhname, rhname)
 
663
        
 
664
 
 
665
 
553
666
    def get_revision(self, revision_id):
554
667
        """Return the Revision object for a named revision"""
 
668
        self._need_readlock()
555
669
        r = Revision.read_xml(self.revision_store[revision_id])
556
670
        assert r.revision_id == revision_id
557
671
        return r
560
674
    def get_inventory(self, inventory_id):
561
675
        """Get Inventory object by hash.
562
676
 
563
 
        :todo: Perhaps for this and similar methods, take a revision
 
677
        TODO: Perhaps for this and similar methods, take a revision
564
678
               parameter which can be either an integer revno or a
565
679
               string hash."""
 
680
        self._need_readlock()
566
681
        i = Inventory.read_xml(self.inventory_store[inventory_id])
567
682
        return i
568
683
 
569
684
 
570
685
    def get_revision_inventory(self, revision_id):
571
686
        """Return inventory of a past revision."""
 
687
        self._need_readlock()
572
688
        if revision_id == None:
573
689
            return Inventory()
574
690
        else:
581
697
        >>> ScratchBranch().revision_history()
582
698
        []
583
699
        """
584
 
        return [chomp(l) for l in self.controlfile('revision-history').readlines()]
 
700
        self._need_readlock()
 
701
        return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()]
 
702
 
 
703
 
 
704
    def enum_history(self, direction):
 
705
        """Return (revno, revision_id) for history of branch.
 
706
 
 
707
        direction
 
708
            'forward' is from earliest to latest
 
709
            'reverse' is from latest to earliest
 
710
        """
 
711
        rh = self.revision_history()
 
712
        if direction == 'forward':
 
713
            i = 1
 
714
            for rid in rh:
 
715
                yield i, rid
 
716
                i += 1
 
717
        elif direction == 'reverse':
 
718
            i = len(rh)
 
719
            while i > 0:
 
720
                yield i, rh[i-1]
 
721
                i -= 1
 
722
        else:
 
723
            raise BzrError('invalid history direction %r' % direction)
585
724
 
586
725
 
587
726
    def revno(self):
609
748
        ph = self.revision_history()
610
749
        if ph:
611
750
            return ph[-1]
612
 
 
 
751
        else:
 
752
            return None
 
753
        
613
754
 
614
755
    def lookup_revision(self, revno):
615
756
        """Return revision hash for revision number."""
620
761
            # list is 0-based; revisions are 1-based
621
762
            return self.revision_history()[revno-1]
622
763
        except IndexError:
623
 
            bailout("no such revision %s" % revno)
 
764
            raise BzrError("no such revision %s" % revno)
624
765
 
625
766
 
626
767
    def revision_tree(self, revision_id):
628
769
 
629
770
        `revision_id` may be None for the null revision, in which case
630
771
        an `EmptyTree` is returned."""
631
 
 
 
772
        self._need_readlock()
632
773
        if revision_id == None:
633
774
            return EmptyTree()
634
775
        else:
664
805
 
665
806
 
666
807
 
667
 
    def write_log(self, show_timezone='original'):
668
 
        """Write out human-readable log of commits to this branch
669
 
 
670
 
        :param utc: If true, show dates in universal time, not local time."""
671
 
        ## TODO: Option to choose either original, utc or local timezone
672
 
        revno = 1
673
 
        precursor = None
674
 
        for p in self.revision_history():
675
 
            print '-' * 40
676
 
            print 'revno:', revno
677
 
            ## TODO: Show hash if --id is given.
678
 
            ##print 'revision-hash:', p
679
 
            rev = self.get_revision(p)
680
 
            print 'committer:', rev.committer
681
 
            print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0,
682
 
                                                 show_timezone))
683
 
 
684
 
            ## opportunistic consistency check, same as check_patch_chaining
685
 
            if rev.precursor != precursor:
686
 
                bailout("mismatched precursor!")
687
 
 
688
 
            print 'message:'
689
 
            if not rev.message:
690
 
                print '  (no message)'
691
 
            else:
692
 
                for l in rev.message.split('\n'):
693
 
                    print '  ' + l
694
 
 
695
 
            revno += 1
696
 
            precursor = p
697
 
 
698
 
 
699
 
 
700
 
    def show_status(branch, show_all=False):
 
808
    def rename_one(self, from_rel, to_rel):
 
809
        """Rename one file.
 
810
 
 
811
        This can change the directory or the filename or both.
 
812
        """
 
813
        self._need_writelock()
 
814
        tree = self.working_tree()
 
815
        inv = tree.inventory
 
816
        if not tree.has_filename(from_rel):
 
817
            bailout("can't rename: old working file %r does not exist" % from_rel)
 
818
        if tree.has_filename(to_rel):
 
819
            bailout("can't rename: new working file %r already exists" % to_rel)
 
820
            
 
821
        file_id = inv.path2id(from_rel)
 
822
        if file_id == None:
 
823
            bailout("can't rename: old name %r is not versioned" % from_rel)
 
824
 
 
825
        if inv.path2id(to_rel):
 
826
            bailout("can't rename: new name %r is already versioned" % to_rel)
 
827
 
 
828
        to_dir, to_tail = os.path.split(to_rel)
 
829
        to_dir_id = inv.path2id(to_dir)
 
830
        if to_dir_id == None and to_dir != '':
 
831
            bailout("can't determine destination directory id for %r" % to_dir)
 
832
 
 
833
        mutter("rename_one:")
 
834
        mutter("  file_id    {%s}" % file_id)
 
835
        mutter("  from_rel   %r" % from_rel)
 
836
        mutter("  to_rel     %r" % to_rel)
 
837
        mutter("  to_dir     %r" % to_dir)
 
838
        mutter("  to_dir_id  {%s}" % to_dir_id)
 
839
            
 
840
        inv.rename(file_id, to_dir_id, to_tail)
 
841
 
 
842
        print "%s => %s" % (from_rel, to_rel)
 
843
        
 
844
        from_abs = self.abspath(from_rel)
 
845
        to_abs = self.abspath(to_rel)
 
846
        try:
 
847
            os.rename(from_abs, to_abs)
 
848
        except OSError, e:
 
849
            bailout("failed to rename %r to %r: %s"
 
850
                    % (from_abs, to_abs, e[1]),
 
851
                    ["rename rolled back"])
 
852
 
 
853
        self._write_inventory(inv)
 
854
            
 
855
 
 
856
 
 
857
    def move(self, from_paths, to_name):
 
858
        """Rename files.
 
859
 
 
860
        to_name must exist as a versioned directory.
 
861
 
 
862
        If to_name exists and is a directory, the files are moved into
 
863
        it, keeping their old names.  If it is a directory, 
 
864
 
 
865
        Note that to_name is only the last component of the new name;
 
866
        this doesn't change the directory.
 
867
        """
 
868
        self._need_writelock()
 
869
        ## TODO: Option to move IDs only
 
870
        assert not isinstance(from_paths, basestring)
 
871
        tree = self.working_tree()
 
872
        inv = tree.inventory
 
873
        to_abs = self.abspath(to_name)
 
874
        if not isdir(to_abs):
 
875
            bailout("destination %r is not a directory" % to_abs)
 
876
        if not tree.has_filename(to_name):
 
877
            bailout("destination %r not in working directory" % to_abs)
 
878
        to_dir_id = inv.path2id(to_name)
 
879
        if to_dir_id == None and to_name != '':
 
880
            bailout("destination %r is not a versioned directory" % to_name)
 
881
        to_dir_ie = inv[to_dir_id]
 
882
        if to_dir_ie.kind not in ('directory', 'root_directory'):
 
883
            bailout("destination %r is not a directory" % to_abs)
 
884
 
 
885
        to_idpath = Set(inv.get_idpath(to_dir_id))
 
886
 
 
887
        for f in from_paths:
 
888
            if not tree.has_filename(f):
 
889
                bailout("%r does not exist in working tree" % f)
 
890
            f_id = inv.path2id(f)
 
891
            if f_id == None:
 
892
                bailout("%r is not versioned" % f)
 
893
            name_tail = splitpath(f)[-1]
 
894
            dest_path = appendpath(to_name, name_tail)
 
895
            if tree.has_filename(dest_path):
 
896
                bailout("destination %r already exists" % dest_path)
 
897
            if f_id in to_idpath:
 
898
                bailout("can't move %r to a subdirectory of itself" % f)
 
899
 
 
900
        # OK, so there's a race here, it's possible that someone will
 
901
        # create a file in this interval and then the rename might be
 
902
        # left half-done.  But we should have caught most problems.
 
903
 
 
904
        for f in from_paths:
 
905
            name_tail = splitpath(f)[-1]
 
906
            dest_path = appendpath(to_name, name_tail)
 
907
            print "%s => %s" % (f, dest_path)
 
908
            inv.rename(inv.path2id(f), to_dir_id, name_tail)
 
909
            try:
 
910
                os.rename(self.abspath(f), self.abspath(dest_path))
 
911
            except OSError, e:
 
912
                bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
 
913
                        ["rename rolled back"])
 
914
 
 
915
        self._write_inventory(inv)
 
916
 
 
917
 
 
918
 
 
919
    def show_status(self, show_all=False, file_list=None):
701
920
        """Display single-line status for non-ignored working files.
702
921
 
703
922
        The list is show sorted in order by file name.
713
932
        >>> os.unlink(b.abspath('foo'))
714
933
        >>> b.show_status()
715
934
        D       foo
716
 
        
717
 
 
718
 
        :todo: Get state for single files.
719
 
 
720
 
        :todo: Perhaps show a slash at the end of directory names.        
721
 
 
722
935
        """
 
936
        self._need_readlock()
723
937
 
724
938
        # We have to build everything into a list first so that it can
725
939
        # sorted by name, incorporating all the different sources.
730
944
        # Interesting case: the old ID for a file has been removed,
731
945
        # but a new file has been created under that name.
732
946
 
733
 
        old = branch.basis_tree()
734
 
        old_inv = old.inventory
735
 
        new = branch.working_tree()
736
 
        new_inv = new.inventory
737
 
 
738
 
        for fs, fid, oldname, newname, kind in diff_trees(old, new):
 
947
        old = self.basis_tree()
 
948
        new = self.working_tree()
 
949
 
 
950
        items = diff_trees(old, new)
 
951
        # We want to filter out only if any file was provided in the file_list.
 
952
        if isinstance(file_list, list) and len(file_list):
 
953
            items = [item for item in items if item[3] in file_list]
 
954
 
 
955
        for fs, fid, oldname, newname, kind in items:
739
956
            if fs == 'R':
740
957
                show_status(fs, kind,
741
958
                            oldname + ' => ' + newname)
752
969
            elif fs == '?':
753
970
                show_status(fs, kind, newname)
754
971
            else:
755
 
                bailout("wierd file state %r" % ((fs, fid),))
 
972
                bailout("weird file state %r" % ((fs, fid),))
756
973
                
757
974
 
758
975
 
763
980
    >>> isdir(b.base)
764
981
    True
765
982
    >>> bd = b.base
766
 
    >>> del b
 
983
    >>> b.destroy()
767
984
    >>> isdir(bd)
768
985
    False
769
986
    """
770
 
    def __init__(self, files = []):
 
987
    def __init__(self, files=[], dirs=[]):
771
988
        """Make a test branch.
772
989
 
773
990
        This creates a temporary directory and runs init-tree in it.
775
992
        If any files are listed, they are created in the working copy.
776
993
        """
777
994
        Branch.__init__(self, tempfile.mkdtemp(), init=True)
 
995
        for d in dirs:
 
996
            os.mkdir(self.abspath(d))
 
997
            
778
998
        for f in files:
779
999
            file(os.path.join(self.base, f), 'w').write('content of %s' % f)
780
1000
 
781
1001
 
782
1002
    def __del__(self):
 
1003
        self.destroy()
 
1004
 
 
1005
    def destroy(self):
783
1006
        """Destroy the test branch, removing the scratch directory."""
784
 
        shutil.rmtree(self.base)
 
1007
        try:
 
1008
            mutter("delete ScratchBranch %s" % self.base)
 
1009
            shutil.rmtree(self.base)
 
1010
        except OSError, e:
 
1011
            # Work around for shutil.rmtree failing on Windows when
 
1012
            # readonly files are encountered
 
1013
            mutter("hit exception in destroying ScratchBranch: %s" % e)
 
1014
            for root, dirs, files in os.walk(self.base, topdown=False):
 
1015
                for name in files:
 
1016
                    os.chmod(os.path.join(root, name), 0700)
 
1017
            shutil.rmtree(self.base)
 
1018
        self.base = None
785
1019
 
786
1020
    
787
1021
 
820
1054
    idx = name.rfind('/')
821
1055
    if idx != -1:
822
1056
        name = name[idx+1 : ]
 
1057
    idx = name.rfind('\\')
 
1058
    if idx != -1:
 
1059
        name = name[idx+1 : ]
823
1060
 
824
1061
    name = name.lstrip('.')
825
1062
 
826
1063
    s = hexlify(rand_bytes(8))
827
1064
    return '-'.join((name, compact_date(time.time()), s))
828
 
 
829