#!/usr/bin/env sh config_file=.version.json changelog_file=CHANGELOG.md changelog_header="# Changelog All notable changes to this project will be documented in this file. See [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) for commit guidelines. This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ---- " reverse() { if which tac >/dev/null 2>&1; then tac else tail -r fi } generate_config() { jq --null-input \ --arg version "0.0.0" \ --arg sha "" \ --arg url "$1" \ '{ "version": $version, "sha": $sha, "url": $url}' >$config_file } update_config() { # store in a var as reading and writing in one go to one file is not good config=$(jq '.sha |= $sha | .version |= $version' --arg sha "$1" --arg version "$2" $config_file) echo "$config" >$config_file } get_config_version() { jq '.version' <$config_file | tr -d '"' } get_config_sha() { jq '.sha' <$config_file | tr -d '"' } get_config_url() { jq '.url' <$config_file | tr -d '"' } split_version_string() { version_number=$(echo "$1" | cut -d'-' -f1 | cut -d'+' -f1) pre_release=$(echo "$1" | cut -d'-' -f2 | cut -d'+' -f1) build=$(echo "$1" | cut -d'-' -f2 | cut -d'+' -f2) if [ "$pre_release" = "$1" ]; then pre_release="" fi if [ "$build" = "$1" ] || [ "$build" = "$pre_release" ]; then build="" fi major=$(echo "$version_number" | cut -d'.' -f1) minor=$(echo "$version_number" | cut -d'.' -f2) patch=$(echo "$version_number" | cut -d'.' -f3) pre_release_id=$(echo "$pre_release." | cut -d'.' -f2) echo "${major:-0} ${minor:-0} ${patch:-0} $build $pre_release_id" } calculate_version() { version_string=$(split_version_string "$1") major=$(echo "$version_string" | cut -d' ' -f1) minor=$(echo "$version_string" | cut -d' ' -f2) patch=$(echo "$version_string" | cut -d' ' -f3) pre_release_id=$(echo "$version_string" | cut -d' ' -f5) pre_release="" build="" if [ "$2" -ge 100 ]; then major=$((major + 1)) minor=0 patch=0 elif [ "$2" -ge 10 ]; then minor=$((minor + 1)) patch=0 elif [ "$2" -ge 1 ]; then patch=$((patch + 1)) fi if [ "$3" = "rc" ]; then if [ -z "$pre_release_identifier" ]; then pre_release_identifier=1 else pre_release_identifier=$((pre_release_identifier + 1)) fi pre_release="rc.$pre_release_identifier" fi if [ "$4" = "build" ]; then build="$(git rev-parse --short HEAD)" fi version_number="${major}.${minor}.${patch}" if [ -n "$pre_release" ]; then pre_release="-$pre_release" fi if [ -n "$build" ]; then build="+$build" fi echo "${version_number}${pre_release}${build}" } get_git_url() { url=$(git config --get remote.origin.url) if ! echo "$url" | grep -q "^http"; then url=$(echo "$url" | cut -d@ -f2 | sed 's/:/\//') url="https://${url:-localhost}" fi echo "$url" | sed 's/\.git$//' } get_git_commits() { since_hash=$(get_config_sha) if [ -n "$since_hash" ]; then git rev-list "$since_hash..HEAD" else git rev-list HEAD fi } get_git_commit_hash() { git rev-parse --short "$1" } get_git_first_commit_hash() { git rev-list --max-parents=0 --abbrev-commit HEAD } get_git_commit_subject() { git show -s --format=%s "$1" } get_git_commit_body() { git show -s --format=%b "$1" } get_commit_type() { echo "$1" | sed -r -n 's/([a-z]+)(\(?|:?).*/\1/p' | tr '[:upper:]' '[:lower:]' } get_commit_scope() { echo "$1" | sed -r -n 's/[a-z]+\(([^)]+)\).*/\1/p' } get_commit_description() { echo "$1" | sed -r -n 's/.*:[ ]+(.*)/\1/p' } get_commit_footer() { with_colon=$(echo "$1" | sed -n '/^[A-Za-z-]\{1,\}: /,//{p;}') with_hash=$(echo "$1" | sed -n '/^[A-Za-z-]\{1,\} #/,//{p;}') fallback=$(echo "$1" | reverse | awk '/^$/{exit}1' | reverse) if [ -z "$with_colon" ] && [ -z "$with_hash" ]; then echo "$fallback" elif [ ${#with_colon} -gt ${#with_hash} ]; then echo "$with_colon" else echo "$with_hash" fi } get_commit_body() { pattern=$(get_commit_footer "$1" | head -n 1 | sed -e 's/[]\/$*.^[]/\\&/g') if [ -n "$pattern" ]; then echo "$1" | sed -e "/$pattern/,\$d" else echo "$1" fi } is_breaking_change() { if echo "$1" | grep -q '!' || echo "$2" | grep -q 'BREAKING[ -]CHANGE: '; then echo 1 else echo 0 fi } get_breaking_change() { with_colon=$(echo "$1" | sed -n '/^BREAKING[ -]CHANGE: /,/^[A-Za-z-]\{1,\}: /{ s/BREAKING[ -]CHANGE: \(.*\)/\1/; /^[A-Za-z-]\{1,\}: /!p;}') with_hash=$(echo "$1" | sed -n '/^BREAKING[ -]CHANGE: /,/^[A-Za-z-]\{1,\} #/{ s/BREAKING[ -]CHANGE: \(.*\)/\1/; /^[A-Za-z-]\{1,\} #/!p;}') if [ -n "$with_colon" ] && ! echo "$with_colon" | grep -q '[A-Za-z-]\{1,\} #'; then echo "$with_colon" else echo "$with_hash" fi } format_entry_line() { line="" if [ -n "$2" ]; then line="**$2:** " fi line="$line$1" echo "$line ([$3]($(get_config_url)/commit/$3))" | sed -e '2,$s/^[ ]*/ /' } breaking_change_lines="" build_lines="" chore_lines="" ci_lines="" doc_lines="" feature_lines="" bugfix_lines="" perf_lines="" refactor_lines="" revert_lines="" style_lines="" test_lines="" add_changelog_entry_line() { case "$1" in build) build_lines=$(printf '%s\n* %s\n' "$build_lines" "$2") ;; chore) chore_lines=$(printf '%s\n* %s\n' "$chore_lines" "$2") ;; ci) ci_lines=$(printf '%s\n* %s\n' "$ci_lines" "$2") ;; docs) doc_lines=$(printf '%s\n* %s\n' "$doc_lines" "$2") ;; feat) feature_lines=$(printf '%s\n* %s\n' "$feature_lines" "$2") ;; fix) bugfix_lines=$(printf '%s\n* %s\n' "$bugfix_lines" "$2") ;; perf) perf_lines=$(printf '%s\n* %s\n' "$perf_lines" "$2") ;; refactor) refactor_lines=$(printf '%s\n* %s\n' "$refactor_lines" "$2") ;; revert) revert_lines=$(printf '%s\n* %s\n' "$revert_lines" "$2") ;; style) style_lines=$(printf '%s\n* %s\n' "$style_lines" "$2") ;; test) test_lines=$(printf '%s\n* %s\n' "$test_lines" "$2") ;; esac } format_entry() { entry="" if [ -n "$breaking_change_lines" ]; then entry=$(printf '%s\n\n\n### BREAKING CHANGES ๐Ÿšจ\n%s\n' "$entry" "$breaking_change_lines") fi if [ -n "$bugfix_lines" ]; then entry=$(printf '%s\n\n\n### Bug fixes ๐Ÿฉน\n%s\n' "$entry" "$bugfix_lines") fi if [ -n "$feature_lines" ]; then entry=$(printf '%s\n\n\n### Features ๐Ÿ“ฆ\n%s\n' "$entry" "$feature_lines") fi if [ -n "$revert_lines" ]; then entry=$(printf '%s\n\n\n### Reverts ๐Ÿ”™\n%s\n' "$entry" "$revert_lines") fi if [ -n "$build_lines" ]; then entry=$(printf '%s\n\n\n### Build System ๐Ÿ—\n%s\n' "$entry" "$build_lines") fi if [ -n "$chore_lines" ]; then entry=$(printf '%s\n\n\n### Chores ๐Ÿงฝ\n%s\n' "$entry" "$chore_lines") fi if [ -n "$ci_lines" ]; then entry=$(printf '%s\n\n\n### CIs โš™๏ธ\n%s\n' "$entry" "$ci_lines") fi if [ -n "$doc_lines" ]; then entry=$(printf '%s\n\n\n### Docs ๐Ÿ“‘\n%s\n' "$entry" "$doc_lines") fi if [ -n "$test_lines" ]; then entry=$(printf '%s\n\n\n### Tests ๐Ÿงช\n%s\n' "$entry" "$test_lines") fi if [ -n "$style_lines" ]; then entry=$(printf '%s\n\n\n### Styles ๐Ÿ–ผ\n%s\n' "$entry" "$style_lines") fi if [ -n "$refactor_lines" ]; then entry=$(printf '%s\n\n\n### Refactors ๐Ÿ› \n%s\n' "$entry" "$refactor_lines") fi if [ -n "$perf_lines" ]; then entry=$(printf '%s\n\n\n### Performance ๐ŸŽ\n%s\n' "$entry" "$perf_lines") fi if [ -n "$entry" ]; then entry=$(printf '## [%s](%s/compare/%s...%s) (%s)%s' "$version" "$(get_config_url)" "$from_hash" "$to_hash" "$(date '+%Y-%m-%d')" "$entry") fi echo "$entry" } generate_changelog_entry() { is_major=0 is_minor=0 is_patch=0 version=$(get_config_version) from_hash=$(get_config_sha) if [ -z "$from_hash" ]; then from_hash=$(get_git_first_commit_hash) fi to_hash=$(get_git_commit_hash HEAD) for commit in $(get_git_commits); do git_subject=$(get_git_commit_subject "$commit") git_body=$(get_git_commit_body "$commit") hash=$(get_git_commit_hash "$commit") type=$(get_commit_type "$git_subject") scope=$(get_commit_scope "$git_subject") description=$(get_commit_description "$git_subject") body=$(get_commit_body "$git_body") footer=$(get_commit_footer "$git_body") breaking_change=$(get_breaking_change "$footer") is_breaking=$(is_breaking_change "$git_subject" "$footer") line=$(format_entry_line "$description" "$scope" "$hash") if [ "$is_breaking" -eq 1 ] && [ -z "$breaking_change" ]; then if [ -n "$body" ]; then breaking_change="$body" else breaking_change="$description" fi fi if [ "$is_breaking" -eq 1 ]; then is_major=1 breaking_change=$(echo "$breaking_change" | sed -e '2,$s/^[ ]*/ /') breaking_change_lines=$(printf '%s\n* %s\n' "$breaking_change_lines" "$breaking_change") fi case "$type" in feat) is_minor=1 ;; fix) is_patch=1 ;; esac add_changelog_entry_line "$type" "$line" done version_update=$((is_major * 100 + is_minor * 10 + is_patch)) if [ $version_update -gt 0 ]; then version=$(calculate_version "$version" "$version_update") else # no version change, no need to generate changelog return fi update_config "$to_hash" "$version" format_entry } new_changelog() { echo "$changelog_header" >$changelog_file } new_changelog_entry() { entry=$(generate_changelog_entry) if [ -z "$entry" ]; then return fi changelog=$(sed -n '/^----/,//{/^----/!p;}' <$changelog_file) if [ -n "$changelog" ]; then changelog=$(printf '%s\n%s\n%s' "$changelog_header" "$entry" "$changelog") else changelog=$(printf '%s\n%s' "$changelog_header" "$entry") fi echo "$changelog" >$changelog_file } init() { if [ "$1" = "--help" ]; then echo "$(basename "$0") [--help] [init|get_version]" exit 0 elif [ "$1" = "init" ]; then echo "Generating a config ... " echo "We guessed that the url for git links will be: $(get_git_url)" echo "Please change inside the '${config_file}' file if needed!" generate_config "$(get_git_url)" new_changelog exit 0 fi if [ ! -f "$config_file" ]; then echo >&2 "ERROR: config file is missing to generate changelog entry" exit 1 fi if [ "$1" = "get_version" ]; then get_config_version exit 0 fi echo "Using existing config to generate changelog entry" new_changelog_entry } init "$@"