# bash/zsh git prompt support # # Copyright (C) 2013 David Xu # Based on the earlier work by Shawn O. Pearce # Distributed under the GNU General Public License, version 2.0. # # This script allows you to see the current branch in your prompt, # posh-git style # # To enable: # # 1) Copy this file to somewhere (e.g. ~/.git-prompt.sh). # 2) Add the following line to your .bashrc/.zshrc: # source ~/.git-prompt.sh # 3a) Change your PS1 to call __git_ps1 as # command-substitution: # Bash: PS1='[\u@\h \W$(__git_ps1 " (%s)")]\$ ' # ZSH: PS1='[%n@%m %c$(__git_ps1 " (%s)")]\$ ' # the optional argument will be used as format string. # 3b) Alternatively, if you are using bash, __git_ps1 can be # used for PROMPT_COMMAND with two parameters,
 and
#        , which are strings you would put in $PS1 before
#        and after the status string generated by the git-prompt
#        machinery.  e.g.
#           PROMPT_COMMAND='__git_ps1 "\u@\h:\w" "\\\$ "'
#        will show username, at-sign, host, colon, cwd, then
#        various status string, followed by dollar and SP, as
#        your prompt.
#        Optionally, you can supply a third argument with a printf
#        format string to finetune the output of the branch status
#
# The argument to __git_ps1 will be displayed only if you are currently
# in a git repository.  The %s token will be the name of the current
# branch.
#
# __gitdir accepts 0 or 1 arguments (i.e., location)
# returns location of .git repo
# __git_ps1 accepts 0 or 1 arguments (i.e., format string)
# when called from PS1 using command substitution
# in this mode it prints text to add to bash PS1 prompt (includes branch name)
#
# __git_ps1 requires 2 or 3 arguments when called from PROMPT_COMMAND (pc)
# in that case it _sets_ PS1. The arguments are parts of a PS1 string.
# when two arguments are given, the first is prepended and the second appended
# to the state string when assigned to PS1.
# The optional third parameter will be used as printf format string to further
# customize the output of the git-status string.
#
# CONFIG OPTIONS
# ==============
#
# This script should work out of the box. Available options are set through
# your git configuration files. This allows you to control the prompt display on a
# per-repository basis. These files are most likely called `.gitconfig`; an
# example illustrating the syntax of these files can be found at
# http://git-scm.com/docs/git-config#_example.
# ```
# bash.describeStyle
# bash.enableFileStatus
# bash.enableGitStatus
# bash.showStatusWhenZero
# bash.showUpstream
# ```
#
# bash.describeStyle
# ------------------
#
# This option controls if you would like to see more information about the
# identity of commits checked out as a detached `HEAD`. This is also controlled
# by the legacy environment variable `GIT_PS1_DESCRIBESTYLE`.
#
# Option   | Description
# -------- | -----------
# contains | relative to newer annotated tag `(v1.6.3.2~35)`
# branch   | relative to newer tag or branch `(master~4)`
# describe | relative to older annotated tag `(v1.6.3.1-13-gdd42c2f)`
# default  | exactly matching tag
#
# bash.enableFileStatus
# ---------------------
#
# Option | Description
# ------ | -----------
# true   | _Default_. The script will query for all file indicators every time.
# false  | No file indicators will be displayed. The script will not query
#          upstream for differences. Branch color-coding information is still
#          displayed.
#
# bash.enableGitStatus
# --------------------
#
# Option | Description
# ------ | -----------
# true   | _Default_. Color coding and indicators will be shown.
# false  | The script will not run.
#
# bash.showStashState
# -------------------
#
# Option | Description
# ------ | -----------
# true   | _Default_. An indicator will display if the stash is not empty.
# false  | An indicator will not display the stash status.
#
# bash.showStatusWhenZero
# -----------------------
#
# Option | Description
# ------ | -----------
# true   | Indicators will be shown even if there are no updates to the index or
#          working tree.
# false  | _Default_. No file change indicators will be shown if there are no
#          changes to the index or working tree.
#
# bash.showUpstream
# -----------------
#
# By default, `__git_ps1` will compare `HEAD` to your `SVN` upstream if it can
# find one, or `@{upstream}` otherwise. This is also controlled by the legacy
# environment variable `GIT_PS1_SHOWUPSTREAM`.
#
# Option | Description
# ------ | -----------
# legacy | Does not use the `--count` option available in recent versions of
#          `git-rev-list`
# git    | _Default_. Always compares `HEAD` to `@{upstream}`
# svn    | Always compares `HEAD` to `SVN` upstream
__git_ps1 ()
{
    if [ "$(git config --bool bash.enableGitStatus)" == "false" ]; then return; fi

    local DefaultForegroundColor='\e[m' # Default no color
    local DefaultBackgroundColor=

    local BeforeText=' ['
    local BeforeForegroundColor='\e[1;33m' # Yellow
    local BeforeBackgroundColor=
    local DelimText=' |'
    local DelimForegroundColor='\e[1;33m' # Yellow
    local DelimBackgroundColor=

    local AfterText=']'
    local AfterForegroundColor='\e[1;33m' # Yellow
    local AfterBackgroundColor=

    local BranchForegroundColor='\e[1;36m' # Cyan
    local BranchBackgroundColor=
    local BranchAheadForegroundColor='\e[1;32m' # Green
    local BranchAheadBackgroundColor=
    local BranchBehindForegroundColor='\e[0;31m' # Red
    local BranchBehindBackgroundColor=
    local BranchBehindAndAheadForegroundColor='\e[1;33m' # Yellow
    local BranchBehindAndAheadBackgroundColor=

    local BeforeIndexText=""
    local BeforeIndexForegroundColor='\e[1;32m' #Dark green
    local BeforeIndexBackgroundColor=

    local IndexForegroundColor='\e[1;32m' # Dark green
    local IndexBackgroundColor=

    local WorkingForegroundColor='\e[0;31m' # Dark red
    local WorkingBackgroundColor=

    local StashForegroundColor='\e[0;34m' # Darker blue
    local StashBackgroundColor=
    local StashText='$'


    local EnableFileStatus=`git config --bool bash.enableFileStatus`
    case "$EnableFileStatus" in
        true)  EnableFileStatus=true ;;
        false) EnableFileStatus=false ;;
        *)     EnableFileStatus=true ;;
    esac
    local ShowStatusWhenZero=`git config --bool bash.showStatusWhenZero`
    case "$ShowStatusWhenZero" in
        true)  ShowStatusWhenZero=true ;;
        false) ShowStatusWhenZero=false ;;
        *)     ShowStatusWhenZero=false ;;
    esac
    local ShowStashState=`git config --bool bash.showStashState`
    case "$ShowStashState" in
        true)  ShowStashState=true ;;
        false) ShowStashState=false ;;
        *)     ShowStashState=true ;;
    esac

    aheadBy=0                   # these globals are updated by __git_ps1_showupstream
    behindBy=0

    local is_pcmode=false
    local is_detached=false
    local ps1pc_start='\u@\h:\w '
    local ps1pc_end='\$ '
    local printf_format='%s'

    case "$#" in
        2|3)
            is_pcmode=true
            ps1pc_start="$1"
            ps1pc_end="$2"
            printf_format="${3:-$printf_format}"
            ;;
        0|1)
            printf_format="${1:-$printf_format}"
            ;;
        *)
            return
            ;;
    esac

    local g="$(__gitdir)"
    if [ -z "$g" ]; then
        if [ $is_pcmode ]; then
            #In PROMPT_COMMAND mode PS1 always needs to be set
            PS1="$ps1pc_start$ps1pc_end"
        fi
        return
    fi
    local rebase=""
    local b=""
    local step=""
    local total=""
    if [ -d "$g/rebase-merge" ]; then
        b=$(cat "$g/rebase-merge/head-name" 2>/dev/null)
        step=$(cat "$g/rebase-merge/msgnum" 2>/dev/null)
        total=$(cat "$g/rebase-merge/end" 2>/dev/null)
        if [ -f "$g/rebase-merge/interactive" ]; then
            rebase="|REBASE-i"
        else
            rebase="|REBASE-m"
        fi
    else
        if [ -d "$g/rebase-apply" ]; then
            step=$(cat "$g/rebase-apply/next")
            total=$(cat "$g/rebase-apply/last")
            if [ -f "$g/rebase-apply/rebasing" ]; then
                rebase="|REBASE"
            elif [ -f "$g/rebase-apply/applying" ]; then
                rebase="|AM"
            else
                rebase="|AM/REBASE"
            fi
        elif [ -f "$g/MERGE_HEAD" ]; then
            rebase="|MERGING"
        elif [ -f "$g/CHERRY_PICK_HEAD" ]; then
            rebase="|CHERRY-PICKING"
        elif [ -f "$g/REVERT_HEAD" ]; then
            rebase="|REVERTING"
        elif [ -f "$g/BISECT_LOG" ]; then
            rebase="|BISECTING"
        fi

        b="$(git symbolic-ref HEAD 2>/dev/null)" || {
            is_detached=true
            local output="$(git config -z --get bash.describeStyle)"
            if [ -n "$output" ]; then
                GIT_PS1_DESCRIBESTYLE=$output
            fi
            b="$(
            case "${GIT_PS1_DESCRIBESTYLE-}" in
            (contains)
                git describe --contains HEAD ;;
            (branch)
                git describe --contains --all HEAD ;;
            (describe)
                git describe HEAD ;;
            (* | default)
                git describe --tags --exact-match HEAD ;;
            esac 2>/dev/null)" ||

            b="$(cut -c1-7 "$g/HEAD" 2>/dev/null)..." ||
            b="unknown"
            b="($b)"
        }
    fi

    if [ -n "$step" ] && [ -n "$total" ]; then
        rebase="$rebase $step/$total"
    fi

    local isDirtyUnstaged=""
    local isDirtyStaged=""
    local hasStash=false
    local isBare=""

    if [ "true" = "$(git rev-parse --is-inside-git-dir 2>/dev/null)" ]; then
        if [ "true" = "$(git rev-parse --is-bare-repository 2>/dev/null)" ]; then
            isBare="BARE:"
        else
            b="GIT_DIR!"
        fi
    elif [ "true" = "$(git rev-parse --is-inside-work-tree 2>/dev/null)" ]; then
        if $ShowStashState; then
            git rev-parse --verify refs/stash >/dev/null 2>&1 && hasStash=true
        fi
        __git_ps1_show_upstream
    fi

    # show index status and working directory status
    if $EnableFileStatus; then
        local indexAdded=0
        local indexModified=0
        local indexDeleted=0
        local indexUnmerged=0
        local filesAdded=0
        local filesModified=0
        local filesDeleted=0
        local filesUnmerged=0
        while IFS="\n" read -r tag rest
        do
            case "${tag:0:1}" in
                A )
                    (( indexAdded++ ))
                    ;;
                M )
                    (( indexModified++ ))
                    ;;
                R )
                    (( indexModified++ ))
                    ;;
                C )
                    (( indexModified++ ))
                    ;;
                D )
                    (( indexDeleted++ ))
                    ;;
                U )
                    (( indexUnmerged++ ))
                    ;;
            esac
            case "${tag:1:1}" in
                \? )
                    (( filesAdded++ ))
                    ;;
                A )
                    (( filesAdded++ ))
                    ;;
                M )
                    (( filesModified++ ))
                    ;;
                D )
                    (( filesDeleted++ ))
                    ;;
                U )
                    (( filesUnmerged++ ))
                    ;;
            esac
        done <<< "`git status --porcelain 2>/dev/null`"
    fi

    local gitstring=
    local branchstring="$isBare${b##refs/heads/}"
    # before-branch text
    gitstring="\[$BeforeBackgroundColor\]\[$BeforeForegroundColor\]$BeforeText"

    # branch

    if (( $behindBy > 0 && $aheadBy > 0 )); then
        gitstring+="\[$BranchBehindAndAheadBackgroundColor\]\[$BranchBehindAndAheadForegroundColor\]$branchstring"
    elif (( $behindBy > 0 )); then
        gitstring+="\[$BranchBehindBackgroundColor\]\[$BranchBehindForegroundColor\]$branchstring"
    elif (( $aheadBy > 0 )); then
        gitstring+="\[$BranchAheadBackgroundColor\]\[$BranchAheadForegroundColor\]$branchstring"
    else
        gitstring+="\[$BranchBackgroundColor\]\[$BranchForegroundColor\]$branchstring"
    fi

    local indexCount="$(( $indexAdded + $indexModified + $indexDeleted + $indexUnmerged ))"
    local workingCount="$(( $filesAdded + $filesModified + $filesDeleted + $filesUnmerged ))"
    # index status
    if $EnableFileStatus; then
        if (( $indexCount != 0 )) || $ShowStatusWhenZero; then
            gitstring+="\[$IndexBackgroundColor\]\[$IndexForegroundColor\] +$indexAdded ~$indexModified -$indexDeleted"
        fi
        if (( $indexUnmerged != 0 )); then
            gitstring+=" \[$IndexBackgroundColor\]\[$IndexForegroundColor\]!$indexUnmerged"
        fi
        if (( $indexCount != 0 && ($workingCount != 0 || $ShowStatusWhenZero) )); then
            gitstring+="\[$DelimBackgroundColor\]\[$DelimForegroundColor\]$DelimText"
        fi
    fi
    if $EnableFileStatus; then
        if (( $workingCount != 0 )) || $ShowStatusWhenZero; then
            gitstring+="\[$WorkingBackgroundColor\]\[$WorkingForegroundColor\] +$filesAdded ~$filesModified -$filesDeleted"
        fi
        if (( $filesUnmerged != 0 )); then
            gitstring+=" \[$WorkingBackgroundColor\]\[$WorkingForegroundColor\]!$filesUnmerged"
        fi
    fi
    gitstring+=${rebase:+'\[\e[0m\]'$rebase}

    # after-branch text
    gitstring+="\[$AfterBackgroundColor\]\[$AfterForegroundColor\]$AfterText"

    if $ShowStashState && $hasStash; then
        gitstring+="\[$StashBackgroundColor\]\[$StashForegroundColor\]"$StashText
    fi
    gitstring=`printf -- "$printf_format" "$gitstring\[$DefaultBackgroundColor\]\[$DefaultForegroundColor\]"`
    if $is_pcmode; then
        PS1="$ps1pc_start$gitstring$ps1pc_end"
    else
        printf -- "$printf_format" "$gitstring"
    fi
}

