#!/bin/bash

# Copyright (c) 2012 dak180
# See http://opensource.org/licenses/bsd-license.php for licence terms
#
# autorevision - extracts metadata about the head version from your repository.

# Usage message.
function arUsage {
	cat > "/dev/stderr" << EOF
usage: ./autorevision {-t output-type | -s symbol} [-o cache-file [-f] ] [-V]
	Options include:
	-t output-type		= specify output type
	-s symbol		= specify symbol output
	-o cache-file		= specify cache file location
	-f			= force the use of cache data
	-V			= emit version and exit
	-?			= help message

The folowing are valid output types:
	h			= Header for use with c/c++
	xcode			= Header useful for populating info.plist files
	sh			= Bash sytax
	py			= Python file
	pl			= Perl file
	lua			= Lua file
	php			= PHP file
	ini			= INI file
	js			= javascript file
	java			= Java file
	javaprop		= Java properties file

The following are valid symbols:
	VCS_TYPE
	VCS_BASENAME
	VCS_NUM
	VCS_DATE
	VCS_BRANCH
	VCS_TAG
	VCS_TICK
	VCS_FULL_HASH
	VCS_SHORT_HASH
	VCS_WC_MODIFIED
EOF
	exit 1
}

# Config
ARVERSION="1.1"
TARGETFILE="/dev/stdout"
while getopts ":t:o:s:Vf" OPTION; do
	case "${OPTION}" in
		t)
			AFILETYPE="${OPTARG}"
		;;
		o)
			CACHEFILE="${OPTARG}"
		;;
		f)
			CACHEFORCE="1"
		;;
		s)
			VAROUT="${OPTARG}"
		;;
		V)
			echo "autorevision ${ARVERSION}"
			exit 0
		;;
		?)
			# If an unknown flag is used (or -?):
			arUsage
		;;
	esac
done

if [[ ! -z "${VAROUT}" ]] && [[ ! -z "${AFILETYPE}" ]]; then
	# If both -s and -t are specified:
	echo "error: Improper argument combination." 1>&2
	exit 1
elif [[ -z "${VAROUT}" ]] && [[ -z "${AFILETYPE}" ]]; then
	# If neither -s or -t are specified:
	arUsage
elif [[ -z "${CACHEFILE}" ]] && [[ "${CACHEFORCE}" = "1" ]]; then
	# If -f is specified without -o:
	arUsage
fi


# Functions to extract data from different repo types.
# For git repos
function gitRepo {
	cd "$(git rev-parse --show-toplevel)"

	VCS_TYPE="git"

	VCS_BASENAME="$(basename "${PWD}")"

	# Is the working copy clean?
	git diff --quiet HEAD &> /dev/null
	VCS_WC_MODIFIED="${?}"

	# Enumeration of changesets
	VCS_NUM="$(git rev-list --count HEAD 2>/dev/null)"
	if [[ -z "${VCS_NUM}" ]]; then
		echo "warning: Counting the number of revisions may be slower due to an outdated git version less than 1.7.2.3. If something breaks, please update it." 1>&2
		VCS_NUM="$(git rev-list HEAD | wc -l)"
	fi

	# The full revision hash
	VCS_FULL_HASH="$(git rev-parse HEAD)"

	# The short hash
	VCS_SHORT_HASH="$(echo "${VCS_FULL_HASH}" | cut -b 1-7)"

	# Current branch
	VCS_BRANCH="$(git rev-parse --symbolic-full-name --verify "$(git name-rev --name-only --no-undefined HEAD 2>/dev/null)" 2>/dev/null | sed -e 's:refs/heads/::' | sed -e 's:refs/::')"

	# Cache the description
	DESCRIPTION="$(git describe --long --tags 2>/dev/null)"

	# Current or last tag ancestor (empty if no tags)
	VCS_TAG="$(echo "${DESCRIPTION}" | sed -e "s:-g${VCS_SHORT_HASH}\$::" | sed -e 's:-[0-9]*$::')"

	# Distance to last tag or an alias of VCS_NUM if there is no tag
	if [[ ! -z "${DESCRIPTION}" ]]; then
		VCS_TICK="$(echo "${DESCRIPTION}" | sed -e "s:${VCS_TAG}-::" -e "s:-g${VCS_SHORT_HASH}::")"
	else
		VCS_TICK="${VCS_NUM}"
	fi

	# Date of the current commit
	VCS_DATE="$(git log -1 --pretty=format:%ci | sed -e 's: :T:' | sed -e 's: ::')"
}

# For hg repos
function hgRepo {
	cd "$(hg root)"

	VCS_TYPE="hg"

	VCS_BASENAME="$(basename "${PWD}")"

	# Is the working copy clean?
	hg sum | grep -q 'commit: (clean)'
	VCS_WC_MODIFIED="${?}"

	# Enumeration of changesets
	VCS_NUM="$(hg id -n | tr -d '+')"

	# The full revision hash
	VCS_FULL_HASH="$(hg log -r "${VCS_NUM}" -l 1 --template '{node}\n')"

	# The short hash
	VCS_SHORT_HASH="$(hg id -i | tr -d '+')"

	# Current bookmark (bookmarks are roughly equivalent to git's branches)
	# or branch if no bookmark
	VCS_BRANCH="$(hg id -B | cut -d ' ' -f 1)"
	# Fall back to the branch if there are no bookmarks
	if [[ -z "${VCS_BRANCH}" ]]; then
		VCS_BRANCH="$(hg id -b)"
	fi

	# Current or last tag ancestor (excluding auto tags, empty if no tags)
	VCS_TAG="$(hg log -r "${VCS_NUM}" -l 1 --template '{latesttag}\n' 2>/dev/null | sed -e 's:qtip::' -e 's:tip::' -e 's:qbase::' -e 's:qparent::' -e "s:$(hg --config 'extensions.color=' --color never qtop 2>/dev/null)::" | cut -d ' ' -f 1)"

	# Distance to last tag or an alias of VCS_NUM if there is no tag
	if [[ ! -z "${VCS_TAG}" ]]; then
		VCS_TICK="$(hg log -r "${VCS_NUM}" -l 1 --template '{latesttagdistance}\n' 2>/dev/null)"
	else
		VCS_TICK="${VCS_NUM}"
	fi

	# Date of the current commit
	VCS_DATE="$(hg log -r "${VCS_NUM}" -l 1 --template '{date|isodatesec}\n' 2>/dev/null | sed -e 's: :T:' | sed -e 's: ::')"
}

# For bzr repos
function bzrRepo {
	cd "$(bzr root)"

	VCS_TYPE="bzr"

	VCS_BASENAME="$(basename "${PWD}")"

	# Is the working copy clean?
	bzr version-info --custom --template='{clean}\n' | grep -q '1'
	VCS_WC_MODIFIED="${?}"

	# Enumeration of changesets
	VCS_NUM="$(bzr revno)"

	# The full revision hash
	VCS_FULL_HASH="$(bzr version-info --custom --template='{revision_id}\n')"

	# The short hash
	VCS_SHORT_HASH="$VCS_NUM"

	# Nick of the current branch
	VCS_BRANCH="$(bzr nick)"

	# Current or last tag ancestor (excluding auto tags, empty if no tags)
	VCS_TAG="$(bzr tags --sort=time | sed '/?$/d' | tail -n1 | cut -d ' ' -f1)"

	# Distance to last tag or an alias of VCS_NUM if there is no tag
	if [[ ! -z "${VCS_TAG}" ]]; then
		VCS_TICK="$(bzr log --line -r "tag:${VCS_TAG}.." | tail -n +2 | wc -l | sed -e 's:^ *::')"
	else
		VCS_TICK="${VCS_NUM}"
	fi

	# Date of the current commit
	VCS_DATE="$(bzr version-info --custom --template='{date}\n' | sed -e 's: :T:' | sed -e 's: ::')"
}

