~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Martin Pool
  • Date: 2005-05-31 08:10:44 UTC
  • Revision ID: mbp@sourcefrog.net-20050531081044-0f6d28e39b8e19de
- replace Branch.lock(mode) with separate lock_read and lock_write 
  methods

Show diffs side-by-side

added added

removed removed

Lines of Context:
46
46
 
47
47
 
48
48
 
 
49
def with_writelock(method):
 
50
    """Method decorator for functions run with the branch locked."""
 
51
    def d(self, *a, **k):
 
52
        # called with self set to the branch
 
53
        self.lock_write()
 
54
        try:
 
55
            return method(self, *a, **k)
 
56
        finally:
 
57
            self.unlock()
 
58
    return d
 
59
 
 
60
 
 
61
def with_readlock(method):
 
62
    def d(self, *a, **k):
 
63
        self.lock_read()
 
64
        try:
 
65
            return method(self, *a, **k)
 
66
        finally:
 
67
            self.unlock()
 
68
    return d
 
69
 
 
70
 
49
71
def _relpath(base, path):
50
72
    """Return path relative to base, or raise exception.
51
73
 
104
126
            raise BzrError('%r is not in a branch' % orig_f)
105
127
        f = head
106
128
    
107
 
class DivergedBranches(Exception):
108
 
    def __init__(self, branch1, branch2):
109
 
        self.branch1 = branch1
110
 
        self.branch2 = branch2
111
 
        Exception.__init__(self, "These branches have diverged.")
 
129
 
112
130
 
113
131
######################################################################
114
132
# branch objects
126
144
        If _lock_mode is true, a positive count of the number of times the
127
145
        lock has been taken.
128
146
 
129
 
    _lock
130
 
        Lock object from bzrlib.lock.
 
147
    _lockfile
 
148
        Open file used for locking.
131
149
    """
132
150
    base = None
133
151
    _lock_mode = None
134
152
    _lock_count = None
135
 
    _lock = None
136
153
    
137
154
    def __init__(self, base, init=False, find_root=True):
138
155
        """Create new branch object at a particular location.
162
179
                                     ['use "bzr init" to initialize a new working tree',
163
180
                                      'current bzr can only operate from top-of-tree'])
164
181
        self._check_format()
 
182
        self._lockfile = self.controlfile('branch-lock', 'wb')
165
183
 
166
184
        self.text_store = ImmutableStore(self.controlfilename('text-store'))
167
185
        self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
176
194
 
177
195
 
178
196
    def __del__(self):
179
 
        if self._lock_mode or self._lock:
 
197
        if self._lock_mode:
180
198
            from warnings import warn
181
199
            warn("branch %r was not explicitly unlocked" % self)
182
 
            self._lock.unlock()
 
200
            self.unlock()
183
201
 
184
202
 
185
203
 
191
209
                                self._lock_mode)
192
210
            self._lock_count += 1
193
211
        else:
194
 
            from bzrlib.lock import WriteLock
 
212
            from bzrlib.lock import lock, LOCK_EX
195
213
 
196
 
            self._lock = WriteLock(self.controlfilename('branch-lock'))
 
214
            lock(self._lockfile, LOCK_EX)
197
215
            self._lock_mode = 'w'
198
216
            self._lock_count = 1
199
217
 
205
223
                   "invalid lock mode %r" % self._lock_mode
206
224
            self._lock_count += 1
207
225
        else:
208
 
            from bzrlib.lock import ReadLock
 
226
            from bzrlib.lock import lock, LOCK_SH
209
227
 
210
 
            self._lock = ReadLock(self.controlfilename('branch-lock'))
 
228
            lock(self._lockfile, LOCK_SH)
211
229
            self._lock_mode = 'r'
212
230
            self._lock_count = 1
213
231
                        
221
239
        if self._lock_count > 1:
222
240
            self._lock_count -= 1
223
241
        else:
224
 
            self._lock.unlock()
225
 
            self._lock = None
 
242
            assert self._lock_count == 1
 
243
            from bzrlib.lock import unlock
 
244
            unlock(self._lockfile)
226
245
            self._lock_mode = self._lock_count = None
227
246
 
228
247
 
308
327
 
309
328
 
310
329
 
 
330
    @with_readlock
311
331
    def read_working_inventory(self):
312
332
        """Read the working inventory."""
313
333
        before = time.time()
314
334
        # ElementTree does its own conversion from UTF-8, so open in
315
335
        # binary.
316
 
        self.lock_read()
317
 
        try:
318
 
            inv = Inventory.read_xml(self.controlfile('inventory', 'rb'))
319
 
            mutter("loaded inventory of %d items in %f"
320
 
                   % (len(inv), time.time() - before))
321
 
            return inv
322
 
        finally:
323
 
            self.unlock()
 
336
        inv = Inventory.read_xml(self.controlfile('inventory', 'rb'))
 
337
        mutter("loaded inventory of %d items in %f"
 
338
               % (len(inv), time.time() - before))
 
339
        return inv
324
340
            
325
341
 
326
342
    def _write_inventory(self, inv):
346
362
                         """Inventory for the working copy.""")
347
363
 
348
364
 
 
365
    @with_writelock
349
366
    def add(self, files, verbose=False, ids=None):
350
367
        """Make files versioned.
351
368
 
385
402
        else:
386
403
            assert(len(ids) == len(files))
387
404
 
388
 
        self.lock_write()
389
 
        try:
390
 
            inv = self.read_working_inventory()
391
 
            for f,file_id in zip(files, ids):
392
 
                if is_control_file(f):
393
 
                    raise BzrError("cannot add control file %s" % quotefn(f))
394
 
 
395
 
                fp = splitpath(f)
396
 
 
397
 
                if len(fp) == 0:
398
 
                    raise BzrError("cannot add top-level %r" % f)
399
 
 
400
 
                fullpath = os.path.normpath(self.abspath(f))
401
 
 
402
 
                try:
403
 
                    kind = file_kind(fullpath)
404
 
                except OSError:
405
 
                    # maybe something better?
406
 
                    raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
407
 
 
408
 
                if kind != 'file' and kind != 'directory':
409
 
                    raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
410
 
 
411
 
                if file_id is None:
412
 
                    file_id = gen_file_id(f)
413
 
                inv.add_path(f, kind=kind, file_id=file_id)
414
 
 
415
 
                if verbose:
416
 
                    show_status('A', kind, quotefn(f))
417
 
 
418
 
                mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
419
 
 
420
 
            self._write_inventory(inv)
421
 
        finally:
422
 
            self.unlock()
 
405
        inv = self.read_working_inventory()
 
406
        for f,file_id in zip(files, ids):
 
407
            if is_control_file(f):
 
408
                raise BzrError("cannot add control file %s" % quotefn(f))
 
409
 
 
410
            fp = splitpath(f)
 
411
 
 
412
            if len(fp) == 0:
 
413
                raise BzrError("cannot add top-level %r" % f)
 
414
 
 
415
            fullpath = os.path.normpath(self.abspath(f))
 
416
 
 
417
            try:
 
418
                kind = file_kind(fullpath)
 
419
            except OSError:
 
420
                # maybe something better?
 
421
                raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
 
422
 
 
423
            if kind != 'file' and kind != 'directory':
 
424
                raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
 
425
 
 
426
            if file_id is None:
 
427
                file_id = gen_file_id(f)
 
428
            inv.add_path(f, kind=kind, file_id=file_id)
 
429
 
 
430
            if verbose:
 
431
                show_status('A', kind, quotefn(f))
 
432
 
 
433
            mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
 
434
 
 
435
        self._write_inventory(inv)
423
436
            
424
437
 
425
438
    def print_file(self, file, revno):
426
439
        """Print `file` to stdout."""
427
 
        self.lock_read()
428
 
        try:
429
 
            tree = self.revision_tree(self.lookup_revision(revno))
430
 
            # use inventory as it was in that revision
431
 
            file_id = tree.inventory.path2id(file)
432
 
            if not file_id:
433
 
                raise BzrError("%r is not present in revision %d" % (file, revno))
434
 
            tree.print_file(file_id)
435
 
        finally:
436
 
            self.unlock()
437
 
 
438
 
 
 
440
        tree = self.revision_tree(self.lookup_revision(revno))
 
441
        # use inventory as it was in that revision
 
442
        file_id = tree.inventory.path2id(file)
 
443
        if not file_id:
 
444
            raise BzrError("%r is not present in revision %d" % (file, revno))
 
445
        tree.print_file(file_id)
 
446
 
 
447
 
 
448
    @with_writelock
439
449
    def remove(self, files, verbose=False):
440
450
        """Mark nominated files for removal from the inventory.
441
451
 
455
465
        if isinstance(files, types.StringTypes):
456
466
            files = [files]
457
467
 
458
 
        self.lock_write()
459
 
 
460
 
        try:
461
 
            tree = self.working_tree()
462
 
            inv = tree.inventory
463
 
 
464
 
            # do this before any modifications
465
 
            for f in files:
466
 
                fid = inv.path2id(f)
467
 
                if not fid:
468
 
                    raise BzrError("cannot remove unversioned file %s" % quotefn(f))
469
 
                mutter("remove inventory entry %s {%s}" % (quotefn(f), fid))
470
 
                if verbose:
471
 
                    # having remove it, it must be either ignored or unknown
472
 
                    if tree.is_ignored(f):
473
 
                        new_status = 'I'
474
 
                    else:
475
 
                        new_status = '?'
476
 
                    show_status(new_status, inv[fid].kind, quotefn(f))
477
 
                del inv[fid]
478
 
 
479
 
            self._write_inventory(inv)
480
 
        finally:
481
 
            self.unlock()
482
 
 
483
 
 
484
 
    # FIXME: this doesn't need to be a branch method
 
468
        tree = self.working_tree()
 
469
        inv = tree.inventory
 
470
 
 
471
        # do this before any modifications
 
472
        for f in files:
 
473
            fid = inv.path2id(f)
 
474
            if not fid:
 
475
                raise BzrError("cannot remove unversioned file %s" % quotefn(f))
 
476
            mutter("remove inventory entry %s {%s}" % (quotefn(f), fid))
 
477
            if verbose:
 
478
                # having remove it, it must be either ignored or unknown
 
479
                if tree.is_ignored(f):
 
480
                    new_status = 'I'
 
481
                else:
 
482
                    new_status = '?'
 
483
                show_status(new_status, inv[fid].kind, quotefn(f))
 
484
            del inv[fid]
 
485
 
 
486
        self._write_inventory(inv)
 
487
 
 
488
 
485
489
    def set_inventory(self, new_inventory_list):
486
490
        inv = Inventory()
487
491
        for path, file_id, parent, kind in new_inventory_list:
555
559
            return self.get_inventory(self.get_revision(revision_id).inventory_id)
556
560
 
557
561
 
 
562
    @with_readlock
558
563
    def revision_history(self):
559
564
        """Return sequence of revision hashes on to this branch.
560
565
 
561
566
        >>> ScratchBranch().revision_history()
562
567
        []
563
568
        """
564
 
        self.lock_read()
565
 
        try:
566
 
            return [l.rstrip('\r\n') for l in
567
 
                    self.controlfile('revision-history', 'r').readlines()]
568
 
        finally:
569
 
            self.unlock()
570
 
 
571
 
 
572
 
    def common_ancestor(self, other, self_revno=None, other_revno=None):
573
 
        """
574
 
        >>> import commit
575
 
        >>> sb = ScratchBranch(files=['foo', 'foo~'])
576
 
        >>> sb.common_ancestor(sb) == (None, None)
577
 
        True
578
 
        >>> commit.commit(sb, "Committing first revision", verbose=False)
579
 
        >>> sb.common_ancestor(sb)[0]
580
 
        1
581
 
        >>> clone = sb.clone()
582
 
        >>> commit.commit(sb, "Committing second revision", verbose=False)
583
 
        >>> sb.common_ancestor(sb)[0]
584
 
        2
585
 
        >>> sb.common_ancestor(clone)[0]
586
 
        1
587
 
        >>> commit.commit(clone, "Committing divergent second revision", 
588
 
        ...               verbose=False)
589
 
        >>> sb.common_ancestor(clone)[0]
590
 
        1
591
 
        >>> sb.common_ancestor(clone) == clone.common_ancestor(sb)
592
 
        True
593
 
        >>> sb.common_ancestor(sb) != clone.common_ancestor(clone)
594
 
        True
595
 
        >>> clone2 = sb.clone()
596
 
        >>> sb.common_ancestor(clone2)[0]
597
 
        2
598
 
        >>> sb.common_ancestor(clone2, self_revno=1)[0]
599
 
        1
600
 
        >>> sb.common_ancestor(clone2, other_revno=1)[0]
601
 
        1
602
 
        """
603
 
        my_history = self.revision_history()
604
 
        other_history = other.revision_history()
605
 
        if self_revno is None:
606
 
            self_revno = len(my_history)
607
 
        if other_revno is None:
608
 
            other_revno = len(other_history)
609
 
        indices = range(min((self_revno, other_revno)))
610
 
        indices.reverse()
611
 
        for r in indices:
612
 
            if my_history[r] == other_history[r]:
613
 
                return r+1, my_history[r]
614
 
        return None, None
 
569
        return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()]
 
570
 
615
571
 
616
572
    def enum_history(self, direction):
617
573
        """Return (revno, revision_id) for history of branch.
654
610
            return None
655
611
 
656
612
 
657
 
    def missing_revisions(self, other):
658
 
        """
659
 
        If self and other have not diverged, return a list of the revisions
660
 
        present in other, but missing from self.
661
 
 
662
 
        >>> from bzrlib.commit import commit
663
 
        >>> bzrlib.trace.silent = True
664
 
        >>> br1 = ScratchBranch()
665
 
        >>> br2 = ScratchBranch()
666
 
        >>> br1.missing_revisions(br2)
667
 
        []
668
 
        >>> commit(br2, "lala!", rev_id="REVISION-ID-1")
669
 
        >>> br1.missing_revisions(br2)
670
 
        [u'REVISION-ID-1']
671
 
        >>> br2.missing_revisions(br1)
672
 
        []
673
 
        >>> commit(br1, "lala!", rev_id="REVISION-ID-1")
674
 
        >>> br1.missing_revisions(br2)
675
 
        []
676
 
        >>> commit(br2, "lala!", rev_id="REVISION-ID-2A")
677
 
        >>> br1.missing_revisions(br2)
678
 
        [u'REVISION-ID-2A']
679
 
        >>> commit(br1, "lala!", rev_id="REVISION-ID-2B")
680
 
        >>> br1.missing_revisions(br2)
681
 
        Traceback (most recent call last):
682
 
        DivergedBranches: These branches have diverged.
683
 
        """
684
 
        self_history = self.revision_history()
685
 
        self_len = len(self_history)
686
 
        other_history = other.revision_history()
687
 
        other_len = len(other_history)
688
 
        common_index = min(self_len, other_len) -1
689
 
        if common_index >= 0 and \
690
 
            self_history[common_index] != other_history[common_index]:
691
 
            raise DivergedBranches(self, other)
692
 
        if self_len < other_len:
693
 
            return other_history[self_len:]
694
 
        return []
695
 
 
696
 
 
697
 
    def update_revisions(self, other):
698
 
        """If self and other have not diverged, ensure self has all the
699
 
        revisions in other
700
 
 
701
 
        >>> from bzrlib.commit import commit
702
 
        >>> bzrlib.trace.silent = True
703
 
        >>> br1 = ScratchBranch(files=['foo', 'bar'])
704
 
        >>> br1.add('foo')
705
 
        >>> br1.add('bar')
706
 
        >>> commit(br1, "lala!", rev_id="REVISION-ID-1", verbose=False)
707
 
        >>> br2 = ScratchBranch()
708
 
        >>> br2.update_revisions(br1)
709
 
        Added 2 texts.
710
 
        Added 1 inventories.
711
 
        Added 1 revisions.
712
 
        >>> br2.revision_history()
713
 
        [u'REVISION-ID-1']
714
 
        >>> br2.update_revisions(br1)
715
 
        Added 0 texts.
716
 
        Added 0 inventories.
717
 
        Added 0 revisions.
718
 
        >>> br1.text_store.total_size() == br2.text_store.total_size()
719
 
        True
720
 
        """
721
 
        revision_ids = self.missing_revisions(other)
722
 
        revisions = [other.get_revision(f) for f in revision_ids]
723
 
        needed_texts = sets.Set()
724
 
        for rev in revisions:
725
 
            inv = other.get_inventory(str(rev.inventory_id))
726
 
            for key, entry in inv.iter_entries():
727
 
                if entry.text_id is None:
728
 
                    continue
729
 
                if entry.text_id not in self.text_store:
730
 
                    needed_texts.add(entry.text_id)
731
 
        count = self.text_store.copy_multi(other.text_store, needed_texts)
732
 
        print "Added %d texts." % count 
733
 
        inventory_ids = [ f.inventory_id for f in revisions ]
734
 
        count = self.inventory_store.copy_multi(other.inventory_store, 
735
 
                                                inventory_ids)
736
 
        print "Added %d inventories." % count 
737
 
        revision_ids = [ f.revision_id for f in revisions]
738
 
        count = self.revision_store.copy_multi(other.revision_store, 
739
 
                                               revision_ids)
740
 
        for revision_id in revision_ids:
741
 
            self.append_revision(revision_id)
742
 
        print "Added %d revisions." % count
743
 
                    
744
 
        
745
613
    def commit(self, *args, **kw):
746
614
        """Deprecated"""
747
615
        from bzrlib.commit import commit
793
661
 
794
662
 
795
663
 
 
664
    @with_writelock
796
665
    def rename_one(self, from_rel, to_rel):
797
666
        """Rename one file.
798
667
 
799
668
        This can change the directory or the filename or both.
800
669
        """
801
 
        self.lock_write()
 
670
        tree = self.working_tree()
 
671
        inv = tree.inventory
 
672
        if not tree.has_filename(from_rel):
 
673
            raise BzrError("can't rename: old working file %r does not exist" % from_rel)
 
674
        if tree.has_filename(to_rel):
 
675
            raise BzrError("can't rename: new working file %r already exists" % to_rel)
 
676
 
 
677
        file_id = inv.path2id(from_rel)
 
678
        if file_id == None:
 
679
            raise BzrError("can't rename: old name %r is not versioned" % from_rel)
 
680
 
 
681
        if inv.path2id(to_rel):
 
682
            raise BzrError("can't rename: new name %r is already versioned" % to_rel)
 
683
 
 
684
        to_dir, to_tail = os.path.split(to_rel)
 
685
        to_dir_id = inv.path2id(to_dir)
 
686
        if to_dir_id == None and to_dir != '':
 
687
            raise BzrError("can't determine destination directory id for %r" % to_dir)
 
688
 
 
689
        mutter("rename_one:")
 
690
        mutter("  file_id    {%s}" % file_id)
 
691
        mutter("  from_rel   %r" % from_rel)
 
692
        mutter("  to_rel     %r" % to_rel)
 
693
        mutter("  to_dir     %r" % to_dir)
 
694
        mutter("  to_dir_id  {%s}" % to_dir_id)
 
695
 
 
696
        inv.rename(file_id, to_dir_id, to_tail)
 
697
 
 
698
        print "%s => %s" % (from_rel, to_rel)
 
699
 
 
700
        from_abs = self.abspath(from_rel)
 
701
        to_abs = self.abspath(to_rel)
802
702
        try:
803
 
            tree = self.working_tree()
804
 
            inv = tree.inventory
805
 
            if not tree.has_filename(from_rel):
806
 
                raise BzrError("can't rename: old working file %r does not exist" % from_rel)
807
 
            if tree.has_filename(to_rel):
808
 
                raise BzrError("can't rename: new working file %r already exists" % to_rel)
809
 
 
810
 
            file_id = inv.path2id(from_rel)
811
 
            if file_id == None:
812
 
                raise BzrError("can't rename: old name %r is not versioned" % from_rel)
813
 
 
814
 
            if inv.path2id(to_rel):
815
 
                raise BzrError("can't rename: new name %r is already versioned" % to_rel)
816
 
 
817
 
            to_dir, to_tail = os.path.split(to_rel)
818
 
            to_dir_id = inv.path2id(to_dir)
