Corredor

ウェブ、プログラミングの勉強メモ。

Windows GitBash のプロンプト表示が遅いのをなんとかしたかった

Windows GitBash のプロンプト表示がやたらと遅い。何のコマンドも打たずに Enter を押しただけでも、何か表示がつっかえる。

何が原因かと思って調べてみたところ、どうも GitBash デフォルトのプロンプト内に設定されている __git_ps1 という関数が遅いようだ。

その証拠に、プロンプトを PS1='$ ' と簡素化すると、かなりサクサク動くようになった。

It looks like there problem lies in your bash prompt setting. Try setting PS1='$ ' so that whatever fancy prompt setting is deactivated, then see if it is still slow to you.

そこで、この関数の中身を探って、プロンプト表示を早く出来ないか、色々調べてみた。

__git_ps1 関数とは

Windows GitBash 標準のプロンプトは、以下のような構成になっている。

$ echo $PS1
\[\033]0;$TITLEPREFIX:$PWD\007\]\n\[\033[32m\]\u@\h \[\033[35m\]$MSYSTEM \[\033[33m\]\w\[\033[36m\]`__git_ps1`\[\033[0m\]\n$

カラーリングの設定が含まれていて分かりにくいが、

  • \u:ユーザ名
  • \h:ホスト名 (コンピュータ名)
  • $MSYSTEM:MSYS2 で起動したことを示す環境変数
  • \w:カレントディレクトリのパス
  • __git_ps1:コレが Git リポジトリを開いたときに (master *) などのようなプロンプトを表示させている関数

以上のようなモノが含まれている。

このプロンプトは /etc/profile.d/git-prompt.sh (C:\Program Files\Git\etc\profile.d\git-prompt.sh) というファイルが起動時に設定している。ファイルの中身を抜粋すると以下のとおり。

PS1='\[\033]0;$TITLEPREFIX:$PWD\007\]' # set window title
PS1="$PS1"'\n'                 # new line
PS1="$PS1"'\[\033[32m\]'       # change to green
PS1="$PS1"'\u@\h '             # user@host<space>
PS1="$PS1"'\[\033[35m\]'       # change to purple
PS1="$PS1"'$MSYSTEM '          # show MSYSTEM
PS1="$PS1"'\[\033[33m\]'       # change to brownish yellow
PS1="$PS1"'\w'                 # current working directory
if test -z "$WINELOADERNOEXEC"
then
  GIT_EXEC_PATH="$(git --exec-path 2>/dev/null)"
  COMPLETION_PATH="${GIT_EXEC_PATH%/libexec/git-core}"
  COMPLETION_PATH="${COMPLETION_PATH%/lib/git-core}"
  COMPLETION_PATH="$COMPLETION_PATH/share/git/completion"
  if test -f "$COMPLETION_PATH/git-prompt.sh"
  then
    . "$COMPLETION_PATH/git-completion.bash"  # ★
    . "$COMPLETION_PATH/git-prompt.sh"        # ★
    PS1="$PS1"'\[\033[36m\]'  # change color to cyan
    PS1="$PS1"'`__git_ps1`'   # bash function
  fi
fi
PS1="$PS1"'\[\033[0m\]'        # change color
PS1="$PS1"'\n'                 # new line
PS1="$PS1"'$ '                 # prompt: always $

このうち、コメントで # ★ と付けた部分で、タブ補完を実現する git-completion.bash と、__git_ps1 関数を提供する git-prompt.sh というファイルを読み込んでいる。いずれも、C:\Program Files\Git\mingw64\share\git\completion\ ディレクトリ配下に置いてある。

GitBash 起動後の実行順序としては、

  1. /etc/profile … このファイルが以降のファイルを source している
  2. /etc/msystem
  3. /etc/profile.d/git-prompt.sh$PS1 を定義している
    1. /mingw64/share/git/completion/git-completion.bash
    2. /mingw64/share/git/completion/git-prompt.sh__git_ps1 関数を持つファイル
  4. /etc/bash.bashrc

こんな感じになっている。起動時から色々読み込んでいるようだ。

さて、プロンプトを定義している /etc/profile.d/git-prompt.sh から読み込まれている、__git_ps1 関数を提供している方の git-prompto.sh の存在が分かったところで、この関数の中身を見ていこうと思う。

__git_ps1 関数について調べる

git-prompt.sh の中身は以下で読める。Windows GitBash にインストールされるモノも、Mac にインストールされるモノも、いずれも同じ内容のファイルだった。OS 環境を問わず使い回せる、Git 提供のスクリプトのようだ。

関数全体の処理時間をチェック。以下の文献では time コマンドを使って __git_ps1 関数の実行速度を計測している。自分の環境でも試してみたが、1回あたり0.7秒くらい実行にかかっているようだった。

次に、git add 前後のファイルがあると *+ といった記号をプロンプトに併記してくれる、$GIT_PS1_SHOWDIRTYSTATE オプションを外してみると、少し動作が速くなった。コレは git-prompt.sh 内で、git diff が実行されなくなったからなようだ。

# git-prompt.sh の L480~L488 を抜粋
if [ -n "${GIT_PS1_SHOWDIRTYSTATE-}" ] &&
   [ "$(git config --bool bash.showDirtyState)" != "false" ]
then
  git diff --no-ext-diff --quiet || w="*"
  git diff --no-ext-diff --cached --quiet || i="+"
  if [ -z "$short_sha" ] && [ -z "$i" ]; then
    i="#"
  fi
fi

同様に、git add 前の新規作成ファイルが存在するときに % 記号を表示する $GIT_PS1_SHOWUNTRACKEDFILES オプションを外すと、これまた速くなった。git ls-files が遅いようだ。

# git-prompt.sh の L495~L500 を抜粋
if [ -n "${GIT_PS1_SHOWUNTRACKEDFILES-}" ] &&
   [ "$(git config --bool bash.showUntrackedFiles)" != "false" ] &&
   git ls-files --others --exclude-standard --directory --no-empty-directory --error-unmatch -- ':/*' >/dev/null 2>/dev/null
then
  u="%${ZSH_VERSION+%}"
fi

これらの動作が遅いことは先程の記事などでも指摘されていて、この git-prompt.sh はちょくちょく改善されているようだ。

__git_ps1 関数を代替するオリジナルの関数を作る

この関数のやりたいことといえば、

  • Git 管理されているディレクトリだったらブランチ名を表示する
  • ($GIT_PS1_SHOWDIRTYSTATE オプションがある場合) git add 前のファイルがあれば *git add 後のファイルがあれば + 記号を表示する
  • ($GIT_PS1_SHOWUNTRACKEDFILES オプションがある場合) git add 前の新規作成ファイルがあれば % 記号を表示する

…とこのぐらいなのに、(コメント込みだが) 535行もある。何度か git コマンドを実行しているのが遅くなっていそうなのは予想がつく。

それならば、自分で簡易版の __git_ps1 関数を作ってみたらどうだろう、と思いつくと、既に同じことを考えている人たちがいた。

ただ、このミニマム版は、ブランチ名の表示しか対応しておらず、差分の有無は確認しないようにしているみたいだ。

他に調べていると、git diff ではなく git status を使って差分をプロンプトに表示しているコードが見つかった。

function git_status_string () {
  local statuses=$(git status -s 2> /dev/null | sed 's/^ *//' | cut -d ' ' -f 1 | sort | uniq)
  if [ -z "$statuses" ]; then echo $CAWAII_PROMPT_STATUS_OK; return; fi
  if [ -z "${statuses/*U*/}" ]; then echo $CAWAII_PROMPT_STATUS_NG; return; fi
  if [ -z "${statuses/*[MA?]*/}" ]; then echo $CAWAII_PROMPT_STATUS_WARN; return; fi
  echo $CAWAII_PROMPT_STATUS_BUG
}

このコードは cutuniq を使っていて、git add の前後で *+ を区別したりしていない。

.gitconfig で Git コマンドの動作を速くする

__git_ps1 関数を簡略化して自作するにしても、結局は git コマンドを呼ぶことになり、その動作が遅いのであれば、Git とファイルシステムの問題ともいえる。そこでもう少し調べてみると、「Git コマンド自体がトロい」という人も多く見られ、.gitconfig にて次のような設定をすることで改善された、という文献が見つかった。

$ git config --global core.preloadindex true
$ git config --global core.fscache true
$ git config --global gc.auto 256

オプションの内容は以下のとおり。

効果のほどはよく分からなかったが、気休め程度に設定してみた。

オレオレ __git_ps1 を作った

色々なスクリプトを参考にして、オレオレ __git_ps1 を作ってみた。

実装時に考慮したこと。

  • ブランチ名の取得は、速度を考えて git symbolic-ref を使用。タグなどが表示できないので、用途によっては git describe の方がいいかも。そちらもコメントアウトで残しておいた。
  • 結局、どうやってもパフォーマンスを改善できなかったので、ブランチ名だけ取得して表示して終わりにするコードも残しておいた。自分は普段こっちを使っているので、GIT_PS1 系のオプションをチェックするところから先のコードはデッドコードになっている。
  • 差分は git status --short を見ることにした。
    • cut コマンドで各行の1文字目を取得すれば git add 後の Staged なファイルの有無が分かる。
    • 同様に2文字目を見ると git add 前の Unstaged なファイルの有無が分かる。
    • Untracked なファイルは ? の有無で見る。
  • 差分情報を連結して出力している。

ブランチ名取得の時と git status を叩く時に、終了コードで判断した方が良いのかな?とも思う。特に git status --short --branch--branch オプションを呼ぶ必要があんまりないので (直後に if [ -z ] で判断したいがために書いてる)、パフォーマンス改善の余地はありそう。

あと echo よりも printf の方が良いのかなぁ?パフォーマンス差よく分からなかったので echo 使ってみたが…。

途中にも書いたけど、結局 git status を呼んだらどうやっても遅くなってしまったので、自分は諦めてブランチ名だけ出力する関数にしてしまった。

以下は参考にした文献。

その他

git-prompt.sh による __git_ps1 関数だが、Mac のターミナルに組み込んでも全く遅くならない。スクリプトの中身は同じなのに、Windows GitBash だと異様に遅いのだ。コレはやはり、GitBash (MSYS2) が仮想的に Bash ターミナルを再現しているために、ファイル情報の読み込みが遅いのだろう。

あと、AMD Radeon Graphics Driver を使っている環境ではコレを無効にすると Git の動作が速くなるんだとか。NVIDIA GTX1080 使いなので関係なかった。

git-completion.bash について

今回の git-prompt.sh とは関係ないが、タブ補完を実現するための git-completion.bash についてもバージョン差異があるらしく、Windows と Mac とでスクリプトの内容が違った。

本稿執筆時点で最新の git-completion.bash は以下の 2018-11-13 のコミット。

コレについてはあまり気にならなかったものの、Windows 環境ではやはり遅いことが多いようだ。

そんなワケで2018年末の .bash_profile.bashrc を紹介

色々試行錯誤してみたものの、Windows 環境下では git statusgit diff 等の実行コストを削減しきれなかったので、差分情報をプロンプト出力するのは諦めた。Mac ではこれまでどおり、git-prompt.sh が提供する __git_ps1 関数を使い、Windows ではブランチ名のみ出力する自作の __git_ps1 関数を使うことにした。

そんなこんなで構築した、2018年末時点の僕の .bash_profile.bashrc の中身は以下のとおり。

  • .bash_profile
# ================================================================================
# .bash_profile
# ================================================================================


# Detect OS And Settings
# ================================================================================

if [ "$(uname)" == "Darwin" ]; then
  echo '[MacOS] .bash_profile'
  # ==============================================================================
  
  # Git Prompt : 標準の __git_ps1 を使う
  test -r ~/.git-prompt.sh && . ~/.git-prompt.sh
  GIT_PS1_SHOWDIRTYSTATE=true
  GIT_PS1_SHOWUNTRACKEDFILES=true
  export PS1='\n\[\033[32m\]\u@\h \[\033[33m\]\w\[\033[36m\]`__git_ps1`\[\033[0m\]\n$ '
  
  # Nodebrew
  export PATH="$HOME/.nodebrew/current/bin:$PATH"
  
  # VSCode
  export PATH="/Applications/Visual Studio Code.app/Contents/Resources/app/bin:$PATH"
  
  # PostgreSQL
  export PATH="/Library/PostgreSQL/11/bin:$PATH"
  
  # RBEnv
  eval "$(rbenv init - 2> /dev/null)"
  
  
  # ------------------------------------------------------------------------------
elif [ "$(expr substr $(uname -s) 1 5)" == "MINGW" ]; then
  echo '[Windows] .bash_profile'
  # ==============================================================================
  
  # Git Prompt : 標準の __git_ps1 は未使用
  # test -r ~/.git-prompt.sh && . ~/.git-prompt.sh
  # GIT_PS1_SHOWDIRTYSTATE=true
  # GIT_PS1_SHOWUNTRACKEDFILES=true
  
  # Neo's __git_ps1 : 標準の __git_ps1 が Windows 環境で遅いので簡易版を自作した
  function __git_ps1() {
    # ブランチ名 : symbolic-ref はブランチ名しか出せないが、タグなどにも対応している describe よりは若干高速
    local branch_name="$(git symbolic-ref --short HEAD 2> /dev/null)"  # "$(git describe --all 2> /dev/null | sed 's/heads\///' 2> /dev/null)"
    
    # ブランチ名がなければ Git リポジトリ配下ではないと見なす・何も出力せず中断する
    if [ -z "$branch_name" ]; then
      exit 0
    fi
    
    # どうしてもパフォーマンスが出ないのでブランチ名だけ出すことにする
    echo " [$branch_name]"
    exit 0
  }
  
  export PS1='\n\[\033[32m\]\u@\h \[\033[33m\]\w\[\033[36m\]`__git_ps1`\[\033[0m\]\n$ '
  
  # Nodist
  NODIST_BIN_DIR__=$(echo "$NODIST_PREFIX" | sed -e 's,\\,/,g')/bin;
  test -r "$NODIST_BIN_DIR__/nodist.sh" && . "$NODIST_BIN_DIR__/nodist.sh"
  unset NODIST_BIN_DIR__;
  
  
  # ------------------------------------------------------------------------------
elif [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then
  echo '[Linux] .bash_profile'
  # ==============================================================================
  
  
  # ------------------------------------------------------------------------------
else
  echo '[Unknown OS] .bash_profile'
  # ==============================================================================
  
  
  # ------------------------------------------------------------------------------
fi


# History Control
# ================================================================================

export HISTCONTROL=ignoreboth


# Git Completion
# ================================================================================

test -r ~/.git-completion.bash && . ~/.git-completion.bash

# My Aliases : 念のため関数定義を確認してエイリアス用の Completion を設定する
if type __git_complete 1>/dev/null 2>/dev/null; then
  # Git
  __git_complete g __git_main
  # Add
  __git_complete ga _git_add
  # Branch
  __git_complete gb _git_branch
  __git_complete gba _git_branch
  __git_complete gbd _git_branch
  # Checkout
  __git_complete gco _git_checkout
  __git_complete gcob _git_checkout
  # Commit
  __git_complete gc _git_commit
  __git_complete gce _git_commit
  __git_complete gcem _git_commit
  __git_complete gcm _git_commit
  # Diff
  __git_complete gdf _git_diff
  __git_complete gdfc _git_diff
  __git_complete gdfn _git_diff
  __git_complete gdfnc _git_diff
  __git_complete gdfw _git_diff
  __git_complete gdfwc _git_diff
  __git_complete gdfwo _git_diff
  __git_complete gdfwoc _git_diff
  # Fetch
  __git_complete gfe _git_fetch
  # Log
  __git_complete gl _git_log
  __git_complete glf _git_log
  __git_complete glo _git_log
  __git_complete glr _git_log
  # Merge
  __git_complete gm _git_merge
  # Pull
  __git_complete gpl _git_pull
  # Push
  __git_complete gps _git_push
  # Reset
  __git_complete gre _git_reset
  __git_complete greh _git_reset
  # Status
  __git_complete gst _git_statusstatus
  __git_complete gs _git_statusstatus
fi


# Source .bashrc
# ================================================================================

test -r ~/.bashrc && . ~/.bashrc
  • .bashrc
# ================================================================================
# .bashrc
# ================================================================================


# Detect OS And Settings
# ================================================================================

if [ "$(uname)" == "Darwin" ]; then
  echo '[MacOS] .bashrc'
  # ==============================================================================
  
  
  # Ls
  export CLICOLOR=1
  export LSCOLORS=gxfxcxdxbxegedabagacad
  alias ls='ls -G'
  
  # Open = Start
  alias start='open'
  
  # sed
  alias sed='gsed'
  
  # tree
  alias tree='tree -N'
  
  # Open App
  alias chrome='open -a "Google Chrome"'
  alias cot='open -a CotEditor'
  
  # Nodebrew
  alias nb='nodebrew'
  
  # Sudo コマンドの補完を有効化
  complete -cf sudo
  
  # カレントディレクトリ配下の .DS_Store を全て消す
  alias delds='find . -name ".DS_Store" -delete'
  
  
  # ------------------------------------------------------------------------------
elif [ "$(expr substr $(uname -s) 1 5)" == "MINGW" ]; then
  echo '[Windows] .bashrc'
  # ==============================================================================
  
  
  # VSCode のターミナルで日本語が文字化けするので設定する
  export LANG=ja_JP.UTF-8
  
  # Ls
  # C:\Program Files\Git\etc\DIR_COLORS が色設定を持っている
  # 「DIR 01;34」を「DIR 01;36」にするとディレクトリが水色になる
  alias ls='ls -F --color=auto --show-control-chars'
  eval $(dircolors /etc/DIR_COLORS 2> /dev/null)
  
  # Start = Open
  alias open='start'
  
  # Notepad++
  alias np='"/c/Program Files/Notepad++/notepad++.exe" &'
  
  
  # ------------------------------------------------------------------------------
elif [ "$(expr substr $(uname -s) 1 5)" == "Linux" ]; then
  echo '[Linux] .bashrc'
  # ==============================================================================
  
  
  # ------------------------------------------------------------------------------
else
  echo '[Unknown OS] .bashrc'
  # ==============================================================================
  
  
  # ------------------------------------------------------------------------------
fi


# --------------------------------------------------------------------------------
# Alias
# --------------------------------------------------------------------------------


# Alias : General
# ================================================================================

alias quit='exit'
alias cls='reset'

# Ls
alias la='ls -a'
alias ll='ls -l'
alias lla='ls -la'
alias lal='ls -la'

# Cd
alias ..='cdd ..'
alias ...='cdd ../..'
alias -- -='cd - && ls'
alias -- --='cd - && ls'

# Grep : 検索文字列を色付けする
alias grep='grep --color'
alias grepinr='grep -inR'

# Df : バイト表示を単位変換する
alias df='df -h'

# PostgreSQL : パスワードは .pgpass (pgpass.conf) で設定
alias mpsql='psql -U postgres --dbname=my_local_db'

# Edit .bash_profile
alias ebp='vi ~/.bash_profile'
alias bp='. ~/.bash_profile'

# Edit .bashrc
alias erc='vi ~/.bashrc'
alias rc='. ~/.bashrc'


# Alias : Git
# ================================================================================

alias g='git'

alias ga='git add'
alias gb='git branch'
alias gba='git branch -a'
alias gbd='git branch -D'
alias gco='git checkout'
alias gcob='git checkout -b'
alias gc='git commit'
alias gce='git commit --allow-empty'
alias gcem='git commit --allow-empty -m'
alias gcm='git commit -m'
alias gdf='git diff'
alias gdfc='git diff --cached'
alias gdfn='git diff --name-only'
alias gdfnc='git diff --name-only --cached'
alias gdfw='git diff --color-words --word-diff-regex='\''\\w+|[^[:space:]]'\'''
alias gdfwc='git diff --color-words --word-diff-regex='\''\\w+|[^[:space:]]'\'' --cached'
alias gdfwo='git diff --word-diff'
alias gdfwoc='git diff --word-diff --cached'
alias gfe='git fetch'
alias gl=' git log -10 --date=short --pretty=format:"%C(Yellow)%h %C(Cyan)%cd %C(Reset)%s %C(Blue)[%cn]%C(Red)%d"'
alias glf='git log --pretty=fuller'
alias glo='git log'
alias glr='git log -10 --date=short --pretty=format:"%C(Yellow)%h %C(Cyan)%cd %C(Reset)%s %C(Blue)[%cn]%C(Red)%d" --graph'
alias gm='git merge'
alias gpl='git pull'
alias gps='git push'
alias gre='git reset'
alias greh='git reset --hard'
alias gst='git status'
alias gs='git status -s -b'

# Show Git Alias
alias galias='git config --global -l | grep alias'

# Tig
alias tiga='tig --all'


# Alias : Node
# ================================================================================

alias n='npm'

alias ni='npm install --progress=true'
alias nl='npm list --depth=0'
alias nls='npm list --depth=0'
alias nlg='npm list --depth=0 -g'
alias nlsg='npm list --depth=0 -g'
alias nr='npm run'
alias ns='npm start || npm run dev'
alias nt='npm test'
alias nu='npm uninstall --progress=true'

# For Angular CLI
alias nn='npm run ng'


# Alias : Vagrant
# ================================================================================

alias v='vagrant'

alias vup='vagrant up'
alias vsh='vagrant ssh'
alias vha='vagrant halt'


# --------------------------------------------------------------------------------
# Function
# --------------------------------------------------------------------------------


# Function : mkdir したディレクトリに cd する
#   http://qiita.com/0x60df/items/303666033788b937c578
# ================================================================================

function mkcd() {
  exec 3>&1
  cd "`
  if mkdir "$@" 1>&3; then
    while [ $# -gt 0 ]
    do
      case "$1" in
        -- ) printf '%s' "$2"; exit 0;;
        -* ) shift;;
        * ) printf '%s' "$1"; exit 0;;
      esac
    done
    printf '.'
    exit 0
  else
    printf '.'
    exit 1
  fi
  `"
  exec 3>&-
}


# Function : cd したあと ls する
#   http://thehacker.jp/alias-settings/
# ================================================================================

function cdd() {
  \cd "$@" && pwd && ls
}

今回作成した関数は、.bash_profile 内の Windows 向けの if 文の中に定義している。

# 抜粋・再掲

# Git Prompt : 標準の __git_ps1 は未使用
# test -r ~/.git-prompt.sh && . ~/.git-prompt.sh
# GIT_PS1_SHOWDIRTYSTATE=true
# GIT_PS1_SHOWUNTRACKEDFILES=true

# Neo's __git_ps1 : 標準の __git_ps1 が Windows 環境で遅いので簡易版を自作した
function __git_ps1() {
  # ブランチ名 : symbolic-ref はブランチ名しか出せないが、タグなどにも対応している describe よりは若干高速
  local branch_name="$(git symbolic-ref --short HEAD 2> /dev/null)"  # "$(git describe --all 2> /dev/null | sed 's/heads\///' 2> /dev/null)"
  
  # ブランチ名がなければ Git リポジトリ配下ではないと見なす・何も出力せず中断する
  if [ -z "$branch_name" ]; then
    exit 0
  fi
  
  # どうしてもパフォーマンスが出ないのでブランチ名だけ出すことにする
  echo " [$branch_name]"
  exit 0
}

export PS1='\n\[\033[32m\]\u@\h \[\033[33m\]\w\[\033[36m\]`__git_ps1`\[\033[0m\]\n$ '

~/.git-prompt.shsource 部分はコメントアウトして残してある。必要になったら呼び出せるように。この .git-prompt.sh は、公式の最新の git=prompt.sh を自前で配置しているだけ。Mac で Xcode Command Line Tools や Homebrew 経由でインストールされたモノがうまく参照できなくても動いてほしいので自分で持っておくことにした。

自前の関数は __git_ps1 と同名で定義しているので、実は export PS1 部分は Mac 版と同一のモノが指定できる。


この他に、.git-completion.bash というファイルも読み込んでいるが、コレも .git-prompt.sh と同様、公式の最新のファイルそのまんまを持っているだけ。.bash_profile 側で .git-completion.bash を読み込んだあと、エイリアス用のタブ補完を設定しているが、コレは .bashrc 側の alias git 系のところに並べて書いておく方が良いかしら…?ココらへん .bash_profile に書くべきか .bashrc に書くべきか悩ましい…。

以上

というワケで、2018年はコレにて終了です。2018年もお世話になりました。

[改訂第3版]Linuxコマンドポケットリファレンス

[改訂第3版]Linuxコマンドポケットリファレンス

[改訂第4版] UNIXコマンドポケットリファレンス ビギナー編

[改訂第4版] UNIXコマンドポケットリファレンス ビギナー編