From cd454a4e0a333676eda3b7dbfe8578d114f5e891 Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Thu, 14 May 2026 03:30:58 +0700 Subject: [PATCH] Make Git dirty advanced look opt-in --- pure.zsh | 28 ++++++++--- readme.md | 26 ++++++---- tests/git-dirty.zsh | 120 ++++++++++++++++++++++++++++++-------------- 3 files changed, 117 insertions(+), 57 deletions(-) diff --git a/pure.zsh b/pure.zsh index e7cf07d..6df6b98 100644 --- a/pure.zsh +++ b/pure.zsh @@ -322,11 +322,12 @@ prompt_pure_async_vcs_info() { print -r - ${(@kvq)info} } -# Check if a Git repo is dirty and report detailed status. -# Outputs a markers string: * (unstaged), + (staged), ? (untracked). +# Fastest possible way to check if a Git repo is dirty. +# When detailed mode is enabled, outputs markers: * (unstaged), + (staged), ? (untracked). prompt_pure_async_git_dirty() { setopt localoptions noshwordsplit local untracked_dirty=$1 + local detailed=${2:-0} local untracked_git_mode=$(command git config --get status.showUntrackedFiles) if [[ "$untracked_git_mode" != 'no' ]]; then untracked_git_mode='normal' @@ -335,6 +336,17 @@ prompt_pure_async_git_dirty() { # Prevent e.g. `git status` from refreshing the index as a side effect. export GIT_OPTIONAL_LOCKS=0 + if (( ! detailed )); then + if [[ $untracked_dirty = 0 ]]; then + command git diff --no-ext-diff --quiet --exit-code || return $? + command git diff --no-ext-diff --cached --quiet --exit-code + else + test -z "$(command git status --porcelain -u${untracked_git_mode})" + fi + + return + fi + local u_flag if [[ $untracked_dirty = 0 ]]; then u_flag='-uno' @@ -593,8 +605,10 @@ prompt_pure_async_refresh() { integer time_since_last_dirty_check=$(( EPOCHSECONDS - ${prompt_pure_git_last_dirty_check_timestamp:-0} )) if (( time_since_last_dirty_check > ${PURE_GIT_DELAY_DIRTY_CHECK:-1800} )); then unset prompt_pure_git_last_dirty_check_timestamp - # Check check if there is anything to pull. - async_job "prompt_pure" prompt_pure_async_git_dirty ${PURE_GIT_UNTRACKED_DIRTY:-1} || return + # Check if the working tree is dirty. + zstyle -t ":prompt:pure:git:dirty" detailed + local detailed_dirty=$((? == 0)) + async_job "prompt_pure" prompt_pure_async_git_dirty ${PURE_GIT_UNTRACKED_DIRTY:-1} $detailed_dirty || return fi # If stash is enabled, tell async worker to count stashes @@ -949,7 +963,7 @@ prompt_pure_preview() { fi # Sample preprompt with all components visible. - print -P "%F{$c[custom:prefix]}prefix%f %F{$c[suspended_jobs]}${PURE_SUSPENDED_JOBS_SYMBOL-✦}%f %F{$c[user]}zaphod%f${host_sample} ${path_sample} %F{$c[git:branch]}main%f%F{$c[git:dirty]}*+%f %F{$c[git:action]}rebase-i%f %F{$c[git:arrow]}${PURE_GIT_DOWN_ARROW:-⇣}${PURE_GIT_UP_ARROW:-⇡}%f %F{$c[git:stash]}${PURE_GIT_STASH_SYMBOL-≡}%f %F{$c[node_version]}${node_symbol}22%f %F{$c[execution_time]}42s%f %F{$c[custom:suffix]}suffix%f" + print -P "%F{$c[custom:prefix]}prefix%f %F{$c[suspended_jobs]}${PURE_SUSPENDED_JOBS_SYMBOL-✦}%f %F{$c[user]}zaphod%f${host_sample} ${path_sample} %F{$c[git:branch]}main%f%F{$c[git:dirty]}*%f %F{$c[git:action]}rebase-i%f %F{$c[git:arrow]}${PURE_GIT_DOWN_ARROW:-⇣}${PURE_GIT_UP_ARROW:-⇡}%f %F{$c[git:stash]}${PURE_GIT_STASH_SYMBOL-≡}%f %F{$c[node_version]}${node_symbol}22%f %F{$c[execution_time]}42s%f %F{$c[custom:suffix]}suffix%f" print -P "%F{$c[virtualenv]}venv%f %F{$c[prompt:success]}${PURE_PROMPT_SYMBOL:-❯}%f" print print -P "%F{$c[prompt:error]}${PURE_PROMPT_SYMBOL:-❯}%f prompt after error" @@ -1041,7 +1055,7 @@ prompt_pure_setup() { # psvar[12] = suspended jobs symbol (e.g. ✦) # psvar[13] = username flag, renders user/host (e.g. user@host) # psvar[14] = git branch - # psvar[15] = git dirty markers (* unstaged, + staged, ? untracked) + # psvar[15] = git dirty marker, nested inside [14] conditional # psvar[16] = git action (e.g. rebase, merge) # psvar[17] = git arrows (e.g. ⇣⇡) # psvar[18] = git stash symbol (e.g. ≡) @@ -1052,7 +1066,7 @@ prompt_pure_setup() { # psvar[23] = custom suffix (set by prompt_pure_precustom) # # Example output: - # prefix ✦ user@host ~/Code/pure main*+ rebase ⇣⇡ ≡ ⬢22 3s suffix + # prefix ✦ user@host ~/Code/pure main* rebase ⇣⇡ ≡ ⬢22 3s suffix # myenv ❯ # # Preprompt line: each %(NV..) section only renders when its psvar is non-empty. diff --git a/readme.md b/readme.md index 2dce6ce..9e29d53 100644 --- a/readme.md +++ b/readme.md @@ -12,7 +12,7 @@ Most prompts are cluttered, ugly and slow. We wanted something visually pleasing - Comes with the perfect prompt character. Author went through the whole Unicode range to find it. -- Shows `git` branch and whether it's dirty (`*` unstaged, `+` staged, `?` untracked). +- Shows `git` branch and whether it's dirty (with a `*`). - Indicates when you have unpushed/unpulled `git` commits with up/down arrows. *(Check is done asynchronously!)* - Prompt character turns red if the last command didn't exit with `0`. - Command execution time will be displayed if it exceeds the set threshold. @@ -121,6 +121,10 @@ Git integration is enabled by default, you can disable it with: `zstyle :prompt:pure:git show no` +Detailed dirty indicators differentiate between unstaged (`*`), staged (`+`), and untracked (`?`) changes instead of showing a single `*`. It is not enabled by default. You can enable it with: + +`zstyle :prompt:pure:git:dirty detailed yes` + Path separator dimming makes `/` characters in the path visually dimmer to help distinguish path components. It is not enabled by default. You can enable it with: `zstyle :prompt:pure:path:separator dim yes` @@ -181,7 +185,7 @@ Colors can be changed by using [`zstyle`](http://zsh.sourceforge.net/Doc/Release - `git:branch` (242) - The name of the current branch when in a Git repository. - `git:branch:cached` (red) - The name of the current branch when the data isn't fresh. - `git:action` (yellow) - The current action in progress (cherry-pick, rebase, etc.) when in a Git repository. -- `git:dirty` (218) - The dirty indicators showing the branch state (`*` unstaged, `+` staged, `?` untracked). +- `git:dirty` (218) - The asterisk showing the branch is dirty. - `host` (242) - The hostname when on a remote machine. - `node_version` (green) - The current major Node.js version in directories with `package.json`. - `path` (blue) - The current path, for example, `PWD`. @@ -202,15 +206,15 @@ The following diagram shows where each color is applied on the prompt: │ │ │ ┌────────────────────────────────────────────────────────── host │ │ │ │ ┌────────────────────────────────────────────── path │ │ │ │ │ ┌─────────────────────────────────── git:branch -│ │ │ │ │ │ ┌─────────────────────────────── git:dirty -│ │ │ │ │ │ │ ┌──────────────────────────── git:action -│ │ │ │ │ │ │ │ ┌─────────────────── git:arrow -│ │ │ │ │ │ │ │ │ ┌───────────────── git:stash -│ │ │ │ │ │ │ │ │ │ ┌─────────────── node_version -│ │ │ │ │ │ │ │ │ │ │ ┌─────────── execution_time -│ │ │ │ │ │ │ │ │ │ │ │ ┌────── custom:suffix -│ │ │ │ │ │ │ │ │ │ │ │ │ -prefix ✦ zaphod@heartofgold ~/dev/pure main*+ rebase-i ⇡ ≡ ⬢22 42s suffix +│ │ │ │ │ │ ┌────────────────────────────── git:dirty +│ │ │ │ │ │ │ ┌──────────────────────────── git:action +│ │ │ │ │ │ │ │ ┌─────────────────── git:arrow +│ │ │ │ │ │ │ │ │ ┌───────────────── git:stash +│ │ │ │ │ │ │ │ │ │ ┌─────────────── node_version +│ │ │ │ │ │ │ │ │ │ │ ┌─────────── execution_time +│ │ │ │ │ │ │ │ │ │ │ │ ┌────── custom:suffix +│ │ │ │ │ │ │ │ │ │ │ │ │ +prefix ✦ zaphod@heartofgold ~/dev/pure main* rebase-i ⇡ ≡ ⬢22 42s suffix venv ❯ │ │ │ └───────────────────────────────────────────────────────────────────── prompt diff --git a/tests/git-dirty.zsh b/tests/git-dirty.zsh index f81161b..158828b 100644 --- a/tests/git-dirty.zsh +++ b/tests/git-dirty.zsh @@ -33,76 +33,118 @@ check_dirty() { setup_repo +# ── Default mode (no detailed flag) ── + # Clean repo. check_dirty 1 assert_equal 0 $check_code "clean repo should return 0" assert_empty "$check_output" "clean repo should have no output" -# Unstaged changes only. +# Unstaged changes. echo "modified" > file.txt check_dirty 1 -assert_equal 1 $check_code "unstaged should return 1" -assert_equal "*" "$check_output" "unstaged only should show *" +assert_equal 1 $check_code "dirty repo should return 1" +assert_empty "$check_output" "default mode should have no output" -# Staged changes only. +# Staged changes. command git add file.txt check_dirty 1 -assert_equal 1 $check_code "staged should return 1" -assert_equal "+" "$check_output" "staged only should show +" +assert_equal 1 $check_code "staged dirty should return 1" +assert_empty "$check_output" "default mode staged should have no output" + +# Untracked files. +command git commit -q -m "second" +echo "untracked" > newfile.txt +check_dirty 1 +assert_equal 1 $check_code "untracked dirty should return 1" +assert_empty "$check_output" "default mode untracked should have no output" + +# PURE_GIT_UNTRACKED_DIRTY=0 ignores untracked. +check_dirty 0 +assert_equal 0 $check_code "untracked with PURE_GIT_UNTRACKED_DIRTY=0 should be clean" +assert_empty "$check_output" "untracked with PURE_GIT_UNTRACKED_DIRTY=0 should have no output" +zf_rm -f newfile.txt + +# PURE_GIT_UNTRACKED_DIRTY=0 still detects staged tracked changes. +echo "staged with ignored untracked" > file.txt +command git add file.txt +check_dirty 0 +assert_equal 1 $check_code "staged with PURE_GIT_UNTRACKED_DIRTY=0 should return 1" +assert_empty "$check_output" "staged with PURE_GIT_UNTRACKED_DIRTY=0 should have no output" +command git commit -q -m "staged with ignored untracked" + +# ── Detailed mode ── + +# Clean repo. +check_dirty 1 1 +assert_equal 0 $check_code "detailed: clean repo should return 0" +assert_empty "$check_output" "detailed: clean repo should have no output" + +# Unstaged changes only. +echo "detailed-modified" > file.txt +check_dirty 1 1 +assert_equal 1 $check_code "detailed: unstaged should return 1" +assert_equal "*" "$check_output" "detailed: unstaged only should show *" + +# Staged changes only. +command git add file.txt +check_dirty 1 1 +assert_equal 1 $check_code "detailed: staged should return 1" +assert_equal "+" "$check_output" "detailed: staged only should show +" # Staged + intent-to-add. echo "intent" > intent.txt command git add -N intent.txt -check_dirty 1 -assert_equal 1 $check_code "staged+intent-to-add should return 1" -assert_equal "*+" "$check_output" "staged+intent-to-add should show *+" +check_dirty 1 1 +assert_equal 1 $check_code "detailed: staged+intent-to-add should return 1" +assert_equal "*+" "$check_output" "detailed: staged+intent-to-add should show *+" command git reset -q HEAD -- intent.txt zf_rm -f intent.txt # Staged + unstaged (same file). echo "more changes" > file.txt -check_dirty 1 -assert_equal 1 $check_code "staged+unstaged should return 1" -assert_equal "*+" "$check_output" "staged+unstaged should show *+" +check_dirty 1 1 +assert_equal 1 $check_code "detailed: staged+unstaged should return 1" +assert_equal "*+" "$check_output" "detailed: staged+unstaged should show *+" # Untracked only (commit current changes first). command git add file.txt -command git commit -q -m "second" +command git commit -q -m "third" echo "new" > newfile.txt -check_dirty 1 -assert_equal 1 $check_code "untracked should return 1" -assert_equal "?" "$check_output" "untracked only should show ?" +check_dirty 1 1 +assert_equal 1 $check_code "detailed: untracked should return 1" +assert_equal "?" "$check_output" "detailed: untracked only should show ?" # All three: unstaged + staged + untracked. echo "modified again" > file.txt command git add file.txt echo "even more" > file.txt -check_dirty 1 -assert_equal 1 $check_code "all three should return 1" -assert_equal "*+?" "$check_output" "all three should show *+?" +check_dirty 1 1 +assert_equal 1 $check_code "detailed: all three should return 1" +assert_equal "*+?" "$check_output" "detailed: all three should show *+?" # PURE_GIT_UNTRACKED_DIRTY=0 with only untracked files. command git add file.txt -command git commit -q -m "third" +command git commit -q -m "fourth" zf_rm -f newfile.txt echo "untracked" > anotherfile.txt -check_dirty 0 -assert_equal 0 $check_code "untracked with PURE_GIT_UNTRACKED_DIRTY=0 should be clean" -assert_empty "$check_output" "untracked with PURE_GIT_UNTRACKED_DIRTY=0 should have no output" +check_dirty 0 1 +assert_equal 0 $check_code "detailed: untracked with PURE_GIT_UNTRACKED_DIRTY=0 should be clean" +assert_empty "$check_output" "detailed: untracked with PURE_GIT_UNTRACKED_DIRTY=0 should have no output" # PURE_GIT_UNTRACKED_DIRTY=0 with unstaged changes only. -echo "modified" > file.txt -check_dirty 0 -assert_equal 1 $check_code "unstaged with PURE_GIT_UNTRACKED_DIRTY=0 should return 1" -assert_equal "*" "$check_output" "unstaged with PURE_GIT_UNTRACKED_DIRTY=0 should show *" +echo "changed" > file.txt +check_dirty 0 1 +assert_equal 1 $check_code "detailed: unstaged with PURE_GIT_UNTRACKED_DIRTY=0 should return 1" +assert_equal "*" "$check_output" "detailed: unstaged with PURE_GIT_UNTRACKED_DIRTY=0 should show *" # PURE_GIT_UNTRACKED_DIRTY=0 with staged changes. command git checkout -- file.txt echo "staged" > staged.txt command git add staged.txt -check_dirty 0 -assert_equal 1 $check_code "staged with PURE_GIT_UNTRACKED_DIRTY=0 should return 1" -assert_equal "+" "$check_output" "staged with PURE_GIT_UNTRACKED_DIRTY=0 should show +" +check_dirty 0 1 +assert_equal 1 $check_code "detailed: staged with PURE_GIT_UNTRACKED_DIRTY=0 should return 1" +assert_equal "+" "$check_output" "detailed: staged with PURE_GIT_UNTRACKED_DIRTY=0 should show +" # Deleted file (unstaged). command git commit -q --allow-empty -m "prep" @@ -114,15 +156,15 @@ echo "to delete" > deleteme.txt command git add deleteme.txt command git commit -q -m "add deleteme" zf_rm -f deleteme.txt -check_dirty 1 -assert_equal 1 $check_code "deleted unstaged should return 1" -assert_equal "*" "$check_output" "deleted unstaged should show *" +check_dirty 1 1 +assert_equal 1 $check_code "detailed: deleted unstaged should return 1" +assert_equal "*" "$check_output" "detailed: deleted unstaged should show *" # Deleted file (staged). command git rm -q deleteme.txt -check_dirty 1 -assert_equal 1 $check_code "deleted staged should return 1" -assert_equal "+" "$check_output" "deleted staged should show +" +check_dirty 1 1 +assert_equal 1 $check_code "detailed: deleted staged should return 1" +assert_equal "+" "$check_output" "detailed: deleted staged should show +" # Renamed file (staged). command git reset -q HEAD -- deleteme.txt @@ -131,8 +173,8 @@ echo "to rename" > renameme.txt command git add renameme.txt command git commit -q -m "add renameme" command git mv renameme.txt renamed.txt -check_dirty 1 -assert_equal 1 $check_code "renamed staged should return 1" -assert_equal "+" "$check_output" "renamed staged should show +" +check_dirty 1 1 +assert_equal 1 $check_code "detailed: renamed staged should return 1" +assert_equal "+" "$check_output" "detailed: renamed staged should show +" print "git-dirty tests passed" -- 2.51.2