def cleanupnodes(repo, replacements, operation, moves=None, metadata=None):
    """do common cleanups when old nodes are replaced by new nodes

    That includes writing obsmarkers or stripping nodes, and moving bookmarks.
    (we might also want to move working directory parent in the future)

    By default, bookmark moves are calculated automatically from 'replacements',
    but 'moves' can be used to override that. Also, 'moves' may include
    additional bookmark moves that should not have associated obsmarkers.

    replacements is {oldnode: [newnode]} or a iterable of nodes if they do not
    have replacements. operation is a string, like "rebase".

    metadata is dictionary containing metadata to be stored in obsmarker if
    obsolescence is enabled.

    Return the calculated 'moves' mapping that is from a single old node to a
    single new node.
    """
    if not replacements and not moves:
        return {}

    # translate mapping's other forms
    if not util.safehasattr(replacements, "items"):
        replacements = {n: () for n in replacements}

    # Calculate bookmark movements
    if moves is None:
        moves = {}
    # Unfiltered repo is needed since nodes in replacements might be hidden.
    unfi = repo
    for oldnode, newnodes in replacements.items():
        if oldnode in moves:
            continue
        if len(newnodes) > 1:
            # usually a split, take the one with biggest rev number
            newnode = next(unfi.set("max(%ln)", newnodes)).node()
        elif len(newnodes) == 0:
            # Handle them in a second loop
            continue
        else:
            newnode = newnodes[0]
        moves[oldnode] = newnode

    # Move bookmarks pointing to stripped commits backwards.
    # If hit a replaced node, use the replacement.
    def movebackwards(node):
        p1 = unfi.changelog.parents(node)[0]
        if p1 == nullid:
            return p1
        elif p1 in moves:
            return moves[p1]
        elif p1 in replacements:
            return movebackwards(p1)
        else:
            return p1

    for oldnode, newnodes in replacements.items():
        if oldnode in moves:
            continue
        assert len(newnodes) == 0
        moves[oldnode] = movebackwards(oldnode)

    with repo.transaction("cleanup") as tr:
        # Move bookmarks
        bmarks = repo._bookmarks
        bmarkchanges = []
        allnewnodes = [n for ns in replacements.values() for n in ns]
        for oldnode, newnode in moves.items():
            oldbmarks = repo.nodebookmarks(oldnode)
            if not oldbmarks:
                continue
            from . import bookmarks  # avoid import cycle

            repo.ui.debug(
                "moving bookmarks %r from %s to %s\n"
                % (oldbmarks, hex(oldnode), hex(newnode))
            )
            # Delete divergent bookmarks being parents of related newnodes
            deleterevs = repo.revs(
                "parents(roots(%ln & (::%n))) - parents(%n)",
                allnewnodes,
                newnode,
                oldnode,
            )
            deletenodes = _containsnode(repo, deleterevs)
            for name in oldbmarks:
                bmarkchanges.append((name, newnode))
                for b in bookmarks.divergent2delete(repo, deletenodes, name):
                    bmarkchanges.append((b, None))

        if bmarkchanges:
            bmarks.applychanges(repo, tr, bmarkchanges)

        # adjust visibility, or strip nodes
        strip = True
        if visibility.tracking(repo):
            visibility.remove(repo, replacements.keys())
            strip = False

        if strip:
            from . import repair  # avoid import cycle

            tostrip = list(replacements)
            if tostrip:
                repair.delayedstrip(repo.ui, repo, tostrip, operation)

        return moves