~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Martin Pool
  • Date: 2005-05-19 08:31:06 UTC
  • Revision ID: mbp@sourcefrog.net-20050519083106-ebe71562d3bda4a7
- fix typo

Show diffs side-by-side

added added

removed removed

Lines of Context:
29
29
     joinpath, sha_string, file_kind, local_time_offset, appendpath
30
30
from store import ImmutableStore
31
31
from revision import Revision
32
 
from errors import BzrError
 
32
from errors import bailout, BzrError
33
33
from textui import show_status
34
34
 
35
35
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
43
43
        return remotebranch.RemoteBranch(f, **args)
44
44
    else:
45
45
        return Branch(f, **args)
46
 
 
47
 
 
48
 
 
49
 
def _relpath(base, path):
50
 
    """Return path relative to base, or raise exception.
51
 
 
52
 
    The path may be either an absolute path or a path relative to the
53
 
    current working directory.
54
 
 
55
 
    Lifted out of Branch.relpath for ease of testing.
56
 
 
57
 
    os.path.commonprefix (python2.4) has a bad bug that it works just
58
 
    on string prefixes, assuming that '/u' is a prefix of '/u2'.  This
59
 
    avoids that problem."""
60
 
    rp = os.path.abspath(path)
61
 
 
62
 
    s = []
63
 
    head = rp
64
 
    while len(head) >= len(base):
65
 
        if head == base:
66
 
            break
67
 
        head, tail = os.path.split(head)
68
 
        if tail:
69
 
            s.insert(0, tail)
70
 
    else:
71
 
        from errors import NotBranchError
72
 
        raise NotBranchError("path %r is not within branch %r" % (rp, base))
73
 
 
74
 
    return os.sep.join(s)
75
46
        
76
47
 
77
48
def find_branch_root(f=None):
109
80
######################################################################
110
81
# branch objects
111
82
 
112
 
class Branch(object):
 
83
class Branch:
113
84
    """Branch holding a history of revisions.
114
85
 
115
86
    base
116
87
        Base directory of the branch.
117
 
 
118
 
    _lock_mode
119
 
        None, or 'r' or 'w'
120
 
 
121
 
    _lock_count
122
 
        If _lock_mode is true, a positive count of the number of times the
123
 
        lock has been taken.
124
 
 
125
 
    _lock
126
 
        Lock object from bzrlib.lock.
127
88
    """
128
 
    base = None
129
 
    _lock_mode = None
130
 
    _lock_count = None
 
89
    _lockmode = None
131
90
    
132
 
    def __init__(self, base, init=False, find_root=True):
 
91
    def __init__(self, base, init=False, find_root=True, lock_mode='w'):
