一个程序员的辩白

13 May 2021

Oh My Zsh git status 响应过慢的解决方案

ohmyzsh/ohmyzsh内置的众多主题中大部分都支持显示git相关的信息。

比如默认的robbyrussell主题:

你可以通过

grep -IRl '$(git_prompt_info)' ~/.oh-my-zsh/themes | sort -u | xargs -I{} basename {} .zsh-theme

列出Oh My Zsh所有支持显示git status的主题。

 

不过当你git clone一个有很多commit历史的Repository的时候,你就会发现Oh My Zsh在终端的体验极差,按一次回车可能需要得等好几秒。

原因是git status这个命令占用了主要的耗时,git status需要找到索引文件和工作区目录之间的差别,这就需要扫描所有的索引文件,产生大量的磁盘IO(stat(2))。

The most resource-intensive part of the status command is finding the difference between index and workdir (git_diff_index_to_workdir in libgit2). Index is a list of all files in the git repository with their last modification times.

https://github.com/romkatv/gitstatus#problem-statement

 

i9-9900T(8C16T), 16G RAM, 512G M.2 NVME SSDchromium/chromium为例,单次git status耗时约800ms。

$ time git status --porcelain
git status --porcelain  0.65s user 0.66s system 166% cpu 0.785 total

以2015款MacBook Pro(i5-5287U 2C4T, 16G 1867MHz DDR3, 512G SATA SSD)为例,单次耗时约5s。

$ time git status --porcelain
git status --porcelain  1.53s user 8.15s system 180% cpu 5.374 total

 

ys主题为例,其文件路径~/.oh-my-zsh/themes/ys.zsh-theme,代码片断:

# ......

# Git info
local git_info='$(git_prompt_info)'

# ......

# Prompt format:
#
# PRIVILEGES USER @ MACHINE in DIRECTORY on git:BRANCH STATE [TIME] C:LAST_EXIT_CODE
# $ COMMAND
#
# For example:
#
# % ys @ ys-mbp in ~/.oh-my-zsh on git:master x [21:47:42] C:0
# $
PROMPT="
%{$terminfo[bold]$fg[blue]%}#%{$reset_color%} \
%(#,%{$bg[yellow]%}%{$fg[black]%}%n%{$reset_color%},%{$fg[cyan]%}%n) \
%{$fg[white]%}@ \
%{$fg[green]%}%m \
%{$fg[white]%}in \
%{$terminfo[bold]$fg[yellow]%}%~%{$reset_color%}\
${hg_info}\
${git_info}\
${venv_info}\
 \
%{$fg[white]%}[%*] $exit_code
%{$terminfo[bold]$fg[red]%}$ %{$reset_color%}"

git信息的显示是通过调用$(git_prompt_info)来实现。git_prompt_info函数实现位于文件~/.oh-my-zsh/lib/git.zsh

# ......
function __git_prompt_git() {
  GIT_OPTIONAL_LOCKS=0 command git "$@"
}

function git_prompt_info() {
  # ......

  echo "${ZSH_THEME_GIT_PROMPT_PREFIX}${ref}${upstream}$(parse_git_dirty)${ZSH_THEME_GIT_PROMPT_SUFFIX}"
}

# Checks if working tree is dirty
function parse_git_dirty() {
  local STATUS
  local -a FLAGS
  FLAGS=('--porcelain')
  if [[ "$(__git_prompt_git config --get oh-my-zsh.hide-dirty)" != "1" ]]; then
    # ......
    STATUS=$(__git_prompt_git status ${FLAGS} 2> /dev/null | tail -1)
  fi
  if [[ -n $STATUS ]]; then
    echo "$ZSH_THEME_GIT_PROMPT_DIRTY"
  else
    echo "$ZSH_THEME_GIT_PROMPT_CLEAN"
  fi
}

可以看出,最终git_prompt_info会调用git status --porcelain ...来获取git相关的信息。

 

最近找到一个解决Oh My Zsh git status过慢的方法:

利用romkatv/gitstatus作为git status的后端用于显示git当前目录的状态。

我们可以参考Using from Zsh一节的描述,利用source ~/gitstatus/gitstatus.prompt.zsh来达到目的。不过其默认的主题样式可能并不是你需要的。

也可以考虑source ~/gitstatus/gitstatus.plugin.zsh,自行定义提示信息。gitstatus.plugin.zsh相对与gitstatus.prompt.zsh而言更加地底层(low-level)。

一个比较好的方式是,romkatv/gitstatus组合我们现在正在使用的主题,这样可以做到改动最小,同时保留我们的使用习惯。

还是以ys主题为例,我们可以这样修改~/.oh-my-zsh/themes/ys.zsh-theme

diff --git a/themes/ys.zsh-theme b/themes/ys.zsh-theme
index 303c898b..f91467eb 100644
--- a/themes/ys.zsh-theme
+++ b/themes/ys.zsh-theme
@@ -13,7 +13,8 @@ YS_VCS_PROMPT_DIRTY=" %{$fg[red]%}x"
 YS_VCS_PROMPT_CLEAN=" %{$fg[green]%}o"
 
 # Git info
-local git_info='$(git_prompt_info)'
+source ~/.gitstatus/gitstatus.prompt.zsh
+local git_info=' git:$GITSTATUS_PROMPT'
 ZSH_THEME_GIT_PROMPT_PREFIX="${YS_VCS_PROMPT_PREFIX1}git${YS_VCS_PROMPT_PREFIX2}"
 ZSH_THEME_GIT_PROMPT_SUFFIX="$YS_VCS_PROMPT_SUFFIX"
 ZSH_THEME_GIT_PROMPT_DIRTY="$YS_VCS_PROMPT_DIRTY"

然后新开一个Terminal就可以看到效果了:

注意,~/.zshrc 里面设置的ZSH_THEME需要是ys

当然,也可以直接使用gitstatus作者的主题romkatv/powerlevel10k

 

替换之后,后台会启动gitstatusd进程,其中-t是线程数,默认是CPU逻辑核心数量的两倍。

$ ps aux | grep '[g]itstatusd'
lei       171244  0.0  0.0   6964  1068 pts/1    Sl   21:41   0:00 /home/lei/.cache/gitstatus/gitstatusd-linux-x86_64 -G v1.3.1 -s -1 -u -1 -c -1 -d -1 -v FATAL -t 32

The diffing algorithm in gitstatusd was designed from the ground up with the intention of using it concurrently from multiple threads. With a fast SSD, status is CPU bound, so taking advantage of all available CPU cores is an obvious way to yield results faster.

https://github.com/romkatv/gitstatus#multithreading

注意事项

  1. 建议复制~/.oh-my-zsh/themes内的主题,修改复制的主题。主要原因在于Oh My Zsh也是一个git Repository,每次更新的时候从上游拉取更新到本地,这种情况有可能导致merge冲突进而导致Oh My Zsh更新失败。

    ys.zsh-theme主题为例,我们可以将其复制为ys-fastgit.zsh-theme,再应用前述的修改之后,最后修改~/.zshrc设置ZSH_THEME="ys-fastgit"

附录 - 完整的脚本

git clone --depth=1 https://github.com/romkatv/gitstatus.git ~/.gitstatus
cp ~/.oh-my-zsh/themes/ys.zsh-theme ~/.oh-my-zsh/themes/ys-fastgit.zsh-theme

# 用
# source ~/.gitstatus/gitstatus.prompt.zsh
# local git_info=' git:$GITSTATUS_PROMPT'
# 替换 ys-fastgit.zsh-theme 中的
# local git_info='$(git_prompt_info)'

# 修改 ~/.zshrc 使得 ZSH_THEME="ys-fastgit"