__gitdir ()
{
    # Note: this function is duplicated in git-completion.bash
    # When updating it, make sure you update the other one to match.
    if [ -z "${1-}" ]; then
        if [ -n "${__git_dir-}" ]; then
            echo "$__git_dir"
        elif [ -n "${GIT_DIR-}" ]; then
            test -d "${GIT_DIR-}" || return 1
            echo "$GIT_DIR"
        elif [ -d .git ]; then
            echo .git
        else
            git rev-parse --git-dir 2>/dev/null
        fi
    elif [ -d "$1/.git" ]; then
        echo "$1/.git"
    else
        echo "$1"
    fi
}

# Updates the global variables `aheadBy` and `behindBy`
__git_ps1_show_upstream ()
{
    local key value
    local svn_remote svn_url_pattern n
    local upstream=git          # default
    legacy=""

    svn_remote=()
    # get some config options from git-config
    local output="$(git config -z --get-regexp '^(svn-remote\..*\.url|bash\.showUpstream)$' 2>/dev/null | tr '\0\n' '\n ')"
    while read -r key value; do
        case "$key" in
        bash.showUpstream)
            GIT_PS1_SHOWUPSTREAM="$value"
            if [[ -z "${GIT_PS1_SHOWUPSTREAM}" ]]; then
                return
            fi
            ;;
        svn-remote.*.url)
            svn_remote[ $((${#svn_remote[@]} + 1)) ]="$value"
            svn_url_pattern+="\\|$value"
            upstream=svn+git # default upstream is SVN if available, else git
            ;;
        esac
    done <<< "$output"

    # parse configuration values
    for option in ${GIT_PS1_SHOWUPSTREAM}; do
        case "$option" in
        git|svn) upstream="$option" ;;
        legacy)  legacy=1  ;;
        esac
    done

    # Find our upstream
    case "$upstream" in
    git)    upstream="@{upstream}" ;;
    svn*)
        # get the upstream from the "git-svn-id: ..." in a commit message
        # (git-svn uses essentially the same procedure internally)
        local svn_upstream=($(git log --first-parent -1 \
                    --grep="^git-svn-id: \(${svn_url_pattern#??}\)" 2>/dev/null))
        if (( 0 != ${#svn_upstream[@]} )); then
            svn_upstream=${svn_upstream[ ${#svn_upstream[@]} - 2 ]}
            svn_upstream=${svn_upstream%@*}
            local n_stop="${#svn_remote[@]}"
            for ((n=1; n <= n_stop; n++)); do
                svn_upstream=${svn_upstream#${svn_remote[$n]}}
            done

            if [[ -z "$svn_upstream" ]]; then
                # default branch name for checkouts with no layout:
                upstream=${GIT_SVN_ID:-git-svn}
            else
                upstream=${svn_upstream#/}
            fi
        elif [[ "svn+git" = "$upstream" ]]; then
            upstream="@{upstream}"
        fi
        ;;
    esac

    aheadBy=0
    behindBy=0
    # Find how many commits we are ahead/behind our upstream
    if [ -z "$legacy" ]; then
        IFS=$' \t\n' read -r behindBy aheadBy <<< "`git rev-list --count --left-right $upstream...HEAD 2>/dev/null`"
    else
        # produce equivalent output to --count for older versions of git
        while IFS=$' \t\n' read -r commit; do
            case "$commit" in
            "<*") (( behindBy++ )) ;;
            ">*") (( aheadBy++ ))  ;;
            esac
        done <<< "`git rev-list --left-right $upstream...HEAD 2>/dev/null`"
    fi
    : ${aheadBy:=0}
    : ${behindBy:=0}
}

write_prompt() {
    printf --
}