133
92
        """Create new branch object at a particular location.
134
93
 
135
94
        base -- Base directory for the branch.
152
111
        else:
153
112
            self.base = os.path.realpath(base)
154
113
            if not isdir(self.controlfilename('.')):
155
 
                from errors import NotBranchError
156
 
                raise NotBranchError("not a bzr branch: %s" % quotefn(base),
157
 
                                     ['use "bzr init" to initialize a new working tree',
158
 
                                      'current bzr can only operate from top-of-tree'])
 
114
                bailout("not a bzr branch: %s" % quotefn(base),
 
115
                        ['use "bzr init" to initialize a new working tree',
 
116
                         'current bzr can only operate from top-of-tree'])
159
117
        self._check_format()
160
 
        self._lockfile = self.controlfile('branch-lock', 'wb')
 
118
        self.lock(lock_mode)
161
119
 
162
120
        self.text_store = ImmutableStore(self.controlfilename('text-store'))
163
121
        self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
171
129
    __repr__ = __str__
172
130
 
173
131
 
174
 
    def __del__(self):
175
 
        if self._lock_mode:
176
 
            from warnings import warn
177
 
            warn("branch %r was not explicitly unlocked" % self)
178
 
            self.unlock()
179
 
 
180
 
 
181
 
 
182
 
    def lock_write(self):
183
 
        if self._lock_mode:
184
 
            if self._lock_mode != 'w':
185
 
                from errors import LockError
186
 
                raise LockError("can't upgrade to a write lock from %r" %
187
 
                                self._lock_mode)
188
 
            self._lock_count += 1
189
 
        else:
190
 
            from bzrlib.lock import lock, LOCK_EX
191
 
 
192
 
            lock(self._lockfile, LOCK_EX)
193
 
            self._lock_mode = 'w'
194
 
            self._lock_count = 1
195
 
 
196
 
 
197
 
 
198
 
    def lock_read(self):
199
 
        if self._lock_mode:
200
 
            assert self._lock_mode in ('r', 'w'), \
201
 
                   "invalid lock mode %r" % self._lock_mode
202
 
            self._lock_count += 1
203
 
        else:
204
 
            from bzrlib.lock import lock, LOCK_SH
205
 
 
206
 
            lock(self._lockfile, LOCK_SH)
207
 
            self._lock_mode = 'r'
208
 
            self._lock_count = 1
209
 
                        
210
 
 
 
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
211
156
            
212
 
    def unlock(self):
213
 
        if not self._lock_mode:
214
 
            from errors import LockError
215
 
            raise LockError('branch %r is not locked' % (self))
216
 
 
217
 
        if self._lock_count > 1:
218
 
            self._lock_count -= 1
219
 
        else:
220
 
            assert self._lock_count == 1
221
 
            from bzrlib.lock import unlock
222
 
            unlock(self._lockfile)
223
 
            self._lock_mode = self._lock_count = None
 
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)
224
179
 
225
180
 
226
181
    def abspath(self, name):
232
187
        """Return path relative to this branch of something inside it.
233
188
 
234
189
        Raises an error if path is not in this branch."""
235
 
        return _relpath(self.base, path)
 
190
        rp = os.path.realpath(path)
 
191
        # FIXME: windows
 
192
        if not rp.startswith(self.base):
 
193
            bailout("path %r is not within branch %r" % (rp, self.base))
 
194
        rp = rp[len(self.base):]
 
195
        rp = rp.lstrip(os.sep)
 
196
        return rp
236
197
 
237
198
 
238
199
    def controlfilename(self, file_or_path):
299
260
        fmt = self.controlfile('branch-format', 'r').read()
300
261
        fmt.replace('\r\n', '')
301
262
        if fmt != BZR_BRANCH_FORMAT:
302
 
            raise BzrError('sorry, branch format %r not supported' % fmt,
303
 
                           ['use a different bzr version',
304
 
                            'or remove the .bzr directory and "bzr init" again'])
305
 
 
 
263
            bailout('sorry, branch format %r not supported' % fmt,
 
264
                    ['use a different bzr version',
 
265
                     'or remove the .bzr directory and "bzr init" again'])
306
266
 
307
267
 
308
268
    def read_working_inventory(self):
309
269
        """Read the working inventory."""
 
270
        self._need_readlock()
310
271
        before = time.time()
311
272
        # ElementTree does its own conversion from UTF-8, so open in
312
273
        # binary.
313
 
        self.lock_read()
314
 
        try:
315
 
            inv = Inventory.read_xml(self.controlfile('inventory', 'rb'))
316
 
            mutter("loaded inventory of %d items in %f"
317
 
                   % (len(inv), time.time() - before))
318
 
            return inv
319
 
        finally:
320
 
            self.unlock()
321
 
            
 
274
        inv = Inventory.read_xml(self.controlfile('inventory', 'rb'))
 
275
        mutter("loaded inventory of %d items in %f"
 
276
               % (len(inv), time.time() - before))
 
277
        return inv
 
278
 
322
279
 
323
280
    def _write_inventory(self, inv):
324
281
        """Update the working inventory.
326
283
        That is to say, the inventory describing changes underway, that
327
284
        will be committed to the next revision.
328
285
        """
 
286
        self._need_writelock()
329
287
        ## TODO: factor out to atomicfile?  is rename safe on windows?
330
288
        ## TODO: Maybe some kind of clean/dirty marker on inventory?
331
289
        tmpfname = self.controlfilename('inventory.tmp')
337
295
            os.remove(inv_fname)
338
296
        os.rename(tmpfname, inv_fname)
339
297
        mutter('wrote working inventory')
340
 
            
 
298
 
341
299
 
342
300
    inventory = property(read_working_inventory, _write_inventory, None,
343
301
                         """Inventory for the working copy.""")
351
309
        This puts the files in the Added state, so that they will be
352
310
        recorded by the next commit.
353
311
 
354
 
        files
355
 
            List of paths to add, relative to the base of the tree.
356
 
 
357
 
        ids
358
 
            If set, use these instead of automatically generated ids.
359
 
            Must be the same length as the list of files, but may
360
 
            contain None for ids that are to be autogenerated.
361
 
 
362
312
        TODO: Perhaps have an option to add the ids even if the files do
363
 
              not (yet) exist.
 
313
               not (yet) exist.
364
314
 
365
315
        TODO: Perhaps return the ids of the files?  But then again it
366
 
              is easy to retrieve them if they're needed.
 
316
               is easy to retrieve them if they're needed.
 
317
 
 
318
        TODO: Option to specify file id.
367
319
 
368
320
        TODO: Adding a directory should optionally recurse down and
369
 
              add all non-ignored children.  Perhaps do that in a
370
 
              higher-level method.
 
321
               add all non-ignored children.  Perhaps do that in a
 
322
               higher-level method.
371
323
        """
 
324
        self._need_writelock()
 
325
 
372
326
        # TODO: Re-adding a file that is removed in the working copy
373
327
        # should probably put it back with the previous ID.
374
328
        if isinstance(files, types.StringTypes):
381
335
            ids = [None] * len(files)
382
336
        else:
383
337
            assert(len(ids) == len(files))
384
 
 
385
 
        self.lock_write()
386
 
        try:
387
 
            inv = self.read_working_inventory()
388
 
            for f,file_id in zip(files, ids):
389
 
                if is_control_file(f):
390
 
                    raise BzrError("cannot add control file %s" % quotefn(f))
391
 
 
392
 
                fp = splitpath(f)
393
 
 
394
 
                if len(fp) == 0:
395
 
                    raise BzrError("cannot add top-level %r" % f)
396
 
 
397
 
                fullpath = os.path.normpath(self.abspath(f))
398
 
 
399
 
                try:
400
 
                    kind = file_kind(fullpath)
401
 
                except OSError:
402
 
                    # maybe something better?
403
 
                    raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
404
 
 
405
 
                if kind != 'file' and kind != 'directory':
406
 
                    raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
407
 
 
408
 
                if file_id is None:
409
 
                    file_id = gen_file_id(f)
410
 
                inv.add_path(f, kind=kind, file_id=file_id)
411
 
 
412
 
                if verbose:
413
 
                    show_status('A', kind, quotefn(f))
414
 
 
415
 
                mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
416
 
 
417
 
            self._write_inventory(inv)
418
 
        finally:
419
 
            self.unlock()
420
 
            
 
338
        
 
339
        inv = self.read_working_inventory()
 
340
        for f,file_id in zip(files, ids):
 
341
            if is_control_file(f):
 
342
                bailout("cannot add control file %s" % quotefn(f))
 
343
 
 
344
            fp = splitpath(f)
 
345
 
 
346
            if len(fp) == 0:
 
347
                bailout("cannot add top-level %r" % f)
 
348
                
 
349
            fullpath = os.path.normpath(self.abspath(f))
 
350
 
 
351
            try:
 
352
                kind = file_kind(fullpath)
 
353
            except OSError:
 
354
                # maybe something better?
 
355
                bailout('cannot add: not a regular file or directory: %s' % quotefn(f))
 
356
            
 
357
            if kind != 'file' and kind != 'directory':
 
358
                bailout('cannot add: not a regular file or directory: %s' % quotefn(f))
 
359
 
 
360
            if file_id is None:
 
361
                file_id = gen_file_id(f)
 
362
            inv.add_path(f, kind=kind, file_id=file_id)
 
363
 
 
364
            if verbose:
 
365
                show_status('A', kind, quotefn(f))
 
366
                
 
367
            mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
 
368
            
 
369
        self._write_inventory(inv)
 
370
 
421
371
 
422
372
    def print_file(self, file, revno):
423
373
        """Print `file` to stdout."""
424
 
        self.lock_read()
425
 
        try:
426
 
            tree = self.revision_tree(self.lookup_revision(revno))
427
 
            # use inventory as it was in that revision
428
 
            file_id = tree.inventory.path2id(file)
429
 
            if not file_id:
430
 
                raise BzrError("%r is not present in revision %d" % (file, revno))
431
 
            tree.print_file(file_id)
432
 
        finally:
433
 
            self.unlock()
434
 
 
 
374
        self._need_readlock()
 
375
        tree = self.revision_tree(self.lookup_revision(revno))
 
376
        # use inventory as it was in that revision
 
377
        file_id = tree.inventory.path2id(file)
 
378
        if not file_id:
 
379
            bailout("%r is not present in revision %d" % (file, revno))
 
380
        tree.print_file(file_id)
 
381
        
435
382
 
436
383
    def remove(self, files, verbose=False):
437
384
        """Mark nominated files for removal from the inventory.
449
396
        """
450
397
        ## TODO: Normalize names
451
398
        ## TODO: Remove nested loops; better scalability
 
399
        self._need_writelock()
 
400
 
452
401
        if isinstance(files, types.StringTypes):
453
402
            files = [files]
454
 
 
455
 
        self.lock_write()
456
 
 
457
 
        try:
458
 
            tree = self.working_tree()
459
 
            inv = tree.inventory
460
 
 
461
 
            # do this before any modifications
462
 
            for f in files:
463
 
                fid = inv.path2id(f)
464
 
                if not fid:
465
 
                    raise BzrError("cannot remove unversioned file %s" % quotefn(f))
466
 
                mutter("remove inventory entry %s {%s}" % (quotefn(f), fid))
467
 
                if verbose:
468
 
                    # having remove it, it must be either ignored or unknown
469
 
                    if tree.is_ignored(f):
470
 
                        new_status = 'I'
471
 
                    else:
472
 
                        new_status = '?'
473
 
                    show_status(new_status, inv[fid].kind, quotefn(f))
474
 
                del inv[fid]
475
 
 
476
 
            self._write_inventory(inv)
477
 
        finally:
478
 
            self.unlock()
479
 
 
480
 
 
481
 
    # FIXME: this doesn't need to be a branch method
 
403
        
 
404
        tree = self.working_tree()
 
405
        inv = tree.inventory
 
406
 
 
407
        # do this before any modifications
 
408
        for f in files:
 
409
            fid = inv.path2id(f)
 
410
            if not fid:
 
411
                bailout("cannot remove unversioned file %s" % quotefn(f))
 
412
            mutter("remove inventory entry %s {%s}" % (quotefn(f), fid))
 
413
            if verbose:
 
414
                # having remove it, it must be either ignored or unknown
 
415
                if tree.is_ignored(f):
 
416
                    new_status = 'I'
 
417
                else:
 
418
                    new_status = '?'
 
419
                show_status(new_status, inv[fid].kind, quotefn(f))
 
420
            del inv[fid]
 
421
 
 
422
        self._write_inventory(inv)
 
423
 
482
424
    def set_inventory(self, new_inventory_list):
483
425
        inv = Inventory()
484
426
        for path, file_id, parent, kind in new_inventory_list:
529
471
 
530
472
    def get_revision(self, revision_id):
531
473
        """Return the Revision object for a named revision"""
 
474
        self._need_readlock()
532
475
        r = Revision.read_xml(self.revision_store[revision_id])
533
476
        assert r.revision_id == revision_id
534
477
        return r
540
483
        TODO: Perhaps for this and similar methods, take a revision
541
484
               parameter which can be either an integer revno or a
542
485
               string hash."""
 
486
        self._need_readlock()
543
487
        i = Inventory.read_xml(self.inventory_store[inventory_id])
544
488
        return i
545
489
 
546
490
 
547
491
    def get_revision_inventory(self, revision_id):
548
492
        """Return inventory of a past revision."""
 
493
        self._need_readlock()
549
494
        if revision_id == None:
550
495
            return Inventory()
551
496
        else:
558
503
        >>> ScratchBranch().revision_history()
559
504
        []
560
505
        """
561
 
        self.lock_read()
562
 
        try:
563
 
            return [l.rstrip('\r\n') for l in
564
 
                    self.controlfile('revision-history', 'r').readlines()]
565
 
        finally:
566
 
            self.unlock()
 
506
        self._need_readlock()
 
507
        return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()]
567
508
 
568
509
 
569
510
    def enum_history(self, direction):
632
573
        an `EmptyTree` is returned."""
633
574
        # TODO: refactor this to use an existing revision object
634
575
        # so we don't need to read it in twice.
 
576
        self._need_readlock()
635
577
        if revision_id == None:
636
578
            return EmptyTree()
637
579
        else:
663
605
 
664
606
        This can change the directory or the filename or both.
665
607
        """
666
 
        self.lock_write()
 
608
        self._need_writelock()
 
609
        tree = self.working_tree()
 
610
        inv = tree.inventory
 
611
        if not tree.has_filename(from_rel):
 
612
            bailout("can't rename: old working file %r does not exist" % from_rel)
 
613
        if tree.has_filename(to_rel):
 
614
            bailout("can't rename: new working file %r already exists" % to_rel)
 
615
            
 
616
        file_id = inv.path2id(from_rel)
 
617
        if file_id == None:
 
618
            bailout("can't rename: old name %r is not versioned" % from_rel)
 
619
 
 
620
        if inv.path2id(to_rel):
 
621
            bailout("can't rename: new name %r is already versioned" % to_rel)
 
622
 
 
623
        to_dir, to_tail = os.path.split(to_rel)
 
624
        to_dir_id = inv.path2id(to_dir)
 
625
        if to_dir_id == None and to_dir != '':
 
626
            bailout("can't determine destination directory id for %r" % to_dir)
 
627
 
 
628
        mutter("rename_one:")
 
629
        mutter("  file_id    {%s}" % file_id)
 
630
        mutter("  from_rel   %r" % from_rel)
 
631
        mutter("  to_rel     %r" % to_rel)
 
632
        mutter("  to_dir     %r" % to_dir)
 
633
        mutter("  to_dir_id  {%s}" % to_dir_id)
 
634
            
 
635
        inv.rename(file_id, to_dir_id, to_tail)
 
636
 
 
637
        print "%s => %s" % (from_rel, to_rel)
 
638
        
 
639
        from_abs = self.abspath(from_rel)
 
640
        to_abs = self.abspath(to_rel)
667
641
        try:
668
 
            tree = self.working_tree()
669
 
            inv = tree.inventory
670
 
            if not tree.has_filename(from_rel):
671
 
                raise BzrError("can't rename: old working file %r does not exist" % from_rel)
672
 
            if tree.has_filename(to_rel):
673
 
                raise BzrError("can't rename: new working file %r already exists" % to_rel)
674
 
 
675
 
            file_id = inv.path2id(from_rel)
676
 
            if file_id == None:
677
 
                raise BzrError("can't rename: old name %r is not versioned" % from_rel)
678
 
 
679
 
            if inv.path2id(to_rel):
680
 
                raise BzrError("can't rename: new name %r is already versioned" % to_rel)
681
 
 
682
 
            to_dir, to_tail = os.path.split(to_rel)
683
 
            to_dir_id = inv.path2id(to_dir)
684
 
            if to_dir_id == None and to_dir != '':
685
 
                raise BzrError("can't determine destination directory id for %r" % to_dir)
686
 
 
687
 
            mutter("rename_one:")
688
 
            mutter("  file_id    {%s}" % file_id)
689
 
            mutter("  from_rel   %r" % from_rel)
690
 
            mutter("  to_rel     %r" % to_rel)
691
 
            mutter("  to_dir     %r" % to_dir)
692
 
            mutter("  to_dir_id  {%s}" % to_dir_id)
693
 
 
694
 
            inv.rename(file_id, to_dir_id, to_tail)
695
 
 
696
 
            print "%s => %s" % (from_rel, to_rel)
