#!/bin/bash -p


fail () {
    set +exu
    local ret="${1}"
    shift
    echo "${@}" >&2
    exit "${ret}"
}


require_program () {
    command -v "${1}" &>/dev/null ||
        fail 127 "Required program '${1}' not found in PATH"
}


set -eu
set -o pipefail
IFS=$'\n\t'

true <<'=cut'
=head3 init_repo

Initialize the git repo, add the quilt files to the exclude list, and
commit all the other files.  Tag the commit 'base'.

=cut

init_repo () {
    git init .
    if $lineendings; then
        git config core.safecrlf false
    fi
    printf "%s\n" '' .pc patches series ./*.patch tags TAGS >> .git/info/exclude
    git add .
    # set the date so we get the same hash each run
    export GIT_AUTHOR_DATE="1970-01-01 00:00:00"
    export GIT_COMMITTER_DATE="1970-01-01 00:00:00"
    git commit -aqm "Base sources"
    unset GIT_AUTHOR_DATE
    unset GIT_COMMITTER_DATE
    git tag base
}


true <<'=cut'
=head3 fix_line_endings

Convert the line endings of each patch, and each file affected by the
patches, to Unix (LF) format.  The source tree ends up with no patches
applied.

=cut

fix_line_endings () {
    [[ -e patches ]] || return

    find . -name .git -prune -o -type f -print0 | xargs --max-procs=3 -0 dos2unix
}


true <<'=cut'
=head3 convert_patch_to_commit

Turn a single patch into a git commit.  If the patch has been created by
git-format-patch (guessed by looking for a C<From> line with a hash at
the top of the patch, then use C<git am> to restore the commit.  If that
fails, or if the patch was not created by git, then create a commit with
the filename of the patch in the log message.

Tag each commit as C<patchN>, where N is the index of the patch in the
series.

=cut

convert_patch_to_commit () {
    local patchname patchnum
    patchnum="$1"
    patchname=$(quilt top)
    patchname=${patchname#patches/}

    if grep -qE '^From [0-9a-f]{40} ' <(quilt header | head -1); then
        # git made this patch
        # Undo quilt push's changes to the working tree
        # and use git am to apply the patches instead

        # stash the changes the patch made to undo them and in case we
        # need to redo them later
        git stash -q
        # nuke stray files/dirs
        git clean -df
        if git am -q --committer-date-is-author-date "patches/$patchname"; then
            # git am was successful, so we don't need the stashed
            # changes anymore
            git stash drop -q || :
        else
            # fallback to previous method if git am fails

            # undo anything git am might have done
            git am --abort || :
            git reset --hard HEAD || :

            # apply the patch again
            git stash pop -q || :

            # commit changes and new files
            git add . || :
            git commit -qm "${patchname%.patch}"
        fi
    else
        # commit changes and new files
        git add . || :
        git commit -qm "${patchname%.patch}"
    fi
    git tag "patch$patchnum"
}


true <<'=cut'
=head3 apply_all_patches

Apply all patches in the quilt series, converting each patch to a commit
on the way.  Tag each patch C<patchN>, where N is the index of the patch
in the series.  Tag the fully patched state of the repo C<fullypatched>.

=cut

apply_all_patches () {
    local patchnum=1
    quilt pop -afq &> /dev/null || :
    while quilt push; do
        convert_patch_to_commit "$patchnum"
        (( patchnum++ ))
    done

    # quilt next returns nonzero if at the last patch
    quilt next &> /dev/null || git tag fullypatched -m 'Fully patched'
}


true <<'=cut'
=head3 fix_patches_dir

Turn the C<patches> dir from a symlink (which may be a symlink to C<..>,
creating a loop) to a regular directory containing just the patches.

=cut

fix_patches_dir () {
    # check if we really need to do anything first
    if [[ -L patches ]]; then
        # save all the patch files, since the dir 'patches' links to
        # may have other files in it
        local patches_tempdir
        patches_tempdir="$(mktemp -d)"
        quilt series | while read -r line; do
            patchfile=${line#patches/}
            cp -f "patches/$patchfile" "$patches_tempdir/$patchfile" || :
        done
        rm -f patches
        mv "$patches_tempdir" patches
    fi
}


########
# MAIN #
########


require_program quilt
require_program git


lineendings=false
fixpatchesdir=false
force=false

while getopts :lpf OPT; do
    case $OPT in
        l)
            lineendings=true
            ;;
        p)
            fixpatchesdir=true
            ;;
        f)  force=true
            ;;
        *)
            echo "usage: ${0##*/} [-lpf] [--] [DIRECTORY]"
            echo ""
            echo "-l  Fix line endings in patches"
            echo "-p  Turn patches symlink into directory"
            echo "-f  Don't check for presence of quilt files"
            exit 2
    esac
done
shift $(( OPTIND - 1 ))
OPTIND=1

directory=${1:-$(pwd)}

cd "$directory"
if [[ $(pwd | wc -l) -gt 1 ]]; then
    fail 3 "No embedded newline shenanigans please!"
fi

if ! $force && [[ ! -d patches || ! -f series ]]; then
    fail 4 "No quilt files found"
fi


init_repo

if $fixpatchesdir; then
    fix_patches_dir
fi

if $lineendings; then
    fix_line_endings
fi

apply_all_patches

