Skip to content

Instantly share code, notes, and snippets.

@WomB0ComB0
Last active March 29, 2025 05:11
Show Gist options
  • Save WomB0ComB0/43acb03ecbd012d761cadb440698faa6 to your computer and use it in GitHub Desktop.
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.
#!/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
# 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
@WomB0ComB0
Copy link
Author

@WomB0ComB0
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment