#!/bin/sh -efu
#
# Copyright (C) 2006-2010  Dmitry V. Levin <ldv@altlinux.org>
# Copyright (C) 2006-2008  Alexey Gladkov <legion@altlinux.org>
# Copyright (C) 2006  Sergey Vlasov <vsu@altlinux.org>
# Copyright (C) 2008  Alexey I. Froloff <raorn@altlinux.org>
#
# gear common shell functions.
#
# This file is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA.
#

. shell-error
. shell-quote
. shell-args

PROG="${0##*/}"
PROG_VERSION='1.7.4'

gear_short_options='r:,t:,h,q,v,V'
gear_long_options='no-compress,commit,bzip2,gzip,command,hasher,rpmbuild,update-spec,export-dir:,describe,rules:,tree-ish:,help,quiet,verbose,version'

__cleanup_handler_name=
__cleanup_handler()
{
	trap - EXIT
	[ -z "$__cleanup_handler_name" ] ||
		"$__cleanup_handler_name" "$@" ||:
	exit "$@"
}

__exit_handler()
{
	__cleanup_handler $?
}

__signal_handler()
{
	__cleanup_handler 1
}

install_cleanup_handler()
{
	__cleanup_handler_name="${1-}"
	trap __exit_handler EXIT
	trap __signal_handler HUP PIPE INT QUIT TERM
}

uninstall_cleanup_handler()
{
	trap - EXIT HUP PIPE INT QUIT TERM
	__cleanup_handler_name=
}

lineno=
rules=
rules_error()
{
	local lineno_text=
	[ -z "$lineno" ] || lineno_text=" line $lineno"
	fatal "$rules$lineno_text: $*"
}

rules_info()
{
	local lineno_text=
	[ -z "$lineno" ] || lineno_text=" line $lineno"
	message "$rules$lineno_text: $*"
}

: ${__gear_exported_vars:=__gear_exported_vars}
export __gear_exported_vars
gear_export_vars()
{
	export "$@"
	__gear_exported_vars="$__gear_exported_vars $*"
}

gear_unset_exported_vars()
{
	unset $__gear_exported_vars
}

is_hex_sha1()
{
	[ "${#1}" -eq 40 ] || return 1

	# "${1##*[![:xdigit:]]*}" does not work in ash
	[ -n "${1##*[!0123456789abcdefABCDEF]*}" ] || return 1

	return 0
}

chdir_to_toplevel()
{
	local cdup
	cdup="$(git rev-parse --show-cdup)"
	[ -z "$cdup" ] ||
		cd "$cdup" ||
			fatal "Cannot chdir to $cdup, the toplevel of the working tree"
}

get_object_sha1()
{
	local name="$1" && shift

	local sha1
	sha1="$(git rev-parse --verify "$name")" || return 1

	# Verify that we really got a SHA-1 (git rev-parse --verify accepts
	# things like ^COMMIT and returns ^SHA1 for them).
	is_hex_sha1 "$sha1" || return 1

	printf %s "$sha1"
}

get_commit_sha1()
{
	local name="$1" && shift

	local sha1
	sha1="$(get_object_sha1 "$name")" || return 1
	git rev-parse --verify "$sha1^0" || return 1
}

get_tree_sha1()
{
	local name="$1" && shift

	local sha1
	sha1="$(get_object_sha1 "$name")" || return 1
	git rev-parse --verify "$sha1^{tree}" || return 1
}

tree_entry_name()
{
	local tree="$1" && shift
	local path="$1" && shift

	if [ -z "${path#.}" ]; then
		printf %s "$tree"
	elif [ -z "${tree##*:*}" ]; then
		printf %s "$tree/$path"
	else
		printf %s "$tree:$path"
	fi
}

traverse_tree()
{
	local tree="$1" && shift
	local path="$1" && shift
	local optional="$1" && shift

	local id="$(tree_entry_name "$tree" "$path")"
	if git cat-file tree "$id" >/dev/null 2>&1; then
		printf %s "$id"
	elif [ "$optional" = 1 ]; then
		return 2
	else
		rules_error "tree \"$path\" not found in \"$tree\""
	fi
}

