diff --git a/internal/sync/sync.go b/internal/sync/sync.go index 0fa00d2..6abfdc8 100644 --- a/internal/sync/sync.go +++ b/internal/sync/sync.go @@ -303,11 +303,15 @@ func (s *Syncer) syncWorker(ctx context.Context, jobs <-chan *gh.Repository, res if s.opts.DryRun { if s.git.IsGitRepo(localPath) { - fmt.Printf("[DRY RUN] Would update: %s\n", repoName) + if s.opts.Verbose { + fmt.Printf("[DRY RUN] Would update: %s\n", repoName) + } s.reportProgress(repoName, ProgressUpdated, "dry-run") results <- syncResult{repoName: repoName, status: ProgressUpdated} } else { - fmt.Printf("[DRY RUN] Would clone: %s\n", repoName) + if s.opts.Verbose { + fmt.Printf("[DRY RUN] Would clone: %s\n", repoName) + } s.reportProgress(repoName, ProgressCloned, "dry-run") results <- syncResult{repoName: repoName, status: ProgressCloned} } @@ -379,11 +383,15 @@ func (s *Syncer) syncSingleRepo(ctx context.Context, repo *gh.Repository, result if s.opts.DryRun { if s.git.IsGitRepo(localPath) { - fmt.Printf("[DRY RUN] Would update: %s\n", repoName) + if s.opts.Verbose { + fmt.Printf("[DRY RUN] Would update: %s\n", repoName) + } s.reportProgress(repoName, ProgressUpdated, "dry-run") result.Updated = append(result.Updated, repoName) } else { - fmt.Printf("[DRY RUN] Would clone: %s\n", repoName) + if s.opts.Verbose { + fmt.Printf("[DRY RUN] Would clone: %s\n", repoName) + } s.reportProgress(repoName, ProgressCloned, "dry-run") result.Cloned = append(result.Cloned, repoName) } @@ -400,7 +408,9 @@ func (s *Syncer) syncSingleRepo(ctx context.Context, repo *gh.Repository, result if err != nil { result.Failed[repoName] = err s.reportProgress(repoName, ProgressFailed, err.Error()) - fmt.Printf("Failed to update %s: %v\n", repoName, err) + if s.opts.Verbose { + fmt.Printf("Failed to update %s: %v\n", repoName, err) + } } else { if status == ProgressUpToDate { result.UpToDate = append(result.UpToDate, repoName) @@ -421,7 +431,9 @@ func (s *Syncer) syncSingleRepo(ctx context.Context, repo *gh.Repository, result if err := s.cloneRepo(ctx, repo, localPath); err != nil { result.Failed[repoName] = err s.reportProgress(repoName, ProgressFailed, err.Error()) - fmt.Printf("Failed to clone %s: %v\n", repoName, err) + if s.opts.Verbose { + fmt.Printf("Failed to clone %s: %v\n", repoName, err) + } } else { result.Cloned = append(result.Cloned, repoName) s.reportProgress(repoName, ProgressCloned, "") diff --git a/internal/tui/screens/sync_progress.go b/internal/tui/screens/sync_progress.go index 6bf2382..170107d 100644 --- a/internal/tui/screens/sync_progress.go +++ b/internal/tui/screens/sync_progress.go @@ -3,6 +3,7 @@ package screens import ( "context" "fmt" + "sort" "strings" "time" @@ -76,6 +77,9 @@ type SyncProgressScreen struct { // Current repo being synced currentRepo string + + // Failed repositories with error messages + failedRepos map[string]string // map[repoName]errorMessage } type syncProgressItem struct { @@ -97,15 +101,16 @@ func NewSyncProgress(ctx context.Context, app *tui.App) *SyncProgressScreen { s.Style = tui.GetStyles().Spinner return &SyncProgressScreen{ - ctx: ctx, - app: app, - styles: tui.GetStyles(), - keys: tui.GetKeyMap(), - progress: p, - spinner: s, - width: 80, - height: 24, - loading: true, + ctx: ctx, + app: app, + styles: tui.GetStyles(), + keys: tui.GetKeyMap(), + progress: p, + spinner: s, + width: 80, + height: 24, + loading: true, + failedRepos: make(map[string]string), } } @@ -295,6 +300,10 @@ func (s *SyncProgressScreen) Update(msg tea.Msg) (tea.Model, tea.Cmd) { s.collecting = false s.failed++ s.reposCompleted++ + // Store error message if provided + if msg.update.err != nil && msg.update.repoName != "" { + s.failedRepos[msg.update.repoName] = msg.update.err.Error() + } case "archived": s.collecting = false s.archived++ @@ -473,6 +482,25 @@ func (s *SyncProgressScreen) View() string { } if s.failed > 0 { content.WriteString(fmt.Sprintf(" %s Failed: %d\n", s.styles.Error.Render("●"), s.failed)) + // Show detailed error messages for failed repos + if len(s.failedRepos) > 0 { + content.WriteString("\n") + content.WriteString(s.styles.Error.Render("Failed repositories:")) + content.WriteString("\n") + + // Sort repo names for consistent display order + repoNames := make([]string, 0, len(s.failedRepos)) + for repoName := range s.failedRepos { + repoNames = append(repoNames, repoName) + } + sort.Strings(repoNames) + + for _, repoName := range repoNames { + errMsg := s.failedRepos[repoName] + content.WriteString(fmt.Sprintf(" • %s\n", repoName)) + content.WriteString(fmt.Sprintf(" %s\n", s.styles.Muted.Render(errMsg))) + } + } } if s.archived > 0 { content.WriteString(fmt.Sprintf(" %s Archived: %d (preserved locally, no longer on remote)\n", s.styles.Info.Render("●"), s.archived)) @@ -688,6 +716,7 @@ func (s *SyncProgressScreen) runSyncInBackground() { results := make(chan struct { status string idx int + err error }, len(allRepos)) // Track completed count for progress @@ -731,11 +760,18 @@ func (s *SyncProgressScreen) runSyncInBackground() { result, err := syncer.SyncRepoWithData(s.ctx, repo) status := "skipped" + var syncErr error if err != nil { status = "failed" + syncErr = err } else if result != nil { if len(result.Failed) > 0 { status = "failed" + // Get the error from the Failed map + for _, failErr := range result.Failed { + syncErr = failErr + break // Use the first error + } } else if len(result.Cloned) > 0 { status = "cloned" } else if len(result.Updated) > 0 { @@ -754,7 +790,8 @@ func (s *SyncProgressScreen) runSyncInBackground() { results <- struct { status string idx int - }{status: status, idx: idx} + err error + }{status: status, idx: idx, err: syncErr} } }() } @@ -794,6 +831,7 @@ func (s *SyncProgressScreen) runSyncInBackground() { status: res.status, current: int(completedCount), total: total, + err: res.err, } } @@ -866,7 +904,7 @@ type profileSyncProgressUpdate struct { status string // "collecting", "syncing", "cloned", "updated", "up-to-date", "skipped", "failed", "complete" current int total int - err error // Only set when status="complete" and there was an error + err error // Set when status="complete" for overall errors, or status="failed" for individual repo errors } type profileSyncProgressMsg struct {