#!/bin/sh

# patch generator from git history
# handles partial source keeping, when git contains only changed files, not the whole project

VERSION='1.1'

KEEP_TEMP=

print_version()
{
    echo "${VERSION}"
    exit 0
}

print_help()
{
    echo "\
Usage:
genpatch-git [-d] ORIGDIR
genpatch-git --help | --version

Generates patches from git history, handling partial source keeping:
when changed files only are commited to git repo.
Needs to be launched from within git repository.
Saves results to genpatch/.

ORIGDIR contains path to directory with original (unchanged) sources.
"
    exit 0
}

in_arg_cycle=1
while [ -n "$in_arg_cycle" ] ; do
    arg="$1"
    in_arg_cycle=

    checked_arg="${arg#-}" || true
    if [ "$checked_arg" != "${arg}" ] ; then
        in_arg_cycle=1
        shift

        case ${checked_arg} in
            -version)
                print_version
            ;;
            -help)
                print_help
            ;;
            d)
                KEEP_TEMP=1
            ;;
            -)
                in_arg_cycle=
            ;;
            *)
                printf "%s\n" "Unknown option ${arg}"
                exit 3
            ;;
        esac
    fi
done

orig="$1"

if [ ! -d "${orig}" ] ; then
    printf "%s\n" "First argument must contain directory with original sources" 1>&2
    exit 1
fi

orig="$(cd "${orig}" ; pwd -P)"

if [ ${orig} = "$(pwd)" ] ; then
    printf "%s\n" "Must be launched from directory with changed sources, not original ones" 1>&2
    exit 2
fi

orig_dir="$(pwd)"
output_dir="${orig_dir}/genpatch"
rm -rf "${output_dir}"
mkdir -p "${output_dir}"

printf "%s\n" "Generating patches:"
first_commit="$(git log --format=format:%H --reverse | head -1)"
git show ${first_commit} > "${output_dir}/0000-init.patch"
printf "%s\n" "${output_dir}/0000-init.patch"
git format-patch -N -o "${output_dir}" ${first_commit}
cd "${output_dir}"

patch_marker_null='--- /dev/null'
patch_marker_added_file='+++ b/'
patch_marker_first_file='a'
patch_marker_second_file='b'
patch_marker_removed_file='--- a/'
patch_marker_null_removed='+++ /dev/null'

patches="$(ls)"
temp_patch="${output_dir}/temp.patch"
checkout_dir="${orig_dir}/../tmp_checkout"
plainfiles_dir="${output_dir}/files"
rm -rf ${checkout_dir}

for single_patch in ${patches} ; do
    printf "%s\n" "processing ${single_patch}"
    input="${single_patch}"
    commit="$(head -1 ${input} | cut -d' ' -f2)"

    binary_files="$(sed -e "\%^+[^+]%d" ${input} | \
        sed -n -e '\%[ ]*|[ ]*Bin [0-9]* -> [1-9][0-9]* bytes%p' | sed -e 's% | .*$%%')"
    rm_binary_files="$(sed -e "\%^+[^+]%d" ${input} | \
        sed -n -e '\%[ ]*|[ ]*Bin [0-9]* -> 0 bytes%p' ${input} | sed -e 's% | .*$%%')"
    if [ -n "${binary_files}" -o -n "${rm_binary_files}" ] ; then

        modified_name="${input%.patch}"
        modified_name="${modified_name}.nonbin.patch"
        modified_nonbin_patch="$(pwd)/${modified_name}"
        sed -n '\%diff --git%,\%GIT binary%{\%diff --git%{p;n};\%GIT binary patch%{b};p}' \
            ${input} > ${modified_nonbin_patch}
        rm ${input}
        input="${modified_name}"
    fi

    if [ -n "${binary_files}" ] ; then
        if [ -z "${commit}" ] ; then
            printf "%s\n" "Patch ${input} doesn't contain (correctly) a hash, binary files require it." 1>&2
        else
            if [ ! -d "${checkout_dir}" ] ; then
                cp -pRf "${orig_dir}" "${checkout_dir}"
            fi
            (
            cd ${checkout_dir}
            git checkout ${commit}
            mkdir -p ${plainfiles_dir}
            for single_bin in ${binary_files} ; do
                basedir="$(dirname ${single_bin})"
                mkdir -p ${plainfiles_dir}/${basedir}
                cp -pf ${single_bin} ${plainfiles_dir}/${basedir}/
            done
            )
        fi
    fi

    if [ -n "${rm_binary_files}" ] ; then
        for single_rm_bin in ${rm_binary_files} ; do
            if [ -f ${plainfiles_dir}/${single_rm_bin} ] ; then
                rm -f ${plainfiles_dir}/${single_rm_bin}
            else
                printf "%s\n" "Removing of binary files from original set is not supported." 1>&2
            fi
        done
    fi

    new_original_files=
    rm_original_files=
    new_files="$(sed -e "\%^+[^+]%d" ${input} | \
                 sed -n -e "\%${patch_marker_null}%,\%${patch_marker_added_file}%p" | \
                 sed -n -e "s%${patch_marker_added_file}\(.*\)%\1%p" \
                )"
    if [ -n "${new_files}" ] ; then
        : > ${temp_patch}
        modified_name="${input%.patch}"
        modified_name="${modified_name}.add.patch"
        modified_add_patch="$(pwd)/${modified_name}"
        : > ${modified_add_patch}
        for single_file in ${new_files} ; do
            if [ -f ${orig}/${single_file} ] ; then
                sed -n "\%diff --git a/${single_file} b/${single_file}%,\%diff --git %p" ${input} >> ${temp_patch}
                new_original_files="${new_original_files} ${single_file}"
            else
                sed -n "\%diff --git a/${single_file} b/${single_file}%,\%diff --git %p" ${input} >> ${modified_add_patch}
            fi
        done
        tmpdir="tmpdir-${input}"
        mkdir ${tmpdir}
        cd ${tmpdir}
        patch -p1 < ${temp_patch} > /dev/null
        current_dir="$(pwd)"
        for single_file in ${new_original_files} ; do
            diff -c ${orig}/${single_file} ${current_dir}/${single_file} | \
                sed -e "s%${orig}%${patch_marker_first_file}%g" \
                    -e "s%${current_dir}%${patch_marker_second_file}%" \
                >> ${modified_add_patch}
        done
        if [ -z "$(head -1 ${modified_add_patch})" ] ; then
            rm ${modified_add_patch}
        fi
        cd ..
        mv ${tmpdir} add-${tmpdir}
    fi

    rm_files="$( sed -e "\%^+[^+]%d" ${input} | \
                 sed -n -e "\%${patch_marker_null_removed}%{x;p;d;}" -e x ${input} | \
                 sed -n -e "s%${patch_marker_removed_file}\(.*\)%\1%p" \
               )"
    if [ -n "${rm_files}" ] ; then
        : > ${temp_patch}
        modified_name="${input%.patch}"
        modified_name="${modified_name}.rm.patch"
        modified_rm_patch="$(pwd)/${modified_name}"
        : > ${modified_rm_patch}
        for single_file in ${rm_files} ; do
            if [ -f ${orig}/${single_file} ] ; then
                sed -n "\%diff --git a/${single_file} b/${single_file}%,\%diff --git %p" ${input} >> ${temp_patch}
                rm_original_files="${rm_original_files} ${single_file}"
            else
                sed -n "\%diff --git a/${single_file} b/${single_file}%,\%diff --git %p" ${input} >> ${modified_rm_patch}
            fi
        done
        tmpdir="tmpdir-${input}"
        mkdir ${tmpdir}
        cd ${tmpdir}
        patch -p1 -R < ${temp_patch}
        current_dir="$(pwd)"
        for single_file in ${rm_original_files} ; do
            diff -c ${current_dir}/${single_file} ${orig}/${single_file}  | \
                sed -e "s%${orig}%${patch_marker_first_file}%g" \
                    -e "s%${current_dir}%${patch_marker_second_file}%" \
                >> ${modified_rm_patch}
        done
        if [ -z "$(head -1 ${modified_rm_patch})" ] ; then
            rm ${modified_rm_patch}
        fi
        cd ..
        mv ${tmpdir} rm-${tmpdir}
    fi
    if [ -n "${new_files}" -o -n "${rm_files}" ] ; then
        modified_name="${input%.patch}"
        modified_name="${modified_name}.mod.patch"
        modified_mod_patch="$(pwd)/${modified_name}"

        modified_files="$(git show --name-status --pretty=oneline ${commit} | \
                sed -n -e 's/^M[ \t]*//p' | \
                tr  '\n'  ' ' )"
        if [ -n "${modified_files}" ] ; then
            : > ${modified_mod_patch}
            for single_file in ${modified_files} ; do
                sed -n "\%diff --git a/${single_file} b/${single_file}%,\%diff --git %p" ${input} >> ${modified_mod_patch}
            done
            if [ -z "$(head -1 ${modified_mod_patch})" ] ; then
                rm ${modified_mod_patch}
            fi
        fi
        rm ${input}
    fi
done

rm ${temp_patch}
rmdir ${output_dir} || true
if [ -z "${KEEP_TEMP}" ] ; then
    rm -rf add-tmpdir-*
    rm -rf rm-tmpdir-*
    rm -rf ${checkout_dir}
fi

# ---------------------------------------------------------

# Copyright (c) 2017-2018 Random Crew.
#
# Available under ISC License.

# ---------------------------------------------------------

