[VAULT-40043]: pipeline: add go diff mod command (#10188) (#10292)

* [VAULT-40043]: pipeline: add `go diff mod` command

Add a `pipeline go diff mod` command that is capable of comparing two
go.mod files at a directive level. We also support strict or lax
comparisons of several directives to flexible diff comparisons. This is
especially useful when you want to compare two go.mod files that have
some different dependencies (CE vs. Ent) but still want to compare
versions of like dependencies.

This command is not currently used in the pipeline but was useful in
developing the diff library that is used. Subsequent work will use the
library and be integrated into CI.



* review feedback



* one more comment fix



---------

Signed-off-by: Ryan Cragun <me@ryan.ec>
Co-authored-by: Ryan Cragun <me@ryan.ec>
This commit is contained in:
Vault Automation 2025-10-21 18:08:05 -04:00 committed by GitHub
parent 0c6c13dd38
commit 7a4d71f95a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 1952 additions and 21 deletions

View file

@ -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
)

View file

@ -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=

View file

@ -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
}

View file

@ -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
}

View file

@ -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 </path/a/go.mod> </path/b/go.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
}

View file

@ -32,6 +32,7 @@ func newRootCmd() *cobra.Command {
rootCmd.AddCommand(newGenerateCmd())
rootCmd.AddCommand(newGithubCmd())
rootCmd.AddCommand(newGoCmd())
rootCmd.AddCommand(newHCPCmd())
rootCmd.AddCommand(newReleasesCmd())

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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

View file

@ -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
)

View file

@ -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=

View file

@ -0,0 +1,8 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package main
import (
_ "github.com/99designs/keyring"
)

View file

@ -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

View file

@ -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=

View file

@ -0,0 +1,8 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package main
import (
_ "github.com/99designs/keyring"
)

View file

@ -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
}

View file

@ -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
}