#!/usr/bin/env bash # Generate changelog from conventional commits # Usage: gc-changelog [from_ref] [to_ref] # gc-changelog # all commits # gc-changelog v1.0.0 # from tag to HEAD # gc-changelog v1.0.0 v2.0.0 # between tags set -o errexit set -o nounset from_ref=${1:-} to_ref=${2:-HEAD} if [ -n "$from_ref" ]; then range="$from_ref..$to_ref" else range="" fi # Collect commits by type declare -A titles=( [feat]="Features" [fix]="Bug Fixes" [docs]="Documentation" [style]="Styles" [refactor]="Refactoring" [test]="Tests" [chore]="Chores" [perf]="Performance" [ci]="CI" [build]="Build" ) declare -A commits declare -a breaking_changes for type in feat fix docs style refactor test chore perf ci build; do commits[$type]="" done # Parse commits # Full pattern: type(scope)!: description while IFS= read -r line; do [ -z "$line" ] && continue msg=$(echo "$line" | cut -d' ' -f2-) for type in feat fix docs style refactor test chore perf ci build; do # Match: type, optional (scope), optional !, colon, space if echo "$msg" | grep -qE "^$type(\([^)]+\))?!?: "; then is_breaking=false if echo "$msg" | grep -qE "^$type(\([^)]+\))?!:"; then is_breaking=true fi # Extract scope and description if echo "$msg" | grep -qE "^$type\([^)]+\)"; then scope=$(echo "$msg" | sed -E "s/^$type\(([^)]+)\)!?: .*/\1/") desc=$(echo "$msg" | sed -E "s/^$type\([^)]+\)!?: //") else scope="" desc=$(echo "$msg" | sed -E "s/^$type!?: //") fi # Format the entry if [ -n "$scope" ]; then entry="- **$scope**: $desc" else entry="- $desc" fi if [ "$is_breaking" = true ]; then breaking_changes+=("$entry") fi commits[$type]+="$entry"$'\n' break fi done done < <(git log --oneline $range) # Output changelog echo "# Changelog" echo "" # Breaking changes first if [ ${#breaking_changes[@]} -gt 0 ]; then echo "## BREAKING CHANGES" echo "" for change in "${breaking_changes[@]}"; do echo "$change" done echo "" fi # Then by type for type in feat fix perf refactor docs style test chore ci build; do if [ -n "${commits[$type]}" ]; then echo "## ${titles[$type]}" echo "" echo -n "${commits[$type]}" echo "" fi done