Created
June 11, 2025 22:39
-
-
Save brandonzylstra/e37172ea0b78f84f2356bc42ae1f88e6 to your computer and use it in GitHub Desktop.
Synchronize .gitignore patterns with tracked files
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
#!/usr/bin/env zsh | |
# | |
# git-clean-ignore: Synchronize .gitignore patterns with tracked files | |
# | |
# This script scans a Git repository for files that match patterns in .gitignore | |
# but are still being tracked. It prompts the user to either remove these files | |
# from tracking or remove the corresponding patterns from .gitignore. | |
# | |
# Usage: | |
# git-clean-ignore [options] | |
# | |
# Options: | |
# --verbose Show detailed information about all patterns | |
# --clean repo Automatically remove ignored files from tracking | |
# --clean gitignore Remove patterns from .gitignore that match tracked files | |
# --help Show this help message | |
# Text formatting | |
bold=$(tput bold) | |
normal=$(tput sgr0) | |
red=$(tput setaf 1) | |
green=$(tput setaf 2) | |
yellow=$(tput setaf 3) | |
blue=$(tput setaf 4) | |
# Default options | |
VERBOSE=false | |
AUTO_CLEAN_REPO=false | |
AUTO_CLEAN_GITIGNORE=false | |
# Helper functions | |
function print_help() { | |
echo "${bold}Git Ignore Cleanup Utility${normal}" | |
echo "" | |
echo "Usage:" | |
echo " $(basename $0) [options]" | |
echo "" | |
echo "Options:" | |
echo " --verbose Show detailed information about all patterns" | |
echo " --clean repo Automatically remove ignored files from tracking" | |
echo " --clean gitignore Remove patterns from .gitignore that match tracked files" | |
echo " --help Show this help message" | |
exit 0 | |
} | |
function print_header() { | |
echo "\n${bold}${blue}$1${normal}\n" | |
} | |
function print_success() { | |
echo "${green}✓${normal} $1" | |
} | |
function print_warning() { | |
echo "${yellow}!${normal} $1" | |
} | |
function print_error() { | |
echo "${red}✗${normal} $1" | |
} | |
function ask_yes_no() { | |
local prompt="$1" | |
local default="$2" | |
local yn | |
if [[ "$default" == "y" ]]; then | |
prompt="$prompt [Y/n] " | |
else | |
prompt="$prompt [y/N] " | |
fi | |
read -r "yn?$prompt" | |
case "$yn" in | |
[Yy]*) return 0 ;; | |
[Nn]*) return 1 ;; | |
*) | |
if [[ "$default" == "y" ]]; then | |
return 0 | |
else | |
return 1 | |
fi | |
;; | |
esac | |
} | |
function is_git_repo() { | |
git rev-parse --is-inside-work-tree &>/dev/null | |
} | |
function get_repo_root() { | |
git rev-parse --show-toplevel | |
} | |
# Parse command line arguments | |
for arg in "$@"; do | |
case "$arg" in | |
--verbose) | |
VERBOSE=true | |
;; | |
--clean) | |
echo "Error: The --clean option requires an argument (repo or gitignore)" | |
exit 1 | |
;; | |
--clean=*) | |
CLEAN_TARGET=${arg#*=} | |
if [[ "$CLEAN_TARGET" == "repo" ]]; then | |
AUTO_CLEAN_REPO=true | |
elif [[ "$CLEAN_TARGET" == "gitignore" ]]; then | |
AUTO_CLEAN_GITIGNORE=true | |
else | |
echo "Error: Invalid --clean target: $CLEAN_TARGET" | |
echo "Valid targets are 'repo' or 'gitignore'" | |
exit 1 | |
fi | |
;; | |
--clean\ repo) | |
AUTO_CLEAN_REPO=true | |
;; | |
--clean\ gitignore) | |
AUTO_CLEAN_GITIGNORE=true | |
;; | |
-h|--help) | |
print_help | |
;; | |
*) | |
echo "Unknown option: $arg" | |
print_help | |
;; | |
esac | |
shift | |
done | |
# Validate requirements | |
if ! is_git_repo; then | |
print_error "Not a Git repository. Please run this script from within a Git repository." | |
exit 1 | |
fi | |
REPO_ROOT=$(get_repo_root) | |
GITIGNORE_FILE="${REPO_ROOT}/.gitignore" | |
if [[ ! -f "$GITIGNORE_FILE" ]]; then | |
print_error "No .gitignore file found at $GITIGNORE_FILE" | |
exit 1 | |
fi | |
# Initial message | |
if $VERBOSE; then | |
print_header "Git Ignore Synchronization Tool" | |
echo "This tool will help you synchronize your .gitignore patterns with tracked files." | |
echo "It will find files that are currently tracked but match ignore patterns." | |
fi | |
# Get all ignored but tracked files in one go first | |
if $VERBOSE; then | |
print_header "Finding tracked files that should be ignored..." | |
fi | |
TOTAL_MATCHED_FILES=0 | |
PATTERNS_TO_REMOVE=() | |
MATCHED_PATTERNS=() | |
MATCHED_FILES_MAP=() | |
# Collect all ignored files that are still tracked | |
all_tracked_ignored_files=$(git ls-files --cached -i --exclude-from="$GITIGNORE_FILE" 2>/dev/null) | |
if [[ -z "$all_tracked_ignored_files" ]]; then | |
if $VERBOSE; then | |
print_success "No tracked files matching ignore patterns were found." | |
echo "Your repository is in sync with your .gitignore file!" | |
else | |
print_success "No tracked files matching ignore patterns." | |
fi | |
exit 0 | |
fi | |
# Count matched files | |
matched_files_count=$(echo "$all_tracked_ignored_files" | wc -l | tr -d ' ') | |
print_warning "Found ${bold}$matched_files_count${normal} tracked files that match patterns in .gitignore:" | |
echo "$all_tracked_ignored_files" | sed 's/^/ - /' | |
# Process each file to map it to the pattern that's ignoring it | |
while read -r file; do | |
matched_pattern="" | |
while IFS= read -r line || [[ -n "$line" ]]; do | |
# Skip empty lines and comments | |
if [[ -z "$line" || "$line" =~ ^[[:space:]]*# ]]; then | |
continue | |
fi | |
# Extract the pattern (ignore inline comments) | |
pattern=$(echo "$line" | sed 's/\s*#.*$//') | |
pattern=$(echo "$pattern" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') | |
if [[ -z "$pattern" ]]; then | |
continue | |
fi | |
# Check if this pattern matches the file | |
if git check-ignore -q --no-index --exclude="$pattern" "$file" 2>/dev/null; then | |
matched_pattern="$pattern" | |
if ! [[ " ${MATCHED_PATTERNS[@]} " =~ " ${pattern} " ]]; then | |
MATCHED_PATTERNS+=("$pattern") | |
MATCHED_FILES_MAP+=("$pattern:$file") | |
else | |
MATCHED_FILES_MAP+=("$pattern:$file") | |
fi | |
break | |
fi | |
done < "$GITIGNORE_FILE" | |
done <<< "$all_tracked_ignored_files" | |
# Now handle each pattern with its matched files | |
for pattern in "${MATCHED_PATTERNS[@]}"; do | |
# Get all files matching this pattern | |
matched_files="" | |
for entry in "${MATCHED_FILES_MAP[@]}"; do | |
IFS=':' read -r p f <<< "$entry" | |
if [[ "$p" == "$pattern" ]]; then | |
matched_files="$matched_files$f\n" | |
fi | |
done | |
matched_files=$(echo "$matched_files" | sed '/^$/d') | |
# Count matched files for this pattern | |
matched_count=$(echo "$matched_files" | wc -l | tr -d ' ') | |
if $VERBOSE; then | |
echo "Pattern: ${bold}$pattern${normal} matches ${bold}$matched_count${normal} files:" | |
echo "$matched_files" | sed 's/^/ - /' | |
echo | |
else | |
echo "Pattern: ${bold}$pattern${normal} matches ${bold}$matched_count${normal} files." | |
fi | |
# Handle auto-clean modes or prompt user | |
if $AUTO_CLEAN_REPO; then | |
echo "Removing matched files from tracking (--clean repo specified)..." | |
echo "$matched_files" | xargs git rm --cached -q | |
print_success "Files removed from tracking but kept on disk." | |
elif $AUTO_CLEAN_GITIGNORE; then | |
PATTERNS_TO_REMOVE+=("$pattern") | |
print_warning "Pattern '$pattern' marked for removal from .gitignore (--clean gitignore specified)." | |
else | |
# Interactive mode - prompt the user | |
echo "Found ${bold}$matched_count${normal} tracked files matching pattern: ${bold}$pattern${normal}" | |
if $VERBOSE; then | |
echo "$matched_files" | sed 's/^/ - /' | |
else | |
echo "Use --verbose to see the list of files" | |
fi | |
echo "Choose an action:" | |
echo " 1) Remove these files from Git tracking (keep on disk)" | |
echo " 2) Keep tracking these files (remove pattern from .gitignore)" | |
echo " 3) Skip this pattern (no action)" | |
read -r "choice?Your choice [1-3]: " | |
case "$choice" in | |
1) | |
echo "Removing files from tracking..." | |
echo "$matched_files" | xargs git rm --cached -q | |
print_success "Files removed from tracking but kept on disk." | |
;; | |
2) | |
PATTERNS_TO_REMOVE+=("$pattern") | |
print_warning "Pattern '$pattern' marked for removal from .gitignore." | |
;; | |
3) | |
print_warning "No action taken for pattern '$pattern'." | |
;; | |
*) | |
print_error "Invalid choice. Skipping this pattern." | |
;; | |
esac | |
fi | |
echo "" | |
done | |
# Remove patterns from .gitignore if needed | |
if [[ ${#PATTERNS_TO_REMOVE[@]} -gt 0 ]]; then | |
print_header "Updating .gitignore file..." | |
if ! $AUTO_CLEAN_GITIGNORE; then | |
echo "The following patterns will be removed from .gitignore:" | |
for pattern in "${PATTERNS_TO_REMOVE[@]}"; do | |
echo " - $pattern" | |
done | |
if ! ask_yes_no "Proceed with removing these patterns?" "y"; then | |
print_warning "No changes made to .gitignore file." | |
PATTERNS_TO_REMOVE=() | |
fi | |
fi | |
if [[ ${#PATTERNS_TO_REMOVE[@]} -gt 0 ]]; then | |
temp_file=$(mktemp) | |
while IFS= read -r line || [[ -n "$line" ]]; do | |
pattern_match=0 | |
# Keep comments and empty lines | |
if [[ -z "$line" || "$line" =~ ^[[:space:]]*# ]]; then | |
echo "$line" >> "$temp_file" | |
continue | |
fi | |
# Extract the pattern (ignore inline comments) | |
pattern=$(echo "$line" | sed 's/\s*#.*$//') | |
pattern=$(echo "$pattern" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//') | |
for remove_pattern in "${PATTERNS_TO_REMOVE[@]}"; do | |
if [[ "$pattern" == "$remove_pattern" ]]; then | |
pattern_match=1 | |
break | |
fi | |
done | |
# Keep the line if pattern doesn't match any to remove | |
if [[ $pattern_match -eq 0 ]]; then | |
echo "$line" >> "$temp_file" | |
else | |
print_warning "Removed pattern: $pattern" | |
fi | |
done < "$GITIGNORE_FILE" | |
cp "$temp_file" "$GITIGNORE_FILE" | |
rm "$temp_file" | |
print_success "Updated .gitignore file." | |
fi | |
fi | |
# Summary - only show in verbose mode | |
if $VERBOSE; then | |
print_header "Summary" | |
# Check if we originally found tracked ignored files (rely on the original variable) | |
if [[ -z "$all_tracked_ignored_files" ]]; then | |
print_success "No tracked files matching ignore patterns were found." | |
echo "Your repository is in sync with your .gitignore file!" | |
else | |
if [[ ${#PATTERNS_TO_REMOVE[@]} -gt 0 ]]; then | |
print_success "Removed ${#PATTERNS_TO_REMOVE[@]} patterns from .gitignore." | |
fi | |
echo "\nNext steps:" | |
echo " 1. Review any modified files with: ${bold}git status${normal}" | |
echo " 2. Commit your changes with: ${bold}git commit -m \"Update ignored files and patterns\"${normal}" | |
fi | |
fi | |
exit 0 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment