/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: aaron.bentley at utoronto
  • Date: 2005-08-26 06:34:07 UTC
  • mto: (1185.3.4)
  • mto: This revision was merged to the branch mainline in revision 1178.
  • Revision ID: aaron.bentley@utoronto.ca-20050826063406-84d09e206c6c5e73
Shortened conflict markers to 7 characters, to please smerge

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
 
19
import os
 
20
 
 
21
import bzrlib
 
22
from bzrlib.trace import mutter, note
 
23
from bzrlib.osutils import isdir, quotefn, compact_date, rand_bytes, \
 
24
     splitpath, \
 
25
     sha_file, appendpath, file_kind
 
26
from bzrlib.errors import BzrError, InvalidRevisionNumber, InvalidRevisionId
 
27
import bzrlib.errors
 
28
from bzrlib.textui import show_status
 
29
from bzrlib.revision import Revision
 
30
from bzrlib.xml import unpack_xml
 
31
from bzrlib.delta import compare_trees
 
32
from bzrlib.tree import EmptyTree, RevisionTree
 
33
from bzrlib.progress import ProgressBar
 
34
 
 
35
       
 
36
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
 
37
## TODO: Maybe include checks for common corruption of newlines, etc?
 
38
 
 
39
 
 
40
# TODO: Some operations like log might retrieve the same revisions
 
41
# repeatedly to calculate deltas.  We could perhaps have a weakref
 
42
# cache in memory to make this faster.
 
43
 
 
44
# TODO: please move the revision-string syntax stuff out of the branch
 
45
# object; it's clutter
 
46
 
 
47
 
 
48
def find_branch(f, **args):
 
49
    if f and (f.startswith('http://') or f.startswith('https://')):
 
50
        import remotebranch 
 
51
        return remotebranch.RemoteBranch(f, **args)
 
52
    else:
 
53
        return Branch(f, **args)
 
54
 
 
55
 
 
56
def find_cached_branch(f, cache_root, **args):
 
57
    from remotebranch import RemoteBranch
 
58
    br = find_branch(f, **args)
 
59
    def cacheify(br, store_name):
 
60
        from meta_store import CachedStore
 
61
        cache_path = os.path.join(cache_root, store_name)
 
62
        os.mkdir(cache_path)
 
63
        new_store = CachedStore(getattr(br, store_name), cache_path)
 
64
        setattr(br, store_name, new_store)
 
65
 
 
66
    if isinstance(br, RemoteBranch):
 
67
        cacheify(br, 'inventory_store')
 
68
        cacheify(br, 'text_store')
 
69
        cacheify(br, 'revision_store')
 
70
    return br
 
71
 
 
72
 
 
73
def _relpath(base, path):
 
74
    """Return path relative to base, or raise exception.
 
75
 
 
76
    The path may be either an absolute path or a path relative to the
 
77
    current working directory.
 
78
 
 
79
    Lifted out of Branch.relpath for ease of testing.
 
80
 
 
81
    os.path.commonprefix (python2.4) has a bad bug that it works just
 
82
    on string prefixes, assuming that '/u' is a prefix of '/u2'.  This
 
83
    avoids that problem."""
 
84
    rp = os.path.abspath(path)
 
85
 
 
86
    s = []
 
87
    head = rp
 
88
    while len(head) >= len(base):
 
89
        if head == base:
 
90
            break
 
91
        head, tail = os.path.split(head)
 
92
        if tail:
 
93
            s.insert(0, tail)
 
94
    else:
 
95
        from errors import NotBranchError
 
96
        raise NotBranchError("path %r is not within branch %r" % (rp, base))
 
97
 
 
98
    return os.sep.join(s)
 
99
        
 
100
 
 
101
def find_branch_root(f=None):
 
102
    """Find the branch root enclosing f, or pwd.
 
103
 
 
104
    f may be a filename or a URL.
 
105
 
 
106
    It is not necessary that f exists.
 
107
 
 
108
    Basically we keep looking up until we find the control directory or
 
109
    run into the root."""
 
110
    if f == None:
 
111
        f = os.getcwd()
 
112
    elif hasattr(os.path, 'realpath'):
 
113
        f = os.path.realpath(f)
 
114
    else:
 
115
        f = os.path.abspath(f)
 
116
    if not os.path.exists(f):
 
117
        raise BzrError('%r does not exist' % f)
 
118
        
 
119
 
 
120
    orig_f = f
 
121
 
 
122
    while True:
 
123
        if os.path.exists(os.path.join(f, bzrlib.BZRDIR)):
 
124
            return f
 
125
        head, tail = os.path.split(f)
 
126
        if head == f:
 
127
            # reached the root, whatever that may be
 
128
            raise BzrError('%r is not in a branch' % orig_f)
 
129
        f = head
 
130
    
 
131
class DivergedBranches(Exception):
 
132
    def __init__(self, branch1, branch2):
 
133
        self.branch1 = branch1
 
134
        self.branch2 = branch2
 
135
        Exception.__init__(self, "These branches have diverged.")
 
136
 
 
137
 
 
138
######################################################################
 
139
# branch objects
 
140
 
 
141
class Branch(object):
 
142
    """Branch holding a history of revisions.
 
143
 
 
144
    base
 
145
        Base directory of the branch.
 
146
 
 
147
    _lock_mode
 
148
        None, or 'r' or 'w'
 
149
 
 
150
    _lock_count
 
151
        If _lock_mode is true, a positive count of the number of times the
 
152
        lock has been taken.
 
153
 
 
154
    _lock
 
155
        Lock object from bzrlib.lock.
 
156
    """
 
157
    base = None
 
158
    _lock_mode = None
 
159
    _lock_count = None
 
160
    _lock = None
 
161
    
 
162
    # Map some sort of prefix into a namespace
 
163
    # stuff like "revno:10", "revid:", etc.
 
164
    # This should match a prefix with a function which accepts
 
165
    REVISION_NAMESPACES = {}
 
166
 
 
167
    def __init__(self, base, init=False, find_root=True):
 
168
        """Create new branch object at a particular location.
 
169
 
 
170
        base -- Base directory for the branch.
 
171
        
 
172
        init -- If True, create new control files in a previously
 
173
             unversioned directory.  If False, the branch must already
 
174
             be versioned.
 
175
 
 
176
        find_root -- If true and init is false, find the root of the
 
177
             existing branch containing base.
 
178
 
 
179
        In the test suite, creation of new trees is tested using the
 
180
        `ScratchBranch` class.
 
181
        """
 
182
        from bzrlib.store import ImmutableStore
 
183
        if init:
 
184
            self.base = os.path.realpath(base)
 
185
            self._make_control()
 
186
        elif find_root:
 
187
            self.base = find_branch_root(base)
 
188
        else:
 
189
            self.base = os.path.realpath(base)
 
190
            if not isdir(self.controlfilename('.')):
 
191
                from errors import NotBranchError
 
192
                raise NotBranchError("not a bzr branch: %s" % quotefn(base),
 
193
                                     ['use "bzr init" to initialize a new working tree',
 
194
                                      'current bzr can only operate from top-of-tree'])
 
195
        self._check_format()
 
196
 
 
197
        self.text_store = ImmutableStore(self.controlfilename('text-store'))
 
198
        self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
 
199
        self.inventory_store = ImmutableStore(self.controlfilename('inventory-store'))
 
