#!/bin/sh
#
# git-vendor.sh: manage vendored repos via git-subtree
#
# Copyright (c) 2016 Brett Langdon <me@brett.is>
#
_usage()
{
    cat <<EOF
Usage:
  git vendor --help
  git vendor add [--prefix <dir>] <name> <repository> [<ref>]
  git vendor list [<name>]
  git vendor remove <name>
  git vendor update <name> [<ref>]
  git vendor upstream <name> [<ref>] [--repo <repository>]
EOF
}

case "$1" in
    ""|"-h"|"--help") _usage && exit ;;
esac

PATH=$PATH:$(git --exec-path)
. git-sh-setup
require_work_tree

command="$1"
shift
case "$command" in
    "add"|"list"|"remove"|"update"|"upstream") ;;
    *) >&2 echo "error: unknown command \"$command\"" && _usage && exit 1 ;;
esac

prefix="vendor"
if [ "$1" = "--prefix" ]; then
    prefix="$2"
    shift; shift
fi

# Simulate an associative array (for older versions of bash)
var_name()
{
    printf "name_$1" | tr -c '[A-Za-z0-9]' '_'
}

vendor_names_from_log()
{
    name=
    dir=
    git log --grep="^git-vendor-name: .*\$" \
	--pretty=format:'START %H%n%s%n%n%b%nEND%n' HEAD |
	while read a b junk; do
	    case "$a" in
		START) ;;
		git-vendor-name:) name="$b" ;;
                git-vendor-dir:) dir="$b" ;;
		END)
                    # Only consider dependencies which still exist on disk
                    # and haven't been renamed
                    eval `printf val='$'$(var_name "$dir")`
                    if [ -d "$dir" -a -z "$val" ]; then
                        echo "$name"
                        eval `printf $(var_name "$dir")=1`
                    fi
                    name=
                    dir=
                    ;;
	    esac
	done | sort -u
}

vendor_git_log_from_name()
{
    name="$1"
    git log -1 --grep="^git-vendor-name: $name\$" --pretty=format:'START %H%n%s%n%n%b%nEND%n' HEAD
}

vendor_name_from_dir()
{
    name=
    dir="$1"
    git log -1 --grep="^git-vendor-dir: $dir\$" --pretty=format:'START %H%n%s%n%n%b%nEND%n' HEAD |
        while read a b junk; do
            case "$a" in
                git-vendor-name:) name="$b" ;;
                END)
                    if [ -n "$name" ]; then
                        echo "$name"
                    fi
                    name=
                    ;;
            esac
        done
}

cmd_add()
{
    require_clean_work_tree
    name="$1"
    repository="$2"
    ref="master"
    if [ "$3" != "" ]; then
        ref="$3"
    fi
    if [ $# -lt 2 ];
    then
        die "Incorrect options provided: git vendor add <name> <repository> [<ref>]"
    fi

    dir="$prefix/$(echo "$repository" | sed -E 's/^[a-zA-Z]+((:\/\/)|@)//' | sed 's/:/\//' | sed -E 's/\.git$//')"
    message="\
Add \"$name\" from \"$repository@$ref\"

git-vendor-name: $name
git-vendor-dir: $dir
git-vendor-repository: $repository
git-vendor-ref: $ref
"
    set -x
    git subtree add --prefix "$dir" --message "$message" "$repository" "$ref" --squash
}

cmd_list()
{
    showOnly="$1"
    for name in $(vendor_names_from_log);
    do
        vendor_git_log_from_name "$name" |
	    while read a b junk; do
	        case "$a" in
		    START) commit="$b" ;;
                    git-vendor-name:) name="$b" ;;
                    git-vendor-dir:) dir="$b" ;;
                    git-vendor-ref:) ref="$b" ;;
		    git-vendor-repository:) repository="$b" ;;
		    END)
                        if [ ! -z "$repository" ];
                        then
                            if [ -z "$showOnly" -o "$showOnly" = "$name" ]; then
                                printf "$name@$ref:\n"
                                printf "\tname:\t$name\n"
                                printf "\tdir:\t$dir\n"
                                printf "\trepo:\t$repository\n"
                                printf "\tref:\t$ref\n"
                                printf "\tcommit:\t$commit\n"
                                printf "\n"
                            fi
                        fi
                        name=
                        dir=
                        ref=
                        commit=
                        repository=
                        ;;
	        esac
	    done
    done;

}

cmd_update()
{
    require_clean_work_tree
    name="$1"
    ref="master"
    if [ "$2" != "" ]; then
        ref="$2"
    fi
    if [ $# -lt 1 ]; then
        die "Incorrect options provided: git vendor update <name> [<ref>]"
    fi
    vendor_git_log_from_name "$name" |
	while read a b junk; do
	    case "$a" in
		START) ;;
                git-vendor-dir:) dir="$b" ;;
		git-vendor-repository:) repository="$b" ;;
		git-vendor-ref:) curr_ref="$b" ;;
		END)
                    # Make sure the dependency exists on disk
                    if [ ! -d "$dir" ]; then
                        die "Dependency \"$1\" is missing from \"$dir\""
                    fi

                    # And hasn't been renamed
                    logname=$(vendor_name_from_dir "$dir")
                    if [ "$name" != "$logname" ]; then
                        die "Dependency \"$1\" was renamed \"$logname\""
                    fi

                    if [ ! -z "$repository" ];
                    then
                        message="\
Update \"$name\" from \"$repository@$ref\"

git-vendor-name: $name
git-vendor-dir: $dir
git-vendor-repository: $repository
git-vendor-ref: $ref
"
                        git fetch "$repository" "$curr_ref"
                        git subtree pull --prefix "$dir" --message "$message" "$repository" "$ref" --squash
                        break
                    fi
                    ;;
	    esac
	done
}

cmd_upstream()
{
    require_clean_work_tree
    while [ $# -gt 0 ] ; do
        case $1 in
            --repo)
            repository_arg="$2"
            shift # argument value
            ;;
            *)
            if [ -z "$name" ]; then
                name="$1"
            elif [ -z "$ref" ]; then
                ref="$1"
            fi
            ;;
        esac
        shift # current argument
    done
    if [ -z "$name" ]; then
        die "Incorrect options provided: git vendor upstream <name> [<ref>] [--repo <repository>]"
    fi
    if [ -z "$ref" ]; then
        ref="master"
    fi
    vendor_git_log_from_name "$name" |
    while read a b junk; do
        case "$a" in
        START) ;;
        git-vendor-dir:) dir="$b" ;;
        git-vendor-repository:) repository="$b" ;;
        END)
            # Make sure the dependency exists on disk
            if [ ! -d "$dir" ]; then
                die "Dependency \"$1\" is missing from \"$dir\""
            fi

            # And hasn't been renamed
            logname=$(vendor_name_from_dir "$dir")
            if [ "$name" != "$logname" ]; then
                die "Dependency \"$1\" was renamed \"$logname\""
            fi

            if [ ! -z "$repository_arg" ];
            then
                # override the repository read from the commit logs
                # with the one read from the command line arguments
                repository="$repository_arg"
            fi
            
            if [ ! -z "$repository" ];
            then
                git subtree push --prefix "$dir" "$repository" "$ref"
                break
            fi
            ;;
        esac
    done
}

cmd_remove()
{
    require_clean_work_tree
    name="$1"
    if [ $# -lt 1 ]; then
        die "Incorrect options provided: git vendor remove <name>"
    fi
    vendor_git_log_from_name "$name" |
	while read a b junk; do
	    case "$a" in
		START) ;;
                git-vendor-dir:) dir="$b" ;;
		END)
                    # Make sure the dependency exists
                    if [ ! -d "$dir" ]; then
                        die "Dependency \"$1\" is missing from \"$dir\""
                    fi
                    # And hasn't been renamed
                    logname=$(vendor_name_from_dir "$dir")
                    if [ "$name" != "$logname" ]; then
                        die "Dependency \"$1\" was renamed \"$logname\""
                    fi

                    git rm -rf "$dir"
                    git commit --message "Removing \"$name\" from \"$dir\""
                    break
                    ;;
	    esac
	done
}

# Run the command
"cmd_$command" "$@"
