Merge branch 'jc/prompt-document-ps1-state-separator'
[gitweb.git] / contrib / completion / git-prompt.sh
index be5467bb45d2c77baa261df7c1fb0f0985dafec6..366f0bc1e9b5597f1714574700452cd241c14f8d 100644 (file)
@@ -3,7 +3,7 @@
 # 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 the current branch in your prompt.
+# This script allows you to see repository status in your prompt.
 #
 # To enable:
 #
 #    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)")]\$ '
+#        ZSH:  setopt PROMPT_SUBST ; 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, <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.
-#           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.
+#    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 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.
+# 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
@@ -56,6 +60,7 @@
 # of values:
 #
 #     verbose       show number of commits ahead/behind (+/-) upstream
+#     name          if verbose, then also show the upstream abbrev name
 #     legacy        don't use the '--count' option available in recent
 #                   versions of git-rev-list
 #     git           always compare HEAD to @{upstream}
 #
 # 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".
+# the colored output of "git status -sb" and are available only when
+# using __git_ps1 for PROMPT_COMMAND or precmd.
+#
+# If you would like __git_ps1 to do nothing in the case when the current
+# directory is set up to be ignored by git, then set
+# GIT_PS1_HIDE_IF_PWD_IGNORED to a nonempty value. Override this on the
+# repository level by setting bash.hideIfPwdIgnored to "false".
 
-# __gitdir accepts 0 or 1 arguments (i.e., location)
-# returns location of .git repo
-__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
-}
+# check whether printf supports -v
+__git_printf_supports_v=
+printf -v __git_printf_supports_v -- '%s' yes >/dev/null 2>&1
 
 # stores the divergence from upstream in $p
 # used by GIT_PS1_SHOWUPSTREAM
@@ -113,7 +104,7 @@ __git_ps1_show_upstream ()
 {
        local key value
        local svn_remote svn_url_pattern count n
-       local upstream=git legacy="" verbose=""
+       local upstream=git legacy="" verbose="" name=""
 
        svn_remote=()
        # get some config options from git-config
@@ -128,8 +119,8 @@ __git_ps1_show_upstream ()
                        fi
                        ;;
                svn-remote.*.url)
-                       svn_remote[ $((${#svn_remote[@]} + 1)) ]="$value"
-                       svn_url_pattern+="\\|$value"
+                       svn_remote[$((${#svn_remote[@]} + 1))]="$value"
+                       svn_url_pattern="$svn_url_pattern\\|$value"
                        upstream=svn+git # default upstream is SVN if available, else git
                        ;;
                esac
@@ -141,6 +132,7 @@ __git_ps1_show_upstream ()
                git|svn) upstream="$option" ;;
                verbose) verbose=1 ;;
                legacy)  legacy=1  ;;
+               name)    name=1 ;;
                esac
        done
 
@@ -150,10 +142,11 @@ __git_ps1_show_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 \
+               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[${#svn_upstream[@]} - 2]}
                        svn_upstream=${svn_upstream%@*}
                        local n_stop="${#svn_remote[@]}"
                        for ((n=1; n <= n_stop; n++)); do
@@ -222,10 +215,74 @@ __git_ps1_show_upstream ()
                *)          # diverged from upstream
                        p=" u+${count#* }-${count%      *}" ;;
                esac
+               if [[ -n "$count" && -n "$name" ]]; then
+                       __git_ps1_upstream_name=$(git rev-parse \
+                               --abbrev-ref "$upstream" 2>/dev/null)
+                       if [ $pcmode = yes ] && [ $ps1_expanded = yes ]; then
+                               p="$p \${__git_ps1_upstream_name}"
+                       else
+                               p="$p ${__git_ps1_upstream_name}"
+                               # not needed anymore; keep user's
+                               # environment clean
+                               unset __git_ps1_upstream_name
+                       fi
+               fi
+       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_eread ()
+{
+       local f="$1"
+       shift
+       test -r "$f" && read "$@" <"$f"
+}
 
 # __git_ps1 accepts 0 or 1 arguments (i.e., format string)
 # when called from PS1 using command substitution
@@ -240,6 +297,8 @@ __git_ps1_show_upstream ()
 # In this mode you can request colored hints using GIT_PS1_SHOWCOLORHINTS=true
 __git_ps1 ()
 {
+       # preserve exit status
+       local exit=$?
        local pcmode=no
        local detached=no
        local ps1pc_start='\u@\h:\w '
@@ -251,55 +310,132 @@ __git_ps1 ()
                        ps1pc_start="$1"
                        ps1pc_end="$2"
                        printf_format="${3:-$printf_format}"
+                       # set PS1 to a plain prompt so that we can
+                       # simply return early if the prompt should not
+                       # be decorated
+                       PS1="$ps1pc_start$ps1pc_end"
                ;;
                0|1)    printf_format="${1:-$printf_format}"
                ;;
-               *)      return
+               *)      return $exit
                ;;
        esac
 
-       local g="$(__gitdir)"
-       if [ -z "$g" ]; then
-               if [ $pcmode = yes ]; then
-                       #In PC mode PS1 always needs to be set
-                       PS1="$ps1pc_start$ps1pc_end"
+       # ps1_expanded:  This variable is set to 'yes' if the shell
+       # subjects the value of PS1 to parameter expansion:
+       #
+       #   * bash does unless the promptvars option is disabled
+       #   * zsh does not unless the PROMPT_SUBST option is set
+       #   * POSIX shells always do
+       #
+       # If the shell would expand the contents of PS1 when drawing
+       # the prompt, a raw ref name must not be included in PS1.
+       # This protects the user from arbitrary code execution via
+       # specially crafted ref names.  For example, a ref named
+       # 'refs/heads/$(IFS=_;cmd=sudo_rm_-rf_/;$cmd)' might cause the
+       # shell to execute 'sudo rm -rf /' when the prompt is drawn.
+       #
+       # Instead, the ref name should be placed in a separate global
+       # variable (in the __git_ps1_* namespace to avoid colliding
+       # with the user's environment) and that variable should be
+       # referenced from PS1.  For example:
+       #
+       #     __git_ps1_foo=$(do_something_to_get_ref_name)
+       #     PS1="...stuff...\${__git_ps1_foo}...stuff..."
+       #
+       # If the shell does not expand the contents of PS1, the raw
+       # ref name must be included in PS1.
+       #
+       # The value of this variable is only relevant when in pcmode.
+       #
+       # Assume that the shell follows the POSIX specification and
+       # expands PS1 unless determined otherwise.  (This is more
+       # likely to be correct if the user has a non-bash, non-zsh
+       # shell and safer than the alternative if the assumption is
+       # incorrect.)
+       #
+       local ps1_expanded=yes
+       [ -z "$ZSH_VERSION" ] || [[ -o PROMPT_SUBST ]] || ps1_expanded=no
+       [ -z "$BASH_VERSION" ] || shopt -q promptvars || ps1_expanded=no
+
+       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
+               return $exit
+       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'*}"
+
+       if [ "true" = "$inside_worktree" ] &&
+          [ -n "${GIT_PS1_HIDE_IF_PWD_IGNORED-}" ] &&
+          [ "$(git config --bool bash.hideIfPwdIgnored)" != "false" ] &&
+          git check-ignore -q .
+       then
+               return $exit
+       fi
+
+       local r=""
+       local b=""
+       local step=""
+       local total=""
+       if [ -d "$g/rebase-merge" ]; then
+               __git_eread "$g/rebase-merge/head-name" b
+               __git_eread "$g/rebase-merge/msgnum" step
+               __git_eread "$g/rebase-merge/end" total
+               if [ -f "$g/rebase-merge/interactive" ]; then
+                       r="|REBASE-i"
+               else
+                       r="|REBASE-m"
                fi
        else
-               local r=""
-               local b=""
-               local step=""
-               local total=""
-               if [ -d "$g/rebase-merge" ]; then
-                       b="$(cat "$g/rebase-merge/head-name")"
-                       step=$(cat "$g/rebase-merge/msgnum")
-                       total=$(cat "$g/rebase-merge/end")
-                       if [ -f "$g/rebase-merge/interactive" ]; then
-                               r="|REBASE-i"
+               if [ -d "$g/rebase-apply" ]; then
+                       __git_eread "$g/rebase-apply/next" step
+                       __git_eread "$g/rebase-apply/last" total
+                       if [ -f "$g/rebase-apply/rebasing" ]; then
+                               __git_eread "$g/rebase-apply/head-name" b
+                               r="|REBASE"
+                       elif [ -f "$g/rebase-apply/applying" ]; then
+                               r="|AM"
                        else
-                               r="|REBASE-m"
+                               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
-                       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
-                                       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"
+                       local head=""
+                       if ! __git_eread "$g/HEAD" head; then
+                               return $exit
                        fi
-
-                       b="$(git symbolic-ref HEAD 2>/dev/null)" || {
+                       # is it a symbolic ref?
+                       b="${head#ref: }"
+                       if [ "$head" = "$b" ]; then
                                detached=yes
                                b="$(
                                case "${GIT_PS1_DESCRIBE_STYLE-}" in
@@ -313,105 +449,84 @@ __git_ps1 ()
                                        git describe --tags --exact-match HEAD ;;
                                esac 2>/dev/null)" ||
 
-                               b="$(cut -c1-7 "$g/HEAD" 2>/dev/null)..." ||
-                               b="unknown"
+                               b="$short_sha..."
                                b="($b)"
-                       }
+                       fi
                fi
+       fi
 
-               if [ -n "$step" ] && [ -n "$total" ]; then
-                       r="$r $step/$total"
-               fi
+       if [ -n "$step" ] && [ -n "$total" ]; then
+               r="$r $step/$total"
+       fi
 
-               local w=""
-               local i=""
-               local s=""
-               local u=""
-               local c=""
-               local p=""
+       local w=""
+       local i=""
+       local s=""
+       local u=""
+       local c=""
+       local p=""
 
-               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
-                               c="BARE:"
+       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
-                               b="GIT_DIR!"
-                       fi
-               elif [ "true" = "$(git rev-parse --is-inside-work-tree 2>/dev/null)" ]; then
-                       if [ -n "${GIT_PS1_SHOWDIRTYSTATE-}" ] &&
-                          [ "$(git config --bool bash.showDirtyState)" != "false" ]
-                       then
-                               git diff --no-ext-diff --quiet --exit-code || w="*"
-                               if git rev-parse --quiet --verify HEAD >/dev/null; then
-                                       git diff-index --cached --quiet HEAD -- || i="+"
-                               else
-                                       i="#"
-                               fi
-                       fi
-                       if [ -n "${GIT_PS1_SHOWSTASHSTATE-}" ]; then
-                               git rev-parse --verify refs/stash >/dev/null 2>&1 && s="$"
+                               i="#"
                        fi
+               fi
+               if [ -n "${GIT_PS1_SHOWSTASHSTATE-}" ] &&
+                  git rev-parse --verify --quiet refs/stash >/dev/null
+               then
+                       s="$"
+               fi
 
-                       if [ -n "${GIT_PS1_SHOWUNTRACKEDFILES-}" ] &&
-                          [ "$(git config --bool bash.showUntrackedFiles)" != "false" ] &&
-                          [ -n "$(git ls-files --others --exclude-standard)" ]
-                       then
-                               u="%${ZSH_VERSION+%}"
-                       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
+               if [ -n "${GIT_PS1_SHOWUPSTREAM-}" ]; then
+                       __git_ps1_show_upstream
                fi
+       fi
 
-               local z="${GIT_PS1_STATESEPARATOR-" "}"
-               local f="$w$i$s$u"
-               if [ $pcmode = yes ]; then
-                       local gitstring=
-                       if [ -n "${GIT_PS1_SHOWCOLORHINTS-}" ]; then
-                               local c_red='\e[31m'
-                               local c_green='\e[32m'
-                               local c_lblue='\e[1;34m'
-                               local c_clear='\e[0m'
-                               local bad_color=$c_red
-                               local ok_color=$c_green
-                               local branch_color="$c_clear"
-                               local flags_color="$c_lblue"
-                               local branchstring="$c${b##refs/heads/}"
-
-                               if [ $detached = no ]; then
-                                       branch_color="$ok_color"
-                               else
-                                       branch_color="$bad_color"
-                               fi
-
-                               # Setting gitstring directly with \[ and \] around colors
-                               # is necessary to prevent wrapping issues!
-                               gitstring="\[$branch_color\]$branchstring\[$c_clear\]"
-
-                               if [ -n "$w$i$s$u$r$p" ]; then
-                                       gitstring="$gitstring$z"
-                               fi
-                               if [ "$w" = "*" ]; then
-                                       gitstring="$gitstring\[$bad_color\]$w"
-                               fi
-                               if [ -n "$i" ]; then
-                                       gitstring="$gitstring\[$ok_color\]$i"
-                               fi
-                               if [ -n "$s" ]; then
-                                       gitstring="$gitstring\[$flags_color\]$s"
-                               fi
-                               if [ -n "$u" ]; then
-                                       gitstring="$gitstring\[$bad_color\]$u"
-                               fi
-                               gitstring="$gitstring\[$c_clear\]$r$p"
-                       else
-                               gitstring="$c${b##refs/heads/}${f:+$z$f}$r$p"
-                       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
+
+       b=${b##refs/heads/}
+       if [ $pcmode = yes ] && [ $ps1_expanded = yes ]; then
+               __git_ps1_branch_name=$b
+               b="\${__git_ps1_branch_name}"
+       fi
+
+       local f="$w$i$s$u"
+       local gitstring="$c$b${f:+$z$f}$r$p"
+
+       if [ $pcmode = yes ]; then
+               if [ "${__git_printf_supports_v-}" != yes ]; then
                        gitstring=$(printf -- "$printf_format" "$gitstring")
-                       PS1="$ps1pc_start$gitstring$ps1pc_end"
                else
-                       # NO color option unless in PROMPT_COMMAND mode
-                       printf -- "$printf_format" "$c${b##refs/heads/}${f:+$z$f}$r$p"
+                       printf -v gitstring -- "$printf_format" "$gitstring"
                fi
+               PS1="$ps1pc_start$gitstring$ps1pc_end"
+       else
+               printf -- "$printf_format" "$gitstring"
        fi
+
+       return $exit
 }