/brz/remove-bazaar

To get this branch, use:
bzr branch http://gegoxaren.bato24.eu/bzr/brz/remove-bazaar

« back to all changes in this revision

Viewing changes to breezy/merge.py

  • Committer: Jelmer Vernooij
  • Date: 2018-11-11 04:08:32 UTC
  • mto: (7143.16.20 even-more-cleanups)
  • mto: This revision was merged to the branch mainline in revision 7175.
  • Revision ID: jelmer@jelmer.uk-20181111040832-nsljjynzzwmznf3h
Run autopep8.

Show diffs side-by-side

added added

removed removed

Lines of Context:
57
57
def transform_tree(from_tree, to_tree, interesting_files=None):
58
58
    with from_tree.lock_tree_write():
59
59
        merge_inner(from_tree.branch, to_tree, from_tree,
60
 
            ignore_zero=True, this_tree=from_tree,
61
 
            interesting_files=interesting_files)
 
60
                    ignore_zero=True, this_tree=from_tree,
 
61
                    interesting_files=interesting_files)
62
62
 
63
63
 
64
64
class MergeHooks(hooks.Hooks):
66
66
    def __init__(self):
67
67
        hooks.Hooks.__init__(self, "breezy.merge", "Merger.hooks")
68
68
        self.add_hook('merge_file_content',
69
 
            "Called with a breezy.merge.Merger object to create a per file "
70
 
            "merge object when starting a merge. "
71
 
            "Should return either None or a subclass of "
72
 
            "``breezy.merge.AbstractPerFileMerger``. "
73
 
            "Such objects will then be called per file "
74
 
            "that needs to be merged (including when one "
75
 
            "side has deleted the file and the other has changed it). "
76
 
            "See the AbstractPerFileMerger API docs for details on how it is "
77
 
            "used by merge.",
78
 
            (2, 1))
 
69
                      "Called with a breezy.merge.Merger object to create a per file "
 
70
                      "merge object when starting a merge. "
 
71
                      "Should return either None or a subclass of "
 
72
                      "``breezy.merge.AbstractPerFileMerger``. "
 
73
                      "Such objects will then be called per file "
 
74
                      "that needs to be merged (including when one "
 
75
                      "side has deleted the file and the other has changed it). "
 
76
                      "See the AbstractPerFileMerger API docs for details on how it is "
 
77
                      "used by merge.",
 
78
                      (2, 1))
79
79
        self.add_hook('pre_merge',
80
 
            'Called before a merge. '
81
 
            'Receives a Merger object as the single argument.',
82
 
            (2, 5))
 
80
                      'Called before a merge. '
 
81
                      'Receives a Merger object as the single argument.',
 
82
                      (2, 5))
83
83
        self.add_hook('post_merge',
84
 
            'Called after a merge. '
85
 
            'Receives a Merger object as the single argument. '
86
 
            'The return value is ignored.',
87
 
            (2, 5))
 
84
                      'Called after a merge. '
 
85
                      'Receives a Merger object as the single argument. '
 
86
                      'The return value is ignored.',
 
87
                      (2, 5))
88
88
 
89
89
 
90
90
class AbstractPerFileMerger(object):
136
136
            # THIS and OTHER aren't both files.
137
137
            not params.is_file_merge() or
138
138
            # The filename doesn't match
139
 
            not self.file_matches(params)):
 
139
                not self.file_matches(params)):
140
140
            return 'not_applicable', None
141
141
        return self.merge_matching(params)
142
142
 
233
233
    """
234
234
 
235
235
    def __init__(self, merger, file_id, paths, trans_id, this_kind, other_kind,
236
 
            winner):
 
236
                 winner):
237
237
        self._merger = merger
238
238
        self.file_id = file_id
239
239
        self.paths = paths
369
369
        if base_revision_id is not None:
370
370
            if (base_revision_id != _mod_revision.NULL_REVISION and
371
371
                revision_graph.is_ancestor(
372
 
                base_revision_id, tree.branch.last_revision())):
 
372
                    base_revision_id, tree.branch.last_revision())):
373
373
                base_revision_id = None
374
374
            else:
375
375
                trace.warning('Performing cherrypick')
376
376
        merger = klass.from_revision_ids(tree, other_revision_id,
377
 
                                         base_revision_id, revision_graph=
378
 
                                         revision_graph)
 
377
                                         base_revision_id, revision_graph=revision_graph)
379
378
        return merger, verified
380
379
 
381
380
    @staticmethod
442
441
 
443
442
    def set_pending(self):
444
443
        if (not self.base_is_ancestor or not self.base_is_other_ancestor
445
 
            or self.other_rev_id is None):
 
444
                or self.other_rev_id is None):
446
445
            return
447
446
        self._add_parent()
448
447
 
487
486
                raise errors.NoCommits(self.other_branch)
488
487
        if self.other_rev_id is not None:
489
488
            self._cached_trees[self.other_rev_id] = self.other_tree
490
 
        self._maybe_fetch(self.other_branch, self.this_branch, self.other_basis)
 
489
        self._maybe_fetch(self.other_branch,
 
490
                          self.this_branch, self.other_basis)
491
491
 
492
492
    def set_other_revision(self, revision_id, other_branch):
493
493
        """Set 'other' based on a branch and revision id
530
530
                self.base_rev_id = _mod_revision.NULL_REVISION
531
531
            elif len(lcas) == 1:
532
532
                self.base_rev_id = list(lcas)[0]
533
 
            else: # len(lcas) > 1
 
533
            else:  # len(lcas) > 1
534
534
                self._is_criss_cross = True
535
535
                if len(lcas) > 2:
536
536
                    # find_unique_lca can only handle 2 nodes, so we have to
538
538
                    # the graph again, but better than re-implementing
539
539
                    # find_unique_lca.
540
540
                    self.base_rev_id = self.revision_graph.find_unique_lca(
541
 
                                            revisions[0], revisions[1])
 
541
                        revisions[0], revisions[1])
542
542
                else:
543
543
                    self.base_rev_id = self.revision_graph.find_unique_lca(
544
 
                                            *lcas)
 
544
                        *lcas)
545
545
                sorted_lca_keys = self.revision_graph.find_merge_order(
546
546
                    revisions[0], lcas)
547
547
                if self.base_rev_id == _mod_revision.NULL_REVISION:
560
560
                interesting_revision_ids = set(lcas)
561
561
                interesting_revision_ids.add(self.base_rev_id)
562
562
                interesting_trees = dict((t.get_revision_id(), t)
563
 
                    for t in self.this_branch.repository.revision_trees(
564
 
                        interesting_revision_ids))
 
563
                                         for t in self.this_branch.repository.revision_trees(
 
564
                    interesting_revision_ids))
565
565
                self._cached_trees.update(interesting_trees)
566
566
                if self.base_rev_id in lcas:
567
567
                    self.base_tree = interesting_trees[self.base_rev_id]
615
615
            raise errors.BzrError("Showing base is not supported for this"
616
616
                                  " merge type. %s" % self.merge_type)
617
617
        if (not getattr(self.merge_type, 'supports_reverse_cherrypick', True)
618
 
            and not self.base_is_other_ancestor):
 
618
                and not self.base_is_other_ancestor):
619
619
            raise errors.CannotReverseCherrypick()
620
620
        if self.merge_type.supports_cherrypick:
621
621
            kwargs['cherrypick'] = (not self.base_is_ancestor or
640
640
                sub_tree = self.this_tree.get_nested_tree(relpath, file_id)
641
641
                other_revision = self.other_tree.get_reference_revision(
642
642
                    relpath, file_id)
643
 
                if  other_revision == sub_tree.last_revision():
 
643
                if other_revision == sub_tree.last_revision():
644
644
                    continue
645
645
                sub_merge = Merger(sub_tree.branch, this_tree=sub_tree)
646
646
                sub_merge.merge_type = self.merge_type
692
692
    symlink_target = None
693
693
    text_sha1 = None
694
694
 
 
695
 
695
696
_none_entry = _InventoryNoneEntry()
696
697
 
697
698
 
806
807
                # Try merging each entry
807
808
                child_pb.update(gettext('Preparing file merge'),
808
809
                                num, len(entries))
809
 
                self._merge_names(file_id, paths3, parents3, names3, resolver=resolver)
 
810
                self._merge_names(file_id, paths3, parents3,
 
811
                                  names3, resolver=resolver)
810
812
                if changed:
811
813
                    file_status = self._do_merge_contents(paths3, file_id)
812
814
                else:
813
815
                    file_status = 'unmodified'
814
816
                self._merge_executable(paths3, file_id, executable3,
815
 
                        file_status, resolver=resolver)
 
817
                                       file_status, resolver=resolver)
816
818
        self.tt.fixup_new_roots()
817
819
        self._finish_computing_transform()
818
820
 
823
825
        """
824
826
        with ui.ui_factory.nested_progress_bar() as child_pb:
825
827
            fs_conflicts = transform.resolve_conflicts(self.tt, child_pb,
826
 
                lambda t, c: transform.conflict_pass(t, c, self.other_tree))
 
828
                                                       lambda t, c: transform.conflict_pass(t, c, self.other_tree))
827
829
        if self.change_reporter is not None:
828
830
            from breezy import delta
829
831
            delta.report_changes(
843
845
        """
844
846
        result = []
845
847
        iterator = self.other_tree.iter_changes(self.base_tree,
846
 
                specific_files=self.interesting_files,
847
 
                extra_trees=[self.this_tree])
 
848
                                                specific_files=self.interesting_files,
 
849
                                                extra_trees=[self.this_tree])
848
850
        this_interesting_files = self.this_tree.find_related_paths_across_trees(
849
 
                self.interesting_files, trees=[self.other_tree])
 
851
            self.interesting_files, trees=[self.other_tree])
850
852
        this_entries = dict(self.this_tree.iter_entries_by_dir(
851
853
                            specific_files=this_interesting_files))
852
854
        for (file_id, paths, changed, versioned, parents, names, kind,
853
855
             executable) in iterator:
854
856
            if paths[0] is not None:
855
857
                this_path = _mod_tree.find_previous_path(
856
 
                        self.base_tree, self.this_tree, paths[0])
 
858
                    self.base_tree, self.this_tree, paths[0])
857
859
            else:
858
860
                this_path = _mod_tree.find_previous_path(
859
 
                        self.other_tree, self.this_tree, paths[1])
 
861
                    self.other_tree, self.this_tree, paths[1])
860
862
            this_entry = this_entries.get(this_path)
861
863
            if this_entry is not None:
862
864
                this_name = this_entry.name
870
872
            names3 = names + (this_name,)
871
873
            paths3 = paths + (this_path, )
872
874
            executable3 = executable + (this_executable,)
873
 
            result.append((file_id, changed, paths3, parents3, names3, executable3))
 
875
            result.append((file_id, changed, paths3,
 
876
                           parents3, names3, executable3))
874
877
        return result
875
878
 
876
879
    def _entries_lca(self):
897
900
            lookup_trees.extend(self._lca_trees)
898
901
            # I think we should include the lca trees as well
899
902
            interesting_files = self.other_tree.find_related_paths_across_trees(
900
 
                    self.interesting_files, lookup_trees)
 
903
                self.interesting_files, lookup_trees)
901
904
        else:
902
905
            interesting_files = None
903
906
        result = []
1009
1012
                        (base_ie.executable, lca_executable),
1010
1013
                        other_ie.executable, this_ie.executable)
1011
1014
                    if (parent_id_winner == 'this' and name_winner == 'this'
1012
 
                        and sha1_winner == 'this' and exec_winner == 'this'):
 
1015
                            and sha1_winner == 'this' and exec_winner == 'this'):
1013
1016
                        # No kind, parent, name, exec, or content change for
1014
1017
                        # OTHER, so this node is not considered interesting
1015
1018
                        continue
1020
1023
                        if ie.kind != 'symlink':
1021
1024
                            return None
1022
1025
                        return tree.get_symlink_target(path, file_id)
1023
 
                    base_target = get_target(base_ie, self.base_tree, base_path)
 
1026
                    base_target = get_target(
 
1027
                        base_ie, self.base_tree, base_path)
1024
1028
                    lca_targets = [get_target(ie, tree, lca_path) for ie, tree, lca_path
1025
1029
                                   in zip(lca_entries, self._lca_trees, lca_paths)]
1026
 
                    this_target = get_target(this_ie, self.this_tree, this_path)
1027
 
                    other_target = get_target(other_ie, self.other_tree, other_path)
 
1030
                    this_target = get_target(
 
1031
                        this_ie, self.this_tree, this_path)
 
1032
                    other_target = get_target(
 
1033
                        other_ie, self.other_tree, other_path)
1028
1034
                    target_winner = self._lca_multi_way(
1029
1035
                        (base_target, lca_targets),
1030
1036
                        other_target, this_target)
1031
1037
                    if (parent_id_winner == 'this' and name_winner == 'this'
1032
 
                        and target_winner == 'this'):
 
1038
                            and target_winner == 'this'):
1033
1039
                        # No kind, parent, name, or symlink target change
1034
1040
                        # not interesting
1035
1041
                        continue
1056
1062
                            other_ie.name, this_ie.name),
1057
1063
                           ((base_ie.executable, lca_executable),
1058
1064
                            other_ie.executable, this_ie.executable)
1059
 
                          ))
 
1065
                           ))
1060
1066
        return result
1061
1067
 
1062
1068
    def write_modified(self, results):
1155
1161
        base_val, lca_vals = bases
1156
1162
        # Remove 'base_val' from the lca_vals, because it is not interesting
1157
1163
        filtered_lca_vals = [lca_val for lca_val in lca_vals
1158
 
                                      if lca_val != base_val]
 
1164
                             if lca_val != base_val]
1159
1165
        if len(filtered_lca_vals) == 0:
1160
1166
            return Merge3Merger._three_way(base_val, other, this)
1161
1167
 
1296
1302
        trans_id = self.tt.trans_id_file_id(file_id)
1297
1303
        params = MergeFileHookParams(
1298
1304
            self, file_id, (base_path, other_path,
1299
 
            this_path), trans_id, this_pair[0],
 
1305
                            this_path), trans_id, this_pair[0],
1300
1306
            other_pair[0], winner)
1301
1307
        hooks = self.active_hooks
1302
1308
        hook_status = 'not_applicable'
1318
1324
            parent_id = self.tt.final_parent(trans_id)
1319
1325
            duplicate = False
1320
1326
            inhibit_content_conflict = False
1321
 
            if params.this_kind is None: # file_id is not in THIS
 
1327
            if params.this_kind is None:  # file_id is not in THIS
1322
1328
                # Is the name used for a different file_id ?
1323
1329
                if self.this_tree.is_versioned(other_path):
1324
1330
                    # Two entries for the same path
1331
1337
                        other_path, file_id=file_id,
1332
1338
                        filter_tree_path=self._get_filter_tree_path(file_id))
1333
1339
                    inhibit_content_conflict = True
1334
 
            elif params.other_kind is None: # file_id is not in OTHER
 
1340
            elif params.other_kind is None:  # file_id is not in OTHER
1335
1341
                # Is the name used for a different file_id ?
1336
1342
                if self.other_tree.is_versioned(this_path):
1337
1343
                    # Two entries for the same path again, but here, the other
1348
1354
                # This is a contents conflict, because none of the available
1349
1355
                # functions could merge it.
1350
1356
                file_group = self._dump_conflicts(
1351
 
                        name, (base_path, other_path, this_path), parent_id,
1352
 
                        file_id, set_version=True)
 
1357
                    name, (base_path, other_path, this_path), parent_id,
 
1358
                    file_id, set_version=True)
1353
1359
                self._raw_conflicts.append(('contents conflict', file_group))
1354
1360
        elif hook_status == 'success':
1355
1361
            self.tt.create_file(lines, trans_id)
1450
1456
 
1451
1457
        def iter_merge3(retval):
1452
1458
            retval["text_conflicts"] = False
1453
 
            for line in m3.merge_lines(name_a = b"TREE",
1454
 
                                       name_b = b"MERGE-SOURCE",
1455
 
                                       name_base = b"BASE-REVISION",
 
1459
            for line in m3.merge_lines(name_a=b"TREE",
 
1460
                                       name_b=b"MERGE-SOURCE",
 
1461
                                       name_base=b"BASE-REVISION",
1456
1462
                                       start_marker=start_marker,
1457
1463
                                       base_marker=base_marker,
1458
1464
                                       reprocess=self.reprocess):
1518
1524
        for suffix, tree, path, lines in data:
1519
1525
            if path is not None:
1520
1526
                trans_id = self._conflict_file(
1521
 
                        name, parent_id, path, tree, file_id, suffix, lines,
1522
 
                        filter_tree_path)
 
1527
                    name, parent_id, path, tree, file_id, suffix, lines,
 
1528
                    filter_tree_path)
1523
1529
                file_group.append(trans_id)
1524
1530
                if set_version and not versioned:
1525
1531
                    self.tt.version_file(file_id, trans_id)
1532
1538
        name = name + '.' + suffix
1533
1539
        trans_id = self.tt.create_path(name, parent_id)
1534
1540
        transform.create_from_tree(
1535
 
                self.tt, trans_id, tree, path,
1536
 
                file_id=file_id, chunks=lines,
1537
 
                filter_tree_path=filter_tree_path)
 
1541
            self.tt, trans_id, tree, path,
 
1542
            file_id=file_id, chunks=lines,
 
1543
            filter_tree_path=filter_tree_path)
1538
1544
        return trans_id
1539
1545
 
1540
1546
    def merge_executable(self, paths, file_id, file_status):
1541
1547
        """Perform a merge on the execute bit."""
1542
1548
        executable = [self.executable(t, p, file_id) for t, p in zip([self.base_tree,
1543
 
                      self.other_tree, self.this_tree], paths)]
 
1549
                                                                      self.other_tree, self.this_tree], paths)]
1544
1550
        self._merge_executable(paths, file_id, executable, file_status,
1545
1551
                               resolver=self._three_way)
1546
1552
 
1553
1559
            return
1554
1560
        winner = resolver(*executable)
1555
1561
        if winner == "conflict":
1556
 
        # There must be a None in here, if we have a conflict, but we
1557
 
        # need executability since file status was not deleted.
 
1562
            # There must be a None in here, if we have a conflict, but we
 
1563
            # need executability since file status was not deleted.
1558
1564
            if other_path is None:
1559
1565
                winner = "this"
1560
1566
            else:
1586
1592
            conflict_type = conflict[0]
1587
1593
            if conflict_type == 'path conflict':
1588
1594
                (trans_id, file_id,
1589
 
                this_parent, this_name,
1590
 
                other_parent, other_name) = conflict[1:]
 
1595
                 this_parent, this_name,
 
1596
                 other_parent, other_name) = conflict[1:]
1591
1597
                if this_parent is None or this_name is None:
1592
1598
                    this_path = '<deleted>'
1593
1599
                else:
1594
 
                    parent_path =  fp.get_path(
 
1600
                    parent_path = fp.get_path(
1595
1601
                        self.tt.trans_id_file_id(this_parent))
1596
1602
                    this_path = osutils.pathjoin(parent_path, this_name)
1597
1603
                if other_parent is None or other_name is None:
1603
1609
                        # exception
1604
1610
                        parent_path = ''
1605
1611
                    else:
1606
 
                        parent_path =  fp.get_path(
 
1612
                        parent_path = fp.get_path(
1607
1613
                            self.tt.trans_id_file_id(other_parent))
1608
1614
                    other_path = osutils.pathjoin(parent_path, other_name)
1609
1615
                c = _mod_conflicts.Conflict.factory(
1642
1648
        # conflict is enough.
1643
1649
        for c in cooked_conflicts:
1644
1650
            if (c.typestring == 'path conflict'
1645
 
                and c.file_id in content_conflict_file_ids):
 
1651
                    and c.file_id in content_conflict_file_ids):
1646
1652
                continue
1647
1653
            self.cooked_conflicts.append(c)
1648
1654
        self.cooked_conflicts.sort(key=_mod_conflicts.Conflict.sort_key)
1716
1722
        return self.this_tree.plan_file_lca_merge(file_id, self.other_tree,
1717
1723
                                                  base=base)
1718
1724
 
 
1725
 
1719
1726
class Diff3Merger(Merge3Merger):
1720
1727
    """Three-way merger using external diff3 for text merging"""
1721
1728
 
1739
1746
        temp_dir = osutils.mkdtemp(prefix="bzr-")
1740
1747
        try:
1741
1748
            new_file = osutils.pathjoin(temp_dir, "new")
1742
 
            this = self.dump_file(temp_dir, "this", self.this_tree, this_path, file_id)
1743
 
            base = self.dump_file(temp_dir, "base", self.base_tree, base_path, file_id)
1744
 
            other = self.dump_file(temp_dir, "other", self.other_tree, other_path, file_id)
 
1749
            this = self.dump_file(
 
1750
                temp_dir, "this", self.this_tree, this_path, file_id)
 
1751
            base = self.dump_file(
 
1752
                temp_dir, "base", self.base_tree, base_path, file_id)
 
1753
            other = self.dump_file(
 
1754
                temp_dir, "other", self.other_tree, other_path, file_id)
1745
1755
            status = breezy.patch.diff3(new_file, this, base, other)
1746
1756
            if status not in (0, 1):
1747
1757
                raise errors.BzrError("Unhandled diff3 exit code")
1772
1782
    """
1773
1783
 
1774
1784
    def __init__(self, this_tree, other_branch, other_tree, target_subdir,
1775
 
            source_subpath, other_rev_id=None):
 
1785
                 source_subpath, other_rev_id=None):
1776
1786
        """Create a new MergeIntoMerger object.
1777
1787
 
1778
1788
        source_subpath in other_tree will be effectively copied to
1789
1799
        # It is assumed that we are merging a tree that is not in our current
1790
1800
        # ancestry, which means we are using the "EmptyTree" as our basis.
1791
1801
        null_ancestor_tree = this_tree.branch.repository.revision_tree(
1792
 
                                _mod_revision.NULL_REVISION)
 
1802
            _mod_revision.NULL_REVISION)
1793
1803
        super(MergeIntoMerger, self).__init__(
1794
1804
            this_branch=this_tree.branch,
1795
1805
            this_tree=this_tree,
1809
1819
        self.reprocess = False
1810
1820
        self.interesting_files = None
1811
1821
        self.merge_type = _MergeTypeParameterizer(MergeIntoMergeType,
1812
 
              target_subdir=self._target_subdir,
1813
 
              source_subpath=self._source_subpath)
 
1822
                                                  target_subdir=self._target_subdir,
 
1823
                                                  source_subpath=self._source_subpath)
1814
1824
        if self._source_subpath != '':
1815
1825
            # If this isn't a partial merge make sure the revisions will be
1816
1826
            # present.
1817
1827
            self._maybe_fetch(self.other_branch, self.this_branch,
1818
 
                self.other_basis)
 
1828
                              self.other_basis)
1819
1829
 
1820
1830
    def set_pending(self):
1821
1831
        if self._source_subpath != '':
1870
1880
            entries = self._entries_to_incorporate()
1871
1881
            entries = list(entries)
1872
1882
            for num, (entry, parent_id, relpath) in enumerate(entries):
1873
 
                child_pb.update(gettext('Preparing file merge'), num, len(entries))
 
1883
                child_pb.update(gettext('Preparing file merge'),
 
1884
                                num, len(entries))
1874
1885
                parent_trans_id = self.tt.trans_id_file_id(parent_id)
1875
1886
                path = osutils.pathjoin(self._source_subpath, relpath)
1876
1887
                trans_id = transform.new_by_entry(path, self.tt, entry,
1877
 
                    parent_trans_id, self.other_tree)
 
1888
                                                  parent_trans_id, self.other_tree)
1878
1889
        self._finish_computing_transform()
1879
1890
 
1880
1891
    def _entries_to_incorporate(self):
2081
2092
                else:
2082
2093
                    yield 'killed-a', self.lines_b[b_index]
2083
2094
            # handle common lines
2084
 
            for a_index in range(i, i+n):
 
2095
            for a_index in range(i, i + n):
2085
2096
                yield 'unchanged', self.lines_a[a_index]
2086
 
            last_i = i+n
2087
 
            last_j = j+n
 
2097
            last_i = i + n
 
2098
            last_j = j + n
2088
2099
 
2089
2100
    def _get_matching_blocks(self, left_revision, right_revision):
2090
2101
        """Return a description of which sections of two revisions match.
2146
2157
        for i, j, n in matcher.get_matching_blocks():
2147
2158
            for jj in range(last_j, j):
2148
2159
                yield new_plan[jj]
2149
 
            for jj in range(j, j+n):
 
2160
            for jj in range(j, j + n):
2150
2161
                plan_line = new_plan[jj]
2151
2162
                if plan_line[0] == 'new-b':
2152
2163
                    pass
2383
2394
        are combined, they are written out in the format described in
2384
2395
        VersionedFile.plan_merge
2385
2396
        """
2386
 
        if self._head_key is not None: # There was a single head
 
2397
        if self._head_key is not None:  # There was a single head
2387
2398
            if self._head_key == self.a_key:
2388
2399
                plan = 'new-a'
2389
2400
            else: