#!/bin/sh -e
#############################################################################
#
# git-ime: Split changes on a file into multiple git commits
#
#   Copyright (c) 2015-2021 Osamu Aoki
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License as
# published by the Free Software Foundation; either version 2 of
# the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public
# License along with this program; if not, write to the Free
# Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
# Boston, MA 02110-1301, USA.
#
#############################################################################

IMEDIFF="/usr/bin/imediff"
VERBOSE=""

vecho () {
    if [ -n "$VERBOSE" ]; then
        echo "$*" >&2
    fi
}


help() {

    echo "\
${0##*/}, version @version@

Usage: ${0##*/} [--verbose|-v] [--auto|-a]

DESCRIPTION: Split changes into multiple git commits"
}


init_version () {
    HASH="$(git rev-parse --short=8 --quiet HEAD)"
    if [ ! -r ".git/COMMIT_EDITMSG" ]; then
        echo "Dummy commit message (init) $HASH" > ".git/COMMIT_EDITMSG"
    fi
    LAST_VERSION="$( sed -n -e '1s/^.*@_\([0-9]*\)_@$/\1/p' \
            ".git/COMMIT_EDITMSG" )"
    if [ -z "$LAST_VERSION" ]; then
        LAST_VERSION="00"
        sed -i -e '1s/$/ @_00_@/' ".git/COMMIT_EDITMSG"
    else
        LAST_VERSION=$(printf "%02i" $((LAST_VERSION + 1)))
        sed -i -e "1s/@_[0-9]*_@/${LAST_VERSION} @_00_@/" ".git/COMMIT_EDITMSG"
    fi
}

set_version () {
    HASH="$(git rev-parse --short=8 --quiet HEAD)"
    if [ ! -r ".git/COMMIT_EDITMSG" ]; then
        echo "Dummy commit message (set) $HASH" > ".git/COMMIT_EDITMSG"
    fi
    LAST_VERSION="$( sed -n -e '1s/^.*@_\([0-9]*\)_@$/\1/p' \
            ".git/COMMIT_EDITMSG" )"
    if [ -z "$LAST_VERSION" ]; then
        LAST_VERSION="00"
        sed -i -e '1s/$/ @_00_@/' ".git/COMMIT_EDITMSG"
    else
        LAST_VERSION=$(printf "%02i" $((LAST_VERSION + 1)))
        sed -i -e "1s/@_[0-9]*_@/@_${LAST_VERSION}_@/" ".git/COMMIT_EDITMSG"
    fi
    # Drop old comments
    sed -i -n -e '/^\([^#]\|$\)/p' ".git/COMMIT_EDITMSG"
    vecho "I: LAST_VERSION: $LAST_VERSION"
}

set_name () {
    NAME="$(echo "$1"|sed 's/#/\\#/g')" # escape #
    HASH="$(git rev-parse --short=8 --quiet HEAD)"
    if [ ! -r ".git/COMMIT_EDITMSG" ]; then
        echo "Dummy commit message (name) $HASH" > ".git/COMMIT_EDITMSG"
    fi
    # set name
    sed -i -e '1s/:@: .*$//' ".git/COMMIT_EDITMSG"
    sed -i -e "1s#\$#:@: ${NAME}#" ".git/COMMIT_EDITMSG"
    # Drop old comments
    sed -i -n -e '/^\([^#]\|$\)/p' ".git/COMMIT_EDITMSG"
}

apply_imediff () {
    # shellcheck disable=SC2039
    local FLNM="$1"
    # sanity checks
    if [ -e "$FLNM.tmp_a" ]; then
        echo "File conflict, aborting... : $FLNM.tmp_a" >&2
        help
        exit 1
    elif [ -e "$FLNM.tmp_b" ]; then
        echo "File conflict, aborting... : $FLNM.tmp_b" >&2
        help
        exit 1
    fi
    init_version
    # newer file from local
    if [ -e "$FLNM" ]; then
        mv "$FLNM" "$FLNM.tmp_b"
    else
        : > "$FLNM.tmp_b"
    fi
    # older file
    git checkout "$HASH_OLD" "$FLNM"
    git reset "$HASH_OLD"
    if [ -e "$FLNM" ]; then
        mv "$FLNM" "$FLNM.tmp_a"
    else
        : > "$FLNM.tmp_a"
    fi
    vecho "I: ready to loop"
    while true
    do
        if [ -z "$AUTO" ]; then
            "$IMEDIFF" -o "$FLNM" "$FLNM.tmp_a" "$FLNM.tmp_b"
        else
            "$IMEDIFF" --macro "Abw" --non-interactive -o "$FLNM" "$FLNM.tmp_a" "$FLNM.tmp_b"
        fi
        chmod --reference "$FLNM.tmp_b" "$FLNM"
        if diff -q "$FLNM.tmp_a" "$FLNM" ; then
            vecho "I: no changes to commit for $FLNM"
        else
            vecho "I: found changes to commit for $FLNM"
            git add "$FLNM"
            set_version
            if [ -z "$AUTO" ]; then
                git commit --edit -F ".git/COMMIT_EDITMSG"
            else
                git commit --no-edit -F ".git/COMMIT_EDITMSG"
            fi
        fi
        if diff -q "$FLNM" "$FLNM.tmp_b" ; then
            vecho "I: no more changes for $FLNM"
            rm -f "$FLNM.tmp_a" "$FLNM.tmp_b"
            break
        fi
        if [ -e "$FLNM" ]; then
            mv -f "$FLNM" "$FLNM.tmp_a"
        else
            : > "$FLNM.tmp_a"
        fi
    done
    vecho "I: finish committed series for $FLNM"
}

if [ ! -x $IMEDIFF ]; then
    echo "E: install the $(basename $IMEDIFF) program." >&2
    exit 1
fi

AUTO=""
while true; do
    case "$1" in
        --auto|-a)
            AUTO="Yes"
            ;;
        --verbose|-v)
            VERBOSE="Yes"
            ;;
        '')
            break
            ;;
        *)
            help
            exit
            ;;
    esac
    shift