# fetch blob by id+name.
cat_blob()
{
	local tree="$1" && shift
	local name="$1" && shift

	local id="$(tree_entry_name "$tree" "$name")"
	git cat-file blob "$id" ||
		rules_error "blob \"$name\" not found in \"$tree\""
}

# Check that given variable contains non-empty path without directory
# traversal.
check_pathname()
{
	local name="$1" && shift
	local value="$1" && shift

	[ -n "$value" ] ||
		rules_error "Empty $name \"$value\" specified"

	[ "$value" != '..' -a \
	  -n "${value##/*}" -a \
	  -n "${value##../*}" -a \
	  -n "${value%%*/..}" -a \
	  -n "${value##*/../*}" ] ||
		rules_error "Invalid $name \"$value\" specified"
}

# Check that given variable contains non-empty file name without directory
# separators or directory traversal.
check_filename()
{
	local name="$1" && shift
	local value="$1" && shift

	[ -n "$value" ] ||
		rules_error "Empty $name \"$value\" specified"

	[ "$value" != '..' -a \
	  -n "${value##*/*}" ] ||
		rules_error "Invalid $name \"$value\" specified"
}

rules_cleanup()
{
	sed -i 's/^[[:space:]]*\([^[:space:]:]\+\)[[:space:]]*:[[:space:]]*/\1: /' "$1"
}

workdir=
main_tree_id=

gear_find_rules_check_tree()
{
	[ -n "$(git ls-tree "$main_tree_id" "$1")" ]
}

gear_find_rules_check_cwd()
{
	[ -s "$1" ]
}

gear_find_rules_cat_tree()
{
	cat_blob "$main_tree_id" "$rules"
}

gear_find_rules_cat_cwd()
{
	cat -- "$rules"
}

gear_find_rules_common()
{
	local check_rules="$1"; shift
	local cat_rules="$1"; shift

	if [ -f "$workdir/rules" ]; then
		return 0
	fi
	local r=
	if [ -n "$rules" ]; then
		if $check_rules "$rules"; then
			r="$rules"
		fi
	else
		local n
		for n in .gear-rules .gear/rules; do
			if $check_rules "$n"; then
				rules="$n"
				r="$rules"
				break
			fi
		done
	fi
	if [ -n "$r" ]; then
		$cat_rules
	fi >"$workdir/rules"
	rules_cleanup "$workdir/rules"
	[ -n "$rules" ] || rules='.gear/rules'
}

find_rules_in_tree()
{
	gear_find_rules_common gear_find_rules_check_tree gear_find_rules_cat_tree
}

find_rules_in_cwd()
{
	gear_find_rules_common gear_find_rules_check_cwd gear_find_rules_cat_cwd
}

get_filename_from_rules()
{
	local directive="$1" && shift
	local name="$1" && shift

	find_rules_in_tree
	[ -s "$workdir/rules" ] || return 0

	local file="$(sed -ne "s/^$directive:[[:space:]]\\+\\([^[:space:]]\\+\\)/\\1/p" "$workdir/rules")"
	[ -n "$file" ] || return 0

	[ `printf %s "$file" |wc -l` -le 0 ] ||
		rules_error "More than one $name specified"
	check_pathname "$name" "$file"
	printf %s "$file"
}

expand_rpm_macros()
{
	local spec="$1" && shift
	local var_name="$1" && shift

	local var_value
	eval "var_value=\"\$$var_name\""
	[ -n "$var_value" -a -z "${var_value##*%*}" ] || return 0

	var_value="$(awk '
	BEGIN {
		macros["nil"] = ""
	}
	match($0, /^%(define|global)[[:space:]]+([^[:space:]]+)[[:space:]]+(.*)$/, f) {
		expansion = expand(f[3])
		if ((expansion == 10) && (expansion > 2))
			delete macros[f[2]]
		else
			macros[f[2]] = expansion
	}
	END {
		expansion = expand(value)
		if ((expansion == 10) && (expansion > 2))
			expansion = value
		print expansion
	}
	function expand(input,
			output, pos, macro, f)
	{
		output = ""
		while ((pos = index(input, "%")) != 0) {
			output = output substr(input, 1, pos - 1)
			input = substr(input, pos + 1)
			if (input ~ /^%/) {
				output = output "%"
				input = substr(input, 2)
				continue
			}
			if (match(input, /^([[:alpha:]_][[:alnum:]_]*)(.*)$/, f) ||
			    match(input, /^\{([[:alpha:]_][[:alnum:]_]*)\}(.*)$/, f)) {
				macro = f[1]
				input = f[2]
			} else
				return 10
			if (!(macro in macros))
				return 10
			output = output macros[macro]
		}
		return output input
	}
	' value="$var_value" "$spec" )"

	eval "$var_name=\"$(quote_shell "$var_value")\""
}

spec_name=
spec_version=
spec_release=
get_NVR_from_spec()
{
	local spec="$1" && shift

	spec_name="$(sed '/^name:[[:space:]]*/I!d;s///;q' "$spec" |sed 's/[[:space:]]\+$//')"
	spec_version="$(sed '/^version:[[:space:]]*/I!d;s///;q' "$spec" |sed 's/[[:space:]]\+$//')"
	spec_release="$(sed '/^release:[[:space:]]*/I!d;s///;q' "$spec" |sed 's/[[:space:]]\+$//')"

	expand_rpm_macros "$spec" spec_name
	expand_rpm_macros "$spec" spec_version
	expand_rpm_macros "$spec" spec_release
}

specfile=
pkg_name=
pkg_version=
pkg_release=
find_specfile()
{
	# first try specfile defined in $rules if any.
	find_rules_in_tree
	specfile="$(get_filename_from_rules spec specfile)"

	# second try specfile in toplevel tree.
	if [ -z "$specfile" ]; then
		specfile="$(git ls-tree "$main_tree_id" |
			sed -ne 's/^[^[:space:]]\+[[:space:]]\+blob[[:space:]]\+[^[:space:]]\+[[:space:]]\+\([^/[:space:]]\+\.spec\)$/\1/p')"
		[ `printf %s "$specfile" |wc -l` -le 0 ] ||
			fatal "Too many specfiles found${GIT_DIR:+ in $GIT_DIR}"
	fi
	[ -n "$specfile" ] ||
		fatal "No specfiles found${GIT_DIR:+ in $GIT_DIR}"
	cat_blob "$main_tree_id" "$specfile" >"$workdir/specfile"

	local spec_name spec_version spec_release
	get_NVR_from_spec "$workdir/specfile"
	pkg_name="$spec_name"
	pkg_version="$spec_version"
	pkg_release="$spec_release"
}

tag_dir=
gear_find_tags_check_tree()
{
	local t="$1"; shift
	local id type
	id="$(tree_entry_name "$main_tree_id" "$t/list")"
	type="$(git cat-file -t "$id" 2>/dev/null)" || type=
	if [ "$type" = 'blob' ]; then
		tag_dir="$t"
		return 0
	fi
	return 1
}

gear_find_tags_check_cwd()
{
	local t="$1"; shift
	if [ -s "$t/list" ]; then
		tag_dir="$t"
		return 0
	fi
	return 1
}

gear_find_tags_cat_tree()
{
	cat_blob "$main_tree_id" "$tag_dir/list"
}

gear_find_tags_cat_cwd()
{
	cat -- "$tag_dir/list"
}

tag_dir_default=
gear_find_tags_common()
{
	local check_tags="$1"; shift
	local cat_tags="$1"; shift

	# first try tag directory defined in rules if any.
	tag_dir="$(get_filename_from_rules tags 'tag directory')"

	# second try tag directory in toplevel tree.
	if [ -z "$tag_dir" ]; then
		if [ -n "$tag_dir_default" ]; then
			if $check_tags "$tag_dir_default"; then
				:
			fi
		else
			local t
			for t in .gear-tags .gear/tags; do
				if $check_tags "$t"; then
					break
				fi
			done
		fi
	fi

	if [ -n "$tag_dir" ]; then
		$cat_tags
	fi >"$workdir/tags"
	[ -n "$tag_dir" ] || tag_dir='.gear/tags'
}

find_tags_in_tree()
{
	gear_find_tags_common gear_find_tags_check_tree gear_find_tags_cat_tree
}

