#!/usr/bin/env bash

################################################################################
# Author:       Fabien Dubosson <fabien.dubosson@gmail.com>                    #
# OS:           (Probably?) All Linux distributions                            #
# Requirements: git, bash > 4.0                                                #
# License:      MIT (See below)                                                #
# Version:      0.1.16                                                         #
#                                                                              #
# 'gws' is the abbreviation of 'Git WorkSpace'.                                #
# This is a helper to manage workspaces composed of git repositories.          #
################################################################################

#-------------------------------------------------------------------------------
#  License
#-------------------------------------------------------------------------------

# The MIT License (MIT)
#
# Copyright (c) 2015 Fabien Dubosson <fabien.dubosson@gmail.com>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.


#-------------------------------------------------------------------------------
#  Bash options
#-------------------------------------------------------------------------------

# Uncomment for Debug.
# set -x

# Propagate fail in pipes.
set -o pipefail

# Unset $CDPATH to avoid paths being printed and breaking functions.
# See: https://github.com/StreakyCobra/gws/pull/18
CDPATH=""


#-------------------------------------------------------------------------------
#  Parameters
#-------------------------------------------------------------------------------

# Version number.
VERSION="0.1.16"

# Starting directory.
START_PWD="$(pwd)"

# Name of the file containing the projects list.
PROJECTS_FILE=".projects.gws"

# Name of the file containing the ignored patterns.
IGNORE_FILE=".ignore.gws"

# Name of the file containing the cache.
CACHE_FILE=".cache.gws"

# Name of the file overriding common colors definition
THEME_FILE_PATH=(
  ".git/theme.gws"
  "${HOME}/.theme.gws"
  "${HOME}/.config/gws/theme"
)

# Field separator in the projects list.
FIELD_SEP='|'

# Array lines separator.
ARRAY_LINE_SEP=', '

# Separator between the URL and its name in config file.
URL_NAME_SEP=' '

# Git name of the origin branch of repositories.
GIT_ORIGIN="origin"

# Git name of the upstream branch of repositories.
GIT_UPSTREAM="upstream"

# Git folder name. Used to detect unlisted git repositories.
GIT_FOLDER=".git"

# Indentation for status display.
INDENT="    "

# Max length of branch names. Used to align information about branches in
# status.
MBL=25

# Command to run when displaying the status.
S_NONE=0
S_FETCH=1
S_FAST_FORWARD=2

# Default colors
C_ERROR="\e[91m"
C_NOT_SYNC="\e[91m"
C_VERSION="\e[91m"
C_HELP_PROGNAME="\e[91m"
C_CLEAN="\e[92m"
C_HELP_DIR="\e[92m"
C_NO_REMOTE="\e[93m"
C_REPO="\e[94m"
C_BRANCH="\e[95m"
C_LOGS="\e[96m"
C_OFF="\e[0m"

function read_theme() {
  # Printing in color.
  if [[ -t 1 ]]; then
    for theme_file in "${THEME_FILE_PATH[@]}"; do
      if [[ -e "$theme_file" ]]; then
        source "$theme_file"
        break
      fi
    done
  else
    # Disable colors if standard out is not a terminal
    C_ERROR=""
    C_NOT_SYNC=""
    C_VERSION=""
    C_HELP_PROGNAME=""
    C_CLEAN=""
    C_HELP_DIR=""
    C_NO_REMOTE=""
    C_REPO=""
    C_BRANCH=""
    C_LOGS=""
    C_OFF=""
  fi
}


#-------------------------------------------------------------------------------
#  Variable declarations
#-------------------------------------------------------------------------------

# Associative array containing projects' information, associated by the key
# available in `projects_indexes`.
declare -A projects

# Array containing projects' names, sorted.
declare -a projects_indexes

# Array containing ignored patterns.
declare -a ignored_patterns

# Array used to transmit the list of branches.
declare -a branches


# Default values for command line options
option_only_changes=false

#-------------------------------------------------------------------------------
#  General functions
#-------------------------------------------------------------------------------

# Check if an array contains a value.
function array_contains()
{
    local seeking=$1; shift
    local in=1
    local element

    for element; do
        if [[ "$element" == "$seeking" ]]; then
            in=0
            break
        fi
    done

    return $in
}

# Remove elements from the first list that match a pattern in the second list.
function remove_matching()
{
    local set_a set_b a b ok

    # Reconstruct input arrays
    declare -a set_a=( "${!1}" )
    declare -a set_b=( "${!2}" )

    # Filter element in `a` that match a pattern in `b`
    for a in "${set_a[@]}"
    do
        ok=0

        # For all prefixes in `b`
        for b in "${set_b[@]}"
        do
            # If `a` matches the prefix, store result and exit the loop
            [[ $a =~ $b ]] && ok=1 && break
        done

        # If it is still okay, print the element
        [[ $ok -eq 0 ]] && echo -n "$a "
    done

    return 0
}

