/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

More work on roundtrip push support.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
# Copyright (C) 2005 Canonical Ltd
2
 
 
3
 
# This program is free software; you can redistribute it and/or modify
4
 
# it under the terms of the GNU General Public License as published by
5
 
# the Free Software Foundation; either version 2 of the License, or
6
 
# (at your option) any later version.
7
 
 
8
 
# This program is distributed in the hope that it will be useful,
9
 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
10
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11
 
# GNU General Public License for more details.
12
 
 
13
 
# You should have received a copy of the GNU General Public License
14
 
# along with this program; if not, write to the Free Software
15
 
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
 
 
17
 
 
18
 
import sys, os, os.path, random, time, sha, sets, types, re, shutil, tempfile
19
 
import traceback, socket, fnmatch, difflib, time
20
 
from binascii import hexlify
21
 
 
22
 
import bzrlib
23
 
from inventory import Inventory
24
 
from trace import mutter, note
25
 
from tree import Tree, EmptyTree, RevisionTree
26
 
from inventory import InventoryEntry, Inventory
27
 
from osutils import isdir, quotefn, isfile, uuid, sha_file, username, \
28
 
     format_date, compact_date, pumpfile, user_email, rand_bytes, splitpath, \
29
 
     joinpath, sha_string, file_kind, local_time_offset, appendpath
30
 
from store import ImmutableStore
31
 
from revision import Revision
32
 
from errors import BzrError
33
 
from textui import show_status
34
 
 
35
 
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
36
 
## TODO: Maybe include checks for common corruption of newlines, etc?
37
 
 
38
 
 
39
 
 
40
 
def find_branch(f, **args):
41
 
    if f and (f.startswith('http://') or f.startswith('https://')):
42
 
        import remotebranch 
43
 
        return remotebranch.RemoteBranch(f, **args)
44
 
    else:
45
 
        return Branch(f, **args)
46
 
 
47
 
 
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('w')
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('r')
64
 
        try:
65
 
            return method(self, *a, **k)
66
 
        finally:
67
 
            self.unlock()
68
 
    return d
69
 
 
70
 
 
71
 
def _relpath(base, path):
72
 
    """Return path relative to base, or raise exception.
73
 
 
74
 
    The path may be either an absolute path or a path relative to the
75
 
    current working directory.
76
 
 
77
 
    Lifted out of Branch.relpath for ease of testing.
78
 
 
79
 
    os.path.commonprefix (python2.4) has a bad bug that it works just
80
 
    on string prefixes, assuming that '/u' is a prefix of '/u2'.  This
81
 
    avoids that problem."""
82
 
    rp = os.path.abspath(path)
83
 
 
84
 
    s = []
85
 
    head = rp
86
 
    while len(head) >= len(base):
87
 
        if head == base:
88
 
            break
89
 
        head, tail = os.path.split(head)
90
 
        if tail:
91
 
            s.insert(0, tail)
92
 
    else:
93
 
        from errors import NotBranchError
94
 
        raise NotBranchError("path %r is not within branch %r" % (rp, base))
95
 
 
96
 
    return os.sep.join(s)
97
 
        
98
 
 
99
 
def find_branch_root(f=None):
100
 
    """Find the branch root enclosing f, or pwd.
101
 
 
102
 
    f may be a filename or a URL.
103
 
 
104
 
    It is not necessary that f exists.
105
 
 
106
 
    Basically we keep looking up until we find the control directory or
107
 
    run into the root."""
108
 
    if f == None:
109
 
        f = os.getcwd()
110
 
    elif hasattr(os.path, 'realpath'):
111
 
        f = os.path.realpath(f)
112
 
    else:
113
 
        f = os.path.abspath(f)
114
 
    if not os.path.exists(f):
115
 
        raise BzrError('%r does not exist' % f)
116
 
        
117
 
 
118
 
    orig_f = f
119
 
 
120
 
    while True:
121
 
        if os.path.exists(os.path.join(f, bzrlib.BZRDIR)):
122
 
            return f
123
 
        head, tail = os.path.split(f)
124
 
        if head == f:
125
 
            # reached the root, whatever that may be
126
 
            raise BzrError('%r is not in a branch' % orig_f)
127
 
        f = head
128
 
    
129
 
 
130
 
 
131
 
######################################################################
132
 
# branch objects
133
 
 
134
 
class Branch(object):
135
 
    """Branch holding a history of revisions.
136
 
 
137
 
    base
138
 
        Base directory of the branch.
139
 
 
140
 
    _lock_mode
141
 
        None, or 'r' or 'w'
142
 
 
143
 
    _lock_count
144
 
        If _lock_mode is true, a positive count of the number of times the
145
 
        lock has been taken.
146
 
 
147
 
    _lockfile
148
 
        Open file used for locking.
149
 
    """
150
 
    base = None
151
 
    _lock_mode = None
152
 
    _lock_count = None
153
 
    
154
 
    def __init__(self, base, init=False, find_root=True):
155
 
        """Create new branch object at a particular location.
156
 
 
157
 
        base -- Base directory for the branch.
158
 
        
159
 
        init -- If True, create new control files in a previously
160
 
             unversioned directory.  If False, the branch must already
161
 
             be versioned.
162
 
 
163
 
        find_root -- If true and init is false, find the root of the
164
 
             existing branch containing base.
165
 
 
166
 
        In the test suite, creation of new trees is tested using the
167
 
        `ScratchBranch` class.
168
 
        """
169
 
        if init:
170
 
            self.base = os.path.realpath(base)
171
 
            self._make_control()
172
 
        elif find_root:
173
 
            self.base = find_branch_root(base)
174
 
        else:
175
 
            self.base = os.path.realpath(base)
176
 
            if not isdir(self.controlfilename('.')):
177
 
                from errors import NotBranchError
178
 
                raise NotBranchError("not a bzr branch: %s" % quotefn(base),
179
 
                                     ['use "bzr init" to initialize a new working tree',
180
 
                                      'current bzr can only operate from top-of-tree'])
181
 
        self._check_format()
182
 
        self._lockfile = self.controlfile('branch-lock', 'wb')
183
 
 
184
 
        self.text_store = ImmutableStore(self.controlfilename('text-store'))
185
 
        self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
186
 
        self.inventory_store = ImmutableStore(self.controlfilename('inventory-store'))
187
 
 
188
 
 
189
 
    def __str__(self):
190
 
        return '%s(%r)' % (self.__class__.__name__, self.base)
191
 
 
192
 
 
193
 
    __repr__ = __str__
194
 
 
195
 
 
196
 
    def __del__(self):
197
 
        if self._lock_mode:
198
 
            from warnings import warn
199
 
            warn("branch %r was not explicitly unlocked" % self)
200
 
            self.unlock()
201
 
 
202
 
 
203
 
    def lock(self, mode):
204
 
        if self._lock_mode:
205
 
            if mode == 'w' and cur_lm == 'r':
206
 
                raise BzrError("can't upgrade to a write lock")
207
 
            
208
 
            assert self._lock_count >= 1
209
 
            self._lock_count += 1
210
 
        else:
211
 
            from bzrlib.lock import lock, LOCK_SH, LOCK_EX
212
 
            if mode == 'r':
213
 
                m = LOCK_SH
214
 
            elif mode == 'w':
215
 
                m = LOCK_EX
216
 
            else:
217
 
                raise ValueError('invalid lock mode %r' % mode)
218
 
 
219
 
            lock(self._lockfile, m)
220
 
            self._lock_mode = mode
221
 
            self._lock_count = 1
222
 
 
223
 
 
224
 
    def unlock(self):
225
 
        if not self._lock_mode:
226
 
            raise BzrError('branch %r is not locked' % (self))
227
 
 
228
 
        if self._lock_count > 1:
229
 
            self._lock_count -= 1
230
 
        else:
231
 
            assert self._lock_count == 1
232
 
            from bzrlib.lock import unlock
233
 
            unlock(self._lockfile)
234
 
            self._lock_mode = self._lock_count = None
235
 
 
236
 
 
237
 
    def abspath(self, name):
238
 
        """Return absolute filename for something in the branch"""
239
 
        return os.path.join(self.base, name)
240
 
 
241
 
 
242
 
    def relpath(self, path):
243
 
        """Return path relative to this branch of something inside it.
244
 
 
245
 
        Raises an error if path is not in this branch."""
246
 
        return _relpath(self.base, path)
247
 
 
248
 
 
249
 
    def controlfilename(self, file_or_path):
250
 
        """Return location relative to branch."""
251
 
        if isinstance(file_or_path, types.StringTypes):
252
 
            file_or_path = [file_or_path]
253
 
        return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path)
254
 
 
255
 
 
256
 
    def controlfile(self, file_or_path, mode='r'):
257
 
        """Open a control file for this branch.
258
 
 
259
 
        There are two classes of file in the control directory: text
260
 
        and binary.  binary files are untranslated byte streams.  Text
261
 
        control files are stored with Unix newlines and in UTF-8, even
262
 
        if the platform or locale defaults are different.
263
 
 
264
 
        Controlfiles should almost never be opened in write mode but
265
 
        rather should be atomically copied and replaced using atomicfile.
266
 
        """
267
 
 
268
 
        fn = self.controlfilename(file_or_path)
269
 
 
270
 
        if mode == 'rb' or mode == 'wb':
271
 
            return file(fn, mode)
272
 
        elif mode == 'r' or mode == 'w':
273
 
            # open in binary mode anyhow so there's no newline translation;
274
 
            # codecs uses line buffering by default; don't want that.
275
 
            import codecs
276
 
            return codecs.open(fn, mode + 'b', 'utf-8',
277
 
                               buffering=60000)
278
 
        else:
279
 
            raise BzrError("invalid controlfile mode %r" % mode)
280
 
 
281
 
 
282
 
 
283
 
    def _make_control(self):
284
 
        os.mkdir(self.controlfilename([]))
285
 
        self.controlfile('README', 'w').write(
286
 
            "This is a Bazaar-NG control directory.\n"
287
 
            "Do not change any files in this directory.")
288
 
        self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT)
289
 
        for d in ('text-store', 'inventory-store', 'revision-store'):
290
 
            os.mkdir(self.controlfilename(d))
291
 
        for f in ('revision-history', 'merged-patches',
292
 
                  'pending-merged-patches', 'branch-name',
293
 
                  'branch-lock'):
294
 
            self.controlfile(f, 'w').write('')
295
 
        mutter('created control directory in ' + self.base)
296
 
        Inventory().write_xml(self.controlfile('inventory','w'))
297
 
 
298
 
 
299
 
    def _check_format(self):
300
 
        """Check this branch format is supported.
301
 
 
302
 
        The current tool only supports the current unstable format.
303
 
 
304
 
        In the future, we might need different in-memory Branch
305
 
        classes to support downlevel branches.  But not yet.
306
 
        """
307
 
        # This ignores newlines so that we can open branches created
308
 
        # on Windows from Linux and so on.  I think it might be better
309
 
        # to always make all internal files in unix format.
310
 
        fmt = self.controlfile('branch-format', 'r').read()
311
 
        fmt.replace('\r\n', '')
312
 
        if fmt != BZR_BRANCH_FORMAT:
313
 
            raise BzrError('sorry, branch format %r not supported' % fmt,
314
 
                           ['use a different bzr version',
315
 
                            'or remove the .bzr directory and "bzr init" again'])
316
 
 
317
 
 
318
 
 
319
 
    @with_readlock
320
 
    def read_working_inventory(self):
321
 
        """Read the working inventory."""
322
 
        before = time.time()
323
 
        # ElementTree does its own conversion from UTF-8, so open in
324
 
        # binary.
325
 
        inv = Inventory.read_xml(self.controlfile('inventory', 'rb'))
326
 
        mutter("loaded inventory of %d items in %f"
327
 
               % (len(inv), time.time() - before))
328
 
        return inv
329
 
            
330
 
 
331
 
    def _write_inventory(self, inv):
332
 
        """Update the working inventory.
333
 
 
334
 
        That is to say, the inventory describing changes underway, that
335
 
        will be committed to the next revision.
336
 
        """
337
 
        ## TODO: factor out to atomicfile?  is rename safe on windows?
338
 
        ## TODO: Maybe some kind of clean/dirty marker on inventory?
339
 
        tmpfname = self.controlfilename('inventory.tmp')
340
 
        tmpf = file(tmpfname, 'wb')
341
 
        inv.write_xml(tmpf)
342
 
        tmpf.close()
343
 
        inv_fname = self.controlfilename('inventory')
344
 
        if sys.platform == 'win32':
345
 
            os.remove(inv_fname)
346
 
        os.rename(tmpfname, inv_fname)
347
 
        mutter('wrote working inventory')
348
 
            
349
 
 
350
 
    inventory = property(read_working_inventory, _write_inventory, None,
351
 
                         """Inventory for the working copy.""")
352
 
 
353
 
 
354
 
    @with_writelock
355
 
    def add(self, files, verbose=False, ids=None):
356
 
        """Make files versioned.
357
 
 
358
 
        Note that the command line normally calls smart_add instead.
359
 
 
360
 
        This puts the files in the Added state, so that they will be
361
 
        recorded by the next commit.
362
 
 
363
 
        files
364
 
            List of paths to add, relative to the base of the tree.
365
 
 
366
 
        ids
367
 
            If set, use these instead of automatically generated ids.
368
 
            Must be the same length as the list of files, but may
369
 
            contain None for ids that are to be autogenerated.
370
 
 
371
 
        TODO: Perhaps have an option to add the ids even if the files do
372
 
              not (yet) exist.
373
 
 
374
 
        TODO: Perhaps return the ids of the files?  But then again it
375
 
              is easy to retrieve them if they're needed.
376
 
 
377
 
        TODO: Adding a directory should optionally recurse down and
378
 
              add all non-ignored children.  Perhaps do that in a
379
 
              higher-level method.
380
 
        """
381
 
        # TODO: Re-adding a file that is removed in the working copy
382
 
        # should probably put it back with the previous ID.
383
 
        if isinstance(files, types.StringTypes):
384
 
            assert(ids is None or isinstance(ids, types.StringTypes))
385
 
            files = [files]
386
 
            if ids is not None:
387
 
                ids = [ids]
388
 
 
389
 
        if ids is None:
390
 
            ids = [None] * len(files)
391
 
        else:
392
 
            assert(len(ids) == len(files))
393
 
 
394
 
        inv = self.read_working_inventory()
395
 
        for f,file_id in zip(files, ids):
396
 
            if is_control_file(f):
397
 
                raise BzrError("cannot add control file %s" % quotefn(f))
398
 
 
399
 
            fp = splitpath(f)
400
 
 
401
 
            if len(fp) == 0:
402
 
                raise BzrError("cannot add top-level %r" % f)
403
 
 
404
 
            fullpath = os.path.normpath(self.abspath(f))
405
 
 
406
 
            try:
407
 
                kind = file_kind(fullpath)
408
 
            except OSError:
409
 
                # maybe something better?
410
 
                raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
411
 
 
412
 
            if kind != 'file' and kind != 'directory':
413
 
                raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
414
 
 
415
 
            if file_id is None:
416
 
                file_id = gen_file_id(f)
417
 
            inv.add_path(f, kind=kind, file_id=file_id)
418
 
 
419
 
            if verbose:
420
 
                show_status('A', kind, quotefn(f))
421
 
 
422
 
            mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
423
 
 
424
 
        self._write_inventory(inv)
425
 
            
426
 
 
427
 
    def print_file(self, file, revno):
428
 
        """Print `file` to stdout."""
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
 
 
436
 
 
437
 
    @with_writelock
438
 
    def remove(self, files, verbose=False):
439
 
        """Mark nominated files for removal from the inventory.
440
 
 
441
 
        This does not remove their text.  This does not run on 
442
 
 
443
 
        TODO: Refuse to remove modified files unless --force is given?
444
 
 
445
 
        TODO: Do something useful with directories.
446
 
 
447
 
        TODO: Should this remove the text or not?  Tough call; not
448
 
        removing may be useful and the user can just use use rm, and
449
 
        is the opposite of add.  Removing it is consistent with most
450
 
        other tools.  Maybe an option.
451
 
        """
452
 
        ## TODO: Normalize names
453
 
        ## TODO: Remove nested loops; better scalability
454
 
        if isinstance(files, types.StringTypes):
455
 
            files = [files]
456
 
 
457
 
        tree = self.working_tree()
458
 
        inv = tree.inventory
459
 
 
460
 
        # do this before any modifications
461
 
        for f in files:
462
 
            fid = inv.path2id(f)
463
 
            if not fid:
464
 
                raise BzrError("cannot remove unversioned file %s" % quotefn(f))
465
 
            mutter("remove inventory entry %s {%s}" % (quotefn(f), fid))
466
 
            if verbose:
467
 
                # having remove it, it must be either ignored or unknown
468
 
                if tree.is_ignored(f):
469
 
                    new_status = 'I'
470
 
                else:
471
 
                    new_status = '?'
472
 
                show_status(new_status, inv[fid].kind, quotefn(f))
473
 
            del inv[fid]
474
 
 
475
 
        self._write_inventory(inv)
476
 
 
477
 
 
478
 
    def set_inventory(self, new_inventory_list):
479
 
        inv = Inventory()
480
 
        for path, file_id, parent, kind in new_inventory_list:
481
 
            name = os.path.basename(path)
482
 
            if name == "":
483
 
                continue
484
 
            inv.add(InventoryEntry(file_id, name, kind, parent))
485
 
        self._write_inventory(inv)
486
 
 
487
 
 
488
 
    def unknowns(self):
489
 
        """Return all unknown files.
490
 
 
491
 
        These are files in the working directory that are not versioned or
492
 
        control files or ignored.
493
 
        
494
 
        >>> b = ScratchBranch(files=['foo', 'foo~'])
495
 
        >>> list(b.unknowns())
496
 
        ['foo']
497
 
        >>> b.add('foo')
498
 
        >>> list(b.unknowns())
499
 
        []
500
 
        >>> b.remove('foo')
501
 
        >>> list(b.unknowns())
502
 
        ['foo']
503
 
        """
504
 
        return self.working_tree().unknowns()
505
 
 
506
 
 
507
 
    def append_revision(self, revision_id):
508
 
        mutter("add {%s} to revision-history" % revision_id)
509
 
        rev_history = self.revision_history()
510
 
 
511
 
        tmprhname = self.controlfilename('revision-history.tmp')
512
 
        rhname = self.controlfilename('revision-history')
513
 
        
514
 
        f = file(tmprhname, 'wt')
515
 
        rev_history.append(revision_id)
516
 
        f.write('\n'.join(rev_history))
517
 
        f.write('\n')
518
 
        f.close()
519
 
 
520
 
        if sys.platform == 'win32':
521
 
            os.remove(rhname)
522
 
        os.rename(tmprhname, rhname)
523
 
        
524
 
 
525
 
 
526
 
    def get_revision(self, revision_id):
527
 
        """Return the Revision object for a named revision"""
528
 
        r = Revision.read_xml(self.revision_store[revision_id])
529
 
        assert r.revision_id == revision_id
530
 
        return r
531
 
 
532
 
 
533
 
    def get_inventory(self, inventory_id):
534
 
        """Get Inventory object by hash.
535
 
 
536
 
        TODO: Perhaps for this and similar methods, take a revision
537
 
               parameter which can be either an integer revno or a
538
 
               string hash."""
539
 
        i = Inventory.read_xml(self.inventory_store[inventory_id])
540
 
        return i
541
 
 
542
 
 
543
 
    def get_revision_inventory(self, revision_id):
544
 
        """Return inventory of a past revision."""
545
 
        if revision_id == None:
546
 
            return Inventory()
547
 
        else:
548
 
            return self.get_inventory(self.get_revision(revision_id).inventory_id)
549
 
 
550
 
 
551
 
    @with_readlock
552
 
    def revision_history(self):
553
 
        """Return sequence of revision hashes on to this branch.
554
 
 
555
 
        >>> ScratchBranch().revision_history()
556
 
        []
557
 
        """
558
 
        return [l.rstrip('\r\n') for l in self.controlfile('revision-history', 'r').readlines()]
559
 
 
560
 
 
561
 
    def enum_history(self, direction):
562
 
        """Return (revno, revision_id) for history of branch.
563
 
 
564
 
        direction
565
 
            'forward' is from earliest to latest
566
 
            'reverse' is from latest to earliest
567
 
        """
568
 
        rh = self.revision_history()
569
 
        if direction == 'forward':
570
 
            i = 1
571
 
            for rid in rh:
572
 
                yield i, rid
573
 
                i += 1
574
 
        elif direction == 'reverse':
575
 
            i = len(rh)
576
 
            while i > 0:
577
 
                yield i, rh[i-1]
578
 
                i -= 1
579
 
        else:
580
 
            raise ValueError('invalid history direction', direction)
581
 
 
582
 
 
583
 
    def revno(self):
584
 
        """Return current revision number for this branch.
585
 
 
586
 
        That is equivalent to the number of revisions committed to
587
 
        this branch.
588
 
        """
589
 
        return len(self.revision_history())
590
 
 
591
 
 
592
 
    def last_patch(self):
593
 
        """Return last patch hash, or None if no history.
594
 
        """
595
 
        ph = self.revision_history()
596
 
        if ph:
597
 
            return ph[-1]
598
 
        else:
599
 
            return None
600
 
 
601
 
 
602
 
    def commit(self, *args, **kw):
603
 
        """Deprecated"""
604
 
        from bzrlib.commit import commit
605
 
        commit(self, *args, **kw)
606
 
        
607
 
 
608
 
    def lookup_revision(self, revno):
609
 
        """Return revision hash for revision number."""
610
 
        if revno == 0:
611
 
            return None
612
 
 
613
 
        try:
614
 
            # list is 0-based; revisions are 1-based
615
 
            return self.revision_history()[revno-1]
616
 
        except IndexError:
617
 
            raise BzrError("no such revision %s" % revno)
618
 
 
619
 
 
620
 
    def revision_tree(self, revision_id):
621
 
        """Return Tree for a revision on this branch.
622
 
 
623
 
        `revision_id` may be None for the null revision, in which case
624
 
        an `EmptyTree` is returned."""
625
 
        # TODO: refactor this to use an existing revision object
626
 
        # so we don't need to read it in twice.
627
 
        if revision_id == None:
628
 
            return EmptyTree()
629
 
        else:
630
 
            inv = self.get_revision_inventory(revision_id)
631
 
            return RevisionTree(self.text_store, inv)
632
 
 
633
 
 
634
 
    def working_tree(self):
635
 
        """Return a `Tree` for the working copy."""
636
 
        from workingtree import WorkingTree
637
 
        return WorkingTree(self.base, self.read_working_inventory())
638
 
 
639
 
 
640
 
    def basis_tree(self):
641
 
        """Return `Tree` object for last revision.
642
 
 
643
 
        If there are no revisions yet, return an `EmptyTree`.
644
 
        """
645
 
        r = self.last_patch()
646
 
        if r == None:
647
 
            return EmptyTree()
648
 
        else:
649
 
            return RevisionTree(self.text_store, self.get_revision_inventory(r))
650
 
 
651
 
 
652
 
 
653
 
    @with_writelock
654
 
    def rename_one(self, from_rel, to_rel):
655
 
        """Rename one file.
656
 
 
657
 
        This can change the directory or the filename or both.
658
 
        """
659
 
        tree = self.working_tree()
660
 
        inv = tree.inventory
661
 
        if not tree.has_filename(from_rel):
662
 
            raise BzrError("can't rename: old working file %r does not exist" % from_rel)
663
 
        if tree.has_filename(to_rel):
664
 
            raise BzrError("can't rename: new working file %r already exists" % to_rel)
665
 
 
666
 
        file_id = inv.path2id(from_rel)
667
 
        if file_id == None:
668
 
            raise BzrError("can't rename: old name %r is not versioned" % from_rel)
669
 
 
670
 
        if inv.path2id(to_rel):
671
 
            raise BzrError("can't rename: new name %r is already versioned" % to_rel)
672
 
 
673
 
        to_dir, to_tail = os.path.split(to_rel)
674
 
        to_dir_id = inv.path2id(to_dir)
675
 
        if to_dir_id == None and to_dir != '':
676
 
            raise BzrError("can't determine destination directory id for %r" % to_dir)
677
 
 
678
 
        mutter("rename_one:")
679
 
        mutter("  file_id    {%s}" % file_id)
680
 
        mutter("  from_rel   %r" % from_rel)
681
 
        mutter("  to_rel     %r" % to_rel)
682
 
        mutter("  to_dir     %r" % to_dir)
683
 
        mutter("  to_dir_id  {%s}" % to_dir_id)
684
 
 
685
 
        inv.rename(file_id, to_dir_id, to_tail)
686
 
 
687
 
        print "%s => %s" % (from_rel, to_rel)
688
 
 
689
 
        from_abs = self.abspath(from_rel)
690
 
        to_abs = self.abspath(to_rel)
691
 
        try:
692
 
            os.rename(from_abs, to_abs)
693
 
        except OSError, e:
694
 
            raise BzrError("failed to rename %r to %r: %s"
695
 
                    % (from_abs, to_abs, e[1]),
696
 
                    ["rename rolled back"])
697
 
 
698
 
        self._write_inventory(inv)
699
 
 
700
 
 
701
 
 
702
 
    @with_writelock
703
 
    def move(self, from_paths, to_name):
704
 
        """Rename files.
705
 
 
706
 
        to_name must exist as a versioned directory.
707
 
 
708
 
        If to_name exists and is a directory, the files are moved into
709
 
        it, keeping their old names.  If it is a directory, 
710
 
 
711
 
        Note that to_name is only the last component of the new name;
712
 
        this doesn't change the directory.
713
 
        """
714
 
        ## TODO: Option to move IDs only
715
 
        assert not isinstance(from_paths, basestring)
716
 
        tree = self.working_tree()
717
 
        inv = tree.inventory
718
 
        to_abs = self.abspath(to_name)
719
 
        if not isdir(to_abs):
720
 
            raise BzrError("destination %r is not a directory" % to_abs)
721
 
        if not tree.has_filename(to_name):
722
 
            raise BzrError("destination %r not in working directory" % to_abs)
723
 
        to_dir_id = inv.path2id(to_name)
724
 
        if to_dir_id == None and to_name != '':
725
 
            raise BzrError("destination %r is not a versioned directory" % to_name)
726
 
        to_dir_ie = inv[to_dir_id]
727
 
        if to_dir_ie.kind not in ('directory', 'root_directory'):
728
 
            raise BzrError("destination %r is not a directory" % to_abs)
729
 
 
730
 
        to_idpath = inv.get_idpath(to_dir_id)
731
 
 
732
 
        for f in from_paths:
733
 
            if not tree.has_filename(f):
734
 
                raise BzrError("%r does not exist in working tree" % f)
735
 
            f_id = inv.path2id(f)
736
 
            if f_id == None:
737
 
                raise BzrError("%r is not versioned" % f)
738
 
            name_tail = splitpath(f)[-1]
739
 
            dest_path = appendpath(to_name, name_tail)
740
 
            if tree.has_filename(dest_path):
741
 
                raise BzrError("destination %r already exists" % dest_path)
742
 
            if f_id in to_idpath:
743
 
                raise BzrError("can't move %r to a subdirectory of itself" % f)
744
 
 
745
 
        # OK, so there's a race here, it's possible that someone will
746
 
        # create a file in this interval and then the rename might be
747
 
        # left half-done.  But we should have caught most problems.
748
 
 
749
 
        for f in from_paths:
750
 
            name_tail = splitpath(f)[-1]
751
 
            dest_path = appendpath(to_name, name_tail)
752
 
            print "%s => %s" % (f, dest_path)
753
 
            inv.rename(inv.path2id(f), to_dir_id, name_tail)
754
 
            try:
755
 
                os.rename(self.abspath(f), self.abspath(dest_path))
756
 
            except OSError, e:
757
 
                raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
758
 
                        ["rename rolled back"])
759
 
 
760
 
        self._write_inventory(inv)
761
 
 
762
 
 
763
 
 
764
 
 
765
 
class ScratchBranch(Branch):
766
 
    """Special test class: a branch that cleans up after itself.
767
 
 
768
 
    >>> b = ScratchBranch()
769
 
    >>> isdir(b.base)
770
 
    True
771
 
    >>> bd = b.base
772
 
    >>> b.destroy()
773
 
    >>> isdir(bd)
774
 
    False
775
 
    """
776
 
    def __init__(self, files=[], dirs=[]):
777
 
        """Make a test branch.
778
 
 
779
 
        This creates a temporary directory and runs init-tree in it.
780
 
 
781
 
        If any files are listed, they are created in the working copy.
782
 
        """
783
 
        Branch.__init__(self, tempfile.mkdtemp(), init=True)
784
 
        for d in dirs:
785
 
            os.mkdir(self.abspath(d))
786
 
            
787
 
        for f in files:
788
 
            file(os.path.join(self.base, f), 'w').write('content of %s' % f)
789
 
 
790
 
 
791
 
    def __del__(self):
792
 
        self.destroy()
793
 
 
794
 
    def destroy(self):
795
 
        """Destroy the test branch, removing the scratch directory."""
796
 
        try:
797
 
            mutter("delete ScratchBranch %s" % self.base)
798
 
            shutil.rmtree(self.base)
799
 
        except OSError, e:
800
 
            # Work around for shutil.rmtree failing on Windows when
801
 
            # readonly files are encountered
802
 
            mutter("hit exception in destroying ScratchBranch: %s" % e)
803
 
            for root, dirs, files in os.walk(self.base, topdown=False):
804
 
                for name in files:
805
 
                    os.chmod(os.path.join(root, name), 0700)
806
 
            shutil.rmtree(self.base)
807
 
        self.base = None
808
 
 
809
 
    
810
 
 
811
 
######################################################################
812
 
# predicates
813
 
 
814
 
 
815
 
def is_control_file(filename):
816
 
    ## FIXME: better check
817
 
    filename = os.path.normpath(filename)
818
 
    while filename != '':
819
 
        head, tail = os.path.split(filename)
820
 
        ## mutter('check %r for control file' % ((head, tail), ))
821
 
        if tail == bzrlib.BZRDIR:
822
 
            return True
823
 
        if filename == head:
824
 
            break
825
 
        filename = head
826
 
    return False
827
 
 
828
 
 
829
 
 
830
 
def gen_file_id(name):
831
 
    """Return new file id.
832
 
 
833
 
    This should probably generate proper UUIDs, but for the moment we
834
 
    cope with just randomness because running uuidgen every time is
835
 
    slow."""
836
 
    import re
837
 
 
838
 
    # get last component
839
 
    idx = name.rfind('/')
840
 
    if idx != -1:
841
 
        name = name[idx+1 : ]
842
 
    idx = name.rfind('\\')
843
 
    if idx != -1:
844
 
        name = name[idx+1 : ]
845
 
 
846
 
    # make it not a hidden file
847
 
    name = name.lstrip('.')
848
 
 
849
 
    # remove any wierd characters; we don't escape them but rather
850
 
    # just pull them out
851
 
    name = re.sub(r'[^\w.]', '', name)
852
 
 
853
 
    s = hexlify(rand_bytes(8))
854
 
    return '-'.join((name, compact_date(time.time()), s))