diff --git a/release.sh b/release.sh
index 6447044..4d36d1a 100755
--- a/release.sh
+++ b/release.sh
@@ -1,10 +1,256 @@
#!/usr/bin/env bash
# -----------------------------------------------------------------------------
-# bat-extras | Copyright (C) 2019-2020 eth-p | MIT License
+# bat-extras | Copyright (C) 2019-2023 eth-p | MIT License
#
# Repository: https://github.com/eth-p/bat-extras
# Issues: https://github.com/eth-p/bat-extras/issues
# -----------------------------------------------------------------------------
+source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/build.sh"
+
+# -----------------------------------------------------------------------------
+# Release-as-a-Library Functions:
+# -----------------------------------------------------------------------------
+
+# Prints the path to the git workspace.
+if ! batextras:is_function_defined ::get_git_workspace; then
+ batextras:get_git_workspace() {
+ batextras:lazy_done "${FUNCNAME[0]}" \
+ "$(batextras:get_project_directory)"
+ }
+fi
+
+# Prints the commit of the latest-tagged version of bat-extras.
+if ! batextras:is_function_defined ::get_current_commit; then
+ batextras:get_current_commit() {
+ batextras:lazy_done "${FUNCNAME[0]}" \
+ "$(batextras:git rev-parse HEAD)"
+ }
+fi
+
+# Prints the commit of the latest-tagged version of bat-extras.
+if ! batextras:is_function_defined ::get_previous_tag_commit; then
+ batextras:get_previous_tag_commit() {
+ local latest_tag
+ local before_latest_tag
+ {
+ read -r latest_tag
+ read -r before_latest_tag
+ } < <(batextras:git rev-list --tags --max-count=2)
+
+ # If the latest commit is a tag, go to the one before that.
+ if [[ "$(batextras:get_current_commit)" = "$latest_tag" ]]; then
+ latest_tag="${before_latest_tag}"
+ fi
+
+ batextras:lazy_done "${FUNCNAME[0]}" "$latest_tag"
+ }
+fi
+
+# Prints the ref name of the latest-tagged version of bat-extras.
+if ! batextras:is_function_defined ::get_previous_tag_name; then
+ batextras:get_previous_tag_name() {
+ batextras:lazy_done "${FUNCNAME[0]}" \
+ "$(batextras:git describe --tags --abbrev=0 "$(batextras:get_previous_tag_commit)")"
+ }
+fi
+
+# Returns the suffix for a day of the month.
+#
+# Arguments:
+# 1 -- The day number.
+#
+# Output:
+# The suffix.
+batextras:day_suffix() {
+ case "$1" in
+ 11 | 12 | 13) echo "th" ;;
+ *1) echo "st" ;;
+ *2) echo "nd" ;;
+ *3) echo "rd" ;;
+ *) echo "th" ;;
+ esac
+}
+
+# Runs `git` within the project directory.
+#
+# This takes the same arguments as git (with the exception of `-C`), and
+# does exactly what `git` would normally do.
+batextras:git() {
+ git -C "$(batextras:get_git_workspace)" "$@"
+ return $?
+}
+
+# Creates the zipball for release.
+# YOU MUST BUILD THE PROJECT FIRST!
+#
+# Arguments:
+# 1 -- The absolute path to the output zip file.
+#
+# Stderr:
+# Messages.
+batextras:create_package() {
+ local artifact="$1"
+ local bin_dir man_dir doc_dir
+ bin_dir="$(batextras:get_output_bin_directory)"
+ man_dir="$(batextras:get_output_man_directory)"
+ doc_dir="$(batextras:get_docs_directory)"
+
+ (
+ # Remove the old zipball, if one exists.
+ if [[ -f "$artifact" ]]; then
+ rm "$artifact" || return $?
+ fi
+
+ # Add the bin directory.
+ cd "$(dirname -- "$bin_dir")" || return $?
+ zip -r "$artifact" "$(basename -- "$bin_dir")"
+
+ # Add the doc directory.
+ cd "$(dirname -- "$doc_dir")" || return $?
+ zip -ru "$artifact" "$(basename -- "$doc_dir")"
+
+ # Add the man directory.
+ if [[ -d "$man_dir" ]]; then
+ cd "$(dirname -- "$man_dir")" || return $?
+ zip -ru "$artifact" "$(basename -- "$man_dir")"
+ fi
+ ) 1>&2
+}
+
+# Generates a Markdown changelog for all changes between two commits.
+#
+# Arguments:
+# 1 -- The first commit, exclusive.
+# 2 -- The second commit, inclusive.
+# 3 -- A filter in regex.
+#
+# Output:
+# The changelog.
+batextras:generate_changelog() {
+ local start_commit="$1"
+ local end_commit="$2"
+ local filter="${3}"
+
+ # Generate sed replacement patterns.
+ local script_links=()
+ local script_names=()
+ local script script_name
+ while read -r script; do
+ script_name="$(basename "$script" .sh)"
+ script_names+=("$script_name")
+ done < <(batextras:get_source_paths)
+
+ local script_pattern
+ script_pattern="$(printf 's/\\(%s\\)/`\\1`/;' "${script_names[@]}")"
+
+ # Generate the changelog.
+ local changelog=''
+ local commit
+ local affected_module
+ local commit_message
+ while read -r commit; do
+ commit_message="$(batextras:git show -s --format=%s "$commit")"
+
+ if ! [[ "$commit_message" =~ ^([a-z-]+):[[:space:]]*(.*)$ ]]; then
+ continue
+ fi
+
+ affected_module="${BASH_REMATCH[1]}"
+
+ # Make module names consistent.
+ case "$affected_module" in
+ dev | lib | mdroff) affected_module="developer" ;;
+ tests) affected_module="test" ;;
+ doc) affected_module="docs" ;;
+ esac
+
+ # Append to changelog.
+ if [[ "$affected_module" =~ ^($filter)$ ]]; then
+ changelog="$changelog"$'\n'" - ${commit_message}"
+ fi
+ done < <(batextras:git rev-list "${start_commit}..${end_commit}")
+
+ # Print the changelog.
+ changelog="$(sed "$script_pattern" <<< "$changelog")"
+ printf "%s\n" "${changelog:1}"
+ return 0
+}
+
+# Generates the Markdown release notes.
+#
+# Arguments:
+# 1 -- The oldest commit, exclusive.
+# 2 -- The newest commit, inclusive.
+#
+# Output:
+# The changelog.
+batextras:generate_release_notes() {
+ local commit_oldest="$1"
+ local commit_newest="$2"
+ local commit_newest_url="https://github.com/eth-p/bat-extras/tree/${commit_newest}"
+ local date_str
+
+ # Get the commit date.
+ local date_year date_month date_day date_month_text date_day_suffix
+ read -r date_year date_month date_day date_month_text \
+ < <(batextras:git show -s --format="%cd" --date="format:%Y %m %d %B" "$commit_newest")
+
+ date_day_suffix="$(batextras:day_suffix "$date_day")"
+ date_str="${date_month_text} ${date_day}${date_day_suffix}, ${date_year}"
+
+ # For each built script, we want to:
+ # - Get the name of the script.
+ # - Get a link to the documentation.
+ # - Add it to the filter for non-developer items.
+ local script_name script_names script_links script_filters script_list_markdown
+ script_links=()
+ script_names=()
+ script_filters=''
+
+ while read -r script; do
+ script_name="$(basename "$script" .sh)"
+ script_names+=("$script_name")
+ script_links+=("[\`${script_name}\`](https://github.com/eth-p/bat-extras/blob/${commit_newest}/doc/${script_name}.md)")
+ script_filters="${script_filters}|$(printf "%q" "$script_name")"
+ done < <(batextras:get_source_paths)
+
+ script_filters="${script_filters:1}" # Remove the leading "|"
+ script_list_markdown="$(printf "%s, " "${script_links[@]:0:$((${#script_links[@]} - 1))}")"
+ script_list_markdown="${script_list_markdown}and ${script_links[$((${#script_links[@]} - 1))]}"
+
+ # Get the changelog.
+ local changelog changelog_dev
+ changelog="$(batextras:generate_changelog "$commit_oldest" "$commit_newest" "$script_filters")"
+ changelog_dev="$(batextras:generate_changelog "$commit_oldest" "$commit_newest" "test|developer|ci|build")"
+
+ # Print the template.
+ { sed '/\\$/{N;s/\\\n//;s/\n//p;}'; } <<- EOF
+ This contains the latest versions of ${script_list_markdown} as of commit [${commit_newest}](${commit_newest_url}) (${date_str}).
+
+ **This is provided as a convenience only.**
+ I would still recommend following the installation instructions in [the README](https://github.com/eth-p/bat-extras#installation-) for the most up-to-date versions.
+
+ ### Changes
+ ${changelog}
+
+ ### Developer
+
+
+
+ ${changelog_dev}
+
+
+
+ EOF
+
+}
+
+# -----------------------------------------------------------------------------
+# Main:
+# Only run everything past this point if the script is not sourced.
+# -----------------------------------------------------------------------------
+(return 0 2>/dev/null) && return 0
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DATE="$(date +%Y%m%d)"
VERSION="$(< "${HERE}/version.txt")"
@@ -14,22 +260,23 @@ SRC="$HERE/src"
source "${LIB}/print.sh"
source "${LIB}/opt.sh"
# -----------------------------------------------------------------------------
+set -euo pipefail
# -----------------------------------------------------------------------------
# Options.
OPT_ARTIFACT="bat-extras-${DATE}.zip"
OPT_SINCE=
OPT_BAD_IDEA=false
-OPT_BIN_DIR="$HERE/bin"
-OPT_DOC_DIR="$HERE/doc"
-OPT_MAN_DIR="$HERE/man"
+OPT_BIN_DIR="$(batextras:get_output_bin_directory)"
+OPT_DOC_DIR="$(batextras:get_docs_directory)"
+OPT_MAN_DIR="$(batextras:get_output_man_directory)"
while shiftopt; do
case "$OPT" in
--since)
shiftval
OPT_SINCE="$OPT_VAL"
- if ! git rev-parse "$OPT_SINCE" &> /dev/null; then
+ if ! batextras:git rev-parse "$OPT_SINCE" &> /dev/null; then
printc "%{RED}%s: unknown commit or tag for '%s'\n" "$PROGRAM" "$OPT"
exit 1
fi
@@ -48,6 +295,10 @@ done
# -----------------------------------------------------------------------------
# Verify the version matches today's date.
+
+VERSION="$(source "${LIB}/constants.sh" && echo "${PROGRAM_VERSION}")"
+VERSION_EXPECTED="$(date +%Y.%m.%d)"
+
if [[ "$VERSION" != "$VERSION_EXPECTED" ]] && ! "$OPT_BAD_IDEA"; then
printc "%{RED}The expected version does not match %{DEFAULT}version.txt%{RED}!%{CLEAR}\n"
printc "%{RED}Expected: %{YELLOW}%s%{CLEAR}\n" "$VERSION_EXPECTED"
@@ -55,6 +306,18 @@ if [[ "$VERSION" != "$VERSION_EXPECTED" ]] && ! "$OPT_BAD_IDEA"; then
exit 1
fi
+# -----------------------------------------------------------------------------
+# Verify the working tree is clean-ish.
+
+if ! "$OPT_BAD_IDEA"; then
+ while read -r flags file; do
+ if [[ "$flags" =~ M ]]; then
+ printc "%{RED}Found an uncommitted change in %{DEFAULT}%s%{RED}!%{CLEAR}\n" "$file"
+ exit 1
+ fi
+ done < <(batextras:git status --porcelain)
+fi
+
# -----------------------------------------------------------------------------
# Build files.
@@ -75,110 +338,13 @@ printc "%{YELLOW}Building scripts...%{CLEAR}\n"
# Build package.
printc "%{YELLOW}Packaging artifacts...%{CLEAR}\n"
-(
- rm "$OPT_ARTIFACT"
- cd "$(dirname "$OPT_BIN_DIR")"
- zip -r "$OPT_ARTIFACT" "$(basename "$OPT_BIN_DIR")"
- cd "$(dirname "$OPT_DOC_DIR")"
- zip -ru "$OPT_ARTIFACT" "$(basename "$OPT_DOC_DIR")"
- if [[ -d "$OPT_MAN_DIR" ]]; then
- cd "$(dirname "$OPT_MAN_DIR")"
- zip -ru "$OPT_ARTIFACT" "$(basename "$OPT_MAN_DIR")"
- fi
-)
-
+batextras:create_package "$OPT_ARTIFACT"
printc "%{YELLOW}Package created as %{BLUE}%s%{YELLOW}.%{CLEAR}\n" "$OPT_ARTIFACT"
# -----------------------------------------------------------------------------
# Print template description package.
printc "%{YELLOW}Release description:%{CLEAR}\n"
-
-# Get the commit hash.
-COMMIT="$(git rev-parse HEAD)"
-COMMIT_URL="https://github.com/eth-p/bat-extras/tree/${COMMIT}"
-
-# Get the release date string.
-DATE_DAY="$(date +%e | sed 's/ //')"
-DATE_SUFFIX=""
-case "$DATE_DAY" in
- 11 | 12 | 13) DATE_SUFFIX="th" ;;
- *1) DATE_SUFFIX="st" ;;
- *2) DATE_SUFFIX="nd" ;;
- *3) DATE_SUFFIX="rd" ;;
- *) DATE_SUFFIX="th" ;;
-esac
-DATE_STR="$(date +'%B') ${DATE_DAY}${DATE_SUFFIX}, $(date +'%Y')"
-
-# Get the script names.
-script_links=()
-script_names=()
-for script in "$SRC"/*.sh; do
- script_name="$(basename "$script" .sh)"
- script_names+=("$script_name")
- script_links+=("[\`${script_name}\`](https://github.com/eth-p/bat-extras/blob/${COMMIT}/doc/${script_name}.md)")
-done
-
-script_pattern="$(printf 's/\\(%s\\)/`\\1`/;' "${script_names[@]}")"
-SCRIPTS="$(printf "%s, " "${script_links[@]:0:$((${#script_links[@]} - 1))}")"
-SCRIPTS="${SCRIPTS}and ${script_links[$((${#script_links[@]} - 1))]}"
-
-# Get the changelog.
-CHANGELOG_DEV=''
-CHANGELOG=''
-if [[ -n "$OPT_SINCE" ]]; then
- ref="$(git rev-parse HEAD)"
- end="$(git rev-parse "$OPT_SINCE")"
- while [[ "$ref" != "$end" ]]; do
- is_developer=false
- ref_message="$(git show -s --format=%s "$ref")"
- ref="$(git rev-parse "${ref}~1")"
-
- if [[ "$ref_message" =~ ^([a-z-]+):[[:space:]]*(.*)$ ]]; then
- affected_module="${BASH_REMATCH[1]}"
-
- # Make module names consistent.
- case "$affected_module" in
- dev | lib | mdroff) affected_module="developer" ;;
- tests) affected_module="test" ;;
- doc) affected_module="docs" ;;
- esac
-
- # Switch to the correct changelog.
- case "$affected_module" in
- test | developer | ci | build) is_developer=true ;;
- esac
- fi
-
- # Append to changelog.
- if "$is_developer"; then
- CHANGELOG_DEV="$CHANGELOG_DEV"$'\n'" - ${ref_message}"
- else
- CHANGELOG="$CHANGELOG"$'\n'" - ${ref_message}"
- fi
- done
-fi
-
-CHANGELOG="$(sed "$script_pattern" <<< "$CHANGELOG")"
-CHANGELOG_DEV="$(sed "$script_pattern" <<< "$CHANGELOG_DEV")"
-
-# Print the template.
-sed '/\\$/{N;s/\\\n//;s/\n//p;}' <<- EOF
- This contains the latest versions of $SCRIPTS as of commit [$(git rev-parse --short HEAD)]($COMMIT_URL) (${DATE_STR}).
-
- **This is provided as a convenience only.**
- I would still recommend following the installation instructions in \
- [the README](https://github.com/eth-p/bat-extras#installation-) for the most up-to-date versions.
-
- ### Changes
- $CHANGELOG
-
- ### Developer
-
-
-
- $CHANGELOG_DEV
-
-
-
-EOF
+batextras:generate_release_notes \
+ "$(batextras:get_previous_tag_name)" \
+ "$(batextras:get_current_commit)"