# gitpkg hook script helper to query git-config when out of the repo tree
#
# To use it, simply source it from your own hook with:
# . /usr/share/gitpkg/hooks/repo-config-helper
# then just call repo_config like you'd call 'git config'.
#
# It just passes any arguments verbatim to git-config, but in the right dir
# for getting correct answers for this particular package.
repo_config()
{
    ( cd "$REPO_DIR" && git config "$@" )
}


# sanitise_git_ref <ref>
# {{{
# This function replaces all consecutive illegal constructs in a git refname
# with a single '_'.  The rules for git refnames are described in the manual
# page for git-check-ref-format(1).  Since they've been known to change from
# release to release, including being renumbered, the version implemented is
# described explicitly here too now.  As of git version 2.1.0:
#
# Git imposes the following rules on how references are named:
#
#   1. They can include slash / for hierarchical (directory) grouping, but
#      no slash-separated component can begin with a dot .  or end with
#      the sequence .lock.
#
#   2. They must contain at least one /. This enforces the presence of a
#      category like heads/, tags/ etc. but the actual names are not
#      restricted. If the --allow-onelevel option is used, this rule is
#      waived.
#
#   3. They cannot have two consecutive dots ..  anywhere.
#
#   4. They cannot have ASCII control characters (i.e. bytes whose values
#      are lower than \040, or \177 DEL), space, tilde ~, caret ^, or
#      colon : anywhere.
#
#   5. They cannot have question-mark ?, asterisk *, or open bracket [
#      anywhere. See the --refspec-pattern option below for an exception
#      to this rule.
#
#   6. They cannot begin or end with a slash / or contain multiple
#      consecutive slashes (see the --normalize option below for an
#      exception to this rule)
#
#   7. They cannot end with a dot ..
#
#   8. They cannot contain a sequence @{.
#
#   9. They cannot be the single character @.
#
#  10. They cannot contain a \.
#
# We don't enforce rule 9 here, you're probably just as doomed with a ref that
# is just a single _ too, so you're on your own with the mess you made there.
# }}}
sanitise_git_ref()
{
    (
	shopt -s extglob

	ref=${1//\/.//_}				# rule 1.
	# must have at least 1 '/'			# rule 2 is NFU.
	ref=${ref//../_}				# rule 3.
	ref=${ref//[[:cntrl:][:space:]:~^?*[]/_}	# rule 4 & 5.
	ref=${ref##+(/)}				# rule 6.
	ref=${ref//+(\/)\//_}				# rule 6.
	ref=${ref%%+(/|.)}				# rule 6 & 7.
	ref=${ref/%.lock/.loc}				# rule 1 (was rule 6).
	ref=${ref//@{/_}				# rule 8.
	# must not be a single @			# rule 9 is NFU.
	ref=${ref//\\\\/_}				# rule 10.
	ref=${ref//+(_)_/_}

	echo "$ref"
    )
}


# require_bash_version <major[.minor[.patchlevel]]>
# {{{
# Assert we have a recent enough version of bash to support required features.
# Bash 4.3 or later is needed for nameref and ${!var} indirection support.
# Bash 4.4 or later is needed for the ${var@Q} parameter transformation.
#
# For lists of what features were added when, see:
# https://mywiki.wooledge.org/BashFAQ/061
# https://wiki.bash-hackers.org/scripting/bashchanges
# }}}
require_bash_version()
{
    local -a ver=( ${1//./ } )
    local n=${#ver[@]}
    local i

    # If you need a finer grain version requirement than major.minor.patchlevel
    # then you're probably doing something terribly wrong.
    if [[ $n -gt 3 ]]; then
	n=3
	printf "\n  NOTE: require_bash_version($1) is silly, only checking >= "
	printf "%s." "${ver[@]:0:$n}"
	printf "\n\n"
    fi

    for (( i=0; i<n; ++i )); do
	[[ ${BASH_VERSINFO[$i]:-0} -le ${ver[$i]} ]] || return 0
	[[ ${BASH_VERSINFO[$i]:-0} -eq ${ver[$i]} ]] ||
	{ echo
	  echo "  Sorry, bash $BASH_VERSION is not supported,"
	  echo "  ${BASH_SOURCE[1]} requires bash $1 or later."
	  echo
	  exit 1
	}
    done
}

# We need bash 4.3 or later for the nameref support used below, so check and
# bail out here with a friendly warning instead of crashing obscurely later.
require_bash_version 4.3


# This convenience function parses an array of literal command line options
# to extract the values set for a particular option.  It is mostly only needed
# for options which may be passed multiple times where all of the values cannot
# be obtained by simply accessing the associative array GITPKG_AOPTS.  It will
# populate an indexed array EXTRACTED_OPTS with one element for each value.
# Options passed without a value will have an empty element in that array.
# For example:
#
# extract_values_for_option my-option "${GITPKG_IOPTS[@]}"
# MY_OPTS=( "${EXTRACTED_OPTS[@]}" )
#
# Will set MY_OPTS[] to ( "foo" "" "bar baz" ) if the command line was
# gitpkg --my-option=foo --my-option --my-option='bar baz'.
extract_values_for_option()
{
    if [ -z "$extract_values_for_option_WARN_ONCE" ]; then
	extract_values_for_option_WARN_ONCE='dont be an annoying dick about it'
	echo
	echo "  The extract_values_for_option() helper function is deprecated, you should"
	echo "  be able to do what it does much cleaner by using extract_values_for(), or"
	echo "  the get_option_values() function in your hook code instead."
	echo

	local -a callstack=( $(printf "%s\n" "${BASH_SOURCE[@]##*/}" | uniq) )
	printf -- "  Called from: "
	printf -- "< %s " "${callstack[@]}"
	printf -- "\n\n"
    fi

    local option_name=$1
    shift

    EXTRACTED_OPTS=()

    for opt; do
	case $opt in
	    --$option_name=*)
		EXTRACTED_OPTS+=( "${opt#*=}" )
		;;

	    --$option_name)
		EXTRACTED_OPTS+=( "" )
		;;
	esac
    done;
}


# trim_array <NAME>
# Remove any empty elements from the indexed array NAME.
# It does not remove elements which contain only whitespace or IFS characters.
trim_array()
{
    local -n ref=$1
    local -a res=()
    local v

    for v in "${ref[@]}"; do
	[ -z "$v" ] || res+=( "$v" )
    done

    ref=( "${res[@]}" )
}


# have_commandline_option <option-name> [ALL-OPTIONS]
#
# Simple true or false test of whether either --option-name or --no-option-name
# were passed on the command line, evaluates to false if neither were.
have_commandline_option()
{
    local -n all_options=${2:-GITPKG_AOPTS}

    [[ ${all_options[$1]+set} || ${all_options[no-$1]+set} ]]
}

# have_any_of_these_commandline_options <option-name ...>
#
# For each option-name check whether either --option-name or --no-option-name
# were passed on the command line, evaluates to false if neither were for any
# of the list of options.
have_any_of_these_commandline_options()
{
    local opt

    for opt; do
	! have_commandline_option "$opt" || return 0
    done
    return 1
}


# extract_values_for <DEST-ARRAY> <option-name> [ALL-OPTIONS]
# {{{
# This is a refinement of the extract_values_for_option function that requires
# the bash 4.3 nameref functionality.  It fills a caller-selected array with
# all of the values passed for <option-name> directly, and defaults to sourcing
# the values from GITPKG_IOPTS unless an alternative array is provided.
#
# The treatment of command line arguments by this function is designed to suit
# multi-valued options, where repeated use is normally additive and preserving
# order may be important.  It supports collecting options for tools where
# passing an empty value is handled specially (to signal a default value, or
# overriding previously set values etc.)
#
# It will interpret the alias --no-option as if a bare --option or --option=''
# were used, adding an empty element to DEST-ARRAY.  If option-name was not
# passed on the command line (or rather if it does not appear in the array of
# ALL-OPTIONS), then the prior content of DEST-ARRAY is not changed.
#
# For example:
#
# extract_values_for MY_OPTS my-option
#
# Will set MY_OPTS[] to ( 'foo' '' 'bar baz' '' ) if the command line was
# gitpkg --my-option=foo --my-option --my-option='bar baz' --no-my-option
# }}}
extract_values_for()
{
    local -n ref=$1
    local -n option_array=${3:-GITPKG_IOPTS}
    local option_name=$2
    local opt

    for opt in "${option_array[@]}"; do
	case $opt in
	    --$option_name=*)
		ref+=( "${opt#*=}" )
		;;

	    --$option_name|--no-$option_name)
		ref+=( "" )
		;;
	esac
    done;
}

# extract_value_for <DEST-VAR> <option-name> [ALL-OPTIONS]
# {{{
# This function is specialised for the case of a single-value option, which may
# still be passed multiple times, but each new value will strictly override the
# last, they are not additive.
#
# If --option-name=<value>  is passed, DEST-VAR is set to <value>.
# If --option-name          is passed, DEST-VAR is set to an empty string.
# If --no-option-name       is passed, DEST-VAR is unset.
#
# If option-name was not passed on the command line (or rather if it does not
# appear in the array of ALL-OPTIONS), then the prior content of DEST-VAR is
# not changed.
# }}}
extract_value_for()
{
    local -n ref=$1
    local -n option_array=${3:-GITPKG_IOPTS}
    local option_name=$2
    local opt

    # Since we aren't looking for multiple values, only the last one set,
    # we could just look at GITPKG_AOPTS here, but that doesn't give us
    # an ordering between --option and --no-option if both were used ...
    for opt in "${option_array[@]}"; do
	case $opt in
	    --$option_name=*)
		ref=${opt#*=}
		;;

	    --$option_name)
		ref=
		;;

	    --no-$option_name)
		unset ref
		;;
	esac
    done
}

# extract_bool_for <DEST-VAR> <option-name> [ALL-OPTIONS]
# {{{
# This function is specialised for the case of a single-value boolean option,
# which may still be passed multiple times, but each new value will strictly
# override the last, they are not additive, and there is little nuance to the
# range of values, the option is essentially an on/off switch.
#
# It is in fact slightly more nuanced than that, DEST-VAR is a quad-state
# variable which may be true, false, empty, or unset.
#
# Passing --option-name sets it true, --no-option-name sets it false,
# and --option-name=<value> sets it according to <value>.
#
# Valid values for <value> are in line with what git-config accepts:
# yes, on, 1, or true, will all set DEST-VAR=true
# no, off, 0, or false, will all set DEST-VAR=false
#
# As a special case, passing --option='' will set DEST-VAR to an empty string.
# This may be interpreted specially by the caller (for example to ignore any
# prior configuration and restore some default), or it may simply be treated
# as an alias for either true or false as appropiate to the use case.
#
# Any other <value> passed will emit a warning, but will otherwise be ignored.
#
# If option-name was not passed on the command line (or rather if it does not
# appear in the array of ALL-OPTIONS), then the prior content of DEST-VAR is
# not changed.
# }}}
extract_bool_for()
{
    local -n ref=$1
    local -n option_array=${3:-GITPKG_IOPTS}
    local option_name=$2
    local opt

    for opt in "${option_array[@]}"; do
	case $opt in
	    --$option_name=*)
		case ${opt#*=} in
		    yes|on|true|1)  ref='true'  ;;
		    no|off|false|0) ref='false' ;;
		    "")             ref=''      ;;
		    *)
			printf "\n  WARNING: unexpected value '${opt#*=}'"
			printf " ignored for bool option '$option_name'\n\n"
			;;
		esac
		;;

	    --$option_name)
		ref='true'
		;;

	    --no-$option_name)
		ref='false'
		;;
	esac
    done
}


