~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: aaron.bentley at utoronto
  • Date: 2005-08-21 00:08:08 UTC
  • mto: (1092.1.41) (1185.3.4)
  • mto: This revision was merged to the branch mainline in revision 1110.
  • Revision ID: aaron.bentley@utoronto.ca-20050821000808-2a0e6ef95b1bca59
Changed copy_multi to permit failure and return a tuple, tested missing required revisions

Show diffs side-by-side

added added

removed removed

Lines of Context:
23
23
from bzrlib.osutils import isdir, quotefn, compact_date, rand_bytes, \
24
24
     splitpath, \
25
25
     sha_file, appendpath, file_kind
26
 
 
27
26
from bzrlib.errors import BzrError, InvalidRevisionNumber, InvalidRevisionId
28
27
import bzrlib.errors
29
28
from bzrlib.textui import show_status
30
29
from bzrlib.revision import Revision
 
30
from bzrlib.xml import unpack_xml
31
31
from bzrlib.delta import compare_trees
32
32
from bzrlib.tree import EmptyTree, RevisionTree
33
 
import bzrlib.xml
34
 
import bzrlib.ui
35
 
 
36
 
 
37
 
 
 
33
from bzrlib.progress import ProgressBar
 
34
 
 
35
       
38
36
BZR_BRANCH_FORMAT = "Bazaar-NG branch, format 0.0.4\n"
39
37
## TODO: Maybe include checks for common corruption of newlines, etc?
40
38
 
108
106
    It is not necessary that f exists.
109
107
 
110
108
    Basically we keep looking up until we find the control directory or
111
 
    run into the root.  If there isn't one, raises NotBranchError.
112
 
    """
 
109
    run into the root."""
113
110
    if f == None:
114
111
        f = os.getcwd()
115
112
    elif hasattr(os.path, 'realpath'):
128
125
        head, tail = os.path.split(f)
129
126
        if head == f:
130
127
            # reached the root, whatever that may be
131
 
            raise bzrlib.errors.NotBranchError('%s is not in a branch' % orig_f)
 
128
            raise BzrError('%r is not in a branch' % orig_f)
132
129
        f = head
133
 
 
134
 
 
135
 
 
136
 
# XXX: move into bzrlib.errors; subclass BzrError    
 
130
    
137
131
class DivergedBranches(Exception):
138
132
    def __init__(self, branch1, branch2):
139
133
        self.branch1 = branch1
219
213
            self._lock.unlock()
220
214
 
221
215
 
 
216
 
222
217
    def lock_write(self):
223
218
        if self._lock_mode:
224
219
            if self._lock_mode != 'w':
234
229
            self._lock_count = 1
235
230
 
236
231
 
 
232
 
237
233
    def lock_read(self):
238
234
        if self._lock_mode:
239
235
            assert self._lock_mode in ('r', 'w'), \
246
242
            self._lock_mode = 'r'
247
243
            self._lock_count = 1
248
244
                        
 
245
 
 
246
            
249
247
    def unlock(self):
250
248
        if not self._lock_mode:
251
249
            from errors import LockError
258
256
            self._lock = None
259
257
            self._lock_mode = self._lock_count = None
260
258
 
 
259
 
261
260
    def abspath(self, name):
262
261
        """Return absolute filename for something in the branch"""
263
262
        return os.path.join(self.base, name)
264
263
 
 
264
 
265
265
    def relpath(self, path):
266
266
        """Return path relative to this branch of something inside it.
267
267
 
268
268
        Raises an error if path is not in this branch."""
269
269
        return _relpath(self.base, path)
270
270
 
 
271
 
271
272
    def controlfilename(self, file_or_path):
272
273
        """Return location relative to branch."""
273
274
        if isinstance(file_or_path, basestring):
300
301
        else:
301
302
            raise BzrError("invalid controlfile mode %r" % mode)
302
303
 
 
304
 
 
305
 
303
306
    def _make_control(self):
304
307
        from bzrlib.inventory import Inventory
 
308
        from bzrlib.xml import pack_xml
305
309
        
306
310
        os.mkdir(self.controlfilename([]))
307
311
        self.controlfile('README', 'w').write(
320
324
        # if we want per-tree root ids then this is the place to set
321
325
        # them; they're not needed for now and so ommitted for
322
326
        # simplicity.
323
 
        f = self.controlfile('inventory','w')
324
 
        bzrlib.xml.serializer_v4.write_inventory(Inventory(), f)
 
327
        pack_xml(Inventory(), self.controlfile('inventory','w'))
325
328
 
326
329
 
327
330
    def _check_format(self):
336
339
        # on Windows from Linux and so on.  I think it might be better
337
340
        # to always make all internal files in unix format.
338
341
        fmt = self.controlfile('branch-format', 'r').read()
339
 
        fmt = fmt.replace('\r\n', '\n')
 
342
        fmt.replace('\r\n', '')
340
343
        if fmt != BZR_BRANCH_FORMAT:
341
344
            raise BzrError('sorry, branch format %r not supported' % fmt,
342
345
                           ['use a different bzr version',
362
365
    def read_working_inventory(self):
363
366
        """Read the working inventory."""
364
367
        from bzrlib.inventory import Inventory
 
368
        from bzrlib.xml import unpack_xml
 
369
        from time import time
 
370
        before = time()
365
371
        self.lock_read()
366
372
        try:
367
373
            # ElementTree does its own conversion from UTF-8, so open in
368
374
            # binary.
369
 
            f = self.controlfile('inventory', 'rb')
370
 
            return bzrlib.xml.serializer_v4.read_inventory(f)
 
375
            inv = unpack_xml(Inventory,
 
376
                             self.controlfile('inventory', 'rb'))
 
377
            mutter("loaded inventory of %d items in %f"
 
378
                   % (len(inv), time() - before))
 
379
            return inv
371
380
        finally:
372
381
            self.unlock()
373
382
            
379
388
        will be committed to the next revision.
380
389
        """
381
390
        from bzrlib.atomicfile import AtomicFile
 
391
        from bzrlib.xml import pack_xml
382
392
        
383
393
        self.lock_write()
384
394
        try:
385
395
            f = AtomicFile(self.controlfilename('inventory'), 'wb')
386
396
            try:
387
 
                bzrlib.xml.serializer_v4.write_inventory(inv, f)
 
397
                pack_xml(inv, f)
388
398
                f.commit()
389
399
            finally:
390
400
                f.close()
398
408
                         """Inventory for the working copy.""")
399
409
 
400
410
 
401
 
    def add(self, files, ids=None):
 
411
    def add(self, files, verbose=False, ids=None):
402
412
        """Make files versioned.
403
413
 
404
 
        Note that the command line normally calls smart_add instead,
405
 
        which can automatically recurse.
 
414
        Note that the command line normally calls smart_add instead.
406
415
 
407
416
        This puts the files in the Added state, so that they will be
408
417
        recorded by the next commit.
418
427
        TODO: Perhaps have an option to add the ids even if the files do
419
428
              not (yet) exist.
420
429
 
421
 
        TODO: Perhaps yield the ids and paths as they're added.
 
430
        TODO: Perhaps return the ids of the files?  But then again it
 
431
              is easy to retrieve them if they're needed.
 
432
 
 
433
        TODO: Adding a directory should optionally recurse down and
 
434
              add all non-ignored children.  Perhaps do that in a
 
435
              higher-level method.
422
436
        """
423
437
        # TODO: Re-adding a file that is removed in the working copy
424
438
        # should probably put it back with the previous ID.
460
474
                    file_id = gen_file_id(f)
461
475
                inv.add_path(f, kind=kind, file_id=file_id)
462
476
 
 
477
                if verbose:
 
478
                    print 'added', quotefn(f)
 
479
 
463
480
                mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
464
481
 
465
482
            self._write_inventory(inv)
575
592
            f.close()
576
593
 
577
594
 
578
 
    def get_revision_xml_file(self, revision_id):
 
595
    def get_revision_xml(self, revision_id):
579
596
        """Return XML file object for revision object."""
580
597
        if not revision_id or not isinstance(revision_id, basestring):
581
598
            raise InvalidRevisionId(revision_id)
584
601
        try:
585
602
            try:
586
603
                return self.revision_store[revision_id]
587
 
            except KeyError:
 
604
            except IndexError:
588
605
                raise bzrlib.errors.NoSuchRevision(self, revision_id)
589
606
        finally:
590
607
            self.unlock()
591
608
 
592
609
 
593
 
    #deprecated
594
 
    get_revision_xml = get_revision_xml_file
595
 
 
596
 
 
597
610
    def get_revision(self, revision_id):
598
611
        """Return the Revision object for a named revision"""
599
 
        xml_file = self.get_revision_xml_file(revision_id)
 
612
        xml_file = self.get_revision_xml(revision_id)
600
613
 
601
614
        try:
602
 
            r = bzrlib.xml.serializer_v4.read_revision(xml_file)
 
615
            r = unpack_xml(Revision, xml_file)
603
616
        except SyntaxError, e:
604
617
            raise bzrlib.errors.BzrError('failed to unpack revision_xml',
605
618
                                         [revision_id,
650
663
               parameter which can be either an integer revno or a
651
664
               string hash."""
652
665
        from bzrlib.inventory import Inventory
 
666
        from bzrlib.xml import unpack_xml
653
667
 
654
 
        f = self.get_inventory_xml_file(inventory_id)
655
 
        return bzrlib.xml.serializer_v4.read_inventory(f)
 
668
        return unpack_xml(Inventory, self.get_inventory_xml(inventory_id))
656
669
 
657
670
 
658
671
    def get_inventory_xml(self, inventory_id):
659
672
        """Get inventory XML as a file object."""
660
673
        return self.inventory_store[inventory_id]
661
 
 
662
 
    get_inventory_xml_file = get_inventory_xml
663
674
            
664
675
 
665
676
    def get_inventory_sha1(self, inventory_id):
796
807
        if stop_revision is None:
797
808
            stop_revision = other_len
798
809
        elif stop_revision > other_len:
799
 
            raise bzrlib.errors.NoSuchRevision(self, stop_revision)
 
810
            raise NoSuchRevision(self, stop_revision)
800
811
        
801
812
        return other_history[self_len:stop_revision]
802
813
 
803
814
 
804
815
    def update_revisions(self, other, stop_revision=None):
805
816
        """Pull in all new revisions from other branch.
 
817
        
 
818
        >>> from bzrlib.commit import commit
 
819
        >>> bzrlib.trace.silent = True
 
820
        >>> br1 = ScratchBranch(files=['foo', 'bar'])
 
821
        >>> br1.add('foo')
 
822
        >>> br1.add('bar')
 
823
        >>> commit(br1, "lala!", rev_id="REVISION-ID-1", verbose=False)
 
824
        >>> br2 = ScratchBranch()
 
825
        >>> br2.update_revisions(br1)
 
826
        Added 2 texts.
 
827
        Added 1 inventories.
 
828
        Added 1 revisions.
 
829
        >>> br2.revision_history()
 
830
        [u'REVISION-ID-1']
 
831
        >>> br2.update_revisions(br1)
 
832
        Added 0 texts.
 
833
        Added 0 inventories.
 
834
        Added 0 revisions.
 
835
        >>> br1.text_store.total_size() == br2.text_store.total_size()
 
836
        True
806
837
        """
807
 
        from bzrlib.fetch import greedy_fetch
808
 
        from bzrlib.revision import get_intervening_revisions
809
 
 
810
 
        pb = bzrlib.ui.ui_factory.progress_bar()
 
838
        pb = ProgressBar()
811
839
        pb.update('comparing histories')
812
 
        if stop_revision is None:
813
 
            other_revision = other.last_patch()
814
 
        else:
815
 
            other_revision = other.lookup_revision(stop_revision)
816
 
        count = greedy_fetch(self, other, other_revision, pb)[0]
817
 
        try:
818
 
            revision_ids = self.missing_revisions(other, stop_revision)
819
 
        except DivergedBranches, e:
820
 
            try:
821
 
                revision_ids = get_intervening_revisions(self.last_patch(), 
822
 
                                                         other_revision, self)
823
 
                assert self.last_patch() not in revision_ids
824
 
            except bzrlib.errors.NotAncestor:
825
 
                raise e
826
 
 
 
840
        revision_ids = self.missing_revisions(other, stop_revision)
 
841
        count, failures = self.install_revisions(other, revision_ids, pb=pb)
827
842
        self.append_revision(*revision_ids)
828
 
        pb.clear()
829
 
 
830
 
    def install_revisions(self, other, revision_ids, pb):
 
843
        print "Added %d revisions." % count
 
844
        assert len(failures) == 0
 
845
                    
 
846
    def install_revisions(self, other, revision_ids, pb=None):
 
847
        if pb is None:
 
848
            pb = ProgressBar()
831
849
        if hasattr(other.revision_store, "prefetch"):
832
850
            other.revision_store.prefetch(revision_ids)
833
851
        if hasattr(other.inventory_store, "prefetch"):
834
 
            inventory_ids = []
835
 
            for rev_id in revision_ids:
836
 
                try:
837
 
                    revision = other.get_revision(rev_id).inventory_id
838
 
                    inventory_ids.append(revision)
839
 
                except bzrlib.errors.NoSuchRevision:
840
 
                    pass
 
852
            inventory_ids = [other.get_revision(r).inventory_id
 
853
                             for r in revision_ids]
841
854
            other.inventory_store.prefetch(inventory_ids)
842
 
 
843
 
        if pb is None:
844
 
            pb = bzrlib.ui.ui_factory.progress_bar()
845
855
                
846
856
        revisions = []
847
857
        needed_texts = set()
848
858
        i = 0
849
 
 
850
859
        failures = set()
851
860
        for i, rev_id in enumerate(revision_ids):
852
861
            pb.update('fetching revision', i+1, len(revision_ids))
855
864
            except bzrlib.errors.NoSuchRevision:
856
865
                failures.add(rev_id)
857
866
                continue
858
 
 
859
867
            revisions.append(rev)
860
868
            inv = other.get_inventory(str(rev.inventory_id))
861
869
            for key, entry in inv.iter_entries():
868
876
                    
869
877
        count, cp_fail = self.text_store.copy_multi(other.text_store, 
870
878
                                                    needed_texts)
871
 
        #print "Added %d texts." % count 
 
879
        print "Added %d texts." % count 
872
880
        inventory_ids = [ f.inventory_id for f in revisions ]
873
881
        count, cp_fail = self.inventory_store.copy_multi(other.inventory_store, 
874
882
                                                         inventory_ids)
875
 
        #print "Added %d inventories." % count 
 
883
        print "Added %d inventories." % count 
876
884
        revision_ids = [ f.revision_id for f in revisions]
877
 
 
878
885
        count, cp_fail = self.revision_store.copy_multi(other.revision_store, 
879
886
                                                          revision_ids,
880
887
                                                          permit_failure=True)
881
888
        assert len(cp_fail) == 0 
882
889
        return count, failures
883
890
       
884
 
 
885
891
    def commit(self, *args, **kw):
886
892
        from bzrlib.commit import commit
887
893
        commit(self, *args, **kw)
889
895
 
890
896
    def lookup_revision(self, revision):
891
897
        """Return the revision identifier for a given revision information."""
892
 
        revno, info = self._get_revision_info(revision)
 
898
        revno, info = self.get_revision_info(revision)
893
899
        return info
894
900
 
895
 
 
896
 
    def revision_id_to_revno(self, revision_id):
897
 
        """Given a revision id, return its revno"""
898
 
        history = self.revision_history()
899
 
        try:
900
 
            return history.index(revision_id) + 1
901
 
        except ValueError:
902
 
            raise bzrlib.errors.NoSuchRevision(self, revision_id)
903
 
 
904
 
 
905
901
    def get_revision_info(self, revision):
906
902
        """Return (revno, revision id) for revision identifier.
907
903
 
910
906
        revision can also be a string, in which case it is parsed for something like
911
907
            'date:' or 'revid:' etc.
912
908
        """
913
 
        revno, rev_id = self._get_revision_info(revision)
914
 
        if revno is None:
915
 
            raise bzrlib.errors.NoSuchRevision(self, revision)
916
 
        return revno, rev_id
917
 
 
918
 
    def get_rev_id(self, revno, history=None):
919
 
        """Find the revision id of the specified revno."""
920
 
        if revno == 0:
921
 
            return None
922
 
        if history is None:
923
 
            history = self.revision_history()
924
 
        elif revno <= 0 or revno > len(history):
925
 
            raise bzrlib.errors.NoSuchRevision(self, revno)
926
 
        return history[revno - 1]
927
 
 
928
 
    def _get_revision_info(self, revision):
929
 
        """Return (revno, revision id) for revision specifier.
930
 
 
931
 
        revision can be an integer, in which case it is assumed to be revno
932
 
        (though this will translate negative values into positive ones)
933
 
        revision can also be a string, in which case it is parsed for something
934
 
        like 'date:' or 'revid:' etc.
935
 
 
936
 
        A revid is always returned.  If it is None, the specifier referred to
937
 
        the null revision.  If the revid does not occur in the revision
938
 
        history, revno will be None.
939
 
        """
940
 
        
941
909
        if revision is None:
942
910
            return 0, None
943
911
        revno = None
947
915
            pass
948
916
        revs = self.revision_history()
949
917
        if isinstance(revision, int):
 
918
            if revision == 0:
 
919
                return 0, None
 
920
            # Mabye we should do this first, but we don't need it if revision == 0
950
921
            if revision < 0:
951
922
                revno = len(revs) + revision + 1
952
923
            else:
953
924
                revno = revision
954
 
            rev_id = self.get_rev_id(revno, revs)
955
925
        elif isinstance(revision, basestring):
956
926
            for prefix, func in Branch.REVISION_NAMESPACES.iteritems():
957
927
                if revision.startswith(prefix):
958
 
                    result = func(self, revs, revision)
959
 
                    if len(result) > 1:
960
 
                        revno, rev_id = result
961
 
                    else:
962
 
                        revno = result[0]
963
 
                        rev_id = self.get_rev_id(revno, revs)
 
928
                    revno = func(self, revs, revision)
964
929
                    break
965
930
            else:
966
 
                raise BzrError('No namespace registered for string: %r' %
967
 
                               revision)
968
 
        else:
969
 
            raise TypeError('Unhandled revision type %s' % revision)
 
931
                raise BzrError('No namespace registered for string: %r' % revision)
970
932
 
971
 
        if revno is None:
972
 
            if rev_id is None:
973
 
                raise bzrlib.errors.NoSuchRevision(self, revision)
974
 
        return revno, rev_id
 
933
        if revno is None or revno <= 0 or revno > len(revs):
 
934
            raise BzrError("no such revision %s" % revision)
 
935
        return revno, revs[revno-1]
975
936
 
976
937
    def _namespace_revno(self, revs, revision):
977
938
        """Lookup a revision by revision number"""
978
939
        assert revision.startswith('revno:')
979
940
        try:
980
 
            return (int(revision[6:]),)
 
941
            return int(revision[6:])
981
942
        except ValueError:
982
943
            return None
983
944
    REVISION_NAMESPACES['revno:'] = _namespace_revno
984
945
 
985
946
    def _namespace_revid(self, revs, revision):
986
947
        assert revision.startswith('revid:')
987
 
        rev_id = revision[len('revid:'):]
988
948
        try:
989
 
            return revs.index(rev_id) + 1, rev_id
 
949
            return revs.index(revision[6:]) + 1
990
950
        except ValueError:
991
 
            return None, rev_id
 
951
            return None
992
952
    REVISION_NAMESPACES['revid:'] = _namespace_revid
993
953
 
994
954
    def _namespace_last(self, revs, revision):
996
956
        try:
997
957
            offset = int(revision[5:])
998
958
        except ValueError:
999
 
            return (None,)
 
959
            return None
1000
960
        else:
1001
961
            if offset <= 0:
1002
962
                raise BzrError('You must supply a positive value for --revision last:XXX')
1003
 
            return (len(revs) - offset + 1,)
 
963
            return len(revs) - offset + 1
1004
964
    REVISION_NAMESPACES['last:'] = _namespace_last
1005
965
 
1006
966
    def _namespace_tag(self, revs, revision):
1081
1041
                # TODO: Handle timezone.
1082
1042
                dt = datetime.datetime.fromtimestamp(r.timestamp)
1083
1043
                if first >= dt and (last is None or dt >= last):
1084
 
                    return (i+1,)
 
1044
                    return i+1
1085
1045
        else:
1086
1046
            for i in range(len(revs)):
1087
1047
                r = self.get_revision(revs[i])
1088
1048
                # TODO: Handle timezone.
1089
1049
                dt = datetime.datetime.fromtimestamp(r.timestamp)
1090
1050
                if first <= dt and (last is None or dt <= last):
1091
 
                    return (i+1,)
 
1051
                    return i+1
1092
1052
    REVISION_NAMESPACES['date:'] = _namespace_date
1093
1053
 
1094
 
 
1095
 
    def _namespace_ancestor(self, revs, revision):
1096
 
        from revision import common_ancestor, MultipleRevisionSources
1097
 
        other_branch = find_branch(_trim_namespace('ancestor', revision))
1098
 
        revision_a = self.last_patch()
1099
 
        revision_b = other_branch.last_patch()
1100
 
        for r, b in ((revision_a, self), (revision_b, other_branch)):
1101
 
            if r is None:
1102
 
                raise bzrlib.errors.NoCommits(b)
1103
 
        revision_source = MultipleRevisionSources(self, other_branch)
1104
 
        result = common_ancestor(revision_a, revision_b, revision_source)
1105
 
        try:
1106
 
            revno = self.revision_id_to_revno(result)
1107
 
        except bzrlib.errors.NoSuchRevision:
1108
 
            revno = None
1109
 
        return revno,result
1110
 
        
1111
 
 
1112
 
    REVISION_NAMESPACES['ancestor:'] = _namespace_ancestor
1113
 
 
1114
1054
    def revision_tree(self, revision_id):
1115
1055
        """Return Tree for a revision on this branch.
1116
1056
 
1179
1119
 
1180
1120
            inv.rename(file_id, to_dir_id, to_tail)
1181
1121
 
 
1122
            print "%s => %s" % (from_rel, to_rel)
 
1123
 
1182
1124
            from_abs = self.abspath(from_rel)
1183
1125
            to_abs = self.abspath(to_rel)
1184
1126
            try:
1203
1145
 
1204
1146
        Note that to_name is only the last component of the new name;
1205
1147
        this doesn't change the directory.
1206
 
 
1207
 
        This returns a list of (from_path, to_path) pairs for each
1208
 
        entry that is moved.
1209
1148
        """
1210
 
        result = []
1211
1149
        self.lock_write()
1212
1150
        try:
1213
1151
            ## TODO: Option to move IDs only
1248
1186
            for f in from_paths:
1249
1187
                name_tail = splitpath(f)[-1]
1250
1188
                dest_path = appendpath(to_name, name_tail)
1251
 
                result.append((f, dest_path))
 
1189
                print "%s => %s" % (f, dest_path)
1252
1190
                inv.rename(inv.path2id(f), to_dir_id, name_tail)
1253
1191
                try:
1254
1192
                    os.rename(self.abspath(f), self.abspath(dest_path))
1260
1198
        finally:
1261
1199
            self.unlock()
1262
1200
 
1263
 
        return result
1264
 
 
1265
1201
 
1266
1202
    def revert(self, filenames, old_tree=None, backups=True):
1267
1203
        """Restore selected files to the versions from a previous tree.
1349
1285
            self.unlock()
1350
1286
 
1351
1287
 
1352
 
    def get_parent(self):
1353
 
        """Return the parent location of the branch.
1354
 
 
1355
 
        This is the default location for push/pull/missing.  The usual
1356
 
        pattern is that the user can override it by specifying a
1357
 
        location.
1358
 
        """
1359
 
        import errno
1360
 
        _locs = ['parent', 'pull', 'x-pull']
1361
 
        for l in _locs:
1362
 
            try:
1363
 
                return self.controlfile(l, 'r').read().strip('\n')
1364
 
            except IOError, e:
1365
 
                if e.errno != errno.ENOENT:
1366
 
                    raise
1367
 
        return None
1368
 
 
1369
 
 
1370
 
    def set_parent(self, url):
1371
 
        # TODO: Maybe delete old location files?
1372
 
        from bzrlib.atomicfile import AtomicFile
1373
 
        self.lock_write()
1374
 
        try:
1375
 
            f = AtomicFile(self.controlfilename('parent'))
1376
 
            try:
1377
 
                f.write(url + '\n')
1378
 
                f.commit()
1379
 
            finally:
1380
 
                f.close()
1381
 
        finally:
1382
 
            self.unlock()
1383
 
 
1384
 
    def check_revno(self, revno):
1385
 
        """\
1386
 
        Check whether a revno corresponds to any revision.
1387
 
        Zero (the NULL revision) is considered valid.
1388
 
        """
1389
 
        if revno != 0:
1390
 
            self.check_real_revno(revno)
1391
 
            
1392
 
    def check_real_revno(self, revno):
1393
 
        """\
1394
 
        Check whether a revno corresponds to a real revision.
1395
 
        Zero (the NULL revision) is considered invalid
1396
 
        """
1397
 
        if revno < 1 or revno > self.revno():
1398
 
            raise InvalidRevisionNumber(revno)
1399
 
        
1400
 
        
1401
 
 
1402
1288
 
1403
1289
class ScratchBranch(Branch):
1404
1290
    """Special test class: a branch that cleans up after itself.
1446
1332
        os.rmdir(base)
1447
1333
        copytree(self.base, base, symlinks=True)
1448
1334
        return ScratchBranch(base=base)
1449
 
 
1450
 
 
1451
1335
        
1452
1336
    def __del__(self):
1453
1337
        self.destroy()
1523
1407
    """Return a new tree-root file id."""
1524
1408
    return gen_file_id('TREE_ROOT')
1525
1409
 
1526
 
 
1527
 
def pull_loc(branch):
1528
 
    # TODO: Should perhaps just make attribute be 'base' in
1529
 
    # RemoteBranch and Branch?
1530
 
    if hasattr(branch, "baseurl"):
1531
 
        return branch.baseurl
1532
 
    else:
1533
 
        return branch.base
1534
 
 
1535
 
 
1536
 
def copy_branch(branch_from, to_location, revision=None):
1537
 
    """Copy branch_from into the existing directory to_location.
1538
 
 
1539
 
    revision
1540
 
        If not None, only revisions up to this point will be copied.
1541
 
        The head of the new branch will be that revision.
1542
 
 
1543
 
    to_location
1544
 
        The name of a local directory that exists but is empty.
1545
 
    """
1546
 
    from bzrlib.merge import merge
1547
 
    from bzrlib.branch import Branch
1548
 
 
1549
 
    assert isinstance(branch_from, Branch)
1550
 
    assert isinstance(to_location, basestring)
1551
 
    
1552
 
    br_to = Branch(to_location, init=True)
1553
 
    br_to.set_root_id(branch_from.get_root_id())
1554
 
    if revision is None:
1555
 
        revno = branch_from.revno()
1556
 
    else:
1557
 
        revno, rev_id = branch_from.get_revision_info(revision)
1558
 
    br_to.update_revisions(branch_from, stop_revision=revno)
1559
 
    merge((to_location, -1), (to_location, 0), this_dir=to_location,
1560
 
          check_clean=False, ignore_zero=True)
1561
 
    
1562
 
    from_location = pull_loc(branch_from)
1563
 
    br_to.set_parent(pull_loc(branch_from))
1564
 
    return br_to
1565
 
 
1566
 
def _trim_namespace(namespace, spec):
1567
 
    full_namespace = namespace + ':'
1568
 
    assert spec.startswith(full_namespace)
1569
 
    return spec[len(full_namespace):]