github.com/go-asm/go@v1.21.1-0.20240213172139-40c5ead50c48/cmd/go/workcmd/edit.go (about)

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