Repository navigation
feat(ui): live per-repo rain status in --rain TUI (PR F) #27
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
bschellenberger2600
wants to merge
23
commits into
cursor/fetch-retry-transient-971d
Choose a base branch
from
cursor/tui-rain-status-971d
base: cursor/fetch-retry-transient-971d
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
3c2baef
feat(ui): live per-repo rain status in --rain TUI
cursoragent 983a45a
fix(git): classify timeout and cancel errors after process kill
cursoragent f9b9aa2
Merge remote-tracking branch 'origin/cursor/network-failfast-env-971d…
cursoragent 90f4bf3
fix(ui): correct rain skip accounting and timeout highlight
cursoragent 8471400
fix(test): satisfy errcheck on HTTP retry test body close
cursoragent e81f872
Merge remote-tracking branch 'origin/cursor/fetch-retry-transient-971…
cursoragent d5facd2
Merge cursor/fetch-retry-transient-971d into cursor/tui-rain-status-971d
cursoragent 9c4765a
Merge cursor/fetch-retry-transient-971d into cursor/tui-rain-status-971d
cursoragent 9de0ddf
Merge cursor/fetch-retry-transient-971d into cursor/tui-rain-status-971d
cursoragent 437bd5e
fix(cmd): drop unused processRainRepositories on TUI stack tip
cursoragent 9cfd227
Merge cursor/fetch-retry-transient-971d into cursor/tui-rain-status-971d
cursoragent 1f49541
Merge cursor/fetch-retry-transient-971d into cursor/tui-rain-status-971d
cursoragent 235ec8d
fix(tui): drain rain events, abort on q, truncate before color
cursoragent 31b7adf
fix(ui): classify fetch timeouts without human CLI buffer
cursoragent 357fda9
fix(cmd): treat branch skips as incomplete on TUI cancel
cursoragent f17b0b7
fix(cmd): update parallel index test for TUI worker signature
cursoragent 3a925f0
Merge cursor/fetch-retry-transient-971d into cursor/tui-rain-status-971d
cursoragent 006fb4a
fix(cmd): count cancelled repos as skipped in rain summary
cursoragent a26a459
Merge cursor/fetch-retry-transient-971d into cursor/tui-rain-status-971d
cursoragent 648d3df
fix(cmd): count queued repos as skipped when rain run aborts
cursoragent 42a4aac
Merge cursor/fetch-retry-transient-971d into cursor/tui-rain-status-971d
cursoragent 1a8a15d
fix(ui): scroll rain status list to keep cursor visible
cursoragent d5239f5
fix(ui): satisfy staticcheck QF1012 in rain list indicators
cursoragent File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| package cmd | ||
|
|
||
| import ( | ||
| "context" | ||
| "sync" | ||
| ) | ||
|
|
||
| // rainCoordinator provides per-repo cancel contexts under a shared run context. | ||
| type rainCoordinator struct { | ||
| parent context.Context | ||
| mu sync.Mutex | ||
| repos map[string]context.Context | ||
| cancel map[string]context.CancelFunc | ||
| } | ||
|
|
||
| func newRainCoordinator(parent context.Context, repoPaths []string) *rainCoordinator { | ||
| if parent == nil { | ||
| parent = context.Background() | ||
| } | ||
| c := &rainCoordinator{ | ||
| parent: parent, | ||
| repos: make(map[string]context.Context, len(repoPaths)), | ||
| cancel: make(map[string]context.CancelFunc, len(repoPaths)), | ||
| } | ||
| for _, p := range repoPaths { | ||
| ctx, cancel := context.WithCancel(parent) | ||
| c.repos[p] = ctx | ||
| c.cancel[p] = cancel | ||
| } | ||
| return c | ||
| } | ||
|
|
||
| func (c *rainCoordinator) ContextFor(repoPath string) context.Context { | ||
| c.mu.Lock() | ||
| defer c.mu.Unlock() | ||
| if ctx, ok := c.repos[repoPath]; ok { | ||
| return ctx | ||
| } | ||
| return c.parent | ||
| } | ||
|
|
||
| func (c *rainCoordinator) SkipRepo(repoPath string) { | ||
| c.mu.Lock() | ||
| defer c.mu.Unlock() | ||
| if cancel, ok := c.cancel[repoPath]; ok { | ||
| cancel() | ||
| } | ||
| } | ||
|
|
||
| func (c *rainCoordinator) AbortAll() { | ||
| c.mu.Lock() | ||
| defer c.mu.Unlock() | ||
| for _, cancel := range c.cancel { | ||
| cancel() | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| package cmd | ||
|
|
||
| import ( | ||
| "context" | ||
| "errors" | ||
| "strings" | ||
|
|
||
| "github.com/git-rain/git-rain/internal/rainexec" | ||
| ) | ||
|
|
||
| // classifyRepoRainOutcome maps runRainOnRepo totals to a TUI state and adjusts | ||
| // counters when a per-repo cancel should count as skipped, not frozen. | ||
| // Timeout detection uses the structured report (Error / branch Messages) so it | ||
| // works when human CLI output is suppressed (quiet TUI rain phase). | ||
| func classifyRepoRainOutcome(repoCtx context.Context, delta rainTotals, report jsonRepoResult) (rainexec.RepoState, rainTotals) { | ||
| cancelled := repoCtx != nil && errors.Is(repoCtx.Err(), context.Canceled) | ||
| // Only successful updates count as completed work. Branch skips (no-upstream, | ||
| // etc.) are recorded before fetch and must not mask a user cancel as done. | ||
| workDone := delta.updated > 0 | ||
|
|
||
| if cancelled { | ||
| if workDone { | ||
| return rainexec.StateDone, delta | ||
| } | ||
| d := delta | ||
| d.frozen = 0 | ||
| d.failed = 0 | ||
| // Paths that record cancel as frozen (e.g. --fetch-mainline) leave | ||
| // skipped at 0; ensure the repo still appears in the summary. | ||
| if d.skipped == 0 { | ||
| d.skipped = 1 | ||
| } | ||
| return rainexec.StateSkipped, d | ||
|
cursor[bot] marked this conversation as resolved.
Resolved
|
||
| } | ||
|
|
||
| switch { | ||
| case delta.failed > 0: | ||
| return rainexec.StateFailed, delta | ||
| case delta.frozen > 0: | ||
| if repoRainTimedOut(report) { | ||
| return rainexec.StateTimedOut, delta | ||
| } | ||
|
cursor[bot] marked this conversation as resolved.
Resolved
|
||
| return rainexec.StateFrozen, delta | ||
| default: | ||
| return rainexec.StateDone, delta | ||
| } | ||
| } | ||
|
|
||
| func repoRainTimedOut(report jsonRepoResult) bool { | ||
| if strings.Contains(report.Error, "timed out") { | ||
| return true | ||
| } | ||
| for _, br := range report.Branches { | ||
| if strings.Contains(br.Message, "timed out") { | ||
| return true | ||
| } | ||
| } | ||
| return false | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,80 @@ | ||
| package cmd | ||
|
|
||
| import ( | ||
| "context" | ||
| "testing" | ||
|
|
||
| "github.com/git-rain/git-rain/internal/rainexec" | ||
| ) | ||
|
|
||
| func TestClassifyRepoRainOutcome_SkipDoesNotCountFrozen(t *testing.T) { | ||
| ctx, cancel := context.WithCancel(context.Background()) | ||
| cancel() | ||
| state, delta := classifyRepoRainOutcome(ctx, rainTotals{frozen: 1}, jsonRepoResult{Error: "context canceled"}) | ||
| if state != rainexec.StateSkipped { | ||
| t.Fatalf("state = %q, want skipped", state) | ||
| } | ||
| if delta.frozen != 0 { | ||
| t.Fatalf("frozen = %d, want 0", delta.frozen) | ||
| } | ||
| if delta.skipped != 1 { | ||
| t.Fatalf("skipped = %d, want 1", delta.skipped) | ||
| } | ||
| } | ||
|
|
||
| func TestClassifyRepoRainOutcome_SkipAfterWorkShowsDone(t *testing.T) { | ||
| ctx, cancel := context.WithCancel(context.Background()) | ||
| cancel() | ||
| state, delta := classifyRepoRainOutcome(ctx, rainTotals{updated: 1, frozen: 1}, jsonRepoResult{}) | ||
| if state != rainexec.StateDone { | ||
| t.Fatalf("state = %q, want done", state) | ||
| } | ||
| if delta.frozen != 1 { | ||
| t.Fatalf("delta frozen unchanged when work completed") | ||
| } | ||
| } | ||
|
|
||
| func TestClassifyRepoRainOutcome_TimedOutFromReportError(t *testing.T) { | ||
| // Quiet TUI path never fills the human buffer; Error still carries the timeout. | ||
| state, _ := classifyRepoRainOutcome(context.Background(), rainTotals{frozen: 1}, jsonRepoResult{ | ||
| Error: "timed out after 120s", | ||
| }) | ||
| if state != rainexec.StateTimedOut { | ||
| t.Fatalf("state = %q, want timed_out", state) | ||
| } | ||
| } | ||
|
|
||
| func TestClassifyRepoRainOutcome_TimedOutFromBranchMessage(t *testing.T) { | ||
| state, _ := classifyRepoRainOutcome(context.Background(), rainTotals{frozen: 1}, jsonRepoResult{ | ||
| Branches: []jsonBranchResult{{Message: "timed out after 30s"}}, | ||
| }) | ||
| if state != rainexec.StateTimedOut { | ||
| t.Fatalf("state = %q, want timed_out", state) | ||
| } | ||
| } | ||
|
|
||
| func TestClassifyRepoRainOutcome_FrozenWithoutTimeout(t *testing.T) { | ||
| state, _ := classifyRepoRainOutcome(context.Background(), rainTotals{frozen: 1}, jsonRepoResult{ | ||
| Error: "could not reach remote — check your network and try again", | ||
| }) | ||
| if state != rainexec.StateFrozen { | ||
| t.Fatalf("state = %q, want frozen", state) | ||
| } | ||
| } | ||
|
|
||
| func TestClassifyRepoRainOutcome_SkipAfterBranchSkipsNotDone(t *testing.T) { | ||
| ctx, cancel := context.WithCancel(context.Background()) | ||
| cancel() | ||
| // Pre-fetch skips (e.g. no-upstream) plus frozen from a cancelled fetch must | ||
| // still classify as skipped, not done. | ||
| state, delta := classifyRepoRainOutcome(ctx, rainTotals{skipped: 1, frozen: 1}, jsonRepoResult{Error: "context canceled"}) | ||
| if state != rainexec.StateSkipped { | ||
| t.Fatalf("state = %q, want skipped", state) | ||
| } | ||
| if delta.frozen != 0 { | ||
| t.Fatalf("frozen = %d, want 0", delta.frozen) | ||
| } | ||
| if delta.skipped != 1 { | ||
| t.Fatalf("skipped = %d, want 1", delta.skipped) | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| package cmd | ||
|
|
||
| import ( | ||
| "github.com/git-rain/git-rain/internal/config" | ||
| "github.com/git-rain/git-rain/internal/git" | ||
| "github.com/git-rain/git-rain/internal/rainexec" | ||
| "github.com/git-rain/git-rain/internal/registry" | ||
| "github.com/git-rain/git-rain/internal/ui" | ||
| ) | ||
|
|
||
| func runRainOnReposTUI(cfg *config.Config, reg *registry.Registry, repos []git.Repository, opts git.RainOptions, fullSync bool, abortRun func()) error { | ||
| if len(repos) == 0 { | ||
| return nil | ||
| } | ||
|
|
||
| paths := make([]string, len(repos)) | ||
| for i, r := range repos { | ||
| paths[i] = r.Path | ||
| } | ||
| coord := newRainCoordinator(opts.RunCtx, paths) | ||
| events := make(chan rainexec.Event, len(repos)*8) | ||
| done := make(chan rainTotals, 1) | ||
|
|
||
| go func() { | ||
| totals, _ := runRainReposParallel(reg, repos, opts, fullSync, cfg.Global.FetchWorkers, events, coord, true) | ||
| close(events) | ||
| done <- totals | ||
| }() | ||
|
|
||
| ctrl := ui.RainControls{ | ||
| Skip: coord.SkipRepo, | ||
| Abort: func() { | ||
| coord.AbortAll() | ||
| if abortRun != nil { | ||
| abortRun() | ||
| } | ||
| }, | ||
| } | ||
| _ = ui.RunRainProgressTUI(repos, events, ctrl, opts.FetchTimeoutSec) | ||
| // Drain so workers never block on emitRainEvent after the TUI stops consuming. | ||
| go func() { | ||
| for range events { | ||
| } | ||
| }() | ||
| totals := <-done | ||
|
cursor[bot] marked this conversation as resolved.
Resolved
|
||
| return printRainSummary(totals, opts.RunCtx) | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.