#!/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() { config=$(jq '.sha |= $sha | .version |= $version' --arg sha "$1" --arg version "$2" $config_file) echo "$config" > $config_file } calculate_version() { 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_identifier=$(echo "$pre_release." | cut -d'.' -f2) pre_release="" build="" case "$2" in major) if [ -n "$major" ]; then major=$((major+1)) minor=0 patch=0 fi ;; minor) if [ -n "$minor" ]; then minor=$((minor+1)) patch=0 fi ;; patch) if [ -n "$patch" ]; then patch=$((patch+1)) fi ;; rc) 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" ;; esac if [ "$3" = "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" fi echo "$url" | sed 's/\.git$//' } get_git_commits() { since_hash="$(jq '.sha' < $config_file | tr -d '"')" 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) 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() { if [ -n "$2" ]; then line="**$2:** $1" else line="$1" fi url="$(jq '.url' < $config_file | tr -d '"')" echo "$line ([$3]($url/commit/$3))" | sed -e '2,$s/^[ ]*/ /' } generate_changelog_entry() { is_major=0 is_minor=0 is_patch=0 version=$(jq '.version' < $config_file | tr -d '"') from_hash=$(jq '.sha' < $config_file | tr -d '"') if [ -z "$from_hash" ]; then from_hash=$(get_git_first_commit_hash) fi to_hash=$(get_git_commit_hash HEAD) breakingchanges="" features="" bugfixes="" builds="" chores="" cis="" docs="" styles="" refactors="" perfs="" tests="" reverts="" 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") breakingchange=$(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 "$breakingchange" ]; then breakingchange="$body" fi if [ "$is_breaking" -eq 1 ]; then is_major=1 breakingchange=$(echo "$breakingchange" | sed -e '2,$s/^[ ]*/ /') breakingchanges="$breakingchanges\n* $breakingchange\n" fi case "$type" in feat) is_minor=1 features="$features\n* $line\n" ;; fix) is_patch=1 bugfixes="$bugfixes\n* $line\n" ;; build) builds="$builds\n* $line\n" ;; chore) chores="$chores\n* $line\n" ;; ci) cis="$cis\n* $line\n" ;; docs) docs="$docs\n* $line\n" ;; style) styles="$styles\n* $line\n" ;; refactor) refactors="$refactors\n* $line\n" ;; perf) perfs="$perfs\n* $line\n" ;; test) tests="$tests\n* $line\n" ;; revert) reverts="$reverts\n* $line\n" ;; esac done if [ $is_major -eq 1 ]; then version=$(calculate_version "$version" "major") elif [ $is_minor -eq 1 ]; then version=$(calculate_version "$version" "minor") elif [ $is_patch -eq 1 ]; then version=$(calculate_version "$version" "patch") fi update_config "$to_hash" "$version" entry="" if [ -n "$breakingchanges" ]; then entry="$entry### BREAKING CHANGES\n$breakingchanges\n\n" fi if [ -n "$bugfixes" ]; then entry="$entry### Bug fixes\n$bugfixes\n\n" fi if [ -n "$features" ]; then entry="$entry### Features\n$features\n\n" fi if [ -n "$reverts" ]; then entry="$entry### Reverts\n$reverts\n\n" fi if [ -n "$builds" ]; then entry="$entry### Build System\n$builds\n\n" fi if [ -n "$chores" ]; then entry="$entry### Chores\n$chores\n\n" fi if [ -n "$cis" ]; then entry="$entry### CIs\n$cis\n\n" fi if [ -n "$docs" ]; then entry="$entry### Docs\n$docs\n\n" fi if [ -n "$tests" ]; then entry="$entry### Tests\n$tests\n\n" fi if [ -n "$styles" ]; then entry="$entry### Styles\n$styles\n\n" fi if [ -n "$refactors" ]; then entry="$entry### Refactors\n$refactors\n\n" fi if [ -n "$perfs" ]; then entry="$entry### Perfs\n$perfs\n\n" fi if [ -n "$entry" ]; then url="$(jq '.url' < $config_file | tr -d '"')" entry="## [$version]($url/compare/$from_hash...$to_hash) ($(date '+%Y-%m-%d'))\n\n\n$entry" fi echo "$entry" } new_changelog() { echo "$changelog_header" > $changelog_file } new_changelog_entry() { entry=$(generate_changelog_entry) if [ -n "$entry" ]; then changelog=$(sed -n '/^----/,//{/^----/!p;}' < $changelog_file) if [ -n "$changelog" ]; then changelog="$changelog_header\n$entry\n$changelog" else changelog="$changelog_header\n$entry" fi echo "$changelog" > $changelog_file fi } init() { if [ ! -f "$config_file" ]; then echo "Generating a config ... " generate_config "$(get_git_url)" new_changelog else echo "Using existing config" fi new_changelog_entry } init