305 lines
7.4 KiB
Bash
Executable File
305 lines
7.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Script: body-composer.sh
|
|
# Purpose: Compose commit message body with proper formatting and wrapping
|
|
# Author: Git Commit Assistant Plugin
|
|
# Version: 1.0.0
|
|
#
|
|
# Usage:
|
|
# export CHANGES="change1,change2,change3"
|
|
# export WRAP_LENGTH=72
|
|
# export FORMAT=bullets
|
|
# ./body-composer.sh
|
|
#
|
|
# Environment Variables:
|
|
# CHANGES - Comma-separated list of changes or file paths
|
|
# WRAP_LENGTH - Line wrap length (default: 72)
|
|
# FORMAT - Output format: bullets or paragraphs (default: bullets)
|
|
# WHY_CONTEXT - Optional context about why changes were made
|
|
#
|
|
# Returns:
|
|
# Formatted body text to stdout
|
|
# JSON summary to stderr (optional)
|
|
#
|
|
# Exit Codes:
|
|
# 0 - Success
|
|
# 1 - Invalid input
|
|
# 2 - Processing error
|
|
|
|
set -euo pipefail
|
|
|
|
# Default values
|
|
WRAP_LENGTH="${WRAP_LENGTH:-72}"
|
|
FORMAT="${FORMAT:-bullets}"
|
|
WHY_CONTEXT="${WHY_CONTEXT:-}"
|
|
|
|
# Validate CHANGES is provided
|
|
if [ -z "${CHANGES:-}" ]; then
|
|
echo "ERROR: CHANGES environment variable is required" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Validate FORMAT
|
|
if [ "$FORMAT" != "bullets" ] && [ "$FORMAT" != "paragraphs" ]; then
|
|
echo "ERROR: FORMAT must be 'bullets' or 'paragraphs'" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Validate WRAP_LENGTH
|
|
if ! [[ "$WRAP_LENGTH" =~ ^[0-9]+$ ]]; then
|
|
echo "ERROR: WRAP_LENGTH must be a positive integer" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Function to convert to imperative mood
|
|
convert_to_imperative() {
|
|
local text="$1"
|
|
|
|
# Common past tense -> imperative
|
|
text=$(echo "$text" | sed -E 's/\b(added|adds)\b/add/gi')
|
|
text=$(echo "$text" | sed -E 's/\b(fixed|fixes)\b/fix/gi')
|
|
text=$(echo "$text" | sed -E 's/\b(updated|updates)\b/update/gi')
|
|
text=$(echo "$text" | sed -E 's/\b(removed|removes)\b/remove/gi')
|
|
text=$(echo "$text" | sed -E 's/\b(changed|changes)\b/change/gi')
|
|
text=$(echo "$text" | sed -E 's/\b(improved|improves)\b/improve/gi')
|
|
text=$(echo "$text" | sed -E 's/\b(refactored|refactors)\b/refactor/gi')
|
|
text=$(echo "$text" | sed -E 's/\b(implemented|implements)\b/implement/gi')
|
|
text=$(echo "$text" | sed -E 's/\b(created|creates)\b/create/gi')
|
|
text=$(echo "$text" | sed -E 's/\b(deleted|deletes)\b/delete/gi')
|
|
|
|
# Lowercase first letter
|
|
text="$(echo "${text:0:1}" | tr '[:upper:]' '[:lower:]')${text:1}"
|
|
|
|
echo "$text"
|
|
}
|
|
|
|
# Function to wrap text at specified length
|
|
wrap_text() {
|
|
local text="$1"
|
|
local width="$2"
|
|
|
|
echo "$text" | fold -s -w "$width"
|
|
}
|
|
|
|
# Function to format file path as readable change
|
|
format_file_path() {
|
|
local filepath="$1"
|
|
|
|
# Extract filename and directory
|
|
local filename=$(basename "$filepath")
|
|
local dirname=$(dirname "$filepath")
|
|
|
|
# Remove extension
|
|
local name_no_ext="${filename%.*}"
|
|
|
|
# Convert to readable format
|
|
# Example: src/auth/oauth.js -> "add OAuth authentication module"
|
|
# Example: tests/unit/user.test.js -> "add user unit tests"
|
|
|
|
if [[ "$filepath" == *"/test/"* ]] || [[ "$filepath" == *"/tests/"* ]] || [[ "$filename" == *".test."* ]] || [[ "$filename" == *".spec."* ]]; then
|
|
echo "add ${name_no_ext} tests"
|
|
elif [[ "$dirname" == "." ]]; then
|
|
echo "update ${name_no_ext}"
|
|
else
|
|
# Extract meaningful part from path
|
|
local component=$(echo "$dirname" | sed 's|.*/||')
|
|
echo "update ${component} ${name_no_ext}"
|
|
fi
|
|
}
|
|
|
|
# Function to generate bullet points
|
|
generate_bullets() {
|
|
local changes_list="$1"
|
|
|
|
# Split by comma
|
|
IFS=',' read -ra items <<< "$changes_list"
|
|
|
|
local body=""
|
|
local bullet_count=0
|
|
|
|
for item in "${items[@]}"; do
|
|
# Trim whitespace
|
|
item=$(echo "$item" | xargs)
|
|
|
|
if [ -z "$item" ]; then
|
|
continue
|
|
fi
|
|
|
|
# Check if it's a file path
|
|
if [[ "$item" == *"/"* ]] || [[ "$item" == *"."* ]]; then
|
|
# Format as file path
|
|
item=$(format_file_path "$item")
|
|
fi
|
|
|
|
# Convert to imperative mood
|
|
item=$(convert_to_imperative "$item")
|
|
|
|
# Ensure first letter is capitalized for bullet
|
|
item="$(echo "${item:0:1}" | tr '[:lower:]' '[:upper:]')${item:1}"
|
|
|
|
# Wrap if needed (account for "- " prefix)
|
|
local max_width=$((WRAP_LENGTH - 2))
|
|
local wrapped=$(wrap_text "$item" "$max_width")
|
|
|
|
# Add bullet point
|
|
echo "$wrapped" | while IFS= read -r line; do
|
|
if [ "$bullet_count" -eq 0 ] || [ -z "$line" ]; then
|
|
body="${body}- ${line}\n"
|
|
else
|
|
body="${body} ${line}\n" # Indent continuation lines
|
|
fi
|
|
done
|
|
|
|
bullet_count=$((bullet_count + 1))
|
|
done
|
|
|
|
# Output body (remove trailing newline)
|
|
echo -ne "$body"
|
|
}
|
|
|
|
# Function to generate paragraphs
|
|
generate_paragraphs() {
|
|
local changes_list="$1"
|
|
|
|
# Split by comma and join into sentences
|
|
IFS=',' read -ra items <<< "$changes_list"
|
|
|
|
local body=""
|
|
|
|
for item in "${items[@]}"; do
|
|
# Trim whitespace
|
|
item=$(echo "$item" | xargs)
|
|
|
|
if [ -z "$item" ]; then
|
|
continue
|
|
fi
|
|
|
|
# Check if it's a file path
|
|
if [[ "$item" == *"/"* ]] || [[ "$item" == *"."* ]]; then
|
|
item=$(format_file_path "$item")
|
|
fi
|
|
|
|
# Convert to imperative mood
|
|
item=$(convert_to_imperative "$item")
|
|
|
|
# Ensure first letter is capitalized
|
|
item="$(echo "${item:0:1}" | tr '[:lower:]' '[:upper:]')${item:1}"
|
|
|
|
# Add to body
|
|
if [ -z "$body" ]; then
|
|
body="$item"
|
|
else
|
|
body="${body}. ${item}"
|
|
fi
|
|
done
|
|
|
|
# Add period at end if not present
|
|
if [[ ! "$body" =~ \.$ ]]; then
|
|
body="${body}."
|
|
fi
|
|
|
|
# Wrap text
|
|
wrapped=$(wrap_text "$body" "$WRAP_LENGTH")
|
|
|
|
echo "$wrapped"
|
|
}
|
|
|
|
# Function to add context (why)
|
|
add_context() {
|
|
local body="$1"
|
|
local context="$2"
|
|
|
|
if [ -z "$context" ]; then
|
|
echo "$body"
|
|
return
|
|
fi
|
|
|
|
# Ensure first letter is capitalized
|
|
context="$(echo "${context:0:1}" | tr '[:lower:]' '[:upper:]')${context:1}"
|
|
|
|
# Add period if not present
|
|
if [[ ! "$context" =~ \.$ ]]; then
|
|
context="${context}."
|
|
fi
|
|
|
|
# Wrap context
|
|
wrapped_context=$(wrap_text "$context" "$WRAP_LENGTH")
|
|
|
|
# Combine with blank line
|
|
echo -e "${body}\n\n${wrapped_context}"
|
|
}
|
|
|
|
# Main composition logic
|
|
compose_body() {
|
|
local body=""
|
|
|
|
# Generate based on format
|
|
if [ "$FORMAT" = "bullets" ]; then
|
|
body=$(generate_bullets "$CHANGES")
|
|
else
|
|
body=$(generate_paragraphs "$CHANGES")
|
|
fi
|
|
|
|
# Add context if provided
|
|
if [ -n "$WHY_CONTEXT" ]; then
|
|
body=$(add_context "$body" "$WHY_CONTEXT")
|
|
fi
|
|
|
|
echo "$body"
|
|
}
|
|
|
|
# Validate body
|
|
validate_body() {
|
|
local body="$1"
|
|
|
|
local line_count=$(echo "$body" | wc -l)
|
|
local longest_line=$(echo "$body" | awk '{ print length }' | sort -rn | head -1)
|
|
local bullet_count=$(echo "$body" | grep -c '^- ' || true)
|
|
|
|
local warnings=()
|
|
|
|
# Check longest line
|
|
if [ "$longest_line" -gt "$WRAP_LENGTH" ]; then
|
|
warnings+=("Line exceeds $WRAP_LENGTH characters ($longest_line chars)")
|
|
fi
|
|
|
|
# Check for empty lines at start
|
|
if echo "$body" | head -1 | grep -q '^$'; then
|
|
warnings+=("Body starts with empty line")
|
|
fi
|
|
|
|
# Output validation summary to stderr
|
|
{
|
|
echo "{"
|
|
echo " \"line_count\": $line_count,"
|
|
echo " \"longest_line\": $longest_line,"
|
|
echo " \"wrap_length\": $WRAP_LENGTH,"
|
|
echo " \"bullet_count\": $bullet_count,"
|
|
echo " \"format\": \"$FORMAT\","
|
|
echo " \"has_context\": $([ -n "$WHY_CONTEXT" ] && echo "true" || echo "false"),"
|
|
echo " \"warnings\": ["
|
|
for i in "${!warnings[@]}"; do
|
|
echo " \"${warnings[$i]}\"$([ $i -lt $((${#warnings[@]} - 1)) ] && echo "," || echo "")"
|
|
done
|
|
echo " ],"
|
|
echo " \"valid\": $([ ${#warnings[@]} -eq 0 ] && echo "true" || echo "false")"
|
|
echo "}"
|
|
} >&2
|
|
}
|
|
|
|
# Main execution
|
|
main() {
|
|
# Compose body
|
|
body=$(compose_body)
|
|
|
|
# Validate
|
|
validate_body "$body"
|
|
|
|
# Output body to stdout
|
|
echo "$body"
|
|
|
|
exit 0
|
|
}
|
|
|
|
# Run main
|
|
main
|