~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Martin Pool
  • Date: 2005-09-12 08:45:58 UTC
  • Revision ID: mbp@sourcefrog.net-20050912084558-93fecb4c84386c17
doc

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
19
20
 
20
21
import bzrlib
21
22
from bzrlib.trace import mutter, note
22
 
from bzrlib.osutils import isdir, quotefn, compact_date, rand_bytes, splitpath, \
 
23
from bzrlib.osutils import isdir, quotefn, compact_date, rand_bytes, \
 
24
     splitpath, \
23
25
     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
 
 
27
from bzrlib.errors import (BzrError, InvalidRevisionNumber, InvalidRevisionId,
 
28
                           NoSuchRevision)
 
29
from bzrlib.textui import show_status
 
30
from bzrlib.revision import Revision
 
31
from bzrlib.delta import compare_trees
 
32
from bzrlib.tree import EmptyTree, RevisionTree
 
33
from bzrlib.inventory import Inventory
 
34
from bzrlib.weavestore import WeaveStore
 
35
import bzrlib.xml5
 
36
import bzrlib.ui
 
37
 
 
38
 
 
39
 
 
40
BZR_BRANCH_FORMAT_4 = "Bazaar-NG branch, format 0.0.4\n"
 
41
BZR_BRANCH_FORMAT_5 = "Bazaar-NG branch, format 5\n"
27
42
## TODO: Maybe include checks for common corruption of newlines, etc?
28
43
 
29
44
 
 
45
# TODO: Some operations like log might retrieve the same revisions
 
46
# repeatedly to calculate deltas.  We could perhaps have a weakref
 
47
# cache in memory to make this faster.
 
48
 
 
49
# TODO: please move the revision-string syntax stuff out of the branch
 
50
# object; it's clutter
 
51
 
30
52
 
31
53
def find_branch(f, **args):
32
54
    if f and (f.startswith('http://') or f.startswith('https://')):
89
111
    It is not necessary that f exists.
90
112
 
91
113
    Basically we keep looking up until we find the control directory or
92
 
    run into the root."""
 
114
    run into the root.  If there isn't one, raises NotBranchError.
 
115
    """
93
116
    if f == None:
94
117
        f = os.getcwd()
95
118
    elif hasattr(os.path, 'realpath'):
108
131
        head, tail = os.path.split(f)
109
132
        if head == f:
110
133
            # reached the root, whatever that may be
111
 
            raise BzrError('%r is not in a branch' % orig_f)
 
134
            raise bzrlib.errors.NotBranchError('%s is not in a branch' % orig_f)
112
135
        f = head
113
 
    
 
136
 
 
137
 
 
138
 
 
139
# XXX: move into bzrlib.errors; subclass BzrError    
114
140
class DivergedBranches(Exception):
115
141
    def __init__(self, branch1, branch2):
116
142
        self.branch1 = branch1
118
144
        Exception.__init__(self, "These branches have diverged.")
119
145
 
120
146
 
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)
127
 
 
128
 
 
129
147
######################################################################
130
148
# branch objects
131
149
 
185
203
                                      'current bzr can only operate from top-of-tree'])
186
204
        self._check_format()
187
205
 
188
 
        self.text_store = ImmutableStore(self.controlfilename('text-store'))
 
206
        self.weave_store = WeaveStore(self.controlfilename('weaves'))
189
207
        self.revision_store = ImmutableStore(self.controlfilename('revision-store'))
190
208
        self.inventory_store = ImmutableStore(self.controlfilename('inventory-store'))
191
209
 
204
222
            self._lock.unlock()
205
223
 
206
224
 
207
 
 
208
225
    def lock_write(self):
209
226
        if self._lock_mode:
210
227
            if self._lock_mode != 'w':
220
237
            self._lock_count = 1
221
238
 
222
239
 
223
 
 
224
240
    def lock_read(self):
225
241
        if self._lock_mode:
226
242
            assert self._lock_mode in ('r', 'w'), \
233
249
            self._lock_mode = 'r'
234
250
            self._lock_count = 1
235
251
                        
236
 
 
237
 
            
238
252
    def unlock(self):
239
253
        if not self._lock_mode:
240
254
            from errors import LockError
247
261
            self._lock = None
248
262
            self._lock_mode = self._lock_count = None
249
263
 
250
 
 
251
264
    def abspath(self, name):
252
265
        """Return absolute filename for something in the branch"""
253
266
        return os.path.join(self.base, name)
254
267
 
255
 
 
256
268
    def relpath(self, path):
257
269
        """Return path relative to this branch of something inside it.
258
270
 
259
271
        Raises an error if path is not in this branch."""
260
272
        return _relpath(self.base, path)
261
273
 
262
 
 
263
274
    def controlfilename(self, file_or_path):
264
275
        """Return location relative to branch."""
265
276
        if isinstance(file_or_path, basestring):
292
303
        else:
293
304
            raise BzrError("invalid controlfile mode %r" % mode)
294
305
 
295
 
 
296
 
 
297
306
    def _make_control(self):
298
 
        from bzrlib.inventory import Inventory
299
 
        from bzrlib.xml import pack_xml
300
 
        
301
307
        os.mkdir(self.controlfilename([]))
302
308
        self.controlfile('README', 'w').write(
303
309
            "This is a Bazaar-NG control directory.\n"
304
310
            "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'):
 
311
        self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT_5)
 
312
        for d in ('text-store', 'inventory-store', 'revision-store',
 
313
                  'weaves'):
307
314
            os.mkdir(self.controlfilename(d))
308
315
        for f in ('revision-history', 'merged-patches',
309
316
                  'pending-merged-patches', 'branch-name',
312
319
            self.controlfile(f, 'w').write('')
313
320
        mutter('created control directory in ' + self.base)
314
321
 
315
 
        pack_xml(Inventory(gen_root_id()), self.controlfile('inventory','w'))
 
322
        # if we want per-tree root ids then this is the place to set
 
323
        # them; they're not needed for now and so ommitted for
 
324
        # simplicity.
 
325
        f = self.controlfile('inventory','w')
 
326
        bzrlib.xml5.serializer_v5.write_inventory(Inventory(), f)
316
327
 
317
328
 
318
329
    def _check_format(self):
319
330
        """Check this branch format is supported.
320
331
 
321
 
        The current tool only supports the current unstable format.
 
332
        The format level is stored, as an integer, in
 
333
        self._branch_format for code that needs to check it later.
322
334
 
323
335
        In the future, we might need different in-memory Branch
324
336
        classes to support downlevel branches.  But not yet.
325
337
        """
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
338
        fmt = self.controlfile('branch-format', 'r').read()
330
 
        fmt.replace('\r\n', '')
331
 
        if fmt != BZR_BRANCH_FORMAT:
332
 
            raise BzrError('sorry, branch format %r not supported' % fmt,
333
 
                           ['use a different bzr version',
334
 
                            'or remove the .bzr directory and "bzr init" again'])
 
339
        if fmt == BZR_BRANCH_FORMAT_5:
 
340
            self._branch_format = 5
 
341
        else:
 
342
            raise BzrError('sorry, branch format "%s" not supported; ' 
 
343
                           'use a different bzr version, '
 
344
                           'or run "bzr upgrade", '
 
345
                           'or remove the .bzr directory and "bzr init" again'
 
346
                           % fmt.rstrip('\n\r'))
335
347
 
336
348
    def get_root_id(self):
337
349
        """Return the id of this branches root"""
352
364
 
353
365
    def read_working_inventory(self):
354
366
        """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
367
        self.lock_read()
360
368
        try:
361
369
            # ElementTree does its own conversion from UTF-8, so open in
362
370
            # 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
 
371
            f = self.controlfile('inventory', 'rb')
 
372
            return bzrlib.xml5.serializer_v5.read_inventory(f)
368
373
        finally:
369
374
            self.unlock()
370
375
            
376
381
        will be committed to the next revision.
377
382
        """
378
383
        from bzrlib.atomicfile import AtomicFile
379
 
        from bzrlib.xml import pack_xml
380
384
        
381
385
        self.lock_write()
382
386
        try:
383
387
            f = AtomicFile(self.controlfilename('inventory'), 'wb')
384
388
            try:
385
 
                pack_xml(inv, f)
 
389
                bzrlib.xml5.serializer_v5.write_inventory(inv, f)
386
390
                f.commit()
387
391
            finally:
388
392
                f.close()
396
400
                         """Inventory for the working copy.""")
397
401
 
398
402
 
399
 
    def add(self, files, verbose=False, ids=None):
 
403
    def add(self, files, ids=None):
400
404
        """Make files versioned.
401
405
 
402
 
        Note that the command line normally calls smart_add instead.
 
406
        Note that the command line normally calls smart_add instead,
 
407
        which can automatically recurse.
403
408
 
404
409
        This puts the files in the Added state, so that they will be
405
410
        recorded by the next commit.
415
420
        TODO: Perhaps have an option to add the ids even if the files do
416
421
              not (yet) exist.
417
422
 
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.
 
423
        TODO: Perhaps yield the ids and paths as they're added.
424
424
        """
425
 
        from bzrlib.textui import show_status
426
425
        # TODO: Re-adding a file that is removed in the working copy
427
426
        # should probably put it back with the previous ID.
428
427
        if isinstance(files, basestring):
463
462
                    file_id = gen_file_id(f)
464
463
                inv.add_path(f, kind=kind, file_id=file_id)
465
464
 
466
 
                if verbose:
467
 
                    print 'added', quotefn(f)
468
 
 
469
465
                mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
470
466
 
471
467
            self._write_inventory(inv)
501
497
        is the opposite of add.  Removing it is consistent with most
502
498
        other tools.  Maybe an option.
503
499
        """
504
 
        from bzrlib.textui import show_status
505
500
        ## TODO: Normalize names
506
501
        ## TODO: Remove nested loops; better scalability
507
502
        if isinstance(files, basestring):
582
577
            f.close()
583
578
 
584
579
 
 
580
    def get_revision_xml_file(self, revision_id):
 
581
        """Return XML file object for revision object."""
 
582
        if not revision_id or not isinstance(revision_id, basestring):
 
583
            raise InvalidRevisionId(revision_id)
 
584
 
 
585
        self.lock_read()
 
586
        try:
 
587
            try:
 
588
                return self.revision_store[revision_id]
 
589
            except IndexError:
 
590
                raise bzrlib.errors.NoSuchRevision(self, revision_id)
 
591
        finally:
 
592
            self.unlock()
 
593
 
 
594
 
 
595
    #deprecated
 
596
    get_revision_xml = get_revision_xml_file
 
597
 
 
598
 
585
599
    def get_revision(self, revision_id):
586
600
        """Return the Revision object for a named revision"""
587
 
        from bzrlib.revision import Revision
588
 
        from bzrlib.xml import unpack_xml
 
601
        xml_file = self.get_revision_xml_file(revision_id)
589
602
 
590
 
        self.lock_read()
591
603
        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()
 
604
            r = bzrlib.xml5.serializer_v5.read_revision(xml_file)
 
605
        except SyntaxError, e:
 
606
            raise bzrlib.errors.BzrError('failed to unpack revision_xml',
 
607
                                         [revision_id,
 
608
                                          str(e)])
597
609
            
598
610
        assert r.revision_id == revision_id
599
611
        return r
 
612
 
 
613
 
 
614
    def get_revision_delta(self, revno):
 
615
        """Return the delta for one revision.
 
616
 
 
617
        The delta is relative to its mainline predecessor, or the
 
618
        empty tree for revision 1.
 
619
        """
 
620
        assert isinstance(revno, int)
 
621
        rh = self.revision_history()
 
622
        if not (1 <= revno <= len(rh)):
 
623
            raise InvalidRevisionNumber(revno)
 
624
 
 
625
        # revno is 1-based; list is 0-based
 
626
 
 
627
        new_tree = self.revision_tree(rh[revno-1])
 
628
        if revno == 1:
 
629
            old_tree = EmptyTree()
 
630
        else:
 
631
            old_tree = self.revision_tree(rh[revno-2])
 
632
 
 
633
        return compare_trees(old_tree, new_tree)
 
634
 
600
635
        
601
636
 
602
637
    def get_revision_sha1(self, revision_id):
607
642
        # the revision, (add signatures/remove signatures) and still
608
643
        # have all hash pointers stay consistent.
609
644
        # 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):
 
645
        return bzrlib.osutils.sha_file(self.get_revision_xml(revision_id))
 
646
 
 
647
 
 
648
    def get_inventory(self, revision_id):
614
649
        """Get Inventory object by hash.
615
650
 
616
651
        TODO: Perhaps for this and similar methods, take a revision
617
652
               parameter which can be either an integer revno or a
618
653
               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])
 
654
        f = self.get_inventory_xml_file(revision_id)
 
655
        return bzrlib.xml5.serializer_v5.read_inventory(f)
 
656
 
 
657
 
 
658
    def get_inventory_xml(self, revision_id):
 
659
        """Get inventory XML as a file object."""
 
660
        try:
 
661
            assert isinstance(revision_id, basestring), type(revision_id)
 
662
            return self.inventory_store[revision_id]
 
663
        except IndexError:
 
664
            raise bzrlib.errors.HistoryMissing(self, 'inventory', revision_id)
 
665
 
 
666
    get_inventory_xml_file = get_inventory_xml
623
667
            
624
668
 
625
 
    def get_inventory_sha1(self, inventory_id):
 
669
    def get_inventory_sha1(self, revision_id):
626
670
        """Return the sha1 hash of the inventory entry
627
671
        """
628
 
        return sha_file(self.inventory_store[inventory_id])
 
672
        return sha_file(self.get_inventory_xml_file(revision_id))
629
673
 
630
674
 
631
675
    def get_revision_inventory(self, revision_id):
633
677
        # bzr 0.0.6 imposes the constraint that the inventory_id
634
678
        # must be the same as its revision, so this is trivial.
635
679
        if revision_id == None:
636
 
            from bzrlib.inventory import Inventory
637
680
            return Inventory(self.get_root_id())
638
681
        else:
639
682
            return self.get_inventory(revision_id)
697
740
                return r+1, my_history[r]
698
741
        return None, None
699
742
 
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
743
 
722
744
    def revno(self):
723
745
        """Return current revision number for this branch.
738
760
            return None
739
761
 
740
762
 
741
 
    def missing_revisions(self, other, stop_revision=None):
 
763
    def missing_revisions(self, other, stop_revision=None, diverged_ok=False):
742
764
        """
743
765
        If self and other have not diverged, return a list of the revisions
744
766
        present in other, but missing from self.
777
799
        if stop_revision is None:
778
800
            stop_revision = other_len
779
801
        elif stop_revision > other_len:
780
 
            raise NoSuchRevision(self, stop_revision)
 
802
            raise bzrlib.errors.NoSuchRevision(self, stop_revision)
781
803
        
782
804
        return other_history[self_len:stop_revision]
783
805
 
784
806
 
785
807
    def update_revisions(self, other, stop_revision=None):
786
808
        """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
809
        """
808
 
        from bzrlib.progress import ProgressBar
809
 
 
810
 
        pb = ProgressBar()
811
 
 
 
810
        from bzrlib.fetch import greedy_fetch
 
811
 
 
812
        pb = bzrlib.ui.ui_factory.progress_bar()
812
813
        pb.update('comparing histories')
 
814
 
813
815
        revision_ids = self.missing_revisions(other, stop_revision)
814
816
 
 
817
        if len(revision_ids) > 0:
 
818
            count = greedy_fetch(self, other, revision_ids[-1], pb)[0]
 
819
        else:
 
820
            count = 0
 
821
        self.append_revision(*revision_ids)
 
822
        ## note("Added %d revisions." % count)
 
823
        pb.clear()
 
824
 
 
825
    def install_revisions(self, other, revision_ids, pb):
815
826
        if hasattr(other.revision_store, "prefetch"):
816
827
            other.revision_store.prefetch(revision_ids)
817
828
        if hasattr(other.inventory_store, "prefetch"):
818
829
            inventory_ids = [other.get_revision(r).inventory_id
819
830
                             for r in revision_ids]
820
831
            other.inventory_store.prefetch(inventory_ids)
 
832
 
 
833
        if pb is None:
 
834
            pb = bzrlib.ui.ui_factory.progress_bar()
821
835
                
822
836
        revisions = []
823
837
        needed_texts = set()
824
838
        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)
 
839
 
 
840
        failures = set()
 
841
        for i, rev_id in enumerate(revision_ids):
 
842
            pb.update('fetching revision', i+1, len(revision_ids))
 
843
            try:
 
844
                rev = other.get_revision(rev_id)
 
845
            except bzrlib.errors.NoSuchRevision:
 
846
                failures.add(rev_id)
 
847
                continue
 
848
 
829
849
            revisions.append(rev)
830
850
            inv = other.get_inventory(str(rev.inventory_id))
831
851
            for key, entry in inv.iter_entries():
836
856
 
837
857
        pb.clear()
838
858
                    
839
 
        count = self.text_store.copy_multi(other.text_store, needed_texts)
840
 
        print "Added %d texts." % count 
 
859
        count, cp_fail = self.text_store.copy_multi(other.text_store, 
 
860
                                                    needed_texts)
 
861
        #print "Added %d texts." % count 
841
862
        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 
 
863
        count, cp_fail = self.inventory_store.copy_multi(other.inventory_store, 
 
864
                                                         inventory_ids)
 
865
        #print "Added %d inventories." % count 
845
866
        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
 
        
 
867
 
 
868
        count, cp_fail = self.revision_store.copy_multi(other.revision_store, 
 
869
                                                          revision_ids,
 
870
                                                          permit_failure=True)
 
871
        assert len(cp_fail) == 0 
 
872
        return count, failures
 
873
       
 
874
 
853
875
    def commit(self, *args, **kw):
854
 
        from bzrlib.commit import commit
855
 
        commit(self, *args, **kw)
 
876
        from bzrlib.commit import Commit
 
877
        Commit().commit(self, *args, **kw)
856
878
        
857
879
 
858
880
    def lookup_revision(self, revision):
859
881
        """Return the revision identifier for a given revision information."""
860
 
        revno, info = self.get_revision_info(revision)
 
882
        revno, info = self._get_revision_info(revision)
861
883
        return info
862
884
 
 
885
 
 
886
    def revision_id_to_revno(self, revision_id):
 
887
        """Given a revision id, return its revno"""
 
888
        history = self.revision_history()
 
889
        try:
 
890
            return history.index(revision_id) + 1
 
891
        except ValueError:
 
892
            raise bzrlib.errors.NoSuchRevision(self, revision_id)
 
893
 
 
894
 
863
895
    def get_revision_info(self, revision):
864
896
        """Return (revno, revision id) for revision identifier.
865
897
 
868
900
        revision can also be a string, in which case it is parsed for something like
869
901
            'date:' or 'revid:' etc.
870
902
        """
 
