diff --git a/bin/git-changelog b/bin/git-changelog index 17bd79b..6d60b04 100755 --- a/bin/git-changelog +++ b/bin/git-changelog @@ -40,6 +40,102 @@ _error() { echo } +# _setValueForKeyFakeAssocArray() +# /*! +# @abstract Set value for key from a fake associative array +# @discussion +# Iterates over target_ary (an indexed array), searching for target_key, if the +# key is found its value is set to new_value otherwise the target_key and +# new_value are appended to the array. +# +# The indexed array values must conform to this format: +# "key:value" +# Where key and value are separated by a single colon character. +# +# Specify empty values as an empty, quoted string. +# +# So-called "fake" associative arrays are useful for environments where the +# installed version of bash(1) precedes 4.0. +# @param target_key Key to retrieve +# @param new_value New or updated value +# @param target_ary Indexed array to scan +# @return Returns new array with updated key (status 0) or an empty array +# (status 1) on failure. +# */ +_setValueForKeyFakeAssocArray() { + # parameter list supports empty arguments! + local target_key="$1"; shift + local new_value="$1"; shift + local target_ary=() + local defaultIFS="$IFS" + local IFS="$defaultIFS" + local found=false + + IFS=$' ' target_ary=( $1 ) IFS="$defaultIFS" + + [[ -z "${target_key}" || "${#target_ary[@]}" -eq 0 ]] && echo "${value}" && return 1 + + local _target_ary_length="${#target_ary[@]}" + local i + for (( i=0; i<"${_target_ary_length}"; i++ )); do + local __val="${target_ary[$i]}" + + if [[ "${__val%%:*}" == "${target_key}" ]]; then + target_ary[$i]="${__val%%:*}:${new_value}" + found=true + break + fi + + unset __val + done + unset i _target_ary_length + + # key not found, append + [[ "$found" == false ]] && target_ary+=( "${target_key}:${new_value}" ) + + printf "%s" "${target_ary[*]}" +} + +# _valueForKeyFakeAssocArray() +# /*! +# @abstract Fetch value for key from a fake associative array +# @discussion +# Iterates over target_ary (an indexed array), searching for target_key, if the +# key is found its value is returned. +# +# The indexed array values must conform to this format: +# "key:value" +# Where key and value are separated by a single colon character. +# +# So-called "fake" associative arrays are useful for environments where the +# installed version of bash(1) precedes 4.0. +# @param target_key Key to retrieve +# @param target_ary Indexed array to scan +# @return Returns string containing value (status 0) or an empty string +# (status 1) on failure. +# */ +_valueForKeyFakeAssocArray() { + local target_key="$1" + local target_ary=() + local defaultIFS="$IFS" + local IFS="$defaultIFS" + local value="" + + IFS=$' ' target_ary=( $2 ) IFS="$defaultIFS" + + [[ -z "${target_key}" || "${#target_ary[@]}" -eq 0 ]] && echo "${value}" && return 1 + + local t + for t in "${target_ary[@]}"; do + if [[ "${t%%:*}" == "${target_key}" ]]; then + value="${t#*:}" + break + fi + done + unset t + + echo -e "${value}"; return 0 +} _fetchCommitRange() { local list_all="${1:-false}" @@ -91,7 +187,7 @@ commitList() { local list_style="${1:-false}" # enable/disable list format local changelog="$FILE" local title_date="$(date +'%Y-%m-%d')" - local -A tags_list=() + local tags_list=() local tags_list_keys=() local defaultIFS="$IFS" local IFS="$defaultIFS" @@ -99,17 +195,17 @@ commitList() { # # Tags look like this: # - # >git log --tags --simplify-by-decoration --date="short" --pretty="format:%h$%x09%ad$%x09%D" + # >git log --tags --simplify-by-decoration --date="short" --pretty="format:%h$%x09%ad$%x09%d" # - # ecf1f2b$ 2015-03-15$ HEAD, tag: v1.0.1, origin/master, origin/HEAD, master, hotfix/1.0.2 - # a473e9c$ 2015-03-04$ tag: v1.0.0 - # f2cb562$ 2015-02-19$ tag: v0.9.2 - # 6197c2b$ 2015-02-19$ tag: v0.9.1 - # 1e5f5e6$ 2015-02-16$ tag: v0.9.0 - # 3de8ab5$ 2015-02-11$ origin/feature/restore-auto - # a15afd1$ 2015-02-02$ origin/feature/versionable - # 38a44e0$ 2015-02-02$ origin/feature/save-auto - # 3244b80$ 2015-01-16$ origin/feature/silent-history, upstream + # ecf1f2b$ 2015-03-15$ (HEAD, tag: v1.0.1, origin/master, origin/HEAD, master, hotfix/1.0.2) + # a473e9c$ 2015-03-04$ (tag: v1.0.0) + # f2cb562$ 2015-02-19$ (tag: v0.9.2) + # 6197c2b$ 2015-02-19$ (tag: v0.9.1) + # 1e5f5e6$ 2015-02-16$ (tag: v0.9.0) + # 3de8ab5$ 2015-02-11$ (origin/feature/restore-auto) + # a15afd1$ 2015-02-02$ (origin/feature/versionable) + # 38a44e0$ 2015-02-02$ (origin/feature/save-auto) + # 3244b80$ 2015-01-16$ (origin/feature/silent-history, upstream) # 85e45f8$ 2014-08-25$ # # The most-recent tag will be preceded by "HEAD, " if there have been zero @@ -121,6 +217,9 @@ commitList() { local _tag_regex='^[[:alnum:][:blank:][:punct:]]+/.*' while IFS=$'\t' read _ref _date _tag; do [[ -z "${_tag}" ]] && continue + # strip out tags form () + # git v2.2.0+ supports '%D', like '%d' without the " (", ")" wrapping. One day we should use it instead. + _tag="${_tag:2:-1}" # trap tag if it points to last commit (HEAD) _tag="${_tag##HEAD, }" # strip out any additional tags pointing to same commit, remove tag label @@ -128,9 +227,9 @@ commitList() { # strip out tags that are actually feature branches (git-flow support) [[ "${_tag}" =~ ${_tag_regex} ]] && continue # add tag to assoc array; copy tag to tag_list_keys for ordered iteration - tags_list["${_tag}"]="${_ref}=>${_date}" + tags_list+=( "${_tag}:${_ref}=>${_date}" ) tags_list_keys+=( "${_tag}" ) - done <<< "$(git log --tags --simplify-by-decoration --date="short" --pretty="format:%h${_tab}%ad${_tab}%D")" + done <<< "$(git log --tags --simplify-by-decoration --date="short" --pretty="format:%h${_tab}%ad${_tab}%d")" IFS="$defaultIFS" unset _tag_regex unset _ref _date _tag _tab @@ -142,7 +241,8 @@ commitList() { for (( i=0; i<"${_tags_list_keys_length}"; i++ )); do local __curr_tag="${tags_list_keys[$i]}" local __prev_tag="${tags_list_keys[$i+1]:-null}" - local __curr_date="${tags_list[${__curr_tag}]##*=>}" + local __curr_date="$(_valueForKeyFakeAssocArray "${__curr_tag}" "${tags_list[*]}")" + __curr_date="${__curr_date##*=>}" # output latest commits, up until the most-recent tag, these are all # new commits made since the last tagged commit. @@ -176,10 +276,14 @@ commitList() { else _formatCommitPretty "${__curr_tag}" "${__curr_date}" "${__prev_tag}" "${__curr_tag}" fi + unset __curr_date + unset __prev_tag + unset __curr_tag done unset i unset _start_tag_found unset _final_tag_found + unset _tags_list_keys_length return } @@ -206,15 +310,16 @@ main() { local start_tag="null" # empty string and "null" mean two different things! local final_tag="null" - local -A option=() - option["list_all"]=false - option["list_style"]=false - option["title_tag"]="$DEF_TAG_RECENT" - option["start_tag"]="" - option["final_tag"]="" - option["output_file"]="" - option["use_stdout"]=false - option["prune_old"]=false + local option=( + "list_all:false" + "list_style:false" + "title_tag:$DEF_TAG_RECENT" + "start_tag:" + "final_tag:" + "output_file:" + "use_stdout:false" + "prune_old:false" + ) # # We work chronologically backwards from NOW towards start_tag where NOW also @@ -229,31 +334,31 @@ main() { while [ "$1" != "" ]; do case $1 in -a | --all ) - option["list_all"]=true + option=( $(_setValueForKeyFakeAssocArray "list_all" true "${option[*]}") ) ;; -l | --list ) - option["list_style"]=true + option=( $(_setValueForKeyFakeAssocArray "list_style" true "${option[*]}") ) ;; -t | --tag ) - option["title_tag"]="$2" + option=( $(_setValueForKeyFakeAssocArray "title_tag" "$2" "${option[*]}") ) shift ;; -f | --final-tag ) - option["final_tag"]="$2" + option=( $(_setValueForKeyFakeAssocArray "final_tag" "$2" "${option[*]}") ) shift ;; -s | --start-tag ) - option["start_tag"]="$2" + option=( $(_setValueForKeyFakeAssocArray "start_tag" "$2" "${option[*]}") ) shift ;; -n | --no-merges ) GIT_LOG_OPTS='--no-merges' ;; -p | --prune-old ) - option["prune_old"]=true + option=( $(_setValueForKeyFakeAssocArray "prune_old" true "${option[*]}") ) ;; -x | --stdout ) - option["use_stdout"]=true + option=( $(_setValueForKeyFakeAssocArray "use_stdout" true "${option[*]}") ) ;; -h | ? | help | --help ) _usage @@ -261,43 +366,47 @@ main() { ;; * ) [[ "${1:0:1}" == '-' ]] && _error "Invalid option: $1" && _usage && exit 1 - option["output_file"]="$1" + option=( $(_setValueForKeyFakeAssocArray "output_file" "$1" "${option[*]}") ) ;; esac shift done - if [[ -n "${option["start_tag"]}" ]]; then - start_tag="$(git describe --tags --abbrev=0 "${option["start_tag"]}" 2>/dev/null)" + local _tag="$(_valueForKeyFakeAssocArray "start_tag" "${option[*]}")" + if [[ -n "${_tag}" ]]; then + start_tag="$(git describe --tags --abbrev=0 "${_tag}" 2>/dev/null)" if [[ -z "$start_tag" ]]; then _error "Specified start-tag does not exist!" return 1 fi fi + unset _tag - if [[ -n "${option["final_tag"]}" ]]; then - final_tag="$(git describe --tags --abbrev=0 "${option["final_tag"]}" 2>/dev/null)" + local _tag="$(_valueForKeyFakeAssocArray "final_tag" "${option[*]}")" + if [[ -n "${_tag}" ]]; then + final_tag="$(git describe --tags --abbrev=0 "${_tag}" 2>/dev/null)" if [[ -z "$final_tag" ]]; then _error "Specified final-tag does not exist!" return 1 fi fi + unset _tag # # generate changelog # local tmpfile="$(git_extra_mktemp)" - local changelog="${option["output_file"]}" - local title_tag="${option["title_tag"]}" + local changelog="$(_valueForKeyFakeAssocArray "output_file" "${option[*]}")" + local title_tag="$(_valueForKeyFakeAssocArray "title_tag" "${option[*]}")" - if [[ "${option["list_style"]}" == true ]]; then - if [[ "${option["list_all"]}" == true ]]; then + if [[ "$(_valueForKeyFakeAssocArray "list_style" "${option[*]}")" == true ]]; then + if [[ "$(_valueForKeyFakeAssocArray "list_all" "${option[*]}")" == true ]]; then commitListPlain "true" >> "$tmpfile" else commitListPlain "false" "$start_tag" "$final_tag" >> "$tmpfile" fi else - if [[ "${option["list_all"]}" == true ]]; then + if [[ "$(_valueForKeyFakeAssocArray "list_all" "${option[*]}")" == true ]]; then commitListPretty "true" "$title_tag" >> "$tmpfile" else commitListPretty "false" "$title_tag" "$start_tag" "$final_tag" >> "$tmpfile" @@ -312,12 +421,13 @@ main() { fi # append existing changelog? - if [[ -f "$changelog" && "${option["prune_old"]}" == false ]]; then + if [[ -f "$changelog" \ + && "$(_valueForKeyFakeAssocArray "prune_old" "${option[*]}")" == false ]]; then cat "$changelog" >> "$tmpfile" fi # output file to stdout or move into place - if [[ "${option["use_stdout"]}" == true ]]; then + if [[ "$(_valueForKeyFakeAssocArray "use_stdout" "${option[*]}")" == true ]]; then cat "$tmpfile" rm -f "$tmpfile" else