git-prompt.sh 13.4 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445
# bash/zsh git prompt support
#
# Copyright (C) 2006,2007 Shawn O. Pearce <spearce@spearce.org>
# Distributed under the GNU General Public License, version 2.0.
#
# This script allows you to see repository status in your prompt.
#
# 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:  setopt PROMPT_SUBST ; PS1='[%n@%m %c$(__git_ps1 " (%s)")]\$ '
#        the optional argument will be used as format string.
#    3b) Alternatively, for a slightly faster prompt, __git_ps1 can
#        be used for PROMPT_COMMAND in Bash or for precmd() in Zsh
#        with two parameters, <pre> and <post>, which are strings
#        you would put in $PS1 before and after the status string
#        generated by the git-prompt machinery.  e.g.
#        Bash: 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.
#        ZSH:  precmd () { __git_ps1 "%n" ":%~$ " "|%s" }
#          will show username, pipe, then various status string,
#          followed by colon, cwd, 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 repository status will be displayed only if you are currently in a
# git repository. The %s token is the placeholder for the shown status.
#
# The prompt status always includes the current branch name.
#
# In addition, if you set GIT_PS1_SHOWDIRTYSTATE to a nonempty value,
# unstaged (*) and staged (+) changes will be shown next to the branch
# name.  You can configure this per-repository with the
# bash.showDirtyState variable, which defaults to true once
# GIT_PS1_SHOWDIRTYSTATE is enabled.
#
# You can also see if currently something is stashed, by setting
# GIT_PS1_SHOWSTASHSTATE to a nonempty value. If something is stashed,
# then a '$' will be shown next to the branch name.
#
# If you would like to see if there're untracked files, then you can set
# GIT_PS1_SHOWUNTRACKEDFILES to a nonempty value. If there're untracked
# files, then a '%' will be shown next to the branch name.  You can
# configure this per-repository with the bash.showUntrackedFiles
# variable, which defaults to true once GIT_PS1_SHOWUNTRACKEDFILES is
# enabled.
#
# If you would like to see the difference between HEAD and its upstream,
# set GIT_PS1_SHOWUPSTREAM="auto".  A "<" indicates you are behind, ">"
# indicates you are ahead, "<>" indicates you have diverged and "="
# indicates that there is no difference. You can further control
# behaviour by setting GIT_PS1_SHOWUPSTREAM to a space-separated list
# of values:
#
#     verbose       show number of commits ahead/behind (+/-) upstream
#     legacy        don't use the '--count' option available in recent
#                   versions of git-rev-list
#     git           always compare HEAD to @{upstream}
#     svn           always compare HEAD to your SVN upstream
#
# By default, __git_ps1 will compare HEAD to your SVN upstream if it can
# find one, or @{upstream} otherwise.  Once you have set
# GIT_PS1_SHOWUPSTREAM, you can override it on a per-repository basis by
# setting the bash.showUpstream config variable.
#
# If you would like to see more information about the identity of
# commits checked out as a detached HEAD, set GIT_PS1_DESCRIBE_STYLE
# to one of these values:
#
#     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
#
# If you would like a colored hint about the current dirty state, set
# GIT_PS1_SHOWCOLORHINTS to a nonempty value. The colors are based on
# the colored output of "git status -sb" and are available only when
# using __git_ps1 for PROMPT_COMMAND or precmd.

# stores the divergence from upstream in $p
# used by GIT_PS1_SHOWUPSTREAM
__git_ps1_show_upstream ()
{
  local key value
  local svn_remote svn_url_pattern count n
  local upstream=git legacy="" verbose=""

  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
        p=""
        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" ;;
    verbose) verbose=1 ;;
    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 -a svn_upstream
    svn_upstream=($(git log --first-parent -1 \
          --grep="^git-svn-id: \(${svn_url_pattern#??}\)" 2>/dev/null))
    if [[ 0 -ne ${#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

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

  # calculate the result
  if [[ -z "$verbose" ]]; then
    case "$count" in
    "") # no upstream
      p="" ;;
    "0  0") # equal to upstream
      p="=" ;;
    "0  "*) # ahead of upstream
      p=">" ;;
    *"  0") # behind upstream
      p="<" ;;
    *)      # diverged from upstream
      p="<>" ;;
    esac
  else
    case "$count" in
    "") # no upstream
      p="" ;;
    "0  0") # equal to upstream
      p=" u=" ;;
    "0  "*) # ahead of upstream
      p=" u+${count#0 }" ;;
    *"  0") # behind upstream
      p=" u-${count%  0}" ;;
    *)      # diverged from upstream
      p=" u+${count#* }-${count%  *}" ;;
    esac
  fi

}

# Helper function that is meant to be called from __git_ps1.  It
# injects color codes into the appropriate gitstring variables used
# to build a gitstring.
__git_ps1_colorize_gitstring ()
{
  if [[ -n ${ZSH_VERSION-} ]]; then
    local c_red='%F{red}'
    local c_green='%F{green}'
    local c_lblue='%F{blue}'
    local c_clear='%f'
  else
    # Using \[ and \] around colors is necessary to prevent
    # issues with command line editing/browsing/completion!
    local c_red='\[\e[31m\]'
    local c_green='\[\e[32m\]'
    local c_lblue='\[\e[1;34m\]'
    local c_clear='\[\e[0m\]'
  fi
  local bad_color=$c_red
  local ok_color=$c_green
  local flags_color="$c_lblue"

  local branch_color=""
  if [ $detached = no ]; then
    branch_color="$ok_color"
  else
    branch_color="$bad_color"
  fi
  c="$branch_color$c"

  z="$c_clear$z"
  if [ "$w" = "*" ]; then
    w="$bad_color$w"
  fi
  if [ -n "$i" ]; then
    i="$ok_color$i"
  fi
  if [ -n "$s" ]; then
    s="$flags_color$s"
  fi
  if [ -n "$u" ]; then
    u="$bad_color$u"
  fi
  r="$c_clear$r"
}

