Skip to content

docs(readme): behavior accuracy, dry-run note, config vs registry locking#5

Open
bschellenberger2600 wants to merge 14 commits intomainfrom
docs/readme-dry-run-durability-locks
Open

docs(readme): behavior accuracy, dry-run note, config vs registry locking#5
bschellenberger2600 wants to merge 14 commits intomainfrom
docs/readme-dry-run-durability-locks

Conversation

@bschellenberger2600
Copy link
Copy Markdown
Member

@bschellenberger2600 bschellenberger2600 commented Apr 17, 2026

Summary

  • README: behavior aligned with code (full hydration, prune, backup refs, config example), dry-run pun, config vs registry durability, strict default_mode note.
  • Legacy cleanup: example config.toml and TUI Default mode row now use canonical global.default_mode strings (sync-default, sync-all, sync-current-branch, leave-untouched) matching git.ParseMode. Removed mistaken push-* labels that never matched the parser.
  • Validation: global.default_mode is validated on load; empty normalizes to sync-default; invalid values error.
  • Trimmed product copy that only mirrored git-fire (--rain flag, README); shortened CLAUDE.md maintenance notes.

Testing

  • make test-race
  • make lint

Note

Medium Risk
Medium risk because it changes config persistence semantics (adds cross-process file locking and stricter global.default_mode validation) and adjusts concurrent scan/TUI cancellation and buffering, which could affect interactive behavior and shutdown paths.

Overview
Updates docs to more precisely describe fetch vs full-hydration triggers, --prune precedence, --dry-run behavior, and registry/config durability.

Makes global.default_mode canonical and strict: defaults to sync-default, validates allowed values on load, updates TUI settings/options and registry entry comments to match, and adds tests for invalid/empty cases.

Hardens interactive --rain flow and scanning: ScanRepositoriesStream now passes a context into per-repo git calls so cancel aborts in-flight subprocesses, runRainTUIStream cancels before draining channels and increases buffering to avoid goroutine stalls, and the TUI layout is refactored to shared panel width helpers with clamped rendering plus explicit ctrl+c/interrupt-to-cancel handling.

Reviewed by Cursor Bugbot for commit cf8e229. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • Documentation

    • Reworked docs/help to clarify operational modes, risky/prune/dry-run behaviors, locking/exit differences, TUI keybindings and quit/cancel behavior, and backup ref naming.
  • Bug Fixes

    • Serialized concurrent config writes with locking and atomic PID-scoped temp-file replaces.
    • Improved subprocess cancellation during repository scans.
  • New Features

    • Added/validated sync-default and related mode options; configurable fetch/workers and tag sync.
  • Tests

    • Added tests for dry-run prune messaging, concurrent config saves, cancelable scans, and panel layout.

cursoragent and others added 9 commits April 16, 2026 16:45
Replace the previous default mainline-only remote fetch with git fetch
--all --prune per repository (honoring sync_tags). Introduce
--mainline-fetch to opt into the old MainlineFetchRemotes behavior.
Remove the redundant --fetch-only flag and update docs, UAT, and tests.

Co-authored-by: Ben Schellenberger <bschellenberger2600@users.noreply.github.com>
README and CLAUDE describe default full fetch, --fetch-mainline,
--sync with branch_mode (including all-branches), and --risky on sync.
Rename CLI flag from --mainline-fetch to --fetch-mainline and fix
variable naming; update UAT labels and test messages.

Co-authored-by: Ben Schellenberger <bschellenberger2600@users.noreply.github.com>
Default fetch is git fetch --all without --prune. Add global fetch_prune,
registry fetch_prune, local rain.fetchprune, and --prune/--no-prune flags
(CLI wins). Wire RainOptions.FetchPrune through fetchOnly, MainlineFetchRemotes,
and RainRepository. Document precedence and pruning risks in README with a
warning callout. Add unit tests for resolution order and ReadRainFetchPrune.

Co-authored-by: Ben Schellenberger <bschellenberger2600@users.noreply.github.com>
Dry-run now prints resolved Fetch --prune for --sync as well as fetch paths.
Registry Upsert for-loop indentation matches other methods after gofmt.
…guard

- applyRepoFetchPrune: skip git config read when CLI sets prune; warn and
  fall back on ReadRainFetchPrune errors
- Reject --fetch-mainline with full-sync triggers; add test
- runDryRun: show fetch --prune resolution for --sync path too
- Fix Upsert for-loop indentation (gofmt)
- UAT scenario 7 echo matches prune-opt-in default
- CLAUDE/README: prune precedence and --fetch-mainline note
- Replace misleading frozen label (fetch --all)

Co-authored-by: Ben Schellenberger <bschellenberger2600@users.noreply.github.com>
Co-authored-by: Ben Schellenberger <bschellenberger2600@users.noreply.github.com>
- Resolve conflicts in cmd/root.go and cmd/root_test.go (keep fetch-mainline
  note and dry-run prune test).
- SaveConfig and WriteExampleConfig: exclusive flock on config.toml.lock,
  PID-scoped temp file, atomic rename (avoids corruption / stale .tmp).
- Add github.com/gofrs/flock; concurrent SaveConfig test.

Co-authored-by: Ben Schellenberger <bschellenberger2600@users.noreply.github.com>
Match full-branch-hydration triggers to computeFullSync, fix default_mode
example, document prune precedence and backup ref shape. Add a dry-run
pun and a durability note contrasting repos.toml locking with config.toml
atomic writes and remaining race scenarios.
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 17, 2026

📝 Walkthrough

Walkthrough

Documentation clarified fetch/sync/prune/risky semantics and TUI behavior; config defaults and validation for default_mode changed to sync-default; config writes now use PID-scoped temp files with a file lock; git repo scanning was made context-aware and cancellable; TUI channel/cancellation ordering and panel sizing/layout were adjusted. Tests added for dry-run prune messaging, scanner cancellation drain, concurrent config writes, and layout math.

Changes

Cohort / File(s) Summary
Documentation
README.md, CLAUDE.md
Rewrote operational-mode ordering and --risky semantics to include full branch-hydration paths; documented --prune precedence and opt-in behavior; clarified --dry-run; updated TUI, backup-ref naming, and config field docs.
CLI & TUI runtime
cmd/root.go
Adjusted help text for flags; changed TUI stream buffer to fixed 256; reordered scan cancellation so cancelScan() runs before draining the TUI channel.
CLI tests
cmd/root_test.go
Added TestRunRain_DryRunSyncShowsPruneResolution to assert dry-run prune resolution and "Would hydrate" messaging.
Config loader & defaults
internal/config/loader.go, internal/config/defaults.go, internal/config/config_test.go
Writes now take exclusive .lock, use PID-scoped temp files, and atomically rename into place; Validate() normalizes/validates Global.DefaultMode and defaults empty to sync-default; tests for concurrent saves and validation added.
Config dependency
go.mod
Added indirect dependency github.com/gofrs/flock v0.12.1.
Registry types
internal/registry/types.go
Updated inline doc for RegistryEntry.Mode allowed values (`leave-untouched
Git scanner (cancellation)
internal/git/scanner.go, internal/git/scanner_test.go
Threaded context.Context through repository analysis and subprocess calls (exec.CommandContext); analyzer returns on context cancellation; added test ensuring pre-cancelled ScanRepositoriesStream drains output without blocking.
UI layout & sizing
internal/ui/panel_layout.go, internal/ui/panel_layout_test.go, internal/ui/path_display.go
Added panel sizing helpers and tests; PathWidthFor now computes widths from panel text width and measures fragments; function signature renamed to accept terminalWidth.
Repo selector & config UI
internal/ui/repo_selector.go, internal/ui/config_view.go, internal/ui/path_display.go
TUI: centralized rain width, improved clamping/truncation, global interrupt mapping to ErrCancelled, adjusted keybindings/quit behavior, and updated default-mode enum options (swap push-*sync-*).

Sequence Diagram(s)

sequenceDiagram
  participant TUI as "TUI (picker)"
  participant Scanner as "Scanner (filesystem/git)"
  participant TUIChan as "tuiRepoChan (buffered)"
  participant CancelCtx as "Scan cancel context"

  Note over TUI,Scanner: Start streaming scan
  TUI->>Scanner: Start ScanRepositoriesStream(ctx)
  Scanner-->>TUIChan: send Repo items (async)
  TUIChan-->>TUI: receive Repo items (render)

  alt User selects repo / exits
    TUI->>CancelCtx: cancelScan()  /* cancel before draining */
    CancelCtx-->>Scanner: context cancelled
    Scanner--xTUIChan: stop sending / close channel
    TUI->>TUIChan: drain remaining items
    TUI->>TUI: shutdown UI and return
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐰 I hopped through README fields tonight,

swapped modes and tuned the TUI light.
Locks and temp files set in line,
scans that cancel, UIs that shine.
A little rabbit, tidy and spry — code sleeps soundly by.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately describes the main documentation updates addressing behavior accuracy, dry-run semantics, and locking mechanisms between config and registry files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch docs/readme-dry-run-durability-locks

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

- Example config and TUI config screen use sync-default/sync-all/
  sync-current-branch/leave-untouched (matches ParseMode).
- Validate global.default_mode strictly; empty normalizes to sync-default.
- Trim git-fire mirroring from CLI flag text and README; tighten CLAUDE.md.
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
README.md (1)

244-244: ⚠️ Potential issue | 🟡 Minor

Resolve the --branch-mode trigger contradiction.

Line 244 says --branch-mode applies “With --sync”, but Line 28 and Line 305 say any CLI --branch-mode triggers full branch hydration even without --sync. These statements conflict and will confuse users.

Suggested doc fix
-| `--branch-mode` | With `--sync`: `mainline`, `checked-out`, `all-local`, or `all-branches` (overrides config for this run) |
+| `--branch-mode` | Branch selection mode: `mainline`, `checked-out`, `all-local`, or `all-branches`. On CLI, providing this flag enters the full branch-hydration path for the run (overrides config). |

Based on learnings: Use RainRepository for local branch hydration when --sync, non-mainline branch_mode, or risky-only config forces full sync.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` at line 244, Update the README to remove the contradictory wording
that `--branch-mode` only applies "With `--sync`" and clearly document the
actual behavior: state that supplying the CLI flag `--branch-mode` triggers full
branch hydration even when `--sync` is not provided, and additionally explain
that when `--sync` is used the system uses `RainRepository` for local branch
hydration and that non-mainline `branch_mode` or a risky-only config also forces
full sync; reference the flags `--branch-mode` and `--sync`, the config key
`branch_mode`, and the class `RainRepository` so readers can map the behavior to
the implementation.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@README.md`:
- Line 240: Update the README entry for `--dry-run` to clarify its behavior when
combined with `--no-scan`: mention that `--dry-run` normally skips git
fetch/branch updates but still scans the disk to list repos, and explicitly add
a caveat that if `--no-scan` is also provided, the disk scan is skipped and
`--dry-run` will not list repos from disk; reference the `--dry-run` and
`--no-scan` flags so readers understand the interaction.

---

Outside diff comments:
In `@README.md`:
- Line 244: Update the README to remove the contradictory wording that
`--branch-mode` only applies "With `--sync`" and clearly document the actual
behavior: state that supplying the CLI flag `--branch-mode` triggers full branch
hydration even when `--sync` is not provided, and additionally explain that when
`--sync` is used the system uses `RainRepository` for local branch hydration and
that non-mainline `branch_mode` or a risky-only config also forces full sync;
reference the flags `--branch-mode` and `--sync`, the config key `branch_mode`,
and the class `RainRepository` so readers can map the behavior to the
implementation.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9a59ebd6-1ad1-459e-8fa1-fce7c79d1219

📥 Commits

Reviewing files that changed from the base of the PR and between 95f0929 and 80df3cb.

📒 Files selected for processing (1)
  • README.md

Comment thread README.md
| Flag | Description |
|---|---|
| `--dry-run` | Show what would run without making changes |
| `--dry-run` | No `git fetch` / branch updates — still scans disk to list repos. The name is weather-themed irony: no “wet” git work, but not a no-op. |
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Clarify --dry-run behavior with --no-scan.

The current text says dry-run “still scans disk,” which is not universally true if users also pass --no-scan. A small caveat would make this precise.

Suggested doc fix
-| `--dry-run` | No `git fetch` / branch updates — still scans disk to list repos. The name is weather-themed irony: no “wet” git work, but not a no-op. |
+| `--dry-run` | No `git fetch` / branch updates — still scans disk to list repos (unless `--no-scan` is set). The name is weather-themed irony: no “wet” git work, but not a no-op. |
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
| `--dry-run` | No `git fetch` / branch updates — still scans disk to list repos. The name is weather-themed irony: no wet git work, but not a no-op. |
| `--dry-run` | No `git fetch` / branch updates — still scans disk to list repos (unless `--no-scan` is set). The name is weather-themed irony: no "wet" git work, but not a no-op. |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` at line 240, Update the README entry for `--dry-run` to clarify
its behavior when combined with `--no-scan`: mention that `--dry-run` normally
skips git fetch/branch updates but still scans the disk to list repos, and
explicitly add a caveat that if `--no-scan` is also provided, the disk scan is
skipped and `--dry-run` will not list repos from disk; reference the `--dry-run`
and `--no-scan` flags so readers understand the interaction.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/config/loader.go (1)

118-125: ⚠️ Potential issue | 🟡 Minor

Cleanup stale tmp on os.WriteFile failure too.

If os.WriteFile fails mid-write (e.g. ENOSPC, permissions), a partial path.<pid>.tmp can be left behind — cleanup only happens on Rename failure. Mirror the same os.Remove(tmp) on the WriteFile error path for symmetry and to avoid littered temp files across runs.

Proposed fix
 	tmp := fmt.Sprintf("%s.%d.tmp", path, os.Getpid())
 	if err := os.WriteFile(tmp, data, 0o600); err != nil {
+		_ = os.Remove(tmp)
 		return fmt.Errorf("writing temp config: %w", err)
 	}

Apply the same to WriteExampleConfig at line 228.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/config/loader.go` around lines 118 - 125, The temp file created as
tmp := fmt.Sprintf("%s.%d.tmp", path, os.Getpid()) is not removed if
os.WriteFile(tmp, data, 0o600) fails, leaving stale tmp files; update the error
path in the block that writes and renames the temp file to call os.Remove(tmp)
before returning when os.WriteFile fails, and apply the same cleanup change to
the analogous WriteExampleConfig function so both write-failure and
rename-failure paths remove the temporary file (reference tmp, path,
os.WriteFile, os.Rename, and WriteExampleConfig to locate the spots to change).
🧹 Nitpick comments (1)
internal/config/loader.go (1)

215-235: Duplication with SaveConfig — consider extracting.

WriteExampleConfig is now structurally identical to SaveConfig (lock → write tmp → rename), differing only in payload source and error-message wording. Worth extracting a small atomicWrite(path string, data []byte) error helper to keep the lock/tmp/rename invariants in one place.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/config/loader.go` around lines 215 - 235, WriteExampleConfig
duplicates SaveConfig's lock/tmp/rename logic; extract that shared behavior into
a new helper atomicWrite(path string, data []byte) error that acquires the same
file lock (flock.New(path+".lock")), writes to a temp file
(fmt.Sprintf("%s.%d.tmp", path, os.Getpid())), renames it into place, and cleans
up on error; then update SaveConfig and WriteExampleConfig to call atomicWrite
with their respective payloads (e.g., ExampleConfigTOML() output) and preserve
existing error wrapping and permissions.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/config/loader.go`:
- Around line 108-112: Replace the blocking flock.Lock() call in SaveConfig with
a non-blocking TryLockContext: create a context with a short timeout (e.g.,
500ms) and call lock.TryLockContext(ctx, retryDelay) instead of lock.Lock(); on
timeout or error propagate the error through the existing configSaveErr
mechanism so the TUI can surface the failure; ensure you still defer unlocking
(lock.Unlock()) when TryLockContext succeeds and keep using flock.New(path +
".lock") as before.

---

Outside diff comments:
In `@internal/config/loader.go`:
- Around line 118-125: The temp file created as tmp := fmt.Sprintf("%s.%d.tmp",
path, os.Getpid()) is not removed if os.WriteFile(tmp, data, 0o600) fails,
leaving stale tmp files; update the error path in the block that writes and
renames the temp file to call os.Remove(tmp) before returning when os.WriteFile
fails, and apply the same cleanup change to the analogous WriteExampleConfig
function so both write-failure and rename-failure paths remove the temporary
file (reference tmp, path, os.WriteFile, os.Rename, and WriteExampleConfig to
locate the spots to change).

---

Nitpick comments:
In `@internal/config/loader.go`:
- Around line 215-235: WriteExampleConfig duplicates SaveConfig's
lock/tmp/rename logic; extract that shared behavior into a new helper
atomicWrite(path string, data []byte) error that acquires the same file lock
(flock.New(path+".lock")), writes to a temp file (fmt.Sprintf("%s.%d.tmp", path,
os.Getpid())), renames it into place, and cleans up on error; then update
SaveConfig and WriteExampleConfig to call atomicWrite with their respective
payloads (e.g., ExampleConfigTOML() output) and preserve existing error wrapping
and permissions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 95376c1a-20e6-4f86-b9f1-7add88bf9878

📥 Commits

Reviewing files that changed from the base of the PR and between 80df3cb and 7ce1ab6.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (10)
  • CLAUDE.md
  • README.md
  • cmd/root.go
  • cmd/root_test.go
  • go.mod
  • internal/config/config_test.go
  • internal/config/defaults.go
  • internal/config/loader.go
  • internal/registry/types.go
  • internal/ui/config_view.go
✅ Files skipped from review due to trivial changes (6)
  • go.mod
  • internal/registry/types.go
  • CLAUDE.md
  • cmd/root.go
  • internal/config/defaults.go
  • internal/config/config_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
  • README.md

Comment thread internal/config/loader.go
Comment on lines +108 to +112
lock := flock.New(path + ".lock")
if err := lock.Lock(); err != nil {
return fmt.Errorf("config file lock: %w", err)
}
defer func() { _ = lock.Unlock() }()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

gofrs/flock TryLockContext usage and default retry interval

💡 Result:

TryLockContext in gofrs/flock is a method on Flock that attempts to acquire an exclusive file lock with retries. It takes a context.Context for cancellation/timeout and a time.Duration for the retryDelay between attempts. Signature: func (f Flock) TryLockContext(ctx context.Context, retryDelay time.Duration) (bool, error) Behavior: It repeatedly calls f.TryLock in a loop until: - TryLock succeeds (returns true, locked) - TryLock fails with an error - The context is done (cancelled or timed out) Between attempts, it waits using time.After(retryDelay), or returns immediately if ctx.Err != nil at start. There is no fixed "default retry interval". The retryDelay must be provided by the caller; it has no predefined default value. Examples use 678time.Millisecond, but any time.Duration can be passed (including 0 for no delay). Usage example from official example: f := flock.New(filepath.Join(os.TempDir, "go-lock.lock")) lockCtx, cancel := context.WithTimeout(context.Background, 30time.Second) defer cancel locked, err := f.TryLockContext(lockCtx, 678*time.Millisecond) if err != nil { // handle error } if locked { fmt.Printf("path: %s; locked: %v\n", f.Path, f.Locked) f.Unlock } Source code confirms the implementation uses the provided retryDelay directly in time.After(retryDelay). This is from the latest version v0.13.0 (2025-10-09), actively maintained as of 2026.

Citations:


🏁 Script executed:

# Check if SaveConfig is invoked on keypresses in config_view.go
fd "config_view.go" --type f

Repository: git-fire/git-rain

Length of output: 87


🏁 Script executed:

# Check the SaveConfig implementation in loader.go
cat -n internal/config/loader.go | sed -n '100,120p'

Repository: git-fire/git-rain

Length of output: 1060


🏁 Script executed:

# Search for SaveConfig calls with context
rg "SaveConfig" --type go -B 2 -A 2

Repository: git-fire/git-rain

Length of output: 2488


🏁 Script executed:

# Check lines around 218, 226, 237 in config_view.go to verify keypress handling
wc -l internal/ui/config_view.go

Repository: git-fire/git-rain

Length of output: 91


🏁 Script executed:

# Read the relevant section from config_view.go
cat -n internal/ui/config_view.go | sed -n '210,245p'

Repository: git-fire/git-rain

Length of output: 1071


Use TryLockContext with timeout in SaveConfig to prevent UI freezes on contended or stale locks.

SaveConfig is invoked synchronously from the TUI event loop on space// keypresses (lines 218, 226). flock.Lock() blocks indefinitely, so a stale config.toml.lock, an NFS/remote-FS lock holder, or a concurrently-running git-rain CLI invocation will hang the UI with no feedback. Replace lock.Lock() with lock.TryLockContext(ctx, retryDelay) using a context with short timeout (e.g., 500ms) and surface errors via the existing configSaveErr mechanism.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/config/loader.go` around lines 108 - 112, Replace the blocking
flock.Lock() call in SaveConfig with a non-blocking TryLockContext: create a
context with a short timeout (e.g., 500ms) and call lock.TryLockContext(ctx,
retryDelay) instead of lock.Lock(); on timeout or error propagate the error
through the existing configSaveErr mechanism so the TUI can surface the failure;
ensure you still defer unlocking (lock.Unlock()) when TryLockContext succeeds
and keep using flock.New(path + ".lock") as before.

cursoragent and others added 3 commits April 17, 2026 05:26
Add PanelBlockWidth/PanelTextWidth/RainDisplayWidth so list, settings, rain,
and PathWidthFor share one layout budget (outer margin 6 + box padding 4).
Clamp list/settings rows with lipgloss; align lipgloss padding in tests.
Treat tea.InterruptMsg like quit; map tea.ErrInterrupted to ErrCancelled.

Co-authored-by: bschellenberger2600 <bschellenberger2600@gmail.com>
…lysis

Cancel scan context immediately after the TUI returns so filepath walk and
git subprocesses unwind. Buffer tuiRepoChan to reduce bridge backpressure.
Use exec.CommandContext in ScanRepositoriesStream analysis; add test that a
pre-cancelled scan still closes the output channel.

Co-authored-by: bschellenberger2600 <bschellenberger2600@gmail.com>
Document q/ctrl+c and scan cancellation in README; point maintainers at
panel_layout and runRainTUIStream shutdown in CLAUDE.md.

Co-authored-by: bschellenberger2600 <bschellenberger2600@gmail.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
cmd/root.go (1)

552-564: ⚠️ Potential issue | 🟡 Minor

Bridge goroutine's saveRegistry isn't awaited.

<-scanDone only waits for the scanner goroutine, not for the bridge goroutine at lines 526–536 that does saveRegistry(reg, regPath) after scanChan closes. On the ui.ErrCancelled path a few lines below, os.Exit(0) fires as soon as the drain returns, which can happen before the bridge finalizes and persists registry changes (e.g., newly-discovered repos, LastSeen bumps).

♻️ Suggested fix
 	var scanErr error
 	scanDone := make(chan struct{})
 	go func() {
 		defer close(scanDone)
 		scanErr = git.ScanRepositoriesStream(opts, scanChan)
 	}()

 	now := time.Now()
 	defaultMode := git.ParseMode(cfg.Global.DefaultMode)
+	bridgeDone := make(chan struct{})
 	go func() {
+		defer close(bridgeDone)
 		defer close(tuiRepoChan)
 		for repo := range scanChan {
 			repo, include := upsertRepoIntoRegistry(reg, repo, now, defaultMode)
 			if include {
 				repo.Selected = true
 				tuiRepoChan <- repo
 			}
 		}
 		saveRegistry(reg, regPath)
 	}()
 	...
 	<-scanDone
+	<-bridgeDone
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/root.go` around lines 552 - 564, The bridge goroutine that reads scanChan
and calls saveRegistry(reg, regPath) is not awaited: current code only waits on
<-scanDone (scanner) so the program can exit before the bridge finishes
persisting registry changes. Fix by adding an explicit synchronization for the
bridge (e.g., a done channel or sync.WaitGroup) that the bridge signals after
saveRegistry returns, then wait for that signal before returning/exiting (on the
ui.ErrCancelled path and the normal shutdown path); reference the bridge
goroutine that closes over scanChan and calls saveRegistry(reg, regPath), the
existing scanDone waiter, and the cancelScan() call to ensure the new wait
happens after cancelScan() and draining.
internal/ui/repo_selector.go (1)

1226-1231: ⚠️ Potential issue | 🔴 Critical

Remove unused min — CI lint is failing.

golangci-lint reports func min is unused. After switching to resolveRainBackgroundWidth / RainDisplayWidth, nothing calls the local helper anymore, and Go 1.21+ provides a built-in min anyway.

🧹 Proposed fix
-func min(a, b int) int {
-	if a < b {
-		return a
-	}
-	return b
-}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/ui/repo_selector.go` around lines 1226 - 1231, Remove the unused
local helper function min in internal/ui/repo_selector.go: delete the min(a, b
int) int definition since golangci-lint flags it as unused and Go 1.21+ provides
a built-in min; also search for any residual callers (e.g., inside
resolveRainBackgroundWidth or RainDisplayWidth) and replace them with the
language built-in min if any remain, ensuring no references to the min function
are left behind.
🧹 Nitpick comments (2)
internal/ui/path_display.go (1)

84-89: Document what reserve = 34 accounts for.

The measured prefixW/suffixW exclude the dirty indicator ( 💧, ~3 cells) and the scroll hint ( << SCROLL PATH >>, ~19 cells) which only appears on the focused row. A single opaque 34 makes this easy to drift if either string is tweaked elsewhere. Consider either breaking it out or at least commenting the derivation.

♻️ Example
-	suffixW := lipgloss.Width(fmt.Sprintf("›)  [%s] %s", repo.Mode.String(), remotesInfo))
-	const reserve = 34
+	suffixW := lipgloss.Width(fmt.Sprintf("›)  [%s] %s", repo.Mode.String(), remotesInfo))
+	// reserve = dirty indicator (" 💧" ≈ 3) + scroll hint ("  << SCROLL PATH >>" ≈ 19) + safety margin.
+	const reserve = 34
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/ui/path_display.go` around lines 84 - 89, The hard-coded reserve =
34 is undocumented and hides the width contributions from the per-row dirty
indicator and the focused-row scroll hint; update the pw calculation in
path_display.go to replace the magic number with a named constant (e.g.,
scrollHintWidth or reservedWidth) or add a clear inline comment explaining the
derivation (dirty indicator ~3 cells + scroll hint ~19 cells + any padding = 34)
and use that constant when computing pw alongside prefixW and suffixW so future
changes to the dirty indicator or scroll hint are obvious; reference the
existing variables prefixW, suffixW, reserve (or new constant) and pw to locate
where to apply the change.
internal/git/scanner.go (1)

163-179: Remove the errors.Is(err, context.Canceled) checks—they never match for killed subprocesses.

When exec.CommandContext cancels a child process, cmd.Output() returns an *exec.ExitError with signal information, not an error satisfying errors.Is(err, context.Canceled). The subsequent ctx.Err() guards already detect cancellation correctly, making these branches unreachable dead code.

♻️ Suggested simplification
 	remotes, err := getRemotes(ctx, repoPath)
 	if err == nil {
 		repo.Remotes = remotes
-	} else if errors.Is(err, context.Canceled) {
-		return Repository{}, err
 	}
 
 	if err := ctx.Err(); err != nil {
 		return Repository{}, err
 	}
 
 	dirty, err := isDirty(ctx, repoPath)
 	if err == nil {
 		repo.IsDirty = dirty
-	} else if errors.Is(err, context.Canceled) {
-		return Repository{}, err
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/git/scanner.go` around lines 163 - 179, The checks for
errors.Is(err, context.Canceled) after calling getRemotes and isDirty are
dead/unreliable for killed subprocesses; remove those branches and rely on the
existing ctx.Err() guard instead. Specifically, in internal/git/scanner.go
update the getRemotes and isDirty call sites (the variables remotes, err :=
getRemotes(ctx, repoPath) and dirty, err := isDirty(ctx, repoPath)) to drop the
else if errors.Is(err, context.Canceled) return Repository{}, err blocks and
simply handle success by assigning repo.Remotes / repo.IsDirty and otherwise let
the subsequent ctx.Err() check and the generic error return handle cancellation
and other errors.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In `@cmd/root.go`:
- Around line 552-564: The bridge goroutine that reads scanChan and calls
saveRegistry(reg, regPath) is not awaited: current code only waits on <-scanDone
(scanner) so the program can exit before the bridge finishes persisting registry
changes. Fix by adding an explicit synchronization for the bridge (e.g., a done
channel or sync.WaitGroup) that the bridge signals after saveRegistry returns,
then wait for that signal before returning/exiting (on the ui.ErrCancelled path
and the normal shutdown path); reference the bridge goroutine that closes over
scanChan and calls saveRegistry(reg, regPath), the existing scanDone waiter, and
the cancelScan() call to ensure the new wait happens after cancelScan() and
draining.

In `@internal/ui/repo_selector.go`:
- Around line 1226-1231: Remove the unused local helper function min in
internal/ui/repo_selector.go: delete the min(a, b int) int definition since
golangci-lint flags it as unused and Go 1.21+ provides a built-in min; also
search for any residual callers (e.g., inside resolveRainBackgroundWidth or
RainDisplayWidth) and replace them with the language built-in min if any remain,
ensuring no references to the min function are left behind.

---

Nitpick comments:
In `@internal/git/scanner.go`:
- Around line 163-179: The checks for errors.Is(err, context.Canceled) after
calling getRemotes and isDirty are dead/unreliable for killed subprocesses;
remove those branches and rely on the existing ctx.Err() guard instead.
Specifically, in internal/git/scanner.go update the getRemotes and isDirty call
sites (the variables remotes, err := getRemotes(ctx, repoPath) and dirty, err :=
isDirty(ctx, repoPath)) to drop the else if errors.Is(err, context.Canceled)
return Repository{}, err blocks and simply handle success by assigning
repo.Remotes / repo.IsDirty and otherwise let the subsequent ctx.Err() check and
the generic error return handle cancellation and other errors.

In `@internal/ui/path_display.go`:
- Around line 84-89: The hard-coded reserve = 34 is undocumented and hides the
width contributions from the per-row dirty indicator and the focused-row scroll
hint; update the pw calculation in path_display.go to replace the magic number
with a named constant (e.g., scrollHintWidth or reservedWidth) or add a clear
inline comment explaining the derivation (dirty indicator ~3 cells + scroll hint
~19 cells + any padding = 34) and use that constant when computing pw alongside
prefixW and suffixW so future changes to the dirty indicator or scroll hint are
obvious; reference the existing variables prefixW, suffixW, reserve (or new
constant) and pw to locate where to apply the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 139853fb-5261-473b-8d30-5b4d28edcc8d

📥 Commits

Reviewing files that changed from the base of the PR and between 7ce1ab6 and cf8e229.

📒 Files selected for processing (10)
  • CLAUDE.md
  • README.md
  • cmd/root.go
  • internal/git/scanner.go
  • internal/git/scanner_test.go
  • internal/ui/config_view.go
  • internal/ui/panel_layout.go
  • internal/ui/panel_layout_test.go
  • internal/ui/path_display.go
  • internal/ui/repo_selector.go
✅ Files skipped from review due to trivial changes (1)
  • CLAUDE.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • internal/ui/config_view.go
  • README.md

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