903
        revno, rev_id = self._get_revision_info(revision)
 
904
        if revno is None:
 
905
            raise bzrlib.errors.NoSuchRevision(self, revision)
 
906
        return revno, rev_id
 
907
 
 
908
    def get_rev_id(self, revno, history=None):
 
909
        """Find the revision id of the specified revno."""
 
910
        if revno == 0:
 
911
            return None
 
912
        if history is None:
 
913
            history = self.revision_history()
 
914
        elif revno <= 0 or revno > len(history):
 
915
            raise bzrlib.errors.NoSuchRevision(self, revno)
 
916
        return history[revno - 1]
 
917
 
 
918
    def _get_revision_info(self, revision):
 
919
        """Return (revno, revision id) for revision specifier.
 
920
 
 
921
        revision can be an integer, in which case it is assumed to be revno
 
922
        (though this will translate negative values into positive ones)
 
923
        revision can also be a string, in which case it is parsed for something
 
924
        like 'date:' or 'revid:' etc.
 
925
 
 
926
        A revid is always returned.  If it is None, the specifier referred to
 
927
        the null revision.  If the revid does not occur in the revision
 
928
        history, revno will be None.
 
929
        """
 
930
        
871
931
        if revision is None:
872
932
            return 0, None
873
933
        revno = None
877
937
            pass
878
938
        revs = self.revision_history()
879
939
        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
940
            if revision < 0:
884
941
                revno = len(revs) + revision + 1
885
942
            else:
886
943
                revno = revision
 
944
            rev_id = self.get_rev_id(revno, revs)
887
945
        elif isinstance(revision, basestring):
888
946
            for prefix, func in Branch.REVISION_NAMESPACES.iteritems():
889
947
                if revision.startswith(prefix):
890
 
                    revno = func(self, revs, revision)
 
948
                    result = func(self, revs, revision)
 
949
                    if len(result) > 1:
 
950
                        revno, rev_id = result
 
951
                    else:
 
952
                        revno = result[0]
 
953
                        rev_id = self.get_rev_id(revno, revs)
891
954
                    break
892
955
            else:
893
 
                raise BzrError('No namespace registered for string: %r' % revision)
 
956
                raise BzrError('No namespace registered for string: %r' %
 
957
                               revision)
 
958
        else:
 
959
            raise TypeError('Unhandled revision type %s' % revision)
894
960
 
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]
 
961
        if revno is None:
 
962
            if rev_id is None:
 
963
                raise bzrlib.errors.NoSuchRevision(self, revision)
 
964
        return revno, rev_id
898
965
 
899
966
    def _namespace_revno(self, revs, revision):
900
967
        """Lookup a revision by revision number"""
901
968
        assert revision.startswith('revno:')
902
969
        try:
903
 
            return int(revision[6:])
 
970
            return (int(revision[6:]),)
904
971
        except ValueError:
905
972
            return None
906
973
    REVISION_NAMESPACES['revno:'] = _namespace_revno
907
974
 
908
975
    def _namespace_revid(self, revs, revision):
909
976
        assert revision.startswith('revid:')
 
977
        rev_id = revision[len('revid:'):]
910
978
        try:
911
 
            return revs.index(revision[6:]) + 1
 
979
            return revs.index(rev_id) + 1, rev_id
912
980
        except ValueError:
913
 
            return None
 
981
            return None, rev_id
914
982
    REVISION_NAMESPACES['revid:'] = _namespace_revid
915
983
 
916
984
    def _namespace_last(self, revs, revision):
918
986
        try:
919
987
            offset = int(revision[5:])
920
988
        except ValueError:
921
 
            return None
 
989
            return (None,)
922
990
        else:
923
991
            if offset <= 0:
924
992
                raise BzrError('You must supply a positive value for --revision last:XXX')
925
 
            return len(revs) - offset + 1
 
993
            return (len(revs) - offset + 1,)
926
994
    REVISION_NAMESPACES['last:'] = _namespace_last
927
995
 
928
996
    def _namespace_tag(self, revs, revision):
1003
1071
                # TODO: Handle timezone.
1004
1072
                dt = datetime.datetime.fromtimestamp(r.timestamp)
1005
1073
                if first >= dt and (last is None or dt >= last):
1006
 
                    return i+1
 
1074
                    return (i+1,)
1007
1075
        else:
1008
1076
            for i in range(len(revs)):
1009
1077
                r = self.get_revision(revs[i])
1010
1078
                # TODO: Handle timezone.
1011
1079
                dt = datetime.datetime.fromtimestamp(r.timestamp)
1012
1080
                if first <= dt and (last is None or dt <= last):
1013
 
                    return i+1
 
1081
                    return (i+1,)
1014
1082
    REVISION_NAMESPACES['date:'] = _namespace_date
1015
1083
 
1016
1084
    def revision_tree(self, revision_id):
1018
1086
 
1019
1087
        `revision_id` may be None for the null revision, in which case
1020
1088
        an `EmptyTree` is returned."""
1021
 
        from bzrlib.tree import EmptyTree, RevisionTree
1022
1089
        # TODO: refactor this to use an existing revision object
1023
1090
        # so we don't need to read it in twice.
1024
1091
        if revision_id == None:
1025
 
            return EmptyTree(self.get_root_id())
 
1092
            return EmptyTree()
1026
1093
        else:
1027
1094
            inv = self.get_revision_inventory(revision_id)
1028
 
            return RevisionTree(self.text_store, inv)
 
1095
            return RevisionTree(self.weave_store, inv, revision_id)
1029
1096
 
1030
1097
 
1031
1098
    def working_tree(self):
1039
1106
 
1040
1107
        If there are no revisions yet, return an `EmptyTree`.
1041
1108
        """
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
 
 
 
1109
        return self.revision_tree(self.last_patch())
1049
1110
 
1050
1111
 
1051
1112
    def rename_one(self, from_rel, to_rel):
1083
1144
 
1084
1145
            inv.rename(file_id, to_dir_id, to_tail)
1085
1146
 
1086
 
            print "%s => %s" % (from_rel, to_rel)
1087
 
 
1088
1147
            from_abs = self.abspath(from_rel)
1089
1148
            to_abs = self.abspath(to_rel)
1090
1149
            try:
1109
1168
 
1110
1169
        Note that to_name is only the last component of the new name;
1111
1170
        this doesn't change the directory.
 
1171
 
 
1172
        This returns a list of (from_path, to_path) pairs for each
 
1173
        entry that is moved.
1112
1174
        """
 
1175
        result = []
1113
1176
        self.lock_write()
1114
1177
        try:
1115
1178
            ## TODO: Option to move IDs only
1150
1213
            for f in from_paths:
1151
1214
                name_tail = splitpath(f)[-1]
1152
1215
                dest_path = appendpath(to_name, name_tail)
1153
 
                print "%s => %s" % (f, dest_path)
 
1216
                result.append((f, dest_path))
1154
1217
                inv.rename(inv.path2id(f), to_dir_id, name_tail)
1155
1218
                try:
1156
1219
                    os.rename(self.abspath(f), self.abspath(dest_path))
1162
1225
        finally:
1163
1226
            self.unlock()
1164
1227
 
 
1228
        return result
 
1229
 
1165
1230
 
1166
1231
    def revert(self, filenames, old_tree=None, backups=True):
1167
1232
        """Restore selected files to the versions from a previous tree.
1249
1314
            self.unlock()
1250
1315
 
1251
1316
 
 
1317
    def get_parent(self):
 
1318
        """Return the parent location of the branch.
 
1319
 
 
1320
        This is the default location for push/pull/missing.  The usual
 
1321
        pattern is that the user can override it by specifying a
 
1322
        location.
 
1323
        """
 
1324
        import errno
 
1325
        _locs = ['parent', 'pull', 'x-pull']
 
1326
        for l in _locs:
 
1327
            try:
 
1328
                return self.controlfile(l, 'r').read().strip('\n')
 
1329
            except IOError, e:
 
1330
                if e.errno != errno.ENOENT:
 
1331
                    raise
 
1332
        return None
 
1333
 
 
1334
 
 
1335
    def set_parent(self, url):
 
1336
        # TODO: Maybe delete old location files?
 
1337
        from bzrlib.atomicfile import AtomicFile
 
1338
        self.lock_write()
 
1339
        try:
 
1340
            f = AtomicFile(self.controlfilename('parent'))
 
1341
            try:
 
1342
                f.write(url + '\n')
 
1343
                f.commit()
 
1344
            finally:
 
1345
                f.close()
 
1346
        finally:
 
1347
            self.unlock()
 
1348
 
 
1349
    def check_revno(self, revno):
 
1350
        """\
 
1351
        Check whether a revno corresponds to any revision.
 
1352
        Zero (the NULL revision) is considered valid.
 
1353
        """
 
1354
        if revno != 0:
 
1355
            self.check_real_revno(revno)
 
1356
            
 
1357
    def check_real_revno(self, revno):
 
1358
        """\
 
1359
        Check whether a revno corresponds to a real revision.
 
1360
        Zero (the NULL revision) is considered invalid
 
1361
        """
 
1362
        if revno < 1 or revno > self.revno():
 
1363
            raise InvalidRevisionNumber(revno)
 
1364
        
 
1365
        
 
1366
 
1252
1367
 
1253
1368
class ScratchBranch(Branch):
1254
1369
    """Special test class: a branch that cleans up after itself.
1296
1411
        os.rmdir(base)
1297
1412
        copytree(self.base, base, symlinks=True)
1298
1413
        return ScratchBranch(base=base)
 
1414
 
 
1415
 
1299
1416
        
1300
1417
    def __del__(self):
1301
1418
        self.destroy()
1371
1488
    """Return a new tree-root file id."""
1372
1489
    return gen_file_id('TREE_ROOT')
1373
1490
 
 
1491
 
 
1492
def pull_loc(branch):
 
1493
    # TODO: Should perhaps just make attribute be 'base' in
 
1494
    # RemoteBranch and Branch?
 
1495
    if hasattr(branch, "baseurl"):
 
1496
        return branch.baseurl
 
1497
    else:
 
1498
        return branch.base
 
1499
 
 
1500
 
 
1501
def copy_branch(branch_from, to_location, revision=None):
 
1502
    """Copy branch_from into the existing directory to_location.
 
1503
 
 
1504
    revision
 
1505
        If not None, only revisions up to this point will be copied.
 
1506
        The head of the new branch will be that revision.
 
1507
 
 
1508
    to_location
 
1509
        The name of a local directory that exists but is empty.
 
1510
    """
 
1511
    from bzrlib.merge import merge
 
1512
    from bzrlib.branch import Branch
 
1513
 
 
1514
    assert isinstance(branch_from, Branch)
 
1515
    assert isinstance(to_location, basestring)
 
1516
    
 
1517
    br_to = Branch(to_location, init=True)
 
1518
    br_to.set_root_id(branch_from.get_root_id())
 
1519
    if revision is None:
 
1520
        revno = branch_from.revno()
 
1521
    else:
 
1522
        revno, rev_id = branch_from.get_revision_info(revision)
 
1523
    br_to.update_revisions(branch_from, stop_revision=revno)
 
1524
    merge((to_location, -1), (to_location, 0), this_dir=to_location,
 
1525
          check_clean=False, ignore_zero=True)
 
1526
    
 
1527
    from_location = pull_loc(branch_from)
 
1528
    br_to.set_parent(pull_loc(branch_from))
 
1529