~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: mbp at sourcefrog
  • Date: 2005-03-25 03:24:47 UTC
  • Revision ID: mbp@sourcefrog.net-20050325032447-aef4d334620f490a
- don't flush the debug log file so often

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
 
     joinpath, sha_string, file_kind, local_time_offset, appendpath
 
31
     joinpath, sha_string, file_kind, local_time_offset
32
32
from store import ImmutableStore
33
33
from revision import Revision
34
 
from errors import bailout, BzrError
 
34
from errors import bailout
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 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
59
49
    run into the root."""
60
 
    if f == None:
 
50
    if f is None:
61
51
        f = os.getcwd()
62
52
    elif hasattr(os.path, 'realpath'):
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
 
 
59
    last_f = f
72
60
    while True:
73
61
        if os.path.exists(os.path.join(f, bzrlib.BZRDIR)):
74
62
            return f
75
63
        head, tail = os.path.split(f)
76
64
        if head == f:
77
65
            # reached the root, whatever that may be
78
 
            raise BzrError('%r is not in a branch' % orig_f)
 
66
            bailout('%r is not in a branch' % orig_f)
79
67
        f = head
80
68
    
81
69
 
86
74
class Branch:
87
75
    """Branch holding a history of revisions.
88
76
 
89
 
    base
90
 
        Base directory of the branch.
 
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.
91
89
    """
92
 
    _lockmode = None
93
 
    
94
 
    def __init__(self, base, init=False, find_root=True, lock_mode='w'):
 
90
    def __init__(self, base, init=False, find_root=True):
95
91
        """Create new branch object at a particular location.
96
92
 
97
 
        base -- Base directory for the branch.
 
93
        :param base: Base directory for the branch.
98
94
        
99
 
        init -- If True, create new control files in a previously
 
95
        :param init: If True, create new control files in a previously
100
96
             unversioned directory.  If False, the branch must already
101
97
             be versioned.
102
98
 
103
 
        find_root -- If true and init is false, find the root of the
 
99
        :param find_root: If true and init is false, find the root of the
104
100
             existing branch containing base.
105
101
 
106
102
        In the test suite, creation of new trees is tested using the
118
114
                        ['use "bzr init" to initialize a new working tree',
119
115
                         'current bzr can only operate from top-of-tree'])
120
116
        self._check_format()
121
 
        self.lock(lock_mode)
122
117
 
123
118
        self.text_store = ImmutableStore(self.controlfilename('text-store'))
124
119
        self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
132
127
    __repr__ = __str__
133
128
 
134
129
 
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
130
    def abspath(self, name):
185
131
        """Return absolute filename for something in the branch"""
186
132
        return os.path.join(self.base, name)
207
153
 
208
154
 
209
155
    def controlfile(self, file_or_path, mode='r'):
210
 
        """Open a control file for this branch.
211
 
 
212
 
        There are two classes of file in the control directory: text
213
 
        and binary.  binary files are untranslated byte streams.  Text
214
 
        control files are stored with Unix newlines and in UTF-8, even
215
 
        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
 
        """
220
 
 
221
 
        fn = self.controlfilename(file_or_path)
222
 
 
223
 
        if mode == 'rb' or mode == 'wb':
224
 
            return file(fn, mode)
225
 
        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.
228
 
            import codecs
229
 
            return codecs.open(fn, mode + 'b', 'utf-8',
230
 
                               buffering=60000)
231
 
        else:
232
 
            raise BzrError("invalid controlfile mode %r" % mode)
233
 
 
 
156
        """Open a control file for this branch"""
 
157
        return file(self.controlfilename(file_or_path), mode)
234
158
 
235
159
 
236
160
    def _make_control(self):
242
166
        for d in ('text-store', 'inventory-store', 'revision-store'):
243
167
            os.mkdir(self.controlfilename(d))
244
168
        for f in ('revision-history', 'merged-patches',
245
 
                  'pending-merged-patches', 'branch-name',
246
 
                  'branch-lock'):
 
169
                  'pending-merged-patches', 'branch-name'):
247
170
            self.controlfile(f, 'w').write('')
248
171
        mutter('created control directory in ' + self.base)
249
172
        Inventory().write_xml(self.controlfile('inventory','w'))
256
179
 
257
180
        In the future, we might need different in-memory Branch
258
181
        classes to support downlevel branches.  But not yet.
259
 
        """
260
 
        # This ignores newlines so that we can open branches created
261
 
        # on Windows from Linux and so on.  I think it might be better
262
 
        # to always make all internal files in unix format.
263
 
        fmt = self.controlfile('branch-format', 'r').read()
264
 
        fmt.replace('\r\n', '')
 
182
        """        
 
183
        # read in binary mode to detect newline wierdness.
 
184
        fmt = self.controlfile('branch-format', 'rb').read()
265
185
        if fmt != BZR_BRANCH_FORMAT:
266
186
            bailout('sorry, branch format %r not supported' % fmt,
267
187
                    ['use a different bzr version',
270
190
 
271
191
    def read_working_inventory(self):
272
192
        """Read the working inventory."""
273
 
        self._need_readlock()
274
193
        before = time.time()
275
 
        # ElementTree does its own conversion from UTF-8, so open in
276
 
        # binary.
277
 
        inv = Inventory.read_xml(self.controlfile('inventory', 'rb'))
 
194
        inv = Inventory.read_xml(self.controlfile('inventory', 'r'))
278
195
        mutter("loaded inventory of %d items in %f"
279
196
               % (len(inv), time.time() - before))
280
197
        return inv
286
203
        That is to say, the inventory describing changes underway, that
287
204
        will be committed to the next revision.
288
205
        """
289
 
        self._need_writelock()
290
206
        ## TODO: factor out to atomicfile?  is rename safe on windows?
291
207
        ## TODO: Maybe some kind of clean/dirty marker on inventory?
292
208
        tmpfname = self.controlfilename('inventory.tmp')
293
 
        tmpf = file(tmpfname, 'wb')
 
209
        tmpf = file(tmpfname, 'w')
294
210
        inv.write_xml(tmpf)
295
211
        tmpf.close()
296
 
        inv_fname = self.controlfilename('inventory')
297
 
        if sys.platform == 'win32':
298
 
            os.remove(inv_fname)
299
 
        os.rename(tmpfname, inv_fname)
 
212
        os.rename(tmpfname, self.controlfilename('inventory'))
300
213
        mutter('wrote working inventory')
301
214
 
302
215
 
307
220
    def add(self, files, verbose=False):
308
221
        """Make files versioned.
309
222
 
310
 
        Note that the command line normally calls smart_add instead.
311
 
 
312
223
        This puts the files in the Added state, so that they will be
313
224
        recorded by the next commit.
314
225
 
315
 
        TODO: Perhaps have an option to add the ids even if the files do
 
226
        :todo: Perhaps have an option to add the ids even if the files do
316
227
               not (yet) exist.
317
228
 
318
 
        TODO: Perhaps return the ids of the files?  But then again it
 
229
        :todo: Perhaps return the ids of the files?  But then again it
319
230
               is easy to retrieve them if they're needed.
320
231
 
321
 
        TODO: Option to specify file id.
 
232
        :todo: Option to specify file id.
322
233
 
323
 
        TODO: Adding a directory should optionally recurse down and
 
234
        :todo: Adding a directory should optionally recurse down and
324
235
               add all non-ignored children.  Perhaps do that in a
325
236
               higher-level method.
326
237
 
346
257
        Traceback (most recent call last):
347
258
        BzrError: ('cannot add: not a regular file or directory: nothere', [])
348
259
        """
349
 
        self._need_writelock()
350
260
 
351
261
        # TODO: Re-adding a file that is removed in the working copy
352
262
        # should probably put it back with the previous ID.
385
295
        self._write_inventory(inv)
386
296
 
387
297
 
388
 
    def print_file(self, file, revno):
389
 
        """Print `file` to stdout."""
390
 
        self._need_readlock()
391
 
        tree = self.revision_tree(self.lookup_revision(revno))
392
 
        # use inventory as it was in that revision
393
 
        file_id = tree.inventory.path2id(file)
394
 
        if not file_id:
395
 
            bailout("%r is not present in revision %d" % (file, revno))
396
 
        tree.print_file(file_id)
397
 
        
398
298
 
399
299
    def remove(self, files, verbose=False):
400
300
        """Mark nominated files for removal from the inventory.
401
301
 
402
302
        This does not remove their text.  This does not run on 
403
303
 
404
 
        TODO: Refuse to remove modified files unless --force is given?
 
304
        :todo: Refuse to remove modified files unless --force is given?
405
305
 
406
306
        >>> b = ScratchBranch(files=['foo'])
407
307
        >>> b.add('foo')
425
325
        >>> b.working_tree().has_filename('foo') 
426
326
        True
427
327
 
428
 
        TODO: Do something useful with directories.
 
328
        :todo: Do something useful with directories.
429
329
 
430
 
        TODO: Should this remove the text or not?  Tough call; not
 
330
        :todo: Should this remove the text or not?  Tough call; not
431
331
        removing may be useful and the user can just use use rm, and
432
332
        is the opposite of add.  Removing it is consistent with most
433
333
        other tools.  Maybe an option.
434
334
        """
435
335
        ## TODO: Normalize names
436
336
        ## TODO: Remove nested loops; better scalability
437
 
        self._need_writelock()
438
337
 
439
338
        if isinstance(files, types.StringTypes):
440
339
            files = [files]
498
397
        be robust against files disappearing, moving, etc.  So the
499
398
        whole thing is a bit hard.
500
399
 
501
 
        timestamp -- if not None, seconds-since-epoch for a
 
400
        :param timestamp: if not None, seconds-since-epoch for a
502
401
             postdated/predated commit.
503
402
        """
504
 
        self._need_writelock()
505
403
 
506
404
        ## TODO: Show branch names
507
405
 
582
480
                            state = 'A'
583
481
                        elif (old_ie.name == entry.name
584
482
                              and old_ie.parent_id == entry.parent_id):
 
483
                            state = 'R'
 
484
                        else:
585
485
                            state = 'M'
586
 
                        else:
587
 
                            state = 'R'
588
486
 
589
487
                        show_status(state, entry.kind, quotefn(path))
590
488
 
643
541
        ## TODO: Also calculate and store the inventory SHA1
644
542
        mutter("committing patch r%d" % (self.revno() + 1))
645
543
 
646
 
 
647
 
        self.append_revision(rev_id)
648
 
        
649
 
        if verbose:
650
 
            note("commited r%d" % self.revno())
651
 
 
652
 
 
653
 
    def append_revision(self, revision_id):
654
 
        mutter("add {%s} to revision-history" % revision_id)
655
 
        rev_history = self.revision_history()
656
 
 
657
 
        tmprhname = self.controlfilename('revision-history.tmp')
658
 
        rhname = self.controlfilename('revision-history')
659
 
        
660
 
        f = file(tmprhname, 'wt')
661
 
        rev_history.append(revision_id)
662
 
        f.write('\n'.join(rev_history))
663
 
        f.write('\n')
664
 
        f.close()
665
 
 
666
 
        if sys.platform == 'win32':
667
 
            os.remove(rhname)
668
 
        os.rename(tmprhname, rhname)
669
 
        
 
544
        mutter("append to revision-history")
 
545
        self.controlfile('revision-history', 'at').write(rev_id + '\n')
 
546
 
 
547
        mutter("done!")
670
548
 
671
549
 
672
550
    def get_revision(self, revision_id):
673
551
        """Return the Revision object for a named revision"""
674
 
        self._need_readlock()
675
552
        r = Revision.read_xml(self.revision_store[revision_id])
676
553
        assert r.revision_id == revision_id
677
554
        return r
680
557
    def get_inventory(self, inventory_id):
681
558
        """Get Inventory object by hash.
682
559
 
683
 
        TODO: Perhaps for this and similar methods, take a revision
 
560
        :todo: Perhaps for this and similar methods, take a revision
684
561
               parameter which can be either an integer revno or a
685
562
               string hash."""
686
 
        self._need_readlock()
687
563
        i = Inventory.read_xml(self.inventory_store[inventory_id])
688
564
        return i
689
565
 
690
566
 
691
567
    def get_revision_inventory(self, revision_id):
692
568
        """Return inventory of a past revision."""
693
 
        self._need_readlock()
694
569
        if revision_id == None:
695
570
            return Inventory()
696
571
        else:
703
578
        >>> ScratchBranch().revision_history()
704
579
        []
705
580
        """
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)
 
581
        return [chomp(l) for l in self.controlfile('revision-history').readlines()]
730
582
 
731
583
 
732
584
    def revno(self):
754
606
        ph = self.revision_history()
755
607
        if ph:
756
608
            return ph[-1]
757
 
        else:
758
 
            return None
759
 
        
 
609
 
760
610
 
761
611
    def lookup_revision(self, revno):
762
612
        """Return revision hash for revision number."""
767
617
            # list is 0-based; revisions are 1-based
768
618
            return self.revision_history()[revno-1]
769
619
        except IndexError:
770
 
            raise BzrError("no such revision %s" % revno)
 
620
            bailout("no such revision %s" % revno)
771
621
 
772
622
 
773
623
    def revision_tree(self, revision_id):
775
625
 
776
626
        `revision_id` may be None for the null revision, in which case
777
627
        an `EmptyTree` is returned."""
778
 
        self._need_readlock()
 
628
 
779
629
        if revision_id == None:
780
630
            return EmptyTree()
781
631
        else:
785
635
 
786
636
    def working_tree(self):
787
637
        """Return a `Tree` for the working copy."""
788
 
        from workingtree import WorkingTree
789
638
        return WorkingTree(self.base, self.read_working_inventory())
790
639
 
791
640
 
812
661
 
813
662
 
814
663
 
815
 
    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
 
        tree = self.working_tree()
822
 
        inv = tree.inventory
823
 
        if not tree.has_filename(from_rel):
824
 
            bailout("can't rename: old working file %r does not exist" % from_rel)
825
 
        if tree.has_filename(to_rel):
826
 
            bailout("can't rename: new working file %r already exists" % to_rel)
827
 
            
828
 
        file_id = inv.path2id(from_rel)
829
 
        if file_id == None:
830
 
            bailout("can't rename: old name %r is not versioned" % from_rel)
831
 
 
832
 
        if inv.path2id(to_rel):
833
 
            bailout("can't rename: new name %r is already versioned" % to_rel)
834
 
 
835
 
        to_dir, to_tail = os.path.split(to_rel)
836
 
        to_dir_id = inv.path2id(to_dir)
837
 
        if to_dir_id == None and to_dir != '':
838
 
            bailout("can't determine destination directory id for %r" % to_dir)
839
 
 
840
 
        mutter("rename_one:")
841
 
        mutter("  file_id    {%s}" % file_id)
842
 
        mutter("  from_rel   %r" % from_rel)
843
 
        mutter("  to_rel     %r" % to_rel)
844
 
        mutter("  to_dir     %r" % to_dir)
845
 
        mutter("  to_dir_id  {%s}" % to_dir_id)
846
 
            
847
 
        inv.rename(file_id, to_dir_id, to_tail)
848
 
 
849
 
        print "%s => %s" % (from_rel, to_rel)
 
664
    def write_log(self, show_timezone='original'):
 
665
        """Write out human-readable log of commits to this branch
 
666
 
 
667
        :param utc: If true, show dates in universal time, not local time."""
 
668
        ## TODO: Option to choose either original, utc or local timezone
 
669
        revno = 1
 
670
        precursor = None
 
671
        for p in self.revision_history():
 
672
            print '-' * 40
 
673
            print 'revno:', revno
 
674
            ## TODO: Show hash if --id is given.
 
675
            ##print 'revision-hash:', p
 
676
            rev = self.get_revision(p)
 
677
            print 'committer:', rev.committer
 
678
            print 'timestamp: %s' % (format_date(rev.timestamp, rev.timezone or 0,
 
679
                                                 show_timezone))
 
680
 
 
681
            ## opportunistic consistency check, same as check_patch_chaining
 
682
            if rev.precursor != precursor:
 
683
                bailout("mismatched precursor!")
 
684
 
 
685
            print 'message:'
 
686
            if not rev.message:
 
687
                print '  (no message)'
 
688
            else:
 
689
                for l in rev.message.split('\n'):
 
690
                    print '  ' + l
 
691
 
 
692
            revno += 1
 
693
            precursor = p
 
694
 
 
695
 
 
696
 
 
697
    def show_status(branch, show_all=False):
 
698
        """Display single-line status for non-ignored working files.
 
699
 
 
700
        The list is show sorted in order by file name.
 
701
 
 
702
        >>> b = ScratchBranch(files=['foo', 'foo~'])
 
703
        >>> b.show_status()
 
704
        ?       foo
 
705
        >>> b.add('foo')
 
706
        >>> b.show_status()
 
707
        A       foo
 
708
        >>> b.commit("add foo")
 
709
        >>> b.show_status()
 
710
        >>> os.unlink(b.abspath('foo'))
 
711
        >>> b.show_status()
 
712
        D       foo
850
713
        
851
 
        from_abs = self.abspath(from_rel)
852
 
        to_abs = self.abspath(to_rel)
853
 
        try:
854
 
            os.rename(from_abs, to_abs)
855
 
        except OSError, e:
856
 
            bailout("failed to rename %r to %r: %s"
857
 
                    % (from_abs, to_abs, e[1]),
858
 
                    ["rename rolled back"])
859
 
 
860
 
        self._write_inventory(inv)
861
 
            
862
 
 
863
 
 
864
 
    def move(self, from_paths, to_name):
865
 
        """Rename files.
866
 
 
867
 
        to_name must exist as a versioned directory.
868
 
 
869
 
        If to_name exists and is a directory, the files are moved into
870
 
        it, keeping their old names.  If it is a directory, 
871
 
 
872
 
        Note that to_name is only the last component of the new name;
873
 
        this doesn't change the directory.
 
714
 
 
715
        :todo: Get state for single files.
 
716
 
 
717
        :todo: Perhaps show a slash at the end of directory names.        
 
718
 
874
719
        """
875
 
        self._need_writelock()
876
 
        ## TODO: Option to move IDs only
877
 
        assert not isinstance(from_paths, basestring)
878
 
        tree = self.working_tree()
879
 
        inv = tree.inventory
880
 
        to_abs = self.abspath(to_name)
881
 
        if not isdir(to_abs):
882
 
            bailout("destination %r is not a directory" % to_abs)
883
 
        if not tree.has_filename(to_name):
884
 
            bailout("destination %r not in working directory" % to_abs)
885
 
        to_dir_id = inv.path2id(to_name)
886
 
        if to_dir_id == None and to_name != '':
887
 
            bailout("destination %r is not a versioned directory" % to_name)
888
 
        to_dir_ie = inv[to_dir_id]
889
 
        if to_dir_ie.kind not in ('directory', 'root_directory'):
890
 
            bailout("destination %r is not a directory" % to_abs)
891
 
 
892
 
        to_idpath = Set(inv.get_idpath(to_dir_id))
893
 
 
894
 
        for f in from_paths:
895
 
            if not tree.has_filename(f):
896
 
                bailout("%r does not exist in working tree" % f)
897
 
            f_id = inv.path2id(f)
898
 
            if f_id == None:
899
 
                bailout("%r is not versioned" % f)
900
 
            name_tail = splitpath(f)[-1]
901
 
            dest_path = appendpath(to_name, name_tail)
902
 
            if tree.has_filename(dest_path):
903
 
                bailout("destination %r already exists" % dest_path)
904
 
            if f_id in to_idpath:
905
 
                bailout("can't move %r to a subdirectory of itself" % f)
906
 
 
907
 
        # OK, so there's a race here, it's possible that someone will
908
 
        # create a file in this interval and then the rename might be
909
 
        # left half-done.  But we should have caught most problems.
910
 
 
911
 
        for f in from_paths:
912
 
            name_tail = splitpath(f)[-1]
913
 
            dest_path = appendpath(to_name, name_tail)
914
 
            print "%s => %s" % (f, dest_path)
915
 
            inv.rename(inv.path2id(f), to_dir_id, name_tail)
916
 
            try:
917
 
                os.rename(self.abspath(f), self.abspath(dest_path))
918
 
            except OSError, e:
919
 
                bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
920
 
                        ["rename rolled back"])
921
 
 
922
 
        self._write_inventory(inv)
923
 
 
924
 
 
 
720
 
 
721
        # We have to build everything into a list first so that it can
 
722
        # sorted by name, incorporating all the different sources.
 
723
 
 
724
        # FIXME: Rather than getting things in random order and then sorting,
 
725
        # just step through in order.
 
726
 
 
727
        # Interesting case: the old ID for a file has been removed,
 
728
        # but a new file has been created under that name.
 
729
 
 
730
        old = branch.basis_tree()
 
731
        old_inv = old.inventory
 
732
        new = branch.working_tree()
 
733
        new_inv = new.inventory
 
734
 
 
735
        for fs, fid, oldname, newname, kind in diff_trees(old, new):
 
736
            if fs == 'R':
 
737
                show_status(fs, kind,
 
738
                            oldname + ' => ' + newname)
 
739
            elif fs == 'A' or fs == 'M':
 
740
                show_status(fs, kind, newname)
 
741
            elif fs == 'D':
 
742
                show_status(fs, kind, oldname)
 
743
            elif fs == '.':
 
744
                if show_all:
 
745
                    show_status(fs, kind, newname)
 
746
            elif fs == 'I':
 
747
                if show_all:
 
748
                    show_status(fs, kind, newname)
 
749
            elif fs == '?':
 
750
                show_status(fs, kind, newname)
 
751
            else:
 
752
                bailout("wierd file state %r" % ((fs, fid),))
 
753
                
925
754
 
926
755
 
927
756
class ScratchBranch(Branch):
931
760
    >>> isdir(b.base)
932
761
    True
933
762
    >>> bd = b.base
934
 
    >>> b.destroy()
 
763
    >>> del b
935
764
    >>> isdir(bd)
936
765
    False
937
766
    """
938
 
    def __init__(self, files=[], dirs=[]):
 
767
    def __init__(self, files = []):
939
768
        """Make a test branch.
940
769
 
941
770
        This creates a temporary directory and runs init-tree in it.
943
772
        If any files are listed, they are created in the working copy.
944
773
        """
945
774
        Branch.__init__(self, tempfile.mkdtemp(), init=True)
946
 
        for d in dirs:
947
 
            os.mkdir(self.abspath(d))
948
 
            
949
775
        for f in files:
950
776
            file(os.path.join(self.base, f), 'w').write('content of %s' % f)
951
777
 
952
778
 
953
779
    def __del__(self):
954
 
        self.destroy()
955
 
 
956
 
    def destroy(self):
957
780
        """Destroy the test branch, removing the scratch directory."""
958
 
        try:
959
 
            mutter("delete ScratchBranch %s" % self.base)
960
 
            shutil.rmtree(self.base)
961
 
        except OSError, e:
962
 
            # Work around for shutil.rmtree failing on Windows when
963
 
            # readonly files are encountered
964
 
            mutter("hit exception in destroying ScratchBranch: %s" % e)
965
 
            for root, dirs, files in os.walk(self.base, topdown=False):
966
 
                for name in files:
967
 
                    os.chmod(os.path.join(root, name), 0700)
968
 
            shutil.rmtree(self.base)
969
 
        self.base = None
 
781
        shutil.rmtree(self.base)
970
782
 
971
783
    
972
784
 
1005
817
    idx = name.rfind('/')
1006
818
    if idx != -1:
1007
819
        name = name[idx+1 : ]
1008
 
    idx = name.rfind('\\')
1009
 
    if idx != -1:
1010
 
        name = name[idx+1 : ]
1011
820
 
1012
821
    name = name.lstrip('.')
1013
822
 
1014
823
    s = hexlify(rand_bytes(8))
1015
824
    return '-'.join((name, compact_date(time.time()), s))
 
825
 
 
826