diff --git a/script/changelog-generator.sh b/script/changelog-generator.sh index acbdd54b..337132ed 100755 --- a/script/changelog-generator.sh +++ b/script/changelog-generator.sh @@ -3,64 +3,42 @@ set -euo pipefail -# Source shared output library SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" && pwd)" # shellcheck source=/dev/null source "$SCRIPT_DIR/lib/output.sh" 2>/dev/null || { readonly RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' } -# Options SINCE_TAG="" OUTPUT_FILE="CHANGELOG.md" INCLUDE_ALL=false DRY_RUN=false SHOW_CONTRIBUTORS=false -# Parse arguments +show_help() { + cat < /dev/null 2>&1; then echo -e "${RED}✗ Not in a git repository${NC}" exit 1 fi -# Get repository info REPO_URL=$(git config --get remote.origin.url | sed 's/\.git$//' | sed 's/git@github.com:/https:\/\/github.com\//') -# Determine range -if [ -z "$SINCE_TAG" ]; then - LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") - if [ -n "$LATEST_TAG" ]; then - SINCE_TAG="$LATEST_TAG" - echo "📌 Latest tag: $LATEST_TAG" +resolve_since_tag() { + local latest + if [ -n "$SINCE_TAG" ]; then + echo "📌 Generating since: $SINCE_TAG" + return + fi + latest=$(git describe --tags --abbrev=0 2>/dev/null || echo "") + if [ -n "$latest" ]; then + SINCE_TAG="$latest" + echo "📌 Latest tag: $latest" else echo "📌 No tags found, generating from all commits" - SINCE_TAG="" fi -else - echo "📌 Generating since: $SINCE_TAG" -fi +} +resolve_since_tag echo "" -# Generate changelog content -CHANGELOG_CONTENT="# Changelog +declare -A FEATURES FIXES PERF DOCS BREAKING OTHER -All notable changes to this project will be documented in this file. +categorize_commit() { + local hash=$1 subject=$2 body=$3 + local regex='^([a-z]+)(\([^)]+\))?: (.+)$' + [[ "$subject" =~ $regex ]] || return 0 + local type="${BASH_REMATCH[1]}" + local message="${BASH_REMATCH[3]}" -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Conventional Commits](https://conventionalcommits.org/). + echo "$body" | grep -q "BREAKING CHANGE" && BREAKING["$hash"]="$message" -" + case "$type" in + feat) FEATURES["$hash"]="$message" ;; + fix) FIXES["$hash"]="$message" ;; + perf) PERF["$hash"]="$message" ;; + docs) DOCS["$hash"]="$message" ;; + *) [[ "$INCLUDE_ALL" == "true" ]] && OTHER["$hash"]="$message ($type)" ;; + esac +} -# Get commits if [ -n "$SINCE_TAG" ]; then COMMITS=$(git log "$SINCE_TAG"..HEAD --pretty=format:"%H|%s|%b" 2>/dev/null || git log --pretty=format:"%H|%s|%b") else COMMITS=$(git log --pretty=format:"%H|%s|%b") fi -# Group commits by type -declare -A FEATURES -declare -A FIXES -declare -A PERF -declare -A DOCS -declare -A BREAKING -declare -A OTHER - while IFS='|' read -r hash subject body; do - # Extract type from conventional commit - regex='^([a-z]+)(\([^)]+\))?: (.+)$' - if [[ "$subject" =~ $regex ]]; then - TYPE="${BASH_REMATCH[1]}" - MESSAGE="${BASH_REMATCH[3]}" - - # Check for breaking changes - if echo "$body" | grep -q "BREAKING CHANGE"; then - BREAKING["$hash"]="$MESSAGE" - fi - - # Categorize - case "$TYPE" in - feat) - FEATURES["$hash"]="$MESSAGE" - ;; - fix) - FIXES["$hash"]="$MESSAGE" - ;; - perf) - PERF["$hash"]="$MESSAGE" - ;; - docs) - DOCS["$hash"]="$MESSAGE" - ;; - *) - if [ "$INCLUDE_ALL" = true ]; then - OTHER["$hash"]="$MESSAGE ($TYPE)" - fi - ;; - esac - fi + categorize_commit "$hash" "$subject" "$body" done <<< "$COMMITS" -# Add unreleased section -CHANGELOG_CONTENT+="## [Unreleased] - -" - -# Breaking changes first -if [ ${#BREAKING[@]} -gt 0 ]; then - CHANGELOG_CONTENT+="### BREAKING CHANGES - -" - for hash in "${!BREAKING[@]}"; do - SHORT_HASH=$(echo "$hash" | cut -c1-7) - CHANGELOG_CONTENT+="- ${BREAKING[$hash]} ([$SHORT_HASH]($REPO_URL/commit/$hash)) -" - done - CHANGELOG_CONTENT+=" -" -fi - -# Features -if [ ${#FEATURES[@]} -gt 0 ]; then - CHANGELOG_CONTENT+="### Features - -" - for hash in "${!FEATURES[@]}"; do - SHORT_HASH=$(echo "$hash" | cut -c1-7) - MESSAGE="${FEATURES[$hash]}" - - # Check for PR reference - if [[ "$MESSAGE" =~ \(#([0-9]+)\) ]]; then - PR_NUM="${BASH_REMATCH[1]}" - MESSAGE=$(echo "$MESSAGE" | sed "s/(#$PR_NUM)/([#$PR_NUM]($REPO_URL\/pull\/$PR_NUM))/") - fi - - CHANGELOG_CONTENT+="- $MESSAGE ([$SHORT_HASH]($REPO_URL/commit/$hash)) -" - done - CHANGELOG_CONTENT+=" -" -fi - -# Bug fixes -if [ ${#FIXES[@]} -gt 0 ]; then - CHANGELOG_CONTENT+="### Bug Fixes - -" - for hash in "${!FIXES[@]}"; do - SHORT_HASH=$(echo "$hash" | cut -c1-7) - MESSAGE="${FIXES[$hash]}" - - if [[ "$MESSAGE" =~ \(#([0-9]+)\) ]]; then - PR_NUM="${BASH_REMATCH[1]}" - MESSAGE=$(echo "$MESSAGE" | sed "s/(#$PR_NUM)/([#$PR_NUM]($REPO_URL\/pull\/$PR_NUM))/") - fi +format_message() { + local msg=$1 + if [[ "$msg" =~ \(#([0-9]+)\) ]]; then + local pr="${BASH_REMATCH[1]}" + msg="${msg//(#$pr)/([#$pr]($REPO_URL/pull/$pr))}" + fi + echo "$msg" +} - CHANGELOG_CONTENT+="- $MESSAGE ([$SHORT_HASH]($REPO_URL/commit/$hash)) -" +# Render a section: title and array name (indirect access avoids nameref + set -u quirk). +render_section() { + local title=$1 name=$2 + local size_var="${name}[@]" + local -a keys=() + eval "keys=(\"\${!${name}[@]}\")" + [[ ${#keys[@]} -eq 0 ]] && return 0 + printf '### %s\n\n' "$title" + local hash short msg val_var + for hash in "${keys[@]}"; do + short=${hash:0:7} + val_var="${name}[$hash]" + msg=$(format_message "${!val_var}") + printf -- '- %s ([%s](%s/commit/%s))\n' "$msg" "$short" "$REPO_URL" "$hash" done - CHANGELOG_CONTENT+=" -" -fi - -# Performance -if [ ${#PERF[@]} -gt 0 ]; then - CHANGELOG_CONTENT+="### Performance Improvements + printf '\n' + : "${size_var}" # silence shellcheck "unused" +} -" - for hash in "${!PERF[@]}"; do - SHORT_HASH=$(echo "$hash" | cut -c1-7) - CHANGELOG_CONTENT+="- ${PERF[$hash]} ([$SHORT_HASH]($REPO_URL/commit/$hash)) -" - done - CHANGELOG_CONTENT+=" -" -fi +build_changelog() { + cat <
"$OUTPUT_FILE" echo -e "${GREEN}✓${NC} Changelog written to $OUTPUT_FILE" - - # Show stats - TOTAL_ENTRIES=$((${#FEATURES[@]} + ${#FIXES[@]} + ${#PERF[@]} + ${#DOCS[@]} + ${#OTHER[@]})) echo "" echo "📊 Statistics:" echo " • Features: ${#FEATURES[@]}" echo " • Bug Fixes: ${#FIXES[@]}" echo " • Performance: ${#PERF[@]}" echo " • Documentation: ${#DOCS[@]}" - if [ "$INCLUDE_ALL" = true ]; then - echo " • Other: ${#OTHER[@]}" - fi + [[ "$INCLUDE_ALL" == "true" ]] && echo " • Other: ${#OTHER[@]}" echo " • Breaking Changes: ${#BREAKING[@]}" - echo " • Total Entries: $TOTAL_ENTRIES" + TOTAL=$((${#FEATURES[@]} + ${#FIXES[@]} + ${#PERF[@]} + ${#DOCS[@]} + ${#OTHER[@]})) + echo " • Total Entries: $TOTAL" fi