mercurial/obsutil.py
changeset 43076 2372284d9457
parent 41713 9de6c4f61608
child 43077 687b865b95ad
equal deleted inserted replaced
43075:57875cf423c9 43076:2372284d9457
    15     encoding,
    15     encoding,
    16     node as nodemod,
    16     node as nodemod,
    17     phases,
    17     phases,
    18     util,
    18     util,
    19 )
    19 )
    20 from .utils import (
    20 from .utils import dateutil
    21     dateutil,
       
    22 )
       
    23 
    21 
    24 ### obsolescence marker flag
    22 ### obsolescence marker flag
    25 
    23 
    26 ## bumpedfix flag
    24 ## bumpedfix flag
    27 #
    25 #
    52 # bumped version and fix the situation, breaking the transitivity of
    50 # bumped version and fix the situation, breaking the transitivity of
    53 # "bumped" here.
    51 # "bumped" here.
    54 bumpedfix = 1
    52 bumpedfix = 1
    55 usingsha256 = 2
    53 usingsha256 = 2
    56 
    54 
       
    55 
    57 class marker(object):
    56 class marker(object):
    58     """Wrap obsolete marker raw data"""
    57     """Wrap obsolete marker raw data"""
    59 
    58 
    60     def __init__(self, repo, data):
    59     def __init__(self, repo, data):
    61         # the repo argument will be used to create changectx in later version
    60         # the repo argument will be used to create changectx in later version
    92         return self._data[4]
    91         return self._data[4]
    93 
    92 
    94     def flags(self):
    93     def flags(self):
    95         """The flags field of the marker"""
    94         """The flags field of the marker"""
    96         return self._data[2]
    95         return self._data[2]
       
    96 
    97 
    97 
    98 def getmarkers(repo, nodes=None, exclusive=False):
    98 def getmarkers(repo, nodes=None, exclusive=False):
    99     """returns markers known in a repository
    99     """returns markers known in a repository
   100 
   100 
   101     If <nodes> is specified, only markers "relevant" to those nodes are are
   101     If <nodes> is specified, only markers "relevant" to those nodes are are
   108         rawmarkers = repo.obsstore.relevantmarkers(nodes)
   108         rawmarkers = repo.obsstore.relevantmarkers(nodes)
   109 
   109 
   110     for markerdata in rawmarkers:
   110     for markerdata in rawmarkers:
   111         yield marker(repo, markerdata)
   111         yield marker(repo, markerdata)
   112 
   112 
       
   113 
   113 def closestpredecessors(repo, nodeid):
   114 def closestpredecessors(repo, nodeid):
   114     """yield the list of next predecessors pointing on visible changectx nodes
   115     """yield the list of next predecessors pointing on visible changectx nodes
   115 
   116 
   116     This function respect the repoview filtering, filtered revision will be
   117     This function respect the repoview filtering, filtered revision will be
   117     considered missing.
   118     considered missing.
   135 
   136 
   136             if precnodeid in repo:
   137             if precnodeid in repo:
   137                 yield precnodeid
   138                 yield precnodeid
   138             else:
   139             else:
   139                 stack.append(precnodeid)
   140                 stack.append(precnodeid)
       
   141 
   140 
   142 
   141 def allpredecessors(obsstore, nodes, ignoreflags=0):
   143 def allpredecessors(obsstore, nodes, ignoreflags=0):
   142     """Yield node for every precursors of <nodes>.
   144     """Yield node for every precursors of <nodes>.
   143 
   145 
   144     Some precursors may be unknown locally.
   146     Some precursors may be unknown locally.
   158                 continue
   160                 continue
   159             suc = mark[0]
   161             suc = mark[0]
   160             if suc not in seen:
   162             if suc not in seen:
   161                 seen.add(suc)
   163                 seen.add(suc)
   162                 remaining.add(suc)
   164                 remaining.add(suc)
       
   165 
   163 
   166 
   164 def allsuccessors(obsstore, nodes, ignoreflags=0):
   167 def allsuccessors(obsstore, nodes, ignoreflags=0):
   165     """Yield node for every successor of <nodes>.
   168     """Yield node for every successor of <nodes>.
   166 
   169 
   167     Some successors may be unknown locally.
   170     Some successors may be unknown locally.
   180             for suc in mark[1]:
   183             for suc in mark[1]:
   181                 if suc not in seen:
   184                 if suc not in seen:
   182                     seen.add(suc)
   185                     seen.add(suc)
   183                     remaining.add(suc)
   186                     remaining.add(suc)
   184 
   187 
       
   188 
   185 def _filterprunes(markers):
   189 def _filterprunes(markers):
   186     """return a set with no prune markers"""
   190     """return a set with no prune markers"""
   187     return set(m for m in markers if m[1])
   191     return set(m for m in markers if m[1])
       
   192 
   188 
   193 
   189 def exclusivemarkers(repo, nodes):
   194 def exclusivemarkers(repo, nodes):
   190     """set of markers relevant to "nodes" but no other locally-known nodes
   195     """set of markers relevant to "nodes" but no other locally-known nodes
   191 
   196 
   192     This function compute the set of markers "exclusive" to a locally-known
   197     This function compute the set of markers "exclusive" to a locally-known
   305                 seennodes.add(prec)
   310                 seennodes.add(prec)
   306                 stack.append(prec)
   311                 stack.append(prec)
   307 
   312 
   308     return exclmarkers
   313     return exclmarkers
   309 
   314 
       
   315 
   310 def foreground(repo, nodes):
   316 def foreground(repo, nodes):
   311     """return all nodes in the "foreground" of other node
   317     """return all nodes in the "foreground" of other node
   312 
   318 
   313     The foreground of a revision is anything reachable using parent -> children
   319     The foreground of a revision is anything reachable using parent -> children
   314     or precursor -> successor relation. It is very similar to "descendant" but
   320     or precursor -> successor relation. It is very similar to "descendant" but
   331             succs.update(allsuccessors(repo.obsstore, mutable))
   337             succs.update(allsuccessors(repo.obsstore, mutable))
   332             known = (n for n in succs if n in nm)
   338             known = (n for n in succs if n in nm)
   333             foreground = set(repo.set('%ln::', known))
   339             foreground = set(repo.set('%ln::', known))
   334     return set(c.node() for c in foreground)
   340     return set(c.node() for c in foreground)
   335 
   341 
       
   342 
   336 # effectflag field
   343 # effectflag field
   337 #
   344 #
   338 # Effect-flag is a 1-byte bit field used to store what changed between a
   345 # Effect-flag is a 1-byte bit field used to store what changed between a
   339 # changeset and its successor(s).
   346 # changeset and its successor(s).
   340 #
   347 #
   348 # `effect-flags` set to off by default.
   355 # `effect-flags` set to off by default.
   349 #
   356 #
   350 
   357 
   351 EFFECTFLAGFIELD = "ef1"
   358 EFFECTFLAGFIELD = "ef1"
   352 
   359 
   353 DESCCHANGED = 1 << 0 # action changed the description
   360 DESCCHANGED = 1 << 0  # action changed the description
   354 METACHANGED = 1 << 1 # action change the meta
   361 METACHANGED = 1 << 1  # action change the meta
   355 DIFFCHANGED = 1 << 3 # action change diff introduced by the changeset
   362 DIFFCHANGED = 1 << 3  # action change diff introduced by the changeset
   356 PARENTCHANGED = 1 << 2 # action change the parent
   363 PARENTCHANGED = 1 << 2  # action change the parent
   357 USERCHANGED = 1 << 4 # the user changed
   364 USERCHANGED = 1 << 4  # the user changed
   358 DATECHANGED = 1 << 5 # the date changed
   365 DATECHANGED = 1 << 5  # the date changed
   359 BRANCHCHANGED = 1 << 6 # the branch changed
   366 BRANCHCHANGED = 1 << 6  # the branch changed
   360 
   367 
   361 METABLACKLIST = [
   368 METABLACKLIST = [
   362     re.compile('^branch$'),
   369     re.compile('^branch$'),
   363     re.compile('^.*-source$'),
   370     re.compile('^.*-source$'),
   364     re.compile('^.*_source$'),
   371     re.compile('^.*_source$'),
   365     re.compile('^source$'),
   372     re.compile('^source$'),
   366 ]
   373 ]
   367 
   374 
       
   375 
   368 def metanotblacklisted(metaitem):
   376 def metanotblacklisted(metaitem):
   369     """ Check that the key of a meta item (extrakey, extravalue) does not
   377     """ Check that the key of a meta item (extrakey, extravalue) does not
   370     match at least one of the blacklist pattern
   378     match at least one of the blacklist pattern
   371     """
   379     """
   372     metakey = metaitem[0]
   380     metakey = metaitem[0]
   373 
   381 
   374     return not any(pattern.match(metakey) for pattern in METABLACKLIST)
   382     return not any(pattern.match(metakey) for pattern in METABLACKLIST)
       
   383 
   375 
   384 
   376 def _prepare_hunk(hunk):
   385 def _prepare_hunk(hunk):
   377     """Drop all information but the username and patch"""
   386     """Drop all information but the username and patch"""
   378     cleanhunk = []
   387     cleanhunk = []
   379     for line in hunk.splitlines():
   388     for line in hunk.splitlines():
   381             if line.startswith(b'@@'):
   390             if line.startswith(b'@@'):
   382                 line = b'@@\n'
   391                 line = b'@@\n'
   383             cleanhunk.append(line)
   392             cleanhunk.append(line)
   384     return cleanhunk
   393     return cleanhunk
   385 
   394 
       
   395 
   386 def _getdifflines(iterdiff):
   396 def _getdifflines(iterdiff):
   387     """return a cleaned up lines"""
   397     """return a cleaned up lines"""
   388     lines = next(iterdiff, None)
   398     lines = next(iterdiff, None)
   389 
   399 
   390     if lines is None:
   400     if lines is None:
   391         return lines
   401         return lines
   392 
   402 
   393     return _prepare_hunk(lines)
   403     return _prepare_hunk(lines)
       
   404 
   394 
   405 
   395 def _cmpdiff(leftctx, rightctx):
   406 def _cmpdiff(leftctx, rightctx):
   396     """return True if both ctx introduce the "same diff"
   407     """return True if both ctx introduce the "same diff"
   397 
   408 
   398     This is a first and basic implementation, with many shortcoming.
   409     This is a first and basic implementation, with many shortcoming.
   417 
   428 
   418         if left != right:
   429         if left != right:
   419             return False
   430             return False
   420     return True
   431     return True
   421 
   432 
       
   433 
   422 def geteffectflag(source, successors):
   434 def geteffectflag(source, successors):
   423     """ From an obs-marker relation, compute what changed between the
   435     """ From an obs-marker relation, compute what changed between the
   424     predecessor and the successor.
   436     predecessor and the successor.
   425     """
   437     """
   426     effects = 0
   438     effects = 0
   459         # Check if the diff has changed
   471         # Check if the diff has changed
   460         if not _cmpdiff(source, changectx):
   472         if not _cmpdiff(source, changectx):
   461             effects |= DIFFCHANGED
   473             effects |= DIFFCHANGED
   462 
   474 
   463     return effects
   475     return effects
       
   476 
   464 
   477 
   465 def getobsoleted(repo, tr):
   478 def getobsoleted(repo, tr):
   466     """return the set of pre-existing revisions obsoleted by a transaction"""
   479     """return the set of pre-existing revisions obsoleted by a transaction"""
   467     torev = repo.unfiltered().changelog.nodemap.get
   480     torev = repo.unfiltered().changelog.nodemap.get
   468     phase = repo._phasecache.phase
   481     phase = repo._phasecache.phase
   482             continue
   495             continue
   483         if set(succsmarkers(node) or []).issubset(addedmarkers):
   496         if set(succsmarkers(node) or []).issubset(addedmarkers):
   484             obsoleted.add(rev)
   497             obsoleted.add(rev)
   485     return obsoleted
   498     return obsoleted
   486 
   499 
       
   500 
   487 class _succs(list):
   501 class _succs(list):
   488     """small class to represent a successors with some metadata about it"""
   502     """small class to represent a successors with some metadata about it"""
   489 
   503 
   490     def __init__(self, *args, **kwargs):
   504     def __init__(self, *args, **kwargs):
   491         super(_succs, self).__init__(*args, **kwargs)
   505         super(_succs, self).__init__(*args, **kwargs)
   501         # immutable
   515         # immutable
   502         return set(self)
   516         return set(self)
   503 
   517 
   504     def canmerge(self, other):
   518     def canmerge(self, other):
   505         return self._set.issubset(other._set)
   519         return self._set.issubset(other._set)
       
   520 
   506 
   521 
   507 def successorssets(repo, initialnode, closest=False, cache=None):
   522 def successorssets(repo, initialnode, closest=False, cache=None):
   508     """Return set of all latest successors of initial nodes
   523     """Return set of all latest successors of initial nodes
   509 
   524 
   510     The successors set of a changeset A are the group of revisions that succeed
   525     The successors set of a changeset A are the group of revisions that succeed
   609         #
   624         #
   610         current = toproceed[-1]
   625         current = toproceed[-1]
   611 
   626 
   612         # case 2 condition is a bit hairy because of closest,
   627         # case 2 condition is a bit hairy because of closest,
   613         # we compute it on its own
   628         # we compute it on its own
   614         case2condition =  ((current not in succmarkers)
   629         case2condition = (current not in succmarkers) or (
   615                            or (closest and current != initialnode
   630             closest and current != initialnode and current in repo
   616                                and current in repo))
   631         )
   617 
   632 
   618         if current in cache:
   633         if current in cache:
   619             # case (1): We already know the successors sets
   634             # case (1): We already know the successors sets
   620             stackedset.remove(toproceed.pop())
   635             stackedset.remove(toproceed.pop())
   621         elif case2condition:
   636         elif case2condition:
   718                             markss = productresult
   733                             markss = productresult
   719                     succssets.extend(markss)
   734                     succssets.extend(markss)
   720                 # remove duplicated and subset
   735                 # remove duplicated and subset
   721                 seen = []
   736                 seen = []
   722                 final = []
   737                 final = []
   723                 candidates = sorted((s for s in succssets if s),
   738                 candidates = sorted(
   724                                     key=len, reverse=True)
   739                     (s for s in succssets if s), key=len, reverse=True
       
   740                 )
   725                 for cand in candidates:
   741                 for cand in candidates:
   726                     for seensuccs in seen:
   742                     for seensuccs in seen:
   727                         if cand.canmerge(seensuccs):
   743                         if cand.canmerge(seensuccs):
   728                             seensuccs.markers.update(cand.markers)
   744                             seensuccs.markers.update(cand.markers)
   729                             break
   745                             break
   730                     else:
   746                     else:
   731                         final.append(cand)
   747                         final.append(cand)
   732                         seen.append(cand)
   748                         seen.append(cand)
   733                 final.reverse() # put small successors set first
   749                 final.reverse()  # put small successors set first
   734                 cache[current] = final
   750                 cache[current] = final
   735     return cache[initialnode]
   751     return cache[initialnode]
       
   752 
   736 
   753 
   737 def successorsandmarkers(repo, ctx):
   754 def successorsandmarkers(repo, ctx):
   738     """compute the raw data needed for computing obsfate
   755     """compute the raw data needed for computing obsfate
   739     Returns a list of dict, one dict per successors set
   756     Returns a list of dict, one dict per successors set
   740     """
   757     """
   748     if ssets == []:
   765     if ssets == []:
   749         ssets = [[]]
   766         ssets = [[]]
   750 
   767 
   751     # Try to recover pruned markers
   768     # Try to recover pruned markers
   752     succsmap = repo.obsstore.successors
   769     succsmap = repo.obsstore.successors
   753     fullsuccessorsets = [] # successor set + markers
   770     fullsuccessorsets = []  # successor set + markers
   754     for sset in ssets:
   771     for sset in ssets:
   755         if sset:
   772         if sset:
   756             fullsuccessorsets.append(sset)
   773             fullsuccessorsets.append(sset)
   757         else:
   774         else:
   758             # successorsset return an empty set() when ctx or one of its
   775             # successorsset return an empty set() when ctx or one of its
   779     for sset in fullsuccessorsets:
   796     for sset in fullsuccessorsets:
   780         values.append({'successors': sset, 'markers': sset.markers})
   797         values.append({'successors': sset, 'markers': sset.markers})
   781 
   798 
   782     return values
   799     return values
   783 
   800 
       
   801 
   784 def _getobsfate(successorssets):
   802 def _getobsfate(successorssets):
   785     """ Compute a changeset obsolescence fate based on its successorssets.
   803     """ Compute a changeset obsolescence fate based on its successorssets.
   786     Successors can be the tipmost ones or the immediate ones. This function
   804     Successors can be the tipmost ones or the immediate ones. This function
   787     return values are not meant to be shown directly to users, it is meant to
   805     return values are not meant to be shown directly to users, it is meant to
   788     be used by internal functions only.
   806     be used by internal functions only.
   805         if len(successors) == 1:
   823         if len(successors) == 1:
   806             return 'superseded'
   824             return 'superseded'
   807         else:
   825         else:
   808             return 'superseded_split'
   826             return 'superseded_split'
   809 
   827 
       
   828 
   810 def obsfateverb(successorset, markers):
   829 def obsfateverb(successorset, markers):
   811     """ Return the verb summarizing the successorset and potentially using
   830     """ Return the verb summarizing the successorset and potentially using
   812     information from the markers
   831     information from the markers
   813     """
   832     """
   814     if not successorset:
   833     if not successorset:
   817         verb = 'rewritten'
   836         verb = 'rewritten'
   818     else:
   837     else:
   819         verb = 'split'
   838         verb = 'split'
   820     return verb
   839     return verb
   821 
   840 
       
   841 
   822 def markersdates(markers):
   842 def markersdates(markers):
   823     """returns the list of dates for a list of markers
   843     """returns the list of dates for a list of markers
   824     """
   844     """
   825     return [m[4] for m in markers]
   845     return [m[4] for m in markers]
   826 
   846 
       
   847 
   827 def markersusers(markers):
   848 def markersusers(markers):
   828     """ Returns a sorted list of markers users without duplicates
   849     """ Returns a sorted list of markers users without duplicates
   829     """
   850     """
   830     markersmeta = [dict(m[3]) for m in markers]
   851     markersmeta = [dict(m[3]) for m in markers]
   831     users = set(encoding.tolocal(meta['user']) for meta in markersmeta
   852     users = set(
   832                 if meta.get('user'))
   853         encoding.tolocal(meta['user'])
       
   854         for meta in markersmeta
       
   855         if meta.get('user')
       
   856     )
   833 
   857 
   834     return sorted(users)
   858     return sorted(users)
       
   859 
   835 
   860 
   836 def markersoperations(markers):
   861 def markersoperations(markers):
   837     """ Returns a sorted list of markers operations without duplicates
   862     """ Returns a sorted list of markers operations without duplicates
   838     """
   863     """
   839     markersmeta = [dict(m[3]) for m in markers]
   864     markersmeta = [dict(m[3]) for m in markers]
   840     operations = set(meta.get('operation') for meta in markersmeta
   865     operations = set(
   841                      if meta.get('operation'))
   866         meta.get('operation') for meta in markersmeta if meta.get('operation')
       
   867     )
   842 
   868 
   843     return sorted(operations)
   869     return sorted(operations)
       
   870 
   844 
   871 
   845 def obsfateprinter(ui, repo, successors, markers, formatctx):
   872 def obsfateprinter(ui, repo, successors, markers, formatctx):
   846     """ Build a obsfate string for a single successorset using all obsfate
   873     """ Build a obsfate string for a single successorset using all obsfate
   847     related function defined in obsutil
   874     related function defined in obsutil
   848     """
   875     """
   898 filteredmsgtable = {
   925 filteredmsgtable = {
   899     "pruned": _("hidden revision '%s' is pruned"),
   926     "pruned": _("hidden revision '%s' is pruned"),
   900     "diverged": _("hidden revision '%s' has diverged"),
   927     "diverged": _("hidden revision '%s' has diverged"),
   901     "superseded": _("hidden revision '%s' was rewritten as: %s"),
   928     "superseded": _("hidden revision '%s' was rewritten as: %s"),
   902     "superseded_split": _("hidden revision '%s' was split as: %s"),
   929     "superseded_split": _("hidden revision '%s' was split as: %s"),
   903     "superseded_split_several": _("hidden revision '%s' was split as: %s and "
   930     "superseded_split_several": _(
   904                                   "%d more"),
   931         "hidden revision '%s' was split as: %s and " "%d more"
       
   932     ),
   905 }
   933 }
       
   934 
   906 
   935 
   907 def _getfilteredreason(repo, changeid, ctx):
   936 def _getfilteredreason(repo, changeid, ctx):
   908     """return a human-friendly string on why a obsolete changeset is hidden
   937     """return a human-friendly string on why a obsolete changeset is hidden
   909     """
   938     """
   910     successors = successorssets(repo, ctx.node())
   939     successors = successorssets(repo, ctx.node())
   931             firstsuccessors = ', '.join(succs[:2])
   960             firstsuccessors = ', '.join(succs[:2])
   932             remainingnumber = len(succs) - 2
   961             remainingnumber = len(succs) - 2
   933 
   962 
   934             args = (changeid, firstsuccessors, remainingnumber)
   963             args = (changeid, firstsuccessors, remainingnumber)
   935             return filteredmsgtable['superseded_split_several'] % args
   964             return filteredmsgtable['superseded_split_several'] % args
       
   965 
   936 
   966 
   937 def divergentsets(repo, ctx):
   967 def divergentsets(repo, ctx):
   938     """Compute sets of commits divergent with a given one"""
   968     """Compute sets of commits divergent with a given one"""
   939     cache = {}
   969     cache = {}
   940     base = {}
   970     base = {}
   949                 continue
   979                 continue
   950             if tuple(nsuccset) in base:
   980             if tuple(nsuccset) in base:
   951                 # we already know the latest base for this divergency
   981                 # we already know the latest base for this divergency
   952                 continue
   982                 continue
   953             base[tuple(nsuccset)] = n
   983             base[tuple(nsuccset)] = n
   954     return [{'divergentnodes': divset, 'commonpredecessor': b}
   984     return [
   955             for divset, b in base.iteritems()]
   985         {'divergentnodes': divset, 'commonpredecessor': b}
       
   986         for divset, b in base.iteritems()
       
   987     ]
       
   988 
   956 
   989 
   957 def whyunstable(repo, ctx):
   990 def whyunstable(repo, ctx):
   958     result = []
   991     result = []
   959     if ctx.orphan():
   992     if ctx.orphan():
   960         for parent in ctx.parents():
   993         for parent in ctx.parents():
   962             if parent.orphan():
   995             if parent.orphan():
   963                 kind = 'orphan'
   996                 kind = 'orphan'
   964             elif parent.obsolete():
   997             elif parent.obsolete():
   965                 kind = 'obsolete'
   998                 kind = 'obsolete'
   966             if kind is not None:
   999             if kind is not None:
   967                 result.append({'instability': 'orphan',
  1000                 result.append(
   968                                'reason': '%s parent' % kind,
  1001                     {
   969                                'node': parent.hex()})
  1002                         'instability': 'orphan',
       
  1003                         'reason': '%s parent' % kind,
       
  1004                         'node': parent.hex(),
       
  1005                     }
       
  1006                 )
   970     if ctx.phasedivergent():
  1007     if ctx.phasedivergent():
   971         predecessors = allpredecessors(repo.obsstore, [ctx.node()],
  1008         predecessors = allpredecessors(
   972                                        ignoreflags=bumpedfix)
  1009             repo.obsstore, [ctx.node()], ignoreflags=bumpedfix
   973         immutable = [repo[p] for p in predecessors
  1010         )
   974                      if p in repo and not repo[p].mutable()]
  1011         immutable = [
       
  1012             repo[p] for p in predecessors if p in repo and not repo[p].mutable()
       
  1013         ]
   975         for predecessor in immutable:
  1014         for predecessor in immutable:
   976             result.append({'instability': 'phase-divergent',
  1015             result.append(
   977                            'reason': 'immutable predecessor',
  1016                 {
   978                            'node': predecessor.hex()})
  1017                     'instability': 'phase-divergent',
       
  1018                     'reason': 'immutable predecessor',
       
  1019                     'node': predecessor.hex(),
       
  1020                 }
       
  1021             )
   979     if ctx.contentdivergent():
  1022     if ctx.contentdivergent():
   980         dsets = divergentsets(repo, ctx)
  1023         dsets = divergentsets(repo, ctx)
   981         for dset in dsets:
  1024         for dset in dsets:
   982             divnodes = [repo[n] for n in dset['divergentnodes']]
  1025             divnodes = [repo[n] for n in dset['divergentnodes']]
   983             result.append({'instability': 'content-divergent',
  1026             result.append(
   984                            'divergentnodes': divnodes,
  1027                 {
   985                            'reason': 'predecessor',
  1028                     'instability': 'content-divergent',
   986                            'node': nodemod.hex(dset['commonpredecessor'])})
  1029                     'divergentnodes': divnodes,
       
  1030                     'reason': 'predecessor',
       
  1031                     'node': nodemod.hex(dset['commonpredecessor']),
       
  1032                 }
       
  1033             )
   987     return result
  1034     return result