Skip to content

Commit

Permalink
Support multiple files as inputs (#64)
Browse files Browse the repository at this point in the history
This is a refactor to enable Ratchet to support multiple files in a
single run. All files are parsed once (to limit upstream API calls) and
then updated in serial.

Closes #29
  • Loading branch information
sethvargo authored Feb 3, 2024
1 parent c5be605 commit 135ea47
Show file tree
Hide file tree
Showing 21 changed files with 321 additions and 340 deletions.
11 changes: 0 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,16 +237,5 @@ only applies to the line on which it appears.
- uses: 'actions/checkout@v${{ matrix.version }}'
```

## Experiments

Use these options to configure the default behavior of Ratchet.

### Experiment: Keep Newlines

Experimental functionality to enable keeping newlines in the output. This only
applies to cli commands that modify output. As of v0.5.0, this functionality is
enabled by default. To disable it, set the environment variable
`RATCHET_EXP_KEEP_NEWLINES=false`.

[containers]: https://github.com/sethvargo/ratchet/pkgs/container/ratchet
[releases]: https://github.com/sethvargo/ratchet/releases
27 changes: 10 additions & 17 deletions command/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import (
const checkCommandDesc = `Check if all versions are pinned`

const checkCommandHelp = `
Usage: ratchet check [FILE...]
The "check" command checks if all versions are pinned to an absolute version,
ignoring any versions with the "ratchet:exclude" comment.
Expand Down Expand Up @@ -48,31 +50,22 @@ func (c *CheckCommand) Flags() *flag.FlagSet {
}

func (c *CheckCommand) Run(ctx context.Context, originalArgs []string) error {
f := c.Flags()

if err := f.Parse(originalArgs); err != nil {
return fmt.Errorf("failed to parse flags: %w", err)
}

args := f.Args()
if got := len(args); got != 1 {
return fmt.Errorf("expected exactly one argument, got %d", got)
}

inFile := args[0]
m, err := parseYAMLFile(inFile)
args, err := parseFlags(c.Flags(), originalArgs)
if err != nil {
return fmt.Errorf("failed to parse %s: %w", inFile, err)
return fmt.Errorf("failed to parse flags: %w", err)
}

par, err := parser.For(ctx, c.flagParser)
if err != nil {
return err
}

if err := parser.Check(ctx, par, m); err != nil {
return fmt.Errorf("check failed: %w", err)
fsys := os.DirFS(".")

files, err := loadYAMLFiles(fsys, args)
if err != nil {
return err
}

return nil
return parser.Check(ctx, par, files.nodes())
}
2 changes: 1 addition & 1 deletion command/cmd/gen/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ func realMain() error {

root := folderRoot()
pth := path.Join(root, "command_gen.go")
if err := os.WriteFile(pth, w.Bytes(), 0o644); err != nil {
if err := os.WriteFile(pth, w.Bytes(), 0o600); err != nil {
return fmt.Errorf("failed to write generated file: %w", err)
}

Expand Down
151 changes: 58 additions & 93 deletions command/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@
package command

import (
"bytes"
"context"
"errors"
"flag"
"fmt"
"io"
"io/fs"
"os"
"strconv"
"strings"

// Using banydonk/yaml instead of the default yaml pkg because the default
Expand All @@ -15,8 +17,6 @@ import (
"github.com/hexops/gotextdiff"
"github.com/hexops/gotextdiff/myers"
"github.com/hexops/gotextdiff/span"

"github.com/sethvargo/ratchet/internal/atomic"
"github.com/sethvargo/ratchet/internal/version"
)

Expand Down Expand Up @@ -57,14 +57,26 @@ func Run(ctx context.Context, args []string) error {
return cmd.Run(ctx, args)
}

func keepNewlinesEnv() bool {
value := true
if v, ok := os.LookupEnv("RATCHET_EXP_KEEP_NEWLINES"); ok {
if t, err := strconv.ParseBool(v); err == nil {
value = t
// parseFlags is a helper that parses flags. Unlike [flags.Parse], it handles
// flags that occur after or between positional arguments.
func parseFlags(f *flag.FlagSet, args []string) ([]string, error) {
var finalArgs []string
var merr error

merr = errors.Join(merr, f.Parse(args))

for i := len(args) - len(f.Args()) + 1; i < len(args); {
// Stop parsing if we hit an actual "stop parsing"
if i > 1 && args[i-2] == "--" {
break
}
finalArgs = append(finalArgs, f.Arg(0))
merr = errors.Join(merr, f.Parse(args[i:]))
i += 1 + len(args[i:]) - len(f.Args())
}
return value
finalArgs = append(finalArgs, f.Args()...)

return finalArgs, merr
}

// extractCommandAndArgs is a helper that pulls the subcommand and arguments.
Expand All @@ -79,106 +91,59 @@ func extractCommandAndArgs(args []string) (string, []string) {
}
}

// writeYAML encodes the yaml node into the given writer.
func writeYAML(w io.Writer, m *yaml.Node) error {
enc := yaml.NewEncoder(w)
// marshalYAML encodes the yaml node into the given writer.
func marshalYAML(m *yaml.Node) ([]byte, error) {
var b bytes.Buffer

enc := yaml.NewEncoder(&b)
enc.SetIndent(2)
if err := enc.Encode(m); err != nil {
return fmt.Errorf("failed to encode yaml: %w", err)
return nil, fmt.Errorf("failed to encode yaml: %w", err)
}
if err := enc.Close(); err != nil {
return fmt.Errorf("failed to finalize yaml: %w", err)
return nil, fmt.Errorf("failed to finalize yaml: %w", err)
}
return nil
return b.Bytes(), nil
}

// writeYAMLFile renders the given yaml and atomically writes it to the provided
// filepath.
func writeYAMLFile(src, dst string, m *yaml.Node) (retErr error) {
r, w := io.Pipe()
defer func() {
if err := r.Close(); err != nil && retErr == nil {
retErr = fmt.Errorf("failed to close reader: %w", err)
}
}()
defer func() {
if err := w.Close(); err != nil && retErr == nil {
retErr = fmt.Errorf("failed to closer writer: %w", err)
}
}()

errCh := make(chan error, 1)
go func() {
if err := writeYAML(w, m); err != nil {
select {
case errCh <- fmt.Errorf("failed to render yaml: %w", err):
default:
}
}

if err := w.Close(); err != nil {
select {
case errCh <- fmt.Errorf("failed to close writer: %w", err):
default:
}
}
}()
type loadResult struct {
path string
node *yaml.Node
contents []byte
}

if err := atomic.Write(src, dst, r); err != nil {
retErr = fmt.Errorf("failed to save file %s: %w", dst, err)
return
}
type loadResults []*loadResult

select {
case err := <-errCh:
retErr = err
return
default:
return
func (r loadResults) nodes() []*yaml.Node {
n := make([]*yaml.Node, 0, len(r))
for _, v := range r {
n = append(n, v.node)
}
return n
}

// parseYAML parses the given reader as a yaml node.
func parseYAML(r io.Reader) (*yaml.Node, error) {
var m yaml.Node
if err := yaml.NewDecoder(r).Decode(&m); err != nil {
return nil, fmt.Errorf("failed to decode yaml: %w", err)
}
return &m, nil
}
func loadYAMLFiles(fsys fs.FS, paths []string) (loadResults, error) {
r := make(loadResults, 0, len(paths))

// parseYAMLFile opens the file at the path and parses it as yaml. It closes the
// file handle.
func parseYAMLFile(pth string) (m *yaml.Node, retErr error) {
f, err := os.Open(pth)
if err != nil {
retErr = fmt.Errorf("failed to open file: %w", err)
return
}
defer func() {
if err := f.Close(); err != nil && retErr == nil {
retErr = fmt.Errorf("failed to close file: %w", err)
for _, pth := range paths {
contents, err := fs.ReadFile(fsys, pth)
if err != nil {
return nil, fmt.Errorf("failed to read file %s: %w", pth, err)
}
}()

m, retErr = parseYAML(f)
return
}
var node yaml.Node
if err := yaml.Unmarshal(contents, &node); err != nil {
return nil, fmt.Errorf("failed to parse yaml for %s: %w", pth, err)
}

func parseFile(pth string) (contents string, retErr error) {
f, err := os.Open(pth)
if err != nil {
retErr = fmt.Errorf("failed to open file: %w", err)
return
r = append(r, &loadResult{
path: pth,
node: &node,
contents: contents,
})
}
defer func() {
if err := f.Close(); err != nil && retErr == nil {
retErr = fmt.Errorf("failed to close file: %w", err)
}
}()
c, retErr := io.ReadAll(f)
contents = string(c)
return

return r, nil
}

func removeNewLineChanges(beforeContent, afterContent string) string {
Expand Down
42 changes: 16 additions & 26 deletions command/command_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,10 @@ package command

import (
"bytes"
"fmt"
"path/filepath"
"os"
"reflect"
"runtime"
"testing"

// Using banydonk/yaml instead of the default yaml pkg because the default
// pkg incorrectly escapes unicode. https://github.com/go-yaml/yaml/issues/737
"github.com/braydonk/yaml"
"github.com/google/go-cmp/cmp"
)
Expand Down Expand Up @@ -232,7 +228,7 @@ jobs:
it has many lines
some of them even
have new new lines
have new lines
`
yamlDChanges = `
jobs:
Expand All @@ -253,7 +249,7 @@ jobs:
it has many lines
some of them even
have new new lines
have new lines
`
yamlDChangesFormatted = `
jobs:
Expand All @@ -275,7 +271,7 @@ jobs:
it has many lines
some of them even
have new new lines
have new lines
`
)

Expand Down Expand Up @@ -329,17 +325,17 @@ func Test_removeNewLineChanges(t *testing.T) {
}
}

func Test_parseYAMLFile(t *testing.T) {
func Test_loadYAMLFiles(t *testing.T) {
t.Parallel()

cases := []struct {
name string
yamlFilename string
want string
name string
yamlFilenames []string
want string
}{
{
name: "yamlA_multiple_empty_lines",
yamlFilename: "testdata/github.yml",
name: "yamlA_multiple_empty_lines",
yamlFilenames: []string{"testdata/github.yml"},
want: `jobs:
my_job:
runs-on: 'ubuntu-latest'
Expand Down Expand Up @@ -370,26 +366,20 @@ func Test_parseYAMLFile(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()

node, err := parseYAMLFile(rootPath(tc.yamlFilename))
files, err := loadYAMLFiles(os.DirFS(".."), tc.yamlFilenames)
if err != nil {
t.Errorf("parseYAMLFile() returned error: %v", err)
t.Fatalf("loadYAMLFiles() returned error: %s", err)
}

var buf bytes.Buffer
err = yaml.NewEncoder(&buf).Encode(node)
if err != nil {
t.Errorf("failed to marshal yaml to string: %v", err)
if err := yaml.NewEncoder(&buf).Encode(files.nodes()[0]); err != nil {
t.Errorf("failed to marshal yaml to string: %s", err)
}
got := buf.String()

if diff := cmp.Diff(tc.want, got); diff != "" {
t.Errorf("removeBindingFromPolicy() returned diff (-want +got):\n%s", diff)
t.Errorf("returned diff (-want, +got):\n%s", diff)
}
})
}
}

func rootPath(filename string) (fn string) {
_, fn, _, _ = runtime.Caller(0)
repoRoot := filepath.Dir(filepath.Dir(fn))
return fmt.Sprintf("%s/%s", repoRoot, filename)
}
Loading

0 comments on commit 135ea47

Please sign in to comment.