#!/usr/bin/env bash
# Copyright (C) 2016 Kernc, Google Inc., authors, and contributors
# Licensed under http://www.apache.org/licenses/LICENSE-2.0
# Created By: miha@reciprocitylabs.com

set -o pipefail
set -o nounset
set -o errexit

ARG1=${1:-}
GIT_REPO="$(pwd)"
TMP_REPO="$GIT_REPO/$(mktemp -d pylint_diff.XXXXXXX)"
CACHE_DIR="$GIT_REPO/.pylint_cache"
UNCOMMITED_PATCH="$TMP_REPO/uncommited.patch"
SCRIPT=$(basename "$0")
PYLINT="$(command -v pylint 2>/dev/null || true)"
RADON="$(command -v radon 2>/dev/null || true)"
PYLINT_ARGS="--output-format=parseable"
RADON_ARGS='cc --min C --no-assert --show-closures --show-complexity --average'

trap "status=\$?; cd '$GIT_REPO'; rm -rf '$TMP_REPO'; exit \$status" EXIT
mkdir -p "$CACHE_DIR"


print_help ()
{
    echo "
Usage: $SCRIPT [TEST_COMMIT | -h]

This script will compare pylint error count from two different commits.
Note: all changes that are not committed will be ignored.

The script will work only if the current commit is a merge commit, or if the
second test_commit argument is provided.

Given the commit tree:

       D---E---F---G---H
            \\         /
             A---B---C

- Running '$SCRIPT' on H will check the diff between G and H.
- Running '$SCRIPT F' on H will check the diff between F and H.
- Running '$SCRIPT F' on C will check the diff between E and C. The E commit is
  set by the merge base of the current head and the specified commit F.
"
    exit 0
}

case $ARG1 in -h|--help) print_help ; esac

if [ ! "$PYLINT$RADON" ]; then
    echo 'Error: pylint and/or radon required'
    exit 3
fi

# Make a local clone: prevents copying of objects
# Handle shallow git clones
is_shallow=$([ -f "$GIT_REPO/.git/shallow" ] && echo true || echo)
if [ "$is_shallow" ]; then
    mv "$GIT_REPO/.git/shallow" "$GIT_REPO/.git/shallow-bak"
fi
git clone -q --local --depth=50 "$GIT_REPO" "$TMP_REPO"  2>/dev/null
if [ "$is_shallow" ]; then
    mv "$GIT_REPO/.git/shallow-bak" "$GIT_REPO/.git/shallow"
    cp "$GIT_REPO/.git/shallow" "$TMP_REPO/.git/shallow"
fi

# Move over any modified but uncommited files ...
if ! git diff-index --quiet HEAD; then
    git stash save -q --keep-index
    git stash show -p stash@\{0\} > "$UNCOMMITED_PATCH"
    git stash pop -q --index
fi

cd "$TMP_REPO"

# ... and commit them
if [ "$(cat "$UNCOMMITED_PATCH" 2>/dev/null || true)" ]; then
    git apply "$UNCOMMITED_PATCH"
    git commit -a -m 'Commit changed files'
    was_dirty='+'
fi >/dev/null 2>&1

git reset --hard -q HEAD

CURRENT_COMMIT=$(git rev-parse HEAD)
if [ "$ARG1"  ]; then
    PREVIOUS_COMMIT=$(git merge-base HEAD "$ARG1")
else
    PREVIOUS_COMMIT=$(git show --pretty=raw HEAD |
                      awk '/^parent /{ print $2; exit }')
fi

echo
echo "Comparing commits ${CURRENT_COMMIT:0:10}${was_dirty:-} and ${PREVIOUS_COMMIT:0:10}"

CHANGED_FILES=$(git diff --name-only $CURRENT_COMMIT $PREVIOUS_COMMIT |
                grep "\.py$" || true )
