Skip to content

Commit

Permalink
[crosslink] add --skip flag (#480)
Browse files Browse the repository at this point in the history
This allows modules in monorepos to opt out of crosslink. This will be used in mdatagen in the Collector core repo for example, where mdatagen has dependencies on internal collector modules, but we don't want to use replace statements as it breaks the ability to `go install` mdatagen

---------

Signed-off-by: Alex Boten <aboten@lightstep.com>
  • Loading branch information
Alex Boten authored Jan 17, 2024
1 parent c2a5fde commit 6a6b657
Show file tree
Hide file tree
Showing 10 changed files with 185 additions and 1 deletion.
16 changes: 16 additions & 0 deletions .chloggen/codeboten_crosslink-skip.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix'
change_type: enhancement

# The name of the component, or a single word describing the area of concern, (e.g. crosslink)
component: crosslink

# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`).
note: "Add `--skip` flag to ignore specified go modules"

# One or more tracking issues related to the change
issues: [480]

# (Optional) One or more lines of additional information to render under the primary note.
# These lines will be padded with 2 spaces and then inserted directly into the document.
# Use pipe (|) for multiline entries.
subtext:
18 changes: 18 additions & 0 deletions crosslink/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,24 @@ Multiple calls to exclude can also be made
--exclude=example.com/foo/bar/modC \
--exclude=example.com/foo/bar/modJ,example.com/modZ

### --skip

Skip is a set of go.mod files that will not be changed by crosslink.
It is expected that a list of comma-separated values will be provided in one or
multiple calls to skip. These go.mod files will be left unchanged by
crosslink. An example where using this may be appropriate is to avoid
adding replace statements in a go module that is intended to be installed
via `go install`.

A single call to skip

crosslink --skip cmd/example/go.mod,cmd/example2/go.mod,

Multiple calls to exclude can also be made

crosslink --skip cmd/example/go.mod \
--skip cmd/example2/go.mod

### –-verbose / -v

Verbose enables crosslink to log all replace (destructive and non-destructive) and
Expand Down
4 changes: 4 additions & 0 deletions crosslink/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
type commandConfig struct {
runConfig cl.RunConfig
excludeFlags []string
skipFlags []string
rootCommand cobra.Command
pruneCommand cobra.Command
workCommand cobra.Command
Expand All @@ -43,6 +44,7 @@ func newCommandConfig() *commandConfig {

preRunSetup := func(cmd *cobra.Command, args []string) error {
c.runConfig.ExcludedPaths = transformExclude(c.excludeFlags)
c.runConfig.SkippedPaths = transformExclude(c.skipFlags)

if c.runConfig.RootPath == "" {
rp, err := repo.FindRoot()
Expand Down Expand Up @@ -139,6 +141,8 @@ func init() {
comCfg.rootCommand.PersistentFlags().BoolVarP(&comCfg.runConfig.Verbose, "verbose", "v", false, "verbose output")
comCfg.rootCommand.Flags().StringSliceVar(&comCfg.excludeFlags, "exclude", []string{}, "list of comma separated go modules that crosslink will ignore in operations."+
"multiple calls of --exclude can be made")
comCfg.rootCommand.Flags().StringSliceVar(&comCfg.skipFlags, "skip", []string{}, "list of comma separated go.mod files that will not be affected (changed) by crosslink."+
"multiple calls of --skip can be made")
comCfg.rootCommand.Flags().BoolVar(&comCfg.runConfig.Overwrite, "overwrite", false, "overwrite flag allows crosslink to make destructive (replacing or updating) actions to existing go.mod files")
comCfg.rootCommand.Flags().BoolVarP(&comCfg.runConfig.Prune, "prune", "p", false, "enables pruning operations on all go.mod files inside root repository")
comCfg.pruneCommand.Flags().StringSliceVar(&comCfg.excludeFlags, "exclude", []string{}, "list of comma separated go modules that crosslink will ignore in operations."+
Expand Down
2 changes: 1 addition & 1 deletion crosslink/cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ func TestPreRun(t *testing.T) {
err = testPreRun(&comCfg.rootCommand, nil)
assert.NoError(t, err, "Pre Run returned error")

if diff := cmp.Diff(test.expectedConfig, comCfg.runConfig, cmpopts.IgnoreFields(cl.RunConfig{}, "Logger", "ExcludedPaths")); diff != "" {
if diff := cmp.Diff(test.expectedConfig, comCfg.runConfig, cmpopts.IgnoreFields(cl.RunConfig{}, "Logger", "ExcludedPaths", "SkippedPaths")); diff != "" {
t.Errorf("TestCase: %s \n Config{} mismatch (-want +got):\n%s", test.testName, diff)
}
})
Expand Down
1 change: 1 addition & 0 deletions crosslink/internal/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ type RunConfig struct {
RootPath string
Verbose bool
ExcludedPaths map[string]struct{}
SkippedPaths map[string]struct{}
Overwrite bool
Prune bool
GoVersion string
Expand Down
117 changes: 117 additions & 0 deletions crosslink/internal/crosslink_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ import (
"fmt"
"os"
"path/filepath"
"strings"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"golang.org/x/mod/modfile"
)
Expand Down Expand Up @@ -441,3 +443,118 @@ func TestBadRootPath(t *testing.T) {
}

}

// Testing skipping specified go modules.
func TestSkip(t *testing.T) {
testName := "testSkip"
lg, _ := zap.NewDevelopment()
tests := []struct {
testCase string
config RunConfig
}{
{
testCase: "No skipped go.mod",
config: RunConfig{
Prune: true,
Verbose: true,
Logger: lg,
},
},
{
testCase: "Include skipped go.mod",
config: RunConfig{
Prune: true,
SkippedPaths: map[string]struct{}{
"testA/go.mod": {},
},
Logger: lg,
Verbose: true,
},
},
{
testCase: "Include non-existent go.mod",
config: RunConfig{
Prune: true,
SkippedPaths: map[string]struct{}{
"non-existent/go.mod": {},
},
Logger: lg,
Verbose: true,
},
},
}

for _, test := range tests {
t.Run(test.testCase, func(t *testing.T) {
tmpRootDir, err := createTempTestDir(testName)
if err != nil {
t.Fatal("creating temp dir:", err)
}
t.Cleanup(func() { os.RemoveAll(tmpRootDir) })

err = renameGoMod(tmpRootDir)
if err != nil {
t.Errorf("error renaming gomod files: %v", err)
}

test.config.RootPath = tmpRootDir

err = Crosslink(test.config)
require.NoError(t, err, "error message on execution %s")

modFilesExpected := map[string][]byte{
filepath.Join(tmpRootDir, "go.mod"): []byte("module go.opentelemetry.io/build-tools/crosslink/testroot\n\n" +
"go 1.20\n\n" +
"require (\n\t" +
"go.opentelemetry.io/build-tools/crosslink/testroot/testA v1.0.0\n" +
")\n" +
"replace go.opentelemetry.io/build-tools/crosslink/testroot/testA => ./testA\n\n" +
"replace go.opentelemetry.io/build-tools/excludeme => ../excludeme\n\n"),
filepath.Join(tmpRootDir, "testA", "go.mod"): []byte("module go.opentelemetry.io/build-tools/crosslink/testroot/testA\n\n" +
"go 1.20\n\n" +
"require (\n\t" +
"go.opentelemetry.io/build-tools/crosslink/testroot/testB v1.0.0\n" +
")\n" +
"replace go.opentelemetry.io/build-tools/crosslink/testroot/testB => ../testB"),
}

for modFilePath, modFilesExpected := range modFilesExpected {
shouldDiffer := false
for path := range test.config.SkippedPaths {
if strings.HasSuffix(modFilePath, path) {
shouldDiffer = true
}
}
modFileActual, err := os.ReadFile(filepath.Clean(modFilePath))

if err != nil {
t.Fatalf("TestCase: %s, error reading actual mod files: %v", test.testCase, err)
}

actual, err := modfile.Parse("go.mod", modFileActual, nil)
if err != nil {
t.Fatalf("error decoding original mod files: %v", err)
}
actual.Cleanup()

expected, err := modfile.Parse("go.mod", modFilesExpected, nil)
if err != nil {
t.Fatalf("TestCase: %s ,error decoding expected mod file: %v", test.testCase, err)
}
expected.Cleanup()

// replace structs need to be assorted to avoid flaky fails in test
replaceSortFunc := func(x, y *modfile.Replace) bool {
return x.Old.Path < y.Old.Path
}

if diff := cmp.Diff(expected, actual, cmpopts.IgnoreFields(modfile.Replace{}, "Syntax"),
cmpopts.IgnoreFields(modfile.File{}, "Require", "Exclude", "Retract", "Syntax"),
cmpopts.SortSlices(replaceSortFunc),
); diff != "" && shouldDiffer {
t.Errorf("TestCase: %s \n Replace{} mismatch (-want +got):\n%s", test.testCase, diff)
}
}
})
}
}
4 changes: 4 additions & 0 deletions crosslink/internal/graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ func buildDepedencyGraph(rc RunConfig, rootModulePath string) (map[string]*modul
moduleMap := make(map[string]*moduleInfo)

err := forGoModules(rc.Logger, rc.RootPath, func(path string) error {
if _, ok := rc.SkippedPaths[path]; ok {
rc.Logger.Debug("skipping", zap.String("path", path))
return nil
}
fullPath := filepath.Join(rc.RootPath, path)
modFile, err := os.ReadFile(filepath.Clean(fullPath))
if err != nil {
Expand Down
14 changes: 14 additions & 0 deletions crosslink/internal/mock_test_data/testSkip/gomod
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module go.opentelemetry.io/build-tools/crosslink/testroot

go 1.20

require go.opentelemetry.io/build-tools/crosslink/testroot/testA v1.0.0

// should not be replaced or overwritten
replace go.opentelemetry.io/build-tools/crosslink/testroot/testA => ./testA

// included in exclude slice and should be ignored
// replace go.opentelemetry.io/build-tools/crosslink/testroot/testB => ./testB"

// should not be pruned
replace go.opentelemetry.io/build-tools/excludeme => ../excludeme
7 changes: 7 additions & 0 deletions crosslink/internal/mock_test_data/testSkip/testA/gomod
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module go.opentelemetry.io/build-tools/crosslink/testroot/testA

go 1.20

require go.opentelemetry.io/build-tools/crosslink/testroot/testB v1.0.0

replace go.opentelemetry.io/build-tools/crosslink/testroot/testB => ../testB
3 changes: 3 additions & 0 deletions crosslink/internal/mock_test_data/testSkip/testB/gomod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module go.opentelemetry.io/build-tools/crosslink/testroot/testB

go 1.20

0 comments on commit 6a6b657

Please sign in to comment.