#!/bin/sh
#
# Copyright (c) 2026 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-cvsmirror <ref>...
#
#	<cvsmirrorroot> must be a cvsmirror(1) root with the given
#	module configured and branches checked out.  git-cvsmirror list
#	each commit on the given git branches since the last git commit
#	that was mirrored to CVS, applies them to the CVS mirror with
#	`git cvsexportcommit -acpv <parent> <commit>', and updates the
#	CVS mirror's record of the last git commit.
#
#	Each branch is updated independently; if the process is
#	interrupted partway through, some branches may have been
#	updated while others were not.  However, the operation is
#	idempotent so it can safely pick up where it left off.
#
#	For merge commits, the diff is taken from the first parent, so
#	make sure to merge feature or vendor branches as the second
#	parent into trunk as the first parent.
#
#	If the git config options netbsd.cvsmirrorroot,
#	netbsd.cvsmodule, and netbsd.cvsbranches are not all defined to
#	nonempty strings, do nothing and exit with status 0.
#

set -Ceu

progname=${0##*/}

CVSMIRRORROOT=$(git config netbsd.cvsmirrorroot || :)
module=$(git config netbsd.cvsmodule || :)
branches=$(git config netbsd.cvsbranches || :)

# If the parameters are not all provided, nothing to do.
if [ -z "$CVSMIRRORROOT" ] || [ -z "$module" ] || [ -z "$branches" ]; then
	exit 0
fi

export CVSMIRRORROOT

per_branch()
{
	local branch
	local status

	branch=$1

	# Find what is already there ($old_latest) and what commit we
	# intend to advance to ($new_latest).  If they are the same,
	# there's nothing to do.
	old_latest=$(${CVSMIRROR:-cvsmirror} latest "$module" "$branch") || {
		status=$?
		printf >&2 '%s: module %s branch %s not checked out\n' \
		    "$progname" "$module" "$branch"
		return $status
	}
	new_latest=$(git rev-list --first-parent -n1 "$branch")
	if [ "$old_latest" = "$new_latest" ]; then
		return 0
	fi

	# List the commits since the old latest, and update the branch
	# to the new latest by running `git cvsexportcommit'.  To save
	# the cost of syncs on multi-commit updates, we do a series of
	# updates to each branch at once in a single `cvsmirror commit'
	# operation (of course, this means if we're interrupted partway
	# through a multi-commit update, any progress will be lost).
	git rev-list --first-parent "$old_latest".."$branch" \
	| ${CVSMIRROR:-cvsmirror} commit "$module" "$branch" "$new_latest" \
	    sh -c '
		set -Ceu
		d=$1
		parent=$2
		while read commit; do
			git --git-dir="$d" cvsexportcommit -acpv "$parent" \
			    "$commit"
			parent=$commit
		done
	    ' -- "$(pwd)" "$old_latest" || {
		status=$?
		printf >&2 '%s: failed to update module %s branch %s\n' \
		    "$progname" "$module" "$branch"
		return $status
	}

	return 0
}

status=0
case $# in
0)	# No arguments passed.  Update all configured branches.
	for branch in $branches; do
		per_branch "$branch" || status=$?
	done
	;;
*)	# Some arguments passed.  Find the corresponding branches,
	# ignore arguments that are not mirrored, and update the subset
	# of the configured branches.
	for ref; do
		# Map git ref refs/heads/FOO to CVS branch FOO.  Ignore
		# non-head refs, like tags.
		case $ref in
		refs/heads/*)
			branch=${ref##refs/heads/}
			;;
		*)	printf >&2 '%s: ignoring non-head ref %s\n' \
			    "$progname" "$ref"
			continue
			;;
		esac

		# Ignore branches that aren't configured.
		case " $branches " in
		*" $branch "*)
			;;
		*)	printf >&2 '%s: ignoring unmirrored ref %s\n' \
			    "$progname" "$ref"
			continue
		esac

		# It's a branch and it's configured.  Update it.
		per_branch "$branch" || status=$?
	done
	;;
esac
exit $status
