Commit a7fc7109 authored by Bryan C. Mills's avatar Bryan C. Mills

cmd/go/internal/modget: support the suffix '@patch' in 'go get'

As of this change, an explicit '@patch' suffix is to '-u=patch' as
'@latest' is to '-u'.

RELNOTE='go get' in module mode now supports the version suffix '@patch'.

Fixes #26812

Change-Id: Ib5eee40de640440f7470d37a574b311ef8a67f67
Reviewed-on: https://go-review.googlesource.com/c/go/+/167747
Run-TryBot: Bryan C. Mills <bcmills@google.com>
Reviewed-by: default avatarJay Conrod <jayconrod@google.com>
parent d6b2b35e
......@@ -558,8 +558,6 @@
// For modules stored in source control repositories, the version suffix can
// also be a commit hash, branch identifier, or other syntax known to the
// source control system, as in 'go get golang.org/x/text@master'.
// The version suffix @latest explicitly requests the default behavior
// described above.
//
// If a module under consideration is already a dependency of the current
// development module, then get will update the required version.
......@@ -568,6 +566,13 @@
// dependency should be removed entirely, downgrading or removing modules
// depending on it as needed.
//
// The version suffix @latest explicitly requests the latest minor release of the
// given path.
//
// The suffix @patch requests the latest patch release: if the path is already in
// the build list, the selected version will have the same minor version.
// If the path is not already in the build list, @patch is equivalent to @latest.
//
// Although get defaults to using the latest version of the module containing
// a named package, it does not use the latest version of that module's
// dependencies. Instead it prefers to use the specific dependency versions
......@@ -581,9 +586,11 @@
// patch releases when available. Continuing the previous example,
// 'go get -u A' will use the latest A with B v1.3.1 (not B v1.2.3).
//
// The -u=patch flag (not -u patch) instructs get to update dependencies
// to use newer patch releases when available. Continuing the previous example,
// 'go get -u=patch A' will use the latest A with B v1.2.4 (not B v1.2.3).
// The -u=patch flag (not -u patch) also instructs get to update dependencies,
// but changes the default to select patch releases.
// Continuing the previous example,
// 'go get -u=patch A@latest' will use the latest A with B v1.2.4 (not B v1.2.3),
// while 'go get -u=patch A' will use a patch release of A instead.
//
// In general, adding a new dependency may require upgrading
// existing dependencies to keep a working build, and 'go get' does
......
......@@ -21,7 +21,6 @@ import (
"cmd/go/internal/work"
"fmt"
"os"
pathpkg "path"
"path/filepath"
"strings"
)
......@@ -49,8 +48,6 @@ suffix to the package argument, as in 'go get golang.org/x/text@v0.3.0'.
For modules stored in source control repositories, the version suffix can
also be a commit hash, branch identifier, or other syntax known to the
source control system, as in 'go get golang.org/x/text@master'.
The version suffix @latest explicitly requests the default behavior
described above.
If a module under consideration is already a dependency of the current
development module, then get will update the required version.
......@@ -59,6 +56,13 @@ downgrades the dependency. The version suffix @none indicates that the
dependency should be removed entirely, downgrading or removing modules
depending on it as needed.
The version suffix @latest explicitly requests the latest minor release of the
given path.
The suffix @patch requests the latest patch release: if the path is already in
the build list, the selected version will have the same minor version.
If the path is not already in the build list, @patch is equivalent to @latest.
Although get defaults to using the latest version of the module containing
a named package, it does not use the latest version of that module's
dependencies. Instead it prefers to use the specific dependency versions
......@@ -72,9 +76,11 @@ The -u flag instructs get to update dependencies to use newer minor or
patch releases when available. Continuing the previous example,
'go get -u A' will use the latest A with B v1.3.1 (not B v1.2.3).
The -u=patch flag (not -u patch) instructs get to update dependencies
to use newer patch releases when available. Continuing the previous example,
'go get -u=patch A' will use the latest A with B v1.2.4 (not B v1.2.3).
The -u=patch flag (not -u patch) also instructs get to update dependencies,
but changes the default to select patch releases.
Continuing the previous example,
'go get -u=patch A@latest' will use the latest A with B v1.2.4 (not B v1.2.3),
while 'go get -u=patch A' will use a patch release of A instead.
In general, adding a new dependency may require upgrading
existing dependencies to keep a working build, and 'go get' does
......@@ -165,6 +171,9 @@ func (v *upgradeFlag) Set(s string) error {
if s == "false" {
s = ""
}
if s == "true" {
s = "latest"
}
*v = upgradeFlag(s)
return nil
}
......@@ -180,12 +189,12 @@ func init() {
// A task holds the state for processing a single get argument (path@vers).
type task struct {
arg string // original argument
index int
arg string // original argument
path string // package path part of arg
forceModulePath bool // path must be interpreted as a module path
vers string // version part of arg
m module.Version // module version indicated by argument
prevM module.Version // module version from initial build list
req []module.Version // m's requirement list (not upgraded)
}
......@@ -196,7 +205,7 @@ func runGet(cmd *base.Command, args []string) {
}
switch getU {
case "", "patch", "true":
case "", "latest", "patch":
// ok
default:
base.Fatalf("go get: unknown upgrade flag -u=%s", getU)
......@@ -230,6 +239,7 @@ func runGet(cmd *base.Command, args []string) {
// and a list of install targets (for the "go install" at the end).
var tasks []*task
var install []string
var needModule []*task
for _, arg := range search.CleanPatterns(args) {
// Argument is module query path@vers, or else path with implicit @latest.
path := arg
......@@ -245,6 +255,12 @@ func runGet(cmd *base.Command, args []string) {
install = append(install, path)
}
// If the user runs 'go get -u=patch some/module', update some/module to a
// patch release, not a minor version.
if vers == "" && getU != "" {
vers = string(getU)
}
// Deciding which module to upgrade/downgrade for a particular argument is difficult.
// Patterns only make it more difficult.
// We impose restrictions to avoid needing to interlace pattern expansion,
......@@ -271,25 +287,43 @@ func runGet(cmd *base.Command, args []string) {
// - Import paths without patterns are left as is, for resolution by getQuery (eventually modload.Import).
//
if search.IsRelativePath(path) {
// Check that this relative pattern only matches directories in the current module,
// and then record the current module as the target.
dir := path
if i := strings.Index(path, "..."); i >= 0 {
dir, _ = pathpkg.Split(path[:i])
}
abs, err := filepath.Abs(dir)
if err != nil {
base.Errorf("go get %s: %v", arg, err)
continue
t := &task{arg: arg, path: modload.Target.Path, vers: "", prevM: modload.Target, forceModulePath: true}
// If the path is relative, always upgrade the entire main module.
// (TODO(golang.org/issue/26902): maybe we should upgrade the modules
// containing the dependencies of the requested packages instead.)
//
// If the path is explicit, at least check that it is a package in the main module.
if len(args) > 0 {
if *getM {
base.Errorf("go get %s: -m requires a module path, but a relative path must be a package in the main module", arg)
continue
}
pkgPath := modload.DirImportPath(filepath.FromSlash(path))
if pkgs := modload.TargetPackages(pkgPath); len(pkgs) == 0 {
if strings.Contains(path, "...") {
fmt.Fprintf(os.Stderr, "go get %s: warning: pattern patched no packages", arg)
} else {
abs, err := filepath.Abs(path)
if err != nil {
abs = path
}
base.Errorf("go get %s: path %s is not in module rooted at %s", arg, abs, modload.ModRoot())
}
continue
}
}
if !str.HasFilePathPrefix(abs, modload.ModRoot()) {
base.Errorf("go get %s: directory %s is outside module root %s", arg, abs, modload.ModRoot())
continue
switch vers {
case "", "latest", "patch":
tasks = append(tasks, t)
default:
base.Errorf("go get %s: can't request explicit version of path in main module", arg)
}
// TODO: Check if abs is inside a nested module.
tasks = append(tasks, &task{arg: arg, path: modload.Target.Path, vers: ""})
continue
}
if path == "all" {
// TODO: If *getM, should this be the module pattern "all"?
......@@ -306,30 +340,19 @@ func runGet(cmd *base.Command, args []string) {
m := modload.PackageModule(pkg)
if m.Path != "" && !seen[m] {
seen[m] = true
tasks = append(tasks, &task{arg: arg, path: m.Path, vers: "latest", forceModulePath: true})
tasks = append(tasks, &task{arg: arg, path: m.Path, vers: vers, prevM: m, forceModulePath: true})
}
}
continue
}
if search.IsMetaPackage(path) {
// Already handled "all", so this must be "std" or "cmd",
// which are entirely in the standard library.
if path != arg {
base.Errorf("go get %s: cannot use pattern %q with explicit version", arg, arg)
}
if *getM {
base.Errorf("go get %s: cannot use pattern %q with -m", arg, arg)
continue
}
continue
}
if strings.Contains(path, "...") {
// Apply to modules in build list matched by pattern (golang.org/x/...), if any.
match := search.MatchPattern(path)
matched := false
for _, m := range modload.BuildList() {
if match(m.Path) || str.HasPathPrefix(path, m.Path) {
tasks = append(tasks, &task{arg: arg, path: m.Path, vers: vers, forceModulePath: true})
tasks = append(tasks, &task{arg: arg, path: m.Path, vers: vers, prevM: m, forceModulePath: true})
matched = true
}
}
......@@ -345,10 +368,66 @@ func runGet(cmd *base.Command, args []string) {
continue
}
}
tasks = append(tasks, &task{arg: arg, path: path, vers: vers})
t := &task{arg: arg, path: path, vers: vers}
if vers == "patch" {
if *getM {
for _, m := range modload.BuildList() {
if m.Path == path {
t.prevM = m
break
}
}
tasks = append(tasks, t)
} else {
// We need to know the module containing t so that we can restrict the patch to its minor version.
needModule = append(needModule, t)
}
} else {
// The requested version of path doesn't depend on the existing version,
// so don't bother resolving it.
tasks = append(tasks, t)
}
}
base.ExitIfErrors()
if len(needModule) > 0 {
paths := make([]string, len(needModule))
for i, t := range needModule {
paths[i] = t.path
}
matches := modload.ImportPaths(paths)
if len(matches) != len(paths) {
base.Fatalf("go get: internal error: ImportPaths resolved %d paths to %d matches", len(paths), len(matches))
}
for i, match := range matches {
t := needModule[i]
if len(match.Pkgs) == 0 {
// Let modload.Query resolve the path during task processing.
tasks = append(tasks, t)
continue
}
allStd := true
for _, pkg := range match.Pkgs {
m := modload.PackageModule(pkg)
if m.Path == "" {
// pkg is in the standard library.
} else {
allStd = false
tasks = append(tasks, &task{arg: t.arg, path: pkg, vers: t.vers, prevM: m})
}
}
if allStd {
if *getM {
base.Errorf("go get %s: cannot use pattern %q with -m", t.arg, t.arg)
} else if t.path != t.arg {
base.Errorf("go get %s: cannot use pattern %q with explicit version", t.arg, t.arg)
}
}
}
}
// Now we've reduced the upgrade/downgrade work to a list of path@vers pairs (tasks).
// Resolve each one in parallel.
reqs := modload.Reqs()
......@@ -363,7 +442,7 @@ func runGet(cmd *base.Command, args []string) {
t.m = module.Version{Path: t.path, Version: "none"}
return
}
m, err := getQuery(t.path, t.vers, t.forceModulePath)
m, err := getQuery(t.path, t.vers, t.prevM, t.forceModulePath)
if err != nil {
base.Errorf("go get %v: %v", t.arg, err)
return
......@@ -412,7 +491,6 @@ func runGet(cmd *base.Command, args []string) {
upgraded, err := mvs.UpgradeAll(upgradeTarget, &upgrader{
Reqs: modload.Reqs(),
targets: named,
patch: getU == "patch",
tasks: byPath,
})
if err != nil {
......@@ -554,9 +632,16 @@ func runGet(cmd *base.Command, args []string) {
// to determine the underlying module version being requested.
// If forceModulePath is set, getQuery must interpret path
// as a module path.
func getQuery(path, vers string, forceModulePath bool) (module.Version, error) {
if vers == "" {
func getQuery(path, vers string, prevM module.Version, forceModulePath bool) (module.Version, error) {
switch vers {
case "":
vers = "latest"
case "patch":
if prevM.Version == "" {
vers = "latest"
} else {
vers = semver.MajorMinor(prevM.Version)
}
}
// First choice is always to assume path is a module path.
......@@ -625,7 +710,7 @@ func (u *upgrader) Upgrade(m module.Version) (module.Version, error) {
// only ever returns untagged versions,
// which is not what we want.
query := "latest"
if u.patch {
if getU == "patch" {
// For patch upgrade, query "v1.2".
query = semver.MajorMinor(m.Version)
}
......
......@@ -339,7 +339,7 @@ func loadAll(testAll bool) []string {
if !testAll {
loaded.testRoots = true
}
all := TargetPackages()
all := TargetPackages("...")
loaded.load(func() []string { return all })
WriteGoMod()
......@@ -357,10 +357,11 @@ func loadAll(testAll bool) []string {
// Only "ignore" and malformed build tag requirements are considered false.
var anyTags = map[string]bool{"*": true}
// TargetPackages returns the list of packages in the target (top-level) module,
// under all build tag settings.
func TargetPackages() []string {
return matchPackages("...", anyTags, false, []module.Version{Target})
// TargetPackages returns the list of packages in the target (top-level) module
// matching pattern, which may be relative to the working directory, under all
// build tag settings.
func TargetPackages(pattern string) []string {
return matchPackages(pattern, anyTags, false, []module.Version{Target})
}
// BuildList returns the module build list,
......
patch.example.com/depofdirectpatch v1.0.0
written by hand
-- .mod --
module patch.example.com/depofdirectpatch
-- .info --
{"Version":"v1.0.0"}
-- go.mod --
module patch.example.com/depofdirectpatch
-- depofdirectpatch.go --
package depofdirectpatch
patch.example.com/depofdirectpatch v1.0.1
written by hand
-- .mod --
module patch.example.com/depofdirectpatch
-- .info --
{"Version":"v1.0.1"}
-- go.mod --
module patch.example.com/depofdirectpatch
-- depofdirectpatch.go --
package depofdirectpatch
patch.example.com/direct v1.0.0
written by hand
-- .mod --
module patch.example.com/direct
require (
patch.example.com/indirect v1.0.0
)
-- .info --
{"Version":"v1.0.0"}
-- go.mod --
module patch.example.com/direct
require (
patch.example.com/indirect v1.0.0
)
-- direct.go --
package direct
import _ "patch.example.com/indirect"
patch.example.com/direct v1.0.1
written by hand
-- .mod --
module patch.example.com/direct
require (
patch.example.com/indirect v1.0.0
patch.example.com/depofdirectpatch v1.0.0
)
-- .info --
{"Version":"v1.0.1"}
-- go.mod --
module patch.example.com/direct
require (
patch.example.com/indirect v1.0.0
patch.example.com/depofdirectpatch v1.0.0
)
-- direct.go --
package direct
import _ "patch.example.com/indirect"
-- usedepofdirectpatch/unused.go --
package usedepofdirectpatch
import _ "patch.example.com/depofdirectpatch"
patch.example.com/direct v1.1.0
written by hand
-- .mod --
module patch.example.com/direct
require (
patch.example.com/indirect v1.0.0
)
-- .info --
{"Version":"v1.1.0"}
-- go.mod --
module patch.example.com/direct
require (
patch.example.com/indirect v1.0.0
)
-- direct.go --
package direct
import _ "patch.example.com/indirect"
patch.example.com/indirect v1.0.0
written by hand
-- .mod --
module patch.example.com/indirect
-- .info --
{"Version":"v1.0.0"}
-- go.mod --
module patch.example.com/indirect
-- direct.go --
package indirect
patch.example.com/indirect v1.0.1
written by hand
-- .mod --
module patch.example.com/indirect
-- .info --
{"Version":"v1.0.1"}
-- go.mod --
module patch.example.com/indirect
-- direct.go --
package indirect
patch.example.com/indirect v1.1.0
written by hand
-- .mod --
module patch.example.com/indirect
-- .info --
{"Version":"v1.1.0"}
-- go.mod --
module patch.example.com/indirect
-- direct.go --
package indirect
env GO111MODULE=on
go list -m all
stdout '^rsc.io/quote v1.4.0'
stdout '^rsc.io/sampler v1.0.0'
# get -u=patch rsc.io/quote should take latest quote & patch update its deps
go get -m -u=patch rsc.io/quote
go list -m all
stdout '^rsc.io/quote v1.5.2'
stdout '^rsc.io/sampler v1.3.1'
stdout '^golang.org/x/text v0.0.0-'
# get -u=patch quote@v1.2.0 should take that version of quote & patch update its deps
go get -m -u=patch rsc.io/quote@v1.2.0
go list -m all
stdout '^rsc.io/quote v1.2.0'
stdout '^rsc.io/sampler v1.3.1'
stdout '^golang.org/x/text v0.0.0-'
# get -u=patch with no args applies to all deps
go get -m -u=patch
go list -m all
stdout '^rsc.io/quote v1.2.1'
-- go.mod --
module x
require rsc.io/quote v1.4.0
env GO111MODULE=on
# Initially, we are at v1.0.0 for all dependencies.
cp go.mod go.mod.orig
go list -m all
stdout '^patch.example.com/direct v1.0.0'
stdout '^patch.example.com/indirect v1.0.0'
! stdout '^patch.example.com/depofdirectpatch'
# get -m -u=patch, with no arguments, should patch-update all dependencies,
# pulling in transitive dependencies and also patching those.
#
# TODO(golang.org/issue/26902): We should not update transitive dependencies
# that don't affect the transitive import graph of the main module in any way.
cp go.mod.orig go.mod
go get -m -u=patch
go list -m all
stdout '^patch.example.com/direct v1.0.1'
stdout '^patch.example.com/indirect v1.0.1'
stdout '^patch.example.com/depofdirectpatch v1.0.1' # TODO: leave at v1.0.0
# 'get -m all@patch' should be equivalent to 'get -u=patch -m all'
cp go.mod.orig go.mod
go get -m all@patch
go list -m all
stdout '^patch.example.com/direct v1.0.1'
stdout '^patch.example.com/indirect v1.0.1'
stdout '^patch.example.com/depofdirectpatch v1.0.0'
# Requesting the direct dependency with -u=patch but without an explicit version
# should patch-update it and its dependencies.
cp go.mod.orig go.mod
go get -m -u=patch patch.example.com/direct
go list -m all
stdout '^patch.example.com/direct v1.0.1'
stdout '^patch.example.com/indirect v1.0.1'
stdout '^patch.example.com/depofdirectpatch v1.0.1' # TODO: leave at v1.0.0
# Requesting only the indirect dependency should not update the direct one.
cp go.mod.orig go.mod
go get -m -u=patch patch.example.com/indirect
go list -m all
stdout '^patch.example.com/direct v1.0.0'
stdout '^patch.example.com/indirect v1.0.1'
! stdout '^patch.example.com/depofdirectpatch'
# @patch should apply only to the specific module.
# but the result must reflect its upgraded requirements.
cp go.mod.orig go.mod
go get -m patch.example.com/direct@patch
go list -m all
stdout '^patch.example.com/direct v1.0.1'
stdout '^patch.example.com/indirect v1.0.0'
stdout '^patch.example.com/depofdirectpatch v1.0.0'
# An explicit @patch should override a general -u.
cp go.mod.orig go.mod
go get -m -u patch.example.com/direct@patch
go list -m all
stdout '^patch.example.com/direct v1.0.1'
stdout '^patch.example.com/indirect v1.1.0'
stdout '^patch.example.com/depofdirectpatch v1.0.1'
# An explicit @latest should override a general -u=patch.
cp go.mod.orig go.mod
go get -m -u=patch patch.example.com/direct@latest
go list -m all
stdout '^patch.example.com/direct v1.1.0'
stdout '^patch.example.com/indirect v1.0.1'
! stdout '^patch.example.com/depofdirectpatch'
# Standard-library modules cannot be upgraded explicitly.
cp go.mod.orig go.mod
! go get -m std@patch
stderr 'explicit requirement on standard-library module std not allowed'
-- go.mod --
module x
require patch.example.com/direct v1.0.0
-- main.go --
package x
import _ "patch.example.com/direct"
env GO111MODULE=on
# Initially, we are at v1.0.0 for all dependencies.
cp go.mod go.mod.orig
go list -m all
stdout '^patch.example.com/direct v1.0.0'
stdout '^patch.example.com/indirect v1.0.0'
! stdout '^patch.example.com/depofdirectpatch'
# get -u=patch, with no arguments, should patch-update all dependencies,
# pulling in transitive dependencies and also patching those.
#
# TODO(golang.org/issue/26902): We should not update dependencies
# that don't affect the transitive import graph of the main module in any way.
cp go.mod.orig go.mod
go get -u=patch
go list -m all
stdout '^patch.example.com/direct v1.0.1'
stdout '^patch.example.com/indirect v1.0.1'
stdout '^patch.example.com/depofdirectpatch v1.0.1' # TODO: leave at v1.0.0
# 'get all@patch' should be equivalent to 'get -u=patch all'
cp go.mod.orig go.mod
go get all@patch
go list -m all
stdout '^patch.example.com/direct v1.0.1'
stdout '^patch.example.com/indirect v1.0.1'
stdout '^patch.example.com/depofdirectpatch v1.0.0'
# Requesting the direct dependency with -u=patch but without an explicit version
# should patch-update it and its dependencies.
cp go.mod.orig go.mod
go get -u=patch patch.example.com/direct
go list -m all
stdout '^patch.example.com/direct v1.0.1'
stdout '^patch.example.com/indirect v1.0.1'
stdout '^patch.example.com/depofdirectpatch v1.0.1' # TODO: leave at v1.0.0
# Requesting only the indirect dependency should not update the direct one.
cp go.mod.orig go.mod
go get -u=patch patch.example.com/indirect
go list -m all
stdout '^patch.example.com/direct v1.0.0'
stdout '^patch.example.com/indirect v1.0.1'
! stdout '^patch.example.com/depofdirectpatch'
# @patch should apply only to the specific module,
# but the result must reflect its upgraded requirements.
cp go.mod.orig go.mod
go get patch.example.com/direct@patch
go list -m all
stdout '^patch.example.com/direct v1.0.1'
stdout '^patch.example.com/indirect v1.0.0'
stdout '^patch.example.com/depofdirectpatch v1.0.0'
# An explicit @patch should override a general -u.
cp go.mod.orig go.mod
go get -u patch.example.com/direct@patch
go list -m all
stdout '^patch.example.com/direct v1.0.1'
stdout '^patch.example.com/indirect v1.1.0'
stdout '^patch.example.com/depofdirectpatch v1.0.1'
# An explicit @latest should override a general -u=patch.
cp go.mod.orig go.mod
go get -u=patch patch.example.com/direct@latest
go list -m all
stdout '^patch.example.com/direct v1.1.0'
stdout '^patch.example.com/indirect v1.0.1'
! stdout '^patch.example.com/depofdirectpatch'
# Standard-library packages cannot be upgraded explicitly.
cp go.mod.orig go.mod
! go get cmd/vet@patch
stderr 'cannot use pattern .* with explicit version'
# However, standard-library packages without explicit versions are fine.
go get -u=patch -d cmd/get
-- go.mod --
module x
require patch.example.com/direct v1.0.0
-- main.go --
package x
import _ "patch.example.com/direct"
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment