Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 5 additions & 165 deletions internal/campaigns/run_steps.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import (
"os"
"os/exec"
"strings"
"text/template"
"time"

"github.com/hashicorp/go-multierror"
Expand Down Expand Up @@ -84,17 +83,14 @@ func runSteps(ctx context.Context, rf RepoFetcher, wc WorkspaceCreator, repo *gr
}
defer os.Remove(runScriptFile.Name())

// Parse step.Run as a template...
tmpl, err := parseAsTemplate("step-run", step.Run, &stepContext)
if err != nil {
// Parse step.Run as a template and render it into a buffer and the
// temp file we just created.
var runScript bytes.Buffer
out := io.MultiWriter(&runScript, runScriptFile)
if err := renderTemplate("step-run", step.Run, out, &stepContext); err != nil {
return nil, errors.Wrap(err, "parsing step run")
}

// ... and render it into a buffer and the temp file we just created.
var runScript bytes.Buffer
if err := tmpl.Execute(io.MultiWriter(&runScript, runScriptFile), stepContext); err != nil {
return nil, errors.Wrap(err, "executing template")
}
if err := runScriptFile.Close(); err != nil {
return nil, errors.Wrap(err, "closing temporary file")
}
Expand Down Expand Up @@ -333,159 +329,3 @@ func (e stepFailedErr) SingleLineError() string {

return strings.Split(out, "\n")[0]
}

func parseAsTemplate(name, input string, stepCtx *StepContext) (*template.Template, error) {
return template.New(name).Delims("${{", "}}").Funcs(stepCtx.ToFuncMap()).Parse(input)
}

func renderMap(m map[string]string, stepCtx *StepContext) (map[string]string, error) {
rendered := make(map[string]string, len(m))

for k, v := range rendered {
var out bytes.Buffer

tmpl, err := parseAsTemplate(k, v, stepCtx)
if err != nil {
return rendered, err
}

if err := tmpl.Execute(&out, stepCtx); err != nil {
return rendered, err
}

rendered[k] = out.String()
}

return rendered, nil
}

// StepContext represents the contextual information available when executing a
// step that's defined in a campaign spec.
type StepContext struct {
PreviousStep StepResult
Repository graphql.Repository
}

// ToFuncMap returns a template.FuncMap to access fields on the StepContext in a
// text/template.
func (stepCtx *StepContext) ToFuncMap() template.FuncMap {
return template.FuncMap{
"join": func(list []string, sep string) string {
return strings.Join(list, sep)
},
"split": func(s string, sep string) []string {
return strings.Split(s, sep)
},
"previous_step": func() map[string]interface{} {
result := map[string]interface{}{
"modified_files": stepCtx.PreviousStep.ModifiedFiles(),
"added_files": stepCtx.PreviousStep.AddedFiles(),
"deleted_files": stepCtx.PreviousStep.DeletedFiles(),
"renamed_files": stepCtx.PreviousStep.RenamedFiles(),
}

if stepCtx.PreviousStep.Stdout != nil {
result["stdout"] = stepCtx.PreviousStep.Stdout.String()
} else {
result["stdout"] = ""
}

if stepCtx.PreviousStep.Stderr != nil {
result["stderr"] = stepCtx.PreviousStep.Stderr.String()
} else {
result["stderr"] = ""
}

return result
},
"repository": func() map[string]interface{} {
return map[string]interface{}{
"search_result_paths": stepCtx.Repository.SearchResultPaths(),
"name": stepCtx.Repository.Name,
}
},
}
}

// StepResult represents the result of a previously executed step.
type StepResult struct {
// files are the changes made to files by the step.
files *StepChanges

// Stdout is the output produced by the step on standard out.
Stdout *bytes.Buffer
// Stderr is the output produced by the step on standard error.
Stderr *bytes.Buffer
}

// StepChanges are the changes made to files by a previous step in a repository.
type StepChanges struct {
Modified []string
Added []string
Deleted []string
Renamed []string
}

// ModifiedFiles returns the files modified by a step.
func (r StepResult) ModifiedFiles() []string {
if r.files != nil {
return r.files.Modified
}
return []string{}
}

// AddedFiles returns the files added by a step.
func (r StepResult) AddedFiles() []string {
if r.files != nil {
return r.files.Added
}
return []string{}
}

// DeletedFiles returns the files deleted by a step.
func (r StepResult) DeletedFiles() []string {
if r.files != nil {
return r.files.Deleted
}
return []string{}
}

// RenamedFiles returns the new name of files that have been renamed by a step.
func (r StepResult) RenamedFiles() []string {
if r.files != nil {
return r.files.Renamed
}
return []string{}
}

func parseGitStatus(out []byte) (StepChanges, error) {
result := StepChanges{}

stripped := strings.TrimSpace(string(out))
if len(stripped) == 0 {
return result, nil
}

for _, line := range strings.Split(stripped, "\n") {
if len(line) < 4 {
return result, fmt.Errorf("git status line has unrecognized format: %q", line)
}

file := line[3:]

switch line[0] {
case 'M':
result.Modified = append(result.Modified, file)
case 'A':
result.Added = append(result.Added, file)
case 'D':
result.Deleted = append(result.Deleted, file)
case 'R':
files := strings.Split(file, " -> ")
newFile := files[len(files)-1]
result.Renamed = append(result.Renamed, newFile)
}
}

return result, nil
}
172 changes: 172 additions & 0 deletions internal/campaigns/templating.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
package campaigns

import (
"bytes"
"fmt"
"io"
"strings"
"text/template"

"github.com/pkg/errors"
"github.com/sourcegraph/src-cli/internal/campaigns/graphql"
)

func renderTemplate(name, tmpl string, out io.Writer, stepCtx *StepContext) error {
t, err := parseAsTemplate(name, tmpl, stepCtx)
if err != nil {
return errors.Wrap(err, "parsing step run")
}

return t.Execute(out, stepCtx)
}

func parseAsTemplate(name, input string, stepCtx *StepContext) (*template.Template, error) {
return template.New(name).Delims("${{", "}}").Funcs(stepCtx.ToFuncMap()).Parse(input)
}

func renderMap(m map[string]string, stepCtx *StepContext) (map[string]string, error) {
rendered := make(map[string]string, len(m))

for k, v := range rendered {
var out bytes.Buffer

if err := renderTemplate(k, v, &out, stepCtx); err != nil {
return rendered, err
}

rendered[k] = out.String()
}

return rendered, nil
}

// StepContext represents the contextual information available when executing a
// step that's defined in a campaign spec.
type StepContext struct {
PreviousStep StepResult
Repository graphql.Repository
}

// ToFuncMap returns a template.FuncMap to access fields on the StepContext in a
// text/template.
func (stepCtx *StepContext) ToFuncMap() template.FuncMap {
return template.FuncMap{
"join": func(list []string, sep string) string {
return strings.Join(list, sep)
},
"split": func(s string, sep string) []string {
return strings.Split(s, sep)
},
"previous_step": func() map[string]interface{} {
result := map[string]interface{}{
"modified_files": stepCtx.PreviousStep.ModifiedFiles(),
"added_files": stepCtx.PreviousStep.AddedFiles(),
"deleted_files": stepCtx.PreviousStep.DeletedFiles(),
"renamed_files": stepCtx.PreviousStep.RenamedFiles(),
}

if stepCtx.PreviousStep.Stdout != nil {
result["stdout"] = stepCtx.PreviousStep.Stdout.String()
} else {
result["stdout"] = ""
}

if stepCtx.PreviousStep.Stderr != nil {
result["stderr"] = stepCtx.PreviousStep.Stderr.String()
} else {
result["stderr"] = ""
}

return result
},
"repository": func() map[string]interface{} {
return map[string]interface{}{
"search_result_paths": stepCtx.Repository.SearchResultPaths(),
"name": stepCtx.Repository.Name,
}
},
}
}

// StepResult represents the result of a previously executed step.
type StepResult struct {
// files are the changes made to files by the step.
files *StepChanges

// Stdout is the output produced by the step on standard out.
Stdout *bytes.Buffer
// Stderr is the output produced by the step on standard error.
Stderr *bytes.Buffer
}

// StepChanges are the changes made to files by a previous step in a repository.
type StepChanges struct {
Modified []string
Added []string
Deleted []string
Renamed []string
}

// ModifiedFiles returns the files modified by a step.
func (r StepResult) ModifiedFiles() []string {
if r.files != nil {
return r.files.Modified
}
return []string{}
}

// AddedFiles returns the files added by a step.
func (r StepResult) AddedFiles() []string {
if r.files != nil {
return r.files.Added
}
return []string{}
}

// DeletedFiles returns the files deleted by a step.
func (r StepResult) DeletedFiles() []string {
if r.files != nil {
return r.files.Deleted
}
return []string{}
}

// RenamedFiles returns the new name of files that have been renamed by a step.
func (r StepResult) RenamedFiles() []string {
if r.files != nil {
return r.files.Renamed
}
return []string{}
}

func parseGitStatus(out []byte) (StepChanges, error) {
result := StepChanges{}

stripped := strings.TrimSpace(string(out))
if len(stripped) == 0 {
return result, nil
}

for _, line := range strings.Split(stripped, "\n") {
if len(line) < 4 {
return result, fmt.Errorf("git status line has unrecognized format: %q", line)
}

file := line[3:]

switch line[0] {
case 'M':
result.Modified = append(result.Modified, file)
case 'A':
result.Added = append(result.Added, file)
case 'D':
result.Deleted = append(result.Deleted, file)
case 'R':
files := strings.Split(file, " -> ")
newFile := files[len(files)-1]
result.Renamed = append(result.Renamed, newFile)
}
}

return result, nil
}