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

     1  // Copyright 2021 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 work edit
     6  
     7  package workcmd
     8  
     9  import (
    10  	"cmd/go/internal/base"
    11  	"cmd/go/internal/modload"
    12  	"context"
    13  	"encoding/json"
    14  	"fmt"
    15  	"os"
    16  	"path/filepath"
    17  	"strings"
    18  
    19  	"golang.org/x/mod/module"
    20  
    21  	"golang.org/x/mod/modfile"
    22  )
    23  
    24  var cmdEdit = &base.Command{
    25  	UsageLine: "go work edit [editing flags] [go.work]",
    26  	Short:     "edit go.work from tools or scripts",
    27  	Long: `Edit provides a command-line interface for editing go.work,
    28  for use primarily by tools or scripts. It only reads go.work;
    29  it does not look up information about the modules involved.
    30  If no file is specified, Edit looks for a go.work file in the current
    31  directory and its parent directories
    32  
    33  The editing flags specify a sequence of editing operations.
    34  
    35  The -fmt flag reformats the go.work file without making other changes.
    36  This reformatting is also implied by any other modifications that use or
    37  rewrite the go.mod file. The only time this flag is needed is if no other
    38  flags are specified, as in 'go work edit -fmt'.
    39  
    40  The -use=path and -dropuse=path flags
    41  add and drop a use directive from the go.work file's set of module directories.
    42  
    43  The -replace=old[@v]=new[@v] flag adds a replacement of the given
    44  module path and version pair. If the @v in old@v is omitted, a
    45  replacement without a version on the left side is added, which applies
    46  to all versions of the old module path. If the @v in new@v is omitted,
    47  the new path should be a local module root directory, not a module
    48  path. Note that -replace overrides any redundant replacements for old[@v],
    49  so omitting @v will drop existing replacements for specific versions.
    50  
    51  The -dropreplace=old[@v] flag drops a replacement of the given
    52  module path and version pair. If the @v is omitted, a replacement without
    53  a version on the left side is dropped.
    54  
    55  The -use, -dropuse, -replace, and -dropreplace,
    56  editing flags may be repeated, and the changes are applied in the order given.
    57  
    58  The -go=version flag sets the expected Go language version.
    59  
    60  The -print flag prints the final go.work in its text format instead of
    61  writing it back to go.mod.
    62  
    63  The -json flag prints the final go.work file in JSON format instead of
    64  writing it back to go.mod. The JSON output corresponds to these Go types:
    65  
    66  	type GoWork struct {
    67  		Go      string
    68  		Use     []Use
    69  		Replace []Replace
    70  	}
    71  
    72  	type Use struct {
    73  		DiskPath   string
    74  		ModulePath string
    75  	}
    76  
    77  	type Replace struct {
    78  		Old Module
    79  		New Module
    80  	}
    81  
    82  	type Module struct {
    83  		Path    string
    84  		Version string
    85  	}
    86  
    87  See the workspaces reference at https://go.dev/ref/mod#workspaces
    88  for more information.
    89  `,
    90  }
    91  
    92  var (
    93  	editFmt   = cmdEdit.Flag.Bool("fmt", false, "")
    94  	editGo    = cmdEdit.Flag.String("go", "", "")
    95  	editJSON  = cmdEdit.Flag.Bool("json", false, "")
    96  	editPrint = cmdEdit.Flag.Bool("print", false, "")
    97  	workedits []func(file *modfile.WorkFile) // edits specified in flags
    98  )
    99  
   100  type flagFunc func(string)
   101  
   102  func (f flagFunc) String() string     { return "" }
   103  func (f flagFunc) Set(s string) error { f(s); return nil }
   104  
   105  func init() {
   106  	cmdEdit.Run = runEditwork // break init cycle
   107  
   108  	cmdEdit.Flag.Var(flagFunc(flagEditworkUse), "use", "")
   109  	cmdEdit.Flag.Var(flagFunc(flagEditworkDropUse), "dropuse", "")
   110  	cmdEdit.Flag.Var(flagFunc(flagEditworkReplace), "replace", "")
   111  	cmdEdit.Flag.Var(flagFunc(flagEditworkDropReplace), "dropreplace", "")
   112  }
   113  
   114  func runEditwork(ctx context.Context, cmd *base.Command, args []string) {
   115  	if *editJSON && *editPrint {
   116  		base.Fatalf("go: cannot use both -json and -print")
   117  	}
   118  
   119  	if len(args) > 1 {
   120  		base.Fatalf("go: 'go help work edit' accepts at most one argument")
   121  	}
   122  	var gowork string
   123  	if len(args) == 1 {
   124  		gowork = args[0]
   125  	} else {
   126  		modload.InitWorkfile()
   127  		gowork = modload.WorkFilePath()
   128  	}
   129  
   130  	if *editGo != "" {
   131  		if !modfile.GoVersionRE.MatchString(*editGo) {
   132  			base.Fatalf(`go mod: invalid -go option; expecting something like "-go %s"`, modload.LatestGoVersion())
   133  		}
   134  	}
   135  
   136  	if gowork == "" {
   137  		base.Fatalf("go: no go.work file found\n\t(run 'go work init' first or specify path using GOWORK environment variable)")
   138  	}
   139  
   140  	anyFlags :=
   141  		*editGo != "" ||
   142  			*editJSON ||
   143  			*editPrint ||
   144  			*editFmt ||
   145  			len(workedits) > 0
   146  
   147  	if !anyFlags {
   148  		base.Fatalf("go: no flags specified (see 'go help work edit').")
   149  	}
   150  
   151  	workFile, err := modload.ReadWorkFile(gowork)
   152  	if err != nil {
   153  		base.Fatalf("go: errors parsing %s:\n%s", base.ShortPath(gowork), err)
   154  	}
   155  
   156  	if *editGo != "" {
   157  		if err := workFile.AddGoStmt(*editGo); err != nil {
   158  			base.Fatalf("go: internal error: %v", err)
   159  		}
   160  	}
   161  
   162  	if len(workedits) > 0 {
   163  		for _, edit := range workedits {
   164  			edit(workFile)
   165  		}
   166  	}
   167  
   168  	modload.UpdateWorkFile(workFile)
   169  
   170  	workFile.SortBlocks()
   171  	workFile.Cleanup() // clean file after edits
   172  
   173  	if *editJSON {
   174  		editPrintJSON(workFile)
   175  		return
   176  	}
   177  
   178  	if *editPrint {
   179  		os.Stdout.Write(modfile.Format(workFile.Syntax))
   180  		return
   181  	}
   182  
   183  	modload.WriteWorkFile(gowork, workFile)
   184  }
   185  
   186  // flagEditworkUse implements the -use flag.
   187  func flagEditworkUse(arg string) {
   188  	workedits = append(workedits, func(f *modfile.WorkFile) {
   189  		_, mf, err := modload.ReadModFile(filepath.Join(arg, "go.mod"), nil)
   190  		modulePath := ""
   191  		if err == nil {
   192  			modulePath = mf.Module.Mod.Path
   193  		}
   194  		f.AddUse(modload.ToDirectoryPath(arg), modulePath)
   195  		if err := f.AddUse(modload.ToDirectoryPath(arg), ""); err != nil {
   196  			base.Fatalf("go: -use=%s: %v", arg, err)
   197  		}
   198  	})
   199  }
   200  
   201  // flagEditworkDropUse implements the -dropuse flag.
   202  func flagEditworkDropUse(arg string) {
   203  	workedits = append(workedits, func(f *modfile.WorkFile) {
   204  		if err := f.DropUse(modload.ToDirectoryPath(arg)); err != nil {
   205  			base.Fatalf("go: -dropdirectory=%s: %v", arg, err)
   206  		}
   207  	})
   208  }
   209  
   210  // allowedVersionArg returns whether a token may be used as a version in go.mod.
   211  // We don't call modfile.CheckPathVersion, because that insists on versions
   212  // being in semver form, but here we want to allow versions like "master" or
   213  // "1234abcdef", which the go command will resolve the next time it runs (or
   214  // during -fix).  Even so, we need to make sure the version is a valid token.
   215  func allowedVersionArg(arg string) bool {
   216  	return !modfile.MustQuote(arg)
   217  }
   218  
   219  // parsePathVersionOptional parses path[@version], using adj to
   220  // describe any errors.
   221  func parsePathVersionOptional(adj, arg string, allowDirPath bool) (path, version string, err error) {
   222  	if i := strings.Index(arg, "@"); i < 0 {
   223  		path = arg
   224  	} else {
   225  		path, version = strings.TrimSpace(arg[:i]), strings.TrimSpace(arg[i+1:])
   226  	}
   227  	if err := module.CheckImportPath(path); err != nil {
   228  		if !allowDirPath || !modfile.IsDirectoryPath(path) {
   229  			return path, version, fmt.Errorf("invalid %s path: %v", adj, err)
   230  		}
   231  	}
   232  	if path != arg && !allowedVersionArg(version) {
   233  		return path, version, fmt.Errorf("invalid %s version: %q", adj, version)
   234  	}
   235  	return path, version, nil
   236  }
   237  
   238  // flagReplace implements the -replace flag.
   239  func flagEditworkReplace(arg string) {
   240  	var i int
   241  	if i = strings.Index(arg, "="); i < 0 {
   242  		base.Fatalf("go: -replace=%s: need old[@v]=new[@w] (missing =)", arg)
   243  	}
   244  	old, new := strings.TrimSpace(arg[:i]), strings.TrimSpace(arg[i+1:])
   245  	if strings.HasPrefix(new, ">") {
   246  		base.Fatalf("go: -replace=%s: separator between old and new is =, not =>", arg)
   247  	}
   248  	oldPath, oldVersion, err := parsePathVersionOptional("old", old, false)
   249  	if err != nil {
   250  		base.Fatalf("go: -replace=%s: %v", arg, err)
   251  	}
   252  	newPath, newVersion, err := parsePathVersionOptional("new", new, true)
   253  	if err != nil {
   254  		base.Fatalf("go: -replace=%s: %v", arg, err)
   255  	}
   256  	if newPath == new && !modfile.IsDirectoryPath(new) {
   257  		base.Fatalf("go: -replace=%s: unversioned new path must be local directory", arg)
   258  	}
   259  
   260  	workedits = append(workedits, func(f *modfile.WorkFile) {
   261  		if err := f.AddReplace(oldPath, oldVersion, newPath, newVersion); err != nil {
   262  			base.Fatalf("go: -replace=%s: %v", arg, err)
   263  		}
   264  	})
   265  }
   266  
   267  // flagDropReplace implements the -dropreplace flag.
   268  func flagEditworkDropReplace(arg string) {
   269  	path, version, err := parsePathVersionOptional("old", arg, true)
   270  	if err != nil {
   271  		base.Fatalf("go: -dropreplace=%s: %v", arg, err)
   272  	}
   273  	workedits = append(workedits, func(f *modfile.WorkFile) {
   274  		if err := f.DropReplace(path, version); err != nil {
   275  			base.Fatalf("go: -dropreplace=%s: %v", arg, err)
   276  		}
   277  	})
   278  }
   279  
   280  type replaceJSON struct {
   281  	Old module.Version
   282  	New module.Version
   283  }
   284  
   285  // editPrintJSON prints the -json output.
   286  func editPrintJSON(workFile *modfile.WorkFile) {
   287  	var f workfileJSON
   288  	if workFile.Go != nil {
   289  		f.Go = workFile.Go.Version
   290  	}
   291  	for _, d := range workFile.Use {
   292  		f.Use = append(f.Use, useJSON{DiskPath: d.Path, ModPath: d.ModulePath})
   293  	}
   294  
   295  	for _, r := range workFile.Replace {
   296  		f.Replace = append(f.Replace, replaceJSON{r.Old, r.New})
   297  	}
   298  	data, err := json.MarshalIndent(&f, "", "\t")
   299  	if err != nil {
   300  		base.Fatalf("go: internal error: %v", err)
   301  	}
   302  	data = append(data, '\n')
   303  	os.Stdout.Write(data)
   304  }
   305  
   306  // workfileJSON is the -json output data structure.
   307  type workfileJSON struct {
   308  	Go      string `json:",omitempty"`
   309  	Use     []useJSON
   310  	Replace []replaceJSON
   311  }
   312  
   313  type useJSON struct {
   314  	DiskPath string
   315  	ModPath  string `json:",omitempty"`
   316  }
   317  

View as plain text