#!/bin/sh
# MAINTAINER: portmgr@FreeBSD.org
# $FreeBSD: head/Mk/Scripts/smart_makepatch.sh 502102 2019-05-20 13:04:03Z mat $

# This script regenerates patches.  It conserves existing comments and
# file names, even if the file name does not meet any current or
# previous convention.  It will keep multiple patches in the same file
# rather than splitting them into individual files.
#
# If a generated patch was not present before, it will create a file
# name where forward slashes are replaced with an underscore and
# underscores are appended by another underscore.
#
# Limitations:
# 1) If a file is modified by multiple patches, it will be regenerated
#    as a single patch.  That means if two multi-patch files modified
#    the same source file, when regenerated, the source file's patch
#    will only appear in one of patch file.
# 2) It's possible that trailing garbage at the end of a patch in a
#    multipatch file might corrupt the comment (or be interpreted as
#    a comment) of the following patch.  (garbage in, garbage out)
#
# Reminder
# Don't forget to disable post-patch targets before regenerating patches
# if those targets modify source files (e.g. with sed).  You may also
# want to disable EXTRA_PATCHES as well if that is being used.

[ -n "${DEBUG_MK_SCRIPTS}" -o -n "${DEBUG_MK_SCRIPTS_SMART_MAKEPATCH}" ] && set -x

if [ -z "${PATCHDIR}" -o -z "${PATCH_WRKSRC}" -o -z "${WRKDIR}" ]; then
	echo "WRKDIR, PATCHDIR, and PATCH_WRKSRC required in environment." >&2
	exit 1
fi

WORKAREA=${WRKDIR}/.makepatch-tmp
PATCHMAP=${WORKAREA}/pregen.map
COMMENTS=${WORKAREA}/comments
REGENNED=${WORKAREA}/regenerated
DESTDIR=${WORKAREA}/staged
SAVEDIR=${WORKAREA}/archived-patches

case "${STRIP_COMPONENTS}" in
	[123456789])	;;
	1[0123456789])	;;
	*)		STRIP_COMPONENTS=0
esac

strip_path() {
	local raw_name=$1
	if [ "${STRIP_COMPONENTS}" = "0" ]; then
		echo ${raw_name}
	else
		echo ${raw_name} | awk -v sc=${STRIP_COMPONENTS} -F "/" \
		'{ for (x = sc + 1; x <= NF; x++) {
			slash = (x>sc+1) ? "/" : "";
			printf ("%s%s", slash, $x);
		   }}'
	fi
}

std_patch_filename() {
	local sans_cwd
	local raw_name
	sans_cwd=$(echo $1 | sed 's|^\.\/||')
	raw_name=$(strip_path ${sans_cwd})
	echo "patch-$(echo ${raw_name} | sed -e 's|_|&&|g; s|/|_|g')"
}

patchdir_files_list() {
	if [ -d "${PATCHDIR}" ]; then
		(cd ${PATCHDIR} && \
			find ./* -type f -name "patch-*" -maxdepth 0 \
			2>/dev/null | sed -e 's,^\./,,; /\.orig$/d'
		)
	fi;
}

valid_name() {
	local current_patch_name=$1
	local result=$3
	local first_target
	local testres
	local lps
	first_target=$(echo $2 | sed 's|^\.\/||')
	for lps in __ - + ; do
		testres=patch-$(echo ${first_target} | sed -e "s|/|${lps}|g")
		if [ "${testres}" = "${current_patch_name}" ]; then
			result=${testres}
			break
		fi
	done
	echo ${result}
}

map_existing_patches() {
	mkdir -p ${WORKAREA}
	: > ${PATCHMAP}
	local target
	local future_name
	local std_target
	local P
	local t
	for P in ${old_patch_list}; do
		target=$(cd ${PATCHDIR} && \
			grep "^+++ " ${P} | awk '{print $2}'
		)
		# For single patches, we honor previous separators, but use
		# a standard patch name if the current patch name does not
		# conform.  However, if two or more patches are contained in
		# single file, then we do *NOT* rename the file
		future_name=
		for t in ${target}; do
			if [ -n "${future_name}" ]; then
				future_name=${P}
				break;
			fi
			std_target=$(std_patch_filename ${t})
			future_name=$(valid_name ${P} ${t} ${std_target})
		done
		for t in ${target}; do
			std_target=$(std_patch_filename ${t})
			echo "${future_name}	${std_target}" >> ${PATCHMAP}
		done
	done
}

extract_comment_from_patch() {
	local existing_patch=${PATCHDIR}/$1
	local contains
	local rawname
	local fname
	local num
	contains=$(grep "^+++ " ${existing_patch} | awk '{x++; print x}')
	for num in ${contains}; do
		rawname=$(grep "^+++ " ${existing_patch} | \
			awk -v num=${num} '{x++; if (x==num) print $2}')
		fname=$(std_patch_filename $rawname)
		awk -v num=${num} '
		BEGIN { done=0; x=0; hunk=0; looking=(num==1) }
		{
		    if (!done) {
		        if ($1 == "@@") {
		            split ($2,a,",");
		            split ($3,b,",");
		            hca = a[2];
		            hcb = a[3];
		            hunk = 1;
		        } else if (hunk) {
		            first=substr($1,1,1);
		            if (first == "-") { hca-- }
		            else if (first == "+") { hcb-- }
		            else {hca--; hcb--}
		            if (hca == 0 && hcb == 0) {hunk = 0}
		        }
			if ($1 == "---") {
			   x++;
			   if (x == num) { done = 1 }
			   if (x + 1 == num) { looking = 1 }
			} else if (!hunk && looking) {
		            if ($1!="diff" && $1!="index" && $1!="+++") {
		                print $0
		            }
		        }
		    }
		}' ${existing_patch} > ${COMMENTS}/${fname}
	done
}

extract_comments() {
	mkdir -p ${COMMENTS}
	rm -f ${COMMENTS}/*
	local P
	for P in ${old_patch_list}; do
		extract_comment_from_patch ${P}
	done
}

regenerate_patches() {
	mkdir -p ${REGENNED}
	rm -f ${REGENNED}/*
	[ ! -d "${PATCH_WRKSRC}" ] && return

	local F
	local NEW
	local OUT
	local ORIG
	local new_list
	new_list=$(cd "${PATCH_WRKSRC}" && \
		find -s ./* -type f -name '*.orig' 2>/dev/null)
	(cd "${PATCH_WRKSRC}" && for F in ${new_list}; do
		ORIG=${F#./}
		NEW=${ORIG%.orig}
		cmp -s ${ORIG} ${NEW} && continue
		OUT=${REGENNED}/$(std_patch_filename ${NEW})
		TZ=UTC diff -udp ${ORIG} ${NEW} | sed \
			-e '/^---/s|\.[0-9]* +0000$| UTC|' \
			-e '/^+++/s|\([[:blank:]][-0-9:.+]*\)*$||' \
			> ${OUT} || true
	done)
}

get_patch_name() {
	awk -v name=$1 '
	{ if ($2 == name)
	  {
	      if (!done) { print $1 };
	      done = 1;
	  }
	}
	END { if (!done) print name }' ${PATCHMAP}
}

stage_patches() {
	mkdir -p ${DESTDIR}
	rm -f ${DESTDIR}/*
	local P
	local name
	local patch_list
	patch_list=$(cd ${REGENNED} && find ./* -name "patch-*" 2>/dev/null)
	for P in ${patch_list}; do
		P=${P#./}
		name=$(get_patch_name ${P})
		[ -e ${COMMENTS}/${P} ] && cat ${COMMENTS}/${P} \
			>> ${DESTDIR}/${name}
		if [ "${P}" = "${name}" ]; then
			echo "Generated ${P}"
		else
			echo "Generated ${P} >> ${name} (legacy)"
		fi
		cat ${REGENNED}/${P} >> ${DESTDIR}/${name}
	done
}

compare_common_patches() {
	[ -z "${old_patch_list}" ] && return
	local archive_patch_list
	local P
	local ppatch
	local ppatch_stripped
	local cpatch
	local cpatch_stripped
	for P in ${old_patch_list}; do
		if [ -e ${DESTDIR}/${P} ]; then
			ppatch=${PATCHDIR}/${P}
			cpatch=${DESTDIR}/${P}
			ppatch_stripped=$(mktemp -t portpatch)
			cpatch_stripped=$(mktemp -t portpatch)
			sed -E -e '/^--- .+ UTC$/d; s/^(@@ [^@]* @@).*/\1/' \
				${ppatch} > ${ppatch_stripped}
			sed -E -e '/^--- .+ UTC$/d; s/^(@@ [^@]* @@).*/\1/' \
				${cpatch} > ${cpatch_stripped}
			# Don't replace patches with only metadata changes
			if ! cmp -s ${ppatch_stripped} ${cpatch_stripped}; then
				archive_patch_list="${archive_patch_list} ${P}"
			else
				echo "${P} only contains metadata changes; not replacing"
				rm ${cpatch}
			fi
			rm ${ppatch_stripped}
			rm ${cpatch_stripped}
		fi
	done
	old_patch_list=${archive_patch_list}
}

conserve_old_patches() {
	mkdir -p ${SAVEDIR}
	rm -f ${SAVEDIR}/*
	[ -z "${old_patch_list}" ] && return

	local P
	for P in ${old_patch_list}; do
		mv ${PATCHDIR}/${P} ${SAVEDIR}/${P}
	done
	echo "The previous patches have been placed here:"
	echo ${SAVEDIR}
}

install_regenerated_patches() {
	local testdir
	testdir=$(find ${DESTDIR} -empty)
	if [ -z "${testdir}" ]; then
		mkdir -p ${PATCHDIR}
		find ${DESTDIR} -type f -exec mv {} ${PATCHDIR}/ \;
	fi
}

old_patch_list=$(patchdir_files_list)

map_existing_patches
extract_comments
regenerate_patches
stage_patches
compare_common_patches
conserve_old_patches
install_regenerated_patches
