# -*- mode: sh; sh-shell: bash -*-
# vim: set ft=bash:
# shellcheck shell=bash
# shellcheck disable=2016

### Git support library

# prototype: "branch" = "ext git_branch_complete"
# prototype: "git-ls-files-opts" = "extcomp git ls-files --cached --exclude-standard"

## Returns the path to the toplevel git repository.
function git_toplevel
{
    memo git rev-parse --show-toplevel
}

## Returns the path to the bar that is initialized in the git toplevel.
## Fails when there is no bar initialized.
function git_bar
{
    # shellcheck disable=SC2155
    local tl="$(git_toplevel)"
    if [[ -f "$tl/bar" ]]; then
        echo "$tl/bar"
    elif [[ -f "$tl/.bar" ]]; then
        echo "$tl/.bar"
    else
        die "no bar file found in $tl"
    fi
}
memofn git_bar

#git rev-parse --is-inside-work-tree

function git_dir ## Returns the absolute path to the 'GIT_DIR' (.git/).
{
    ## Note that this may be distinct from '$(git_toplevel)/.git/' when worktrees or other git
    ## features are used.
    memo git rev-parse --absolute-git-dir
}

function git_ls_files ## [git-ls-files-opts].. - Sorted list of files in the git repository.
{
    ## git-ls-files '--exclude-standard' is always implied. Use '-c' or '--cached' to list
    ## tracked files, '--others' to show untracked files, or both for all files.
    local zero_terminated=
    local z
    for z in "$@"; do
        [[ "$z" = "-z" ]] && { zero_terminated=true; break; }
    done
    git ls-files --exclude-standard "$@" | sort -u ${zero_terminated:+-z}
}

function git_branch_name ## Returns the branch name that is checked out, dies when not in a git branch.
{
    git branch --show-current
}

function git_branch_complete ## List all git branches for completion
{
    ## This function is called by bar_complete to provide completions for branch parameters.
    git branch --list --format="%(refname:short)" 2>/dev/null
}

function git_branch_find ## [patterns].. - lists branches matching the given patterns
{
   git branch --list --format="%(refname:short)" "$@"
}

function git_branch_find_one ## [patterns].. - lists the matching the given patterns, fails when there are more than one
{
    local branches
    mapfile branches < <(git_branch_find "$@")
    [[ ${#branches[*]} != 1 ]] && return 1
    echo "${branches[0]}"
}

function git_is_ancestor ## [patterns].. - checks if the current branch is a ancestor of the one branch matching the patterns
{
    git merge-base --is-ancestor "$(git_branch_find_one "$@")" "$(git_branch_name)"
}

# TODO: rename git_dir_hash or similar
function git_tree_hash ## [git-ls-files-opts].. - Returns a sha1 hash over the current directory
{
    ## This hash is a identifier used by bar. It is not the same hash git using for storing trees.
    local hash
    # needs || true here to suppress errors on directories
    hash="$(git_ls_files -z "$@" | { xargs -0 sha1sum 2>/dev/null || true ; } | sha1sum)"
    echo "${hash:0:40}"
}

memofn git_tree_hash

function git_add_ignore ## [patterns..] - Adds new ignore patterns to '.gitignore'.
{
    ## Patterns that are already present are skipped.
    [[ -f "$(git_toplevel)/.gitignore" ]] || touch "$(git_toplevel)/.gitignore"

    local pattern
    for pattern in "$@"; do
        if ! grep -F "$pattern" "$(git_toplevel)/.gitignore" >/dev/null; then
            [[ -n "$(tail -c 1 "$(git_toplevel)/.gitignore")" && "$(wc -c "$(git_toplevel)/.gitignore")" != 0 ]] &&
                echo >>"$(git_toplevel)/.gitignore"
            echo "$pattern" >>"$(git_toplevel)/.gitignore"
        fi
    done
}

function git_parse_worktrees ## [main [dirs [branches [heads]]]] - Parses git worktrees into variables provided by the caller
{
    ## - main:     variable storing the main worktree (dir)
    ## - dirs:     associative array storing 'branch:directory'
    ## - branches: associative array storing 'directory:branch'
    ## - heads:    associative array storing 'directory:head'
    iset "${1:-}" ""
    iset "${2:-}"
    iset "${3:-}"
    iset "${4:-}"

    local key value dir head
    while read -r key value; do
        case "$key" in
        worktree)
            dir="$value"
            [[ -n "${1:-}" && -z "${!1}" ]] && iset "$1" "$value"
            ;;
        HEAD)
            head="$value"
            ;;
        branch)
            iset "${2:-}[${value##*/}]" "$dir"
            iset "${3:-}[$dir]" "${value##*/}"
            iset "${4:-}[$dir]" "$head"
            ;;
        esac
    done < <(git worktree list --porcelain)
}

# Define --bare rules for read-only git query functions
rule --bare git_toplevel: -
rule --bare git_bar: -
rule --bare git_dir: -
rule --bare git_ls_files: -
rule --bare git_branch_name: -
rule --bare git_branch_find: -
rule --bare git_branch_find_one: -
rule --bare git_is_ancestor: -
rule --bare git_tree_hash: -


