From f6b79bae9e46f98e988050a30dfa24192b97d7d8 Mon Sep 17 00:00:00 2001 From: Marat Reimers Date: Fri, 9 Jan 2026 03:22:45 +0100 Subject: [PATCH] feat: Add diff mode Closes #111 --- README.md | 5 ++++ cmd/cmd.go | 61 ++++++++++++++++++++++++++++++++++-------- go.mod | 2 ++ go.sum | 4 +++ goldens/golden_test.go | 49 +++++++++++++++++++++++++++++---- 5 files changed, 105 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index fc62a8e..496c68b 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,11 @@ bar = [ If the file is `-`, the tool will read from stdin and write to stdout. + You can select a mode with the `--mode` flag: + - `fix` (default) sorts files in place. + - `lint` prints JSON findings and exits non-zero if anything is out of order. + - `diff` prints a unified diff instead of rewriting files and exits non-zero if anything would change. + #### pre-commit You can run keep-sorted automatically by adding this repository to your diff --git a/cmd/cmd.go b/cmd/cmd.go index af10f62..077b2c4 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -25,6 +25,7 @@ import ( "strconv" "strings" + "github.com/aymanbagabas/go-udiff" "github.com/google/keep-sorted/keepsorted" "github.com/rs/zerolog/log" flag "github.com/spf13/pflag" @@ -84,6 +85,7 @@ var ( operations = map[string]operation{ "lint": lint, "fix": fix, + "diff": diff, } ) @@ -228,18 +230,9 @@ func fix(fixer *keepsorted.Fixer, filenames []string, modifiedLines []keepsorted } } - for _, warn := range warnings { + if len(warnings) > 0 { ok = false - log := log.Warn() - if warn.Path != stdin { - log = log.Str("file", warn.Path) - } - if warn.Lines.Start == warn.Lines.End { - log = log.Int("line", warn.Lines.Start) - } else { - log = log.Ints("[start,end]", []int{warn.Lines.Start, warn.Lines.End}) - } - log.Msg(warn.Message) + logWarnings(warnings) } } @@ -269,6 +262,52 @@ func lint(fixer *keepsorted.Fixer, filenames []string, modifiedLines []keepsorte return false, nil } +func diff(fixer *keepsorted.Fixer, filenames []string, modifiedLines []keepsorted.LineRange) (ok bool, err error) { + ok = true + + for _, fn := range filenames { + contents, err := read(fn) + if err != nil { + return false, err + } + want, alreadyFixed, warnings := fixer.Fix(fn, contents, modifiedLines) + if !alreadyFixed { + ok = false + inName := fn + outName := fn + if fn == stdin { + inName = "stdin" + outName = "stdout" + } + if _, err := os.Stdout.WriteString(udiff.Unified(inName, outName, contents, want)); err != nil { + return false, err + } + } + + if len(warnings) > 0 { + ok = false + logWarnings(warnings) + } + } + + return ok, nil +} + +func logWarnings(warnings []*keepsorted.Finding) { + for _, warn := range warnings { + log := log.Warn() + if warn.Path != stdin { + log = log.Str("file", warn.Path) + } + if warn.Lines.Start == warn.Lines.End { + log = log.Int("line", warn.Lines.Start) + } else { + log = log.Ints("[start,end]", []int{warn.Lines.Start, warn.Lines.End}) + } + log.Msg(warn.Message) + } +} + func read(fn string) (string, error) { if fn == stdin { b, err := io.ReadAll(os.Stdin) diff --git a/go.mod b/go.mod index fdaca38..a7839b4 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,8 @@ go 1.23.1 require ( github.com/Workiva/go-datastructures v1.0.53 + github.com/aymanbagabas/go-udiff v0.3.1 + github.com/bluekeyes/go-gitdiff v0.8.1 github.com/google/go-cmp v0.5.8 github.com/mattn/go-isatty v0.0.20 github.com/rs/zerolog v1.31.0 diff --git a/go.sum b/go.sum index bd75113..6f39e10 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,9 @@ github.com/Workiva/go-datastructures v1.0.53 h1:J6Y/52yX10Xc5JjXmGtWoSSxs3mZnGSaq37xZZh7Yig= github.com/Workiva/go-datastructures v1.0.53/go.mod h1:1yZL+zfsztete+ePzZz/Zb1/t5BnDuE2Ya2MMGhzP6A= +github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY= +github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E= +github.com/bluekeyes/go-gitdiff v0.8.1 h1:lL1GofKMywO17c0lgQmJYcKek5+s8X6tXVNOLxy4smI= +github.com/bluekeyes/go-gitdiff v0.8.1/go.mod h1:WWAk1Mc6EgWarCrPFO+xeYlujPu98VuLW3Tu+B/85AE= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= diff --git a/goldens/golden_test.go b/goldens/golden_test.go index 165b506..b2b8acf 100644 --- a/goldens/golden_test.go +++ b/goldens/golden_test.go @@ -15,6 +15,7 @@ package golden_test import ( + "bytes" "errors" "fmt" "io" @@ -27,6 +28,7 @@ import ( "strings" "testing" + "github.com/bluekeyes/go-gitdiff/gitdiff" "github.com/google/go-cmp/cmp" ) @@ -68,7 +70,7 @@ func TestGoldens(t *testing.T) { t.Run(tc, func(t *testing.T) { t.Parallel() inFile := filepath.Join(dir, tc+".in") - in, err := os.Open(inFile) + in, err := os.ReadFile(inFile) if err != nil { t.Fatalf("Could not open .in file: %v", err) } @@ -91,7 +93,7 @@ func TestGoldens(t *testing.T) { wantErr = []byte(strings.ReplaceAll(string(wantErr), "\r\n", "\n")) wantErr = []byte(strings.ReplaceAll(string(wantErr), "\r", "\n")) - gotOut, gotErr, exitCode, err := runKeepSorted(in) + gotOut, gotErr, exitCode, err := runKeepSorted(bytes.NewReader(in), "fix") if err != nil { t.Errorf("Had trouble running keep-sorted: %v", err) } @@ -104,7 +106,9 @@ func TestGoldens(t *testing.T) { needsRegen <- inFile } - gotOut2, _, exitCode2, err := runKeepSorted(strings.NewReader(gotOut)) + testDiffMode(t, in, wantOut) + + gotOut2, _, exitCode2, err := runKeepSorted(strings.NewReader(gotOut), "fix") if err != nil { t.Errorf("Had trouble running keep-sorted on keep-sorted output: %v", err) } @@ -129,13 +133,48 @@ func TestGoldens(t *testing.T) { } } +func testDiffMode(t *testing.T, in []byte, wantOut []byte) { + t.Run("diff", func(t *testing.T) { + t.Parallel() + gotDiff, _, _, err := runKeepSorted(bytes.NewReader(in), "diff") + if err != nil { + t.Fatalf("Had trouble running keep-sorted --mode diff: %v", err) + } + files, _, err := gitdiff.Parse(strings.NewReader(gotDiff)) + if err != nil { + t.Fatalf("Had trouble parsing diff: %v", err) + } + if len(files) != 1 { + t.Fatalf("Exactly one file is expected in diff, got %d", len(files)) + } + var b strings.Builder + err = gitdiff.Apply(&b, bytes.NewReader(in), files[0]) + if err != nil { + t.Fatalf("Had trouble applying diff: %v", err) + } + if diff := cmp.Diff(string(wantOut), b.String()); diff != "" { + t.Fatalf("Diff applied to the input didn't match expected out:\n%s", diff) + } + }) + t.Run("diff after fix", func(t *testing.T) { + t.Parallel() + gotDiff, _, _, err := runKeepSorted(bytes.NewReader(wantOut), "diff") + if err != nil { + t.Fatalf("Had trouble running keep-sorted --mode diff: %v", err) + } + if gotDiff != "" { + t.Errorf("Non-empty diff produced:\n%s", gotDiff) + } + }) +} + func showTopLevel(dir string) (string, error) { b, err := exec.Command("git", "-C", dir, "rev-parse", "--show-toplevel").Output() return strings.TrimSpace(string(b)), err } -func runKeepSorted(stdin io.Reader) (stdout, stderr string, exitCode int, err error) { - cmd := exec.Command("go", "run", gitDir, "--id=keep-sorted-test", "--omit-timestamps", "-") +func runKeepSorted(stdin io.Reader, mode string) (stdout, stderr string, exitCode int, err error) { + cmd := exec.Command("go", "run", gitDir, "--id=keep-sorted-test", "--mode="+mode, "--omit-timestamps", "-") cmd.Stdin = stdin outPipe, err := cmd.StdoutPipe() if err != nil {