#!/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.8                                                          #
#                                                                              #
# 'gws' is the abbreviation of 'Git WorkSpace'.                                #
# This is an helper to manage workspaces which contain git repositories.       #
################################################################################

# {{{ License

# The MIT License (MIT)
#
# Copyright (c) 2015 Fabien Dubosson
#
# 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

# }}}

# {{{ Parameters

# Version number
VERSION="0.1.8"

# 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"

# 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 identify git unlisted repositories
GIT_FOLDER=".git"

# Indentation for status display
INDENT="    "

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

# Status command
S_NONE=0
S_FETCH=1
S_FAST_FORWARD=2

# Colors
if [[ -t 1 ]]; then
    C_RED="\e[91m"
    C_GREEN="\e[92m"
    C_YELLOW="\e[93m"
    C_BLUE="\e[94m"
    C_MAGENTA="\e[95m"
    C_CYAN="\e[96m"
    C_WHITE="\e[97m"
    C_OFF="\e[0m"
else
    C_RED=""
    C_GREEN=""
    C_YELLOW=""
    C_BLUE=""
    C_MAGENTA=""
    C_CYAN=""
    C_WHITE=""
    C_OFF=""
fi

# }}}

# {{{ Variables declarations

# Associative array containing the projects
declare -A projects

# List of sorted projects index of the associative array
declare -a projects_indexes

# Array containing the ignored patterns
declare -a ignored_patterns

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

# }}}

# {{{ General functions

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

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

    return $in
}

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

    # Reconstruct array
    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

        # Look for prefix
        for b in "${set_b[@]}"
        do
            [[ $a =~ $b ]] && ok=1 && break
        done

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

    return 0
}