# __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.
# In this mode you can request colored hints using GIT_PS1_SHOWCOLORHINTS=true
__git_ps1 ()
{
  local pcmode=no
  local detached=no
  local ps1pc_start='\u@\h:\w '
  local ps1pc_end='\$ '
  local printf_format=' (%s)'

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

  local repo_info rev_parse_exit_code
  repo_info="$(git rev-parse --git-dir --is-inside-git-dir \
    --is-bare-repository --is-inside-work-tree \
    --short HEAD 2>/dev/null)"
  rev_parse_exit_code="$?"

  if [ -z "$repo_info" ]; then
    if [ $pcmode = yes ]; then
      #In PC mode PS1 always needs to be set
      PS1="$ps1pc_start$ps1pc_end"
    fi
    return
  fi

  local short_sha
  if [ "$rev_parse_exit_code" = "0" ]; then
    short_sha="${repo_info##*$'\n'}"
    repo_info="${repo_info%$'\n'*}"
  fi
  local inside_worktree="${repo_info##*$'\n'}"
  repo_info="${repo_info%$'\n'*}"
  local bare_repo="${repo_info##*$'\n'}"
  repo_info="${repo_info%$'\n'*}"
  local inside_gitdir="${repo_info##*$'\n'}"
  local g="${repo_info%$'\n'*}"

  local r=""
  local b=""
  local step=""
  local total=""
  if [ -d "$g/rebase-merge" ]; then
    read b 2>/dev/null <"$g/rebase-merge/head-name"
    read step 2>/dev/null <"$g/rebase-merge/msgnum"
    read total 2>/dev/null <"$g/rebase-merge/end"
    if [ -f "$g/rebase-merge/interactive" ]; then
      r="|REBASE-i"
    else
      r="|REBASE-m"
    fi
  else
    if [ -d "$g/rebase-apply" ]; then
      read step 2>/dev/null <"$g/rebase-apply/next"
      read total 2>/dev/null <"$g/rebase-apply/last"
      if [ -f "$g/rebase-apply/rebasing" ]; then
        read b 2>/dev/null <"$g/rebase-apply/head-name"
        r="|REBASE"
      elif [ -f "$g/rebase-apply/applying" ]; then
        r="|AM"
      else
        r="|AM/REBASE"
      fi
    elif [ -f "$g/MERGE_HEAD" ]; then
      r="|MERGING"
    elif [ -f "$g/CHERRY_PICK_HEAD" ]; then
      r="|CHERRY-PICKING"
    elif [ -f "$g/REVERT_HEAD" ]; then
      r="|REVERTING"
    elif [ -f "$g/BISECT_LOG" ]; then
      r="|BISECTING"
    fi

    if [ -n "$b" ]; then
      :
    elif [ -h "$g/HEAD" ]; then
      # symlink symbolic ref
      b="$(git symbolic-ref HEAD 2>/dev/null)"
    else
      local head=""
      if ! read head 2>/dev/null <"$g/HEAD"; then
        if [ $pcmode = yes ]; then
          PS1="$ps1pc_start$ps1pc_end"
        fi
        return
      fi
      # is it a symbolic ref?
      b="${head#ref: }"
      if [ "$head" = "$b" ]; then
        detached=yes
        b="$(
        case "${GIT_PS1_DESCRIBE_STYLE-}" 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="$short_sha..."
        b="($b)"
      fi
    fi
  fi

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

  local w=""
  local i=""
  local s=""
  local u=""
  local c=""
  local p=""

  if [ "true" = "$inside_gitdir" ]; then
    if [ "true" = "$bare_repo" ]; then
      c="BARE:"
    else
      b="GIT_DIR!"
    fi
  elif [ "true" = "$inside_worktree" ]; then
    if [ -n "${GIT_PS1_SHOWDIRTYSTATE-}" ] &&
       [ "$(git config --bool bash.showDirtyState)" != "false" ]
    then
      git diff --no-ext-diff --quiet --exit-code || w="*"
      if [ -n "$short_sha" ]; then
        git diff-index --cached --quiet HEAD -- || i="+"
      else
        i="#"
      fi
    fi
    if [ -n "${GIT_PS1_SHOWSTASHSTATE-}" ] &&
       [ -r "$g/refs/stash" ]; then
      s="$"
    fi

    if [ -n "${GIT_PS1_SHOWUNTRACKEDFILES-}" ] &&
       [ "$(git config --bool bash.showUntrackedFiles)" != "false" ] &&
       git ls-files --others --exclude-standard --error-unmatch -- '*' >/dev/null 2>/dev/null
    then
      u="%${ZSH_VERSION+%}"
    fi

    if [ -n "${GIT_PS1_SHOWUPSTREAM-}" ]; then
      __git_ps1_show_upstream
    fi
  fi

  local z="${GIT_PS1_STATESEPARATOR-" "}"

  # NO color option unless in PROMPT_COMMAND mode
  if [ $pcmode = yes ] && [ -n "${GIT_PS1_SHOWCOLORHINTS-}" ]; then
    __git_ps1_colorize_gitstring
  fi

  local f="$w$i$s$u"
  local gitstring="$c${b##refs/heads/}${f:+$z$f}$r$p"

  if [ $pcmode = yes ]; then
    if [[ -n ${ZSH_VERSION-} ]]; then
      gitstring=$(printf -- "$printf_format" "$gitstring")
    else
      printf -v gitstring -- "$printf_format" "$gitstring"
    fi
    PS1="$ps1pc_start$gitstring$ps1pc_end"
  else
    printf -- "$printf_format" "$gitstring"
  fi
}