@@ -202,81 +202,141 @@ func runRain(_ *cobra.Command, _ []string) error {
202202
203203 totalUpdated := 0
204204 totalSkipped := 0
205+ totalFrozen := 0
205206 totalFailed := 0
206- repoFailures := 0
207207
208208 for _ , repo := range active {
209- fmt .Printf ("Repo: %s\n " , repo .Name )
209+ fmt .Printf (" %s\n " , repo .Name )
210210
211211 if rainFetchOnly {
212212 if fetchErr := fetchOnly (repo .Path ); fetchErr != nil {
213- repoFailures ++
214- fmt .Printf (" ❌ fetch failed: %s\n \n " , safety .SanitizeText (fetchErr .Error ()))
213+ fmt .Printf (" ❄ (all branches): %s\n \n " ,
214+ safety .SanitizeText (fetchFailureMessage (fetchErr .Error ())))
215+ totalFrozen ++
215216 continue
216217 }
217- fmt .Println (" ✓ fetched" )
218+ fmt .Println (" ↓ fetched" )
218219 fmt .Println ()
219220 totalUpdated ++
220221 continue
221222 }
222223
223224 res , rainErr := git .RainRepository (repo .Path , rainOpts )
224225 if rainErr != nil {
225- repoFailures ++
226- fmt . Printf ( " ❌ failed: %s \n \n " , safety . SanitizeText ( rainErr . Error ()))
226+ fmt . Printf ( " ✗ failed: %s \n \n " , safety . SanitizeText ( rainErr . Error ()))
227+ totalFailed ++
227228 continue
228229 }
229230 if len (res .Branches ) == 0 {
230- fmt .Println (" ⊘ no local branches found " )
231+ fmt .Println (" · no local branches" )
231232 fmt .Println ()
232233 continue
233234 }
234235
235236 for _ , br := range res .Branches {
236- symbol := "⊘"
237- switch br .Outcome {
238- case git .RainOutcomeUpdated :
239- symbol = "✓"
240- case git .RainOutcomeUpdatedRisky :
241- symbol = "⚠️"
242- case git .RainOutcomeFailed :
243- symbol = "❌"
244- }
245- line := fmt .Sprintf (" %s %s" , symbol , br .Branch )
237+ symbol := weatherSymbol (br .Outcome )
238+ line := fmt .Sprintf (" %s %s" , symbol , br .Branch )
246239 if br .Upstream != "" {
247- line += " <- " + br .Upstream
240+ line += " ← " + br .Upstream
248241 }
249- line += ": " + br .Outcome
242+ line += ": " + outcomeLabel ( br .Outcome )
250243 if br .Message != "" {
251- line += " ( " + safety .SanitizeText (strings .TrimSpace (br .Message )) + ")"
244+ line += " — " + safety .SanitizeText (strings .TrimSpace (br .Message ))
252245 }
253246 if br .BackupBranch != "" {
254- line += " backup= " + br .BackupBranch
247+ line += " ( backup: " + br .BackupBranch + ")"
255248 }
256249 fmt .Println (line )
257250 }
258251 fmt .Println ()
259252
260253 totalUpdated += res .Updated
261254 totalSkipped += res .Skipped
255+ totalFrozen += res .Frozen
262256 totalFailed += res .Failed
263- if res .Failed > 0 {
264- repoFailures ++
265- }
266257 }
267258
268- fmt .Println (strings .Repeat ("=" , 50 ))
269- fmt .Println ("🌧️ Rain complete" )
270- fmt .Printf ("Updated branches: %d\n " , totalUpdated )
271- fmt .Printf ("Skipped branches: %d\n " , totalSkipped )
272- fmt .Printf ("Failed branches: %d\n " , totalFailed )
273-
274- if repoFailures > 0 || totalFailed > 0 {
275- return fmt .Errorf ("rain completed with failures" )
259+ fmt .Println (strings .Repeat ("─" , 48 ))
260+ if totalFailed > 0 {
261+ fmt .Printf ("🌧 rain stopped — %d updated, %d skipped, %d frozen, %d failed\n " ,
262+ totalUpdated , totalSkipped , totalFrozen , totalFailed )
263+ return fmt .Errorf ("%d branch(es) failed — check output above" , totalFailed )
276264 }
265+ if totalFrozen > 0 {
266+ fmt .Printf ("🌧 rain delivered — %d updated, %d skipped, %d frozen (try again when reachable)\n " ,
267+ totalUpdated , totalSkipped , totalFrozen )
268+ return nil
269+ }
270+ fmt .Printf ("🌧 rain delivered — %d updated, %d skipped\n " , totalUpdated , totalSkipped )
277271 return nil
278272}
279273
274+ // weatherSymbol returns the terminal symbol for a branch outcome.
275+ func weatherSymbol (outcome string ) string {
276+ switch outcome {
277+ case git .RainOutcomeUpdated :
278+ return "↓"
279+ case git .RainOutcomeUpdatedRisky :
280+ return "⚡"
281+ case git .RainOutcomeUpToDate :
282+ return "·"
283+ case git .RainOutcomeFrozen :
284+ return "❄"
285+ case git .RainOutcomeFailed :
286+ return "✗"
287+ default :
288+ return "~" // fog — skipped for any local-state reason
289+ }
290+ }
291+
292+ // outcomeLabel returns a calm, readable label for a branch outcome.
293+ func outcomeLabel (outcome string ) string {
294+ switch outcome {
295+ case git .RainOutcomeUpdated :
296+ return "synced"
297+ case git .RainOutcomeUpdatedRisky :
298+ return "realigned"
299+ case git .RainOutcomeUpToDate :
300+ return "current"
301+ case git .RainOutcomeFrozen :
302+ return "frozen"
303+ case git .RainOutcomeFailed :
304+ return "failed"
305+ case git .RainOutcomeSkippedNoUpstream :
306+ return "no upstream"
307+ case git .RainOutcomeSkippedAmbiguousUpstream :
308+ return "ambiguous upstream"
309+ case git .RainOutcomeSkippedUpstreamMissing :
310+ return "upstream missing"
311+ case git .RainOutcomeSkippedCheckedOut :
312+ return "checked out elsewhere"
313+ case git .RainOutcomeSkippedLocalAhead :
314+ return "local ahead"
315+ case git .RainOutcomeSkippedDiverged :
316+ return "diverged"
317+ case git .RainOutcomeSkippedUnsafeMerge :
318+ return "unsafe merge"
319+ case git .RainOutcomeSkippedUnsafeDirty :
320+ return "dirty worktree"
321+ default :
322+ return outcome
323+ }
324+ }
325+
326+ // fetchFailureMessage extracts a calm message from a fetchOnly error.
327+ func fetchFailureMessage (errMsg string ) string {
328+ s := strings .ToLower (errMsg )
329+ if strings .Contains (s , "authentication" ) || strings .Contains (s , "permission denied" ) ||
330+ strings .Contains (s , "could not read" ) || strings .Contains (s , "401" ) || strings .Contains (s , "403" ) {
331+ return "could not authenticate with remote — check your credentials and try again"
332+ }
333+ if strings .Contains (s , "could not resolve" ) || strings .Contains (s , "connection" ) ||
334+ strings .Contains (s , "timed out" ) || strings .Contains (s , "network" ) {
335+ return "could not reach remote — check your network and try again"
336+ }
337+ return "fetch did not complete — try again when the remote is reachable"
338+ }
339+
280340func runDryRun (scanOpts git.ScanOptions , rainOpts git.RainOptions ) error {
281341 repos , err := git .ScanRepositories (scanOpts )
282342 if err != nil {
@@ -411,78 +471,72 @@ func runRainOnRepos(repos []git.Repository, opts git.RainOptions) error {
411471
412472 totalUpdated := 0
413473 totalSkipped := 0
474+ totalFrozen := 0
414475 totalFailed := 0
415- repoFailures := 0
416476
417477 for _ , repo := range repos {
418- fmt .Printf ("Repo: %s\n " , repo .Name )
478+ fmt .Printf (" %s\n " , repo .Name )
419479
420480 if rainFetchOnly {
421481 if fetchErr := fetchOnly (repo .Path ); fetchErr != nil {
422- repoFailures ++
423- fmt .Printf (" ❌ fetch failed: %s\n \n " , safety .SanitizeText (fetchErr .Error ()))
482+ fmt .Printf (" ❄ (all branches): %s\n \n " ,
483+ safety .SanitizeText (fetchFailureMessage (fetchErr .Error ())))
484+ totalFrozen ++
424485 continue
425486 }
426- fmt .Println (" ✓ fetched" )
487+ fmt .Println (" ↓ fetched" )
427488 fmt .Println ()
428489 totalUpdated ++
429490 continue
430491 }
431492
432493 res , rainErr := git .RainRepository (repo .Path , opts )
433494 if rainErr != nil {
434- repoFailures ++
435- fmt . Printf ( " ❌ failed: %s \n \n " , safety . SanitizeText ( rainErr . Error ()))
495+ fmt . Printf ( " ✗ failed: %s \n \n " , safety . SanitizeText ( rainErr . Error ()))
496+ totalFailed ++
436497 continue
437498 }
438499 if len (res .Branches ) == 0 {
439- fmt .Println (" ⊘ no local branches found " )
500+ fmt .Println (" · no local branches" )
440501 fmt .Println ()
441502 continue
442503 }
443504
444505 for _ , br := range res .Branches {
445- symbol := "⊘"
446- switch br .Outcome {
447- case git .RainOutcomeUpdated :
448- symbol = "✓"
449- case git .RainOutcomeUpdatedRisky :
450- symbol = "⚠️"
451- case git .RainOutcomeFailed :
452- symbol = "❌"
453- }
454- line := fmt .Sprintf (" %s %s" , symbol , br .Branch )
506+ symbol := weatherSymbol (br .Outcome )
507+ line := fmt .Sprintf (" %s %s" , symbol , br .Branch )
455508 if br .Upstream != "" {
456- line += " <- " + br .Upstream
509+ line += " ← " + br .Upstream
457510 }
458- line += ": " + br .Outcome
511+ line += ": " + outcomeLabel ( br .Outcome )
459512 if br .Message != "" {
460- line += " ( " + safety .SanitizeText (strings .TrimSpace (br .Message )) + ")"
513+ line += " — " + safety .SanitizeText (strings .TrimSpace (br .Message ))
461514 }
462515 if br .BackupBranch != "" {
463- line += " backup= " + br .BackupBranch
516+ line += " ( backup: " + br .BackupBranch + ")"
464517 }
465518 fmt .Println (line )
466519 }
467520 fmt .Println ()
468521
469522 totalUpdated += res .Updated
470523 totalSkipped += res .Skipped
524+ totalFrozen += res .Frozen
471525 totalFailed += res .Failed
472- if res .Failed > 0 {
473- repoFailures ++
474- }
475526 }
476527
477- fmt .Println (strings .Repeat ("=" , 50 ))
478- fmt .Println ("🌧️ Rain complete" )
479- fmt .Printf ("Updated branches: %d\n " , totalUpdated )
480- fmt .Printf ("Skipped branches: %d\n " , totalSkipped )
481- fmt .Printf ("Failed branches: %d\n " , totalFailed )
482-
483- if repoFailures > 0 || totalFailed > 0 {
484- return fmt .Errorf ("rain completed with failures" )
528+ fmt .Println (strings .Repeat ("─" , 48 ))
529+ if totalFailed > 0 {
530+ fmt .Printf ("🌧 rain stopped — %d updated, %d skipped, %d frozen, %d failed\n " ,
531+ totalUpdated , totalSkipped , totalFrozen , totalFailed )
532+ return fmt .Errorf ("%d branch(es) failed — check output above" , totalFailed )
533+ }
534+ if totalFrozen > 0 {
535+ fmt .Printf ("🌧 rain delivered — %d updated, %d skipped, %d frozen (try again when reachable)\n " ,
536+ totalUpdated , totalSkipped , totalFrozen )
537+ return nil
485538 }
539+ fmt .Printf ("🌧 rain delivered — %d updated, %d skipped\n " , totalUpdated , totalSkipped )
486540 return nil
487541}
488542
0 commit comments