/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-09-16 09:14:54 UTC
  • Revision ID: mbp@sourcefrog.net-20050916091454-092fc433a3d9cc31
- cosmetic

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