diff --git a/CHANGELOG.md b/CHANGELOG.md index fdd47d7f7d..84c9a3722f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ All notable changes to `src-cli` are documented in this file. ### Changed +- The progress bar in `src campaign [preview|apply]` now shows when executing a step failed in a repository by styling the line red and displaying standard error output. [#355](https://github.com/sourcegraph/src-cli/pull/355) + ### Fixed ## 3.21.2 diff --git a/cmd/src/campaign_progress_printer.go b/cmd/src/campaign_progress_printer.go index 60d69db17d..88ea4926c9 100644 --- a/cmd/src/campaign_progress_printer.go +++ b/cmd/src/campaign_progress_printer.go @@ -134,7 +134,11 @@ func (p *campaignProgressPrinter) PrintStatuses(statuses []*campaigns.TaskStatus // Log that this task completed, but only if there is no // currently executing one in this bar, to avoid flicker. if _, ok := p.statusBarRepo[idx]; !ok { - p.progress.StatusBarCompletef(idx, statusText) + if ts.Err != nil { + p.progress.StatusBarFailf(idx, statusText) + } else { + p.progress.StatusBarCompletef(idx, statusText) + } } delete(p.repoStatusBar, ts.RepoName) } @@ -161,12 +165,24 @@ func (p *campaignProgressPrinter) PrintStatuses(statuses []*campaigns.TaskStatus } } +type statusTexter interface { + StatusText() string +} + func taskStatusText(ts *campaigns.TaskStatus) (string, error) { var statusText string if ts.IsCompleted() { if ts.ChangesetSpec == nil { - statusText = "No changes" + if ts.Err != nil { + if texter, ok := ts.Err.(statusTexter); ok { + statusText = texter.StatusText() + } else { + statusText = ts.Err.Error() + } + } else { + statusText = "No changes" + } } else { fileDiffs, err := diff.ParseMultiFileDiff([]byte(ts.ChangesetSpec.Commits[0].Diff)) if err != nil { diff --git a/internal/campaigns/executor.go b/internal/campaigns/executor.go index 413995e13c..60d7924102 100644 --- a/internal/campaigns/executor.go +++ b/internal/campaigns/executor.go @@ -32,6 +32,13 @@ func (e TaskExecutionErr) Error() string { ) } +func (e TaskExecutionErr) StatusText() string { + if stepErr, ok := e.Err.(stepFailedErr); ok { + return stepErr.SingleLineError() + } + return e.Err.Error() +} + type Executor interface { AddTask(repo *graphql.Repository, steps []Step, template *ChangesetTemplate) *TaskStatus LogFiles() []string diff --git a/internal/campaigns/run_steps.go b/internal/campaigns/run_steps.go index e692fc3453..42b844d9b7 100644 --- a/internal/campaigns/run_steps.go +++ b/internal/campaigns/run_steps.go @@ -262,3 +262,12 @@ func (e stepFailedErr) Error() string { return out.String() } + +func (e stepFailedErr) SingleLineError() string { + out := e.Err.Error() + if len(e.Stderr) > 0 { + out = e.Stderr + } + + return strings.Split(out, "\n")[0] +} diff --git a/internal/output/progress_with_status_bars.go b/internal/output/progress_with_status_bars.go index a266954911..43ba899cb1 100644 --- a/internal/output/progress_with_status_bars.go +++ b/internal/output/progress_with_status_bars.go @@ -5,6 +5,7 @@ type ProgressWithStatusBars interface { StatusBarUpdatef(i int, format string, args ...interface{}) StatusBarCompletef(i int, format string, args ...interface{}) + StatusBarFailf(i int, format string, args ...interface{}) StatusBarResetf(i int, label, format string, args ...interface{}) } diff --git a/internal/output/progress_with_status_bars_simple.go b/internal/output/progress_with_status_bars_simple.go index c32eae9a5f..5a2c7df278 100644 --- a/internal/output/progress_with_status_bars_simple.go +++ b/internal/output/progress_with_status_bars_simple.go @@ -32,6 +32,16 @@ func (p *progressWithStatusBarsSimple) StatusBarCompletef(i int, format string, } } +func (p *progressWithStatusBarsSimple) StatusBarFailf(i int, format string, args ...interface{}) { + if p.statusBars[i] != nil { + wasCompleted := p.statusBars[i].completed + p.statusBars[i].Failf(format, args...) + if !wasCompleted { + writeStatusBar(p.Output, p.statusBars[i]) + } + } +} + func (p *progressWithStatusBarsSimple) StatusBarResetf(i int, label, format string, args ...interface{}) { if p.statusBars[i] != nil { p.statusBars[i].Resetf(label, format, args...) diff --git a/internal/output/progress_with_status_bars_tty.go b/internal/output/progress_with_status_bars_tty.go index b87df211e1..dbbeba8b56 100644 --- a/internal/output/progress_with_status_bars_tty.go +++ b/internal/output/progress_with_status_bars_tty.go @@ -153,6 +153,17 @@ func (p *progressWithStatusBarsTTY) StatusBarCompletef(i int, format string, arg p.drawInSitu() } +func (p *progressWithStatusBarsTTY) StatusBarFailf(i int, format string, args ...interface{}) { + p.o.lock.Lock() + defer p.o.lock.Unlock() + + if p.statusBars[i] != nil { + p.statusBars[i].Failf(format, args...) + } + + p.drawInSitu() +} + func (p *progressWithStatusBarsTTY) draw() { for _, bar := range p.bars { p.writeBar(bar) @@ -205,8 +216,13 @@ func (p *progressWithStatusBarsTTY) determineStatusBarLabelWidth() { func (p *progressWithStatusBarsTTY) writeStatusBar(last bool, statusBar *StatusBar) { style := StylePending if statusBar.completed { - style = StyleSuccess + if statusBar.failed { + style = StyleWarning + } else { + style = StyleSuccess + } } + box := "├── " if last { box = "└── " diff --git a/internal/output/status_bar.go b/internal/output/status_bar.go index 8e9fc33d6f..ae9005b19a 100644 --- a/internal/output/status_bar.go +++ b/internal/output/status_bar.go @@ -6,6 +6,7 @@ import "time" // of a process. type StatusBar struct { completed bool + failed bool label string format string @@ -24,10 +25,17 @@ func (sb *StatusBar) Completef(format string, args ...interface{}) { sb.finishedAt = time.Now() } +// Failf sets the StatusBar to completed and failed and updates its text. +func (sb *StatusBar) Failf(format string, args ...interface{}) { + sb.Completef(format, args...) + sb.failed = true +} + // Resetf sets the status of the StatusBar to incomplete and updates its label and text. func (sb *StatusBar) Resetf(label, format string, args ...interface{}) { sb.initialized = true sb.completed = false + sb.failed = false sb.label = label sb.format = format sb.args = args