~bzr-pqm/bzr/bzr.dev

« back to all changes in this revision

Viewing changes to bzrlib/branch.py

  • Committer: Robert Collins
  • Date: 2005-10-17 22:31:39 UTC
  • mto: This revision was merged to the branch mainline in revision 1462.
  • Revision ID: robertc@robertcollins.net-20051017223139-63e850d7069466d6
Remove self.lock_*...finally: self.unlock() dead chickens from branch.py.

Two decorators, needs_read_lock and needs_write_lock have been added
to the branch module. Use these to cause a function to run in a
read or write lock respectively.

Show diffs side-by-side

added added

removed removed

Lines of Context:
67
67
    raise NotImplementedError('find_branch() is not supported anymore, '
68
68
                              'please use one of the new branch constructors')
69
69
 
 
70
 
 
71
def needs_read_lock(unbound):
 
72
    """Decorate unbound to take out and release a read lock."""
 
73
    def decorated(self, *args, **kwargs):
 
74
        self.lock_read()
 
75
        try:
 
76
            return unbound(self, *args, **kwargs)
 
77
        finally:
 
78
            self.unlock()
 
79
    return decorated
 
80
 
 
81
 
 
82
def needs_write_lock(unbound):
 
83
    """Decorate unbound to take out and release a write lock."""
 
84
    def decorated(self, *args, **kwargs):
 
85
        self.lock_write()
 
86
        try:
 
87
            return unbound(self, *args, **kwargs)
 
88
        finally:
 
89
            self.unlock()
 
90
    return decorated
 
91
 
70
92
######################################################################
71
93
# branch objects
72
94
 
507
529
                entry.parent_id = inv.root.file_id
508
530
        self._write_inventory(inv)
509
531
 
 
532
    @needs_read_lock
510
533
    def read_working_inventory(self):
511
534
        """Read the working inventory."""
512
 
        self.lock_read()
513
 
        try:
514
 
            # ElementTree does its own conversion from UTF-8, so open in
515
 
            # binary.
516
 
            f = self.controlfile('inventory', 'rb')
517
 
            return bzrlib.xml5.serializer_v5.read_inventory(f)
518
 
        finally:
519
 
            self.unlock()
520
 
            
 
535
        # ElementTree does its own conversion from UTF-8, so open in
 
536
        # binary.
 
537
        f = self.controlfile('inventory', 'rb')
 
538
        return bzrlib.xml5.serializer_v5.read_inventory(f)
521
539
 
 
540
    @needs_write_lock
522
541
    def _write_inventory(self, inv):
523
542
        """Update the working inventory.
524
543
 
526
545
        will be committed to the next revision.
527
546
        """
528
547
        from cStringIO import StringIO
529
 
        self.lock_write()
530
 
        try:
531
 
            sio = StringIO()
532
 
            bzrlib.xml5.serializer_v5.write_inventory(inv, sio)
533
 
            sio.seek(0)
534
 
            # Transport handles atomicity
535
 
            self.put_controlfile('inventory', sio)
536
 
        finally:
537
 
            self.unlock()
 
548
        sio = StringIO()
 
549
        bzrlib.xml5.serializer_v5.write_inventory(inv, sio)
 
550
        sio.seek(0)
 
551
        # Transport handles atomicity
 
552
        self.put_controlfile('inventory', sio)
538
553
        
539
554
        mutter('wrote working inventory')
540
555
            
541
556
    inventory = property(read_working_inventory, _write_inventory, None,
542
557
                         """Inventory for the working copy.""")
543
558
 
 
559
    @needs_write_lock
544
560
    def add(self, files, ids=None):
545
561
        """Make files versioned.
546
562
 
576
592
        else:
577
593
            assert(len(ids) == len(files))
578
594
 
579
 
        self.lock_write()
580
 
        try:
581
 
            inv = self.read_working_inventory()
582
 
            for f,file_id in zip(files, ids):
583
 
                if is_control_file(f):
584
 
                    raise BzrError("cannot add control file %s" % quotefn(f))
585
 
 
586
 
                fp = splitpath(f)
587
 
 
588
 
                if len(fp) == 0:
589
 
                    raise BzrError("cannot add top-level %r" % f)
590
 
 
591
 
                fullpath = os.path.normpath(self.abspath(f))
592
 
 
593
 
                try:
594
 
                    kind = file_kind(fullpath)
595
 
                except OSError:
596
 
                    # maybe something better?
597
 
                    raise BzrError('cannot add: not a regular file, symlink or directory: %s' % quotefn(f))
598
 
 
599
 
                if not InventoryEntry.versionable_kind(kind):
600
 
                    raise BzrError('cannot add: not a versionable file ('
601
 
                                   'i.e. regular file, symlink or directory): %s' % quotefn(f))
602
 
 
603
 
                if file_id is None:
604
 
                    file_id = gen_file_id(f)
605
 
                inv.add_path(f, kind=kind, file_id=file_id)
606
 
 
607
 
                mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
608
 
 
609
 
            self._write_inventory(inv)
610
 
        finally:
611
 
            self.unlock()
612
 
            
613
 
 
 
595
        inv = self.read_working_inventory()
 
596
        for f,file_id in zip(files, ids):
 
597
            if is_control_file(f):
 
598
                raise BzrError("cannot add control file %s" % quotefn(f))
 
599
 
 
600
            fp = splitpath(f)
 
601
 
 
602
            if len(fp) == 0:
 
603
                raise BzrError("cannot add top-level %r" % f)
 
604
 
 
605
            fullpath = os.path.normpath(self.abspath(f))
 
606
 
 
607
            try:
 
608
                kind = file_kind(fullpath)
 
609
            except OSError:
 
610
                # maybe something better?
 
611
                raise BzrError('cannot add: not a regular file, symlink or directory: %s' % quotefn(f))
 
612
 
 
613
            if not InventoryEntry.versionable_kind(kind):
 
614
                raise BzrError('cannot add: not a versionable file ('
 
615
                               'i.e. regular file, symlink or directory): %s' % quotefn(f))
 
616
 
 
617
            if file_id is None:
 
618
                file_id = gen_file_id(f)
 
619
            inv.add_path(f, kind=kind, file_id=file_id)
 
620
 
 
621
            mutter("add file %s file_id:{%s} kind=%r" % (f, file_id, kind))
 
622
 
 
623
        self._write_inventory(inv)
 
624
 
 
625
    @needs_read_lock
614
626
    def print_file(self, file, revno):
615
627
        """Print `file` to stdout."""
616
 
        self.lock_read()
617
 
        try:
618
 
            tree = self.revision_tree(self.get_rev_id(revno))
619
 
            # use inventory as it was in that revision
620
 
            file_id = tree.inventory.path2id(file)
621
 
            if not file_id:
622
 
                raise BzrError("%r is not present in revision %s" % (file, revno))
623
 
            tree.print_file(file_id)
624
 
        finally:
625
 
            self.unlock()
626
 
 
627
 
 
 
628
        tree = self.revision_tree(self.get_rev_id(revno))
 
629
        # use inventory as it was in that revision
 
630
        file_id = tree.inventory.path2id(file)
 
631
        if not file_id:
 
632
            raise BzrError("%r is not present in revision %s" % (file, revno))
 
633
        tree.print_file(file_id)
 
634
 
 
635
    @needs_write_lock
628
636
    def remove(self, files, verbose=False):
629
637
        """Mark nominated files for removal from the inventory.
630
638
 
644
652
        if isinstance(files, basestring):
645
653
            files = [files]
646
654
 
647
 
        self.lock_write()
648
 
 
649
 
        try:
650
 
            tree = self.working_tree()
651
 
            inv = tree.inventory
652
 
 
653
 
            # do this before any modifications
654
 
            for f in files:
655
 
                fid = inv.path2id(f)
656
 
                if not fid:
657
 
                    raise BzrError("cannot remove unversioned file %s" % quotefn(f))
658
 
                mutter("remove inventory entry %s {%s}" % (quotefn(f), fid))
659
 
                if verbose:
660
 
                    # having remove it, it must be either ignored or unknown
661
 
                    if tree.is_ignored(f):
662
 
                        new_status = 'I'
663
 
                    else:
664
 
                        new_status = '?'
665
 
                    show_status(new_status, inv[fid].kind, quotefn(f))
666
 
                del inv[fid]
667
 
 
668
 
            self._write_inventory(inv)
669
 
        finally:
670
 
            self.unlock()
 
