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