#!/bin/bash

usage()
{
	cat 1>&2 <<-EOF
		$(basename $0): construct a version string using git

		usage: $(basename $0) [--dirty] [ref=HEAD] [tag_pattern='[0-9]*']

		Applies the following logic:
		  * If ref is a conforming annotated tag (see below), returns the ref
		  * Otherwise, returns a conforming annotated tag attached to the ref
		  * Otherwise, returns [branch-g]<sha1>[-dirty]

		A conforming tag is one that matches <tag_pattern>.

		If multiple tags are elegible to describe the ref, uses the
		one that's the lowest when compared per-component (i.e.,
		given tags "1.2.3", "1.2.4", and "1.12" it will use 1.2.3).

		If --dirty is given, ref must be empty or HEAD.
	EOF
}

_lex_normalize()
{
	# Given a list of dot-separated versions, echo a two-column list
	# where the first column is equal to, eg., sprintf "%10s%10s%10s" x,
	# y, z for a tag x.y.z.  The columns are separated by '|'.  This
	# makes it possible to compare the versions by lexicographically
	# sorting on the first column.
	#
	# Returns via STDOUT

	local tag
	for tag in "$@"; do
		IFS="."
		local VC=( $tag )
		unset IFS

		local VH=$(printf "%10s" "${VC[@]}")

		echo "$VH|$tag"
	done
}

_earliest_version()
{
	# Given a list of dot-separated versions, return the earliest one.
	#
	# Returns via STDOUT

	_lex_normalize "$@" | LC_COLLATE=C sort -t '|' -k1,1 | head -n 1 | cut -d'|' -f 2
}

_eups_compat_version()
{
	# Make a string "EUPS compatible" by converting all / to ., and
	# removing anything other than alphanumerics, dot, dash, and
	# underscore.
	#
	# Returns via STDOUT

	echo "$@" | tr '/' '.' | tr -c -d '[0-9a-zA-z._\-]'
	echo
}

_get_default_branch()
{
	# Determine the default branch.
	# If in a clone, use the origin remote's HEAD as the default.
	# If not, look for "main" and then "master".
	# If none of those exist, this is likely a brand-new repo.
	# In that case, use the current (initial) branch.

	local origin_branch=$(git symbolic-ref refs/remotes/origin/HEAD 2>/dev/null)
	if [[ -n "$origin_branch" ]]; then
		basename "$origin_branch"
		return
	fi
	git show-ref -q --heads main && echo main && return
	git show-ref -q --heads master && echo master && return
	git branch --show-current
}

_get_best_branch()
{
	# Usage: _get_best_branch <ref> <sha1>
	#
	# If commit $1 is on the currently checked out branch,
	# return it as best.
	#
	# If it's on the default branch, return that as best.
	#
	# Otherwise, return the branch where $1 is the closest to the tip.
	#
	local REF="$1"
	local SHA1=$(git rev-parse "$REF^{}")

	if git show-ref -q --verify refs/heads/"$REF"; then
		echo "$REF"
		return
	fi

	local CUR_BRANCH=$(git symbolic-ref -q HEAD | sed 's,refs/heads/,,')

	# (*) the 'grep -v' bit is there since git branch --contains lists the detached
	# head as a "branch" containing the commit, eg:
	#
	#   [mjuric@lsst-dev A (81fdb1f...)]$ git branch --contains 81fdb1fde3 | cut -b 3-
	#   (detached from 81fdb1f)
	#   next
	#   next2
	local BRANCHES=$(git branch --contains $SHA1 | cut -b 3- | grep -v '^(')

	local default_branch=$(_get_default_branch)
	# is it on current branch?
	local _B
	for _B in ${BRANCHES[@]}; do
		case "$_B" in
			$CUR_BRANCH)	echo $CUR_BRANCH; return ;;
			# Don't immediately return, as we prefer current branch
			$default_branch) local _IN_DEFAULT=1 ;;
		esac
	done

	# Is it on default branch?
	if [[ $_IN_DEFAULT == 1 ]]; then
		echo $default_branch
		return
	fi

	# return the branch where this commit is closest to the tip
	for _B in ${BRANCHES[@]}; do
		echo $(git rev-list $SHA1..$_B | wc -l) $_B
	done | sort -n -k1,1 | head -n 1 | cut -d' ' -f 2
}

git_version()
{
	# See if there's a --dirty option
	if [[ $1 == --dirty ]]; then
		shift
		if [[ ! -z $1 && $1 != HEAD ]]; then
			echo "When --dirty is given, the first argument must be omitted or 'HEAD'"
			exit -1
		fi
		if [[ $(git describe --always --dirty=magicniznakprljavo) == *magicniznakprljavo ]]; then
			local DIRTY="dirty"
		fi
	fi

	local REF=${1:-HEAD}
	local TAG_PATTERN=${2:-'[0-9]*'}	# The pattern defining eligible tags

	# Check if this ref is already an annotated tag matching the tag pattern; if yes, we're done.
	if [[ "$REF" == $TAG_PATTERN && $(git cat-file -t "$REF" 2>/dev/null) == tag ]]; then
		_eups_compat_version "$REF"
		return
	fi

	# See if any annotated tags matching TAG_PATTERN point to $REF. If yes,
	# find the tag with the lowest version number (when versions are
	# interpreted as dot-delimited tuples):

	# get a list of annotated tags pointing to $REF
	TAGS=( $(for tag in $(git tag -l "$TAG_PATTERN" --points-at $(git rev-parse "$REF^{commit}") ); do [[ $(git cat-file -t $tag) == tag ]] && echo -n "$tag "; done) )
	local RES
	if [[ ! -z $TAGS ]]; then
		# Tag found. Use the tag as version
		RES=( "$(_earliest_version ${TAGS[@]})" $DIRTY )
	else
		# No annotated tags found.
		# Use the branch-gSHA1 notation

		local BRANCH=$(_get_best_branch "$REF")
		local default_branch=$(_get_default_branch)
		local ABBRSHA=$(git rev-parse --short=10 "$REF^{}")
		if [[ -z $BRANCH ]]; then
			# Commit is not on any branch (orphaned)
			RES=($ABBRSHA $DIRTY)
		elif [[ $BRANCH = $default_branch ]]; then
			local DESC=$(git describe --first-parent --match "$TAG_PATTERN" 2>/dev/null)
			if [[ -z $DESC ]]; then
				RES=($BRANCH g$ABBRSHA $DIRTY )
			else
				RES=($DESC $DIRTY )
			fi
		else
			RES=($BRANCH g$ABBRSHA $DIRTY )
		fi
	fi

	IFS="-" VERSION="${RES[*]}"
	unset IFS

	_eups_compat_version "$VERSION"
}

##################### ---- BEGIN UNIT TESTS ---- #####################

if [[ "$RUN_UNIT" == 1 ]]; then

	. "$EUPS_DIR/lib/bunit.sh"

	_git_create_repo()
	{
		mkdir $1
		cd $1
		git init > /dev/null
		git branch --show-current
	}

	_git_add_commit()
	{
		for _F in "$@"; do
			echo "$_F" >> "$_F"
		done
		git add "$@" > /dev/null
		git commit -a -m "Added files" > /dev/null
	}

	_ut_earliest_version()
	{
		ut1 "unit tests for _earliest_version()"

		ut "_earliest_version 1.1 1.1.0" == "1.1"
		ut "_earliest_version 1.1.0 1.1.0" == "1.1.0"
		ut "_earliest_version 1.2.3 1.2.2" == "1.2.2"
		ut "_earliest_version 1.2.2 1.2.3" == "1.2.2"

		ut "_earliest_version 1.2 1.2.3" == "1.2"
		ut "_earliest_version 1.2.2 1.2.3 1.3" == "1.2.2"

		ut "_earliest_version 1.2 1.2a" == "1.2"
		ut "_earliest_version 1.2.1 1.2a.2" == "1.2.1"
		ut "_earliest_version 1.2a.2 1.2a.1" == "1.2a.1"
		ut "_earliest_version 1.2a.2 1.2b.1" == "1.2a.2"

		echo
	}

	_ut_git_version()
	{
		# unit test for git_version()
		(
			set +e

			local D="$(mktemp -d -t unittest.XXXXX)"
			cd "$D"
			ut1 unit tests for git_version in $PWD

			(
				ut0 Version w/o no tag
				local default_branch=$(_git_create_repo A)
				cd A
				_git_add_commit README
				COMMIT0=$(git rev-parse --short=10 HEAD)
				_git_add_commit README
				ut "git_version --dirty" == "${default_branch}-g$(git rev-parse --short=10 HEAD)"

				ut0 Version on dirty commit
				echo "stuff" >> README
				ut "git_version --dirty" == "${default_branch}-g$(git rev-parse --short=10 HEAD)-dirty"
				git reset --hard -q

				ut0 Version for commit on branch
				git checkout -q -b next
				_git_add_commit next
				ut "git_version --dirty" == "next-g$(git rev-parse --short=10 HEAD)"

				ut0 Version for commit on two branches
				git checkout -q ${default_branch}
				ut "git_version --dirty" == "${default_branch}-g$(git rev-parse --short=10 HEAD)"

				ut0 "Version for commit on branch (2)"
				git checkout -q next
				git checkout -q -b next2
				_git_add_commit next2
				ut "git_version --dirty" == "next2-g$(git rev-parse --short=10 HEAD)"

				ut0 "Branch name with reserved characters"
				git checkout -q next
				git checkout -q -b a/b/c/d
				_git_add_commit next3
				ut "git_version --dirty" == "a.b.c.d-g$(git rev-parse --short=10 HEAD)"

				ut0 Prefer branch with closest tip
				git checkout -q --detach next2^
				ut "git_version --dirty" == "next-g$(git rev-parse --short=10 HEAD)"

				ut0 Version on dirty commit
				echo "stuff" >> README
				ut "git_version --dirty" == "next-g$(git rev-parse --short=10 HEAD)-dirty"
				git reset --hard -q

				ut0 Ver from branch name
				ut "git_version next" == "next-g$(git rev-parse --short=10 HEAD)"

				ut0 Ver from SHA1, non-tip
				ut "git_version $COMMIT0" == "${default_branch}-g$COMMIT0"

				ut0 Ver from SHA1, tip
				ut "git_version $(git rev-parse --short=10 a/b/c/d)" == "a.b.c.d-g$(git rev-parse --short=10 a/b/c/d)"

				ut0 "Ver from SHA1 on merge pt (HEAD is next2)"
				next2sha=$(git rev-parse --short=10 next2)
				abcdsha=$(git rev-parse --short=10 a/b/c/d)
				git checkout -q next2
				git merge a/b/c/d -m "Merged!" >/dev/null
				git checkout -q a/b/c/d
				git merge -q next2 >/dev/null
				git checkout -q next2
				ut "git_version $(git rev-parse --short=10 HEAD)" == "next2-g$(git rev-parse --short=10 HEAD)"

				ut0 "Ver from SHA1 on merge pt (HEAD is a/b/c/d)"
				git checkout -q a/b/c/d
				ut "git_version $(git rev-parse --short=10 HEAD)" == "a.b.c.d-g$(git rev-parse --short=10 HEAD)"

				ut0 "Ver from name on merge pt (HEAD is a/b/c/d)"
				ut "git_version next2" == "next2-g$(git rev-parse --short=10 HEAD)"

				# return the tree to pre-merged state
				git checkout -q next2
				git reset --hard $next2sha >/dev/null
				git checkout -q a/b/c/d
				git reset --hard $abcdsha >/dev/null


				ut0 Single non-annotated tag on HEAD
				git checkout -q ${default_branch}
				git tag 1.0
				ut "git_version --dirty" == "${default_branch}-g$(git rev-parse --short=10 HEAD)"

				ut0 Non-conforming annotated tag
				git tag -a w.2015.33 -m "Weekly 2015.33"
				ut "git_version --dirty" == "${default_branch}-g$(git rev-parse --short=10 HEAD)"

				ut0 Non-conforming annotated tag, on cmdline
				ut "git_version w.2015.33" == "${default_branch}-g$(git rev-parse --short=10 HEAD)"

				ut0 Annotated and non-annotated tags on HEAD
				git tag -a 2.0 -m "Version 2.0"
				ut "git_version --dirty" == "2.0"

				ut0 Non-conforming annotated tag, on cmdline
				ut "git_version w.2015.33" == "2.0"

				ut0 Added lower version tag on the same commit
				git tag -a 1.1 -m "Version 1.1"
				ut "git_version --dirty" == "1.1"

				ut0 Added higher version tag on the same commit
				git tag -a 2.1 -m "Version 2.1"
				ut "git_version --dirty" == "1.1"

				ut0 Requesting version for tag returns tag
				ut "git_version 2.1" == "2.1"

				ut0 Dirty commit on the tag
				echo "stuff" >> README
				ut "git_version --dirty" == "1.1-dirty"

				ut0 Commit away from the tag
				_git_add_commit newfile
				ut "git_version --dirty" == "1.1-1-g$(git rev-parse --short=7 HEAD)"

				ut0 Commit away from the tag
				_git_add_commit newfile2
				ut "git_version --dirty" == "1.1-2-g$(git rev-parse --short=7 HEAD)"

				ut0 Away w. non-conforming ann. tag
				git tag -a w.2015.34 -m "Weekly 2015.34"
				ut "git_version --dirty" == "1.1-2-g$(git rev-parse --short=7 HEAD)"

				ut0 Away w. non-conforming ann. tag on cmdline
				ut "git_version w.2015.34" == "1.1-2-g$(git rev-parse --short=7 HEAD)"

				ut0 Dirty commit away from the tag
				echo "stuff" >> newfile2
				ut "git_version --dirty" == "1.1-2-g$(git rev-parse --short=7 HEAD)-dirty"
				git reset --hard -q


				ut0 Tag on a side-branch
				git checkout -q next2
				_git_add_commit newfile2
				git tag -a 3.0 -m "Version 3.0"
				ut "git_version --dirty" == "3.0"

				ut0 Version of orphaned commit
				_git_add_commit newfile3
				REF=$(git rev-parse --short=10 HEAD)
				git reset --hard HEAD^ --quiet
				ut "git_version $REF" == "$REF"

				ut0 Where --first-parent makes a difference
				git checkout -q ${default_branch} > /dev/null
				git merge next2 -m "Merge" > /dev/null
				ut "git_version --dirty" == "1.1-3-g$(git rev-parse --short=7 HEAD)"

				#git log --decorate --oneline --graph --all
				#git describe
				#git describe --first-parent

				echo
				ut_exit
			)
		)
	}

	_ut_earliest_version
	_ut_git_version
fi

##################### ---- END UNIT TESTS ---- #####################

# usage information if invoked with -h or --help
if [[ $1 == "-h" || $1 == "--help" ]]; then
	usage
	exit 0
fi

# check for git
if ! hash git 2>/dev/null; then
	echo "fatal: 'git' must be installed and on the PATH for this command to work." 1>&2
	exit -1
fi

# check minimum git version (1.8.4 is the first that supports `git describe --first-parent')
GITVERNUM=$(git --version | cut -d\  -f 3)
GITVER=$(printf "%02d-%02d-%02d\n" $(echo "$GITVERNUM" | cut -d. -f1-3 | tr . ' '))
if [[ $GITVER < "01-08-04" ]]; then
	echo "fatal: need at least git v1.8.4 (you have v$GITVERNUM)" 1>&2
	exit -1
fi

# check if we're inside a git-managed directory
if [[ -z $(git rev-parse --is-inside-work-tree 2>/dev/null) ]]; then
	echo "fatal: this program must run within a directory managed by git." 1>&2
	exit -1
fi

git_version "$@"
