]> Cypherpunks repositories - gostls13.git/commitdiff
cmd/go: adjust pruning and switch toolchains if needed when go get changes go version
authorBryan C. Mills <bcmills@google.com>
Thu, 25 May 2023 21:07:26 +0000 (17:07 -0400)
committerGopher Robot <gobot@golang.org>
Fri, 2 Jun 2023 23:07:08 +0000 (23:07 +0000)
When we do 'go get', the Go version can change now.
That means we need to do the pruning conversions that
until now have only been necessary in go mod tidy -go=version.

We may also need to upgrade the toolchain in order to load enough o
the module graph to finish the edit, so we should let a TooNewError
bubble up to the caller instead of trying to downgrade the affected
module to avoid the error.

Revised from CL 498120.

For #57001.

Change-Id: Ic8994737eca4ed61ccc093a69e46f5a6caa8be87
Reviewed-on: https://go-review.googlesource.com/c/go/+/498267
Reviewed-by: Russ Cox <rsc@golang.org>
Auto-Submit: Bryan Mills <bcmills@google.com>
Run-TryBot: Bryan Mills <bcmills@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>

src/cmd/go/internal/modget/get.go
src/cmd/go/internal/modload/buildlist.go
src/cmd/go/internal/modload/edit.go
src/cmd/go/internal/modload/init.go
src/cmd/go/internal/modload/load.go
src/cmd/go/internal/modload/modfile.go
src/cmd/go/testdata/script/mod_indirect_main.txt
src/cmd/go/testdata/script/mod_skip_write.txt
src/cmd/go/testdata/script/mod_tidy_version.txt
src/cmd/go/testdata/script/work_why_download_graph.txt

index 8a8b8dea22e09daedc8b3225e3ad31d8d2287d30..42ddb9cf38229ed65768627850528eb3f66ec270 100644 (file)
@@ -1777,6 +1777,10 @@ func (r *resolver) reportChanges(oldReqs, newReqs []module.Version) {
                        fmt.Fprintf(os.Stderr, "go: removed %s %s\n", c.path, c.old)
                } else if gover.ModCompare(c.path, c.new, c.old) > 0 {
                        fmt.Fprintf(os.Stderr, "go: upgraded %s %s => %s\n", c.path, c.old, c.new)
+                       if c.path == "go" && gover.Compare(c.old, gover.ExplicitIndirectVersion) < 0 && gover.Compare(c.new, gover.ExplicitIndirectVersion) >= 0 {
+                               fmt.Fprintf(os.Stderr, "\tnote: expanded dependencies to upgrade to go %s or higher; run 'go mod tidy' to clean up\n", gover.ExplicitIndirectVersion)
+                       }
+
                } else {
                        fmt.Fprintf(os.Stderr, "go: downgraded %s %s => %s\n", c.path, c.old, c.new)
                }
index ecc07ed91ff6c52ff53f6df7ea5afc00519efada..a5ad20d9ed54e1f07542ecabc5bfc6e61b8acf20 100644 (file)
@@ -126,28 +126,20 @@ func newRequirements(pruning modPruning, rootModules []module.Version, direct ma
                }
        }
 
-       if workFilePath != "" && pruning != workspace {
-               panic("in workspace mode, but pruning is not workspace in newRequirements")
-       }
-       for i, m := range rootModules {
-               if m.Version == "" && MainModules.Contains(m.Path) {
-                       panic(fmt.Sprintf("newRequirements called with untrimmed build list: rootModules[%v] is a main module", i))
+       if pruning != workspace {
+               if workFilePath != "" {
+                       panic("in workspace mode, but pruning is not workspace in newRequirements")
                }
-               if m.Path == "" || m.Version == "" {
-                       panic(fmt.Sprintf("bad requirement: rootModules[%v] = %v", i, m))
+               for i, m := range rootModules {
+                       if m.Version == "" && MainModules.Contains(m.Path) {
+                               panic(fmt.Sprintf("newRequirements called with untrimmed build list: rootModules[%v] is a main module", i))
+                       }
+                       if m.Path == "" || m.Version == "" {
+                               panic(fmt.Sprintf("bad requirement: rootModules[%v] = %v", i, m))
+                       }
                }
        }
 
-       // Allow unsorted root modules, because go and toolchain
-       // are treated as the final graph roots but not trimmed from the build list,
-       // so they always appear at the beginning of the list.
-       r := slices.Clip(slices.Clone(rootModules))
-       gover.ModSort(r)
-       if !reflect.DeepEqual(r, rootModules) {
-               fmt.Fprintln(os.Stderr, "RM", rootModules)
-               panic("unsorted")
-       }
-
        rs := &Requirements{
                pruning:        pruning,
                rootModules:    rootModules,
@@ -155,12 +147,23 @@ func newRequirements(pruning modPruning, rootModules []module.Version, direct ma
                direct:         direct,
        }
 
-       for _, m := range rootModules {
+       for i, m := range rootModules {
+               if i > 0 {
+                       prev := rootModules[i-1]
+                       if prev.Path > m.Path || (prev.Path == m.Path && gover.ModCompare(m.Path, prev.Version, m.Version) > 0) {
+                               panic(fmt.Sprintf("newRequirements called with unsorted roots: %v", rootModules))
+                       }
+               }
+
                if v, ok := rs.maxRootVersion[m.Path]; ok && gover.ModCompare(m.Path, v, m.Version) >= 0 {
                        continue
                }
                rs.maxRootVersion[m.Path] = m.Version
        }
+
+       if rs.maxRootVersion["go"] == "" {
+               panic(`newRequirements called without a "go" version`)
+       }
        return rs
 }
 
@@ -223,6 +226,12 @@ func (rs *Requirements) initVendor(vendorList []module.Version) {
        })
 }
 
+// GoVersion returns the Go language version for the Requirements.
+func (rs *Requirements) GoVersion() string {
+       v, _ := rs.rootSelected("go")
+       return v
+}
+
 // rootSelected returns the version of the root dependency with the given module
 // path, or the zero module.Version and ok=false if the module is not a root
 // dependency.
index 7ee4db536e96c920d35abf38600f897aabfbbab8..63ee15c76452016c828ed6cf96a2abdf16138390 100644 (file)
@@ -10,6 +10,7 @@ import (
        "cmd/go/internal/mvs"
        "cmd/go/internal/par"
        "context"
+       "errors"
        "fmt"
        "maps"
        "os"
@@ -46,10 +47,51 @@ func editRequirements(ctx context.Context, rs *Requirements, tryUpgrade, mustSel
                panic("editRequirements cannot edit workspace requirements")
        }
 
+       orig := rs
+       // If we already know what go version we will end up on after the edit, and
+       // the pruning for that version is different, go ahead and apply it now.
+       //
+       // If we are changing from pruned to unpruned, then we MUST check the unpruned
+       // graph for conflicts from the start. (Checking only for pruned conflicts
+       // would miss some that would be introduced later.)
+       //
+       // If we are changing from unpruned to pruned, then we would like to avoid
+       // unnecessary downgrades due to conflicts that would be pruned out of the
+       // final graph anyway.
+       //
+       // Note that even if we don't find a go version in mustSelect, it is possible
+       // that we will switch from unpruned to pruned (but not the other way around!)
+       // after applying the edits if we find a dependency that requires a high
+       // enough go version to trigger an upgrade.
+       rootPruning := orig.pruning
+       for _, m := range mustSelect {
+               if m.Path == "go" {
+                       rootPruning = pruningForGoVersion(m.Version)
+                       break
+               } else if m.Path == "toolchain" && pruningForGoVersion(gover.FromToolchain(m.Version)) == unpruned {
+                       // We don't know exactly what go version we will end up at, but we know
+                       // that it must be a version supported by the requested toolchain, and
+                       // that toolchain does not support pruning.
+                       //
+                       // TODO(bcmills): 'go get' ought to reject explicit toolchain versions
+                       // older than gover.GoStrictVersion. Once that is fixed, is this still
+                       // needed?
+                       rootPruning = unpruned
+                       break
+               }
+       }
+
+       if rootPruning != rs.pruning {
+               rs, err = convertPruning(ctx, rs, rootPruning)
+               if err != nil {
+                       return orig, false, err
+               }
+       }
+
        // selectedRoot records the edited version (possibly "none") for each module
        // path that would be a root in the edited requirements.
        var selectedRoot map[string]string // module path → edited version
-       if rs.pruning == pruned {
+       if rootPruning == pruned {
                selectedRoot = maps.Clone(rs.maxRootVersion)
        } else {
                // In a module without graph pruning, modules that provide packages imported
@@ -62,7 +104,7 @@ func editRequirements(ctx context.Context, rs *Requirements, tryUpgrade, mustSel
                if err != nil {
                        // If we couldn't load the graph, we don't know what its requirements were
                        // to begin with, so we can't edit those requirements in a coherent way.
-                       return rs, false, err
+                       return orig, false, err
                }
                bl := mg.BuildList()[MainModules.Len():]
                selectedRoot = make(map[string]string, len(bl))
@@ -182,10 +224,11 @@ func editRequirements(ctx context.Context, rs *Requirements, tryUpgrade, mustSel
                // of every root. The upgraded roots are in addition to the original
                // roots, so we will have enough information to trace a path to each
                // conflict we discover from one or more of the original roots.
-               mg, upgradedRoots, err := extendGraph(ctx, rs, roots, selectedRoot)
+               mg, upgradedRoots, err := extendGraph(ctx, rootPruning, roots, selectedRoot)
                if err != nil {
-                       if mg == nil {
-                               return rs, false, err
+                       var tooNew *gover.TooNewError
+                       if mg == nil || errors.As(err, &tooNew) {
+                               return orig, false, err
                        }
                        // We're about to walk the entire extended module graph, so we will find
                        // any error then — and we will either try to resolve it by downgrading
@@ -196,13 +239,13 @@ func editRequirements(ctx context.Context, rs *Requirements, tryUpgrade, mustSel
                // the extended module graph.
                extendedRootPruning := make(map[module.Version]modPruning, len(roots)+len(upgradedRoots))
                findPruning := func(m module.Version) modPruning {
-                       if rs.pruning == pruned {
+                       if rootPruning == pruned {
                                summary, _ := mg.loadCache.Get(m)
                                if summary != nil && summary.pruning == unpruned {
                                        return unpruned
                                }
                        }
-                       return rs.pruning
+                       return rootPruning
                }
                for _, m := range roots {
                        extendedRootPruning[m] = findPruning(m)
@@ -346,7 +389,7 @@ func editRequirements(ctx context.Context, rs *Requirements, tryUpgrade, mustSel
                        // the edit. We want to make sure we consider keeping it as-is,
                        // even if it wouldn't normally be included. (For example, it might
                        // be a pseudo-version or pre-release.)
-                       origMG, _ := rs.Graph(ctx)
+                       origMG, _ := orig.Graph(ctx)
                        origV := origMG.Selected(m.Path)
 
                        if conflict.Err != nil && origV == m.Version {
@@ -376,14 +419,14 @@ func editRequirements(ctx context.Context, rs *Requirements, tryUpgrade, mustSel
                                        prev.Version = origV
                                } else if err != nil {
                                        // We don't know the next downgrade to try. Give up.
-                                       return rs, false, err
+                                       return orig, false, err
                                }
                                if rejectedRoot[prev] {
                                        // We already rejected prev in a previous round.
                                        // To ensure that this algorithm terminates, don't try it again.
                                        continue
                                }
-                               pruning := rs.pruning
+                               pruning := rootPruning
                                if pruning == pruned {
                                        if summary, err := mg.loadCache.Get(m); err == nil {
                                                pruning = summary.pruning
@@ -460,10 +503,10 @@ func editRequirements(ctx context.Context, rs *Requirements, tryUpgrade, mustSel
                break
        }
        if len(conflicts) > 0 {
-               return rs, false, &ConstraintError{Conflicts: conflicts}
+               return orig, false, &ConstraintError{Conflicts: conflicts}
        }
 
-       if rs.pruning == unpruned {
+       if rootPruning == unpruned {
                // An unpruned go.mod file lists only a subset of the requirements needed
                // for building packages. Figure out which requirements need to be explicit.
                var rootPaths []string
@@ -493,12 +536,12 @@ func editRequirements(ctx context.Context, rs *Requirements, tryUpgrade, mustSel
                }
        }
 
-       changed = !slices.Equal(roots, rs.rootModules)
+       changed = rootPruning != orig.pruning || !slices.Equal(roots, orig.rootModules)
        if !changed {
                // Because the roots we just computed are unchanged, the entire graph must
                // be the same as it was before. Save the original rs, since we have
                // probably already loaded its requirement graph.
-               return rs, false, nil
+               return orig, false, nil
        }
 
        // A module that is not even in the build list necessarily cannot provide
@@ -518,7 +561,37 @@ func editRequirements(ctx context.Context, rs *Requirements, tryUpgrade, mustSel
                        direct[m.Path] = true
                }
        }
-       return newRequirements(rs.pruning, roots, direct), changed, nil
+       edited = newRequirements(rootPruning, roots, direct)
+
+       // If we ended up adding a dependency that upgrades our go version far enough
+       // to activate pruning, we must convert the edited Requirements in order to
+       // avoid dropping transitive dependencies from the build list the next time
+       // someone uses the updated go.mod file.
+       //
+       // Note that it isn't possible to go in the other direction (from pruned to
+       // unpruned) unless the "go" or "toolchain" module is explicitly listed in
+       // mustSelect, which we already handled at the very beginning of the edit.
+       // That is because the virtual "go" module only requires a "toolchain",
+       // and the "toolchain" module never requires anything else, which means that
+       // those two modules will never be downgraded due to a conflict with any other
+       // constraint.
+       if rootPruning == unpruned {
+               if v, ok := edited.rootSelected("go"); ok && pruningForGoVersion(v) == pruned {
+                       // Since we computed the edit with the unpruned graph, and the pruned
+                       // graph is a strict subset of the unpruned graph, this conversion
+                       // preserves the exact (edited) build list that we already computed.
+                       //
+                       // However, it does that by shoving the whole build list into the roots of
+                       // the graph. 'go get' will check for that sort of transition and log a
+                       // message reminding the user how to clean up this mess we're about to
+                       // make. 😅
+                       edited, err = convertPruning(ctx, edited, pruned)
+                       if err != nil {
+                               return orig, false, err
+                       }
+               }
+       }
+       return edited, true, nil
 }
 
 // extendGraph loads the module graph from roots, and iteratively extends it by
@@ -532,15 +605,15 @@ func editRequirements(ctx context.Context, rs *Requirements, tryUpgrade, mustSel
 // The extended graph is useful for diagnosing version conflicts: for each
 // selected module version, it can provide a complete path of requirements from
 // some root to that version.
-func extendGraph(ctx context.Context, rs *Requirements, roots []module.Version, selectedRoot map[string]string) (mg *ModuleGraph, upgradedRoot map[module.Version]bool, err error) {
+func extendGraph(ctx context.Context, rootPruning modPruning, roots []module.Version, selectedRoot map[string]string) (mg *ModuleGraph, upgradedRoot map[module.Version]bool, err error) {
        for {
-               mg, err = readModGraph(ctx, rs.pruning, roots, upgradedRoot)
+               mg, err = readModGraph(ctx, rootPruning, roots, upgradedRoot)
                // We keep on going even if err is non-nil until we reach a steady state.
                // (Note that readModGraph returns a non-nil *ModuleGraph even in case of
                // errors.) The caller may be able to fix the errors by adjusting versions,
                // so we really want to return as complete a result as we can.
 
-               if rs.pruning == unpruned {
+               if rootPruning == unpruned {
                        // Everything is already unpruned, so there isn't anything we can do to
                        // extend it further.
                        break
index 446f4d9fa5fc8a8a761cf84ee281f42382b4771a..9483bac2d8f073acf10021e94181a6951fcfef3b 100644 (file)
@@ -783,17 +783,33 @@ func loadModFile(ctx context.Context, opts *PackageOpts) (*Requirements, error)
                // any module.
                mainModule := module.Version{Path: "command-line-arguments"}
                MainModules = makeMainModules([]module.Version{mainModule}, []string{""}, []*modfile.File{nil}, []*modFileIndex{nil}, nil)
-               goVersion := gover.Local()
-               rawGoVersion.Store(mainModule, goVersion)
-               pruning := pruningForGoVersion(goVersion)
+               var (
+                       goVersion string
+                       pruning   modPruning
+                       roots     []module.Version
+                       direct    = map[string]bool{"go": true}
+               )
                if inWorkspaceMode() {
+                       // Since we are in a workspace, the Go version for the synthetic
+                       // "command-line-arguments" module must not exceed the Go version
+                       // for the workspace.
+                       goVersion = MainModules.GoVersion()
                        pruning = workspace
+                       roots = []module.Version{
+                               mainModule,
+                               {Path: "go", Version: goVersion},
+                               {Path: "toolchain", Version: gover.LocalToolchain()},
+                       }
+               } else {
+                       goVersion = gover.Local()
+                       pruning = pruningForGoVersion(goVersion)
+                       roots = []module.Version{
+                               {Path: "go", Version: goVersion},
+                               {Path: "toolchain", Version: gover.LocalToolchain()},
+                       }
                }
-               roots := []module.Version{
-                       {Path: "go", Version: gover.Local()},
-                       {Path: "toolchain", Version: gover.LocalToolchain()},
-               }
-               requirements = newRequirements(pruning, roots, nil)
+               rawGoVersion.Store(mainModule, goVersion)
+               requirements = newRequirements(pruning, roots, direct)
                if cfg.BuildMod == "vendor" {
                        // For issue 56536: Some users may have GOFLAGS=-mod=vendor set.
                        // Make sure it behaves as though the fake module is vendored
@@ -1213,13 +1229,16 @@ func requirementsFromModFiles(ctx context.Context, workFile *modfile.WorkFile, m
                goVersion = gover.DefaultGoModVersion
        }
        roots = append(roots, module.Version{Path: "go", Version: goVersion})
-       direct["go"] = true
+       direct["go"] = true // Every module directly uses the language and runtime.
 
-       if toolchain == "" {
-               toolchain = "go" + goVersion
+       if toolchain != "" {
+               roots = append(roots, module.Version{Path: "toolchain", Version: toolchain})
+               // Leave the toolchain as indirect: nothing in the user's module directly
+               // imports a package from the toolchain, and (like an indirect dependency in
+               // a module without graph pruning) we may remove the toolchain line
+               // automatically if the 'go' version is changed so that it implies the exact
+               // same toolchain.
        }
-       roots = append(roots, module.Version{Path: "toolchain", Version: toolchain})
-       direct["toolchain"] = true
 
        gover.ModSort(roots)
        rs := newRequirements(pruning, roots, direct)
index a96ce0283de68608dc6c72f506e3591d34f2753e..5384d753bf15be1ae3d396ea0c4e19a29a5736fe 100644 (file)
@@ -1013,6 +1013,8 @@ func loadFromRoots(ctx context.Context, params loaderParams) *loader {
                        ld.errorf("go: go.mod file indicates go %s, but maximum version supported by tidy is %s\n", ld.GoVersion, gover.Local())
                        base.ExitIfErrors()
                }
+       } else {
+               ld.requirements = overrideRoots(ctx, ld.requirements, []module.Version{{Path: "go", Version: ld.GoVersion}})
        }
 
        if ld.Tidy {
index 72fc293d8f2d88af63d687d25a3870a2778c2615..026be5eef753f01eb80e54ea6ccad8825ba9a24c 100644 (file)
@@ -639,8 +639,8 @@ func goModSummary(m module.Version) (*modFileSummary, error) {
 // rawGoModSummary cannot be used on the main module outside of workspace mode.
 func rawGoModSummary(m module.Version) (*modFileSummary, error) {
        if gover.IsToolchain(m.Path) {
-               if m.Path == "go" {
-                       // Declare that go 1.2.3 requires toolchain 1.2.3,
+               if m.Path == "go" && gover.Compare(m.Version, gover.GoStrictVersion) >= 0 {
+                       // Declare that go 1.21.3 requires toolchain 1.21.3,
                        // so that go get knows that downgrading toolchain implies downgrading go
                        // and similarly upgrading go requires upgrading the toolchain.
                        return &modFileSummary{module: m, require: []module.Version{{Path: "toolchain", Version: "go" + m.Version}}}, nil
index 22b344f8668dfe2514cb0bff692a614b623d0fd4..e84eb9c5cd3708f35576d26cece3e26b879a00db 100644 (file)
@@ -62,7 +62,5 @@ golang.org/issue/pkg v0.1.0 => ./pkg-v0.1.0
 -- graph.txt --
 golang.org/issue/root go@1.12
 golang.org/issue/root golang.org/issue/mirror@v0.1.0
-golang.org/issue/root toolchain@go1.12
-go@1.12 toolchain@go1.12
 golang.org/issue/mirror@v0.1.0 golang.org/issue/root@v0.1.0
 golang.org/issue/root@v0.1.0 golang.org/issue/pkg@v0.1.0
index 1850cdf5fdbb0056f81bc46084de891809c57fa3..db47b9c424cfa652811d1ca4c71abaa35dd2f6f4 100644 (file)
@@ -84,8 +84,6 @@ m golang.org/x/text@v0.0.0-20170915032832-14c0d48ead0c
 m rsc.io/quote@v1.5.2
 m rsc.io/sampler@v1.3.0
 m rsc.io/testonly@v1.0.0
-m toolchain@go1.18
-go@1.18 toolchain@go1.18
 rsc.io/quote@v1.5.2 rsc.io/sampler@v1.3.0
 rsc.io/sampler@v1.3.0 golang.org/x/text@v0.0.0-20170915032832-14c0d48ead0c
 -- why.want --
index 11f1d69dc55c1df2cc6e9b45d3d39c0e483fa731..e3f2561f5e367b663cb68e068a6f8967de46105e 100644 (file)
@@ -90,6 +90,37 @@ cp go.mod.orig go.mod
 go mod tidy -go=''
 cmpenv go.mod go.mod.latest
 
+# Repeat with go get go@ instead of mod tidy.
+
+# Go 1.16 -> 1.17 should be a no-op.
+cp go.mod.116 go.mod
+go get go@1.16
+cmp go.mod go.mod.116
+
+# Go 1.17 -> 1.16 should leave b (go get is not tidy).
+cp go.mod.117 go.mod
+go get go@1.16
+cmp go.mod go.mod.116from117
+
+# Go 1.15 -> 1.16 should leave d (go get is not tidy).
+cp go.mod.115 go.mod
+go get go@1.16
+cmp go.mod go.mod.116from115
+
+# Go 1.16 -> 1.17 should add b.
+cp go.mod.116 go.mod
+go get go@1.17
+stderr '^\tnote: expanded dependencies to upgrade to go 1.17 or higher; run ''go mod tidy'' to clean up'
+cmp go.mod go.mod.117
+
+# Go 1.16 -> 1.15 should add d,
+# but 'go get' doesn't load enough packages to know that.
+# (This leaves the module untidy, but the user can fix it by running 'go mod tidy'.)
+cp go.mod.116 go.mod
+go get go@1.15 toolchain@none
+cmp go.mod go.mod.115from116
+go mod tidy
+cmp go.mod go.mod.115-2
 
 # Updating the go line to 1.21 or higher also updates the toolchain line,
 # only if the toolchain is higher than what would be implied by the go line.
@@ -140,6 +171,47 @@ require (
        example.net/d v0.1.0 // indirect
 )
 
+replace (
+       example.net/a v0.1.0 => ./a
+       example.net/a v0.2.0 => ./a
+       example.net/b v0.1.0 => ./b
+       example.net/b v0.2.0 => ./b
+       example.net/c v0.1.0 => ./c
+       example.net/c v0.2.0 => ./c
+       example.net/d v0.1.0 => ./d
+       example.net/d v0.2.0 => ./d
+)
+-- go.mod.115from116 --
+module example.com/m
+
+go 1.15
+
+require example.net/a v0.1.0
+
+require example.net/c v0.1.0 // indirect
+
+replace (
+       example.net/a v0.1.0 => ./a
+       example.net/a v0.2.0 => ./a
+       example.net/b v0.1.0 => ./b
+       example.net/b v0.2.0 => ./b
+       example.net/c v0.1.0 => ./c
+       example.net/c v0.2.0 => ./c
+       example.net/d v0.1.0 => ./d
+       example.net/d v0.2.0 => ./d
+)
+-- go.mod.116from115 --
+module example.com/m
+
+go 1.16
+
+require example.net/a v0.1.0
+
+require (
+       example.net/c v0.1.0 // indirect
+       example.net/d v0.1.0 // indirect
+)
+
 replace (
        example.net/a v0.1.0 => ./a
        example.net/a v0.2.0 => ./a
@@ -203,6 +275,28 @@ require (
        example.net/c v0.1.0 // indirect
 )
 
+replace (
+       example.net/a v0.1.0 => ./a
+       example.net/a v0.2.0 => ./a
+       example.net/b v0.1.0 => ./b
+       example.net/b v0.2.0 => ./b
+       example.net/c v0.1.0 => ./c
+       example.net/c v0.2.0 => ./c
+       example.net/d v0.1.0 => ./d
+       example.net/d v0.2.0 => ./d
+)
+-- go.mod.116from117 --
+module example.com/m
+
+go 1.16
+
+require example.net/a v0.1.0
+
+require (
+       example.net/b v0.1.0 // indirect
+       example.net/c v0.1.0 // indirect
+)
+
 replace (
        example.net/a v0.1.0 => ./a
        example.net/a v0.2.0 => ./a
index b86dc00d4331c5a31391db4aef240be897b236e0..8f1aeddf47106d3abc51ba45610853ed0fab3164 100644 (file)
@@ -25,7 +25,7 @@ go mod why rsc.io/quote
 stdout '# rsc.io/quote\nexample.com/a\nrsc.io/quote'
 
 go mod graph
-stdout 'example.com/a rsc.io/quote@v1.5.2\nexample.com/b example.com/c@v1.0.0\ngo@1.18 toolchain@go1.18\nrsc.io/quote@v1.5.2 rsc.io/sampler@v1.3.0\nrsc.io/sampler@v1.3.0 golang.org/x/text@v0.0.0-20170915032832-14c0d48ead0c'
+stdout 'example.com/a rsc.io/quote@v1.5.2\nexample.com/b example.com/c@v1.0.0\nrsc.io/quote@v1.5.2 rsc.io/sampler@v1.3.0\nrsc.io/sampler@v1.3.0 golang.org/x/text@v0.0.0-20170915032832-14c0d48ead0c'
 
 -- go.work --
 go 1.18