#!/usr/bin/env bash
inverse=$(tput rev)
reset=$(tput sgr0)
txtbld=$(tput bold)
bldred=${txtbld}$(tput setaf 1)

# default option settings
guardedmode=false
singlemode=false
allwsmode=false
quiet=false
no_follow_symlinks=false
no_follow_hidden=false

#
# print usage message
#
usage() {
  echo 1>&2 "usage: git bulk [--no-follow-symlinks] [--no-follow-hidden] [-q|--quiet] [-g] ([-a]|[-w <ws-name>]) <git command>"
  echo 1>&2 "       git bulk --addworkspace <ws-name> <ws-root-directory> (--from <URL or file>)"
  echo 1>&2 "       git bulk --removeworkspace <ws-name>"
  echo 1>&2 "       git bulk --addcurrent <ws-name>"
  echo 1>&2 "       git bulk --purge"
  echo 1>&2 "       git bulk --listall"
}

cdfail() {
  echo 1>&2 "failed to change directory: $1"
  exit 1
}

# add another workspace to global git config
addworkspace() {
  git config --global bulkworkspaces."$wsname" "$wsdir";
  if [ -n "$source" ]; then
        if [ ! -d "$wsdir" ]; then echo 1>&2 "Path of workspace doesn't exist, make it first."; exit 1; fi
        regex='http(s)?://|ssh://|(git@)?.*:.*/.*'
        if [[ "$source" =~ $regex ]]; then
            pushd "$wsdir" > /dev/null || cdfail "$wsdir"
            git clone "$source"
            popd > /dev/null || cdfail "$OLDPWD"
        else
            source=$(realpath "$source" 2>/dev/null)
            if [ -f "$source" ]; then
                pushd "$wsdir" > /dev/null || cdfail "$wsdir"
                while IFS= read -r line; do
                  if [ -n "$line" ]; then
                    # the git clone command to take the complete line in the repository.txt as separate argument. This facilitated the cloning of the repository with a custom folder name.
                    # shellcheck disable=SC2086
                    git clone $line;
                  fi
                done < "$source"
                popd > /dev/null || cdfail "$OLDPWD"
            else
                echo 1>&2 "format of URL or file unknown"
            fi
        fi
    fi
}

# add current directory
addcurrent() { git config --global bulkworkspaces."$wsname" "$PWD"; }

# remove workspace from global git config
removeworkspace() { checkWSName && git config --global --unset bulkworkspaces."$wsname"; }

# remove workspace from global git config
purge() { git config --global --remove-section bulkworkspaces; }

# list all current workspace locations defined
listall() { git config --global --get-regexp bulkworkspaces; }

# guarded execution of a git command in one specific repository
guardedExecution () {
  if [ "${quiet?}" != "true" ] || $guardedmode; then
    echo 1>&2 "${bldred}->${reset} executing ${inverse}git $gitcommand${reset} in repository ${leadingpath%/*}/${bldred}${curdir##*/}${reset}"
  fi

  if $guardedmode; then
    echo 1>&2 -n "   Execute command here (y/n)? "
    read -n 1 -r </dev/tty; echo 1>&2
    if [[ $REPLY =~ ^[Yy]$ ]]; then
      git "$@"
    fi
  else
    git "$@"
  fi
}

# check if the passed command is known as a core git command
checkGitCommand () {
  if git help -a | grep -o -q "\b${corecommand}\b"; then
    echo 1>&2 "Core command \"$corecommand\" accepted."
  else
    if git config --get-regexp alias | grep -o -q "\.${corecommand} "; then
      echo 1>&2 "Alias ${corecommand} accepted."
    else
      usage && echo 1>&2 "error: unknown GIT command: $corecommand" && exit 1
    fi
  fi
}

# check if workspace name is registered
checkWSName () {
  while read -r workspace; do
    parseWsName "$workspace"
    if [[ $rwsname == "$wsname" ]]; then return; fi
  done <<< "$(listall)"
  # when here the ws name was not found
  usage && echo 1>&2 "error: unknown workspace name: $wsname" && exit 1
}

# parse out wsname from workspacespec
parseWsName () {
  local wsspec="$1"
  # Get the workspace value from its specification in the `.gitconfig`.
  # May be an absolute path or a variable name of the form: `$VARNAME`
  rwsdir=${wsspec#* }
  if [[ ${rwsdir:0:1} == '$' ]]; then
    # Dereference the `rwsdir` value which is a variable name.
    rwsdir_varname=${rwsdir:1}
    rwsdir=${!rwsdir_varname}
    if [[ -z "${rwsdir}" ]]; then
      echo 1>&2 "error: bad environment variable: $rwsdir_varname" && exit 1
    fi
  fi
  rwsname=${wsspec#*.} && rwsname=${rwsname%% *}
}

# detects the wsname of the current directory
wsnameToCurrent () {
  while read -r workspace; do
    if [ -z "$workspace" ]; then continue; fi
    parseWsName "$workspace"
    if echo "$PWD" | grep -o -q "$rwsdir"; then wsname="$rwsname" && return; fi
  done <<< "$(listall)"
  # when here then not in workspace dir
  echo 1>&2 "error: you are not in a workspace directory. your registered workspaces are:" && \
    wslist="$(listall)" && echo 1>&2 "${wslist:-'<no workspaces defined yet>'}" && exit 1
}

# helper to check number of arguments.
allowedargcount () {
	if [ "$paramcount" -ne "${1:-0}"  ] && [ "$paramcount" -ne "${2:-0}" ]; then
		echo 1>&2 "error: wrong number of arguments" && usage;
		exit 1;
	fi
}

# execute the bulk operation
executBulkOp () {
  checkGitCommand
  if ! $allwsmode && ! $singlemode; then wsnameToCurrent; fi # by default git bulk works within the 'current' workspace
  listall | while read -r workspacespec; do
    parseWsName "$workspacespec"
    if [[ -n $wsname ]] && [[ $rwsname != "$wsname" ]]; then continue; fi
    cd "$rwsdir" || exit 1
    local actual=$PWD
    [ "${quiet?}" != "true" ] && echo 1>&2 "Executing bulk operation in workspace ${inverse}$actual${reset}"

    # build `find` flags depending on command-line options
    local find_flags=()
    if [[ "$no_follow_symlinks" == true ]]; then
      find_flags+=(-P)
    else
      find_flags+=(-L)
    fi
    # find all git repositories under the workspace on which we want to operate
    readarray allGitFolders < <(find "${find_flags[@]}" . -name ".git" 2>/dev/null)

    for line in "${allGitFolders[@]}"; do
      local gitrepodir=${line::${#line}-5} # cut the .git part of find results to have the root git directory of that repository
      cd "$gitrepodir" || exit 1 # into git repo location
      local curdir=$PWD
      local leadingpath=${curdir#"${actual}"}
      # do not execute if we do not want to consider a ".git" directory under a hidden directory
      if [ $no_follow_hidden = false ] || ! [[ "$leadingpath" =~ "/." ]]; then
        guardedExecution "$@"
      fi
      cd "$rwsdir" || exit 1 # back to origin location of last find command
    done
  done
}

paramcount="${#}"

# if no arguments show usage
if [[ $paramcount -le 0 ]]; then usage; fi

# parse command parameters
while [ "${#}" -ge 1 ] ; do
  case "$1" in
		--quiet|-q) quiet='true' ;;
    --listall|--purge)
      butilcommand="${1:2}" && break ;;
    --removeworkspace|--addcurrent|--addworkspace)
      butilcommand="${1:2}" && wsname="$2" && wsdir="$3" && if [ "$4" = "--from" ]; then source="$5"; fi && break ;;
    --no-follow-symlinks)
      no_follow_symlinks=true ;;
    --no-follow-hidden)
      no_follow_hidden=true ;;
    -a)
      allwsmode=true ;;
    -g)
      guardedmode=true ;;
    -w)
      singlemode=true && shift && wsname="$1" && checkWSName ;;
    --*)
      usage && echo 1>&2 "error: unknown argument $1" && exit 1 ;;
    -*)
      usage && echo 1>&2 "error: unknown argument $1" && exit 1 ;;
    *) # git core commands
      butilcommand="executBulkOp" && corecommand="$1" && gitcommand="$*" && break ;;
  esac && shift
done

# check option compatibility
if $allwsmode && $singlemode; then echo 1>&2 "error: options -w and -a are incompatible" && exit 1; fi

# if single mode check the supplied workspace name
if $singlemode; then echo 1>&2 "Selected single workspace mode in workspace: $wsname" && checkWSName; fi

# check right number of arguments
case $butilcommand in
  listall|purge) allowedargcount 1;;
  addcurrent|removeworkspace) allowedargcount 2;;
  addworkspace) allowedargcount 3 5;;
esac

# pass the origin arguments to the 'executBulkOp'
$butilcommand "$@" # run user command