200
 
 
201
 
 
202
    def __str__(self):
 
203
        return '%s(%r)' % (self.__class__.__name__, self.base)
 
204
 
 
205
 
 
206
    __repr__ = __str__
 
207
 
 
208
 
 
209
    def __del__(self):
 
210
        if self._lock_mode or self._lock:
 
211
            from warnings import warn
 
212
            warn("branch %r was not explicitly unlocked" % self)
 
213
            self._lock.unlock()
 
214
 
 
215
 
 
216
 
 
217
    def lock_write(self):
 
218
        if self._lock_mode:
 
219
            if self._lock_mode != 'w':
 
220
                from errors import LockError
 
221
                raise LockError("can't upgrade to a write lock from %r" %
 
222
                                self._lock_mode)
 
223
            self._lock_count += 1
 
224
        else:
 
225
            from bzrlib.lock import WriteLock
 
226
 
 
227
            self._lock = WriteLock(self.controlfilename('branch-lock'))
 
228
            self._lock_mode = 'w'
 
229
            self._lock_count = 1
 
230
 
 
231
 
 
232
 
 
233
    def lock_read(self):
 
234
        if self._lock_mode:
 
235
            assert self._lock_mode in ('r', 'w'), \
 
236
                   "invalid lock mode %r" % self._lock_mode
 
237
            self._lock_count += 1
 
238
        else:
 
239
            from bzrlib.lock import ReadLock
 
240
 
 
241
            self._lock = ReadLock(self.controlfilename('branch-lock'))
 
242
            self._lock_mode = 'r'
 
243
            self._lock_count = 1
 
244
                        
 
245
 
 
246
            
 
247
    def unlock(self):
 
248
        if not self._lock_mode:
 
249
            from errors import LockError
 
250
            raise LockError('branch %r is not locked' % (self))
 
251
 
 
252
        if self._lock_count > 1:
 
253
            self._lock_count -= 1
 
254
        else:
 
255
            self._lock.unlock()
 
256
            self._lock = None
 
257
            self._lock_mode = self._lock_count = None
 
258
 
 
259
 
 
260
    def abspath(self, name):
 
261
        """Return absolute filename for something in the branch"""
 
262
        return os.path.join(self.base, name)
 
263
 
 
264
 
 
265
    def relpath(self, path):
 
266
        """Return path relative to this branch of something inside it.
 
267
 
 
268
        Raises an error if path is not in this branch."""
 
269
        return _relpath(self.base, path)
 
270
 
 
271
 
 
272
    def controlfilename(self, file_or_path):
 
273
        """Return location relative to branch."""
 
274
        if isinstance(file_or_path, basestring):
 
275
            file_or_path = [file_or_path]
 
276
        return os.path.join(self.base, bzrlib.BZRDIR, *file_or_path)
 
277
 
 
278
 
 
279
    def controlfile(self, file_or_path, mode='r'):
 
280
        """Open a control file for this branch.
 
281
 
 
282
        There are two classes of file in the control directory: text
 
283
        and binary.  binary files are untranslated byte streams.  Text
 
284
        control files are stored with Unix newlines and in UTF-8, even
 
285
        if the platform or locale defaults are different.
 
286
 
 
287
        Controlfiles should almost never be opened in write mode but
 
288
        rather should be atomically copied and replaced using atomicfile.
 
289
        """
 
290
 
 
291
        fn = self.controlfilename(file_or_path)
 
292
 
 
293
        if mode == 'rb' or mode == 'wb':
 
294
            return file(fn, mode)
 
295
        elif mode == 'r' or mode == 'w':
 
296
            # open in binary mode anyhow so there's no newline translation;
 
297
            # codecs uses line buffering by default; don't want that.
 
298
            import codecs
 
299
            return codecs.open(fn, mode + 'b', 'utf-8',
 
300
                               buffering=60000)
 
301
        else:
 
302
            raise BzrError("invalid controlfile mode %r" % mode)
 
303
 
 
304
 
 
305
 
 
306
    def _make_control(self):
 
307
        from bzrlib.inventory import Inventory
 
308
        from bzrlib.xml import pack_xml
 
309
        
 
310
        os.mkdir(self.controlfilename([]))
 
311
        self.controlfile('README', 'w').write(
 
312
            "This is a Bazaar-NG control directory.\n"
 
313
            "Do not change any files in this directory.\n")
 
314
        self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT)
 
315
        for d in ('text-store', 'inventory-store', 'revision-store'):
 
316
            os.mkdir(self.controlfilename(d))
 
317
        for f in ('revision-history', 'merged-patches',
 
318
                  'pending-merged-patches', 'branch-name',
 
319
                  'branch-lock',
 
320
                  'pending-merges'):
 
321
            self.controlfile(f, 'w').write('')
 
322
        mutter('created control directory in ' + self.base)
 
323
 
 
324
        # if we want per-tree root ids then this is the place to set
 
325
        # them; they're not needed for now and so ommitted for
 
326
        # simplicity.
 
327
        pack_xml(Inventory(), self.controlfile('inventory','w'))
 
328
 
 
329
 
 
330
    def _check_format(self):
 
331
        """Check this branch format is supported.
 
332
 
 
333
        The current tool only supports the current unstable format.
 
334
 
 
335
        In the future, we might need different in-memory Branch
 
336
        classes to support downlevel branches.  But not yet.
 
337
        """
 
338
        # This ignores newlines so that we can open branches created
 
339
        # on Windows from Linux and so on.  I think it might be better
 
340
        # to always make all internal files in unix format.
 
341
        fmt = self.controlfile('branch-format', 'r').read()
 
342
        fmt.replace('\r\n', '')
 
343
        if fmt != BZR_BRANCH_FORMAT:
 
344
            raise BzrError('sorry, branch format %r not supported' % fmt,
 
345
                           ['use a different bzr version',
 
346
                            'or remove the .bzr directory and "bzr init" again'])
 
347
 
 
348
    def get_root_id(self):
 
349
        """Return the id of this branches root"""
 
350
        inv = self.read_working_inventory()
 
351
        return inv.root.file_id
 
352
 
 
353
    def set_root_id(self, file_id):
 
354
        inv = self.read_working_inventory()
 
355
        orig_root_id = inv.root.file_id
 
356
        del inv._byid[inv.root.file_id]
 
357
        inv.root.file_id = file_id
 
358
        inv._byid[inv.root.file_id] = inv.root
 
359
        for fid in inv:
 
360
            entry = inv[fid]
 
361
            if entry.parent_id in (None, orig_root_id):
 
362
                entry.parent_id = inv.root.file_id
 
363
        self._write_inventory(inv)
 
364
 
 
365
    def read_working_inventory(self):
 
366
        """Read the working inventory."""
 
367
        from bzrlib.inventory import Inventory
 
368
        from bzrlib.xml import unpack_xml
 
369
        from time import time
 
370
        before = time()
 
371
        self.lock_read()
 
372
        try:
 
373
            # ElementTree does its own conversion from UTF-8, so open in
 
374
            # binary.
 
375
            inv = unpack_xml(Inventory,
 
376
                             self.controlfile('inventory', 'rb'))
 
377
            mutter("loaded inventory of %d items in %f"
 
378
                   % (len(inv), time() - before))
 
379
            return inv
 
380
        finally:
 
381
            self.unlock()
 
382
            
 
383
 
 
384
    def _write_inventory(self, inv):
 
385
        """Update the working inventory.
 
386
 
 
387
        That is to say, the inventory describing changes underway, that
 
388
        will be committed to the next revision.
 
389
        """
 
390
        from bzrlib.atomicfile import AtomicFile
 
391
        from bzrlib.xml import pack_xml
 
392
        
 
393
        self.lock_write()
 
394
        try:
 
395
            f = AtomicFile(self.controlfilename('inventory'), 'wb')
 
396
            try:
 
397
                pack_xml(inv, f)
 
398
                f.commit()
 
399
            finally:
 
400
                f.close()
 
401
        finally:
 
402
            self.unlock()
 
403
        
 
404
        mutter('wrote working inventory')
 
405
            
 
406
 
 
407
    inventory = property(read_working_inventory, _write_inventory, None,
 
408
                         """Inventory for the working copy.""")
 
409
 
 
410
 
 
411
    def add(self, files, verbose=False, ids=None):
 
412
        """Make files versioned.
 
413
 
 
414
        Note that the command line normally calls smart_add instead.
 
415
 
 
416
        This puts the files in the Added state, so that they will be
 
417
        recorded by the next commit.
 
418
 
 
419
        files
 
420
            List of paths to add, relative to the base of the tree.
 
421
 
 
422
        ids
 
423
            If set, use these instead of automatically generated ids.
 
424
            Must be the same length as the list of files, but may
 
425
            contain None for ids that are to be autogenerated.
 
426
 
 
427
        TODO: Perhaps have an option to add the ids even if the files do
 
428
              not (yet) exist.
 
429
 
 
430
        TODO: Perhaps return the ids of the files?  But then again it
 
431
              is easy to retrieve them if they're needed.
 
432
 
 
433
        TODO: Adding a directory should optionally recurse down and
 
434
              add all non-ignored children.  Perhaps do that in a
 
435
              higher-level method.
 
436
        """
 
437
        # TODO: Re-adding a file that is removed in the working copy
 
438
        # should probably put it back with the previous ID.
 
439
        if isinstance(files, basestring):
 
440
            assert(ids is None or isinstance(ids, basestring))
 
441
            files = [files]
 
442
            if ids is not None:
 
443
                ids = [ids]
 
444
 
 
445
        if ids is None:
 
446
            ids = [None] * len(files)
 
447
        else:
 
448
            assert(len(ids) == len(files))
 
449
 
 
450
        self.lock_write()
 
451
        try:
 
452
            inv = self.read_working_inventory()
 
453
            for f,file_id in zip(files, ids):
 
454
                if is_control_file(f):
 
455
                    raise BzrError("cannot add control file %s" % quotefn(f))
 
456
 
 
457
                fp = splitpath(f)
 
458
 
 
459
                if len(fp) == 0:
 
460
                    raise BzrError("cannot add top-level %r" % f)
 
461
 
 
462
                fullpath = os.path.normpath(self.abspath(f))
 
463
 
 
464
                try:
 
465
                    kind = file_kind(fullpath)
 
466
                except OSError:
 
467
                    # maybe something better?
 
468
                    raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
 
469
 
 
470
                if kind != 'file' and kind != 'directory':
 
471
                    raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
 
472
 
 
473
                if file_id is None:
 
474
                    file_id = gen_file_id(f)
 
475
                inv.add_path(f, kind=kind, file_id=file_id)
 
476
 
 
477
                if verbose:
 
478
                    print 'added', quotefn(f)
 
479
 
 
480
                mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
 
481
 
 
482
            self._write_inventory(inv)
 
483
        finally:
 
484
            self.unlock()
 
485
            
 
486
 
 
487
    def print_file(self, file, revno):
 
488
        """Print `file` to stdout."""
 
489
        self.lock_read()
 
490
        try:
 
491
            tree = self.revision_tree(self.lookup_revision(revno))
 
492
            # use inventory as it was in that revision
 
493
            file_id = tree.inventory.path2id(file)
 
494
            if not file_id:
 
495
                raise BzrError("%r is not present in revision %s" % (file, revno))
 
496
            tree.print_file(file_id)
 
497
        finally:
 
498
            self.unlock()
 
499
 
 
500
 
 
501
    def remove(self, files, verbose=False):
 
502
        """Mark nominated files for removal from the inventory.
 
503
 
 
504
        This does not remove their text.  This does not run on 
 
505
 
 
506
        TODO: Refuse to remove modified files unless --force is given?
 
507
 
 
508
        TODO: Do something useful with directories.
 
509
 
 
510
        TODO: Should this remove the text or not?  Tough call; not
 
511
        removing may be useful and the user can just use use rm, and
 
512
        is the opposite of add.  Removing it is consistent with most
 
513
        other tools.  Maybe an option.
 
514
        """
 
515
        ## TODO: Normalize names
 
516
        ## TODO: Remove nested loops; better scalability
 
517
        if isinstance(files, basestring):
 
518
            files = [files]
 
519
 
 
520
        self.lock_write()
 
521
 
 
522
        try:
 
523
            tree = self.working_tree()
 
524
            inv = tree.inventory
 
525
 
 
526
            # do this before any modifications
 
527
            for f in files:
 
528
                fid = inv.path2id(f)
 
529
                if not fid:
 
530
                    raise BzrError("cannot remove unversioned file %s" % quotefn(f))
 
531
                mutter("remove inventory entry %s {%s}" % (quotefn(f), fid))
 
532
                if verbose:
 
533
                    # having remove it, it must be either ignored or unknown
 
534
                    if tree.is_ignored(f):
 
535
                        new_status = 'I'
 
536
                    else:
 
537
                        new_status = '?'
 
538
                    show_status(new_status, inv[fid].kind, quotefn(f))
 
539
                del inv[fid]
 
540
 
 
541
            self._write_inventory(inv)
 
542
        finally:
 
543
            self.unlock()
 
544
 
 
545
 
 
546
    # FIXME: this doesn't need to be a branch method
 
547
    def set_inventory(self, new_inventory_list):
 
548
        from bzrlib.inventory import Inventory, InventoryEntry
 
549
        inv = Inventory(self.get_root_id())
 
550
        for path, file_id, parent, kind in new_inventory_list:
 
551
            name = os.path.basename(path)
 
552
            if name == "":
 
553
                continue
 
554
            inv.add(InventoryEntry(file_id, name, kind, parent))
 
555
        self._write_inventory(inv)
 
556
 
 
557
 
 
558
    def unknowns(self):
 
559
        """Return all unknown files.
 
560
 
 
561
        These are files in the working directory that are not versioned or
 
562
        control files or ignored.
 
563
        
 
564
        >>> b = ScratchBranch(files=['foo', 'foo~'])
 
565
        >>> list(b.unknowns())
 
566
        ['foo']
 
567
        >>> b.add('foo')
 
568
        >>> list(b.unknowns())
 
569
        []
 
570
        >>> b.remove('foo')
 
571
        >>> list(b.unknowns())
 
572
        ['foo']
 
573
        """
 
574
        return self.working_tree().unknowns()
 
575
 
 
576
 
 
577
    def append_revision(self, *revision_ids):
 
578
        from bzrlib.atomicfile import AtomicFile
 
579
 
 
580
        for revision_id in revision_ids:
 
581
            mutter("add {%s} to revision-history" % revision_id)
 
582
 
 
583
        rev_history = self.revision_history()
 
584
        rev_history.extend(revision_ids)
 
585
 
 
586
        f = AtomicFile(self.controlfilename('revision-history'))
 
587
        try:
 
588
            for rev_id in rev_history:
 
589
                print >>f, rev_id
 
590
            f.commit()
 
591
        finally:
 
592
            f.close()
 
593
 
 
594
 
 
595
    def get_revision_xml(self, revision_id):
 
596
        """Return XML file object for revision object."""
 
597
        if not revision_id or not isinstance(revision_id, basestring):
 
598
            raise InvalidRevisionId(revision_id)
 
599
 
 
600
        self.lock_read()
 
601
        try:
 
602
            try:
 
603
                return self.revision_store[revision_id]
 
604
            except IndexError:
 
605
                raise bzrlib.errors.NoSuchRevision(self, revision_id)
 
606
        finally:
 
607
            self.unlock()
 
608
 
 
609
 
 
610
    def get_revision(self, revision_id):
 
611
        """Return the Revision object for a named revision"""
 
612
        xml_file = self.get_revision_xml(revision_id)
 
613
 
 
614
        try:
 
615
            r = unpack_xml(Revision, xml_file)
 
616
        except SyntaxError, e:
 
617
            raise bzrlib.errors.BzrError('failed to unpack revision_xml',
 
618
                                         [revision_id,
 
619
                                          str(e)])
 
620
            
 
621
        assert r.revision_id == revision_id
 
622
        return r
 
623
 
 
624
 
 
625
    def get_revision_delta(self, revno):
 
626
        """Return the delta for one revision.
 
627
 
 
628
        The delta is relative to its mainline predecessor, or the
 
629
        empty tree for revision 1.
 
630
        """
 
631
        assert isinstance(revno, int)
 
632
        rh = self.revision_history()
 
633
        if not (1 <= revno <= len(rh)):
 
634
            raise InvalidRevisionNumber(revno)
 
635
 
 
636
        # revno is 1-based; list is 0-based
 
637
 
 
638
        new_tree = self.revision_tree(rh[revno-1])
 
639
        if revno == 1:
 
640
            old_tree = EmptyTree()
 
641
        else:
 
642
            old_tree = self.revision_tree(rh[revno-2])
 
643
 
 
644
        return compare_trees(old_tree, new_tree)
 
645
 
 
646
        
 
647
 
 
648
    def get_revision_sha1(self, revision_id):
 
649
        """Hash the stored value of a revision, and return it."""
 
650
        # In the future, revision entries will be signed. At that
 
651
        # point, it is probably best *not* to include the signature
 
652
        # in the revision hash. Because that lets you re-sign
 
653
        # the revision, (add signatures/remove signatures) and still
 
654
        # have all hash pointers stay consistent.
 
655
        # But for now, just hash the contents.
 
656
        return bzrlib.osutils.sha_file(self.get_revision_xml(revision_id))
 
657
 
 
658
 
 
659
    def get_inventory(self, inventory_id):
 
660
        """Get Inventory object by hash.
 
661
 
 
662
        TODO: Perhaps for this and similar methods, take a revision
 
663
               parameter which can be either an integer revno or a
 
664
               string hash."""
 
665
        from bzrlib.inventory import Inventory
 
666
        from bzrlib.xml import unpack_xml
 
667
 
 
668
        return unpack_xml(Inventory, self.get_inventory_xml(inventory_id))
 
669
 
 
670
 
 
671
    def get_inventory_xml(self, inventory_id):
 
672
        """Get inventory XML as a file object."""
 
673
        return self.inventory_store[inventory_id]
 
674
            
 
675
 
 
676
    def get_inventory_sha1(self, inventory_id):
 
677
        """Return the sha1 hash of the inventory entry
 
678
        """
 
679
        return sha_file(self.get_inventory_xml(inventory_id))
 
680
 
 
681
 
 
682
    def get_revision_inventory(self, revision_id):
 
683
        """Return inventory of a past revision."""
 
684
        # bzr 0.0.6 imposes the constraint that the inventory_id
 
685
        # must be the same as its revision, so this is trivial.
 
686
        if revision_id == None:
 
687
            from bzrlib.inventory import Inventory
 
688
            return Inventory(self.get_root_id())
 
689
        else:
 
690
            return self.get_inventory(revision_id)
 
691
 
 
692
 
 
693
    def revision_history(self):
 
694
        """Return sequence of revision hashes on to this branch.
 
695
 
 
696
        >>> ScratchBranch().revision_history()
 
697
        []
 
698
        """
 
699
        self.lock_read()
 
700
        try:
 
701
            return [l.rstrip('\r\n') for l in
 
702
                    self.controlfile('revision-history', 'r').readlines()]
 
703
        finally:
 
704
            self.unlock()
 
705
 
 
706
 
 
707
    def common_ancestor(self, other, self_revno=None, other_revno=None):
 
708
        """
 
709
        >>> import commit
 
710
        >>> sb = ScratchBranch(files=['foo', 'foo~'])
 
711
        >>> sb.common_ancestor(sb) == (None, None)
 
712
        True
 
713
        >>> commit.commit(sb, "Committing first revision", verbose=False)
 
714
        >>> sb.common_ancestor(sb)[0]
 
715
        1
 
716
        >>> clone = sb.clone()
 
717
        >>> commit.commit(sb, "Committing second revision", verbose=False)
 
718
        >>> sb.common_ancestor(sb)[0]
 
719
        2
 
720
        >>> sb.common_ancestor(clone)[0]
 
721
        1
 
722
        >>> commit.commit(clone, "Committing divergent second revision", 
 
723
        ...               verbose=False)
 
724
        >>> sb.common_ancestor(clone)[0]
 
725
        1
 
726
        >>> sb.common_ancestor(clone) == clone.common_ancestor(sb)
 
727
        True
 
728
        >>> sb.common_ancestor(sb) != clone.common_ancestor(clone)
 
729
        True
 
730
        >>> clone2 = sb.clone()
 
731
        >>> sb.common_ancestor(clone2)[0]
 
732
        2
 
733
        >>> sb.common_ancestor(clone2, self_revno=1)[0]
 
734
        1
 
735
        >>> sb.common_ancestor(clone2, other_revno=1)[0]
 
736
        1
 
737
        """
 
738
        my_history = self.revision_history()
 
739
        other_history = other.revision_history()
 
740
        if self_revno is None:
 
741
            self_revno = len(my_history)
 
742
        if other_revno is None:
 
743
            other_revno = len(other_history)
 
744
        indices = range(min((self_revno, other_revno)))
 
745
        indices.reverse()
 
746
        for r in indices:
 
747
            if my_history[r] == other_history[r]:
 
748
                return r+1, my_history[r]
 
749
        return None, None
 
750
 
 
751
 
 
752
    def revno(self):
 
753
        """Return current revision number for this branch.
 
754
 
 
755
        That is equivalent to the number of revisions committed to
 
756
        this branch.
 
757
        """
 
758
        return len(self.revision_history())
 
759
 
 
760
 
 
761
    def last_patch(self):
 
762
        """Return last patch hash, or None if no history.
 
763
        """
 
764
        ph = self.revision_history()
 
765
        if ph:
 
766
            return ph[-1]
 
767
        else:
 
768
            return None
 
769
 
 
770
 
 
771
    def missing_revisions(self, other, stop_revision=None, diverged_ok=False):
 
772
        """
 
773
        If self and other have not diverged, return a list of the revisions
 
774
        present in other, but missing from self.
 
775
 
 
776
        >>> from bzrlib.commit import commit
 
777
        >>> bzrlib.trace.silent = True
 
778
        >>> br1 = ScratchBranch()
 
779
        >>> br2 = ScratchBranch()
 
780
        >>> br1.missing_revisions(br2)
 
781
        []
 
782
        >>> commit(br2, "lala!", rev_id="REVISION-ID-1")
 
783
        >>> br1.missing_revisions(br2)
 
784
        [u'REVISION-ID-1']
 
785
        >>> br2.missing_revisions(br1)
 
786
        []
 
787
        >>> commit(br1, "lala!", rev_id="REVISION-ID-1")
 
788
        >>> br1.missing_revisions(br2)
 
789
        []
 
790
        >>> commit(br2, "lala!", rev_id="REVISION-ID-2A")
 
791
        >>> br1.missing_revisions(br2)
 
792
        [u'REVISION-ID-2A']
 
793
        >>> commit(br1, "lala!", rev_id="REVISION-ID-2B")
 
794
        >>> br1.missing_revisions(br2)
 
795
        Traceback (most recent call last):
 
796
        DivergedBranches: These branches have diverged.
 
797
        """
 
798
        self_history = self.revision_history()
 
799
        self_len = len(self_history)
 
800
        other_history = other.revision_history()
 
801
        other_len = len(other_history)
 
802
        common_index = min(self_len, other_len) -1
 
803
        if common_index >= 0 and \
 
804
            self_history[common_index] != other_history[common_index]:
 
805
            raise DivergedBranches(self, other)
 
806
 
 
807
        if stop_revision is None:
 
808
            stop_revision = other_len
 
809
        elif stop_revision > other_len:
 
810
            raise NoSuchRevision(self, stop_revision)
 
811
        
 
812
        return other_history[self_len:stop_revision]
 
813
 
 
814
 
 
815
    def update_revisions(self, other, stop_revision=None):
 
816
        """Pull in all new revisions from other branch.
 
817
        
 
818
        >>> from bzrlib.commit import commit
 
819
        >>> bzrlib.trace.silent = True
 
820
        >>> br1 = ScratchBranch(files=['foo', 'bar'])
 
821
        >>> br1.add('foo')
 
822
        >>> br1.add('bar')
 
823
        >>> commit(br1, "lala!", rev_id="REVISION-ID-1", verbose=False)
 
824
        >>> br2 = ScratchBranch()
 
825
        >>> br2.update_revisions(br1)
 
826
        Added 2 texts.
 
827
        Added 1 inventories.
 
828
        Added 1 revisions.
 
829
        >>> br2.revision_history()
 
830
        [u'REVISION-ID-1']
 
831
        >>> br2.update_revisions(br1)
 
832
        Added 0 revisions.
 
833
        >>> br1.text_store.total_size() == br2.text_store.total_size()
 
834
        True
 
835
        """
 
836
        from bzrlib.fetch import greedy_fetch
 
837
        pb = ProgressBar()
 
838
        pb.update('comparing histories')
 
839
        revision_ids = self.missing_revisions(other, stop_revision)
 
840
        if len(revision_ids) > 0:
 
841
            count = greedy_fetch(self, other, revision_ids[-1], pb)[0]
 
842
        else:
 
843
            count = 0
 
844
        self.append_revision(*revision_ids)
 
845
        print "Added %d revisions." % count
 
846
                    
 
847
    def install_revisions(self, other, revision_ids, pb=None):
 
848
        if pb is None:
 
849
            pb = ProgressBar()
 
850
        if hasattr(other.revision_store, "prefetch"):
 
851
            other.revision_store.prefetch(revision_ids)
 
852
        if hasattr(other.inventory_store, "prefetch"):
 
853
            inventory_ids = [other.get_revision(r).inventory_id
 
854
                             for r in revision_ids]
 
855
            other.inventory_store.prefetch(inventory_ids)
 
856
                
 
857
        revisions = []
 
858
        needed_texts = set()
 
859
        i = 0
 
860
        failures = set()
 
861
        for i, rev_id in enumerate(revision_ids):
 
862
            pb.update('fetching revision', i+1, len(revision_ids))
 
863
            try:
 
864
                rev = other.get_revision(rev_id)
 
865
            except bzrlib.errors.NoSuchRevision:
 
866
                failures.add(rev_id)
 
867
                continue
 
868
            revisions.append(rev)
 
869
            inv = other.get_inventory(str(rev.inventory_id))
 
870
            for key, entry in inv.iter_entries():
 
871
                if entry.text_id is None:
 
872
                    continue
 
873
                if entry.text_id not in self.text_store:
 
874
                    needed_texts.add(entry.text_id)
 
875
 
 
876
        pb.clear()
 
877
                    
 
878
        count, cp_fail = self.text_store.copy_multi(other.text_store, 
 
879
                                                    needed_texts)
 
880
        print "Added %d texts." % count 
 
881
        inventory_ids = [ f.inventory_id for f in revisions ]
 
882
        count, cp_fail = self.inventory_store.copy_multi(other.inventory_store, 
 
883
                                                         inventory_ids)
 
884
        print "Added %d inventories." % count 
 
885
        revision_ids = [ f.revision_id for f in revisions]
 
886
        count, cp_fail = self.revision_store.copy_multi(other.revision_store, 
 
887
                                                          revision_ids,
 
888
                                                          permit_failure=True)
 
889
        assert len(cp_fail) == 0 
 
890
        return count, failures
 
891
       
 
892
    def commit(self, *args, **kw):
 
893
        from bzrlib.commit import commit
 
894
        commit(self, *args, **kw)
 
895
        
 
896
 
 
897
    def lookup_revision(self, revision):
 
898
        """Return the revision identifier for a given revision information."""
 
899
        revno, info = self._get_revision_info(revision)
 
900
        return info
 
901
 
 
902
    def get_revision_info(self, revision):
 
903
        """Return (revno, revision id) for revision identifier.
 
904
 
 
905
        revision can be an integer, in which case it is assumed to be revno (though
 
906
            this will translate negative values into positive ones)
 
907
        revision can also be a string, in which case it is parsed for something like
 
908
            'date:' or 'revid:' etc.
 
909
        """
 
910
        revno, rev_id = self._get_revision_info(revision)
 
911
        if revno is None:
 
912
            raise bzrlib.errors.NoSuchRevision(self, revision)
 
913
        return revno, rev_id
 
914
 
 
915
    def get_rev_id(self, revno, history=None):
 
916
        """Find the revision id of the specified revno."""
 
917
        if revno == 0:
 
918
            return None
 
919
        if history is None:
 
920
            history = self.revision_history()
 
921
        elif revno <= 0 or revno > len(history):
 
922
            raise bzrlib.errors.NoSuchRevision(self, revno)
 
923
        return history[revno - 1]
 
924
 
 
925
    def _get_revision_info(self, revision):
 
926
        """Return (revno, revision id) for revision specifier.
 
927
 
 
928
        revision can be an integer, in which case it is assumed to be revno
 
929
        (though this will translate negative values into positive ones)
 
930
        revision can also be a string, in which case it is parsed for something
 
931
        like 'date:' or 'revid:' etc.
 
932
 
 
933
        A revid is always returned.  If it is None, the specifier referred to
 
934
        the null revision.  If the revid does not occur in the revision
 
935
        history, revno will be None.
 
936
        """
 
937
        
 
938
        if revision is None:
 
939
            return 0, None
 
940
        revno = None
 
941
        try:# Convert to int if possible
 
942
            revision = int(revision)
 
943
        except ValueError:
 
944
            pass
 
945
        revs = self.revision_history()
 
946
        if isinstance(revision, int):
 
947
            if revision < 0:
 
948
                revno = len(revs) + revision + 1
 
949
            else:
 
950
                revno = revision
 
951
            rev_id = self.get_rev_id(revno, revs)
 
952
        elif isinstance(revision, basestring):
 
953
            for prefix, func in Branch.REVISION_NAMESPACES.iteritems():
 
954
                if revision.startswith(prefix):
 
955
                    result = func(self, revs, revision)
 
956
                    if len(result) > 1:
 
957
                        revno, rev_id = result
 
958
                    else:
 
959
                        revno = result[0]
 
960
                        rev_id = self.get_rev_id(revno, revs)
 
961
                    break
 
962
            else:
 
963
                raise BzrError('No namespace registered for string: %r' %
 
964
                               revision)
 
965
        else:
 
966
            raise TypeError('Unhandled revision type %s' % revision)
 
967
 
 
968
        if revno is None:
 
969
            if rev_id is None:
 
970
                raise bzrlib.errors.NoSuchRevision(self, revision)
 
971
        return revno, rev_id
 
972
 
 
973
    def _namespace_revno(self, revs, revision):
 
974
        """Lookup a revision by revision number"""
 
975
        assert revision.startswith('revno:')
 
976
        try:
 
977
            return (int(revision[6:]),)
 
978
        except ValueError:
 
979
            return None
 
980
    REVISION_NAMESPACES['revno:'] = _namespace_revno
 
981
 
 
982
    def _namespace_revid(self, revs, revision):
 
983
        assert revision.startswith('revid:')
 
984
        rev_id = revision[len('revid:'):]
 
985
        try:
 
986
            return revs.index(rev_id) + 1, rev_id
 
987
        except ValueError:
 
988
            return None, rev_id
 
989
    REVISION_NAMESPACES['revid:'] = _namespace_revid
 
990
 
 
991
    def _namespace_last(self, revs, revision):
 
992
        assert revision.startswith('last:')
 
993
        try:
 
994
            offset = int(revision[5:])
 
995
        except ValueError:
 
996
            return (None,)
 
997
        else:
 
998
            if offset <= 0:
 
999
                raise BzrError('You must supply a positive value for --revision last:XXX')
 
1000
            return (len(revs) - offset + 1,)
 
1001
    REVISION_NAMESPACES['last:'] = _namespace_last
 
1002
 
 
1003
    def _namespace_tag(self, revs, revision):
 
1004
        assert revision.startswith('tag:')
 
1005
        raise BzrError('tag: namespace registered, but not implemented.')
 
1006
    REVISION_NAMESPACES['tag:'] = _namespace_tag
 
1007
 
 
1008
    def _namespace_date(self, revs, revision):
 
1009
        assert revision.startswith('date:')
 
1010
        import datetime
 
1011
        # Spec for date revisions:
 
1012
        #   date:value
 
1013
        #   value can be 'yesterday', 'today', 'tomorrow' or a YYYY-MM-DD string.
 
1014
        #   it can also start with a '+/-/='. '+' says match the first
 
1015
        #   entry after the given date. '-' is match the first entry before the date
 
1016
        #   '=' is match the first entry after, but still on the given date.
 
1017
        #
 
1018
        #   +2005-05-12 says find the first matching entry after May 12th, 2005 at 0:00
 
1019
        #   -2005-05-12 says find the first matching entry before May 12th, 2005 at 0:00
 
1020
        #   =2005-05-12 says find the first match after May 12th, 2005 at 0:00 but before
 
1021
        #       May 13th, 2005 at 0:00
 
1022
        #
 
1023
        #   So the proper way of saying 'give me all entries for today' is:
 
1024
        #       -r {date:+today}:{date:-tomorrow}
 
1025
        #   The default is '=' when not supplied
 
1026
        val = revision[5:]
 
1027
        match_style = '='
 
1028
        if val[:1] in ('+', '-', '='):
 
1029
            match_style = val[:1]
 
1030
            val = val[1:]
 
1031
 
 
1032
        today = datetime.datetime.today().replace(hour=0,minute=0,second=0,microsecond=0)
 
1033
        if val.lower() == 'yesterday':
 
1034
            dt = today - datetime.timedelta(days=1)
 
1035
        elif val.lower() == 'today':
 
1036
            dt = today
 
1037
        elif val.lower() == 'tomorrow':
 
1038
            dt = today + datetime.timedelta(days=1)
 
1039
        else:
 
1040
            import re
 
1041
            # This should be done outside the function to avoid recompiling it.
 
1042
            _date_re = re.compile(
 
1043
                    r'(?P<date>(?P<year>\d\d\d\d)-(?P<month>\d\d)-(?P<day>\d\d))?'
 
1044
                    r'(,|T)?\s*'
 
1045
                    r'(?P<time>(?P<hour>\d\d):(?P<minute>\d\d)(:(?P<second>\d\d))?)?'
 
1046
                )
 
1047
            m = _date_re.match(val)
 
1048
            if not m or (not m.group('date') and not m.group('time')):
 
1049
                raise BzrError('Invalid revision date %r' % revision)
 
1050
 
 
1051
            if m.group('date'):
 
1052
                year, month, day = int(m.group('year')), int(m.group('month')), int(m.group('day'))
 
1053
            else:
 
1054
                year, month, day = today.year, today.month, today.day
 
1055
            if m.group('time'):
 
1056
                hour = int(m.group('hour'))
 
1057
                minute = int(m.group('minute'))
 
1058
                if m.group('second'):
 
1059
                    second = int(m.group('second'))
 
1060
                else:
 
1061
                    second = 0
 
1062
            else:
 
1063
                hour, minute, second = 0,0,0
 
1064
 
 
1065
            dt = datetime.datetime(year=year, month=month, day=day,
 
1066
                    hour=hour, minute=minute, second=second)
 
1067
        first = dt
 
1068
        last = None
 
1069
        reversed = False
 
1070
        if match_style == '-':
 
1071
            reversed = True
 
1072
        elif match_style == '=':
 
1073
            last = dt + datetime.timedelta(days=1)
 
1074
 
 
1075
        if reversed:
 
1076
            for i in range(len(revs)-1, -1, -1):
 
1077
                r = self.get_revision(revs[i])
 
1078
                # TODO: Handle timezone.
 
1079
                dt = datetime.datetime.fromtimestamp(r.timestamp)
 
1080
                if first >= dt and (last is None or dt >= last):
 
1081
                    return (i+1,)
 
1082
        else:
 
1083
            for i in range(len(revs)):
 
1084
                r = self.get_revision(revs[i])
 
1085
                # TODO: Handle timezone.
 
1086
                dt = datetime.datetime.fromtimestamp(r.timestamp)
 
1087
                if first <= dt and (last is None or dt <= last):
 
1088
                    return (i+1,)
 
1089
    REVISION_NAMESPACES['date:'] = _namespace_date
 
1090
 
 
1091
    def revision_tree(self, revision_id):
 
1092
        """Return Tree for a revision on this branch.
 
1093
 
 
1094
        `revision_id` may be None for the null revision, in which case
 
1095
        an `EmptyTree` is returned."""
 
1096
        # TODO: refactor this to use an existing revision object
 
1097
        # so we don't need to read it in twice.
 
1098
        if revision_id == None:
 
1099
            return EmptyTree()
 
1100
        else:
 
1101
            inv = self.get_revision_inventory(revision_id)
 
1102
            return RevisionTree(self.text_store, inv)
 
1103
 
 
1104
 
 
1105
    def working_tree(self):
 
1106
        """Return a `Tree` for the working copy."""
 
1107
        from workingtree import WorkingTree
 
1108
        return WorkingTree(self.base, self.read_working_inventory())
 
1109
 
 
1110
 
 
1111
    def basis_tree(self):
 
1112
        """Return `Tree` object for last revision.
 
1113
 
 
1114
        If there are no revisions yet, return an `EmptyTree`.
 
1115
        """
 
1116
        r = self.last_patch()
 
1117
        if r == None:
 
1118
            return EmptyTree()
 
1119
        else:
 
1120
            return RevisionTree(self.text_store, self.get_revision_inventory(r))
 
1121
 
 
1122
 
 
1123
 
 
1124
    def rename_one(self, from_rel, to_rel):
 
1125
        """Rename one file.
 
1126
 
 
1127
        This can change the directory or the filename or both.
 
1128
        """
 
1129
        self.lock_write()
 
1130
        try:
 
1131
            tree = self.working_tree()
 
1132
            inv = tree.inventory
 
1133
            if not tree.has_filename(from_rel):
 
1134
                raise BzrError("can't rename: old working file %r does not exist" % from_rel)
 
1135
            if tree.has_filename(to_rel):
 
1136
                raise BzrError("can't rename: new working file %r already exists" % to_rel)
 
1137
 
 
1138
            file_id = inv.path2id(from_rel)
 
1139
            if file_id == None:
 
1140
                raise BzrError("can't rename: old name %r is not versioned" % from_rel)
 
1141
 
 
1142
            if inv.path2id(to_rel):
 
1143
                raise BzrError("can't rename: new name %r is already versioned" % to_rel)
 
1144
 
 
1145
            to_dir, to_tail = os.path.split(to_rel)
 
1146
            to_dir_id = inv.path2id(to_dir)
 
1147
            if to_dir_id == None and to_dir != '':
 
1148
                raise BzrError("can't determine destination directory id for %r" % to_dir)
 
1149
 
 
1150
            mutter("rename_one:")
 
1151
            mutter("  file_id    {%s}" % file_id)
 
1152
            mutter("  from_rel   %r" % from_rel)
 
1153
            mutter("  to_rel     %r" % to_rel)
 
1154
            mutter("  to_dir     %r" % to_dir)
 
1155
            mutter("  to_dir_id  {%s}" % to_dir_id)
 
1156
 
 
1157
            inv.rename(file_id, to_dir_id, to_tail)
 
1158
 
 
1159
            print "%s => %s" % (from_rel, to_rel)
 
1160
 
 
1161
            from_abs = self.abspath(from_rel)
 
1162
            to_abs = self.abspath(to_rel)
 
1163
            try:
 
1164
                os.rename(from_abs, to_abs)
 
1165
            except OSError, e:
 
1166
                raise BzrError("failed to rename %r to %r: %s"
 
1167
                        % (from_abs, to_abs, e[1]),
 
1168
                        ["rename rolled back"])
 
1169
 
 
1170
            self._write_inventory(inv)
 
1171
        finally:
 
1172
            self.unlock()
 
1173
 
 
1174
 
 
1175
    def move(self, from_paths, to_name):
 
1176
        """Rename files.
 
1177
 
 
1178
        to_name must exist as a versioned directory.
 
1179
 
 
1180
        If to_name exists and is a directory, the files are moved into
 
1181
        it, keeping their old names.  If it is a directory, 
 
1182
 
 
1183
        Note that to_name is only the last component of the new name;
 
1184
        this doesn't change the directory.
 
1185
        """
 
1186
        self.lock_write()
 
1187
        try:
 
1188
            ## TODO: Option to move IDs only
 
1189
            assert not isinstance(from_paths, basestring)
 
1190
            tree = self.working_tree()
 
1191
            inv = tree.inventory
 
1192
            to_abs = self.abspath(to_name)
 
1193
            if not isdir(to_abs):
 
1194
                raise BzrError("destination %r is not a directory" % to_abs)
 
1195
            if not tree.has_filename(to_name):
 
1196
                raise BzrError("destination %r not in working directory" % to_abs)
 
1197
            to_dir_id = inv.path2id(to_name)
 
1198
            if to_dir_id == None and to_name != '':
 
1199
                raise BzrError("destination %r is not a versioned directory" % to_name)
 
1200
            to_dir_ie = inv[to_dir_id]
 
1201
            if to_dir_ie.kind not in ('directory', 'root_directory'):
 
1202
                raise BzrError("destination %r is not a directory" % to_abs)
 
1203
 
 
1204
            to_idpath = inv.get_idpath(to_dir_id)
 
1205
 
 
1206
            for f in from_paths:
 
1207
                if not tree.has_filename(f):
 
1208
                    raise BzrError("%r does not exist in working tree" % f)
 
1209
                f_id = inv.path2id(f)
 
1210
                if f_id == None:
 
1211
                    raise BzrError("%r is not versioned" % f)
 
1212
                name_tail = splitpath(f)[-1]
 
1213
                dest_path = appendpath(to_name, name_tail)
 
1214
                if tree.has_filename(dest_path):
 
1215
                    raise BzrError("destination %r already exists" % dest_path)
 
1216
                if f_id in to_idpath:
 
1217
                    raise BzrError("can't move %r to a subdirectory of itself" % f)
 
1218
 
 
1219
            # OK, so there's a race here, it's possible that someone will
 
1220
            # create a file in this interval and then the rename might be
 
1221
            # left half-done.  But we should have caught most problems.
 
1222
 
 
1223
            for f in from_paths:
 
1224
                name_tail = splitpath(f)[-1]
 
1225
                dest_path = appendpath(to_name, name_tail)
 
1226
                print "%s => %s" % (f, dest_path)
 
1227
                inv.rename(inv.path2id(f), to_dir_id, name_tail)
 
1228
                try:
 
1229
                    os.rename(self.abspath(f), self.abspath(dest_path))
 
1230
                except OSError, e:
 
1231
                    raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
 
1232
                            ["rename rolled back"])
 
1233
 
 
1234
            self._write_inventory(inv)
 
1235
        finally:
 
1236
            self.unlock()
 
1237
 
 
1238
 
 
1239
    def revert(self, filenames, old_tree=None, backups=True):
 
1240
        """Restore selected files to the versions from a previous tree.
 
1241
 
 
1242
        backups
 
1243
            If true (default) backups are made of files before
 
1244
            they're renamed.
 
1245
        """
 
1246
        from bzrlib.errors import NotVersionedError, BzrError
 
1247
        from bzrlib.atomicfile import AtomicFile
 
1248
        from bzrlib.osutils import backup_file
 
1249
        
 
1250
        inv = self.read_working_inventory()
 
1251
        if old_tree is None:
 
1252
            old_tree = self.basis_tree()
 
1253
        old_inv = old_tree.inventory
 
1254
 
 
1255
        nids = []
 
1256
        for fn in filenames:
 
1257
            file_id = inv.path2id(fn)
 
1258
            if not file_id:
 
1259
                raise NotVersionedError("not a versioned file", fn)
 
1260
            if not old_inv.has_id(file_id):
 
1261
                raise BzrError("file not present in old tree", fn, file_id)
 
1262
            nids.append((fn, file_id))
 
1263
            
 
1264
        # TODO: Rename back if it was previously at a different location
 
1265
 
 
1266
        # TODO: If given a directory, restore the entire contents from
 
1267
        # the previous version.
 
1268
 
 
1269
        # TODO: Make a backup to a temporary file.
 
1270
 
 
1271
        # TODO: If the file previously didn't exist, delete it?
 
1272
        for fn, file_id in nids:
 
1273
            backup_file(fn)
 
1274
            
 
1275
            f = AtomicFile(fn, 'wb')
 
1276
            try:
 
1277
                f.write(old_tree.get_file(file_id).read())
 
1278
                f.commit()
 
1279
            finally:
 
1280
                f.close()
 
1281
 
 
1282
 
 
1283
    def pending_merges(self):
 
1284
        """Return a list of pending merges.
 
1285
 
 
1286
        These are revisions that have been merged into the working
 
1287
        directory but not yet committed.
 
1288
        """
 
1289
        cfn = self.controlfilename('pending-merges')
 
1290
        if not os.path.exists(cfn):
 
1291
            return []
 
1292
        p = []
 
1293
        for l in self.controlfile('pending-merges', 'r').readlines():
 
1294
            p.append(l.rstrip('\n'))
 
1295
        return p
 
1296
 
 
1297
 
 
1298
    def add_pending_merge(self, revision_id):
 
1299
        from bzrlib.revision import validate_revision_id
 
1300
 
 
1301
        validate_revision_id(revision_id)
 
1302
 
 
1303
        p = self.pending_merges()
 
1304
        if revision_id in p:
 
1305
            return
 
1306
        p.append(revision_id)
 
1307
        self.set_pending_merges(p)
 
1308
 
 
1309
 
 
1310
    def set_pending_merges(self, rev_list):
 
1311
        from bzrlib.atomicfile import AtomicFile
 
1312
        self.lock_write()
 
1313
        try:
 
1314
            f = AtomicFile(self.controlfilename('pending-merges'))
 
1315
            try:
 
1316
                for l in rev_list:
 
1317
                    print >>f, l
 
1318
                f.commit()
 
1319
            finally:
 
1320
                f.close()
 
1321
        finally:
 
1322
            self.unlock()
 
1323
 
 
1324
 
 
1325
 
 
1326
class ScratchBranch(Branch):
 
1327
    """Special test class: a branch that cleans up after itself.
 
1328
 
 
1329
    >>> b = ScratchBranch()
 
1330
    >>> isdir(b.base)
 
1331
    True
 
1332
    >>> bd = b.base
 
1333
    >>> b.destroy()
 
1334
    >>> isdir(bd)
 
1335
    False
 
1336
    """
 
1337
    def __init__(self, files=[], dirs=[], base=None):
 
1338
        """Make a test branch.
 
1339
 
 
1340
        This creates a temporary directory and runs init-tree in it.
 
1341
 
 
1342
        If any files are listed, they are created in the working copy.
 
1343
        """
 
1344
        from tempfile import mkdtemp
 
1345
        init = False
 
1346
        if base is None:
 
1347
            base = mkdtemp()
 
1348
            init = True
 
1349
        Branch.__init__(self, base, init=init)
 
1350
        for d in dirs:
 
1351
            os.mkdir(self.abspath(d))
 
1352
            
 
1353
        for f in files:
 
1354
            file(os.path.join(self.base, f), 'w').write('content of %s' % f)
 
1355
 
 
1356
 
 
1357
    def clone(self):
 
1358
        """
 
1359
        >>> orig = ScratchBranch(files=["file1", "file2"])
 
1360
        >>> clone = orig.clone()
 
1361
        >>> os.path.samefile(orig.base, clone.base)
 
1362
        False
 
1363
        >>> os.path.isfile(os.path.join(clone.base, "file1"))
 
1364
        True
 
1365
        """
 
1366
        from shutil import copytree
 
1367
        from tempfile import mkdtemp
 
1368
        base = mkdtemp()
 
1369
        os.rmdir(base)
 
1370
        copytree(self.base, base, symlinks=True)
 
1371
        return ScratchBranch(base=base)
 
1372
        
 
1373
    def __del__(self):
 
1374
        self.destroy()
 
1375
 
 
1376
    def destroy(self):
 
1377
        """Destroy the test branch, removing the scratch directory."""
 
1378
        from shutil import rmtree
 
1379
        try:
 
1380
            if self.base:
 
1381
                mutter("delete ScratchBranch %s" % self.base)
 
1382
                rmtree(self.base)
 
1383
        except OSError, e:
 
1384
            # Work around for shutil.rmtree failing on Windows when
 
1385
            # readonly files are encountered
 
1386
            mutter("hit exception in destroying ScratchBranch: %s" % e)
 
1387
            for root, dirs, files in os.walk(self.base, topdown=False):
 
1388
                for name in files:
 
1389
                    os.chmod(os.path.join(root, name), 0700)
 
1390
            rmtree(self.base)
 
1391
        self.base = None
 
1392
 
 
1393
    
 
1394
 
 
1395
######################################################################
 
1396
# predicates
 
1397
 
 
1398
 
 
1399
def is_control_file(filename):
 
1400
    ## FIXME: better check
 
1401
    filename = os.path.normpath(filename)
 
1402
    while filename != '':
 
1403
        head, tail = os.path.split(filename)
 
1404
        ## mutter('check %r for control file' % ((head, tail), ))
 
1405
        if tail == bzrlib.BZRDIR:
 
1406
            return True
 
1407
        if filename == head:
 
1408
            break
 
1409
        filename = head
 
1410
    return False
 
1411
 
 
1412
 
 
1413
 
 
1414
def gen_file_id(name):
 
1415
    """Return new file id.
 
1416
 
 
1417
    This should probably generate proper UUIDs, but for the moment we
 
1418
    cope with just randomness because running uuidgen every time is
 
1419
    slow."""
 
1420
    import re
 
1421
    from binascii import hexlify
 
1422
    from time import time
 
1423
 
 
1424
    # get last component
 
1425
    idx = name.rfind('/')
 
1426
    if idx != -1:
 
1427
        name = name[idx+1 : ]
 
1428
    idx = name.rfind('\\')
 
1429
    if idx != -1:
 
1430
        name = name[idx+1 : ]
 
1431
 
 
1432
    # make it not a hidden file
 
1433
    name = name.lstrip('.')
 
1434
 
 
1435
    # remove any wierd characters; we don't escape them but rather
 
1436
    # just pull them out
 
1437
    name = re.sub(r'[^\w.]', '', name)
 
1438
 
 
1439
    s = hexlify(rand_bytes(8))
 
1440
    return '-'.join((name, compact_date(time()), s))
 
1441
 
 
1442
 
 
1443
def gen_root_id():
 
1444
    """Return a new tree-root file id."""
 
1445
    return gen_file_id('TREE_ROOT')
 
1446