client.go (20935B)
1 // Copyright 2019 The Hugo Authors. All rights reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // http://www.apache.org/licenses/LICENSE-2.0 7 // 8 // Unless required by applicable law or agreed to in writing, software 9 // distributed under the License is distributed on an "AS IS" BASIS, 10 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 // See the License for the specific language governing permissions and 12 // limitations under the License. 13 14 package modules 15 16 import ( 17 "bufio" 18 "bytes" 19 "context" 20 "encoding/json" 21 "fmt" 22 "io" 23 "io/ioutil" 24 "os" 25 "os/exec" 26 "path/filepath" 27 "regexp" 28 "strings" 29 "time" 30 31 "github.com/gohugoio/hugo/common/collections" 32 "github.com/gohugoio/hugo/common/hexec" 33 34 hglob "github.com/gohugoio/hugo/hugofs/glob" 35 36 "github.com/gobwas/glob" 37 38 "github.com/gohugoio/hugo/hugofs" 39 40 "github.com/gohugoio/hugo/hugofs/files" 41 42 "github.com/gohugoio/hugo/common/loggers" 43 44 "github.com/gohugoio/hugo/config" 45 46 "github.com/rogpeppe/go-internal/module" 47 48 "github.com/gohugoio/hugo/common/hugio" 49 50 "errors" 51 52 "github.com/spf13/afero" 53 ) 54 55 var fileSeparator = string(os.PathSeparator) 56 57 const ( 58 goBinaryStatusOK goBinaryStatus = iota 59 goBinaryStatusNotFound 60 goBinaryStatusTooOld 61 ) 62 63 // The "vendor" dir is reserved for Go Modules. 64 const vendord = "_vendor" 65 66 const ( 67 goModFilename = "go.mod" 68 goSumFilename = "go.sum" 69 ) 70 71 // NewClient creates a new Client that can be used to manage the Hugo Components 72 // in a given workingDir. 73 // The Client will resolve the dependencies recursively, but needs the top 74 // level imports to start out. 75 func NewClient(cfg ClientConfig) *Client { 76 fs := cfg.Fs 77 n := filepath.Join(cfg.WorkingDir, goModFilename) 78 goModEnabled, _ := afero.Exists(fs, n) 79 var goModFilename string 80 if goModEnabled { 81 goModFilename = n 82 } 83 84 var env []string 85 mcfg := cfg.ModuleConfig 86 87 config.SetEnvVars(&env, 88 "PWD", cfg.WorkingDir, 89 "GO111MODULE", "on", 90 "GOPROXY", mcfg.Proxy, 91 "GOPRIVATE", mcfg.Private, 92 "GONOPROXY", mcfg.NoProxy, 93 "GOPATH", cfg.CacheDir, 94 "GOWORK", mcfg.Workspace, // Requires Go 1.18, see https://tip.golang.org/doc/go1.18 95 // GOCACHE was introduced in Go 1.15. This matches the location derived from GOPATH above. 96 "GOCACHE", filepath.Join(cfg.CacheDir, "pkg", "mod"), 97 ) 98 99 logger := cfg.Logger 100 if logger == nil { 101 logger = loggers.NewWarningLogger() 102 } 103 104 var noVendor glob.Glob 105 if cfg.ModuleConfig.NoVendor != "" { 106 noVendor, _ = hglob.GetGlob(hglob.NormalizePath(cfg.ModuleConfig.NoVendor)) 107 } 108 109 return &Client{ 110 fs: fs, 111 ccfg: cfg, 112 logger: logger, 113 noVendor: noVendor, 114 moduleConfig: mcfg, 115 environ: env, 116 GoModulesFilename: goModFilename, 117 } 118 } 119 120 // Client contains most of the API provided by this package. 121 type Client struct { 122 fs afero.Fs 123 logger loggers.Logger 124 125 noVendor glob.Glob 126 127 ccfg ClientConfig 128 129 // The top level module config 130 moduleConfig Config 131 132 // Environment variables used in "go get" etc. 133 environ []string 134 135 // Set when Go modules are initialized in the current repo, that is: 136 // a go.mod file exists. 137 GoModulesFilename string 138 139 // Set if we get a exec.ErrNotFound when running Go, which is most likely 140 // due to being run on a system without Go installed. We record it here 141 // so we can give an instructional error at the end if module/theme 142 // resolution fails. 143 goBinaryStatus goBinaryStatus 144 } 145 146 // Graph writes a module dependenchy graph to the given writer. 147 func (c *Client) Graph(w io.Writer) error { 148 mc, coll := c.collect(true) 149 if coll.err != nil { 150 return coll.err 151 } 152 for _, module := range mc.AllModules { 153 if module.Owner() == nil { 154 continue 155 } 156 157 prefix := "" 158 if module.Disabled() { 159 prefix = "DISABLED " 160 } 161 dep := pathVersion(module.Owner()) + " " + pathVersion(module) 162 if replace := module.Replace(); replace != nil { 163 if replace.Version() != "" { 164 dep += " => " + pathVersion(replace) 165 } else { 166 // Local dir. 167 dep += " => " + replace.Dir() 168 } 169 } 170 fmt.Fprintln(w, prefix+dep) 171 } 172 173 return nil 174 } 175 176 // Tidy can be used to remove unused dependencies from go.mod and go.sum. 177 func (c *Client) Tidy() error { 178 tc, coll := c.collect(false) 179 if coll.err != nil { 180 return coll.err 181 } 182 183 if coll.skipTidy { 184 return nil 185 } 186 187 return c.tidy(tc.AllModules, false) 188 } 189 190 // Vendor writes all the module dependencies to a _vendor folder. 191 // 192 // Unlike Go, we support it for any level. 193 // 194 // We, by default, use the /_vendor folder first, if found. To disable, 195 // run with 196 // hugo --ignoreVendorPaths=".*" 197 // 198 // Given a module tree, Hugo will pick the first module for a given path, 199 // meaning that if the top-level module is vendored, that will be the full 200 // set of dependencies. 201 func (c *Client) Vendor() error { 202 vendorDir := filepath.Join(c.ccfg.WorkingDir, vendord) 203 if err := c.rmVendorDir(vendorDir); err != nil { 204 return err 205 } 206 if err := c.fs.MkdirAll(vendorDir, 0755); err != nil { 207 return err 208 } 209 210 // Write the modules list to modules.txt. 211 // 212 // On the form: 213 // 214 // # github.com/alecthomas/chroma v0.6.3 215 // 216 // This is how "go mod vendor" does it. Go also lists 217 // the packages below it, but that is currently not applicable to us. 218 // 219 var modulesContent bytes.Buffer 220 221 tc, coll := c.collect(true) 222 if coll.err != nil { 223 return coll.err 224 } 225 226 for _, t := range tc.AllModules { 227 if t.Owner() == nil { 228 // This is the project. 229 continue 230 } 231 232 if !c.shouldVendor(t.Path()) { 233 continue 234 } 235 236 if !t.IsGoMod() && !t.Vendor() { 237 // We currently do not vendor components living in the 238 // theme directory, see https://github.com/gohugoio/hugo/issues/5993 239 continue 240 } 241 242 // See https://github.com/gohugoio/hugo/issues/8239 243 // This is an error situation. We need something to vendor. 244 if t.Mounts() == nil { 245 return fmt.Errorf("cannot vendor module %q, need at least one mount", t.Path()) 246 } 247 248 fmt.Fprintln(&modulesContent, "# "+t.Path()+" "+t.Version()) 249 250 dir := t.Dir() 251 252 for _, mount := range t.Mounts() { 253 sourceFilename := filepath.Join(dir, mount.Source) 254 targetFilename := filepath.Join(vendorDir, t.Path(), mount.Source) 255 fi, err := c.fs.Stat(sourceFilename) 256 if err != nil { 257 return fmt.Errorf("failed to vendor module: %w", err) 258 } 259 260 if fi.IsDir() { 261 if err := hugio.CopyDir(c.fs, sourceFilename, targetFilename, nil); err != nil { 262 return fmt.Errorf("failed to copy module to vendor dir: %w", err) 263 } 264 } else { 265 targetDir := filepath.Dir(targetFilename) 266 267 if err := c.fs.MkdirAll(targetDir, 0755); err != nil { 268 return fmt.Errorf("failed to make target dir: %w", err) 269 } 270 271 if err := hugio.CopyFile(c.fs, sourceFilename, targetFilename); err != nil { 272 return fmt.Errorf("failed to copy module file to vendor: %w", err) 273 } 274 } 275 } 276 277 // Include the resource cache if present. 278 resourcesDir := filepath.Join(dir, files.FolderResources) 279 _, err := c.fs.Stat(resourcesDir) 280 if err == nil { 281 if err := hugio.CopyDir(c.fs, resourcesDir, filepath.Join(vendorDir, t.Path(), files.FolderResources), nil); err != nil { 282 return fmt.Errorf("failed to copy resources to vendor dir: %w", err) 283 } 284 } 285 286 // Include the config directory if present. 287 configDir := filepath.Join(dir, "config") 288 _, err = c.fs.Stat(configDir) 289 if err == nil { 290 if err := hugio.CopyDir(c.fs, configDir, filepath.Join(vendorDir, t.Path(), "config"), nil); err != nil { 291 return fmt.Errorf("failed to copy config dir to vendor dir: %w", err) 292 } 293 } 294 295 // Also include any theme.toml or config.* files in the root. 296 configFiles, _ := afero.Glob(c.fs, filepath.Join(dir, "config.*")) 297 configFiles = append(configFiles, filepath.Join(dir, "theme.toml")) 298 for _, configFile := range configFiles { 299 if err := hugio.CopyFile(c.fs, configFile, filepath.Join(vendorDir, t.Path(), filepath.Base(configFile))); err != nil { 300 if !os.IsNotExist(err) { 301 return err 302 } 303 } 304 } 305 } 306 307 if modulesContent.Len() > 0 { 308 if err := afero.WriteFile(c.fs, filepath.Join(vendorDir, vendorModulesFilename), modulesContent.Bytes(), 0666); err != nil { 309 return err 310 } 311 } 312 313 return nil 314 } 315 316 // Get runs "go get" with the supplied arguments. 317 func (c *Client) Get(args ...string) error { 318 if len(args) == 0 || (len(args) == 1 && strings.Contains(args[0], "-u")) { 319 update := len(args) != 0 320 patch := update && (args[0] == "-u=patch") // 321 322 // We need to be explicit about the modules to get. 323 for _, m := range c.moduleConfig.Imports { 324 if !isProbablyModule(m.Path) { 325 // Skip themes/components stored below /themes etc. 326 // There may be false positives in the above, but those 327 // should be rare, and they will fail below with an 328 // "cannot find module providing ..." message. 329 continue 330 } 331 var args []string 332 333 if update && !patch { 334 args = append(args, "-u") 335 } else if update && patch { 336 args = append(args, "-u=patch") 337 } 338 args = append(args, m.Path) 339 340 if err := c.get(args...); err != nil { 341 return err 342 } 343 } 344 345 return nil 346 } 347 348 return c.get(args...) 349 } 350 351 func (c *Client) get(args ...string) error { 352 var hasD bool 353 for _, arg := range args { 354 if arg == "-d" { 355 hasD = true 356 break 357 } 358 } 359 if !hasD { 360 // go get without the -d flag does not make sense to us, as 361 // it will try to build and install go packages. 362 args = append([]string{"-d"}, args...) 363 } 364 if err := c.runGo(context.Background(), c.logger.Out(), append([]string{"get"}, args...)...); err != nil { 365 return fmt.Errorf("failed to get %q: %w", args, err) 366 } 367 return nil 368 } 369 370 // Init initializes this as a Go Module with the given path. 371 // If path is empty, Go will try to guess. 372 // If this succeeds, this project will be marked as Go Module. 373 func (c *Client) Init(path string) error { 374 err := c.runGo(context.Background(), c.logger.Out(), "mod", "init", path) 375 if err != nil { 376 return fmt.Errorf("failed to init modules: %w", err) 377 } 378 379 c.GoModulesFilename = filepath.Join(c.ccfg.WorkingDir, goModFilename) 380 381 return nil 382 } 383 384 var verifyErrorDirRe = regexp.MustCompile(`dir has been modified \((.*?)\)`) 385 386 // Verify checks that the dependencies of the current module, 387 // which are stored in a local downloaded source cache, have not been 388 // modified since being downloaded. 389 func (c *Client) Verify(clean bool) error { 390 // TODO(bep) add path to mod clean 391 err := c.runVerify() 392 if err != nil { 393 if clean { 394 m := verifyErrorDirRe.FindAllStringSubmatch(err.Error(), -1) 395 if m != nil { 396 for i := 0; i < len(m); i++ { 397 c, err := hugofs.MakeReadableAndRemoveAllModulePkgDir(c.fs, m[i][1]) 398 if err != nil { 399 return err 400 } 401 fmt.Println("Cleaned", c) 402 } 403 } 404 // Try to verify it again. 405 err = c.runVerify() 406 } 407 } 408 return err 409 } 410 411 func (c *Client) Clean(pattern string) error { 412 mods, err := c.listGoMods() 413 if err != nil { 414 return err 415 } 416 417 var g glob.Glob 418 419 if pattern != "" { 420 var err error 421 g, err = hglob.GetGlob(pattern) 422 if err != nil { 423 return err 424 } 425 } 426 427 for _, m := range mods { 428 if m.Replace != nil || m.Main { 429 continue 430 } 431 432 if g != nil && !g.Match(m.Path) { 433 continue 434 } 435 _, err = hugofs.MakeReadableAndRemoveAllModulePkgDir(c.fs, m.Dir) 436 if err == nil { 437 c.logger.Printf("hugo: cleaned module cache for %q", m.Path) 438 } 439 } 440 return err 441 } 442 443 func (c *Client) runVerify() error { 444 return c.runGo(context.Background(), ioutil.Discard, "mod", "verify") 445 } 446 447 func isProbablyModule(path string) bool { 448 return module.CheckPath(path) == nil 449 } 450 451 func (c *Client) listGoMods() (goModules, error) { 452 if c.GoModulesFilename == "" || !c.moduleConfig.hasModuleImport() { 453 return nil, nil 454 } 455 456 downloadModules := func(modules ...string) error { 457 args := []string{"mod", "download"} 458 args = append(args, modules...) 459 out := ioutil.Discard 460 err := c.runGo(context.Background(), out, args...) 461 if err != nil { 462 return fmt.Errorf("failed to download modules: %w", err) 463 } 464 return nil 465 } 466 467 if err := downloadModules(); err != nil { 468 return nil, err 469 } 470 471 listAndDecodeModules := func(handle func(m *goModule) error, modules ...string) error { 472 b := &bytes.Buffer{} 473 args := []string{"list", "-m", "-json"} 474 if len(modules) > 0 { 475 args = append(args, modules...) 476 } else { 477 args = append(args, "all") 478 } 479 err := c.runGo(context.Background(), b, args...) 480 if err != nil { 481 return fmt.Errorf("failed to list modules: %w", err) 482 } 483 484 dec := json.NewDecoder(b) 485 for { 486 m := &goModule{} 487 if err := dec.Decode(m); err != nil { 488 if err == io.EOF { 489 break 490 } 491 return fmt.Errorf("failed to decode modules list: %w", err) 492 } 493 494 if err := handle(m); err != nil { 495 return err 496 } 497 } 498 return nil 499 } 500 501 var modules goModules 502 err := listAndDecodeModules(func(m *goModule) error { 503 modules = append(modules, m) 504 return nil 505 }) 506 if err != nil { 507 return nil, err 508 } 509 510 // From Go 1.17, go lazy loads transitive dependencies. 511 // That does not work for us. 512 // So, download these modules and update the Dir in the modules list. 513 var modulesToDownload []string 514 for _, m := range modules { 515 if m.Dir == "" { 516 modulesToDownload = append(modulesToDownload, fmt.Sprintf("%s@%s", m.Path, m.Version)) 517 } 518 } 519 520 if len(modulesToDownload) > 0 { 521 if err := downloadModules(modulesToDownload...); err != nil { 522 return nil, err 523 } 524 err := listAndDecodeModules(func(m *goModule) error { 525 if mm := modules.GetByPath(m.Path); mm != nil { 526 mm.Dir = m.Dir 527 } 528 return nil 529 }, modulesToDownload...) 530 if err != nil { 531 return nil, err 532 } 533 } 534 535 return modules, err 536 } 537 538 func (c *Client) rewriteGoMod(name string, isGoMod map[string]bool) error { 539 data, err := c.rewriteGoModRewrite(name, isGoMod) 540 if err != nil { 541 return err 542 } 543 if data != nil { 544 if err := afero.WriteFile(c.fs, filepath.Join(c.ccfg.WorkingDir, name), data, 0666); err != nil { 545 return err 546 } 547 } 548 549 return nil 550 } 551 552 func (c *Client) rewriteGoModRewrite(name string, isGoMod map[string]bool) ([]byte, error) { 553 if name == goModFilename && c.GoModulesFilename == "" { 554 // Already checked. 555 return nil, nil 556 } 557 558 modlineSplitter := getModlineSplitter(name == goModFilename) 559 560 b := &bytes.Buffer{} 561 f, err := c.fs.Open(filepath.Join(c.ccfg.WorkingDir, name)) 562 if err != nil { 563 if os.IsNotExist(err) { 564 // It's been deleted. 565 return nil, nil 566 } 567 return nil, err 568 } 569 defer f.Close() 570 571 scanner := bufio.NewScanner(f) 572 var dirty bool 573 574 for scanner.Scan() { 575 line := scanner.Text() 576 var doWrite bool 577 578 if parts := modlineSplitter(line); parts != nil { 579 modname, modver := parts[0], parts[1] 580 modver = strings.TrimSuffix(modver, "/"+goModFilename) 581 modnameVer := modname + " " + modver 582 doWrite = isGoMod[modnameVer] 583 } else { 584 doWrite = true 585 } 586 587 if doWrite { 588 fmt.Fprintln(b, line) 589 } else { 590 dirty = true 591 } 592 } 593 594 if !dirty { 595 // Nothing changed 596 return nil, nil 597 } 598 599 return b.Bytes(), nil 600 } 601 602 func (c *Client) rmVendorDir(vendorDir string) error { 603 modulestxt := filepath.Join(vendorDir, vendorModulesFilename) 604 605 if _, err := c.fs.Stat(vendorDir); err != nil { 606 return nil 607 } 608 609 _, err := c.fs.Stat(modulestxt) 610 if err != nil { 611 // If we have a _vendor dir without modules.txt it sounds like 612 // a _vendor dir created by others. 613 return errors.New("found _vendor dir without modules.txt, skip delete") 614 } 615 616 return c.fs.RemoveAll(vendorDir) 617 } 618 619 func (c *Client) runGo( 620 ctx context.Context, 621 stdout io.Writer, 622 args ...string) error { 623 if c.goBinaryStatus != 0 { 624 return nil 625 } 626 627 stderr := new(bytes.Buffer) 628 629 argsv := collections.StringSliceToInterfaceSlice(args) 630 argsv = append(argsv, hexec.WithEnviron(c.environ)) 631 argsv = append(argsv, hexec.WithStderr(io.MultiWriter(stderr, os.Stderr))) 632 argsv = append(argsv, hexec.WithStdout(stdout)) 633 argsv = append(argsv, hexec.WithDir(c.ccfg.WorkingDir)) 634 argsv = append(argsv, hexec.WithContext(ctx)) 635 636 cmd, err := c.ccfg.Exec.New("go", argsv...) 637 if err != nil { 638 return err 639 } 640 641 if err := cmd.Run(); err != nil { 642 if ee, ok := err.(*exec.Error); ok && ee.Err == exec.ErrNotFound { 643 c.goBinaryStatus = goBinaryStatusNotFound 644 return nil 645 } 646 647 if strings.Contains(stderr.String(), "invalid version: unknown revision") { 648 // See https://github.com/gohugoio/hugo/issues/6825 649 c.logger.Println(`An unknown revision most likely means that someone has deleted the remote ref (e.g. with a force push to GitHub). 650 To resolve this, you need to manually edit your go.mod file and replace the version for the module in question with a valid ref. 651 652 The easiest is to just enter a valid branch name there, e.g. master, which would be what you put in place of 'v0.5.1' in the example below. 653 654 require github.com/gohugoio/hugo-mod-jslibs/instantpage v0.5.1 655 656 If you then run 'hugo mod graph' it should resolve itself to the most recent version (or commit if no semver versions are available).`) 657 } 658 659 _, ok := err.(*exec.ExitError) 660 if !ok { 661 return fmt.Errorf("failed to execute 'go %v': %s %T", args, err, err) 662 } 663 664 // Too old Go version 665 if strings.Contains(stderr.String(), "flag provided but not defined") { 666 c.goBinaryStatus = goBinaryStatusTooOld 667 return nil 668 } 669 670 return fmt.Errorf("go command failed: %s", stderr) 671 672 } 673 674 return nil 675 } 676 677 func (c *Client) tidy(mods Modules, goModOnly bool) error { 678 isGoMod := make(map[string]bool) 679 for _, m := range mods { 680 if m.Owner() == nil { 681 continue 682 } 683 if m.IsGoMod() { 684 // Matching the format in go.mod 685 pathVer := m.Path() + " " + m.Version() 686 isGoMod[pathVer] = true 687 } 688 } 689 690 if err := c.rewriteGoMod(goModFilename, isGoMod); err != nil { 691 return err 692 } 693 694 if goModOnly { 695 return nil 696 } 697 698 if err := c.rewriteGoMod(goSumFilename, isGoMod); err != nil { 699 return err 700 } 701 702 return nil 703 } 704 705 func (c *Client) shouldVendor(path string) bool { 706 return c.noVendor == nil || !c.noVendor.Match(path) 707 } 708 709 func (c *Client) createThemeDirname(modulePath string, isProjectMod bool) (string, error) { 710 invalid := fmt.Errorf("invalid module path %q; must be relative to themesDir when defined outside of the project", modulePath) 711 712 modulePath = filepath.Clean(modulePath) 713 if filepath.IsAbs(modulePath) { 714 if isProjectMod { 715 return modulePath, nil 716 } 717 return "", invalid 718 } 719 720 moduleDir := filepath.Join(c.ccfg.ThemesDir, modulePath) 721 if !isProjectMod && !strings.HasPrefix(moduleDir, c.ccfg.ThemesDir) { 722 return "", invalid 723 } 724 return moduleDir, nil 725 } 726 727 // ClientConfig configures the module Client. 728 type ClientConfig struct { 729 Fs afero.Fs 730 Logger loggers.Logger 731 732 // If set, it will be run before we do any duplicate checks for modules 733 // etc. 734 HookBeforeFinalize func(m *ModulesConfig) error 735 736 // Ignore any _vendor directory for module paths matching the given pattern. 737 // This can be nil. 738 IgnoreVendor glob.Glob 739 740 // Absolute path to the project dir. 741 WorkingDir string 742 743 // Absolute path to the project's themes dir. 744 ThemesDir string 745 746 // Eg. "production" 747 Environment string 748 749 Exec *hexec.Exec 750 751 CacheDir string // Module cache 752 ModuleConfig Config 753 } 754 755 func (c ClientConfig) shouldIgnoreVendor(path string) bool { 756 return c.IgnoreVendor != nil && c.IgnoreVendor.Match(path) 757 } 758 759 type goBinaryStatus int 760 761 type goModule struct { 762 Path string // module path 763 Version string // module version 764 Versions []string // available module versions (with -versions) 765 Replace *goModule // replaced by this module 766 Time *time.Time // time version was created 767 Update *goModule // available update, if any (with -u) 768 Main bool // is this the main module? 769 Indirect bool // is this module only an indirect dependency of main module? 770 Dir string // directory holding files for this module, if any 771 GoMod string // path to go.mod file for this module, if any 772 Error *goModuleError // error loading module 773 } 774 775 type goModuleError struct { 776 Err string // the error itself 777 } 778 779 type goModules []*goModule 780 781 func (modules goModules) GetByPath(p string) *goModule { 782 if modules == nil { 783 return nil 784 } 785 786 for _, m := range modules { 787 if strings.EqualFold(p, m.Path) { 788 return m 789 } 790 } 791 792 return nil 793 } 794 795 func (modules goModules) GetMain() *goModule { 796 for _, m := range modules { 797 if m.Main { 798 return m 799 } 800 } 801 802 return nil 803 } 804 805 func getModlineSplitter(isGoMod bool) func(line string) []string { 806 if isGoMod { 807 return func(line string) []string { 808 if strings.HasPrefix(line, "require (") { 809 return nil 810 } 811 if !strings.HasPrefix(line, "require") && !strings.HasPrefix(line, "\t") { 812 return nil 813 } 814 line = strings.TrimPrefix(line, "require") 815 line = strings.TrimSpace(line) 816 line = strings.TrimSuffix(line, "// indirect") 817 818 return strings.Fields(line) 819 } 820 } 821 822 return func(line string) []string { 823 return strings.Fields(line) 824 } 825 } 826 827 func pathVersion(m Module) string { 828 versionStr := m.Version() 829 if m.Vendor() { 830 versionStr += "+vendor" 831 } 832 if versionStr == "" { 833 return m.Path() 834 } 835 return fmt.Sprintf("%s@%s", m.Path(), versionStr) 836 }