Re: [PATCH 02/18] Add a new builtin: branch-diff
- From
Johannes Schindelin <johannes.schindelin@gmx.de>
- Date
- May 5, 2018, 20:24 UTC
- Message-ID
- <nycvar.QRO.7.76.6.1805052220360.77@tvgsbejvaqbjf.bet>
- In-Reply-To
- <CABPp-BERh1FGzEJSu+6Z0aGC3dJxx+P=9xwdCCsPgnG8jWvQMg@mail.gmail.com>
Hi Elijah,
On Fri, 4 May 2018, Elijah Newren wrote:
Show 9 quoted lines
> On Thu, May 3, 2018 at 11:40 PM, Johannes Schindelin > <Johannes.Schindelin@gmx.de> wrote: > > I actually have a hacky script to fixup commits in a patch series. It lets > > me stage part of the current changes, then figures out which of the > > commits' changes overlap with the staged changed. If there is only one > > commit, it automatically commits with --fixup, otherwise it lets me choose > > which one I want to fixup (giving me the list of candidates). > > Ooh, interesting. Are you willing to share said hacky script by chance?
It is part of a real huge hacky script of pretty much all things I add as aliases, so I extracted the relevant part for you:
-- snip -- #!/bin/sh
fixup () { # [--upstream=<branch>] [--not=<tip-to-skip>]
upstream=
not=
while case "$1" in
--upstream) shift; upstream="$1";;
--upstream=*) upstream="${1#*=}";;
--not) shift; not="$not $1";;
--not=*) not="$not ${1#*=}";;
-*) die "Unknown option: $1";;
*) break;;
esac; do shift; donetest $# -le 1 || die "Need 0 or 1 commit"
! git diff-index --cached --quiet --ignore-submodules HEAD -- || {
git update-index --ignore-submodules --refresh
! git diff-files --quiet --ignore-submodules -- ||
die "No changes"git add -p || exit } ! git diff-index --cached --quiet --ignore-submodules HEAD -- || die "No staged changes"
test $# = 1 || {
if test -z "$upstream"
then
upstream="$(git rev-parse --symbolic-full-name \
HEAD@{upstream} 2> /dev/null)" &&
test "$(git rev-parse HEAD)" != \
"$(git rev-parse $upstream)" ||
upstream=origin/master
firevs="$(git rev-list $upstream.. --not $not --)" || die "Could not get commits between $upstream and HEAD"
test -n "$revs" || die "No commits between $upstream and HEAD"
while count=$(test -z "$revs" && echo 0 || echo "$revs" | wc -l | tr -dc 0-9) && test $count -gt 1 do printf '\nMultiple candidates:\n' echo $revs | xargs git log --no-walk --oneline | cat -n
read input
case "$input" in
[0-9]|[0-9][0-9]|[0-9][0-9][0-9])
revs="$(echo "$revs" | sed -n "${input}p")"
count=1
break
;;
h|hh)
revs=$(history_of_staged_changes $upstream..)
continue
;;
hhhh)
history_of_staged_changes -p $upstream..
continue
;;
p)
git log -p --no-walk $revs
continue
;;
[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f])
revs=$input
continue
esac
revs="$(git rev-list --no-walk --grep="$input" $revs)"
donetest $count = 1 || die "No commit given"
set $revs }
git commit --fixup "$1"
message="$(git show -s --format=%s%n%n%b)"
case "$message" in
'fixup! fixup! '*)
message="${message#fixup! }"
message="${message#fixup! }"
message="${message#fixup! }"
git commit --amend -m "fixup! $message"
;;
esac
}history_of_staged_changes () { # <commit-range>
pretty=
if test "a-p" = "a$1"
then
pretty=-p
shift
fitest $# -le 1 || die "Usage: $0 <commit-range>"
test $# = 1 || {
upstream=$(git rev-parse --verify HEAD@{u} 2>/dev/null) ||
upstream=origin/master
set $upstream..
} args=$(for file in $(git diff --no-color --cached --name-only)
do
for hunk in $(get_hunks --cached -- "$file")
do
hunk1=${hunk%:*}
start1=${hunk1%,*}
end1=$(($start1+${hunk1#*,}-1))
echo "'$file:$start1-$end1'"
done
done)test -n "$args" || die "No staged files!"
eval hunk_history $pretty "$1" -- $args }
hunk_history () { # <commit-range> -- <file>:<start>[-<end>]...
pretty=
if test "a-p" = "a$1"
then
pretty=t
shift
fitest $# -ge 3 && test a-- = "a$2" || die "Usage: $0 <commit-range> -- <file>:<start>[-<end>]..."
range="$1" shift; shift
files="$(for arg
do
echo "'${arg%:*}'"
done)"for commit in $(eval git rev-list --topo-order "$range" -- $files) do if test -z "$lines" then lines="$(echo "$*" | tr ' ' '\n' | sed "s/^/$commit /")" fi
touched=
for line in $(echo "$lines" |
sed -n "s|^$commit ||p")
do
file="${line%:*}"
curstart="${line#$file:}"
curend="${curstart#*-}"
curstart="${curstart%%-*}"diff_output= parentstart=$curstart parentend=$curend parents=$(git rev-list --no-walk --parents \ $commit -- "$file" | cut -c 41-) if test -z "$parents" then touched=t fi
for parent in $parents
do
for hunk in $(get_hunks ^$parent $commit -- \
"$file")
do
hunk1=${hunk%:*}
start1=${hunk1%,*}
end1=$(($start1+${hunk1#*,}-1)) hunk2=${hunk#*:}
start2=${hunk2%,*}
end2=$(($start2+${hunk2#*,}-1))if test $start2 -le $curend && test $end2 -ge $curstart then touched=t fi
if test $end2 -le $curstart then diff=$(($end1-$end2)) parentstart=$(($curstart+$diff)) elif test $start2 -le $curstart then parentstart=$start1 fi
if test $end2 -le $curend then diff=$(($end1-$end2)) parentend=$(($curend+$diff)) elif test $start2 -le $curend then parentend=$end1 fi done
if test -n "$pretty" &&
test $curstart != $parentstart ||
test $curend != $parentend
then
test -n "$(echo "$diff_output" |
sed -n "s|^\([^ -]\[[^m]*m\)*diff --git a/$file b/||p")" ||
diff_output="$(printf '%s%s\n' "$diff_output" \
"$(git diff --color \
^$parent $commit -- $file |
sed '/^\([^ -]\[[^m]*m\)*@@ /,$d')")"
prefix="$(git rev-parse --git-dir)"
oldfile="${prefix}${prefix:+/}.old"
git show $parent:$file 2>/dev/null |
sed -n "$parentstart,${parentend}p" >$oldfile
newfile="${prefix}${prefix:+/}.new"
git show $commit:$file |
sed -n "$curstart,${curend}p" >$newfile
diff1="$(git diff --no-index --color $oldfile $newfile |
sed '1,4d')"
diff_output="$(printf '%s%s\n' "$diff_output" \
"$diff1")"
fi # TODO: support renames here
prefix="$parent $file:"
lines="$(printf '%s\n%s%s' "$lines" "$prefix" \
"$parentstart-$parentend")"
done
done
test -z "$touched" || {
if test -z "$pretty"
then
echo $commit
else
git show --color -s $commit --
echo "$diff_output"
fi
}
done
}# takes a commit range and a file name
# returns a list of <offset>,<count>:<offset>,<count>
get_hunks () {
# TODO: support renames here
git diff --no-color -U0 "$@" |
sed -n -e 's/\([-+][0-9][0-9]*\) /\1,1 /g' \
-e 's/^@@ -\([0-9]*,[0-9]*\) +\([0-9]*,[0-9]*\) .*/\1 \2/p' |
fix_hunks
}fix_hunks () {
while read hunk1 hunk2
do
case $hunk1 in
*,0)
printf '%d,0:' $((${hunk1%,0}+1))
;;
*)
printf '%s:' $hunk1
esac
case $hunk2 in
*,0)
printf '%d,0\n' $((${hunk2%,0}+1))
;;
*)
printf '%s\n' $hunk2
esac
done
}fixup "$@" -- snap --
Quite a handful to read, eh? And no code comments. I always meant to annotate it with some helpful remarks for future me, but never got around to do that, either. These days, I would probably
1. write the whole thing based on `git log -L <line-range>:<file>`, and
2. either implement it in node.js for speed, or directly in C.
> (And as a total aside, I found your apply-from-public-inbox.sh script > and really like it. Thanks for making it public.)
You're welcome! I am glad it is useful to you.
Ciao, Dscho