/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-08-17 23:31:01 UTC
  • Revision ID: mbp@sourcefrog.net-20050817233101-0939da1cf91f2472
- comment out unused test cases so that the install 
  script doesn't complain

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