...

Source file src/cmd/go/internal/modcmd/edit.go

Documentation: cmd/go/internal/modcmd

     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 -godebug=key=value flag adds a godebug key=value line,
    48  replacing any existing godebug lines with the given key.
    49  
    50  The -dropgodebug=key flag drops any existing godebug lines
    51  with the given key.
    52  
    53  The -require=path@version and -droprequire=path flags
    54  add and drop a requirement on the given module path and version.
    55  Note that -require overrides any existing requirements on path.
    56  These flags are mainly for tools that understand the module graph.
    57  Users should prefer 'go get path@version' or 'go get path@none',
    58  which make other go.mod adjustments as needed to satisfy
    59  constraints imposed by other modules.
    60  
    61  The -go=version flag sets the expected Go language version.
    62  This flag is mainly for tools that understand Go version dependencies.
    63  Users should prefer 'go get go@version'.
    64  
    65  The -toolchain=version flag sets the Go toolchain to use.
    66  This flag is mainly for tools that understand Go version dependencies.
    67  Users should prefer 'go get toolchain@version'.
    68  
    69  The -exclude=path@version and -dropexclude=path@version flags
    70  add and drop an exclusion for the given module path and version.
    71  Note that -exclude=path@version is a no-op if that exclusion already exists.
    72  
    73  The -replace=old[@v]=new[@v] flag adds a replacement of the given
    74  module path and version pair. If the @v in old@v is omitted, a
    75  replacement without a version on the left side is added, which applies
    76  to all versions of the old module path. If the @v in new@v is omitted,
    77  the new path should be a local module root directory, not a module
    78  path. Note that -replace overrides any redundant replacements for old[@v],
    79  so omitting @v will drop existing replacements for specific versions.
    80  
    81  The -dropreplace=old[@v] flag drops a replacement of the given
    82  module path and version pair. If the @v is omitted, a replacement without
    83  a version on the left side is dropped.
    84  
    85  The -retract=version and -dropretract=version flags add and drop a
    86  retraction on the given version. The version may be a single version
    87  like "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  
    90  The -godebug, -dropgodebug, -require, -droprequire, -exclude, -dropexclude,
    91  -replace, -dropreplace, -retract, and -dropretract editing flags may be
    92  repeated, and the changes are applied in the order given.
    93  
    94  The -print flag prints the final go.mod in its text format instead of
    95  writing it back to go.mod.
    96  
    97  The -json flag prints the final go.mod file in JSON format instead of
    98  writing 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  
   143  Retract entries representing a single version (not an interval) will have
   144  the "Low" and "High" fields set to the same value.
   145  
   146  Note that this only describes the go.mod file itself, not other modules
   147  referred to indirectly. For the full set of modules available to a build,
   148  use 'go list -m -json all'.
   149  
   150  Edit also provides the -C, -n, and -x build flags.
   151  
   152  See https://golang.org/ref/mod#go-mod-edit for more about 'go mod edit'.
   153  	`,
   154  }
   155  
   156  var (
   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  
   166  type flagFunc func(string)
   167  
   168  func (f flagFunc) String() string     { return "" }
   169  func (f flagFunc) Set(s string) error { f(s); return nil }
   170  
   171  func 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  
   190  func 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.
   304  func 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).
   322  func 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.
   335  func 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.
   358  func 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.
   386  func allowedVersionArg(arg string) bool {
   387  	return !modfile.MustQuote(arg)
   388  }
   389  
   390  // flagGodebug implements the -godebug flag.
   391  func 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.
   404  func 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.
   413  func 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.
   423  func 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.
   433  func 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.
   443  func 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.
   453  func 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.
   482  func 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.
   495  func 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.
   508  func 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.
   521  type 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  
   531  type editModuleJSON struct {
   532  	Path       string
   533  	Deprecated string `json:",omitempty"`
   534  }
   535  
   536  type requireJSON struct {
   537  	Path     string
   538  	Version  string `json:",omitempty"`
   539  	Indirect bool   `json:",omitempty"`
   540  }
   541  
   542  type replaceJSON struct {
   543  	Old module.Version
   544  	New module.Version
   545  }
   546  
   547  type 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.
   554  func 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  

View as plain text