src/cmd/go/internal/modcmd/edit.go | 10 ++++++++-- src/cmd/go/internal/modfetch/codehost/git.go | 20 ++++++++++---------- src/cmd/go/internal/modfetch/codehost/vcs.go | 20 ++++++++++---------- src/cmd/go/internal/modget/query.go | 5 ++++- src/cmd/go/internal/modload/build.go | 12 ++++++++++-- src/cmd/go/internal/modload/list.go | 30 ++++++++++++++++++++++++++++-- src/cmd/go/internal/toolchain/select.go | 7 +++++-- src/cmd/go/internal/vcs/vcs.go | 353 ++++++----------------------------------------------- src/cmd/go/internal/vcs/vcs_test.go | 36 ++++++++++++++++++++++++++++++++++++ src/cmd/go/internal/workcmd/edit.go | 5 ++++- diff --git a/src/cmd/go/internal/modcmd/edit.go b/src/cmd/go/internal/modcmd/edit.go index f73269378a11e161edadf060a125d545d02eaa7d..7bc92990298e374c0cd033da5bf344744bd9facf 100644 --- a/src/cmd/go/internal/modcmd/edit.go +++ b/src/cmd/go/internal/modcmd/edit.go @@ -321,7 +321,10 @@ } // parsePathVersion parses -flag=arg expecting arg to be path@version. func parsePathVersion(flag, arg string) (path, version string) { - before, after, found := strings.Cut(arg, "@") + before, after, found, err := modload.ParsePathVersion(arg) + if err != nil { + base.Fatalf("go: -%s=%s: %v", flag, arg, err) + } if !found { base.Fatalf("go: -%s=%s: need path@version", flag, arg) } @@ -355,7 +358,10 @@ func parsePathVersionOptional(adj, arg string, allowDirPath bool) (path, version string, err error) { if allowDirPath && modfile.IsDirectoryPath(arg) { return arg, "", nil } - before, after, found := strings.Cut(arg, "@") + before, after, found, err := modload.ParsePathVersion(arg) + if err != nil { + return "", "", err + } if !found { path = arg } else { diff --git a/src/cmd/go/internal/modfetch/codehost/git.go b/src/cmd/go/internal/modfetch/codehost/git.go index b445ac248622021dcf97e72ca6c7df768a7e9d97..89a4cbffd771cfeb981fa60525ed1a39ce8e60c2 100644 --- a/src/cmd/go/internal/modfetch/codehost/git.go +++ b/src/cmd/go/internal/modfetch/codehost/git.go @@ -248,7 +248,7 @@ if err != nil { r.refsErr = err return } - out, gitErr := r.runGit(ctx, "git", "ls-remote", "-q", r.remote) + out, gitErr := r.runGit(ctx, "git", "ls-remote", "-q", "--end-of-options", r.remote) release() if gitErr != nil { @@ -534,7 +534,7 @@ tag, fromTag := strings.CutPrefix(ref, "refs/tags/") if fromTag && !slices.Contains(info.Tags, tag) { // The local repo includes the commit hash we want, but it is missing // the corresponding tag. Add that tag and try again. - _, err := r.runGit(ctx, "git", "tag", tag, hash) + _, err := r.runGit(ctx, "git", "tag", "--end-of-options", tag, hash) if err != nil { return nil, err } @@ -583,7 +583,7 @@ // We explicitly set protocol.version=2 for this command to work around // an apparent Git bug introduced in Git 2.21 (commit 61c771), // which causes the handler for protocol version 1 to sometimes miss // tags that point to the requested commit (see https://go.dev/issue/56881). - _, err = r.runGit(ctx, "git", "-c", "protocol.version=2", "fetch", "-f", "--depth=1", r.remote, refspec) + _, err = r.runGit(ctx, "git", "-c", "protocol.version=2", "fetch", "-f", "--depth=1", "--end-of-options", r.remote, refspec) release() if err == nil { @@ -629,12 +629,12 @@ return err } defer release() - if _, err := r.runGit(ctx, "git", "fetch", "-f", r.remote, "refs/heads/*:refs/heads/*", "refs/tags/*:refs/tags/*"); err != nil { + if _, err := r.runGit(ctx, "git", "fetch", "-f", "--end-of-options", r.remote, "refs/heads/*:refs/heads/*", "refs/tags/*:refs/tags/*"); err != nil { return err } if _, err := os.Stat(filepath.Join(r.dir, "shallow")); err == nil { - if _, err := r.runGit(ctx, "git", "fetch", "--unshallow", "-f", r.remote); err != nil { + if _, err := r.runGit(ctx, "git", "fetch", "--unshallow", "-f", "--end-of-options", r.remote); err != nil { return err } } @@ -647,7 +647,7 @@ // statLocal returns a new RevInfo describing rev in the local git repository. // It uses version as info.Version. func (r *gitRepo) statLocal(ctx context.Context, version, rev string) (*RevInfo, error) { - out, err := r.runGit(ctx, "git", "-c", "log.showsignature=false", "log", "--no-decorate", "-n1", "--format=format:%H %ct %D", rev, "--") + out, err := r.runGit(ctx, "git", "-c", "log.showsignature=false", "log", "--no-decorate", "-n1", "--format=format:%H %ct %D", "--end-of-options", rev, "--") if err != nil { // Return info with Origin.RepoSum if possible to allow caching of negative lookup. var info *RevInfo @@ -737,7 +737,7 @@ info, err := r.Stat(ctx, rev) // download rev into local git repo if err != nil { return nil, err } - out, err := r.runGit(ctx, "git", "cat-file", "blob", info.Name+":"+file) + out, err := r.runGit(ctx, "git", "cat-file", "--end-of-options", "blob", info.Name+":"+file) if err != nil { return nil, fs.ErrNotExist } @@ -755,7 +755,7 @@ // describe sets tag and err using 'git for-each-ref' and reports whether the // result is definitive. describe := func() (definitive bool) { var out []byte - out, err = r.runGit(ctx, "git", "for-each-ref", "--format", "%(refname)", "refs/tags", "--merged", rev) + out, err = r.runGit(ctx, "git", "for-each-ref", "--format=%(refname)", "--merged="+rev) if err != nil { return true } @@ -904,7 +904,7 @@ func (r *gitRepo) ReadZip(ctx context.Context, rev, subdir string, maxSize int64) (zip io.ReadCloser, err error) { // TODO: Use maxSize or drop it. args := []string{} if subdir != "" { - args = append(args, "--", subdir) + args = append(args, subdir) } info, err := r.Stat(ctx, rev) // download rev into local git repo if err != nil { @@ -926,7 +926,7 @@ // it is running on a Windows system or not, in an attempt to normalize // text file line endings. Setting -c core.autocrlf=input means only // translate files on the way into the repo, not on the way out (archive). // The -c core.eol=lf should be unnecessary but set it anyway. - archive, err := r.runGit(ctx, "git", "-c", "core.autocrlf=input", "-c", "core.eol=lf", "archive", "--format=zip", "--prefix=prefix/", info.Name, args) + archive, err := r.runGit(ctx, "git", "-c", "core.autocrlf=input", "-c", "core.eol=lf", "archive", "--format=zip", "--prefix=prefix/", "--end-of-options", info.Name, args) if err != nil { if bytes.Contains(err.(*RunError).Stderr, []byte("did not match any files")) { return nil, fs.ErrNotExist diff --git a/src/cmd/go/internal/modfetch/codehost/vcs.go b/src/cmd/go/internal/modfetch/codehost/vcs.go index 8e59479339be7f13a904a3886eb1117d0e129c9b..4e4524395043ab745cc845ac04cbd96fa8dad59d 100644 --- a/src/cmd/go/internal/modfetch/codehost/vcs.go +++ b/src/cmd/go/internal/modfetch/codehost/vcs.go @@ -176,20 +176,20 @@ }, branchRE: re(`(?m)^[^\n]+$`), badLocalRevRE: re(`(?m)^(tip)$`), statLocal: func(rev, remote string) []string { - return []string{"hg", "log", "-l1", "-r", rev, "--template", "{node} {date|hgdate} {tags}"} + return []string{"hg", "log", "-l1", fmt.Sprintf("--rev=%s", rev), "--template", "{node} {date|hgdate} {tags}"} }, parseStat: hgParseStat, fetch: []string{"hg", "pull", "-f"}, latest: "tip", readFile: func(rev, file, remote string) []string { - return []string{"hg", "cat", "-r", rev, file} + return []string{"hg", "cat", fmt.Sprintf("--rev=%s", rev), "--", file} }, readZip: func(rev, subdir, remote, target string) []string { pattern := []string{} if subdir != "" { - pattern = []string{"-I", subdir + "/**"} + pattern = []string{fmt.Sprintf("--include=%s", subdir+"/**")} } - return str.StringList("hg", "archive", "-t", "zip", "--no-decode", "-r", rev, "--prefix=prefix/", pattern, "--", target) + return str.StringList("hg", "archive", "-t", "zip", "--no-decode", fmt.Sprintf("--rev=%s", rev), "--prefix=prefix/", pattern, "--", target) }, }, @@ -229,19 +229,19 @@ }, tagRE: re(`(?m)^\S+`), badLocalRevRE: re(`^revno:-`), statLocal: func(rev, remote string) []string { - return []string{"bzr", "log", "-l1", "--long", "--show-ids", "-r", rev} + return []string{"bzr", "log", "-l1", "--long", "--show-ids", fmt.Sprintf("--revision=%s", rev)} }, parseStat: bzrParseStat, latest: "revno:-1", readFile: func(rev, file, remote string) []string { - return []string{"bzr", "cat", "-r", rev, file} + return []string{"bzr", "cat", fmt.Sprintf("--revision=%s", rev), "--", file} }, readZip: func(rev, subdir, remote, target string) []string { extra := []string{} if subdir != "" { extra = []string{"./" + subdir} } - return str.StringList("bzr", "export", "--format=zip", "-r", rev, "--root=prefix/", "--", target, extra) + return str.StringList("bzr", "export", "--format=zip", fmt.Sprintf("--revision=%s", rev), "--root=prefix/", "--", target, extra) }, }, @@ -256,17 +256,17 @@ return []string{"fossil", "tag", "-R", ".fossil", "list"} }, tagRE: re(`XXXTODO`), statLocal: func(rev, remote string) []string { - return []string{"fossil", "info", "-R", ".fossil", rev} + return []string{"fossil", "info", "-R", ".fossil", "--", rev} }, parseStat: fossilParseStat, latest: "trunk", readFile: func(rev, file, remote string) []string { - return []string{"fossil", "cat", "-R", ".fossil", "-r", rev, file} + return []string{"fossil", "cat", "-R", ".fossil", fmt.Sprintf("-r=%s", rev), "--", file} }, readZip: func(rev, subdir, remote, target string) []string { extra := []string{} if subdir != "" && !strings.ContainsAny(subdir, "*?[],") { - extra = []string{"--include", subdir} + extra = []string{fmt.Sprintf("--include=%s", subdir)} } // Note that vcsRepo.ReadZip below rewrites this command // to run in a different directory, to work around a fossil bug. diff --git a/src/cmd/go/internal/modget/query.go b/src/cmd/go/internal/modget/query.go index f95b503d8f60346c3ef5e7e45f42f699710744b1..0f275bd0adcf3e5b56e00b7e811b9c229dcdc9bd 100644 --- a/src/cmd/go/internal/modget/query.go +++ b/src/cmd/go/internal/modget/query.go @@ -139,7 +139,10 @@ // newQuery returns a new query parsed from the raw argument, // which must be either path or path@version. func newQuery(raw string) (*query, error) { - pattern, rawVers, found := strings.Cut(raw, "@") + pattern, rawVers, found, err := modload.ParsePathVersion(raw) + if err != nil { + return nil, err + } if found && (strings.Contains(rawVers, "@") || rawVers == "") { return nil, fmt.Errorf("invalid module version syntax %q", raw) } diff --git a/src/cmd/go/internal/modload/build.go b/src/cmd/go/internal/modload/build.go index 6e30afd5247b36f01e5586cd267df176641566aa..b04575ee8cd2c295c5ed88b3cccff1a47ee86ee8 100644 --- a/src/cmd/go/internal/modload/build.go +++ b/src/cmd/go/internal/modload/build.go @@ -12,7 +12,6 @@ "fmt" "io/fs" "os" "path/filepath" - "strings" "cmd/go/internal/base" "cmd/go/internal/cfg" @@ -88,7 +87,16 @@ if !Enabled() { return nil } - if path, vers, found := strings.Cut(path, "@"); found { + path, vers, found, err := ParsePathVersion(path) + if err != nil { + return &modinfo.ModulePublic{ + Path: path, + Error: &modinfo.ModuleError{ + Err: err.Error(), + }, + } + } + if found { m := module.Version{Path: path, Version: vers} return moduleInfo(ctx, nil, m, 0, nil) } diff --git a/src/cmd/go/internal/modload/list.go b/src/cmd/go/internal/modload/list.go index 53cb6c2ffe1406111dac713c44a49075a32d4f55..32c7847bd3b27426ba024736ba7f319c520a604b 100644 --- a/src/cmd/go/internal/modload/list.go +++ b/src/cmd/go/internal/modload/list.go @@ -150,7 +150,11 @@ base.Fatalf("go: cannot match %q: %v", arg, ErrNoModRoot) } continue } - if path, vers, found := strings.Cut(arg, "@"); found { + path, vers, found, err := ParsePathVersion(arg) + if err != nil { + base.Fatalf("go: %v", err) + } + if found { if vers == "upgrade" || vers == "patch" { if _, ok := rs.rootSelected(path); !ok || rs.pruning == unpruned { needFullGraph = true @@ -176,7 +180,11 @@ } matchedModule := map[module.Version]bool{} for _, arg := range args { - if path, vers, found := strings.Cut(arg, "@"); found { + path, vers, found, err := ParsePathVersion(arg) + if err != nil { + base.Fatalf("go: %v", err) + } + if found { var current string if mg == nil { current, _ = rs.rootSelected(path) @@ -319,3 +327,21 @@ } return &modinfo.ModuleError{Err: err.Error()} } + +// ParsePathVersion parses arg expecting arg to be path@version. If there is no +// '@' in arg, found is false, vers is "", and path is arg. This mirrors the +// typical usage of strings.Cut. ParsePathVersion is meant to be a general +// replacement for strings.Cut in module version parsing. If the version is +// invalid, an error is returned. The version is considered invalid if it is +// prefixed with '-' or '/', which can cause security problems when constructing +// commands to execute that use the version. +func ParsePathVersion(arg string) (path, vers string, found bool, err error) { + path, vers, found = strings.Cut(arg, "@") + if !found { + return arg, "", false, nil + } + if len(vers) > 0 && (vers[0] == '-' || vers[0] == '/') { + return "", "", false, fmt.Errorf("invalid module version %q", vers) + } + return path, vers, true, nil +} diff --git a/src/cmd/go/internal/toolchain/select.go b/src/cmd/go/internal/toolchain/select.go index e8712613366e8c8b6d9d596ac9efff23b6db2deb..198cdc706f477d615c30f879bd076e5744af7f51 100644 --- a/src/cmd/go/internal/toolchain/select.go +++ b/src/cmd/go/internal/toolchain/select.go @@ -670,7 +670,10 @@ if !strings.Contains(pkgArg, "@") || build.IsLocalImport(pkgArg) || filepath.IsAbs(pkgArg) { return } - path, version, _ := strings.Cut(pkgArg, "@") + path, version, _, err := modload.ParsePathVersion(pkgArg) + if err != nil { + base.Fatalf("go: %v", err) + } if path == "" || version == "" || gover.IsToolchain(path) { return } @@ -705,7 +708,7 @@ // Don't check for retractions if a specific revision is requested. allowed = nil } noneSelected := func(path string) (version string) { return "none" } - _, err := modload.QueryPackages(ctx, path, version, noneSelected, allowed) + _, err = modload.QueryPackages(ctx, path, version, noneSelected, allowed) if errors.Is(err, gover.ErrTooNew) { // Run early switch, same one go install or go run would eventually do, // if it understood all the command-line flags. diff --git a/src/cmd/go/internal/vcs/vcs.go b/src/cmd/go/internal/vcs/vcs.go index 7e081eb41a1c4edd67493c9231ca684af5f24dff..b8204c7619c905b71316024e246d2724eabd0702 100644 --- a/src/cmd/go/internal/vcs/vcs.go +++ b/src/cmd/go/internal/vcs/vcs.go @@ -17,7 +17,6 @@ urlpkg "net/url" "os" "os/exec" "path/filepath" - "regexp" "strconv" "strings" "sync" @@ -41,20 +40,10 @@ Cmd string // name of binary to invoke command Env []string // any environment values to set/override RootNames []rootName // filename and mode indicating the root of a checkout directory - CreateCmd []string // commands to download a fresh copy of a repository - DownloadCmd []string // commands to download updates into an existing repository - - TagCmd []tagCmd // commands to list tags - TagLookupCmd []tagCmd // commands to lookup tags before running tagSyncCmd - TagSyncCmd []string // commands to sync to specific tag - TagSyncDefault []string // commands to sync to default tag - Scheme []string PingCmd string - RemoteRepo func(v *Cmd, rootDir string) (remoteRepo string, err error) - ResolveRepo func(v *Cmd, rootDir, remoteRepo string) (realRepo string, err error) - Status func(v *Cmd, rootDir string) (Status, error) + Status func(v *Cmd, rootDir string) (Status, error) } // Status is the current state of a local repository. @@ -157,40 +146,16 @@ var vcsHg = &Cmd{ Name: "Mercurial", Cmd: "hg", - // HGPLAIN=1 turns off additional output that a user may have enabled via - // config options or certain extensions. - Env: []string{"HGPLAIN=1"}, + // HGPLAIN=+strictflags turns off additional output that a user may have + // enabled via config options or certain extensions. + Env: []string{"HGPLAIN=+strictflags"}, RootNames: []rootName{ {filename: ".hg", isDir: true}, }, - CreateCmd: []string{"clone -U -- {repo} {dir}"}, - DownloadCmd: []string{"pull"}, - - // We allow both tag and branch names as 'tags' - // for selecting a version. This lets people have - // a go.release.r60 branch and a go1 branch - // and make changes in both, without constantly - // editing .hgtags. - TagCmd: []tagCmd{ - {"tags", `^(\S+)`}, - {"branches", `^(\S+)`}, - }, - TagSyncCmd: []string{"update -r {tag}"}, - TagSyncDefault: []string{"update default"}, - - Scheme: []string{"https", "http", "ssh"}, - PingCmd: "identify -- {scheme}://{repo}", - RemoteRepo: hgRemoteRepo, - Status: hgStatus, -} - -func hgRemoteRepo(vcsHg *Cmd, rootDir string) (remoteRepo string, err error) { - out, err := vcsHg.runOutput(rootDir, "paths default") - if err != nil { - return "", err - } - return strings.TrimSpace(string(out)), nil + Scheme: []string{"https", "http", "ssh"}, + PingCmd: "identify -- {scheme}://{repo}", + Status: hgStatus, } func hgStatus(vcsHg *Cmd, rootDir string) (Status, error) { @@ -253,25 +218,6 @@ RootNames: []rootName{ {filename: ".git", isDir: true}, }, - CreateCmd: []string{"clone -- {repo} {dir}", "-go-internal-cd {dir} submodule update --init --recursive"}, - DownloadCmd: []string{"pull --ff-only", "submodule update --init --recursive"}, - - TagCmd: []tagCmd{ - // tags/xxx matches a git tag named xxx - // origin/xxx matches a git branch named xxx on the default remote repository - {"show-ref", `(?:tags|origin)/(\S+)$`}, - }, - TagLookupCmd: []tagCmd{ - {"show-ref tags/{tag} origin/{tag}", `((?:tags|origin)/\S+)$`}, - }, - TagSyncCmd: []string{"checkout {tag}", "submodule update --init --recursive"}, - // both createCmd and downloadCmd update the working dir. - // No need to do more here. We used to 'checkout master' - // but that doesn't work if the default branch is not named master. - // DO NOT add 'checkout master' here. - // See golang.org/issue/9032. - TagSyncDefault: []string{"submodule update --init --recursive"}, - Scheme: []string{"git", "https", "http", "git+ssh", "ssh"}, // Leave out the '--' separator in the ls-remote command: git 2.7.4 does not @@ -280,54 +226,7 @@ // without it because the {scheme} value comes from the predefined list above. // See golang.org/issue/33836. PingCmd: "ls-remote {scheme}://{repo}", - RemoteRepo: gitRemoteRepo, - Status: gitStatus, -} - -// scpSyntaxRe matches the SCP-like addresses used by Git to access -// repositories by SSH. -var scpSyntaxRe = lazyregexp.New(`^(\w+)@([\w.-]+):(.*)$`) - -func gitRemoteRepo(vcsGit *Cmd, rootDir string) (remoteRepo string, err error) { - const cmd = "config remote.origin.url" - outb, err := vcsGit.run1(rootDir, cmd, nil, false) - if err != nil { - // if it doesn't output any message, it means the config argument is correct, - // but the config value itself doesn't exist - if outb != nil && len(outb) == 0 { - return "", errors.New("remote origin not found") - } - return "", err - } - out := strings.TrimSpace(string(outb)) - - var repoURL *urlpkg.URL - if m := scpSyntaxRe.FindStringSubmatch(out); m != nil { - // Match SCP-like syntax and convert it to a URL. - // Eg, "git@github.com:user/repo" becomes - // "ssh://git@github.com/user/repo". - repoURL = &urlpkg.URL{ - Scheme: "ssh", - User: urlpkg.User(m[1]), - Host: m[2], - Path: m[3], - } - } else { - repoURL, err = urlpkg.Parse(out) - if err != nil { - return "", err - } - } - - // Iterate over insecure schemes too, because this function simply - // reports the state of the repo. If we can't see insecure schemes then - // we can't report the actual repo URL. - for _, s := range vcsGit.Scheme { - if repoURL.Scheme == s { - return repoURL.String(), nil - } - } - return "", errors.New("unable to parse output of git " + cmd) + Status: gitStatus, } func gitStatus(vcsGit *Cmd, rootDir string) (Status, error) { @@ -367,62 +266,9 @@ RootNames: []rootName{ {filename: ".bzr", isDir: true}, }, - CreateCmd: []string{"branch -- {repo} {dir}"}, - - // Without --overwrite bzr will not pull tags that changed. - // Replace by --overwrite-tags after http://pad.lv/681792 goes in. - DownloadCmd: []string{"pull --overwrite"}, - - TagCmd: []tagCmd{{"tags", `^(\S+)`}}, - TagSyncCmd: []string{"update -r {tag}"}, - TagSyncDefault: []string{"update -r revno:-1"}, - - Scheme: []string{"https", "http", "bzr", "bzr+ssh"}, - PingCmd: "info -- {scheme}://{repo}", - RemoteRepo: bzrRemoteRepo, - ResolveRepo: bzrResolveRepo, - Status: bzrStatus, -} - -func bzrRemoteRepo(vcsBzr *Cmd, rootDir string) (remoteRepo string, err error) { - outb, err := vcsBzr.runOutput(rootDir, "config parent_location") - if err != nil { - return "", err - } - return strings.TrimSpace(string(outb)), nil -} - -func bzrResolveRepo(vcsBzr *Cmd, rootDir, remoteRepo string) (realRepo string, err error) { - outb, err := vcsBzr.runOutput(rootDir, "info "+remoteRepo) - if err != nil { - return "", err - } - out := string(outb) - - // Expect: - // ... - // (branch root|repository branch): - // ... - - found := false - for _, prefix := range []string{"\n branch root: ", "\n repository branch: "} { - i := strings.Index(out, prefix) - if i >= 0 { - out = out[i+len(prefix):] - found = true - break - } - } - if !found { - return "", fmt.Errorf("unable to parse output of bzr info") - } - - i := strings.Index(out, "\n") - if i < 0 { - return "", fmt.Errorf("unable to parse output of bzr info") - } - out = out[:i] - return strings.TrimSpace(out), nil + Scheme: []string{"https", "http", "bzr", "bzr+ssh"}, + PingCmd: "info -- {scheme}://{repo}", + Status: bzrStatus, } func bzrStatus(vcsBzr *Cmd, rootDir string) (Status, error) { @@ -490,46 +336,12 @@ RootNames: []rootName{ {filename: ".svn", isDir: true}, }, - CreateCmd: []string{"checkout -- {repo} {dir}"}, - DownloadCmd: []string{"update"}, - // There is no tag command in subversion. // The branch information is all in the path names. - Scheme: []string{"https", "http", "svn", "svn+ssh"}, - PingCmd: "info -- {scheme}://{repo}", - RemoteRepo: svnRemoteRepo, - Status: svnStatus, -} - -func svnRemoteRepo(vcsSvn *Cmd, rootDir string) (remoteRepo string, err error) { - outb, err := vcsSvn.runOutput(rootDir, "info") - if err != nil { - return "", err - } - out := string(outb) - - // Expect: - // - // ... - // URL: - // ... - // - // Note that we're not using the Repository Root line, - // because svn allows checking out subtrees. - // The URL will be the URL of the subtree (what we used with 'svn co') - // while the Repository Root may be a much higher parent. - i := strings.Index(out, "\nURL: ") - if i < 0 { - return "", fmt.Errorf("unable to parse output of svn info") - } - out = out[i+len("\nURL: "):] - i = strings.Index(out, "\n") - if i < 0 { - return "", fmt.Errorf("unable to parse output of svn info") - } - out = out[:i] - return strings.TrimSpace(out), nil + Scheme: []string{"https", "http", "svn", "svn+ssh"}, + PingCmd: "info -- {scheme}://{repo}", + Status: svnStatus, } func svnStatus(vcsSvn *Cmd, rootDir string) (Status, error) { @@ -574,24 +386,8 @@ {filename: ".fslckout", isDir: false}, {filename: "_FOSSIL_", isDir: false}, }, - CreateCmd: []string{"-go-internal-mkdir {dir} clone -- {repo} " + filepath.Join("{dir}", fossilRepoName), "-go-internal-cd {dir} open .fossil"}, - DownloadCmd: []string{"up"}, - - TagCmd: []tagCmd{{"tag ls", `(.*)`}}, - TagSyncCmd: []string{"up tag:{tag}"}, - TagSyncDefault: []string{"up trunk"}, - - Scheme: []string{"https", "http"}, - RemoteRepo: fossilRemoteRepo, - Status: fossilStatus, -} - -func fossilRemoteRepo(vcsFossil *Cmd, rootDir string) (remoteRepo string, err error) { - out, err := vcsFossil.runOutput(rootDir, "remote-url") - if err != nil { - return "", err - } - return strings.TrimSpace(string(out)), nil + Scheme: []string{"https", "http"}, + Status: fossilStatus, } var errFossilInfo = errors.New("unable to parse output of fossil info") @@ -692,7 +488,7 @@ for i, arg := range args { args[i] = expand(m, arg) } - if len(args) >= 2 && args[0] == "-go-internal-mkdir" { + if len(args) >= 2 && args[0] == "--go-internal-mkdir" { var err error if filepath.IsAbs(args[1]) { err = os.Mkdir(args[1], fs.ModePerm) @@ -705,7 +501,7 @@ } args = args[2:] } - if len(args) >= 2 && args[0] == "-go-internal-cd" { + if len(args) >= 2 && args[0] == "--go-internal-cd" { if filepath.IsAbs(args[1]) { dir = args[1] } else { @@ -764,99 +560,6 @@ } defer release() return v.runVerboseOnly(dir, v.PingCmd, "scheme", scheme, "repo", repo) -} - -// Create creates a new copy of repo in dir. -// The parent of dir must exist; dir must not. -func (v *Cmd) Create(dir, repo string) error { - release, err := base.AcquireNet() - if err != nil { - return err - } - defer release() - - for _, cmd := range v.CreateCmd { - if err := v.run(filepath.Dir(dir), cmd, "dir", dir, "repo", repo); err != nil { - return err - } - } - return nil -} - -// Download downloads any new changes for the repo in dir. -func (v *Cmd) Download(dir string) error { - release, err := base.AcquireNet() - if err != nil { - return err - } - defer release() - - for _, cmd := range v.DownloadCmd { - if err := v.run(dir, cmd); err != nil { - return err - } - } - return nil -} - -// Tags returns the list of available tags for the repo in dir. -func (v *Cmd) Tags(dir string) ([]string, error) { - var tags []string - for _, tc := range v.TagCmd { - out, err := v.runOutput(dir, tc.cmd) - if err != nil { - return nil, err - } - re := regexp.MustCompile(`(?m-s)` + tc.pattern) - for _, m := range re.FindAllStringSubmatch(string(out), -1) { - tags = append(tags, m[1]) - } - } - return tags, nil -} - -// TagSync syncs the repo in dir to the named tag, -// which either is a tag returned by tags or is v.tagDefault. -func (v *Cmd) TagSync(dir, tag string) error { - if v.TagSyncCmd == nil { - return nil - } - if tag != "" { - for _, tc := range v.TagLookupCmd { - out, err := v.runOutput(dir, tc.cmd, "tag", tag) - if err != nil { - return err - } - re := regexp.MustCompile(`(?m-s)` + tc.pattern) - m := re.FindStringSubmatch(string(out)) - if len(m) > 1 { - tag = m[1] - break - } - } - } - - release, err := base.AcquireNet() - if err != nil { - return err - } - defer release() - - if tag == "" && v.TagSyncDefault != nil { - for _, cmd := range v.TagSyncDefault { - if err := v.run(dir, cmd); err != nil { - return err - } - } - return nil - } - - for _, cmd := range v.TagSyncCmd { - if err := v.run(dir, cmd, "tag", tag); err != nil { - return err - } - } - return nil } // A vcsPath describes how to convert an import path into a @@ -1385,6 +1088,10 @@ return nil, fmt.Errorf("%s and %s disagree about go-import for %s", resp.URL, url, mmi.Prefix) } } + if err := validateRepoSubDir(mmi.SubDir); err != nil { + return nil, fmt.Errorf("%s: invalid subdirectory %q: %v", resp.URL, mmi.SubDir, err) + } + if err := validateRepoRoot(mmi.RepoRoot); err != nil { return nil, fmt.Errorf("%s: invalid repo root %q: %v", resp.URL, mmi.RepoRoot, err) } @@ -1414,6 +1121,22 @@ IsCustom: true, VCS: vcs, } return rr, nil +} + +// validateRepoSubDir returns an error if subdir is not a valid subdirectory path. +// We consider a subdirectory path to be valid as long as it doesn't have a leading +// slash (/) or hyphen (-). +func validateRepoSubDir(subdir string) error { + if subdir == "" { + return nil + } + if subdir[0] == '/' { + return errors.New("leading slash") + } + if subdir[0] == '-' { + return errors.New("leading hyphen") + } + return nil } // validateRepoRoot returns an error if repoRoot does not seem to be diff --git a/src/cmd/go/internal/vcs/vcs_test.go b/src/cmd/go/internal/vcs/vcs_test.go index 361d85bcfb326f11331601614e107b79233e3404..ab70e517e277f8d594258a7500c778676a48d232 100644 --- a/src/cmd/go/internal/vcs/vcs_test.go +++ b/src/cmd/go/internal/vcs/vcs_test.go @@ -507,6 +507,42 @@ } } } +func TestValidateRepoSubDir(t *testing.T) { + tests := []struct { + subdir string + ok bool + }{ + { + subdir: "", + ok: true, + }, + { + subdir: "sub/dir", + ok: true, + }, + { + subdir: "/leading/slash", + ok: false, + }, + { + subdir: "-leading/hyphen", + ok: false, + }, + } + + for _, test := range tests { + err := validateRepoSubDir(test.subdir) + ok := err == nil + if ok != test.ok { + want := "error" + if test.ok { + want = "nil" + } + t.Errorf("validateRepoSubDir(%q) = %q, want %s", test.subdir, err, want) + } + } +} + var govcsTests = []struct { govcs string path string diff --git a/src/cmd/go/internal/workcmd/edit.go b/src/cmd/go/internal/workcmd/edit.go index 18730436ca82176fe260528abb0136c85805fb19..fa2f018abaf50cdd1ba5d924a4df0a9cd8b33474 100644 --- a/src/cmd/go/internal/workcmd/edit.go +++ b/src/cmd/go/internal/workcmd/edit.go @@ -278,7 +278,10 @@ // parsePathVersionOptional parses path[@version], using adj to // describe any errors. func parsePathVersionOptional(adj, arg string, allowDirPath bool) (path, version string, err error) { - before, after, found := strings.Cut(arg, "@") + before, after, found, err := modload.ParsePathVersion(arg) + if err != nil { + return "", "", err + } if !found { path = arg } else {