From 878fbbce1ad6ec117e8b2197d06889dffeed98de Mon Sep 17 00:00:00 2001 From: Chris Graf Date: Mon, 29 Jun 2026 20:39:59 -0500 Subject: [PATCH] Derive Sley dirty state from counts Summary Separate Sley's dirty-state decision from the display JSON string emitted by `status`. Dirty state is now derived from numeric staged/pending/unstaged/ untracked counts before rendering JSON, so field order or formatting changes cannot affect control flow. Add focused coverage for the count-to-dirty helper to lock in the machine semantics directly. Testing - `shellcheck lib/sley/sley.sh test/suites/sley-behavior-test` - `bash test/suites/sley-behavior-test` --- lib/sley/sley.sh | 46 +++++++++++++++++++++++++++++++--- test/suites/sley-behavior-test | 16 ++++++++++++ 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/lib/sley/sley.sh b/lib/sley/sley.sh index 4accdde..7be1c3c 100644 --- a/lib/sley/sley.sh +++ b/lib/sley/sley.sh @@ -319,7 +319,7 @@ _sley_changes() { fi } -_sley_dirty_counts_json() { +_sley_dirty_counts() { local staged=0 unstaged=0 untracked=0 pending=0 line # `SLEY_SKIP_UNTRACKED=1` disables the untracked-file walk in both repo # types. This is mainly for bare Git worktrees rooted at large directories: @@ -353,10 +353,38 @@ _sley_dirty_counts_json() { fi ;; esac + + _SLEY_DIRTY_STAGED=$staged + _SLEY_DIRTY_PENDING=$pending + _SLEY_DIRTY_UNSTAGED=$unstaged + _SLEY_DIRTY_UNTRACKED=$untracked +} + +_sley_dirty_counts_json_from_values() { + local staged="$1" pending="$2" unstaged="$3" untracked="$4" printf '{"staged":%s,"pending":%s,"unstaged":%s,"untracked":%s}' \ "$staged" "$pending" "$unstaged" "$untracked" } +_sley_dirty_from_counts() { + local staged="$1" pending="$2" unstaged="$3" untracked="$4" + + if ((staged == 0 && pending == 0 && unstaged == 0 && untracked == 0)); then + printf 'false\n' + else + printf 'true\n' + fi +} + +_sley_dirty_counts_json() { + _sley_dirty_counts || return $? + _sley_dirty_counts_json_from_values \ + "$_SLEY_DIRTY_STAGED" \ + "$_SLEY_DIRTY_PENDING" \ + "$_SLEY_DIRTY_UNSTAGED" \ + "$_SLEY_DIRTY_UNTRACKED" +} + _sley_status() { _sley_init_repo || return $? _sley_parse_scope "$@" || return $? @@ -390,9 +418,19 @@ _sley_status() { fi ;; esac - counts=$(_sley_dirty_counts_json) - [[ "$counts" == '{"staged":0,"pending":0,"unstaged":0,"untracked":0}' ]] && - dirty=false || dirty=true + _sley_dirty_counts || return $? + # Dirty state is derived from the numeric counts before display JSON is + # rendered, so changing the JSON field order cannot change control flow. + dirty=$(_sley_dirty_from_counts \ + "$_SLEY_DIRTY_STAGED" \ + "$_SLEY_DIRTY_PENDING" \ + "$_SLEY_DIRTY_UNSTAGED" \ + "$_SLEY_DIRTY_UNTRACKED") + counts=$(_sley_dirty_counts_json_from_values \ + "$_SLEY_DIRTY_STAGED" \ + "$_SLEY_DIRTY_PENDING" \ + "$_SLEY_DIRTY_UNSTAGED" \ + "$_SLEY_DIRTY_UNTRACKED") if [[ "$_SLEY_SCOPE_JSON" == "1" ]]; then _repo_require_json_encoder || return 2 diff --git a/test/suites/sley-behavior-test b/test/suites/sley-behavior-test index bef21f5..3ec6562 100755 --- a/test/suites/sley-behavior-test +++ b/test/suites/sley-behavior-test @@ -318,6 +318,22 @@ _assert_contains "bare repo fallback: skip reports unsupported repo" "unsupporte echo "=== sley: git status and changes ===" +dirty_decisions=$( + SLEY_REPO_ROOT="$SLEY_REPO_ROOT" bash --noprofile --norc -c ' + source "$SLEY_REPO_ROOT/lib/sley/sley.sh" + printf "clean=%s\n" "$(_sley_dirty_from_counts 0 0 0 0)" + printf "staged=%s\n" "$(_sley_dirty_from_counts 1 0 0 0)" + printf "pending=%s\n" "$(_sley_dirty_from_counts 0 1 0 0)" + printf "unstaged=%s\n" "$(_sley_dirty_from_counts 0 0 1 0)" + printf "untracked=%s\n" "$(_sley_dirty_from_counts 0 0 0 1)" + ' +) +_assert_contains "status dirty helper: clean counts are clean" "clean=false" "$dirty_decisions" +_assert_contains "status dirty helper: staged count is dirty" "staged=true" "$dirty_decisions" +_assert_contains "status dirty helper: pending count is dirty" "pending=true" "$dirty_decisions" +_assert_contains "status dirty helper: unstaged count is dirty" "unstaged=true" "$dirty_decisions" +_assert_contains "status dirty helper: untracked count is dirty" "untracked=true" "$dirty_decisions" + repo=$(_new_repo) cat >"$repo/tracked.py" <<'EOF' x = 1