# For svn repos
function svnRepo {
	VCS_TYPE="svn"

	case "${PWD}" in
	/*trunk*|/*branches*|/*tags*)
		fn="${PWD}"
		while [[ "$(basename "${fn}")" != 'trunk' ]] && [[ "$(basename "${fn}")" != 'branches' ]] && [[ "$(basename "${fn}")" != 'tags' ]] && [[ "$(basename "${fn}")" != '/' ]]; do
			fn="$(dirname "${fn}")"
		done
		fn="$(dirname "${fn}")"
		if [[ "${fn}" == '/' ]]; then
			VCS_BASENAME="$(basename "${PWD}")"
		else
			VCS_BASENAME="$(basename "${fn}")"
		fi
		;;
	*) VCS_BASENAME="$(basename "${PWD}")" ;;
	esac

	# Cache svnversion output
	SVNVERSION="$(svnversion)"

	# Is the working copy clean?
	echo "${SVNVERSION}" | grep -q "M"
	case "${?}" in
		0) VCS_WC_MODIFIED="1";;
		1) VCS_WC_MODIFIED="0";;
	esac

	# Enumeration of changesets
	VCS_NUM="$(echo "${SVNVERSION}" | cut -d : -f 1 | sed -e 's:M::' -e 's:S::' -e 's:P::')"

	# The full revision hash
	VCS_FULL_HASH="${SVNVERSION}"

	# The short hash
	VCS_SHORT_HASH="$VCS_NUM"

	# Current branch
	case "${PWD}" in
	/*trunk*|/*branches*|/*tags*)
		lastbase=""
		fn="${PWD}"
		while :
		do
			base="$(basename "${fn}")"
			if [[ "$base" = 'trunk' ]]; then
				VCS_BRANCH='trunk'
				break
			elif [[ "${base}" = 'branches' ]] || [[ "${base}" = 'tags' ]]; then
				VCS_BRANCH="${lastbase}"
				break
			elif [[ "${base}" = '/' ]]; then
				VCS_BRANCH=""
				break
			fi
			lastbase="${base}"
			fn="$(dirname "${fn}")"
		done
		;;
	*) VCS_BRANCH="" ;;
	esac

	# Current or last tag ancestor (empty if no tags). But "current tag"
	# can't be extracted reliably because Subversion doesn't have tags the
	# way other VCSes do.
	VCS_TAG=""
	VCS_TICK=""

	# Date of the current commit
	VCS_DATE="$(svn info | sed -n -e 's:Last Changed Date\: ::p' | sed 's: (.*)::' | sed -e 's: :T:' | sed -e 's: ::')"
}


# Functions to output data in different formats.
# For header output
function hOutput {
	cat > "${TARGETFILE}" << EOF
/* Generated by autorevision - do not hand-hack! */
#ifndef AUTOREVISION_H
#define AUTOREVISION_H

#define VCS_TYPE		"${VCS_TYPE}"
#define VCS_BASENAME	"${VCS_BASENAME}"
#define VCS_NUM			${VCS_NUM}
#define VCS_DATE		"${VCS_DATE}"
#define VCS_BRANCH		"${VCS_BRANCH}"
#define VCS_TAG			"${VCS_TAG}"
#define VCS_TICK		${VCS_TICK}

#define VCS_FULL_HASH		"${VCS_FULL_HASH}"
#define VCS_SHORT_HASH		"${VCS_SHORT_HASH}"

#define VCS_WC_MODIFIED		${VCS_WC_MODIFIED}

#endif

/* end */
EOF
}

# A header output for use with xcode to populate info.plist strings
function xcodeOutput {
	cat > "${TARGETFILE}" << EOF
/* Generated by autorevision - do not hand-hack! */
#ifndef AUTOREVISION_H
#define AUTOREVISION_H

#define VCS_TYPE		${VCS_TYPE}
#define VCS_BASENAME	${VCS_BASENAME}
#define VCS_NUM			${VCS_NUM}
#define VCS_DATE		${VCS_DATE}
#define VCS_BRANCH		${VCS_BRANCH}
#define VCS_TAG			${VCS_TAG}
#define VCS_TICK		${VCS_TICK}

#define VCS_FULL_HASH		${VCS_FULL_HASH}
#define VCS_SHORT_HASH		${VCS_SHORT_HASH}

#define VCS_WC_MODIFIED		${VCS_WC_MODIFIED}

#endif

/* end */
EOF
}

# For bash output
function shOutput {
	cat > "${TARGETFILE}" << EOF
# Generated by autorevision - do not hand-hack!

VCS_TYPE="${VCS_TYPE}"
VCS_BASENAME="${VCS_BASENAME}"
VCS_NUM=${VCS_NUM}
VCS_DATE="${VCS_DATE}"
VCS_BRANCH="${VCS_BRANCH}"
VCS_TAG="${VCS_TAG}"
VCS_TICK=${VCS_TICK}

VCS_FULL_HASH="${VCS_FULL_HASH}"
VCS_SHORT_HASH="${VCS_SHORT_HASH}"

VCS_WC_MODIFIED=${VCS_WC_MODIFIED}

# end
EOF
}

# For Python output
function pyOutput {
	case "${VCS_WC_MODIFIED}" in
		0) VCS_WC_MODIFIED="False" ;;
		1) VCS_WC_MODIFIED="True" ;;
	esac
	cat > "${TARGETFILE}" << EOF
# Generated by autorevision - do not hand-hack!

VCS_TYPE = "${VCS_TYPE}"
VCS_BASENAME = "${VCS_BASENAME}"
VCS_NUM = ${VCS_NUM}
VCS_DATE = "${VCS_DATE}"
VCS_BRANCH = "${VCS_BRANCH}"
VCS_TAG = "${VCS_TAG}"
VCS_TICK = ${VCS_TICK}

VCS_FULL_HASH = "${VCS_FULL_HASH}"
VCS_SHORT_HASH = "${VCS_SHORT_HASH}"

VCS_WC_MODIFIED = ${VCS_WC_MODIFIED}

# end
EOF
}

# For Perl output
function plOutput {
	cat << EOF
# Generated by autorevision - do not hand-hack!

\$VCS_TYPE = "${VCS_TYPE}";
\$VCS_BASENAME = "${VCS_BASENAME}"
\$VCS_NUM = ${VCS_NUM};
\$VCS_DATE = "${VCS_DATE}";
\$VCS_BRANCH = "${VCS_BRANCH}";
\$VCS_TAG = "${VCS_TAG}";
\$VCS_TICK = ${VCS_TICK};

\$VCS_FULL_HASH = "${VCS_FULL_HASH}";
\$VCS_SHORT_HASH = "${VCS_SHORT_HASH}";

\$VCS_WC_MODIFIED = ${VCS_WC_MODIFIED};

# end
EOF
}

# For lua output
function luaOutput {
	case "${VCS_WC_MODIFIED}" in
		0) VCS_WC_MODIFIED="false" ;;
		1) VCS_WC_MODIFIED="true" ;;
	esac
	cat > "${TARGETFILE}" << EOF
-- Generated by autorevision - do not hand-hack!

VCS_TYPE = "${VCS_TYPE}"
VCS_BASENAME = "${VCS_BASENAME}"
VCS_NUM = ${VCS_NUM}
VCS_DATE = "${VCS_DATE}"
VCS_BRANCH = "${VCS_BRANCH}"
VCS_TAG = "${VCS_TAG}"
VCS_TICK = ${VCS_TICK}

VCS_FULL_HASH = "${VCS_FULL_HASH}"
VCS_SHORT_HASH = "${VCS_SHORT_HASH}"

VCS_WC_MODIFIED = ${VCS_WC_MODIFIED}

-- end
EOF
}

# For php output
function phpOutput {
	case "${VCS_WC_MODIFIED}" in
		0) VCS_WC_MODIFIED="false" ;;
		1) VCS_WC_MODIFIED="true" ;;
	esac
	cat > "${TARGETFILE}" << EOF
<?php
# Generated by autorevision - do not hand-hack!

$GLOBALS["VCS_TYPE"] = "${VCS_TYPE}";
$GLOBALS["VCS_BASENAME"] = "${VCS_BASENAME}";
$GLOBALS["VCS_NUM"] = ${VCS_NUM};
$GLOBALS["VCS_DATE"] = "${VCS_DATE}";
$GLOBALS["VCS_BRANCH"] = "${VCS_BRANCH}";
$GLOBALS["VCS_TAG"] = "${VCS_TAG}";
$GLOBALS["VCS_TICK"] = ${VCS_TICK};

