#!/bin/sh

# Automagically do RCS check in if file(s) changed (or not checked in),
# optionally specifying an age (relatively to current time) which must
# be exceeded in order to be automagically checked in (e.g. to keep
# spurious changes of very recently changed work-in-progress changes
# from also automagically getting check in - i.e. wait 'till the file
# has become at least somewhat more "settled" before doing an updated
# check it in).

set -e

progname="$(basename "$0")"

msg='autorcs check in' # our msg for: ci -m[msg], unset for none
ciopts="-d -l -M" # our ci options and option arguments

# vi(1) :se tabstop=4

usage(){
	cat <<- __EOT__
		usage: $progname [--age=] file [file ...] [--age=] [file ...] ...
		--age= non-negative integer number of seconds, decimal digits only,
		or same with at most one of these modifier letters appended:
		m M - minutes
		h H - hours
		d D - days
		w W - weeks
		If --age= is not specified, default is no age constraint.
		--age= can be specified multiple times, the last specified
		before each file applies to that file.
		Note that these multipliers are interpreted as if minutes are
		always 60 seconds, hours 60 such minutes, days 24 such hours,
		and weeks, 7 such days.
		For specified file(s) meeting specified (default none) age
		constraint, if the file differs from its rcs checked in
		version (or if not checked in), it will be checked in.
	__EOT__
}

rc=1 # exit / Return Code - presume we have issues unless all went well
errors= # non-null if errors encountered, track (for (partial) deferral)
caughtsig= # signal number (if any) we caught
signals='1 2 3 15' # signal numbers we may catch

myexit(){
	[ -z "$caughtsig" ] ||
		kill -"$caughtsig" "$$"
	exit "$rc"
}

files= # non-null if we have we successfully processed any files
age= # any currently active --age= specification (will convert to seconds)
agef= # temporary file we use for age determination

startdir="$(pwd -P)" # directory we start from
now="$(date +%s)"

while [ "$#" -ge 1 ]
do
	arg="$1"; shift
	agerc=0
	age="$(expr x"$arg" : 'x--age=\([0-9][0-9]*[MmHhDdWw]\{0,1\}\)$')" ||
		agerc="$?"
	if [ -n "$age" ]; then
		# matched, is age, not file
		file=
		[ "$#" -ge 1 ] || {
			echo "usage: $progname: must have at least one argument" 1>&2
			usage 1>&2
			myexit
		}
		case "$age" in
			*[MmHhDdWw])
				base="$(
						expr x"$age" : 'x\([0-9][0-9]*\)[MmHhDdWw]$'
					)" || :
				[ -n "$base" ] || {
					echo "$progname: internal error, failed to get" \
						"base portion from --age=, aborting" 1>&2
					myexit
				}
				exprrc=0
				multiplier="$(
						expr x"$age" : 'x[0-9][0-9]*\([MmHhDdWw]\)$'
					)" || exprrc="$?"
				if [ -n "$multiplier" ]; then
					case "$multiplier" in
						[Mm])multiplier=60;;
						[Hh])multiplier=3600;;
						[Dd])multiplier=86400;;
						[Ww])multiplier=604800;;
						*)
							echo "$progname: internal error," \
								"multiplier match failure, aborting" \
								1>&2
							myexit
						;;
					esac
				else
					echo "$progname: internal error," \
						"multiplier match failure, aborting" \
						1>&2
					myexit
				fi
			;;
			*)
				base="$age"
				multiplier=1
			;;
		esac
		age="$(expr "$base" '*' "$multiplier")" || :
		[ -n "$age" ] || {
			echo "$progname: internal error, failed to determine age" \
				"in seconds, aborting" 1>&2
			myexit
		}
		[ -n "$agef" ] || {
			for sig in $signals
			do
				trap "caughtsig=$sig" "$sig"
			done
			# need age tracking temporary file
			agef="$(mktemp)" || {
				echo "$progname: mktemp failed, aborting" 1>&2
				myexit
			}
			for sig in $signals
			do
				trap '
					rm '"$agef"' || :
					caughtsig='"$sig"'
					rc=1
					trap - '"$sig"'
					myexit
				' "$sig"
			done
			trap '
				rm '"$agef"' || rc=1
				trap - 0
				myexit
			' 0
			[ -z "$caughtsig" ] ||
				kill -"$caughtsig" "$$"
		}
		# set mtime on our agef for use by find --newer
		s="$(expr "$now" - "$age")" || :
		[ -n "$s" ] || {
			echo "$progname: internal error, unable to" \
				"determine system time for $arg, aborting" 1>&2
			myexit
		}
		[ 0 -le "$s" ] || {
			echo "$progname: $arg too old, aborting" 1>&2
			myexit
		}
		t="$(TZ=GMT0 date +'%Y%m%d%H%M.%S' -d "@$s")" || {
			echo "$progname: internal error with date, aborting" 1>&2
			myexit
		}
		TZ=GMT0 touch -m -t "$t" "$agef" || {
			echo "$progname: internal error with touch, aborting" 1>&2
			myexit
		}
	elif [ "$agerc" -ge 2 ]; then
		echo "$progname: internal error with expr, aborting" 1>&2
		myexit
	else
		# not matched, must be file
		file="$arg"
		basename="$(basename "$file")" || {
			echo "$progname: internal error with basename, aborting" 1>&2
			myexit
		}
		dirname="$(dirname "$file")" || {
			echo "$progname: internal error with dirname, aborting" 1>&2
			myexit
		}
		# files may have absolute or relative pathnames,
		# to work with either we access relative to directory we
		# started in
		{
			cd "$startdir" &&
			cd "$dirname" &&
			[ -f ./"$basename" ]
		} || {
			echo "$progname: unable to process file $file, skipping" 1>&2
			errors=y
			# try next
			continue
		}
		type rcsdiff >>/dev/null 2>&1 || {
			# can't compare without rcsdiff, so treat this as fatal
			echo "$progname: failed to locate rcsdiff program," \
				"aborting" 1>&2
			myexit
		}
		rcsdiffrc=0
		rcsdiff ./"$basename" >>/dev/null 2>&1 || rcsdiffrc="$?"
		case "$rcsdiffrc" in
			0)
				# no differences
				files=y
				# try next
				continue
			;;
			[12])
				# differences or RCS file doesn't exist
				[ -z "$agef" ] || {
					# we need to check age criteria
					findout="$(
						find ./"$basename" \( -type d -prune \) \
							-o \
							\( -type f ! -newer "$agef" -print \)
					)" || {
						echo "$progname: find(1) gave error on file" \
							"$file, skipping" 1>&2
						errors=y
						# try next
						continue
					}
					[ -n "$findout" ] || {
						files=y
						# not old enough
						# try next
						continue
					}
				}
				# we have differences to check in
				type ci >>/dev/null 2>&1 || {
					echo "$progname: failed to locate ci program," \
						"aborting" 1>&2
					myexit
				}
				{
					ci $ciopts "${msg+-m$msg}" ./"$basename" \
					</dev/null >>/dev/null 2>&1 && {
						# successfully checked in
						files=y
						# try next
						continue
					}
				} || :
				# maybe try setting lock first
				rcs -l ./"$basename" </dev/null >>/dev/null 2>&1 || :
				# retry check in:
				{
					ci $ciopts "${msg+-m$msg}" ./"$basename" \
					</dev/null >>/dev/null 2>&1 && {
						# successfully checked in
						files=y
						# try next
						continue
					}
				} || :
				# failed to check in
				echo "$progname: unable to check in file $file," \
					"skipping" 1>&2
				errors=y
				# try next
				continue
			;;
			*)
				echo "$progname: unexpected return/exit value: $? " \
					"from rcsdiff, skipping file $file" 1>&2
				errors=y
				# try next
				continue
			;;
		esac
	fi
done
# if we made it to here, we may have been fully successful
[ -z "$errors" ] &&
[ -n "$files" ] &&
rc=0
myexit
