|
#!/usr/bin/env bash |
|
#DEBUG=true |
|
|
|
# ----------------------------------------------------------------------------- |
|
# Git AI Commit Message Hook |
|
# ----------------------------------------------------------------------------- |
|
# Template / Source: |
|
# Based on: |
|
# - https://gist.github.com/vielhuber/0d311d3e3743f65bd1f7fa370c3b3848 |
|
# - https://vielhuber.de/blog/git-commit-messages-mit-chatgpt/ |
|
# |
|
# Automatically generates commit messages using an AI model based on staged |
|
# git diff output. Filters noise (node_modules, vendor, bundles), builds a |
|
# structured prompt (including commit format rules in user.intro), sends it |
|
# to an OpenAI-compatible /chat/completions API, and writes the generated |
|
# commit message back to the Git commit file. |
|
# |
|
# Configuration (via git config): |
|
# git config --global openai.base-url "https://api.openai.com/v1" |
|
# git config --global openai.model "gpt-4o" |
|
# git config --global openai.api-key "YOUR_API_KEY" |
|
# git config --global --type=bool openai.autocommit |
|
# |
|
# Supported /chat/completions APIs: |
|
# https://api.openai.com/v1/chat/completions |
|
# https://api.llm7.io/v1/chat/completions |
|
# https://hermes.ai.unturf.com/v1/chat/completions |
|
# https://openrouter.ai/api/v1/chat/completions |
|
# Custom OpenAI-compatible endpoints (self-hosted or proxy services) |
|
# |
|
# Output: |
|
# - Generates an AI-powered commit message from staged changes |
|
# - Writes the message into the Git commit message file |
|
# - Appends a structured summary of staged changes |
|
# - Opens the commit message editor after generation for final review/editing |
|
# |
|
# Safety Notes: |
|
# - Excludes large/generated directories (node_modules, vendor, bundles) |
|
# - Avoids sending overly large diffs to the API |
|
# - Logs request/response to /tmp for debugging purposes |
|
# - Handles API, curl, and network failures gracefully |
|
# ----------------------------------------------------------------------------- |
|
|
|
|
|
declare -A prompts=( |
|
["system"]=" |
|
You are a professional code reviewer. |
|
" |
|
["user.intro"]=" |
|
Please review a pull request by summarizing the changes made |
|
with the help of a bullet list of items that have been changed. |
|
Instructions: |
|
- Review the given output of \"git diff\" for the pull request |
|
- Describe only the specific code change that was made, not the code above/below it |
|
- If you cannot identify any changes in the diff, return that no changes have been made |
|
- Lines beginning with ‘-’ have been DELETED. |
|
- Lines beginning with ‘+’ have been ADDED. |
|
- Lines without ‘-’ or ‘+’ are just CONTEXT and have NOT been changed. |
|
- Create a concise commit message that describes ONLY the actual changes. |
|
- Describe only the actual changes (deleted and added lines). Ignore context lines that remain unchanged. |
|
- If only a slight change has been made, don't miss that change |
|
- Use the imperative mood in every bullet list item |
|
- Return always a single bullet list |
|
- Don't mention any sensitive information like passwords |
|
- Finish every bullet list item with a \".\" |
|
- Summarize the overview of the changes made |
|
- Don't mention file names |
|
- Create a bullet list of different items |
|
- The response sentences are no longer than 16 words each |
|
- Keep the response sentences as short as possible |
|
- Don't write any headlines and start with the bullet list |
|
If you have prepared the list and it has more than 3 entries, |
|
then merge the entries so that they add up to exactly 3 entries. |
|
If fewer than 3 items were found, list only 1 or 2. |
|
" |
|
["user.pre"]=" |
|
This is the output of \"git diff\": |
|
" |
|
) |
|
|
|
|
|
# Path to the temporary file where Git expects the commit message |
|
COMMIT_MSG_FILE="$1" |
|
|
|
# Commit source/mode passed by Git |
|
# Examples: message, template, merge, squash, etc. |
|
COMMIT_MODE="$2" |
|
|
|
# Current content of the commit message file |
|
EXISTING_MSG=$(<"$COMMIT_MSG_FILE") |
|
|
|
# Debug log files |
|
DATA_LOG="/tmp/prepare-commit-msg-data.log" # request payload |
|
OUTPUT_LOG="/tmp/prepare-commit-msg-output.log" # API response |
|
DIFF_LOG="/tmp/prepare-commit-msg-diff.log" # staged diff content |
|
CURL_LOG="/tmp/prepare-commit-msg-curl.log" # logs full curl command with arguments |
|
|
|
# Network timeout settings |
|
CONNECTION_TIMEOUT=10 # Maximum time to establish connection |
|
REQUEST_TIMEOUT=10 # Total request lifetime (connect + send + receive) |
|
|
|
# Path where stderr will be logged in non-debug mode |
|
STDERR_LOG="/tmp/prepare-commit-msg-stderr.log" |
|
|
|
# Metadata section line (n '#') |
|
HASHES_LINE="$(printf '%*s' 50 '' | tr ' ' '#')" |
|
|
|
# Internal marker used to locate metadata during cleanup operations |
|
META_MARKER=$'\u200b\u200d\u200b' |
|
|
|
|
|
# Enable strict mode: |
|
# -E : ERR trap is inherited by functions/subshells |
|
# -e : exit immediately on command failure |
|
# -u : treat unset variables as errors |
|
# -o pipefail : pipeline fails if any command fails |
|
#set -Eeuo pipefail |
|
set -Euo pipefail |
|
|
|
# On any command error, print a formatted error message with line number and failing command |
|
#trap 'printf "%b" "\n❌ Error at line $LINENO: $BASH_COMMAND"' ERR |
|
|
|
# Always run cleanup function on script exit (success or failure) |
|
trap 'stderr_tap_stop' EXIT |
|
|
|
# If DEBUG is not set or is explicitly "false" |
|
if [[ "${DEBUG:-false}" == false ]]; then |
|
# Redirect FD 3 to append to the log file |
|
# (FD 3 is an extra file descriptor we use as a reference point) |
|
exec 3>> "$STDERR_LOG" |
|
|
|
# Redirect stderr (FD 2) to FD 3 |
|
# → all error output goes into /tmp/stderr.log |
|
exec 2>&3 |
|
fi |
|
|
|
|
|
# Load OpenAI git config, validate required values |
|
function get_config() { |
|
printf "%-*s" $STEP_DESCRIPTION_LEN "Read git config" |
|
|
|
AI_BASEURL="$(git config --get openai.base-url)" |
|
AI_MODEL="$(git config --get openai.model)" |
|
AI_API_KEY="$(git config --get openai.api-key || echo '')" |
|
|
|
if [ -z "$AI_BASEURL" ] || [ -z "$AI_MODEL" ]; then |
|
return 1 |
|
fi |
|
} |
|
|
|
# Fetch the staged git diff with unified context of 10 lines, no color and strip out all lines longer than 1000 chars |
|
function get_staged_diffs() { |
|
printf "%-*s" $STEP_DESCRIPTION_LEN "Extract staged code changes" |
|
|
|
diff=$( |
|
set -o pipefail |
|
git diff \ |
|
--unified=10 \ |
|
--staged \ |
|
--no-color | \ |
|
sed \ |
|
-e '/^diff --git.*node_modules/,/^diff --git/d' \ |
|
-e '/^diff --git.*vendor/,/^diff --git/d' \ |
|
-e '/^diff --git.*bundle\.\(js\|css\)/,/^diff --git/d' \ |
|
-e '/.\{1000\}./d' |
|
) |
|
rc=$? |
|
|
|
# Log the diff for reference |
|
echo "$diff" > "$DIFF_LOG" |
|
# Add diff to prompts array |
|
prompts["user.ai"]="$diff" |
|
|
|
return "$rc" |
|
} |
|
|
|
function prepare_json_payload() { |
|
printf "%-*s" $STEP_DESCRIPTION_LEN "Prepare paylod" |
|
|
|
# Escape special characters for JSON formatting |
|
for k in "${!prompts[@]}"; do |
|
prompts[$k]=$(sed -r 's/\\/\\\\/g' <<< "${prompts[$k]}") # \ |
|
prompts[$k]=$(sed -r 's/"/\\\"/g' <<< "${prompts[$k]}") # " |
|
prompts[$k]=$(sed ':a;N;$!ba;s/\n/\\n/g' <<< "${prompts[$k]}") # \n |
|
prompts[$k]=$(sed 's/\t/ /g' <<< "${prompts[$k]}") # tabs |
|
done |
|
|
|
# Trim strings |
|
for k in "${!prompts[@]}"; do |
|
if [[ "$k" != "user.ai" ]]; then |
|
prompts[$k]=$(sed -r 's/\\n( +)/\\n/g' <<< "${prompts[$k]}") # Remove blank space |
|
prompts[$k]=$(sed -r 's/^\\n//g' <<< "${prompts[$k]}") # Remove first line break |
|
prompts[$k]=$(sed -r 's/\\n$//g' <<< "${prompts[$k]}") # Remove last line break |
|
fi |
|
done |
|
for k in "${!prompts[@]}"; do |
|
prompts[$k]=$(sed -r '/^[[:space:]]*$/d' <<< "${prompts[$k]}") # Remove blank lines |
|
prompts[$k]=$(sed -e 's/^[[:space:]]*//' -e '/^$/d' <<< "${prompts[$k]}") # Remove blank lines |
|
done |
|
|
|
# Prepare the payload for the API request |
|
payload="{ |
|
\"model\": \"$AI_MODEL\", |
|
\"messages\": [ |
|
{ \"role\": \"system\", \"content\": \"${prompts["system"]}\" }, |
|
{ \"role\": \"user\", \"content\": \"${prompts["user.intro"]}\" }, |
|
{ \"role\": \"user\", \"content\": \"${prompts["user.pre"]}\" }, |
|
{ \"role\": \"user\", \"content\": \"${prompts["user.ai"]}\" } |
|
] |
|
}" |
|
|
|
# Save payload to log for debugging purposes |
|
echo "$payload" > "$DATA_LOG" || return 1 |
|
} |
|
|
|
function do_request() { |
|
printf "%-*s" $STEP_DESCRIPTION_LEN "Execute request" |
|
|
|
# Create a unique temp file for response body |
|
tmpfile=$(mktemp) || return 1 |
|
|
|
# Make the API call using curl and capture the response |
|
AI_BASEURL="$(sed -E 's#(/chat/completions)+$#/chat/completions#' <<< "$AI_BASEURL")" |
|
|
|
# Build curl command for AI chat completions request |
|
# (ensures each argument is passed exactly as intended) |
|
curl_args=( |
|
--silent |
|
--output "$tmpfile" |
|
--write-out "%{http_code}" |
|
"$AI_BASEURL/chat/completions" |
|
-H "Content-Type: application/json" |
|
${AI_API_KEY:+-H "Authorization: Bearer $AI_API_KEY"} |
|
--retry 1 |
|
--connect-timeout "$CONNECTION_TIMEOUT" |
|
--max-time "$REQUEST_TIMEOUT" |
|
--data-binary "@$DATA_LOG" |
|
) |
|
|
|
# Write a human-readable debug version of the command for troubleshooting |
|
# Uses shell-escaped arguments so it can be copy-pasted and re-run |
|
{ |
|
echo "curl " |
|
for arg in "${curl_args[@]}"; do |
|
printf ' %q \n' "$arg" |
|
done |
|
} > "$CURL_LOG" |
|
|
|
# Execute request to AI chat completions endpoint and capture HTTP status code via curl --write-out |
|
http_code=$(curl "${curl_args[@]}") |
|
|
|
#http_code=$(curl --silent --output "$tmpfile" --write-out "%{http_code}" \ |
|
# "$AI_BASEURL/chat/completions" \ |
|
# -H "Content-Type: application/json" \ |
|
# ${ai_api_key:+-H "Authorization: Bearer $AI_API_KEY"} \ |
|
# --retry 1 \ |
|
# --connect-timeout $CONNECTION_TIMEOUT \ |
|
# --max-time $REQUEST_TIMEOUT \ |
|
# --data-binary "@$DATA_LOG") |
|
|
|
# Capture curl's exit status (0 = success, non-zero = error) |
|
curl_exit=$? |
|
|
|
# Read response and delete temp file |
|
response=$(<"$tmpfile") && rm -f "$tmpfile" |
|
|
|
# Log the raw response for debugging |
|
echo "$response" > "$OUTPUT_LOG" |
|
|
|
# Exit if response contains errors |
|
if [[ $curl_exit -ne 0 || $http_code -ne 200 || $response == *"\"error\": {"* ]]; then |
|
printf "${RED}✖${RESET} Error calling api (HTTP %s, curl %s)\n" "${http_code:-unknown}" "${curl_exit:-unknown}" |
|
|
|
# Prefer curl_exit if it failed, otherwise HTTP code, fallback to 1 |
|
rc=$curl_exit |
|
if [[ $rc -eq 0 ]]; then |
|
if [[ $http_code != 200 && $http_code != 000 ]]; then |
|
# Shell return codes should be 0–255 (HTTP code like 404 becomes 404 % 256 = 148) |
|
#rc=$http_code |
|
rc=2 |
|
else |
|
rc=1 |
|
fi |
|
fi |
|
|
|
# Use exit to prevent success or failure status from main |
|
exit ${rc:-1} |
|
fi |
|
} |
|
|
|
# Extract the content from the response (handles potential formatting issues) |
|
function extract_response_content() { |
|
printf "%-*s" $STEP_DESCRIPTION_LEN "Extract response" |
|
|
|
response_created=$(sed -nr 's/.*"created":[[:space:]]*([0-9]+).*/\1/p' <<< "$response") |
|
response_created=$(date -d @"$response_created" "+%Y-%m-%dT%H:%M:%S%z") |
|
response_object=$(sed -nr 's/.*"object":[[:space:]]*"(([^"\\]|\\.)*)".*/\1/p' <<< "$response") |
|
response_model=$(sed -nr 's/.*"model":[[:space:]]*"(([^"\\]|\\.)*)".*/\1/p' <<< "$response") |
|
|
|
response=$(sed -nr 's/.*"content":[[:space:]]*"(([^"\\]|\\.)*)".*/\1/p' <<< "$response") |
|
response=$(sed -r 's/\\n/\n/g' <<< "$response") |
|
response=$(sed -r 's/\\"/"/g' <<< "$response") |
|
response=$(sed -r 's/\\\\/\\/g' <<< "$response") |
|
|
|
if [[ -z "$response_created" || -z "$response_object" || -z "$response_model" || -z "$response" ]]; then |
|
return 1 |
|
fi |
|
} |
|
|
|
if [[ "${DEBUG:-false}" == false ]]; then |
|
# Define cleanup function only when DEBUG is disabled. |
|
# In DEBUG mode, the function is not defined at all. |
|
function delete_debug_files() { |
|
printf "%-*s" $STEP_DESCRIPTION_LEN "Delete debug files" |
|
|
|
# Delete debug files |
|
rm -f "$DATA_LOG" |
|
rm -f "$OUTPUT_LOG" |
|
rm -f "$DIFF_LOG" |
|
} |
|
fi |
|
|
|
function write_commit() { |
|
printf "%-*s" $STEP_DESCRIPTION_LEN "Write response to ${COMMIT_MSG_FILE#.git/}" |
|
|
|
commit_meta=() |
|
commit_meta+=("$HASHES_LINE") |
|
commit_meta+=("Generated with AI - $response_created") |
|
commit_meta+=("API : $AI_BASEURL") |
|
commit_meta+=("Endpoint : $response_object") |
|
commit_meta+=("AI model : $response_model") |
|
commit_meta+=("$HASHES_LINE") |
|
|
|
process_commit_meta commit_meta && commit_msg=("${commit_meta[@]}") |
|
commit_msg+=("") |
|
commit_msg+=("$response") |
|
|
|
# If commit mode is 'message' => `git commit -m '…'` |
|
if [[ "$COMMIT_MODE" = "message" ]]; then |
|
# If commit message file contains only a '.' |
|
if [[ "$EXISTING_MSG" = "." ]]; then |
|
printf "%b\n" "${commit_msg[@]}" > "$COMMIT_MSG_FILE" || return 1 |
|
if [ "$(git config --type=bool --get openai.autocommit 2>/dev/null || echo false)" = "false" ]; then |
|
new_steps=() |
|
for step in "${STEPS[@]}"; do |
|
if [[ "$step" == finalize_commit_msg ]]; then |
|
new_steps+=(open_editor) |
|
fi |
|
new_steps+=("$step") |
|
done |
|
STEPS=("${new_steps[@]}") |
|
fi |
|
fi |
|
# If commit mode is 'commit' => `git commit` |
|
else |
|
# If the first line is empty, then user didn’t use `git commit --amend` |
|
first_line=$(head -n1 "$COMMIT_MSG_FILE") |
|
if [[ -z "$first_line" ]]; then |
|
printf "%b\n" "${commit_msg[@]}" > "$COMMIT_MSG_FILE" || return 1 |
|
fi |
|
fi |
|
} |
|
|
|
function append_informations() { |
|
EXTENDED=true # include insertions(+) and deletions(-) |
|
printf "%-*s" $STEP_DESCRIPTION_LEN "Append informations to ${COMMIT_MSG_FILE#.git/}" |
|
|
|
declare -A states=([M]=modified [A]=added [D]=deleted [R]=renamed [C]=copied) |
|
if [[ "$EXTENDED" == "true" ]]; then |
|
diff=$( |
|
set -o pipefail |
|
git diff \ |
|
--numstat \ |
|
--staged \ |
|
--no-color \ |
|
':(exclude)node_modules/*' \ |
|
':(exclude)vendor/*' \ |
|
':(exclude)*bundle.js' \ |
|
':(exclude)*bundle.css' | \ |
|
while read add del file; do |
|
status=$(git diff --staged --name-status -- "$file" | awk '{print $1}') |
|
local rc=$? |
|
(( rc != 0 )) && return $rc |
|
status="${states[$status]}" |
|
printf "%-12s +%-4s -%-4s %s\n" "$status" "$add" "$del" "$file" |
|
done |
|
) |
|
rc=$? |
|
else |
|
diff=$( |
|
set -o pipefail |
|
git diff \ |
|
--name-status \ |
|
--staged \ |
|
--no-color \ |
|
':(exclude)node_modules/*' \ |
|
':(exclude)vendor/*' \ |
|
':(exclude)*bundle.js' \ |
|
':(exclude)*bundle.css' | \ |
|
while read status file; do |
|
status="${states[$status]}:" |
|
printf "%-12s%s\n" "$status" "$file" |
|
done |
|
) |
|
rc=$? |
|
fi |
|
(( rc != 0 )) && return $rc |
|
|
|
commit_meta=() |
|
commit_meta+=("$HASHES_LINE") |
|
commit_meta+=("Please check the commit message for your changes. Lines starting") |
|
commit_meta+=("with '#' will be ignored, and an empty message aborts the commit.") |
|
commit_meta+=("") |
|
commit_meta+=("On branch $(git branch --show-current)") |
|
commit_meta+=("Changes to be committed:") |
|
while IFS= read -r line; do |
|
commit_meta+=($'\t'"$line") |
|
done <<< "$diff" |
|
commit_meta+=("") |
|
|
|
commit_msg=("") |
|
process_commit_meta commit_meta && commit_msg+=("${commit_meta[@]}") |
|
#commit_meta+=("${info[@]}") |
|
|
|
#for i in "${!info[@]}"; do |
|
# [[ -z ${info[$i]} || ${info[$i]:0:1} =~ [[:blank:]] ]] && s='' || s=' ' |
|
# info[$i]="\u200b#$s${info[$i]}" |
|
#done |
|
#echo "" >> "$COMMIT_MSG_FILE" || return 1 |
|
printf "%b\n" "${commit_msg[@]}" >> "$COMMIT_MSG_FILE" || return 1 |
|
} |
|
|
|
function finalize_commit_msg() { |
|
printf "%-*s" $STEP_DESCRIPTION_LEN "Finalize ${COMMIT_MSG_FILE#.git/}" |
|
|
|
# Delete any line where META_MARKER appears at the beginning (allow leading spaces/tabs) |
|
sed -i "/^[[:space:]]*${META_MARKER}/d" "$COMMIT_MSG_FILE" || return 1 |
|
|
|
# Remove leading blank lines at the top of the file |
|
sed -i '/./,$!d' "$COMMIT_MSG_FILE" || return 1 |
|
|
|
# Remove trailing blank lines at the end of the file |
|
sed -i ':a; /^\s*$/{$d; N; ba}' "$COMMIT_MSG_FILE" || return 1 |
|
} |
|
|
|
if [[ -t 1 && -z "${CI:-}" ]]; then |
|
# Only define open_editor in interactive sessions (TTY) and non-CI environments. |
|
# In CI or non-interactive shells, the function is not defined. |
|
function open_editor() { |
|
# only run in interactive terminal, else return exits with status 0 (success) |
|
[[ -t 1 ]] || return 0 |
|
|
|
printf "%-*s" $STEP_DESCRIPTION_LEN "Edit ${COMMIT_MSG_FILE#.git/}" |
|
|
|
_GIT_EDITOR="$(git var GIT_EDITOR 2>/dev/null)" |
|
[[ "$_GIT_EDITOR" == ":" ]] && _GIT_EDITOR="" |
|
_GIT_EDITOR="${_GIT_EDITOR:-$(git config --get core.editor)}" |
|
_GIT_EDITOR="${_GIT_EDITOR:-$VISUAL}" |
|
_GIT_EDITOR="${_GIT_EDITOR:-$EDITOR}" |
|
_GIT_EDITOR="${_GIT_EDITOR:-vim}" |
|
|
|
tput sc # save cursor position |
|
echo -ne "hint: Waiting for your editor to close the file..." |
|
|
|
## Problem with this approach |
|
## If GIT_EDITOR contains anything unexpected, e.g `GIT_EDITOR='rm -rf /'` |
|
## -> you now have a shell injection vulnerability |
|
## Even if you "trust" the environment, hooks often run in mixed contexts (IDE, CI, templates). |
|
#( eval "set -- $GIT_EDITOR"; "$@" "$COMMIT_MSG_FILE" ) |
|
|
|
# Better version (safe + simple) |
|
sh -c "$_GIT_EDITOR \"\$@\"" _ "$COMMIT_MSG_FILE" #2>$STDERR |
|
rc=$? |
|
|
|
|
|
|
|
|
|
|
|
tput rc # restore cursor position |
|
tput el # clear to end of line |
|
|
|
return $rc |
|
} |
|
fi |
|
|
|
# Prefix commit meta items with: |
|
# - META_MARKER + optional '#' + optional space based on content rules |
|
function process_commit_meta() { |
|
local -n arr="$1" |
|
|
|
for i in "${!arr[@]}"; do |
|
[[ ${arr[$i]} == [![:space:]#]* ]] && s=' ' || s='' |
|
[[ ${arr[$i]} != \#* ]] && c='#' || c='' |
|
arr[$i]="$META_MARKER$c$s${arr[$i]}" |
|
done |
|
} |
|
|
|
# Start stderr capture (tap stderr into a log file silently) |
|
# FD2 → file |
|
# FD3 → original terminal stderr backup |
|
function stderr_tap_start() { |
|
# Save original stderr (FD 2 → FD 3) |
|
exec 3>&2 |
|
|
|
# Init log file if missing |
|
: "${STDERR_FILE:=$(mktemp)}" |
|
|
|
# Redirect stderr → terminal + file |
|
#exec 2> >(tee "$STDERR_FILE" >/dev/null) |
|
# Redirect stderr → file (no terminal output) |
|
exec 2> "$STDERR_FILE" |
|
} |
|
|
|
# Stop stderr capture and cleanup |
|
# FD2 → restored terminal |
|
# FD3 → closed |
|
function stderr_tap_stop() { |
|
# Restore original stderr |
|
if { true >&3; } 2>/dev/null; then |
|
exec 2>&3 |
|
exec 3>&- |
|
fi |
|
|
|
# Print captured log |
|
stderr_print |
|
|
|
# Cleanup |
|
[[ -s "${STDERR_FILE:-}" ]] && rm -f "$STDERR_FILE" |
|
} |
|
|
|
# Print captured stderr log (formatted or raw) |
|
function stderr_print() { |
|
mode="${1:-formatted}" |
|
|
|
local RED=$'\e[31m' |
|
local BOLD=$'\e[1m' |
|
local RESET=$'\e[0m' |
|
|
|
# Only proceed if log file exists and is not empty |
|
if [[ -s "${STDERR_FILE:-}" ]]; then |
|
# Raw dump of full log |
|
if [[ "$mode" == "raw" ]]; then |
|
cat "$STDERR_FILE" >&2 |
|
# Formatted output per line |
|
else |
|
while IFS= read -r line; do |
|
printf "${BOLD}${RED}[ ERROR ]${RESET} %s\n" "$line" >&2 |
|
done < "$STDERR_FILE" |
|
fi |
|
fi |
|
} |
|
|
|
|
|
# ANSI colors for terminal output |
|
GREEN="\e[32m" |
|
RED="\e[31m" |
|
YELLOW="\e[33m" |
|
RESET="\e[0m" |
|
|
|
# Fixed width for step description formatting |
|
STEP_DESCRIPTION_LEN=40 |
|
|
|
|
|
# Stop execution if a commit message already exists in "message" mode |
|
if [[ "$COMMIT_MODE" == "message" && "$EXISTING_MSG" != "." ]]; then |
|
exit |
|
fi |
|
|
|
# Inform the user that commit generation has started |
|
printf "${YELLOW}⚡${RESET} Automatically generating git commit message... ${YELLOW}⚡${RESET}\n" |
|
|
|
# Ordered list of functions to execute |
|
declare -a STEPS |
|
STEPS=( |
|
get_config |
|
get_staged_diffs |
|
prepare_json_payload |
|
do_request |
|
extract_response_content |
|
delete_debug_files |
|
write_commit |
|
append_informations |
|
finalize_commit_msg |
|
) |
|
|
|
# Filter STEPS to include only existing functions |
|
function check_functions() { |
|
valid_steps=() |
|
for step in "${STEPS[@]}"; do |
|
declare -f "$step" >/dev/null && valid_steps+=("$step") |
|
done |
|
STEPS=("${valid_steps[@]}") |
|
unset valid_steps |
|
} |
|
|
|
cur_step=0 |
|
# Run each function in sequence |
|
while (( cur_step < ${#STEPS[@]} )); do |
|
# ensure STEPS contains only valid functions before executing next step |
|
check_functions |
|
|
|
step="${STEPS[cur_step++]}" |
|
|
|
# Start stderr capture for this step |
|
stderr_tap_start |
|
|
|
# Print current step label |
|
printf "➤ %s %2d of %2d:\t" "Step" $cur_step ${#STEPS[@]} |
|
|
|
# Execute the step |
|
"$step" #2>/dev/null |
|
rc=$? |
|
|
|
# Show success or failure status |
|
[[ ${rc:-1} -eq 0 ]] \ |
|
&& printf "%b✔%b done\n" "$GREEN" "$RESET" \ |
|
|| printf "%b✖%b failed\n" "$RED" "$RESET" |
|
|
|
# Stop stderr capture + print log |
|
stderr_tap_stop |
|
|
|
# Exit on error |
|
[[ $rc -ne 0 ]] && exit "$rc" |
|
done |
|
|
|
echo "" |
|
cat "$COMMIT_MSG_FILE" |
|
|
|
exit "$rc" |