You can make use of the --word-diff=porcelain
mode of git diff
(along with a sufficiently large value passed to the -U
option, in order to preserve all the context between changes) and process its output with a simple enough script that will correct the wrong replacement.
--word-diff[=<mode>]
Show a word diff, using the <mode>
to delimit changed words. By default, words are delimited by whitespace;
see --word-diff-regex
below. The <mode>
defaults to plain
, and
must be one of:
- ...
porcelain
: Use a special line-based format intended for script consumption. Added/removed/unchanged runs are printed in the usual
unified diff format, starting with a +/-/` ` character at the
beginning of the line and extending to the end of the line. Newlines
in the input are represented by a tilde ~
on a line of its own.
Below you will find a prototype sed
-based implementation of the above approach.
Usage:
fix_wrong_replacements
path
revision
replacement_fix
where
Effects:
Assuming that the working copy of the file at path
when compared
to its committed revision revision
contains results of replacing
certain instances of orig_pattern
with incorrect_replacement_str
,
identifies those replacements and changes them to correct_replacement_str
.
Examples:
# In last two commits (and, maybe, in the working copy) some "int"s
# were incorrectly changed to "unsigned", now change those to "long"
$myname main.c HEAD~2 /int/unsigned/long/
# In the working copy of somefile.txt all "abc" case-insensitive words
# were changed to "pqrs", now change them to "xyz"
$myname somefile.txt HEAD '/[aA][bB][cC]/pqrs/xyz/'
Known limitations/issues:
It works for a single file. To fix all wrong replacements in a commit, commit range or local changes, must identify the list of changed files and call this script in a loop for all of them.
If during the original (wrong) replacement case-insensitive mode was used, then the orig_pattern
part of of the replacement_fix
argument must use a [aA]
, [bB]
, etc, regex atom for each letter.
Replacements immediately adjacent to other changes aren't handled.
Sometimes a superfluous blank line may be added (because of a slight inconsistency in the output of git diff --word-diff
)
fix_wrong_replacements:
#!/usr/bin/env bash
myname="$(basename "$0")"
if [ $# -ne 3 ]
then
cat<<END
Usage:
$myname <path> <revision> <replacement_fix>
where
- <path> is the (relative) path of the file in the working tree
- <revision> is the revision since which the wrong replacements that
must be fixed were made
- <replacement_fix> is a string of the form
/orig_pattern/incorrect_replacement_str/correct_replacement_str/
Effects:
Assuming that the working copy of the file at <path> when compared
to its committed revision <revision> contains results of replacing
certain instances of <orig_pattern> with <incorrect_replacement_str>,
identifies those replacements and changes them to <correct_replacement_str>.
Examples:
# In last two commits (and, maybe, in the working copy) some "int"s
# were incorrectly changed to "unsigned", now change those to "long"
$myname main.c HEAD~2 /int/unsigned/long/
# In the working copy of somefile.txt all "abc" case-insensitive words
# were changed to "pqrs", now change them to "xyz"
$myname somefile.txt HEAD '/[aA][bB][cC]/pqrs/xyz/'
END
exit 1
fi
file="$1"
revision="$2"
s=(${3//// })
orig_pattern="${s[0]}"
incorrect_replacement="${s[1]}"
correct_replacement="${s[2]}"
pat="-$orig_pattern\n+$incorrect_replacement"
git_word_diff()
{
git diff -U100000 \
--word-diff=porcelain \
--word-diff-regex='[[:alpha:]][[:alnum:]]*' \
"$@"
}
word_diff_file="$(mktemp)"
trap "rm $word_diff_file" EXIT
git_word_diff "$revision" -- "$file" > "$word_diff_file"
sed -n -e '
1,5 d;
/^-/ N;
/\n~$/ d;
/\n[- ]/ D;
/^'"$pat"'$/ {x;G;s/\n'"$pat"'$/'"$correct_replacement"'/;x;d;};
/^-.*\n+/ {s/^-.*\n+//;H;x;s/\n//;x;d;};
/^~$/ {s/.*//;x;p;d;};
{s/^.//;H;x;s/\n//;x;};
' "$word_diff_file" > "$file"