/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-06-01 04:09:38 UTC
  • Revision ID: mbp@sourcefrog.net-20050601040938-d905145b57ae017f
- unify two defintions of LockError

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