#!/bin/bash
#
# Copyright (C) 2010 Bart Trojanowski <bart@jukie.net>
#
# This script imports patches generated from a linux/klips git tree into
# an libreswan git tree.

set -e

prog=$(basename $0)

say() {
        echo >&2 "$@"
}
warn() {
        say "$prog: $@"
}
die() {
        warn "$@"
        exit 1
}

do_help() {
        local err=$1
        local out=1
        [[ -n "$err" && "$err" -eq 0 ]] || out=2
        cat >&$out <<END
This is a tool that helps in importing patches from klips to libreswan.

You can import a single patch that changes klips code:

  $prog -p1 < some.patch

  -p[<num>] --patch[=<num>]     - import from stdin, -p<num> is passed to patch
                                  (default is 1)

Or import from an existing linux-2.6.git tree with klips applied:

  $prog -k <dir> [ -n <num> ] [ -h <ref> ]

  -k --kernel <dir>             - path to kernel git tree
  -n --number <num>             - number of patches to import (default is 1)
  -r --head <ref>               - git ref/head to start from (default is HEAD)

Generic options:

  -h --help                     - this help
  --dry-run                     - don't apply anything
  -q --quiet                    - pass quiet flag to git-am
  -t --tmp-dir <dir>            - use this temporary directory

END
        exit $err
}

# set defaults
kernel_mode=false
patch_mode=false
dryrun_mode=false
arg_head=
arg_kernel=
arg_count=
arg_pnum=
arg_quiet=
arg_tmpdir=/tmp

# this will queue a patch if it looks like it would apply
queue_patch() {
        local queue="$1"
        local patch="$2"

        # make sure this patch contains only changes to files we care about
        diffstat -l -p1 "$patch" \
        | while read fn ; do
                local dir=$(dirname "$fn")
                if ! [[ -d "linux/$dir" ]] ; then
                        fn=$(basename "$fn")
                        warn "Patch modifies $fn in $dir, but linux/$dir is not an libreswan"
                        warn "directory.  You could 'mkdir $dir' to skip this warning."
                        exit 1
                fi
        done

        # convert paths to contain linux/ prefix
        sed -ie 's,^\(\(---\|+++\) \([a-z]\+/\)\{'"$arg_pnum"'\}\)\(.*\)$,\1linux/\4,' "$patch"

        # add it to the queue
        cat "$patch" >> "$queue"

        # check if the patch applies (including all the ones that came before it
        git apply --check -p"$arg_pnum" "$queue" || die "patch doesn't apply"
}

apply_queue() {
        local queue="$1"

        $dryrun_mode && return 0

        # import the patch
        git am < "$queue"
}

is_git_dir() {
        local dir="$1"
        [[ -d "$dir" \
        && -f "$dir/HEAD" \
        && -d "$dir/objects" \
        && -d "$dir/refs" ]]
}

find_kernel_git_dir() {
        for dir in "$arg_kernel" "$arg_kernel/.git" ; do
                if is_git_dir "$dir" ; then
                        echo "$dir"
                        return 0
                fi
        done

        die "cannot find git dir at $arg_kernel"
}

# imports $arg_count patches from $arg_kernel
do_kernel_import() {
        local workdir="$arg_tmpdir/$prog-$$"
        local series="$workdir/series"
        local queue="$workdir/queue"

        [[ -d $workdir ]] && die "$workdir already exists, remove it"
        mkdir -p "$workdir" || die "failed to create: $workdir"
        trap "rm -rf $workdir" EXIT HUP INT QUIT ABRT

        # find the kernel dir
        local kernel_git=$(find_kernel_git_dir)

        # generate those patches
        git --git-dir "$kernel_git" \
            format-patch \
                -"$arg_count" \
                --suffix=.patch \
                --output-directory "$workdir" \
                "$arg_head" \
        > "$series" \
        || die "failed to generate $arg_count patch(es) in from $kernel_git"

        for patch in $(cat "$series") ; do
                local name=$(basename "$patch")

                say "Testing $name..."
                queue_patch "$queue" "$patch"
        done

        say "Looks good, now applying..."
        apply_queue "$queue"

        say "DONE"
}

# import patch read from /dev/stdin
do_patch_from_stdin() {
        local patch="$arg_tmpdir/$prog-$$.patch"
        local queue="$arg_tmpdir/$prog-$$.queue"
        trap "rm -f $patch $queue" EXIT HUP INT QUIT ABRT

        # read in the patch
        cat > "$patch"

        # reset the queue
        cat </dev/null >"$queue"

        say "Testing patch from stdin..."
        queue_patch "$queue" "$patch"

        say "Looks good, now applying..."
        apply_queue "$queue"

        say "DONE"
}

# parse parameters
while [[ -n "$1" ]] ; do
        cmd="$1"
        shift
        case "$cmd" in
            -h|--help)
                do_help 0
                ;;
            --dry-run)
                dryrun_mode=true
                ;;
            --debug)
                set -x
                ;;
            -r|--head)
                arg_head="$1"
                shift || die "--head requires an argument"
                ;;
            -k|--kernel)
                kernel_mode=true
                arg_kernel="$1"
                [[ -d "$arg_kernel" ]] || die "no such directory: $arg_kernel"
                shift
                ;;
            -p|--patch)
                patch_mode=true
                ;;
            -p[0-9]*)
                patch_mode=true
                arg_pnum="${cmd#-p}"
                ;;
            --patch=[0-9]*)
                patch_mode=true
                arg_pnum="${cmd#*=}"
                ;;
            -n|--number)
                arg_count="$1"
                shift || die "--number requires an argument"
                ;;
            -[0-9]*)
                arg_count="${cmd#-}"
                ;;
            -q|--quiet)
                arg_quiet="--quiet"
                ;;
            -t|--tmp-dir)
                arg_tmpdir="$1"
                [[ -d "$arg_tmpdir" ]] || die "no such directory: $arg_tmpdir"
                shift
                ;;
            *)
                warn "invalid option: $cmd"
                do_help 1
                ;;
        esac
done

( $kernel_mode || $patch_mode ) || die "need to use --kernel or --patch mode"
( $kernel_mode && $patch_mode ) && die "only use --kernel or --patch mode"

for n in diffstat filterdiff ; do
        $n --help </dev/null >/dev/null 2>&1 || die "cannot find '$n' (part of patchutils package)"
done

if [[ -z "$arg_pnum" ]] ; then
        arg_pnum=1
elif [[ "$arg_pnum" -gt 0 ]] ; then
        die "--patch/-p level must be a number"
fi

if $kernel_mode; then
        [[ -z "$arg_count" ]] && arg_count=1
        [[ -z "$arg_head" ]] && arg_head=HEAD
        do_kernel_import
        ret=$?

elif $patch_mode; then
        do_patch_from_stdin
        ret=$?

else
        ret=1
fi

exit $ret
