~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Robert Collins
  • Date: 2005-09-27 07:24:40 UTC
  • mfrom: (1185.1.41)
  • Revision ID: robertc@robertcollins.net-20050927072440-1bf4d99c3e1db5b3
pair programming worx... merge integration and weave

Show diffs side-by-side

added added

removed removed

Lines of Context:
15
15
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
16
16
 
17
17
 
18
 
import sys, os
 
18
import sys
 
19
import os
 
20
import errno
 
21
from warnings import warn
 
22
 
19
23
 
20
24
import bzrlib
21
25
from bzrlib.trace import mutter, note
22
 
from bzrlib.osutils import isdir, quotefn, compact_date, rand_bytes, splitpath, \
23
 
     sha_file, appendpath, file_kind
24
 
from bzrlib.errors import BzrError
25
 
 
26
 
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
 
26
from bzrlib.osutils import (isdir, quotefn, compact_date, rand_bytes, 
 
27
                            rename, splitpath, sha_file, appendpath, 
 
28
                            file_kind)
 
29
from bzrlib.errors import (BzrError, InvalidRevisionNumber, InvalidRevisionId,
 
30
                           NoSuchRevision, HistoryMissing, NotBranchError,
 
31
                           DivergedBranches, LockError)
 
32
from bzrlib.textui import show_status
 
33
from bzrlib.revision import Revision, validate_revision_id
 
34
from bzrlib.delta import compare_trees
 
35
from bzrlib.tree import EmptyTree, RevisionTree
 
36
from bzrlib.inventory import Inventory
 
37
from bzrlib.weavestore import WeaveStore
 
38
from bzrlib.store import ImmutableStore
 
39
import bzrlib.xml5
 
40
import bzrlib.ui
 
41
 
 
42
 
 
43
BZR_BRANCH_FORMAT_4 = "Bazaar-NG branch, format 0.0.4\n"
 
44
BZR_BRANCH_FORMAT_5 = "Bazaar-NG branch, format 5\n"
27
45
## TODO: Maybe include checks for common corruption of newlines, etc?
28
46
 
29
47
 
30
 
 
31
 
def find_branch(f, **args):
32
 
    if f and (f.startswith('http://') or f.startswith('https://')):
33
 
        import remotebranch 
34
 
        return remotebranch.RemoteBranch(f, **args)
35
 
    else:
36
 
        return Branch(f, **args)
37
 
 
38
 
 
39
 
def find_cached_branch(f, cache_root, **args):
40
 
    from remotebranch import RemoteBranch
41
 
    br = find_branch(f, **args)
42
 
    def cacheify(br, store_name):
43
 
        from meta_store import CachedStore
44
 
        cache_path = os.path.join(cache_root, store_name)
45
 
        os.mkdir(cache_path)
46
 
        new_store = CachedStore(getattr(br, store_name), cache_path)
47
 
        setattr(br, store_name, new_store)
48
 
 
49
 
    if isinstance(br, RemoteBranch):
50
 
        cacheify(br, 'inventory_store')
51
 
        cacheify(br, 'text_store')
52
 
        cacheify(br, 'revision_store')
53
 
    return br
54
 
 
 
48
# TODO: Some operations like log might retrieve the same revisions
 
49
# repeatedly to calculate deltas.  We could perhaps have a weakref
 
50
# cache in memory to make this faster.  In general anything can be
 
51
# cached in memory between lock and unlock operations.
 
52
 
 
53
def find_branch(*ignored, **ignored_too):
 
54
    # XXX: leave this here for about one release, then remove it
 
55
    raise NotImplementedError('find_branch() is not supported anymore, '
 
56
                              'please use one of the new branch constructors')
55
57
 
56
58
def _relpath(base, path):
57
59
    """Return path relative to base, or raise exception.
75
77
        if tail:
76
78
            s.insert(0, tail)
77
79
    else:
78
 
        from errors import NotBranchError
79
80
        raise NotBranchError("path %r is not within branch %r" % (rp, base))
80
81
 
81
82
    return os.sep.join(s)
89
90
    It is not necessary that f exists.
90
91
 
91
92
    Basically we keep looking up until we find the control directory or
92
 
    run into the root."""
 
93
    run into the root.  If there isn't one, raises NotBranchError.
 
94
    """
93
95
    if f == None:
94
96
        f = os.getcwd()
95
97
    elif hasattr(os.path, 'realpath'):
108
110
        head, tail = os.path.split(f)
109
111
        if head == f:
110
112
            # reached the root, whatever that may be
111
 
            raise BzrError('%r is not in a branch' % orig_f)
 
113
            raise NotBranchError('%s is not in a branch' % orig_f)
112
114
        f = head
113
 
    
114
 
class DivergedBranches(Exception):
115
 
    def __init__(self, branch1, branch2):
116
 
        self.branch1 = branch1
117
 
        self.branch2 = branch2
118
 
        Exception.__init__(self, "These branches have diverged.")
119
 
 
120
 
 
121
 
class NoSuchRevision(BzrError):
122
 
    def __init__(self, branch, revision):
123
 
        self.branch = branch
124
 
        self.revision = revision
125
 
        msg = "Branch %s has no revision %d" % (branch, revision)
126
 
        BzrError.__init__(self, msg)
 
115
 
 
116
 
127
117
 
128
118
 
129
119
######################################################################
133
123
    """Branch holding a history of revisions.
134
124
 
135
125
    base
136
 
        Base directory of the branch.
 
126
        Base directory/url of the branch.
 
127
    """
 
128
    base = None
 
129
 
 
130
    def __init__(self, *ignored, **ignored_too):
 
131
        raise NotImplementedError('The Branch class is abstract')
 
132
 
 
133
    @staticmethod
 
134
    def open(base):
 
135
        """Open an existing branch, rooted at 'base' (url)"""
 
136
        if base and (base.startswith('http://') or base.startswith('https://')):
 
137
            from bzrlib.remotebranch import RemoteBranch
 
138
            return RemoteBranch(base, find_root=False)
 
139
        else:
 
140
            return LocalBranch(base, find_root=False)
 
141
 
 
142
    @staticmethod
 
143
    def open_containing(url):
 
144
        """Open an existing branch which contains url.
 
145
        
 
146
        This probes for a branch at url, and searches upwards from there.
 
147
        """
 
148
        if url and (url.startswith('http://') or url.startswith('https://')):
 
149
            from bzrlib.remotebranch import RemoteBranch
 
150
            return RemoteBranch(url)
 
151
        else:
 
152
            return LocalBranch(url)
 
153
 
 
154
    @staticmethod
 
155
    def initialize(base):
 
156
        """Create a new branch, rooted at 'base' (url)"""
 
157
        if base and (base.startswith('http://') or base.startswith('https://')):
 
158
            from bzrlib.remotebranch import RemoteBranch
 
159
            return RemoteBranch(base, init=True)
 
160
        else:
 
161
            return LocalBranch(base, init=True)
 
162
 
 
163
    def setup_caching(self, cache_root):
 
164
        """Subclasses that care about caching should override this, and set
 
165
        up cached stores located under cache_root.
 
166
        """
 
167
 
 
168
 
 
169
class LocalBranch(Branch):
 
170
    """A branch stored in the actual filesystem.
 
171
 
 
172
    Note that it's "local" in the context of the filesystem; it doesn't
 
173
    really matter if it's on an nfs/smb/afs/coda/... share, as long as
 
174
    it's writable, and can be accessed via the normal filesystem API.
137
175
 
138
176
    _lock_mode
139
177
        None, or 'r' or 'w'
145
183
    _lock
146
184
        Lock object from bzrlib.lock.
147
185
    """
148
 
    base = None
 
186
    # We actually expect this class to be somewhat short-lived; part of its
 
187
    # purpose is to try to isolate what bits of the branch logic are tied to
 
188
    # filesystem access, so that in a later step, we can extricate them to
 
189
    # a separarte ("storage") class.
149
190
    _lock_mode = None
150
191
    _lock_count = None
151
192
    _lock = None
 
193
    _inventory_weave = None
152
194
    
153
195
    # Map some sort of prefix into a namespace
154
196
    # stuff like "revno:10", "revid:", etc.
155
197
    # This should match a prefix with a function which accepts
156
198
    REVISION_NAMESPACES = {}
157
199
 
158
 
    def __init__(self, base, init=False, find_root=True):
 
200
    def __init__(self, base, init=False, find_root=True,
 
201
                 relax_version_check=False):
159
202
        """Create new branch object at a particular location.
160
203
 
161
 
        base -- Base directory for the branch.
 
204
        base -- Base directory for the branch. May be a file:// url.
162
205
        
163
206
        init -- If True, create new control files in a previously
164
207
             unversioned directory.  If False, the branch must already
167
210
        find_root -- If true and init is false, find the root of the
168
211
             existing branch containing base.
169
212
 
 
213
        relax_version_check -- If true, the usual check for the branch
 
214
            version is not applied.  This is intended only for
 
215
            upgrade/recovery type use; it's not guaranteed that
 
216
            all operations will work on old format branches.
 
217
 
170
218
        In the test suite, creation of new trees is tested using the
171
219
        `ScratchBranch` class.
172
220
        """
173
 
        from bzrlib.store import ImmutableStore
174
221
        if init:
175
222
            self.base = os.path.realpath(base)
176
223
            self._make_control()
177
224
        elif find_root:
178
225
            self.base = find_branch_root(base)
179
226
        else:
 
227
            if base.startswith("file://"):
 
228
                base = base[7:]
180
229
            self.base = os.path.realpath(base)
181
230
            if not isdir(self.controlfilename('.')):
182
 
                from errors import NotBranchError
183
 
                raise NotBranchError("not a bzr branch: %s" % quotefn(base),
184
 
                                     ['use "bzr init" to initialize a new working tree',
185
 
                                      'current bzr can only operate from top-of-tree'])
186
 
        self._check_format()
187
 
 
188
 
        self.text_store = ImmutableStore(self.controlfilename('text-store'))
189
 
        self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
190
 
        self.inventory_store = ImmutableStore(self.controlfilename('inventory-store'))
 
231
                raise NotBranchError('not a bzr branch: %s' % quotefn(base),
 
232
                                     ['use "bzr init" to initialize a '
 
233
                                      'new working tree'])
 
234
        self._check_format(relax_version_check)
 
235
        cfn = self.controlfilename
 
236
        if self._branch_format == 4:
 
237
            self.inventory_store = ImmutableStore(cfn('inventory-store'))
 
238
            self.text_store = ImmutableStore(cfn('text-store'))
 
239
        elif self._branch_format == 5:
 
240
            self.control_weaves = WeaveStore(cfn([]))
 
241
            self.weave_store = WeaveStore(cfn('weaves'))
 
242
            if init:
 
243
                # FIXME: Unify with make_control_files
 
244
                self.control_weaves.put_empty_weave('inventory')
 
245
                self.control_weaves.put_empty_weave('ancestry')
 
246
        self.revision_store = ImmutableStore(cfn('revision-store'))
191
247
 
192
248
 
193
249
    def __str__(self):
199
255
 
200
256
    def __del__(self):
201
257
        if self._lock_mode or self._lock:
202
 
            from warnings import warn
 
258
            # XXX: This should show something every time, and be suitable for
 
259
            # headless operation and embedding
203
260
            warn("branch %r was not explicitly unlocked" % self)
204
261
            self._lock.unlock()
205
262
 
206
 
 
207
 
 
208
263
    def lock_write(self):
209
264
        if self._lock_mode:
210
265
            if self._lock_mode != 'w':
211
 
                from errors import LockError
212
266
                raise LockError("can't upgrade to a write lock from %r" %
213
267
                                self._lock_mode)
214
268
            self._lock_count += 1
220
274
            self._lock_count = 1
221
275
 
222
276
 
223
 
 
224
277
    def lock_read(self):
225
278
        if self._lock_mode:
226
279
            assert self._lock_mode in ('r', 'w'), \
233
286
            self._lock_mode = 'r'
234
287
            self._lock_count = 1
235
288
                        
236
 
 
237
 
            
238
289
    def unlock(self):
239
290
        if not self._lock_mode:
240
 
            from errors import LockError
241
291
            raise LockError('branch %r is not locked' % (self))
242
292
 
243
293
        if self._lock_count > 1:
247
297
            self._lock = None
248
298
            self._lock_mode = self._lock_count = None
249
299
 
250
 
 
251
300
    def abspath(self, name):
252
301
        """Return absolute filename for something in the branch"""
253
302
        return os.path.join(self.base, name)
254
303
 
255
 
 
256
304
    def relpath(self, path):
257
305
        """Return path relative to this branch of something inside it.
258
306
 
