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

arch= branch= workdir= mailto= subj= opt_join= opt_noreplace=
while getopts a:b:d:jnm:s:h opt; do
	case "$opt" in
		a) arch="${OPTARG:?}"
			readonly arch ;;
		b) branch="${OPTARG:?}"
			readonly branch ;;
		d) workdir="$(readlink -ev "${OPTARG:?}")"
			readonly workdir ;;
		m) mailto="${OPTARG:?}" 
			readonly mailto ;;
		s) subj="${OPTARG:?}" ;;
		j) opt_join=1 ;;
		n) opt_noreplace=1 ;;
		h) pod2usage --exit=0 "$0"; exit 0 ;;
		*) pod2usage --exit=2 "$0"; exit 2 ;;
	esac
done
shift "$((OPTIND-1))"

if [ -z "$*" ]; then
	echo "${0##*/}: not enough arguments" >&2
	pod2usage --exit=2 "$0"; exit 2
fi

cmd="$1"; shift; OPTIND=1
which "$cmd" >/dev/null
name="$(basename "$cmd")"

count() { inflect --noun -- "$@"; }

noun="${name%s}"
[ -n "$subj" ] || subj="I: ${branch:-Sisyphus}-$(date +%Y%m%d)${arch:+ }${arch:-} $(count 2 "$noun"):"

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

if [ -z "$workdir" ]; then
	branch_or_arch="$branch$arch"
	workdir="$(readlink -ev "$HOME")/.qa-robot/$name${branch_or_arch:+/}$branch${arch:+/}${arch:-}"
	mkdir -p "$workdir"
fi
readonly workdir

. trap.sh
lockfile -r0 "$workdir"/lock
add_trap rm -f "$workdir"/lock

. "$cmd" ${1+"$@"} >"$workdir/dump.new"

cd "$workdir"
readonly PWD

if [ ! -s dump.new ]; then
	echo "${0##*/}: empty $name state 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 [ ! -s dump.old ]; then
	mv -f dump.new dump.old
	n=`wc -l <dump.old`
	echo "${0##*/}: initialized $name state ($n entries)." >&2
	exit 0
fi

qa_unchanged()
{
	[ -t 2 ] || exit 0
	n=`wc -l <dump.old`
	echo "${0##*/}: $name state unchanged ($n entries)." >&2
	exit 0
}

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

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 ] || qa_unchanged
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

zmail()
{
	local subj="$1" file="$2"; shift 2
	local size="$(du -bk "$file" |cut -f1)"
	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
}

[ -z "$mailto" ] || zmail "$subj" message $mailto
{ echo "$subj"; echo; cat message; } >message$$
mv -f message$$ message
[ -n "$mailto" ] || cat message

if [ -z "$opt_noreplace" ]; then
	[ -f dump.log ] ||: >dump.log
	diff -U1 dump.old dump.new |cat - dump.log >dump.log$$
	mv -f dump.log$$ dump.log
	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<-h>] 
[B<-a> I<arch>]
[B<-b> I<branch>]
[B<-d> I<workdir>]
[B<-j>] 
[B<-n>] 
[B<-m> I<mailto>] 
[B<-s> I<subj>] 
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<-a> I<string>

Set arch name. No defaults.

=item	B<-b> I<string>

Set branch name. Default is C<Sisyphus>.

=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>)/I<branch>>>.

=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<-s> I<subj>

Specify initial subject for email message.

=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>

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__
