Last active
March 29, 2025 05:11
-
-
Save WomB0ComB0/43acb03ecbd012d761cadb440698faa6 to your computer and use it in GitHub Desktop.
Utilizing an AI-powered CLI to incrementally create AI-generated commit messages based on you git changes.
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 | |
# Still want to catch undefined variables, but don't exit on errors | |
set -u | |
# Script configuration | |
DEFAULT_REPO_DIR="$HOME/github" | |
DEFAULT_AI_COMMAND="ask cm -m gemini-2.0-flash" | |
LOG_FILE="/tmp/git-auto-commit-$(date +%Y%m%d-%H%M%S).log" | |
MAX_RETRIES=3 | |
TIMEOUT_SECONDS=30 | |
DEFAULT_GITDIFF_EXCLUDE="$HOME/.config/git/gitdiff-exclude" | |
DEFAULT_GH_AUTH_EMAIL="" # Default authorized email (empty means use global) | |
DEFAULT_AUTO_PUSH=true # Whether to automati#!/bin/bash | |
# Still want to catch undefined variables, but don't exit on errors | |
set -u | |
# Script configuration | |
DEFAULT_REPO_DIR="$HOME/github" | |
DEFAULT_AI_COMMAND="ask cm -m gemini-2.0-flash" | |
LOG_FILE="/tmp/git-auto-commit-$(date +%Y%m%d-%H%M%S).log" | |
MAX_RETRIES=3 | |
TIMEOUT_SECONDS=30 | |
DEFAULT_GITDIFF_EXCLUDE="$HOME/.config/git/gitdiff-exclude" | |
DEFAULT_GH_AUTH_EMAIL="" # Default authorized email (empty means use global) | |
DEFAULT_AUTO_PUSH=true # Whether to automatintecally push changes | |
# Configuration file | |
CONFIG_FILE="$HOME/.git-auto-commit.conf" | |
# Load configuration if exists | |
if [ -f "$CONFIG_FILE" ]; then | |
source "$CONFIG_FILE" | |
fi | |
# Use defaults or config file values | |
REPO_DIR="${REPO_DIR:-$DEFAULT_REPO_DIR}" | |
AI_COMMAND="${AI_COMMAND:-$DEFAULT_AI_COMMAND}" | |
GITDIFF_EXCLUDE="${GITDIFF_EXCLUDE:-$DEFAULT_GITDIFF_EXCLUDE}" | |
GH_AUTH_EMAIL="${GH_AUTH_EMAIL:-$DEFAULT_GH_AUTH_EMAIL}" | |
AUTO_PUSH="${AUTO_PUSH:-$DEFAULT_AUTO_PUSH}" | |
# Logging function | |
log() { | |
local level="$1" | |
shift | |
local message="$*" | |
local timestamp | |
timestamp=$(date '+%Y-%m-%d %H:%M:%S') | |
echo "[$timestamp] [$level] $message" | tee -a "$LOG_FILE" | |
} | |
# Error handling function - log but don't exit | |
handle_error() { | |
local exit_code=$? | |
log "ERROR" "An error occurred in line $1" | |
log "ERROR" "Exit code: $exit_code" | |
# Don't exit, just return the error code | |
return $exit_code | |
} | |
# Set up error handling | |
trap 'handle_error $LINENO' ERR | |
# Check required commands | |
check_dependencies() { | |
local missing_deps=() | |
local deps=("git" "grep" "timeout" "xargs" "gh") | |
for dep in "${deps[@]}"; do | |
if ! command -v "$dep" >/dev/null 2>&1; then | |
missing_deps+=("$dep") | |
fi | |
done | |
if [ ${#missing_deps[@]} -gt 0 ]; then | |
log "ERROR" "Required commands not found: ${missing_deps[*]}" | |
return 1 | |
fi | |
return 0 | |
} | |
# Check if gh cli is authenticated | |
check_gh_auth() { | |
if ! gh auth status &>/dev/null; then | |
log "ERROR" "GitHub CLI not authenticated. Please run 'gh auth login' first" | |
return 1 | |
fi | |
return 0 | |
} | |
# Get authorized email for a repository | |
get_authorized_email() { | |
local repo_path="$1" | |
local repo_name | |
repo_name=$(basename "$repo_path") | |
local remote_url | |
local repo_owner | |
local repo_name_only | |
local authorized_email | |
# Try to get the remote URL | |
remote_url=$(git -C "$repo_path" config --get remote.origin.url 2>/dev/null) | |
if [ -z "$remote_url" ]; then | |
log "WARNING" "Could not determine remote URL for $repo_name" >&2 | |
return 1 | |
fi | |
# Extract owner and repo name from URL | |
if [[ "$remote_url" =~ github\.com[:/]([^/]+)/([^/.]+) ]]; then | |
repo_owner="${BASH_REMATCH[1]}" | |
repo_name_only="${BASH_REMATCH[2]}" | |
# Use GitHub CLI to check authorized emails | |
# This assumes the user is logged into gh CLI and has appropriate permissions | |
log "INFO" "Checking authorized emails for $repo_owner/$repo_name_only" >&2 | |
# If we have a default auth email set, use it | |
if [ -n "$GH_AUTH_EMAIL" ]; then | |
log "INFO" "Using configured authorized email: $GH_AUTH_EMAIL" >&2 | |
echo "$GH_AUTH_EMAIL" | |
return 0 | |
fi | |
# Try to get email from gh api if possible | |
authorized_email=$(gh api user/emails 2>/dev/null | grep -o '"email":"[^"]*"' | head -1 | cut -d'"' -f4) | |
if [ -n "$authorized_email" ]; then | |
log "INFO" "Found authorized email: $authorized_email" >&2 | |
echo "$authorized_email" | |
return 0 | |
fi | |
# Fallback to global git email | |
authorized_email=$(git config --global user.email) | |
if [ -n "$authorized_email" ]; then | |
log "INFO" "Using global git email: $authorized_email" >&2 | |
echo "$authorized_email" | |
return 0 | |
fi | |
else | |
log "WARNING" "Could not parse GitHub repository information from URL: $remote_url" >&2 | |
return 1 | |
fi | |
log "WARNING" "Could not determine authorized email for $repo_name" >&2 | |
return 1 | |
} | |
# Get default branch for repository | |
get_default_branch() { | |
local repo_path="$1" | |
local default_branch | |
# Try to get the default branch from git | |
default_branch=$(git -C "$repo_path" symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@') | |
# If that fails, try to get it from GitHub CLI | |
if [ -z "$default_branch" ]; then | |
local remote_url | |
remote_url=$(git -C "$repo_path" config --get remote.origin.url 2>/dev/null) | |
if [[ "$remote_url" =~ github\.com[:/]([^/]+)/([^/.]+) ]]; then | |
local repo_owner="${BASH_REMATCH[1]}" | |
local repo_name="${BASH_REMATCH[2]}" | |
default_branch=$(gh api repos/"$repo_owner"/"$repo_name" 2>/dev/null | grep -o '"default_branch":"[^"]*"' | cut -d'"' -f4) | |
fi | |
fi | |
# Fallback to common default branch names | |
if [ -z "$default_branch" ]; then | |
# Check if main or master branch exists | |
if git -C "$repo_path" show-ref --verify --quiet refs/heads/main; then | |
default_branch="main" | |
elif git -C "$repo_path" show-ref --verify --quiet refs/heads/master; then | |
default_branch="master" | |
fi | |
fi | |
if [ -n "$default_branch" ]; then | |
echo "$default_branch" | |
return 0 | |
else | |
return 1 | |
fi | |
} | |
# Validate repository directory | |
validate_repo_dir() { | |
if [ ! -d "$REPO_DIR" ]; then | |
log "ERROR" "Repository directory not found: $REPO_DIR" | |
return 1 | |
fi | |
return 0 | |
} | |
# Check if .env variants are ignored | |
check_env_ignored() { | |
local repo_path="$1" | |
local gitignore_file="$repo_path/.gitignore" | |
local env_variants=(".env" ".env.local" ".env.development" ".env.production" ".env.test") | |
# Check for existence of any .env files first | |
local has_env_files=false | |
for variant in "${env_variants[@]}"; do | |
if [ -f "$repo_path/$variant" ]; then | |
has_env_files=true | |
if [ ! -f "$gitignore_file" ] || ! grep -q "^$variant$" "$gitignore_file"; then | |
log "WARNING" "Found $variant but it's not properly ignored in $(basename "$repo_path")" | |
fi | |
fi | |
done | |
# If no .env files found, just return success | |
if [ "$has_env_files" = false ]; then | |
log "DEBUG" "No .env files found in $(basename "$repo_path")" | |
fi | |
# Always return success to continue processing | |
return 0 | |
} | |
# Generate commit message with retry mechanism | |
generate_commit_message() { | |
local attempt=1 | |
local commit_message="" | |
local exclude_args="" | |
# Build exclude args if gitdiff-exclude file exists | |
if [ -f "$GITDIFF_EXCLUDE" ]; then | |
# Process each line in the exclude file that's not a comment | |
while IFS= read -r line || [ -n "$line" ]; do | |
if [[ ! "$line" =~ ^# ]] && [ -n "$line" ]; then | |
# Check if the file exists before excluding it | |
if [ -e "$line" ]; then | |
exclude_args="$exclude_args :!$line" | |
fi | |
fi | |
done < <(grep -v '^#' "$GITDIFF_EXCLUDE" 2>/dev/null || echo "") | |
fi | |
while [ $attempt -le $MAX_RETRIES ]; do | |
log "INFO" "Generating commit message (attempt $attempt/$MAX_RETRIES)..." >&2 | |
log "DEBUG" "Using gitdiff-exclude: $exclude_args" >&2 | |
# Check if running in interactive mode | |
if [ -t 0 ]; then | |
# Create a temporary file for the git diff | |
local diff_file | |
diff_file=$(mktemp) | |
# Create a temporary file for the AI command output | |
local output_file | |
output_file=$(mktemp) | |
# Capture the git diff to a file first | |
git diff --cached --diff-filter=ACMRTUXB $exclude_args > "$diff_file" | |
# Check if diff is empty | |
if [ ! -s "$diff_file" ]; then | |
log "WARNING" "Git diff is empty, using default commit message" >&2 | |
rm -f "$diff_file" "$output_file" | |
echo "Auto-commit: Empty diff detected" | |
return 0 | |
fi | |
# Log the diff size for debugging | |
local diff_size | |
diff_size=$(wc -l < "$diff_file") | |
log "DEBUG" "Git diff size: $diff_size lines" >&2 | |
# Run AI command with increased timeout and capture output | |
log "DEBUG" "Running AI command: $AI_COMMAND" >&2 | |
if timeout $((TIMEOUT_SECONDS*2)) bash -c "cat \"$diff_file\" | $AI_COMMAND 'Could you write a commit message for this? Please only provide the commit message without any additional commentary.'" > "$output_file" 2>/dev/null; then | |
# Read from the output file | |
commit_message=$(cat "$output_file") | |
# Debug the output | |
log "DEBUG" "AI command raw output length: $(echo "$commit_message" | wc -c) bytes" >&2 | |
# Clean up the output - remove any markdown formatting or quotes | |
commit_message=$(echo "$commit_message" | sed -e 's/^```.*$//' -e 's/^```$//' -e 's/^"//' -e 's/"$//') | |
# Make sure it's not empty | |
if [ -n "$commit_message" ]; then | |
log "DEBUG" "Cleaned commit message: $commit_message" >&2 | |
rm -f "$diff_file" "$output_file" | |
echo "$commit_message" | |
return 0 | |
fi | |
fi | |
# Clean up temp files | |
rm -f "$diff_file" "$output_file" | |
else | |
# Non-interactive mode: use a default commit message | |
commit_message="Auto-commit: $(date +%Y-%m-%d %H:%M:%S)" | |
echo "$commit_message" | |
return 0 | |
fi | |
log "WARNING" "Failed to generate commit message on attempt $attempt" >&2 | |
((attempt++)) | |
[ $attempt -le $MAX_RETRIES ] && sleep 2 | |
done | |
# Fallback to a timestamp-based commit message instead of empty | |
echo "Auto-commit: Changes at $(date +%Y-%m-%d\ %H:%M:%S)" | |
return 0 | |
} | |
# Push changes to remote repository | |
push_changes() { | |
local repo_path="$1" | |
local repo_name | |
repo_name=$(basename "$repo_path") | |
local current_branch | |
local default_branch | |
# Get current branch | |
current_branch=$(git -C "$repo_path" symbolic-ref --short HEAD 2>/dev/null) | |
if [ -z "$current_branch" ]; then | |
log "ERROR" "Could not determine current branch for $repo_name" | |
return 1 | |
fi | |
# Get default branch | |
default_branch=$(get_default_branch "$repo_path") | |
if [ -z "$default_branch" ]; then | |
log "WARNING" "Could not determine default branch for $repo_name, using current branch: $current_branch" | |
default_branch="$current_branch" | |
fi | |
log "INFO" "Pushing changes to $current_branch in $repo_name" | |
# Try to push changes | |
if git -C "$repo_path" push origin "$current_branch" 2>/dev/null; then | |
log "SUCCESS" "Successfully pushed changes to $current_branch in $repo_name" | |
# If we're not on the default branch, suggest creating a PR | |
if [ "$current_branch" != "$default_branch" ]; then | |
log "INFO" "You're on branch '$current_branch', not on default branch '$default_branch'" | |
log "INFO" "Consider creating a pull request with: gh pr create -R $(git -C "$repo_path" config --get remote.origin.url | sed 's/.*github.com[:/]\(.*\)\.git/\1/')" | |
fi | |
return 0 | |
else | |
log "ERROR" "Failed to push changes to $current_branch in $repo_name" | |
return 1 | |
fi | |
} | |
# Process single repository | |
process_repository() { | |
local repo="$1" | |
local repo_name | |
repo_name=$(basename "$repo") | |
local original_email | |
local authorized_email | |
local has_committed=false | |
log "INFO" "Processing repository: $repo_name" | |
# Verify it's a Git repository | |
if [ ! -d "$repo/.git" ]; then | |
log "WARNING" "Not a Git repository: $repo_name" | |
return 0 # Continue to next repo | |
fi | |
# Check .env files - always continue | |
check_env_ignored "$repo" | |
# Navigate to repository | |
if ! cd "$repo" 2>/dev/null; then | |
log "ERROR" "Failed to change directory to $repo_name" | |
return 0 # Continue to next repo | |
fi | |
# Store original email configuration | |
original_email=$(git config user.email) | |
# Get authorized email for this repository | |
if authorized_email=$(get_authorized_email "$repo"); then | |
# Set the authorized email for this commit | |
log "INFO" "Setting commit email to: $authorized_email for $repo_name" | |
git config user.email "$authorized_email" | |
else | |
log "WARNING" "Using default email configuration for $repo_name" | |
fi | |
# Check for changes | |
if git diff --quiet && git diff --cached --quiet; then | |
log "INFO" "No changes in $repo_name" | |
# Restore original email if it was set | |
if [ -n "$original_email" ]; then | |
git config user.email "$original_email" | |
fi | |
return 0 | |
fi | |
log "INFO" "Changes detected in $repo_name" | |
# Stage all changes | |
git add . | |
# Generate commit message | |
local commit_message | |
commit_message=$(generate_commit_message) | |
# Check if we should use empty commit message | |
if [ "$commit_message" = "__USE_EMPTY_COMMIT__" ]; then | |
log "WARNING" "AI command failed to generate commit message, using empty commit message" | |
# Commit changes with empty message | |
if git commit --allow-empty-message -m ""; then | |
log "SUCCESS" "Changes committed in $repo_name with empty message" | |
has_committed=true | |
# Push changes if auto-push is enabled | |
if [ "$AUTO_PUSH" = true ]; then | |
push_changes "$repo" | |
fi | |
else | |
log "ERROR" "Failed to commit changes with empty message in $repo_name" | |
fi | |
else | |
log "INFO" "Generated commit message for $repo_name:" | |
log "INFO" "$commit_message" | |
# Commit changes with the generated message | |
if git commit -m "$commit_message"; then | |
log "SUCCESS" "Changes committed in $repo_name" | |
has_committed=true | |
# Push changes if auto-push is enabled | |
if [ "$AUTO_PUSH" = true ]; then | |
push_changes "$repo" | |
fi | |
else | |
log "ERROR" "Failed to commit changes in $repo_name" | |
fi | |
fi | |
# Restore original email if it was set | |
if [ -n "$original_email" ]; then | |
git config user.email "$original_email" | |
fi | |
if [ "$has_committed" = true ]; then | |
return 0 | |
else | |
return 1 | |
fi | |
} | |
main() { | |
log "INFO" "Starting auto-commit process" | |
# Check dependencies but continue anyway | |
check_dependencies || true | |
# Check GitHub CLI authentication but continue anyway | |
check_gh_auth || true | |
# Validate repository directory but continue anyway | |
validate_repo_dir || true | |
# Process each repository | |
local success_count=0 | |
local fail_count=0 | |
# Use nullglob to handle no matches gracefully | |
shopt -s nullglob | |
for repo in "$REPO_DIR"/*; do | |
if [ -d "$repo" ]; then | |
if process_repository "$repo"; then | |
((success_count++)) | |
else | |
((fail_count++)) | |
fi | |
fi | |
done | |
shopt -u nullglob | |
log "INFO" "Auto-commit process completed" | |
log "INFO" "Successful repositories: $success_count" | |
log "INFO" "Failed repositories: $fail_count" | |
# Always exit successfully | |
return 0 | |
} | |
# Run main function | |
main | |
ntecally push changes | |
# Configuration file | |
CONFIG_FILE="$HOME/.git-auto-commit.conf" | |
# Load configuration if exists | |
if [ -f "$CONFIG_FILE" ]; then | |
source "$CONFIG_FILE" | |
fi | |
# Use defaults or config file values | |
REPO_DIR="${REPO_DIR:-$DEFAULT_REPO_DIR}" | |
AI_COMMAND="${AI_COMMAND:-$DEFAULT_AI_COMMAND}" | |
GITDIFF_EXCLUDE="${GITDIFF_EXCLUDE:-$DEFAULT_GITDIFF_EXCLUDE}" | |
GH_AUTH_EMAIL="${GH_AUTH_EMAIL:-$DEFAULT_GH_AUTH_EMAIL}" | |
AUTO_PUSH="${AUTO_PUSH:-$DEFAULT_AUTO_PUSH}" | |
# Logging function | |
log() { | |
local level="$1" | |
shift | |
local message="$*" | |
local timestamp | |
timestamp=$(date '+%Y-%m-%d %H:%M:%S') | |
echo "[$timestamp] [$level] $message" | tee -a "$LOG_FILE" | |
} | |
# Error handling function - log but don't exit | |
handle_error() { | |
local exit_code=$? | |
log "ERROR" "An error occurred in line $1" | |
log "ERROR" "Exit code: $exit_code" | |
# Don't exit, just return the error code | |
return $exit_code | |
} | |
# Set up error handling | |
trap 'handle_error $LINENO' ERR | |
# Check required commands | |
check_dependencies() { | |
local missing_deps=() | |
local deps=("git" "grep" "timeout" "xargs" "gh") | |
for dep in "${deps[@]}"; do | |
if ! command -v "$dep" >/dev/null 2>&1; then | |
missing_deps+=("$dep") | |
fi | |
done | |
if [ ${#missing_deps[@]} -gt 0 ]; then | |
log "ERROR" "Required commands not found: ${missing_deps[*]}" | |
return 1 | |
fi | |
return 0 | |
} | |
# Check if gh cli is authenticated | |
check_gh_auth() { | |
if ! gh auth status &>/dev/null; then | |
log "ERROR" "GitHub CLI not authenticated. Please run 'gh auth login' first" | |
return 1 | |
fi | |
return 0 | |
} | |
# Get authorized email for a repository | |
get_authorized_email() { | |
local repo_path="$1" | |
local repo_name | |
repo_name=$(basename "$repo_path") | |
local remote_url | |
local repo_owner | |
local repo_name_only | |
local authorized_email | |
# Try to get the remote URL | |
remote_url=$(git -C "$repo_path" config --get remote.origin.url 2>/dev/null) | |
if [ -z "$remote_url" ]; then | |
log "WARNING" "Could not determine remote URL for $repo_name" >&2 | |
return 1 | |
fi | |
# Extract owner and repo name from URL | |
if [[ "$remote_url" =~ github\.com[:/]([^/]+)/([^/.]+) ]]; then | |
repo_owner="${BASH_REMATCH[1]}" | |
repo_name_only="${BASH_REMATCH[2]}" | |
# Use GitHub CLI to check authorized emails | |
# This assumes the user is logged into gh CLI and has appropriate permissions | |
log "INFO" "Checking authorized emails for $repo_owner/$repo_name_only" >&2 | |
# If we have a default auth email set, use it | |
if [ -n "$GH_AUTH_EMAIL" ]; then | |
log "INFO" "Using configured authorized email: $GH_AUTH_EMAIL" >&2 | |
echo "$GH_AUTH_EMAIL" | |
return 0 | |
fi | |
# Try to get email from gh api if possible | |
authorized_email=$(gh api user/emails 2>/dev/null | grep -o '"email":"[^"]*"' | head -1 | cut -d'"' -f4) | |
if [ -n "$authorized_email" ]; then | |
log "INFO" "Found authorized email: $authorized_email" >&2 | |
echo "$authorized_email" | |
return 0 | |
fi | |
# Fallback to global git email | |
authorized_email=$(git config --global user.email) | |
if [ -n "$authorized_email" ]; then | |
log "INFO" "Using global git email: $authorized_email" >&2 | |
echo "$authorized_email" | |
return 0 | |
fi | |
else | |
log "WARNING" "Could not parse GitHub repository information from URL: $remote_url" >&2 | |
return 1 | |
fi | |
log "WARNING" "Could not determine authorized email for $repo_name" >&2 | |
return 1 | |
} | |
# Get default branch for repository | |
get_default_branch() { | |
local repo_path="$1" | |
local default_branch | |
# Try to get the default branch from git | |
default_branch=$(git -C "$repo_path" symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@') | |
# If that fails, try to get it from GitHub CLI | |
if [ -z "$default_branch" ]; then | |
local remote_url | |
remote_url=$(git -C "$repo_path" config --get remote.origin.url 2>/dev/null) | |
if [[ "$remote_url" =~ github\.com[:/]([^/]+)/([^/.]+) ]]; then | |
local repo_owner="${BASH_REMATCH[1]}" | |
local repo_name="${BASH_REMATCH[2]}" | |
default_branch=$(gh api repos/"$repo_owner"/"$repo_name" 2>/dev/null | grep -o '"default_branch":"[^"]*"' | cut -d'"' -f4) | |
fi | |
fi | |
# Fallback to common default branch names | |
if [ -z "$default_branch" ]; then | |
# Check if main or master branch exists | |
if git -C "$repo_path" show-ref --verify --quiet refs/heads/main; then | |
default_branch="main" | |
elif git -C "$repo_path" show-ref --verify --quiet refs/heads/master; then | |
default_branch="master" | |
fi | |
fi | |
if [ -n "$default_branch" ]; then | |
echo "$default_branch" | |
return 0 | |
else | |
return 1 | |
fi | |
} | |
# Validate repository directory | |
validate_repo_dir() { | |
if [ ! -d "$REPO_DIR" ]; then | |
log "ERROR" "Repository directory not found: $REPO_DIR" | |
return 1 | |
fi | |
return 0 | |
} | |
# Check if .env variants are ignored | |
check_env_ignored() { | |
local repo_path="$1" | |
local gitignore_file="$repo_path/.gitignore" | |
local env_variants=(".env" ".env.local" ".env.development" ".env.production" ".env.test") | |
# Check for existence of any .env files first | |
local has_env_files=false | |
for variant in "${env_variants[@]}"; do | |
if [ -f "$repo_path/$variant" ]; then | |
has_env_files=true | |
if [ ! -f "$gitignore_file" ] || ! grep -q "^$variant$" "$gitignore_file"; then | |
log "WARNING" "Found $variant but it's not properly ignored in $(basename "$repo_path")" | |
fi | |
fi | |
done | |
# If no .env files found, just return success | |
if [ "$has_env_files" = false ]; then | |
log "DEBUG" "No .env files found in $(basename "$repo_path")" | |
fi | |
# Always return success to continue processing | |
return 0 | |
} | |
# Generate commit message with retry mechanism | |
generate_commit_message() { | |
local attempt=1 | |
local commit_message="" | |
local exclude_args="" | |
# Build exclude args if gitdiff-exclude file exists | |
if [ -f "$GITDIFF_EXCLUDE" ]; then | |
# Process each line in the exclude file that's not a comment | |
while IFS= read -r line || [ -n "$line" ]; do | |
if [[ ! "$line" =~ ^# ]] && [ -n "$line" ]; then | |
# Check if the file exists before excluding it | |
if [ -e "$line" ]; then | |
exclude_args="$exclude_args :!$line" | |
fi | |
fi | |
done < <(grep -v '^#' "$GITDIFF_EXCLUDE" 2>/dev/null || echo "") | |
fi | |
while [ $attempt -le $MAX_RETRIES ]; do | |
log "INFO" "Generating commit message (attempt $attempt/$MAX_RETRIES)..." >&2 | |
log "DEBUG" "Using gitdiff-exclude: $exclude_args" >&2 | |
# Check if running in interactive mode | |
if [ -t 0 ]; then | |
# Create a temporary file for the git diff | |
local diff_file | |
diff_file=$(mktemp) | |
# Create a temporary file for the AI command output | |
local output_file | |
output_file=$(mktemp) | |
# Capture the git diff to a file first | |
git diff --cached --diff-filter=ACMRTUXB $exclude_args > "$diff_file" | |
# Check if diff is empty | |
if [ ! -s "$diff_file" ]; then | |
log "WARNING" "Git diff is empty, using default commit message" >&2 | |
rm -f "$diff_file" "$output_file" | |
echo "Auto-commit: Empty diff detected" | |
return 0 | |
fi | |
# Log the diff size for debugging | |
local diff_size | |
diff_size=$(wc -l < "$diff_file") | |
log "DEBUG" "Git diff size: $diff_size lines" >&2 | |
# Run AI command with increased timeout and capture output | |
log "DEBUG" "Running AI command: $AI_COMMAND" >&2 | |
if timeout $((TIMEOUT_SECONDS*2)) bash -c "cat \"$diff_file\" | $AI_COMMAND 'Could you write a commit message for this? Please only provide the commit message without any additional commentary.'" > "$output_file" 2>/dev/null; then | |
# Read from the output file | |
commit_message=$(cat "$output_file") | |
# Debug the output | |
log "DEBUG" "AI command raw output length: $(echo "$commit_message" | wc -c) bytes" >&2 | |
# Clean up the output - remove any markdown formatting or quotes | |
commit_message=$(echo "$commit_message" | sed -e 's/^```.*$//' -e 's/^```$//' -e 's/^"//' -e 's/"$//') | |
# Make sure it's not empty | |
if [ -n "$commit_message" ]; then | |
log "DEBUG" "Cleaned commit message: $commit_message" >&2 | |
rm -f "$diff_file" "$output_file" | |
echo "$commit_message" | |
return 0 | |
fi | |
fi | |
# Clean up temp files | |
rm -f "$diff_file" "$output_file" | |
else | |
# Non-interactive mode: use a default commit message | |
commit_message="Auto-commit: $(date +%Y-%m-%d %H:%M:%S)" | |
echo "$commit_message" | |
return 0 | |
fi | |
log "WARNING" "Failed to generate commit message on attempt $attempt" >&2 | |
((attempt++)) | |
[ $attempt -le $MAX_RETRIES ] && sleep 2 | |
done | |
# Fallback to a timestamp-based commit message instead of empty | |
echo "Auto-commit: Changes at $(date +%Y-%m-%d\ %H:%M:%S)" | |
return 0 | |
} | |
# Push changes to remote repository | |
push_changes() { | |
local repo_path="$1" | |
local repo_name | |
repo_name=$(basename "$repo_path") | |
local current_branch | |
local default_branch | |
# Get current branch | |
current_branch=$(git -C "$repo_path" symbolic-ref --short HEAD 2>/dev/null) | |
if [ -z "$current_branch" ]; then | |
log "ERROR" "Could not determine current branch for $repo_name" | |
return 1 | |
fi | |
# Get default branch | |
default_branch=$(get_default_branch "$repo_path") | |
if [ -z "$default_branch" ]; then | |
log "WARNING" "Could not determine default branch for $repo_name, using current branch: $current_branch" | |
default_branch="$current_branch" | |
fi | |
log "INFO" "Pushing changes to $current_branch in $repo_name" | |
# Try to push changes | |
if git -C "$repo_path" push origin "$current_branch" 2>/dev/null; then | |
log "SUCCESS" "Successfully pushed changes to $current_branch in $repo_name" | |
# If we're not on the default branch, suggest creating a PR | |
if [ "$current_branch" != "$default_branch" ]; then | |
log "INFO" "You're on branch '$current_branch', not on default branch '$default_branch'" | |
log "INFO" "Consider creating a pull request with: gh pr create -R $(git -C "$repo_path" config --get remote.origin.url | sed 's/.*github.com[:/]\(.*\)\.git/\1/')" | |
fi | |
return 0 | |
else | |
log "ERROR" "Failed to push changes to $current_branch in $repo_name" | |
return 1 | |
fi | |
} | |
# Process single repository | |
process_repository() { | |
local repo="$1" | |
local repo_name | |
repo_name=$(basename "$repo") | |
local original_email | |
local authorized_email | |
local has_committed=false | |
log "INFO" "Processing repository: $repo_name" | |
# Verify it's a Git repository | |
if [ ! -d "$repo/.git" ]; then | |
log "WARNING" "Not a Git repository: $repo_name" | |
return 0 # Continue to next repo | |
fi | |
# Check .env files - always continue | |
check_env_ignored "$repo" | |
# Navigate to repository | |
if ! cd "$repo" 2>/dev/null; then | |
log "ERROR" "Failed to change directory to $repo_name" | |
return 0 # Continue to next repo | |
fi | |
# Store original email configuration | |
original_email=$(git config user.email) | |
# Get authorized email for this repository | |
if authorized_email=$(get_authorized_email "$repo"); then | |
# Set the authorized email for this commit | |
log "INFO" "Setting commit email to: $authorized_email for $repo_name" | |
git config user.email "$authorized_email" | |
else | |
log "WARNING" "Using default email configuration for $repo_name" | |
fi | |
# Check for changes | |
if ! git status --porcelain | grep '^[A-Z]' > /dev/null; then | |
log "INFO" "No changes to commit in $repo_name" | |
# Restore original email if it was set | |
if [ -n "$original_email" ]; then | |
git config user.email "$original_email" | |
fi | |
return 0 | |
fi | |
log "INFO" "Changes detected in $repo_name" | |
# Stage all changes | |
git add . | |
# Generate commit message | |
local commit_message | |
commit_message=$(generate_commit_message) | |
# Check if we should use empty commit message | |
if [ "$commit_message" = "__USE_EMPTY_COMMIT__" ]; then | |
log "WARNING" "AI command failed to generate commit message, using empty commit message" | |
# Commit changes with empty message | |
if git commit --allow-empty-message -m ""; then | |
log "SUCCESS" "Changes committed in $repo_name with empty message" | |
has_committed=true | |
# Push changes if auto-push is enabled | |
if [ "$AUTO_PUSH" = true ]; then | |
push_changes "$repo" | |
fi | |
else | |
log "ERROR" "Failed to commit changes with empty message in $repo_name" | |
fi | |
else | |
log "INFO" "Generated commit message for $repo_name:" | |
log "INFO" "$commit_message" | |
# Commit changes with the generated message | |
if git commit -m "$commit_message"; then | |
log "SUCCESS" "Changes committed in $repo_name" | |
has_committed=true | |
# Push changes if auto-push is enabled | |
if [ "$AUTO_PUSH" = true ]; then | |
push_changes "$repo" | |
fi | |
else | |
log "ERROR" "Failed to commit changes in $repo_name" | |
fi | |
fi | |
# Restore original email if it was set | |
if [ -n "$original_email" ]; then | |
git config user.email "$original_email" | |
fi | |
if [ "$has_committed" = true ]; then | |
return 0 | |
else | |
return 1 | |
fi | |
} | |
main() { | |
log "INFO" "Starting auto-commit process" | |
# Check dependencies but continue anyway | |
check_dependencies || true | |
# Check GitHub CLI authentication but continue anyway | |
check_gh_auth || true | |
# Validate repository directory but continue anyway | |
validate_repo_dir || true | |
# Process each repository | |
local success_count=0 | |
local fail_count=0 | |
# Use nullglob to handle no matches gracefully | |
shopt -s nullglob | |
for repo in "$REPO_DIR"/*; do | |
if [ -d "$repo" ]; then | |
if process_repository "$repo"; then | |
((success_count++)) | |
else | |
((fail_count++)) | |
fi | |
fi | |
done | |
shopt -u nullglob | |
log "INFO" "Auto-commit process completed" | |
log "INFO" "Successful repositories: $success_count" | |
log "INFO" "Failed repositories: $fail_count" | |
# Always exit successfully | |
return 0 | |
} | |
# Run main function | |
main |
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
# General | |
*.log | |
*.lock | |
package-lock.json | |
yarn.lock | |
pnpm-lock.yaml | |
.DS_Store | |
Thumbs.db | |
# Node.js / TypeScript / JavaScript | |
node_modules/ | |
dist/ | |
build/ | |
coverage/ | |
*.min.js | |
*.map | |
.npm/ | |
.nvmrc | |
# Python | |
__pycache__/ | |
*.pyc | |
*.pyo | |
*.pyd | |
env/ | |
venv/ | |
.Python | |
mypy_cache/ | |
pylint.d/ | |
# Dart / Flutter | |
.dart_tool/ | |
.buildlog/ | |
pubspec.lock | |
*.iml | |
# Java / Kotlin | |
*.class | |
*.jar | |
*.war | |
*.ear | |
.gradle/ | |
build/ | |
out/ | |
# Rust | |
target/ | |
Cargo.lock | |
# Go | |
bin/ | |
*.test | |
*.mod | |
# C / C++ | |
*.o | |
*.obj | |
*.so | |
*.exe | |
*.dll | |
*.dylib | |
CMakeCache.txt | |
# Miscellaneous | |
.vscode/ | |
.idea/ | |
*.swp | |
*.swo | |
*.swn | |
*.bak | |
*.tmp |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
see https://github.com/egoist/shell-ask