/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-31 02:45:19 UTC
  • Revision ID: mbp@sourcefrog.net-20050531024518-0e984fd2a34ac245
- new statcache format: use nul field separators rather than 
  unicode escaping to avoid troubles with backslashes in paths.

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))