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

View as plain text