-
-
Save justinvdm/fcd6da9d28057addd263214d90793137 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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