#!/usr/bin/env bash
#
# Common code shared by the Cogito toolkit.
# Copyright (c) Petr Baudis, 2005
#
# This file provides a library containing common code shared with all the
# Cogito programs.

_cg_cmd=${0##*/}
_cleanup_code=


die()
{
	echo $_cg_cmd: $@ >&2
	eval "$_cleanup_code"
	exit 1
}

usage()
{
	die "usage: $USAGE"
}

# Do this in case we get interrupted or prematurely die
cleanup_trap()
{
	_cleanup_code="$*"
	# die will execute the $_cleanup_code
	trap "echo; die \"interrupted\"" SIGINT SIGTERM
}

pager()
{
	local cgless
	# A little trick to tell the difference between unset and set-to-empty
	# variable:
	if [ "${CG_LESS+set}" = "set" ]; then
		cgless="$CG_LESS"
	else
		cgless="R$_local_CG_LESS$LESS"
	fi
	local line
	# Invoke pager only if there's any actual output
	if IFS=$'\n' read -r line; then
		( echo "$line"; cat; ) | LESS="$cgless" ${PAGER:-less} $PAGER_FLAGS
	fi
}

mktemp()
{
	if [ "$has_mktemp" ]; then
		$has_mktemp "$@"
		return
	fi

	dirarg=
	if [ x"$1" = x"-d" ]; then
		dirarg="-d"
		shift
	fi
	prefix=
	if [ x"$1" = x"-t" ]; then
		prefix=${TMPDIR:-/tmp}/
		shift
	fi

	$(which mktemp) $dirarg $prefix"$1"
}

stat()
{
	if [ "$1" != "-c" ] || [ "$2" != "%s" -a "$2" != "%i" ]; then
		echo "INTERNAL ERROR: Unsupported stat call $@" >&2
		return 1
	fi
	if [ "$has_stat" ]; then
		$has_stat "$@"
		return
	fi

	# It's always -c '%s' now.
	if [ "$2" = "%s" ]; then
		ls -l "$3" | awk '{ print $5 }'
	elif [ "$2" = "%i" ]; then
		ls -lid "$3" | awk '{ print $1 }'
	fi
}

readlink()
{
	if [ "$has_readlink" ]; then
		$has_readlink "$@"
		return
	fi

	if [ "$1" = "-f" ]; then
		shift
		target="$(maynormpath "$1")"
		target="${target%/}"

		# -e will test the existence of the final target; therefore,
		# it will also protect against recursive symlinks and such
		[ -e "$target" ] || return 1

		while true; do
			if ! [ -L "$target" ]; then
				echo "$target"
				return 0
			fi
			target2="$(readlink "$target" 2>/dev/null)" || return 1
			[ "$target2" ] || return 1
			target="$(maynormpath "$target2" "$target"/..)"
		done
		return 42
	fi

	line=$(ls -ld "$1" 2>/dev/null) || return 1
	case "$line" in
	*-\>*)
		echo "${line#* -> }";;
	*)
		return 1;;
	esac
	return 0
}

# echo PATH | normpath
# Normalize the path, handling and removing any superfluous .. and .
# elements. Typically
#	echo ABSPATH/RELPATH | normpath
# to get new absolute path.
normpath()
{
local inp
while IFS= read -r inp; do
	local path path2
	path=()
	path2=()

	while [[ "$inp" == */* ]]; do
		path[${#path[@]}]="${inp%%/*}"
		inp="${inp#*/}"
	done
	path[${#path[@]}]="$inp"
	for (( i=0; $i < ${#path[@]}; i++ )); do
		[ "${path[$i]}" = "." ] && continue
		if [ "${path[$i]}" = ".." ]; then
			[ "${#path2[@]}" -gt 0 ] && unset path2[$((${#path2[@]} - 1))]
			continue
		fi
		path2[${#path2[@]}]="${path[$i]}"
	done
	for (( i=0; $i < ${#path2[@]}; i++ )); do
		echo -n "${path2[$i]}"
		[ $i -lt $((${#path2[@]} - 1)) ] && echo -n /
	done
	echo
done
}

# maynormpath PATH [BASE]
# If $PATH is relative, make it absolute wrt. $(pwd) or $BASE if specified.
# Basically, call this instead of normpath() if $PATH can ever be absolute.
maynormpath()
{
	case "$1" in
	/*)
		echo "$1";;
	*)
		base="$2"; [ "$base" ] || base="$(pwd)"
		echo "$base/$1" | normpath
	esac
}

# xargs with one path argument per line
path_xargs()
{
	normpath | tr '\n' '\0' | xargs -0 "$@"
}

# Equivalent to cg-status -w -n -s '?', but the filenames are delimited
# by '\0' instead of '\n'.
# Usage: list_untracked_files DO_EXCLUDE [EXTRAEXCLUDE]...
# DO_EXCLUDE: "no", "noexclude" means not to exclude anything,
#             otherwise the exclude rules apply
# EXTRAEXCLUDE: extra exclude pattern
list_untracked_files()
{
	excludeflag="$1"; shift
	EXCLUDE=
	if [ "$excludeflag" != "no" -a "$excludeflag" != "noexclude" ]; then
		for excl in '*.[ao]' '.*' '!.gitignore' tags '*~' '#*' "$@"; do
			EXCLUDE="$EXCLUDE --exclude=$excl"
		done
		EXCLUDEFILE=$_git/info/exclude
		if [ -f $EXCLUDEFILE ]; then
			EXCLUDE="$EXCLUDE --exclude-from=$EXCLUDEFILE"
		fi
		# This is just for compatibility (2005-09-16).
		# To be removed later.
		EXCLUDEFILE=$_git/exclude
		if [ -f $EXCLUDEFILE ]; then
			echo Warning: .git/exclude is obsolete, use .git/info/exclude instead. >&2
			EXCLUDE="$EXCLUDE --exclude-from=$EXCLUDEFILE"
		fi
		EXCLUDE="$EXCLUDE --exclude-per-directory=.gitignore"
	fi
	git-ls-files -z --others $EXCLUDE
}

# Usage: showdate SECONDS TIMEZONE [FORMAT]
# Display date nicely based on how GIT stores it.
# Save the date to $_showdate
showdate()
{
	local secs=$1 tzhours=${2:0:3} tzmins=${2:0:1}${2:3} format="$3"
	# bash doesn't like leading zeros
	[ "${tzhours:1:1}" = 0 ] && tzhours=${2:0:1}${2:2:1}
	secs=$(($secs + $tzhours * 3600 + $tzmins * 60))
	[ "$format" ] || format="+%a, %d %b %Y %H:%M:%S $2"
	if [ "$has_gnudate" ]; then
		_showdate="$(LANG=C $has_gnudate -ud "1970-01-01 UTC + $secs sec" "$format")"
	else
		_showdate="$(LANG=C date -u -r $secs "$format")"
	fi
}

# Usage: tree_timewarp [--no-head-update] DIRECTION_STR ROLLBACK_BOOL BASE BRANCH
# Reset the current tree from version BASE to version BRANCH, properly updating
# the working copy (if ROLLBACK_BOOL) and trying to keep local changes.
tree_timewarp()
{
	local no_head_update=
	if [ "$1" = "--no-head-update" ]; then
		no_head_update=1
		shift
	fi
	local dirstr=$1; shift
	local rollback=$1; shift
	local base=$1; shift
	local branch=$1; shift

	[ -s "$_git/merging" ] && die "merge in progress - cancel it by cg-reset first"

	local patchfile=$(mktemp -t gituncommit.XXXXXX)
	if [ "$rollback" ]; then
		cg-diff >$patchfile
		[ -s "$patchfile" ] &&
			echo "Warning: uncommitted local changes, trying to bring them $dirstr" >&2
	else
		# XXX: This may be suboptimal, but it is also non-trivial to keep
		# the adds/removes properly.  So this is just a quick hack to get it
		# working without much fuss.
		cg-diff -r $branch >$patchfile
	fi

	git-read-tree -m "$branch" || die "$branch: bad commit"
	[ "$no_head_update" ] || git-update-ref HEAD "$branch" || :

	# Kill gone files
	git-diff-tree -r $base $branch |
		while IFS=$'\t' read header file; do
			# match ":100755 000000 14d43b1abf... 000000000... D"
			if echo "$header" | egrep "^:([^ ][^ ]* ){4}D" >/dev/null; then
				rm -- "$file"
			fi
		done
	git-checkout-index -f -a

	# FIXME: Can produce bogus "contains only garbage" messages.
	cat $patchfile | cg-patch
	rm $patchfile

	git-update-index --refresh >/dev/null
}

# Determine the most conservative merge base of two commits - keep
# recursing until we get only a single candidate for a merge base.
# The merge base is returned as $_cg_baselist. If we had to recurse,
# a non-zero number is stored in $_cg_base_conservative (otherwise,
# it's set empty).
conservative_merge_base()
{
	local baselist
	baselist=("$@")
	_cg_base_conservative=
	for (( safecounter=0; $safecounter < 1000; safecounter++ )) ; do
		baselist=($(git-merge-base --all "${baselist[@]}")) || return 1
		[ "${#baselist[@]}" -le "1" ] && break
	done
	[ $safecounter -gt 0 ] && _cg_base_conservative=$safecounter
	_cg_baselist=("${baselist[@]}")
}

# update_index will refresh the index and list the local modifications
# Note that this isn't usually safe, since some of the modifications may
# be recorded in the index file - modulo adds and removes also cg-restore
# to historical revisions. Besides, it gives confusing output for relpath.
# Never use it. If you do, accompany it with a comment explaining why is
# it safe to use it.
update_index()
{
	git-update-index --refresh | sed 's/needs update$/locally modified/'
}

# Takes two object directories and checks if they are the same (symlinked
# or so).
is_same_repo()
{
	local dir1="$1" dir2="$2" diff=1

	# Originally, I wanted to compare readlink output, but that fails
	# in binding setup; it isn't likely the object database directories
	# themselves would be binded, but some trunk directories might.
	# So we just create a file inside and see if it appears on the
	# second side...
	if [ ! -w "$dir1" -o ! -w "$dir2" ]; then
		# ...except in readonly setups.
		[ "$(readlink -f "$dir1")" = "$(readlink -f "$dir2")" ] && diff=0
	else
		n=$$
		while [ -e "$dir1/.,,lnstest-$n" -o -e "$dir2/.,,lnstest-$n" ]; do
			n=$((n+1))
		done
		touch "$dir1/.,,lnstest-$n"
		[ -e "$dir2/.,,lnstest-$n" ] && diff=0
		rm "$dir1/.,,lnstest-$n"
	fi
	return $diff
}


# Checks if we weren't called through a deprecated alias
deprecated_alias()
{
	cmd="${0##*/}"
	propername="$1"; shift
	for a in "$@"; do
		[ "$cmd" = "$a" ] && \
			echo "Warning: '$a' is a deprecated alias, please use '$propername' instead" >&2
	done
}


print_help()
{
	which "cg-$1" >/dev/null 2>&1 || exit 1
	sed -n '/^USAGE=/,0s/.*"\(.*\)"/Usage: \1/p;
	        /^deprecated_alias/,0s/^deprecated_alias \([^ ]*\)/\1 is the new name for/p' < $(which cg-$1) 
	echo
	sed -n '3,/^$/s/^# *//p' < $(which cg-$1)
	exit
}

for option in "$@"; do
	[ x"$option" = x-- ] && break
	if [ x"$option" = x"-h" -o x"$option" = x"--help" ]; then
		print_help ${_cg_cmd##cg-}
	fi
done


ARGS=("$@")
ARGPOS=0
set '' # clear positional parameters - use $ARGS[] instead

if [ -t 1 -a -e "$HOME/.cgrc" ]; then
	_cg_name=${_cg_cmd#cg-}
	_cg_defaults1="$(sed -n "/^$_cg_cmd/s/^$_cg_cmd //p" < "$HOME/.cgrc")"
	_cg_defaults2="$(sed -n "/^$_cg_name/s/^$_cg_name //p" < "$HOME/.cgrc")"
	ARGS=($_cg_defaults1 $_cg_defaults2 "${ARGS[@]}")
fi

optshift()
{
	unset ARGS[$ARGPOS]
	ARGS=("${ARGS[@]}")
	[ -z "$1" -o -n "${ARGS[$ARGPOS]}" ] ||
		die "option \`$1' requires an argument"
}

optfail()
{
	die "unrecognized option \`${ARGS[$ARGPOS]}'"
}

optconflict()
{
	die "conflicting option \`$CUROPT'"
}

optparse()
{
	unset OPTARG
	if [ -z "$1" ]; then
		case ${ARGS[$ARGPOS]} in
		--)	optshift; return 1 ;;
		-*)	return 0 ;;
		*)	while (( ++ARGPOS < ${#ARGS[@]} )); do
				[[ "${ARGS[$ARGPOS]}" == -- ]] && return 1
				[[ "${ARGS[$ARGPOS]}" == -* ]] && return 0
			done;
			return 1 ;;
		esac
	fi

	CUROPT=${ARGS[$ARGPOS]}
	local match=${1%=} minmatch=${2:-1} opt=$CUROPT o=$CUROPT val
	[[ "$1" == *= ]] && val=$match
	case $match in
	--*)
		[ "$val" ] && o=${o%%=*}
		[ ${#o} -ge $((2 + $minmatch)) -a \
			"${match:0:${#o}}" = "$o" ] || return 1
		[[ -n "$val" && "$opt" == *=?* ]] \
			&& ARGS[$ARGPOS]=${opt#*=} \
			|| optshift $val ;;
	-?)
		[[ "$o" == $match* ]] || return 1
		[[ "$o" != -?-* || -n "$val" ]] || optfail
		ARGS[$ARGPOS]=${o#$match}
		[ "${ARGS[$ARGPOS]}" ] \
			&& { [ "$val" ] || ARGS[$ARGPOS]=-${ARGS[$ARGPOS]}; } \
			|| optshift $val ;;
	*)
		die "optparse cannot handle $1" ;;
	esac

	if [ "$val" ]; then
		OPTARG=${ARGS[$ARGPOS]}
		optshift
	fi
}



# Optional tools detection/stubbing

# check_tool_presence NAME COMMAND EXENAME...
# (use $cmd in COMMAND)
check_tool()
{
	cmdname="$1"; shift
	cmdtest="$1"; shift
	hasname="has_$cmdname"

	export $hasname=
	for exename in "$@"; do
		# We do our own $PATH iteration as it's faster than the fork()
		# of $(which), and this happens many times every time we
		# execute some cg tool.
		save_IFS="$IFS"; IFS=:
		for dir in $PATH; do
			IFS="$save_IFS"
			cmd="$dir/$exename"
			if [ -x "$cmd" ] && eval "$cmdtest"; then
				export $hasname="$cmd"
				break
			fi
		done
		IFS="$save_IFS"
		[ "$hasname" ] && break
	done 2>/dev/null
}

if ! [ "$__cogito_subsequent" ]; then
	export __cogito_subsequent=1

	check_tool mktemp 'todel=$($cmd -t) && rm $todel' mktemp
	check_tool stat '$cmd -c %s / >/dev/null' stat gnustat gstat
	check_tool readlink '$cmd -f / >/dev/null' readlink
	check_tool gnudate '$cmd -Rud "1970-01-01 UTC" >/dev/null' date gnudate gdate
fi



_git=${GIT_DIR:-.git}
if [ ! "$_git_repo_unneeded" ] && [ ! "$GIT_DIR" ] && [ ! -d $_git ]; then
	rootpath=.
	# while not /
	while [ ! -d $rootpath/.git ] && [ "$(stat -c %i $rootpath)" != "$(stat -c %i $rootpath/..)" ]; do
		rootpath=../$rootpath
	done
	if [ -d $rootpath/.git ]; then
		mainpath="$(echo "$(pwd)/$rootpath" | normpath)"
		_git_relpath=$(pwd)/
		export _git_relpath=${_git_relpath:$((${#mainpath}+1))}
		cd "$rootpath"
	fi
fi
_git_objects="${GIT_OBJECT_DIRECTORY:-$_git/objects}"


# Check if we have something to work on, unless the script can do w/o it.
if [ ! "$_git_repo_unneeded" ]; then
	if [ ! -d "$_git" ]; then
	       echo "There is no GIT repository here ($_git not found)" >&2
	       exit 1
	elif [ ! -x "$_git" ]; then
	       echo "You do not have permission to access this GIT repository" >&2
	       exit 1
	fi
	_git_head=master
	[ -s "$_git/HEAD" ] && { _git_head="$(git-symbolic-ref HEAD)"; _git_head=${_git_head#refs/heads/}; }
	[ -s "$_git/head-name" ] && _git_head="$(cat "$_git/head-name")"
fi

# Check if the script requires to be called from the workdir root.
if [ "$_git_requires_root" ] && [ "$_git_relpath" ]; then
	echo "This command can be ran only from the project root" >&2
	exit 1
fi


# Backward compatibility hacks:
# Fortunately none as of now.
