55# This script scans a Git repository for files that match patterns in .gitignore
66# but are still being tracked. It prompts the user to either remove these files
77# from tracking or remove the corresponding patterns from .gitignore.
8-
9- set -e
8+ #
9+ # Usage:
10+ # git-clean-ignore [options]
11+ #
12+ # Options:
13+ # --verbose Show detailed information about all patterns
14+ # --clean repo Automatically remove ignored files from tracking
15+ # --clean gitignore Remove patterns from .gitignore that match tracked files
16+ # --help Show this help message
1017
1118# Text formatting
1219bold=$( tput bold)
@@ -16,7 +23,26 @@ green=$(tput setaf 2)
1623yellow=$( tput setaf 3)
1724blue=$( tput setaf 4)
1825
26+ # Default options
27+ VERBOSE=false
28+ AUTO_CLEAN_REPO=false
29+ AUTO_CLEAN_GITIGNORE=false
30+
1931# Helper functions
32+ function print_help() {
33+ echo " ${bold} Git Ignore Cleanup Utility${normal} "
34+ echo " "
35+ echo " Usage:"
36+ echo " $( basename $0 ) [options]"
37+ echo " "
38+ echo " Options:"
39+ echo " --verbose Show detailed information about all patterns"
40+ echo " --clean repo Automatically remove ignored files from tracking"
41+ echo " --clean gitignore Remove patterns from .gitignore that match tracked files"
42+ echo " --help Show this help message"
43+ exit 0
44+ }
45+
2046function print_header() {
2147 echo " \n${bold}${blue} $1 ${normal} \n"
2248}
@@ -67,122 +93,243 @@ function get_repo_root() {
6793 git rev-parse --show-toplevel
6894}
6995
96+ # Parse command line arguments
97+ for arg in " $@ " ; do
98+ case " $arg " in
99+ --verbose)
100+ VERBOSE=true
101+ ;;
102+ --clean)
103+ echo " Error: The --clean option requires an argument (repo or gitignore)"
104+ exit 1
105+ ;;
106+ --clean=* )
107+ CLEAN_TARGET=${arg#* =}
108+ if [[ " $CLEAN_TARGET " == " repo" ]]; then
109+ AUTO_CLEAN_REPO=true
110+ elif [[ " $CLEAN_TARGET " == " gitignore" ]]; then
111+ AUTO_CLEAN_GITIGNORE=true
112+ else
113+ echo " Error: Invalid --clean target: $CLEAN_TARGET "
114+ echo " Valid targets are 'repo' or 'gitignore'"
115+ exit 1
116+ fi
117+ ;;
118+ --clean\ repo)
119+ AUTO_CLEAN_REPO=true
120+ ;;
121+ --clean\ gitignore)
122+ AUTO_CLEAN_GITIGNORE=true
123+ ;;
124+ --help)
125+ print_help
126+ ;;
127+ * )
128+ echo " Unknown option: $arg "
129+ print_help
130+ ;;
131+ esac
132+ shift
133+ done
134+
70135# Validate requirements
71136if ! is_git_repo; then
72137 print_error " Not a Git repository. Please run this script from within a Git repository."
73138 exit 1
74139fi
75140
76- if [[ ! -f " .gitignore" ]]; then
77- print_error " No .gitignore file found in the current directory."
78- exit 1
79- fi
80-
81141REPO_ROOT=$( get_repo_root)
82142GITIGNORE_FILE=" ${REPO_ROOT} /.gitignore"
83143
84- print_header " Git Ignore Synchronization Tool"
85- echo " This tool will help you synchronize your .gitignore patterns with tracked files."
86- echo " It will find files that are currently tracked but match ignore patterns."
144+ if [[ ! -f " $GITIGNORE_FILE " ]]; then
145+ print_error " No .gitignore file found at $GITIGNORE_FILE "
146+ exit 1
147+ fi
148+
149+ # Initial message
150+ if $VERBOSE ; then
151+ print_header " Git Ignore Synchronization Tool"
152+ echo " This tool will help you synchronize your .gitignore patterns with tracked files."
153+ echo " It will find files that are currently tracked but match ignore patterns."
154+ fi
87155
88- # Process each non-empty, non-comment line in .gitignore
89- print_header " Scanning .gitignore patterns ..."
156+ # Get all ignored but tracked files in one go first
157+ print_header " Finding tracked files that should be ignored ..."
90158
91159TOTAL_MATCHED_FILES=0
92160PATTERNS_TO_REMOVE=()
93- PROCESSED_PATTERNS=()
161+ MATCHED_PATTERNS=()
162+ MATCHED_FILES_MAP=()
94163
95- # Read .gitignore line by line
96- while IFS= read -r line || [[ -n " $line " ]]; do
97- # Skip empty lines and comments
98- if [[ -z " $line " || " $line " =~ ^[[:space:]]* # ]]; then
99- continue
100- fi
101-
102- # Extract the actual pattern (ignore inline comments)
103- pattern= $( echo " $line " | sed ' s/\s*#.*$//' )
104- pattern= $( echo " $pattern " | sed ' s/^[[:space:]]*//;s/[[:space:]]*$//' )
105-
106- # Skip if pattern is empty or already processed
107- if [[ -z " $pattern " || " ${PROCESSED_PATTERNS[@]} " =~ " ${pattern} " ]]; then
108- continue
109- fi
110-
111- PROCESSED_PATTERNS+= (" $pattern " )
112-
113- echo " Checking pattern: ${bold} $pattern ${normal} "
114-
115- # Check if any tracked files match this pattern
116- matched_files= $( git ls-files --cached -i --exclude=" $pattern " 2> /dev/null)
117- if [[ -z " $matched_files " ]]; then
118- continue
119- fi
164+ # Collect all ignored files that are still tracked
165+ all_tracked_ignored_files=$( git ls-files --cached -i --exclude-from=" $GITIGNORE_FILE " 2> /dev/null)
166+
167+ if [[ -z " $all_tracked_ignored_files " ]]; then
168+ print_success " No tracked files matching ignore patterns were found."
169+ echo " Your repository is in sync with your .gitignore file!"
170+ exit 0
171+ fi
172+
173+ # Count matched files
174+ matched_files_count=$( echo " $all_tracked_ignored_files " | wc -l | tr -d ' ' )
175+ print_warning " Found ${bold} $matched_files_count ${normal} tracked files that match patterns in .gitignore:"
176+ echo " $all_tracked_ignored_files " | sed ' s/^/ - /'
177+ echo
178+
179+ # Process each file to map it to the pattern that's ignoring it
180+ while read -r file; do
181+ matched_pattern=" "
182+ while IFS= read -r line || [[ -n " $line " ]]; do
183+ # Skip empty lines and comments
184+ if [[ -z " $line " || " $line " =~ ^[[:space:]]* # ]]; then
185+ continue
186+ fi
187+
188+ # Extract the pattern (ignore inline comments)
189+ pattern= $( echo " $line " | sed ' s/\s*#.*$//' )
190+ pattern= $( echo " $pattern " | sed ' s/^[[:space:]]*//;s/[[:space:]]*$//' )
191+
192+ if [[ -z " $pattern " ]]; then
193+ continue
194+ fi
195+
196+ # Check if this pattern matches the file
197+ if git check-ignore -q --no-index --exclude=" $pattern " " $file " 2> /dev/null; then
198+ matched_pattern=" $pattern "
199+ if ! [[ " ${MATCHED_PATTERNS[@]} " =~ " ${pattern} " ]]; then
200+ MATCHED_PATTERNS+=(" $pattern " )
201+ MATCHED_FILES_MAP+=(" $pattern :$file " )
202+ else
203+ MATCHED_FILES_MAP+=(" $pattern :$file " )
204+ fi
205+ break
206+ fi
207+ done < " $GITIGNORE_FILE "
208+ done <<< " $all_tracked_ignored_files "
209+
210+ # Now handle each pattern with its matched files
211+ for pattern in " ${MATCHED_PATTERNS[@]} " ; do
212+ # Get all files matching this pattern
213+ matched_files=" "
214+ for entry in " ${MATCHED_FILES_MAP[@]} " ; do
215+ IFS=' :' read -r p f <<< " $entry"
216+ if [[ " $p " == " $pattern " ]]; then
217+ matched_files=" $matched_files$f \n"
218+ fi
219+ done
220+ matched_files=$( echo " $matched_files " | sed ' /^$/d' )
120221
121- # Count matched files
222+ # Count matched files for this pattern
122223 matched_count=$( echo " $matched_files " | wc -l | tr -d ' ' )
123- TOTAL_MATCHED_FILES= $(( TOTAL_MATCHED_FILES + matched_count))
124224
125- echo " Found ${bold} $matched_count ${normal} tracked files matching pattern:"
126- echo " $matched_files " | sed ' s/^/ - /'
225+ if $VERBOSE ; then
226+ echo " Pattern: ${bold} $pattern ${normal} matches ${bold} $matched_count ${normal} files:"
227+ echo " $matched_files " | sed ' s/^/ - /'
228+ echo
229+ else
230+ echo " Pattern: ${bold} $pattern ${normal} matches ${bold} $matched_count ${normal} files."
231+ fi
127232
128- # Prompt to remove files from tracking
129- if ask_yes_no " Remove these files from Git tracking (they will remain on disk)?" " y" ; then
130- echo " $matched_files " | xargs git rm --cached
233+ # Handle auto-clean modes or prompt user
234+ if $AUTO_CLEAN_REPO ; then
235+ echo " Removing matched files from tracking (--clean repo specified)..."
236+ echo " $matched_files " | xargs git rm --cached -q
131237 print_success " Files removed from tracking but kept on disk."
238+ elif $AUTO_CLEAN_GITIGNORE ; then
239+ PATTERNS_TO_REMOVE+=(" $pattern " )
240+ print_warning " Pattern '$pattern ' marked for removal from .gitignore (--clean gitignore specified)."
132241 else
133- # If user doesn't want to remove files, offer to remove the pattern
134- if ask_yes_no " Keep these files tracked? (This will remove the pattern from .gitignore) " " n " ; then
135- PATTERNS_TO_REMOVE+=( " $pattern " )
136- print_warning " Pattern ' $pattern ' marked for removal from .gitignore. "
242+ # Interactive mode - prompt the user
243+ echo " Found ${bold} $matched_count ${normal} tracked files matching pattern: ${bold} $pattern ${normal} "
244+ if $VERBOSE ; then
245+ echo " $matched_files " | sed ' s/^/ - / '
137246 else
138- print_warning " No action taken for pattern ' $pattern '. "
247+ echo " Use --verbose to see the list of files "
139248 fi
249+
250+ echo " Choose an action:"
251+ echo " 1) Remove these files from Git tracking (keep on disk)"
252+ echo " 2) Keep tracking these files (remove pattern from .gitignore)"
253+ echo " 3) Skip this pattern (no action)"
254+ read -r " choice?Your choice [1-3]: "
255+
256+ case " $choice " in
257+ 1)
258+ echo " Removing files from tracking..."
259+ echo " $matched_files " | xargs git rm --cached -q
260+ print_success " Files removed from tracking but kept on disk."
261+ ;;
262+ 2)
263+ PATTERNS_TO_REMOVE+=(" $pattern " )
264+ print_warning " Pattern '$pattern ' marked for removal from .gitignore."
265+ ;;
266+ 3)
267+ print_warning " No action taken for pattern '$pattern '."
268+ ;;
269+ * )
270+ print_error " Invalid choice. Skipping this pattern."
271+ ;;
272+ esac
140273 fi
141274
142275 echo " "
143- done < " $GITIGNORE_FILE "
276+ done
144277
145278# Remove patterns from .gitignore if needed
146279if [[ ${# PATTERNS_TO_REMOVE[@]} -gt 0 ]]; then
147280 print_header " Updating .gitignore file..."
148281
149- temp_file=$( mktemp)
150- while IFS= read -r line || [[ -n " $line " ]]; do
151- pattern_match=0
152-
153- # Skip empty lines and comments
154- if [[ -z " $line " || " $line " =~ ^[[:space:]]* # ]]; then
155- echo " $line " >> " $temp_file "
156- continue
157- fi
158-
159- # Extract the pattern (ignore inline comments)
160- pattern= $( echo " $line " | sed ' s/\s*#.*$//' )
161- pattern= $( echo " $pattern " | sed ' s/^[[:space:]]*//;s/[[:space:]]*$//' )
162-
163- for remove_pattern in " ${PATTERNS_TO_REMOVE[@]} " ; do
164- if [[ " $pattern " == " $remove_pattern " ]]; then
165- pattern_match=1
166- break
167- fi
282+ if ! $AUTO_CLEAN_GITIGNORE ; then
283+ echo " The following patterns will be removed from .gitignore:"
284+ for pattern in " ${PATTERNS_TO_REMOVE[@]} " ; do
285+ echo " - $pattern "
168286 done
169287
170- # Keep the line if pattern doesn't match any to remove
171- if [[ $pattern_match -eq 0 ]]; then
172- echo " $line " >> " $temp_file "
173- else
174- print_warning " Removed pattern: $pattern "
288+ if ! ask_yes_no " Proceed with removing these patterns?" " y" ; then
289+ print_warning " No changes made to .gitignore file."
290+ PATTERNS_TO_REMOVE=()
175291 fi
176- done < " $GITIGNORE_FILE "
292+ fi
177293
178- cp " $temp_file " " $GITIGNORE_FILE "
179- rm " $temp_file "
180- print_success " Updated .gitignore file."
294+ if [[ ${# PATTERNS_TO_REMOVE[@]} -gt 0 ]]; then
295+ temp_file=$( mktemp)
296+ while IFS= read -r line || [[ -n " $line " ]]; do
297+ pattern_match=0
298+
299+ # Keep comments and empty lines
300+ if [[ -z " $line " || " $line " =~ ^[[:space:]]* # ]]; then
301+ echo " $line " >> " $temp_file "
302+ continue
303+ fi
304+
305+ # Extract the pattern (ignore inline comments)
306+ pattern= $( echo " $line " | sed ' s/\s*#.*$//' )
307+ pattern= $( echo " $pattern " | sed ' s/^[[:space:]]*//;s/[[:space:]]*$//' )
308+
309+ for remove_pattern in " ${PATTERNS_TO_REMOVE[@]} " ; do
310+ if [[ " $pattern " == " $remove_pattern " ]]; then
311+ pattern_match=1
312+ break
313+ fi
314+ done
315+
316+ # Keep the line if pattern doesn't match any to remove
317+ if [[ $pattern_match -eq 0 ]]; then
318+ echo " $line " >> " $temp_file "
319+ else
320+ print_warning " Removed pattern: $pattern "
321+ fi
322+ done < " $GITIGNORE_FILE "
323+
324+ cp " $temp_file " " $GITIGNORE_FILE "
325+ rm " $temp_file "
326+ print_success " Updated .gitignore file."
327+ fi
181328fi
182329
183330# Summary
184331print_header " Summary"
185- if [[ $TOTAL_MATCHED_FILES -eq 0 ]]; then
332+ if [[ ${ # MATCHED_PATTERNS[@]} -eq 0 ]]; then
186333 print_success " No tracked files matching ignore patterns were found."
187334 echo " Your repository is in sync with your .gitignore file!"
188335else
0 commit comments