697
 
 
698
 
            from_abs = self.abspath(from_rel)
699
 
            to_abs = self.abspath(to_rel)
700
 
            try:
701
 
                os.rename(from_abs, to_abs)
702
 
            except OSError, e:
703
 
                raise BzrError("failed to rename %r to %r: %s"
704
 
                        % (from_abs, to_abs, e[1]),
705
 
                        ["rename rolled back"])
706
 
 
707
 
            self._write_inventory(inv)
708
 
        finally:
709
 
            self.unlock()
 
642
            os.rename(from_abs, to_abs)
 
643
        except OSError, e:
 
644
            bailout("failed to rename %r to %r: %s"
 
645
                    % (from_abs, to_abs, e[1]),
 
646
                    ["rename rolled back"])
 
647
 
 
648
        self._write_inventory(inv)
 
649
            
710
650
 
711
651
 
712
652
    def move(self, from_paths, to_name):
720
660
        Note that to_name is only the last component of the new name;
721
661
        this doesn't change the directory.
722
662
        """
723
 
        self.lock_write()
724
 
        try:
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
 
        finally:
773
 
            self.unlock()
 
663
        self._need_writelock()
 
664
        ## TODO: Option to move IDs only
 
665
        assert not isinstance(from_paths, basestring)
 
666
        tree = self.working_tree()
 
667
        inv = tree.inventory
 
668
        to_abs = self.abspath(to_name)
 
669
        if not isdir(to_abs):
 
670
            bailout("destination %r is not a directory" % to_abs)
 
671
        if not tree.has_filename(to_name):
 
672
            bailout("destination %r not in working directory" % to_abs)
 
673
        to_dir_id = inv.path2id(to_name)
 
674
        if to_dir_id == None and to_name != '':
 
675
            bailout("destination %r is not a versioned directory" % to_name)
 
676
        to_dir_ie = inv[to_dir_id]
 
677
        if to_dir_ie.kind not in ('directory', 'root_directory'):
 
678
            bailout("destination %r is not a directory" % to_abs)
 
679
 
 
680
        to_idpath = inv.get_idpath(to_dir_id)
 
681
 
 
682
        for f in from_paths:
 
683
            if not tree.has_filename(f):
 
684
                bailout("%r does not exist in working tree" % f)
 
685
            f_id = inv.path2id(f)
 
686
            if f_id == None:
 
687
                bailout("%r is not versioned" % f)
 
688
            name_tail = splitpath(f)[-1]
 
689
            dest_path = appendpath(to_name, name_tail)
 
690
            if tree.has_filename(dest_path):
 
691
                bailout("destination %r already exists" % dest_path)
 
692
            if f_id in to_idpath:
 
693
                bailout("can't move %r to a subdirectory of itself" % f)
 
694
 
 
695
        # OK, so there's a race here, it's possible that someone will
 
696
        # create a file in this interval and then the rename might be
 
697
        # left half-done.  But we should have caught most problems.
 
698
 
 
699
        for f in from_paths:
 
700
            name_tail = splitpath(f)[-1]
 
701
            dest_path = appendpath(to_name, name_tail)
 
702
            print "%s => %s" % (f, dest_path)
 
703
            inv.rename(inv.path2id(f), to_dir_id, name_tail)
 
704
            try:
 
705
                os.rename(self.abspath(f), self.abspath(dest_path))
 
706
            except OSError, e:
 
707
                bailout("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
 
708
                        ["rename rolled back"])
 
709
 
 
710
        self._write_inventory(inv)
 
711
 
774
712
 
775
713
 
776
714
 
806
744
    def destroy(self):
807
745
        """Destroy the test branch, removing the scratch directory."""
808
746
        try:
809
 
            if self.base:
810
 
                mutter("delete ScratchBranch %s" % self.base)
811
 
                shutil.rmtree(self.base)
 
747
            mutter("delete ScratchBranch %s" % self.base)
 
748
            shutil.rmtree(self.base)
812
749
        except OSError, e:
813
750
            # Work around for shutil.rmtree failing on Windows when
814
751
            # readonly files are encountered