Skip to content

Instantly share code, notes, and snippets.

@justinvdm
Last active November 1, 2025 06:31
Show Gist options
  • Save justinvdm/fcd6da9d28057addd263214d90793137 to your computer and use it in GitHub Desktop.
Save justinvdm/fcd6da9d28057addd263214d90793137 to your computer and use it in GitHub Desktop.
#!/bin/bash
set -euo pipefail
# A script to commit with an AI-generated message and push.
#
# Usage:
# ad
#
# Environment Variables:
# NO_AI: If set to "true", it will use a fallback commit message
# instead of calling the AI. Defaults to "false".
# GROQ_API_KEY: Your API key for the Groq API. The script will use the
# fallback if this is not set.
# --- Configuration ---
# The Groq model to use for generating the commit message.
# 'llama-3.1-8b-instant' is a good, fast choice for this task.
GROQ_MODEL="llama-3.1-8b-instant"
# The prompt to send to the AI. The script will append the git diff to this.
AI_PROMPT="Based on the following git diff, generate a concise commit message. The message should be in the imperative mood, short, and suitable for a git commit. Do not include any introductory phrases like 'The commit message is:' or markdown formatting."
# --- Script ---
# A list of common lockfile patterns to exclude from the diff.
# This uses git's pathspec magic to exclude files.
LOCKFILE_EXCLUDES=(
':(exclude)*package-lock.json'
':(exclude)*pnpm-lock.yaml'
':(exclude)*yarn.lock'
':(exclude)*composer.lock'
':(exclude)*Gemfile.lock'
':(exclude)*Pipfile.lock'
':(exclude)*poetry.lock'
':(exclude)Cargo.lock'
':(exclude)go.sum'
':(exclude)mix.lock'
':(exclude)pubspec.lock'
':(exclude)Podfile.lock'
':(exclude)packages.lock.json'
)
# Function to generate an intelligent fallback commit message locally.
# It finds the file with the most changes and uses it in the message.
generate_fallback_commit_message() {
local stats
# We use git diff --stat to get a summary of changes, excluding lockfiles.
stats=$(git diff --cached --stat -- "${LOCKFILE_EXCLUDES[@]}")
if [ -z "$stats" ]; then
# This can happen if only lockfiles were changed.
# We'll generate a generic message.
echo "Update lockfile"
return
fi
# This bit of awk magic finds the file with the most changes (adds + dels).
local primary_file
primary_file=$(echo "$stats" | awk '
BEGIN { max_changes = -1; primary_file = ""; }
{
# The file path is all but the last two fields.
file_path = "";
for (i = 1; i <= NF - 2; i++) {
file_path = file_path (i > 1 ? " " : "") $i;
}
# The last field is the +/ - summary.
plus_minus = $NF;
# Count the number of '+' and '-' characters.
additions = gsub(/\+/, "+", plus_minus);
deletions = gsub(/\-/, "-", plus_minus);
total_changes = additions + deletions;
if (total_changes > max_changes) {
max_changes = total_changes;
primary_file = file_path;
}
}
END { print primary_file; }
')
local num_files
num_files=$(echo "$stats" | wc -l | tr -d ' ')
num_files=$((num_files - 1)) # Subtract the summary line
if [ "$num_files" -le 1 ]; then
echo "Update $primary_file"
else
local other_files_count=$((num_files - 1))
echo "Update $primary_file and $other_files_count other file(s)"
fi
}
# Function to generate a commit message using the Groq API.
generate_ai_commit_message() {
if [ -z "${GROQ_API_KEY:-}" ]; then
echo "GROQ_API_KEY is not set. Cannot generate AI commit message." >&2
return 1
fi
local diff
# Get the diff, excluding lockfiles.
diff=$(git diff --cached -- "${LOCKFILE_EXCLUDES[@]}")
if [ -z "$diff" ]; then
echo "No meaningful changes to generate an AI commit message." >&2
return 1
fi
# Simple truncation if the diff is too large
local MAX_CHARS=18000
if [ ${#diff} -gt $MAX_CHARS ]; then
diff=$(echo "$diff" | head -c $MAX_CHARS)
diff="$diff
[... diff truncated due to size ...]"
fi
local prompt_with_diff
prompt_with_diff="${AI_PROMPT}
diff:
${diff}"
# Construct the JSON payload for the Groq API.
# We use jq to build the JSON to avoid any shell escaping issues.
local data
data=$(jq -n \
--arg model "$GROQ_MODEL" \
--arg prompt "$prompt_with_diff" \
'{
"model": $model,
"messages": [
{
"role": "user",
"content": $prompt
}
],
"temperature": 0.2,
"max_tokens": 60,
"top_p": 1,
"stream": false
}')
# Call the Groq API. The timeout is set to 20 seconds.
# We use jq to parse the response and get the message content.
local response
if ! response=$(curl -s -m 20 -X POST "https://api.groq.com/openai/v1/chat/completions" \
-H "Authorization: Bearer $GROQ_API_KEY" \
-H "Content-Type: application/json" \
-d "$data"); then
echo "API request to Groq failed." >&2
return 1
fi
local message
message=$(echo "$response" | jq -r '.choices[0].message.content')
if [ -z "$message" ] || [ "$message" = "null" ]; then
echo "AI commit message generation failed. Response from API was empty." >&2
echo "API Response: $response" >&2
return 1
fi
# Check for polite refusals from the AI. This regex is intentionally broad
# to catch common variations of "I can't help without the diff."
if echo "$message" | grep -qE "hav(e|en't|ent) provided|please provide|unable to assist|as an AI"; then
echo "AI refused to generate a commit message. It may not have understood the diff." >&2
echo "AI Response: $message" >&2
return 1
fi
echo "$message"
}
main() {
# Add all unstaged changes to the index before diffing.
# This makes sure the diff includes all the work in progress.
git -C "$(git rev-parse --show-toplevel)" add .
# Check if there are any staged changes. If not, we're done.
if git diff --cached --quiet; then
echo "No changes staged for commit."
exit 0
fi
local commit_message
local non_lockfile_changes
non_lockfile_changes=$(git diff --cached --name-only -- "${LOCKFILE_EXCLUDES[@]}")
if [ -z "$non_lockfile_changes" ]; then
commit_message="update lock file"
else
# Check if PP_NO_AI is set to "true"
if [ "${PP_NO_AI:-false}" = "true" ]; then
commit_message=$(generate_fallback_commit_message)
else
# Attempt to use the AI for the commit message
if ! commit_message=$(generate_ai_commit_message); then
# Fallback to local commit message generation if AI fails
echo "Using fallback commit message." >&2
commit_message=$(generate_fallback_commit_message)
fi
fi
fi
# Remove potential quotes and newlines from the AI's output
commit_message=$(echo "$commit_message" | tr -d '"' | tr -d "'" | head -n 1)
echo "Committing with message: $commit_message"
git commit -m "$commit_message"
# Push the changes. It first tries a normal push.
# If that fails, it pulls with rebase and then pushes again.
if ! git push; then
echo "Push failed. Trying to pull with rebase and push again." >&2
if git pull --rebase && git push; then
echo "Push successful after rebase."
else
echo "Could not automatically push after rebase. Please resolve conflicts and push manually." >&2
exit 1
fi
else
echo "Push successful."
fi
}
main
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment