~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Martin Pool
  • Date: 2005-09-06 07:39:09 UTC
  • Revision ID: mbp@sourcefrog.net-20050906073909-234eb078dfff15d9
- improved check for branch version

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
import bzrlib.errors
 
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
import bzrlib.xml
 
34
import bzrlib.ui
 
35
 
 
36
 
 
37
 
 
38
BZR_BRANCH_FORMAT_4 = "Bazaar-NG branch, format 0.0.4\n"
 
39
BZR_BRANCH_FORMAT_5 = "Bazaar-NG branch, format 5\n"
27
40
## TODO: Maybe include checks for common corruption of newlines, etc?
28
41
 
29
42
 
 
43
# TODO: Some operations like log might retrieve the same revisions
 
44
# repeatedly to calculate deltas.  We could perhaps have a weakref
 
45
# cache in memory to make this faster.
 
46
 
 
47
# TODO: please move the revision-string syntax stuff out of the branch
 
48
# object; it's clutter
 
49
 
30
50
 
31
51
def find_branch(f, **args):
32
52
    if f and (f.startswith('http://') or f.startswith('https://')):
89
109
    It is not necessary that f exists.
90
110
 
91
111
    Basically we keep looking up until we find the control directory or
92
 
    run into the root."""
 
112
    run into the root.  If there isn't one, raises NotBranchError.
 
113
    """
93
114
    if f == None:
94
115
        f = os.getcwd()
95
116
    elif hasattr(os.path, 'realpath'):
108
129
        head, tail = os.path.split(f)
109
130
        if head == f:
110
131
            # reached the root, whatever that may be
111
 
            raise BzrError('%r is not in a branch' % orig_f)
 
132
            raise bzrlib.errors.NotBranchError('%s is not in a branch' % orig_f)
112
133
        f = head
113
 
    
 
134
 
 
135
 
 
136
 
 
137
# XXX: move into bzrlib.errors; subclass BzrError    
114
138
class DivergedBranches(Exception):
115
139
    def __init__(self, branch1, branch2):
116
140
        self.branch1 = branch1
118
142
        Exception.__init__(self, "These branches have diverged.")
119
143
 
120
144
 
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
145
######################################################################
130
146
# branch objects
131
147
 
204
220
            self._lock.unlock()
205
221
 
206
222
 
207
 
 
208
223
    def lock_write(self):
209
224
        if self._lock_mode:
210
225
            if self._lock_mode != 'w':
220
235
            self._lock_count = 1
221
236
 
222
237
 
223
 
 
224
238
    def lock_read(self):
225
239
        if self._lock_mode:
226
240
            assert self._lock_mode in ('r', 'w'), \
233
247
            self._lock_mode = 'r'
234
248
            self._lock_count = 1
235
249
                        
236
 
 
237
 
            
238
250
    def unlock(self):
239
251
        if not self._lock_mode:
240
252
            from errors import LockError
247
259
            self._lock = None
248
260
            self._lock_mode = self._lock_count = None
249
261
 
250
 
 
251
262
    def abspath(self, name):
252
263
        """Return absolute filename for something in the branch"""
253
264
        return os.path.join(self.base, name)
254
265
 
255
 
 
256
266
    def relpath(self, path):
257
267
        """Return path relative to this branch of something inside it.
258
268
 
259
269
        Raises an error if path is not in this branch."""
260
270
        return _relpath(self.base, path)
261
271
 
262
 
 
263
272
    def controlfilename(self, file_or_path):
264
273
        """Return location relative to branch."""
265
274
        if isinstance(file_or_path, basestring):
292
301
        else:
293
302
            raise BzrError("invalid controlfile mode %r" % mode)
294
303
 
295
 
 
296
 
 
297
304
    def _make_control(self):
298
305
        from bzrlib.inventory import Inventory
299
 
        from bzrlib.xml import pack_xml
300
306
        
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)
 
311
        self.controlfile('branch-format', 'w').write(BZR_BRANCH_FORMAT_5)
306
312
        for d in ('text-store', 'inventory-store', 'revision-store'):
307
313
            os.mkdir(self.controlfilename(d))
308
314
        for f in ('revision-history', 'merged-patches',
312
318
            self.controlfile(f, 'w').write('')
313
319
        mutter('created control directory in ' + self.base)
314
320
 
315
 
        pack_xml(Inventory(gen_root_id()), self.controlfile('inventory','w'))
 
321
        # if we want per-tree root ids then this is the place to set
 
322
        # them; they're not needed for now and so ommitted for
 
323
        # simplicity.
 
324
        f = self.controlfile('inventory','w')
 
325
        bzrlib.xml.serializer_v4.write_inventory(Inventory(), f)
316
326
 
317
327
 
318
328
    def _check_format(self):
319
329
        """Check this branch format is supported.
320
330
 
321
 
        The current tool only supports the current unstable format.
 
331
        The format level is stored, as an integer, in
 
332
        self._branch_format for code that needs to check it later.
322
333
 
323
334
        In the future, we might need different in-memory Branch
324
335
        classes to support downlevel branches.  But not yet.
325
336
        """
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
337
        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'])
 
338
        if fmt == BZR_BRANCH_FORMAT_5:
 
339
            self._branch_format = 5
 
340
        else:
 
341
            raise BzrError('sorry, branch format "%s" not supported; ' 
 
342
                           'use a different bzr version, '
 
343
                           'or run "bzr upgrade", '
 
344
                           'or remove the .bzr directory and "bzr init" again'
 
345
                           % fmt)
335
346
 
336
347
    def get_root_id(self):
337
348
        """Return the id of this branches root"""
353
364
    def read_working_inventory(self):
354
365
        """Read the working inventory."""
355
366
        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.xml.serializer_v4.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.xml.serializer_v4.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.xml.serializer_v4.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])
 
645
        return bzrlib.osutils.sha_file(self.get_revision_xml(revision_id))
611
646
 
612
647
 
613
648
    def get_inventory(self, inventory_id):
617
652
               parameter which can be either an integer revno or a
618
653
               string hash."""
619
654
        from bzrlib.inventory import Inventory
620
 
        from bzrlib.xml import unpack_xml
621
 
 
622
 
        return unpack_xml(Inventory, self.inventory_store[inventory_id])
 
655
 
 
656
        f = self.get_inventory_xml_file(inventory_id)
 
657
        return bzrlib.xml.serializer_v4.read_inventory(f)
 
658
 
 
659
 
 
660
    def get_inventory_xml(self, inventory_id):
 
661
        """Get inventory XML as a file object."""
 
662
        return self.inventory_store[inventory_id]
 
663
 
 
664
    get_inventory_xml_file = get_inventory_xml
623
665
            
624
666
 
625
667
    def get_inventory_sha1(self, inventory_id):
626
668
        """Return the sha1 hash of the inventory entry
627
669
        """
628
 
        return sha_file(self.inventory_store[inventory_id])
 
670
        return sha_file(self.get_inventory_xml(inventory_id))
629
671
 
630
672
 
631
673
    def get_revision_inventory(self, revision_id):
697
739
                return r+1, my_history[r]
698
740
        return None, None
699
741
 
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
742
 
722
743
    def revno(self):
723
744
        """Return current revision number for this branch.
738
759
            return None
739
760
 
740
761
 
741
 
    def missing_revisions(self, other, stop_revision=None):
 
762
    def missing_revisions(self, other, stop_revision=None, diverged_ok=False):
742
763
        """
743
764
        If self and other have not diverged, return a list of the revisions
744
765
        present in other, but missing from self.
777
798
        if stop_revision is None:
778
799
            stop_revision = other_len
779
800
        elif stop_revision > other_len:
780
 
            raise NoSuchRevision(self, stop_revision)
 
801
            raise bzrlib.errors.NoSuchRevision(self, stop_revision)
781
802
        
782
803
        return other_history[self_len:stop_revision]
783
804
 
784
805
 
785
806
    def update_revisions(self, other, stop_revision=None):
786
807
        """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
        """
808
 
        from bzrlib.progress import ProgressBar
809
 
 
810
 
        pb = ProgressBar()
811
 
 
 
809
        from bzrlib.fetch import greedy_fetch
 
810
 
 
811
        pb = bzrlib.ui.ui_factory.progress_bar()
812
812
        pb.update('comparing histories')
 
813
 
813
814
        revision_ids = self.missing_revisions(other, stop_revision)
814
815
 
 
816
        if len(revision_ids) > 0:
 
817
            count = greedy_fetch(self, other, revision_ids[-1], pb)[0]
 
818
        else:
 
819
            count = 0
 
820
        self.append_revision(*revision_ids)
 
821
        ## note("Added %d revisions." % count)
 
822
        pb.clear()
 
823
 
 
824
    def install_revisions(self, other, revision_ids, pb):
815
825
        if hasattr(other.revision_store, "prefetch"):
816
826
            other.revision_store.prefetch(revision_ids)
817
827
        if hasattr(other.inventory_store, "prefetch"):
818
828
            inventory_ids = [other.get_revision(r).inventory_id
819
829
                             for r in revision_ids]
820
830
            other.inventory_store.prefetch(inventory_ids)
 
831
 
 
832
        if pb is None:
 
833
            pb = bzrlib.ui.ui_factory.progress_bar()
821
834
                
822
835
        revisions = []
823
836
        needed_texts = set()
824
837
        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)
 
838
 
 
839
        failures = set()
 
840
        for i, rev_id in enumerate(revision_ids):
 
841
            pb.update('fetching revision', i+1, len(revision_ids))
 
842
            try:
 
843
                rev = other.get_revision(rev_id)
 
844
            except bzrlib.errors.NoSuchRevision:
 
845
                failures.add(rev_id)
 
846
                continue
 
847
 
829
848
            revisions.append(rev)
830
849
            inv = other.get_inventory(str(rev.inventory_id))
831
850
            for key, entry in inv.iter_entries():
836
855
 
837
856
        pb.clear()
838
857
                    
839
 
        count = self.text_store.copy_multi(other.text_store, needed_texts)
840
 
        print "Added %d texts." % count 
 
858
        count, cp_fail = self.text_store.copy_multi(other.text_store, 
 
859
                                                    needed_texts)
 
860
        #print "Added %d texts." % count 
841
861
        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 
 
862
        count, cp_fail = self.inventory_store.copy_multi(other.inventory_store, 
 
863
                                                         inventory_ids)
 
864
        #print "Added %d inventories." % count 
845
865
        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
 
        
 
866
 
 
867
        count, cp_fail = self.revision_store.copy_multi(other.revision_store, 
 
868
                                                          revision_ids,
 
869
                                                          permit_failure=True)
 
870
        assert len(cp_fail) == 0 
 
871
        return count, failures
 
872
       
 
873
 
853
874
    def commit(self, *args, **kw):
854
875
        from bzrlib.commit import commit
855
876
        commit(self, *args, **kw)
857
878
 
858
879
    def lookup_revision(self, revision):
859
880
        """Return the revision identifier for a given revision information."""
860
 
        revno, info = self.get_revision_info(revision)
 
881
        revno, info = self._get_revision_info(revision)
861
882
        return info
862
883
 
 
884
 
 
885
    def revision_id_to_revno(self, revision_id):
 
886
        """Given a revision id, return its revno"""
 
887
        history = self.revision_history()
 
888
        try:
 
889
            return history.index(revision_id) + 1
 
890
        except ValueError:
 
891
            raise bzrlib.errors.NoSuchRevision(self, revision_id)
 
892
 
 
893
 
863
894
    def get_revision_info(self, revision):
864
895
        """Return (revno, revision id) for revision identifier.
865
896
 
868
899
        revision can also be a string, in which case it is parsed for something like
869
900
            'date:' or 'revid:' etc.
870
901
        """
 
902
        revno, rev_id = self._get_revision_info(revision)
 
903
        if revno is None:
 
904
            raise bzrlib.errors.NoSuchRevision(self, revision)
 
905
        return revno, rev_id
 
906
 
 
907
    def get_rev_id(self, revno, history=None):
 
908
        """Find the revision id of the specified revno."""
 
909
        if revno == 0:
 
910
            return None
 
911
        if history is None:
 
912
            history = self.revision_history()
 
913
        elif revno <= 0 or revno > len(history):
 
914
            raise bzrlib.errors.NoSuchRevision(self, revno)
 
915
        return history[revno - 1]
 
916
 
 
917
    def _get_revision_info(self, revision):
 
918
        """Return (revno, revision id) for revision specifier.
 
919
 
 
920
        revision can be an integer, in which case it is assumed to be revno
 
921
        (though this will translate negative values into positive ones)
 
922
        revision can also be a string, in which case it is parsed for something
 
923
        like 'date:' or 'revid:' etc.
 
924
 
 
925
        A revid is always returned.  If it is None, the specifier referred to
 
926
        the null revision.  If the revid does not occur in the revision
 
927
        history, revno will be None.
 
928
        """
 
929
        
871
930
        if revision is None:
872
931
            return 0, None
873
932
        revno = None
877
936
            pass
878
937
        revs = self.revision_history()
879
938
        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
939
            if revision < 0:
884
940
                revno = len(revs) + revision + 1
885
941
            else:
886
942
                revno = revision
 
943
            rev_id = self.get_rev_id(revno, revs)
887
944
        elif isinstance(revision, basestring):
888
945
            for prefix, func in Branch.REVISION_NAMESPACES.iteritems():
889
946
                if revision.startswith(prefix):
890
 
                    revno = func(self, revs, revision)
 
947
                    result = func(self, revs, revision)
 
948
                    if len(result) > 1:
 
949
                        revno, rev_id = result
 
950
                    else:
 
951
                        revno = result[0]
 
952
                        rev_id = self.get_rev_id(revno, revs)
891
953
                    break
892
954
            else:
893
 
                raise BzrError('No namespace registered for string: %r' % revision)
 
955
                raise BzrError('No namespace registered for string: %r' %
 
956
                               revision)
 
957
        else:
 
958
            raise TypeError('Unhandled revision type %s' % revision)
894
959
 
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]
 
960
        if revno is None:
 
961
            if rev_id is None:
 
962
                raise bzrlib.errors.NoSuchRevision(self, revision)
 
963
        return revno, rev_id
898
964
 
899
965
    def _namespace_revno(self, revs, revision):
900
966
        """Lookup a revision by revision number"""
901
967
        assert revision.startswith('revno:')
902
968
        try:
903
 
            return int(revision[6:])
 
969
            return (int(revision[6:]),)
904
970
        except ValueError:
905
971
            return None
906
972
    REVISION_NAMESPACES['revno:'] = _namespace_revno
907
973
 
908
974
    def _namespace_revid(self, revs, revision):
909
975
        assert revision.startswith('revid:')
 
976
        rev_id = revision[len('revid:'):]
910
977
        try:
911
 
            return revs.index(revision[6:]) + 1
 
978
            return revs.index(rev_id) + 1, rev_id
912
979
        except ValueError:
913
 
            return None
 
980
            return None, rev_id
914
981
    REVISION_NAMESPACES['revid:'] = _namespace_revid
915
982
 
916
983
    def _namespace_last(self, revs, revision):
918
985
        try:
919
986
            offset = int(revision[5:])
920
987
        except ValueError:
921
 
            return None
 
988
            return (None,)
922
989
        else:
923
990
            if offset <= 0:
924
991
                raise BzrError('You must supply a positive value for --revision last:XXX')
925
 
            return len(revs) - offset + 1
 
992
            return (len(revs) - offset + 1,)
926
993
    REVISION_NAMESPACES['last:'] = _namespace_last
927
994
 
928
995
    def _namespace_tag(self, revs, revision):
1003
1070
                # TODO: Handle timezone.
1004
1071
                dt = datetime.datetime.fromtimestamp(r.timestamp)
1005
1072
                if first >= dt and (last is None or dt >= last):
1006
 
                    return i+1
 
1073
                    return (i+1,)
1007
1074
        else:
1008
1075
            for i in range(len(revs)):
1009
1076
                r = self.get_revision(revs[i])
1010
1077
                # TODO: Handle timezone.
1011
1078
                dt = datetime.datetime.fromtimestamp(r.timestamp)
1012
1079
                if first <= dt and (last is None or dt <= last):
1013
 
                    return i+1
 
1080
                    return (i+1,)
1014
1081
    REVISION_NAMESPACES['date:'] = _namespace_date
1015
1082
 
1016
1083
    def revision_tree(self, revision_id):
1018
1085
 
1019
1086
        `revision_id` may be None for the null revision, in which case
1020
1087
        an `EmptyTree` is returned."""
1021
 
        from bzrlib.tree import EmptyTree, RevisionTree
1022
1088
        # TODO: refactor this to use an existing revision object
1023
1089
        # so we don't need to read it in twice.
1024
1090
        if revision_id == None:
1025
 
            return EmptyTree(self.get_root_id())
 
1091
            return EmptyTree()
1026
1092
        else:
1027
1093
            inv = self.get_revision_inventory(revision_id)
1028
1094
            return RevisionTree(self.text_store, inv)
1039
1105
 
1040
1106
        If there are no revisions yet, return an `EmptyTree`.
1041
1107
        """
1042
 
        from bzrlib.tree import EmptyTree, RevisionTree
1043
1108
        r = self.last_patch()
1044
1109
        if r == None:
1045
 
            return EmptyTree(self.get_root_id())
 
1110
            return EmptyTree()
1046
1111
        else:
1047
1112
            return RevisionTree(self.text_store, self.get_revision_inventory(r))
1048
1113
 
1083
1148
 
1084
1149
            inv.rename(file_id, to_dir_id, to_tail)
1085
1150
 
1086
 
            print "%s => %s" % (from_rel, to_rel)
1087
 
 
1088
1151
            from_abs = self.abspath(from_rel)
1089
1152
            to_abs = self.abspath(to_rel)
1090
1153
            try:
1109
1172
 
1110
1173
        Note that to_name is only the last component of the new name;
1111
1174
        this doesn't change the directory.
 
1175
 
 
1176
        This returns a list of (from_path, to_path) pairs for each
 
1177
        entry that is moved.
1112
1178
        """
 
1179
        result = []
1113
1180
        self.lock_write()
1114
1181
        try:
1115
1182
            ## TODO: Option to move IDs only
1150
1217
            for f in from_paths:
1151
1218
                name_tail = splitpath(f)[-1]
1152
1219
                dest_path = appendpath(to_name, name_tail)
1153
 
                print "%s => %s" % (f, dest_path)
 
1220
                result.append((f, dest_path))
1154
1221
                inv.rename(inv.path2id(f), to_dir_id, name_tail)
1155
1222
                try:
1156
1223
                    os.rename(self.abspath(f), self.abspath(dest_path))
1162
1229
        finally:
1163
1230
            self.unlock()
1164
1231
 
 
1232
        return result
 
1233
 
1165
1234
 
1166
1235
    def revert(self, filenames, old_tree=None, backups=True):
1167
1236
        """Restore selected files to the versions from a previous tree.
1249
1318
            self.unlock()
1250
1319
 
1251
1320
 
 
1321
    def get_parent(self):
 
1322
        """Return the parent location of the branch.
 
1323
 
 
1324
        This is the default location for push/pull/missing.  The usual
 
1325
        pattern is that the user can override it by specifying a
 
1326
        location.
 
1327
        """
 
1328
        import errno
 
1329
        _locs = ['parent', 'pull', 'x-pull']
 
1330
        for l in _locs:
 
1331
            try:
 
1332
                return self.controlfile(l, 'r').read().strip('\n')
 
1333
            except IOError, e:
 
1334
                if e.errno != errno.ENOENT:
 
1335
                    raise
 
1336
        return None
 
1337
 
 
1338
 
 
1339
    def set_parent(self, url):
 
1340
        # TODO: Maybe delete old location files?
 
1341
        from bzrlib.atomicfile import AtomicFile
 
1342
        self.lock_write()
 
1343
        try:
 
1344
            f = AtomicFile(self.controlfilename('parent'))
 
1345
            try:
 
1346
                f.write(url + '\n')
 
1347
                f.commit()
 
1348
            finally:
 
1349
                f.close()
 
1350
        finally:
 
1351
            self.unlock()
 
1352
 
 
1353
    def check_revno(self, revno):
 
1354
        """\
 
1355
        Check whether a revno corresponds to any revision.
 
1356
        Zero (the NULL revision) is considered valid.
 
1357
        """
 
1358
        if revno != 0:
 
1359
            self.check_real_revno(revno)
 
1360
            
 
1361
    def check_real_revno(self, revno):
 
1362
        """\
 
1363
        Check whether a revno corresponds to a real revision.
 
1364
        Zero (the NULL revision) is considered invalid
 
1365
        """
 
1366
        if revno < 1 or revno > self.revno():
 
1367
            raise InvalidRevisionNumber(revno)
 
1368
        
 
1369
        
 
1370
 
1252
1371
 
1253
1372
class ScratchBranch(Branch):
1254
1373
    """Special test class: a branch that cleans up after itself.
1296
1415
        os.rmdir(base)
1297
1416
        copytree(self.base, base, symlinks=True)
1298
1417
        return ScratchBranch(base=base)
 
1418
 
 
1419
 
1299
1420
        
1300
1421
    def __del__(self):
1301
1422
        self.destroy()
1371
1492
    """Return a new tree-root file id."""
1372
1493
    return gen_file_id('TREE_ROOT')
1373
1494
 
 
1495
 
 
1496
def pull_loc(branch):
 
1497
    # TODO: Should perhaps just make attribute be 'base' in
 
1498
    # RemoteBranch and Branch?
 
1499
    if hasattr(branch, "baseurl"):
 
1500
        return branch.baseurl
 
1501
    else:
 
1502
        return branch.base
 
1503
 
 
1504
 
 
1505
def copy_branch(branch_from, to_location, revision=None):
 
1506
    """Copy branch_from into the existing directory to_location.
 
1507
 
 
1508
    revision
 
1509
        If not None, only revisions up to this point will be copied.
 
1510
        The head of the new branch will be that revision.
 
1511
 
 
1512
    to_location
 
1513
        The name of a local directory that exists but is empty.
 
1514
    """
 
1515
    from bzrlib.merge import merge
 
1516
    from bzrlib.branch import Branch
 
1517
 
 
1518
    assert isinstance(branch_from, Branch)
 
1519
    assert isinstance(to_location, basestring)
 
1520
    
 
1521
    br_to = Branch(to_location, init=True)
 
1522
    br_to.set_root_id(branch_from.get_root_id())
 
1523
    if revision is None:
 
1524
        revno = branch_from.revno()
 
1525
    else:
 
1526
        revno, rev_id = branch_from.get_revision_info(revision)
 
1527
    br_to.update_revisions(branch_from, stop_revision=revno)
 
1528
    merge((to_location, -1), (to_location, 0), this_dir=to_location,
 
1529
          check_clean=False, ignore_zero=True)
 
1530
    
 
1531
    from_location = pull_loc(branch_from)
 
1532
    br_to.set_parent(pull_loc(branch_from))
 
1533