diff --git a/README.md b/README.md index 2fdff8f..70eb0b5 100644 --- a/README.md +++ b/README.md @@ -78,13 +78,14 @@ On a real project: [age](https://github.com/FiloSottile/age) at commit `b74dce4` ## Usage - godebugdiff [--from VERSION] [--to VERSION] [--tests] [--json] [--fail] [DIR] + godebugdiff [--from VERSION] [--to VERSION] [--tests] [--only PACKAGES] [--json] [--fail] [DIR] - `DIR` is the module root, the directory with go.mod. It defaults to `.`. - `--to` defaults to the installed toolchain's version, such as 1.27 for go1.27.3. It can't be newer than the toolchain, because older toolchains don't know about later settings. - `--from` defaults to the go line in go.mod. Pass the base branch's version to review a change that already raised it. - `--tests` adds test binaries. A module with no main package, such as a library, is checked through its test binaries automatically. -- `--json` prints the full result. `--fail` exits 1 when a change reaches a package one of your binaries imports. Exit code 2 means bad usage, or the go command couldn't load the module. +- `--only net/http,crypto/tls` includes only settings with those exact package names in text and JSON output. `--fail` considers only included settings. Package names can be separated by commas with optional whitespace; subpackages are not included automatically. +- `--json` prints the result. `--fail` exits 1 when a change reaches a package one of your binaries imports. Exit code 2 means bad usage, or the go command couldn't load the module. In CI, on a pull request that changes go.mod: diff --git a/ROADMAP.md b/ROADMAP.md index 097844c..18c0bd9 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,5 +1,8 @@ # Roadmap +## Available +- Filter settings by exact package names with `--only` in text and JSON output + ## Next - A Markdown output for pull request comments - Read the go line from go.work in workspace mode, instead of only noting it diff --git a/plugin/scripts/godebugdiff.go b/plugin/scripts/godebugdiff.go index 9fd898c..60ab54f 100644 --- a/plugin/scripts/godebugdiff.go +++ b/plugin/scripts/godebugdiff.go @@ -34,6 +34,7 @@ from --from to --to, and which binaries import the affected packages. --from VERSION go line to compare from (default: the one in go.mod) --to VERSION go line to compare to (default: the installed toolchain) + --only PACKAGES comma-separated exact package names to include --tests also check test binaries (always on for modules with no main package) --json print the result as JSON --fail exit 1 when a change reaches a package a binary imports @@ -82,6 +83,7 @@ type Result struct { TestsOnly bool `json:"testsOnly"` Changes []Change `json:"changes"` Notes []string `json:"notes,omitempty"` + Only []string `json:"only,omitempty"` reachCount int } @@ -96,6 +98,7 @@ func run(args []string, stdout, stderr io.Writer) int { from := fs.String("from", "", "") to := fs.String("to", "", "") tests := fs.Bool("tests", false, "") + only := fs.String("only", "", "") asJSON := fs.Bool("json", false, "") fail := fs.Bool("fail", false, "") showVersion := fs.Bool("version", false, "") @@ -119,6 +122,10 @@ func run(args []string, stdout, stderr io.Writer) int { fmt.Fprintln(stderr, "godebugdiff:", err) return 2 } + if err := filterPackages(res, *only); err != nil { + fmt.Fprintln(stderr, "godebugdiff:", err) + return 2 + } if *asJSON { enc := json.NewEncoder(stdout) enc.SetIndent("", " ") @@ -132,11 +139,43 @@ func run(args []string, stdout, stderr io.Writer) int { return 0 } +// filterPackages applies the same exact-package selection to text, JSON and --fail. +func filterPackages(r *Result, only string) error { + if only == "" { + return nil + } + selected := make(map[string]bool) + var packages []string + for _, name := range strings.Split(only, ",") { + name = strings.TrimSpace(name) + if name == "" { + return fmt.Errorf("--only requires comma-separated non-empty package names") + } + if !selected[name] { + packages = append(packages, name) + selected[name] = true + } + } + r.Only = packages + changes := make([]Change, 0) + r.reachCount = 0 + for _, c := range r.Changes { + if selected[c.Package] { + changes = append(changes, c) + if len(c.ReachedBy) > 0 { + r.reachCount++ + } + } + } + r.Changes = changes + return nil +} + // reorder moves flags before the directory argument, so // `godebugdiff DIR --to 1.24` works like `godebugdiff --to 1.24 DIR`. func reorder(args []string) []string { var flags, rest []string - valued := map[string]bool{"from": true, "to": true} + valued := map[string]bool{"from": true, "to": true, "only": true} for i := 0; i < len(args); i++ { a := args[i] if a == "--" { @@ -753,13 +792,20 @@ func Print(w io.Writer, r *Result) { count = strings.TrimSuffix(kind, "aries") + "ary" } fmt.Fprintf(w, "go %s -> %s in %s (%s, %d %s)\n", r.From, r.To, r.Module, r.Toolchain, len(r.Binaries), count) + if len(r.Only) > 0 { + fmt.Fprintf(w, "Packages: %s (exact matches; other changes omitted)\n", strings.Join(r.Only, ", ")) + } if len(r.Binaries) == 0 { fmt.Fprintf(w, "\nNo main packages or tests found, so there's nothing to compare.\n") printNotes(w, r) return } if len(r.Changes) == 0 { - fmt.Fprintf(w, "\nNo GODEBUG defaults change.\n") + if len(r.Only) > 0 { + fmt.Fprintf(w, "\nNo GODEBUG defaults change in the selected packages.\n") + } else { + fmt.Fprintf(w, "\nNo GODEBUG defaults change.\n") + } printNotes(w, r) return } diff --git a/plugin/scripts/godebugdiff_test.go b/plugin/scripts/godebugdiff_test.go index ef93409..d8a3e2e 100644 --- a/plugin/scripts/godebugdiff_test.go +++ b/plugin/scripts/godebugdiff_test.go @@ -246,3 +246,45 @@ func run1(t *testing.T, name string, args ...string) string { } return string(out) } + +func TestFilterPackages(t *testing.T) { + r := &Result{ + Binaries: []string{"example.com/app"}, + Changes: []Change{ + {Name: "http", Package: "net/http", ReachedBy: []string{"example.com/app"}}, + {Name: "child", Package: "net/http/httputil", ReachedBy: []string{"example.com/app"}}, + {Name: "tls", Package: "crypto/tls"}, + {Name: "unknown", Package: ""}, + }, + reachCount: 2, + } + if err := filterPackages(r, " net/http,crypto/tls,net/http "); err != nil { + t.Fatal(err) + } + if len(r.Changes) != 2 || r.Changes[0].Name != "http" || r.Changes[1].Name != "tls" || r.reachCount != 1 { + t.Fatalf("filter did not preserve exact matches and recount reach: %+v", r) + } + if !reflect.DeepEqual(r.Only, []string{"net/http", "crypto/tls"}) { + t.Fatalf("filter names = %v", r.Only) + } + if err := filterPackages(r, "net"); err != nil || len(r.Changes) != 0 || r.reachCount != 0 { + t.Fatalf("prefix must not match subpackages: %+v, %v", r, err) + } + var output bytes.Buffer + Print(&output, r) + if !strings.Contains(output.String(), "No GODEBUG defaults change in the selected packages.") { + t.Fatalf("empty filtered result needs scoped wording: %s", output.String()) + } +} + +func TestFilterPackagesValidation(t *testing.T) { + for _, input := range []string{",", "net/http,", " ,net/http"} { + if err := filterPackages(&Result{}, input); err == nil { + t.Errorf("expected empty package name in %q to be rejected", input) + } + } + r := &Result{Changes: []Change{{Name: "keep"}}, reachCount: 1} + if err := filterPackages(r, ""); err != nil || len(r.Changes) != 1 || r.reachCount != 1 { + t.Fatalf("omitted filter changed the result: %+v, %v", r, err) + } +} diff --git a/tests/integration_test.go b/tests/integration_test.go index 2a894f9..ae78791 100644 --- a/tests/integration_test.go +++ b/tests/integration_test.go @@ -309,3 +309,20 @@ func TestErrors(t *testing.T) { t.Errorf("removed setting: %d %s", code, stderr) } } + +func TestOnlyPackages(t *testing.T) { + needGo(t, 24) + r, code, stderr := diff(t, "../examples/shop", "--to", "1.24", "--only", "net/http,crypto/tls", "--fail") + if code != 1 || len(r.Changes) == 0 { + t.Fatalf("expected selected changes and exit 1, got %d: %s", code, stderr) + } + for _, change := range r.Changes { + if change.Package != "net/http" && change.Package != "crypto/tls" { + t.Errorf("unexpected package in filtered report: %s", change.Package) + } + } + r, code, stderr = diff(t, "../examples/shop", "--to", "1.24", "--only=example.com/not-imported", "--fail") + if code != 0 || len(r.Changes) != 0 { + t.Fatalf("excluded changes must not trigger --fail: %d, %+v, %s", code, r.Changes, stderr) + } +}