655
        tree = self.working_tree()
 
656
        inv = tree.inventory
 
657
 
 
658
        # do this before any modifications
 
659
        for f in files:
 
660
            fid = inv.path2id(f)
 
661
            if not fid:
 
662
                raise BzrError("cannot remove unversioned file %s" % quotefn(f))
 
663
            mutter("remove inventory entry %s {%s}" % (quotefn(f), fid))
 
664
            if verbose:
 
665
                # having remove it, it must be either ignored or unknown
 
666
                if tree.is_ignored(f):
 
667
                    new_status = 'I'
 
668
                else:
 
669
                    new_status = '?'
 
670
                show_status(new_status, inv[fid].kind, quotefn(f))
 
671
            del inv[fid]
 
672
 
 
673
        self._write_inventory(inv)
671
674
 
672
675
    # FIXME: this doesn't need to be a branch method
673
676
    def set_inventory(self, new_inventory_list):
706
709
        """
707
710
        return self.working_tree().unknowns()
708
711
 
709
 
 
 
712
    @needs_write_lock
710
713
    def append_revision(self, *revision_ids):
711
714
        for revision_id in revision_ids:
712
715
            mutter("add {%s} to revision-history" % revision_id)
713
 
        self.lock_write()
714
 
        try:
715
 
            rev_history = self.revision_history()
716
 
            rev_history.extend(revision_ids)
717
 
            self.put_controlfile('revision-history', '\n'.join(rev_history))
718
 
        finally:
719
 
            self.unlock()
 
716
        rev_history = self.revision_history()
 
717
        rev_history.extend(revision_ids)
 
718
        self.put_controlfile('revision-history', '\n'.join(rev_history))
720
719
 
721
720
    def has_revision(self, revision_id):
722
721
        """True if this branch has a copy of the revision.
726
725
        return (revision_id is None
727
726
                or self.revision_store.has_id(revision_id))
728
727
 
 
728
    @needs_read_lock
729
729
    def get_revision_xml_file(self, revision_id):
730
730
        """Return XML file object for revision object."""
731
731
        if not revision_id or not isinstance(revision_id, basestring):
732
732
            raise InvalidRevisionId(revision_id)
733
 
 
734
 
        self.lock_read()
735
733
        try:
736
 
            try:
737
 
                return self.revision_store.get(revision_id)
738
 
            except (IndexError, KeyError):
739
 
                raise bzrlib.errors.NoSuchRevision(self, revision_id)
740
 
        finally:
741
 
            self.unlock()
 
734
            return self.revision_store.get(revision_id)
 
735
        except (IndexError, KeyError):
 
736
            raise bzrlib.errors.NoSuchRevision(self, revision_id)
742
737
 
743
738
    #deprecated
744
739
    get_revision_xml = get_revision_xml_file
837
832
        else:
838
833
            return self.get_inventory(revision_id)
839
834
 
 
835
    @needs_read_lock
840
836
    def revision_history(self):
841
837
        """Return sequence of revision hashes on to this branch."""
842
 
        self.lock_read()
843
 
        try:
844
 
            transaction = self.get_transaction()
845
 
            history = transaction.map.find_revision_history()
846
 
            if history is not None:
847
 
                mutter("cache hit for revision-history in %s", self)
848
 
                return list(history)
849
 
            history = [l.rstrip('\r\n') for l in
850
 
                    self.controlfile('revision-history', 'r').readlines()]
851
 
            transaction.map.add_revision_history(history)
852
 
            # this call is disabled because revision_history is 
853
 
            # not really an object yet, and the transaction is for objects.
854
 
            # transaction.register_clean(history, precious=True)
 
838
        transaction = self.get_transaction()
 
839
        history = transaction.map.find_revision_history()
 
840
        if history is not None:
 
841
            mutter("cache hit for revision-history in %s", self)
855
842
            return list(history)
856
 
        finally:
857
 
            self.unlock()
 
843
        history = [l.rstrip('\r\n') for l in
 
844
                self.controlfile('revision-history', 'r').readlines()]
 
845
        transaction.map.add_revision_history(history)
 
846
        # this call is disabled because revision_history is 
 
847
        # not really an object yet, and the transaction is for objects.
 
848
        # transaction.register_clean(history, precious=True)
 
849
        return list(history)
858
850
 
859
851
    def revno(self):
860
852
        """Return current revision number for this branch.
864
856
        """
865
857
        return len(self.revision_history())
866
858
 
867
 
 
868
859
    def last_revision(self):
869
860
        """Return last patch hash, or None if no history.
870
861
        """
874
865
        else:
875
866
            return None
876
867
 
877
 
 
878
868
    def missing_revisions(self, other, stop_revision=None, diverged_ok=False):
879
869
        """Return a list of new revisions that would perfectly fit.
880
870
        
1009
999
        """
1010
1000
        return self.revision_tree(self.last_revision())
1011
1001
 
1012
 
 
 
1002
    @needs_write_lock
1013
1003
    def rename_one(self, from_rel, to_rel):
1014
1004
        """Rename one file.
1015
1005
 
1016
1006
        This can change the directory or the filename or both.
1017
1007
        """
1018
 
        self.lock_write()
 
1008
        tree = self.working_tree()
 
1009
        inv = tree.inventory
 
1010
        if not tree.has_filename(from_rel):
 
1011
            raise BzrError("can't rename: old working file %r does not exist" % from_rel)
 
1012
        if tree.has_filename(to_rel):
 
1013
            raise BzrError("can't rename: new working file %r already exists" % to_rel)
 
1014
 
 
1015
        file_id = inv.path2id(from_rel)
 
1016
        if file_id == None:
 
1017
            raise BzrError("can't rename: old name %r is not versioned" % from_rel)
 
1018
 
 
1019
        if inv.path2id(to_rel):
 
1020
            raise BzrError("can't rename: new name %r is already versioned" % to_rel)
 
1021
 
 
1022
        to_dir, to_tail = os.path.split(to_rel)
 
1023
        to_dir_id = inv.path2id(to_dir)
 
1024
        if to_dir_id == None and to_dir != '':
 
1025
            raise BzrError("can't determine destination directory id for %r" % to_dir)
 
1026
 
 
1027
        mutter("rename_one:")
 
1028
        mutter("  file_id    {%s}" % file_id)
 
1029
        mutter("  from_rel   %r" % from_rel)
 
1030
        mutter("  to_rel     %r" % to_rel)
 
1031
        mutter("  to_dir     %r" % to_dir)
 
1032
        mutter("  to_dir_id  {%s}" % to_dir_id)
 
1033
 
 
1034
        inv.rename(file_id, to_dir_id, to_tail)
 
1035
 
 
1036
        from_abs = self.abspath(from_rel)
 
1037
        to_abs = self.abspath(to_rel)
1019
1038
        try:
1020
 
            tree = self.working_tree()
1021
 
            inv = tree.inventory
1022
 
            if not tree.has_filename(from_rel):
1023
 
                raise BzrError("can't rename: old working file %r does not exist" % from_rel)
1024
 
            if tree.has_filename(to_rel):
1025
 
                raise BzrError("can't rename: new working file %r already exists" % to_rel)
1026
 
 
1027
 
            file_id = inv.path2id(from_rel)
1028
 
            if file_id == None:
1029
 
                raise BzrError("can't rename: old name %r is not versioned" % from_rel)
1030
 
 
1031
 
            if inv.path2id(to_rel):
1032
 
                raise BzrError("can't rename: new name %r is already versioned" % to_rel)
1033
 
 
1034
 
            to_dir, to_tail = os.path.split(to_rel)
1035
 
            to_dir_id = inv.path2id(to_dir)
1036
 
            if to_dir_id == None and to_dir != '':
1037
 
                raise BzrError("can't determine destination directory id for %r" % to_dir)
1038
 
 
1039
 
            mutter("rename_one:")
1040
 
            mutter("  file_id    {%s}" % file_id)
1041
 
            mutter("  from_rel   %r" % from_rel)
1042
 
            mutter("  to_rel     %r" % to_rel)
1043
 
            mutter("  to_dir     %r" % to_dir)
1044
 
            mutter("  to_dir_id  {%s}" % to_dir_id)
1045
 
 
1046
 
            inv.rename(file_id, to_dir_id, to_tail)
1047
 
 
1048
 
            from_abs = self.abspath(from_rel)
1049
 
            to_abs = self.abspath(to_rel)
1050
 
            try:
1051
 
                rename(from_abs, to_abs)
1052
 
            except OSError, e:
1053
 
                raise BzrError("failed to rename %r to %r: %s"
1054
 
                        % (from_abs, to_abs, e[1]),
1055
 
                        ["rename rolled back"])
1056
 
 
1057
 
            self._write_inventory(inv)
1058
 
        finally:
1059
 
            self.unlock()
1060
 
 
1061
 
 
 
1039
            rename(from_abs, to_abs)
 
1040
        except OSError, e:
 
1041
            raise BzrError("failed to rename %r to %r: %s"
 
1042
                    % (from_abs, to_abs, e[1]),
 
1043
                    ["rename rolled back"])
 
1044
 
 
1045
        self._write_inventory(inv)
 
1046
 
 
1047
    @needs_write_lock
1062
1048
    def move(self, from_paths, to_name):
1063
1049
        """Rename files.
1064
1050
 
1074
1060
        entry that is moved.
1075
1061
        """
1076
1062
        result = []
1077
 
        self.lock_write()
1078
 
        try:
1079
 
            ## TODO: Option to move IDs only
1080
 
            assert not isinstance(from_paths, basestring)
1081
 
            tree = self.working_tree()
1082
 
            inv = tree.inventory
1083
 
            to_abs = self.abspath(to_name)
1084
 
            if not isdir(to_abs):
1085
 
                raise BzrError("destination %r is not a directory" % to_abs)
1086
 
            if not tree.has_filename(to_name):
1087
 
                raise BzrError("destination %r not in working directory" % to_abs)
1088
 
            to_dir_id = inv.path2id(to_name)
1089
 
            if to_dir_id == None and to_name != '':
1090
 
                raise BzrError("destination %r is not a versioned directory" % to_name)
1091
 
            to_dir_ie = inv[to_dir_id]
1092
 
            if to_dir_ie.kind not in ('directory', 'root_directory'):
1093
 
                raise BzrError("destination %r is not a directory" % to_abs)
1094
 
 
1095
 
            to_idpath = inv.get_idpath(to_dir_id)
1096
 
 
1097
 
            for f in from_paths:
1098
 
                if not tree.has_filename(f):
1099
 
                    raise BzrError("%r does not exist in working tree" % f)
1100
 
                f_id = inv.path2id(f)
1101
 
                if f_id == None:
1102
 
                    raise BzrError("%r is not versioned" % f)
1103
 
                name_tail = splitpath(f)[-1]
1104
 
                dest_path = appendpath(to_name, name_tail)
1105
 
                if tree.has_filename(dest_path):
1106
 
                    raise BzrError("destination %r already exists" % dest_path)
1107
 
                if f_id in to_idpath:
1108
 
                    raise BzrError("can't move %r to a subdirectory of itself" % f)
1109
 
 
1110
 
            # OK, so there's a race here, it's possible that someone will
1111
 
            # create a file in this interval and then the rename might be
1112
 
            # left half-done.  But we should have caught most problems.
1113
 
 
1114
 
            for f in from_paths:
1115
 
                name_tail = splitpath(f)[-1]
1116
 
                dest_path = appendpath(to_name, name_tail)
1117
 
                result.append((f, dest_path))
1118
 
                inv.rename(inv.path2id(f), to_dir_id, name_tail)
1119
 
                try:
1120
 
                    rename(self.abspath(f), self.abspath(dest_path))
1121
 
                except OSError, e:
1122
 
                    raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
1123
 
                            ["rename rolled back"])
1124
 
 
1125
 
            self._write_inventory(inv)
1126
 
        finally:
1127
 
            self.unlock()
1128
 
 
 
1063
        ## TODO: Option to move IDs only
 
1064
        assert not isinstance(from_paths, basestring)
 
1065
        tree = self.working_tree()
 
1066
        inv = tree.inventory
 
1067
        to_abs = self.abspath(to_name)
 
1068
        if not isdir(to_abs):
 
1069
            raise BzrError("destination %r is not a directory" % to_abs)
 
1070
        if not tree.has_filename(to_name):
 
1071
            raise BzrError("destination %r not in working directory" % to_abs)
 
1072
        to_dir_id = inv.path2id(to_name)
 
1073
        if to_dir_id == None and to_name != '':
 
1074
            raise BzrError("destination %r is not a versioned directory" % to_name)
 
1075
        to_dir_ie = inv[to_dir_id]
 
1076
        if to_dir_ie.kind not in ('directory', 'root_directory'):
 
1077
            raise BzrError("destination %r is not a directory" % to_abs)
 
1078
 
 
1079
        to_idpath = inv.get_idpath(to_dir_id)
 
1080
 
 
1081
        for f in from_paths:
 
1082
            if not tree.has_filename(f):
 
1083
                raise BzrError("%r does not exist in working tree" % f)
 
1084
            f_id = inv.path2id(f)
 
1085
            if f_id == None:
 
1086
                raise BzrError("%r is not versioned" % f)
 
1087
            name_tail = splitpath(f)[-1]
 
1088
            dest_path = appendpath(to_name, name_tail)
 
1089
            if tree.has_filename(dest_path):
 
1090
                raise BzrError("destination %r already exists" % dest_path)
 
1091
            if f_id in to_idpath:
 
1092
                raise BzrError("can't move %r to a subdirectory of itself" % f)
 
1093
 
 
1094
        # OK, so there's a race here, it's possible that someone will
 
1095
        # create a file in this interval and then the rename might be
 
1096
        # left half-done.  But we should have caught most problems.
 
1097
 
 
1098
        for f in from_paths:
 
1099
            name_tail = splitpath(f)[-1]
 
1100
            dest_path = appendpath(to_name, name_tail)
 
1101
            result.append((f, dest_path))
 
1102
            inv.rename(inv.path2id(f), to_dir_id, name_tail)
 
1103
            try:
 
1104
                rename(self.abspath(f), self.abspath(dest_path))
 
1105
            except OSError, e:
 
1106
                raise BzrError("failed to rename %r to %r: %s" % (f, dest_path, e[1]),
 
1107
                        ["rename rolled back"])
 
1108
 
 
1109
        self._write_inventory(inv)
1129
1110
        return result
1130
1111
 
1131
1112
 
1201
1182
        if updated:
1202
1183
            self.set_pending_merges(p)
1203
1184
 
 
1185
    @needs_write_lock
1204
1186
    def set_pending_merges(self, rev_list):
1205
 
        self.lock_write()
1206
 
        try:
1207
 
            self.put_controlfile('pending-merges', '\n'.join(rev_list))
1208
 
        finally:
1209
 
            self.unlock()
1210
 
 
 
1187
        self.put_controlfile('pending-merges', '\n'.join(rev_list))
1211
1188
 
1212
1189
    def get_parent(self):
1213
1190
        """Return the parent location of the branch.
1226
1203
                    raise
1227
1204
        return None
1228
1205
 
1229
 
 
 
1206
    @needs_write_lock
1230
1207
    def set_parent(self, url):
1231
1208
        # TODO: Maybe delete old location files?
1232
1209
        from bzrlib.atomicfile import AtomicFile
1233
 
        self.lock_write()
 
1210
        f = AtomicFile(self.controlfilename('parent'))
1234
1211
        try:
1235
 
            f = AtomicFile(self.controlfilename('parent'))
1236
 
            try:
1237
 
                f.write(url + '\n')
1238
 
                f.commit()
1239
 
            finally:
1240
 
                f.close()
 
1212
            f.write(url + '\n')
 
1213
            f.commit()
1241
1214
        finally:
1242
 
            self.unlock()
 
1215
            f.close()
1243
1216
 
1244
1217
    def check_revno(self, revno):
1245
1218
        """\
1261
1234
        plaintext = Testament.from_revision(self, revision_id).as_short_text()
1262
1235
        self.store_revision_signature(gpg_strategy, plaintext, revision_id)
1263
1236
 
 
1237
    @needs_write_lock
1264
1238
    def store_revision_signature(self, gpg_strategy, plaintext, revision_id):
1265
 
        self.lock_write()
1266
 
        try:
1267
 
            self.revision_store.add(StringIO(gpg_strategy.sign(plaintext)), 
1268
 
                                    revision_id, "sig")
1269
 
        finally:
1270
 
            self.unlock()
 
1239
        self.revision_store.add(StringIO(gpg_strategy.sign(plaintext)), 
 
1240
                                revision_id, "sig")
1271
1241
 
1272
1242
 
1273
1243
class ScratchBranch(_Branch):