#!/usr/bin/env bash
# $Id: stale-files 32 2026-10-27 18:18:30Z peter $
#
# stale-files --- list, and with --delete remove, the files of this directory that the release it holds no longer has.
#
# Copyright (C) 2026 Péter Felecan
# Assisted by Claude Sonnet 5.5 (Anthropic)
#
# This program is free software: you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
#
# Origin (packpay, 2026-10-17): a new archive unpacked over an older
# tree leaves the files that the new one dropped; a file moved
# elsewhere stayed at its old place in a working copy, where it was
# edited and read by nothing. Every archive now carries a MANIFEST
# (make-archive appends it), the list of the files it ships; this
# script compares the directory with it, over the same file set that
# make-archive ships (tar --exclude-from=.tar-exclude: generated files
# are never reported). `make stale` runs it, and the targets that
# build, check or install something run it as a warning.

set -euo pipefail
cd "$(dirname "$0")"

help() {
	cat <<EOF
Usage: stale-files [--warn] [--delete]
       stale-files -h | --help

List the files of this directory that the release it holds no longer has: the
leftovers of older releases that archives unpacked over an old tree do not remove.
It compares the directory with MANIFEST, the list of the files of the archive,
over the files that make-archive would ship (generated files are not reported).

The same list is an ERROR or a WARNING, according to how it is run:
  stale-files, make stale          an error: list them and exit 1
  stale-files --warn               a warning: list them and exit 0
  make, make check, make install, make pdf, make html, make figures,
  make html-export, and each PDF, HTML or figure file built by name
                                   a warning: they run it with --warn, the files
                                   are listed and the build carries on
  the clean targets                do not run it
A build is not stopped by a leftover that it cannot do anything about, and a
leftover is still seen; a run that is asked for fails, so that a script can use it.

Options:
  --warn       list them and exit 0, whatever there is (a warning)
  --delete     remove them, and the directories left empty, and exit 0
  -h, --help   this text

Files of your own that are meant to stay are listed in .stale-ignore, one path or
glob per line (# starts a comment); a line that ends in / is a directory. With no
MANIFEST (a tree that was not unpacked from an archive) there is nothing to
compare and it says so.
EOF
}

MODE=fail
DELETE=0
while [[ $# -gt 0 ]]
do
	case "$1" in
		--warn) MODE=warn; shift ;;
		--delete) DELETE=1; shift ;;
		-h|--help) help; exit 0 ;;
		*) help >&2; exit 2 ;;
	esac
done

if [[ ! -f MANIFEST ]]
then
	echo "stale-files: there is no MANIFEST here: this directory was not unpacked from a release archive, so there is nothing to compare"
	exit 0
fi
[[ -f .tar-exclude ]] || { echo "stale-files: there is no .tar-exclude here, which says what the project's files are" >&2; exit 1; }

declare -A listed=()
count=0
while IFS= read -r f
do
	[[ -n "$f" ]] && { listed["$f"]=1; count=$((count + 1)); }
done < MANIFEST

patterns=()
if [[ -f .stale-ignore ]]
then
	while IFS= read -r line
	do
		line="${line%%#*}"
		line="${line#"${line%%[![:space:]]*}"}"
		line="${line%"${line##*[![:space:]]}"}"
		[[ -n "$line" ]] && patterns+=("$line")
	done < .stale-ignore
fi

ignored() {
	local f="$1" p
	for p in ${patterns[@]+"${patterns[@]}"}
	do
		case "$p" in
			*/) [[ "$f" == "$p"* ]] && return 0 ;;
			*) [[ "$f" == $p ]] && return 0 ;;
		esac
	done
	return 1
}

stale=()
while IFS= read -r f
do
	f="${f#./}"
	[[ -z "$f" || "$f" == */ || "$f" == MANIFEST || "$f" == .stale-ignore ]] && continue
	[[ -n "${listed[$f]:-}" ]] && continue
	ignored "$f" && continue
	stale+=("$f")
done < <(tar cf - --exclude-from=.tar-exclude . 2>/dev/null | tar tf -)

if [[ ${#stale[@]} -eq 0 ]]
then
	# A warning that is not there says nothing: it runs before every make.
	[[ "$MODE" == warn ]] || echo "stale-files: no stale file ($count files in MANIFEST)"
	exit 0
fi
if [[ $DELETE -eq 1 ]]
then
	for f in "${stale[@]}"
	do
		rm -f -- "$f"
		d="$(dirname "$f")"
		while [[ "$d" != "." && "$d" != "/" ]]
		do
			rmdir "$d" 2> /dev/null || break
			d="$(dirname "$d")"
		done
		echo "stale-files: removed $f"
	done
	echo "stale-files: removed ${#stale[@]} stale files"
	exit 0
fi
echo "stale-files: ${#stale[@]} files are not part of this release (MANIFEST lists $count):"
for f in "${stale[@]}"
do
	echo "  $f"
done
echo "stale-files: they are left from an older release, or they are yours: ./stale-files --delete removes them; files of your own are listed in .stale-ignore"
if [[ "$MODE" == warn ]]
then
	echo "stale-files: this is a warning (exit 0): the build goes on; stale-files by itself, and make stale, report it as an error"
	exit 0
fi
echo "stale-files: this is an error (exit 1): it is what stale-files by itself and make stale report; the targets that build, check or install something (make, make check, make install, make pdf, html, figures, html-export) only warn"
exit 1
