# bash/zsh git prompt support # # Copyright (C) 2006,2007 Shawn O. Pearce # 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,
 and , 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
#     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}
#     svn           always compare HEAD to your SVN upstream
#
# You can change the separator between the branch name and the above
# state symbols by setting GIT_PS1_STATESEPARATOR. The default separator
# is SP.
#
# 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.
#
# 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".

# check whether printf supports -v
setglobal __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
proc __git_ps1_show_upstream
{
	var key = '', value = ''
	var svn_remote = '', svn_url_pattern = '', count = '', n = ''
	var upstream = 'git', legacy = '',"" verbose = '',"" name = ''""

	set svn_remote = ''()
	# get some config options from git-config
	var output = $[git config -z --get-regexp '^(svn-remote\..*\.url|bash\.showupstream)$' !2 >/dev/null | tr '\0\n' '\n ]
	while read -r key value {
		match $key {
		with bash.showupstream
			setglobal GIT_PS1_SHOWUPSTREAM = $value
			if [[ -z "${GIT_PS1_SHOWUPSTREAM}" ]] {
				setglobal p = ''""
				return
			}
			
		with svn-remote.*.url
			compat array-assign svn_remote '$((${#svn_remote[@]} + 1))' $value
			set svn_url_pattern = ""$svn_url_pattern\\|$value""
			set upstream = 'svn+git' # default upstream is SVN if available, else git
			
		}
	} <<< "$output"

	# parse configuration values
	for option in [$(GIT_PS1_SHOWUPSTREAM)] {
		match $option {
		with git|svn set upstream = $option 
		with verbose set verbose = '1' 
		with legacy  set legacy = '1'  
		with name    set name = '1' 
		}
	}

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

			if [[ -z "$svn_upstream" ]] {
				# default branch name for checkouts with no layout:
				set upstream = $(GIT_SVN_ID:-git-svn)
			} else {
				set upstream = $(svn_upstream#/)
			}
		} elif [[ "svn+git" = "$upstream" ]] {
			set upstream = '"@{upstream}'"
		}
		
	}

	# Find how many commits we are ahead/behind our upstream
	if [[ -z "$legacy" ]] {
		set count = $[git rev-list --count --left-right \
				"$upstream"...HEAD !2 >/dev/null]
	} else {
		# produce equivalent output to --count for older versions of git
		var commits = ''
		if set commits = $[git rev-list --left-right "$upstream"...HEAD !2 >/dev/null]
		{
			var commit = '', behind = '0', ahead = '0'
			for commit in [$commits]
			{
				match $commit {
				with "<"* sh-expr 'behind++' 
				with *    sh-expr 'ahead++'  
				}
			}
			set count = ""$behind	$ahead""
		} else {
			set count = ''""
		}
	}

	# calculate the result
	if [[ -z "$verbose" ]] {
		match $count {
		with "" # no upstream
			setglobal p = ''"" 
		with "0	0" # equal to upstream
			setglobal p = '"='" 
		with "0	"* # ahead of upstream
			setglobal p = '">'" 
		with *"	0" # behind upstream
			setglobal p = '"<'" 
		with *	    # diverged from upstream
			setglobal p = '"<>'" 
		}
	} else {
		match $count {
		with "" # no upstream
			setglobal p = ''"" 
		with "0	0" # equal to upstream
			setglobal p = '" u='" 
		with "0	"* # ahead of upstream
			setglobal p = "" u+$(count#0	)"" 
		with *"	0" # behind upstream
			setglobal p = "" u-$(count%	0)"" 
		with *	    # diverged from upstream
			setglobal p = "" u+$(count#*	)-$(count%	*)"" 
		}
		if [[ -n "$count" && -n "$name" ]] {
			setglobal __git_ps1_upstream_name = $[git rev-parse \
				--abbrev-ref $upstream !2 >/dev/null]
			if test $pcmode = yes && test $ps1_expanded = yes {
				setglobal p = ""$p \${__git_ps1_upstream_name}""
			} else {
				setglobal p = ""$p $(__git_ps1_upstream_name)""
				# not needed anymore; keep user's
				# environment clean
				unset __git_ps1_upstream_name
			}
		}
	}

}

# 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.
proc __git_ps1_colorize_gitstring
{
	if [[ -n ${ZSH_VERSION-} ]] {
		var c_red = ''%F{red}''
		var c_green = ''%F{green}''
		var c_lblue = ''%F{blue}''
		var c_clear = ''%f''
	} else {
		# Using \[ and \] around colors is necessary to prevent
		# issues with command line editing/browsing/completion!
		var c_red = ''\[\e[31m\]''
		var c_green = ''\[\e[32m\]''
		var c_lblue = ''\[\e[1;34m\]''
		var c_clear = ''\[\e[0m\]''
	}
	var bad_color = $c_red
	var ok_color = $c_green
	var flags_color = $c_lblue

	var branch_color = ''""
	if test $detached = no {
		set branch_color = $ok_color
	} else {
		set branch_color = $bad_color
	}
	setglobal c = ""$branch_color$c""

	setglobal z = ""$c_clear$z""
	if test $w = "*" {
		setglobal w = ""$bad_color$w""
	}
	if test -n $i {
		setglobal i = ""$ok_color$i""
	}
	if test -n $s {
		setglobal s = ""$flags_color$s""
	}
	if test -n $u {
		setglobal u = ""$bad_color$u""
	}
	setglobal r = ""$c_clear$r""
}

proc __git_eread
{
	var f = $1
	shift
	test -r $f && read @Argv <$f
}

# __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
proc __git_ps1
{
	# preserve exit status
	var exit = $Status
	var pcmode = 'no'
	var detached = 'no'
	var ps1pc_start = ''\u@\h:\w ''
	var ps1pc_end = ''\$ ''
	var printf_format = '' (%s)''

	match "$Argc" {
		with 2|3	set pcmode = 'yes'
			set ps1pc_start = $1
			set ps1pc_end = $2
			set 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
			setglobal PS1 = ""$ps1pc_start$ps1pc_end""
		
		with 0|1	set printf_format = $(1:-$printf_format)
		
		with *	return $exit
		
	}

	# 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.)
	#
	var ps1_expanded = 'yes'
	test -z $(ZSH_VERSION-) || [[ -o PROMPT_SUBST ]] || set ps1_expanded = 'no'
	test -z $(BASH_VERSION-) || shopt -q promptvars || set ps1_expanded = 'no'

	var repo_info = '', rev_parse_exit_code = ''
	set repo_info = $[git rev-parse --git-dir --is-inside-git-dir \
		--is-bare-repository --is-inside-work-tree \
		--short HEAD !2 >/dev/null]
	set rev_parse_exit_code = "$Status"

	if test -z $repo_info {
		return $exit
	}

	var short_sha = ''""
	if test $rev_parse_exit_code = "0" {
		set short_sha = $(repo_info##*$'\n')
		set repo_info = $(repo_info%$'\n'*)
	}
	var inside_worktree = $(repo_info##*$'\n')
	set repo_info = $(repo_info%$'\n'*)
	var bare_repo = $(repo_info##*$'\n')
	set repo_info = $(repo_info%$'\n'*)
	var inside_gitdir = $(repo_info##*$'\n')
	var g = $(repo_info%$'\n'*)

	if test "true" = $inside_worktree &&
	   test -n $(GIT_PS1_HIDE_IF_PWD_IGNORED-) &&
	   test $[git config --bool bash.hideIfPwdIgnored] != "false" &&
	   git check-ignore -q .
	{
		return $exit
	}

	var r = ''""
	var b = ''""
	var step = ''""
	var total = ''""
	if test -d "$g/rebase-merge" {
		__git_eread "$g/rebase-merge/head-name" b
		__git_eread "$g/rebase-merge/msgnum" step
		__git_eread "$g/rebase-merge/end" total
		if test -f "$g/rebase-merge/interactive" {
			set r = '"|REBASE-i'"
		} else {
			set r = '"|REBASE-m'"
		}
	} else {
		if test -d "$g/rebase-apply" {
			__git_eread "$g/rebase-apply/next" step
			__git_eread "$g/rebase-apply/last" total
			if test -f "$g/rebase-apply/rebasing" {
				__git_eread "$g/rebase-apply/head-name" b
				set r = '"|REBASE'"
			} elif test -f "$g/rebase-apply/applying" {
				set r = '"|AM'"
			} else {
				set r = '"|AM/REBASE'"
			}
		} elif test -f "$g/MERGE_HEAD" {
			set r = '"|MERGING'"
		} elif test -f "$g/CHERRY_PICK_HEAD" {
			set r = '"|CHERRY-PICKING'"
		} elif test -f "$g/REVERT_HEAD" {
			set r = '"|REVERTING'"
		} elif test -f "$g/BISECT_LOG" {
			set r = '"|BISECTING'"
		}

		if test -n $b {
			:
		} elif test -h "$g/HEAD" {
			# symlink symbolic ref
			set b = $[git symbolic-ref HEAD !2 >/dev/null]
		} else {
			var head = ''""
			if ! __git_eread "$g/HEAD" head {
				return $exit
			}
			# is it a symbolic ref?
			set b = $(head#ref: )
			if test $head = $b {
				set detached = 'yes'
				set b = $[
				match $(GIT_PS1_DESCRIBE_STYLE-) {
				with (contains
					git describe --contains HEAD 
				with (branch
					git describe --contains --all HEAD 
				with (describe
					git describe HEAD 
				with (* | default
					git describe --tags --exact-match HEAD 
				}] ||

				set b = ""$short_sha...""
				set b = ""($b)""
			}
		}
	}

	if test -n $step && test -n $total {
		set r = ""$r $step/$total""
	}

	var w = ''""
	var i = ''""
	var s = ''""
	var u = ''""
	var c = ''""
	var p = ''""

	if test "true" = $inside_gitdir {
		if test "true" = $bare_repo {
			set c = '"BARE:'"
		} else {
			set b = '"GIT_DIR!'"
		}
	} elif test "true" = $inside_worktree {
		if test -n $(GIT_PS1_SHOWDIRTYSTATE-) &&
		   test $[git config --bool bash.showDirtyState] != "false"
		{
			git diff --no-ext-diff --quiet || set w = '"*'"
			git diff --no-ext-diff --cached --quiet || set i = '"+'"
			if test -z $short_sha && test -z $i {
				set i = '"#'"
			}
		}
		if test -n $(GIT_PS1_SHOWSTASHSTATE-) &&
		   git rev-parse --verify --quiet refs/stash >/dev/null
		{
			set s = '"$'"
		}

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

		if test -n $(GIT_PS1_SHOWUPSTREAM-) {
			__git_ps1_show_upstream
		}
	}

	var z = $(GIT_PS1_STATESEPARATOR-" ")

	# NO color option unless in PROMPT_COMMAND mode
	if test $pcmode = yes && test -n $(GIT_PS1_SHOWCOLORHINTS-) {
		__git_ps1_colorize_gitstring
	}

	set b = $(b##refs/heads/)
	if test $pcmode = yes && test $ps1_expanded = yes {
		setglobal __git_ps1_branch_name = $b
		set b = '"\${__git_ps1_branch_name}'"
	}

	var f = ""$w$i$s$u""
	var gitstring = ""$c$b$(f:+$z$f)$r$p""

	if test $pcmode = yes {
		if test $(__git_printf_supports_v-) != yes {
			set gitstring = $[printf -- $printf_format $gitstring]
		} else {
			printf -v gitstring -- $printf_format $gitstring
		}
		setglobal PS1 = ""$ps1pc_start$gitstring$ps1pc_end""
	} else {
		printf -- $printf_format $gitstring
	}

	return $exit
}