Black Lives Matter. Support the Equal Justice Initiative.

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

View as plain text