Parent directory

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}