# Remove elements from a list that have as prefix another element of the same list
# Used to remove subrepositories, e.g. the list ( foo/bar/ foo/ ). The element
# foo/bar/ has for prefix foo/, so removing foo/bar because it is a subrepository
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
            b=$(sed -e 's/[]\/()$*.^|[]/\\&/g' <<< "$b")
            [[ $a =~ ^$b.+ ]] && ok=1 && break
            [[ "$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 )/
    limit_to=$(sed -e 's/[]\/()$*.^|[]/\\&/g' <<< "$limit_to")

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

        # If it match, 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 workspace?
function is_project_root()
{
    # If there is a project 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 failure. Must never be reached... normally
    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()
{
    # Store cache state
    CACHED_PROJECTS_HASH=$(md5sum "${PROJECTS_FILE}" 2>/dev/null || echo NONE)
    sed -i '/^declare -- CACHED_PROJECTS_HASH=/d' "${CACHE_FILE}"
    declare -p CACHED_PROJECTS_HASH >> "${CACHE_FILE}"

    # Clear previous cache state
    sed -i '/^declare -A projects=/d' "${CACHE_FILE}"
    sed -i '/^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 line
    do
        # Remove inline comments
        line=$(sed -e 's/#.*$//' <<< "$line")

        # We get the directory
        dir=$(cut -d${FIELD_SEP} -f1 <<< "$line" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')
        # We get the rest of the configuration line containing remotes
        remotes=$(cut -d${FIELD_SEP} -f1 --complement <<< "$line")

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

            # If name its not set we correct it
            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_RED}The URL at position $count for \"$dir\" is missing a name.${C_OFF}"
                    echo -e "$error_msg"
                    exit 1
                fi
            fi

            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)

    # Cache the result
    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()
{
    # Store cache state
    CACHED_IGNORE_HASH=$(md5sum "${IGNORE_FILE}" 2>/dev/null || echo NONE)
    sed -i '/^declare -- CACHED_IGNORE_HASH=/d' "${CACHE_FILE}"
    declare -p CACHED_IGNORE_HASH >> "${CACHE_FILE}"

    # Remove previous version in cache
    sed -i '/^declare -a ignored_patterns=/d' "${CACHE_FILE}"
    ignored_patterns=()

    [[ -e "$IGNORE_FILE" ]] || return 0

    local pattern

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

        # Check if this is an empty pattern: continue
        [[ -z $pattern ]] && continue

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

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

    # Cache the results
    if [[ ! ${#ignored_patterns[@]} -eq 0 ]]; then
        declare -p ignored_patterns >> "${CACHE_FILE}"
    fi

    return 0
}

# Get the repo url from associative array values
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 URL
    cut -d${FIELD_SEP} -f2 <<< ${array[${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 from 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" )

    # 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" )

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

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

    return 0
}

# Check for changes not commited
function git_check_uncached_uncommited()
{
    local cmd

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

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

    # Otherwise return failure
    return 1
}

# Check for changes not commited but cached
function git_check_cached_uncommited()
{
    local cmd

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

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

    # Otherwise return failure
    return 1
}

# Check for changes not commited but cached
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 return success
    [[ $nb -eq 0 ]] && return 0

    # Otherwise return failure
    return 1
}

# Check for changes not commited
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... exiting
    local_hash=$(cd "$1"; "${local_cmd[@]}" 2>/dev/null) || return 3

    # Execute the command to get the remote hash. If it fails, that mean 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"

    # Check if already a workspace
    [[ -f ${PROJECTS_FILE} ]] && echo -e "${C_RED}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
        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_GREEN}Workspace file «${PROJECTS_FILE}» created.${C_OFF}" && return 0

    # Display informations messages
    echo -e "${C_YELLOW}No repository found.${C_OFF}"
    return 1
}

# Update command
function cmd_update()
{
    local dir repo remote remote_name remote_url

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

        # Print the repository
        echo -e "${C_BLUE}$dir${C_OFF}:"

        # Check if repository already exists, and continue if it is the case
        if [ -d "$dir" ]; then
            # Print the information
            printf "${INDENT}%-${MBL}s${C_GREEN} %s${C_OFF} " " " "Already exists"
        elif [[ -z $repo ]]; then
            # Print the information
            printf "${INDENT}%-${MBL}s${C_RED} %s${C_OFF} " " " "No URL defined"
        fi

        # Print information for local only repositories
        [[ -z $repo ]] && echo -e "${C_BLUE}[Local only repository]${C_OFF}" && continue

        # Finish the newline
        printf "\n"

        # Next repository if already existing
        if [[ ! -d "$dir" ]]; then

            # Print the information
            printf "${INDENT}%-${MBL}s${C_CYAN} %s${C_OFF}\n" " " "Cloning…"

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

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

        # Verify all remotes, create if not existing
        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
}

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

    uptodate=1

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

        # Print the project name
        echo -e "${C_BLUE}$dir${C_OFF}:"

        # Check if repository already exists, and continue if it is not the case
        if [ ! -d "$dir" ]; then
            printf "${INDENT}%-${MBL}s${C_YELLOW} %s${C_OFF} " " " "Missing repository"
            [[ -z $repo ]] && echo -e "${C_BLUE}[Local only repository]${C_OFF}"
            printf "\n"
            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 not commited not cached changes
        if ! git_check_uncached_uncommited "$dir"; then
            printf "${INDENT}${C_MAGENTA}%-${MBL}s${C_OFF} " "$display_current"
            echo -ne "${C_RED}Dirty (Uncached changes)${C_OFF} "
            branch_done=1
            uptodate=0
            printed=1
        # Check for not commited changes
        elif ! git_check_cached_uncommited "$dir"; then
            printf "${INDENT}${C_MAGENTA}%-${MBL}s${C_OFF} " "$display_current"
            echo -ne "${C_RED}Dirty (Uncommitted changes)${C_OFF} "
            branch_done=1
            uptodate=0
            printed=1
        # Check for untracked files
        elif ! git_check_untracked "$dir"; then
            printf "${INDENT}${C_MAGENTA}%-${MBL}s${C_OFF} " "$display_current"
            echo -ne "${C_RED}Dirty (Untracked files)${C_OFF} "
            branch_done=1
            uptodate=0
            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 uncommited changes.
        elif [[ -z $repo ]]; then
            printf "${INDENT}${C_MAGENTA}%-${MBL}s${C_OFF} " "$display_current"
            echo -ne "${C_GREEN}Clean${C_OFF} "
            printed=1
        fi

        # Add special information for local only repositories
        if [[ -z $repo ]]; then
            echo -e "${C_BLUE}[Local only repository]${C_OFF}"
            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
        [[ 0 -eq ${#branches[@]} ]] && printf "${INDENT}%-${MBL}s${C_YELLOW} %s${C_OFF}\n" " " "Empty repository"

        # Fetch origin
        [[ $1 -eq $S_FETCH ]] && git_fetch "$dir" && printf "${INDENT}%-${MBL}s${C_CYAN} %s${C_OFF}\n" " " "Fetched from origin"

        # 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
            [[ $branch_done -eq 1 ]] && [ "$branch" = "$current" ] && continue

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

            # Check for not consistant branches
            git_check_branch_origin "$dir" "$branch";

            # Get the return of the function
            rc=$?

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

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

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

            # Otherwise
            else
                printf "${INDENT}${C_MAGENTA}%-${MBL}s${C_OFF} " "$display_branch :"
                echo -en "${C_GREEN}Clean${C_OFF}"

            fi

            # Print after informations
            echo -en "${after}"
        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
    found=( $(find ./* -type d -name "$GIT_FOLDER" | sed -e "s#/${GIT_FOLDER}\$##" | cut -c 3- | sort) )
    found=( $(remove_prefixed found[@]) )
    all=( "${found[@]}" "${projects_all_indexes[@]}" )
    readarray -t all < <(for a in "${all[@]}"; do echo "$a"; done | sort -u)

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

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

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

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

    return 0
}

# Display the usage of this program
function usage()
{
    echo -e "gws is an helper to manage workspaces which contain git repositories."
    echo -e ""
    echo -e "Usages: ${C_RED}$(basename "$0")${C_OFF} ${C_BLUE}<command>${C_OFF} [${C_GREEN}<directory>${C_OFF}]"
    echo -e "        ${C_RED}$(basename "$0")${C_OFF} [${C_GREEN}<directory>${C_OFF}]"
    echo -e ""
    echo -e "where ${C_BLUE}<command>${C_OFF} is:"
    echo -e "    ${C_BLUE}init${C_OFF}   - Detect the repositories and create the projects list"
    echo -e "    ${C_BLUE}update${C_OFF} - Update the workspace to get new repositories from projects list"
    echo -e "    ${C_BLUE}status${C_OFF} - Print status for all repositories in the workspace"
    echo -e "    ${C_BLUE}fetch${C_OFF}  - Print status for all repositories in the workspace, but fetch the origin before"
    echo -e "    ${C_BLUE}ff${C_OFF}     - Print status for all repositories in the workspace, but fast forward from origin before"
    echo -e "    ${C_BLUE}check${C_OFF}  - Check the workspace for all repositories (known/unknown/missing)"
    echo -e ""
    echo -e "If no ${C_BLUE}<command>${C_OFF} is specified, the command ${C_BLUE}status${C_OFF} is assumed."
    echo -e ""
    echo -e "where ${C_GREEN}<directory>${C_OFF} can be a path to limit the scope of the commands to a specific subfolder"
    echo -e "of the workspace."
    echo -e ""
    exit 1
}

# }}}

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

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

    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 '/^declare -a projects_indexes=/d' "${CACHE_FILE}"
        declare -p projects_indexes >> "${CACHE_FILE}"
    fi

    # If a path is specified as second argument, limit projects to the ones matching
    # the path
    if [[ -n "$2" ]]; then
        error_msg="${C_RED}The directory '$2' is not found.${C_OFF}"
        projects_list=$(keep_prefixed_projects "$2") || (echo -e "$error_msg" && exit 1) || exit 1
        projects_indexes=( ${projects_list} )
    fi
fi


# Finally select the desired command
case $1 in
    "init")
        cmd_init
        ;;
    "update")
        cmd_update
        ;;
    "status")
        cmd_status $S_NONE
        ;;
    "fetch")
        cmd_status $S_FETCH
        ;;
    "ff")
        cmd_status $S_FAST_FORWARD
        ;;
    "check")
        cmd_check
        ;;
    "--version"|"-v")
        echo -e "gws version ${C_RED}$VERSION${C_OFF}"
        ;;
    "--help"|"-h")
        usage
        ;;
    *)
        if [[ -n "$1" ]]; then
            error_msg="${C_RED}The directory '$1' is not found 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
        cmd_status $S_NONE
        ;;
esac

# vim: fdm=marker