# Remove from a list all elements that have as prefix another element in the
# same list. Used to remove subrepositories, e.g. in the list `( foo/bar/ foo/
# )` the element `foo/bar/` has `foo/` as a prefix, so `foo/bar` is removed
# because it is a subrepository.
# IMPORTANT: The input list must be sorted.
function remove_prefixed()
{
    local set_a a b ok

    # Reconstruct array
    declare -a set_a=( "${!1}" )

    # Filter element that have already a prefix present
    for a in "${set_a[@]}"
    do
        ok=0

        # Look for prefix
        for b in "${set_a[@]}"
        do
            # If `a` matches the prefix, store result and exit the loop
            [[ $a =~ ^$b.+ ]] && ok=1 && break
            # Because input is sorted, we can stop as soon as we are further
            # than the current entry
            [[ "$b" > "$a" ]] && break
        done

        # If it is still okay, print the element
        [[ $ok -eq 0 ]] && echo -n "$a "
    done

    return 0
}

# Keep projects that are prefixed by the given directory.
function keep_prefixed_projects()
{
    local limit_to dir current

    # First check if the folder exists
    [[ ! -d "${START_PWD}/$1" ]] && return 1

    # Get the full path to limit_to in regexp form
    limit_to=$(cd "${START_PWD}/$1" && pwd )/

    # Iterate over each project
    for dir in "${projects_indexes[@]}"
    do
        # Get its full path
        current=$(cd "${PWD}/${dir}/" && pwd )/

        # If it matches, add it to the output
        [[ $current =~ ^$limit_to ]] && echo -n "$dir "
    done

    # Everything is right
    return 0
}


#-------------------------------------------------------------------------------
#  Projects functions
#-------------------------------------------------------------------------------

# Is the current directory the root of a workspace?
function is_project_root()
{

    # If there is a projects file, this is a project root
    (ls "$PROJECTS_FILE" 1>/dev/null 2>&1) && return 0

    # If we reach root, and there is no projects file, exit with an error
    # message
    [[ $(pwd) = "/" ]] && echo "Not in a workspace" && exit 1

    # Otherwise return false.
    return 1
}

# Add a project to the list of projects.
function add_project()
{
    # Add the project to the list
    projects[$1]="$2"

    return 0
}

# Check if the project exists in the list of projects
function exists_project()
{
    array_contains "$1" "${projects_indexes[@]}"
}

# Read the list of projects from the projects list file
function read_projects()
{
    # Replace the hash of PROJECTS_FILE in the cache
    CACHED_PROJECTS_HASH=$(md5sum "${PROJECTS_FILE}" 2>/dev/null || echo NONE)
    sed -i -e '/^declare -- CACHED_PROJECTS_HASH=/d' "${CACHE_FILE}"
    declare -p CACHED_PROJECTS_HASH >> "${CACHE_FILE}"

    # Remove arrays from the cache
    sed -i -e '/^declare -A projects=/d' "${CACHE_FILE}"
    sed -i -e '/^declare -a projects_indexes=/d' "${CACHE_FILE}"
    projects=()
    projects_indexes=()

    local line dir remotes count repo remotes_list

    # Read line by line (discard comments and empty lines)
    while read -r line
    do
        # Remove inline comments
        line=$(sed -e 's/#.*$//' <<< "$line")

        # Get the directory
        dir=$(cut -d${FIELD_SEP} -f1 <<< "$line" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')

        # Get the rest of the configuration line containing remotes
        remotes=$(sed -E -e "s/^[^${FIELD_SEP}]*\\${FIELD_SEP}?//" <<< "$line")

        # Iterate over all the remotes
        count=0
        remotes_list=""
        while [ -n "$remotes" ];
        do
            count=$((count + 1))
            # Get the first remote defined in the "remotes" variable
            remote=$(cut -d${FIELD_SEP} -f1 <<< "$remotes" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' -e 's/[[:space:]]\+/ /g')
            # Remove the current remote from the line for next iteration
            remotes=$(sed -E -e "s/^[^${FIELD_SEP}]*\\${FIELD_SEP}?//" <<< "$remotes")
            # Get its url
            remote_url=$(cut -d"${URL_NAME_SEP}" -f1 <<< "$remote" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
            # Get its name, if any
            remote_name=$(cut -d"${URL_NAME_SEP}" -f2 -s <<< "$remote")

            # If name is not set we infer it as:
            # 1st: origin
            # 2nd: upstream
            # Else crash
            if [[ -z "$remote_name" ]]; then
                if [[ $count == 1 ]]; then
                    remote_name=$GIT_ORIGIN
                elif [[ $count == 2 ]]; then
                    remote_name=$GIT_UPSTREAM
                else
                    error_msg="${C_ERROR}The URL at position ${count} for \"${dir}\" is missing a name.${C_OFF}"
                    echo -e "$error_msg"
                    exit 1
                fi
            fi

            # Store the current remote in the list of remotes
            remotes_list+="${remote_name}${FIELD_SEP}${remote_url}${ARRAY_LINE_SEP}"
        done

        # Skip if the dir is empty
        [ -z "${dir}" ] && continue

        # Otherwise add the project to the list
        add_project "${dir}" "${remotes_list}"
    done < <(grep -v "^#\|^$" $PROJECTS_FILE)

    # Extract sorted index of projects
    readarray -t projects_indexes < <(for a in "${!projects[@]}"; do echo "$a"; done | sort)

    # Save the result in the cache
    if [[ ! ${#projects[@]} -eq 0 ]]; then
        declare -p projects >> "${CACHE_FILE}"
        declare -p projects_indexes >> "${CACHE_FILE}"
    fi

    return 0
}

# Read the list of ignored patterns from the file
function read_ignored()
{
    # Replace the hash of IGNORE_FILE in the cache
    CACHED_IGNORE_HASH=$(md5sum "${IGNORE_FILE}" 2>/dev/null || echo NONE)
    sed -i -e '/^declare -- CACHED_IGNORE_HASH=/d' "${CACHE_FILE}"
    declare -p CACHED_IGNORE_HASH >> "${CACHE_FILE}"

    # Remove array from the cache
    sed -i -e '/^declare -a ignored_patterns=/d' "${CACHE_FILE}"
    ignored_patterns=()

    # If ignore file is empty, skip the rest
    [[ -e "$IGNORE_FILE" ]] || return 0

    local pattern

    # Read line by line
    while read -r pattern
    do
        # Remove inline comments
        pattern=$(sed -e 's/#.*$//' <<< "$pattern")

        # Go to next pattern if this pattern is empty
        [[ -z $pattern ]] && continue

        # Escape regex characters
        pattern=$(sed -e 's/[/&]/\\&/g' <<< "$pattern")

        # Add it to the list of ignored patterns
        ignored_patterns+=( "$pattern" )
    done < <(grep -v "^#\|^$" $IGNORE_FILE)

    # Save the result in the cache
    if [[ ! ${#ignored_patterns[@]} -eq 0 ]]; then
        declare -p ignored_patterns >> "${CACHE_FILE}"
    fi

    return 0
}

# Get the url of a repository from an associative array
function get_repo_url()
{
    local remote remote_name remote_url
    declare -A assoc

    # Read the projects info
    IFS=${ARRAY_LINE_SEP} read -a array <<< "$1"

    # Check if origin is present
    for remote in "${array[@]}";
    do
        remote_name=$(cut -d${FIELD_SEP} -f1 <<< "${remote}")
        remote_url=$(cut -d${FIELD_SEP} -f2 <<< "${remote}")
        assoc["${remote_name}"]="${remote_url}"
    done

    [ "${assoc[${GIT_ORIGIN}]+isset}" ] || return 1

    # Return the origin URL
    cut -d${FIELD_SEP} -f2 <<< "${assoc[${GIT_ORIGIN}]}"

    return 0
}


#-------------------------------------------------------------------------------
#  Git functions
#-------------------------------------------------------------------------------

# Clone a repository
function git_clone()
{
    local cmd

    # Git command to execute
    cmd=( "git" "clone" "$1" "$2" )

    # Run the command and print the output in case of error
    if ! output=$("${cmd[@]}" 2>&1); then
        echo "$output"
        return 1
    fi

    return 0
}

# Fetch from the origin
function git_fetch()
{
    local cmd

    # Git command to execute
    cmd=( "git" "fetch" )

    # Execute the command
    if ! output=$(cd "$1" && "${cmd[@]}" 2>&1); then
        return 1
    fi

    if [ -z "$output" ] ; then
        return 1
    fi

    return 0
}

# Fetch from the origin and update ref at same time
function git_fetch_update()
{
    local cmd

    # Git command to execute
    cmd=( "git" "fetch" "${GIT_ORIGIN}" "$2:$2")

    # Execute the command
    if ! output=$(cd "$1" && "${cmd[@]}" 2>&1); then
        return 1
    fi

    if [ -z "$output" ] ; then
        return 1
    fi

    return 0
}

# Fast-forward to the origin
function git_fast_forward()
{
    local cmd

    # Git command to execute
    cmd=( "git" "pull" "--ff-only" )

    # Execute the command
    if ! output=$(cd "$1" && "${cmd[@]}" 2>&1); then
        return 1
    fi

    if [ "$output" = "Already up-to-date." ] ; then
        return 1
    fi

    return 0
}

# Add an upstream branch to a repository
function git_add_remote()
{
    local cmd

    # Git command to execute
    cmd=( "git" "remote" "add" "$2" "$3")

    # Run the command and print the output in case of error
    if ! output=$(cd "$1" && "${cmd[@]}"); then
        echo "$output"
        return 1
    fi

    return 0
}

# Get a remote url
function git_remote_url()
{
    local cmd

    # Git command to execute
    cmd=( "git" "remote" "-v" )

    # Run the command and print the output
    (cd "$1" && "${cmd[@]}" | grep "$2" | head -n 1 | cut -d' ' -f1 | cut -d'	' -f 2 | tr -d ' ')

    return 0
}

# Get the list of remotes
function git_remotes()
{
    local cmd

    # Git command to execute
    cmd=( "git" "remote" )

    # Run the command and print the output
    (cd "$1" && "${cmd[@]}")

    return 0
}

# Check if a given remote name exists
function git_remote_exists()
{
    local cmd

    # Git command to execute
    cmd=( "git" "remote" )

    # Run the command
    (cd "$1" && "${cmd[@]}" | grep "^$2\$") > /dev/null 2>&1

    return $?
}

# Get the current branch name
function git_branch()
{
    local cmd

    # Git command to execute
    cmd=( "git" "branch" "--no-color" )

    # Run the command and print the output
    (cd "$1" && "${cmd[@]}" | grep "*" | cut -d'*' -f 2 | tr -d ' ')

    return 0
}

# Get all the branch names, result is passed by global variable
function git_branches()
{
    local cmd output

    # Git command to execute
    cmd=( "git" "branch" "--no-color" )

    # Run the command and get the output
    output=$(cd "$1" && "${cmd[@]}" | cut -d'*' -f 2 | tr -d ' ')

    # Save to the global branches array to be accessed by the caller
    branches=( $output )

    return 0
}

# Check for uncommitted changes
function git_check_uncached_uncommitted()
{
    local cmd

    # Git command to execute
    cmd=( "git" "diff" "--exit-code" )

    # Run the command, and return success if it succeeds
    (cd "$1" && "${cmd[@]}" 1>/dev/null 2>&1) && return 0

    # Otherwise return failure
    return 1
}

# Check for cached but uncommitted changes
function git_check_cached_uncommitted()
{
    local cmd

    # Git command to execute
    cmd=( "git" "diff" "--cached" "--exit-code" )

    # Run the command, and return success if it succeeds
    (cd "$1" && "${cmd[@]}" 1>/dev/null 2>&1) && return 0

    # Otherwise return failure
    return 1
}

# Check for untracked files
function git_check_untracked()
{
    local cmd nb

    # Git command to execute
    cmd=( "git" "status" "--porcelain" )

    # Run the command
    nb=$(cd "$1" && "${cmd[@]}" 2>/dev/null | grep -c "^??")

    # If no untracked files exist, return success
    [[ $nb -eq 0 ]] && return 0

    # Otherwise return failure
    return 1
}

# Check if a local branch points to the same commit as a remote branch
function git_check_branch_origin()
{
    local local_cmd remote_cmd local_hash remote_hash

    # Git commands to execute
    local_cmd=( "git" "rev-parse" "--verify" "$2" )
    remote_cmd=( "git" "rev-parse" "--verify" "${GIT_ORIGIN}/$2" )

    # Execute the command to get the local hash, If it fails this is weird,
    # so... return failure
    local_hash=$(cd "$1"; "${local_cmd[@]}" 2>/dev/null) || return 3

    # Execute the command to get the remote hash. If it fails that means there
    # is no remote branch - return special code
    remote_hash=$(cd "$1"; "${remote_cmd[@]}" 2>/dev/null) || return 2

    # If the hashes are equal, return success
    [ "$local_hash" == "$remote_hash" ] && return 0

    # Otherwise return failure
    return 1
}


#-------------------------------------------------------------------------------
#  Command functions
#-------------------------------------------------------------------------------

# Init command
function cmd_init()
{
    # Go back to start directory
    cd "$START_PWD" || (echo "Initial folder ${START_PWD} doesn't exist any longer" && exit 1)

    # Check if already a workspace
    [[ -f ${PROJECTS_FILE} ]] && echo -e "${C_NOT_SYNC}Already a workspace.${C_OFF}" && return 1

    local found remote
    declare -a found

    # Prepare the list of all existing projects, sorted
    found=( $(find ./* -type d -name "$GIT_FOLDER" | sed -e "s#/${GIT_FOLDER}\$#/#" | cut -c 3- | sort) )
    found=( $(remove_prefixed found[@]) )

    # Create the list of repositories
    output=$(for dir in "${found[@]}"
    do
        dir="${dir%/}"
        echo -n "$dir | $(git_remote_url "$dir" "${GIT_ORIGIN}")"
        for remote in $(git_remotes "$dir");
        do
            [[ "$remote" != "${GIT_ORIGIN}" ]] && echo -n " | $(git_remote_url "$dir" "$remote") $remote"
        done
        echo
    done)

    # Write the file if it is not empty
    [[ ! -z "$output" ]] && (echo "$output" > ${PROJECTS_FILE}) && echo -e "${C_CLEAN}Workspace file «${PROJECTS_FILE}» created.${C_OFF}" && return 0

    echo -e "${C_NO_REMOTE}No repository found.${C_OFF}"
    return 1
}

# Selective clone command
function cmd_clone()
{
    local dir repo remote remote_name remote_url

    if [[ -z "$1" ]]; then
        echo -e "Usage: ${C_HELP_PROGNAME}$(basename "$0")${C_OFF} ${C_REPO}clone${C_OFF} ${C_HELP_DIR}<directory>...${C_OFF}"
        return 1
    fi

    # For all projects
    for dir in "$@"
    do
        # Get information about the current project
        repo=$(get_repo_url "${projects[$dir]}")

        # Print the repository
        local project_name_printed=0
        local after=""
        local skip_clone=0

        # Print information for local only repositories
        if [[ -z $repo ]]; then
            print_project_name_unless_done_already "$dir" $project_name_printed
            project_name_printed=1
            after="${C_REPO}[Local only repository]${C_OFF}"
            skip_clone=1
        fi

        # Check if repository already exists, and continue if it is the case
        if [ -d "$dir" ]; then
            if ! $option_only_changes || [[ -n "$after" ]]; then
                # Print the information
                print_project_name_unless_done_already "$dir" $project_name_printed
                project_name_printed=1
                printf "${INDENT}%-${MBL}s${C_CLEAN} %s${C_OFF} " " " "Already exists"
                skip_clone=1
            fi
        elif [[ -z $repo ]]; then
            # Print the information
            print_project_name_unless_done_already "$dir" $project_name_printed
            project_name_printed=1
            printf "${INDENT}%-${MBL}s${C_NOT_SYNC} %s${C_OFF} " " " "No URL defined"
            skip_clone=1
        fi

        if [[ $project_name_printed -eq 1 ]] || [[ -n "$after" ]]; then
            printf "$after\n"
        fi
        if [[ $skip_clone -eq 1 ]]; then
            continue
        fi

        # Clone repository if missing
        if [[ ! -d "$dir" ]]; then
            print_project_name_unless_done_already "$dir" $project_name_printed
            project_name_printed=1

            printf "${INDENT}%-${MBL}s${C_LOGS} %s${C_OFF}\n" " " "Cloning…"

            # Clone the repository
            if ! git_clone "$repo" "$dir"; then
                printf "${INDENT}%-${MBL}s${C_ERROR} %s${C_OFF}\n" " " "Error"
                return 1
            fi

            printf "${INDENT}%-${MBL}s${C_CLEAN} %s${C_OFF}\n" " " "Cloned"
        fi

        # Create any missing remotes
        IFS=${ARRAY_LINE_SEP} read -a array <<< "${projects[$dir]}"
        for remote in "${array[@]}"
        do
            remote_name=$(cut -d${FIELD_SEP} -f1 <<< "${remote}")
            remote_url=$(cut -d${FIELD_SEP} -f2 <<< "${remote}")
            if ! git_remote_exists "${dir}" "${remote_name}"; then
                git_add_remote "${dir}" "${remote_name}" "${remote_url}"
            fi
        done
    done

    return 0
}


# Update command
function cmd_update()
{
    local dir

    for dir in "${projects_indexes[@]}"
    do
        cmd_clone "$dir"
    done

    return 0
}

function print_project_name_unless_done_already() {
    local project_name done_already
    project_name="$1"
    done_already="$2"
    if [[ "$done_already" -eq 0 ]]; then
        # Print the project name
        echo -e "${C_REPO}$project_name${C_OFF}:"
    fi
}

# Status command
function cmd_status()
{
    local dir repo branch branch_done rc uptodate printed

    uptodate=1

    # For each project
    for dir in "${projects_indexes[@]}"
    do
        # Get information about the current project
        repo=$(get_repo_url "${projects[$dir]}")

        # Project name has not been printed yet
        project_name_printed=0

        # Check if repository already exists, and continue if it is not the case
        if [ ! -d "$dir" ]; then
            if ! $option_only_changes; then
              print_project_name_unless_done_already "$dir" $project_name_printed
              printf "${INDENT}%-${MBL}s${C_NO_REMOTE} %s${C_OFF} " " " "Missing repository"
              [[ -z $repo ]] && echo -e "${C_REPO}[Local only repository]${C_OFF}"
              printf "\n"
              project_name_printed=1
            fi
            uptodate=0
            continue
        fi

        # Get the current branch name
        current=$(git_branch "$dir")

        # Cut branch name
        if [ ${#current} -gt $((MBL - 3)) ]; then
            display_current="${current:0:$((MBL - 3))}… :"
        else
            display_current="$current :"
        fi
        branch_done=0

        # If there is no "origin" URL defined, don't print branch information (useless)
        [[ -z $repo ]] && display_current=" "

        # Nothing is printed yet
        printed=0

        # Check for uncached and uncommitted changes
        if ! git_check_uncached_uncommitted "$dir"; then
            print_project_name_unless_done_already "$dir" $project_name_printed
            printf "${INDENT}${C_BRANCH}%-${MBL}s${C_OFF} " "$display_current"
            echo -ne "${C_NOT_SYNC}Dirty (Uncached changes)${C_OFF} "
            branch_done=1
            uptodate=0
            printed=1
            project_name_printed=1
        # Check for cached but uncommitted changes
        elif ! git_check_cached_uncommitted "$dir"; then
            print_project_name_unless_done_already "$dir" $project_name_printed
            printf "${INDENT}${C_BRANCH}%-${MBL}s${C_OFF} " "$display_current"
            echo -ne "${C_NOT_SYNC}Dirty (Uncommitted changes)${C_OFF} "
            branch_done=1
            uptodate=0
            printed=1
            project_name_printed=1
        # Check for untracked files
        elif ! git_check_untracked "$dir"; then
            print_project_name_unless_done_already "$dir" $project_name_printed
            printf "${INDENT}${C_BRANCH}%-${MBL}s${C_OFF} " "$display_current"
            echo -ne "${C_NOT_SYNC}Dirty (Untracked files)${C_OFF} "
            branch_done=1
            uptodate=0
            printed=1
            project_name_printed=1
        # If the "origin" URL is not defined in the project list, then no need
        # to check for synchronization. It is clean if there is no untracked,
        # uncached or uncommitted changes.
        elif [[ -z $repo ]]; then
            print_project_name_unless_done_already "$dir" $project_name_printed
            printf "${INDENT}${C_BRANCH}%-${MBL}s${C_OFF} " "$display_current"
            echo -ne "${C_CLEAN}Clean${C_OFF} "
            printed=1
            project_name_printed=1
        fi

        # Add special information for local only repositories
        if [[ -z $repo ]]; then
            print_project_name_unless_done_already "$dir" $project_name_printed
            echo -e "${C_REPO}[Local only repository]${C_OFF}"
            project_name_printed=1
            continue
        fi

        # If something was printed, finish the line
        [[ $printed -eq 1 ]] && printf "\n"

        # List branches of current repository
        git_branches "$dir"

        # If no branches
        if [[ 0 -eq ${#branches[@]} ]]; then
            print_project_name_unless_done_already "$dir" $project_name_printed
            printf "${INDENT}%-${MBL}s${C_NO_REMOTE} %s${C_OFF}\n" " " "Empty repository"
            project_name_printed=1
        fi

        # Fetch origin
        if [[ $1 -eq $S_FETCH ]]; then
            print_project_name_unless_done_already "$dir" $project_name_printed
            git_fetch "$dir" && printf "${INDENT}%-${MBL}s${C_LOGS} %s${C_OFF}\n" " " "Fetched from origin"
            project_name_printed=1
        fi

        # Check for difference with origin
        for branch in "${branches[@]}"
        do
            # Text to display after branch
            after="\n"

            # Cut branch name
            if [ ${#branch} -gt $((MBL - 3)) ]; then
                display_branch="${branch:0:$((MBL - 3))}…"
            else
                display_branch="$branch"
            fi

            # If the branch is already done, skip it
            if [[ $branch_done -eq 1 ]] && [ "$branch" = "$current" ]; then
                continue
            fi

            # Fast forward to origin
            if [[ $1 -eq $S_FAST_FORWARD ]]; then
                # Pull fast forward for current branch
                if [ "$branch" = "$current" ]; then
                    git_fast_forward "$dir" && after=" ${C_LOGS}(fast-forwarded)${C_OFF}${after}"
                # Fetch update for others
                else
                    git_fetch_update "$dir" "$branch" && after=" ${C_LOGS}(fast-forwarded)${C_OFF}${after}"
                fi
            fi

            printed=0

            # Check for diverged branches
            git_check_branch_origin "$dir" "$branch";

            # Get the return code
            rc=$?

            # If the hashes are different
            if [[ "$rc" -eq 1 ]]; then
                print_project_name_unless_done_already "$dir" $project_name_printed
                printf "${INDENT}${C_BRANCH}%-${MBL}s${C_OFF} " "$display_branch :"
                echo -en "${C_NOT_SYNC}Not in sync with ${GIT_ORIGIN}/$branch${C_OFF}"
                uptodate=0
                printed=1
                project_name_printed=1

            # If the remote doesn't exist
            elif [[ "$rc" -eq 2 ]]; then
                print_project_name_unless_done_already "$dir" $project_name_printed
                printf "${INDENT}${C_BRANCH}%-${MBL}s${C_OFF} " "$display_branch :"
                echo -en "${C_NO_REMOTE}No remote branch ${GIT_ORIGIN}/$branch${C_OFF}"
                uptodate=0
                printed=1
                project_name_printed=1

            # If there is no local hash (must never happen... but who knows?)
            elif [[ "$rc" -eq 3 ]]; then
                print_project_name_unless_done_already "$dir" $project_name_printed
                printf "${INDENT}${C_BRANCH}%-${MBL}s${C_OFF} " "$display_branch :"
                echo -en "${C_ERROR}Internal error${C_OFF}"
                uptodate=0
                printed=1
                project_name_printed=1

            # Otherwise it's clean
            else
                if ! $option_only_changes || [[ $printed -eq 1 ]] || [[ $project_name_printed -eq 1 ]] || [[ "$after" != "\n" ]]; then
                    print_project_name_unless_done_already "$dir" $project_name_printed
                    printf "${INDENT}${C_BRANCH}%-${MBL}s${C_OFF} " "$display_branch :"
                    echo -en "${C_CLEAN}Clean${C_OFF}"
                    printed=1
                    project_name_printed=1
                fi
            fi

            # Print any additional info
            if [[ $printed -eq 1 ]]; then
                echo -en "${after}"
            fi
        done
    done

    if [[ $uptodate -eq 0 ]]; then
        exit 1
    fi

    return 0
}

# Verify command
function cmd_check()
{
    local found all repo dir

    declare -a projects_all_indexes
    declare -a projects_ignored
    declare -a found
    declare -a all

    # Create the list of all projects, including ignored ones
    readarray -t projects_all_indexes < <(for a in "${!projects[@]}"; do echo "$a"; done | sort)

    # Create the list of ignored projects only
    readarray -t projects_ignored < <(comm -23 <(for a in "${projects_all_indexes[@]}"; do echo "$a"; done | sort) <(for a in "${projects_indexes[@]}"; do echo "$a"; done | sort))

    # Prepare list of all projects, existing or missing, sorted with no duplicates
    found=( $(find ./* -type d -name "$GIT_FOLDER" | sed -e "s#/${GIT_FOLDER}\$##" | cut -c 3- | sort) )
    all=( "${found[@]}" "${projects_all_indexes[@]}" )
    readarray -t all < <(for a in "${all[@]}"; do echo "$a"; done | sort -u)

    # For each repository
    for dir in "${all[@]}"
    do
        # Print the repository name
        echo -e "${C_REPO}$dir${C_OFF}:"

        # Check if the directory is ignored
        if array_contains "$dir" "${projects_ignored[@]}"; then
            printf "${INDENT}%-${MBL}s${C_LOGS} %s${C_OFF}\n" " " "Ignored"
            continue
        fi

        # Check if the directory exists
        if [ ! -d "$dir" ]; then
            printf "${INDENT}%-${MBL}s${C_NO_REMOTE} %s${C_OFF}\n" " " "Missing"
            continue
        fi

        # Check if it is listed as a project and print result
        if exists_project "$dir"; then
            printf "${INDENT}%-${MBL}s${C_CLEAN} %s${C_OFF}\n" " " "Known"
        else
            printf "${INDENT}%-${MBL}s${C_NOT_SYNC} %s${C_OFF}\n" " " "Unknown"
        fi
    done

    return 0
}

# Display usage help
function usage()
{
    echo -e "gws is a helper to manage workspaces which contain git repositories.

Usages: ${C_HELP_PROGNAME}$(basename "$0")${C_OFF} ${C_REPO}<command>${C_OFF} [${C_HELP_DIR}<directory>${C_OFF}]
        ${C_HELP_PROGNAME}$(basename "$0")${C_OFF} [${C_HELP_DIR}<directory>${C_OFF}]

where ${C_REPO}<command>${C_OFF} is:
    ${C_REPO}init${C_OFF}   - Detect repositories and create the projects list
    ${C_REPO}update${C_OFF} - Clone any repositories in the projects list that are missing in the workspace
    ${C_REPO}clone${C_OFF}  - Selectively clone specific repositories from projects list
    ${C_REPO}status${C_OFF} - Print status for all repositories in the workspace
    ${C_REPO}fetch${C_OFF}  - Print status for all repositories in the workspace, but fetch the origin first
    ${C_REPO}ff${C_OFF}     - Print status for all repositories in the workspace, but fast forward to origin first
    ${C_REPO}check${C_OFF}  - Print difference between projects list and workspace (known/unknown/missing)

where ${C_HELP_DIR}<directory>${C_OFF} can be a path to limit the scope of the commands to a specific subfolder
of the workspace.

If no ${C_REPO}<command>${C_OFF} is specified, the command ${C_REPO}status${C_OFF} is assumed.

The commands ${C_REPO}status${C_OFF}, ${C_REPO}fetch${C_OFF} and ${C_REPO}ff${C_OFF} accept the option
--only-changes before the ${C_HELP_DIR}<directory>${C_OFF}. If given, only repositories with changes will be shown.
"
}



command=status
implicit_command=false
# Identify the desired command
case $1 in
    init|clone|update|status|fetch|ff|check)
        command="$1"
        shift
        ;;
    --version|-v)
        command=version
        shift
        ;;
    --help|-h)
        command=help
        shift
        ;;
    *)
        command=status
        implicit_command=true
esac

while [[ "$1" =~ ^- ]]; do
    case "$1" in
        --only-changes)
            option_only_changes=true
            ;;
        *)
            echo -e "${C_ERROR}Unknown option: $1${C_OFF}"
            exit 1
            ;;
    esac
    shift
done

# Except for the special case of "init" in which there is no projects file
if [[ "$command" != "init" ]] && [[ "$command" != "help" ]]; then
    # First move to the first parent directory containing a projects file
    while ! is_project_root
    do
        cd ..
    done

    read_theme

    # Read the cache if present, otherwise create it
    touch "${CACHE_FILE}"
    [[ -e "${CACHE_FILE}" ]] && source "${CACHE_FILE}"

    # If cache is not up to date, read the projects/ignore files again
    if [[ "$CACHED_PROJECTS_HASH" != "$(md5sum ${PROJECTS_FILE} 2>/dev/null || echo NONE)" ]] ||
       [[ "$CACHED_IGNORE_HASH" != "$(md5sum ${IGNORE_FILE} 2>/dev/null || echo NONE)" ]]; then
        read_projects
        read_ignored

        projects_indexes=( $(remove_matching projects_indexes[@] ignored_patterns[@]) )
        sed -i -e '/^declare -a projects_indexes=/d' "${CACHE_FILE}"
        declare -p projects_indexes >> "${CACHE_FILE}"
    fi
fi

if $implicit_command; then
    if [[ -n "$1" ]]; then
        error_msg="${C_ERROR}The directory '$1' does not exist and is not a recognized command.${C_OFF}"
        projects_list=$(keep_prefixed_projects "$1") || (echo -e "$error_msg" && exit 1) || exit 1
        projects_indexes=( ${projects_list} )
    fi
fi

# If a path is specified as positional argument, limit projects to the ones matching
# the path
if [[ -n "$1" ]]; then
    # But don't error out in the case of "clone", because the directory will probably not exist
    if [[ "$command" != "clone" ]]; then
        error_msg="${C_ERROR}The directory '$1' does not exist.${C_OFF}"
        projects_list=$(keep_prefixed_projects "$1") || (echo -e "$error_msg" && exit 1) || exit 1
        projects_indexes=( ${projects_list} )
    fi
fi

# Finally execute the selected command
case $command in
    init)
        cmd_init
        ;;
    clone)
        cmd_clone "$@"
        ;;
    update)
        cmd_update
        ;;
    status)
        cmd_status $S_NONE
        ;;
    fetch)
        cmd_status $S_FETCH
        ;;
    ff)
        cmd_status $S_FAST_FORWARD
        ;;
    check)
        cmd_check
        ;;
    version)
        echo -e "gws version ${C_VERSION}$VERSION${C_OFF}"
        ;;
    help)
        usage
        ;;
esac

# vim: fdm=marker ts=4 sts=4 sw=4 et
