-
-
Notifications
You must be signed in to change notification settings - Fork 59
/
Copy pathgit-filechange-search.sh
executable file
·293 lines (253 loc) · 12.1 KB
/
git-filechange-search.sh
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
#!/bin/bash
# Author: Gabriel Staples
# Status: IT WORKS! Ready for usage by anybody.
# This file is part of eRCaGuy_dotfiles: https://github.com/ElectricRCAircraftGuy/eRCaGuy_dotfiles
# git-filechange-search.sh
# - See which commit changed a certain keyword (regex search pattern) in a certain file.
# - This allows you to track down the exact commit which changed a certain variable or function
# of interest, for example, in a particular file.
# INSTALLATION INSTRUCTIONS:
# 1. Create a symlink in ~/bin to this script so you can run it from anywhere:
# cd /path/to/here
# mkdir -p ~/bin
# ln -si "${PWD}/git-filechange-search.sh" ~/bin/gs_git-filechange-search
# ln -si "${PWD}/git-filechange-search.sh" ~/bin/git-filechange-search
# 2. Now you can use the `gs_git-filechange-search` command directly anywhere you like.
# References:
# 1. My "find_and_replace.sh" script in this repo
# 1. See `man test` for [ ] style comparisons (=, !=, -eq, -ge, -gt, -lt, etc.)
# Test cases:
# gs_git-filechange-search "git & Linux cmds, help, tips & tricks - Gabriel.txt"
# gs_git-filechange-search -v "git & Linux cmds, help, tips & tricks - Gabriel.txt"
# gs_git-filechange-search -vv "git & Linux cmds, help, tips & tricks - Gabriel.txt"
# gs_git-filechange-search "git & Linux cmds, help, tips & tricks - Gabriel.txt" "difftool"
# gs_git-filechange-search -v "git & Linux cmds, help, tips & tricks - Gabriel.txt" "difftool"
# gs_git-filechange-search -vv "git & Linux cmds, help, tips & tricks - Gabriel.txt" "difftool"
# TODO:
# 1. Once this script is tried and proven and working well, try to get it merged upstream into
# git itself, if possible.
EXIT_SUCCESS=0
EXIT_ERROR=1
VERSION="0.1.0"
AUTHOR="Gabriel Staples"
SCRIPT_NAME="$(basename "$0")"
NAME_AND_VERSION_STR="git-filechange-search (run as '$SCRIPT_NAME') version $VERSION"
print_help() {
echo ''
echo "$NAME_AND_VERSION_STR"
echo " - find out which commit changed a certain variable (ie: keyword, or regex search"
echo " pattern) in a certain file"
echo ""
echo "Main Usage: $SCRIPT_NAME [-v|-vv] <filename> <regex_search>"
echo ""
echo "'$SCRIPT_NAME <filename> <regex_search>' = print the commit hashes which modify this"
echo " file AND contain changes which match the regular expression <regex_search>"
echo "'$SCRIPT_NAME -v <filename> <regex_search>' = same as above, except with 'v'erbose output"
echo "'$SCRIPT_NAME -vv <filename> <regex_search>' = same as above, except with extra 'v'erbose"
echo " output"
echo "'$SCRIPT_NAME <filename>' = print all commit hashes which modify this file"
echo "'$SCRIPT_NAME -v <filename>' = same as above, except with 'v'erbose output"
echo "'$SCRIPT_NAME -vv <filename>' = same as above, except with extra 'v'erbose output"
echo "'$SCRIPT_NAME' = print help menu"
echo "'$SCRIPT_NAME -h' = print help menu"
echo "'$SCRIPT_NAME -?' = print help menu"
echo "'$SCRIPT_NAME --version' = print author & version"
echo ""
echo "This program is part of: https://github.com/ElectricRCAircraftGuy/eRCaGuy_dotfiles"
echo ""
}
print_version() {
echo "$NAME_AND_VERSION_STR"
echo "Author = $AUTHOR"
echo "See '$SCRIPT_NAME -h' for more info."
}
parse_args() {
if [ $# -lt 1 ]; then
echo "ERROR: Not enough arguments supplied"
print_help
exit $EXIT_ERROR
fi
if [ $# -gt 3 ]; then
echo "ERROR: Too many arguments supplied"
print_help
exit $EXIT_ERROR
fi
# Help menu
if [ "$1" == "-h" ] || [ "$1" == "-?" ]; then
print_help
exit $EXIT_SUCCESS
fi
# Version
if [ "$1" == "--version" ]; then
print_version
exit $EXIT_SUCCESS
fi
VERBOSITY_LEVEL=0
FILENAME="$1"
REGEX_SEARCH=""
# Verbosity level
if [ "$1" == "-v" ]; then
VERBOSITY_LEVEL=1
elif [ "$1" == "-vv" ]; then
VERBOSITY_LEVEL=2
fi
echo "Verbosity level $VERBOSITY_LEVEL"
if [ "$VERBOSITY_LEVEL" -gt 0 ]; then
FILENAME="$2"
if [ $# -eq 1 ]; then
echo "ERROR: Not enough arguments supplied"
print_help
exit $EXIT_ERROR
elif [ $# -eq 3 ]; then
REGEX_SEARCH="$3"
fi
else # NOT verbose (VERBOSITY_LEVEL is 0)
if [ $# -gt 2 ]; then
echo "ERROR: Too many arguments supplied, or invalid order. Note that -v and "
echo " -vv are positional."
print_help
exit $EXIT_ERROR
elif [ $# -eq 2 ]; then
REGEX_SEARCH="$2"
fi
fi
} # parse_args()
main() {
echo "Searching for regex pattern \"$REGEX_SEARCH\" in filename \"$FILENAME\"."
# List of all commit hashes which have modified FILENAME
COMMIT_HASHES="$(git log --follow "$FILENAME" | grep -E "commit [0-9a-f]{30,}" | \
grep -E --only-matching "[0-9a-f]{30,}")"
# echo "$COMMIT_HASHES" # for debugging
# Convert the multi-line string into an array
COMMIT_HASHES_ARRAY=($(echo "$COMMIT_HASHES"))
# Num elements in array; see:
# https://unix.stackexchange.com/questions/193039/how-to-count-the-length-of-an-array-defined-in-bash/193042#193042
num_elements="${#COMMIT_HASHES_ARRAY[@]}"
echo "${num_elements} commits modify this file."
commit="commit_hash"
# Copy to other VERBOSE_INSTRUCTIONS in this file to match if you modify this block
VERBOSE_INSTRUCTIONS="\
Run this command to see the commit log header for a given commit:\n\
git log -n 1 ${commit}\n\
Run this command to see the commit changes:\n\
git diff --color=always ${commit}~..${commit} \"$FILENAME\"\n\
OR:\n\
git difftool ${commit}~..${commit} \"$FILENAME\""
if [ "$VERBOSITY_LEVEL" -eq 0 ]; then
echo -e "$VERBOSE_INSTRUCTIONS"
fi
# If no 2nd argument provided, there's no regex pattern to search for, so just print the commits
# which modify this file
if [ -z "$REGEX_SEARCH" ]; then
echo "All commits which modify this file are (most recent FIRST):"
# Loop through the array; see:
# https://stackoverflow.com/questions/8880603/loop-through-an-array-of-strings-in-bash/8880633#8880633
count=0
for commit in "${COMMIT_HASHES_ARRAY[@]}"
do
((count++))
# Copy to other git_log_oneline_output variables in this file to match if you modify it
git_log_oneline_output="$(git log --color=always -n 1 --pretty=oneline "${commit}")"
VERBOSE_STR="${count}/${num_elements}: ${git_log_oneline_output}"
VERBOSE_STR_REDUCED="${count}/${num_elements}: ${commit}"
# Copy to other VERBOSE_INSTRUCTIONS in this file to match if you modify this block
VERBOSE_INSTRUCTIONS="\
Run this command to see the commit log header for this commit:\n\
git log -n 1 ${commit}\n\
Run this command to see the commit changes:\n\
git diff --color=always ${commit}~..${commit} \"$FILENAME\"\n\
OR:\n\
git difftool ${commit}~..${commit} \"$FILENAME\""
if [ "$VERBOSITY_LEVEL" -eq 0 ]; then
echo "$VERBOSE_STR"
elif [ "$VERBOSITY_LEVEL" -eq 1 ]; then
echo "------"
echo "$VERBOSE_STR"
echo -e "$VERBOSE_INSTRUCTIONS"
elif [ "$VERBOSITY_LEVEL" -eq 2 ]; then
echo "======"
echo "$VERBOSE_STR_REDUCED"
echo "---full commit header start---"
git log -n 1 "$commit"
echo "---full commit header end---"
echo -e "$VERBOSE_INSTRUCTIONS"
echo ""
fi
done
echo "($num_elements total commits)"
exit $EXIT_SUCCESS
fi
# Actually do the searching
echo "Listing commits which **contain changes which match the regex pattern \
\"${REGEX_SEARCH}\"** (most recent FIRST):"
total_count=0
match_count=0 # number of commits found with changes matching the regex pattern
for commit in "${COMMIT_HASHES_ARRAY[@]}"
do
((total_count++))
# Use `--unified=0` (or `-U0`) to NOT show surrounding, unchanged lines in the `git diff`,
# so that ONLY commits which ACTUALLY CHANGED our regex search strings of interest show up!
# See:
# 1. https://stackoverflow.com/questions/18810623/git-diff-to-show-only-lines-that-have-been-modified/18810673#18810673
# 2. *****[BEST ANS!] https://stackoverflow.com/questions/18810623/git-diff-to-show-only-lines-that-have-been-modified/26622262#26622262
# - Note that the `| grep '^[+-]' | grep -Ev '^(--- a/|\+\+\+ b/)'` part at the end
# first filters only lines which begin with "+" or "-", thereby filtering out all
# `git diff` header lines which begin with "@@", and second it does an "in'v'ert match"
# to filter out all lines which begin with "--- a" or "+++ b", as those are used by `git
# diff` to denote filenames which contain changes!
# - See my comments under the answer above!
# 3. *****[MY OWN ANS] https://stackoverflow.com/questions/18810623/git-diff-to-show-only-lines-that-have-been-modified/61929887#61929887
# 4. `man git diff` and search for "-U<n>" or "--unified"
# 5.
# Test command:
# git diff --unified=0 "bcd1674fb2576dd5ae8c0bc040496a087650f03a~..bcd1674fb2576dd5ae8c0bc040496a087650f03a" "git & Linux cmds, help, tips & tricks - Gabriel.txt" | grep '^[+-]' | grep -Ev '^(--- a/|\+\+\+ b/)'
num_lines_changed="$(git diff --unified=0 "${commit}~..${commit}" "$FILENAME" | \
grep '^[+-]' | grep -Ev '^(--- a/|\+\+\+ b/)' | grep -E "$REGEX_SEARCH" | wc -l)"
if [ "$num_lines_changed" -gt 0 ]; then
((match_count++))
# Copy to other git_log_oneline_output variables in this file to match if you modify it
git_log_oneline_output="$(git log --color=always -n 1 --pretty=oneline "${commit}")"
VERBOSE_STR="${match_count} (${total_count}/${num_elements}): ${num_lines_changed} \
lines match in commit ${git_log_oneline_output}"
VERBOSE_STR_REDUCED="${match_count} (${total_count}/${num_elements}): \
${num_lines_changed} lines match in commit ${commit}"
# MODIFY *SIMILAR TO* other VERBOSE_INSTRUCTIONS in this file to match if you
# modify this block
VERBOSE_INSTRUCTIONS_LONGER="\
Run this command to see the commit log header for this commit:\n\
git log -n 1 ${commit}\n\
Run this command to see the commit changes:\n\
git diff -U3 --color=always ${commit}~..${commit} \"$FILENAME\" | grep \
--color=always -E -B 3 -A 3 \"$REGEX_SEARCH\"\n\
OR:\n\
git difftool ${commit}~..${commit} \"$FILENAME\""
if [ "$VERBOSITY_LEVEL" -eq 0 ]; then
echo "$VERBOSE_STR"
elif [ "$VERBOSITY_LEVEL" -eq 1 ]; then
echo "------"
echo "$VERBOSE_STR"
echo -e "$VERBOSE_INSTRUCTIONS_LONGER"
elif [ "$VERBOSITY_LEVEL" -eq 2 ]; then
echo "======"
echo "$VERBOSE_STR_REDUCED"
echo "---full commit header start---"
git log -n 1 "$commit"
echo "---full commit header end / git diff start---"
git diff -U3 --color=always ${commit}~..${commit} "$FILENAME" | grep \
--color=always -E -B 3 -A 3 "$REGEX_SEARCH"
echo "---git diff end---"
echo -e "$VERBOSE_INSTRUCTIONS_LONGER"
echo ""
fi
fi
done
echo "($match_count matching commits out of $total_count total commits which modify this file)"
if [ "$match_count" -eq 0 ]; then
echo "NO MATCHES FOUND!"
fi
} # main()
# ----------------------------------------------------------------------------------------------------------------------
# Program entry point
# ----------------------------------------------------------------------------------------------------------------------
parse_args "$@"
time main # run main, while also timing how long it takes