/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

  • Committer: Martin Pool
  • Date: 2005-05-30 03:14:07 UTC
  • Revision ID: mbp@sourcefrog.net-20050530031407-d37f43ff76a5e0d9
- tests for add --no-recurse

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