[ ! "$(command -v md5sum 2>/dev/null)" ] && md5sum() { md5; }  # for OS X
CHANGED_FILES_HASH=$(echo "$CHANGED_FILES" | md5sum | cut -d ' ' -f 1)
if [ ! "$CHANGED_FILES" ]; then
    echo "No python files changed. Skipping lint checks."
    exit 0
fi

echo
echo "Comparing files"
echo "==============="
echo "$CHANGED_FILES"
echo

# Run pylint on the old and new code, to compare the quality.
# If pylint is run multiple times it will store the previous results and show
# the change in quality with a non-negative number if code was improved or not
# changed, and a negative number if more code issues have been introduced.

checkout ()
{
    { git checkout -q "$1"
      git reset --hard -q HEAD
    } 2>/dev/null
}

Number_of_issues ()
{
    cached="$1"
    {   cat "$cached" 2>/dev/null ||
        echo "$CHANGED_FILES" |
            xargs "$PYLINT" $PYLINT_ARGS |
            tee "$cached"
    } | awk -F'[\\. ]' '/^Your code has been rated at /{ print $7 }' || true
}

Cyclomatic_complexity ()
{
    cached="$1"
    {   cat "$cached" 2>/dev/null ||
        echo "$CHANGED_FILES" |
            xargs "$RADON" $RADON_ARGS |
            tee "$cached"
    } | awk -F'[()]' '/ .+\([0-9]+\)$/ { tot += $2 } END { print tot }' || true
}

Get_diffable ()
{
    sed -E "/$diff_block_end/,\$d" |
        sort |
        sed -E "s/$match_line_num/$replace_line_num/"
}

for check in \
    'Pylint,Number_of_issues,^Report$,^([^:]+:)[0-9]+:,\\1,^\\+' \
    'radon,Cyclomatic_complexity,^[0-9]+ blocks,^( +[MCF]) [0-9:]+,\\1:,^[+-]'
do
    IFS=',' read check              \
                 func               \
                 diff_block_end     \
                 match_line_num     \
                 replace_line_num   \
                 show_diff_lines    < <(echo "$check")
    # If command not available, skip it
    if [ ! "$(eval echo \$$(echo $check | tr '[:lower:]' '[:upper:]') )" ]; then
        continue
    fi

    cached_previous="$CACHE_DIR/previous.$check.$PREVIOUS_COMMIT.$CHANGED_FILES_HASH"
    cached_current="$CACHE_DIR/current.$check.$CURRENT_COMMIT.$CHANGED_FILES_HASH"
    [ -f "$cached_previous" ] || rm -r "$CACHE_DIR/previous."* 2>/dev/null || true
    [ -f "$cached_current" ] || rm -r "$CACHE_DIR/current."* 2>/dev/null || true

    [ -f "$cached_previous" ] || checkout $PREVIOUS_COMMIT
    RESULT_PARENT=$($func "$cached_previous")
    [ -f "$cached_current" ] || checkout $CURRENT_COMMIT
    RESULT_CURRENT=$($func "$cached_current")

    echo
    echo "$check result"
    echo "================================================================="
    cat "$cached_current"
    echo

    echo
    echo "$check diff"
    echo "================================================================="
    diff --unified=0 --minimal                   \
            <(Get_diffable < "$cached_previous") \
            <(Get_diffable < "$cached_current")  |
        grep -E "$show_diff_lines" | tail -n +3 || true
    echo

    echo
    echo "$check results"
    echo "================================================================="
    echo "${func//_/ } on parent commit: $RESULT_PARENT"
    echo "${func//_/ } on the pull request: $RESULT_CURRENT ($(printf "%+d" $((RESULT_CURRENT - RESULT_PARENT))))"
    echo

    if awk "BEGIN { exit ${RESULT_CURRENT:-0} > ${RESULT_PARENT:-0} ? 0 : 1 }"; then
        echo "FAIL: ${func//_/ } got worse"
        exit 1
    fi
done

echo "OK"
