diff --git a/cmd/src/campaigns_common.go b/cmd/src/campaigns_common.go index 9a8d61353c..031ee1c7e0 100644 --- a/cmd/src/campaigns_common.go +++ b/cmd/src/campaigns_common.go @@ -8,11 +8,13 @@ import ( "os" "path" "runtime" + "strings" "time" "github.com/hashicorp/go-multierror" "github.com/neelance/parallel" "github.com/pkg/errors" + "github.com/sourcegraph/go-diff/diff" "github.com/sourcegraph/src-cli/internal/api" "github.com/sourcegraph/src-cli/internal/campaigns" "github.com/sourcegraph/src-cli/internal/output" @@ -222,22 +224,58 @@ func campaignsExecute(ctx context.Context, out *output.Output, svc *campaigns.Se } var progress output.Progress + var maxRepoName int + completed := map[string]bool{} specs, err := svc.ExecuteCampaignSpec(ctx, repos, executor, campaignSpec, func(statuses []*campaigns.TaskStatus) { if progress == nil { progress = out.Progress([]output.ProgressBar{{ - Label: "Executing steps", + Label: fmt.Sprintf("Executing steps in %d repositories", len(statuses)), Max: float64(len(statuses)), }}, nil) } - complete := 0 + unloggedCompleted := []*campaigns.TaskStatus{} + for _, ts := range statuses { - if !ts.FinishedAt.IsZero() { - complete += 1 + if len(ts.RepoName) > maxRepoName { + maxRepoName = len(ts.RepoName) + } + + if ts.FinishedAt.IsZero() { + continue } + + if !completed[ts.RepoName] { + completed[ts.RepoName] = true + unloggedCompleted = append(unloggedCompleted, ts) + } + + } + + progress.SetValue(0, float64(len(completed))) + + for _, ts := range unloggedCompleted { + var statusText string + + if ts.ChangesetSpec == nil { + statusText = "No changes" + } else { + fileDiffs, err := diff.ParseMultiFileDiff([]byte(ts.ChangesetSpec.Commits[0].Diff)) + if err != nil { + panic(err) + } + + statusText = diffStatDescription(fileDiffs) + " " + diffStatDiagram(sumDiffStats(fileDiffs)) + } + + if ts.Cached { + statusText += " (cached)" + } + + progress.Verbosef("%-*s %s", maxRepoName, ts.RepoName, statusText) } - progress.SetValue(0, float64(complete)) }) + if err != nil { return "", "", err } @@ -331,3 +369,38 @@ func formatTaskExecutionErr(err campaigns.TaskExecutionErr) string { err.Logfile, ) } + +func sumDiffStats(fileDiffs []*diff.FileDiff) diff.Stat { + sum := diff.Stat{} + for _, fileDiff := range fileDiffs { + stat := fileDiff.Stat() + sum.Added += stat.Added + sum.Changed += stat.Changed + sum.Deleted += stat.Deleted + } + return sum +} + +func diffStatDescription(fileDiffs []*diff.FileDiff) string { + var plural string + if len(fileDiffs) > 1 { + plural = "s" + } + + return fmt.Sprintf("%d file%s changed", len(fileDiffs), plural) +} + +func diffStatDiagram(stat diff.Stat) string { + const maxWidth = 20 + added := float64(stat.Added + stat.Changed) + deleted := float64(stat.Deleted + stat.Changed) + if total := added + deleted; total > maxWidth { + x := float64(20) / total + added *= x + deleted *= x + } + return fmt.Sprintf("%s%s%s%s", + output.StyleLinesAdded, strings.Repeat("+", int(added)), + output.StyleLinesDeleted, strings.Repeat("-", int(deleted)), + ) +} diff --git a/go.mod b/go.mod index 067c819352..10a5703612 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.13 require ( github.com/dustin/go-humanize v1.0.0 github.com/efritz/pentimento v0.0.0-20190429011147-ade47d831101 - github.com/google/go-cmp v0.4.1 + github.com/google/go-cmp v0.5.2 github.com/hashicorp/go-multierror v1.1.0 github.com/jig/teereadcloser v0.0.0-20181016160506-953720c48e05 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 @@ -17,6 +17,7 @@ require ( github.com/pkg/browser v0.0.0-20180916011732-0a3d74bf9ce4 github.com/pkg/errors v0.9.1 github.com/sourcegraph/codeintelutils v0.0.0-20200824140252-1db3aed5cf58 + github.com/sourcegraph/go-diff v0.6.0 github.com/sourcegraph/jsonx v0.0.0-20200629203448-1a936bd500cf github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect diff --git a/go.sum b/go.sum index 9615471e37..23dd565b92 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,7 @@ github.com/efritz/pentimento v0.0.0-20190429011147-ade47d831101 h1:RylpU+KNJJNEJ github.com/efritz/pentimento v0.0.0-20190429011147-ade47d831101/go.mod h1:5ALWO82UZwfAtNRUtwzsWimcrcuYzyieTyyXOXrP6EQ= github.com/google/go-cmp v0.4.1 h1:/exdXoGamhu5ONeUJH0deniYLWYvQwW66yvlfiiKTu0= github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-multierror v1.1.0 h1:B9UzwGQJehnUY1yNrnwREHc3fGbC2xefo8g4TbElacI= @@ -31,10 +32,14 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/shurcooL/go v0.0.0-20180423040247-9e1955d9fb6e/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk= +github.com/shurcooL/go-goon v0.0.0-20170922171312-37c2f522c041/go.mod h1:N5mDOmsrJOB+vfqUK+7DmDyjhSLIIBnXo9lvZJj3MWQ= github.com/sourcegraph/codeintelutils v0.0.0-20200706141440-54ddac67b5b6 h1:91WE5oskxcHBJIiK8GeUDqGQJWaUBiI0LBfvRxAcDX4= github.com/sourcegraph/codeintelutils v0.0.0-20200706141440-54ddac67b5b6/go.mod h1:HplI8gRslTrTUUsSYwu28hSOderix7m5dHNca7xBzeo= github.com/sourcegraph/codeintelutils v0.0.0-20200824140252-1db3aed5cf58 h1:Ps+U1xoZP+Zoph39YfRB6Q846ghO8pgrAgp0MiObvPs= github.com/sourcegraph/codeintelutils v0.0.0-20200824140252-1db3aed5cf58/go.mod h1:HplI8gRslTrTUUsSYwu28hSOderix7m5dHNca7xBzeo= +github.com/sourcegraph/go-diff v0.6.0 h1:WbN9e/jD8ujU+o0vd9IFN5AEwtfB0rn/zM/AANaClqQ= +github.com/sourcegraph/go-diff v0.6.0/go.mod h1:iBszgVvyxdc8SFZ7gm69go2KDdt3ag071iBaWPF6cjs= github.com/sourcegraph/jsonx v0.0.0-20200629203448-1a936bd500cf h1:oAdWFqhStsWiiMP/vkkHiMXqFXzl1XfUNOdxKJbd6bI= github.com/sourcegraph/jsonx v0.0.0-20200629203448-1a936bd500cf/go.mod h1:ppFaPm6kpcHnZGqQTFhUIAQRIEhdQDWP1PCv4/ON354= github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo= diff --git a/internal/campaigns/executor.go b/internal/campaigns/executor.go index 2e81d3b0d4..57133036a0 100644 --- a/internal/campaigns/executor.go +++ b/internal/campaigns/executor.go @@ -46,6 +46,8 @@ func (t *Task) cacheKey() ExecutionCacheKey { } type TaskStatus struct { + RepoName string + Cached bool LogFile string @@ -95,7 +97,7 @@ func newExecutor(opts ExecutorOpts, client api.Client, update ExecutorUpdateCall func (x *executor) AddTask(repo *graphql.Repository, steps []Step, template *ChangesetTemplate) *TaskStatus { task := &Task{repo, steps, template} - ts := &TaskStatus{EnqueuedAt: time.Now()} + ts := &TaskStatus{RepoName: repo.Name, EnqueuedAt: time.Now()} x.tasks.Store(task, ts) return ts } diff --git a/internal/campaigns/service.go b/internal/campaigns/service.go index 778d66fa2b..723090ad1a 100644 --- a/internal/campaigns/service.go +++ b/internal/campaigns/service.go @@ -195,6 +195,7 @@ func (svc *Service) ExecuteCampaignSpec(ctx context.Context, repos []*graphql.Re x.Start(ctx) specs, err := x.Wait() if progress != nil { + progress(statuses) done <- struct{}{} } if err != nil { diff --git a/internal/output/style.go b/internal/output/style.go index 36470905f0..1c6d29a1b4 100644 --- a/internal/output/style.go +++ b/internal/output/style.go @@ -55,4 +55,7 @@ var ( StyleSearchAlertProposedTitle = &style{""} StyleSearchAlertProposedQuery = Fg256Color(69) StyleSearchAlertProposedDescription = &style{""} + + StyleLinesDeleted = Fg256Color(196) + StyleLinesAdded = Fg256Color(2) )