contrib / completion / git-prompt.shon commit show color hints based on state of the git tree (9b7e776)
   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# If you would like a colored hint about the current dirty state, set
  59# GIT_PS1_SHOWCOLORHINTS to a nonempty value. When tracked files are
  60# modified, the branch name turns red, when all modifications are staged
  61# the branch name turns yellow and when all changes are checked in, the
  62# color changes to green. The colors are currently hardcoded in the function.
  63
  64# __gitdir accepts 0 or 1 arguments (i.e., location)
  65# returns location of .git repo
  66__gitdir ()
  67{
  68        # Note: this function is duplicated in git-completion.bash
  69        # When updating it, make sure you update the other one to match.
  70        if [ -z "${1-}" ]; then
  71                if [ -n "${__git_dir-}" ]; then
  72                        echo "$__git_dir"
  73                elif [ -n "${GIT_DIR-}" ]; then
  74                        test -d "${GIT_DIR-}" || return 1
  75                        echo "$GIT_DIR"
  76                elif [ -d .git ]; then
  77                        echo .git
  78                else
  79                        git rev-parse --git-dir 2>/dev/null
  80                fi
  81        elif [ -d "$1/.git" ]; then
  82                echo "$1/.git"
  83        else
  84                echo "$1"
  85        fi
  86}
  87
  88# stores the divergence from upstream in $p
  89# used by GIT_PS1_SHOWUPSTREAM
  90__git_ps1_show_upstream ()
  91{
  92        local key value
  93        local svn_remote svn_url_pattern count n
  94        local upstream=git legacy="" verbose=""
  95
  96        svn_remote=()
  97        # get some config options from git-config
  98        local output="$(git config -z --get-regexp '^(svn-remote\..*\.url|bash\.showupstream)$' 2>/dev/null | tr '\0\n' '\n ')"
  99        while read -r key value; do
 100                case "$key" in
 101                bash.showupstream)
 102                        GIT_PS1_SHOWUPSTREAM="$value"
 103                        if [[ -z "${GIT_PS1_SHOWUPSTREAM}" ]]; then
 104                                p=""
 105                                return
 106                        fi
 107                        ;;
 108                svn-remote.*.url)
 109                        svn_remote[ $((${#svn_remote[@]} + 1)) ]="$value"
 110                        svn_url_pattern+="\\|$value"
 111                        upstream=svn+git # default upstream is SVN if available, else git
 112                        ;;
 113                esac
 114        done <<< "$output"
 115
 116        # parse configuration values
 117        for option in ${GIT_PS1_SHOWUPSTREAM}; do
 118                case "$option" in
 119                git|svn) upstream="$option" ;;
 120                verbose) verbose=1 ;;
 121                legacy)  legacy=1  ;;
 122                esac
 123        done
 124
 125        # Find our upstream
 126        case "$upstream" in
 127        git)    upstream="@{upstream}" ;;
 128        svn*)
 129                # get the upstream from the "git-svn-id: ..." in a commit message
 130                # (git-svn uses essentially the same procedure internally)
 131                local svn_upstream=($(git log --first-parent -1 \
 132                                        --grep="^git-svn-id: \(${svn_url_pattern#??}\)" 2>/dev/null))
 133                if [[ 0 -ne ${#svn_upstream[@]} ]]; then
 134                        svn_upstream=${svn_upstream[ ${#svn_upstream[@]} - 2 ]}
 135                        svn_upstream=${svn_upstream%@*}
 136                        local n_stop="${#svn_remote[@]}"
 137                        for ((n=1; n <= n_stop; n++)); do
 138                                svn_upstream=${svn_upstream#${svn_remote[$n]}}
 139                        done
 140
 141                        if [[ -z "$svn_upstream" ]]; then
 142                                # default branch name for checkouts with no layout:
 143                                upstream=${GIT_SVN_ID:-git-svn}
 144                        else
 145                                upstream=${svn_upstream#/}
 146                        fi
 147                elif [[ "svn+git" = "$upstream" ]]; then
 148                        upstream="@{upstream}"
 149                fi
 150                ;;
 151        esac
 152
 153        # Find how many commits we are ahead/behind our upstream
 154        if [[ -z "$legacy" ]]; then
 155                count="$(git rev-list --count --left-right \
 156                                "$upstream"...HEAD 2>/dev/null)"
 157        else
 158                # produce equivalent output to --count for older versions of git
 159                local commits
 160                if commits="$(git rev-list --left-right "$upstream"...HEAD 2>/dev/null)"
 161                then
 162                        local commit behind=0 ahead=0
 163                        for commit in $commits
 164                        do
 165                                case "$commit" in
 166                                "<"*) ((behind++)) ;;
 167                                *)    ((ahead++))  ;;
 168                                esac
 169                        done
 170                        count="$behind  $ahead"
 171                else
 172                        count=""
 173                fi
 174        fi
 175
 176        # calculate the result
 177        if [[ -z "$verbose" ]]; then
 178                case "$count" in
 179                "") # no upstream
 180                        p="" ;;
 181                "0      0") # equal to upstream
 182                        p="=" ;;
 183                "0      "*) # ahead of upstream
 184                        p=">" ;;
 185                *"      0") # behind upstream
 186                        p="<" ;;
 187                *)          # diverged from upstream
 188                        p="<>" ;;
 189                esac
 190        else
 191                case "$count" in
 192                "") # no upstream
 193                        p="" ;;
 194                "0      0") # equal to upstream
 195                        p=" u=" ;;
 196                "0      "*) # ahead of upstream
 197                        p=" u+${count#0 }" ;;
 198                *"      0") # behind upstream
 199                        p=" u-${count%  0}" ;;
 200                *)          # diverged from upstream
 201                        p=" u+${count#* }-${count%      *}" ;;
 202                esac
 203        fi
 204
 205}
 206
 207
 208# __git_ps1 accepts 0 or 1 arguments (i.e., format string)
 209# when called from PS1 using command substitution
 210# in this mode it prints text to add to bash PS1 prompt (includes branch name)
 211#
 212# __git_ps1 requires 2 arguments when called from PROMPT_COMMAND (pc)
 213# in that case it _sets_ PS1. The arguments are parts of a PS1 string.
 214# when both arguments are given, the first is prepended and the second appended
 215# to the state string when assigned to PS1.
 216# In this mode you can request colored hints using GIT_PS1_SHOWCOLORHINTS=true
 217__git_ps1 ()
 218{
 219        local pcmode=no
 220        #defaults/examples:
 221        local ps1pc_start='\u@\h:\w '
 222        local ps1pc_end='\$ '
 223        local printf_format=' (%s)'
 224
 225        case "$#" in
 226                2)      pcmode=yes
 227                        ps1pc_start="$1"
 228                        ps1pc_end="$2"
 229                ;;
 230                0|1)    printf_format="${1:-$printf_format}"
 231                ;;
 232                *)      return
 233                ;;
 234        esac
 235
 236        local g="$(__gitdir)"
 237        if [ -z "$g" ]; then
 238                if [ $pcmode = yes ]; then
 239                        #In PC mode PS1 always needs to be set
 240                        PS1="$ps1pc_start$ps1pc_end"
 241                fi
 242        else
 243                local r=""
 244                local b=""
 245                if [ -f "$g/rebase-merge/interactive" ]; then
 246                        r="|REBASE-i"
 247                        b="$(cat "$g/rebase-merge/head-name")"
 248                elif [ -d "$g/rebase-merge" ]; then
 249                        r="|REBASE-m"
 250                        b="$(cat "$g/rebase-merge/head-name")"
 251                else
 252                        if [ -d "$g/rebase-apply" ]; then
 253                                if [ -f "$g/rebase-apply/rebasing" ]; then
 254                                        r="|REBASE"
 255                                elif [ -f "$g/rebase-apply/applying" ]; then
 256                                        r="|AM"
 257                                else
 258                                        r="|AM/REBASE"
 259                                fi
 260                        elif [ -f "$g/MERGE_HEAD" ]; then
 261                                r="|MERGING"
 262                        elif [ -f "$g/CHERRY_PICK_HEAD" ]; then
 263                                r="|CHERRY-PICKING"
 264                        elif [ -f "$g/BISECT_LOG" ]; then
 265                                r="|BISECTING"
 266                        fi
 267
 268                        b="$(git symbolic-ref HEAD 2>/dev/null)" || {
 269
 270                                b="$(
 271                                case "${GIT_PS1_DESCRIBE_STYLE-}" in
 272                                (contains)
 273                                        git describe --contains HEAD ;;
 274                                (branch)
 275                                        git describe --contains --all HEAD ;;
 276                                (describe)
 277                                        git describe HEAD ;;
 278                                (* | default)
 279                                        git describe --tags --exact-match HEAD ;;
 280                                esac 2>/dev/null)" ||
 281
 282                                b="$(cut -c1-7 "$g/HEAD" 2>/dev/null)..." ||
 283                                b="unknown"
 284                                b="($b)"
 285                        }
 286                fi
 287
 288                local w=""
 289                local i=""
 290                local s=""
 291                local u=""
 292                local c=""
 293                local p=""
 294
 295                if [ "true" = "$(git rev-parse --is-inside-git-dir 2>/dev/null)" ]; then
 296                        if [ "true" = "$(git rev-parse --is-bare-repository 2>/dev/null)" ]; then
 297                                c="BARE:"
 298                        else
 299                                b="GIT_DIR!"
 300                        fi
 301                elif [ "true" = "$(git rev-parse --is-inside-work-tree 2>/dev/null)" ]; then
 302                        if [ -n "${GIT_PS1_SHOWDIRTYSTATE-}" ]; then
 303                                if [ "$(git config --bool bash.showDirtyState)" != "false" ]; then
 304                                        git diff --no-ext-diff --quiet --exit-code || w="*"
 305                                        if git rev-parse --quiet --verify HEAD >/dev/null; then
 306                                                git diff-index --cached --quiet HEAD -- || i="+"
 307                                        else
 308                                                i="#"
 309                                        fi
 310                                fi
 311                        fi
 312                        if [ -n "${GIT_PS1_SHOWSTASHSTATE-}" ]; then
 313                                git rev-parse --verify refs/stash >/dev/null 2>&1 && s="$"
 314                        fi
 315
 316                        if [ -n "${GIT_PS1_SHOWUNTRACKEDFILES-}" ]; then
 317                                if [ -n "$(git ls-files --others --exclude-standard)" ]; then
 318                                        u="%"
 319                                fi
 320                        fi
 321
 322                        if [ -n "${GIT_PS1_SHOWUPSTREAM-}" ]; then
 323                                __git_ps1_show_upstream
 324                        fi
 325                fi
 326
 327                local f="$w$i$s$u"
 328                if [ $pcmode = yes ]; then
 329                        PS1="$ps1pc_start("
 330                        if [ -n "${GIT_PS1_SHOWCOLORHINT-}" ]; then
 331                                local c_red='\e[31m'
 332                                local c_green='\e[32m'
 333                                local c_yellow='\e[33m'
 334                                local c_lblue='\e[1;34m'
 335                                local c_purple='\e[35m'
 336                                local c_cyan='\e[36m'
 337                                local c_clear='\e[0m'
 338                                local branchstring="$c${b##refs/heads/}"
 339                                local branch_color="$c_green"
 340                                local flags_color="$c_cyan"
 341
 342                                if [ "$w" = "*" ]; then
 343                                        branch_color="$c_red"
 344                                elif [ -n "$i" ]; then
 345                                        branch_color="$c_yellow"
 346                                fi
 347
 348                                # Setting PS1 directly with \[ and \] around colors
 349                                # is necessary to prevent wrapping issues!
 350                                PS1="$PS1\[$branch_color\]$branchstring\[$c_clear\]"
 351                                if [ -n "$f" ]; then
 352                                        PS1="$PS1 \[$flags_color\]$f\[$c_clear\]"
 353                                fi
 354                        else
 355                                PS1="$PS1$c${b##refs/heads/}${f:+ $f}$r$p"
 356                        fi
 357                        PS1="$PS1)$ps1pc_end"
 358                else
 359                        # NO color option unless in PROMPT_COMMAND mode
 360                        printf -- "$printf_format" "$c${b##refs/heads/}${f:+ $f}$r$p"
 361                fi
 362        fi
 363}