#!/usr/bin/env sh
GITDIFFOPTS="$GITDIFFOPTS -p"


usage() {
    script=$(basename "$0")
    cat << EOF
usage: $script [OPTIONS] GITREVRANGE

Prepare Git patches for distro packaging

Basically \`git format-patch\` as a POSIX-compliant shell script, but using
unified patch format instead of the UNIX mailbox format for output. The script
does patch enumeration and automatic filename generation with commit title and
patch number for commits of a given revision range selection.

Use a software's Git-controlled sources to manage and generate patches for
packaging it for a software distribution like ArchLinux, MSYS2, Ubuntu, etc.

While it is probably okay to make quick (monkey-)patches that fix the software
for a specific software distribution (and possibly break it for every other), it
is better to make proper surgical-precision patches and attempt to have them
accepted by the software developer ("upstream").

That's what this script helps with: Use Git on the sources, track patches as
commits for creating a merge request, while generating packagaging patch files
in unified-format for each commit, until the changes were accepted upstream.

Options

    -h                      - show this message
    -i INITIAL_PATCH_NUMBER - initial patch number [optional]
    -o OUTPUT_DIRECTORY     - output directory [optional]

Environment Variables:

    AS_LIB               - don't actually run the script. Can be used to
                           import functions from this shell script.
    GITDIFFOPTS          - optional arguments to supply to \`git diff\`
    GITLOGOPTS           - optional arguments to supply to \`git log\`
    GITREVRANGE          - revision range selector, e.g. HEAD...HEAD^
    INITIAL_PATCH_NUMBER - number to start counting patches (upward) from
    OUTPUT_DIRECTORY     - directory to store generated patch files under

Examples:

    Output to stdut

        $script HEAD...v6

    Output to directory

        $script -o ../../msys2/MINGW-packages/mingw-w64-mypackage HEAD...v6

    Start patch numbering at 5

        $script -i 5 HEAD...v6

    Pass output directory as environment variable

        OUTPUT_DIRECTORY=some/dir $script -i 30 HEAD...v6

    dot import a function and call it directly

        AS_LIB=yes . $script
        patch_from_commit -o some/dir 1a34e 1

    Disable renames in diffs by passing \`git diff\` option

        GITDIFFOPTS='--no-renames' $script HEAD...v6

    Trim revision range selection to 3 by passing \`git log\` option

        GITLOGOPTS="-3" sh $script 'HEAD'

See Also:

    - https://www.msys2.org/wiki/Creating-Packages/#patch-software
    - https://git-scm.com/docs/git-format-patch
    - https://git-scm.com/docs/git-diff
    - https://www.gnu.org/software/diffutils/manual/html_node/patch-Options.html
EOF
}


patch_from_commit() {
    stdout=yes

    while getopts "i:o:" opt; do
        case $opt in
            o) output_directory="$OPTARG";;
            i) patch_number="$OPTARG";;
            \?)
                echo "Invalid option: -$OPTARG" >&2
                return 1
                ;;
            :)
                echo "Option -$OPTARG requires an argument." >&2
                return 2
                ;;
        esac
    done

    shift $((OPTIND -1))

    commit_id=$1

    test -z "$patch_number" && patch_number=1
    ! test -z "$output_directory" && stdout=no

    name="$(
        git log --pretty=format:%s $commit_id -1 \
        | sed -e 's|[^A-Za-z0-9_]|-|g' \
        | sed -E 's|-*-|-|g' \
        | sed -E 's|-$||'
    )"

    padded="0000$patch_number"
    ppatchnumber="$(
        echo $padded | sed "s|$(echo "$padded" | sed -e 's|....$||')||"
    )"

    path="$output_directory"/"$ppatchnumber-$name".patch

    cmd="git diff $GITDIFFOPTS $commit_id^..$commit_id"

    if ! test "$stdout" '=' 'no'; then
        $cmd --color | tee /dev/null
    else
        echo "$(basename "$0"): $path"
        $cmd --no-color | tee "$path"
    fi
}


format_patch() {
    local OPTIND
    local OPTARG

    while getopts "o:i:" opt; do
        case $opt in
            o) optargs_patch_from_commit="$optargs_patch_from_commit -o $OPTARG";;
            i) patch_number=$OPTARG;;
            \?)
                echo "Invalid option: -$OPTARG" >&2
                return 4
                ;;
            :)
                echo "Option -$OPTARG requires an argument." >&2
                return 5
                ;;
        esac
    done

    shift $((OPTIND -1))
    unset OPTIND
    unset OPTARG

    test -z "$patch_number" && patch_number=1

    for commit_id in $(git log $GITLOGOPTS --oneline $@ | cut -d ' ' -f1 | tac); do

        git log $commit_id -1 >&2 | tee /dev/null

        patch_from_commit $optargs_patch_from_commit -i $patch_number $commit_id \
        | sed "s|^|$commit_id: |"

        patch_number=$(expr $patch_number '+' 1)
    done
}


while getopts "i:o:h" opt; do
    case $opt in
        o) OUTPUT_DIRECTORY="$OPTARG";;
        i) INITIAL_PATCH_NUMBER=$OPTARG;;
        h) usage; exit 0;;
        :)
            echo "Option -$OPTARG requires an argument." >&2
            usage
            exit 6
            ;;
    esac
done

shift $((OPTIND -1))

unset OPTIND
unset OPTARG

! test -z "$1" && GITREVRANGE="$1"
test -z "$GITREVRANGE" && {
    echo "error: missing first argument: Git revision range" >&2
    exit 3
}

! test -z "$OUTPUT_DIRECTORY" && optargs="$optargs -o "$OUTPUT_DIRECTORY""
! test -z "$INITIAL_PATCH_NUMBER" && optargs="$optargs -i $INITIAL_PATCH_NUMBER"

! test "$AS_LIB" '=' 'yes' && format_patch $optargs "$GITREVRANGE"
