]> Cypherpunks repositories - gostls13.git/commitdiff
cmd/go: add global ignore mechanism
authorSam Thanawalla <samthanawalla@google.com>
Wed, 8 Jan 2025 18:30:50 +0000 (18:30 +0000)
committerSam Thanawalla <samthanawalla@google.com>
Fri, 16 May 2025 15:39:57 +0000 (08:39 -0700)
This CL adds the ignore directive which enables users to tell the Go
Command to skip traversing into a given directory.

This behaves similar to how '_' or 'testdata' are currently treated.
This mainly has benefits for go list and go mod tidy.
This does not affect what is packed into a module.

Fixes: #42965
Change-Id: I232e27c1a065bb6eb2d210dbddad0208426a1fdd
Reviewed-on: https://go-review.googlesource.com/c/go/+/643355
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Michael Matloob <matloob@golang.org>
Reviewed-by: Michael Matloob <matloob@google.com>
13 files changed:
src/cmd/go/alldocs.go
src/cmd/go/internal/load/pkg.go
src/cmd/go/internal/modcmd/edit.go
src/cmd/go/internal/modload/modfile.go
src/cmd/go/internal/modload/search.go
src/cmd/go/internal/search/search.go
src/cmd/go/testdata/script/build_dash_x.txt
src/cmd/go/testdata/script/build_ignoredirective.txt [new file with mode: 0644]
src/cmd/go/testdata/script/list_ignore.txt [new file with mode: 0644]
src/cmd/go/testdata/script/list_ignore_dependency.txt [new file with mode: 0644]
src/cmd/go/testdata/script/list_ignore_workspace.txt [new file with mode: 0644]
src/cmd/go/testdata/script/mod_edit.txt
src/cmd/go/testdata/script/mod_tidy_ignore.txt [new file with mode: 0644]

index fe53486b4009491b68bb617262cc73fa9e5ee9e2..e8034bf5d17a2e5e5e25003ef64af84564514832 100644 (file)
 // The -tool=path and -droptool=path flags add and drop a tool declaration
 // for the given path.
 //
+// The -ignore=path and -dropignore=path flags add and drop a ignore declaration
+// for the given path.
+//
 // The -godebug, -dropgodebug, -require, -droprequire, -exclude, -dropexclude,
-// -replace, -dropreplace, -retract, -dropretract, -tool, and -droptool editing
-// flags may be repeated, and the changes are applied in the order given.
+// -replace, -dropreplace, -retract, -dropretract, -tool, -droptool, -ignore,
+// and -dropignore editing flags may be repeated, and the changes are applied
+// in the order given.
 //
 // The -print flag prints the final go.mod in its text format instead of
 // writing it back to go.mod.
 //             Path string
 //     }
 //
+//     type Ignore struct {
+//             Path string
+//     }
+//
 // Retract entries representing a single version (not an interval) will have
 // the "Low" and "High" fields set to the same value.
 //
index 934a97aba1c577c06bd5dd4ae792cb0f552521c3..e913f9885234fda9330bb6b7ead384cd39228b18 100644 (file)
@@ -2927,8 +2927,7 @@ func PackagesAndErrors(ctx context.Context, opts PackageOpts, patterns []string)
                }
                matches, _ = modload.LoadPackages(ctx, modOpts, patterns...)
        } else {
-               noModRoots := []string{}
-               matches = search.ImportPaths(patterns, noModRoots)
+               matches = search.ImportPaths(patterns)
        }
 
        var (
index ba3d6ed2997e974ae48716db4fc401433526028b..f73269378a11e161edadf060a125d545d02eaa7d 100644 (file)
@@ -90,9 +90,13 @@ like "v1.2.3" or a closed interval like "[v1.1.0,v1.1.9]". Note that
 The -tool=path and -droptool=path flags add and drop a tool declaration
 for the given path.
 
+The -ignore=path and -dropignore=path flags add and drop a ignore declaration
+for the given path.
+
 The -godebug, -dropgodebug, -require, -droprequire, -exclude, -dropexclude,
--replace, -dropreplace, -retract, -dropretract, -tool, and -droptool editing
-flags may be repeated, and the changes are applied in the order given.
+-replace, -dropreplace, -retract, -dropretract, -tool, -droptool, -ignore,
+and -dropignore editing flags may be repeated, and the changes are applied
+in the order given.
 
 The -print flag prints the final go.mod in its text format instead of
 writing it back to go.mod.
@@ -147,6 +151,10 @@ writing it back to go.mod. The JSON output corresponds to these Go types:
                Path string
        }
 
+       type Ignore struct {
+               Path string
+       }
+
 Retract entries representing a single version (not an interval) will have
 the "Low" and "High" fields set to the same value.
 
@@ -190,6 +198,8 @@ func init() {
        cmdEdit.Flag.Var(flagFunc(flagDropRetract), "dropretract", "")
        cmdEdit.Flag.Var(flagFunc(flagTool), "tool", "")
        cmdEdit.Flag.Var(flagFunc(flagDropTool), "droptool", "")
+       cmdEdit.Flag.Var(flagFunc(flagIgnore), "ignore", "")
+       cmdEdit.Flag.Var(flagFunc(flagDropIgnore), "dropignore", "")
 
        base.AddBuildFlagsNX(&cmdEdit.Flag)
        base.AddChdirFlag(&cmdEdit.Flag)
@@ -546,6 +556,24 @@ func flagDropTool(arg string) {
        })
 }
 
+// flagIgnore implements the -ignore flag.
+func flagIgnore(arg string) {
+       edits = append(edits, func(f *modfile.File) {
+               if err := f.AddIgnore(arg); err != nil {
+                       base.Fatalf("go: -ignore=%s: %v", arg, err)
+               }
+       })
+}
+
+// flagDropIgnore implements the -dropignore flag.
+func flagDropIgnore(arg string) {
+       edits = append(edits, func(f *modfile.File) {
+               if err := f.DropIgnore(arg); err != nil {
+                       base.Fatalf("go: -dropignore=%s: %v", arg, err)
+               }
+       })
+}
+
 // fileJSON is the -json output data structure.
 type fileJSON struct {
        Module    editModuleJSON
@@ -556,6 +584,7 @@ type fileJSON struct {
        Replace   []replaceJSON
        Retract   []retractJSON
        Tool      []toolJSON
+       Ignore    []ignoreJSON
 }
 
 type editModuleJSON struct {
@@ -584,6 +613,10 @@ type toolJSON struct {
        Path string
 }
 
+type ignoreJSON struct {
+       Path string
+}
+
 // editPrintJSON prints the -json output.
 func editPrintJSON(modFile *modfile.File) {
        var f fileJSON
@@ -614,6 +647,9 @@ func editPrintJSON(modFile *modfile.File) {
        for _, t := range modFile.Tool {
                f.Tool = append(f.Tool, toolJSON{t.Path})
        }
+       for _, i := range modFile.Ignore {
+               f.Ignore = append(f.Ignore, ignoreJSON{i.Path})
+       }
        data, err := json.MarshalIndent(&f, "", "\t")
        if err != nil {
                base.Fatalf("go: internal error: %v", err)
index 4687deae686c89b46a2cace590c3953dc00d7188..3b82b857c4bc080a27124f571681fd637f9daab6 100644 (file)
@@ -94,6 +94,7 @@ type modFileIndex struct {
        require      map[module.Version]requireMeta
        replace      map[module.Version]module.Version
        exclude      map[module.Version]bool
+       ignore       []string
 }
 
 type requireMeta struct {
@@ -455,7 +456,11 @@ func indexModFile(data []byte, modFile *modfile.File, mod module.Version, needsF
        for _, x := range modFile.Exclude {
                i.exclude[x.Mod] = true
        }
-
+       if modFile.Ignore != nil {
+               for _, x := range modFile.Ignore {
+                       i.ignore = append(i.ignore, x.Path)
+               }
+       }
        return i
 }
 
@@ -539,6 +544,7 @@ type modFileSummary struct {
        module     module.Version
        goVersion  string
        toolchain  string
+       ignore     []string
        pruning    modPruning
        require    []module.Version
        retract    []retraction
@@ -714,6 +720,11 @@ func rawGoModSummary(m module.Version) (*modFileSummary, error) {
                if f.Toolchain != nil {
                        summary.toolchain = f.Toolchain.Name
                }
+               if f.Ignore != nil {
+                       for _, i := range f.Ignore {
+                               summary.ignore = append(summary.ignore, i.Path)
+                       }
+               }
                if len(f.Require) > 0 {
                        summary.require = make([]module.Version, 0, len(f.Require)+1)
                        for _, req := range f.Require {
index 6c60101c8b46b715349a29060135f89e8ed7f294..c3e54d62b6dbf0861d0ceaf844d0415926686013 100644 (file)
@@ -74,7 +74,7 @@ func matchPackages(ctx context.Context, m *search.Match, tags map[string]bool, f
        )
 
        q := par.NewQueue(runtime.GOMAXPROCS(0))
-
+       ignorePatternsMap := parseIgnorePatterns(ctx, treeCanMatch, modules)
        walkPkgs := func(root, importPathRoot string, prune pruning) {
                _, span := trace.StartSpan(ctx, "walkPkgs "+root)
                defer span.Done()
@@ -82,7 +82,8 @@ func matchPackages(ctx context.Context, m *search.Match, tags map[string]bool, f
                // If the root itself is a symlink to a directory,
                // we want to follow it (see https://go.dev/issue/50807).
                // Add a trailing separator to force that to happen.
-               root = str.WithFilePathSeparator(filepath.Clean(root))
+               cleanRoot := filepath.Clean(root)
+               root = str.WithFilePathSeparator(cleanRoot)
                err := fsys.WalkDir(root, func(pkgDir string, d fs.DirEntry, err error) error {
                        if err != nil {
                                m.AddError(err)
@@ -91,6 +92,7 @@ func matchPackages(ctx context.Context, m *search.Match, tags map[string]bool, f
 
                        want := true
                        elem := ""
+                       relPkgDir := filepath.ToSlash(pkgDir[len(root):])
 
                        // Don't use GOROOT/src but do walk down into it.
                        if pkgDir == root {
@@ -102,10 +104,15 @@ func matchPackages(ctx context.Context, m *search.Match, tags map[string]bool, f
                                _, elem = filepath.Split(pkgDir)
                                if strings.HasPrefix(elem, ".") || strings.HasPrefix(elem, "_") || elem == "testdata" {
                                        want = false
+                               } else if ignorePatternsMap[cleanRoot] != nil && ignorePatternsMap[cleanRoot].ShouldIgnore(relPkgDir) {
+                                       if cfg.BuildX {
+                                               fmt.Fprintf(os.Stderr, "# ignoring directory %s\n", pkgDir)
+                                       }
+                                       want = false
                                }
                        }
 
-                       name := path.Join(importPathRoot, filepath.ToSlash(pkgDir[len(root):]))
+                       name := path.Join(importPathRoot, relPkgDir)
                        if !treeCanMatch(name) {
                                want = false
                        }
@@ -303,3 +310,43 @@ func MatchInModule(ctx context.Context, pattern string, m module.Version, tags m
        }
        return match
 }
+
+// parseIgnorePatterns collects all ignore patterns associated with the
+// provided list of modules.
+// It returns a map of module root -> *search.IgnorePatterns.
+func parseIgnorePatterns(ctx context.Context, treeCanMatch func(string) bool, modules []module.Version) map[string]*search.IgnorePatterns {
+       ignorePatternsMap := make(map[string]*search.IgnorePatterns)
+       for _, mod := range modules {
+               if gover.IsToolchain(mod.Path) || !treeCanMatch(mod.Path) {
+                       continue
+               }
+               var modRoot string
+               var ignorePatterns []string
+               if MainModules.Contains(mod.Path) {
+                       modRoot = MainModules.ModRoot(mod)
+                       if modRoot == "" {
+                               continue
+                       }
+                       modIndex := MainModules.Index(mod)
+                       if modIndex == nil {
+                               continue
+                       }
+                       ignorePatterns = modIndex.ignore
+               } else if cfg.BuildMod != "vendor" {
+                       // Skip getting ignore patterns for vendored modules because they
+                       // do not have go.mod files.
+                       var err error
+                       modRoot, _, err = fetch(ctx, mod)
+                       if err != nil {
+                               continue
+                       }
+                       summary, err := goModSummary(mod)
+                       if err != nil {
+                               continue
+                       }
+                       ignorePatterns = summary.ignore
+               }
+               ignorePatternsMap[modRoot] = search.NewIgnorePatterns(ignorePatterns)
+       }
+       return ignorePatternsMap
+}
index 0954b82a028304590968e190dde65ab512b46b07..a54486e540ca16f7f97040e21974197163ac66ec 100644 (file)
@@ -17,6 +17,8 @@ import (
        "path"
        "path/filepath"
        "strings"
+
+       "golang.org/x/mod/modfile"
 )
 
 // A Match represents the result of matching a single package pattern.
@@ -208,6 +210,69 @@ func (m *Match) MatchPackages() {
        }
 }
 
+// IgnorePatterns is normalized with normalizePath.
+type IgnorePatterns struct {
+       relativePatterns []string
+       anyPatterns      []string
+}
+
+// ShouldIgnore returns true if the given directory should be ignored
+// based on the ignore patterns.
+//
+// An ignore pattern "x" will cause any file or directory named "x"
+// (and its entire subtree) to be ignored, regardless of its location
+// within the module.
+//
+// An ignore pattern "./x" will only cause the specific file or directory
+// named "x" at the root of the module to be ignored.
+// Wildcards in ignore patterns are not supported.
+func (ignorePatterns *IgnorePatterns) ShouldIgnore(dir string) bool {
+       if dir == "" {
+               return false
+       }
+       dir = normalizePath(dir)
+       for _, pattern := range ignorePatterns.relativePatterns {
+               if strings.HasPrefix(dir, pattern) {
+                       return true
+               }
+       }
+       for _, pattern := range ignorePatterns.anyPatterns {
+               if strings.Contains(dir, pattern) {
+                       return true
+               }
+       }
+       return false
+}
+
+func NewIgnorePatterns(patterns []string) *IgnorePatterns {
+       var relativePatterns, anyPatterns []string
+       for _, pattern := range patterns {
+               ignorePatternPath, isRelative := strings.CutPrefix(pattern, "./")
+               ignorePatternPath = normalizePath(ignorePatternPath)
+               if isRelative {
+                       relativePatterns = append(relativePatterns, ignorePatternPath)
+               } else {
+                       anyPatterns = append(anyPatterns, ignorePatternPath)
+               }
+       }
+       return &IgnorePatterns{
+               relativePatterns: relativePatterns,
+               anyPatterns:      anyPatterns,
+       }
+}
+
+// normalizePath adds slashes to the front and end of the given path.
+func normalizePath(path string) string {
+       path = filepath.ToSlash(path)
+       if !strings.HasPrefix(path, "/") {
+               path = "/" + path
+       }
+       if !strings.HasSuffix(path, "/") {
+               path += "/"
+       }
+       return path
+}
+
 // MatchDirs sets m.Dirs to a non-nil slice containing all directories that
 // potentially match a local pattern. The pattern must begin with an absolute
 // path, or "./", or "../". On Windows, the pattern may use slash or backslash
@@ -253,16 +318,18 @@ func (m *Match) MatchDirs(modRoots []string) {
        // We need to preserve the ./ for pattern matching
        // and in the returned import paths.
 
-       if len(modRoots) > 1 {
+       var modRoot string
+       if len(modRoots) > 0 {
                abs, err := filepath.Abs(dir)
                if err != nil {
                        m.AddError(err)
                        return
                }
                var found bool
-               for _, modRoot := range modRoots {
-                       if modRoot != "" && str.HasFilePathPrefix(abs, modRoot) {
+               for _, mr := range modRoots {
+                       if mr != "" && str.HasFilePathPrefix(abs, mr) {
                                found = true
+                               modRoot = mr
                        }
                }
                if !found {
@@ -274,6 +341,7 @@ func (m *Match) MatchDirs(modRoots []string) {
                }
        }
 
+       ignorePatterns := parseIgnorePatterns(modRoot)
        // If dir is actually a symlink to a directory,
        // we want to follow it (see https://go.dev/issue/50807).
        // Add a trailing separator to force that to happen.
@@ -305,6 +373,17 @@ func (m *Match) MatchDirs(modRoots []string) {
                if dot || strings.HasPrefix(elem, "_") || elem == "testdata" {
                        return filepath.SkipDir
                }
+               absPath, err := filepath.Abs(path)
+               if err != nil {
+                       return err
+               }
+
+               if ignorePatterns != nil && ignorePatterns.ShouldIgnore(InDir(absPath, modRoot)) {
+                       if cfg.BuildX {
+                               fmt.Fprintf(os.Stderr, "# ignoring directory %s\n", absPath)
+                       }
+                       return filepath.SkipDir
+               }
 
                if !top && cfg.ModulesEnabled {
                        // Ignore other modules found in subdirectories.
@@ -353,20 +432,20 @@ func WarnUnmatched(matches []*Match) {
 
 // ImportPaths returns the matching paths to use for the given command line.
 // It calls ImportPathsQuiet and then WarnUnmatched.
-func ImportPaths(patterns, modRoots []string) []*Match {
-       matches := ImportPathsQuiet(patterns, modRoots)
+func ImportPaths(patterns []string) []*Match {
+       matches := ImportPathsQuiet(patterns)
        WarnUnmatched(matches)
        return matches
 }
 
 // ImportPathsQuiet is like ImportPaths but does not warn about patterns with no matches.
-func ImportPathsQuiet(patterns, modRoots []string) []*Match {
+func ImportPathsQuiet(patterns []string) []*Match {
        patterns = CleanPatterns(patterns)
        out := make([]*Match, 0, len(patterns))
        for _, a := range patterns {
                m := NewMatch(a)
                if m.IsLocal() {
-                       m.MatchDirs(modRoots)
+                       m.MatchDirs(nil)
 
                        // Change the file import path to a regular import path if the package
                        // is in GOPATH or GOROOT. We don't report errors here; LoadImport
@@ -509,3 +588,25 @@ func InDir(path, dir string) string {
        }
        return ""
 }
+
+// parseIgnorePatterns reads the go.mod file at the given module root
+// and extracts the ignore patterns defined within it.
+// If modRoot is empty, it returns nil.
+func parseIgnorePatterns(modRoot string) *IgnorePatterns {
+       if modRoot == "" {
+               return nil
+       }
+       data, err := os.ReadFile(filepath.Join(modRoot, "go.mod"))
+       if err != nil {
+               return nil
+       }
+       modFile, err := modfile.Parse("go.mod", data, nil)
+       if err != nil {
+               return nil
+       }
+       var patterns []string
+       for _, i := range modFile.Ignore {
+               patterns = append(patterns, i.Path)
+       }
+       return NewIgnorePatterns(patterns)
+}
index e5580a2cc69ed11320243d3d5cdea9d50768a081..c7a2c6fe671ca72bdeca1c20c877e9857ef3a15a 100644 (file)
@@ -19,7 +19,7 @@ env GOCACHE=$WORK/tmp/cache
 # loading to properly affect the import graph.)
 go build runtime/cgo
 
-go build -x -o main main.go
+go build -x -o main ./...
 cp stderr commands.txt
 cat header.txt commands.txt
 cp stdout test.sh
@@ -42,6 +42,13 @@ import "C"
 func main() {
        print("hello\n")
 }
+-- go.mod --
+module example
+
+go 1.24
+
+ignore foo
+-- foo/foo.txt --
 -- header.txt --
 set -e
 -- hello.txt --
diff --git a/src/cmd/go/testdata/script/build_ignoredirective.txt b/src/cmd/go/testdata/script/build_ignoredirective.txt
new file mode 100644 (file)
index 0000000..d3da9d6
--- /dev/null
@@ -0,0 +1,147 @@
+# go build ./... should skip 'ignore' directives
+# See golang.org/issue/42965
+
+env ROOT=$WORK${/}gopath${/}src
+
+# no ignore directive; should not skip any directories.
+cp go.mod.orig go.mod
+go build -x ./...
+stderr 'packagefile example/foo/secret'
+stderr 'packagefile example/pkg/foo'
+stderr 'packagefile example/pkg/fo'
+! stderr 'ignoring directory'
+
+# ignored ./foo should be skipped.
+cp go.mod.relative go.mod
+go build -x ./...
+stderr 'packagefile example/pkg/foo'
+stderr 'packagefile example/pkg/fo'
+! stderr 'packagefile example/foo/secret'
+stderr 'ignoring directory '$ROOT''${/}'foo'
+! stderr 'ignoring directory '$ROOT''${/}'pkg'${/}'foo'
+
+# ignored foo; any foo should be skipped.
+cp go.mod.any go.mod
+go build -x ./...
+stderr 'packagefile example/pkg/fo'
+! stderr 'packagefile example/pkg/foo'
+! stderr 'packagefile example/foo/secret'
+stderr 'ignoring directory '$ROOT''${/}'foo'
+stderr 'ignoring directory '$ROOT''${/}'pkg'${/}'foo'
+
+# non-existent ignore; should not skip any directories.
+cp go.mod.dne go.mod
+go build -x ./...
+stderr 'packagefile example/foo/secret'
+stderr 'packagefile example/pkg/foo'
+stderr 'packagefile example/pkg/fo'
+! stderr 'ignoring directory'
+
+# ignored fo; should not skip foo/ and should skip fo/
+cp go.mod.partial go.mod
+go build -x ./...
+! stderr 'ignoring directory '$ROOT''${/}'foo'
+stderr 'ignoring directory '$ROOT''${/}'pkg'${/}'fo$'
+! stderr 'ignoring directory '$ROOT''${/}'pkg'${/}'foo'
+
+# ignored pkg/foo; should skip pkg/foo/
+cp go.mod.tree go.mod
+go build -x ./...
+stderr 'packagefile example/foo/secret'
+stderr 'packagefile example/pkg/fo'
+stderr 'ignoring directory '$ROOT''${/}'pkg'${/}'foo'
+
+# ignored /pkg/foo/; should skip pkg/foo/
+cp go.mod.sep1 go.mod
+go build -x ./...
+stderr 'packagefile example/foo/secret'
+stderr 'packagefile example/pkg/fo'
+stderr 'ignoring directory '$ROOT''${/}'pkg'${/}'foo'
+
+# ignored pkg/foo/; should skip pkg/foo/
+cp go.mod.sep2 go.mod
+go build -x ./...
+stderr 'packagefile example/foo/secret'
+stderr 'packagefile example/pkg/fo'
+stderr 'ignoring directory '$ROOT''${/}'pkg'${/}'foo'
+
+# ignored /pkg/foo; should skip pkg/foo/
+cp go.mod.sep3 go.mod
+go build -x ./...
+stderr 'packagefile example/foo/secret'
+stderr 'packagefile example/pkg/fo'
+stderr 'ignoring directory '$ROOT''${/}'pkg'${/}'foo'
+
+-- foo/secret/secret.go --
+package main
+func main() {}
+-- pkg/foo/foo.go --
+package main
+func main() {}
+-- pkg/fo/fo.go --
+package main
+func main() {}
+-- go.mod.orig --
+module example
+
+go 1.24
+
+-- go.mod.relative --
+module example
+
+go 1.24
+
+ignore ./foo
+
+-- go.mod.any --
+module example
+
+go 1.24
+
+ignore foo
+
+-- go.mod.dne --
+module example
+
+go 1.24
+
+ignore bar
+
+-- go.mod.partial --
+module example
+
+go 1.24
+
+ignore fo
+
+-- go.mod.tree --
+module example
+
+go 1.24
+
+ignore pkg/foo
+
+-- go.mod.sep1 --
+module example
+
+go 1.24
+
+ignore /pkg/foo/
+
+-- go.mod.sep2 --
+module example
+
+go 1.24
+
+ignore pkg/foo/
+
+-- go.mod.sep3 --
+module example
+
+go 1.24
+
+ignore /pkg/foo
+
+-- main.go --
+package main
+func main() {}
diff --git a/src/cmd/go/testdata/script/list_ignore.txt b/src/cmd/go/testdata/script/list_ignore.txt
new file mode 100644 (file)
index 0000000..2ba25ca
--- /dev/null
@@ -0,0 +1,151 @@
+# go list should skip 'ignore' directives
+# See golang.org/issue/42965
+
+env ROOT=$WORK${/}gopath${/}src
+
+# no ignore directive; should not skip any directories.
+cp go.mod.orig go.mod
+go list -x ./...
+stdout 'example/foo/secret'
+stdout 'example/pkg/foo'
+stdout 'example/pkg/fo$'
+! stderr 'ignoring directory'
+
+# ignored ./foo should be skipped.
+cp go.mod.relative go.mod
+go list -x ./...
+stdout 'example/pkg/foo'
+stdout 'example/pkg/fo$'
+! stdout 'example/foo/secret'
+stderr 'ignoring directory '$ROOT''${/}'foo'
+! stderr 'ignoring directory '$ROOT''${/}'pkg'${/}'foo'
+
+# ignored foo; any foo should be skipped.
+cp go.mod.any go.mod
+go list -x ./...
+stdout 'example/pkg/fo$'
+! stdout 'example/pkg/foo'
+! stdout 'example/foo/secret'
+stderr 'ignoring directory '$ROOT''${/}'foo'
+stderr 'ignoring directory '$ROOT''${/}'pkg'${/}'foo'
+
+# non-existent ignore; should not skip any directories.
+cp go.mod.dne go.mod
+go list -x ./...
+stdout 'example/foo/secret'
+stdout 'example/pkg/foo'
+stdout 'example/pkg/fo$'
+! stderr 'ignoring directory'
+
+# ignored fo; should not skip foo/ and should skip fo/
+cp go.mod.partial go.mod
+go list -x ./...
+! stderr 'ignoring directory '$ROOT''${/}'foo'
+stderr 'ignoring directory '$ROOT''${/}'pkg'${/}'fo$'
+! stderr 'ignoring directory '$ROOT''${/}'pkg'${/}'foo'
+
+# ignored pkg/foo; should skip pkg/foo/
+cp go.mod.tree go.mod
+go list -x ./...
+stdout 'example/foo/secret'
+stdout 'example/pkg/fo$'
+stderr 'ignoring directory '$ROOT''${/}'pkg'${/}'foo'
+
+# ignored /pkg/foo/; should skip pkg/foo/
+cp go.mod.sep1 go.mod
+go list -x ./...
+stdout 'example/foo/secret'
+stdout 'example/pkg/fo$'
+stderr 'ignoring directory '$ROOT''${/}'pkg'${/}'foo'
+
+# ignored pkg/foo/; should skip pkg/foo/
+cp go.mod.sep2 go.mod
+go list -x ./...
+stdout 'example/foo/secret'
+stdout 'example/pkg/fo$'
+stderr 'ignoring directory '$ROOT''${/}'pkg'${/}'foo'
+
+# ignored /pkg/foo; should skip pkg/foo/
+cp go.mod.sep3 go.mod
+go list -x ./...
+stdout 'example/foo/secret'
+stdout 'example/pkg/fo$'
+stderr 'ignoring directory '$ROOT''${/}'pkg'${/}'foo'
+
+-- foo/secret/secret.go --
+package secret
+
+const Secret = "this should be ignored"
+-- pkg/foo/foo.go --
+package foo
+
+const Bar = "Hello from foo!"
+-- pkg/fo/fo.go --
+package fo
+
+const Gar = "Hello from fo!"
+-- go.mod.orig --
+module example
+
+go 1.24
+
+-- go.mod.relative --
+module example
+
+go 1.24
+
+ignore ./foo
+
+-- go.mod.any --
+module example
+
+go 1.24
+
+ignore foo
+
+-- go.mod.dne --
+module example
+
+go 1.24
+
+ignore bar
+
+-- go.mod.partial --
+module example
+
+go 1.24
+
+ignore fo
+
+-- go.mod.tree --
+module example
+
+go 1.24
+
+ignore pkg/foo
+
+-- go.mod.sep1 --
+module example
+
+go 1.24
+
+ignore /pkg/foo/
+
+-- go.mod.sep2 --
+module example
+
+go 1.24
+
+ignore pkg/foo/
+
+-- go.mod.sep3 --
+module example
+
+go 1.24
+
+ignore /pkg/foo
+
+-- main.go --
+package main
+
+func main() {}
diff --git a/src/cmd/go/testdata/script/list_ignore_dependency.txt b/src/cmd/go/testdata/script/list_ignore_dependency.txt
new file mode 100644 (file)
index 0000000..cafa284
--- /dev/null
@@ -0,0 +1,76 @@
+# go list should skip 'ignore' directives with respect to module boundaries.
+# See golang.org/issue/42965
+
+env ROOT=$WORK${/}gopath${/}src
+
+# Lists all packages known to the Go toolchain.
+# Since go list already does not traverse into other modules found in
+# subdirectories, it should only ignore the root node_modules.
+go list -x all
+stdout 'example$'
+stdout 'example/depA'
+stderr 'ignoring directory '$ROOT''${/}'node_modules'
+! stderr 'ignoring directory '$ROOT''${/}'depA'${/}'node_modules'
+
+# Lists all packages within the current Go module.
+# Since go list already does not traverse into other modules found in
+# subdirectories, it should only ignore the root node_modules.
+go list -x ./...
+stdout 'example$'
+stderr 'ignoring directory '$ROOT''${/}'node_modules'
+! stderr 'ignoring directory '$ROOT''${/}'depA'${/}'node_modules'
+
+# Lists all packages belonging to the module whose import path starts with
+# example.
+# In this case, go list will traverse into each module that starts with example.
+# So it should ignore the root node_modules and the subdirectories' node_modules.
+go list -x example/...
+stdout 'example$'
+stdout 'example/depA'
+stderr 'ignoring directory '$ROOT''${/}'node_modules'
+stderr 'ignoring directory '$ROOT''${/}'depA'${/}'node_modules'
+
+# Entering the submodule should now cause go list to ignore depA/node_modules.
+cd depA
+go list -x all
+stdout 'example/depA'
+stderr 'ignoring directory '$ROOT''${/}'depA'${/}'node_modules'
+! stderr 'ignoring directory '$ROOT''${/}'node_modules'
+
+go list -x ./...
+stdout 'example/depA'
+stderr 'ignoring directory '$ROOT''${/}'depA'${/}'node_modules'
+! stderr 'ignoring directory '$ROOT''${/}'node_modules'
+
+-- depA/go.mod --
+module example/depA
+
+go 1.24
+ignore ./node_modules
+-- depA/depA.go --
+package depA
+
+const Foo = "This is Foo!"
+-- depA/node_modules/some_pkg/index.js --
+console.log("This should be ignored!");
+-- node_modules/some_pkg/index.js --
+console.log("This should be ignored!");
+-- go.mod --
+module example
+
+go 1.24
+
+ignore ./node_modules
+require example/depA v1.0.0
+replace example/depA => ./depA
+
+-- main.go --
+package main
+import (
+        "fmt"
+        "example/depA"
+)
+func main() {
+        fmt.Println("test")
+        fmt.Println(depA.Foo)
+}
diff --git a/src/cmd/go/testdata/script/list_ignore_workspace.txt b/src/cmd/go/testdata/script/list_ignore_workspace.txt
new file mode 100644 (file)
index 0000000..609e976
--- /dev/null
@@ -0,0 +1,89 @@
+# go list should skip 'ignore' directives in workspaces
+# See golang.org/issue/42965
+
+env ROOT=$WORK${/}gopath${/}src
+
+# go list ./... should only consider the current module's ignore directive
+cd moduleA
+go list -x ./...
+stdout 'moduleA$'
+stdout 'moduleA/pkg$'
+stderr 'ignoring directory '$ROOT''${/}'moduleA'${/}'node_modules'
+
+# go list ./... should only consider the current module's ignore directive
+cd ../moduleB
+go list -x ./...
+stdout 'moduleB$'
+! stdout 'moduleB/pkg/helper'
+stderr 'ignoring directory '$ROOT''${/}'moduleB'${/}'pkg'
+
+# go list should respect module boundaries for ignore directives.
+# moduleA ignores './node_modules', moduleB ignores 'pkg'
+cd ..
+go list -x all
+stderr 'ignoring directory '$ROOT''${/}'moduleA'${/}'node_modules'
+stderr 'ignoring directory '$ROOT''${/}'moduleB'${/}'pkg'
+! stderr 'ignoring directory '$ROOT''${/}'moduleA'${/}'pkg'
+stdout 'moduleA$'
+stdout 'moduleA/pkg$'
+stdout 'moduleB$'
+stdout 'moduleB/pkg/helper'
+
+-- go.work --
+go 1.24
+
+use (
+    ./moduleA
+    ./moduleB
+)
+
+-- moduleA/go.mod --
+module moduleA
+
+go 1.24
+
+ignore ./node_modules
+
+-- moduleA/main.go --
+package main
+
+import (
+        "fmt"
+        "moduleB/pkg/helper"
+)
+
+func main() {
+        fmt.Println("Running moduleA")
+        fmt.Println(helper.Message())
+        fmt.Println(hello.Hello())
+}
+-- moduleA/node_modules/some_pkg/index.js --
+console.log("This should be ignored!");
+-- moduleA/pkg/hello.go --
+package hello
+
+func Hello() string {
+        return "Hello from moduleA"
+}
+-- moduleB/go.mod --
+module moduleB
+
+go 1.24
+
+ignore pkg
+
+-- moduleB/main.go --
+package main
+
+import "fmt"
+
+func main() {
+        fmt.Println("Running moduleB")
+}
+
+-- moduleB/pkg/helper/helper.go --
+package helper
+
+func Message() string {
+        return "Helper from moduleB"
+}
index e52575683327b5a61ce02dddd196f9f4b73c4236..6b7dd2c2c5e21cdfbb2017627c68235810ea45df 100644 (file)
@@ -107,6 +107,16 @@ cmpenv go.mod go.mod.edit
 go mod edit -droptool example.com/tool
 cmpenv go.mod go.mod.start
 
+# go mod edit -ignore
+cd $WORK/i
+cp go.mod.start go.mod
+go mod edit -ignore example.com/ignore
+cmpenv go.mod go.mod.edit
+go mod edit -dropignore example.com/ignore2
+cmpenv go.mod go.mod.edit
+go mod edit -dropignore example.com/ignore
+cmpenv go.mod go.mod.start
+
 -- x.go --
 package x
 
@@ -195,7 +205,8 @@ require x.3 v1.99.0
                        "High": "v1.4.0"
                }
        ],
-       "Tool": null
+       "Tool": null,
+       "Ignore": null
 }
 -- $WORK/go.mod.edit3 --
 module x.x/y/z
@@ -333,7 +344,8 @@ retract (
                        "Rationale": "c"
                }
        ],
-       "Tool": null
+       "Tool": null,
+       "Ignore": null
 }
 -- $WORK/go.mod.deprecation --
 // Deprecated: and the new one is not ready yet
@@ -348,7 +360,8 @@ module m
        "Exclude": null,
        "Replace": null,
        "Retract": null,
-       "Tool": null
+       "Tool": null,
+       "Ignore": null
 }
 -- $WORK/go.mod.empty --
 -- $WORK/go.mod.empty.json --
@@ -360,7 +373,8 @@ module m
        "Exclude": null,
        "Replace": null,
        "Retract": null,
-       "Tool": null
+       "Tool": null,
+       "Ignore": null
 }
 -- $WORK/g/go.mod.start --
 module g
@@ -382,3 +396,13 @@ module g
 go 1.24
 
 tool example.com/tool
+-- $WORK/i/go.mod.start --
+module g
+
+go 1.24
+-- $WORK/i/go.mod.edit --
+module g
+
+go 1.24
+
+ignore example.com/ignore
\ No newline at end of file
diff --git a/src/cmd/go/testdata/script/mod_tidy_ignore.txt b/src/cmd/go/testdata/script/mod_tidy_ignore.txt
new file mode 100644 (file)
index 0000000..d3019bb
--- /dev/null
@@ -0,0 +1,78 @@
+# go mod tidy should skip 'ignore' directives
+# See golang.org/issue/42965
+env ROOT=$WORK${/}gopath${/}src
+
+# no ignore directive; should not skip any directories.
+cp go.mod.orig go.mod
+go mod tidy -x
+! stderr 'ignoring directory'
+
+# ignored ./foo should be skipped.
+cp go.mod.relative go.mod
+go mod tidy -x
+stderr 'ignoring directory '$ROOT''${/}'foo'
+! stderr 'ignoring directory '$ROOT''${/}'pkg'${/}'foo'
+! stderr 'ignoring directory '$ROOT''${/}'pkg'${/}'fo$'
+
+# ignored foo; any foo should be skipped.
+cp go.mod.any go.mod
+go mod tidy -x
+stderr 'ignoring directory '$ROOT''${/}'foo'
+stderr 'ignoring directory '$ROOT''${/}'pkg'${/}'foo'
+! stderr 'ignoring directory '$ROOT''${/}'pkg'${/}'fo$'
+
+# non-existent ignore; should not skip any directories.
+cp go.mod.dne go.mod
+go mod tidy -x
+! stderr 'ignoring directory'
+
+# ignored fo; should not skip foo/ but should skip fo/
+cp go.mod.partial go.mod
+go mod tidy -x
+stderr 'ignoring directory '$ROOT''${/}'pkg'${/}'fo$'
+! stderr 'ignoring directory '$ROOT''${/}'pkg'${/}'foo'
+-- foo/secret/secret.go --
+package secret
+
+const Secret = "this should be ignored"
+-- pkg/foo/foo.go --
+package example/pkg/foo
+
+const Bar = "Hello from foo!"
+-- pkg/fo/fo.go --
+package fo
+
+const Gar = "Hello from fo!"
+-- go.mod.orig --
+module example
+
+go 1.24
+-- go.mod.relative --
+module example
+
+go 1.24
+
+ignore ./foo
+-- go.mod.any --
+module example
+
+go 1.24
+
+ignore foo
+-- go.mod.dne --
+module example
+
+go 1.24
+
+ignore bar
+-- go.mod.partial --
+module example
+
+go 1.24
+
+ignore fo
+
+-- main.go --
+package main
+
+func main() {}