find_tags_in_cwd()
{
	gear_find_tags_common gear_find_tags_check_cwd gear_find_tags_cat_cwd
}

tag_list_lineno=
tag_list_error()
{
	local lineno_text=
	[ -z "$tag_list_lineno" ] || lineno_text=" line $tag_list_lineno"
	fatal "$tag_dir/list$lineno_text: $*"
}

lookup_tag()
{
	local tag_list_file="$1" && shift
	local requested_tag_name="$1" && shift
	local variable="$1" && shift

	local sha1 name
	tag_list_lineno=0
	while read -r sha1 name; do
		tag_list_lineno="$((tag_list_lineno+1))"
		if [ "$name" = "$requested_tag_name" ]; then
			eval "$variable=\"$(quote_shell "$sha1")\""
			return 0
		fi
	done <"$tag_list_file"

	return 1
}

extract_stored_tag_chain()
{
	local sha1="$1" && shift

	local id type real_sha1 next
	id="$(tree_entry_name "$main_tree_id" "$tag_dir/$sha1")"
	type="$(git cat-file -t "$id" 2>/dev/null)" || type=
	if [ "$type" = "blob" ]; then
		next="$(git cat-file blob "$id" | sed -ne '1s/^object \(.*\)$/\1/p')" ||
			tag_list_error "Bad stored tag $sha1: parse failed"
		is_hex_sha1 "$next" ||
			tag_list_error "Bad stored tag $sha1: invalid format"
		real_sha1="$(git cat-file blob "$id" | git hash-object -t tag -w --stdin)" ||
			tag_list_error "Bad stored tag $sha1: extract failed"
		[ "$real_sha1" = "$sha1" ] ||
			tag_list_error "Bad stored tag $sha1: hash mismatch (got $real_sha1)"
		extract_stored_tag_chain "$next"
	elif [ -n "$type" ]; then
		tag_list_error "Bad stored tag $sha1: type=$type"
	else
		printf %s "$sha1"
	fi
}

is_ancestor_commit()
{
	local ancestor="$1" && shift
	local descendant="$1" && shift

	local bases
	bases="$(git log -n1 --pretty=format:1 ^"$descendant" "$ancestor")" ||
		return 1
	[ -z "$bases" ] || return 1
}

check_tag_name()
{
	local name="$1" && shift

	[ -n "$name" ] ||
		rules_error 'Empty tag name is not allowed'
	[ -n "${name##/*}" ] ||
		rules_error "Invalid tag name \"$name\": initial '/' is not allowed"
	[ -n "${name%%*/}" ] ||
		rules_error "Invalid tag name \"$name\": trailing '/' is not allowed"
	[ -n "${name##*[][*?^~:@[:space:]]*}" ] ||
		rules_error "Invalid tag name \"$name\": invalid characters found"
	git check-ref-format "tags/$name" ||
		rules_error "Invalid tag name \"$name\""
}

main_commit_sha1=
resolve_commit_name()
{
	local name="$1" && shift

	local result found_commit type final_sha1
	if is_hex_sha1 "$name"; then
		result="$name"
		found_commit="$name"
	else
		check_tag_name "$name"
		lookup_tag "$workdir/tags" "$name" result ||
			rules_error "Name \"$name\" not found in tag list"
		is_hex_sha1 "$result" ||
			tag_list_error "Invalid SHA-1 \"$result\" for name \"$name\""
		found_commit="$(extract_stored_tag_chain "$result")" ||
			tag_list_error "Broken tag chain for name \"$name\""
	fi

	type="$(git cat-file -t "$found_commit")" ||
		rules_error "Name \"$name\" specifies a nonexistent object $found_commit"
	if [ "$type" = "tag" ]; then
		rules_error "Tag object $found_commit for name \"$name\" is not stored"
	elif [ "$type" != "commit" ]; then
		rules_error "Name \"$name\" specifies a $type, not a commit"
	fi

	# Clean up possible upper/lower case differences in raw SHA-1 ids
	final_sha1="$(git rev-parse --verify "$found_commit")" ||
		rules_error "Name \"$name\" resolved to invalid object ID \"$found_commit\""

	# Ensure than $final_sha1 is an ancestor of $main_commit_sha1
	is_ancestor_commit "$final_sha1" "$main_commit_sha1" ||
		rules_error "Name \"$name\" resolved to $final_sha1, which is not an ancestor of commit $main_commit_sha1"

	printf %s "$result"
}

