1
# Copyright (C) 2005 Canonical Ltd
 
 
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.
 
 
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.
 
 
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
 
 
20
from cStringIO import StringIO
 
 
23
from bzrlib.trace import mutter, note
 
 
24
from bzrlib.osutils import isdir, quotefn, compact_date, rand_bytes, \
 
 
26
     sha_file, appendpath, file_kind
 
 
28
from bzrlib.errors import (BzrError, InvalidRevisionNumber, InvalidRevisionId,
 
 
29
                           NoSuchRevision, HistoryMissing, NotBranchError,
 
 
31
from bzrlib.textui import show_status
 
 
32
from bzrlib.revision import Revision, validate_revision_id
 
 
33
from bzrlib.delta import compare_trees
 
 
34
from bzrlib.tree import EmptyTree, RevisionTree
 
 
35
from bzrlib.inventory import Inventory
 
 
36
from bzrlib.weavestore import WeaveStore
 
 
37
from bzrlib.store import ImmutableStore
 
 
42
INVENTORY_FILEID = '__inventory'
 
 
43
ANCESTRY_FILEID = '__ancestry'
 
 
46
BZR_BRANCH_FORMAT_4 = "Bazaar-NG branch, format 0.0.4\n"
 
 
47
BZR_BRANCH_FORMAT_5 = "Bazaar-NG branch, format 5\n"
 
 
48
## TODO: Maybe include checks for common corruption of newlines, etc?
 
 
51
# TODO: Some operations like log might retrieve the same revisions
 
 
52
# repeatedly to calculate deltas.  We could perhaps have a weakref
 
 
53
# cache in memory to make this faster.  In general anything can be
 
 
54
# cached in memory between lock and unlock operations.
 
 
56
# TODO: please move the revision-string syntax stuff out of the branch
 
 
57
# object; it's clutter
 
 
60
def find_branch(f, **args):
 
 
61
    if f and (f.startswith('http://') or f.startswith('https://')):
 
 
63
        return remotebranch.RemoteBranch(f, **args)
 
 
65
        return Branch(f, **args)
 
 
68
def find_cached_branch(f, cache_root, **args):
 
 
69
    from remotebranch import RemoteBranch
 
 
70
    br = find_branch(f, **args)
 
 
71
    def cacheify(br, store_name):
 
 
72
        from meta_store import CachedStore
 
 
73
        cache_path = os.path.join(cache_root, store_name)
 
 
75
        new_store = CachedStore(getattr(br, store_name), cache_path)
 
 
76
        setattr(br, store_name, new_store)
 
 
78
    if isinstance(br, RemoteBranch):
 
 
79
        cacheify(br, 'inventory_store')
 
 
80
        cacheify(br, 'text_store')
 
 
81
        cacheify(br, 'revision_store')
 
 
85
def _relpath(base, path):
 
 
86
    """Return path relative to base, or raise exception.
 
 
88
    The path may be either an absolute path or a path relative to the
 
 
89
    current working directory.
 
 
91
    Lifted out of Branch.relpath for ease of testing.
 
 
93
    os.path.commonprefix (python2.4) has a bad bug that it works just
 
 
94
    on string prefixes, assuming that '/u' is a prefix of '/u2'.  This
 
 
95
    avoids that problem."""
 
 
96
    rp = os.path.abspath(path)
 
 
100
    while len(head) >= len(base):
 
 
103
        head, tail = os.path.split(head)
 
 
107
        raise NotBranchError("path %r is not within branch %r" % (rp, base))
 
 
109
    return os.sep.join(s)
 
 
112
def find_branch_root(f=None):
 
 
113
    """Find the branch root enclosing f, or pwd.
 
 
115
    f may be a filename or a URL.
 
 
117
    It is not necessary that f exists.
 
 
119
    Basically we keep looking up until we find the control directory or
 
 
120
    run into the root.  If there isn't one, raises NotBranchError.
 
 
124
    elif hasattr(os.path, 'realpath'):
 
 
125
        f = os.path.realpath(f)
 
 
127
        f = os.path.abspath(f)
 
 
128
    if not os.path.exists(f):
 
 
129
        raise BzrError('%r does not exist' % f)
 
 
135
        if os.path.exists(os.path.join(f, bzrlib.BZRDIR)):
 
 
137
        head, tail = os.path.split(f)
 
 
139
            # reached the root, whatever that may be
 
 
140
            raise NotBranchError('%s is not in a branch' % orig_f)
 
 
145
# XXX: move into bzrlib.errors; subclass BzrError    
 
 
146
class DivergedBranches(Exception):
 
 
147
    def __init__(self, branch1, branch2):
 
 
148
        self.branch1 = branch1
 
 
149
        self.branch2 = branch2
 
 
150
        Exception.__init__(self, "These branches have diverged.")
 
 
153
######################################################################
 
 
156
class Branch(object):
 
 
157
    """Branch holding a history of revisions.
 
 
160
        Base directory of the branch.
 
 
166
        If _lock_mode is true, a positive count of the number of times the
 
 
170
        Lock object from bzrlib.lock.
 
 
176
    _inventory_weave = None
 
 
178
    # Map some sort of prefix into a namespace
 
 
179
    # stuff like "revno:10", "revid:", etc.
 
 
180
    # This should match a prefix with a function which accepts
 
 
181
    REVISION_NAMESPACES = {}
 
 
183
    def __init__(self, base, init=False, find_root=True,
 
 
184
                 relax_version_check=False):
 
 
185
        """Create new branch object at a particular location.
 
 
187
        base -- Base directory for the branch.
 
 
189
        init -- If True, create new control files in a previously
 
 
190
             unversioned directory.  If False, the branch must already
 
 
193
        find_root -- If true and init is false, find the root of the
 
 
194
             existing branch containing base.
 
 
196
        relax_version_check -- If true, the usual check for the branch
 
 
197
            version is not applied.  This is intended only for
 
 
198
            upgrade/recovery type use; it's not guaranteed that
 
 
199
            all operations will work on old format branches.
 
 
201
        In the test suite, creation of new trees is tested using the
 
 
202
        `ScratchBranch` class.
 
 
205
            self.base = os.path.realpath(base)
 
 
208
            self.base = find_branch_root(base)
 
 
210
            self.base = os.path.realpath(base)
 
 
211
            if not isdir(self.controlfilename('.')):
 
 
212
                raise NotBranchError('not a bzr branch: %s' % quotefn(base),
 
 
213
                                     ['use "bzr init" to initialize a '
 
 
216
        self._check_format(relax_version_check)
 
 
217
        if self._branch_format == 4:
 
 
218
            self.inventory_store = \
 
 
219
                ImmutableStore(self.controlfilename('inventory-store'))
 
 
221
                ImmutableStore(self.controlfilename('text-store'))
 
 
222
        self.weave_store = WeaveStore(self.controlfilename('weaves'))
 
 
223
        self.revision_store = \
 
 
224
            ImmutableStore(self.controlfilename('revision-store'))
 
 
228
        return '%s(%r)' % (self.__class__.__name__, self.base)
 
 
235
        if self._lock_mode or self._lock:
 
 
236
            from warnings import warn
 
 
237
            warn("branch %r was not explicitly unlocked" % self)
 
 
241
    def lock_write(self):
 
 
243
            if self._lock_mode != 'w':
 
 
244
                raise LockError("can't upgrade to a write lock from %r" %
 
 
246
            self._lock_count += 1
 
 
248
            from bzrlib.lock import WriteLock
 
 
250
            self._lock = WriteLock(self.controlfilename('branch-lock'))
 
 
251
            self._lock_mode = 'w'
 
 
257
            assert self._lock_mode in ('r', 'w'), \
 
 
258
                   "invalid lock mode %r" % self._lock_mode
 
 
259
            self._lock_count += 1
 
 
261
            from bzrlib.lock import ReadLock
 
 
263
            self._lock = ReadLock(self.controlfilename('branch-lock'))
 
 
264
            self._lock_mode = 'r'
 
 
268
        if not self._lock_mode:
 
 
269
            raise LockError('branch %r is not locked' % (self))
 
 
271
        if self._lock_count > 1:
 
 
272
            self._lock_count -= 1
 
 
276
            self._lock_mode = self._lock_count = None
 
 
278
    def abspath(self, name):
 
 
279
        """Return absolute filename for something in the branch"""
 
 
280
        return os.path.join(self.base, name)
 
 
282
    def relpath(self, path):
 
 
283
        """Return path relative to this branch of something inside it.
 
 
285
        Raises an error if path is not in this branch."""
 
 
286
        return _relpath(self.base, path)
 
 
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)
 
 
295
    def controlfile(self, file_or_path, mode='r'):
 
 
296
        """Open a control file for this branch.
 
 
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.
 
 
303
        Controlfiles should almost never be opened in write mode but
 
 
304
        rather should be atomically copied and replaced using atomicfile.
 
 
307
        fn = self.controlfilename(file_or_path)
 
 
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.
 
 
315
            return codecs.open(fn, mode + 'b', 'utf-8',
 
 
318
            raise BzrError("invalid controlfile mode %r" % mode)
 
 
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',
 
 
328
            os.mkdir(self.controlfilename(d))
 
 
329
        for f in ('revision-history', 'merged-patches',
 
 
330
                  'pending-merged-patches', 'branch-name',
 
 
333
            self.controlfile(f, 'w').write('')
 
 
334
        mutter('created control directory in ' + self.base)
 
 
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
 
 
339
        f = self.controlfile('inventory','w')
 
 
340
        bzrlib.xml5.serializer_v5.write_inventory(Inventory(), f)
 
 
344
    def _check_format(self, relax_version_check):
 
 
345
        """Check this branch format is supported.
 
 
347
        The format level is stored, as an integer, in
 
 
348
        self._branch_format for code that needs to check it later.
 
 
350
        In the future, we might need different in-memory Branch
 
 
351
        classes to support downlevel branches.  But not yet.
 
 
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
 
 
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'))
 
 
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
 
 
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
 
 
380
            if entry.parent_id in (None, orig_root_id):
 
 
381
                entry.parent_id = inv.root.file_id
 
 
382
        self._write_inventory(inv)
 
 
384
    def read_working_inventory(self):
 
 
385
        """Read the working inventory."""
 
 
388
            # ElementTree does its own conversion from UTF-8, so open in
 
 
390
            f = self.controlfile('inventory', 'rb')
 
 
391
            return bzrlib.xml5.serializer_v5.read_inventory(f)
 
 
396
    def _write_inventory(self, inv):
 
 
397
        """Update the working inventory.
 
 
399
        That is to say, the inventory describing changes underway, that
 
 
400
        will be committed to the next revision.
 
 
402
        from bzrlib.atomicfile import AtomicFile
 
 
406
            f = AtomicFile(self.controlfilename('inventory'), 'wb')
 
 
408
                bzrlib.xml5.serializer_v5.write_inventory(inv, f)
 
 
415
        mutter('wrote working inventory')
 
 
418
    inventory = property(read_working_inventory, _write_inventory, None,
 
 
419
                         """Inventory for the working copy.""")
 
 
422
    def add(self, files, ids=None):
 
 
423
        """Make files versioned.
 
 
425
        Note that the command line normally calls smart_add instead,
 
 
426
        which can automatically recurse.
 
 
428
        This puts the files in the Added state, so that they will be
 
 
429
        recorded by the next commit.
 
 
432
            List of paths to add, relative to the base of the tree.
 
 
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.
 
 
439
        TODO: Perhaps have an option to add the ids even if the files do
 
 
442
        TODO: Perhaps yield the ids and paths as they're added.
 
 
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))
 
 
453
            ids = [None] * len(files)
 
 
455
            assert(len(ids) == len(files))
 
 
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))
 
 
467
                    raise BzrError("cannot add top-level %r" % f)
 
 
469
                fullpath = os.path.normpath(self.abspath(f))
 
 
472
                    kind = file_kind(fullpath)
 
 
474
                    # maybe something better?
 
 
475
                    raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
 
 
477
                if kind != 'file' and kind != 'directory':
 
 
478
                    raise BzrError('cannot add: not a regular file or directory: %s' % quotefn(f))
 
 
481
                    file_id = gen_file_id(f)
 
 
482
                inv.add_path(f, kind=kind, file_id=file_id)
 
 
484
                mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
 
 
486
            self._write_inventory(inv)
 
 
491
    def print_file(self, file, revno):
 
 
492
        """Print `file` to stdout."""
 
 
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)
 
 
499
                raise BzrError("%r is not present in revision %s" % (file, revno))
 
 
500
            tree.print_file(file_id)
 
 
505
    def remove(self, files, verbose=False):
 
 
506
        """Mark nominated files for removal from the inventory.
 
 
508
        This does not remove their text.  This does not run on 
 
 
510
        TODO: Refuse to remove modified files unless --force is given?
 
 
512
        TODO: Do something useful with directories.
 
 
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.
 
 
519
        ## TODO: Normalize names
 
 
520
        ## TODO: Remove nested loops; better scalability
 
 
521
        if isinstance(files, basestring):
 
 
527
            tree = self.working_tree()
 
 
530
            # do this before any modifications
 
 
534
                    raise BzrError("cannot remove unversioned file %s" % quotefn(f))
 
 
535
                mutter("remove inventory entry %s {%s}" % (quotefn(f), fid))
 
 
537
                    # having remove it, it must be either ignored or unknown
 
 
538
                    if tree.is_ignored(f):
 
 
542
                    show_status(new_status, inv[fid].kind, quotefn(f))
 
 
545
            self._write_inventory(inv)
 
 
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)
 
 
558
            inv.add(InventoryEntry(file_id, name, kind, parent))
 
 
559
        self._write_inventory(inv)
 
 
563
        """Return all unknown files.
 
 
565
        These are files in the working directory that are not versioned or
 
 
566
        control files or ignored.
 
 
568
        >>> b = ScratchBranch(files=['foo', 'foo~'])
 
 
569
        >>> list(b.unknowns())
 
 
572
        >>> list(b.unknowns())
 
 
575
        >>> list(b.unknowns())
 
 
578
        return self.working_tree().unknowns()
 
 
581
    def append_revision(self, *revision_ids):
 
 
582
        from bzrlib.atomicfile import AtomicFile
 
 
584
        for revision_id in revision_ids:
 
 
585
            mutter("add {%s} to revision-history" % revision_id)
 
 
587
        rev_history = self.revision_history()
 
 
588
        rev_history.extend(revision_ids)
 
 
590
        f = AtomicFile(self.controlfilename('revision-history'))
 
 
592
            for rev_id in rev_history:
 
 
599
    def has_revision(self, revision_id):
 
 
600
        """True if this branch has a copy of the revision.
 
 
602
        This does not necessarily imply the revision is merge
 
 
603
        or on the mainline."""
 
 
604
        return revision_id in self.revision_store
 
 
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)
 
 
615
                return self.revision_store[revision_id]
 
 
617
                raise bzrlib.errors.NoSuchRevision(self, revision_id)
 
 
622
    def get_revision_xml(self, revision_id):
 
 
623
        return self.get_revision_xml_file(revision_id).read()
 
 
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)
 
 
631
            r = bzrlib.xml5.serializer_v5.read_revision(xml_file)
 
 
632
        except SyntaxError, e:
 
 
633
            raise bzrlib.errors.BzrError('failed to unpack revision_xml',
 
 
637
        assert r.revision_id == revision_id
 
 
641
    def get_revision_delta(self, revno):
 
 
642
        """Return the delta for one revision.
 
 
644
        The delta is relative to its mainline predecessor, or the
 
 
645
        empty tree for revision 1.
 
 
647
        assert isinstance(revno, int)
 
 
648
        rh = self.revision_history()
 
 
649
        if not (1 <= revno <= len(rh)):
 
 
650
            raise InvalidRevisionNumber(revno)
 
 
652
        # revno is 1-based; list is 0-based
 
 
654
        new_tree = self.revision_tree(rh[revno-1])
 
 
656
            old_tree = EmptyTree()
 
 
658
            old_tree = self.revision_tree(rh[revno-2])
 
 
660
        return compare_trees(old_tree, new_tree)
 
 
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))
 
 
669
    def get_ancestry(self, revision_id):
 
 
670
        """Return a list of revision-ids integrated by a revision.
 
 
672
        w = self.weave_store.get_weave(ANCESTRY_FILEID)
 
 
674
        return [l[:-1] for l in w.get_iter(w.lookup(revision_id))]
 
 
677
    def get_inventory_weave(self):
 
 
678
        return self.weave_store.get_weave(INVENTORY_FILEID)
 
 
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)
 
 
688
    def get_inventory_xml(self, revision_id):
 
 
689
        """Get inventory XML as a file object."""
 
 
691
            assert isinstance(revision_id, basestring), type(revision_id)
 
 
692
            iw = self.get_inventory_weave()
 
 
693
            return iw.get_text(iw.lookup(revision_id))
 
 
695
            raise bzrlib.errors.HistoryMissing(self, 'inventory', revision_id)
 
 
698
    def get_inventory_sha1(self, revision_id):
 
 
699
        """Return the sha1 hash of the inventory entry
 
 
701
        return self.get_revision(revision_id).inventory_sha1
 
 
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())
 
 
711
            return self.get_inventory(revision_id)
 
 
714
    def revision_history(self):
 
 
715
        """Return sequence of revision hashes on to this branch."""
 
 
718
            return [l.rstrip('\r\n') for l in
 
 
719
                    self.controlfile('revision-history', 'r').readlines()]
 
 
724
    def common_ancestor(self, other, self_revno=None, other_revno=None):
 
 
727
        >>> sb = ScratchBranch(files=['foo', 'foo~'])
 
 
728
        >>> sb.common_ancestor(sb) == (None, None)
 
 
730
        >>> commit.commit(sb, "Committing first revision")
 
 
731
        >>> sb.common_ancestor(sb)[0]
 
 
733
        >>> clone = sb.clone()
 
 
734
        >>> commit.commit(sb, "Committing second revision")
 
 
735
        >>> sb.common_ancestor(sb)[0]
 
 
737
        >>> sb.common_ancestor(clone)[0]
 
 
739
        >>> commit.commit(clone, "Committing divergent second revision")
 
 
740
        >>> sb.common_ancestor(clone)[0]
 
 
742
        >>> sb.common_ancestor(clone) == clone.common_ancestor(sb)
 
 
744
        >>> sb.common_ancestor(sb) != clone.common_ancestor(clone)
 
 
746
        >>> clone2 = sb.clone()
 
 
747
        >>> sb.common_ancestor(clone2)[0]
 
 
749
        >>> sb.common_ancestor(clone2, self_revno=1)[0]
 
 
751
        >>> sb.common_ancestor(clone2, other_revno=1)[0]
 
 
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)))
 
 
763
            if my_history[r] == other_history[r]:
 
 
764
                return r+1, my_history[r]
 
 
769
        """Return current revision number for this branch.
 
 
771
        That is equivalent to the number of revisions committed to
 
 
774
        return len(self.revision_history())
 
 
777
    def last_revision(self):
 
 
778
        """Return last patch hash, or None if no history.
 
 
780
        ph = self.revision_history()
 
 
787
    def missing_revisions(self, other, stop_revision=None, diverged_ok=False):
 
 
788
        """Return a list of new revisions that would perfectly fit.
 
 
790
        If self and other have not diverged, return a list of the revisions
 
 
791
        present in other, but missing from self.
 
 
793
        >>> from bzrlib.commit import commit
 
 
794
        >>> bzrlib.trace.silent = True
 
 
795
        >>> br1 = ScratchBranch()
 
 
796
        >>> br2 = ScratchBranch()
 
 
797
        >>> br1.missing_revisions(br2)
 
 
799
        >>> commit(br2, "lala!", rev_id="REVISION-ID-1")
 
 
800
        >>> br1.missing_revisions(br2)
 
 
802
        >>> br2.missing_revisions(br1)
 
 
804
        >>> commit(br1, "lala!", rev_id="REVISION-ID-1")
 
 
805
        >>> br1.missing_revisions(br2)
 
 
807
        >>> commit(br2, "lala!", rev_id="REVISION-ID-2A")
 
 
808
        >>> br1.missing_revisions(br2)
 
 
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.
 
 
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)
 
 
827
        if stop_revision is None:
 
 
828
            stop_revision = other_len
 
 
830
            assert isinstance(stop_revision, int)
 
 
831
            if stop_revision > other_len:
 
 
832
                raise bzrlib.errors.NoSuchRevision(self, stop_revision)
 
 
834
        return other_history[self_len:stop_revision]
 
 
837
    def update_revisions(self, other, stop_revno=None):
 
 
838
        """Pull in new perfect-fit revisions.
 
 
840
        from bzrlib.fetch import greedy_fetch
 
 
843
            stop_revision = other.lookup_revision(stop_revno)
 
 
846
        greedy_fetch(to_branch=self, from_branch=other,
 
 
847
                     revision=stop_revision)
 
 
849
        pullable_revs = self.missing_revisions(other, stop_revision)
 
 
852
            greedy_fetch(to_branch=self,
 
 
854
                         revision=pullable_revs[-1])
 
 
855
            self.append_revision(*pullable_revs)
 
 
858
    def commit(self, *args, **kw):
 
 
859
        from bzrlib.commit import Commit
 
 
860
        Commit().commit(self, *args, **kw)
 
 
863
    def lookup_revision(self, revision):
 
 
864
        """Return the revision identifier for a given revision information."""
 
 
865
        revno, info = self._get_revision_info(revision)
 
 
869
    def revision_id_to_revno(self, revision_id):
 
 
870
        """Given a revision id, return its revno"""
 
 
871
        history = self.revision_history()
 
 
873
            return history.index(revision_id) + 1
 
 
875
            raise bzrlib.errors.NoSuchRevision(self, revision_id)
 
 
878
    def get_revision_info(self, revision):
 
 
879
        """Return (revno, revision id) for revision identifier.
 
 
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.
 
 
886
        revno, rev_id = self._get_revision_info(revision)
 
 
888
            raise bzrlib.errors.NoSuchRevision(self, revision)
 
 
891
    def get_rev_id(self, revno, history=None):
 
 
892
        """Find the revision id of the specified revno."""
 
 
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]
 
 
901
    def _get_revision_info(self, revision):
 
 
902
        """Return (revno, revision id) for revision specifier.
 
 
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.
 
 
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.
 
 
917
        try:# Convert to int if possible
 
 
918
            revision = int(revision)
 
 
921
        revs = self.revision_history()
 
 
922
        if isinstance(revision, int):
 
 
924
                revno = len(revs) + revision + 1
 
 
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)
 
 
933
                        revno, rev_id = result
 
 
936
                        rev_id = self.get_rev_id(revno, revs)
 
 
939
                raise BzrError('No namespace registered for string: %r' %
 
 
942
            raise TypeError('Unhandled revision type %s' % revision)
 
 
946
                raise bzrlib.errors.NoSuchRevision(self, revision)
 
 
949
    def _namespace_revno(self, revs, revision):
 
 
950
        """Lookup a revision by revision number"""
 
 
951
        assert revision.startswith('revno:')
 
 
953
            return (int(revision[6:]),)
 
 
956
    REVISION_NAMESPACES['revno:'] = _namespace_revno
 
 
958
    def _namespace_revid(self, revs, revision):
 
 
959
        assert revision.startswith('revid:')
 
 
960
        rev_id = revision[len('revid:'):]
 
 
962
            return revs.index(rev_id) + 1, rev_id
 
 
965
    REVISION_NAMESPACES['revid:'] = _namespace_revid
 
 
967
    def _namespace_last(self, revs, revision):
 
 
968
        assert revision.startswith('last:')
 
 
970
            offset = int(revision[5:])
 
 
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
 
 
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
 
 
984
    def _namespace_date(self, revs, revision):
 
 
985
        assert revision.startswith('date:')
 
 
987
        # Spec for date revisions:
 
 
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.
 
 
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
 
 
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
 
 
1004
        if val[:1] in ('+', '-', '='):
 
 
1005
            match_style = val[:1]
 
 
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':
 
 
1013
        elif val.lower() == 'tomorrow':
 
 
1014
            dt = today + datetime.timedelta(days=1)
 
 
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))?'
 
 
1021
                    r'(?P<time>(?P<hour>\d\d):(?P<minute>\d\d)(:(?P<second>\d\d))?)?'
 
 
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)
 
 
1028
                year, month, day = int(m.group('year')), int(m.group('month')), int(m.group('day'))
 
 
1030
                year, month, day = today.year, today.month, today.day
 
 
1032
                hour = int(m.group('hour'))
 
 
1033
                minute = int(m.group('minute'))
 
 
1034
                if m.group('second'):
 
 
1035
                    second = int(m.group('second'))
 
 
1039
                hour, minute, second = 0,0,0
 
 
1041
            dt = datetime.datetime(year=year, month=month, day=day,
 
 
1042
                    hour=hour, minute=minute, second=second)
 
 
1046
        if match_style == '-':
 
 
1048
        elif match_style == '=':
 
 
1049
            last = dt + datetime.timedelta(days=1)
 
 
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):
 
 
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):
 
 
1065
    REVISION_NAMESPACES['date:'] = _namespace_date
 
 
1067
    def revision_tree(self, revision_id):
 
 
1068
        """Return Tree for a revision on this branch.
 
 
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:
 
 
1077
            inv = self.get_revision_inventory(revision_id)
 
 
1078
            return RevisionTree(self.weave_store, inv, revision_id)
 
 
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())
 
 
1087
    def basis_tree(self):
 
 
1088
        """Return `Tree` object for last revision.
 
 
1090
        If there are no revisions yet, return an `EmptyTree`.
 
 
1092
        return self.revision_tree(self.last_revision())
 
 
1095
    def rename_one(self, from_rel, to_rel):
 
 
1098
        This can change the directory or the filename or both.
 
 
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)
 
 
1109
            file_id = inv.path2id(from_rel)
 
 
1111
                raise BzrError("can't rename: old name %r is not versioned" % from_rel)
 
 
1113
            if inv.path2id(to_rel):
 
 
1114
                raise BzrError("can't rename: new name %r is already versioned" % to_rel)
 
 
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)
 
 
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)
 
 
1128
            inv.rename(file_id, to_dir_id, to_tail)
 
 
1130
            from_abs = self.abspath(from_rel)
 
 
1131
            to_abs = self.abspath(to_rel)
 
 
1133
                os.rename(from_abs, to_abs)
 
 
1135
                raise BzrError("failed to rename %r to %r: %s"
 
 
1136
                        % (from_abs, to_abs, e[1]),
 
 
1137
                        ["rename rolled back"])
 
 
1139
            self._write_inventory(inv)
 
 
1144
    def move(self, from_paths, to_name):
 
 
1147
        to_name must exist as a versioned directory.
 
 
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, 
 
 
1152
        Note that to_name is only the last component of the new name;
 
 
1153
        this doesn't change the directory.
 
 
1155
        This returns a list of (from_path, to_path) pairs for each
 
 
1156
        entry that is moved.
 
 
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)
 
 
1177
            to_idpath = inv.get_idpath(to_dir_id)
 
 
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)
 
 
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)
 
 
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.
 
 
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)
 
 
1202
                    os.rename(self.abspath(f), self.abspath(dest_path))
 
 
1204
                    raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
 
 
1205
                            ["rename rolled back"])
 
 
1207
            self._write_inventory(inv)
 
 
1214
    def revert(self, filenames, old_tree=None, backups=True):
 
 
1215
        """Restore selected files to the versions from a previous tree.
 
 
1218
            If true (default) backups are made of files before
 
 
1221
        from bzrlib.errors import NotVersionedError, BzrError
 
 
1222
        from bzrlib.atomicfile import AtomicFile
 
 
1223
        from bzrlib.osutils import backup_file
 
 
1225
        inv = self.read_working_inventory()
 
 
1226
        if old_tree is None:
 
 
1227
            old_tree = self.basis_tree()
 
 
1228
        old_inv = old_tree.inventory
 
 
1231
        for fn in filenames:
 
 
1232
            file_id = inv.path2id(fn)
 
 
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))
 
 
1239
        # TODO: Rename back if it was previously at a different location
 
 
1241
        # TODO: If given a directory, restore the entire contents from
 
 
1242
        # the previous version.
 
 
1244
        # TODO: Make a backup to a temporary file.
 
 
1246
        # TODO: If the file previously didn't exist, delete it?
 
 
1247
        for fn, file_id in nids:
 
 
1250
            f = AtomicFile(fn, 'wb')
 
 
1252
                f.write(old_tree.get_file(file_id).read())
 
 
1258
    def pending_merges(self):
 
 
1259
        """Return a list of pending merges.
 
 
1261
        These are revisions that have been merged into the working
 
 
1262
        directory but not yet committed.
 
 
1264
        cfn = self.controlfilename('pending-merges')
 
 
1265
        if not os.path.exists(cfn):
 
 
1268
        for l in self.controlfile('pending-merges', 'r').readlines():
 
 
1269
            p.append(l.rstrip('\n'))
 
 
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:
 
 
1280
        p.append(revision_id)
 
 
1281
        self.set_pending_merges(p)
 
 
1284
    def set_pending_merges(self, rev_list):
 
 
1285
        from bzrlib.atomicfile import AtomicFile
 
 
1288
            f = AtomicFile(self.controlfilename('pending-merges'))
 
 
1299
    def get_parent(self):
 
 
1300
        """Return the parent location of the branch.
 
 
1302
        This is the default location for push/pull/missing.  The usual
 
 
1303
        pattern is that the user can override it by specifying a
 
 
1307
        _locs = ['parent', 'pull', 'x-pull']
 
 
1310
                return self.controlfile(l, 'r').read().strip('\n')
 
 
1312
                if e.errno != errno.ENOENT:
 
 
1317
    def set_parent(self, url):
 
 
1318
        # TODO: Maybe delete old location files?
 
 
1319
        from bzrlib.atomicfile import AtomicFile
 
 
1322
            f = AtomicFile(self.controlfilename('parent'))
 
 
1331
    def check_revno(self, revno):
 
 
1333
        Check whether a revno corresponds to any revision.
 
 
1334
        Zero (the NULL revision) is considered valid.
 
 
1337
            self.check_real_revno(revno)
 
 
1339
    def check_real_revno(self, revno):
 
 
1341
        Check whether a revno corresponds to a real revision.
 
 
1342
        Zero (the NULL revision) is considered invalid
 
 
1344
        if revno < 1 or revno > self.revno():
 
 
1345
            raise InvalidRevisionNumber(revno)
 
 
1350
class ScratchBranch(Branch):
 
 
1351
    """Special test class: a branch that cleans up after itself.
 
 
1353
    >>> b = ScratchBranch()
 
 
1361
    def __init__(self, files=[], dirs=[], base=None):
 
 
1362
        """Make a test branch.
 
 
1364
        This creates a temporary directory and runs init-tree in it.
 
 
1366
        If any files are listed, they are created in the working copy.
 
 
1368
        from tempfile import mkdtemp
 
 
1373
        Branch.__init__(self, base, init=init)
 
 
1375
            os.mkdir(self.abspath(d))
 
 
1378
            file(os.path.join(self.base, f), 'w').write('content of %s' % f)
 
 
1383
        >>> orig = ScratchBranch(files=["file1", "file2"])
 
 
1384
        >>> clone = orig.clone()
 
 
1385
        >>> os.path.samefile(orig.base, clone.base)
 
 
1387
        >>> os.path.isfile(os.path.join(clone.base, "file1"))
 
 
1390
        from shutil import copytree
 
 
1391
        from tempfile import mkdtemp
 
 
1394
        copytree(self.base, base, symlinks=True)
 
 
1395
        return ScratchBranch(base=base)
 
 
1403
        """Destroy the test branch, removing the scratch directory."""
 
 
1404
        from shutil import rmtree
 
 
1407
                mutter("delete ScratchBranch %s" % self.base)
 
 
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):
 
 
1415
                    os.chmod(os.path.join(root, name), 0700)
 
 
1421
######################################################################
 
 
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:
 
 
1433
        if filename == head:
 
 
1440
def gen_file_id(name):
 
 
1441
    """Return new file id.
 
 
1443
    This should probably generate proper UUIDs, but for the moment we
 
 
1444
    cope with just randomness because running uuidgen every time is
 
 
1447
    from binascii import hexlify
 
 
1448
    from time import time
 
 
1450
    # get last component
 
 
1451
    idx = name.rfind('/')
 
 
1453
        name = name[idx+1 : ]
 
 
1454
    idx = name.rfind('\\')
 
 
1456
        name = name[idx+1 : ]
 
 
1458
    # make it not a hidden file
 
 
1459
    name = name.lstrip('.')
 
 
1461
    # remove any wierd characters; we don't escape them but rather
 
 
1462
    # just pull them out
 
 
1463
    name = re.sub(r'[^\w.]', '', name)
 
 
1465
    s = hexlify(rand_bytes(8))
 
 
1466
    return '-'.join((name, compact_date(time()), s))
 
 
1470
    """Return a new tree-root file id."""
 
 
1471
    return gen_file_id('TREE_ROOT')
 
 
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
 
 
1483
def copy_branch(branch_from, to_location, revision=None):
 
 
1484
    """Copy branch_from into the existing directory to_location.
 
 
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
 
 
1492
        The name of a local directory that exists but is empty.
 
 
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
 
 
1500
    assert isinstance(branch_from, Branch)
 
 
1501
    assert isinstance(to_location, basestring)
 
 
1503
    br_to = Branch(to_location, init=True)
 
 
1504
    br_to.set_root_id(branch_from.get_root_id())
 
 
1505
    if revision is None:
 
 
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)
 
 
1513
    from_location = pull_loc(branch_from)
 
 
1514
    br_to.set_parent(pull_loc(branch_from))