diff --git a/command/check.go b/command/check.go index e41f1aa81b..40753f2029 100644 --- a/command/check.go +++ b/command/check.go @@ -62,7 +62,7 @@ func (c *CheckCommand) Run(ctx context.Context, originalArgs []string) error { fsys := os.DirFS(".") - files, err := loadYAMLFiles(fsys, args) + files, err := loadYAMLFiles(fsys, args, false) if err != nil { return err } diff --git a/command/command.go b/command/command.go index ab13ddd025..5acfc5a170 100644 --- a/command/command.go +++ b/command/command.go @@ -123,7 +123,7 @@ func (r loadResults) nodes() []*yaml.Node { return n } -func loadYAMLFiles(fsys fs.FS, paths []string) (loadResults, error) { +func loadYAMLFiles(fsys fs.FS, paths []string, format bool) (loadResults, error) { r := make(loadResults, 0, len(paths)) for _, pth := range paths { @@ -137,17 +137,59 @@ func loadYAMLFiles(fsys fs.FS, paths []string) (loadResults, error) { if err := yaml.Unmarshal(contents, &node); err != nil { return nil, fmt.Errorf("failed to parse yaml for %s: %w", pth, err) } - - r = append(r, &loadResult{ + lr := &loadResult{ path: pth, node: &node, contents: contents, - }) + } + + if format { + if err := FixIndentation(lr); err != nil { + return nil, fmt.Errorf("failed to format indentation: %w", err) + } + } + + r = append(r, lr) } return r, nil } +// FixIndentation corrects the indentation for the given loadResult and edits it in-place. +func FixIndentation(f *loadResult) error { + updated, err := marshalYAML(f.node) + if err != nil { + return fmt.Errorf("failed to marshal yaml for %s: %w", f.path, err) + } + lines := strings.Split(string(f.contents), "\n") + afterLines := strings.Split(string(updated), "\n") + + editedLines := []string{} + afterIndex := 0 + // Loop through both lists line by line using a two-pointer technique. + for _, l := range lines { + token := strings.TrimSpace(l) + if token == "" { + editedLines = append(editedLines, l) + continue + } + currentAfterLine := afterLines[afterIndex] + indexInAfterLine := strings.Index(currentAfterLine, token) + for indexInAfterLine == -1 { + afterIndex++ + currentAfterLine = afterLines[afterIndex] + indexInAfterLine = strings.Index(currentAfterLine, token) + } + + lineWithCorrectIndent := currentAfterLine[:indexInAfterLine] + token + editedLines = append(editedLines, lineWithCorrectIndent) + afterIndex++ + } + + f.contents = []byte(strings.Join(editedLines, "\n")) + return nil +} + func removeNewLineChanges(beforeContent, afterContent string) string { lines := strings.Split(beforeContent, "\n") edits := myers.ComputeEdits(span.URIFromPath("before.txt"), beforeContent, afterContent) diff --git a/command/command_test.go b/command/command_test.go index bacc4bb26a..badda21c17 100644 --- a/command/command_test.go +++ b/command/command_test.go @@ -1,12 +1,10 @@ package command import ( - "bytes" "os" "reflect" "testing" - "github.com/braydonk/yaml" "github.com/google/go-cmp/cmp" ) @@ -331,56 +329,123 @@ func Test_loadYAMLFiles(t *testing.T) { cases := []struct { name string yamlFilenames []string + format bool want string }{ { name: "yamlA_multiple_empty_lines", yamlFilenames: []string{"testdata/github.yml"}, + format: false, want: `jobs: - my_job: - runs-on: 'ubuntu-latest' - container: - image: 'ubuntu:20.04' - services: - nginx: - image: 'nginx:1.21' - steps: - - uses: 'actions/checkout@v3' - - uses: 'docker://ubuntu:20.04' - with: - uses: '/path/to/user.png' - image: '/path/to/image.jpg' - - runs: |- - echo "Hello 😀" - other_job: - uses: 'my-org/my-repo/.github/workflows/my-workflow.yml@v0' - final_job: - uses: './local/path/to/action' + my_job: + runs-on: 'ubuntu-latest' + container: + image: 'ubuntu:20.04' + services: + nginx: + image: 'nginx:1.21' + steps: + - uses: 'actions/checkout@v3' + - uses: 'docker://ubuntu:20.04' + with: + uses: '/path/to/user.png' + image: '/path/to/image.jpg' + - runs: |- + echo "Hello 😀" + if [ "true" == "false" ]; + echo "NOPE" + fi + other_job: + uses: 'my-org/my-repo/.github/workflows/my-workflow.yml@v0' + final_job: + uses: './local/path/to/action' `, }, { name: "handles-leading-dot-slash", yamlFilenames: []string{"./testdata/github.yml"}, + format: false, + want: `jobs: + my_job: + runs-on: 'ubuntu-latest' + container: + image: 'ubuntu:20.04' + services: + nginx: + image: 'nginx:1.21' + steps: + - uses: 'actions/checkout@v3' + - uses: 'docker://ubuntu:20.04' + with: + uses: '/path/to/user.png' + image: '/path/to/image.jpg' + - runs: |- + echo "Hello 😀" + if [ "true" == "false" ]; + echo "NOPE" + fi + other_job: + uses: 'my-org/my-repo/.github/workflows/my-workflow.yml@v0' + final_job: + uses: './local/path/to/action' +`, + }, + { + name: "yaml_steps_indent_change", + yamlFilenames: []string{"testdata/github.yml"}, + format: true, want: `jobs: - my_job: - runs-on: 'ubuntu-latest' - container: - image: 'ubuntu:20.04' - services: - nginx: - image: 'nginx:1.21' - steps: - - uses: 'actions/checkout@v3' - - uses: 'docker://ubuntu:20.04' - with: - uses: '/path/to/user.png' - image: '/path/to/image.jpg' - - runs: |- - echo "Hello 😀" - other_job: - uses: 'my-org/my-repo/.github/workflows/my-workflow.yml@v0' - final_job: - uses: './local/path/to/action' + my_job: + runs-on: 'ubuntu-latest' + container: + image: 'ubuntu:20.04' + services: + nginx: + image: 'nginx:1.21' + steps: + - uses: 'actions/checkout@v3' + - uses: 'docker://ubuntu:20.04' + with: + uses: '/path/to/user.png' + image: '/path/to/image.jpg' + - runs: |- + echo "Hello 😀" + if [ "true" == "false" ]; + echo "NOPE" + fi + other_job: + uses: 'my-org/my-repo/.github/workflows/my-workflow.yml@v0' + final_job: + uses: './local/path/to/action' +`, + }, + { + name: "yaml_all_indent_change", + yamlFilenames: []string{"testdata/github-crazy-indent.yml"}, + format: true, + want: `jobs: + my_job: + runs-on: 'ubuntu-latest' + container: + image: 'ubuntu:20.04' + services: + nginx: + image: 'nginx:1.21' + steps: + - uses: 'actions/checkout@v3' + - uses: 'docker://ubuntu:20.04' + with: + uses: '/path/to/user.png' + image: '/path/to/image.jpg' + - runs: |- + echo "Hello 😀" + if [ "true" == "false" ]; + echo "NOPE" + fi + other_job: + uses: 'my-org/my-repo/.github/workflows/my-workflow.yml@v0' + final_job: + uses: './local/path/to/action' `, }, } @@ -391,16 +456,16 @@ func Test_loadYAMLFiles(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - files, err := loadYAMLFiles(os.DirFS(".."), tc.yamlFilenames) + files, err := loadYAMLFiles(os.DirFS(".."), tc.yamlFilenames, tc.format) if err != nil { t.Fatalf("loadYAMLFiles() returned error: %s", err) } - var buf bytes.Buffer - if err := yaml.NewEncoder(&buf).Encode(files.nodes()[0]); err != nil { - t.Errorf("failed to marshal yaml to string: %s", err) + b, err := marshalYAML(files[0].node) + if err != nil { + t.Fatalf("marshalYAML() returned error: %s", err) } - got := buf.String() + got := string(b) if diff := cmp.Diff(tc.want, got); diff != "" { t.Errorf("returned diff (-want, +got):\n%s", diff) diff --git a/command/pin.go b/command/pin.go index cb7e7980b8..de9b97cc84 100644 --- a/command/pin.go +++ b/command/pin.go @@ -80,7 +80,7 @@ func (c *PinCommand) Run(ctx context.Context, originalArgs []string) error { fsys := os.DirFS(".") - files, err := loadYAMLFiles(fsys, args) + files, err := loadYAMLFiles(fsys, args, true) if err != nil { return err } diff --git a/command/unpin.go b/command/unpin.go index c09e3fb427..b007dfd3ee 100644 --- a/command/unpin.go +++ b/command/unpin.go @@ -64,7 +64,7 @@ func (c *UnpinCommand) Run(ctx context.Context, originalArgs []string) error { fsys := os.DirFS(".") - files, err := loadYAMLFiles(fsys, args) + files, err := loadYAMLFiles(fsys, args, true) if err != nil { return err } diff --git a/command/update.go b/command/update.go index f83d19497c..e9f3a9060d 100644 --- a/command/update.go +++ b/command/update.go @@ -69,7 +69,7 @@ func (c *UpdateCommand) Run(ctx context.Context, originalArgs []string) error { fsys := os.DirFS(".") - files, err := loadYAMLFiles(fsys, args) + files, err := loadYAMLFiles(fsys, args, true) if err != nil { return err } diff --git a/command/upgrade.go b/command/upgrade.go index 3e25439f87..edf308d6a0 100644 --- a/command/upgrade.go +++ b/command/upgrade.go @@ -77,7 +77,7 @@ func (c *UpgradeCommand) Run(ctx context.Context, originalArgs []string) error { fsys := os.DirFS(".") - files, err := loadYAMLFiles(fsys, args) + files, err := loadYAMLFiles(fsys, args, true) if err != nil { return err } diff --git a/testdata/github-crazy-indent.yml b/testdata/github-crazy-indent.yml new file mode 100644 index 0000000000..b36859f86f --- /dev/null +++ b/testdata/github-crazy-indent.yml @@ -0,0 +1,30 @@ +jobs: + my_job: + runs-on: 'ubuntu-latest' + + container: + image: 'ubuntu:20.04' + + services: + nginx: + image: 'nginx:1.21' + + steps: + - uses: 'actions/checkout@v3' + + - uses: 'docker://ubuntu:20.04' + with: + uses: '/path/to/user.png' + image: '/path/to/image.jpg' + + - runs: |- + echo "Hello 😀" + if [ "true" == "false" ]; + echo "NOPE" + fi + + other_job: + uses: 'my-org/my-repo/.github/workflows/my-workflow.yml@v0' + + final_job: + uses: './local/path/to/action' diff --git a/testdata/github.yml b/testdata/github.yml index b319a31718..2819213e5f 100644 --- a/testdata/github.yml +++ b/testdata/github.yml @@ -19,6 +19,9 @@ jobs: - runs: |- echo "Hello 😀" + if [ "true" == "false" ]; + echo "NOPE" + fi other_job: uses: 'my-org/my-repo/.github/workflows/my-workflow.yml@v0'