Skip to content

fix(git): start the git cache TTL when the command finishes - #750

Merged
sirmalloc merged 1 commit into
sirmalloc:mainfrom
eric-engberg:fix/git-cache-slow-command
Oct 29, 2026
Merged

sirmalloc merged 1 commit into
sirmalloc:mainfrom
eric-engberg:fix/git-cache-slow-command

Conversation

@eric-engberg

Copy link
Copy Markdown
Contributor

What

A git cache entry's TTL now starts when git finishes, not when the lookup began. A git command that takes as long as the TTL is no longer stored already expired, so the widgets that share it reuse one run instead of each running it again.

Why

runGitArgs read Date.now() before running git and stamped the cache entry with that time, after both a success and a failure. Freshness is now - createdAt <= ttl. So a command that takes the TTL or longer was expired the moment it was stored, both in memory and on disk. With the defaults (5s TTL, 5s git timeout), that is any command that runs into the timeout, such as git status in a large repository.

Several widgets share each command:

  • Git Status, Clean Status, and the staged, unstaged and untracked flags and file counts all read status --porcelain -z.
  • Insertions, Deletions and Changes all read diff --shortstat and diff --cached --shortstat.

Each of them ran the slow command again. Large repositories are where this shows up: in #520, git widgets in a large codebase pushed a render to about 22s and Claude Code stopped showing the status line.

Measured with a scratch repo, a line of six git widgets (Git Branch, Git Status, Git Staged Files, Git Untracked Files, Git Insertions, Git Deletions), and a stand-in git first on PATH. It waits before status (and work-tree diff), then runs the real git, logging every run. Each case ran two renders back to back:

Case Runtime main this branch
Default 5s TTL; status and diff take 6s (cut off at the 5s timeout) Bun 1.4.2 25.4s, 25.3s; 5 slow runs per render (3x status, 2x diff) 10.3s, 10.2s; 2 slow runs (1x status, 1x diff)
same Node 26.10.0 25.5s, 25.4s; 5 slow runs 10.6s, 10.3s; 2 slow runs
TTL 1s; status takes 1.5s Bun 1.4.2 5.1s, 4.9s; 3x status per render 1.9s, then 0.2s with no slow run
same Node 26.10.0 5.0s, 5.0s; 3x status 1.9s, then 0.3s with no slow run

How

  • runGitArgs runs git, then builds a single cache entry, stamped once git returns, for output and failure alike. It stores that entry in both the in-process map and the on-disk cache. The time read before the run is still what the freshness lookups compare against. The custom command cache already stamps its entries after the run.
  • Two new tests in git.test.ts use a fake git that moves the clock past the TTL while it runs. A lookup within the TTL of the command's end must reuse the result, from memory and then, after clearGitCache(), from disk. One test covers output and the other a failure (a timeout). Both fail on main, where git runs twice.

Demo

Six git widgets, piped twice in a row through a scratch repo whose git status takes 1.5s, with the git cache TTL at 1s. This scales down a large repository's status outlasting the 5s default, so the GIF stays short. Each render prints how long it took and how many slow git runs it waited on. The status line itself is the same before and after; the wait is what changes.

Powerline

Before:

Six git widgets in Powerline mode before the fix

After:

Six git widgets in Powerline mode after the fix

Plain

Before:

Six git widgets in plain mode before the fix

After:

Six git widgets in plain mode after the fix

Testing

  • bun test: 3170 pass, 0 fail (178 files), including the 2 new tests. bun run lint is clean.
  • git.test.ts under vitest on Node 26.10.0: the failures are the existing ESM spy failures (vi.spyOn(os, 'homedir') in the shared temp-home helper), the same as on main. The two new tests also use that helper. With the spy line removed locally, they pass under Node.
  • The built CLI renders the same in plain and Powerline mode under Bun 1.4.2 and Node 26.10.0, and the same as main. The TUI opens and exits cleanly under both.
  • End to end with a stand-in slow git: the table above.

The git command cache timed each entry from the moment the lookup
began, before git ran. A command that takes as long as the TTL (5
seconds by default), such as `git status` in a large repository or any
command cut off by the 5-second timeout, was therefore stored already
expired. Every widget sharing that command missed the cache and ran it
again: Git Status, Clean Status and the staged, unstaged and untracked
flags and file counts all read `status --porcelain -z`, and Insertions,
Deletions and Changes all read the two `diff --shortstat` commands. A
line with several of them waited on the same slow command once per
widget, long enough for the status line to stay blank or stale.

Stamp the entry once git returns, after a success or a failure alike,
and store that one entry in both the in-process and the on-disk cache.
Lookups still compare against the time they began. The custom command
cache already works this way.
@sirmalloc
sirmalloc merged commit a6bb8c1 into sirmalloc:main Oct 29, 2026
3 checks passed
axisrow added a commit to axisrow/ccstatusline that referenced this pull request Oct 29, 2026
sirmalloc#755 live picker preview, sirmalloc#754 fast mode widget, sirmalloc#753 thinking-effort test stub, sirmalloc#752 transcript block fallback fixes, sirmalloc#751 editor keys no longer pause on global install lookup, sirmalloc#750 git cache TTL starts after the run, sirmalloc#749 npx cache skip, sirmalloc#747 Block Usage hides at 100% and swaps with Block Reset Timer)

Conflict policy: upstream infra wins, fork features ported on top. Joins:
sirmalloc#750 stamps the git cache after the run while keeping the fork's execGit
hardening/env snapshot and daemon prefetch bookkeeping; sirmalloc#752's
isApiErrorMessage guard lands in the fork's shared collectTimestampFromLine
(sync + async twins), upstream's duplicate getActivityTimestamp dropped;
sirmalloc#747 adds SESSION_AT_LIMIT alongside the fork's zero hide state
(BlockResetTimer keeps zero + under-limit via isAtSessionLimit, SessionUsage
is no-data/zero/at-limit); sirmalloc#755 replaces the fork's picker window + "Showing
X-Y of N" note with upstream's renderWindowedList/getPickerMaxVisible and
live preview (markers ↑/↓ N more cover the counts); sirmalloc#751's installationState
useMemo replaces the fork's inline lookups, the fork's mainMenuIndex helper
is kept.

Co-Authored-By: Claude Code <[email protected]>
pcvelz added a commit to pcvelz/ccstatusline-usage that referenced this pull request Oct 29, 2026
Upstream: Extra Usage Limit widget (sirmalloc#756), live widget preview while picking (sirmalloc#755), Fast Mode widget (sirmalloc#754), transcript block fallback fixes (sirmalloc#752), editor keys on a global install (sirmalloc#751), git cache TTL (sirmalloc#750), npx cached copy skip (sirmalloc#749), Block Usage hide at 100% (sirmalloc#747), Block Reset Timer hide until 100% (sirmalloc#697), fish-style branch abbreviation (sirmalloc#614), jj detached HEAD (sirmalloc#705), hooks save warning (sirmalloc#703), dev reload (sirmalloc#700), merge ends at a hidden widget (sirmalloc#698), Block Forecast and Block Limit Timer widgets (sirmalloc#699), contributor guide (sirmalloc#706), POSIX path case fix (sirmalloc#707), whitespace-distinct conflict filenames (sirmalloc#709), Quick Start reorder (sirmalloc#715), bunx launch guidance (sirmalloc#714), jj diff summary counts (sirmalloc#711), powerline manual separators (sirmalloc#713), keychain credential by account (sirmalloc#532), Keychain probe timeout (sirmalloc#760), usage cache scoped per CLAUDE_CONFIG_DIR (sirmalloc#266), test isolation (sirmalloc#717, sirmalloc#753), dev-dependency bumps (sirmalloc#745)

Conflicts resolved per the standard-resolution table; fork deletions re-applied.

Hand edits outside conflicts:
- src/utils/global-command-resolution.ts: fork-rename (ccstatusline@ -> ccstatusline-usage@ in upstream's new bunx pattern)
- src/utils/__tests__/global-package-manager.test.ts: fork-rename (package dir ccstatusline-usage in upstream's new tests)
- src/utils/usage-fetch.ts: compat fix - cacheUsageData gained a fetchedAtMs arg (upstream sirmalloc#266); adapted the fork-only fetchFreshUsageData call site
- src/utils/__tests__/usage-fetch.test.ts: compat fixes - cache/lock now hashed per CLAUDE_CONFIG_DIR (usageFilePath helper) in four fork tests; upstream fableUsage renamed to fork's weeklyFableUsage; seedUsageCache takes TokenHome
- src/utils/__tests__/usage-prefetch.test.ts: compat fix - mockFetchUsageData renamed to upstream's mockFetchUsage in cleanly-merged tests
- README.md: took upstream's Quick Start move (sirmalloc#715) and dropped the fork's old copy; fork-rename of its ccstatusline@ specifiers; links to fork-deleted docs (docs/USAGE.md, docs/WINDOWS.md) replaced with the fork's inline text
- CONTRIBUTING.md: links to the fork-deleted docs/DEVELOPMENT.md pointed at the README Development section
- docs/test-retirements.md: ledger entries for the 12 upstream test renames (sirmalloc#697, sirmalloc#698, sirmalloc#700, sirmalloc#705, sirmalloc#713, sirmalloc#760)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants