git.zsh
13929 bytes
1autoload -Uz is-at-least
2
3# The git prompt's git commands are read-only and should not interfere with
4# other processes. This environment variable is equivalent to running with `git
5# --no-optional-locks`, but falls back gracefully for older versions of git.
6# See git(1) for and git-status(1) for a description of that flag.
7#
8# We wrap in a local function instead of exporting the variable directly in
9# order to avoid interfering with manually-run git commands by the user.
10function __git_prompt_git() {
11 GIT_OPTIONAL_LOCKS=0 command git "$@"
12}
13
14function _omz_git_prompt_info() {
15 # If we are on a folder not tracked by git, get out.
16 # Otherwise, check for hide-info at global and local repository level
17 if ! __git_prompt_git rev-parse --git-dir &> /dev/null \
18 || [[ "$(__git_prompt_git config --get oh-my-zsh.hide-info 2>/dev/null)" == 1 ]]; then
19 return 0
20 fi
21
22 # Get either:
23 # - the current branch name
24 # - the tag name if we are on a tag
25 # - the short SHA of the current commit
26 local ref
27 ref=$(__git_prompt_git symbolic-ref --short HEAD 2> /dev/null) \
28 || ref=$(__git_prompt_git describe --tags --exact-match HEAD 2> /dev/null) \
29 || ref=$(__git_prompt_git rev-parse --short HEAD 2> /dev/null) \
30 || return 0
31
32 # Use global ZSH_THEME_GIT_SHOW_UPSTREAM=1 for including upstream remote info
33 local upstream
34 if (( ${+ZSH_THEME_GIT_SHOW_UPSTREAM} )); then
35 upstream=$(__git_prompt_git rev-parse --abbrev-ref --symbolic-full-name "@{upstream}" 2>/dev/null) \
36 && upstream=" -> ${upstream}"
37 fi
38
39 echo "${ZSH_THEME_GIT_PROMPT_PREFIX}${ref//\%/%%}${upstream//\%/%%}$(parse_git_dirty)${ZSH_THEME_GIT_PROMPT_SUFFIX}"
40}
41
42function _omz_git_prompt_status() {
43 # OHMYZSH-13330: avoid "regex matching error: illegal byte sequence".
44 # zsh's "=~" operator delegates to the C library regex, which aborts with
45 # REG_ILLSEQ when the subject contains an invalid byte sequence under a
46 # multibyte locale (e.g. a filename with non-UTF-8 bytes in `git status`
47 # output). Forcing the C locale makes every byte a valid character, so the
48 # regex matching below never fails that way. `git status --porcelain` is
49 # locale-independent, so this does not change the parsed output.
50 local -x LC_ALL=C
51
52 [[ "$(__git_prompt_git config --get oh-my-zsh.hide-status 2>/dev/null)" = 1 ]] && return
53
54 # Maps a git status prefix to an internal constant
55 # This cannot use the prompt constants, as they may be empty
56 local -A prefix_constant_map
57 prefix_constant_map=(
58 '\?\? ' 'UNTRACKED'
59 'A ' 'ADDED'
60 'M ' 'MODIFIED'
61 'MM ' 'MODIFIED'
62 ' M ' 'MODIFIED'
63 'AM ' 'MODIFIED'
64 ' T ' 'MODIFIED'
65 'R ' 'RENAMED'
66 ' D ' 'DELETED'
67 'D ' 'DELETED'
68 'UU ' 'UNMERGED'
69 'ahead' 'AHEAD'
70 'behind' 'BEHIND'
71 'diverged' 'DIVERGED'
72 'stashed' 'STASHED'
73 )
74
75 # Maps the internal constant to the prompt theme
76 local -A constant_prompt_map
77 constant_prompt_map=(
78 'UNTRACKED' "$ZSH_THEME_GIT_PROMPT_UNTRACKED"
79 'ADDED' "$ZSH_THEME_GIT_PROMPT_ADDED"
80 'MODIFIED' "$ZSH_THEME_GIT_PROMPT_MODIFIED"
81 'RENAMED' "$ZSH_THEME_GIT_PROMPT_RENAMED"
82 'DELETED' "$ZSH_THEME_GIT_PROMPT_DELETED"
83 'UNMERGED' "$ZSH_THEME_GIT_PROMPT_UNMERGED"
84 'AHEAD' "$ZSH_THEME_GIT_PROMPT_AHEAD"
85 'BEHIND' "$ZSH_THEME_GIT_PROMPT_BEHIND"
86 'DIVERGED' "$ZSH_THEME_GIT_PROMPT_DIVERGED"
87 'STASHED' "$ZSH_THEME_GIT_PROMPT_STASHED"
88 )
89
90 # The order that the prompt displays should be added to the prompt
91 local status_constants
92 status_constants=(
93 UNTRACKED ADDED MODIFIED RENAMED DELETED
94 STASHED UNMERGED AHEAD BEHIND DIVERGED
95 )
96
97 local status_text
98 status_text="$(__git_prompt_git status --porcelain -b 2> /dev/null)"
99
100 # Don't continue on a catastrophic failure
101 if [[ $? -eq 128 ]]; then
102 return 1
103 fi
104
105 # A lookup table of each git status encountered
106 local -A statuses_seen
107
108 if __git_prompt_git rev-parse --verify refs/stash &>/dev/null; then
109 statuses_seen[STASHED]=1
110 fi
111
112 local status_lines
113 status_lines=("${(@f)${status_text}}")
114
115 # If the tracking line exists, get and parse it
116 if [[ "$status_lines[1]" =~ "^## [^ ]+ \[(.*)\]" ]]; then
117 local branch_statuses
118 branch_statuses=("${(@s/,/)match}")
119 for branch_status in $branch_statuses; do
120 if [[ ! $branch_status =~ "(behind|diverged|ahead) ([0-9]+)?" ]]; then
121 continue
122 fi
123 local last_parsed_status=$prefix_constant_map[$match[1]]
124 statuses_seen[$last_parsed_status]=$match[2]
125 done
126 fi
127
128 # For each status prefix, do a regex comparison
129 for status_prefix in "${(@k)prefix_constant_map}"; do
130 local status_constant="${prefix_constant_map[$status_prefix]}"
131 local status_regex=$'(^|\n)'"$status_prefix"
132
133 if [[ "$status_text" =~ $status_regex ]]; then
134 statuses_seen[$status_constant]=1
135 fi
136 done
137
138 # Display the seen statuses in the order specified
139 local status_prompt
140 for status_constant in $status_constants; do
141 if (( ${+statuses_seen[$status_constant]} )); then
142 local next_display=$constant_prompt_map[$status_constant]
143 status_prompt="$next_display$status_prompt"
144 fi
145 done
146
147 echo $status_prompt
148}
149
150# Use async version if setting is enabled, or unset but zsh version is at least 5.0.6.
151# This avoids async prompt issues caused by previous zsh versions:
152# - https://github.com/ohmyzsh/ohmyzsh/issues/12331
153# - https://github.com/ohmyzsh/ohmyzsh/issues/12360
154# TODO(2024-06-12): @mcornella remove workaround when CentOS 7 reaches EOL
155local _style
156if zstyle -t ':omz:alpha:lib:git' async-prompt \
157 || { is-at-least 5.0.6 && zstyle -T ':omz:alpha:lib:git' async-prompt }; then
158 function git_prompt_info() {
159 if [[ -n "${_OMZ_ASYNC_OUTPUT[_omz_git_prompt_info]}" ]]; then
160 echo -n "${_OMZ_ASYNC_OUTPUT[_omz_git_prompt_info]}"
161 fi
162 }
163
164 function git_prompt_status() {
165 if [[ -n "${_OMZ_ASYNC_OUTPUT[_omz_git_prompt_status]}" ]]; then
166 echo -n "${_OMZ_ASYNC_OUTPUT[_omz_git_prompt_status]}"
167 fi
168 }
169
170 # Conditionally register the async handler, only if it's needed in $PROMPT
171 # or any of the other prompt variables
172 function _defer_async_git_register() {
173 # Check if git_prompt_info is used in a prompt variable
174 case "${PS1}:${PS2}:${PS3}:${PS4}:${RPROMPT-}:${RPS1-}:${RPS2-}:${RPS3-}:${RPS4-}" in
175 *(\$\(git_prompt_info\)|\`git_prompt_info\`)*)
176 _omz_register_handler _omz_git_prompt_info
177 ;;
178 esac
179
180 case "${PS1}:${PS2}:${PS3}:${PS4}:${RPROMPT-}:${RPS1-}:${RPS2-}:${RPS3-}:${RPS4-}" in
181 *(\$\(git_prompt_status\)|\`git_prompt_status\`)*)
182 _omz_register_handler _omz_git_prompt_status
183 ;;
184 esac
185
186 add-zsh-hook -d precmd _defer_async_git_register
187 unset -f _defer_async_git_register
188 }
189
190 # Register the async handler first. This needs to be done before
191 # the async request prompt is run
192 precmd_functions=(_defer_async_git_register $precmd_functions)
193elif zstyle -s ':omz:alpha:lib:git' async-prompt _style && [[ $_style == "force" ]]; then
194 function git_prompt_info() {
195 if [[ -n "${_OMZ_ASYNC_OUTPUT[_omz_git_prompt_info]}" ]]; then
196 echo -n "${_OMZ_ASYNC_OUTPUT[_omz_git_prompt_info]}"
197 fi
198 }
199
200 function git_prompt_status() {
201 if [[ -n "${_OMZ_ASYNC_OUTPUT[_omz_git_prompt_status]}" ]]; then
202 echo -n "${_OMZ_ASYNC_OUTPUT[_omz_git_prompt_status]}"
203 fi
204 }
205
206 _omz_register_handler _omz_git_prompt_info
207 _omz_register_handler _omz_git_prompt_status
208else
209 function git_prompt_info() {
210 _omz_git_prompt_info
211 }
212 function git_prompt_status() {
213 _omz_git_prompt_status
214 }
215fi
216
217# Checks if working tree is dirty
218function parse_git_dirty() {
219 local STATUS
220 local -a FLAGS
221 FLAGS=('--porcelain')
222 if [[ "$(__git_prompt_git config --get oh-my-zsh.hide-dirty)" != "1" ]]; then
223 if [[ "${DISABLE_UNTRACKED_FILES_DIRTY:-}" == "true" ]]; then
224 FLAGS+='--untracked-files=no'
225 fi
226 case "${GIT_STATUS_IGNORE_SUBMODULES:-}" in
227 git)
228 # let git decide (this respects per-repo config in .gitmodules)
229 ;;
230 *)
231 # if unset: ignore dirty submodules
232 # other values are passed to --ignore-submodules
233 FLAGS+="--ignore-submodules=${GIT_STATUS_IGNORE_SUBMODULES:-dirty}"
234 ;;
235 esac
236 STATUS=$(__git_prompt_git status ${FLAGS} 2> /dev/null | tail -n 1)
237 fi
238 if [[ -n $STATUS ]]; then
239 echo "$ZSH_THEME_GIT_PROMPT_DIRTY"
240 else
241 echo "$ZSH_THEME_GIT_PROMPT_CLEAN"
242 fi
243}
244
245# Gets the difference between the local and remote branches
246function git_remote_status() {
247 local remote ahead behind git_remote_status git_remote_status_detailed
248 remote=${$(__git_prompt_git rev-parse --verify ${hook_com[branch]}@{upstream} --symbolic-full-name 2>/dev/null)/refs\/remotes\/}
249 if [[ -n ${remote} ]]; then
250 ahead=$(__git_prompt_git rev-list ${hook_com[branch]}@{upstream}..HEAD 2>/dev/null | wc -l)
251 behind=$(__git_prompt_git rev-list HEAD..${hook_com[branch]}@{upstream} 2>/dev/null | wc -l)
252
253 if [[ $ahead -eq 0 ]] && [[ $behind -eq 0 ]]; then
254 git_remote_status="$ZSH_THEME_GIT_PROMPT_EQUAL_REMOTE"
255 elif [[ $ahead -gt 0 ]] && [[ $behind -eq 0 ]]; then
256 git_remote_status="$ZSH_THEME_GIT_PROMPT_AHEAD_REMOTE"
257 git_remote_status_detailed="$ZSH_THEME_GIT_PROMPT_AHEAD_REMOTE_COLOR$ZSH_THEME_GIT_PROMPT_AHEAD_REMOTE$((ahead))%{$reset_color%}"
258 elif [[ $behind -gt 0 ]] && [[ $ahead -eq 0 ]]; then
259 git_remote_status="$ZSH_THEME_GIT_PROMPT_BEHIND_REMOTE"
260 git_remote_status_detailed="$ZSH_THEME_GIT_PROMPT_BEHIND_REMOTE_COLOR$ZSH_THEME_GIT_PROMPT_BEHIND_REMOTE$((behind))%{$reset_color%}"
261 elif [[ $ahead -gt 0 ]] && [[ $behind -gt 0 ]]; then
262 git_remote_status="$ZSH_THEME_GIT_PROMPT_DIVERGED_REMOTE"
263 git_remote_status_detailed="$ZSH_THEME_GIT_PROMPT_AHEAD_REMOTE_COLOR$ZSH_THEME_GIT_PROMPT_AHEAD_REMOTE$((ahead))%{$reset_color%}$ZSH_THEME_GIT_PROMPT_BEHIND_REMOTE_COLOR$ZSH_THEME_GIT_PROMPT_BEHIND_REMOTE$((behind))%{$reset_color%}"
264 fi
265
266 if [[ -n $ZSH_THEME_GIT_PROMPT_REMOTE_STATUS_DETAILED ]]; then
267 git_remote_status="$ZSH_THEME_GIT_PROMPT_REMOTE_STATUS_PREFIX${remote//\%/%%}$git_remote_status_detailed$ZSH_THEME_GIT_PROMPT_REMOTE_STATUS_SUFFIX"
268 fi
269
270 echo $git_remote_status
271 fi
272}
273
274# Outputs the name of the current branch
275# Usage example: git pull origin $(git_current_branch)
276# Using '--quiet' with 'symbolic-ref' will not cause a fatal error (128) if
277# it's not a symbolic ref, but in a Git repo.
278function git_current_branch() {
279 local ref
280 ref=$(__git_prompt_git symbolic-ref --quiet HEAD 2> /dev/null)
281 local ret=$?
282 if [[ $ret != 0 ]]; then
283 [[ $ret == 128 ]] && return # no git repo.
284 ref=$(__git_prompt_git rev-parse --short HEAD 2> /dev/null) || return
285 fi
286 echo ${ref#refs/heads/}
287}
288
289# Outputs the name of the previously checked out branch
290# Usage example: git pull origin $(git_previous_branch)
291# rev-parse --symbolic-full-name @{-1} only prints if it is a branch
292function git_previous_branch() {
293 local ref
294 ref=$(__git_prompt_git rev-parse --quiet --symbolic-full-name @{-1} 2> /dev/null)
295 local ret=$?
296 if [[ $ret != 0 ]] || [[ -z $ref ]]; then
297 return # no git repo or non-branch previous ref
298 fi
299 echo ${ref#refs/heads/}
300}
301
302# Gets the number of commits ahead from remote
303function git_commits_ahead() {
304 if __git_prompt_git rev-parse --git-dir &>/dev/null; then
305 local commits="$(__git_prompt_git rev-list --count @{upstream}..HEAD 2>/dev/null)"
306 if [[ -n "$commits" && "$commits" != 0 ]]; then
307 echo "$ZSH_THEME_GIT_COMMITS_AHEAD_PREFIX$commits$ZSH_THEME_GIT_COMMITS_AHEAD_SUFFIX"
308 fi
309 fi
310}
311
312# Gets the number of commits behind remote
313function git_commits_behind() {
314 if __git_prompt_git rev-parse --git-dir &>/dev/null; then
315 local commits="$(__git_prompt_git rev-list --count HEAD..@{upstream} 2>/dev/null)"
316 if [[ -n "$commits" && "$commits" != 0 ]]; then
317 echo "$ZSH_THEME_GIT_COMMITS_BEHIND_PREFIX$commits$ZSH_THEME_GIT_COMMITS_BEHIND_SUFFIX"
318 fi
319 fi
320}
321
322# Outputs if current branch is ahead of remote
323function git_prompt_ahead() {
324 if [[ -n "$(__git_prompt_git rev-list origin/$(git_current_branch)..HEAD 2> /dev/null)" ]]; then
325 echo "$ZSH_THEME_GIT_PROMPT_AHEAD"
326 fi
327}
328
329# Outputs if current branch is behind remote
330function git_prompt_behind() {
331 if [[ -n "$(__git_prompt_git rev-list HEAD..origin/$(git_current_branch) 2> /dev/null)" ]]; then
332 echo "$ZSH_THEME_GIT_PROMPT_BEHIND"
333 fi
334}
335
336# Outputs if current branch exists on remote or not
337function git_prompt_remote() {
338 if [[ -n "$(__git_prompt_git show-ref origin/$(git_current_branch) 2> /dev/null)" ]]; then
339 echo "$ZSH_THEME_GIT_PROMPT_REMOTE_EXISTS"
340 else
341 echo "$ZSH_THEME_GIT_PROMPT_REMOTE_MISSING"
342 fi
343}
344
345# Formats prompt string for current git commit short SHA
346function git_prompt_short_sha() {
347 local SHA
348 SHA=$(__git_prompt_git rev-parse --short HEAD 2> /dev/null) && echo "$ZSH_THEME_GIT_PROMPT_SHA_BEFORE$SHA$ZSH_THEME_GIT_PROMPT_SHA_AFTER"
349}
350
351# Formats prompt string for current git commit long SHA
352function git_prompt_long_sha() {
353 local SHA
354 SHA=$(__git_prompt_git rev-parse HEAD 2> /dev/null) && echo "$ZSH_THEME_GIT_PROMPT_SHA_BEFORE$SHA$ZSH_THEME_GIT_PROMPT_SHA_AFTER"
355}
356
357# Outputs the name of the current user
358# Usage example: $(git_current_user_name)
359function git_current_user_name() {
360 __git_prompt_git config user.name 2>/dev/null
361}
362
363# Outputs the email of the current user
364# Usage example: $(git_current_user_email)
365function git_current_user_email() {
366 __git_prompt_git config user.email 2>/dev/null
367}
368
369# Output the name of the root directory of the git repository
370# Usage example: $(git_repo_name)
371function git_repo_name() {
372 local repo_path
373 if repo_path="$(__git_prompt_git rev-parse --show-toplevel 2>/dev/null)" && [[ -n "$repo_path" ]]; then
374 echo ${repo_path:t}
375 fi
376}