259
307
        Raises an error if path is not in this branch."""
260
308
        return _relpath(self.base, path)
261
309
 
262
 
 
263
310
    def controlfilename(self, file_or_path):
264
311
        """Return location relative to branch."""
265
312
        if isinstance(file_or_path, basestring):
292
339
        else:
293
340
            raise BzrError("invalid controlfile mode %r" % mode)
294
341
 
295
 
 
296
 
 
297
342
    def _make_control(self):
298
 
        from bzrlib.inventory import Inventory
299
 
        from bzrlib.xml import pack_xml
300
 
        
301
343
        os.mkdir(self.controlfilename([]))
302
344
        self.controlfile('README', 'w').write(
303
345
            "This is a Bazaar-NG control directory.\n"
304
346
            "Do not change any files in this directory.\n")
305
 
        self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT)
306
 
        for d in ('text-store', 'inventory-store', 'revision-store'):
 
347
        self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT_5)
 
348
        for d in ('text-store', 'revision-store',
 
349
                  'weaves'):
307
350
            os.mkdir(self.controlfilename(d))
308
 
        for f in ('revision-history', 'merged-patches',
309
 
                  'pending-merged-patches', 'branch-name',
 
351
        for f in ('revision-history',
 
352
                  'branch-name',
310
353
                  'branch-lock',
311
354
                  'pending-merges'):
312
355
            self.controlfile(f, 'w').write('')
313
356
        mutter('created control directory in ' + self.base)
314
357
 
315
 
        pack_xml(Inventory(gen_root_id()), self.controlfile('inventory','w'))
316
 
 
317
 
 
318
 
    def _check_format(self):
 
358
        # if we want per-tree root ids then this is the place to set
 
359
        # them; they're not needed for now and so ommitted for
 
360
        # simplicity.
 
361
        f = self.controlfile('inventory','w')
 
362
        bzrlib.xml5.serializer_v5.write_inventory(Inventory(), f)
 
363
 
 
364
 
 
365
    def _check_format(self, relax_version_check):
319
366
        """Check this branch format is supported.
320
367
 
321
 
        The current tool only supports the current unstable format.
 
368
        The format level is stored, as an integer, in
 
369
        self._branch_format for code that needs to check it later.
322
370
 
323
371
        In the future, we might need different in-memory Branch
324
372
        classes to support downlevel branches.  But not yet.
325
373
        """
326
 
        # This ignores newlines so that we can open branches created
327
 
        # on Windows from Linux and so on.  I think it might be better
328
 
        # to always make all internal files in unix format.
329
 
        fmt = self.controlfile('branch-format', 'r').read()
330
 
        fmt.replace('\r\n', '')
331
 
        if fmt != BZR_BRANCH_FORMAT:
 
374
        try:
 
375
            fmt = self.controlfile('branch-format', 'r').read()
 
376
        except IOError, e:
 
377
            if e.errno == errno.ENOENT:
 
378
                raise NotBranchError(self.base)
 
379
            else:
 
380
                raise
 
381
 
 
382
        if fmt == BZR_BRANCH_FORMAT_5:
 
383
            self._branch_format = 5
 
384
        elif fmt == BZR_BRANCH_FORMAT_4:
 
385
            self._branch_format = 4
 
386
 
 
387
        if (not relax_version_check
 
388
            and self._branch_format != 5):
332
389
            raise BzrError('sorry, branch format %r not supported' % fmt,
333
390
                           ['use a different bzr version',
334
391
                            'or remove the .bzr directory and "bzr init" again'])
352
409
 
353
410
    def read_working_inventory(self):
354
411
        """Read the working inventory."""
355
 
        from bzrlib.inventory import Inventory
356
 
        from bzrlib.xml import unpack_xml
357
 
        from time import time
358
 
        before = time()
359
412
        self.lock_read()
360
413
        try:
361
414
            # ElementTree does its own conversion from UTF-8, so open in
362
415
            # binary.
363
 
            inv = unpack_xml(Inventory,
364
 
                             self.controlfile('inventory', 'rb'))
365
 
            mutter("loaded inventory of %d items in %f"
366
 
                   % (len(inv), time() - before))
367
 
            return inv
 
416
            f = self.controlfile('inventory', 'rb')
 
417
            return bzrlib.xml5.serializer_v5.read_inventory(f)
368
418
        finally:
369
419
            self.unlock()
370
420
            
376
426
        will be committed to the next revision.
377
427
        """
378
428
        from bzrlib.atomicfile import AtomicFile
379
 
        from bzrlib.xml import pack_xml
380
429
        
381
430
        self.lock_write()
382
431
        try:
383
432
            f = AtomicFile(self.controlfilename('inventory'), 'wb')
384
433
            try:
385
 
                pack_xml(inv, f)
 
434
                bzrlib.xml5.serializer_v5.write_inventory(inv, f)
386
435
                f.commit()
387
436
            finally:
388
437
                f.close()
396
445
                         """Inventory for the working copy.""")
397
446
 
398
447
 
399
 
    def add(self, files, verbose=False, ids=None):
 
448
    def add(self, files, ids=None):
400
449
        """Make files versioned.
401
450
 
402
 
        Note that the command line normally calls smart_add instead.
 
451
        Note that the command line normally calls smart_add instead,
 
452
        which can automatically recurse.
403
453
 
404
454
        This puts the files in the Added state, so that they will be
405
455
        recorded by the next commit.
415
465
        TODO: Perhaps have an option to add the ids even if the files do
416
466
              not (yet) exist.
417
467
 
418
 
        TODO: Perhaps return the ids of the files?  But then again it
419
 
              is easy to retrieve them if they're needed.
420
 
 
421
 
        TODO: Adding a directory should optionally recurse down and
422
 
              add all non-ignored children.  Perhaps do that in a
423
 
              higher-level method.
 
468
        TODO: Perhaps yield the ids and paths as they're added.
424
469
        """
425
 
        from bzrlib.textui import show_status
426
470
        # TODO: Re-adding a file that is removed in the working copy
427
471
        # should probably put it back with the previous ID.
428
472
        if isinstance(files, basestring):
463
507
                    file_id = gen_file_id(f)
464
508
                inv.add_path(f, kind=kind, file_id=file_id)
465
509
 
466
 
                if verbose:
467
 
                    print 'added', quotefn(f)
468
 
 
469
510
                mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
470
511
 
471
512
            self._write_inventory(inv)
477
518
        """Print `file` to stdout."""
478
519
        self.lock_read()
479
520
        try:
