Last active
January 2, 2026 04:25
-
-
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.
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 | |
| # 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-flash-latest" "--no-stream") | |
| 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_IGNORE_FILE="$HOME/.git-auto-commit-ignore" | |
| readonly DEFAULT_AUTO_PUSH=true | |
| readonly DEFAULT_MIN_REPO_AGE_DAYS=7 | |
| readonly MAX_COMMIT_MSG_LENGTH=72 | |
| readonly MAX_BRANCH_NAME_LENGTH=30 | |
| # Initialize AI_COMMAND with default | |
| declare -a AI_COMMAND=("${DEFAULT_AI_COMMAND[@]}") | |
| 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}" | |
| IGNORE_FILE="${IGNORE_FILE:-$DEFAULT_IGNORE_FILE}" | |
| AUTO_PUSH="${AUTO_PUSH:-$DEFAULT_AUTO_PUSH}" | |
| MIN_REPO_AGE_DAYS="${MIN_REPO_AGE_DAYS:-$DEFAULT_MIN_REPO_AGE_DAYS}" | |
| # --- 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 [[ ! "$MIN_REPO_AGE_DAYS" =~ ^[0-9]+$ ]] || (( MIN_REPO_AGE_DAYS < 0 )); then | |
| log "ERROR" "MIN_REPO_AGE_DAYS must be a non-negative integer" | |
| 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" "find") | |
| # Optional dependencies | |
| local optional_deps=("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_gha_auth() { | |
| if ! command -v gha >/dev/null 2>&1; then | |
| log "WARNING" "gha wrapper 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 gha >/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 gha wrapper | |
| switch_to_repo_account() { | |
| local repo_path="$1" | |
| local repo_name | |
| repo_name=$(basename "$repo_path") | |
| if ! command -v gha >/dev/null 2>&1; then | |
| log "DEBUG" "gha wrapper 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 | |
| # Check if ACCOUNT_MAPPINGS is defined in config, otherwise use empty array | |
| if [[ -z "${ACCOUNT_MAPPINGS:-}" ]]; then | |
| declare -A ACCOUNT_MAPPINGS | |
| fi | |
| # Add default mapping if not present (optional, or just rely on config) | |
| # 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" | |
| # Use gha wrapper which handles both auth switch and git config | |
| if gha --user "$target_account" >/dev/null 2>&1; then | |
| log "INFO" "Successfully switched to $target_account (git config updated by gha)" | |
| 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 | |
| } | |
| 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 | |
| } | |
| # Check if repository should be ignored based on ignore file | |
| is_repo_ignored() { | |
| local repo_path="$1" | |
| local repo_name | |
| repo_name=$(basename "$repo_path") | |
| if [[ ! -f "$IGNORE_FILE" ]]; then | |
| log "DEBUG" "No ignore file found at $IGNORE_FILE" | |
| return 1 # Not ignored | |
| fi | |
| while IFS= read -r pattern || [[ -n "$pattern" ]]; do | |
| # Skip empty lines and comments | |
| [[ -z "$pattern" || "$pattern" =~ ^[[:space:]]*# ]] && continue | |
| # Trim whitespace | |
| pattern=$(echo "$pattern" | xargs) | |
| # Check if pattern matches repo name or full path | |
| if [[ "$repo_name" == $pattern || "$repo_path" == $pattern ]]; then | |
| log "DEBUG" "Repository $repo_name matches ignore pattern: $pattern" | |
| return 0 # Ignored | |
| fi | |
| done < "$IGNORE_FILE" | |
| return 1 # Not ignored | |
| } | |
| # Check if repository is old enough to process | |
| check_repo_age() { | |
| local repo_path="$1" | |
| local repo_name | |
| repo_name=$(basename "$repo_path") | |
| # If MIN_REPO_AGE_DAYS is 0, skip age check | |
| if (( MIN_REPO_AGE_DAYS == 0 )); then | |
| return 0 | |
| fi | |
| # Get the timestamp of the first commit (repository creation) | |
| local first_commit_timestamp | |
| first_commit_timestamp=$(git -C "$repo_path" log --reverse --format=%ct --max-parents=0 2>/dev/null | head -1 || echo "") | |
| if [[ -z "$first_commit_timestamp" ]]; then | |
| log "DEBUG" "Could not determine repository age for $repo_name (no commits?)" | |
| return 0 # Process it anyway | |
| fi | |
| local current_timestamp | |
| current_timestamp=$(date +%s) | |
| local age_seconds=$((current_timestamp - first_commit_timestamp)) | |
| local age_days=$((age_seconds / 86400)) | |
| if (( age_days < MIN_REPO_AGE_DAYS )); then | |
| log "DEBUG" "Repository $repo_name is $age_days days old (minimum: $MIN_REPO_AGE_DAYS days)" | |
| return 1 # Too new | |
| fi | |
| log "DEBUG" "Repository $repo_name is $age_days days old (passes age check)" | |
| return 0 # Old enough | |
| } | |
| # 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)" | |
| # Use cat to pipe content instead of stdin redirection | |
| # This ensures ask reads the diff content properly | |
| if cat "$input_file" | timeout "$TIMEOUT_SECONDS" "${AI_COMMAND[@]}" "$prompt" > "$output_file" 2>&1; 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+=1)) | |
| [[ $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="Generate a Git branch name in format 'prefix/description' (e.g. feature/add-login, fix/auth-bug, chore/update-deps). Prefixes: feature,fix,chore,docs,refactor,test,style,perf. Use kebab-case. Max 30 chars. Return ONLY the branch name." | |
| if branch_name=$(call_ai_command "$prompt" "$diff_file"); then | |
| # Clean and validate branch name | |
| # First, remove markdown, quotes, and extra whitespace | |
| branch_name=$(echo "$branch_name" | sed -e 's/^```.*$//' -e 's/^```$//' -e 's/^"//' -e 's/"$//' | head -1 | xargs) | |
| # Try to extract a pattern that looks like prefix/name | |
| if [[ "$branch_name" =~ (feature|fix|chore|docs|refactor|test|style|perf|ci|build)/[a-zA-Z0-9-]+ ]]; then | |
| branch_name="${BASH_REMATCH[0]}" | |
| else | |
| # If no valid pattern found, try to construct one from the text | |
| # Remove everything except alphanumeric, hyphens, and slashes | |
| branch_name=$(echo "$branch_name" | sed 's/[^a-zA-Z0-9\/-]//g' | sed 's/^-*//' | sed 's/\/\//\//') | |
| fi | |
| # Validate branch name (must be prefix/name format) | |
| if [[ -n "$branch_name" && ${#branch_name} -le $MAX_BRANCH_NAME_LENGTH && "$branch_name" =~ ^(feature|fix|chore|docs|refactor|test|style|perf|ci|build)/[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="Generate a conventional commit message in format 'type: description' (e.g. feat: add user auth, fix: resolve login bug). Types: feat,fix,chore,docs,refactor,test,style,perf,ci,build. Brief lowercase description. Max 72 chars. Return ONLY the commit message." | |
| if commit_message=$(call_ai_command "$prompt" "$diff_file"); then | |
| # Clean and validate commit message | |
| # First, remove markdown, quotes, and get first line | |
| commit_message=$(echo "$commit_message" | sed -e 's/^```.*$//' -e 's/^```$//' -e 's/^"//' -e 's/"$//' | head -1 | xargs) | |
| # Try to extract a conventional commit pattern (type: description) | |
| if [[ "$commit_message" =~ ^(feat|fix|chore|docs|refactor|test|style|perf|ci|build):[[:space:]]*.+ ]]; then | |
| # Already in correct format, just clean it up | |
| commit_message=$(echo "$commit_message" | sed 's/^\([a-z]*\):[[:space:]]*/\1: /') | |
| else | |
| # If not in correct format, log warning and use fallback | |
| log "DEBUG" "AI returned non-conventional commit format: $commit_message" | |
| commit_message="" | |
| fi | |
| # Validate commit message | |
| if [[ -n "$commit_message" && ${#commit_message} -gt 5 && ${#commit_message} -le $MAX_COMMIT_MSG_LENGTH && "$commit_message" =~ ^(feat|fix|chore|docs|refactor|test|style|perf|ci|build):[[:space:]]*.+ ]]; 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" | |
| # Check if repository is ignored | |
| if is_repo_ignored "$repo"; then | |
| log "INFO" "Repository $repo_name is in ignore list. Skipping" | |
| return 1 | |
| fi | |
| 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)" | |
| # Check repository age | |
| if ! check_repo_age "$(pwd)"; then | |
| log "INFO" "Repository $repo_name is too new (created within last $MIN_REPO_AGE_DAYS days). Skipping" | |
| return 1 | |
| fi | |
| # 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 | |
| # gha auth is optional, so we don't fail on this | |
| check_gha_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" | |
| local repo_name | |
| repo_name=$(basename "$repo") | |
| # Check if repository is ignored (do this before dry-run check) | |
| if is_repo_ignored "$repo"; then | |
| log "INFO" "Repository $repo_name is in ignore list. Skipping" | |
| continue | |
| fi | |
| # Check if it's a git repository | |
| if [[ ! -d "$repo/.git" ]]; then | |
| log "DEBUG" "Not a Git repository: $repo_name. Skipping" | |
| continue | |
| fi | |
| # Check repository age (do this before dry-run check) | |
| if ! check_repo_age "$repo"; then | |
| log "INFO" "Repository $repo_name is too new (created within last $MIN_REPO_AGE_DAYS days). Skipping" | |
| continue | |
| fi | |
| ((total_repos+=1)) | |
| if [[ "${DRY_RUN:-false}" == "true" ]]; then | |
| log "INFO" "[DRY RUN] Would process repository: $repo_name" | |
| continue | |
| fi | |
| # Process repository with error handling | |
| if process_repository "$repo"; then | |
| ((success_count+=1)) | |
| else | |
| ((fail_count+=1)) | |
| log "DEBUG" "Repository $repo_name 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 |
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
| [Unit] | |
| Description=Git Auto-Commit Service | |
| After=network-online.target | |
| Wants=network-online.target | |
| [Service] | |
| Type=oneshot | |
| User=%u | |
| WorkingDirectory=%h | |
| ExecStart=%h/scripts/auto-commit.sh | |
| StandardOutput=journal | |
| StandardError=journal | |
| SyslogIdentifier=git-auto-commit | |
| # Security hardening | |
| PrivateTmp=yes | |
| NoNewPrivileges=yes | |
| ProtectSystem=strict | |
| ProtectHome=read-only | |
| ReadWritePaths=%h/github | |
| # Environment | |
| Environment="PATH=/usr/local/bin:/usr/bin:/bin" |
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
| [Unit] | |
| Description=Git Auto-Commit Timer | |
| Requires=git-auto-commit.service | |
| [Timer] | |
| # Run every 3 hours | |
| OnCalendar=*-*-* 00,03,06,09,12,15,18,21:00:00 | |
| # Run 5 minutes after boot if we missed a scheduled run | |
| OnBootSec=5min | |
| # Run 5 minutes after the timer is activated | |
| OnStartupSec=5min | |
| # Allow some randomization to avoid exact timing patterns | |
| RandomizedDelaySec=10min | |
| # Ensure timer persists across reboots | |
| Persistent=true | |
| [Install] | |
| WantedBy=timers.target |
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
| # ============================================================================= | |
| # 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 |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
~/.config/shell-ask/config.json{ ... }^ -> https://github.com/egoist/shell-ask/blob/main/docs/config.md