]> Cypherpunks.ru repositories - gostls13.git/blob - src/cmd/go/internal/modcmd/edit.go
cmd/go: handle '@' in local path when running 'go mod edit -replace'
[gostls13.git] / src / cmd / go / internal / modcmd / edit.go
1 // Copyright 2018 The Go Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style
3 // license that can be found in the LICENSE file.
4
5 // go mod edit
6
7 package modcmd
8
9 import (
10         "bytes"
11         "context"
12         "encoding/json"
13         "errors"
14         "fmt"
15         "os"
16         "strings"
17
18         "cmd/go/internal/base"
19         "cmd/go/internal/gover"
20         "cmd/go/internal/lockedfile"
21         "cmd/go/internal/modfetch"
22         "cmd/go/internal/modload"
23
24         "golang.org/x/mod/modfile"
25         "golang.org/x/mod/module"
26 )
27
28 var cmdEdit = &base.Command{
29         UsageLine: "go mod edit [editing flags] [-fmt|-print|-json] [go.mod]",
30         Short:     "edit go.mod from tools or scripts",
31         Long: `
32 Edit provides a command-line interface for editing go.mod,
33 for use primarily by tools or scripts. It reads only go.mod;
34 it does not look up information about the modules involved.
35 By default, edit reads and writes the go.mod file of the main module,
36 but a different target file can be specified after the editing flags.
37
38 The editing flags specify a sequence of editing operations.
39
40 The -fmt flag reformats the go.mod file without making other changes.
41 This reformatting is also implied by any other modifications that use or
42 rewrite the go.mod file. The only time this flag is needed is if no other
43 flags are specified, as in 'go mod edit -fmt'.
44
45 The -module flag changes the module's path (the go.mod file's module line).
46
47 The -require=path@version and -droprequire=path flags
48 add and drop a requirement on the given module path and version.
49 Note that -require overrides any existing requirements on path.
50 These flags are mainly for tools that understand the module graph.
51 Users should prefer 'go get path@version' or 'go get path@none',
52 which make other go.mod adjustments as needed to satisfy
53 constraints imposed by other modules.
54
55 The -exclude=path@version and -dropexclude=path@version flags
56 add and drop an exclusion for the given module path and version.
57 Note that -exclude=path@version is a no-op if that exclusion already exists.
58
59 The -replace=old[@v]=new[@v] flag adds a replacement of the given
60 module path and version pair. If the @v in old@v is omitted, a
61 replacement without a version on the left side is added, which applies
62 to all versions of the old module path. If the @v in new@v is omitted,
63 the new path should be a local module root directory, not a module
64 path. Note that -replace overrides any redundant replacements for old[@v],
65 so omitting @v will drop existing replacements for specific versions.
66
67 The -dropreplace=old[@v] flag drops a replacement of the given
68 module path and version pair. If the @v is omitted, a replacement without
69 a version on the left side is dropped.
70
71 The -retract=version and -dropretract=version flags add and drop a
72 retraction on the given version. The version may be a single version
73 like "v1.2.3" or a closed interval like "[v1.1.0,v1.1.9]". Note that
74 -retract=version is a no-op if that retraction already exists.
75
76 The -require, -droprequire, -exclude, -dropexclude, -replace,
77 -dropreplace, -retract, and -dropretract editing flags may be repeated,
78 and the changes are applied in the order given.
79
80 The -go=version flag sets the expected Go language version.
81
82 The -toolchain=name flag sets the Go toolchain to use.
83
84 The -print flag prints the final go.mod in its text format instead of
85 writing it back to go.mod.
86
87 The -json flag prints the final go.mod file in JSON format instead of
88 writing it back to go.mod. The JSON output corresponds to these Go types:
89
90         type Module struct {
91                 Path    string
92                 Version string
93         }
94
95         type GoMod struct {
96                 Module    ModPath
97                 Go        string
98                 Toolchain string
99                 Require   []Require
100                 Exclude   []Module
101                 Replace   []Replace
102                 Retract   []Retract
103         }
104
105         type ModPath struct {
106                 Path       string
107                 Deprecated string
108         }
109
110         type Require struct {
111                 Path string
112                 Version string
113                 Indirect bool
114         }
115
116         type Replace struct {
117                 Old Module
118                 New Module
119         }
120
121         type Retract struct {
122                 Low       string
123                 High      string
124                 Rationale string
125         }
126
127 Retract entries representing a single version (not an interval) will have
128 the "Low" and "High" fields set to the same value.
129
130 Note that this only describes the go.mod file itself, not other modules
131 referred to indirectly. For the full set of modules available to a build,
132 use 'go list -m -json all'.
133
134 Edit also provides the -C, -n, and -x build flags.
135
136 See https://golang.org/ref/mod#go-mod-edit for more about 'go mod edit'.
137         `,
138 }
139
140 var (
141         editFmt       = cmdEdit.Flag.Bool("fmt", false, "")
142         editGo        = cmdEdit.Flag.String("go", "", "")
143         editToolchain = cmdEdit.Flag.String("toolchain", "", "")
144         editJSON      = cmdEdit.Flag.Bool("json", false, "")
145         editPrint     = cmdEdit.Flag.Bool("print", false, "")
146         editModule    = cmdEdit.Flag.String("module", "", "")
147         edits         []func(*modfile.File) // edits specified in flags
148 )
149
150 type flagFunc func(string)
151
152 func (f flagFunc) String() string     { return "" }
153 func (f flagFunc) Set(s string) error { f(s); return nil }
154
155 func init() {
156         cmdEdit.Run = runEdit // break init cycle
157
158         cmdEdit.Flag.Var(flagFunc(flagRequire), "require", "")
159         cmdEdit.Flag.Var(flagFunc(flagDropRequire), "droprequire", "")
160         cmdEdit.Flag.Var(flagFunc(flagExclude), "exclude", "")
161         cmdEdit.Flag.Var(flagFunc(flagDropReplace), "dropreplace", "")
162         cmdEdit.Flag.Var(flagFunc(flagReplace), "replace", "")
163         cmdEdit.Flag.Var(flagFunc(flagDropExclude), "dropexclude", "")
164         cmdEdit.Flag.Var(flagFunc(flagRetract), "retract", "")
165         cmdEdit.Flag.Var(flagFunc(flagDropRetract), "dropretract", "")
166
167         base.AddBuildFlagsNX(&cmdEdit.Flag)
168         base.AddChdirFlag(&cmdEdit.Flag)
169         base.AddModCommonFlags(&cmdEdit.Flag)
170 }
171
172 func runEdit(ctx context.Context, cmd *base.Command, args []string) {
173         anyFlags := *editModule != "" ||
174                 *editGo != "" ||
175                 *editToolchain != "" ||
176                 *editJSON ||
177                 *editPrint ||
178                 *editFmt ||
179                 len(edits) > 0
180
181         if !anyFlags {
182                 base.Fatalf("go: no flags specified (see 'go help mod edit').")
183         }
184
185         if *editJSON && *editPrint {
186                 base.Fatalf("go: cannot use both -json and -print")
187         }
188
189         if len(args) > 1 {
190                 base.Fatalf("go: too many arguments")
191         }
192         var gomod string
193         if len(args) == 1 {
194                 gomod = args[0]
195         } else {
196                 gomod = modload.ModFilePath()
197         }
198
199         if *editModule != "" {
200                 if err := module.CheckImportPath(*editModule); err != nil {
201                         base.Fatalf("go: invalid -module: %v", err)
202                 }
203         }
204
205         if *editGo != "" && *editGo != "none" {
206                 if !modfile.GoVersionRE.MatchString(*editGo) {
207                         base.Fatalf(`go mod: invalid -go option; expecting something like "-go %s"`, gover.Local())
208                 }
209         }
210         if *editToolchain != "" && *editToolchain != "none" {
211                 if !modfile.ToolchainRE.MatchString(*editToolchain) {
212                         base.Fatalf(`go mod: invalid -toolchain option; expecting something like "-toolchain go%s"`, gover.Local())
213                 }
214         }
215
216         data, err := lockedfile.Read(gomod)
217         if err != nil {
218                 base.Fatal(err)
219         }
220
221         modFile, err := modfile.Parse(gomod, data, nil)
222         if err != nil {
223                 base.Fatalf("go: errors parsing %s:\n%s", base.ShortPath(gomod), err)
224         }
225
226         if *editModule != "" {
227                 modFile.AddModuleStmt(*editModule)
228         }
229
230         if *editGo == "none" {
231                 modFile.DropGoStmt()
232         } else if *editGo != "" {
233                 if err := modFile.AddGoStmt(*editGo); err != nil {
234                         base.Fatalf("go: internal error: %v", err)
235                 }
236         }
237         if *editToolchain == "none" {
238                 modFile.DropToolchainStmt()
239         } else if *editToolchain != "" {
240                 if err := modFile.AddToolchainStmt(*editToolchain); err != nil {
241                         base.Fatalf("go: internal error: %v", err)
242                 }
243         }
244
245         if len(edits) > 0 {
246                 for _, edit := range edits {
247                         edit(modFile)
248                 }
249         }
250         modFile.SortBlocks()
251         modFile.Cleanup() // clean file after edits
252
253         if *editJSON {
254                 editPrintJSON(modFile)
255                 return
256         }
257
258         out, err := modFile.Format()
259         if err != nil {
260                 base.Fatal(err)
261         }
262
263         if *editPrint {
264                 os.Stdout.Write(out)
265                 return
266         }
267
268         // Make a best-effort attempt to acquire the side lock, only to exclude
269         // previous versions of the 'go' command from making simultaneous edits.
270         if unlock, err := modfetch.SideLock(ctx); err == nil {
271                 defer unlock()
272         }
273
274         err = lockedfile.Transform(gomod, func(lockedData []byte) ([]byte, error) {
275                 if !bytes.Equal(lockedData, data) {
276                         return nil, errors.New("go.mod changed during editing; not overwriting")
277                 }
278                 return out, nil
279         })
280         if err != nil {
281                 base.Fatal(err)
282         }
283 }
284
285 // parsePathVersion parses -flag=arg expecting arg to be path@version.
286 func parsePathVersion(flag, arg string) (path, version string) {
287         before, after, found := strings.Cut(arg, "@")
288         if !found {
289                 base.Fatalf("go: -%s=%s: need path@version", flag, arg)
290         }
291         path, version = strings.TrimSpace(before), strings.TrimSpace(after)
292         if err := module.CheckImportPath(path); err != nil {
293                 base.Fatalf("go: -%s=%s: invalid path: %v", flag, arg, err)
294         }
295
296         if !allowedVersionArg(version) {
297                 base.Fatalf("go: -%s=%s: invalid version %q", flag, arg, version)
298         }
299
300         return path, version
301 }
302
303 // parsePath parses -flag=arg expecting arg to be path (not path@version).
304 func parsePath(flag, arg string) (path string) {
305         if strings.Contains(arg, "@") {
306                 base.Fatalf("go: -%s=%s: need just path, not path@version", flag, arg)
307         }
308         path = arg
309         if err := module.CheckImportPath(path); err != nil {
310                 base.Fatalf("go: -%s=%s: invalid path: %v", flag, arg, err)
311         }
312         return path
313 }
314
315 // parsePathVersionOptional parses path[@version], using adj to
316 // describe any errors.
317 func parsePathVersionOptional(adj, arg string, allowDirPath bool) (path, version string, err error) {
318         if allowDirPath && modfile.IsDirectoryPath(arg) {
319                 return arg, "", nil
320         }
321         before, after, found := strings.Cut(arg, "@")
322         if !found {
323                 path = arg
324         } else {
325                 path, version = strings.TrimSpace(before), strings.TrimSpace(after)
326         }
327         if err := module.CheckImportPath(path); err != nil {
328                 return path, version, fmt.Errorf("invalid %s path: %v", adj, err)
329         }
330         if path != arg && !allowedVersionArg(version) {
331                 return path, version, fmt.Errorf("invalid %s version: %q", adj, version)
332         }
333         return path, version, nil
334 }
335
336 // parseVersionInterval parses a single version like "v1.2.3" or a closed
337 // interval like "[v1.2.3,v1.4.5]". Note that a single version has the same
338 // representation as an interval with equal upper and lower bounds: both
339 // Low and High are set.
340 func parseVersionInterval(arg string) (modfile.VersionInterval, error) {
341         if !strings.HasPrefix(arg, "[") {
342                 if !allowedVersionArg(arg) {
343                         return modfile.VersionInterval{}, fmt.Errorf("invalid version: %q", arg)
344                 }
345                 return modfile.VersionInterval{Low: arg, High: arg}, nil
346         }
347         if !strings.HasSuffix(arg, "]") {
348                 return modfile.VersionInterval{}, fmt.Errorf("invalid version interval: %q", arg)
349         }
350         s := arg[1 : len(arg)-1]
351         before, after, found := strings.Cut(s, ",")
352         if !found {
353                 return modfile.VersionInterval{}, fmt.Errorf("invalid version interval: %q", arg)
354         }
355         low := strings.TrimSpace(before)
356         high := strings.TrimSpace(after)
357         if !allowedVersionArg(low) || !allowedVersionArg(high) {
358                 return modfile.VersionInterval{}, fmt.Errorf("invalid version interval: %q", arg)
359         }
360         return modfile.VersionInterval{Low: low, High: high}, nil
361 }
362
363 // allowedVersionArg returns whether a token may be used as a version in go.mod.
364 // We don't call modfile.CheckPathVersion, because that insists on versions
365 // being in semver form, but here we want to allow versions like "master" or
366 // "1234abcdef", which the go command will resolve the next time it runs (or
367 // during -fix).  Even so, we need to make sure the version is a valid token.
368 func allowedVersionArg(arg string) bool {
369         return !modfile.MustQuote(arg)
370 }
371
372 // flagRequire implements the -require flag.
373 func flagRequire(arg string) {
374         path, version := parsePathVersion("require", arg)
375         edits = append(edits, func(f *modfile.File) {
376                 if err := f.AddRequire(path, version); err != nil {
377                         base.Fatalf("go: -require=%s: %v", arg, err)
378                 }
379         })
380 }
381
382 // flagDropRequire implements the -droprequire flag.
383 func flagDropRequire(arg string) {
384         path := parsePath("droprequire", arg)
385         edits = append(edits, func(f *modfile.File) {
386                 if err := f.DropRequire(path); err != nil {
387                         base.Fatalf("go: -droprequire=%s: %v", arg, err)
388                 }
389         })
390 }
391
392 // flagExclude implements the -exclude flag.
393 func flagExclude(arg string) {
394         path, version := parsePathVersion("exclude", arg)
395         edits = append(edits, func(f *modfile.File) {
396                 if err := f.AddExclude(path, version); err != nil {
397                         base.Fatalf("go: -exclude=%s: %v", arg, err)
398                 }
399         })
400 }
401
402 // flagDropExclude implements the -dropexclude flag.
403 func flagDropExclude(arg string) {
404         path, version := parsePathVersion("dropexclude", arg)
405         edits = append(edits, func(f *modfile.File) {
406                 if err := f.DropExclude(path, version); err != nil {
407                         base.Fatalf("go: -dropexclude=%s: %v", arg, err)
408                 }
409         })
410 }
411
412 // flagReplace implements the -replace flag.
413 func flagReplace(arg string) {
414         before, after, found := strings.Cut(arg, "=")
415         if !found {
416                 base.Fatalf("go: -replace=%s: need old[@v]=new[@w] (missing =)", arg)
417         }
418         old, new := strings.TrimSpace(before), strings.TrimSpace(after)
419         if strings.HasPrefix(new, ">") {
420                 base.Fatalf("go: -replace=%s: separator between old and new is =, not =>", arg)
421         }
422         oldPath, oldVersion, err := parsePathVersionOptional("old", old, false)
423         if err != nil {
424                 base.Fatalf("go: -replace=%s: %v", arg, err)
425         }
426         newPath, newVersion, err := parsePathVersionOptional("new", new, true)
427         if err != nil {
428                 base.Fatalf("go: -replace=%s: %v", arg, err)
429         }
430         if newPath == new && !modfile.IsDirectoryPath(new) {
431                 base.Fatalf("go: -replace=%s: unversioned new path must be local directory", arg)
432         }
433
434         edits = append(edits, func(f *modfile.File) {
435                 if err := f.AddReplace(oldPath, oldVersion, newPath, newVersion); err != nil {
436                         base.Fatalf("go: -replace=%s: %v", arg, err)
437                 }
438         })
439 }
440
441 // flagDropReplace implements the -dropreplace flag.
442 func flagDropReplace(arg string) {
443         path, version, err := parsePathVersionOptional("old", arg, true)
444         if err != nil {
445                 base.Fatalf("go: -dropreplace=%s: %v", arg, err)
446         }
447         edits = append(edits, func(f *modfile.File) {
448                 if err := f.DropReplace(path, version); err != nil {
449                         base.Fatalf("go: -dropreplace=%s: %v", arg, err)
450                 }
451         })
452 }
453
454 // flagRetract implements the -retract flag.
455 func flagRetract(arg string) {
456         vi, err := parseVersionInterval(arg)
457         if err != nil {
458                 base.Fatalf("go: -retract=%s: %v", arg, err)
459         }
460         edits = append(edits, func(f *modfile.File) {
461                 if err := f.AddRetract(vi, ""); err != nil {
462                         base.Fatalf("go: -retract=%s: %v", arg, err)
463                 }
464         })
465 }
466
467 // flagDropRetract implements the -dropretract flag.
468 func flagDropRetract(arg string) {
469         vi, err := parseVersionInterval(arg)
470         if err != nil {
471                 base.Fatalf("go: -dropretract=%s: %v", arg, err)
472         }
473         edits = append(edits, func(f *modfile.File) {
474                 if err := f.DropRetract(vi); err != nil {
475                         base.Fatalf("go: -dropretract=%s: %v", arg, err)
476                 }
477         })
478 }
479
480 // fileJSON is the -json output data structure.
481 type fileJSON struct {
482         Module    editModuleJSON
483         Go        string `json:",omitempty"`
484         Toolchain string `json:",omitempty"`
485         Require   []requireJSON
486         Exclude   []module.Version
487         Replace   []replaceJSON
488         Retract   []retractJSON
489 }
490
491 type editModuleJSON struct {
492         Path       string
493         Deprecated string `json:",omitempty"`
494 }
495
496 type requireJSON struct {
497         Path     string
498         Version  string `json:",omitempty"`
499         Indirect bool   `json:",omitempty"`
500 }
501
502 type replaceJSON struct {
503         Old module.Version
504         New module.Version
505 }
506
507 type retractJSON struct {
508         Low       string `json:",omitempty"`
509         High      string `json:",omitempty"`
510         Rationale string `json:",omitempty"`
511 }
512
513 // editPrintJSON prints the -json output.
514 func editPrintJSON(modFile *modfile.File) {
515         var f fileJSON
516         if modFile.Module != nil {
517                 f.Module = editModuleJSON{
518                         Path:       modFile.Module.Mod.Path,
519                         Deprecated: modFile.Module.Deprecated,
520                 }
521         }
522         if modFile.Go != nil {
523                 f.Go = modFile.Go.Version
524         }
525         if modFile.Toolchain != nil {
526                 f.Toolchain = modFile.Toolchain.Name
527         }
528         for _, r := range modFile.Require {
529                 f.Require = append(f.Require, requireJSON{Path: r.Mod.Path, Version: r.Mod.Version, Indirect: r.Indirect})
530         }
531         for _, x := range modFile.Exclude {
532                 f.Exclude = append(f.Exclude, x.Mod)
533         }
534         for _, r := range modFile.Replace {
535                 f.Replace = append(f.Replace, replaceJSON{r.Old, r.New})
536         }
537         for _, r := range modFile.Retract {
538                 f.Retract = append(f.Retract, retractJSON{r.Low, r.High, r.Rationale})
539         }
540         data, err := json.MarshalIndent(&f, "", "\t")
541         if err != nil {
542                 base.Fatalf("go: internal error: %v", err)
543         }
544         data = append(data, '\n')
545         os.Stdout.Write(data)
546 }