480
 
            tree = self.revision_tree(self.lookup_revision(revno))
 
521
            tree = self.revision_tree(self.get_rev_id(revno))
481
522
            # use inventory as it was in that revision
482
523
            file_id = tree.inventory.path2id(file)
483
524
            if not file_id:
501
542
        is the opposite of add.  Removing it is consistent with most
502
543
        other tools.  Maybe an option.
503
544
        """
504
 
        from bzrlib.textui import show_status
505
545
        ## TODO: Normalize names
506
546
        ## TODO: Remove nested loops; better scalability
507
547
        if isinstance(files, basestring):
582
622
            f.close()
583
623
 
584
624
 
 
625
    def has_revision(self, revision_id):
 
626
        """True if this branch has a copy of the revision.
 
627
 
 
628
        This does not necessarily imply the revision is merge
 
629
        or on the mainline."""
 
630
        return (revision_id is None
 
631
                or revision_id in self.revision_store)
 
632
 
 
633
 
 
634
    def get_revision_xml_file(self, revision_id):
 
635
        """Return XML file object for revision object."""
 
636
        if not revision_id or not isinstance(revision_id, basestring):
 
637
            raise InvalidRevisionId(revision_id)
 
638
 
 
639
        self.lock_read()
 
640
        try:
 
641
            try:
 
642
                return self.revision_store[revision_id]
 
643
            except (IndexError, KeyError):
 
644
                raise bzrlib.errors.NoSuchRevision(self, revision_id)
 
645
        finally:
 
646
            self.unlock()
 
647
 
 
648
 
 
649
    def get_revision_xml(self, revision_id):
 
650
        return self.get_revision_xml_file(revision_id).read()
 
651
 
 
652
 
585
653
    def get_revision(self, revision_id):
586
654
        """Return the Revision object for a named revision"""
587
 
        from bzrlib.revision import Revision
588
 
        from bzrlib.xml import unpack_xml
 
655
        xml_file = self.get_revision_xml_file(revision_id)
589
656
 
590
 
        self.lock_read()
591
657
        try:
592
 
            if not revision_id or not isinstance(revision_id, basestring):
593
 
                raise ValueError('invalid revision-id: %r' % revision_id)
594
 
            r = unpack_xml(Revision, self.revision_store[revision_id])
595
 
        finally:
596
 
            self.unlock()
 
658
            r = bzrlib.xml5.serializer_v5.read_revision(xml_file)
 
659
        except SyntaxError, e:
 
660
            raise bzrlib.errors.BzrError('failed to unpack revision_xml',
 
661
                                         [revision_id,
 
662
                                          str(e)])
597
663
            
598
664
        assert r.revision_id == revision_id
599
665
        return r
600
 
        
 
666
 
 
667
 
 
668
    def get_revision_delta(self, revno):
 
669
        """Return the delta for one revision.
 
670
 
 
671
        The delta is relative to its mainline predecessor, or the
 
672
        empty tree for revision 1.
 
673
        """
 
674
        assert isinstance(revno, int)
 
675
        rh = self.revision_history()
 
676
        if not (1 <= revno <= len(rh)):
 
677
            raise InvalidRevisionNumber(revno)
 
678
 
 
679
        # revno is 1-based; list is 0-based
 
680
 
 
681
        new_tree = self.revision_tree(rh[revno-1])
 
682
        if revno == 1:
 
683
            old_tree = EmptyTree()
 
684
        else:
 
685
            old_tree = self.revision_tree(rh[revno-2])
 
686
 
 
687
        return compare_trees(old_tree, new_tree)
 
688
 
601
689
 
602
690
    def get_revision_sha1(self, revision_id):
603
691
        """Hash the stored value of a revision, and return it."""
604
 
        # In the future, revision entries will be signed. At that
605
 
        # point, it is probably best *not* to include the signature
606
 
        # in the revision hash. Because that lets you re-sign
607
 
        # the revision, (add signatures/remove signatures) and still
608
 
        # have all hash pointers stay consistent.
609
 
        # But for now, just hash the contents.
610
 
        return sha_file(self.revision_store[revision_id])
611
 
 
612
 
 
613
 
    def get_inventory(self, inventory_id):
614
 
        """Get Inventory object by hash.
615
 
 
616
 
        TODO: Perhaps for this and similar methods, take a revision
617
 
               parameter which can be either an integer revno or a
618
 
               string hash."""
619
 
        from bzrlib.inventory import Inventory
620
 
        from bzrlib.xml import unpack_xml
621
 
 
622
 
        return unpack_xml(Inventory, self.inventory_store[inventory_id])
623
 
            
624
 
 
625
 
    def get_inventory_sha1(self, inventory_id):
 
692
        return bzrlib.osutils.sha_file(self.get_revision_xml_file(revision_id))
 
693
 
 
694
 
 
695
    def _get_ancestry_weave(self):
 
696
        return self.control_weaves.get_weave('ancestry')
 
697
        
 
698
 
 
699
    def get_ancestry(self, revision_id):
 
700
        """Return a list of revision-ids integrated by a revision.
 
701
        """
 
702
        # strip newlines
 
703
        if revision_id is None:
 
704
            return [None]
 
705
        w = self._get_ancestry_weave()
 
706
        return [None] + [l[:-1] for l in w.get_iter(w.lookup(revision_id))]
 
707
 
 
708
 
 
709
    def get_inventory_weave(self):
 
710
        return self.control_weaves.get_weave('inventory')
 
711
 
 
712
 
 
713
    def get_inventory(self, revision_id):
 
714
        """Get Inventory object by hash."""
 
715
        xml = self.get_inventory_xml(revision_id)
 
716
        return bzrlib.xml5.serializer_v5.read_inventory_from_string(xml)
 
717
 
 
718
 
 
719
    def get_inventory_xml(self, revision_id):
 
720
        """Get inventory XML as a file object."""
 
721
        try:
 
722
            assert isinstance(revision_id, basestring), type(revision_id)
 
723
            iw = self.get_inventory_weave()
 
724
            return iw.get_text(iw.lookup(revision_id))
 
725
        except IndexError:
 
726
            raise bzrlib.errors.HistoryMissing(self, 'inventory', revision_id)
 
727
 
 
728
 
 
729
    def get_inventory_sha1(self, revision_id):
626
730
        """Return the sha1 hash of the inventory entry
627
731
        """
628
 
        return sha_file(self.inventory_store[inventory_id])
 
732
        return self.get_revision(revision_id).inventory_sha1
629
733
 
630
734
 
631
735
    def get_revision_inventory(self, revision_id):
632
736
        """Return inventory of a past revision."""
633
 
        # bzr 0.0.6 imposes the constraint that the inventory_id
 
737
        # TODO: Unify this with get_inventory()
 
738
        # bzr 0.0.6 and later imposes the constraint that the inventory_id
634
739
        # must be the same as its revision, so this is trivial.
635
740
        if revision_id == None:
636
 
            from bzrlib.inventory import Inventory
637
741
            return Inventory(self.get_root_id())
638
742
        else:
639
743
            return self.get_inventory(revision_id)
640
744
 
641
745
 
642
746
    def revision_history(self):
643
 
        """Return sequence of revision hashes on to this branch.
644
 
 
645
 
        >>> ScratchBranch().revision_history()
646
 
        []
647
 
        """
 
747
        """Return sequence of revision hashes on to this branch."""
648
748
        self.lock_read()
649
749
        try:
650
750
            return [l.rstrip('\r\n') for l in
655
755
 
656
756
    def common_ancestor(self, other, self_revno=None, other_revno=None):
657
757
        """
658
 
        >>> import commit
 
758
        >>> from bzrlib.commit import commit
659
759
        >>> sb = ScratchBranch(files=['foo', 'foo~'])
660
760
        >>> sb.common_ancestor(sb) == (None, None)
661
761
        True
662
 
        >>> commit.commit(sb, "Committing first revision", verbose=False)
 
762
        >>> commit(sb, "Committing first revision", verbose=False)
663
763
        >>> sb.common_ancestor(sb)[0]
664
764
        1
665
765
        >>> clone = sb.clone()
666
 
        >>> commit.commit(sb, "Committing second revision", verbose=False)
 
766
        >>> commit(sb, "Committing second revision", verbose=False)
667
767
        >>> sb.common_ancestor(sb)[0]
668
768
        2
669
769
        >>> sb.common_ancestor(clone)[0]
670
770
        1
671
 
        >>> commit.commit(clone, "Committing divergent second revision", 
 
771
        >>> commit(clone, "Committing divergent second revision", 
672
772
        ...               verbose=False)
673
773
        >>> sb.common_ancestor(clone)[0]
674
774
        1
697
797
                return r+1, my_history[r]
698
798
        return None, None
699
799
 
700
 
    def enum_history(self, direction):
701
 
        """Return (revno, revision_id) for history of branch.
702
 
 
703
 
        direction
704
 
            'forward' is from earliest to latest
705
 
            'reverse' is from latest to earliest
706
 
        """
707
 
        rh = self.revision_history()
708
 
        if direction == 'forward':
709
 
            i = 1
710
 
            for rid in rh:
711
 
                yield i, rid
712
 
                i += 1
713
 
        elif direction == 'reverse':
714
 
            i = len(rh)
715
 
            while i > 0:
716
 
                yield i, rh[i-1]
717
 
                i -= 1
718
 
        else:
719
 
            raise ValueError('invalid history direction', direction)
720
 
 
721
800
 
722
801
    def revno(self):
723
802
        """Return current revision number for this branch.
728
807
        return len(self.revision_history())
729
808
 
730
809
 
731
 
    def last_patch(self):
 
810
    def last_revision(self):
732
811
        """Return last patch hash, or None if no history.
733
812
        """
734
813
        ph = self.revision_history()
738
817
            return None
739
818
 
740
819
 
741
 
    def missing_revisions(self, other, stop_revision=None):
742
 
        """
 
820
    def missing_revisions(self, other, stop_revision=None, diverged_ok=False):
 
821
        """Return a list of new revisions that would perfectly fit.
 
822
        
743
823
        If self and other have not diverged, return a list of the revisions
744
824
        present in other, but missing from self.
745
825
 
765
845
        Traceback (most recent call last):
766
846
        DivergedBranches: These branches have diverged.
767
847
        """
 
848
        # FIXME: If the branches have diverged, but the latest
 
849
        # revision in this branch is completely merged into the other,
 
850
        # then we should still be able to pull.
768
851
        self_history = self.revision_history()
769
852
        self_len = len(self_history)
770
853
        other_history = other.revision_history()
776
859
 
777
860
        if stop_revision is None:
778
861
            stop_revision = other_len
779
 
        elif stop_revision > other_len:
780
 
            raise NoSuchRevision(self, stop_revision)
781
 
        
 
862
        else:
 
863
            assert isinstance(stop_revision, int)
 
864
            if stop_revision > other_len:
 
865
                raise bzrlib.errors.NoSuchRevision(self, stop_revision)
782
866
        return other_history[self_len:stop_revision]
783
867
 
784
 
 
785
868
    def update_revisions(self, other, stop_revision=None):
786
 
        """Pull in all new revisions from other branch.
787
 
        
788
 
        >>> from bzrlib.commit import commit
789
 
        >>> bzrlib.trace.silent = True
790
 
        >>> br1 = ScratchBranch(files=['foo', 'bar'])
791
 
        >>> br1.add('foo')
792
 
        >>> br1.add('bar')
793
 
        >>> commit(br1, "lala!", rev_id="REVISION-ID-1", verbose=False)
794
 
        >>> br2 = ScratchBranch()
795
 
        >>> br2.update_revisions(br1)
796
 
        Added 2 texts.
797
 
        Added 1 inventories.
798
 
        Added 1 revisions.
799
 
        >>> br2.revision_history()
800
 
        [u'REVISION-ID-1']
801
 
        >>> br2.update_revisions(br1)
802
 
        Added 0 texts.
803
 
        Added 0 inventories.
804
 
        Added 0 revisions.
805
 
        >>> br1.text_store.total_size() == br2.text_store.total_size()
806
 
        True
807
 
        """
808
 
        from bzrlib.progress import ProgressBar
809
 
 
810
 
        pb = ProgressBar()
811
 
 
812
 
        pb.update('comparing histories')
813
 
        revision_ids = self.missing_revisions(other, stop_revision)
814
 
 
815
 
        if hasattr(other.revision_store, "prefetch"):
816
 
            other.revision_store.prefetch(revision_ids)
817
 
        if hasattr(other.inventory_store, "prefetch"):
818
 
            inventory_ids = [other.get_revision(r).inventory_id
819
 
                             for r in revision_ids]
820
 
            other.inventory_store.prefetch(inventory_ids)
821
 
                
822
 
        revisions = []
823
 
        needed_texts = set()
824
 
        i = 0
825
 
        for rev_id in revision_ids:
826
 
            i += 1
827
 
            pb.update('fetching revision', i, len(revision_ids))
828
 
            rev = other.get_revision(rev_id)
829
 
            revisions.append(rev)
830
 
            inv = other.get_inventory(str(rev.inventory_id))
831
 
            for key, entry in inv.iter_entries():
832
 
                if entry.text_id is None:
833
 
                    continue
834
 
                if entry.text_id not in self.text_store:
835
 
                    needed_texts.add(entry.text_id)
836
 
 
837
 
        pb.clear()
838
 
                    
839
 
        count = self.text_store.copy_multi(other.text_store, needed_texts)
840
 
        print "Added %d texts." % count 
841
 
        inventory_ids = [ f.inventory_id for f in revisions ]
842
 
        count = self.inventory_store.copy_multi(other.inventory_store, 
843
 
                                                inventory_ids)
844
 
        print "Added %d inventories." % count 
845
 
        revision_ids = [ f.revision_id for f in revisions]
846
 
        count = self.revision_store.copy_multi(other.revision_store, 
847
 
                                               revision_ids)
848
 
        for revision_id in revision_ids:
849
 
            self.append_revision(revision_id)
850
 
        print "Added %d revisions." % count
851
 
                    
852
 
        
 
869
        """Pull in new perfect-fit revisions."""
 
870
        from bzrlib.fetch import greedy_fetch
 
871
        from bzrlib.revision import get_intervening_revisions
 
872
        if stop_revision is None:
 
873
            stop_revision = other.last_revision()
 
874
        greedy_fetch(to_branch=self, from_branch=other,
 
875
                     revision=stop_revision)
 
876
        pullable_revs = self.missing_revisions(
 
877
            other, other.revision_id_to_revno(stop_revision))
 
878
        if pullable_revs:
 
879
            greedy_fetch(to_branch=self,
 
880
                         from_branch=other,
 
881
                         revision=pullable_revs[-1])
 
882
            self.append_revision(*pullable_revs)
 
883
    
853
884
    def commit(self, *args, **kw):
854
 
        from bzrlib.commit import commit
855
 
        commit(self, *args, **kw)
856
 
        
857
 
 
858
 
    def lookup_revision(self, revision):
859
 
        """Return the revision identifier for a given revision information."""
860
 
        revno, info = self.get_revision_info(revision)
861
 
        return info
862
 
 
863
 
    def get_revision_info(self, revision):
864
 
        """Return (revno, revision id) for revision identifier.
865
 
 
866
 
        revision can be an integer, in which case it is assumed to be revno (though
867
 
            this will translate negative values into positive ones)
868
 
        revision can also be a string, in which case it is parsed for something like
869
 
            'date:' or 'revid:' etc.
870
 
        """
871
 
        if revision is None:
872
 
            return 0, None
873
 
        revno = None
874
 
        try:# Convert to int if possible
875
 
            revision = int(revision)
876
 
        except ValueError:
877
 
            pass
878
 
        revs = self.revision_history()
879
 
        if isinstance(revision, int):
880
 
            if revision == 0:
881
 
                return 0, None
882
 
            # Mabye we should do this first, but we don't need it if revision == 0
883
 
            if revision < 0:
884
 
                revno = len(revs) + revision + 1
885
 
            else:
886
 
                revno = revision
887
 
        elif isinstance(revision, basestring):
888
 
            for prefix, func in Branch.REVISION_NAMESPACES.iteritems():
889
 
                if revision.startswith(prefix):
890
 
                    revno = func(self, revs, revision)
891
 
                    break
892
 
            else:
893
 
                raise BzrError('No namespace registered for string: %r' % revision)
894
 
 
895
 
        if revno is None or revno <= 0 or revno > len(revs):
896
 
            raise BzrError("no such revision %s" % revision)
897
 
        return revno, revs[revno-1]
898
 
 
899
 
    def _namespace_revno(self, revs, revision):
900
 
        """Lookup a revision by revision number"""
901
 
        assert revision.startswith('revno:')
902
 
        try:
903
 
            return int(revision[6:])
904
 
        except ValueError:
905
 
            return None
906
 
    REVISION_NAMESPACES['revno:'] = _namespace_revno
907
 
 
908
 
    def _namespace_revid(self, revs, revision):
909
 
        assert revision.startswith('revid:')
910
 
        try:
911
 
            return revs.index(revision[6:]) + 1
912
 
        except ValueError:
913
 
            return None
914
 
    REVISION_NAMESPACES['revid:'] = _namespace_revid
915
 
 
916
 
    def _namespace_last(self, revs, revision):
917
 
        assert revision.startswith('last:')
918
 
        try:
919
 
            offset = int(revision[5:])
920
 
        except ValueError:
921
 
            return None
922
 
        else:
923
 
            if offset <= 0:
924
 
                raise BzrError('You must supply a positive value for --revision last:XXX')
925
 
            return len(revs) - offset + 1
926
 
    REVISION_NAMESPACES['last:'] = _namespace_last
927
 
 
928
 
    def _namespace_tag(self, revs, revision):
929
 
        assert revision.startswith('tag:')
930
 
        raise BzrError('tag: namespace registered, but not implemented.')
931
 
    REVISION_NAMESPACES['tag:'] = _namespace_tag
932
 
 
933
 
    def _namespace_date(self, revs, revision):
934
 
        assert revision.startswith('date:')
935
 
        import datetime
936
 
        # Spec for date revisions:
937
 
        #   date:value
938
 
        #   value can be 'yesterday', 'today', 'tomorrow' or a YYYY-MM-DD string.
939
 
        #   it can also start with a '+/-/='. '+' says match the first
940
 
        #   entry after the given date. '-' is match the first entry before the date
941
 
        #   '=' is match the first entry after, but still on the given date.
942
 
        #
943
 
        #   +2005-05-12 says find the first matching entry after May 12th, 2005 at 0:00
944
 
        #   -2005-05-12 says find the first matching entry before May 12th, 2005 at 0:00
945
 
        #   =2005-05-12 says find the first match after May 12th, 2005 at 0:00 but before
946
 
        #       May 13th, 2005 at 0:00
947
 
        #
948
 
        #   So the proper way of saying 'give me all entries for today' is:
949
 
        #       -r {date:+today}:{date:-tomorrow}
950
 
        #   The default is '=' when not supplied
951
 
        val = revision[5:]
952
 
        match_style = '='
953
 
        if val[:1] in ('+', '-', '='):
954
 
            match_style = val[:1]
955
 
            val = val[1:]
956
 
 
957
 
        today = datetime.datetime.today().replace(hour=0,minute=0,second=0,microsecond=0)
958
 
        if val.lower() == 'yesterday':
959
 
            dt = today - datetime.timedelta(days=1)
960
 
        elif val.lower() == 'today':
961
 
            dt = today
962
 
        elif val.lower() == 'tomorrow':
963
 
            dt = today + datetime.timedelta(days=1)
964
 
        else:
965
 
            import re
966
 
            # This should be done outside the function to avoid recompiling it.
967
 
            _date_re = re.compile(
968
 
                    r'(?P<date>(?P<year>\d\d\d\d)-(?P<month>\d\d)-(?P<day>\d\d))?'
969
 
                    r'(,|T)?\s*'
970
 
                    r'(?P<time>(?P<hour>\d\d):(?P<minute>\d\d)(:(?P<second>\d\d))?)?'
971
 
                )
972
 
            m = _date_re.match(val)
973
 
            if not m or (not m.group('date') and not m.group('time')):
974
 
                raise BzrError('Invalid revision date %r' % revision)
975
 
 
976
 
            if m.group('date'):
977
 
                year, month, day = int(m.group('year')), int(m.group('month')), int(m.group('day'))
978
 
            else:
979
 
                year, month, day = today.year, today.month, today.day
980
 
            if m.group('time'):
981
 
                hour = int(m.group('hour'))
982
 
                minute = int(m.group('minute'))
983
 
                if m.group('second'):
984
 
                    second = int(m.group('second'))
985
 
                else:
986
 
                    second = 0
987
 
            else:
988
 
                hour, minute, second = 0,0,0
989
 
 
990
 
            dt = datetime.datetime(year=year, month=month, day=day,
991
 
                    hour=hour, minute=minute, second=second)
992
 
        first = dt
993
 
        last = None
994
 
        reversed = False
995
 
        if match_style == '-':
996
 
            reversed = True
997
 
        elif match_style == '=':
998
 
            last = dt + datetime.timedelta(days=1)
999
 
 
1000
 
        if reversed:
1001
 
            for i in range(len(revs)-1, -1, -1):
1002
 
                r = self.get_revision(revs[i])
1003
 
                # TODO: Handle timezone.
1004
 
                dt = datetime.datetime.fromtimestamp(r.timestamp)
1005
 
                if first >= dt and (last is None or dt >= last):
1006
 
                    return i+1
1007
 
        else:
1008
 
            for i in range(len(revs)):
1009
 
                r = self.get_revision(revs[i])
1010
 
                # TODO: Handle timezone.
1011
 
                dt = datetime.datetime.fromtimestamp(r.timestamp)
1012
 
                if first <= dt and (last is None or dt <= last):
1013
 
                    return i+1
1014
 
    REVISION_NAMESPACES['date:'] = _namespace_date
 
885
        from bzrlib.commit import Commit
 
886
        Commit().commit(self, *args, **kw)
 
887
    
 
888
    def revision_id_to_revno(self, revision_id):
 
889
        """Given a revision id, return its revno"""
 
890
        if revision_id is None:
 
891
            return 0
 
892
        history = self.revision_history()
 
893
        try:
 
894
            return history.index(revision_id) + 1
 
895
        except ValueError:
 
896
            raise bzrlib.errors.NoSuchRevision(self, revision_id)
 
897
 
 
898
    def get_rev_id(self, revno, history=None):
 
899
        """Find the revision id of the specified revno."""
 
900
        if revno == 0:
 
901
            return None
 
902
        if history is None:
 
903
            history = self.revision_history()
 
904
        elif revno <= 0 or revno > len(history):
 
905
            raise bzrlib.errors.NoSuchRevision(self, revno)
 
906
        return history[revno - 1]
1015
907
 
1016
908
    def revision_tree(self, revision_id):
1017
909
        """Return Tree for a revision on this branch.
1018
910
 
1019
911
        `revision_id` may be None for the null revision, in which case
1020
912
        an `EmptyTree` is returned."""
1021
 
        from bzrlib.tree import EmptyTree, RevisionTree
1022
913
        # TODO: refactor this to use an existing revision object
1023
914
        # so we don't need to read it in twice.
1024
915
        if revision_id == None:
1025
 
            return EmptyTree(self.get_root_id())
 
916
            return EmptyTree()
1026
917
        else:
1027
918
            inv = self.get_revision_inventory(revision_id)
1028
 
            return RevisionTree(self.text_store, inv)
 
919
            return RevisionTree(self.weave_store, inv, revision_id)
1029
920
 
1030
921
 
1031
922
    def working_tree(self):
1032
923
        """Return a `Tree` for the working copy."""
1033
 
        from workingtree import WorkingTree
 
924
        from bzrlib.workingtree import WorkingTree
1034
925
        return WorkingTree(self.base, self.read_working_inventory())
1035
926
 
1036
927
 
1039
930
 
1040
931
        If there are no revisions yet, return an `EmptyTree`.
1041
932
        """
1042
 
        from bzrlib.tree import EmptyTree, RevisionTree
1043
 
        r = self.last_patch()
1044
 
        if r == None:
1045
 
            return EmptyTree(self.get_root_id())
1046
 
        else:
1047
 
            return RevisionTree(self.text_store, self.get_revision_inventory(r))
1048
 
 
 
933
        return self.revision_tree(self.last_revision())
1049
934
 
1050
935
 
1051
936
    def rename_one(self, from_rel, to_rel):
1083
968
 
1084
969
            inv.rename(file_id, to_dir_id, to_tail)
1085
970
 
1086
 
            print "%s => %s" % (from_rel, to_rel)
1087
 
 
1088
971
            from_abs = self.abspath(from_rel)
1089
972
            to_abs = self.abspath(to_rel)
1090
973
            try:
1091
 
                os.rename(from_abs, to_abs)
 
974
                rename(from_abs, to_abs)
1092
975
            except OSError, e:
1093
976
                raise BzrError("failed to rename %r to %r: %s"
1094
977
                        % (from_abs, to_abs, e[1]),
1109
992
 
1110
993
        Note that to_name is only the last component of the new name;
1111
994
        this doesn't change the directory.
 
995
 
 
996
        This returns a list of (from_path, to_path) pairs for each
 
997
        entry that is moved.
1112
998
        """
 
999
        result = []
1113
1000
        self.lock_write()
1114
1001
        try:
1115
1002
            ## TODO: Option to move IDs only
1150
1037
            for f in from_paths:
1151
1038
                name_tail = splitpath(f)[-1]
1152
1039
                dest_path = appendpath(to_name, name_tail)
1153
 
                print "%s => %s" % (f, dest_path)
 
1040
                result.append((f, dest_path))
1154
1041
                inv.rename(inv.path2id(f), to_dir_id, name_tail)
1155
1042
                try:
1156
 
                    os.rename(self.abspath(f), self.abspath(dest_path))
 
1043
                    rename(self.abspath(f), self.abspath(dest_path))
1157
1044
                except OSError, e:
1158
1045
                    raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
1159
1046
                            ["rename rolled back"])
1162
1049
        finally:
1163
1050
            self.unlock()
1164
1051
 
 
1052
        return result
 
1053
 
1165
1054
 
1166
1055
    def revert(self, filenames, old_tree=None, backups=True):
1167
1056
        """Restore selected files to the versions from a previous tree.
1223
1112
 
1224
1113
 
1225
1114
    def add_pending_merge(self, revision_id):
1226
 
        from bzrlib.revision import validate_revision_id
1227
 
 
1228
1115
        validate_revision_id(revision_id)
1229
 
 
 
1116
        # TODO: Perhaps should check at this point that the
 
1117
        # history of the revision is actually present?
1230
1118
        p = self.pending_merges()
1231
1119
        if revision_id in p:
1232
1120
            return
1249
1137
            self.unlock()
1250
1138
 
1251
1139
 
1252
 
 
1253
 
class ScratchBranch(Branch):
 
1140
    def get_parent(self):
 
1141
        """Return the parent location of the branch.
 
1142
 
 
1143
        This is the default location for push/pull/missing.  The usual
 
1144
        pattern is that the user can override it by specifying a
 
1145
        location.
 
1146
        """
 
1147
        import errno
 
1148
        _locs = ['parent', 'pull', 'x-pull']
 
1149
        for l in _locs:
 
1150
            try:
 
1151
                return self.controlfile(l, 'r').read().strip('\n')
 
1152
            except IOError, e:
 
1153
                if e.errno != errno.ENOENT:
 
1154
                    raise
 
1155
        return None
 
1156
 
 
1157
 
 
1158
    def set_parent(self, url):
 
1159
        # TODO: Maybe delete old location files?
 
1160
        from bzrlib.atomicfile import AtomicFile
 
1161
        self.lock_write()
 
1162
        try:
 
1163
            f = AtomicFile(self.controlfilename('parent'))
 
1164
            try:
 
1165
                f.write(url + '\n')
 
1166
                f.commit()
 
1167
            finally:
 
1168
                f.close()
 
1169
        finally:
 
1170
            self.unlock()
 
1171
 
 
1172
    def check_revno(self, revno):
 
1173
        """\
 
1174
        Check whether a revno corresponds to any revision.
 
1175
        Zero (the NULL revision) is considered valid.
 
1176
        """
 
1177
        if revno != 0:
 
1178
            self.check_real_revno(revno)
 
1179
            
 
1180
    def check_real_revno(self, revno):
 
1181
        """\
 
1182
        Check whether a revno corresponds to a real revision.
 
1183
        Zero (the NULL revision) is considered invalid
 
1184
        """
 
1185
        if revno < 1 or revno > self.revno():
 
1186
            raise InvalidRevisionNumber(revno)
 
1187
        
 
1188
        
 
1189
        
 
1190
 
 
1191
 
 
1192
class ScratchBranch(LocalBranch):
1254
1193
    """Special test class: a branch that cleans up after itself.
1255
1194
 
1256
1195
    >>> b = ScratchBranch()
1273
1212
        if base is None:
1274
1213
            base = mkdtemp()
1275
1214
            init = True
1276
 
        Branch.__init__(self, base, init=init)
 
1215
        LocalBranch.__init__(self, base, init=init)
1277
1216
        for d in dirs:
1278
1217
            os.mkdir(self.abspath(d))
1279
1218
            
1285
1224
        """
1286
1225
        >>> orig = ScratchBranch(files=["file1", "file2"])
1287
1226
        >>> clone = orig.clone()
1288
 
        >>> os.path.samefile(orig.base, clone.base)
 
1227
        >>> if os.name != 'nt':
 
1228
        ...   os.path.samefile(orig.base, clone.base)
 
1229
        ... else:
 
1230
        ...   orig.base == clone.base
 
1231
        ...
1289
1232
        False
1290
1233
        >>> os.path.isfile(os.path.join(clone.base, "file1"))
1291
1234
        True
1296
1239
        os.rmdir(base)
1297
1240
        copytree(self.base, base, symlinks=True)
1298
1241
        return ScratchBranch(base=base)
 
1242
 
 
1243
 
1299
1244
        
1300
1245
    def __del__(self):
1301
1246
        self.destroy()
1371
1316
    """Return a new tree-root file id."""
1372
1317
    return gen_file_id('TREE_ROOT')
1373
1318
 
 
1319
 
 
1320
def copy_branch(branch_from, to_location, revision=None):
 
1321
    """Copy branch_from into the existing directory to_location.
 
1322
 
 
1323
    revision
 
1324
        If not None, only revisions up to this point will be copied.
 
1325
        The head of the new branch will be that revision.  Must be a
 
1326
        revid or None.
 
1327
 
 
1328
    to_location
 
1329
        The name of a local directory that exists but is empty.
 
1330
    """
 
1331
    # TODO: This could be done *much* more efficiently by just copying
 
1332
    # all the whole weaves and revisions, rather than getting one
 
1333
    # revision at a time.
 
1334
    from bzrlib.merge import merge
 
1335
 
 
1336
    assert isinstance(branch_from, Branch)
 
1337
    assert isinstance(to_location, basestring)
 
1338
    
 
1339
    br_to = Branch.initialize(to_location)
 
1340
    br_to.set_root_id(branch_from.get_root_id())
 
1341
    if revision is None:
 
1342
        revision = branch_from.last_revision()
 
1343
    br_to.update_revisions(branch_from, stop_revision=revision)
 
1344
    merge((to_location, -1), (to_location, 0), this_dir=to_location,
 
1345
          check_clean=False, ignore_zero=True)
 
1346
    br_to.set_parent(branch_from.base)
 
1347
    return br_to