# get_option_values <DEST-ARRAY> <cmdline-option> [<config-option>] [default ...]
# {{{
# Fill DEST-ARRAY with all the values passed to --cmdline-option, or if it was
# not passed, all the values returned by `git config --get-all <config-option>`
# finally falling back to the (optional) set of <default ...> values if neither
# of those had set any values for this option.
#
# The argument --no-cmdline-option will also be accepted and interpreted as if
# --cmdline-option='' were passed, adding an empty element to DEST-ARRAY.  The
# order in which values were set is preserved in the elements of DEST-ARRAY.
#
# This function is best suited to handling multi-valued options, where repeated
# use is normally additive and preserving order may be important.  It supports
# collecting options for tools where passing an empty value is somehow special
# (to signal a default value, or overriding previously set values etc.)
#
# Any prior content of DEST-ARRAY will be erased when this function is called.
#
# The DEST_ARRAY values can be expanded again to form the command line for some
# other tool with something like (note the containing double quotes):
#
#   "${DEST_ARRAY[@]/#/--cmdline-option=}"
#
# which expands to nothing (an implicit null argument) if DEST_ARRAY is empty,
# or --cmdline-option=value as a single word (even if the value itself contains
# whitespace) for each value in the array.  To expand the DEST_ARRAY as pairs
# of words (e.g. --cmdline-option 'some value') or the like, you will need to
# loop over the values and create a new array with alternating option and value
# elements in it.
# }}}
get_option_values()
{
    local -n ref=$1
    local opt

    ref=()

    extract_values_for "$1" "$2"
    [ ${#ref[@]} -gt 0 ] || [ -z "$3" ] ||
			    while read opt; do ref+=("$opt")
					    done < <(repo_config --get-all "$3")
    [ ${#ref[@]} -gt 0 ] || ref+=( "${@:4}" )
}

# get_option_value <DEST-VAR> <cmdline-option> [<config-option>] [<default>]
# {{{
# Set DEST-VAR to the single, last-passed, value that is obtained after any
# <config-option> values that are set override the <default> (if set), and are
# in turn overridden by --cmdline-option if it is passed.
#
# If --cmdline-option=<value>  is passed, DEST-VAR is set to <value>.
# If --cmdline-option          is passed, DEST-VAR is set to an empty string.
# If --no-cmdline-option       is passed, DEST-VAR is unset.
#
# If the option is not set by any of these sources then DEST-VAR will be unset
# (not just empty) when this function returns.
#
#
# The DEST_VAR values can be expanded again to form the command line for some
# other tool with something like (note this one has no outer quoting):
#
#   ${DEST_VAR+"--option${DEST_VAR:+=$DEST_VAR}"}
#
# which expands to nothing (an implicit null argument) or one single word:
#
#   nothing                           if DEST_VAR is unset.
#   '--option'                        if it is set but empty,
#   '--option=safely quoted value'    if set with a (non-empty) value.
#
#
# Or for options which must always have some explicit value (even if empty).
#
#   ${DEST_VAR+"--option=${DEST_VAR}"}
#
# which expands to nothing (an implicit null argument) or one single word:
#
#   nothing                           if DEST_VAR is unset.
#   '--option='                       if it is set but empty,
#   '--option=safely quoted value'    if set with a (non-empty) value.
#
#
# To expand DEST_VAR as pairs of option/value words, you can use something
# like (again note no outer quoting for this case):
#
#   ${DEST_VAR+'--option' "$DEST_VAR"}
#
# which expands to nothing (an implicit null argument), one, or two, words:
#
#   nothing                           if DEST_VAR is unset.
#   '--option'                        if it is set but empty,
#   '--option' 'safely quoted value'  if set with a value.
#
# Other variations on that theme are left as an exercise for the reader.
# }}}
get_option_value()
{
    local -n ref=$1
    local val

    # We need to walk through these from the lowest precedence to highest
    # as a command-line --no-option override may intentionally unset it,
    # which if we applied them in the opposite order is indistinguishable
    # from there being no command line option used.
    unset ref

    [ -z "${4+set}" ]				    || ref=$4	# Caller's default
    [ -z "$3" ] || ! val=$(repo_config --get "$3")  || ref=$val	# git config value
    extract_value_for "$1" "$2"					# command line overrides
}

# get_bool_value <DEST-VAR> <cmdline-option> [<config-option>] [<default>]
# {{{
# Set DEST-VAR to the value set by --cmdline-option (or --no-cmdline-option)
# or if that option was not passed on the command line, to the value that is
# returned by `git config --get --bool <config-option>`, or the if no value is
# set there, to the <default> value (if one was set by the caller).
#
# Passing --cmdline-option sets it true, --no-cmdline-option sets it false,
# and --cmdline-option=<value> sets it according to <value>.
#
# Valid values for <value> are in line with what git-config accepts:
# yes, on, 1, or true, will all set DEST-VAR=true
# no, off, 0, or false, will all set DEST-VAR=false
#
# As a special case, passing --option='' will set DEST-VAR to an empty string.
# This may be interpreted specially by the caller (for example to ignore any
# prior configuration and restore some default), or it may simply be treated
# as an alias for either true or false as appropiate to the use case.
#
# Any other <value> passed will emit a warning, but will otherwise be ignored.
#
# Values set for <default> should be either 'true' or 'false' (or unset
# where a tri-state value is desired when the option was not set at all).
#
# If the option is not set by any of these sources then DEST-VAR will be unset
# (not just empty) when this function returns.
#
# DEST-VAR can be expanded back into a command line option again using any of
# the methods described above for get_option_values(), or of course in any
# other way that is appropriate for the caller.
# }}}
get_bool_value()
{
    local -n ref=$1
    local val

    unset ref

    extract_bool_for "$1" "$2"
    [ -n "${ref+set}" ] || [ -z "$3" ] ||
			   ! val=$(repo_config --get --bool "$3")   || ref=$val
    [ -n "${ref+set}" ] || [ -z "${4+set}" ]			    || ref=$4
}

# vi:sts=4:sw=4:noet:foldmethod=marker
