>_EXECUTIVE SUMMARY
zsh-git-repo-cache is the indexing layer for "every git repo on the machine". The plugin crawls the root filesystem once with fd (or falls back to find), strips trailing /.git, removes macOS /System/Volumes read-only mirror prefixes, and writes the deduped, sorted list to $ZPWR_ALL_GIT_DIRS. A second pass walks each path with git diff-index --quiet HEAD + git ls-files --exclude-standard --others and bisects the master list into a dirty and clean cache. All queries — live and cache-backed, list and fzf-interactive — are 6 thin shims over those three files. The plugin is the upstream feeder for cd-like fzf workflows in zpwr; its job is to make the master list cheap enough that the prompt can read it on every command.
Plugin Source — 287 LOC
Entrypoint (zsh-git-repo-cache.plugin.zsh, 55 LOC) only sets default env vars, defines a fallback zpwrExists + zpwrPrettyPrint, appends autoload/ to fpath, autoloads every fn, and registers 10 ZPWR_VERBS when running under zpwr. All real work lives in the 8 autoload functions (232 LOC) lazy-loaded by zsh's autoload -Uz.
#FILE STRUCTURE
Every autoload file follows the canonical zsh-plugin pattern: # -*- mode: sh -*- + function name(){...} + a trailing self-call so the file is also source-able as a script. Stems match function names, so fpath autoload + direct source both resolve identically.
| File | Lines | Role |
|---|---|---|
| zsh-git-repo-cache.plugin.zsh | 55 | Plugin entrypoint: default env, fallback zpwrExists/zpwrPrettyPrint, fpath append, autoload -Uz, ZPWR_VERBS registration |
| autoload/zsh-git-repo-searchDirtyGitRepos | 58 | Live walk: git diff-index --quiet HEAD + git ls-files --exclude-standard --others per path; emits dirty paths; fzf or list mode |
| autoload/zsh-git-repo-searchCleanGitRepos | 55 | Live walk: complement of the above; emits paths with no staged diff and no untracked files |
| autoload/zsh-git-repo-regenAllGitReposDirty | 35 | Bisect master cache into dirty + clean caches in a single pass via exec 3> + exec 4> |
| autoload/zsh-git-repo-searchAllGitRepos | 34 | Cache-backed query: regenerate if missing/empty, then list or fzf-pick |
| autoload/zsh-git-repo-searchCleanGitReposCache | 34 | Cache-backed clean query: regenerates dirty/clean caches if either is missing/empty |
| autoload/zsh-git-repo-searchDirtyGitReposCache | 34 | Cache-backed dirty query: same regen-on-empty contract as the clean variant |
| autoload/zsh-git-repo-regenAllGitRepos | 25 | Scorched-earth rescan: fd '\.git$' / (or find / -name .git -type d -prune), strip trailing /.git, strip macOS /System/Volumes mirror prefixes, sort + uniq |
| autoload/zsh-git-repo-searchGitCommon | 12 | Shared tail: capture zsh-git-repo-goThere output, echo it for transparency, eval to actually cd |
| 9 source files | 287 | 1 entrypoint + 8 autoload fns |
$DATA MODEL
Three plain-text caches under $HOME, one path per line, sorted + deduped. No SQLite, no LMDB, no on-disk format version — the file format is "newline-separated absolute paths." Reading is cat + fzf; writing is sort | uniq. The format choice is load-bearing for the prompt-read use case: read -t0 against a flat file is the fastest possible "is this repo dirty?" answer.
| Env var | Default path | Producer | Purpose |
|---|---|---|---|
| ZPWR_ALL_GIT_DIRS | ~/.zsh-git-repo-cache | regenAllGitRepos | Master list of every git repo on disk |
| ZPWR_ALL_GIT_DIRS_DIRTY | ~/.zsh-git-repo-cache-dirty | regenAllGitReposDirty | Repos with uncommitted changes or untracked files |
| ZPWR_ALL_GIT_DIRS_CLEAN | ~/.zsh-git-repo-cache-clean | regenAllGitReposDirty | Repos with no staged diff and no untracked files |
| ZPWR_TEMPFILE3 | $TMPDIR/.zsh-git-repo-temp | regenAllGitRepos | Scratch buffer for inline perl -i -pe edits before sort+uniq |
| ZPWR_FZF | fzf | plugin entrypoint | Fzf invocation name (override for custom wrappers) |
@SCAN PIPELINE
A single regenAllGitRepos invocation produces a fully normalized master list. Two-tool dispatch (fd when available, find as fallback) means the plugin works on a fresh box and gets the 8-thread fd speedup on developer machines. Two perl -i -pe passes strip trailing /.git and (on macOS) the read-only Apple-mounted mirror prefixes that would otherwise produce a 2x-duplicated list.
/ ZPWR_ALL_GIT_DIRS
│ ▲
│ sudo fd '\.git$' / --type d --threads=8 │ sort | uniq
│ --hidden --no-ignore (or) sudo find / │
▼ -name .git -type d -prune │
┌──────────────────────┐ │
│ raw .git path stream │ │
└─────────┬────────────┘ │
│ perl -ne 'print "$1\n" if m{(/.*.git)/*$}' │
▼ │
┌──────────────────────┐ │
│ ZPWR_TEMPFILE3 │ │
└─────────┬────────────┘ │
│ perl -i -pe 's@/.git$@@' │
│ perl -i -pe 's@^/System/Volumes/Data@@' │ (macOS)
│ perl -i -pe 's@^/System/Volumes/Update/mnt1@@'│ (macOS)
▼ │
┌──────────────────────┐ │
│ normalized repo dirs │──────────────────────────────────┘
└──────────────────────┘
│
│ regenAllGitReposDirty: walk + git diff-index --quiet HEAD
│ + git ls-files --exclude-standard --others
▼
┌──────────────────────┐ ┌──────────────────────┐
│ ZPWR_ALL_GIT_DIRS_ │ │ ZPWR_ALL_GIT_DIRS_ │
│ DIRTY (exec 3>) │ │ CLEAN (exec 4>) │
└──────────────────────┘ └──────────────────────┘
The dirty/clean bisection writes to both files concurrently via two long-lived file descriptors (exec 3>, exec 4>) so a single while read loop over the master list produces both caches in one pass. Branch order matters: git diff-index --quiet HEAD first (cheapest, no I/O when clean), then git ls-files --exclude-standard --others only on the clean side to catch repos that have untracked files but no staged delta. Worktree is saved/restored via origPwd="$PWD" + builtin cd -q "$origPwd" at the end — the function is reentrant and won't poison the caller's CWD even on error.
&QUERY SURFACE
Two axes of query: live (walk master list, ask git fresh) vs cached (read pre-computed dirty/clean files), and list (stdout) vs fzf (interactive). Six query functions + 1 shared tail = full matrix. The cached path is the prompt-friendly one (no fork-per-repo); the live path is the up-to-the-second one (no stale-cache risk).
| Function | Mode | Source | Output |
|---|---|---|---|
| searchAllGitRepos | cache | $ZPWR_ALL_GIT_DIRS | all repos (list or fzf) |
| searchDirtyGitRepos | live | walk master + git probe | dirty repos right now |
| searchCleanGitRepos | live | walk master + git probe | clean repos right now |
| searchDirtyGitReposCache | cache | $ZPWR_ALL_GIT_DIRS_DIRTY | dirty repos as of last bisect |
| searchCleanGitReposCache | cache | $ZPWR_ALL_GIT_DIRS_CLEAN | clean repos as of last bisect |
| searchGitCommon | tail | shared exec for the 5 fns above | echoes the cd command, then evals |
| regenAllGitRepos | rebuild | fd or find | master cache |
| regenAllGitReposDirty | rebuild | walk master + git probe | dirty + clean caches in one pass |
Each query fn defines two inner closures — zsh-git-repo-goThere (pipe through fzf, materialize a cd "..." command) and zsh-git-repo-listThere (raw stdout). The dispatch is one if [[ $cmd == list ]] guard; the regen guard (shouldRegen == regen OR cache missing OR cache empty) is identical across all 5 search variants so adding a new cache type is a copy-paste-rename, not a redesign.
~ZPWR VERB BINDINGS
When the plugin loads inside zpwr ((( ${+ZPWR_VERBS} )) guard), it registers 10 verbs into the global ZPWR_VERBS hash: 5 list-mode + 5 fzf-mode, full cross product of (all / clean / dirty / clean-cached / dirty-cached). Each value is the function name to run plus a free-text help line zpwr surfaces in its help screen.
gitreposlist
zsh-git-repo-searchAllGitRepos n list — emit all repos to stdout
gitreposcleanlist
zsh-git-repo-searchCleanGitRepos n list — live-walk clean repos to stdout
gitreposdirtylist
zsh-git-repo-searchDirtyGitRepos n list — live-walk dirty repos to stdout
gitreposcleancachelist
zsh-git-repo-searchCleanGitReposCache n list — clean cache to stdout
gitreposdirtycachelist
zsh-git-repo-searchDirtyGitReposCache n list — dirty cache to stdout
gitrepos
zsh-git-repo-searchAllGitRepos — fzf-pick + cd
gitreposclean
zsh-git-repo-searchCleanGitRepos — fzf-pick live clean repos
gitreposdirty
zsh-git-repo-searchDirtyGitRepos — fzf-pick live dirty repos
gitreposcleancache
zsh-git-repo-searchCleanGitReposCache — fzf-pick cached clean repos
gitreposdirtycache
zsh-git-repo-searchDirtyGitReposCache — fzf-pick cached dirty repos
*TEST COVERAGE
29 zunit @test cases across 6 test files pin behavioural invariants: plugin-contract (entrypoint name, zsh -n clean parse, #compdef directive on completions), function-level contract (autoload presence, ZPWR_VERBS values, defensive fallback definitions), and surface syntax (every autoload file parses, every fn name matches its file stem). 21 additional .sh doc-hygiene gates pin README/docs/workflow structure — final newlines, body+html tags, h1 presence, https-only links, target=_blank rel=noopener pairing, no inline handlers, no placeholder hrefs, no deprecated tags.
| Test file | @test | Pins |
|---|---|---|
| tests/t-fns.zsh | 8 | Every autoload fn is defined after sourcing the plugin; fn name matches file stem; autoload/ on fpath; fallback zpwrExists + zpwrPrettyPrint respect existing definitions |
| tests/t-contract2.zsh | 6 | ZPWR_VERBS contains all 10 expected keys when sourced under a stubbed zpwr; verb values reference real autoload fns |
| tests/t-contract3.zsh | 5 | Env var defaults: ZPWR_ALL_GIT_DIRS, _DIRTY, _CLEAN, ZPWR_TEMPFILE3, ZPWR_FZF all set when unset |
| tests/t-contract4.zsh | 5 | Existing env values are not clobbered when plugin is re-sourced; idempotent re-source contract |
| tests/t-contract.zsh | 3 | Entrypoint stem matches plugin directory; entrypoint parses under zsh -n; every _* completion file starts with #compdef |
| tests/t-syntax.zsh | 2 | Every autoload/* file parses under zsh -n; no syntax drift on push |
| 6 zunit files | 29 | + 21 .sh doc-hygiene gates |
// DOC GATES
| Gate | Pins |
|---|---|
| docs-final-newline | Every docs/*.html ends with \n; git diff stays one-line clean |
| docs-has-body-tag | Every docs/*.html contains a <body> element |
| docs-has-h1 | Every docs/*.html contains an <h1> |
| docs-has-html-closing | Every docs/*.html closes </html> |
| docs-no-deprecated-tags | No <font>, <center>, <marquee>, <blink>, etc. |
| docs-no-http-links | No bare http:// — https only |
| docs-no-inline-handlers | No inline DOM event-handler attributes (onclick, onchange, ...) — Tauri CSP guard |
| docs-no-placeholder-href | No placeholder anchor hrefs (literal hash or javascript: scheme); real anchors only |
| docs-target-blank-rel-noopener | Every target="_blank" pairs with rel="noopener" |
| readme-final-newline / has-badges / has-h2-section / has-https-link | README structural pins |
| man-page-final-newline / no-trailing-whitespace / synopsis-section | Man-page hygiene (passes vacuously if no man pages) |
| tests-shell-executable / tests-shell-shebang | Every tests/*.sh is +x and has a shebang |
| workflow-final-newline / workflow-no-tabs | GitHub Actions YAML hygiene |
| cargo-final-newline | Vacuous (no Cargo.toml); kept for cross-repo template parity |
?KEY DESIGN DECISIONS
Each call-out is a decision the implementation could have gone either way on, with the rationale for the path taken.
Flat-file caches, not SQLite
One repo per line, sorted+uniq. fzf wants newline-separated input directly; cat $CACHE | fzf is the entire query path. SQLite would force a SELECT ... INTO bridge for every read and add a build-time dep on sqlite3. Flat text is the fastest format for the actual access pattern.
fd preferred, find fallback
zpwrExists fd probe at every regen. fd with --threads=8 + --hidden --no-ignore walks a developer-laptop tree in seconds; find -name .git -type d -prune works everywhere zsh runs. The plugin doesn't break on a fresh server, but speeds up wherever fd is available.
One-pass dirty/clean bisect
regenAllGitReposDirty opens exec 3> on the dirty cache and exec 4> on the clean cache, walks the master list once, and writes each path to exactly one fd. No second pass, no grep -vf diff. The cleanup pair (exec 3>&-, exec 4>&-) closes both fds even on early return.
macOS Volumes mirror strip
APFS mounts the user data root at /System/Volumes/Data/ and the update mirror at /System/Volumes/Update/mnt1/. find / emits paths under both prefixes for the same physical repo. Two perl -i -pe passes strip those prefixes so sort | uniq collapses the dupes. Without this the cache double-counts every repo.
Sudo for the crawl
The walk uses sudo -E "PATH=$PATH" env fd ... (or sudo find /) because the root filesystem contains repos owned by system users and read-restricted dirs that an unprivileged walk would miss or warn on. -E preserves the user's env (and PATH=$PATH ensures fd resolves) instead of switching to root's PATH.
Inner closures, not exported helpers
Each search fn defines zsh-git-repo-goThere + zsh-git-repo-listThere as inner fns visible only inside its body. The shared tail (searchGitCommon) then resolves whichever closure was just defined. Avoids a global namespace collision when two search fns are nested in one shell session.
Echo-then-eval for transparency
searchGitCommon captures zsh-git-repo-goThere output (a literal cd "..." string), echoes it to the terminal, then evals it. The user sees the cd command in scrollback — useful for re-running, sharing, or just understanding what the fzf pick resolved to.
Defensive zpwr stubs
If zpwrExists or zpwrPrettyPrint aren't defined (plugin loaded outside zpwr), the entrypoint defines minimal fallbacks. The ZPWR_VERBS registration is guarded by (( ${+ZPWR_VERBS} )) so a standalone install gets all 8 functions usable directly without zpwr.
!STRATEGIC POSITION
The plugin is the upstream feeder for any "jump to a git repo on this machine" workflow. zpwr's gitrepos* verbs are the public face; the same files back ad-hoc prompts, dirty-tree indicators, and CI-side audits ("which repos on this host have uncommitted work").
Prompt feeder
The dirty cache is small (one line per dirty repo) and cheap to grep -F "$PWD" on every prompt redraw — faster than calling git status in a possibly-large repo.
fzf cd
gitrepos + fzf is the canonical "jump to any of my repos" workflow. Master cache means the user can cd to a 5-deep nested repo by typing two letters of its basename.
CI audit
On a build host, gitreposdirtylist answers "did anything leak uncommitted changes on this box". The list mode is grep-friendly — the canonical use is gitreposdirtylist | wc -l as a health metric.
Multi-repo dispatch
The master cache is the input list for "run X across every repo on this machine" loops — the same primitive that backs parallel-style multi-repo pulls.
+LOAD MODEL
The plugin is autoload-clean: sourcing the entrypoint costs only env-var defaults + fpath append + 10 hash assignments. The 8 autoload functions don't run until called, and each runs the file body exactly once (the trailing self-call inside each file is also the autoload-time body). Zero git invocations, zero fd/find invocations, zero stat calls at source time.
| Step | Cost | Side effect |
|---|---|---|
Source zsh-git-repo-cache.plugin.zsh | ~milliseconds | 5 env-var defaults (only if unset), 2 fallback fn defs (only if missing), fpath+=, autoload -Uz register, ZPWR_VERBS hash assignment |
First call to any search* | cache hit: cheap; cache miss: fd/find walk | Cache files created/refreshed under $HOME |
First call to regenAllGitRepos | fd: ~5s on a developer laptop; find: ~30s | Master cache rewritten |
First call to regenAllGitReposDirty | 2 git calls per repo | Dirty + clean caches rewritten in one pass |
| Subsequent cached queries | cat + fzf only | None (read-only) |
.GIT POSTURE
42 commits on main. The repo carries no submodules, no generated artifacts, and no vendored deps — every byte is hand-written zsh + the docs cyberpunk template (hud-static.css + tutorial.css) shared across the MenkeTechnologies plugin stack. CI runs the zunit + doc-gate suite on every push (.github/workflows/ci.yml).
Compat floor: works on every zsh 5.x. Hard deps: zsh, perl (system perl, for the trailing-slash + Volumes-mirror strip passes), git. Soft deps: fd (accelerated crawl), fzf (interactive pick), sudo (full-disk crawl). No deps on zpwr — the plugin works standalone, ZPWR_VERBS is opt-in.