git-prompt.sh 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248
  1. # Display git repo information in a shell prompt
  2. #
  3. # Format:
  4. #
  5. # [repo:branch:commit] BARE BISECT/MERGE/REBASE NO-REMOTE/NO-RTB/AHEAD/BEHIND/DIVERGED WIP STASH +±xcru?
  6. #
  7. # Usage:
  8. #
  9. # Call `__gitp` in such a way that prompt expansion happens i.e. from bash's
  10. # `PROMPT_COMMAND` or zsh's `PROMPT`/`pre_cmd()`.
  11. #
  12. function __gitp() {
  13. # Preserve the exit status of the previous command.
  14. local exit=$?
  15. # Only run if git exists.
  16. [[ -z $(command -v git) ]] && return $exit
  17. # Try to get git info. Also serves to check if current folder is a git repo.
  18. local gitinfo ret
  19. gitinfo=$(git rev-parse \
  20. --git-dir \
  21. --is-bare-repository \
  22. --is-inside-work-tree \
  23. --short HEAD 2>/dev/null)
  24. ret="$?"
  25. [[ -z $gitinfo ]] && return $exit
  26. # Explode git info into individual variables.
  27. local gitdir=${gitinfo%%$'\n'*}; gitinfo=${gitinfo#*$'\n'}
  28. local bare=${gitinfo%%$'\n'*}; gitinfo=${gitinfo#*$'\n'}
  29. local worktree=${gitinfo%%$'\n'*}; gitinfo=${gitinfo#*$'\n'}
  30. local commit; [[ $ret = "0" ]] && commit=$gitinfo
  31. __gitp_cprep
  32. # Prepare display elements.
  33. [[ $bare = 'true' ]] && bare=${GITP_BARE:-"$cRed BARE"} || bare=''
  34. local location="$(__gitp_location $commit)"
  35. if [[ $worktree = 'true' && -z $GITP_SIMPLE ]]; then
  36. # Many things only matter if in a work tree (i.e. not bare or in gitdir).
  37. __gitp_status
  38. local op="$(__gitp_op $gitdir)"
  39. local remote="$(__gitp_remote)"
  40. local wips="$(__gitp_wips $gitdir)"
  41. local changes="$(__gitp_changes)"
  42. fi
  43. # Colour the location if there are local changes.
  44. local dirty=$(__gitp_dirty)
  45. # Display the git prompt.
  46. local formatting=${1:-' %s'}
  47. printf -- "$formatting" "$dirty$location$bare$op$remote$wips$changes"
  48. return $exit
  49. }
  50. # Get git status output for parsing.
  51. function __gitp_status() {
  52. GITP_STATUS=''
  53. # If available, ignore submodule work tree changes to speed up prompt.
  54. GITP_STATUS=$(git status --porcelain --ignore-submodules=dirty 2>/dev/null) \
  55. || GITP_STATUS=$(git status --porcelain)
  56. }
  57. # Initialise colour strings.
  58. function __gitp_cprep() {
  59. if [[ -n ${BASH_VERSION-} ]]; then
  60. cStart=${cStart:-'\[\e['}
  61. cEnd=${cEnd:-'m\]'}
  62. elif [[ -n ${ZSH_VERSION-} ]]; then
  63. cStart=${cStart:-'%{['}
  64. cEnd=${cEnd:-'m%}'}
  65. fi
  66. cReset=${cReset:-$cStart'0'$cEnd}
  67. cRed=$cStart'38;5;1'$cEnd
  68. cGre=$cStart'38;5;2'$cEnd
  69. cBlu=$cStart'38;5;4'$cEnd
  70. cMag=$cStart'38;5;5'$cEnd
  71. cgGre=$cStart'38;5;70'$cEnd
  72. cgRed=$cStart'38;5;124'$cEnd
  73. cgYel=$cStart'38;5;220'$cEnd
  74. }
  75. # Get location i.e. which repo/branch/commit is this?
  76. function __gitp_location() {
  77. local prefix=${GITP_PREFIX:-'['}
  78. local suffix=${GITP_SUFFIX:-"]"}
  79. local repo=$(__gitp_repo)
  80. local branch=$(__gitp_branch)
  81. [[ -z $branch ]] && branch=$(git describe --tags --exact-match 2>/dev/null)
  82. echo "$prefix$repo:$branch:$1$suffix"
  83. }
  84. # Determine if a repo is in the middle of an "operation" e.g. rebase, bisect.
  85. function __gitp_op() {
  86. local bisect=${GITP_BISECT:-"$cRed BISECT"}
  87. local merge=${GITP_MERGE:-"$cRed MERGE"}
  88. local rebase=${GITP_REBASE:-"$cRed REBASE"}
  89. if [[ -e "$1/BISECT_LOG" ]]; then
  90. echo "$bisect"
  91. elif [[ -e "$1/MERGE_HEAD" ]]; then
  92. echo "$merge"
  93. elif [[ -e "$1/rebase" || -e "$1/rebase-apply" || -e "$1/rebase-merge" || -e "$1/../.dotest" ]]; then
  94. echo "$rebase"
  95. fi
  96. }
  97. # Compare the local and remote tracking branches (upstream).
  98. function __gitp_remote() {
  99. local noremote=${GITP_NOREMOTE:-"$cMag NO-REMOTE"}
  100. local nortb=${GITP_NORTB:-"$cMag NO-RTB"}
  101. local ahead=${GITP_AHEAD:-"$cMag AHEAD"}
  102. local behind=${GITP_BEHIND:-"$cMag BEHIND"}
  103. local diverged=${GITP_DIVERGED:-"$cRed DIVERGED"}
  104. local remote commits count ahd=0 bhd=0
  105. # Count commits ahead and behind.
  106. if commits="$(git rev-list --left-right @{upstream}...HEAD 2>/dev/null)"; then
  107. [[ -n ${ZSH_VERSION-} ]] && commits=(${(f)commits})
  108. for commit in $commits; do
  109. case "$commit" in
  110. "<"*) ((bhd++)) ;;
  111. *) ((ahd++)) ;;
  112. esac
  113. done
  114. count="$bhd $ahd"
  115. fi
  116. # Determine what to display.
  117. case "$count" in
  118. "") # no upstream
  119. if [[ -z "$(git remote -v)" ]]; then
  120. remote="$noremote"
  121. elif [[ -n "$(__gitp_branch)" ]]; then
  122. remote="$nortb"
  123. fi
  124. ;;
  125. "0 0") ;; # Equal to upstream.
  126. "0 "*) remote="$ahead-$ahd" ;; # Ahead of upstream.
  127. *" 0") remote="$behind-$bhd" ;; # Behind upstream.
  128. *) remote="$diverged" ;; # Diverged from upstream.
  129. esac
  130. echo "$remote"
  131. }
  132. # Warn if the current commit is a work-in-progress or if there are stashed
  133. # changes.
  134. #
  135. # WIP commits are made with `gwip` and removed with `gunwip`.
  136. function __gitp_wips() {
  137. local wip=${GITP_WIP:-"$cRed WIP"}
  138. local stash=${GITP_STASH:-"$cRed STASH"}
  139. local wips
  140. if $(git log -n 1 2>/dev/null | grep -q -c "\-\-wip\-\-"); then
  141. wips="$wip"
  142. fi
  143. [[ -e "$1/refs/stash" ]] && wips+="$stash"
  144. echo "$wips"
  145. }
  146. # Work tree change indicators
  147. #
  148. # Prints symbol for each change in `git status`, up to the number of symbols
  149. # indicated by `GITP_CHANGES_MAX`. Gives a good visual of what has changed.
  150. #
  151. # Performance note: if the git prompt is slow, it is more because `git status`
  152. # is slow. This function is near-instantaneous since it is just string
  153. # manipulation.
  154. function __gitp_changes() {
  155. [[ -z $GITP_STATUS ]] && return
  156. local changes_max=${GITP_CHANGES_MAX:-20}
  157. local i_col=${GITP_COLOUR_INDEX:-$cgGre}
  158. local w_col=${GITP_COLOUR_WTREE:-$cgRed}
  159. local u_col=${GITP_COLOUR_UN:-$cgYel}
  160. local e_col=${GITP_COLOUR_END:-$cReset}
  161. local lines line X Y x_set y_set u_set end count=0
  162. # Split git status by newline into array elements.
  163. if [[ -n ${BASH_VERSION-} ]]; then
  164. IFS=$'\n'
  165. lines=$GITP_STATUS
  166. elif [[ -n ${ZSH_VERSION-} ]]; then
  167. lines=(${(f)GITP_STATUS})
  168. fi
  169. for line in $lines; do
  170. (( count+=1 ))
  171. (( count >= $changes_max )) && end='..' && break
  172. X=${line:0:1}
  173. Y=${line:1:1}
  174. [[ $X$Y = '??' ]] && u_set+="?" && continue
  175. [[ $X = 'U' ]] || [[ $Y = 'U' ]] && u_set+="u" && continue
  176. [[ $X$Y = 'DD' ]] || [[ $X$Y = 'AA' ]] && u_set+="u" && continue
  177. [[ $Y = 'M' ]] && y_set+="+"
  178. [[ $Y = 'D' ]] && y_set+="x"
  179. [[ $X = 'M' ]] && x_set+="+" && continue
  180. [[ $X = 'A' ]] && x_set+="±" && continue
  181. [[ $X = 'D' ]] && x_set+="x" && continue
  182. [[ $X = 'R' ]] && x_set+="r" && continue
  183. [[ $X = 'C' ]] && x_set+="c" && continue
  184. done
  185. [[ -n ${BASH_VERSION-} ]] && unset IFS
  186. echo " $i_col$x_set$w_col$y_set$u_col$u_set$e_col$end$cReset"
  187. }
  188. # Check if a repo is modified
  189. function __gitp_dirty() {
  190. local clean=${GITP_CLEAN:-$cReset$cGre}
  191. local dirty=${GITP_DIRTY:-$cReset$cBlu}
  192. [[ -z $GITP_STATUS ]] && echo "$clean" || echo "$dirty"
  193. }
  194. # Get repository name
  195. #
  196. # Check for a name in git remotes, between ':' or '/' and a space.
  197. function __gitp_repo() {
  198. local repo="$(git remote -v | head -n 1 2>/dev/null)"
  199. repo=${repo##*[:|/]}
  200. repo=${repo% *}
  201. printf "${repo%.git}"
  202. }
  203. # Get current branch name
  204. #
  205. # Prints the branch name or tag of the current commit, if any.
  206. function __gitp_branch() {
  207. local ref
  208. ref=$(git symbolic-ref --quiet HEAD 2>/dev/null)
  209. printf "${ref#refs/heads/}"
  210. }
  211. # Toggle git prompt's "simple" mode.
  212. # This mode will simply display the location information and bare state. It is
  213. # useful for slow machines (e.g. tablets) or slow repositories (e.g. hundreds of
  214. # changed files). For a more permanent setting, set the `GITP_SIMPLE` variable
  215. # to a non-zero string before `__gitp()` is run.
  216. function toggle_gitp_simple() {
  217. [[ -z $GITP_SIMPLE ]] && GITP_SIMPLE=true || GITP_SIMPLE=''
  218. }