# Display git repo information in a shell prompt
#
# Format:
#
#     [repo:decoration:commit] BARE BISECT/MERGE/REBASE NO-REMOTE/NO-RTB/AHEAD/BEHIND/DIVERGED WIP STASH +±xcru?
#
# Usage:
#
# Call `__gitp` in such a way that prompt expansion happens i.e. from bash's
# `PROMPT_COMMAND` or zsh's `PROMPT`/`pre_cmd()`.
#
function __gitp() {
  # Preserve the exit status of the previous command.
  local exit=$?

  # Only run if git exists.
  [[ -z $(command -v git) ]] && return $exit

  # Try to get git info. Also serves to check if current folder is a git repo.
  local gitinfo ret
  gitinfo=$(git rev-parse \
    --git-dir \
    --is-bare-repository \
    --is-inside-work-tree \
    --short HEAD 2>/dev/null)
  ret="$?"
  [[ -z $gitinfo ]] && return $exit

  # Explode git info into individual variables.
  local gitdir=${gitinfo%%$'\n'*}; gitinfo=${gitinfo#*$'\n'}
  local bare=${gitinfo%%$'\n'*}; gitinfo=${gitinfo#*$'\n'}
  local worktree=${gitinfo%%$'\n'*}; gitinfo=${gitinfo#*$'\n'}
  local commit; [[ $ret = "0" ]] && commit=$gitinfo

  __gitp_cprep

  # Prepare display elements.
  [[ $bare = 'true' ]] && bare=${GITP_BARE:-"$cRed BARE"} || bare=''
  local location="$(__gitp_location $commit)"
  if [[ $worktree = 'true' && -z $GITP_SIMPLE ]]; then
    # Many things only matter if in a work tree (i.e. not bare or in gitdir).
    __gitp_status
    local op="$(__gitp_op $gitdir)"
    local remote="$(__gitp_remote)"
    local wips="$(__gitp_wips $gitdir)"
    local changes="$(__gitp_changes)"
  fi
  # Colour the location if there are local changes.
  local dirty=$(__gitp_dirty)

  # Display the git prompt.
  local formatting=${1:-' %s'}
  printf -- "$formatting" "$dirty$location$bare$op$remote$wips$changes"
  return $exit
}

# Get git status output for parsing.
function __gitp_status() {
  GITP_STATUS=''

  # If available, ignore submodule work tree changes to speed up prompt.
  GITP_STATUS=$(git status --porcelain --ignore-submodules=dirty 2>/dev/null) \
    || GITP_STATUS=$(git status --porcelain)
}

# Initialise colour strings.
function __gitp_cprep() {
  if [[ -n ${BASH_VERSION-} ]]; then
    cStart=${cStart:-'\[\e['}
    cEnd=${cEnd:-'m\]'}
  elif [[ -n ${ZSH_VERSION-} ]]; then
    cStart=${cStart:-'%{['}
    cEnd=${cEnd:-'m%}'}
  fi
  cReset=${cReset:-$cStart'0'$cEnd}
  cRed=$cStart'38;5;1'$cEnd
  cGre=$cStart'38;5;2'$cEnd
  cBlu=$cStart'38;5;4'$cEnd
  cMag=$cStart'38;5;5'$cEnd
  cgGre=$cStart'38;5;70'$cEnd
  cgRed=$cStart'38;5;124'$cEnd
  cgYel=$cStart'38;5;220'$cEnd
}

# Get location i.e. which repo/decoration/commit is this?
function __gitp_location() {
  local prefix=${GITP_PREFIX:-'['}
  local suffix=${GITP_SUFFIX:-"]"}
  local repo=$(__gitp_repo)
  local deco=$(__gitp_branch)
  [[ -z $deco ]] && deco=$(git describe --tags --exact-match 2>/dev/null)
  echo "$prefix$repo:$deco:$1$suffix"
}

# Determine if a repo is in the middle of an "operation" e.g. rebase, bisect.
function __gitp_op() {
  local bisect=${GITP_BISECT:-"$cRed BISECT"}
  local  merge=${GITP_MERGE:-"$cRed MERGE"}
  local rebase=${GITP_REBASE:-"$cRed REBASE"}

  if [[ -e "$1/BISECT_LOG" ]]; then
    echo "$bisect"
  elif [[ -e "$1/MERGE_HEAD" ]]; then
    echo "$merge"
  elif [[ -e "$1/rebase" || -e "$1/rebase-apply" ||  -e "$1/rebase-merge" || -e "$1/../.dotest" ]]; then
    echo "$rebase"
  fi
}

# Compare the local and remote tracking branches (upstream).
function __gitp_remote() {
  local noremote=${GITP_NOREMOTE:-"$cMag NO-REMOTE"}
  local    nortb=${GITP_NORTB:-"$cMag NO-RTB"}
  local    ahead=${GITP_AHEAD:-"$cMag AHEAD"}
  local   behind=${GITP_BEHIND:-"$cMag BEHIND"}
  local diverged=${GITP_DIVERGED:-"$cRed DIVERGED"}
  local remote commits count ahd=0 bhd=0

  # Count commits ahead and behind.
  if commits="$(git rev-list --left-right @{upstream}...HEAD 2>/dev/null)"; then
    [[ -n ${ZSH_VERSION-} ]] && commits=(${(f)commits})
    for commit in $commits; do
      case "$commit" in
        "<"*) ((bhd++)) ;;
        *)    ((ahd++)) ;;
      esac
    done
    count="$bhd  $ahd"
  fi

  # Determine what to display.
  case "$count" in
    "") # no upstream
    if [[ -z "$(git remote -v)" ]]; then
      remote="$noremote"
    elif [[ -n "$(__gitp_branch)" ]]; then
      remote="$nortb"
    fi
    ;;
    "0  0") ;;                       # Equal to upstream.
    "0  "*) remote="$ahead-$ahd" ;;  # Ahead of upstream.
    *"  0") remote="$behind-$bhd" ;; # Behind upstream.
    *)      remote="$diverged" ;;    # Diverged from upstream.
  esac

  echo "$remote"
}

# Warn if the current commit is a work-in-progress or if there are stashed
# changes.
#
# WIP commits are made with `gwip` and removed with `gunwip`.
function __gitp_wips() {
  local wip=${GITP_WIP:-"$cRed WIP"}
  local stash=${GITP_STASH:-"$cRed STASH"}
  local wips

  if $(git log -n 1 2>/dev/null | grep -q -c "\-\-wip\-\-"); then
    wips="$wip"
  fi

  [[ -e "$1/refs/stash" ]] && wips+="$stash"

  echo "$wips"
}

# Work tree change indicators
#
# Prints symbol for each change in `git status`, up to the number of symbols
# indicated by `GITP_CHANGES_MAX`. Gives a good visual of what has changed.
#
# Performance note: if the git prompt is slow, it is more because `git status`
# is slow. This function is near-instantaneous since it is just string
# manipulation.
function __gitp_changes() {
  [[ -z $GITP_STATUS ]] && return

  local changes_max=${GITP_CHANGES_MAX:-20}
  local       i_col=${GITP_COLOUR_INDEX:-$cgGre}
  local       w_col=${GITP_COLOUR_WTREE:-$cgRed}
  local       u_col=${GITP_COLOUR_UN:-$cgYel}
  local       e_col=${GITP_COLOUR_END:-$cReset}
  local lines line X Y x_set y_set u_set end count=0

  # Split git status by newline into array elements.
  if [[ -n ${BASH_VERSION-} ]]; then
    IFS=$'\n'
    lines=$GITP_STATUS
  elif [[ -n ${ZSH_VERSION-} ]]; then
    lines=(${(f)GITP_STATUS})
  fi

  for line in $lines; do
    (( count+=1 ))
    (( count >= $changes_max )) && end='..' && break
    X=${line:0:1}
    Y=${line:1:1}
    [[ $X$Y = '??' ]] && u_set+="?" && continue
    [[ $X = 'U' ]] || [[ $Y = 'U' ]] && u_set+="u" && continue
    [[ $X$Y = 'DD' ]] || [[ $X$Y = 'AA' ]] && u_set+="u" && continue
    [[ $Y = 'M' ]] && y_set+="+"
    [[ $Y = 'D' ]] && y_set+="x"
    [[ $X = 'M' ]] && x_set+="+" && continue
    [[ $X = 'A' ]] && x_set+="±" && continue
    [[ $X = 'D' ]] && x_set+="x" && continue
    [[ $X = 'R' ]] && x_set+="r" && continue
    [[ $X = 'C' ]] && x_set+="c" && continue
  done

  [[ -n ${BASH_VERSION-} ]] && unset IFS

  echo " $i_col$x_set$w_col$y_set$u_col$u_set$e_col$end$cReset"
}

# Check if a repo is modified
function __gitp_dirty() {
  local clean=${GITP_CLEAN:-$cReset$cGre}
  local dirty=${GITP_DIRTY:-$cReset$cBlu}
  [[ -z $GITP_STATUS ]] && echo "$clean" || echo "$dirty"
}

# Get repository name
#
# Check for a name in git remotes, between ':' or '/' and a space.
function __gitp_repo() {
  local repo="$(git remote -v | head -n 1 2>/dev/null)"
  repo=${repo##*[:|/]}
  repo=${repo% *}
  printf "${repo%.git}"
}

# Get current branch name
#
# Prints the branch name or tag of the current commit, if any.
function __gitp_branch() {
  local ref
  ref=$(git symbolic-ref --quiet HEAD 2>/dev/null)
  printf "${ref#refs/heads/}"
}

# Toggle git prompt's "simple" mode.
# This mode will simply display the location information and bare state. It is
# useful for slow machines (e.g. tablets) or slow repositories (e.g. hundreds of
# changed files). For a more permanent setting, set the `GITP_SIMPLE` variable
# to a non-zero string.
function toggle_gitp_simple() {
  [[ -z $GITP_SIMPLE ]] && GITP_SIMPLE=true || GITP_SIMPLE=''
}
