From c336acfc57bd408dcb93379a171270612c3f1cd3 Mon Sep 17 00:00:00 2001 From: Matt Hunter Date: Wed, 6 May 2026 03:31:13 -0400 Subject: Revert status output to a single prompt element Output for changes staged/unstaged/unmerged/untracked are combined to a single prompt element %{status}, like it was prior to the recent rewrite (though it used to be called 'changes'). The main reason for this is to address script performance. The sed calls in prepare_element start to add up, and having fewer of them helps improve runtimes. The ability to customize the prompt via a flexible format string is a really nice feature that I'd like to keep, so I'm reducing the number of substitutable elements in an effort to balance performance. Reverting this specific change is one of the easier decisions to make in that regard. Furthermore, the actual element_status implementation is updated to rely on fewer external calls, additionally aiding performance. Now that the output is controlled by a single element, the arrangement of the different change stages is no longer fine-tunable via the prompt format string. A fixed order is produced: staged, unmerged, unstaged, untracked - exactly as it was before the recent rewrite. Within the output, these stages are separated by a "status separator" string (as before), however this separator is now exposed to the user as a configurable environment variable. Signed-off-by: Matt Hunter --- git-sonar | 88 ++++++++++++++++++++++++++++++++++++--------------------------- 1 file changed, 51 insertions(+), 37 deletions(-) diff --git a/git-sonar b/git-sonar index 7d71606..06fcf1e 100755 --- a/git-sonar +++ b/git-sonar @@ -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,12 +95,9 @@ 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="" +# Gather information about the current git branch. upstream_name="$(git rev-parse --abbrev-ref --symbolic-full-name '@{upstream}' 2>/dev/null)" branch_name="$(git symbolic-ref --short HEAD 2>/dev/null)" commit_hash="$(git rev-parse --short HEAD 2>/dev/null)" @@ -123,11 +121,6 @@ line_count() { 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" @@ -179,41 +172,65 @@ element_stash() { 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 } @@ -255,7 +272,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)" -- cgit v1.2.3 From 5d17b0508e7957b873301357157945bf2dfe2d42 Mon Sep 17 00:00:00 2001 From: Matt Hunter Date: Wed, 6 May 2026 03:48:53 -0400 Subject: Use faster command to obtain stash count Using git-rev-list, we can directly return a count of stashes without the need for additional external calls like wc. As a result, the line_count helper function is now unused, so remove it. Signed-off-by: Matt Hunter --- git-sonar | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/git-sonar b/git-sonar index 06fcf1e..9fd3264 100755 --- a/git-sonar +++ b/git-sonar @@ -114,13 +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]*' -} - print_commit_range() { if [ -n "$1" ] && [ -n "$2" ]; then ahead="$(git rev-list --count "${1}..${2}" 2>/dev/null)" || ahead="0" @@ -166,7 +159,8 @@ 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 -- cgit v1.2.3 From b9ceda00064d7b93ee05702c2a1fa6858b079c07 Mon Sep 17 00:00:00 2001 From: Matt Hunter Date: Wed, 6 May 2026 03:55:17 -0400 Subject: Remove outer grep in prepare_element for speedup The initial grep in prepare_element serves as a guard to prevent evaluation of element outputs which _arent_ present in the user' format string. However, if in the assumed general case where most (if not all) of the elements will be enabled *somewhere* in the prompt string, this check becomes entirely redundant with the main sed statement which runs at the end of the script. It's not a matter of if an element is present, but where - which is what the sed substitutions address. The downside of this change of course is that the prompt doesn't necessarily benefit from speedups just by omiting elements from the format string. However, the appropriate feature option (eg --no-status) can be used in this case to effectively turn the corresponding element into a no-op. In return, the "general" case (with all features enabled) is a bit faster. Signed-off-by: Matt Hunter --- git-sonar | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/git-sonar b/git-sonar index 9fd3264..66648f3 100755 --- a/git-sonar +++ b/git-sonar @@ -230,19 +230,15 @@ element_status() { # 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 } -- cgit v1.2.3 From f6828ce72ccf6b17220da77177a2015cec770523 Mon Sep 17 00:00:00 2001 From: Matt Hunter Date: Wed, 6 May 2026 04:01:08 -0400 Subject: Remove unneeded option --symbolic-full-name for upstream_name Signed-off-by: Matt Hunter --- git-sonar | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/git-sonar b/git-sonar index 66648f3..4da43f8 100755 --- a/git-sonar +++ b/git-sonar @@ -98,7 +98,7 @@ 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}%{ :status}"}" # Gather information about the current git branch. -upstream_name="$(git rev-parse --abbrev-ref --symbolic-full-name '@{upstream}' 2>/dev/null)" +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)" -- cgit v1.2.3