#!/bin/bash

set -eu
set -o pipefail
set +o posix

helpme() {
    cat << EOF

Iteratively cherry pick what is missing in this branch from various sources.
It will try to import patches and stop if there are conflicts.

Example executions:

    sync all changes since VAST NFS v4.0.28:

        ./scripts/sync-patches.sh --older-branch --sync-target v4.0.31

    sync all changes from Linux since v6.6.28:

        ./scripts/sync-patches.sh --kernel-repo ~/linux --sync-target v6.6.28


More useful parameters:

    --show   List patches and their status. This is what it would try to apply.


Information regarding base files:

    scripts/BASE    - specifies starting point and commit-wise decisions from Linux kernel
    scripts/OLDER   - specifies starting point and commit-wise decisions from an older branch

    Format:

        <base-ref>
        # comment
        skipped-commit <git-hash>

EOF
    exit 1
}

main() {
    local kernel_repo=""
    local sync_target=""
    local show=0
    local base
    local cherry_pick=""
    local older_branch=0
    local make_skip_report_template=0

    while [[ $# != 0 ]] ; do
	if [[ "$1" == "--kernel-repo" ]] ; then
	    shift
	    kernel_repo=$1
	    shift
            continue
	elif [[ "$1" == "--help" ]] ; then
            helpme
            continue
	elif [[ "$1" == "--older-branch" ]] ; then
            older_branch=1
            shift
            continue
	elif [[ "$1" == "--sync-target" ]] ; then
	    shift
	    sync_target=$1
	    shift
	    continue
	elif [[ "$1" == "--show" ]] ; then
	    show=1
	    shift
	    continue
	elif [[ "$1" == "--make-skip-report-template" ]] ; then
	    make_skip_report_template=1
	    shift
	    continue
	elif [[ "$1" == "--cherry-pick" ]] ; then
	    shift
	    cherry_pick="$1"
	    shift
	    continue
	fi
	break # Continue to positional arguments
    done

    local origin_repo
    local subpath_dir
    local subpath_cd

    if [[ "${kernel_repo}" == "" ]] && [[ "${older_branch}" == "0" ]]; then
	echo "Please pass --kernel-repo or --older-branch"
	exit -1
    fi

    if [[ "${kernel_repo}" != "" ]]; then
        origin_repo="$(cd ${kernel_repo} && realpath $(git rev-parse --git-common-dir))"
        base="BASE"
        subpath_dir='--directory=bundle/'
        subpath_cd='cd bundle && '
    else
        origin_repo="$(git rev-parse --git-common-dir)"
        base="OLDER"
        subpath_dir=""
        subpath_cd=""
    fi

    basefile=$(dirname ${BASH_SOURCE})/${base}
    base=$(cat ${basefile} | head -n 1)

    if [[ "${cherry_pick}" == "" ]] ; then
	if [[ "${sync_target}" == "" ]] ; then
	    echo "Neither --cherry-pick pasesed nor --sync-target"
	    exit -1
	fi
    fi

    if [[ "${show}" == "0" ]] ; then
	if [[ -n "$(git status --porcelain)" ]]; then
	    echo "Aborting - there are uncommited changes"
	    echo
	    git status
	    exit -1
	fi
    fi

    local tmp_dir

    tmp_dir=$(mktemp -d -t older-branch-sync-patches-XXXXXXXXXX)

    if [[ "$older_branch" == "1" ]] ; then
        files=()
    else
        files=(fs/nfs/ fs/nfsd/ fs/nfs_common fs/lockd net/sunrpc \
            include/linux/lockd/* include/uapi/linux/nfs* \
            include/linux/nfs* include/linux/sunrpc/ \
            include/trace/events/sunrpc.h include/trace/events/rpcrdma.h \
            include/trace/events/rpcgss.h)
    fi

    local range

    if [[ "${cherry_pick}" == "" ]] ; then
	range="${base}..${sync_target}"
    else
	range="${cherry_pick}~1..${cherry_pick}"
    fi

    echo Checking ${range} -- "${files[@]}"
    git --git-dir ${origin_repo} format-patch -q ${range} -o ${tmp_dir} -- \
	"${files[@]}"

    local save
    save=$(git rev-parse HEAD)
    local fails=0

    set +e
    git log --pretty="%b" | \
	grep -E '^[(]cherry[- ]+picked from commit [a-f0-9]+[)]$' > ${tmp_dir}/cherry-picked.txt
    cat ${basefile} | grep ^skipped-commit >> ${tmp_dir}/skipped.txt
    set -e

    local nr_patches=0
    for i in $(ls ${tmp_dir}/*.patch 2> /dev/null) ; do
	echo $(basename ${i})
	local commithash=$(cat ${i} | head -n 1 | awk '{print $2}')

	if [[ ! -n ${commithash} ]] ; then
	    echo "error obtaining commit hash from ${i}"
	    exit -1
	fi

	if grep -q ${commithash} ${tmp_dir}/cherry-picked.txt ; then
	    echo "Already applied"
	    continue
	fi

	if grep -q ${commithash} ${tmp_dir}/skipped.txt ; then
	    echo "Skipped"
	    continue
	fi

	nr_patches=$((${nr_patches} + 1))

	cat ${i} \
	   | awk '{if ($0 !~ /^[[:space:]]*$/) {print} else {exit} }' \
	    > ${tmp_dir}/patch.txt
	echo "" >> ${tmp_dir}/patch.txt
	echo "(cherry-picked from commit ${commithash})" >> ${tmp_dir}/patch.txt
	cat ${i} | \
	   awk '{if ($0 ~ /^[[:space:]]*$/) {a=1}; if (a) {print} }' \
	   >> ${tmp_dir}/patch.txt

        local subject
        subject=$(grep -E ^Subject: ${tmp_dir}/patch.txt)
        if [[ "${subject}" =~ ^Subject:\ \[.*\]\ (.*)$ ]]; then
            subject="${BASH_REMATCH[1]}"
        fi

	local e=0
	if [[ "${show}" == "0" ]] ; then
	    set +e
	    cat ${tmp_dir}/patch.txt | git am ${subpath_dir}
	    local e=$?
	    set -e
        else
            if [[ "${make_skip_report_template}" == "1" ]] ; then
                echo "# <why>: ${subject}" >> ${tmp_dir}/skip-report.txt
                echo "skipped-commit ${commithash}" >> ${tmp_dir}/skip-report.txt
            fi
	fi

	if [[ $e != 0 ]] ; then
	    echo "Error applying patch ${commithash}"
	    echo
	    echo -e "\033[1;37mApply it manually using\033[0m:"
	    echo
	    echo "	git am --show-current-patch | (${subpath_cd}patch -p1)"
	    echo
	    echo -e "\033[1;37mOr add it to ${basefile}\033[0m:"
	    echo
	    echo "      git am --abort"
	    echo "      echo '# <why>: ${subject}' >> ${basefile}"
	    echo "      echo 'skipped-commit ${commithash}' >> ${basefile}"
	    echo
	    echo "To commit, 'git add' the necessary changes and 'git am --continue'".
	    echo
	    fails=1
	    break
	fi
    done

    if [[ "${make_skip_report_template}" == "1" ]] ; then
        echo
        echo Can add following skips:
        echo
        cat ${tmp_dir}/skip-report.txt
    fi

    if [[ ${fails} == "1" ]] ; then
	echo "Please perform conflict resolution. "
	echo
	echo -e "\033[1;33mWe are currently in 'git am' mode!\033[0m"
    fi

    rm -rf ${tmp_dir}
}

describe-applied() {
    local kernel_repo=""
    local sync_target=""
    local show=0
    local base=$(cat $(dirname ${BASH_SOURCE})/BASE | head -n 1)

    while [[ $# != 0 ]] ; do
	if [[ "$1" == "--kernel-repo" ]] ; then
	    shift
	    kernel_repo=$1
	    shift
            continue
	fi
	break # Continue to positional arguments
    done

    if [[ "${kernel_repo}" == "" ]] ; then
	echo "Please pass --kernel-repo"
	exit -1
    fi

    commit=""
    while read a b c d ; do
	if [[ "$a" == "(cherry-picked" ]] ; then
	    orig_commit=$(echo ${d} | tr -d \))
	    contained=$(cd ${kernel_repo} && git describe --contains ${orig_commit})
	    printf "%15s | " ${contained}
	    echo $(cd ${kernel_repo} && git log --abbrev-commit --abbrev=12 \
		--date=format:%Y-%m-%d \
    	        --format="%cd | %h | %s" -n 1 ${orig_commit});
	else
	    commit=${b}
	fi
    done < <(git log --pretty=fuller | \
	grep -E '^((commit.*)|( *[(]cherry-picked from commit [a-f0-9]+[)]))$')

}

if [[ "$1" == "describe-applied" ]] ; then
    shift
    describe-applied "$@"
    exit 0
fi

main "$@"
