Skip to content

Instantly share code, notes, and snippets.

@rlespinasse
Last active May 4, 2021 01:07
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rlespinasse/78a136cae7d19e83c3e9232b42734d35 to your computer and use it in GitHub Desktop.
Save rlespinasse/78a136cae7d19e83c3e9232b42734d35 to your computer and use it in GitHub Desktop.
Generate changelog of Lerna independant packages as Slack message
#!/usr/bin/env bash
set -eo pipefail
set -u
IFS=$'\n\t'
command -v fzf >/dev/null 2>&1 || {
echo >&2 "I require fzf but it's not installed. Aborting."
exit 1
}
bot_name="${1:-Changelog BOT}"
print_types="${2:-all}"
spacer=" "
get_packages() {
npx lerna list | fzf -m -1 --header="Select packages"
}
get_package_tags() {
local package="$1"
git tag -l "$package@*"
}
get_last_package_version() {
local package="$1"
latest_tag=$(git describe --abbrev=0 HEAD --match "$package@*" | sed 's/.*@//')
get_package_tags "$package" | grep -v "$latest_tag" | \
sed 's/.*@//;s/$/_/' | sort -V | sed 's/_$//' | \
fzf -1 --tac --header="Select last '$package' version (from x to $latest_tag)"
}
get_changelog() {
local package="$1"
local version="$2"
echo "$package@$version" >> generate-changelog-message.tags.log
while read -r commit
do
if [ ! "$(git diff-tree --no-commit-id --name-only -r "$commit" | grep "/$package/")" == "" ]
then
git show -s --format="%s" "$commit" | \
# Remove stash commit from github like '(#000)'
sed 's/(#[0-9]*)$//;s/\s*([^)]*:/:/;s/\s*(/(/' | \
# Trim
sed 's/^\s*//;s/\s*$//'
fi
done < <(git log "$package@$version"..HEAD --oneline --format="%H")
}
correct_commit_type() {
local commit_type="$1"
local commit_context="$2"
local end_of_message="$3"
local options=()
local types
if [ ! "$commit_context" == "" ]; then
commit_context="($commit_context)"
fi
types=(
"feat"
"fix"
"docs"
"refactor"
"perf"
"test"
"style"
"build"
"ci"
)
for type in "${types[@]}"
do
if [[ "$type" == "${commit_type}" ]]; then
echo "${commit_type}${commit_context}${end_of_message}"
return
fi
done
if [ "$commit_context" == "" ] && [ ! "$commit_type" == "" ]; then
commit_context="($commit_type)"
fi
for type in "${types[@]}"
do
options+=("$type$commit_context$end_of_message")
done
printf -- "%s" "${options[*]}" | \
fzf --header="Choose valid commit type instead of '$commit_type'"
}
get_clean_commit_type() {
local message="$1"
echo "$message" | \
# Extract commit type before '(' or ':'
# also manage message without commit type
sed 's/:.*/#/;s/(.*#$/#/;s/$/#/;s/##$//;s/.*#$//' | \
# Commit type is always in lower case
tr '[:upper:]' '[:lower:]' | \
# Clean values
sed 's/feature/feat/;s/bugs/fix/'
}
get_clean_commit_context() {
local message="$1"
# shellcheck disable=SC2001
echo "$message" | sed 's/^.*(\(.*\)).*$/\1#/;s/^.*[^#]//;s/#$//'
}
get_clean_end_of_message() {
local message="$1"
echo "$message" | \
# Remove commit type
sed 's/^.*:/:/' | \
# Remove reverted mode
sed 's/#REVERTED$//' | \
# Add ':' on message without commit type
sed 's/^/#/;s/^#:/:/;s/^#/: /'
}
manage_revert() {
local commits="$1"
local clean_commits
local reverted_commits
clean_commits="$commits"
reverted_commits=""
# Remove reverted commits
while read -r reverted_message
do
clean_commits=$(echo "$clean_commits" | grep -v "^$reverted_message$")
reverted_commits=$(printf "%s\n%s" "$reverted_commits" "Revert \"$reverted_message\"")
done < <(echo "$commits" | grep "Revert" | sed 's/^Revert\s"//;s/"$//')
# Remove revert commits
while read -r revert_commit
do
clean_commits=$(echo "$clean_commits" | grep -v "^$revert_commit$")
done < <(echo "$reverted_commits")
# Transform revert message
while read -r revert_message
do
clean_commits=$(printf "%s\n%s%s" "$clean_commits" "$revert_message" "#REVERTED")
done < <(echo "$clean_commits" | grep "Revert" | sed 's/^Revert\s"//;s/"$//')
echo "$clean_commits" | grep -v "Revert"
}
manage_unknown_commit_type() {
local commits="$1"
local known_types="^build:\|^ci:\|^docs:\|^feat:\|^fix:\|^perf:\|^refactor:\|^style:\|^test:\|^build(\|^ci(\|^docs(\|^feat(\|^fix(\|^perf(\|^refactor(\|^style(\|^test("
local clean_commits
# Get all commits with valid type
clean_commits="$(echo "$commits" | grep "$known_types")"
# Correct all commits with invalid type
while read -r message
do
local commit_type
local commit_context
local end_of_message
local revert_mode
local clean_message
commit_type=$(get_clean_commit_type "$message")
commit_context=$(get_clean_commit_context "$message")
end_of_message=$(get_clean_end_of_message "$message")
revert_mode=""
if [[ "$message" == *"#REVERTED"* ]]; then
revert_mode="#REVERTED"
fi
clean_message=$(correct_commit_type "$commit_type" "$commit_context" "$end_of_message")
clean_commits=$(printf "%s\n%s%s" "$clean_commits" "$clean_message" "$revert_mode")
done < <(echo "$commits" | grep -v "$known_types")
echo "$clean_commits"
}
get_clean_changelog() {
local package="$1"
local commits
commits=$(get_changelog "$package" "$(get_last_package_version "$package")" | \
grep -v "build: release" | \
grep -v "root commit" | \
grep -v "^Merge")
commits=$(manage_revert "$commits")
commits=$(manage_unknown_commit_type "$commits")
echo "$commits"
}
tocsv() {
local package="$1"
while read -r data
do
local subjectcsv
subjectcsv=$(echo "$data" | \
# Manage subject
sed 's/:/#;/' | \
# Manage context
sed 's/(/;/;s/)#;/;/;s/#;/;;/' | \
# Manage reverted commit status
sed 's/$/#/;s/#REVERTED#$/;revert/;s/#$/;/' | \
# Trim each field
sed 's/\s*;/;/g;s/;\s*/;/g'
)
echo "$package;$subjectcsv"
done
}
get_changelogs() {
while read -r package
do
get_clean_changelog "$package" | tocsv "$package"
done < <(get_packages)
}
toslackmarkdown() {
local current_package=""
local current_type=""
while read -r data
do
local package
local type
local context
local subject
local revert
if [ "$data" == "" ]; then
continue
fi
package="$(echo "${data}" | cut -d';' -f1)"
type="$(echo "${data}" | cut -d';' -f2)"
context="$(echo "${data}" | cut -d';' -f3)"
subject="$(echo "${data}" | cut -d';' -f4)"
revert="$(echo "${data}" | cut -d';' -f5)"
if [ ! "$print_types" == "all" ] && [[ ! "$print_types" == *"$type"* ]]; then
continue
fi
if [ ! "$package" == "$current_package" ]; then
current_type=""
current_package="$package"
echo "${spacer}"
latest_tag=$(git describe --tags --abbrev=0 --match "$package@*" | sed 's/.*@//')
since_tag=$(grep "$package@" generate-changelog-message.tags.log | sed 's/.*@//')
echo ":package: Changelog of $current_package v$latest_tag (since v$since_tag)"
fi
if [ ! "$type" == "$current_type" ]; then
current_type="$type"
echo "${spacer}"
if [ "$type" == "feat" ]; then
echo "${spacer}${spacer}:sparkles: Features"
elif [ "$type" == "fix" ]; then
echo "${spacer}${spacer}:bug: Corrections"
elif [ "$type" == "docs" ]; then
echo "${spacer}${spacer}:pencil: Documentation"
elif [ "$type" == "build" ]; then
echo "${spacer}${spacer}:building_construction: Build System"
elif [ "$type" == "ci" ]; then
echo "${spacer}${spacer}:construction_worker: Continuous Delivery System"
elif [ "$type" == "perf" ]; then
echo "${spacer}${spacer}:zap: Performance"
elif [ "$type" == "refactor" ]; then
echo "${spacer}${spacer}:wrench: Refactoring feature"
elif [ "$type" == "style" ]; then
echo "${spacer}${spacer}:art: Code style"
elif [ "$type" == "test" ]; then
echo "${spacer}${spacer}:white_check_mark: Tests"
fi
fi
if [ "$revert" == "revert" ]; then
revert=":rewind: "
fi
if [ ! "$subject" == "" ]; then
if [ "$context" == "" ]; then
echo "${spacer}${spacer}${spacer}${spacer}${revert}$subject"
else
echo "${spacer}${spacer}${spacer}${spacer}${revert}[$context] $subject"
fi
fi
done
}
get_markdown_changelog() {
local content
content=$(get_changelogs | sort)
echo ":robot_face: $bot_name"
echo "$content" | toslackmarkdown
}
cleanup() {
rm -f generate-changelog-message.tags.log || true
}
trap cleanup EXIT
cleanup
get_markdown_changelog
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment