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

View as plain text