#!/bin/sh -ef
export LC_ALL=C

usage()
{
	[ "$1" -eq 0 ] &&
		pod2usage --exit=0 --verbose=1 "$0" ||
		pod2usage --exit=0 --verbose=0 "$0" >&2
	exit "$1"
}

opt_workdir= opt_join= opt_noreplace= opt_mailto=
while getopts d:jnm:h opt; do
	case "$opt" in
		d) opt_workdir="${OPTARG:?}" ;;
		j) opt_join=1 ;;
		n) opt_noreplace=1 ;;
		m) opt_mailto="${OPTARG:?}" ;;
		h) usage 0 ;;
		*) usage 2 ;;
	esac
done
shift "$((OPTIND-1))"
[ -n "$*" ] || usage 2

cmd="$1"; shift; OPTIND=1
PATH="$PATH:/usr/share/qa-robot" which "$cmd" >/dev/null
name="$(basename "$cmd")"

count()
{
	local n="$1" noun="$2"; shift 2
	[ "$n" -eq 1 ] && echo "$noun" ||
	echo "$noun" |sed 's/y$/ie/;s/s$/se/;s/f$/ve/;s/$/s/;'
}

noun="${name%s}"
proj="Sisyphus"
subj="I: $proj-$(date +%Y%m%d) $(count 2 "$noun"):"

fmt_start()	{ :; }
fmt_plus()	{ subj="$subj +$1";	echo "	$1 NEW $2"; cat; echo; }
fmt_minus()	{ subj="$subj -$1";	echo "	$1 OLD $2"; cat; echo; }
fmt_new()	{ subj="$subj +$1!";	echo "	$1 NEW $2"; cat; echo; }
fmt_old()	{ subj="$subj -$1";	echo "	$1 OLD $2"; cat; echo; }
fmt_updated()	{ subj="$subj +$1";	echo "	$1 UPDATED $2"; cat; echo; }
fmt_total()	{ subj="$subj ($1)";	echo "Total $1 $2."; }

workdir="${opt_workdir:-$HOME/.qa-robot/$name}"
mkdir -p -- "$workdir"
workdir="$(realpath "$workdir")"
readonly workdir

exit_handler()
{
	local rc=$?
	trap - EXIT
	rm -f "$workdir/lock"
	exit $rc
}
lockfile -r0 "$workdir/lock"
trap exit_handler SIGHUP SIGPIPE SIGINT SIGQUIT SIGTERM EXIT

PATH="$PATH:/usr/share/qa-robot" . "$cmd" ${1+"$@"} >"$workdir/dump.new"

cd "$workdir"
readonly PWD

if [ ! -s dump.new ]; then
	echo "Empty dump unexpected." >&2
	exit 1
fi

if [ -n "$opt_join" ]; then
	sort -o dump.new -u -t$'\t' -k1,1 dump.new
else
	sort -o dump.new -u dump.new
fi

if [ ! -f dump.old ]; then
	mv dump.new dump.old
	n=`wc -l <dump.old`
	echo "Initialized $name database ($n entries)." >&2
	exit 0
fi

comm -13 dump.old dump.new >comm.plus
comm -23 dump.old dump.new >comm.minus
[ -s comm.plus -o -s comm.minus ] || exit 0

if [ -n "$opt_join" ]; then
	join -t$'\t' comm.minus comm.plus >join.updated
	join -t$'\t' -v1 comm.minus comm.plus >join.old
	join -t$'\t' -v2 comm.minus comm.plus >join.new
	[ -s join.updated -o -s join.old -o -s join.new ] || exit 0
fi

fmt_start >message

if [ -n "$opt_join" ]; then
	for s in new old updated; do
		[ -s join.$s ] || continue
		n=`wc -l <join.$s`
		fmt_$s "$n" "$(count "$n" "$noun")" <join.$s
	done
else
	for s in plus minus; do
		[ -s comm.$s ] || continue
		n=`wc -l <comm.$s`
		fmt_$s "$n" "$(count "$n" "$noun")" <comm.$s
	done
fi >>message

n=`wc -l <dump.new`
fmt_total "$n" "$(count "$n" "$noun")" <dump.new >>message

filesize()
{
	/usr/lib/rpm/filesize "$1" 1024
}

zmail()
{
	local subj="$1" file="$2"; shift 2
	local size; size="$(filesize "$file")"
	local LC_ALL=
	if [ "$size" -gt 1024 ]; then
		echo "Message size is ${size}K, E2BIG." >&2
		return 1
	elif [ "$size" -gt 32 ]; then
		gzip -9nf "$file"
		mutt -x -s "$subj" -a "$file.gz" "$@" </dev/null
		gzip -df "$file.gz"
	elif [ -f signature ]; then
		{ echo; cat signature; } |mutt -x \
			-s "$subj" -i "$file" "$@"
	else
		mutt -x -s "$subj" -i "$file" "$@" </dev/null
	fi
}

if [ -n "$opt_mailto" ]; then
	zmail "$subj" message $opt_mailto
else
	echo "$subj"; echo
	cat message
fi

if [ -z "$opt_noreplace" ]; then
	mv -f dump.old dump.bak
	mv -f dump.new dump.old
fi

: <<'__EOF__'

=head1	NAME

qa-robot - simple notification system

=head1	SYNOPSIS

B<qa-robot> [B<-d> I<workdir>] [B<-j>] [B<-n>] [B<-m> I<mailto>] [B<-h>] I<cmd> [I<args>]

=head1	DESCRIPTION

B<qa-robot> reports various state changes, in terms of new, old, and (possibly)
updated entries.  I<cmd> must be a shell script which, whenever sourced or
executed, dumps its current state to C<stdout>, one line per entry; the script
may also provide its custom formatting routines.

=head1	OPTIONS

=over

=item	B<-d> I<workdir>

Use I<workdir> working directory.
Save I<cmd> state under I<workdir>.
Default working directory is C<S<$HOME/.qa-robot/$(basename I<cmd>)>>.

=item	B<-j>

Enable join mode; join records on the first field.
Fields must be separated by tabs.

=item	B<-n>

Do not finally replace the old I<cmd> state with the new one.
Useful for test runs.

=item	B<-m> I<mailto>

Suppress normal output.  Send email notification to I<mailto>
address(es) instead.  Messages larger than 32K are sent as gzipped
attachments.  Messages larger than 1024K produce a fatal error.

=item	B<-h>

Display this help and exit.

=back

=head1	FILES

=head2	Files in use under the working directory

=over

=item	B<dump.new>, B<dump.old>

Current and previous I<cmd> state files.

=item	B<comm.plus>, B<comm.minus>

Comparison between B<dump.new> and B<dump.old>.  B<comm.plus> has lines
unique to the current state, while B<comm.minus> has lines unique to the
previous state.

=item	B<join.new>, B<join.old>, B<join.updated>

In join mode, B<comm.plus> and B<comm.minus>' lines are treated as records,
whose fields are separated by tabs.  The first field must be a primary key.
B<comm.plus> and B<comm.minus> are then joined on the first field.

Thus B<join.new> has records unique to B<comm.plus> (i.e. lines in B<comm.plus>
unpairable with those in B<comm.minus>), B<join.old> has records unique to
B<comm.minus>, and B<join.updated> contains pairable records joined on the
first field.

=item	B<lock>

Semaphore file, to guard against simultaneous runs.

=item	B<message>

The report is saved to this file.

=item	B<signature>

When email message is sent, this file, should it exist, is appended to
the message (except when sending gzipped attachments).

=back

=head2	Other files

=over

=item	B<$PATH>, B</usr/share/qa-robot>

Default I<cmd> script location.

=item	B<$HOME/.qa-robot/$(basename I<cmd>)>

Default working directory.

=back

=head1	FORMATTING

=head2	Variables

=over

=item	B<noun>

A countable noun that describes the entries, in the singular number.

=item	B<subj>

The message subject.

=back

=head2	Functions

=over

=item	B<fmt_start>

Executed when formatting is started.

=item	B<fmt_plus>, B<fmt_minus>

Main formatting routines, for displaying new and old entries;
executed only when the entries have actually been found.
Calling convention for these routines is as follows:

	fmt_$s $n $noun <comm.$s

where C<$n> is the number of entries, C<$noun> is the noun that
describes the entries (in the proper number, according to C<$n>),
and C<comm.$s> is the appropriate file, connected to C<stdin>,
with C<$n> entries in it.

=item	B<fmt_new>, B<fmt_old>, B<fmt_updated>

Alternative formatting routines for join mode (similar to B<fmt_plus>
and B<fmt_minus>).

=item	B<fmt_total>

Formatting routine for the total number of entries.

	fmt_total $n $noun <dump.new

=back

=head1	EXAMPLES

B<qa-robot> comes with a few real-world components used by ALT QA Team
to notify subscribers of ALT Linux mailing lists:

=over

=item	B<unmets>

Reports new and resolved unmet dependencies.
Creates aptbox to distance the host system setup.

=item	B<bugs> [I<URL>]

Bugzilla watchdog.  Reports new and resolved bugs (whether a bug has
been resolved is subject to specific bugzilla conventions), along with
the total number of pending (i.e. unresolved) bugs.

The I<URL> specifies base Bugzilla URL.
The default base URL is L<https://bugzilla.altlinux.org>.

=item	B<packages> [I<DIR>]

Reports new, old (removed), and updated rpm packages under I<DIR> directory.
Last changelog entry is listed for new and updated packages.

The default I<DIR> is C<$sisyphus/files/SRPMS>, where C<$sisyphus>
is vendor-specific location.

=back

=head1	BUGS

B<qa-robot> is executed in C<noglob> and C<errexit> mode, so is the I<cmd>
sciprt.

After I<cmd> script is sourced, the current working directory is set to
I<workdir> and may not be changed by formatting routines.

The locale is set to C<LC_ALL=C>.

=head1	AUTHOR

Written by Alexey Tourbin <at@altlinux.org>.

=head1	COPYING

Copyright (c) 2005 Alexey Tourbin, ALT Linux Team.

This 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 2 of the License, or (at your option) any later version.

=cut

__EOF__
