From d8c1b799e72bd7775ddf9a36016c5d8c96de9529 Mon Sep 17 00:00:00 2001 From: rios0rios0 Date: Thu, 2 Apr 2026 01:12:36 -0300 Subject: [PATCH 1/3] fix(repo): moved per-repo logging into goroutines for progressive feedback - moved per-repo log calls from post-`wg.Wait()` loops into goroutines so results appear as each repo completes, instead of all at once at the end - applied to all 6 batch-logging operations: sync, prune, fork-sync, failover, mirror, and restore - post-Wait loops now only compute summary counters - logrus mutex ensures thread-safe concurrent writes Co-Authored-By: Claude Opus 4.6 (1M context) --- internal/repo/failover.go | 11 ++++++----- internal/repo/fork_sync.go | 12 ++++++------ internal/repo/mirror.go | 11 ++++++----- internal/repo/prune.go | 19 ++++++++++--------- internal/repo/restore.go | 11 ++++++----- internal/repo/sync.go | 11 ++++++----- 6 files changed, 40 insertions(+), 35 deletions(-) diff --git a/internal/repo/failover.go b/internal/repo/failover.go index 96fb58b..f77e4c8 100644 --- a/internal/repo/failover.go +++ b/internal/repo/failover.go @@ -45,7 +45,12 @@ func RunFailover(cfg FailoverConfig) error { go func(idx int, path string) { defer wg.Done() defer func() { <-sem }() - results[idx] = failoverSingleRepo(path, cfg.RootDir, cfg.Runner) + result := failoverSingleRepo(path, cfg.RootDir, cfg.Runner) + log.WithFields(logger.Fields{ + "repo": result.Name, + "status": result.Status, + }).Info(result.Status) + results[idx] = result }(i, repoPath) } @@ -59,10 +64,6 @@ func RunFailover(cfg FailoverConfig) error { counts := map[string]int{"switched": 0, "skipped": 0, "failed": 0} for _, r := range results { - log.WithFields(logger.Fields{ - "repo": r.Name, - "status": r.Status, - }).Info(r.Status) category, known := failoverStatusCategory[r.Status] if !known { category = "failed" diff --git a/internal/repo/fork_sync.go b/internal/repo/fork_sync.go index 5f63002..9497fcd 100644 --- a/internal/repo/fork_sync.go +++ b/internal/repo/fork_sync.go @@ -114,7 +114,12 @@ func parallelForkSync(forks []globalEntities.Repository, cfg ForkSyncConfig) []F defer wg.Done() defer func() { <-sem }() repoPath := filepath.Join(cfg.RootDir, Key(fork)) - results[idx] = ForkSyncSingleRepo(repoPath, fork, cfg) + result := ForkSyncSingleRepo(repoPath, fork, cfg) + cfg.Output.WithFields(logger.Fields{ + "repo": result.Name, + "status": result.Status, + }).Info("fork-sync result") + results[idx] = result }(i, f) } @@ -307,11 +312,6 @@ func restoreAfterForkSync( func logForkSyncSummary(results []ForkSyncResult, log logger.FieldLogger) { synced, conflicts, failed := 0, 0, 0 for _, r := range results { - log.WithFields(logger.Fields{ - "repo": r.Name, - "status": r.Status, - }).Info("fork-sync result") - switch { case strings.HasPrefix(r.Status, "synced"): synced++ diff --git a/internal/repo/mirror.go b/internal/repo/mirror.go index 30bfa09..7006285 100644 --- a/internal/repo/mirror.go +++ b/internal/repo/mirror.go @@ -86,7 +86,12 @@ func RunMirror(cfg MirrorConfig) error { defer wg.Done() defer func() { <-sem }() time.Sleep(time.Duration(idx) * mirrorAPIDelay / time.Duration(workers)) - results[idx] = mirrorSingleRepo(path, cfg, providerName, owner, mirrorProvider) + result := mirrorSingleRepo(path, cfg, providerName, owner, mirrorProvider) + log.WithFields(logger.Fields{ + "repo": result.Name, + "status": result.Status, + }).Info(result.Status) + results[idx] = result }(i, repoPath) } @@ -100,10 +105,6 @@ func RunMirror(cfg MirrorConfig) error { counts := map[string]int{"mirrored": 0, "skipped": 0, mirrorStatusFailed: 0} for _, r := range results { - log.WithFields(logger.Fields{ - "repo": r.Name, - "status": r.Status, - }).Info(r.Status) category, known := mirrorStatusCategory[r.Status] if !known { category = mirrorStatusFailed diff --git a/internal/repo/prune.go b/internal/repo/prune.go index 0ac9c91..005b2c4 100644 --- a/internal/repo/prune.go +++ b/internal/repo/prune.go @@ -46,7 +46,16 @@ func RunPrune(rootDir string, runner GitRunner, dryRun bool, output io.Writer) e defer wg.Done() defer func() { <-sem }() repoPath := filepath.Join(rootDir, name) - results[idx] = PruneSingleRepo(repoPath, rootDir, runner, dryRun) + result := PruneSingleRepo(repoPath, rootDir, runner, dryRun) + statusLower := strings.ToLower(result.Status) + hasFailure := strings.HasPrefix(result.Status, "FAIL") || strings.Contains(statusLower, "failed") + if len(result.Deleted) > 0 || hasFailure { + log.WithFields(logger.Fields{ + "repo": result.Name, + "status": result.Status, + }).Info("prune result") + } + results[idx] = result }(i, repoName) } @@ -56,14 +65,6 @@ func RunPrune(rootDir string, runner GitRunner, dryRun bool, output io.Writer) e for _, r := range results { statusLower := strings.ToLower(r.Status) hasFailure := strings.HasPrefix(r.Status, "FAIL") || strings.Contains(statusLower, "failed") - - if len(r.Deleted) > 0 || hasFailure { - log.WithFields(logger.Fields{ - "repo": r.Name, - "status": r.Status, - }).Info("prune result") - } - switch { case hasFailure: failed++ diff --git a/internal/repo/restore.go b/internal/repo/restore.go index b36750f..cb25de0 100644 --- a/internal/repo/restore.go +++ b/internal/repo/restore.go @@ -45,7 +45,12 @@ func RunRestore(cfg RestoreConfig) error { go func(idx int, path string) { defer wg.Done() defer func() { <-sem }() - results[idx] = restoreSingleRepo(path, cfg.RootDir, cfg.Runner) + result := restoreSingleRepo(path, cfg.RootDir, cfg.Runner) + log.WithFields(logger.Fields{ + "repo": result.Name, + "status": result.Status, + }).Info(result.Status) + results[idx] = result }(i, repoPath) } @@ -58,10 +63,6 @@ func RunRestore(cfg RestoreConfig) error { counts := map[string]int{"restored": 0, "skipped": 0, "failed": 0} for _, r := range results { - log.WithFields(logger.Fields{ - "repo": r.Name, - "status": r.Status, - }).Info(r.Status) category, known := restoreStatusCategory[r.Status] if !known { category = "failed" diff --git a/internal/repo/sync.go b/internal/repo/sync.go index 3660bf2..19351bf 100644 --- a/internal/repo/sync.go +++ b/internal/repo/sync.go @@ -41,7 +41,12 @@ func RunSync(rootDir string, runner GitRunner, output io.Writer) error { go func(idx int, path string) { defer wg.Done() defer func() { <-sem }() - results[idx] = SyncSingleRepo(path, rootDir, runner) + result := SyncSingleRepo(path, rootDir, runner) + log.WithFields(logger.Fields{ + "repo": result.Name, + "status": result.Status, + }).Info("sync result") + results[idx] = result }(i, repoPath) } @@ -49,10 +54,6 @@ func RunSync(rootDir string, runner GitRunner, output io.Writer) error { synced, wip, failed := 0, 0, 0 for _, r := range results { - log.WithFields(logger.Fields{ - "repo": r.Name, - "status": r.Status, - }).Info("sync result") switch { case strings.HasPrefix(r.Status, "synced"): synced++ From c9194b88046ff436a397333025555f962769db41 Mon Sep 17 00:00:00 2001 From: rios0rios0 Date: Thu, 2 Apr 2026 14:42:09 -0300 Subject: [PATCH 2/3] fix(pr-review): extracted `isPruneFailure` helper to deduplicate failure detection Co-Authored-By: Claude Opus 4.6 (1M context) --- internal/repo/prune.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/internal/repo/prune.go b/internal/repo/prune.go index 005b2c4..85f3ecb 100644 --- a/internal/repo/prune.go +++ b/internal/repo/prune.go @@ -18,6 +18,11 @@ type PruneResult struct { Status string } +// isPruneFailure returns true if the given status string indicates a failure. +func isPruneFailure(status string) bool { + return strings.HasPrefix(status, "FAIL") || strings.Contains(strings.ToLower(status), "failed") +} + // RunPrune deletes merged branches in all repositories under rootDir in parallel. func RunPrune(rootDir string, runner GitRunner, dryRun bool, output io.Writer) error { log := NewLogger(output) @@ -47,9 +52,7 @@ func RunPrune(rootDir string, runner GitRunner, dryRun bool, output io.Writer) e defer func() { <-sem }() repoPath := filepath.Join(rootDir, name) result := PruneSingleRepo(repoPath, rootDir, runner, dryRun) - statusLower := strings.ToLower(result.Status) - hasFailure := strings.HasPrefix(result.Status, "FAIL") || strings.Contains(statusLower, "failed") - if len(result.Deleted) > 0 || hasFailure { + if len(result.Deleted) > 0 || isPruneFailure(result.Status) { log.WithFields(logger.Fields{ "repo": result.Name, "status": result.Status, @@ -63,10 +66,8 @@ func RunPrune(rootDir string, runner GitRunner, dryRun bool, output io.Writer) e pruned, skipped, failed := 0, 0, 0 for _, r := range results { - statusLower := strings.ToLower(r.Status) - hasFailure := strings.HasPrefix(r.Status, "FAIL") || strings.Contains(statusLower, "failed") switch { - case hasFailure: + case isPruneFailure(r.Status): failed++ case len(r.Deleted) > 0: pruned += len(r.Deleted) From f8253235f77636377170997d6edce2f6e44d3a11 Mon Sep 17 00:00:00 2001 From: rios0rios0 Date: Thu, 2 Apr 2026 14:43:44 -0300 Subject: [PATCH 3/3] docs(changelog): added progressive logging entry to CHANGELOG.md Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f040749..26e7baa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ Exceptions are acceptable depending on the circumstances (critical bug fixes tha - changed `DefaultCommandRunner.RunInteractive` to use `sh -c` for proper shell operator support (redirection, pipes) - changed the Go module dependencies to their latest versions - changed the Go module dependencies to their latest versions +- changed per-repo logging in parallel operations to run inside goroutines for progressive feedback ## [0.4.0] - 2026-03-31