#!/usr/bin/env python3
#
# Copyright (c) 2025 The NetBSD Foundation, Inc.
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
#    notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
#    notice, this list of conditions and the following disclaimer in the
#    documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE NETBSD FOUNDATION, INC. AND CONTRIBUTORS
# ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE FOUNDATION OR CONTRIBUTORS
# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#

"""git_amend_topic is a hook to create obsolescence markers from git.

Usage:
  [hooks]
  pretxnclose.git_amend_topic = python:.....git_amend_topic
"""

from __future__ import annotations

import os

from mercurial.i18n import _
from mercurial import (
    error,
    pycompat,
    scmutil,
)


def hook(ui, repo, hooktype, node=None, **kwargs):
    if hooktype != b"pretxnclose":
        raise error.Abort(
            _(b'Unsupported hook type %r') % pycompat.bytestr(hooktype)
        )

    # If nothing was pushed, nothing to do.
    #
    if node is None:
        return

    # If we're not processing a git-push from the git mirror, don't try
    # to infer obsolescence relations -- hg can push its own.
    #
    if 'TNFREPO_HG_GIT_PUSH' not in os.environ:
        return

    # List, for each (branch-qualified) topic, the revisions being
    # pushed on that topic.  Skip any revisions without topics -- we
    # won't infer any obsolescence relations from them (and another
    # hook might reject the push in that case anyway).
    #
    ctx = repo.unfiltered()[node]
    branchtopics = {}
    # XXX check phases
    for revno in repo.changelog.revs(start=ctx.rev()):
        rev = repo[revno]
        if not rev.topic():
            continue
        branchtopic = rev.branch() + b'//' + rev.topic()
        if branchtopic not in branchtopics:
            branchtopics[branchtopic] = set()
        branchtopics[branchtopic].add(revno)

    # For each topic:
    #
    # 1. If there are multiple heads being pushed, reject it.  This
    #    shouldn't happen -- via cinnabar, git will (at time of
    #    writing) only push one topic at a time and will never push
    #    more than one head in a single topic push.
    #
    # 2. If there were siblings/cousins of the revisions being pushed,
    #    obsolete them.
    #
    for branchtopic, revnos in branchtopics.items():
        revnos = tuple(revnos)

        # Check for multiple heads.
        #
        if len(repo.unfiltered().revs('heads(%ld)', revnos)) > 1:
            branchtopicstr = pycompat.bytestr(branchtopic)
            raise error.Abort(
                _(b'Multiple heads pushed on topic %s') % (branchtopicstr,))

        # Check for siblings/cousins to obsolete.
        #
        obsrevnos = repo.unfiltered().revs(
            'branch(%s) and descendants(children(parents(%ld))) and not %ld',
            branchtopic, revnos, revnos
        )
        obsrevnos = tuple(obsrevnos)
        if not obsrevnos:
            continue

        # Unfortunately, we can't express a many-to-many obsolescence
        # relation.  The options are:
        #
        # - a sequence of one-to-one relations (hg prune --pair),
        # - a many-to-one relation (hg histedit fold/roll), or
        # - a one-to-many relation (hg split).
        #
        # Without some intelligence we can't reasonably align
        # one-to-one relations, so the best we can do is fold or split.
        #
        # Obviously, this doesn't always produce a good result -- we
        # may appear to split one change into several unrelated
        # changes, or fold a bunch of changes into an unrelated one
        # made on top of the amended changes.
        #
        # The important things are:
        #
        # 1. We obsolete all the changes that are now unreachable from
        #    the new head -- just like git would render them garbage
        #    (and collect them later) -- so force-pushing a topic from
        #    git doesn't keep growing heads like a hydra.
        #
        # 2. Although the obsolescence relations are messy, we don't
        #    lose any changesets, and the obsolescence relations are
        #    marked as git-push operations so if you need to go
        #    spelunking through the obsolescence history, you will see
        #    why it was a mess.
        #
        if len(obsrevnos) == 1:         # split
            replacements = {
                (repo[obsrevno].node(),): tuple(
                    repo[revno].node() for revno in revnos
                )
                for obsrevno in obsrevnos
            }
        else:                           # fold
            replacements = {
                tuple(repo[obsrevno].node() for obsrevno in obsrevnos):
                (repo[max(revnos)].node(),)
            }

        metadata = {
            b'user': repo[max(revnos)].user(),  # XXX
            b'date': repo[max(revnos)].date(),
        }
        scmutil.cleanupnodes(
            repo, replacements, operation=b'git-push', metadata=metadata
        )