resolve_tree_base_name()
{
	local name="$1" && shift

	if [ "$name" = '.' ]; then
		printf %s "$main_tree_id"
	else
		resolve_commit_name "$name"
	fi
}

parse_tree_path()
{
	local name="$1" && shift
	local value="$1" && shift

	[ -n "$value" ] ||
		rules_error "Empty $name specified"

	local path
	if [ -z "${value##*:*}" ]; then
		[ -n "${value%%:*}" ] ||
			rules_error "Empty commit name in $name specified"
		path="${value#*:}"
		[ -n "$path" ] ||
			rules_error "Empty path in $name specified"
	else
		path="$value"
		value=".:$value"
	fi
	check_pathname "$name" "$path"
	printf %s "$value"
}

def_spec_pattern='*.spec'
spec_pattern="$def_spec_pattern"
find_specfile_in_cwd()
{
	local prefix="$1" && shift

	local f pattern spec= many_specs=
	for pattern in ${spec_pattern}; do
		for f; do
			[ -z "${f##$pattern}" -o -z "${f%%$pattern}" ] ||
				continue
			[ ! -L "$f" -a -f "$f" ] ||
				continue
			[ -z "$spec" ] ||
				many_specs=1
			spec="$f"
		done
		[ -z "$spec" ] ||
			break
	done
	if [ -z "$spec" ]; then
		message "${prefix}Spec file not found."
	elif [ -n "$many_specs" ]; then
		message "${prefix}Too many spec files - ignored all."
		spec=
	fi

	if [ -n "$spec" ]; then
		printf %s "$spec"
		return 0
	else
		return 1
	fi
}

run_command()
{
	verbose "Executing: $*"
	"$@"
}

subst_key()
{
	local var_name="$1" && shift
	local key_name="$1" && shift
	local key_value="$1" && shift

	local quoted var_value
	eval "var_value=\"\$$var_name\""

	if [ "$var_value" != "${var_value#*$key_name*}" ]; then
		check_filename "$key_name" "$key_value"
		quoted="$(quote_sed_regexp "$key_value")"
		var_value="$(printf %s "$var_value" |sed "s/$key_name/$quoted/g")"
		eval "$var_name=\"$(quote_shell "$var_value")\""
	fi
}

subst_key_in_vars()
{
	local key_name="$1" && shift
	local key_value="$1" && shift

	local variable
	for variable; do
		subst_key "$variable" "$key_name" "$key_value"
	done
}

subst_spec_keywords()
{
	local name="$1" && shift
	local version="$1" && shift
	local release="$1" && shift

	subst_key_in_vars '@name@' "$name" "$@"
	subst_key_in_vars '@version@' "$version" "$@"
	subst_key_in_vars '@release@' "$release" "$@"
}

subst_NVR_from_spec_file()
{
	local spec="$1" && shift

	local spec_name= spec_version= spec_release=

	if [ -n "$spec" ]; then
		get_NVR_from_spec "$spec"
	fi
	[ -n "$spec_name" ] || spec_name="$pkg_name"
	[ -n "$spec_version" ] || spec_version="$pkg_version"
	[ -n "$spec_release" ] || spec_release="$pkg_release"

	subst_spec_keywords "$spec_name" "$spec_version" "$spec_release" "$@"
}

gear_config_option()
{
	local var_name="$1" opt_name="$2" def_val="$3"
	local subcmd val

	subcmd=".${PROG#gear-}"
	[ "$subcmd" != '.gear' ] || subcmd=
	val="$(git config --get "gear$subcmd.$opt_name")" ||
		val="$def_val"

	eval "$var_name=\"$(quote_shell "$val")\""
}

gear_config_option verbose verbose ''
[ -z "$verbose" ] || verbose=-v
gear_config_option quiet quiet ''
[ -z "$quiet" ] || quiet=-q
