Skip to content

Instantly share code, notes, and snippets.

@WomB0ComB0
Last active October 2, 2025 16:25
Show Gist options
  • Select an option

  • Save WomB0ComB0/e6090cc36ea95ecfb1f7da743c3750fe to your computer and use it in GitHub Desktop.

Select an option

Save WomB0ComB0/e6090cc36ea95ecfb1f7da743c3750fe 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
set -euo pipefail
# Enhanced Git Auto-Commit Script
# Automatically commits and pushes changes across multiple repositories with AI-generated messages
# --- Configuration and Defaults ---
readonly SCRIPT_NAME="$(basename "$0")"
readonly SCRIPT_VERSION="2.0.1"
readonly HOME="${HOME}"
# IMPORTANT: AI_COMMAND must be a bash array
declare -a DEFAULT_AI_COMMAND=("ask" "cm" "-m" "gemini-2.0-flash")
readonly DEFAULT_REPO_DIR="$HOME/github"
readonly LOG_FILE="/tmp/git-auto-commit-$(date +%Y%m%d-%H%M%S).log"
readonly MAX_RETRIES=3
readonly TIMEOUT_SECONDS=30
readonly DEFAULT_GITDIFF_EXCLUDE="$HOME/.config/git/gitdiff-exclude"
readonly DEFAULT_AUTO_PUSH=true
readonly MAX_COMMIT_MSG_LENGTH=72
readonly MAX_BRANCH_NAME_LENGTH=30
# Initialize AI_COMMAND with default
declare -a AI_COMMAND=("${DEFAULT_AI_COMMAND[@]}")
readonly CONFIG_FILE="$HOME/.git-auto-commit.conf"
# Load configuration if available
load_config() {
if [[ -f "$CONFIG_FILE" ]]; then
log "DEBUG" "Loading configuration from $CONFIG_FILE"
# Validate config file permissions for security
local config_perms
config_perms=$(stat -f %A "$CONFIG_FILE" 2>/dev/null || stat -c %a "$CONFIG_FILE" 2>/dev/null || echo "")
if [[ -n "$config_perms" && "$config_perms" != "600" ]]; then
log "WARNING" "Config file $CONFIG_FILE has insecure permissions. Should be 600."
fi
# Source config file safely
set +e
source "$CONFIG_FILE"
local source_result=$?
set -e
if [[ $source_result -ne 0 ]]; then
log "WARNING" "Failed to load configuration file"
fi
fi
}
# Set configuration variables with defaults
REPO_DIR="${REPO_DIR:-$DEFAULT_REPO_DIR}"
GITDIFF_EXCLUDE="${GITDIFF_EXCLUDE:-$DEFAULT_GITDIFF_EXCLUDE}"
AUTO_PUSH="${AUTO_PUSH:-$DEFAULT_AUTO_PUSH}"
# --- Utility Functions ---
# Check if running as root (security concern)
check_root() {
if [[ $EUID -eq 0 ]]; then
log "ERROR" "This script should not be run as root for security reasons"
return 1
fi
}
# Validate configuration values
validate_config() {
if [[ ! "$AUTO_PUSH" =~ ^(true|false)$ ]]; then
log "ERROR" "AUTO_PUSH must be 'true' or 'false'"
return 1
fi
if [[ ! "$TIMEOUT_SECONDS" =~ ^[0-9]+$ ]] || (( TIMEOUT_SECONDS < 5 || TIMEOUT_SECONDS > 300 )); then
log "ERROR" "TIMEOUT_SECONDS must be between 5 and 300"
return 1
fi
if [[ ${#AI_COMMAND[@]} -eq 0 ]]; then
log "ERROR" "AI_COMMAND cannot be empty"
return 1
fi
}
# Enhanced logging with levels and colors
log() {
local level="$1"
shift
local message="$*"
local timestamp
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
local color=""
local reset="\033[0m"
case "$level" in
"ERROR") color="\033[31m" ;; # Red
"WARNING") color="\033[33m" ;; # Yellow
"SUCCESS") color="\033[32m" ;; # Green
"INFO") color="\033[36m" ;; # Cyan
"DEBUG") color="\033[90m" ;; # Gray
esac
# Log to file without colors
echo "[$timestamp] [$level] $message" >> "$LOG_FILE" || true
# Output to stderr with colors if terminal supports it
if [[ -t 2 ]]; then
echo -e "${color}[$timestamp] [$level] $message${reset}" >&2
else
echo "[$timestamp] [$level] $message" >&2
fi
}
# Cleanup function for temporary files
cleanup() {
local exit_code=$?
if [[ -n "${temp_files:-}" ]]; then
for temp_file in "${temp_files[@]}"; do
[[ -f "$temp_file" ]] && rm -f "$temp_file" 2>/dev/null || true
done
fi
exit $exit_code
}
# Enhanced error handling
handle_error() {
local exit_code=$?
local line_number=${1:-"unknown"}
log "ERROR" "Script failed at line $line_number with exit code $exit_code"
cleanup
}
# Only set error trap if not in dry run mode
if [[ "${DRY_RUN:-false}" != "true" ]]; then
trap 'handle_error $LINENO' ERR
fi
trap 'cleanup' EXIT INT TERM
# Track temporary files for cleanup
declare -a temp_files=()
create_temp_file() {
local temp_file
temp_file=$(mktemp)
temp_files+=("$temp_file")
echo "$temp_file"
}
# --- Dependency and Authentication Checks ---
check_dependencies() {
local missing_deps=()
local deps=("git" "grep" "timeout")
# Optional dependencies
local optional_deps=("gh" "sed" "awk")
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[*]}"
log "INFO" "Please install missing dependencies and try again"
return 1
fi
# Check optional dependencies
for dep in "${optional_deps[@]}"; do
if ! command -v "$dep" >/dev/null 2>&1; then
log "WARNING" "Optional command not found: $dep (some features may be limited)"
fi
done
# Check git version
local git_version
git_version=$(git --version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "unknown")
log "DEBUG" "Using Git version: $git_version"
return 0
}
check_gh_auth() {
if ! command -v gh >/dev/null 2>&1; then
log "WARNING" "GitHub CLI not found. Account switching features will be disabled"
return 0 # Not a fatal error
fi
if ! gh auth status &>/dev/null; then
log "WARNING" "GitHub CLI not authenticated. Account switching features will be disabled"
return 0 # Not a fatal error
fi
# Test API access
if ! gh api user --jq .login >/dev/null 2>&1; then
log "WARNING" "GitHub CLI authentication appears invalid. Account switching features will be disabled"
return 0 # Not a fatal error
fi
return 0
}
# Enhanced GitHub user detection
get_active_gh_user() {
local user
if ! command -v gh >/dev/null 2>&1; then
return 1
fi
user=$(gh api user --jq .login 2>/dev/null || echo "")
if [[ -z "$user" ]]; then
log "DEBUG" "Could not determine active GitHub user"
return 1
fi
echo "$user"
}
# Improved repository account detection
get_repo_account() {
local repo_path="$1"
local remote_url
remote_url=$(git -C "$repo_path" config --get remote.origin.url 2>/dev/null || echo "")
if [[ -z "$remote_url" ]]; then
log "DEBUG" "No remote origin URL found for $(basename "$repo_path")"
return 1
fi
# Handle both SSH and HTTPS URLs
if [[ "$remote_url" =~ github\.com[:/]([^/]+)/([^/]+)(\.git)?$ ]]; then
echo "${BASH_REMATCH[1]}"
return 0
fi
log "DEBUG" "Could not parse GitHub repository owner from: $remote_url"
return 1
}
# Enhanced account switching with better error handling
switch_to_repo_account() {
local repo_path="$1"
local repo_name
repo_name=$(basename "$repo_path")
if ! command -v gh >/dev/null 2>&1; then
log "DEBUG" "GitHub CLI not available, skipping account switching for $repo_name"
return 0
fi
local repo_owner
if ! repo_owner=$(get_repo_account "$repo_path"); then
log "DEBUG" "Could not determine repository owner for $repo_name"
return 0 # Not a fatal error
fi
local current_user
if ! current_user=$(get_active_gh_user); then
log "DEBUG" "Could not determine current GitHub user"
return 0 # Not a fatal error
fi
local target_account="$current_user"
# Account mapping with validation
declare -A account_mappings=(
["WomB0ComB0"]="WomB0ComB0"
)
if [[ -n "${account_mappings[$repo_owner]:-}" ]]; then
target_account="${account_mappings[$repo_owner]}"
fi
if [[ "$current_user" != "$target_account" ]]; then
log "INFO" "Switching from '$current_user' to '$target_account' for $repo_name"
if gh auth switch --user "$target_account" >/dev/null 2>&1; then
configure_git_user "$target_account"
log "INFO" "Successfully switched to $target_account and updated git config"
else
log "WARNING" "Failed to switch to $target_account for $repo_name, continuing with current user"
return 0 # Not a fatal error
fi
else
log "DEBUG" "Already using correct account ($current_user) for $repo_name"
fi
return 0
}
# Separate function for git user configuration
configure_git_user() {
local account="$1"
case "$account" in
"WomB0ComB0")
git config --global user.name "WomB0ComB0"
git config --global user.email "[email protected]"
;;
*)
log "DEBUG" "No git configuration defined for account: $account"
;;
esac
}
get_authorized_email() {
local repo_path="$1"
# Try to switch to repo account (non-fatal if it fails)
switch_to_repo_account "$repo_path"
local authorized_email
authorized_email=$(git config --global user.email 2>/dev/null || echo "")
if [[ -n "$authorized_email" ]]; then
echo "$authorized_email"
return 0
fi
log "DEBUG" "Could not determine authorized email for $(basename "$repo_path")"
return 1
}
# Enhanced default branch detection
get_default_branch() {
local repo_path="$1"
local default_branch
# Try multiple methods to determine default branch
default_branch=$(git -C "$repo_path" symbolic-ref refs/remotes/origin/HEAD 2>/dev/null | sed 's@^refs/remotes/origin/@@' || echo "")
if [[ -z "$default_branch" ]]; then
# Try to determine from remote
default_branch=$(git -C "$repo_path" ls-remote --symref origin HEAD 2>/dev/null | awk '/^ref:/ {print $2}' | sed 's@refs/heads/@@' || echo "")
fi
if [[ -z "$default_branch" ]]; then
# Attempt to detect the default branch by checking for the existence of common branch names in order of preference.
for candidate in main master develop trunk default; do
if git -C "$repo_path" show-ref --verify --quiet "refs/heads/$candidate" 2>/dev/null; then
default_branch="$candidate"
break
fi
done
fi
if [[ -n "$default_branch" ]]; then
echo "$default_branch"
else
log "DEBUG" "Could not determine default branch for $(basename "$repo_path")"
return 1
fi
}
validate_repo_dir() {
if [[ ! -d "$REPO_DIR" ]]; then
log "ERROR" "Repository directory not found: $REPO_DIR"
return 1
fi
if [[ ! -r "$REPO_DIR" ]]; then
log "ERROR" "Repository directory not readable: $REPO_DIR"
return 1
fi
return 0
}
# Enhanced environment file checking
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" ".env.example")
local has_env_files=false
local unignored_files=()
for variant in "${env_variants[@]}"; do
if [[ -f "$repo_path/$variant" ]]; then
has_env_files=true
if [[ "$variant" == ".env.example" ]]; then
continue # .env.example should typically be committed
fi
if [[ ! -f "$gitignore_file" ]] || ! grep -q "^${variant//./\\.}$" "$gitignore_file" 2>/dev/null; then
unignored_files+=("$variant")
fi
fi
done
if [[ ${#unignored_files[@]} -gt 0 ]]; then
log "WARNING" "Found unignored environment files in $(basename "$repo_path"): ${unignored_files[*]}"
log "INFO" "Consider adding these to .gitignore to prevent accidental commits of sensitive data"
fi
if [[ "$has_env_files" == false ]]; then
log "DEBUG" "No .env files found in $(basename "$repo_path")"
fi
return 0
}
# Enhanced AI interaction with better error handling
call_ai_command() {
local prompt="$1"
local input_file="$2"
local output_file
output_file=$(create_temp_file)
# Check if AI command is available
if ! command -v "${AI_COMMAND[0]}" >/dev/null 2>&1; then
log "WARNING" "AI command '${AI_COMMAND[0]}' not found. Using fallback messages"
return 1
fi
local attempt=1
local max_attempts=2
while (( attempt <= max_attempts )); do
log "DEBUG" "Calling AI command (attempt $attempt/$max_attempts)"
if timeout "$TIMEOUT_SECONDS" "${AI_COMMAND[@]}" "$prompt" < "$input_file" > "$output_file" 2>/dev/null; then
local response
response=$(<"$output_file" || echo "")
if [[ -n "$response" ]]; then
echo "$response"
return 0
fi
fi
log "DEBUG" "AI command failed on attempt $attempt"
((attempt++))
[[ $attempt -le $max_attempts ]] && sleep 1
done
return 1
}
# Enhanced branch name generation with validation
generate_branch_name() {
local diff_file="$1"
local branch_name=""
local prompt="Based on this git diff, suggest a concise branch name (kebab-case, no spaces, max ${MAX_BRANCH_NAME_LENGTH} chars). Only return the branch name, nothing else."
if branch_name=$(call_ai_command "$prompt" "$diff_file"); then
# Clean and validate branch name
branch_name=$(echo "$branch_name" | sed -e 's/^```.*$//' -e 's/^```$//' -e 's/^"//' -e 's/"$//' -e 's/[^a-zA-Z0-9-]//g' | head -1 | xargs echo -n)
branch_name=$(echo "$branch_name" | sed 's/^-*//')
# Validate branch name
if [[ -n "$branch_name" && ${#branch_name} -le $MAX_BRANCH_NAME_LENGTH && "$branch_name" =~ ^[a-zA-Z0-9-]+$ ]]; then
log "DEBUG" "Generated branch name: $branch_name"
echo "$branch_name"
return 0
fi
fi
local fallback_name="auto-commit-$(date +%Y%m%d-%H%M%S)"
log "DEBUG" "Using fallback branch name: $fallback_name"
echo "$fallback_name"
return 0
}
# Enhanced commit message generation
generate_commit_message() {
local repo_path="$1"
local exclude_args=""
if [[ -f "$GITDIFF_EXCLUDE" ]]; then
while IFS= read -r line; do
# Skip comments and empty lines
if [[ ! "$line" =~ ^# && -n "$line" ]]; then
# Expand tilde to home directory
local expanded_path="${line/#\~/$HOME}"
if [[ -e "$expanded_path" ]]; then
exclude_args+=" :!$expanded_path"
fi
fi
done < <(grep -v '^#' "$GITDIFF_EXCLUDE" 2>/dev/null || true)
fi
local diff_file
diff_file=$(create_temp_file)
git -C "$repo_path" diff --cached --diff-filter=ACMRTUXB $exclude_args > "$diff_file" 2>/dev/null || true
if [[ ! -s "$diff_file" ]]; then
log "DEBUG" "Git diff is empty, using fallback message"
echo "Auto-commit: No changes to commit"
return 0
fi
local prompt="Write a concise, well-structured commit message for this git diff. Follow conventional commit format if applicable. Return only the commit message, no other text or formatting. Maximum ${MAX_COMMIT_MSG_LENGTH} characters."
if commit_message=$(call_ai_command "$prompt" "$diff_file"); then
# Clean and validate commit message
commit_message=$(echo "$commit_message" | sed -e 's/^```.*$//' -e 's/^```$//' -e 's/^"//' -e 's/"$//' | head -1 | xargs)
# Validate commit message
if [[ -n "$commit_message" && ${#commit_message} -gt 5 && ${#commit_message} -le $MAX_COMMIT_MSG_LENGTH ]]; then
log "DEBUG" "Generated commit message: $commit_message"
echo "$commit_message"
return 0
fi
fi
log "DEBUG" "AI message generation failed. Using fallback message"
echo "Auto-commit: Changes at $(date +%Y-%m-%d\ %H:%M:%S)"
return 0
}
# Enhanced branch status checking
fetch_and_check_status() {
local repo_path="$1"
local current_branch="$2"
log "DEBUG" "Fetching latest changes for $current_branch"
if ! git -C "$repo_path" fetch origin "$current_branch" 2>/dev/null; then
log "DEBUG" "Failed to fetch from origin/$current_branch"
return 1
fi
local behind_count ahead_count
behind_count=$(git -C "$repo_path" rev-list --count HEAD..origin/"$current_branch" 2>/dev/null || echo "0")
ahead_count=$(git -C "$repo_path" rev-list --count origin/"$current_branch"..HEAD 2>/dev/null || echo "0")
if (( behind_count > 0 )); then
log "INFO" "Local branch is $behind_count commits behind origin/$current_branch"
if (( ahead_count > 0 )); then
log "INFO" "Local branch is also $ahead_count commits ahead (diverged)"
fi
return 1
fi
if (( ahead_count > 0 )); then
log "DEBUG" "Local branch is $ahead_count commits ahead of origin/$current_branch"
fi
return 0
}
# Enhanced branch creation and pushing
create_and_push_branch() {
local repo_path="$1"
local repo_name="$2"
local diff_file="$3"
local branch_name
branch_name=$(generate_branch_name "$diff_file")
# Check if branch already exists
if git -C "$repo_path" show-ref --verify --quiet refs/heads/"$branch_name" 2>/dev/null; then
log "WARNING" "Branch '$branch_name' already exists, appending timestamp"
branch_name="${branch_name}-$(date +%H%M%S)"
fi
log "INFO" "Creating new branch '$branch_name' for $repo_name"
if git -C "$repo_path" checkout -b "$branch_name" 2>/dev/null; then
log "SUCCESS" "Created and switched to branch '$branch_name'"
if git -C "$repo_path" push -u origin "$branch_name" 2>/dev/null; then
log "SUCCESS" "Successfully pushed new branch '$branch_name' for $repo_name"
local repo_slug
repo_slug=$(git -C "$repo_path" config --get remote.origin.url | sed -E 's/.*github.com[:/]([^/]+\/[^/.]+)(\.git)?/\1/' || echo "")
if [[ -n "$repo_slug" && -x "$(command -v gh)" ]]; then
log "INFO" "Consider creating a pull request: gh pr create -R $repo_slug --head $branch_name"
fi
return 0
else
log "ERROR" "Failed to push new branch '$branch_name' for $repo_name"
# Cleanup failed branch
git -C "$repo_path" checkout - >/dev/null 2>&1 || true
git -C "$repo_path" branch -D "$branch_name" >/dev/null 2>&1 || true
return 1
fi
else
log "ERROR" "Failed to create branch '$branch_name' for $repo_name"
return 1
fi
}
# Enhanced push logic with better error handling
push_changes() {
local repo_path="$1"
local repo_name
repo_name=$(basename "$repo_path")
local current_branch
current_branch=$(git -C "$repo_path" symbolic-ref --short HEAD 2>/dev/null || echo "")
if [[ -z "$current_branch" ]]; then
log "ERROR" "Could not determine current branch for $repo_name"
return 1
fi
if fetch_and_check_status "$repo_path" "$current_branch"; then
log "INFO" "Pushing changes to origin/$current_branch in $repo_name"
if git -C "$repo_path" push origin "$current_branch" 2>/dev/null; then
log "SUCCESS" "Successfully pushed changes to $current_branch in $repo_name"
return 0
else
log "WARNING" "Failed to push to $current_branch. Remote may have rejected it"
fi
else
log "INFO" "Local branch is behind or diverged from origin/$current_branch"
fi
log "INFO" "Falling back to creating a new branch for changes in $repo_name"
local diff_file
diff_file=$(create_temp_file)
git -C "$repo_path" diff --cached > "$diff_file" || true
if create_and_push_branch "$repo_path" "$repo_name" "$diff_file"; then
return 0
else
log "ERROR" "Failed to create and push new branch for $repo_name"
return 1
fi
}
# --- Main Repository Processing ---
process_repository() {
local repo="$1"
local repo_name
repo_name=$(basename "$repo")
log "INFO" "Processing repository: $repo_name"
if [[ ! -d "$repo/.git" ]]; then
log "WARNING" "Not a Git repository: $repo_name. Skipping"
return 1
fi
# Use a subshell to isolate repository operations
(
# Temporarily disable error exit for this subshell
set +e
cd "$repo" || {
log "ERROR" "Failed to change directory to $repo_name. Skipping"
return 1
}
# Store original email for restoration
local original_email
original_email=$(git config user.email 2>/dev/null || echo "")
# Set up cleanup for this repo
repo_cleanup() {
if [[ -n "$original_email" ]]; then
git config user.email "$original_email" 2>/dev/null || true
fi
}
trap repo_cleanup EXIT
# Security checks
check_env_ignored "$(pwd)"
# Configure authentication
local authorized_email
if authorized_email=$(get_authorized_email "$(pwd)"); then
log "INFO" "Using commit email: $authorized_email"
git config user.email "$authorized_email" || log "WARNING" "Failed to set commit email"
else
log "DEBUG" "Could not determine authorized email. Using existing config for $repo_name"
fi
# Check for changes
local status_output
status_output=$(git status --porcelain 2>/dev/null || echo "")
if [[ -z "$status_output" ]]; then
log "INFO" "No changes to commit in $repo_name"
return 1
fi
log "INFO" "Changes detected in $repo_name"
# Add all changes
if ! git add . 2>/dev/null; then
log "ERROR" "Failed to stage changes in $repo_name"
return 1
fi
# Check if there are actually staged changes
if git diff --cached --quiet 2>/dev/null; then
log "INFO" "No staged changes to commit in $repo_name after 'git add'"
return 1
fi
# Generate commit message
local commit_message
commit_message=$(generate_commit_message "$(pwd)")
if [[ -z "$commit_message" || "$commit_message" == "Auto-commit: No changes to commit" ]]; then
log "WARNING" "Commit message generation resulted in no message. Skipping commit"
return 1
fi
log "INFO" "Committing in $repo_name with message: '$commit_message'"
# Attempt to commit
if git commit -m "$commit_message" 2>/dev/null; then
log "SUCCESS" "Changes committed in $repo_name"
# Push changes if enabled
if [[ "$AUTO_PUSH" == "true" ]]; then
push_changes "$(pwd)" || log "WARNING" "Push failed for $repo_name"
fi
return 0
else
log "ERROR" "Failed to commit changes in $repo_name"
return 1
fi
)
}
# --- Main Function ---
show_usage() {
cat << EOF
$SCRIPT_NAME v$SCRIPT_VERSION - Automated Git commit and push tool
USAGE:
$SCRIPT_NAME [OPTIONS]
OPTIONS:
-h, --help Show this help message
-v, --version Show version information
-d, --dry-run Show what would be done without making changes
--no-push Disable automatic pushing
--config FILE Use alternative configuration file
CONFIGURATION:
Configuration is loaded from: $CONFIG_FILE
Example configuration:
REPO_DIR="$HOME/projects"
AUTO_PUSH=true
AI_COMMAND=("ask" "cm" "-m" "gemini-2.0-flash")
GITDIFF_EXCLUDE="$HOME/.config/git/gitdiff-exclude"
ENVIRONMENT:
LOG_FILE: $LOG_FILE
For more information, see the documentation.
EOF
}
show_version() {
echo "$SCRIPT_NAME v$SCRIPT_VERSION"
}
parse_arguments() {
while [[ $# -gt 0 ]]; do
case $1 in
-h|--help)
show_usage
exit 0
;;
-v|--version)
show_version
exit 0
;;
-d|--dry-run)
DRY_RUN=true
log "INFO" "Dry run mode enabled"
shift
;;
--no-push)
AUTO_PUSH=false
log "INFO" "Auto-push disabled"
shift
;;
--config)
CONFIG_FILE="$2"
shift 2
;;
*)
log "ERROR" "Unknown option: $1"
show_usage
exit 1
;;
esac
done
}
main() {
# Parse arguments first to handle dry-run mode
parse_arguments "$@"
# --- FIX 1: Enable the error handler always ---
# This ensures we get a line number if the script fails for any reason.
trap 'handle_error $LINENO' ERR
log "INFO" "Starting $SCRIPT_NAME v$SCRIPT_VERSION"
log "DEBUG" "Configuration: REPO_DIR=$REPO_DIR, AUTO_PUSH=$AUTO_PUSH"
# Load configuration
load_config
# Perform initial checks
if ! check_root; then
log "ERROR" "Initial checks failed. Aborting"
return 1
fi
if ! validate_config; then
log "ERROR" "Configuration validation failed. Aborting"
return 1
fi
if ! check_dependencies; then
log "ERROR" "Dependency checks failed. Aborting"
return 1
fi
# GitHub CLI auth is optional, so we don't fail on this
check_gh_auth
if ! validate_repo_dir; then
log "ERROR" "Repository directory validation failed. Aborting"
return 1
fi
local success_count=0
local fail_count=0
local total_repos=0
# --- FIX 2: Use a more robust loop with `find` ---
shopt -s nullglob
log "DEBUG" "Starting to process categories in '$REPO_DIR'"
for category in "$REPO_DIR"/*; do
if [[ -d "$category" ]]; then
local category_name
category_name=$(basename "$category")
log "INFO" "Processing category: $category_name"
# Use find to get a clean list of subdirectories. This is safer than globbing.
# -mindepth 1 and -maxdepth 1 ensure we only get immediate children.
local repo_list=()
while IFS= read -r d; do repo_list+=("$d"); done < <(find "$category" -mindepth 1 -maxdepth 1 -type d)
if [[ ${#repo_list[@]} -eq 0 ]]; then
log "DEBUG" "No repository directories found in category '$category_name'."
continue
fi
for repo in "${repo_list[@]}"; do
log "DEBUG" "Processing path: $repo"
((total_repos++))
if [[ "${DRY_RUN:-false}" == "true" ]]; then
log "INFO" "[DRY RUN] Would process repository: $(basename "$repo")"
continue
fi
# Process repository with error handling
if process_repository "$repo"; then
((success_count++))
else
((fail_count++))
log "DEBUG" "Repository $(basename "$repo") processing failed or had no changes"
fi
done
else
log "DEBUG" "Skipping non-directory item: $category"
fi
done
shopt -u nullglob
# Summary
log "INFO" "Auto-commit process completed"
log "INFO" "Total repositories found: $total_repos"
log "INFO" "Repositories with new commits: $success_count"
log "INFO" "Repositories skipped or failed: $fail_count"
if [[ "${DRY_RUN:-false}" == "true" ]]; then
log "INFO" "This was a dry run - no actual changes were made"
fi
return 0
}
# Only execute main if script is run directly
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi
# =============================================================================
# COMPREHENSIVE .GITIGNORE FOR MULTI-LANGUAGE DEVELOPMENT
# =============================================================================
# =============================================================================
# OPERATING SYSTEM FILES
# =============================================================================
# macOS
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Icon?
# Windows
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
*.stackdump
[Dd]esktop.ini
$RECYCLE.BIN/
*.cab
*.msi
*.msix
*.msm
*.msp
*.lnk
# Linux
*~
.fuse_hidden*
.directory
.Trash-*
.nfs*
# =============================================================================
# GENERAL DEVELOPMENT FILES
# =============================================================================
# Logs and temporary files
*.log
*.log.*
*.tmp
*.temp
*.bak
*.swp
*.swo
*.swn
*~
# Archive files
*.zip
*.tar.gz
*.rar
*.7z
# Lock files (general)
*.lock
*.lockb
# =============================================================================
# NODE.JS / JAVASCRIPT / TYPESCRIPT
# =============================================================================
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Package managers
package-lock.json
yarn.lock
pnpm-lock.yaml
.npmrc
.yarnrc
.yarn/
.pnp.*
# Build outputs
dist/
build/
out/
.next/
.nuxt/
.vuepress/dist/
.serverless/
# Coverage and testing
coverage/
.nyc_output/
.coverage/
*.lcov
.mocha-tmp/
# Runtime and environment
.npm/
.node_repl_history
.nvmrc
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Minified files and source maps
*.min.js
*.min.css
*.map
# TypeScript
*.tsbuildinfo
# ESLint and Prettier
.eslintcache
.prettiercache
# Webpack
.webpack/
# =============================================================================
# PYTHON
# =============================================================================
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
*.pyc
*.pyo
*.pyd
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# Virtual environments
venv/
env/
ENV/
env.bak/
venv.bak/
.venv/
.ENV/
.env
pyvenv.cfg
# Testing and coverage
.pytest_cache/
.coverage
.coverage.*
htmlcov/
.tox/
.nox/
coverage.xml
*.cover
*.py,cover
.hypothesis/
# Jupyter Notebook
.ipynb_checkpoints
*/.ipynb_checkpoints/*
# IPython
profile_default/
ipython_config.py
# Type checking and linting
mypy_cache/
.mypy_cache/
.dmypy.json
dmypy.json
pylint.d/
.pylintrc
# Django
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
media/
# Flask
instance/
.webassets-cache
# Scrapy
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
target/
# Celery
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# =============================================================================
# JAVA / KOTLIN / SCALA
# =============================================================================
# Compiled class files
*.class
*.jar
*.war
*.ear
*.nar
# Log files
hs_err_pid*
# BlueJ files
*.ctxt
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files
*.zip
*.tar.gz
*.rar
# Maven
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
.mvn/wrapper/maven-wrapper.jar
# Gradle
.gradle/
build/
out/
gradle-app.setting
!gradle-wrapper.jar
!gradle-wrapper.properties
!gradle/wrapper/gradle-wrapper.jar
!gradle/wrapper/gradle-wrapper.properties
# IntelliJ IDEA
*.iml
*.ipr
*.iws
.idea/
out/
# Eclipse
.classpath
.project
.settings/
bin/
tmp/
*.tmp
*.bak
local.properties
# NetBeans
nbproject/private/
nbbuild/
nbdist/
.nb-gradle/
# Android
*.apk
*.ap_
*.aab
*.dex
bin/
gen/
proguard/
lint.xml
# Kotlin
*.kt.class
# =============================================================================
# C / C++
# =============================================================================
# Prerequisites
*.d
# Object files
*.o
*.ko
*.obj
*.elf
# Linker output
*.ilk
*.map
*.exp
# Precompiled Headers
*.gch
*.pch
# Libraries
*.lib
*.a
*.la
*.lo
# Shared objects (inc. Windows DLLs)
*.dll
*.so
*.so.*
*.dylib
# Executables
*.exe
*.out
*.app
*.i*86
*.x86_64
*.hex
# Debug files
*.dSYM/
*.su
*.idb
*.pdb
# Kernel Module Compile Results
*.mod*
*.cmd
.tmp_versions/
modules.order
Module.symvers
Mkfile.old
dkms.conf
# CMake
CMakeCache.txt
CMakeFiles/
CMakeScripts/
Testing/
Makefile
cmake_install.cmake
install_manifest.txt
compile_commands.json
CTestTestfile.cmake
_deps/
# Build directories
build/
debug/
release/
# =============================================================================
# RUST
# =============================================================================
# Generated by Cargo
/target/
Cargo.lock
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# These are backup files generated by rustfmt
**/*.rs.bk
# MSVC Windows builds of rustc generate these, which store debugging information
*.pdb
# =============================================================================
# GO
# =============================================================================
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib
# Test binary, built with `go test -c`
*.test
# Output of the go coverage tool
*.out
# Dependency directories
vendor/
# Go workspace file
go.work
# Go modules
go.mod
go.sum
# Air (hot reload tool)
tmp/
# =============================================================================
# DART / FLUTTER
# =============================================================================
# Build outputs
/build/
/.dart_tool/
/.buildlog/
# Files and directories created by pub
.dart_tool/
.packages
build/
pubspec.lock
# Web related
lib/generated_plugin_registrant.dart
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android related
**/android/**/gradle-wrapper.jar
**/android/.gradle
**/android/captures/
**/android/gradlew
**/android/gradlew.bat
**/android/local.properties
**/android/**/GeneratedPluginRegistrant.java
# iOS/XCode related
**/ios/**/*.mode1v3
**/ios/**/*.mode2v3
**/ios/**/*.moved-aside
**/ios/**/*.pbxuser
**/ios/**/*.perspectivev3
**/ios/**/*sync/
**/ios/**/.sconsign.dblite
**/ios/**/.tags*
**/ios/**/.vagrant/
**/ios/**/DerivedData/
**/ios/**/Icon?
**/ios/**/Pods/
**/ios/**/.symlinks/
**/ios/**/profile
**/ios/**/xcuserdata
**/ios/.generated/
**/ios/Flutter/App.framework
**/ios/Flutter/Flutter.framework
**/ios/Flutter/Generated.xcconfig
**/ios/Flutter/app.flx
**/ios/Flutter/app.zip
**/ios/Flutter/flutter_assets/
**/ios/ServiceDefinitions.json
**/ios/Runner/GeneratedPluginRegistrant.*
# Coverage
coverage/
# Exceptions to above rules
!**/ios/**/default.mode1v3
!**/ios/**/default.mode2v3
!**/ios/**/default.pbxuser
!**/ios/**/default.perspectivev3
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
# =============================================================================
# DATABASE FILES
# =============================================================================
*.db
*.sqlite
*.sqlite3
*.db-journal
*.db-shm
*.db-wal
# =============================================================================
# IDE AND EDITOR FILES
# =============================================================================
# Visual Studio Code
.vscode/
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# IntelliJ IDEA / PhpStorm / WebStorm / etc.
.idea/
*.iml
*.ipr
*.iws
out/
# Sublime Text
*.tmlanguage.cache
*.tmPreferences.cache
*.stTheme.cache
*.sublime-workspace
*.sublime-project
# Atom
.atom/
# Vim
*.swp
*.swo
*.swn
*~
.netrwhist
tags
# Emacs
*~
\#*\#
/.emacs.desktop
/.emacs.desktop.lock
*.elc
auto-save-list
tramp
.\#*
# =============================================================================
# SECURITY AND ENVIRONMENT
# =============================================================================
# Environment variables
.env
.env.local
.env.*.local
.envrc
# API keys and secrets
secrets/
*.key
*.pem
*.p12
*.p8
*.mobileprovision
# =============================================================================
# DOCKER
# =============================================================================
# Docker
.dockerignore
docker-compose.override.yml
# =============================================================================
# MISCELLANEOUS
# =============================================================================
# Terraform
*.tfstate
*.tfstate.*
.terraform/
.terraform.lock.hcl
# Vagrant
.vagrant/
# MacOS Finder
.DS_Store
# Windows image file caches
Thumbs.db
# Folder config file
Desktop.ini
# NPM error logs
npm-debug.log*
# Optional npm cache directory
.npm
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
@WomB0ComB0
Copy link
Author

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