#!/bin/bash -e

VCS_SUPPORTED="cvs svn git local"
autoDetectVCS()
{
	if [ -d "CVS" ]; then
		echo -n "cvs"
	elif svn info >/dev/null 2>&1; then
		echo -n "svn"
	elif [ -d ".git" ]; then
		echo -n "git"
	else
		echo -n "local"
	fi
}

COMMANDS="check_installation path version update check_sync diff add commit tag branch changelog news checkout push_all"

# Options
PATH="${PATH}:$(dirname $0):/usr/local/share/ui-auto:/usr/share/ui-auto"
. ui-libopt.sh

ui_opt_init "Unified version control tool for ui-auto tasks." \
	"This tool autodetects the used versioning control system and
unifies a selection of common tasks needed by ui-auto
tools. This is not a full featured version control tool.

VC paths have the common syntax 'LOCATION/TAG' (i.e., TAG is
after the last literal '/' and must not contain '/' itself).
This string is interpreted differently for the different VC
systems:

LOCAL(meta): 'LOCALPATH': Will just 'cp -a' locally.
     Example: '/home/user/src/myproject/'.
CVS: 'CVSROOT;MODULE/TAG': TAG is an CVS tag. Empty or
     TAG=HEAD is the same. ';' is separator and must not be
     used elsewhere.
     Example: 'cvs.myhost.org:/my/repo/path/repo;myproject/MYPROJECT_1_2_3'.
SVN: 'SVNURL/TAG': TAG just adds to the svn path (a svnpath alone is unique).
     Example: 'svn://svn.myhost.org/my/repo/path/tags/myproject/MYPROJECT_1_2_3'
GIT: 'GITURL/TAG': TAG is a git branch. If empty, we stay on repo's default branch.
     Example: 'ssh://git.myhost.org/my/git/path/myprojec.git/MYPROJECT_1_2_3'"

ui_opt_add "s"  "Only print the autodetected vc system id."
ui_opt_add "S:" "Set VC System id arbitrarily. Supported: ${VCS_SUPPORTED}." "$(autoDetectVCS)"
ui_opt_add "m:" "Message for this action." "ui-auto-uvc: No user message." "" \
	"Note that it depends on the used UVC command and the VC used how and if this is used."

ui_opt_addPos CMD "UVC command: ${COMMANDS}." "" "\
* check_installation    : Check whether version control tools are installed.
* path                  : Print current working directory's VC path.
* version               : Print a VC version string (suitable for snapshots).
* update                : Update working directory w/ server.
* check_sync            : Check that working directory has no uncommitted changes and is up-to-date w/ server (where applicable).
* diff FILE             : Display diff for file.
* add FILES             : Add files to version control.
* commit FILES          : Check-in FILES (-m may be used).
* tag NAME [DEST]       : Tag repository using NAME (use DEST for VCSes that copy for tags).
* branch NAME [DEST]    : Branch repository using NAME (use DEST for VCSes that copy for branches).
* changelog             : Generate ChangeLog from the VCSes logs.
* news SINCE            : Generate NEWS text (first line of VC checkins).
* checkout VCPATH DIR   : Checkout a from VC path to DIR (you need to explicitly set VCS via -S).
                          See above for the syntax of VCPATH.
* push_all              : Try to push any local changes and tags found in any branches (noop for non git)."

ui_opt_addPos OPT1 "Option 1 for command."
ui_opt_addPos OPT2 "Option 2 for command."
ui_opt_parse "$@"

# Check whether command exists (error exit), and is defined for the VC we use
vcHas()
{
	local c="${1}"
	local k
	for k in ${COMMANDS}; do
		if [ "${k}" = "${c}" ] && [ "$(type -t ${VCS}_cmd_${1})" = "function" ]; then
			return 0
		fi
	done
	ui_opt_error "Unknown command: ${c}"
}

checkOpts()
{
	local n
	for n in ${1}; do
		ui_opt_getPos ${n} >/dev/null
	done
}


#
# CVS
#
cvs_cmd_check_installation()
{
	ui_check_installed "cvs --version"
	ui_check_installed "/usr/bin/cvs2cl --version" warnonly
}

cvs_cmd_version()
{
	# Just use a timestamp for CVS
	echo "${VCS}$(date --utc +%Y%m%d%H%M%S)"
}

cvs_cmd_path()
{
	local cvsroot=$(cat CVS/Root)
	local module=$(cat CVS/Repository)
	# Tag: We must strip first char
	local tag=$(cat CVS/Tag 2>/dev/null)
	tag=${tag:1}

	echo "${cvsroot};${module}/${tag}"
}

cvs_cmd_update()
{
	cvs update
}

cvs_cmd_check_sync()
{
	if cvs -n update 2>&1 | grep "^[ARMCUP] "; then
		ui_opt_error "${VCS} not in sync with server; please resolve above discrepancies" QUIET
	fi
}

