#!/bin/sh
set -e

usage () {
    echo "usage: git modified [-miuqh] [<commit>] [-- <pathspec>...]" >&2
    echo >&2
    echo "Prints list of files that are locally modified (and exist).  The index is not " >&2
    echo "considered, unless the -i flag is provided." >&2
    echo "" >&2
    echo "If a commit is provided, opens all files that locally exist that have been " >&2
    echo "changed in that commit." >&2
    echo "" >&2
    echo "Pathspecs after '--' restrict the output to matching files." >&2
    echo >&2
    echo "This script is ideal for passing all locally modified files into your editor, like:" >&2
    echo '    $ vim `git modified`' >&2
    echo >&2
    echo "Options:" >&2
    echo "-m    Modified files only (excludes untracked files)" >&2
    echo "-i    Consider the index, too" >&2
    echo "-u    Print only files that are unmerged (files with conflicts)" >&2
    echo "-q    Be quiet, only return with 0 exit code when files are modified" >&2
    echo "-h    Show this help" >&2
}

modified=0
index=0
unmerged=0
quiet=0
commit=""

# Parse flags, optional commit, and `--` pathspec separator manually.
# getopts can't be used here because it consumes `--` and advances OPTIND
# past it, which would prevent us from distinguishing
# `git modified <commit>` from `git modified -- <pathspec>`.
while [ $# -gt 0 ]; do
    case "$1" in
        --)
            shift
            break
            ;;
        -h)
            usage
            exit 2
            ;;
        -*)
            opts="${1#-}"
            while [ -n "$opts" ]; do
                c="${opts%${opts#?}}"
                opts="${opts#?}"
                case "$c" in
                    m) modified=1;;
                    i) index=1;;
                    u) unmerged=1;;
                    q) quiet=1;;
                    h) usage; exit 2;;
                    *) echo "Unknown option: -$c" >&2; exit 2;;
                esac
            done
            shift
            ;;
        *)
            if [ -n "$commit" ]; then
                echo "Unexpected argument: $1 (use '--' to pass pathspecs)" >&2
                exit 2
            fi
            commit="$1"
            shift
            ;;
    esac
done
# $@ now holds zero or more pathspecs

if [ -n "$commit" ]; then
    if ! git rev-parse --verify --quiet "$commit^{commit}" >/dev/null 2>&1; then
        echo "Not a valid commit: $commit" >&2
        if [ -e "$commit" ]; then
            echo "Did you mean: git modified -- $commit" >&2
        fi
        exit 2
    fi
    if [ $modified -eq 1 ] || [ $index -eq 1 ] || [ $unmerged -eq 1 ]; then
        echo "Flags -m/-i/-u cannot be combined with a commit." >&2
        exit 2
    fi
fi

#
# git status cheat sheet:
#
# X          Y     Meaning
# -------------------------------------------------
#           [MD]   not updated
# M        [ MD]   updated in index
# A        [ MD]   added to index
# D         [ M]   deleted from index
# R        [ MD]   renamed in index
# C        [ MD]   copied in index
# [MARC]           index and work tree matches
# [ MARC]     M    work tree changed since index
# [ MARC]     D    deleted in work tree
# -------------------------------------------------
# D           D    unmerged, both deleted
# A           U    unmerged, added by us
# U           D    unmerged, deleted by them
# U           A    unmerged, added by them
# D           U    unmerged, deleted by us
# A           A    unmerged, both added
# U           U    unmerged, both modified
# -------------------------------------------------
# ?           ?    untracked
# !           !    ignored
# -------------------------------------------------

status () {
    if [ $# -gt 0 ]; then
        git status --porcelain -- "$@" | grep -vEe '^.D' | grep -vEe '^D '
    else
        git status --porcelain | grep -vEe '^.D' | grep -vEe '^D '
    fi
}

fix_rename_notation () {
    sed -Ee 's/.* -> (.*)/\1/'
}

make_relative () {
    while read f; do
        git relative-path "$f"
    done
}

modified_in_index () {
    status "$@" | cut -c 4- | fix_rename_notation | make_relative
}

modified_unmerged () {
    status "$@" | grep -Ee '^(U.|.U)' | cut -c 4- | fix_rename_notation | make_relative
}

modified_only_locally () {
    status "$@" | cut -c 2- | grep -Ee '^M' | cut -c 3- | fix_rename_notation | make_relative
}

modified_locally () {
    status "$@" | cut -c 2- | grep -vEe '^[ ]' | cut -c 3- | fix_rename_notation | make_relative
}

fail_if_empty () {
    empty=1
    while read line; do
        if [ $quiet -eq 0 ]; then
            echo "$line"
        fi
        empty=0
    done
    test $empty -eq 0
}

if [ -z "$commit" ]; then
    if [ $unmerged -eq 1 ]; then
        modified_unmerged "$@" | fail_if_empty
    elif [ $index -eq 1 ]; then
        modified_in_index "$@" | fail_if_empty
    elif [ $modified -eq 1 ]; then
        modified_only_locally "$@" | fail_if_empty
    else
        modified_locally "$@" | fail_if_empty
    fi
else
    TAB="	" # literal tab char
    if [ $# -gt 0 ]; then
        git log -1 --name-status --pretty=format:"" "$commit" -- "$@" | cut -f2- | rev | cut -d"$TAB" -f1 | rev | make_relative | while read f; do
            if [ -f "$f" ]; then
                echo "$f"
            fi
        done | fail_if_empty
    else
        git log -1 --name-status --pretty=format:"" "$commit" | cut -f2- | rev | cut -d"$TAB" -f1 | rev | make_relative | while read f; do
            if [ -f "$f" ]; then
                echo "$f"
            fi
        done | fail_if_empty
    fi
fi
