diff options
| author | Matt Hunter <m@lfurio.us> | 2026-05-08 03:56:20 -0400 |
|---|---|---|
| committer | Matt Hunter <m@lfurio.us> | 2026-05-08 03:56:20 -0400 |
| commit | 4b9e1e9828892b3c18afb015c9a1a706a9bc5dce (patch) | |
| tree | df7212b7572a545b9666928c37fdb1daf8b6e6a7 | |
| parent | 5f7de1d0b89d9d5cc6f437061a62e38721077667 (diff) | |
| parent | f6828ce72ccf6b17220da77177a2015cec770523 (diff) | |
| download | git-sonar-master.tar.gz git-sonar-master.zip | |
The recent script rewrite of git-sonar resulted in average runtimes 4-5x
higher than the latest release (v0.8.1), and was slower than some
previous releases of git-radar as well. Change milestones for the next
release are nearing completion, so lets take a moment and explore
possible performance improvements to the script. These patches are the
low (ish) hanging fruit.
The performance curve is improved on the whole, but the script is still
marginally slower when operating at smaller scale - that is, in a
smaller repo with little or no "changes" for the prompt to report.
"Changes" in this case refers to the number of commits in the compared
ranges, or the number of paths affected by changes staged, unstaged,
etc.
I consider the low scale case to be the common case, so it annoys me a
little that I haven't improved this case over previous releases.
However, at scale, the difference in runtime can become quite pronounced
in favor of this version over the past few releases (nevermind the
rewrite).
Of course, these aren't apples to apples comparisons, as features have
come and gone between releases. Though the most expensive operations
have more-or-less remained a constant.
Tests were benchmarked using a clone of the linux-stable tree, with
changes ranging up to 2000 changed paths, or commits out-of-date.
* perf:
Remove unneeded option --symbolic-full-name for upstream_name
Remove outer grep in prepare_element for speedup
Use faster command to obtain stash count
Revert status output to a single prompt element
Diffstat (limited to '')
| -rwxr-xr-x | git-sonar | 114 |
1 files changed, 59 insertions, 55 deletions
@@ -83,6 +83,7 @@ FETCH_TIME="${GIT_SONAR_FETCH_TIME:-"300"}" ALERT_ICON="${GIT_SONAR_ALERT_ICON:-"${COLOR_YELLOW}⚡"}" STASH_ICON="${GIT_SONAR_STASH_ICON:-"${COLOR_YELLOW}≡"}" BRANCH_COLOR="${GIT_SONAR_BRANCH_COLOR:-"$COLOR_DEF"}" +STATUS_SEP="${GIT_SONAR_STATUS_SEPARATOR:-" "}" AHEAD_ICON="${GIT_SONAR_AHEAD_ICON:-"${COLOR_GREEN}↑"}" # "←" BEHIND_ICON="${GIT_SONAR_BEHIND_ICON:-"${COLOR_RED}↓"}" # "→" @@ -94,13 +95,10 @@ UNMERGED_COLOR="${GIT_SONAR_UNMERGED_COLOR:-"$COLOR_YELLOW"}" UNTRACKED_COLOR="${GIT_SONAR_UNTRACKED_COLOR:-"$COLOR_WHITE"}" PROMPT_COLOR="${GIT_SONAR_PROMPT_COLOR:-"$COLOR_GRAY"}" -PROMPT_FORMAT="${GIT_SONAR_PROMPT_FORMAT:-" ${PROMPT_COLOR}git:(${COLOR_DEF}%{alert}%{remote: }%{branch}%{ :local}${PROMPT_COLOR})${COLOR_DEF}%{ :stash}%{ :staged}%{ :unmerged}%{ :unstaged}%{ :untracked}"}" +PROMPT_FORMAT="${GIT_SONAR_PROMPT_FORMAT:-" ${PROMPT_COLOR}git:(${COLOR_DEF}%{alert}%{remote: }%{branch}%{ :local}${PROMPT_COLOR})${COLOR_DEF}%{ :stash}%{ :status}"}" -# Gather current git status and branch information. git porcelain status can -# be especially expensive to compute, so bypass it if the prompt disables status -# information. -[ -n "$opt_status" ] && git_status="$(git status --porcelain 2>/dev/null)" || git_status="" -upstream_name="$(git rev-parse --abbrev-ref --symbolic-full-name '@{upstream}' 2>/dev/null)" +# Gather information about the current git branch. +upstream_name="$(git rev-parse --abbrev-ref '@{upstream}' 2>/dev/null)" branch_name="$(git symbolic-ref --short HEAD 2>/dev/null)" commit_hash="$(git rev-parse --short HEAD 2>/dev/null)" @@ -116,18 +114,6 @@ determine_default_branch() { done } -line_count() { - # macos 'wc' produces odd formatting (extra spaces) - # The grep is present to deal with this, filter out cases where the count - # is zero, and provide a return value (true if count is non-zero). - wc -l 2>/dev/null | grep -oE '[1-9][0-9]*' -} - -status_count() { - # Use a sed transform to prevent unmerged paths from being miscounted - echo "$git_status" | sed -nE "s/^AA /AU /;s/^DD /DU /;/${1}/p" | line_count -} - print_commit_range() { if [ -n "$1" ] && [ -n "$2" ]; then ahead="$(git rev-list --count "${1}..${2}" 2>/dev/null)" || ahead="0" @@ -173,65 +159,86 @@ element_local() { element_stash() { if [ -n "$opt_stash" ]; then - if cnt="$(git stash list | line_count)"; then + cnt="$(git rev-list --walk-reflogs --ignore-missing --count refs/stash)" + if [ "$cnt" -ne 0 ]; then printf '%s%b%b' "$cnt" "$STASH_ICON" "$COLOR_DEF" fi fi } -element_staged() { +element_status() { if [ -n "$opt_status" ]; then + # git status is the most expensive subprocess we call, so place it here + # to bypass it if $opt_status is "false". The output is filtered + # through sed to prevent certain unmerged paths from being double + # counted as staged/unstaged ("AA"/"DD"). + git_status="$(\ + git status --porcelain 2>/dev/null \ + | sed 's/^AA /AU /;s/^DD /DU /' \ + )" + + gs="" + st_sep="$STATUS_SEP" + um_sep="$STATUS_SEP" + us_sep="$STATUS_SEP" + ut_sep="$STATUS_SEP" + + # See "man 1 git-status" sections "Short Format" and "Porcelain Format + # Version 1" for an explanation of these status indicators. + + # Staged for x in A M R C D T; do - if cnt="$(status_count "^${x}[^U] ")"; then - printf '%s%b%s%b' "$cnt" "$STAGED_COLOR" "$x" "$COLOR_DEF" + if cnt="$(echo "$git_status" | grep -cE "^${x}[^U] ")"; then + gs="$(printf '%b%b%s%b%s%b' \ + "$gs" "$st_sep" "$cnt" "$STAGED_COLOR" "$x" "$COLOR_DEF" \ + )" + st_sep="" fi done - fi -} -element_unstaged() { - if [ -n "$opt_status" ]; then - for x in M D T; do - if cnt="$(status_count "^[^U]${x} ")"; then - printf '%s%b%s%b' "$cnt" "$UNSTAGED_COLOR" "$x" "$COLOR_DEF" + # Unmerged, conflicted + for x in A U D; do + if cnt="$(echo "$git_status" | grep -cE "^(U${x}|${x}U) ")"; then + gs="$(printf '%b%b%s%b%s%b' \ + "$gs" "$um_sep" "$cnt" "$UNMERGED_COLOR" "$x" "$COLOR_DEF" \ + )" + um_sep="" fi done - fi -} -element_unmerged() { - if [ -n "$opt_status" ]; then - for x in A U D; do - if cnt="$(status_count "^(U${x}|${x}U) ")"; then - printf '%s%b%s%b' "$cnt" "$UNMERGED_COLOR" "$x" "$COLOR_DEF" + # Unstaged + for x in M D T; do # R C omitted + if cnt="$(echo "$git_status" | grep -cE "^[^U]${x} ")"; then + gs="$(printf '%b%b%s%b%s%b' \ + "$gs" "$us_sep" "$cnt" "$UNSTAGED_COLOR" "$x" "$COLOR_DEF" \ + )" + us_sep="" fi done - fi -} -element_untracked() { - if [ -n "$opt_status" ]; then - if cnt="$(status_count "^\?\? ")"; then - printf '%s%b?%b' "$cnt" "$UNTRACKED_COLOR" "$COLOR_DEF" + # Untracked + if cnt="$(echo "$git_status" | grep -cE "^\?\? ")"; then + gs="$(printf '%b%b%s%b?%b' \ + "$gs" "$ut_sep" "$cnt" "$UNTRACKED_COLOR" "$COLOR_DEF" \ + )" + ut_sep="" fi + + printf '%b' "${gs#"$STATUS_SEP"}" fi } # Main functions -IF_PRE="%\{([^%{}]{1,}:){0,1}" -IF_POST="(:[^%{}]{1,}){0,1}\}" SED_PRE="%{\(\([^%^{^}]*\)\:\)\{0,1\}" SED_POST="\(\:\([^%^{^}]*\)\)\{0,1\}}" prepare_element() { - if echo "$PROMPT_FORMAT" | grep -qE "${IF_PRE}${1}${IF_POST}"; then - result="$($2 | sed -e 's/\//\\\//g')" - if [ -n "$result" ]; then - printf '%b' "s/${SED_PRE}${1}${SED_POST}/\\\\2${result}\\\\4/" - else - printf '%b' "s/${SED_PRE}${1}${SED_POST}//" - fi + result="$($2 | sed 's/\//\\\//g')" + if [ -n "$result" ]; then + printf '%b' "s/${SED_PRE}${1}${SED_POST}/\\\\2${result}\\\\4/" + else + printf '%b' "s/${SED_PRE}${1}${SED_POST}//" fi } @@ -255,7 +262,4 @@ printf '%b' "$PROMPT_FORMAT" | sed \ -e "$(prepare_element remote element_remote)" \ -e "$(prepare_element local element_local)" \ -e "$(prepare_element stash element_stash)" \ - -e "$(prepare_element staged element_staged)" \ - -e "$(prepare_element unstaged element_unstaged)" \ - -e "$(prepare_element unmerged element_unmerged)" \ - -e "$(prepare_element untracked element_untracked)" + -e "$(prepare_element status element_status)" |
