#!/bin/bash
#
# This script called in a CVS directory compares local files with
# the repository, and prepares an update package containing all
# changes and new files for submission to a CVS maintainer. If there
# are only changes in text files, then a compressed unified diff is
# made (foo.diff.bz2). If there are also changed binary or new files,
# then an archive is made instead (foo.tar.bz2). The base name ("foo")
# can be given as command line argument. Otherwise the directory name
# is used. The script also leaves a diff in uncompressed/unpackaged
# form. This is only for developer convenience -- for a quick check
# of the diff correctness. It is not to be submitted. The script will
# not overwrite any file, but rather rename conflicting files.
#
# Usage:  fg-submit [-v] [<basename>]
#
# Options:
#         -v ... verbose output
#
# Example:
#     $ cd $FG_ROOT/Aircraft/bo105
#     $ fg-submit                # -> bo105.diff.bz2 or bo105.tar.bz2
#
#     $ fg-submit update         # -> update.diff.bz2 or update.tar.bz2
#
#
# Spaces in the basename are replaced with "%20". People who prefer
# to have the date in the archive name can conveniently achieve this
# by defining a shell alias in ~/.bashrc:
#
#     alias submit='fg-submit "${PWD##*/}-$(date +%Y-%m-%d)"'
#
#
#
# If the script finds an application named "fg-upload", then it calls
# this at the end with two arguments:
#
#     $1 ... archive or compressed diff for submission
#     $2 ... accessory uncompressed diff, *NOT* for submission!
#
# $1 and $2 are guaranteed not to contain spaces, only $1 is guaranteed
# to actually exist. Such a script can be used to upload the file to an
# ftp-/webserver, and/or to remove one or both files. Example using
# KDE's kfmclient for upload (alternatives: ncftpput, gnomevfs-copy):
#
#     $ cat ~/bin/fg-upload
#     #!/bin/bash
#     echo "uploading $1"
#     if kfmclient copy $1 ftp://user:password@server.com; then
#             echo "deleting $1 $2"
#             rm -f $1 $2
#
#             URL=ftp://server.com/$1
#
#             # copy URL to KDE's clipboard, so that MMB-clicking pastes it
#             dcop klipper klipper setClipboardContents $URL
#
#             echo "Done.  -->  $URL"
#     else
#             echo "$0: uploading failed!"
#     fi
#
#
#
# Whether a file should be included in the archive or not, is decided
# by pattern rules. There is a set of reasonable default rules predefined,
# but alternative settings can be defined in a hidden configuration file
# named ".fg-submit". Such a file is searched in the current directory,
# in its parent directory, in its grand-parent directory and so on,
# and finally in the $HOME directory. The first found file is taken.
#
# A file can use a list of three keywords with arguments, each on a
# separate line:
#
#     ALLOW  <pattern-list>     ... accept & report matching file
#     DENY   <pattern-list>     ... reject & report matching file
#     IGNORE <pattern-list>     ... silently reject matching file
#
# A <pattern-list> is a space-separated list of shell pattern.
# It may also be empty, in which case it has no effect. Examples:
#
#     DENY   test.blend
#     ALLOW  *.xcf *.blend
#
# The list of pattern is checked in the same order in which it was
# built. The first match causes a file to be accepted or rejected.
# Further matches are not considered. Comments using the hash
# character '#' are allowed and ignored.
#
# Some default rules are always added at the end. If you want to
# bypass them, then finish your configuration with an "ALLOW *"
# or "DENY *", and no file will ever reach the default rules.
#
#
# Example:
#
#     DENY  test.xcf # throw out the test image, but ...
#     ALLOW *.xcf    # ... allow all other GIMP images (the default
#                    # rules would otherwise throw them out)
#
#     ALLOW  not.old # add this file, but ...
#     IGNORE *.old   # throw out all other "old" files (and don't
#                    # report that to the terminal)
#
#
# .fg-submit configuration files are "sourced" bash scripts, the
# keywords are simple shell functions. That means that you can
# also use other bash commands in that file, such as "echo", or
# write several commands on one line, separated with semicolon.
# You can even put all special rules in your ~/.fg-submit file,
# with rules depending on the working directory:
#
#     case "$PWD" in
#     */bo105*)  DENY *.osg; ALLOW livery.xcf ;;
#     */ufo*)    DENY *.tiff ;;
#     esac



SELF=${0##*/}
DIR=${PWD##*/}

if [ "$1" == "-v" ]; then
	DBG=1
	shift
fi

BASE=${1:-$DIR}
BASE=${BASE// /%20}

CVS=/usr/bin/cvs                 # avoid colorcvs wrapper from
[ -x $CVS ] || CVS=cvs           # http://www.hakubi.us/colorcvs/
UPLOAD=$(which fg-upload 2>/dev/null)

CONFIG_FILE=".fg-submit"
ARCHIVE=$BASE.tar.bz2
DIFF=$BASE.diff
CDIFF=$DIFF.bz2

# these rules are always prepended; the first letter decides if the
# rule accepts (+), rejects (-), or ignores (!) a matching file.
PREFIX_RULES="
	!$DIFF* !$CDIFF* !$ARCHIVE*
	!CVS/* !*/CVS/*
"
# these rules are always appended
DEFAULT_RULES="
	+.cvsignore +*/.cvsignore
	-*~ -*. -*.bak -*.orig
	-*.RGB -*.RGBA -*.MDL
	-*.XCF -*.tga -*.TGA -*.bmp -*.BMP -*.PNG
	-*.blend -*.blend[0-9] -*blend[0-9][0-9] -*.blend[0-9][0-9][0-9]
	-*.gz -*.tgz -*.bz2 -*.zip -*.tar.gz* -*.tar.bz2*
"
POSTFIX_RULES="
	!.* !*/.*
	+*
"



function ERROR   { echo -e "\e[31;1m$*\e[m";   }
function LOG     { echo -e "\e[35m$*\e[m";     }
function NEW     { echo -e "\e[32m\t+ $*\e[m"; }
function CHANGED { echo -e "\e[36m\t+ $*\e[m"; }
function REJECT  { echo -e "\e[31m\t- $*\e[m"; }
function DEBUG   { [ $DBG ] && echo -e "$*";   }

function diffstat {
	# output diff statistics, similar to the "diffstat" utility
	awk '
		function line(a, r, c, f) {
			print "\t\033[32m"a"\033[m\t\033[31m"r"\033[m\t\033[34m"c"\033[m\t"f
		}
		function dofile() {
			if (!file) {
				return
			}
			if (bin) {
				print "\t. . . . binary  . . . . \033[36m"file"\033[m"
			} else {
				line(a, r, c, file)
				at += a; rt += r; ct += c
			}
			a = r = c = 0
		}
		BEGIN      {
			print "\tadded---removed-changed----------------------------------------"
			a = r = c = at = rt = ct = n = bin = 0
		}
		/^Index: / { dofile(); scan = bin = 0; file = $2; n++; next }
		/^@@/      { scan = 1; next }
		/^Binary/  { if (!scan) bin = 1; next }
		/^\+/      { if (scan) a++; next }
		/^-/       { if (scan) r++; next }
		/^!/       { if (scan) c++; next }
		END        {
			dofile()
			print "\t----------------------------------------total------------------"
			line(at, rt, ct, "\033[min "n" files")
		}
	' <$1
}

function backup_filename {
	for ((i = 1; 1; i = i + 1)); do
		name=$1.$i
		if ! [ -a "$name" ]; then
			touch $name
			echo $name
			return
		fi
	done
}



# set up accept/reject rules
function ALLOW   { for i in $*; do RULES="$RULES +$i"; done }
function DENY    { for i in $*; do RULES="$RULES -$i"; done }
function IGNORE  { for i in $*; do RULES="$RULES !$i"; done }


function search_config {
	file="$1/$CONFIG_FILE"
	DEBUG "checking for config file $file"
	if [ -f "$file" ]; then
		CONFIG="$file"
		return 0
	elif [ "$1" ]; then
		search_config ${1%/${1##*/}} # parent dir
		return
	fi
	return 1
}


set -f
RULES=
if search_config "$PWD"; then
	LOG "loading config file $CONFIG"
	source "$CONFIG"
elif [ -f ~/$CONFIG_FILE ]; then
	DEBUG "loading config file ~/$CONFIG_FILE"
	source ~/$CONFIG_FILE
elif [ -f ~/${CONFIG_FILE}rc ]; then
	DEBUG "loading config file ~/${CONFIG}rc"
	source ~/${CONFIG_FILE}rc
fi
RULES="$PREFIX_RULES $RULES $DEFAULT_RULES $POSTFIX_RULES"
set +f


if [ $DBG ]; then
	DEBUG "using these rules: "
	for i in $RULES; do echo -n "$i "; done
	echo
fi



# create temporary dir that's automatically removed on exit
TMP=$(mktemp -d /tmp/$SELF.$BASE.XXXXXX) || (echo "$0: can't create temporary dir"; exit 1)
trap "rm -rf $TMP" 0 1 2 3 13 15



# move old files out of the way adding sequential suffixes
for i in $DIFF $CDIFF $ARCHIVE; do
	[ -f $i ] && mv $i $(backup_filename $i)
done



LOG "updating and checking for new files ..."
$CVS -q up -dP >$TMP/up || exit 1


if grep "^C " $TMP/up &>/dev/null; then
	ERROR "there are conflicts with the following files:"
	grep "^C " $TMP/up
	exit 1
fi



LOG "making diff ..."
if ! $CVS -q diff -up >$DIFF; then
	LOG "diff statistics:"
	diffstat $DIFF
	echo

	# add diff file itself
	echo $DIFF >>$TMP/files

	# add changed binary files
	awk '
		/^Index: / { scan = 1; file = $2; next }
		/^@@/      { scan = 0; next }
		/^Binary/  { if (scan) { print file } }
	' <$DIFF >>$TMP/files
else
	rm -f $DIFF
fi



LOG "checking for files to submit ..."
if [ -f $TMP/files ]; then
	cat $TMP/files|while read i; do
		CHANGED "$i"
	done
fi

grep "^? " $TMP/up|while read i; do
	find ${i#? } -type f >>$TMP/check
done



# filter files according to the pattern rules
if [ -f $TMP/check ]; then
	for i in $(cat $TMP/check); do
		DEBUG "checking whether file '$i' matches"
		for r in $RULES; do
			DEBUG "\t\trule $r"
			class=${r:0:1}
			rule=${r:1}
			case "$i" in $rule)
				case $class in
				!) DEBUG "$i\t\t\"silently\" rejected\t\t$rule" ;;
				-) REJECT "$i\t\t$rule" ;;
				+) NEW "$i\t\t$rule" && echo "$i" >>$TMP/files ;;
				esac
				break
				;;
			esac
		done
	done
fi



if ! [ -f $TMP/files ]; then
	LOG "no changed or new files found"
	exit 0
fi

echo
numfiles=$(awk '//{n++}END{print n}' <$TMP/files)
if [ -f $DIFF -a $numfiles == 1 ]; then
	LOG "only changed non-binary files found"
	LOG "creating compressed diff \e[1;37;40m$CDIFF\e[m\e[35m ..."
	bzip2 --keep $DIFF
	RESULT=$CDIFF
else
	LOG "changed and/or new files found"
	LOG "creating archive \e[1;37;40m$ARCHIVE\e[m\e[35m ..."
	tar --create --bzip2 --file=$ARCHIVE --files-from $TMP/files
	RESULT=$ARCHIVE
fi


[ -x "$UPLOAD" -a -f $RESULT ] && $UPLOAD $RESULT $DIFF