diff --git a/tools/pipeline/go.mod b/tools/pipeline/go.mod index 3cfcc6567f..845ae0a3f1 100644 --- a/tools/pipeline/go.mod +++ b/tools/pipeline/go.mod @@ -2,6 +2,9 @@ module github.com/hashicorp/vault/tools/pipeline go 1.24.0 +// We have test modules in here but they ought to be completely ignored +ignore internal/pkg/golang/fixtures + require ( github.com/Masterminds/semver v1.5.0 github.com/avast/retry-go/v4 v4.6.1 @@ -9,11 +12,13 @@ require ( github.com/hashicorp/hcl/v2 v2.24.0 github.com/hashicorp/releases-api v0.2.3 github.com/jedib0t/go-pretty/v6 v6.6.8 + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 github.com/shurcooL/githubv4 v0.0.0-20240727222349-48295856cce7 github.com/spf13/cobra v1.9.1 github.com/stretchr/testify v1.10.0 github.com/veqryn/slog-context v0.8.0 github.com/zclconf/go-cty v1.16.4 + golang.org/x/mod v0.29.0 golang.org/x/oauth2 v0.31.0 ) @@ -57,7 +62,6 @@ require ( github.com/oklog/ulid v1.3.1 // indirect github.com/oklog/ulid/v2 v2.1.1 // indirect github.com/opentracing/opentracing-go v1.2.0 // indirect - github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 // indirect github.com/spf13/pflag v1.0.7 // indirect @@ -66,12 +70,11 @@ require ( go.opentelemetry.io/otel v1.37.0 // indirect go.opentelemetry.io/otel/metric v1.37.0 // indirect go.opentelemetry.io/otel/trace v1.37.0 // indirect - golang.org/x/mod v0.27.0 // indirect - golang.org/x/net v0.43.0 // indirect - golang.org/x/sync v0.16.0 // indirect - golang.org/x/sys v0.35.0 // indirect - golang.org/x/text v0.28.0 // indirect - golang.org/x/tools v0.36.0 // indirect + golang.org/x/net v0.44.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.36.0 // indirect + golang.org/x/text v0.29.0 // indirect + golang.org/x/tools v0.37.0 // indirect google.golang.org/protobuf v1.36.8 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/tools/pipeline/go.sum b/tools/pipeline/go.sum index 82b130e298..d2aa4c3cd9 100644 --- a/tools/pipeline/go.sum +++ b/tools/pipeline/go.sum @@ -207,30 +207,30 @@ go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mx go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= -golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= -golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= -golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= -golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= -golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= -golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= +golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= +golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= golang.org/x/oauth2 v0.31.0 h1:8Fq0yVZLh4j4YA47vHKFTa9Ew5XIrCP8LC6UeNZnLxo= golang.org/x/oauth2 v0.31.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= -golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw= -golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng= -golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU= +golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= +golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= -golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= +golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= +golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= diff --git a/tools/pipeline/internal/cmd/go.go b/tools/pipeline/internal/cmd/go.go new file mode 100644 index 0000000000..6167ea7ed2 --- /dev/null +++ b/tools/pipeline/internal/cmd/go.go @@ -0,0 +1,18 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package cmd + +import "github.com/spf13/cobra" + +func newGoCmd() *cobra.Command { + goCmd := &cobra.Command{ + Use: "go", + Short: "Go commands", + Long: "Go commands", + } + + goCmd.AddCommand(newGoDiffCmd()) + + return goCmd +} diff --git a/tools/pipeline/internal/cmd/go_diff.go b/tools/pipeline/internal/cmd/go_diff.go new file mode 100644 index 0000000000..6a62b7cde8 --- /dev/null +++ b/tools/pipeline/internal/cmd/go_diff.go @@ -0,0 +1,20 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package cmd + +import ( + "github.com/spf13/cobra" +) + +func newGoDiffCmd() *cobra.Command { + goModCmd := &cobra.Command{ + Use: "diff", + Short: "Go diff commands", + Long: "Go diff commands", + } + + goModCmd.AddCommand(newGoDiffModCmd()) + + return goModCmd +} diff --git a/tools/pipeline/internal/cmd/go_diff_mod.go b/tools/pipeline/internal/cmd/go_diff_mod.go new file mode 100644 index 0000000000..d51d8684f6 --- /dev/null +++ b/tools/pipeline/internal/cmd/go_diff_mod.go @@ -0,0 +1,130 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package cmd + +import ( + "context" + "errors" + "fmt" + "io" + "os" + "path/filepath" + + "github.com/hashicorp/vault/tools/pipeline/internal/pkg/golang" + "github.com/spf13/cobra" +) + +var goModDiffReq = &golang.DiffModReq{ + A: &golang.ModSource{}, + B: &golang.ModSource{}, + Opts: golang.DefaultDiffOpts(), +} + +func newGoDiffModCmd() *cobra.Command { + goModDiffCmd := &cobra.Command{ + Use: "mod [ARGS]", + Short: "Diff two local go.mod files", + Long: "Diff two local go.mod files", + RunE: runGoModDiffCmd, + Args: func(cmd *cobra.Command, args []string) error { + switch len(args) { + case 2: + err := setUpGoModSourceFromPath(args[0], goModDiffReq.A) + if err != nil { + return err + } + return setUpGoModSourceFromPath(args[1], goModDiffReq.B) + case 0, 1: + return errors.New("invalid arguments: you must provide two local file paths") + default: + return fmt.Errorf("invalid arguments: expected two paths as arguments, received %d arguments", len(args)) + } + }, + } + + goModDiffCmd.PersistentFlags().BoolVar(&goModDiffReq.Opts.ParseLax, "lax", false, "Parse the modules in lax mode to ignore newer and unknown directives") + + goModDiffCmd.PersistentFlags().BoolVar(&goModDiffReq.Opts.Module, "module", true, "Compare the module directives in both files") + goModDiffCmd.PersistentFlags().BoolVar(&goModDiffReq.Opts.Go, "go", true, "Compare the go directives in both files") + goModDiffCmd.PersistentFlags().BoolVar(&goModDiffReq.Opts.Toolchain, "toolchain", true, "Compare the toolchain directives in both files") + goModDiffCmd.PersistentFlags().BoolVar(&goModDiffReq.Opts.Godebug, "godebug", true, "Compare the godebug directives in both files") + goModDiffCmd.PersistentFlags().BoolVar(&goModDiffReq.Opts.Require, "require", true, "Compare the require directives in both files") + goModDiffCmd.PersistentFlags().BoolVar(&goModDiffReq.Opts.Replace, "replace", true, "Compare the replace directives in both files") + goModDiffCmd.PersistentFlags().BoolVar(&goModDiffReq.Opts.Retract, "retract", true, "Compare the retract directives in both files") + goModDiffCmd.PersistentFlags().BoolVar(&goModDiffReq.Opts.Tool, "tool", true, "Compare the tool directives in both files") + goModDiffCmd.PersistentFlags().BoolVar(&goModDiffReq.Opts.Ignore, "ignore", true, "Compare the ignore directives in both files") + + goModDiffCmd.PersistentFlags().BoolVar(&goModDiffReq.Opts.StrictDiffRequire, "strict-require", true, "Strictly compare the requires directives in both files. When true all requires are compared, otherwise only shared requires are compared") + goModDiffCmd.PersistentFlags().BoolVar(&goModDiffReq.Opts.StrictDiffExclude, "strict-exclude", true, "Strictly compare the excludes directives in both files. When true all excludes are compared, otherwise only shared excludes are compared") + goModDiffCmd.PersistentFlags().BoolVar(&goModDiffReq.Opts.StrictDiffReplace, "strict-replace", true, "Strictly compare the replace directives in both files. When true all replaces are compared, otherwise only shared replaces are compared") + goModDiffCmd.PersistentFlags().BoolVar(&goModDiffReq.Opts.StrictDiffRetract, "strict-retract", true, "Strictly compare the retract directives in both files. When true all retracts are compared, otherwise only shared retract directives are compared") + + return goModDiffCmd +} + +func runGoModDiffCmd(cmd *cobra.Command, args []string) error { + cmd.SilenceUsage = true // Don't spam the usage on failure + + res, err := goModDiffReq.Run(context.TODO()) + if err != nil { + return err + } + + switch rootCfg.format { + case "json": + b, err1 := res.ToJSON() + if err1 != nil { + err = errors.Join(err, err1) + } else { + fmt.Println(string(b)) + } + case "markdown": + tbl, err1 := res.ToTable(err) + if err1 != nil { + err = errors.Join(err, err1) + } else { + tbl.SetTitle("Go Mod Diff") + fmt.Println(tbl.RenderMarkdown()) + } + default: + tbl, err1 := res.ToTable(err) + if err1 != nil { + err = errors.Join(err, err1) + } else { + if text := tbl.Render(); text != "" { + fmt.Println(text) + } + } + } + + if l := len(res.ModDiff); l > 0 { + err = errors.Join(fmt.Errorf("%d differences were found", l), err) + } + + return err +} + +func setUpGoModSourceFromPath(path string, source *golang.ModSource) (err error) { + if source == nil { + return errors.New("you must provide a mod source") + } + + aPath, err := filepath.Abs(path) + if err != nil { + return err + } + + f, err := os.Open(aPath) + if err != nil { + return err + } + defer func() { + err = errors.Join(f.Close()) + }() + + source.Name = path + source.Data, err = io.ReadAll(f) + + return err +} diff --git a/tools/pipeline/internal/cmd/root.go b/tools/pipeline/internal/cmd/root.go index 4dcab3c66d..86b5b586e5 100644 --- a/tools/pipeline/internal/cmd/root.go +++ b/tools/pipeline/internal/cmd/root.go @@ -32,6 +32,7 @@ func newRootCmd() *cobra.Command { rootCmd.AddCommand(newGenerateCmd()) rootCmd.AddCommand(newGithubCmd()) + rootCmd.AddCommand(newGoCmd()) rootCmd.AddCommand(newHCPCmd()) rootCmd.AddCommand(newReleasesCmd()) diff --git a/tools/pipeline/internal/pkg/golang/diff_exclude.go b/tools/pipeline/internal/pkg/golang/diff_exclude.go new file mode 100644 index 0000000000..1306e7fe11 --- /dev/null +++ b/tools/pipeline/internal/pkg/golang/diff_exclude.go @@ -0,0 +1,102 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package golang + +import ( + "maps" + "slices" + "strings" + + "golang.org/x/mod/modfile" +) + +// diffExclude compares the exclude directives in two module files and returns +// a slice of *Diff's. When strict parsing is set to true, exclude directives +// that are ommitted from one or the other modfile will also be included. +func diffExclude(a *modfile.File, b *modfile.File, strictDiff bool) []*Diff { + if (a == nil && b == nil) || (len(a.Exclude) == 0 && len(b.Exclude) == 0) { + return nil + } + + var diffs []*Diff + if strictDiff { + diffs = append(diffExcludeFindMissing(a, b), diffExcludeFindMissing(b, a)...) + } + versionDiffsA := diffExcludeFindDifferent(a, b) + versionDiffsB := diffExcludeFindDifferent(b, a) + maps.Copy(versionDiffsB, versionDiffsA) + + return slices.DeleteFunc( + append(diffs, slices.Collect(maps.Values(versionDiffsB))...), + func(d *Diff) bool { return d == nil }, + ) +} + +func diffExcludeFindMissing(a, b *modfile.File) []*Diff { + diffs := []*Diff{} + for _, needle := range a.Exclude { + if needle == nil { + continue + } + + // See if there's a matching require + idx := slices.IndexFunc(b.Exclude, func(hay *modfile.Exclude) bool { + if hay == nil { + return false + } + + return needle.Mod.Path == hay.Mod.Path + }) + + if idx >= 0 { + // We have a matching require + continue + } + + diff := newDiffFromModFiles(a, b, DirectiveExclude) + if needle.Syntax != nil { + diff.Diff.A = []string{strings.Join(needle.Syntax.Token, " ")} + } + diffs = append(diffs, diff) + } + + return diffs +} + +func diffExcludeFindDifferent(a, b *modfile.File) map[string]*Diff { + diffs := map[string]*Diff{} + for _, needle := range a.Exclude { + if needle == nil { + continue + } + + // See if there's a matching require + idx := slices.IndexFunc(b.Exclude, func(hay *modfile.Exclude) bool { + if hay == nil { + return false + } + + return needle.Mod.Path == hay.Mod.Path + }) + + if idx < 0 { + // We don't have a matching require + continue + } + + hay := b.Exclude[idx] + if needle.Mod.Version != hay.Mod.Version { + diff := newDiffFromModFiles(a, b, DirectiveExclude) + if needle.Syntax != nil { + diff.Diff.A = []string{strings.Join(needle.Syntax.Token, " ") + "\n"} + } + if hay.Syntax != nil { + diff.Diff.B = []string{strings.Join(hay.Syntax.Token, " ")} + } + diffs[needle.Mod.Path] = diff + } + } + + return diffs +} diff --git a/tools/pipeline/internal/pkg/golang/diff_go.go b/tools/pipeline/internal/pkg/golang/diff_go.go new file mode 100644 index 0000000000..be4f2ebee8 --- /dev/null +++ b/tools/pipeline/internal/pkg/golang/diff_go.go @@ -0,0 +1,31 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package golang + +import ( + "strings" + + "golang.org/x/mod/modfile" +) + +// diffGo compares the go directives in two modules files and returns +// a slice of *Diff's. +func diffGo(a *modfile.File, b *modfile.File) *Diff { + if (a == nil && b == nil) || + (a.Go == nil && b.Go == nil) || + ((a.Go != nil && b.Go != nil) && (a.Go.Version == b.Go.Version)) { + + return nil + } + + diff := newDiffFromModFiles(a, b, DirectiveGo) + if a != nil && a.Go != nil && a.Go.Syntax != nil { + diff.Diff.A = []string{strings.Join(a.Go.Syntax.Token, " ") + "\n"} + } + if b != nil && b.Go != nil && b.Go.Syntax != nil { + diff.Diff.B = []string{strings.Join(b.Go.Syntax.Token, " ")} + } + + return diff +} diff --git a/tools/pipeline/internal/pkg/golang/diff_godebug.go b/tools/pipeline/internal/pkg/golang/diff_godebug.go new file mode 100644 index 0000000000..46c247be52 --- /dev/null +++ b/tools/pipeline/internal/pkg/golang/diff_godebug.go @@ -0,0 +1,92 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package golang + +import ( + "slices" + "strings" + + "golang.org/x/mod/modfile" +) + +// diffGodebug compares the godebug directives in two modules files and returns +// a slice of *Diff's. When strict parsing is set to true, godebug directives +// that are ommitted from one or the other modfile will also be included. +func diffGodebug(a *modfile.File, b *modfile.File) []*Diff { + if (a == nil && b == nil) || (len(a.Godebug) == 0 && len(b.Godebug) == 0) { + return nil + } + + return slices.DeleteFunc( + append(diffGodebugFindDiffs(a, b), diffGodebugFindDiffs(b, a)...), + func(d *Diff) bool { return d == nil }, + ) +} + +func diffGodebugFindDiffs(a, b *modfile.File) []*Diff { + diffs := []*Diff{} + for _, needle := range a.Godebug { + idx := slices.IndexFunc(b.Godebug, func(hay *modfile.Godebug) bool { + return godebugMatchingKey(needle, hay) + }) + + if idx < 0 { + // We don't have matching godebug with the same key, create a single + // sided diff + diff := newDiffFromModFiles(a, b, DirectiveGodebug) + if needle != nil && needle.Syntax != nil { + diff.Diff.A = []string{strings.Join(needle.Syntax.Token, " ")} + } + diffs = append(diffs, diff) + continue + } + + // We have a matching key + hay := b.Godebug[idx] + if godebugEqual(needle, hay) { + continue + } + + // We have differing values. Create a double sided diff + diff := newDiffFromModFiles(a, b, DirectiveGodebug) + if needle != nil && needle.Syntax != nil { + diff.Diff.A = []string{strings.Join(needle.Syntax.Token, " ") + "\n"} + } + if hay != nil && hay.Syntax != nil { + diff.Diff.B = []string{strings.Join(hay.Syntax.Token, " ")} + } + + diffs = append(diffs, diff) + } + + return diffs +} + +func godebugEqual(a *modfile.Godebug, b *modfile.Godebug) bool { + if a == nil && b == nil { + return true + } + + if (a == nil && b != nil) || (a != nil && b == nil) { + return false + } + + if a.Key != b.Key { + return false + } + + return a.Value == b.Value +} + +func godebugMatchingKey(a *modfile.Godebug, b *modfile.Godebug) bool { + if a == nil && b == nil { + return false + } + + if (a == nil && b != nil) || (a != nil && b == nil) { + return false + } + + return a.Key == b.Key +} diff --git a/tools/pipeline/internal/pkg/golang/diff_ignore.go b/tools/pipeline/internal/pkg/golang/diff_ignore.go new file mode 100644 index 0000000000..a979cb9591 --- /dev/null +++ b/tools/pipeline/internal/pkg/golang/diff_ignore.go @@ -0,0 +1,56 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package golang + +import ( + "slices" + "strings" + + "golang.org/x/mod/modfile" +) + +// diffIgnore compares the ignore directives in two module files and returns +// a slice of *Diff's. When strict parsing is set to true, ignore directives +// that are ommitted from one or the other modfile will also be included. +func diffIgnore(a *modfile.File, b *modfile.File) []*Diff { + if (a == nil && b == nil) || (len(a.Ignore) == 0 && len(b.Ignore) == 0) { + return nil + } + + return slices.DeleteFunc( + append(diffIgnoreFindDiffs(a, b), diffIgnoreFindDiffs(b, a)...), + func(d *Diff) bool { return d == nil }, + ) +} + +func diffIgnoreFindDiffs(a, b *modfile.File) []*Diff { + diffs := []*Diff{} + for _, needle := range a.Ignore { + if needle == nil { + continue + } + + // See if there's a matching ignore + idx := slices.IndexFunc(b.Ignore, func(hay *modfile.Ignore) bool { + if hay == nil { + return false + } + + return needle.Path == hay.Path + }) + + if idx >= 0 { + // We have a matching ignore + continue + } + + diff := newDiffFromModFiles(a, b, DirectiveIgnore) + if needle.Syntax != nil { + diff.Diff.A = []string{strings.Join(needle.Syntax.Token, " ")} + } + diffs = append(diffs, diff) + } + + return diffs +} diff --git a/tools/pipeline/internal/pkg/golang/diff_mod_request.go b/tools/pipeline/internal/pkg/golang/diff_mod_request.go new file mode 100644 index 0000000000..bb41a41a89 --- /dev/null +++ b/tools/pipeline/internal/pkg/golang/diff_mod_request.go @@ -0,0 +1,79 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package golang + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/jedib0t/go-pretty/v6/table" + "github.com/pmezard/go-difflib/difflib" +) + +type DiffModReq struct { + A *ModSource `json:"a,omitempty"` + B *ModSource `json:"b,omitempty"` + Opts *DiffOpts `json:"opts,omitempty"` +} + +func (r *DiffModReq) Run(ctx context.Context) (*DiffModRes, error) { + res := &DiffModRes{} + var err error + + res.ModDiff, err = DiffModFiles(r.A, r.B, r.Opts) + + return res, err +} + +type DiffModRes struct { + ModDiff +} + +func (r *DiffModRes) ToJSON() ([]byte, error) { + b, err := json.Marshal(r) + if err != nil { + return nil, fmt.Errorf("marshaling latest HCP image response to JSON: %w", err) + } + + return b, nil +} + +// ToTable marshals the response to a text table. +func (r *DiffModRes) ToTable(err error) (table.Writer, error) { + t := table.NewWriter() + t.Style().Options.DrawBorder = false + t.Style().Options.SeparateColumns = false + t.Style().Options.SeparateFooter = false + t.Style().Options.SeparateHeader = false + t.Style().Options.SeparateRows = false + + if r == nil || r.ModDiff == nil || len(r.ModDiff) == 0 || err != nil { + if err != nil { + t.AppendHeader(table.Row{"error"}) + t.AppendRow(table.Row{err.Error()}) + } + return t, err + } + + t.AppendHeader(table.Row{"explanation", "diff"}) + for _, diff := range r.ModDiff { + if diff == nil { + continue + } + if diff.Diff == nil { + return nil, fmt.Errorf("missing unified diff: %v", diff) + } + + diffText, err := difflib.GetUnifiedDiffString(*diff.Diff) + if err != nil { + return nil, err + } + t.AppendRow(table.Row{diff.Directive.Explanation(), diffText}) + } + t.SuppressEmptyColumns() + t.SuppressTrailingSpaces() + + return t, nil +} diff --git a/tools/pipeline/internal/pkg/golang/diff_module.go b/tools/pipeline/internal/pkg/golang/diff_module.go new file mode 100644 index 0000000000..9eb3137318 --- /dev/null +++ b/tools/pipeline/internal/pkg/golang/diff_module.go @@ -0,0 +1,31 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package golang + +import ( + "strings" + + "golang.org/x/mod/modfile" +) + +// diffModule compares the module directives in two modules files and returns +// a slice of *Diff's. +func diffModule(a *modfile.File, b *modfile.File) *Diff { + if (a == nil && b == nil) || + (a.Module == nil && b.Module == nil) || + (a.Module.Mod.String() == b.Module.Mod.String()) { + + return nil + } + + diff := newDiffFromModFiles(a, b, DirectiveModule) + if a != nil && a.Module != nil && a.Module.Syntax != nil { + diff.Diff.A = []string{strings.Join(a.Module.Syntax.Token, " ") + "\n"} + } + if b != nil && b.Module != nil && b.Module.Syntax != nil { + diff.Diff.B = []string{strings.Join(b.Module.Syntax.Token, " ")} + } + + return diff +} diff --git a/tools/pipeline/internal/pkg/golang/diff_replace.go b/tools/pipeline/internal/pkg/golang/diff_replace.go new file mode 100644 index 0000000000..ddea936257 --- /dev/null +++ b/tools/pipeline/internal/pkg/golang/diff_replace.go @@ -0,0 +1,125 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package golang + +import ( + "maps" + "slices" + "strings" + + "golang.org/x/mod/modfile" +) + +func diffReplace(a *modfile.File, b *modfile.File, strictDiff bool) []*Diff { + if (a == nil && b == nil) || (len(a.Replace) == 0 && len(b.Replace) == 0) { + return nil + } + + var diffs []*Diff + if strictDiff { + diffs = append(diffReplaceFindMissing(a, b), diffReplaceFindMissing(b, a)...) + } + versionDiffsA := diffReplaceFindDifferent(a, b) + versionDiffsB := diffReplaceFindDifferent(b, a) + maps.Copy(versionDiffsB, versionDiffsA) + + return slices.DeleteFunc( + append(diffs, slices.Collect(maps.Values(versionDiffsB))...), + func(d *Diff) bool { return d == nil }, + ) +} + +func diffReplaceFindMissing(a, b *modfile.File) []*Diff { + diffs := []*Diff{} + for _, needle := range a.Replace { + if needle == nil { + continue + } + + // See if there's a matching old exclude + idx := slices.IndexFunc(b.Replace, func(hay *modfile.Replace) bool { + if hay == nil { + return false + } + + return needle.Old.Path == hay.Old.Path + }) + + if idx >= 0 { + // We have a matching old exclude, we'll handle this in the version diff check + continue + } + + diff := newDiffFromModFiles(a, b, DirectiveReplace) + if needle.Syntax != nil { + diff.Diff.A = []string{strings.Join(needle.Syntax.Token, " ")} + } + diffs = append(diffs, diff) + } + + return diffs +} + +func diffReplaceFindDifferent(a, b *modfile.File) map[string]*Diff { + diffs := map[string]*Diff{} + for _, needle := range a.Replace { + if needle == nil { + continue + } + + // See if there's a matching exclude + idx := slices.IndexFunc(b.Replace, func(hay *modfile.Replace) bool { + if hay == nil { + return false + } + + return needle.Old.Path == hay.Old.Path + }) + + if idx < 0 { + // We don't have a matching exclude + continue + } + + hay := b.Replace[idx] + if replaceEqual(needle, hay) { + continue + } + + diff := newDiffFromModFiles(a, b, DirectiveReplace) + if needle.Syntax != nil { + diff.Diff.A = []string{strings.Join(needle.Syntax.Token, " ") + "\n"} + } + if hay.Syntax != nil { + diff.Diff.B = []string{strings.Join(hay.Syntax.Token, " ")} + } + diffs[needle.Old.Path] = diff + } + + return diffs +} + +func replaceEqual(a *modfile.Replace, b *modfile.Replace) bool { + if a == nil && b == nil { + return true + } + + if (a == nil && b != nil) || (a != nil && b == nil) { + return false + } + + if a.Old.Path != b.Old.Path { + return false + } + + if a.Old.Version != b.Old.Version { + return false + } + + if a.New.Path != b.New.Path { + return false + } + + return a.New.Version == b.New.Version +} diff --git a/tools/pipeline/internal/pkg/golang/diff_require.go b/tools/pipeline/internal/pkg/golang/diff_require.go new file mode 100644 index 0000000000..a537e26c85 --- /dev/null +++ b/tools/pipeline/internal/pkg/golang/diff_require.go @@ -0,0 +1,99 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package golang + +import ( + "maps" + "slices" + "strings" + + "golang.org/x/mod/modfile" +) + +func diffRequire(a *modfile.File, b *modfile.File, strictDiff bool) []*Diff { + if (a == nil && b == nil) || (len(a.Require) == 0 && len(b.Require) == 0) { + return nil + } + + var diffs []*Diff + if strictDiff { + diffs = append(diffRequireFindMissing(a, b), diffRequireFindMissing(b, a)...) + } + versionDiffsA := diffRequireFindDifferent(a, b) + versionDiffsB := diffRequireFindDifferent(b, a) + maps.Copy(versionDiffsB, versionDiffsA) + + return slices.DeleteFunc( + append(diffs, slices.Collect(maps.Values(versionDiffsB))...), + func(d *Diff) bool { return d == nil }, + ) +} + +func diffRequireFindMissing(a, b *modfile.File) []*Diff { + diffs := []*Diff{} + for _, needle := range a.Require { + if needle == nil { + continue + } + + // See if there's a matching require + idx := slices.IndexFunc(b.Require, func(hay *modfile.Require) bool { + if hay == nil { + return false + } + + return needle.Mod.Path == hay.Mod.Path + }) + + if idx >= 0 { + // We have a matching require + continue + } + + diff := newDiffFromModFiles(a, b, DirectiveRequire) + if needle.Syntax != nil { + diff.Diff.A = []string{strings.Join(needle.Syntax.Token, " ")} + } + diffs = append(diffs, diff) + } + + return diffs +} + +func diffRequireFindDifferent(a, b *modfile.File) map[string]*Diff { + diffs := map[string]*Diff{} + for _, needle := range a.Require { + if needle == nil { + continue + } + + // See if there's a matching require + idx := slices.IndexFunc(b.Require, func(hay *modfile.Require) bool { + if hay == nil { + return false + } + + return needle.Mod.Path == hay.Mod.Path + }) + + if idx < 0 { + // We don't have a matching require + continue + } + + hay := b.Require[idx] + if needle.Mod.Version != hay.Mod.Version { + diff := newDiffFromModFiles(a, b, DirectiveRequire) + if needle.Syntax != nil { + diff.Diff.A = []string{strings.Join(needle.Syntax.Token, " ") + "\n"} + } + if hay.Syntax != nil { + diff.Diff.B = []string{strings.Join(hay.Syntax.Token, " ")} + } + diffs[needle.Mod.Path] = diff + } + } + + return diffs +} diff --git a/tools/pipeline/internal/pkg/golang/diff_retract.go b/tools/pipeline/internal/pkg/golang/diff_retract.go new file mode 100644 index 0000000000..b0189c300d --- /dev/null +++ b/tools/pipeline/internal/pkg/golang/diff_retract.go @@ -0,0 +1,121 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package golang + +import ( + "maps" + "slices" + "strings" + + "golang.org/x/mod/modfile" +) + +func diffRetract(a *modfile.File, b *modfile.File, strictDiff bool) []*Diff { + if (a == nil && b == nil) || (len(a.Retract) == 0 && len(b.Retract) == 0) { + return nil + } + + var diffs []*Diff + if strictDiff { + diffs = append(diffRetractFindMissing(a, b), diffRetractFindMissing(b, a)...) + } + versionDiffsA := diffRetractFindDifferent(a, b) + versionDiffsB := diffRetractFindDifferent(b, a) + maps.Copy(versionDiffsB, versionDiffsA) + + return slices.DeleteFunc( + append(diffs, slices.Collect(maps.Values(versionDiffsB))...), + func(d *Diff) bool { return d == nil }, + ) +} + +func diffRetractFindMissing(a, b *modfile.File) []*Diff { + diffs := []*Diff{} + for _, needle := range a.Retract { + if needle == nil { + continue + } + + // See if there's a matching retract + idx := slices.IndexFunc(b.Retract, func(hay *modfile.Retract) bool { + if hay == nil { + return false + } + + return needle.VersionInterval.Low == hay.VersionInterval.Low + }) + + if idx >= 0 { + // We have a matching retract + continue + } + + diff := newDiffFromModFiles(a, b, DirectiveRetract) + if needle.Syntax != nil { + diff.Diff.A = []string{strings.Join(needle.Syntax.Token, " ")} + } + diffs = append(diffs, diff) + } + + return diffs +} + +func diffRetractFindDifferent(a, b *modfile.File) map[string]*Diff { + diffs := map[string]*Diff{} + for _, needle := range a.Retract { + if needle == nil { + continue + } + + // See if there's a matching require + idx := slices.IndexFunc(b.Retract, func(hay *modfile.Retract) bool { + if hay == nil { + return false + } + + return needle.VersionInterval.Low == hay.VersionInterval.Low + }) + + if idx < 0 { + // We don't have a matching require + continue + } + + hay := b.Retract[idx] + if retractEqual(needle, hay) { + continue + } + + diff := newDiffFromModFiles(a, b, DirectiveRetract) + if needle.Syntax != nil { + diff.Diff.A = []string{strings.Join(needle.Syntax.Token, " ") + "\n"} + } + if hay.Syntax != nil { + diff.Diff.B = []string{strings.Join(hay.Syntax.Token, " ")} + } + diffs[needle.VersionInterval.Low] = diff + } + + return diffs +} + +func retractEqual(a *modfile.Retract, b *modfile.Retract) bool { + if a == nil && b == nil { + return true + } + + if (a == nil && b != nil) || (a != nil && b == nil) { + return false + } + + if a.VersionInterval.Low != b.VersionInterval.Low { + return false + } + + if a.VersionInterval.High != b.VersionInterval.High { + return false + } + + return a.Rationale == b.Rationale +} diff --git a/tools/pipeline/internal/pkg/golang/diff_tool.go b/tools/pipeline/internal/pkg/golang/diff_tool.go new file mode 100644 index 0000000000..b32d87861d --- /dev/null +++ b/tools/pipeline/internal/pkg/golang/diff_tool.go @@ -0,0 +1,53 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package golang + +import ( + "slices" + "strings" + + "golang.org/x/mod/modfile" +) + +func diffTool(a *modfile.File, b *modfile.File) []*Diff { + if (a == nil && b == nil) || (len(a.Tool) == 0 && len(b.Tool) == 0) { + return nil + } + + return slices.DeleteFunc( + append(diffToolFindDiffs(a, b), diffToolFindDiffs(b, a)...), + func(d *Diff) bool { return d == nil }, + ) +} + +func diffToolFindDiffs(a, b *modfile.File) []*Diff { + diffs := []*Diff{} + for _, needle := range a.Tool { + if needle == nil { + continue + } + + // See if there's a matching tool + idx := slices.IndexFunc(b.Tool, func(hay *modfile.Tool) bool { + if hay == nil { + return false + } + + return needle.Path == hay.Path + }) + + if idx >= 0 { + // We have a matching tool + continue + } + + diff := newDiffFromModFiles(a, b, DirectiveTool) + if needle.Syntax != nil { + diff.Diff.A = []string{strings.Join(needle.Syntax.Token, " ")} + } + diffs = append(diffs, diff) + } + + return diffs +} diff --git a/tools/pipeline/internal/pkg/golang/diff_toolchain.go b/tools/pipeline/internal/pkg/golang/diff_toolchain.go new file mode 100644 index 0000000000..1fa8de7638 --- /dev/null +++ b/tools/pipeline/internal/pkg/golang/diff_toolchain.go @@ -0,0 +1,29 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package golang + +import ( + "strings" + + "golang.org/x/mod/modfile" +) + +func diffToolchain(a *modfile.File, b *modfile.File) *Diff { + if (a == nil && b == nil) || + (a.Toolchain == nil && b.Toolchain == nil) || + ((a.Toolchain != nil && b.Toolchain != nil) && (a.Toolchain.Name == b.Toolchain.Name)) { + + return nil + } + + diff := newDiffFromModFiles(a, b, DirectiveToolchain) + if a != nil && a.Toolchain != nil && a.Toolchain.Syntax != nil { + diff.Diff.A = []string{strings.Join(a.Toolchain.Syntax.Token, " ") + "\n"} + } + if b != nil && b.Toolchain != nil && b.Toolchain.Syntax != nil { + diff.Diff.B = []string{strings.Join(b.Toolchain.Syntax.Token, " ")} + } + + return diff +} diff --git a/tools/pipeline/internal/pkg/golang/doc.go b/tools/pipeline/internal/pkg/golang/doc.go new file mode 100644 index 0000000000..6de1110a53 --- /dev/null +++ b/tools/pipeline/internal/pkg/golang/doc.go @@ -0,0 +1,5 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +// Package golang contains functions and types for Go analysis +package golang diff --git a/tools/pipeline/internal/pkg/golang/fixtures/moda/go.mod b/tools/pipeline/internal/pkg/golang/fixtures/moda/go.mod new file mode 100644 index 0000000000..148e8c9881 --- /dev/null +++ b/tools/pipeline/internal/pkg/golang/fixtures/moda/go.mod @@ -0,0 +1,25 @@ +module github.com/hashicorp/vault/pipeline/golang/moda + +go 1.25 + +require github.com/99designs/keyring v1.2.2 + +require ( + github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect + github.com/danieljoos/wincred v1.1.2 // indirect + github.com/dvsekhvalnov/jose2go v1.5.0 // indirect + github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 // indirect + github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c // indirect + github.com/mtibben/percent v0.2.1 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/term v0.3.0 // indirect + golang.org/x/tools v0.38.0 // indirect +) + +tool golang.org/x/tools/cmd/bisect + +ignore ( + ./third_party/javascript + content/html + static +) diff --git a/tools/pipeline/internal/pkg/golang/fixtures/moda/go.sum b/tools/pipeline/internal/pkg/golang/fixtures/moda/go.sum new file mode 100644 index 0000000000..c4dd84b4ee --- /dev/null +++ b/tools/pipeline/internal/pkg/golang/fixtures/moda/go.sum @@ -0,0 +1,42 @@ +github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs= +github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= +github.com/99designs/keyring v1.2.2 h1:pZd3neh/EmUzWONb35LxQfvuY7kiSXAq3HQd97+XBn0= +github.com/99designs/keyring v1.2.2/go.mod h1:wes/FrByc8j7lFOAGLGSNEg8f/PaI3cgTBqhFkHUrPk= +github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0= +github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= +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= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dvsekhvalnov/jose2go v1.5.0 h1:3j8ya4Z4kMCwT5nXIKFSV84YS+HdqSSO0VsTQxaLAeM= +github.com/dvsekhvalnov/jose2go v1.5.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= +github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2 h1:ZpnhV/YsD2/4cESfV5+Hoeu/iUR3ruzNvZ+yQfO03a0= +github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2/go.mod h1:bBOAhwG1umN6/6ZUMtDFBMQR8jRg9O75tm9K00oMsK4= +github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c h1:6rhixN/i8ZofjG1Y75iExal34USq5p+wiN1tpie8IrU= +github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c/go.mod h1:NMPJylDgVpX0MLRlPy15sqSwOFv/U1GZ2m21JhFfek0= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= +github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +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= +github.com/stretchr/objx v0.3.0 h1:NGXK3lHquSN08v5vWalVI/L8XU9hdzE/G6xsrze47As= +github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI= +golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U= +gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/tools/pipeline/internal/pkg/golang/fixtures/moda/main.go b/tools/pipeline/internal/pkg/golang/fixtures/moda/main.go new file mode 100644 index 0000000000..2814088bfe --- /dev/null +++ b/tools/pipeline/internal/pkg/golang/fixtures/moda/main.go @@ -0,0 +1,8 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package main + +import ( + _ "github.com/99designs/keyring" +) diff --git a/tools/pipeline/internal/pkg/golang/fixtures/modb/go.mod b/tools/pipeline/internal/pkg/golang/fixtures/modb/go.mod new file mode 100644 index 0000000000..f1a08a2871 --- /dev/null +++ b/tools/pipeline/internal/pkg/golang/fixtures/modb/go.mod @@ -0,0 +1,38 @@ +module github.com/hashicorp/vault/pipeline/golang/modb + +go 1.25.2 + +toolchain go1.24 + +godebug ( + default=go1.21 + httpcookiemaxnum=4000 + panicnil=1 +) + +exclude golang.org/x/term v0.2.0 + +replace github.com/99designs/keyring => github.com/Jeffail/keyring v1.2.3 + +require github.com/99designs/keyring v0.0.0-00010101000000-000000000000 + +retract ( + [v1.0.0, v1.9.9] + v0.9.0 +) + +tool golang.org/x/tools/cmd/stringer + +require ( + github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 // indirect + github.com/danieljoos/wincred v1.1.2 // indirect + github.com/dvsekhvalnov/jose2go v1.5.0 // indirect + github.com/mtibben/percent v0.2.1 // indirect + golang.org/x/mod v0.29.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/term v0.3.0 // indirect + golang.org/x/tools v0.38.0 // indirect +) + +ignore ./node_modules diff --git a/tools/pipeline/internal/pkg/golang/fixtures/modb/go.sum b/tools/pipeline/internal/pkg/golang/fixtures/modb/go.sum new file mode 100644 index 0000000000..b6e89f5251 --- /dev/null +++ b/tools/pipeline/internal/pkg/golang/fixtures/modb/go.sum @@ -0,0 +1,44 @@ +github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs= +github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= +github.com/Jeffail/keyring v1.2.3 h1:WRmYdGPmHoJqX66KjGXQBALp6mUN00tD0ds5C4pqEsQ= +github.com/Jeffail/keyring v1.2.3/go.mod h1:xIg4RDmDwDuUFoU4IzDIT3b+HV24JUYlzo6ILZUH3Sc= +github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0= +github.com/danieljoos/wincred v1.1.2/go.mod h1:GijpziifJoIBfYh+S7BbkdUTU4LfM+QnGqR5Vl2tAx0= +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= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dvsekhvalnov/jose2go v1.5.0 h1:3j8ya4Z4kMCwT5nXIKFSV84YS+HdqSSO0VsTQxaLAeM= +github.com/dvsekhvalnov/jose2go v1.5.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= +github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +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= +github.com/stretchr/objx v0.3.0 h1:NGXK3lHquSN08v5vWalVI/L8XU9hdzE/G6xsrze47As= +github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= +golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI= +golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= +golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= +golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U= +gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/tools/pipeline/internal/pkg/golang/fixtures/modb/main.go b/tools/pipeline/internal/pkg/golang/fixtures/modb/main.go new file mode 100644 index 0000000000..2814088bfe --- /dev/null +++ b/tools/pipeline/internal/pkg/golang/fixtures/modb/main.go @@ -0,0 +1,8 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package main + +import ( + _ "github.com/99designs/keyring" +) diff --git a/tools/pipeline/internal/pkg/golang/mod_diff.go b/tools/pipeline/internal/pkg/golang/mod_diff.go new file mode 100644 index 0000000000..1930a6500d --- /dev/null +++ b/tools/pipeline/internal/pkg/golang/mod_diff.go @@ -0,0 +1,254 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package golang + +import ( + "errors" + "fmt" + "slices" + "strings" + + "github.com/pmezard/go-difflib/difflib" + "golang.org/x/mod/modfile" +) + +// DiffOpts are options for the module diff. +type DiffOpts struct { + ParseLax bool + + Module bool + Go bool + Toolchain bool + Godebug bool + Require bool + Exclude bool + Replace bool + Retract bool + Tool bool + Ignore bool + + StrictDiffRequire bool + StrictDiffExclude bool + StrictDiffReplace bool + StrictDiffRetract bool +} + +func DefaultDiffOpts() *DiffOpts { + return &DiffOpts{ + ParseLax: false, + Module: true, + Go: true, + Toolchain: true, + Godebug: true, + Require: true, + Exclude: true, + Replace: true, + Retract: true, + Tool: true, + Ignore: true, + StrictDiffRequire: true, + StrictDiffExclude: true, + StrictDiffReplace: true, + StrictDiffRetract: true, + } +} + +// ModDiff is the result of comparing two Go modules. +type ModDiff []*Diff + +// ModSource is a go.mod file +type ModSource struct { + Name string + Data []byte +} + +type Diff struct { + Directive Directive + Diff *difflib.UnifiedDiff +} + +type Directive string + +const ( + DirectiveModule Directive = "module" + DirectiveGo Directive = "go" + DirectiveToolchain Directive = "toolchain" + DirectiveGodebug Directive = "godebug" + DirectiveRequire Directive = "require" + DirectiveExclude Directive = "exclude" + DirectiveReplace Directive = "replace" + DirectiveRetract Directive = "retract" + DirectiveTool Directive = "tool" + DirectiveIgnore Directive = "ignore" +) + +func (d *Diff) Explanation() string { + if d == nil || d.Directive == "" { + return "" + } + + return d.Directive.Explanation() +} + +func (d *Diff) UnifiedText() string { + if d == nil || d.Diff == nil { + return "" + } + + txt, _ := difflib.GetUnifiedDiffString(*d.Diff) + + return txt +} + +func (d Directive) Explanation() string { + return fmt.Sprintf("The '%s' directives do not match", d) +} + +// DiffModFiles diffs two go.mod "files" and returns a ModDiff. +func DiffModFiles(as *ModSource, bs *ModSource, opts *DiffOpts) (ModDiff, error) { + if as == nil { + return nil, errors.New("missing a mod source") + } + + if bs == nil { + return nil, errors.New("missing b mod source") + } + + var af *modfile.File + var bf *modfile.File + var err error + if opts.ParseLax { + af, err = modfile.ParseLax(as.Name, as.Data, nil) + if err != nil { + return nil, fmt.Errorf("parsing %s contents: %w", as.Name, err) + } + + bf, err = modfile.ParseLax(bs.Name, bs.Data, nil) + if err != nil { + return nil, fmt.Errorf("parsing %s contents: %w", bs.Name, err) + } + } else { + af, err = modfile.Parse(as.Name, as.Data, nil) + if err != nil { + return nil, fmt.Errorf("parsing %s contents: %w", as.Name, err) + } + + bf, err = modfile.Parse(bs.Name, bs.Data, nil) + if err != nil { + return nil, fmt.Errorf("parsing %s contents: %w", bs.Name, err) + } + } + + return diffModFiles(af, bf, opts) +} + +func diffModFiles(a *modfile.File, b *modfile.File, opts *DiffOpts) (ModDiff, error) { + if a == nil { + return nil, errors.New("missing a mod file") + } + + if b == nil { + return nil, errors.New("missing b mod file") + } + + diff := ModDiff{} + if opts.Module { + diff = append(diff, diffModule(a, b)) + } + if opts.Go { + diff = append(diff, diffGo(a, b)) + } + if opts.Toolchain { + diff = append(diff, diffToolchain(a, b)) + } + if opts.Godebug { + diff = append(diff, diffGodebug(a, b)...) + } + if opts.Require || opts.StrictDiffRequire { + diff = append(diff, diffRequire(a, b, opts.StrictDiffRequire)...) + } + if opts.Exclude || opts.StrictDiffExclude { + diff = append(diff, diffExclude(a, b, opts.StrictDiffExclude)...) + } + if opts.Replace || opts.StrictDiffReplace { + diff = append(diff, diffReplace(a, b, opts.StrictDiffReplace)...) + } + if opts.Retract || opts.StrictDiffRetract { + diff = append(diff, diffRetract(a, b, opts.StrictDiffRetract)...) + } + if opts.Tool { + diff = append(diff, diffTool(a, b)...) + } + if opts.Ignore { + diff = append(diff, diffIgnore(a, b)...) + } + + if len(diff) == 0 { + return nil, nil + } + + diff = slices.DeleteFunc(diff, func(d *Diff) bool { return d == nil }) + slices.SortStableFunc(diff, func(a *Diff, b *Diff) int { + // a < b = 1 + // a == b = 0 + // a > b = -1 + if a == nil && b == nil { + return 0 + } + + if a == nil && b != nil { + return 1 + } + + if a != nil && b == nil { + return -1 + } + + if n := strings.Compare(string(a.Directive), string(b.Directive)); n != 0 { + return n + } + + if a.Diff == nil && b.Diff == nil { + return 0 + } + + if a.Diff == nil && b.Diff != nil { + return 1 + } + + if a.Diff != nil && b.Diff == nil { + return -1 + } + + atxt, aerr := difflib.GetUnifiedDiffString(*a.Diff) + btxt, berr := difflib.GetUnifiedDiffString(*b.Diff) + + if aerr == nil && berr != nil { + return 1 + } + + if aerr != nil && berr == nil { + return -1 + } + + return strings.Compare(atxt, btxt) + }) + + return diff, nil +} + +func newDiffFromModFiles(a, b *modfile.File, typ Directive) *Diff { + res := &Diff{ + Directive: typ, + Diff: &difflib.UnifiedDiff{Context: 1}, + } + if a != nil && a.Syntax != nil { + res.Diff.FromFile = a.Syntax.Name + } + if b != nil && b.Syntax != nil { + res.Diff.ToFile = b.Syntax.Name + } + + return res +} diff --git a/tools/pipeline/internal/pkg/golang/mod_diff_test.go b/tools/pipeline/internal/pkg/golang/mod_diff_test.go new file mode 100644 index 0000000000..66d78e28f9 --- /dev/null +++ b/tools/pipeline/internal/pkg/golang/mod_diff_test.go @@ -0,0 +1,517 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: BUSL-1.1 + +package golang + +import ( + "os" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_DiffModFiles_Equal(t *testing.T) { + t.Parallel() + + modA, err := os.ReadFile("./fixtures/moda/go.mod") + require.NoError(t, err) + + modB, err := os.ReadFile("./fixtures/modb/go.mod") + require.NoError(t, err) + + for desc, test := range map[string]struct { + as *ModSource + bs *ModSource + opts *DiffOpts + }{ + "moda not lax not strict": { + &ModSource{Name: "moda-1", Data: modA}, + &ModSource{Name: "moda-2", Data: modA}, + &DiffOpts{StrictDiffRequire: false, ParseLax: false}, + }, + "moda lax not strict": { + &ModSource{Name: "moda-1", Data: modA}, + &ModSource{Name: "moda-2", Data: modA}, + &DiffOpts{StrictDiffRequire: false, ParseLax: true}, + }, + "moda not lax strict": { + &ModSource{Name: "moda-1", Data: modA}, + &ModSource{Name: "moda-2", Data: modA}, + &DiffOpts{StrictDiffRequire: true, ParseLax: false}, + }, + "moda lax strict": { + &ModSource{Name: "moda-1", Data: modA}, + &ModSource{Name: "moda-2", Data: modA}, + &DiffOpts{StrictDiffRequire: true, ParseLax: true}, + }, + "modb not lax not strict": { + &ModSource{Name: "modb-1", Data: modB}, + &ModSource{Name: "modb-2", Data: modB}, + &DiffOpts{StrictDiffRequire: false, ParseLax: false}, + }, + "modb lax not strict": { + &ModSource{Name: "modb-1", Data: modB}, + &ModSource{Name: "modb-2", Data: modB}, + &DiffOpts{StrictDiffRequire: false, ParseLax: true}, + }, + "modb not lax strict": { + &ModSource{Name: "modb-1", Data: modB}, + &ModSource{Name: "modb-2", Data: modB}, + &DiffOpts{StrictDiffRequire: true, ParseLax: false}, + }, + "modb lax strict": { + &ModSource{Name: "modb-1", Data: modB}, + &ModSource{Name: "modb-2", Data: modB}, + &DiffOpts{StrictDiffRequire: true, ParseLax: true}, + }, + } { + t.Run(desc, func(t *testing.T) { + t.Parallel() + diff, err := DiffModFiles(test.as, test.bs, test.opts) + require.NoError(t, err) + require.Nil(t, diff, "expected no diff, got:\n%v", printModDiff(diff)) + }) + } +} + +func Test_DiffModFiles_Diff(t *testing.T) { + t.Parallel() + + modA, err := os.ReadFile("./fixtures/moda/go.mod") + require.NoError(t, err) + + modB, err := os.ReadFile("./fixtures/modb/go.mod") + require.NoError(t, err) + + as := &ModSource{Name: "moda", Data: modA} + bs := &ModSource{Name: "modb", Data: modB} + + for desc, test := range map[string]struct { + opts *DiffOpts + condition func(t *testing.T, got ModDiff) + }{ + "strict parse lax diff": { + &DiffOpts{ + ParseLax: false, + Module: true, + Go: true, + Toolchain: true, + Godebug: true, + Require: true, + Exclude: true, + Replace: true, + Retract: true, + Tool: true, + Ignore: true, + StrictDiffRequire: false, + StrictDiffExclude: false, + StrictDiffReplace: false, + StrictDiffRetract: false, + }, + func(t *testing.T, got ModDiff) { + hasDiffMatching(t, got, DirectiveModule, []string{ + "-module github.com/hashicorp/vault/pipeline/golang/moda", + "+module github.com/hashicorp/vault/pipeline/golang/modb", + }) + hasDiffMatching(t, got, DirectiveGo, []string{ + "-go 1.25", + "+go 1.25.2", + }) + hasDiffMatching(t, got, DirectiveToolchain, []string{ + "+toolchain go1.24", + }) + hasDiffMatching(t, got, DirectiveGodebug, []string{ + "-default=go1.21", + }) + hasDiffMatching(t, got, DirectiveGodebug, []string{ + "-httpcookiemaxnum=4000", + }) + hasDiffMatching(t, got, DirectiveGodebug, []string{ + "-panicnil=1", + }) + hasDiffMatching(t, got, DirectiveRequire, []string{ + "-require github.com/99designs/keyring v1.2.2", + "+require github.com/99designs/keyring v0.0.0-00010101000000-000000000000", + }) + noDiffMatching(t, got, DirectiveRequire, []string{ + "-github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c", + }) + noDiffMatching(t, got, DirectiveExclude, []string{ + "-exclude golang.org/x/term v0.2.0", + }) + noDiffMatching(t, got, DirectiveReplace, []string{ + "-replace github.com/99designs/keyring => github.com/Jeffail/keyring v1.2.3", + }) + noDiffMatching(t, got, DirectiveRetract, []string{ + "-[ v1.0.0 , v1.9.9 ]", + }) + noDiffMatching(t, got, DirectiveRetract, []string{ + "-v0.9.0", + }) + hasDiffMatching(t, got, DirectiveTool, []string{ + "-tool golang.org/x/tools/cmd/bisect", + }) + hasDiffMatching(t, got, DirectiveTool, []string{ + "-tool golang.org/x/tools/cmd/stringer", + }) + hasDiffMatching(t, got, DirectiveIgnore, []string{ + "-./third_party/javascript", + }) + hasDiffMatching(t, got, DirectiveIgnore, []string{ + "-content/html", + }) + hasDiffMatching(t, got, DirectiveIgnore, []string{ + "-static", + }) + hasDiffMatching(t, got, DirectiveIgnore, []string{ + "-ignore ./node_modules", + }) + }, + }, + "lax parse lax diff": { + &DiffOpts{ + ParseLax: true, + Module: true, + Go: true, + Toolchain: true, + Godebug: true, + Require: true, + Exclude: true, + Replace: true, + Retract: true, + Tool: true, + Ignore: true, + StrictDiffRequire: false, + StrictDiffExclude: false, + StrictDiffReplace: false, + StrictDiffRetract: false, + }, + func(t *testing.T, got ModDiff) { + hasDiffMatching(t, got, DirectiveModule, []string{ + "-module github.com/hashicorp/vault/pipeline/golang/moda", + "+module github.com/hashicorp/vault/pipeline/golang/modb", + }) + hasDiffMatching(t, got, DirectiveGo, []string{ + "-go 1.25", + "+go 1.25.2", + }) + noDiffMatching(t, got, DirectiveToolchain, []string{ + "+toolchain go1.24", + }) + noDiffMatching(t, got, DirectiveGodebug, []string{ + "-default=go1.21", + }) + noDiffMatching(t, got, DirectiveGodebug, []string{ + "-httpcookiemaxnum=4000", + }) + noDiffMatching(t, got, DirectiveGodebug, []string{ + "-panicnil=1", + }) + hasDiffMatching(t, got, DirectiveRequire, []string{ + "-require github.com/99designs/keyring v1.2.2", + "+require github.com/99designs/keyring v0.0.0-00010101000000-000000000000", + }) + noDiffMatching(t, got, DirectiveRequire, []string{ + "-github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c", + }) + noDiffMatching(t, got, DirectiveExclude, []string{ + "-exclude golang.org/x/term v0.2.0", + }) + noDiffMatching(t, got, DirectiveReplace, []string{ + "-replace github.com/99designs/keyring => github.com/Jeffail/keyring v1.2.3", + }) + noDiffMatching(t, got, DirectiveRetract, []string{ + "-[ v1.0.0 , v1.9.9 ]", + }) + noDiffMatching(t, got, DirectiveRetract, []string{ + "-v0.9.0", + }) + noDiffMatching(t, got, DirectiveTool, []string{ + "-tool golang.org/x/tools/cmd/bisect", + }) + noDiffMatching(t, got, DirectiveTool, []string{ + "-tool golang.org/x/tools/cmd/stringer", + }) + noDiffMatching(t, got, DirectiveTool, []string{ + "-tool golang.org/x/tools/cmd/bisect", + }) + noDiffMatching(t, got, DirectiveTool, []string{ + "-tool golang.org/x/tools/cmd/stringer", + }) + hasDiffMatching(t, got, DirectiveIgnore, []string{ + "-./third_party/javascript", + }) + hasDiffMatching(t, got, DirectiveIgnore, []string{ + "-content/html", + }) + hasDiffMatching(t, got, DirectiveIgnore, []string{ + "-static", + }) + hasDiffMatching(t, got, DirectiveIgnore, []string{ + "-ignore ./node_modules", + }) + }, + }, + "strict parse strict diff": { + &DiffOpts{ + ParseLax: false, + Module: true, + Go: true, + Toolchain: true, + Godebug: true, + Require: true, + Exclude: true, + Replace: true, + Retract: true, + Tool: true, + Ignore: true, + StrictDiffRequire: true, + StrictDiffExclude: true, + StrictDiffReplace: true, + StrictDiffRetract: true, + }, + func(t *testing.T, got ModDiff) { + hasDiffMatching(t, got, DirectiveModule, []string{ + "-module github.com/hashicorp/vault/pipeline/golang/moda", + "+module github.com/hashicorp/vault/pipeline/golang/modb", + }) + hasDiffMatching(t, got, DirectiveGo, []string{ + "-go 1.25", + "+go 1.25.2", + }) + hasDiffMatching(t, got, DirectiveToolchain, []string{ + "+toolchain go1.24", + }) + hasDiffMatching(t, got, DirectiveGodebug, []string{ + "-default=go1.21", + }) + hasDiffMatching(t, got, DirectiveGodebug, []string{ + "-httpcookiemaxnum=4000", + }) + hasDiffMatching(t, got, DirectiveGodebug, []string{ + "-panicnil=1", + }) + hasDiffMatching(t, got, DirectiveRequire, []string{ + "-require github.com/99designs/keyring v1.2.2", + "+require github.com/99designs/keyring v0.0.0-00010101000000-000000000000", + }) + hasDiffMatching(t, got, DirectiveRequire, []string{ + "-github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c", + }) + hasDiffMatching(t, got, DirectiveExclude, []string{ + "-exclude golang.org/x/term v0.2.0", + }) + hasDiffMatching(t, got, DirectiveReplace, []string{ + "-replace github.com/99designs/keyring => github.com/Jeffail/keyring v1.2.3", + }) + hasDiffMatching(t, got, DirectiveRetract, []string{ + "-[ v1.0.0 , v1.9.9 ]", + }) + hasDiffMatching(t, got, DirectiveRetract, []string{ + "-v0.9.0", + }) + hasDiffMatching(t, got, DirectiveTool, []string{ + "-tool golang.org/x/tools/cmd/bisect", + }) + hasDiffMatching(t, got, DirectiveTool, []string{ + "-tool golang.org/x/tools/cmd/stringer", + }) + hasDiffMatching(t, got, DirectiveIgnore, []string{ + "-./third_party/javascript", + }) + hasDiffMatching(t, got, DirectiveIgnore, []string{ + "-content/html", + }) + hasDiffMatching(t, got, DirectiveIgnore, []string{ + "-static", + }) + hasDiffMatching(t, got, DirectiveIgnore, []string{ + "-ignore ./node_modules", + }) + }, + }, + "lax parse strict diff": { + &DiffOpts{ + ParseLax: true, + Module: true, + Go: true, + Toolchain: true, + Godebug: true, + Require: true, + Exclude: true, + Replace: true, + Retract: true, + Tool: true, + Ignore: true, + StrictDiffRequire: true, + StrictDiffExclude: true, + StrictDiffReplace: true, + StrictDiffRetract: true, + }, + func(t *testing.T, got ModDiff) { + hasDiffMatching(t, got, DirectiveModule, []string{ + "-module github.com/hashicorp/vault/pipeline/golang/moda", + "+module github.com/hashicorp/vault/pipeline/golang/modb", + }) + hasDiffMatching(t, got, DirectiveGo, []string{ + "-go 1.25", + "+go 1.25.2", + }) + noDiffMatching(t, got, DirectiveToolchain, []string{ + "+toolchain go1.24", + }) + noDiffMatching(t, got, DirectiveGodebug, []string{ + "-default=go1.21", + }) + noDiffMatching(t, got, DirectiveGodebug, []string{ + "-httpcookiemaxnum=4000", + }) + noDiffMatching(t, got, DirectiveGodebug, []string{ + "-panicnil=1", + }) + hasDiffMatching(t, got, DirectiveRequire, []string{ + "-require github.com/99designs/keyring v1.2.2", + "+require github.com/99designs/keyring v0.0.0-00010101000000-000000000000", + }) + hasDiffMatching(t, got, DirectiveRequire, []string{ + "-github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c", + }) + noDiffMatching(t, got, DirectiveExclude, []string{ + "-exclude golang.org/x/term v0.2.0", + }) + noDiffMatching(t, got, DirectiveReplace, []string{ + "-replace github.com/99designs/keyring => github.com/Jeffail/keyring v1.2.3", + }) + hasDiffMatching(t, got, DirectiveRetract, []string{ + "-[ v1.0.0 , v1.9.9 ]", + }) + hasDiffMatching(t, got, DirectiveRetract, []string{ + "-v0.9.0", + }) + noDiffMatching(t, got, DirectiveTool, []string{ + "-tool golang.org/x/tools/cmd/bisect", + }) + noDiffMatching(t, got, DirectiveTool, []string{ + "-tool golang.org/x/tools/cmd/stringer", + }) + hasDiffMatching(t, got, DirectiveIgnore, []string{ + "-content/html", + }) + hasDiffMatching(t, got, DirectiveIgnore, []string{ + "-static", + }) + hasDiffMatching(t, got, DirectiveIgnore, []string{ + "-ignore ./node_modules", + }) + }, + }, + } { + t.Run(desc, func(t *testing.T) { + t.Parallel() + diff, err := DiffModFiles(as, bs, test.opts) + require.NoError(t, err) + require.NotNil(t, diff, "expected a module diff") + test.condition(t, diff) + }) + } +} + +func hasDiffMatching(t *testing.T, diff ModDiff, dir Directive, matches []string) { + t.Helper() + + require.NotNil(t, diff) + diffs := getDiffsForDirective(dir, diff) + require.True(t, len(diffs) > 0, "expected %s matching %v, got diff:\n%s", + dir, + matches, + printModDiff(diff), + ) + for _, df := range diffs { + if unifiedTextMatches(df, matches) { + return + } + } + t.Fatalf("expected %s diff matching %v, got diff:\n%s", + dir, + matches, + printModDiff(diff), + ) +} + +func noDiffMatching(t *testing.T, diff ModDiff, dir Directive, matches []string) { + t.Helper() + + require.NotNil(t, diff) + diffs := getDiffsForDirective(dir, diff) + if len(diffs) < 1 { + return + } + for _, df := range diffs { + if unifiedTextMatches(df, matches) { + t.Fatalf("expected no %s diff matching %v, got diff:\n%s", + dir, + matches, + printModDiff(diff), + ) + } + } +} + +func printModDiff(d ModDiff) string { + if d == nil { + return "" + } + + b := strings.Builder{} + for _, diff := range d { + if exp := diff.Explanation(); exp != "" { + b.WriteString(exp + "\n") + } + if dt := diff.UnifiedText(); dt != "" { + b.WriteString(dt + "\n") + } + } + + return b.String() +} + +func getDiffsForDirective(dir Directive, diff ModDiff) []*Diff { + if len(diff) < 1 { + return nil + } + + diffs := []*Diff{} + for _, d := range diff { + if d == nil { + continue + } + if d.Directive == dir { + diffs = append(diffs, d) + } + } + + return diffs +} + +func unifiedTextMatches(diff *Diff, matches []string) bool { + if diff == nil && len(matches) == 0 { + return true + } + + if diff == nil && len(matches) > 0 { + return false + } + + txt := diff.UnifiedText() + if txt == "" { + return false + } + for _, m := range matches { + if !strings.Contains(txt, m) { + return false + } + } + + return true +}