contrib / completion / git-prompt.shon commit Allow __git_ps1 to be used in PROMPT_COMMAND (1bfc51a)
   1# bash/zsh git prompt support
   2#
   3# Copyright (C) 2006,2007 Shawn O. Pearce <spearce@spearce.org>
   4# Distributed under the GNU General Public License, version 2.0.
   5#
   6# This script allows you to see the current branch in your prompt.
   7#
   8# To enable:
   9#
  10#    1) Copy this file to somewhere (e.g. ~/.git-prompt.sh).
  11#    2) Add the following line to your .bashrc/.zshrc:
  12#        source ~/.git-prompt.sh
  13#    3a) In ~/.bashrc set PROMPT_COMMAND=__git_ps1
  14#        To customize the prompt, provide start/end arguments
  15#        PROMPT_COMMAND='__git_ps1 "\u@\h:\w" "\\\$ "'
  16#    3b) Alternatively change your PS1 to call __git_ps1 as
  17#        command-substitution:
  18#        Bash: PS1='[\u@\h \W$(__git_ps1 " (%s)")]\$ '
  19#        ZSH:  PS1='[%n@%m %c$(__git_ps1 " (%s)")]\$ '
  20#        the optional argument will be used as format string
  21#
  22# The argument to __git_ps1 will be displayed only if you are currently
  23# in a git repository.  The %s token will be the name of the current
  24# branch.
  25#
  26# In addition, if you set GIT_PS1_SHOWDIRTYSTATE to a nonempty value,
  27# unstaged (*) and staged (+) changes will be shown next to the branch
  28# name.  You can configure this per-repository with the
  29# bash.showDirtyState variable, which defaults to true once
  30# GIT_PS1_SHOWDIRTYSTATE is enabled.
  31#
  32# You can also see if currently something is stashed, by setting
  33# GIT_PS1_SHOWSTASHSTATE to a nonempty value. If something is stashed,
  34# then a '$' will be shown next to the branch name.
  35#
  36# If you would like to see if there're untracked files, then you can set
  37# GIT_PS1_SHOWUNTRACKEDFILES to a nonempty value. If there're untracked
  38# files, then a '%' will be shown next to the branch name.
  39#
  40# If you would like to see the difference between HEAD and its upstream,
  41# set GIT_PS1_SHOWUPSTREAM="auto".  A "<" indicates you are behind, ">"
  42# indicates you are ahead, "<>" indicates you have diverged and "="
  43# indicates that there is no difference. You can further control
  44# behaviour by setting GIT_PS1_SHOWUPSTREAM to a space-separated list
  45# of values:
  46#
  47#     verbose       show number of commits ahead/behind (+/-) upstream
  48#     legacy        don't use the '--count' option available in recent
  49#                   versions of git-rev-list
  50#     git           always compare HEAD to @{upstream}
  51#     svn           always compare HEAD to your SVN upstream
  52#
  53# By default, __git_ps1 will compare HEAD to your SVN upstream if it can
  54# find one, or @{upstream} otherwise.  Once you have set
  55# GIT_PS1_SHOWUPSTREAM, you can override it on a per-repository basis by
  56# setting the bash.showUpstream config variable.
  57
  58# __gitdir accepts 0 or 1 arguments (i.e., location)
  59# returns location of .git repo
  60__gitdir ()
  61{
  62        # Note: this function is duplicated in git-completion.bash
  63        # When updating it, make sure you update the other one to match.
  64        if [ -z "${1-}" ]; then
  65                if [ -n "${__git_dir-}" ]; then
  66                        echo "$__git_dir"
  67                elif [ -n "${GIT_DIR-}" ]; then
  68                        test -d "${GIT_DIR-}" || return 1
  69                        echo "$GIT_DIR"
  70                elif [ -d .git ]; then
  71                        echo .git
  72                else
  73                        git rev-parse --git-dir 2>/dev/null
  74                fi
  75        elif [ -d "$1/.git" ]; then
  76                echo "$1/.git"
  77        else
  78                echo "$1"
  79        fi
  80}
  81
  82# stores the divergence from upstream in $p
  83# used by GIT_PS1_SHOWUPSTREAM
  84__git_ps1_show_upstream ()
  85{
  86        local key value
  87        local svn_remote svn_url_pattern count n
  88        local upstream=git legacy="" verbose=""
  89
  90        svn_remote=()
  91        # get some config options from git-config
  92        local output="$(git config -z --get-regexp '^(svn-remote\..*\.url|bash\.showupstream)$' 2>/dev/null | tr '\0\n' '\n ')"
  93        while read -r key value; do
  94                case "$key" in
  95                bash.showupstream)
  96                        GIT_PS1_SHOWUPSTREAM="$value"
  97                        if [[ -z "${GIT_PS1_SHOWUPSTREAM}" ]]; then
  98                                p=""
  99                                return
 100                        fi
 101                        ;;
 102                svn-remote.*.url)
 103                        svn_remote[ $((${#svn_remote[@]} + 1)) ]="$value"
 104                        svn_url_pattern+="\\|$value"
 105                        upstream=svn+git # default upstream is SVN if available, else git
 106                        ;;
 107                esac
 108        done <<< "$output"
 109
 110        # parse configuration values
 111        for option in ${GIT_PS1_SHOWUPSTREAM}; do
 112                case "$option" in
 113                git|svn) upstream="$option" ;;
 114                verbose) verbose=1 ;;
 115                legacy)  legacy=1  ;;
 116                esac
 117        done
 118
 119        # Find our upstream
 120        case "$upstream" in
 121        git)    upstream="@{upstream}" ;;
 122        svn*)
 123                # get the upstream from the "git-svn-id: ..." in a commit message
 124                # (git-svn uses essentially the same procedure internally)
 125                local svn_upstream=($(git log --first-parent -1 \
 126                                        --grep="^git-svn-id: \(${svn_url_pattern#??}\)" 2>/dev/null))
 127                if [[ 0 -ne ${#svn_upstream[@]} ]]; then
 128                        svn_upstream=${svn_upstream[ ${#svn_upstream[@]} - 2 ]}
 129                        svn_upstream=${svn_upstream%@*}
 130                        local n_stop="${#svn_remote[@]}"
 131                        for ((n=1; n <= n_stop; n++)); do
 132                                svn_upstream=${svn_upstream#${svn_remote[$n]}}
 133                        done
 134
 135                        if [[ -z "$svn_upstream" ]]; then
 136                                # default branch name for checkouts with no layout:
 137                                upstream=${GIT_SVN_ID:-git-svn}
 138                        else
 139                                upstream=${svn_upstream#/}
 140                        fi
 141                elif [[ "svn+git" = "$upstream" ]]; then
 142                        upstream="@{upstream}"
 143                fi
 144                ;;
 145        esac
 146
 147        # Find how many commits we are ahead/behind our upstream
 148        if [[ -z "$legacy" ]]; then
 149                count="$(git rev-list --count --left-right \
 150                                "$upstream"...HEAD 2>/dev/null)"
 151        else
 152                # produce equivalent output to --count for older versions of git
 153                local commits
 154                if commits="$(git rev-list --left-right "$upstream"...HEAD 2>/dev/null)"
 155                then
 156                        local commit behind=0 ahead=0
 157                        for commit in $commits
 158                        do
 159                                case "$commit" in
 160                                "<"*) ((behind++)) ;;
 161                                *)    ((ahead++))  ;;
 162                                esac
 163                        done
 164                        count="$behind  $ahead"
 165                else
 166                        count=""
 167                fi
 168        fi
 169
 170        # calculate the result
 171        if [[ -z "$verbose" ]]; then
 172                case "$count" in
 173                "") # no upstream
 174                        p="" ;;
 175                "0      0") # equal to upstream
 176                        p="=" ;;
 177                "0      "*) # ahead of upstream
 178                        p=">" ;;
 179                *"      0") # behind upstream
 180                        p="<" ;;
 181                *)          # diverged from upstream
 182                        p="<>" ;;
 183                esac
 184        else
 185                case "$count" in
 186                "") # no upstream
 187                        p="" ;;
 188                "0      0") # equal to upstream
 189                        p=" u=" ;;
 190                "0      "*) # ahead of upstream
 191                        p=" u+${count#0 }" ;;
 192                *"      0") # behind upstream
 193                        p=" u-${count%  0}" ;;
 194                *)          # diverged from upstream
 195                        p=" u+${count#* }-${count%      *}" ;;
 196                esac
 197        fi
 198
 199}
 200
 201
 202# __git_ps1 accepts 0 or 1 arguments (i.e., format string)
 203# when called from PS1 using command substitution
 204# in this mode it prints text to add to bash PS1 prompt (includes branch name)
 205#
 206# __git_ps1 requires 2 arguments when called from PROMPT_COMMAND (pc)
 207# in that case it _sets_ PS1. The arguments are parts of a PS1 string.
 208# when both arguments are given, the first is prepended and the second appended
 209# to the state string when assigned to PS1.
 210__git_ps1 ()
 211{
 212        local pcmode=no
 213        #defaults/examples:
 214        local ps1pc_start='\u@\h:\w '
 215        local ps1pc_end='\$ '
 216        local printf_format=' (%s)'
 217
 218        case "$#" in
 219                2)      pcmode=yes
 220                        ps1pc_start="$1"
 221                        ps1pc_end="$2"
 222                ;;
 223                0|1)    printf_format="${1:-$printf_format}"
 224                ;;
 225                *)      return
 226                ;;
 227        esac
 228
 229        local g="$(__gitdir)"
 230        if [ -z "$g" ]; then
 231                if [ $pcmode = yes ]; then
 232                        #In PC mode PS1 always needs to be set
 233                        PS1="$ps1pc_start$ps1pc_end"
 234                fi
 235        else
 236                local r=""
 237                local b=""
 238                if [ -f "$g/rebase-merge/interactive" ]; then
 239                        r="|REBASE-i"
 240                        b="$(cat "$g/rebase-merge/head-name")"
 241                elif [ -d "$g/rebase-merge" ]; then
 242                        r="|REBASE-m"
 243                        b="$(cat "$g/rebase-merge/head-name")"
 244                else
 245                        if [ -d "$g/rebase-apply" ]; then
 246                                if [ -f "$g/rebase-apply/rebasing" ]; then
 247                                        r="|REBASE"
 248                                elif [ -f "$g/rebase-apply/applying" ]; then
 249                                        r="|AM"
 250                                else
 251                                        r="|AM/REBASE"
 252                                fi
 253                        elif [ -f "$g/MERGE_HEAD" ]; then
 254                                r="|MERGING"
 255                        elif [ -f "$g/CHERRY_PICK_HEAD" ]; then
 256                                r="|CHERRY-PICKING"
 257                        elif [ -f "$g/BISECT_LOG" ]; then
 258                                r="|BISECTING"
 259                        fi
 260
 261                        b="$(git symbolic-ref HEAD 2>/dev/null)" || {
 262
 263                                b="$(
 264                                case "${GIT_PS1_DESCRIBE_STYLE-}" in
 265                                (contains)
 266                                        git describe --contains HEAD ;;
 267                                (branch)
 268                                        git describe --contains --all HEAD ;;
 269                                (describe)
 270                                        git describe HEAD ;;
 271                                (* | default)
 272                                        git describe --tags --exact-match HEAD ;;
 273                                esac 2>/dev/null)" ||
 274
 275                                b="$(cut -c1-7 "$g/HEAD" 2>/dev/null)..." ||
 276                                b="unknown"
 277                                b="($b)"
 278                        }
 279                fi
 280
 281                local w=""
 282                local i=""
 283                local s=""
 284                local u=""
 285                local c=""
 286                local p=""
 287
 288                if [ "true" = "$(git rev-parse --is-inside-git-dir 2>/dev/null)" ]; then
 289                        if [ "true" = "$(git rev-parse --is-bare-repository 2>/dev/null)" ]; then
 290                                c="BARE:"
 291                        else
 292                                b="GIT_DIR!"
 293                        fi
 294                elif [ "true" = "$(git rev-parse --is-inside-work-tree 2>/dev/null)" ]; then
 295                        if [ -n "${GIT_PS1_SHOWDIRTYSTATE-}" ]; then
 296                                if [ "$(git config --bool bash.showDirtyState)" != "false" ]; then
 297                                        git diff --no-ext-diff --quiet --exit-code || w="*"
 298                                        if git rev-parse --quiet --verify HEAD >/dev/null; then
 299                                                git diff-index --cached --quiet HEAD -- || i="+"
 300                                        else
 301                                                i="#"
 302                                        fi
 303                                fi
 304                        fi
 305                        if [ -n "${GIT_PS1_SHOWSTASHSTATE-}" ]; then
 306                                git rev-parse --verify refs/stash >/dev/null 2>&1 && s="$"
 307                        fi
 308
 309                        if [ -n "${GIT_PS1_SHOWUNTRACKEDFILES-}" ]; then
 310                                if [ -n "$(git ls-files --others --exclude-standard)" ]; then
 311                                        u="%"
 312                                fi
 313                        fi
 314
 315                        if [ -n "${GIT_PS1_SHOWUPSTREAM-}" ]; then
 316                                __git_ps1_show_upstream
 317                        fi
 318                fi
 319
 320                local f="$w$i$s$u"
 321                if [ $pcmode = yes ]; then
 322                        PS1="$ps1pc_start("
 323                        PS1="$PS1$c${b##refs/heads/}${f:+ $f}$r$p"
 324                        PS1="$PS1)$ps1pc_end"
 325                else
 326                        printf -- "$printf_format" "$c${b##refs/heads/}${f:+ $f}$r$p"
 327                fi
 328        fi
 329}