Skip to content

Commit 6cccdc7

Browse files
mrnuggetLawnGnome
andauthored
Introduce transformChanges to campaign spec (RFC 265) (#398)
* Add transformChanges to campaign spec * Update Executor integration test * Add a failing test for code transformations * Change execution cache to only cache diffs * Rename from .patch to .diff * Extract groupFileDiffs function * Add test for GroupFileDiffs * Add some comments * Fix campaign progress printer for multiple changeset specs * Display how many changeset specs were produced in one repo * Add more tests for grouping changes * Fix problems after rebase * Switch from branchSuffix to branch * Add a repository filter to the transformChanges.Group * Check whether transformChanges is supported * Validate that multiple changesets don't have same branch * Add minLength to campaign spec * Pluralize message correctly * Update internal/campaigns/executor.go Co-authored-by: Adam Harvey <[email protected]> * Update feature date * Add a changelog entry Co-authored-by: Adam Harvey <[email protected]>
1 parent bce34e8 commit 6cccdc7

12 files changed

+631
-152
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ All notable changes to `src-cli` are documented in this file.
1313

1414
### Added
1515

16+
- Experimental: [`transformChanges` in campaign specs](https://docs.sourcegraph.com/campaigns/references/campaign_spec_yaml_reference#transformchanges) is now available as a feature preview to allow users to create multiple changesets in a single repository. [#398](https://github.com/sourcegraph/src-cli/pull/398)
17+
1618
### Changed
1719

1820
- `src campaign [apply|preview]` now show the current execution progress in numbers next to the progress bar. [#396](https://github.com/sourcegraph/src-cli/pull/396)

cmd/src/campaign_progress_printer.go

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -177,21 +177,16 @@ func (p *campaignProgressPrinter) PrintStatuses(statuses []*campaigns.TaskStatus
177177
}
178178

179179
for _, ts := range newlyCompleted {
180-
var fileDiffs []*diff.FileDiff
181-
182-
if ts.ChangesetSpec != nil {
183-
var err error
184-
fileDiffs, err = diff.ParseMultiFileDiff([]byte(ts.ChangesetSpec.Commits[0].Diff))
185-
if err != nil {
186-
p.progress.Verbosef("%-*s failed to display status: %s", p.maxRepoName, ts.RepoName, err)
187-
continue
188-
}
180+
fileDiffs, hasDiffs, err := ts.FileDiffs()
181+
if err != nil {
182+
p.progress.Verbosef("%-*s failed to display status: %s", p.maxRepoName, ts.RepoName, err)
183+
continue
189184
}
190185

191186
if p.verbose {
192187
p.progress.WriteLine(output.Linef("", output.StylePending, "%s", ts.RepoName))
193188

194-
if ts.ChangesetSpec == nil {
189+
if !hasDiffs {
195190
p.progress.Verbosef(" No changes")
196191
} else {
197192
lines, err := verboseDiffSummary(fileDiffs)
@@ -205,6 +200,9 @@ func (p *campaignProgressPrinter) PrintStatuses(statuses []*campaigns.TaskStatus
205200
}
206201
}
207202

203+
if len(ts.ChangesetSpecs) > 1 {
204+
p.progress.Verbosef(" %d changeset specs generated", len(ts.ChangesetSpecs))
205+
}
208206
p.progress.Verbosef(" Execution took %s", ts.ExecutionTime())
209207
p.progress.Verbose("")
210208
}
@@ -258,7 +256,14 @@ func taskStatusBarText(ts *campaigns.TaskStatus) (string, error) {
258256
var statusText string
259257

260258
if ts.IsCompleted() {
261-
if ts.ChangesetSpec == nil {
259+
diffs, hasDiffs, err := ts.FileDiffs()
260+
if err != nil {
261+
return "", err
262+
}
263+
264+
if hasDiffs {
265+
statusText = diffStatDescription(diffs) + " " + diffStatDiagram(sumDiffStats(diffs))
266+
} else {
262267
if ts.Err != nil {
263268
if texter, ok := ts.Err.(statusTexter); ok {
264269
statusText = texter.StatusText()
@@ -268,13 +273,6 @@ func taskStatusBarText(ts *campaigns.TaskStatus) (string, error) {
268273
} else {
269274
statusText = "No changes"
270275
}
271-
} else {
272-
fileDiffs, err := diff.ParseMultiFileDiff([]byte(ts.ChangesetSpec.Commits[0].Diff))
273-
if err != nil {
274-
return "", err
275-
}
276-
277-
statusText = diffStatDescription(fileDiffs) + " " + diffStatDiagram(sumDiffStats(fileDiffs))
278276
}
279277

280278
if ts.Cached {

cmd/src/campaign_progress_printer_test.go

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -95,22 +95,24 @@ func TestCampaignProgressPrinterIntegration(t *testing.T) {
9595
FinishedAt: now.Add(time.Duration(5) * time.Second),
9696
CurrentlyExecuting: "",
9797
Err: nil,
98-
ChangesetSpec: &campaigns.ChangesetSpec{
99-
BaseRepository: "graphql-id",
100-
CreatedChangeset: &campaigns.CreatedChangeset{
101-
BaseRef: "refs/heads/main",
102-
BaseRev: "d34db33f",
103-
HeadRepository: "graphql-id",
104-
HeadRef: "refs/heads/my-campaign",
105-
Title: "This is my campaign",
106-
Body: "This is my campaign",
107-
Commits: []campaigns.GitCommitDescription{
108-
{
109-
Message: "This is my campaign",
110-
Diff: progressPrinterDiff,
98+
ChangesetSpecs: []*campaigns.ChangesetSpec{
99+
{
100+
BaseRepository: "graphql-id",
101+
CreatedChangeset: &campaigns.CreatedChangeset{
102+
BaseRef: "refs/heads/main",
103+
BaseRev: "d34db33f",
104+
HeadRepository: "graphql-id",
105+
HeadRef: "refs/heads/my-campaign",
106+
Title: "This is my campaign",
107+
Body: "This is my campaign",
108+
Commits: []campaigns.GitCommitDescription{
109+
{
110+
Message: "This is my campaign",
111+
Diff: progressPrinterDiff,
112+
},
111113
},
114+
Published: false,
112115
},
113-
Published: false,
114116
},
115117
},
116118
}

cmd/src/campaigns_common.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -264,9 +264,17 @@ func campaignsExecute(ctx context.Context, out *output.Output, svc *campaigns.Se
264264
ids := make([]campaigns.ChangesetSpecID, len(specs))
265265

266266
if len(specs) > 0 {
267+
var label string
268+
if len(specs) == 1 {
269+
label = "Sending changeset spec"
270+
} else {
271+
label = fmt.Sprintf("Sending %d changeset specs", len(specs))
272+
}
273+
267274
progress := out.Progress([]output.ProgressBar{
268-
{Label: "Sending changeset specs", Max: float64(len(specs))},
275+
{Label: label, Max: float64(len(specs))},
269276
}, nil)
277+
270278
for i, spec := range specs {
271279
id, err := svc.CreateChangesetSpec(ctx, spec)
272280
if err != nil {
@@ -276,7 +284,6 @@ func campaignsExecute(ctx context.Context, out *output.Output, svc *campaigns.Se
276284
progress.SetValue(0, float64(i+1))
277285
}
278286
progress.Complete()
279-
280287
} else {
281288
if len(repos) == 0 {
282289
out.WriteLine(output.Linef(output.EmojiWarning, output.StyleWarning, `No changeset specs created`))

internal/campaigns/campaign_spec.go

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ type CampaignSpec struct {
3232
Description string `json:"description,omitempty" yaml:"description"`
3333
On []OnQueryOrRepository `json:"on,omitempty" yaml:"on"`
3434
Steps []Step `json:"steps,omitempty" yaml:"steps"`
35+
TransformChanges *TransformChanges `json:"transformChanges,omitempty" yaml:"transformChanges,omitempty"`
3536
ImportChangesets []ImportChangeset `json:"importChangesets,omitempty" yaml:"importChangesets"`
3637
ChangesetTemplate *ChangesetTemplate `json:"changesetTemplate,omitempty" yaml:"changesetTemplate"`
3738
}
@@ -74,26 +75,37 @@ type Step struct {
7475
image string
7576
}
7677

78+
type TransformChanges struct {
79+
Group []Group `json:"group,omitempty" yaml:"group"`
80+
}
81+
82+
type Group struct {
83+
Directory string `json:"directory,omitempty" yaml:"directory"`
84+
Branch string `json:"branch,omitempty" yaml:"branch"`
85+
Repository string `json:"repository,omitempty" yaml:"repository"`
86+
}
87+
7788
func ParseCampaignSpec(data []byte, features featureFlags) (*CampaignSpec, error) {
7889
var spec CampaignSpec
7990
if err := yaml.UnmarshalValidate(schema.CampaignSpecJSON, data, &spec); err != nil {
8091
return nil, err
8192
}
8293

94+
var errs *multierror.Error
95+
8396
if !features.allowArrayEnvironments {
84-
var errs *multierror.Error
8597
for i, step := range spec.Steps {
8698
if !step.Env.IsStatic() {
8799
errs = multierror.Append(errs, errors.Errorf("step %d includes one or more dynamic environment variables, which are unsupported in this Sourcegraph version", i+1))
88100
}
89101
}
102+
}
90103

91-
if err := errs.ErrorOrNil(); err != nil {
92-
return nil, err
93-
}
104+
if spec.TransformChanges != nil && !features.allowtransformChanges {
105+
errs = multierror.Append(errs, errors.New("campaign spec includes transformChanges, which is not supported in this Sourcegraph version"))
94106
}
95107

96-
return &spec, nil
108+
return &spec, errs.ErrorOrNil()
97109
}
98110

99111
func (on *OnQueryOrRepository) String() string {

internal/campaigns/execution_cache.go

Lines changed: 32 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@ import (
55
"crypto/sha256"
66
"encoding/base64"
77
"encoding/json"
8+
"fmt"
89
"io/ioutil"
910
"os"
1011
"path/filepath"
12+
"strings"
1113

1214
"github.com/pkg/errors"
1315
)
@@ -60,8 +62,8 @@ func (key ExecutionCacheKey) Key() (string, error) {
6062
}
6163

6264
type ExecutionCache interface {
63-
Get(ctx context.Context, key ExecutionCacheKey) (result *ChangesetSpec, err error)
64-
Set(ctx context.Context, key ExecutionCacheKey, result *ChangesetSpec) error
65+
Get(ctx context.Context, key ExecutionCacheKey) (diff string, found bool, err error)
66+
Set(ctx context.Context, key ExecutionCacheKey, diff string) error
6567
Clear(ctx context.Context, key ExecutionCacheKey) error
6668
}
6769

@@ -75,41 +77,48 @@ func (c ExecutionDiskCache) cacheFilePath(key ExecutionCacheKey) (string, error)
7577
return "", errors.Wrap(err, "calculating execution cache key")
7678
}
7779

78-
return filepath.Join(c.Dir, keyString+".json"), nil
80+
return filepath.Join(c.Dir, keyString+".diff"), nil
7981
}
8082

81-
func (c ExecutionDiskCache) Get(ctx context.Context, key ExecutionCacheKey) (*ChangesetSpec, error) {
83+
func (c ExecutionDiskCache) Get(ctx context.Context, key ExecutionCacheKey) (string, bool, error) {
8284
path, err := c.cacheFilePath(key)
8385
if err != nil {
84-
return nil, err
86+
return "", false, err
8587
}
8688

8789
data, err := ioutil.ReadFile(path)
8890
if err != nil {
8991
if os.IsNotExist(err) {
9092
err = nil // treat as not-found
9193
}
92-
return nil, err
94+
return "", false, err
9395
}
9496

95-
var result ChangesetSpec
96-
if err := json.Unmarshal(data, &result); err != nil {
97-
// Delete the invalid data to avoid causing an error for next time.
98-
if err := os.Remove(path); err != nil {
99-
return nil, errors.Wrap(err, "while deleting cache file with invalid JSON")
97+
// We previously cached complete ChangesetSpecs instead of just the diffs.
98+
// To be backwards compatible, we keep reading these:
99+
if strings.HasSuffix(path, ".json") {
100+
var result ChangesetSpec
101+
if err := json.Unmarshal(data, &result); err != nil {
102+
// Delete the invalid data to avoid causing an error for next time.
103+
if err := os.Remove(path); err != nil {
104+
return "", false, errors.Wrap(err, "while deleting cache file with invalid JSON")
105+
}
106+
return "", false, errors.Wrapf(err, "reading cache file %s", path)
100107
}
101-
return nil, errors.Wrapf(err, "reading cache file %s", path)
108+
if len(result.Commits) != 1 {
109+
return "", false, errors.New("cached result has no commits")
110+
}
111+
return result.Commits[0].Diff, true, nil
102112
}
103113

104-
return &result, nil
105-
}
106-
107-
func (c ExecutionDiskCache) Set(ctx context.Context, key ExecutionCacheKey, result *ChangesetSpec) error {
108-
data, err := json.Marshal(result)
109-
if err != nil {
110-
return err
114+
if strings.HasSuffix(path, ".diff") {
115+
return string(data), true, nil
111116
}
112117

118+
return "", false, fmt.Errorf("unknown file format for cache file %q", path)
119+
}
120+
121+
func (c ExecutionDiskCache) Set(ctx context.Context, key ExecutionCacheKey, diff string) error {
113122
path, err := c.cacheFilePath(key)
114123
if err != nil {
115124
return err
@@ -119,7 +128,7 @@ func (c ExecutionDiskCache) Set(ctx context.Context, key ExecutionCacheKey, resu
119128
return err
120129
}
121130

122-
return ioutil.WriteFile(path, data, 0600)
131+
return ioutil.WriteFile(path, []byte(diff), 0600)
123132
}
124133

125134
func (c ExecutionDiskCache) Clear(ctx context.Context, key ExecutionCacheKey) error {
@@ -139,11 +148,11 @@ func (c ExecutionDiskCache) Clear(ctx context.Context, key ExecutionCacheKey) er
139148
// retrieve cache entries.
140149
type ExecutionNoOpCache struct{}
141150

142-
func (ExecutionNoOpCache) Get(ctx context.Context, key ExecutionCacheKey) (result *ChangesetSpec, err error) {
143-
return nil, nil
151+
func (ExecutionNoOpCache) Get(ctx context.Context, key ExecutionCacheKey) (diff string, found bool, err error) {
152+
return "", false, nil
144153
}
145154

146-
func (ExecutionNoOpCache) Set(ctx context.Context, key ExecutionCacheKey, result *ChangesetSpec) error {
155+
func (ExecutionNoOpCache) Set(ctx context.Context, key ExecutionCacheKey, diff string) error {
147156
return nil
148157
}
149158

0 commit comments

Comments
 (0)