#!/usr/bin/bash
# git-repos.bash
#
# Author: Daniel J. R. May <daniel.may@kada-media.com>
# Version: 0.12, 2022-02-18
#
# Tool to manage multiple git repositories under a given directory.


#############
# Constants #
#############
declare -ir VERBOSITY_QUIET=0
declare -ir VERBOSITY_INFO=1
declare -ir VERBOSITY_VERBOSE=2
declare -ir VERBOSITY_DEBUG=3
declare -ir VERBOSITY_UNDEFINED=4

declare -ir COMMAND_UNDEFINED=0
declare -ir COMMAND_STATUS=1
declare -ir COMMAND_TAG_STATUS=2
declare -ir COMMAND_PULL_ALL=3
declare -ir COMMAND_PUSH_ALL=4

declare -ir EXIT_OK=0
declare -ir EXIT_SYNTAX_ERROR=1
declare -ir EXIT_INTERNAL_ERROR=2
declare -ir EXIT_UNACCESSIBLE_REPO=3


#############
# Variables #
#############
declare -i verbosity=$VERBOSITY_UNDEFINED
declare -i cmd=$COMMAND_UNDEFINED
declare base_dir


#############
# Functions #
#############

function parse_command_line_arguments {
    case $1 in
	status)
	    cmd=$1;;
	tag-status)
	    cmd=$1;;
	pull-all)
	    cmd=$1;;
	push-all)
	    cmd=$1;;
	*)
	    echo "Unknown command $1."
	    exit 1
	    ;;

    esac

    if [[ -n $2 ]]
    then
	base_dir=$(realpath "$2")
    fi
}

# Log debug message to STDOUT in dark grey text if $verbosity allows.
function log_debug {
    if [ $verbosity -ge $VERBOSITY_DEBUG ]
    then
	echo -e "$(tput setaf 8)$1$(tput sgr0)"
    fi
}

# Log verbose message to STDOUT in light grey text if $verbosity allows.
function log_verbose {
    if [ $verbosity -ge $VERBOSITY_VERBOSE ]
    then
	echo -e "$(tput setaf 7)$1$(tput sgr0)"
    fi
}

# Log informational message to STDOUT in standard color if $verbosity
# allows.
function log_info {
    if [ $verbosity -ge $VERBOSITY_INFO ]
    then
	echo -e "$(tput sgr0)$1"
    fi
}

# Log warning message to STDERR in orange.
function log_warning {
    echo -e "$(tput setaf 202)WARNING: $1$(tput sgr0)"  1>&2
}

# Log warning message to STDERR in red.
function log_error {
    echo -e "$(tput setaf 1)ERROR: $1$(tput sgr0)"  1>&2
}

# Execute the status command on the git repository directory given as
# the first argument.
function exec_status {
    log_debug "Executing status on $1"

    if cd "$1"
    then
	log_debug "Changed directory to $1"
    else
	log_warning "Skipping $1 as unable to cd into it."
	return $EXIT_UNACCESSIBLE_REPO
    fi
    
    local git_status
    git_status="$(git status)"
    local clean=true
    
    if [[ $git_status =~ 'Untracked files' ]]
    then
	echo -e "$(tput bold)$(tput setaf 4)Untracked$(tput sgr0)\t$(tput setaf 4)$1$(tput sgr0)"
	clean=false
    fi

    if [[ $git_status =~ 'Changes not staged for commit' ]]
    then
	echo -e "$(tput bold)$(tput setaf 6)Unstaged$(tput sgr0)\t$(tput setaf 6)$1$(tput sgr0)"
	clean=false
    fi

    if [[ $git_status =~ 'Changes to be committed' ]]
    then
	echo -e "$(tput bold)$(tput setaf 11)Uncommitted$(tput sgr0)\t$(tput setaf 11)$1$(tput sgr0)"
	clean=false
    fi

    if [[ $git_status =~ 'Your branch is ahead of' ]]
    then
	echo -e "$(tput bold)$(tput setaf 202)Unpushed$(tput sgr0)\t$(tput setaf 202)$1$(tput sgr0)"
	clean=false
    fi
    
    if [ "$clean" == "true" ] && [[ $git_status =~ 'Your branch is up to date' ]] && [ $verbosity -ge $VERBOSITY_INFO ]
    then	
	echo -e "$(tput bold)$(tput setaf 34)Clean$(tput sgr0)\t\t$(tput setaf 34)$1$(tput sgr0)"
    fi
}

# Execute the status command on the git repository directory given as
# the first argument.
function exec_tag_status {
    log_debug "Executing tag-status on $1"

    if cd "$1"
    then
	log_debug "Changed directory to $1"
    else
	log_warning "Skipping $1 as unable to cd into it."
	return $EXIT_UNACCESSIBLE_REPO
    fi

    local git_log
    git_log=$(git log --decorate=short --max-count=1)

    log_debug "git_log=$git_log"

    local regex='.*tag:\ ([^,]+).*'
    if [[ $git_log =~ $regex ]]
    then
        echo -e "$(tput bold)$(tput setaf 34)Last commit tagged ${BASH_REMATCH[1]}$(tput sgr0)\t$(tput setaf 34)$1$(tput sgr0)"
    else
	echo -e "$(tput bold)$(tput setaf 11)Last commit untagged$(tput sgr0)\t$(tput setaf 11)$1$(tput sgr0)"
    fi
}

# Execute the pull command on the git repository directory given as
# the first argument.
function exec_pull {
    log_debug "Executing pull on $1"

    if cd "$1"
    then
	log_debug "Changed directory to $1"
    else
	log_warning "Skipping $1 as unable to cd into it."
	return $EXIT_UNACCESSIBLE_REPO
    fi

    local git_pull
    
    if  git_pull=$(git pull) 
    then
	log_debug "git_pull=$git_pull"

	if [[ "$git_pull" =~ 'Already up to date' ]] && [ $verbosity -ge $VERBOSITY_INFO ]
	then
	    echo -e "$(tput bold)$(tput setaf 34)Up to date$(tput sgr0)\t$(tput setaf 34)$1$(tput sgr0)"
	elif [[ "$git_pull" =~ 'Updating' ]]
	then
	    echo -e "$(tput bold)$(tput setaf 13)Updated$(tput sgr0)\t$(tput setaf 13)$1$(tput sgr0)"
	fi	
    else
	echo -e "$(tput bold)$(tput setaf 1)Failed$(tput sgr0)\t\t$(tput setaf 1)$1$(tput sgr0)"
    fi
}

# Execute the push command on the git repository directory given as
# the first argument.
function exec_push {
    log_debug "Executing push on $1"

    if cd "$1"
    then
	log_debug "Changed directory to $1"
    else
	log_warning "Skipping $1 as unable to cd into it."
	return $EXIT_UNACCESSIBLE_REPO
    fi

    local git_push

    if  git_push=$(git push --porcelain --verbose) 
    then
	log_debug "git_push=$git_push"

	if [[ "$git_push" =~ 'up to date' ]] && [ $verbosity -ge $VERBOSITY_INFO ]
	then
	    echo -e "$(tput bold)$(tput setaf 34)Up to date$(tput sgr0)\t$(tput setaf 34)$1$(tput sgr0)"
	elif [[ "$git_push" =~ 'Writing objects' ]]
	then
	    echo -e "$(tput bold)$(tput setaf 13)Updated$(tput sgr0)\t$(tput setaf 13)$1$(tput sgr0)"
	fi	
    else
	echo -e "$(tput bold)$(tput setaf 1)Failed$(tput sgr0)\t\t$(tput setaf 1)$1$(tput sgr0)"
    fi
}

# Log error message and exit
function exit_with_duplicate_verbosity_options_error {
    log_error "The -d, -q and -v options are mutually exclusive."
    exit $EXIT_SYNTAX_ERROR
}

# Log error message and exit
function exit_with_duplicate_commands_error {
    log_error "The commands status, tag-status, pull-all and push-all are mutually exclusive."
    exit $EXIT_SYNTAX_ERROR
}

# Log error message and exit
function exit_with_duplicate_base_dirs_error {
    log_error "You can only specify one base directory."
    exit $EXIT_SYNTAX_ERROR    
}

# Log error message and exit
function exit_with_illegal_argument_error {
    log_error "You have specified an illegal option, command or a non-existant directory."
    exit $EXIT_SYNTAX_ERROR        
}

# Log error message and exit
function exit_with_illegal_command_error {
    log_error "Please specify a legal command, one of: pull-all, push-all, status, tag-status."
    exit $EXIT_SYNTAX_ERROR        
}

# Log error message and exit
function exit_with_internal_error {
    log_error "There is a logic error in the program. Please report this."
    exit $EXIT_INTERNAL_ERROR
}

########
# Main #
########

# Parse the command line arugments
for arg in "$@"
do
    case "$arg" in
	-d|--debug)
	    if [ "$verbosity" -eq $VERBOSITY_UNDEFINED ]
	    then
		verbosity=$VERBOSITY_DEBUG
	    else
		exit_with_duplicate_verbosity_options_error
	    fi
	    ;;
	-q|--quiet)
	    if [ "$verbosity" -eq $VERBOSITY_UNDEFINED ]
	    then
		verbosity=$VERBOSITY_QUIET
	    else
		exit_with_duplicate_verbosity_options_error
	    fi
	    ;;
	-v|--verbose)
	    if [ "$verbosity" -eq $VERBOSITY_UNDEFINED ]
	    then
		verbosity=$VERBOSITY_VERBOSE
	    else
		exit_with_duplicate_verbosity_options_error
	    fi
	    ;;
	status)
	    if [ "$cmd" -eq $COMMAND_UNDEFINED ]
	    then
		cmd=$COMMAND_STATUS
	    else
		exit_with_duplicate_commands_error
	    fi
	    ;;
	tag-status)
	    if [ "$cmd" -eq $COMMAND_UNDEFINED ]
	    then
		cmd=$COMMAND_TAG_STATUS
	    else
		exit_with_duplicate_commands_error
	    fi
	    ;;
	pull-all)
	    if [ "$cmd" -eq $COMMAND_UNDEFINED ]
	    then
		cmd=$COMMAND_PULL_ALL
	    else
		exit_with_duplicate_commands_error
	    fi
	    ;;
	push-all)
	    if [ "$cmd" -eq $COMMAND_UNDEFINED ]
	    then
		cmd=$COMMAND_PUSH_ALL
	    else
		exit_with_duplicate_commands_error
	    fi
	    ;;	
	*)
	    if [ -z "$base_dir" ] && [ -d "$arg" ]
	    then
		base_dir=$(realpath "$arg")
	    elif [ -n "$base_dir" ]
	    then
		exit_with_duplicate_base_dirs_error
	    else
		 exit_with_illegal_argument_error
	    fi
	    ;;	      
	esac
done

# Set default verbosity if no verbosity has been specified by the
# command line arguments
if [ "$verbosity" -eq $VERBOSITY_UNDEFINED ]
then
    verbosity=$VERBOSITY_INFO
fi

# Set the default base directory if one has not been specified in the
# command line arguments.
if [ -z "$base_dir" ]
then
    base_dir=$(pwd)
fi

# Output configuration 
log_debug "verbosity=$verbosity"
log_debug "command=$cmd"
log_debug "base_dir=$base_dir"

# Search for .git directories
log_verbose "Searching for git repositories under $base_dir"
dot_git_dirs=$(find "$base_dir" -type d -name .git | sort)
readonly dot_git_dirs

# Iterate over git repositories executing the specified command on
# each one in turn.
for dot_git_dir in $dot_git_dirs
do
    repo_dir=$(dirname "$dot_git_dir")
    log_debug "repo_dir=$repo_dir"

    case $cmd in
	"$COMMAND_STATUS")
	    exec_status "$repo_dir";;
	"$COMMAND_TAG_STATUS")
	    exec_tag_status "$repo_dir";;
	"$COMMAND_PULL_ALL")
	    exec_pull "$repo_dir";;
	"$COMMAND_PUSH_ALL")
	    exec_push "$repo_dir";;
	*)
	    exit_with_illegal_command_error
    esac
done

exit $EXIT_OK
