#!/bin/sh
# sunrise-commit -- a helper script for Sunrise commiters.
# (c) 2010 Michał Górny <gentoo@mgorny.alt.pl>
# Released under the terms of the 3-clause BSD license.

# Few output helpers.
say() {
	echo "${@}" >&2
}

die() {
	say "${RED}${@}${RESET}"
	exit 1
}

sayv() {
	[ -n "${SC_VERBOSE}" ] && say "${GREEN}${@}${RESET}"
}

req() {
	"$@" || die "$@ failed."
}

# POSIX compat.
local_supported() {
	local test 2>/dev/null
}

local_supported || eval 'local() {
	unset ${@}
}'

# See if we're in a repo, and what VCS are we using.
find_repo() {
	svn info >/dev/null 2>&1

	if [ ${?} -eq 0 ]; then
		SC_VCS=svn
	else
		local remotes
		remotes=$(git branch -r 2>/dev/null)

		if [ ${?} -ne 127 -a ${?} -ne 128 ]; then
			echo "${remotes}" | grep git-svn >/dev/null 2>&1
			if [ ${?} -eq 0 ]; then
				SC_VCS=git-svn
			else
				SC_VCS=git
			fi
		else
			die 'Unable to find any familiar repository type (are you inside the repo?).'
		fi
	fi

	sayv "Ok, we're in the ${SC_VCS} working tree. Let's see what I can do around here..."
}

is_whole_dir_removed() {
	if [ ${SC_VCS} = svn ]; then
		local flist
		flist=$(find "${1}" \( -name '.svn' -prune -o ! -name "${1}" -print \))
		[ -z "${flist}" ]
	elif [ ${SC_VCS%-svn} = git ]; then
		[ ! -e "${1}" ]
	fi
}

# Check whether we're having a clean package removal.
is_package_removal() {
	local fields list
	[ -d profiles ] && fields=1-2 || fields=1

	if [ ${SC_VCS%-svn} = git ]; then
		list=$(git diff-index --relative --name-only --diff-filter=D HEAD)
	elif [ ${SC_VCS} = svn ]; then
		list=$(svn status -q | sed -n -e 's/^D       //p')
	fi
	list=$(echo "${list}" | cut -d / -f ${fields} | sort | uniq)

	# 1) We have to have any removes.
	[ -z "${list}" ] && return 1

	# Few more checks.
	local dir olist
	for dir in ${list}; do
		# 2) These removes have to remove whole directories.
		is_whole_dir_removed ${dir} && olist=${olist+${olist} }${dir}
	done

	[ -z "${olist}" ] && return 1

	SC_CHANGE_LIST=${olist}
	return 0
}

# Look around for ebuilds.
find_ebuilds() {
	# POSIX is fun -- look for ebuilds in the current directory.
	if [ -n "$(find \( -name '*.ebuild' -print -o ! -name '.' -prune \))" ]; then
		local stripped

		# Get CATEGORY and PN.
		stripped=${PWD%/*}
		stripped=${stripped%/*}
		SC_CP=${PWD#${stripped}/}

		SC_SCENARIO=ebuild-commit
		sayv "We found ebuilds for ${SC_CP} here."
	elif is_package_removal; then
		local cplist category pkg rootprefix
		# We can either have the category on the list or in PWD.
		if [ -d profiles ]; then
			category=
		else
			local stripped
			stripped=${PWD%/*}
			category=${PWD#${stripped}/}/
		fi
		
		SC_CP=
		# Now we can have multiple packages around.
		for pkg in ${SC_CHANGE_LIST}; do
			if [ -z "${category}" ]; then
				case ${pkg} in
					eclass/*|licenses/*|local/*|profiles/*|scripts/*)
						continue
						;;
				esac
			fi
			SC_CP=${SC_CP:+${SC_CP}, }${category}${pkg}
		done

		local root sdir
		root=${category:+../}

		for sdir in eclass licenses profiles; do
			check_for_changes ${root}${sdir} >/dev/null && SC_CHANGE_LIST="${SC_CHANGE_LIST} ${root}${sdir}"
		done
		SC_SCENARIO=package-removal
		sayv "We're removing ${SC_CP}."
	else
		die 'No familiar scenario found.'
	fi
}

# VCS helpers.
check_for_changes() {
	local output

	if [ ${SC_VCS%-svn} = git ]; then
		output=$(git diff-index --name-only --relative HEAD "$@")
	elif [ ${SC_VCS} = svn ]; then
		output=$(svn status "$@")
	fi

	[ -z "${output}" ] && return 1
	# We do not care about user mangling ChangeLog, we will reset it anyway.
	echo "${output}" | grep -v ChangeLog >/dev/null
}

vcs_reset() {
	if [ ${SC_VCS%-svn} = git ]; then
		req git reset -q HEAD "${1}"
		git checkout -f "${1}" 2>/dev/null || req rm -f ChangeLog
	elif [ ${SC_VCS} = svn ]; then
		req rm -f ChangeLog
		svn up ChangeLog >/dev/null 2>&1
	fi
}

vcs_status() {
	if [ ${SC_VCS%-svn} = git ]; then
		git status -s ${1-.} "${@}"
	elif [ ${SC_VCS} = svn ]; then
		svn status "${@}"
	fi
}

vcs_add() {
	${SC_VCS%-svn} add "$@"
}

vcs_commit() {
	local msg
	msg=${1}
	shift

	if [ ${SC_VCS%-svn} = git ]; then
		exec git commit -m "${msg}" ${1+-o} "${@}"
	elif [ ${SC_VCS} = svn ]; then
		exec svn commit -m "${msg}" "${@}"
	fi
}

vcs_update() {
	# Unlike svn, git doesn't push the changes to origin immediately,
	# and that's why we don't force update to it right here.
	if [ ${SC_VCS} = svn ]; then
		svn up || say 'Warning: svn up failed, trying to proceed anyway.'
	fi
}

print_help() {
	cat <<_EOH_
Synopsis:
	sunrise-commit [options] [--] <commit message>

Options:
	-?, -h, --help		print this message,
	-V, --version		print version string,

	-c, --changelog		backwards compat (ignored),
	-C, --nocolor		disable colorful output,
	-d, --noupdate		disable updating the repository,
	-f, --force		force repoman to proceed with the commit,
	-m, --noformat		do not prepend the commit message with package names,
	-q, --quiet		backwards compat (ignored),
	-t, --trivial		trivial changes (do not add a ChangeLog entry),
	-v, --verbose		enable verbose output.
_EOH_
}

confirm() {
	while true; do
		local answ
		printf '%s' "${WHITE}Commit changes?${RESET} [${BGREEN}Yes${RESET}/${RED}No${RESET}] ${GREEN}" >&2
		read answ
		printf '%s' "${RESET}"

		case "${answ}" in
			[yY]|[yY][eE]|[yY][eE][sS])
				break
				;;
			[nN]|[nN][oO])
				die 'Aborting.'
				;;
			*)
				say "Your response '${answ}' not understood, try again."
		esac
	done
}

# Guess what!
main() {
	local commitmsg force monochrome noprepend noupdate trivial
	unset SC_VERBOSE

	while [ ${#} -gt 0 ]; do
		case "${1}" in
			--help|-\?|-h)
				print_help
				exit 0
				;;
			--version|-V)
				echo 'sunrise-commit 0.3'
				exit 0
				;;

			-c|--changelog)
				# Now changelog entries are implicit -- backwards compat.
				;;
			-C|--nocolor)
				monochrome=1
				;;
			-d|--noupdate)
				noupdate=1
				;;
			-f|--force)
				force=1
				;;
			-m|--noformat)
				noprepend=
				;;
			-q|--quiet)
				;;
			-t|--trivial)
				trivial=1
				;;
			-v|--verbose)
				SC_VERBOSE=1
				;;

			--)
				shift
				commitmsg=${commitmsg+${commitmsg} }${@}
				break
				;;
			-*)
				die "Unknown option: ${1}; see --help." >&2
				;;
			*)
				commitmsg=${commitmsg+${commitmsg} }${1}
				;;
		esac
		shift
	done

	if [ -n "${monochrome}" ]; then
		RESET=
		RED=
		GREEN=
		BGREEN=
		YELLOW=
		WHITE=
	else
		local esc
		esc=$(printf '\033[')

		RESET=${esc}0m
		RED="${esc}1;31m"
		GREEN=${esc}32m
		BGREEN="${esc}1;32m"
		YELLOW="${esc}1;33m"
		WHITE="${esc}1;37m"
	fi

	[ -n "${commitmsg}" ] || die 'No commit message provided.'

	find_repo
	find_ebuilds

	case ${SC_SCENARIO} in
		ebuild-commit)
			check_for_changes || die 'No changes found to commit.'

			# With native git repos, we do not do ChangeLogs by default...
			# ...at least unless they're already there.
			if [ ${SC_VCS} != git -o -f ChangeLog ]; then
				sayv 'Cleaning up the ChangeLog...'
				vcs_reset ChangeLog

				local bns
				# Let's take a lucky guess bugnumbers consist of 4+ digits.
				bns=$(echo "${commitmsg}" | grep -o -E '[0-9]{4,}')

				# Creating a new ChangeLog? Let's take a look at the commit message.
				if [ ! -f ChangeLog ]; then
					[ -n "${trivial}" ] && die "Trivial change for an initial commit? You're joking, right?"
					[ -z "${bns}" ] && die 'Please supply the bug number in the initial commit message!'
					if [ ! -f metadata.xml ]; then
						req cp ../../skel.metadata.xml metadata.xml
						# Output similar to echangelog.
						diff -dup /dev/null metadata.xml
						req vcs_add metadata.xml
					fi
				fi

				if [ -z "${trivial}" ]; then
					sayv '...and appending to it.'
					echangelog "${commitmsg}" || die 'Please correct the problems shown by echangelog.'
					echo
				fi

				if [ -n "${bns}" ]; then
					local bn cbn
					for bn in ${bns}; do
						cbn=#${WHITE}${bn}${NORMAL}
						wget -q http://bugs.gentoo.org/show_bug.cgi?id=${bn} -O - | sed -n \
							-e "s, *<title>Gentoo Bug \([0-9]*\) - \(.*\)</title>,Bug ${cbn}: ${BGREEN}\2${RESET},p" \
							-e "s, *<title>Gentoo \(Invalid Bug ID\)</title>,Bug ${cbn}: ${YELLOW}!! \1${RESET},p"
					done
					echo
				fi
			fi

			vcs_status
			echo

			# Do we have repoman new enough?
			local old_repoman
			repoman --version -a >/dev/null 2>&1
			if [ $? -eq 2 ]; then
				old_repoman=

				#say "${GREEN}Please consider updating portage to newer version.${RESET}"
				#say
				req repoman manifest
				if ! repoman full; then
					[ -n "${force}" ] || die 'Please correct the problems shown by repoman.'
				fi
				confirm
			fi

			if [ -n "${noupdate}" ]; then
				sayv "Updating the repository..."
				vcs_update
			fi

			sayv "Now, let's let repoman do its job..."
			exec repoman commit ${old_repoman--a} ${force+-f} -m "${noprepend-${SC_CP}: }${commitmsg}"
			;;
		package-removal)
			vcs_status ${SC_CHANGE_LIST}
			echo
			say "Ready to commit ${WHITE}$(echo ${SC_CHANGE_LIST} | wc -w)${RESET} package removal(s), with commit message:"
			say "${BGREEN}${SC_CP}: ${commitmsg}${RESET}"
			confirm

			if [ -n "${noupdate}" ]; then
				sayv "Updating the repository..."
				vcs_update ${SC_CHANGE_LIST}
			fi

			vcs_commit "${noprepend-${SC_CP}: }${commitmsg}" ${SC_CHANGE_LIST}
			;;
	esac
}

main "${@}"