From: Sam Thanawalla Date: Tue, 29 Oct 2024 14:22:11 +0000 (+0000) Subject: cmd/go: add subdirectory support to go-import meta tag X-Git-Tag: go1.25rc1~500 X-Git-Url: http://www.git.cypherpunks.su/?a=commitdiff_plain;h=835e36fc7f631f74233edfd4ab43b6b56833db86;p=gostls13.git cmd/go: add subdirectory support to go-import meta tag This CL adds ability to specify a subdirectory in the go-import meta tag. A go-import meta tag now will support: Fixes: #34055 Cq-Include-Trybots: luci.golang.try:gotip-linux-amd64-longtest,gotip-windows-amd64-longtest Change-Id: Iedac520f97e0646254cc1bd2f97d5a9a5236829b Reviewed-on: https://go-review.googlesource.com/c/go/+/625577 Reviewed-by: Michael Matloob LUCI-TryBot-Result: Go LUCI Auto-Submit: Sam Thanawalla --- diff --git a/src/cmd/go/alldocs.go b/src/cmd/go/alldocs.go index 18ba20127d..e40d981bd4 100644 --- a/src/cmd/go/alldocs.go +++ b/src/cmd/go/alldocs.go @@ -2952,6 +2952,11 @@ // // // +// Starting in Go 1.25, an optional subdirectory will be recognized by the +// go command: +// +// +// // The import-prefix is the import path corresponding to the repository // root. It must be a prefix or an exact match of the package being // fetched with "go get". If it's not an exact match, another http @@ -2966,6 +2971,12 @@ // The repo-root is the root of the version control system // containing a scheme and not containing a .vcs qualifier. // +// The subdir specifies the directory within the repo-root where the +// Go module's root (including its go.mod file) is located. It allows +// you to organize your repository with the Go module code in a subdirectory +// rather than directly at the repository's root. +// If set, all vcs tags must be prefixed with "subdir". i.e. "subdir/v1.2.3" +// // For example, // // import "example.org/pkg/foo" @@ -2980,8 +2991,15 @@ // // // the go tool will verify that https://example.org/?go-get=1 contains the -// same meta tag and then git clone https://code.org/r/p/exproj into -// GOPATH/src/example.org. +// same meta tag and then download the code from the Git repository at https://code.org/r/p/exproj +// +// If that page contains the meta tag +// +// +// +// the go tool will verify that https://example.org/?go-get=1 contains the same meta +// tag and then download the code from the "foo/subdir" subdirectory within the Git repository +// at https://code.org/r/p/exproj // // Downloaded packages are stored in the module cache. // See https://golang.org/ref/mod#module-cache. diff --git a/src/cmd/go/internal/help/helpdoc.go b/src/cmd/go/internal/help/helpdoc.go index 47e5d73dd2..7f8565a3cb 100644 --- a/src/cmd/go/internal/help/helpdoc.go +++ b/src/cmd/go/internal/help/helpdoc.go @@ -241,6 +241,11 @@ The meta tag has the form: +Starting in Go 1.25, an optional subdirectory will be recognized by the +go command: + + + The import-prefix is the import path corresponding to the repository root. It must be a prefix or an exact match of the package being fetched with "go get". If it's not an exact match, another http @@ -255,6 +260,12 @@ The vcs is one of "bzr", "fossil", "git", "hg", "svn". The repo-root is the root of the version control system containing a scheme and not containing a .vcs qualifier. +The subdir specifies the directory within the repo-root where the +Go module's root (including its go.mod file) is located. It allows +you to organize your repository with the Go module code in a subdirectory +rather than directly at the repository's root. +If set, all vcs tags must be prefixed with "subdir". i.e. "subdir/v1.2.3" + For example, import "example.org/pkg/foo" @@ -269,8 +280,15 @@ If that page contains the meta tag the go tool will verify that https://example.org/?go-get=1 contains the -same meta tag and then git clone https://code.org/r/p/exproj into -GOPATH/src/example.org. +same meta tag and then download the code from the Git repository at https://code.org/r/p/exproj + +If that page contains the meta tag + + + +the go tool will verify that https://example.org/?go-get=1 contains the same meta +tag and then download the code from the "foo/subdir" subdirectory within the Git repository +at https://code.org/r/p/exproj Downloaded packages are stored in the module cache. See https://golang.org/ref/mod#module-cache. diff --git a/src/cmd/go/internal/modfetch/coderepo.go b/src/cmd/go/internal/modfetch/coderepo.go index 1d0c98f365..afed35c970 100644 --- a/src/cmd/go/internal/modfetch/coderepo.go +++ b/src/cmd/go/internal/modfetch/coderepo.go @@ -59,9 +59,11 @@ type codeRepo struct { } // newCodeRepo returns a Repo that reads the source code for the module with the -// given path, from the repo stored in code, with the root of the repo -// containing the path given by codeRoot. -func newCodeRepo(code codehost.Repo, codeRoot, path string) (Repo, error) { +// given path, from the repo stored in code. +// codeRoot gives the import path corresponding to the root of the repository, +// and subdir gives the subdirectory within the repo containing the module. +// If subdir is empty, the module is at the root of the repo. +func newCodeRepo(code codehost.Repo, codeRoot, subdir, path string) (Repo, error) { if !hasPathPrefix(path, codeRoot) { return nil, fmt.Errorf("mismatched repo: found %s for %s", codeRoot, path) } @@ -108,6 +110,16 @@ func newCodeRepo(code codehost.Repo, codeRoot, path string) (Repo, error) { // pathMajor = .v2 // pseudoMajor = v2 // + // Starting in 1.25, subdir may be passed in by the go-import meta tag. + // So it may be the case that: + // path = github.com/rsc/foo/v2 + // codeRoot = github.com/rsc/foo + // subdir = bar/subdir + // pathPrefix = github.com/rsc/foo + // pathMajor = /v2 + // pseudoMajor = v2 + // which means that codeDir = bar/subdir + codeDir := "" if codeRoot != path { if !hasPathPrefix(pathPrefix, codeRoot) { @@ -115,6 +127,9 @@ func newCodeRepo(code codehost.Repo, codeRoot, path string) (Repo, error) { } codeDir = strings.Trim(pathPrefix[len(codeRoot):], "/") } + if subdir != "" { + codeDir = filepath.ToSlash(filepath.Join(codeDir, subdir)) + } r := &codeRepo{ modPath: path, diff --git a/src/cmd/go/internal/modfetch/coderepo_test.go b/src/cmd/go/internal/modfetch/coderepo_test.go index aad78722c0..6859474660 100644 --- a/src/cmd/go/internal/modfetch/coderepo_test.go +++ b/src/cmd/go/internal/modfetch/coderepo_test.go @@ -884,6 +884,16 @@ var latestTests = []struct { path: "swtch.com/testmod", version: "v1.1.1", }, + { + vcs: "git", + path: "vcs-test.golang.org/go/gitreposubdir", + version: "v1.2.3", + }, + { + vcs: "git", + path: "vcs-test.golang.org/go/gitreposubdirv2/v2", + version: "v2.0.0", + }, } func TestLatest(t *testing.T) { @@ -950,7 +960,7 @@ func TestNonCanonicalSemver(t *testing.T) { }, } - cr, err := newCodeRepo(ch, root, root) + cr, err := newCodeRepo(ch, root, "", root) if err != nil { t.Fatal(err) } diff --git a/src/cmd/go/internal/modfetch/repo.go b/src/cmd/go/internal/modfetch/repo.go index c4dbf8342c..dd707ec264 100644 --- a/src/cmd/go/internal/modfetch/repo.go +++ b/src/cmd/go/internal/modfetch/repo.go @@ -240,7 +240,7 @@ func LookupLocal(ctx context.Context, codeRoot string, path string, dir string) if err != nil { return nil, err } - r, err := newCodeRepo(code, codeRoot, path) + r, err := newCodeRepo(code, codeRoot, "", path) if err == nil && traceRepo { r = newLoggingRepo(r) } @@ -319,7 +319,7 @@ func lookupDirect(ctx context.Context, path string) (Repo, error) { if err != nil { return nil, err } - return newCodeRepo(code, rr.Root, path) + return newCodeRepo(code, rr.Root, rr.SubDir, path) } func lookupCodeRepo(ctx context.Context, rr *vcs.RepoRoot, local bool) (codehost.Repo, error) { diff --git a/src/cmd/go/internal/vcs/discovery.go b/src/cmd/go/internal/vcs/discovery.go index bc2c5a35ac..8129fd4082 100644 --- a/src/cmd/go/internal/vcs/discovery.go +++ b/src/cmd/go/internal/vcs/discovery.go @@ -54,12 +54,17 @@ func parseMetaGoImports(r io.Reader, mod ModuleMode) ([]metaImport, error) { if attrValue(e.Attr, "name") != "go-import" { continue } - if f := strings.Fields(attrValue(e.Attr, "content")); len(f) == 3 { - imports = append(imports, metaImport{ + if f := strings.Fields(attrValue(e.Attr, "content")); len(f) == 3 || len(f) == 4 { + mi := metaImport{ Prefix: f[0], VCS: f[1], RepoRoot: f[2], - }) + } + // An optional subdirectory may be provided. + if len(f) == 4 { + mi.SubDir = f[3] + } + imports = append(imports, mi) } } diff --git a/src/cmd/go/internal/vcs/discovery_test.go b/src/cmd/go/internal/vcs/discovery_test.go index eb99fdf64c..e03eeaaa4c 100644 --- a/src/cmd/go/internal/vcs/discovery_test.go +++ b/src/cmd/go/internal/vcs/discovery_test.go @@ -18,15 +18,15 @@ var parseMetaGoImportsTests = []struct { { ``, IgnoreMod, - []metaImport{{"foo/bar", "git", "https://github.com/rsc/foo/bar"}}, + []metaImport{{"foo/bar", "git", "https://github.com/rsc/foo/bar", ""}}, }, { ` `, IgnoreMod, []metaImport{ - {"foo/bar", "git", "https://github.com/rsc/foo/bar"}, - {"baz/quux", "git", "http://github.com/rsc/baz/quux"}, + {"foo/bar", "git", "https://github.com/rsc/foo/bar", ""}, + {"baz/quux", "git", "http://github.com/rsc/baz/quux", ""}, }, }, { @@ -34,7 +34,7 @@ var parseMetaGoImportsTests = []struct { `, IgnoreMod, []metaImport{ - {"foo/bar", "git", "https://github.com/rsc/foo/bar"}, + {"foo/bar", "git", "https://github.com/rsc/foo/bar", ""}, }, }, { @@ -42,7 +42,7 @@ var parseMetaGoImportsTests = []struct { `, IgnoreMod, []metaImport{ - {"foo/bar", "git", "https://github.com/rsc/foo/bar"}, + {"foo/bar", "git", "https://github.com/rsc/foo/bar", ""}, }, }, { @@ -50,7 +50,7 @@ var parseMetaGoImportsTests = []struct { `, PreferMod, []metaImport{ - {"foo/bar", "mod", "http://github.com/rsc/baz/quux"}, + {"foo/bar", "mod", "http://github.com/rsc/baz/quux", ""}, }, }, { @@ -58,31 +58,31 @@ var parseMetaGoImportsTests = []struct { `, IgnoreMod, - []metaImport{{"foo/bar", "git", "https://github.com/rsc/foo/bar"}}, + []metaImport{{"foo/bar", "git", "https://github.com/rsc/foo/bar", ""}}, }, { ` `, IgnoreMod, - []metaImport{{"foo/bar", "git", "https://github.com/rsc/foo/bar"}}, + []metaImport{{"foo/bar", "git", "https://github.com/rsc/foo/bar", ""}}, }, { ``, IgnoreMod, - []metaImport{{"foo/bar", "git", "https://github.com/rsc/foo/bar"}}, + []metaImport{{"foo/bar", "git", "https://github.com/rsc/foo/bar", ""}}, }, { // XML doesn't like
. `Page Not Found
DRAFT
`, IgnoreMod, - []metaImport{{"chitin.io/chitin", "git", "https://github.com/chitin-io/chitin"}}, + []metaImport{{"chitin.io/chitin", "git", "https://github.com/chitin-io/chitin", ""}}, }, { ` `, IgnoreMod, - []metaImport{{"myitcv.io", "git", "https://github.com/myitcv/x"}}, + []metaImport{{"myitcv.io", "git", "https://github.com/myitcv/x", ""}}, }, { ` @@ -90,10 +90,20 @@ var parseMetaGoImportsTests = []struct { `, PreferMod, []metaImport{ - {"myitcv.io/blah2", "mod", "https://raw.githubusercontent.com/myitcv/pubx/master"}, - {"myitcv.io", "git", "https://github.com/myitcv/x"}, + {"myitcv.io/blah2", "mod", "https://raw.githubusercontent.com/myitcv/pubx/master", ""}, + {"myitcv.io", "git", "https://github.com/myitcv/x", ""}, }, }, + { + ``, + IgnoreMod, + []metaImport{{"foo/bar", "git", "https://github.com/rsc/foo/bar", "subdir"}}, + }, + { + ``, + IgnoreMod, + []metaImport{{"foo/bar", "git", "https://github.com/rsc/foo/bar", "subdir/path"}}, + }, } func TestParseMetaGoImports(t *testing.T) { diff --git a/src/cmd/go/internal/vcs/vcs.go b/src/cmd/go/internal/vcs/vcs.go index 1d10c7f6e9..acf9404aee 100644 --- a/src/cmd/go/internal/vcs/vcs.go +++ b/src/cmd/go/internal/vcs/vcs.go @@ -1056,7 +1056,8 @@ func checkGOVCS(vcs *Cmd, root string) error { // RepoRoot describes the repository root for a tree of source code. type RepoRoot struct { Repo string // repository URL, including scheme - Root string // import path corresponding to root of repo + Root string // import path corresponding to the SubDir + SubDir string // subdirectory within the repo (empty for root) IsCustom bool // defined by served tags (as opposed to hard-coded pattern) VCS *Cmd } @@ -1368,6 +1369,7 @@ func repoRootForImportDynamic(importPath string, mod ModuleMode, security web.Se rr := &RepoRoot{ Repo: repoURL, Root: mmi.Prefix, + SubDir: mmi.SubDir, IsCustom: true, VCS: vcs, } @@ -1457,9 +1459,9 @@ type fetchResult struct { } // metaImport represents the parsed tags from HTML files. +// content="prefix vcs reporoot subdir" /> tags from HTML files. type metaImport struct { - Prefix, VCS, RepoRoot string + Prefix, VCS, RepoRoot, SubDir string } // An ImportMismatchError is returned where metaImport/s are present diff --git a/src/cmd/go/internal/vcs/vcs_test.go b/src/cmd/go/internal/vcs/vcs_test.go index 2ce85ea210..c143154948 100644 --- a/src/cmd/go/internal/vcs/vcs_test.go +++ b/src/cmd/go/internal/vcs/vcs_test.go @@ -425,6 +425,28 @@ func TestMatchGoImport(t *testing.T) { path: "myitcv.io/other", mi: metaImport{Prefix: "myitcv.io", VCS: "git", RepoRoot: "https://github.com/myitcv/x"}, }, + { + imports: []metaImport{ + {Prefix: "example.com/user/foo", VCS: "git", RepoRoot: "https://example.com/repo/target", SubDir: "subdir"}, + }, + path: "example.com/user/foo", + mi: metaImport{Prefix: "example.com/user/foo", VCS: "git", RepoRoot: "https://example.com/repo/target", SubDir: "subdir"}, + }, + { + imports: []metaImport{ + {Prefix: "example.com/user/foo", VCS: "git", RepoRoot: "https://example.com/repo/target", SubDir: "foo/subdir"}, + }, + path: "example.com/user/foo", + mi: metaImport{Prefix: "example.com/user/foo", VCS: "git", RepoRoot: "https://example.com/repo/target", SubDir: "foo/subdir"}, + }, + { + imports: []metaImport{ + {Prefix: "example.com/user/foo", VCS: "git", RepoRoot: "https://example.com/repo/target", SubDir: "subdir"}, + {Prefix: "example.com/user/foo", VCS: "git", RepoRoot: "https://example.com/repo/target", SubDir: ""}, + }, + path: "example.com/user/foo", + err: errors.New("multiple meta tags match import path"), + }, } for _, test := range tests { diff --git a/src/cmd/go/testdata/script/mod_get_subdir.txt b/src/cmd/go/testdata/script/mod_get_subdir.txt new file mode 100644 index 0000000000..d31ecda881 --- /dev/null +++ b/src/cmd/go/testdata/script/mod_get_subdir.txt @@ -0,0 +1,77 @@ +# golang.org/issue/34055 +# Starting in Go 1.25, go-import meta tag support an optional subdirectory paramater. +# The corresponding go-import meta tag is specified as +# +# and contains the module in vcs-test.golang.org/git/gitreposubdir/foo/subdir. +# See testdata/vcstest/go/gitreposubdir.txt and testdata/vcstest/git/gitreposubdir.txt + +[short] skip 'builds a go program' +[!git] skip + +env GO111MODULE=on +env GOPROXY=direct +env GOSUMDB=off + +# Get the module without having to specify the subdir. +cd a +cp go.mod go.mod.orig +go get vcs-test.golang.org/go/gitreposubdir@v1.2.3 +exists $GOPATH/pkg/mod/vcs-test.golang.org/go/gitreposubdir@v1.2.3 +go get vcs-test.golang.org/go/gitreposubdirv2/v2@v2.0.0 +exists $GOPATH/pkg/mod/vcs-test.golang.org/go/gitreposubdirv2/v2@v2.0.0 + +# Import the module without having to specify the subdir. +cp go.mod.orig go.mod +go mod tidy + +# Run main.go which has the import. +go run main.go +stdout 'hello, world' +stdout 'hello, world v2' + +# Fail if subdir is specified in get. +! go get vcs-test.golang.org/go/gitreposubdir/foo/subdir +stderr 'module vcs-test.golang.org/go/gitreposubdir@upgrade found \(v1.2.3\), but does not contain package vcs-test.golang.org/go/gitreposubdir/foo/subdir' +! go get vcs-test.golang.org/go/gitreposubdirv2/v2/foo/subdir +stderr 'module vcs-test.golang.org/go/gitreposubdirv2/v2@upgrade found \(v2.0.0\), but does not contain package vcs-test.golang.org/go/gitreposubdirv2/v2/foo/subdir' + +# Fail if subdir is specified in the import. +cd ../b +! go mod tidy +stderr 'module vcs-test.golang.org/go/gitreposubdir@latest found \(v1.2.3\), but does not contain package vcs-test.golang.org/go/gitreposubdir/foo/subdir' +stderr 'module vcs-test.golang.org/go/gitreposubdirv2/v2@latest found \(v2.0.0\), but does not contain package vcs-test.golang.org/go/gitreposubdirv2/v2/foo/subdir' + +-- a/main.go -- +package main + +import ( + "fmt" + "vcs-test.golang.org/go/gitreposubdir" + "vcs-test.golang.org/go/gitreposubdirv2/v2" +) + +func main() { + fmt.Println(greeter.Hello()) + fmt.Println(greeterv2.Hello()) +} +-- a/go.mod -- +module example + +go 1.24 +-- b/main.go -- +package main + +import ( + "fmt" + "vcs-test.golang.org/go/gitreposubdir/foo/subdir" + "vcs-test.golang.org/go/gitreposubdirv2/v2/foo/subdir" +) + +func main() { + fmt.Println(greeter.Hello()) + fmt.Println(greeterv2.Hello()) +} +-- b/go.mod -- +module example + +go 1.24 diff --git a/src/cmd/go/testdata/vcstest/git/gitreposubdir.txt b/src/cmd/go/testdata/vcstest/git/gitreposubdir.txt new file mode 100644 index 0000000000..f9ba7629cc --- /dev/null +++ b/src/cmd/go/testdata/vcstest/git/gitreposubdir.txt @@ -0,0 +1,26 @@ +handle git + +env GIT_AUTHOR_NAME='Sam Thanawalla' +env GIT_AUTHOR_EMAIL='samthanawalla@google.com' +env GIT_COMMITTER_NAME=$GIT_AUTHOR_NAME +env GIT_COMMITTER_EMAIL=$GIT_AUTHOR_EMAIL + +at 2019-10-07T14:15:32-04:00 + +git init + +git add foo/subdir +git commit -m 'initial commit' +git branch -m master +git tag foo/subdir/v1.2.3 + +-- foo/subdir/go.mod -- +module vcs-test.golang.org/go/gitreposubdir + +go 1.23 +-- foo/subdir/hello.go -- +package greeter + +func Hello() string { + return "hello, world" +} diff --git a/src/cmd/go/testdata/vcstest/git/gitreposubdirv2.txt b/src/cmd/go/testdata/vcstest/git/gitreposubdirv2.txt new file mode 100644 index 0000000000..35b2f9ed3a --- /dev/null +++ b/src/cmd/go/testdata/vcstest/git/gitreposubdirv2.txt @@ -0,0 +1,31 @@ +handle git + +env GIT_AUTHOR_NAME='Sam Thanawalla' +env GIT_AUTHOR_EMAIL='samthanawalla@google.com' +env GIT_COMMITTER_NAME=$GIT_AUTHOR_NAME +env GIT_COMMITTER_EMAIL=$GIT_AUTHOR_EMAIL + +at 2019-10-07T14:15:32-04:00 + +git init + +git add subdir +git commit -m 'initial commit' +git branch -m master +git tag subdir/v2.0.0 +git show-ref --tags --heads +cmp stdout .git-refs + +-- .git-refs -- +5212d800bfd1f6377da46aee6cbceca2f60d4ea6 refs/heads/master +5212d800bfd1f6377da46aee6cbceca2f60d4ea6 refs/tags/subdir/v2.0.0 +-- subdir/go.mod -- +module vcs-test.golang.org/go/gitreposubdirv2/v2 + +go 1.23 +-- subdir/hello.go -- +package greeterv2 + +func Hello() string { + return "hello, world v2" +} diff --git a/src/cmd/go/testdata/vcstest/go/gitreposubdir.txt b/src/cmd/go/testdata/vcstest/go/gitreposubdir.txt new file mode 100644 index 0000000000..53a16078d1 --- /dev/null +++ b/src/cmd/go/testdata/vcstest/go/gitreposubdir.txt @@ -0,0 +1,6 @@ +handle dir + +-- index.html -- + + + diff --git a/src/cmd/go/testdata/vcstest/go/gitreposubdirv2.txt b/src/cmd/go/testdata/vcstest/go/gitreposubdirv2.txt new file mode 100644 index 0000000000..da77226c75 --- /dev/null +++ b/src/cmd/go/testdata/vcstest/go/gitreposubdirv2.txt @@ -0,0 +1,6 @@ +handle dir + +-- v2/index.html -- + + +