Hi Elijah,

On Fri, 4 May 2018, Elijah Newren wrote:

> On Thu, May 3, 2018 at 11:40 PM, Johannes Schindelin
> <johannes.schinde...@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; 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

Reply via email to