cvs_cmd_diff()
{
	checkOpts "1"
	cvs diff $(ui_opt_getPos 1)
}

cvs_cmd_add()
{
	checkOpts "1"
	cvs add $(ui_opt_getPos 1)
}

cvs_cmd_commit()
{
	checkOpts "1"
	cvs commit -m"$(ui_opt_get m)" $(ui_opt_getPos 1)
}

cvs_cmd_tag()
{
	checkOpts "1"
	cvs tag $(ui_opt_getPos 1)
}

cvs_cmd_branch()
{
	checkOpts "1"
	cvs tag -b $(ui_opt_getPos 1)
}

cvs_cmd_changelog()
{
	ui_run_alt "/usr/bin/cvs2cl" "cvs log"
}

# SYNTAX: 'CVSROOT;CVSMODULE/TAG"
cvs_cmd_checkout()
{
	checkOpts "1 2"

	local vcpath=$(ui_opt_getPos 1)
	local dir=$(ui_opt_getPos 2)

	# Parse root, module and tag
	local root=$(echo "${vcpath}" | cut -d";" -f1)
	local rest=$(echo "${vcpath}" | cut -d";" -f2- | rev)
	local module=$(echo "${rest}" | cut -d"/" -f2- | rev)
	local tag=$(echo "${rest}" | cut -d"/" -f1 | rev)

	local tagoption=""
	[ -z "${tag}" ] || tagoption="-r ${tag}"

	cvs -d "${root}" checkout -d "${dir}" ${tagoption} "${module}"
}

cvs_cmd_push_all() { :; }

#
# SVN
#
svn_cmd_check_installation()
{
	ui_check_installed "svn --version"
	ui_check_installed "/usr/bin/svn2cl --version" warnonly
}

svn_cmd_version()
{
	echo "svn$(svnversion)"
}

svn_cmd_path()
{
	local url=$(svn info . | grep -i "^URL:" | cut -d" " -f2-)
	# We don't care about /TAG, it just adds to the path anyway for svn
	echo "${url}"
}

svn_cmd_update()
{
	svn update
}

svn_cmd_check_sync()
{
	local svn_stat=$(svn status --show-updates --quiet | grep --invert-match "^Status " || true)
	if [ -n "${svn_stat}" ]; then
		echo "${svn_stat}"
		ui_opt_error "${VCS} not in sync with server; please resolve above discrepancies" QUIET
	fi
}

svn_cmd_diff()
{
	checkOpts "1"
	svn diff $(ui_opt_getPos 1)
}

svn_cmd_add()
{
	checkOpts "1"
	svn add $(ui_opt_getPos 1)
}

svn_cmd_commit()
{
	checkOpts "1"
	svn commit -m"$(ui_opt_get m)" $(ui_opt_getPos 1)
}

svn_cmd_tag()
{
	checkOpts "1 2"
	local tag="$(ui_opt_getPos 1)"
	local dest="$(ui_opt_getPos 2)"
	svn copy --parents -m"$(ui_opt_get m)" "$(ui-auto-uvc path)" "${dest}/${tag}"
}

svn_cmd_branch()
{
	svn_cmd_tag
}

svn_cmd_changelog()
{
	ui_run_alt "/usr/bin/svn2cl" "svn log"
}

# svn does not support shortlog, or decent custom formatting of
# the logs.
# 'filter_nice' is a (ugly) parse-hack that tries to get very
# close to what got shortlog does.
svn_cmd_news()
{
	filter_ugly()
	{
		sed 's/^-\+/ * SVN commit/'
	}

	filter_nice()
	{
		local -A logs logsNo
		# Skip initial "----" line
		read
		# Read line by line
		while read; do
			local header="${REPLY}"
			local first_line=""
			while read; do
				if printf "%s" "${REPLY}" | grep --quiet '^--------------------[-]*$'; then
					break;
				elif [ -z "${first_line}" ]; then
					first_line="${REPLY}"
				fi
			done
			local revision=$(cut --delimiter='|' -f1 <<< "${header}" | tr --delete '[:space:]')
			local user=$(cut --delimiter='|' -f2 <<< "${header}" | tr --delete '[:space:]')
			logs[${user}]+="$(printf "  * [%s] %s\\\n" "${revision}" "${first_line}" | fmt --width=64)"
			# Note: Setting from empty strings sets no to 0
			# Note: Using 'declare -i logsNo[${user}]' directly instead works for sid/jessie, but <=wheezy bash segfaults big time (glibc vs eglibc?)
			local -i no="${logsNo[${user}]}"
			no+=1
			logsNo[${user}]="${no}"
		done
		local user
		for user in "${!logs[@]}"; do
			printf "%s (%s):\n" "${user}" "${logsNo[${user}]}"
			printf "%b\n" "${logs[${user}]}"
		done
	}

	checkOpts "1"
	svn log --revision "$(ui_opt_getPos 1):HEAD" | filter_nice
}

# SYNTAX: SVNURL/TAG
svn_cmd_checkout()
{
	checkOpts "1 2"

	local vcpath=$(ui_opt_getPos 1)
	local dir=$(ui_opt_getPos 2)
	svn checkout "${vcpath}" "${dir}"
}

svn_cmd_push_all() { :; }

#
# GIT
#

# Get the branch we are currently on
gitGetCurrentBranch()
{
	git branch | grep "^\*" | cut -d ' ' -f2-
}

git_cmd_check_installation()
{
	ui_check_installed "git --version"
	ui_check_installed "/usr/share/gnulib/build-aux/gitlog-to-changelog --version" warnonly
}

git_cmd_version()
{
	git show --pretty="${VCS}%h" ${2} | head -1
}

git_cmd_path()
{
	echo "$(git config remote.origin.url)/$(gitGetCurrentBranch)"
}

git_cmd_update()
{
	git pull
}

git_cmd_check_sync()
{
	# Check for local changes or extra files
	local lchanges=$(git ls-files --exclude-standard --modified --others)
	if [ -n "${lchanges}" ]; then
		echo "${lchanges}" >&2
		ui_opt_error "${VCS} repo has local modifications or untracked files" QUIET
	fi

	# Check for uncommited changes in staging area
	if ! git diff --exit-code --cached; then
		ui_opt_error "You have uncommited changes in staging; please commit first." QUIET
	fi

	# Check if corresponding remote tip equals our tip
	git fetch
	local branch=$(gitGetCurrentBranch)
	if ! git diff --exit-code origin/${branch}; then
		ui_opt_error "Your local branch ${branch} differs from origin/${branch}" QUIET
	fi
}

git_cmd_diff()
{
	checkOpts "1"
	git diff $(ui_opt_getPos 1)
}

git_cmd_add()
{
	checkOpts "1"
	git add $(ui_opt_getPos 1)
}

git_cmd_commit()
{
	checkOpts "1"
	git commit -m"$(ui_opt_get m)" $(ui_opt_getPos 1) && git push origin HEAD
}

git_cmd_tag()
{
	checkOpts "1"
	local tag=$(ui_opt_getPos 1)
	git tag "${tag}" && git push origin "${tag}"
}

git_cmd_branch()
{
	checkOpts "1"
	local branch=$(ui_opt_getPos 1)
	git branch "${branch}" && git push origin "${branch}"
}

git_cmd_changelog()
{
	ui_run_alt "/usr/share/gnulib/build-aux/gitlog-to-changelog" "git log --pretty --numstat --summary"
}

git_cmd_news()
{
	checkOpts "1"
	git shortlog -w64,2,4 --format="* [%h] %s" "$(ui_opt_getPos 1)..HEAD"
}

# SYNTAX: REMOTEURL/BRANCH
git_cmd_checkout()
{
	checkOpts "1 2"

	local vcpath=$(ui_opt_getPos 1)
	local dir=$(ui_opt_getPos 2)

	# Parse root, module and tag
	local remote=$(echo "${vcpath}" | rev | cut -d"/" -f2- | rev)
	local branch=$(echo "${vcpath}" | rev | cut -d"/" -f1 | rev)

	git clone --branch="${branch}" "${remote}" "${dir}"

	# Create same-name local branches for all (other) remote branches
	# Some tools (like gbp) actually need this to work properly
	(
		cd "${dir}"
		local b
		for b in $(git branch -r | grep --invert-match --regexp=HEAD --regexp=${branch}); do
			git branch --track "${b##*/}" "${b}"
		done
	)
}

git_cmd_push_all()
{
	git push --all origin
	git push --tags
}

#
# LOCAL (meta VC)
#
local_cmd_check_installation()
{
	return 0
}

local_cmd_version()
{
	date --utc +%Y%m%d%H%M%S
}

local_cmd_path()
{
	pwd
}

local_cmd_checkout()
{
	checkOpts "1 2"

	local src=$(ui_opt_getPos 1)
	local dest=$(ui_opt_getPos 2)
	if [ -z "${src}" ]; then
		ui_opt_error "Empty source directory" QUIET
	fi
	cp -a "${src}/." "${dest}"
}

local_cmd_push_all() { :; }

#
# Start processing
#

# Set VC system to use
VCS=$(ui_opt_get S)

# Handle special options
if ui_opt_given s; then
	# Print vc system id only
	echo -n "${VCS}"
	exit 0
fi

# Run the given command
CMD=$(ui_opt_getPos 0)
if vcHas ${CMD}; then
	${VCS}_cmd_${CMD}
else
	echo "W: VC(${VCS}): \"${CMD}\": Not implemented (skipping)." >&2
fi

exit 0
