Hi Elijah, On Fri, 4 May 2018, Elijah Newren wrote: > On Thu, May 3, 2018 at 11:40 PM, Johannes Schindelin > <Johannes.Schindelin@xxxxxx> 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; done test $# -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 fi revs="$(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)" done test $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 fi test $# -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 fi test $# -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