Skip to content

Commit

Permalink
feat: -d flag (#65)
Browse files Browse the repository at this point in the history
Adds a new flag -d that, with -o, reports a diff between
the output file and what would be generated by the tool.
It does not change anything on-disk.

This can be used in CI checks and the like
to ensure that the file is up-to-date.
  • Loading branch information
abhinav authored Mar 1, 2023
1 parent 62f87d1 commit 585050f
Show file tree
Hide file tree
Showing 13 changed files with 338 additions and 20 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased
### Added
- `-d` flag to print a diff of the output
instead of writing to the output file.

## v0.2.0 - 2023-02-26
### Added
- `-offset N` flag to offset all headings by a fixed amount
Expand Down
5 changes: 2 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,8 @@ fmtcheck:
fi

.PHONY: readmecheck
readmecheck:
make readme
@DIFF=$$(git diff README.md); \
readmecheck: $(STITCHMD)
@DIFF=$$($(STITCHMD) -d -o README.md doc/README.md); \
if [[ -n "$$DIFF" ]]; then \
echo "README.md is out of date:"; \
echo "$$DIFF"; \
Expand Down
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,7 @@ stitchmd supports the following options:
- [`-no-toc`](#disable-the-toc)
- [`-o FILE`](#write-to-file)
- [`-C DIR`](#change-the-directory)
- [`-d`](#report-a-diff)
#### Read from stdin
Expand Down Expand Up @@ -555,6 +556,25 @@ This is especially useful if your summary file is
... | stitchmd -C docs -
```
#### Report a diff
```
-d
```
stitchmd normally writes output directly to the file
if you pass in a filename with [`-o`](#write-to-file).
Use the `-d` flag to instead have it report what would change
in the output file without actually changing it.
```bash
stitchmd -d -o README.md # ...
```
This can be useful for lint checks and similar,
or to do a dry run and find out what would change
without changing it.
### Syntax
Although the summary file is Markdown,
Expand Down
20 changes: 20 additions & 0 deletions doc/options.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ stitchmd supports the following options:
- [`-no-toc`](#disable-the-toc)
- [`-o FILE`](#write-to-file)
- [`-C DIR`](#change-the-directory)
- [`-d`](#report-a-diff)

## Read from stdin

Expand Down Expand Up @@ -181,3 +182,22 @@ This is especially useful if your summary file is
```bash
... | stitchmd -C docs -
```

## Report a diff

```
-d
```

stitchmd normally writes output directly to the file
if you pass in a filename with [`-o`](#write-to-file).
Use the `-d` flag to instead have it report what would change
in the output file without actually changing it.

```bash
stitchmd -d -o README.md # ...
```

This can be useful for lint checks and similar,
or to do a dry run and find out what would change
without changing it.
10 changes: 10 additions & 0 deletions flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ type params struct {
Dir string
Offset int
NoTOC bool

Diff bool
}

// cliParser parses command line arguments.
Expand All @@ -45,6 +47,7 @@ func (p *cliParser) newFlagSet() (*params, *flag.FlagSet) {
flag.StringVar(&opts.Dir, "C", "", "")
flag.IntVar(&opts.Offset, "offset", 0, "")
flag.BoolVar(&opts.NoTOC, "no-toc", false, "")
flag.BoolVar(&opts.Diff, "d", false, "")

flag.BoolVar(&p.version, "version", false, "")
flag.BoolVar(&p.help, "help", false, "")
Expand Down Expand Up @@ -101,6 +104,13 @@ func (p *cliParser) Parse(args []string) (*params, cliParseResult) {
opts.Output = ""
}

// Reject -d if -o is not set.
if opts.Diff && opts.Output == "" {
fmt.Fprintln(p.Stderr, "cannot use -d without -o")
fset.Usage()
return nil, cliParseError
}

return opts, cliParseSuccess
}

Expand Down
15 changes: 15 additions & 0 deletions flags_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,21 @@ func TestCLIParser_Parse(t *testing.T) {
args: []string{"-no-toc=false", "bar"},
want: params{NoTOC: false, Input: "bar"},
},
{
desc: "diff",
args: []string{"-d", "-o", "foo", "bar"},
want: params{
Diff: true,
Output: "foo",
Input: "bar",
},
},
{
desc: "diff/missing o",
args: []string{"-d", "bar"},
wantRes: cliParseError,
wantErr: "cannot use -d without -o",
},
{
desc: "too many args",
args: []string{"-o", "foo", "bar", "baz"},
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.20

require (
github.com/Kunde21/markdownfmt/v3 v3.1.0
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e
github.com/stretchr/testify v1.8.2
github.com/yuin/goldmark v1.5.4
github.com/yuin/goldmark-meta v1.1.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e h1:aoZm08cpOy4WuID//EZDgcC4zIxODThtZNPirFr42+A=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
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/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
Expand Down
93 changes: 86 additions & 7 deletions integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,27 +19,27 @@ func TestIntegration_e2e(t *testing.T) {
type testCase struct {
Name string `yaml:"name"`
Give string `yaml:"give"`
Files map[string]string `yaml:"files"`
Files map[string]string `yaml:"files,omitempty"`
Want string `yaml:"want"`

Offset int `yaml:"offset"` // -offset
NoTOC bool `yaml:"no-toc"` // -no-toc

// Path to the output directory,
// relative to the test directory.
OutDir string `yaml:"outDir"`
OutDir string `yaml:"outDir,omitempty"`
}

groups := decodeTestGroups[testCase](t, "testdata/e2e/*.yaml")
var allTests []testCase
var tests []testCase
for _, group := range groups {
for _, tt := range group.Tests {
tt.Name = fmt.Sprintf("%s/%s", group.Name, tt.Name)
allTests = append(allTests, tt)
tests = append(tests, tt)
}
}

for _, tt := range allTests {
for _, tt := range tests {
tt := tt
t.Run(tt.Name, func(t *testing.T) {
t.Parallel()
Expand Down Expand Up @@ -95,9 +95,87 @@ func TestIntegration_e2e(t *testing.T) {
}
}

func TestIntegration_diff(t *testing.T) {
t.Parallel()

type testCase struct {
Name string `yaml:"name"`
Give string `yaml:"give"`
Files map[string]string `yaml:"files,omitempty"`
Old *string `yaml:"old,omitempty"`
Diff string `yaml:"diff,omitempty"`
}

groups := decodeTestGroups[testCase](t, "testdata/diff.yaml")
var tests []testCase
for _, group := range groups {
tests = append(tests, group.Tests...)
}

for _, tt := range tests {
tt := tt
t.Run(tt.Name, func(t *testing.T) {
t.Parallel()

dir := t.TempDir()

input := filepath.Join(dir, "summary.md")
require.NoError(t, os.WriteFile(input, []byte(tt.Give), 0o644))

output := filepath.Join(dir, "output.md")
if tt.Old != nil {
require.NoError(t,
os.WriteFile(output, []byte(*tt.Old), 0o644))
}

for filename, content := range tt.Files {
path := filepath.Join(dir, filename)
require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o755))
require.NoError(t, os.WriteFile(path, []byte(content), 0o644))
}

var stdout, stderr bytes.Buffer
defer func() {
if t.Failed() {
t.Logf("stderr:\n%s", stderr.String())
}
}()

cmd := mainCmd{
Stdin: new(bytes.Buffer),
Stdout: &stdout,
Stderr: &stderr,
Getwd: func() (string, error) {
return dir, nil
},
}

require.NoError(t, cmd.run(&params{
Input: input,
Output: output,
Diff: true,
}))

// Drop the file names from the diff.
diffLines := strings.Split(stdout.String(), "\n")
if len(diffLines) > 0 && strings.HasPrefix(diffLines[0], "--- ") {
diffLines = diffLines[1:]
}
if len(diffLines) > 0 && strings.HasPrefix(diffLines[0], "+++ ") {
diffLines = diffLines[1:]
}

got := strings.Join(diffLines, "\n")
assert.Equal(t, tt.Diff, got)
})
}
}

type testGroup[T any] struct {
Name string
Tests []T

filename string
}

func decodeTestGroups[T any](t testing.TB, glob string) []testGroup[T] {
Expand All @@ -117,8 +195,9 @@ func decodeTestGroups[T any](t testing.TB, glob string) []testGroup[T] {
var tests []T
require.NoError(t, yaml.Unmarshal(testdata, &tests))
groups = append(groups, testGroup[T]{
Name: groupname,
Tests: tests,
Name: groupname,
Tests: tests,
filename: testfile,
})
}

Expand Down
74 changes: 64 additions & 10 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
package main

import (
"bytes"
"errors"
"fmt"
"io"
Expand All @@ -14,6 +15,7 @@ import (
"path/filepath"

mdfmt "github.com/Kunde21/markdownfmt/v3/markdown"
"github.com/pkg/diff"
"go.abhg.dev/stitchmd/internal/goldast"
"go.abhg.dev/stitchmd/internal/stitch"
)
Expand Down Expand Up @@ -99,17 +101,26 @@ func (cmd *mainCmd) run(opts *params) error {

output := cmd.Stdout
if len(opts.Output) > 0 {
outDir := filepath.Dir(opts.Output)
if err := os.MkdirAll(outDir, 0o755); err != nil {
return fmt.Errorf("create output directory: %w", err)
if opts.Diff {
dw, err := newDiffWriter(opts.Output)
if err != nil {
return fmt.Errorf("read output: %w", err)
}
defer dw.Diff(cmd.Stdout)
output = dw
} else {
outDir := filepath.Dir(opts.Output)
if err := os.MkdirAll(outDir, 0o755); err != nil {
return fmt.Errorf("create output directory: %w", err)
}

f, err := os.Create(opts.Output)
if err != nil {
return fmt.Errorf("create output: %w", err)
}
defer f.Close()
output = f
}

f, err := os.Create(opts.Output)
if err != nil {
return fmt.Errorf("create output: %w", err)
}
defer f.Close()
output = f
}

// Relative path from the output directory back to the input directory.
Expand Down Expand Up @@ -174,3 +185,46 @@ func (cmd *mainCmd) run(opts *params) error {
}
return g.Generate(f.Source, coll)
}

// diffWriter is an io.Writer that buffers the input
// and compares it against a reference.
// If the input doesn't match the reference,
// a diff is printed to stdout when the writer closes.
type diffWriter struct {
fname string
old []byte
new bytes.Buffer
}

func newDiffWriter(fname string) (*diffWriter, error) {
old, err := os.ReadFile(fname)
if err != nil {
if !errors.Is(err, os.ErrNotExist) {
return nil, err
}
old = nil
}

return &diffWriter{
fname: fname,
old: old,
}, nil
}

func (dw *diffWriter) Write(p []byte) (int, error) {
return dw.new.Write(p)
}

func (dw *diffWriter) Diff(w io.Writer) error {
if bytes.Equal(dw.old, dw.new.Bytes()) {
return nil
}

return diff.Text(
filepath.Join("a", dw.fname),
filepath.Join("b", dw.fname),
dw.old,
dw.new.Bytes(),
w,
)
}
Loading

0 comments on commit 585050f

Please sign in to comment.