819
 
            if to_dir_id == None and to_dir != '':
820
 
                raise BzrError("can't determine destination directory id for %r" % to_dir)
821
 
 
822
 
            mutter("rename_one:")
823
 
            mutter("  file_id    {%s}" % file_id)
824
 
            mutter("  from_rel   %r" % from_rel)
825
 
            mutter("  to_rel     %r" % to_rel)
826
 
            mutter("  to_dir     %r" % to_dir)
827
 
            mutter("  to_dir_id  {%s}" % to_dir_id)
828
 
 
829
 
            inv.rename(file_id, to_dir_id, to_tail)
830
 
 
831
 
            print "%s => %s" % (from_rel, to_rel)
832
 
 
833
 
            from_abs = self.abspath(from_rel)
834
 
            to_abs = self.abspath(to_rel)
835
 
            try:
836
 
                os.rename(from_abs, to_abs)
837
 
            except OSError, e:
838
 
                raise BzrError("failed to rename %r to %r: %s"
839
 
                        % (from_abs, to_abs, e[1]),
840
 
                        ["rename rolled back"])
841
 
 
842
 
            self._write_inventory(inv)
843
 
        finally:
844
 
            self.unlock()
845
 
 
846
 
 
 
703
            os.rename(from_abs, to_abs)
 
704
        except OSError, e:
 
705
            raise BzrError("failed to rename %r to %r: %s"
 
706
                    % (from_abs, to_abs, e[1]),
 
707
                    ["rename rolled back"])
 
708
 
 
709
        self._write_inventory(inv)
 
710
 
 
711
 
 
712
 
 
713
    @with_writelock
847
714
    def move(self, from_paths, to_name):
848
715
        """Rename files.
849
716
 
855
722
        Note that to_name is only the last component of the new name;
856
723
        this doesn't change the directory.
857
724
        """
858
 
        self.lock_write()
859
 
        try:
860
 
            ## TODO: Option to move IDs only
861
 
            assert not isinstance(from_paths, basestring)
862
 
            tree = self.working_tree()
863
 
            inv = tree.inventory
864
 
            to_abs = self.abspath(to_name)
865
 
            if not isdir(to_abs):
866
 
                raise BzrError("destination %r is not a directory" % to_abs)
867
 
            if not tree.has_filename(to_name):
868
 
                raise BzrError("destination %r not in working directory" % to_abs)
869
 
            to_dir_id = inv.path2id(to_name)
870
 
            if to_dir_id == None and to_name != '':
871
 
                raise BzrError("destination %r is not a versioned directory" % to_name)
872
 
            to_dir_ie = inv[to_dir_id]
873
 
            if to_dir_ie.kind not in ('directory', 'root_directory'):
874
 
                raise BzrError("destination %r is not a directory" % to_abs)
875
 
 
876
 
            to_idpath = inv.get_idpath(to_dir_id)
877
 
 
878
 
            for f in from_paths:
879
 
                if not tree.has_filename(f):
880
 
                    raise BzrError("%r does not exist in working tree" % f)
881
 
                f_id = inv.path2id(f)
882
 
                if f_id == None:
883
 
                    raise BzrError("%r is not versioned" % f)
884
 
                name_tail = splitpath(f)[-1]
885
 
                dest_path = appendpath(to_name, name_tail)
886
 
                if tree.has_filename(dest_path):
887
 
                    raise BzrError("destination %r already exists" % dest_path)
888
 
                if f_id in to_idpath:
889
 
                    raise BzrError("can't move %r to a subdirectory of itself" % f)
890
 
 
891
 
            # OK, so there's a race here, it's possible that someone will
892
 
            # create a file in this interval and then the rename might be
893
 
            # left half-done.  But we should have caught most problems.
894
 
 
895
 
            for f in from_paths:
896
 
                name_tail = splitpath(f)[-1]
897
 
                dest_path = appendpath(to_name, name_tail)
898
 
                print "%s => %s" % (f, dest_path)
899
 
                inv.rename(inv.path2id(f), to_dir_id, name_tail)
900
 
                try:
901
 
                    os.rename(self.abspath(f), self.abspath(dest_path))
902
 
                except OSError, e:
903
 
                    raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
904
 
                            ["rename rolled back"])
905
 
 
906
 
            self._write_inventory(inv)
907
 
        finally:
908
 
            self.unlock()
 
725
        ## TODO: Option to move IDs only
 
726
        assert not isinstance(from_paths, basestring)
 
727
        tree = self.working_tree()
 
728
        inv = tree.inventory
 
729
        to_abs = self.abspath(to_name)
 
730
        if not isdir(to_abs):
 
731
            raise BzrError("destination %r is not a directory" % to_abs)
 
732
        if not tree.has_filename(to_name):
 
733
            raise BzrError("destination %r not in working directory" % to_abs)
 
734
        to_dir_id = inv.path2id(to_name)
 
735
        if to_dir_id == None and to_name != '':
 
736
            raise BzrError("destination %r is not a versioned directory" % to_name)
 
737
        to_dir_ie = inv[to_dir_id]
 
738
        if to_dir_ie.kind not in ('directory', 'root_directory'):
 
739
            raise BzrError("destination %r is not a directory" % to_abs)
 
740
 
 
741
        to_idpath = inv.get_idpath(to_dir_id)
 
742
 
 
743
        for f in from_paths:
 
744
            if not tree.has_filename(f):
 
745
                raise BzrError("%r does not exist in working tree" % f)
 
746
            f_id = inv.path2id(f)
 
747
            if f_id == None:
 
748
                raise BzrError("%r is not versioned" % f)
 
749
            name_tail = splitpath(f)[-1]
 
750
            dest_path = appendpath(to_name, name_tail)
 
751
            if tree.has_filename(dest_path):
 
752
                raise BzrError("destination %r already exists" % dest_path)
 
753
            if f_id in to_idpath:
 
754
                raise BzrError("can't move %r to a subdirectory of itself" % f)
 
755
 
 
756
        # OK, so there's a race here, it's possible that someone will
 
757
        # create a file in this interval and then the rename might be
 
758
        # left half-done.  But we should have caught most problems.
 
759
 
 
760
        for f in from_paths:
 
761
            name_tail = splitpath(f)[-1]
 
762
            dest_path = appendpath(to_name, name_tail)
 
763
            print "%s => %s" % (f, dest_path)
 
764
            inv.rename(inv.path2id(f), to_dir_id, name_tail)
 
765
            try:
 
766
                os.rename(self.abspath(f), self.abspath(dest_path))
 
767
            except OSError, e:
 
768
                raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
 
769
                        ["rename rolled back"])
 
770
 
 
771
        self._write_inventory(inv)
 
772
 
909
773
 
910
774
 
911
775
 
920
784
    >>> isdir(bd)
921
785
    False
922
786
    """
923
 
    def __init__(self, files=[], dirs=[], base=None):
 
787
    def __init__(self, files=[], dirs=[]):
924
788
        """Make a test branch.
925
789
 
926
790
        This creates a temporary directory and runs init-tree in it.
927
791
 
928
792
        If any files are listed, they are created in the working copy.
929
793
        """
930
 
        init = False
931
 
        if base is None:
932
 
            base = tempfile.mkdtemp()
933
 
            init = True
934
 
        Branch.__init__(self, base, init=init)
 
794
        Branch.__init__(self, tempfile.mkdtemp(), init=True)
935
795
        for d in dirs:
936
796
            os.mkdir(self.abspath(d))
937
797
            
939
799
            file(os.path.join(self.base, f), 'w').write('content of %s' % f)
940
800
 
941
801
 
942
 
    def clone(self):
943
 
        """
944
 
        >>> orig = ScratchBranch(files=["file1", "file2"])
945
 
        >>> clone = orig.clone()
946
 
        >>> os.path.samefile(orig.base, clone.base)
947
 
        False
948
 
        >>> os.path.isfile(os.path.join(clone.base, "file1"))
949
 
        True
950
 
        """
951
 
        base = tempfile.mkdtemp()
952
 
        os.rmdir(base)
953
 
        shutil.copytree(self.base, base, symlinks=True)
954
 
        return ScratchBranch(base=base)
955
 
        
956
802
    def __del__(self):
957
803
        self.destroy()
958
804