done
d="$(pwd)"
while [ ! -d "$d/.git" ] && [ "$d" != / ];
do d="$(readlink -f "$d/..")"; done
if [ "$d" = "/" ]; then
    echo "Not in the git repository, aborting..." >&2
    exit 1
fi
GIT_BASEDIR="$d"
cd "$GIT_BASEDIR" >/dev/null
unset d

if ! git diff --cached --quiet ; then
    echo "E: staged changes exist.  Commit them or un-stage them first" >&2
    exit 1
fi

if ! git diff --quiet ; then
    echo "E: local changes found.  Commit them or un-stage them first" >&2
    exit 1
fi

# Check how many files changed
N_FILES="$(git diff --name-only HEAD^ HEAD|wc -l)"
HASH_NEW="$(git rev-parse --verify --quiet HEAD)"
HASH_OLD="$(git rev-parse --verify --quiet HEAD^)"
# Set COMMIT_EDITMSG (repo may have unrelated COMMIT_EDITMSG)
vecho '------------------------------------------------------'
vecho 'I: git commit --amend --no-edit -q'
git commit --amend --no-edit -q
vecho '------------------------------------------------------'
vecho "I: commit message:"
vecho "$(sed -e 's/^/I: > /' .git/COMMIT_EDITMSG)"
vecho '------------------------------------------------------'

if [  "$N_FILES" = "0" ]; then
    echo "E: no changes found on HEAD" >&2
    exit 1
elif [  "$N_FILES" = "1" ]; then
    FILENAME="$(git diff --name-only "$HASH_OLD" "$HASH_NEW")"
    vecho "I: working on $FILENAME (split single file)"
    apply_imediff "$FILENAME"
else
    vecho "I: split into $N_FILES commits:"
    vecho "$( git diff --name-only "$HASH_OLD" "$HASH_NEW" | \
            sed -e 's/^/I: > /')"
    vecho '------------------------------------------------------'
    vecho 'I: git reset -q "HEAD^"'
    git reset -q "HEAD^"
    vecho '------------------------------------------------------'
    for FILENAME in $(git diff --name-only "$HASH_OLD" "$HASH_NEW") ; do
        if [ -e "$FILENAME" ]; then
            vecho "I: ... add    to   the index and commit: $FILENAME"
            git add "$FILENAME"
        else
            vecho "I: ... remove from the index and commit: $FILENAME"
            git rm --cached "$FILENAME"
        fi
        set_name "$FILENAME"
        git commit --no-edit -F ".git/COMMIT_EDITMSG"
    done
    vecho "I: split into $N_FILES commits ... done"
fi

# vim:se tw=78 ai sts=4 sw=4 et:
