1#!/bin/sh -e
2#
3# rmport - remove port(s) from mports.
4#
5# Copyright 2006-2007 Vasil Dimov
6# Copyright 2012-2018 Chris Rees
7# Copyright 2016-2023 René Ladan
8# All rights reserved.
9#
10# Redistribution and use in source and binary forms, with or without
11# modification, are permitted provided that the following conditions
12# are met:
13# 1. Redistributions of source code must retain the above copyright
14#    notice, this list of conditions and the following disclaimer.
15# 2. Redistributions in binary form must reproduce the above copyright
16#    notice, this list of conditions and the following disclaimer in the
17#    documentation and/or other materials provided with the distribution.
18#
19# THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
20# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22# ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
23# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
25# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
26# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
28# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
29# SUCH DAMAGE.
30#
31# Authors:
32# Originally written by Vasil Dimov <vd@FreeBSD.org>
33# Others:
34# Chris Rees <crees@FreeBSD.org>
35# René Ladan <rene@FreeBSD.org>
36#
37
38EDITOR=${EDITOR:-/usr/bin/vi}
39PORTSDIR=${PORTSDIR:-/usr/mports}
40INDEX=${PORTSDIR}/$(make -C ${PORTSDIR} -V INDEXFILE)
41
42TODAY=$(date -u +%Y-%m-%d)
43
44SED="sed -i .orig -E"
45# use ~/.ssh/config to set up the desired username if different than $LOGNAME
46GITREPO=${GITREPO:git@github.com:MidnightBSD/mports.git}
47
48if [ -n "$(command -v git 2>/dev/null)" ]; then
49	GIT=git
50else
51	echo "git(1) not found.  Please install devel/git."
52	exit 66
53fi
54
55log()
56{
57	echo "==> $*" >&2
58}
59
60escape()
61{
62	# escape characters that may appear in ports' names and
63	# break regular expressions
64	echo "${1}" |sed -E 's/(\+|\.)/\\\1/g'
65}
66
67pkgname()
68{
69	make -C ${PORTSDIR}/${1} -V PKGNAME
70}
71
72ask()
73{
74	question=${1}
75
76	answer=x
77	while [ "${answer}" != "y" ] && [ "${answer}" != "n" ] ; do
78		read -p "${question}? [yn]" answer
79	done
80
81	echo ${answer}
82}
83
84# return category/port if arg is directly port's directory on the filesystem
85find_catport()
86{
87	arg=${1}
88
89	if [ -d "${PORTSDIR}/${arg}" ] ; then
90		# arg is category/port
91		echo ${arg}
92	elif [ -d "${arg}" ] ; then
93		# arg is the port's directory somewhere in the filesystem
94		# either absolute or relative
95
96		# get the full path
97		rp=$(realpath ${arg})
98
99		category=$(basename $(dirname ${rp}))
100		port=$(basename ${rp})
101		echo ${category}/${port}
102	else
103		echo "What do you mean by '${arg}'?" >&2
104		exit 64
105	fi
106}
107
108find_expired()
109{
110	for category in $(make -C ${PORTSDIR} -V SUBDIR); do
111		for port in $(make -C ${PORTSDIR}/${category} -V SUBDIR); do
112			DATE="$(make -C ${PORTSDIR}/${category}/${port} -V EXPIRATION_DATE)"
113			# shellcheck disable=SC2039
114			if [ -n "${DATE}" ] && [ ! "${DATE}" \> "$${TODAY}" ] ; then
115				if [ "$1" = 1 ] ; then
116					echo -n "${DATE} ${category}/${port}: "
117					make -C ${PORTSDIR}/${category}/${port} -V DEPRECATED
118				else
119					echo "${category}/${port}"
120				fi
121			fi
122		done
123	done
124}
125
126# check if some ports depend on the given port
127# XXX Very Little Chance (tm) for breaking INDEX exists:
128# /usr/ports/INDEX may be outdated and not contain recently added dependencies
129check_dep_core()
130{
131	catport=${1}
132	alltorm=${2}
133	pkgname=$(pkgname ${catport})
134
135	rmpkgs=""
136	rmcatports=""
137	for torm in ${alltorm} ; do
138		torm="$(echo ${torm} | sed 's/\/$//')"
139		rmpkgs="${rmpkgs:+${rmpkgs}|}$(pkgname ${torm})"
140		rmcatports="${rmcatports:+${rmcatports}|}${PORTSDIR}/${torm}/"
141	done
142
143	err=0
144
145	deps=$(grep -E "${pkgname}" ${INDEX} |grep -vE "^(${rmpkgs})" || :)
146	if [ -n "${deps}" ] ; then
147		log "${catport}: some port(s) depend on ${pkgname}:"
148		echo "${deps}" >&2
149		err=1
150	fi
151
152	# check if some Makefiles mention the port to be deleted
153	portdir_grep="^[^#].*/$(basename ${catport})([[:space:]]|@|/|$)"
154	r="$(${GIT} grep '${portdir_grep}' -- '**Makefile*' 'Mk/' \
155		|grep -vE "^(${rmcatports})" || :)"
156	if [ -n "${r}" ] ; then
157		if [ ${err} -eq 1 ] ; then
158			echo >&2
159		fi
160		log "${catport}: some Makefiles mention ${portdir_grep}:"
161		echo "${r}" >&2
162		err=1
163	fi
164
165	return ${err}
166}
167
168check_dep()
169{
170	catport=${1}
171	persist=${2}
172	alltorm=${3}
173
174	log "${catport}: checking dependencies"
175
176	err=0
177
178	res="$(check_dep_core ${catport} "${alltorm}" 2>&1)" || err=1
179
180	if [ ${err} -eq 0 ] ; then
181		return 0
182	fi
183
184	echo "${res}" |${PAGER:-less}
185
186	if [ ${persist} -eq 0 ] ; then
187		return 0
188	fi
189
190	echo "" >&2
191	echo "you can skip ${catport} and continue with the rest or remove it anyway" >&2
192	answer=$(ask "do you want to skip ${catport}")
193	if [ "${answer}" = "y" ] ; then
194		return 1
195	else
196		return 0
197	fi
198}
199
200# query Bugzilla and return the result
201get_PRs()
202{
203	catport=${1}
204	synopsis=${2}
205
206	log "${catport}: getting PRs having ${synopsis} in the synopsis"
207
208	url="https://bugs.freebsd.org/bugzilla/buglist.cgi?quicksearch=${synopsis}"
209
210	raw="$(fetch -q -T 20 -o - "${url}")"
211
212	if [ -z "${raw}" ] ; then
213		log "${catport}: empty result from URL: ${url}"
214		exit 67
215	fi
216
217	printf "%s" "${raw}" \
218	|sed -ne 's,^[[:space:]]*.a href="show_bug.cgi?id=\([0-9][0-9]*\)".\([^0-9][^<]*\).*,\1: \2,p' \
219	|sort
220}
221
222# check if any PRs exist that are related to the port
223check_PRs()
224{
225	catport=${1}
226	synopsis=${2}
227
228	PRs="$(get_PRs ${catport} "${synopsis}")" || exit
229
230	if [ -n "${PRs}" ] ; then
231		log "${catport}: PRs found, related to ${synopsis}:"
232		printf "%s\n" "${PRs}" >&2
233
234		echo "you can skip ${catport} and continue with the rest or remove it anyway" >&2
235		answer=$(ask "do you want to skip ${catport}")
236		if [ "${answer}" = "y" ] ; then
237			return 1
238		else
239			return 0
240		fi
241	fi
242
243	return 0
244}
245
246# add port's entry to ports/MOVED
247edit_MOVED()
248{
249	catport=${1}
250
251	DEPRECATED="$(make -C ${PORTSDIR}/${catport} -V DEPRECATED)"
252	DEPRECATED=${DEPRECATED:+: ${DEPRECATED}}
253	if [ -n "$(make -C ${PORTSDIR}/${catport} -V EXPIRATION_DATE)" ] ; then
254		REASON="Has expired${DEPRECATED}"
255	else
256		REASON="Removed${DEPRECATED}"
257	fi
258
259	log "${catport}: adding entry to ports/MOVED"
260
261	echo "${catport}||${TODAY}|${REASON}" >> MOVED
262	${GIT} add MOVED
263}
264
265# remove port from category/Makefile
266edit_Makefile()
267{
268	cat=${1}
269	port=${2}
270
271	log "${cat}/${port}: removing from ${cat}/Makefile"
272
273	portesc=$(escape ${port})
274
275	${SED} -e "/^[[:space:]]*SUBDIR[[:space:]]*\+=[[:space:]]*${portesc}([[:space:]]+#.*)?$/d" ${cat}/Makefile
276	${GIT} add ${cat}/Makefile
277}
278
279# remove port's files
280rm_port()
281{
282	catport=${1}
283
284	log "${catport}: scheduling port removal"
285
286	echo ${catport} >> ${gitrmlist}
287}
288
289append_Template()
290{
291	catport=${1}
292
293	msg=${catport}
294
295	EXPIRATION_DATE=$(make -C ${PORTSDIR}/${catport} -V EXPIRATION_DATE)
296	if [ -n "${EXPIRATION_DATE}" ] ; then
297		msg="${EXPIRATION_DATE} ${msg}"
298	fi
299
300	DEPRECATED="$(make -C ${PORTSDIR}/${catport} -V DEPRECATED)"
301	if [ -n "${DEPRECATED}" ] ; then
302		msg="${msg}: ${DEPRECATED}"
303	fi
304
305	log "${catport}: adding entry to commit message template"
306
307	echo "${msg}" >> ${gitlog}
308}
309
310# update, ask for confirmation and make a commit
311commit()
312{
313	${GIT} commit --file=${gitlog}
314	answer=$(ask "Do you want to tweak the commit message")
315	if [ "${answer}" = "y" ] ; then
316		${GIT} pull --ff-only --rebase 2>&1
317		${GIT} commit --amend # modify final commit message
318		echo "All done, check the result and push when everything is OK."
319	fi
320}
321
322cleanup()
323{
324	log "cleaning up"
325
326	rm -f ${gitlog} ${gitrmlist}
327}
328
329usage()
330{
331	echo "Usage:" >&2
332	echo "" >&2
333	echo "find expired ports:" >&2
334	echo "${0} -F" >&2
335	echo "" >&2
336	echo "remove port(s):" >&2
337	echo "${0} category1/port1 [ category2/port2 ... ]" >&2
338	echo "" >&2
339	echo "remove all expired ports (as returned by -F):" >&2
340	echo "${0} -a" >&2
341	echo "" >&2
342	echo "just check dependencies:" >&2
343	echo "${0} -d category/port" >&2
344	echo "" >&2
345	echo "just check if any related PRs exist:" >&2
346	echo "${0} -p synopsis" >&2
347
348	exit 64
349}
350
351# main
352
353trap cleanup 1 2 3 15
354
355if [ ! -r ${INDEX} ] ; then
356	echo "${INDEX} not readable, exiting" >&2
357	exit 66
358fi
359
360git_dir="$(${GIT} rev-parse --git-dir)"
361exitcode=$?
362if [ ${exitcode} -ne 0 ] ; then
363	echo "not at a git boundary" >&2
364	exit
365else
366	cd "${git_dir}/.." || exit 1
367fi
368if ! ${GIT} diff --exit-code remotes/origin/main ; then
369	echo "you have local commits or are behind origin/main, exiting" >&2
370	exit 65
371fi
372
373if [ ${#} -eq 0 ] || [ "${1}" = "-h" ] || [ "${1}" = "--help" ] ; then
374	usage
375fi
376
377if [ ${1} = "-d" ] ; then
378	if [ ${#} -ne 2 ] ; then
379		usage
380	fi
381	catport=$(find_catport ${2})
382	check_dep ${catport} 0 ${catport}
383	exit
384fi
385
386if [ ${1} = "-p" ] ; then
387	if [ ${#} -ne 2 ] ; then
388		usage
389	fi
390	get_PRs "dummy" ${2}
391	exit
392fi
393
394if [ ${1} = "-F" ] ; then
395	if [ ${#} -ne 1 ] ; then
396		usage
397	fi
398	find_expired 1
399	exit
400fi
401
402if [ ${1} = "-a" ] ; then
403	if [ ${#} -ne 1 ] ; then
404		usage
405	fi
406	${0} $(find_expired 0)
407	exit
408fi
409
410gitlog=$(mktemp -t gitlog)
411gitrmlist=$(mktemp -t gitrmlist)
412
413if [ $# -eq 1 ] ; then
414	topic="${1%/}"
415	plural=""
416	colon=""
417else
418	log "/!\\ Removing multiple ports at once, commit topic will be generic /!\\"
419	topic="cleanup"
420	plural="s"
421	colon=":"
422fi
423
424echo "${topic}: Remove expired port${plural}${colon}" > ${gitlog}
425echo "" >> ${gitlog}
426
427for catport in $* ; do
428	# convert to category/port
429	catport=$(find_catport ${catport})
430	cat=$(dirname ${catport})
431	port=$(basename ${catport})
432	# remove any trailing slashes
433	catport="${cat}/${port}"
434	pkgname=$(pkgname ${catport})
435
436	if ! check_dep ${catport} 1 "${*}" ; then
437		continue
438	fi
439
440	if ! check_PRs ${catport} ${port} ; then
441		continue
442	fi
443
444	# everything seems ok, edit the files
445
446	edit_MOVED ${catport}
447
448	edit_Makefile ${cat} ${port}
449
450	append_Template ${catport}
451
452	rm_port ${catport}
453done
454
455if [ -s ${gitrmlist} ] ; then
456	${GIT} rm -r $(cat ${gitrmlist})
457else
458	log "No port directories to remove"
459fi
460
461# give a chance to the committer to edit files by hand and recreate/review
462# the diff afterwards
463answer=y
464while [ "${answer}" = "y" ] ; do
465	${GIT} diff --staged --irreversible-delete
466
467	echo "" >&2
468	echo "you can now edit files by hand" >&2
469	answer=$(ask "do you want to recreate the diff")
470	if [ "${answer}" = "y" ] ; then
471		${GIT} add MOVED
472	fi
473done
474
475commit
476
477cleanup
478