$GLOBALS["VCS_FULL_HASH"] = "${VCS_FULL_HASH}";
$GLOBALS["VCS_SHORT_HASH"] = "${VCS_SHORT_HASH}";

$GLOBALS["VCS_WC_MODIFIED"] = ${VCS_WC_MODIFIED};

# end
?>
EOF
}

# For ini output
function iniOutput {
	case "${VCS_WC_MODIFIED}" in
		0) VCS_WC_MODIFIED="false" ;;
		1) VCS_WC_MODIFIED="true" ;;
	esac
	cat > "${TARGETFILE}" << EOF
; Generated by autorevision - do not hand-hack!
[VCS]
VCS_TYPE = "${VCS_TYPE}"
VCS_BASENAME = "${VCS_BASENAME}"
VCS_NUM = ${VCS_NUM}
VCS_DATE = "${VCS_DATE}"
VCS_BRANCH = "${VCS_BRANCH}"
VCS_TAG = "${VCS_TAG}"
VCS_TICK = ${VCS_TICK}
VCS_FULL_HASH = "${VCS_FULL_HASH}"
VCS_SHORT_HASH = "${VCS_SHORT_HASH}"
VCS_WC_MODIFIED = ${VCS_WC_MODIFIED}
; end
EOF
}

# For javascript output
function jsOutput {
	case "${VCS_WC_MODIFIED}" in
		1) VCS_WC_MODIFIED="true" ;;
		0) VCS_WC_MODIFIED="false" ;;
	esac
	cat > "${TARGETFILE}" << EOF
/** Generated by autorevision - do not hand-hack! */

var autorevision = {
	VCS_TYPE: "${VCS_TYPE}",
	VCS_BASENAME: "${VCS_BASENAME}",
	VCS_NUM: ${VCS_NUM},
	VCS_DATE: "${VCS_DATE}",
	VCS_BRANCH: "${VCS_BRANCH}",
	VCS_TAG: "${VCS_TAG}",
	VCS_TICK: ${VCS_TICK},

	VCS_FULL_HASH: "${VCS_FULL_HASH}",
	VCS_SHORT_HASH: "${VCS_SHORT_HASH}",

	VCS_WC_MODIFIED: ${VCS_WC_MODIFIED}
};

/** Node.js compatibility */
if (typeof module !== 'undefined') {
	module.exports = autorevision;
}

/** end */
EOF
}

# For Java output
function javaOutput {
	case "${VCS_WC_MODIFIED}" in
		1) VCS_WC_MODIFIED="true" ;;
		0) VCS_WC_MODIFIED="false" ;;
	esac
	cat > "${TARGETFILE}" << EOF
/* Generated by autorevision - do not hand-hack! */

import java.util.Date;

public class autorevision {
    public static final String VCS_TYPE = "${VCS_TYPE}";
    public static final String VCS_BASENAME = "${VCS_BASENAME}";
    public static final long VCS_NUM = ${VCS_NUM};
    public static final Date VCS_DATE = new Date($(date -d ${VCS_DATE} +%s));
    public static final String VCS_BRANCH = "${VCS_BRANCH}";
    public static final String VCS_TAG = "${VCS_TAG}";
    public static final long VCS_TICK = ${VCS_TICK};

    public static final String VCS_FULL_HASH = "${VCS_FULL_HASH}";
    public static final String VCS_SHORT_HASH = "${VCS_SHORT_HASH}";

    public static final boolean VCS_WC_MODIFIED = ${VCS_WC_MODIFIED};
}
EOF
}

# For Java properties output
function javapropOutput {
	case "${VCS_WC_MODIFIED}" in
		1) VCS_WC_MODIFIED="true" ;;
		0) VCS_WC_MODIFIED="false" ;;
	esac
	cat > "${TARGETFILE}" << EOF
# Generated by autorevision - do not hand-hack!

VCS_TYPE=${VCS_TYPE}
VCS_BASENAME=${VCS_BASENAME}
VCS_NUM=${VCS_NUM}
VCS_DATE=${VCS_DATE}
VCS_BRANCH=${VCS_BRANCH}
VCS_TAG=${VCS_TAG}
VCS_TICK=${VCS_TICK}

VCS_FULL_HASH=${VCS_FULL_HASH}
VCS_SHORT_HASH=${VCS_SHORT_HASH}

VCS_WC_MODIFIED=${VCS_WC_MODIFIED}
EOF
}


# Detect and collect repo data.
if [[ -f "${CACHEFILE}" ]] && [[ "${CACHEFORCE}" = "1" ]]; then
	# When requested only read from the cache to populate our symbols.
	source "${CACHEFILE}"
elif [[ ! -z "$(git rev-parse HEAD 2>/dev/null)" ]]; then
	gitRepo
elif [[ ! -z "$(hg root 2>/dev/null)" ]]; then
	hgRepo
elif [[ ! -z "$(bzr root 2>/dev/null)" ]]; then
	bzrRepo
elif [[ ! -z "$(svn info 2>/dev/null)" ]]; then
	svnRepo
elif [[ -f "${CACHEFILE}" ]]; then
	# We are not in a repo; try to use a previously generated cache to populate our symbols.
	source "${CACHEFILE}"
else
	echo "error: No repo or cache detected." 1>&2
	exit 1
fi


# -s output is handled here.
if [[ ! -z "${VAROUT}" ]]; then
	if [[ "${VAROUT}" = "VCS_TYPE" ]]; then
		echo "${VCS_TYPE}"
	elif [[ "${VAROUT}" = "VCS_BASENAME" ]]; then
		echo "${VCS_BASENAME}"
	elif [[ "${VAROUT}" = "VCS_NUM" ]]; then
		echo "${VCS_NUM}"
	elif [[ "${VAROUT}" = "VCS_DATE" ]]; then
		echo "${VCS_DATE}"
	elif [[ "${VAROUT}" = "VCS_BRANCH" ]]; then
		echo "${VCS_BRANCH}"
	elif [[ "${VAROUT}" = "VCS_TAG" ]]; then
		echo "${VCS_TAG}"
	elif [[ "${VAROUT}" = "VCS_TICK" ]]; then
		echo "${VCS_TICK}"
	elif [[ "${VAROUT}" = "VCS_FULL_HASH" ]]; then
		echo "${VCS_FULL_HASH}"
	elif [[ "${VAROUT}" = "VCS_SHORT_HASH" ]]; then
		echo "${VCS_SHORT_HASH}"
	elif [[ "${VAROUT}" = "VCS_WC_MODIFIED" ]]; then
		echo "${VCS_WC_MODIFIED}"
	fi
fi


# Detect requested output type and use it.
if [[ ! -z "${AFILETYPE}" ]]; then
	if [[ "${AFILETYPE}" = "h" ]]; then
		hOutput
	elif [[ "${AFILETYPE}" = "xcode" ]]; then
		xcodeOutput
	elif [[ "${AFILETYPE}" = "sh" ]]; then
		shOutput
	elif [[ "${AFILETYPE}" = "py" ]]; then
		pyOutput
	elif [[ "${AFILETYPE}" = "pl" ]]; then
		plOutput
	elif [[ "${AFILETYPE}" = "lua" ]]; then
		luaOutput
	elif [[ "${AFILETYPE}" = "php" ]]; then
		phpOutput
	elif [[ "${AFILETYPE}" = "ini" ]]; then
		iniOutput
	elif [[ "${AFILETYPE}" = "js" ]]; then
		jsOutput
	elif [[ "${AFILETYPE}" = "java" ]]; then
		javaOutput
	elif [[ "${AFILETYPE}" = "javaprop" ]]; then
		javapropOutput
	else
		echo "error: Not a valid output type." 1>&2
		exit 1
	fi
fi


# If requested, make a cache file.
if [[ ! -z "${CACHEFILE}" ]] && [[ ! "${CACHEFORCE}" = "1" ]]; then
	TARGETFILE="${CACHEFILE}"
	shOutput
fi
