Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
3 changes: 3 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
50 changes: 48 additions & 2 deletions plugin/scripts/godebugdiff.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}

Expand All @@ -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, "")
Expand All @@ -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("", " ")
Expand All @@ -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 == "--" {
Expand Down Expand Up @@ -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
}
Expand Down
42 changes: 42 additions & 0 deletions plugin/scripts/godebugdiff_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
17 changes: 17 additions & 0 deletions tests/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}