github.com/linchen2chris/hugo@v0.0.0-20230307053224-cec209389705/modules/client.go (about) 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 "os" 24 "os/exec" 25 "path/filepath" 26 "regexp" 27 "strings" 28 "time" 29 30 "github.com/gohugoio/hugo/common/collections" 31 "github.com/gohugoio/hugo/common/herrors" 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 // 197 // hugo --ignoreVendorPaths=".*" 198 // 199 // Given a module tree, Hugo will pick the first module for a given path, 200 // meaning that if the top-level module is vendored, that will be the full 201 // set of dependencies. 202 func (c *Client) Vendor() error { 203 vendorDir := filepath.Join(c.ccfg.WorkingDir, vendord) 204 if err := c.rmVendorDir(vendorDir); err != nil { 205 return err 206 } 207 if err := c.fs.MkdirAll(vendorDir, 0755); err != nil { 208 return err 209 } 210 211 // Write the modules list to modules.txt. 212 // 213 // On the form: 214 // 215 // # github.com/alecthomas/chroma v0.6.3 216 // 217 // This is how "go mod vendor" does it. Go also lists 218 // the packages below it, but that is currently not applicable to us. 219 // 220 var modulesContent bytes.Buffer 221 222 tc, coll := c.collect(true) 223 if coll.err != nil { 224 return coll.err 225 } 226 227 for _, t := range tc.AllModules { 228 if t.Owner() == nil { 229 // This is the project. 230 continue 231 } 232 233 if !c.shouldVendor(t.Path()) { 234 continue 235 } 236 237 if !t.IsGoMod() && !t.Vendor() { 238 // We currently do not vendor components living in the 239 // theme directory, see https://github.com/gohugoio/hugo/issues/5993 240 continue 241 } 242 243 // See https://github.com/gohugoio/hugo/issues/8239 244 // This is an error situation. We need something to vendor. 245 if t.Mounts() == nil { 246 return fmt.Errorf("cannot vendor module %q, need at least one mount", t.Path()) 247 } 248 249 fmt.Fprintln(&modulesContent, "# "+t.Path()+" "+t.Version()) 250 251 dir := t.Dir() 252 253 for _, mount := range t.Mounts() { 254 sourceFilename := filepath.Join(dir, mount.Source) 255 targetFilename := filepath.Join(vendorDir, t.Path(), mount.Source) 256 fi, err := c.fs.Stat(sourceFilename) 257 if err != nil { 258 return fmt.Errorf("failed to vendor module: %w", err) 259 } 260 261 if fi.IsDir() { 262 if err := hugio.CopyDir(c.fs, sourceFilename, targetFilename, nil); err != nil { 263 return fmt.Errorf("failed to copy module to vendor dir: %w", err) 264 } 265 } else { 266 targetDir := filepath.Dir(targetFilename) 267 268 if err := c.fs.MkdirAll(targetDir, 0755); err != nil { 269 return fmt.Errorf("failed to make target dir: %w", err) 270 } 271 272 if err := hugio.CopyFile(c.fs, sourceFilename, targetFilename); err != nil { 273 return fmt.Errorf("failed to copy module file to vendor: %w", err) 274 } 275 } 276 } 277 278 // Include the resource cache if present. 279 resourcesDir := filepath.Join(dir, files.FolderResources) 280 _, err := c.fs.Stat(resourcesDir) 281 if err == nil { 282 if err := hugio.CopyDir(c.fs, resourcesDir, filepath.Join(vendorDir, t.Path(), files.FolderResources), nil); err != nil { 283 return fmt.Errorf("failed to copy resources to vendor dir: %w", err) 284 } 285 } 286 287 // Include the config directory if present. 288 configDir := filepath.Join(dir, "config") 289 _, err = c.fs.Stat(configDir) 290 if err == nil { 291 if err := hugio.CopyDir(c.fs, configDir, filepath.Join(vendorDir, t.Path(), "config"), nil); err != nil { 292 return fmt.Errorf("failed to copy config dir to vendor dir: %w", err) 293 } 294 } 295 296 // Also include any theme.toml or config.* files in the root. 297 configFiles, _ := afero.Glob(c.fs, filepath.Join(dir, "config.*")) 298 configFiles = append(configFiles, filepath.Join(dir, "theme.toml")) 299 for _, configFile := range configFiles { 300 if err := hugio.CopyFile(c.fs, configFile, filepath.Join(vendorDir, t.Path(), filepath.Base(configFile))); err != nil { 301 if !herrors.IsNotExist(err) { 302 return err 303 } 304 } 305 } 306 } 307 308 if modulesContent.Len() > 0 { 309 if err := afero.WriteFile(c.fs, filepath.Join(vendorDir, vendorModulesFilename), modulesContent.Bytes(), 0666); err != nil { 310 return err 311 } 312 } 313 314 return nil 315 } 316 317 // Get runs "go get" with the supplied arguments. 318 func (c *Client) Get(args ...string) error { 319 if len(args) == 0 || (len(args) == 1 && strings.Contains(args[0], "-u")) { 320 update := len(args) != 0 321 patch := update && (args[0] == "-u=patch") // 322 323 // We need to be explicit about the modules to get. 324 for _, m := range c.moduleConfig.Imports { 325 if !isProbablyModule(m.Path) { 326 // Skip themes/components stored below /themes etc. 327 // There may be false positives in the above, but those 328 // should be rare, and they will fail below with an 329 // "cannot find module providing ..." message. 330 continue 331 } 332 var args []string 333 334 if update && !patch { 335 args = append(args, "-u") 336 } else if update && patch { 337 args = append(args, "-u=patch") 338 } 339 args = append(args, m.Path) 340 341 if err := c.get(args...); err != nil { 342 return err 343 } 344 } 345 346 return nil 347 } 348 349 return c.get(args...) 350 } 351 352 func (c *Client) get(args ...string) error { 353 var hasD bool 354 for _, arg := range args { 355 if arg == "-d" { 356 hasD = true 357 break 358 } 359 } 360 if !hasD { 361 // go get without the -d flag does not make sense to us, as 362 // it will try to build and install go packages. 363 args = append([]string{"-d"}, args...) 364 } 365 if err := c.runGo(context.Background(), c.logger.Out(), append([]string{"get"}, args...)...); err != nil { 366 return fmt.Errorf("failed to get %q: %w", args, err) 367 } 368 return nil 369 } 370 371 // Init initializes this as a Go Module with the given path. 372 // If path is empty, Go will try to guess. 373 // If this succeeds, this project will be marked as Go Module. 374 func (c *Client) Init(path string) error { 375 err := c.runGo(context.Background(), c.logger.Out(), "mod", "init", path) 376 if err != nil { 377 return fmt.Errorf("failed to init modules: %w", err) 378 } 379 380 c.GoModulesFilename = filepath.Join(c.ccfg.WorkingDir, goModFilename) 381 382 return nil 383 } 384 385 var verifyErrorDirRe = regexp.MustCompile(`dir has been modified \((.*?)\)`) 386 387 // Verify checks that the dependencies of the current module, 388 // which are stored in a local downloaded source cache, have not been 389 // modified since being downloaded. 390 func (c *Client) Verify(clean bool) error { 391 // TODO(bep) add path to mod clean 392 err := c.runVerify() 393 if err != nil { 394 if clean { 395 m := verifyErrorDirRe.FindAllStringSubmatch(err.Error(), -1) 396 if m != nil { 397 for i := 0; i < len(m); i++ { 398 c, err := hugofs.MakeReadableAndRemoveAllModulePkgDir(c.fs, m[i][1]) 399 if err != nil { 400 return err 401 } 402 fmt.Println("Cleaned", c) 403 } 404 } 405 // Try to verify it again. 406 err = c.runVerify() 407 } 408 } 409 return err 410 } 411 412 func (c *Client) Clean(pattern string) error { 413 mods, err := c.listGoMods() 414 if err != nil { 415 return err 416 } 417 418 var g glob.Glob 419 420 if pattern != "" { 421 var err error 422 g, err = hglob.GetGlob(pattern) 423 if err != nil { 424 return err 425 } 426 } 427 428 for _, m := range mods { 429 if m.Replace != nil || m.Main { 430 continue 431 } 432 433 if g != nil && !g.Match(m.Path) { 434 continue 435 } 436 _, err = hugofs.MakeReadableAndRemoveAllModulePkgDir(c.fs, m.Dir) 437 if err == nil { 438 c.logger.Printf("hugo: cleaned module cache for %q", m.Path) 439 } 440 } 441 return err 442 } 443 444 func (c *Client) runVerify() error { 445 return c.runGo(context.Background(), io.Discard, "mod", "verify") 446 } 447 448 func isProbablyModule(path string) bool { 449 return module.CheckPath(path) == nil 450 } 451 452 func (c *Client) listGoMods() (goModules, error) { 453 if c.GoModulesFilename == "" || !c.moduleConfig.hasModuleImport() { 454 return nil, nil 455 } 456 457 downloadModules := func(modules ...string) error { 458 args := []string{"mod", "download"} 459 args = append(args, modules...) 460 out := io.Discard 461 err := c.runGo(context.Background(), out, args...) 462 if err != nil { 463 return fmt.Errorf("failed to download modules: %w", err) 464 } 465 return nil 466 } 467 468 if err := downloadModules(); err != nil { 469 return nil, err 470 } 471 472 listAndDecodeModules := func(handle func(m *goModule) error, modules ...string) error { 473 b := &bytes.Buffer{} 474 args := []string{"list", "-m", "-json"} 475 if len(modules) > 0 { 476 args = append(args, modules...) 477 } else { 478 args = append(args, "all") 479 } 480 err := c.runGo(context.Background(), b, args...) 481 if err != nil { 482 return fmt.Errorf("failed to list modules: %w", err) 483 } 484 485 dec := json.NewDecoder(b) 486 for { 487 m := &goModule{} 488 if err := dec.Decode(m); err != nil { 489 if err == io.EOF { 490 break 491 } 492 return fmt.Errorf("failed to decode modules list: %w", err) 493 } 494 495 if err := handle(m); err != nil { 496 return err 497 } 498 } 499 return nil 500 } 501 502 var modules goModules 503 err := listAndDecodeModules(func(m *goModule) error { 504 modules = append(modules, m) 505 return nil 506 }) 507 if err != nil { 508 return nil, err 509 } 510 511 // From Go 1.17, go lazy loads transitive dependencies. 512 // That does not work for us. 513 // So, download these modules and update the Dir in the modules list. 514 var modulesToDownload []string 515 for _, m := range modules { 516 if m.Dir == "" { 517 modulesToDownload = append(modulesToDownload, fmt.Sprintf("%s@%s", m.Path, m.Version)) 518 } 519 } 520 521 if len(modulesToDownload) > 0 { 522 if err := downloadModules(modulesToDownload...); err != nil { 523 return nil, err 524 } 525 err := listAndDecodeModules(func(m *goModule) error { 526 if mm := modules.GetByPath(m.Path); mm != nil { 527 mm.Dir = m.Dir 528 } 529 return nil 530 }, modulesToDownload...) 531 if err != nil { 532 return nil, err 533 } 534 } 535 536 return modules, err 537 } 538 539 func (c *Client) rewriteGoMod(name string, isGoMod map[string]bool) error { 540 data, err := c.rewriteGoModRewrite(name, isGoMod) 541 if err != nil { 542 return err 543 } 544 if data != nil { 545 if err := afero.WriteFile(c.fs, filepath.Join(c.ccfg.WorkingDir, name), data, 0666); err != nil { 546 return err 547 } 548 } 549 550 return nil 551 } 552 553 func (c *Client) rewriteGoModRewrite(name string, isGoMod map[string]bool) ([]byte, error) { 554 if name == goModFilename && c.GoModulesFilename == "" { 555 // Already checked. 556 return nil, nil 557 } 558 559 modlineSplitter := getModlineSplitter(name == goModFilename) 560 561 b := &bytes.Buffer{} 562 f, err := c.fs.Open(filepath.Join(c.ccfg.WorkingDir, name)) 563 if err != nil { 564 if herrors.IsNotExist(err) { 565 // It's been deleted. 566 return nil, nil 567 } 568 return nil, err 569 } 570 defer f.Close() 571 572 scanner := bufio.NewScanner(f) 573 var dirty bool 574 575 for scanner.Scan() { 576 line := scanner.Text() 577 var doWrite bool 578 579 if parts := modlineSplitter(line); parts != nil { 580 modname, modver := parts[0], parts[1] 581 modver = strings.TrimSuffix(modver, "/"+goModFilename) 582 modnameVer := modname + " " + modver 583 doWrite = isGoMod[modnameVer] 584 } else { 585 doWrite = true 586 } 587 588 if doWrite { 589 fmt.Fprintln(b, line) 590 } else { 591 dirty = true 592 } 593 } 594 595 if !dirty { 596 // Nothing changed 597 return nil, nil 598 } 599 600 return b.Bytes(), nil 601 } 602 603 func (c *Client) rmVendorDir(vendorDir string) error { 604 modulestxt := filepath.Join(vendorDir, vendorModulesFilename) 605 606 if _, err := c.fs.Stat(vendorDir); err != nil { 607 return nil 608 } 609 610 _, err := c.fs.Stat(modulestxt) 611 if err != nil { 612 // If we have a _vendor dir without modules.txt it sounds like 613 // a _vendor dir created by others. 614 return errors.New("found _vendor dir without modules.txt, skip delete") 615 } 616 617 return c.fs.RemoveAll(vendorDir) 618 } 619 620 func (c *Client) runGo( 621 ctx context.Context, 622 stdout io.Writer, 623 args ...string) error { 624 if c.goBinaryStatus != 0 { 625 return nil 626 } 627 628 stderr := new(bytes.Buffer) 629 630 argsv := collections.StringSliceToInterfaceSlice(args) 631 argsv = append(argsv, hexec.WithEnviron(c.environ)) 632 argsv = append(argsv, hexec.WithStderr(io.MultiWriter(stderr, os.Stderr))) 633 argsv = append(argsv, hexec.WithStdout(stdout)) 634 argsv = append(argsv, hexec.WithDir(c.ccfg.WorkingDir)) 635 argsv = append(argsv, hexec.WithContext(ctx)) 636 637 cmd, err := c.ccfg.Exec.New("go", argsv...) 638 if err != nil { 639 return err 640 } 641 642 if err := cmd.Run(); err != nil { 643 if ee, ok := err.(*exec.Error); ok && ee.Err == exec.ErrNotFound { 644 c.goBinaryStatus = goBinaryStatusNotFound 645 return nil 646 } 647 648 if strings.Contains(stderr.String(), "invalid version: unknown revision") { 649 // See https://github.com/gohugoio/hugo/issues/6825 650 c.logger.Println(`An unknown revision most likely means that someone has deleted the remote ref (e.g. with a force push to GitHub). 651 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. 652 653 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. 654 655 require github.com/gohugoio/hugo-mod-jslibs/instantpage v0.5.1 656 657 If you then run 'hugo mod graph' it should resolve itself to the most recent version (or commit if no semver versions are available).`) 658 } 659 660 _, ok := err.(*exec.ExitError) 661 if !ok { 662 return fmt.Errorf("failed to execute 'go %v': %s %T", args, err, err) 663 } 664 665 // Too old Go version 666 if strings.Contains(stderr.String(), "flag provided but not defined") { 667 c.goBinaryStatus = goBinaryStatusTooOld 668 return nil 669 } 670 671 return fmt.Errorf("go command failed: %s", stderr) 672 673 } 674 675 return nil 676 } 677 678 func (c *Client) tidy(mods Modules, goModOnly bool) error { 679 isGoMod := make(map[string]bool) 680 for _, m := range mods { 681 if m.Owner() == nil { 682 continue 683 } 684 if m.IsGoMod() { 685 // Matching the format in go.mod 686 pathVer := m.Path() + " " + m.Version() 687 isGoMod[pathVer] = true 688 } 689 } 690 691 if err := c.rewriteGoMod(goModFilename, isGoMod); err != nil { 692 return err 693 } 694 695 if goModOnly { 696 return nil 697 } 698 699 if err := c.rewriteGoMod(goSumFilename, isGoMod); err != nil { 700 return err 701 } 702 703 return nil 704 } 705 706 func (c *Client) shouldVendor(path string) bool { 707 return c.noVendor == nil || !c.noVendor.Match(path) 708 } 709 710 func (c *Client) createThemeDirname(modulePath string, isProjectMod bool) (string, error) { 711 invalid := fmt.Errorf("invalid module path %q; must be relative to themesDir when defined outside of the project", modulePath) 712 713 modulePath = filepath.Clean(modulePath) 714 if filepath.IsAbs(modulePath) { 715 if isProjectMod { 716 return modulePath, nil 717 } 718 return "", invalid 719 } 720 721 moduleDir := filepath.Join(c.ccfg.ThemesDir, modulePath) 722 if !isProjectMod && !strings.HasPrefix(moduleDir, c.ccfg.ThemesDir) { 723 return "", invalid 724 } 725 return moduleDir, nil 726 } 727 728 // ClientConfig configures the module Client. 729 type ClientConfig struct { 730 Fs afero.Fs 731 Logger loggers.Logger 732 733 // If set, it will be run before we do any duplicate checks for modules 734 // etc. 735 HookBeforeFinalize func(m *ModulesConfig) error 736 737 // Ignore any _vendor directory for module paths matching the given pattern. 738 // This can be nil. 739 IgnoreVendor glob.Glob 740 741 // Absolute path to the project dir. 742 WorkingDir string 743 744 // Absolute path to the project's themes dir. 745 ThemesDir string 746 747 // Eg. "production" 748 Environment string 749 750 Exec *hexec.Exec 751 752 CacheDir string // Module cache 753 ModuleConfig Config 754 } 755 756 func (c ClientConfig) shouldIgnoreVendor(path string) bool { 757 return c.IgnoreVendor != nil && c.IgnoreVendor.Match(path) 758 } 759 760 type goBinaryStatus int 761 762 type goModule struct { 763 Path string // module path 764 Version string // module version 765 Versions []string // available module versions (with -versions) 766 Replace *goModule // replaced by this module 767 Time *time.Time // time version was created 768 Update *goModule // available update, if any (with -u) 769 Main bool // is this the main module? 770 Indirect bool // is this module only an indirect dependency of main module? 771 Dir string // directory holding files for this module, if any 772 GoMod string // path to go.mod file for this module, if any 773 Error *goModuleError // error loading module 774 } 775 776 type goModuleError struct { 777 Err string // the error itself 778 } 779 780 type goModules []*goModule 781 782 func (modules goModules) GetByPath(p string) *goModule { 783 if modules == nil { 784 return nil 785 } 786 787 for _, m := range modules { 788 if strings.EqualFold(p, m.Path) { 789 return m 790 } 791 } 792 793 return nil 794 } 795 796 func (modules goModules) GetMain() *goModule { 797 for _, m := range modules { 798 if m.Main { 799 return m 800 } 801 } 802 803 return nil 804 } 805 806 func getModlineSplitter(isGoMod bool) func(line string) []string { 807 if isGoMod { 808 return func(line string) []string { 809 if strings.HasPrefix(line, "require (") { 810 return nil 811 } 812 if !strings.HasPrefix(line, "require") && !strings.HasPrefix(line, "\t") { 813 return nil 814 } 815 line = strings.TrimPrefix(line, "require") 816 line = strings.TrimSpace(line) 817 line = strings.TrimSuffix(line, "// indirect") 818 819 return strings.Fields(line) 820 } 821 } 822 823 return func(line string) []string { 824 return strings.Fields(line) 825 } 826 } 827 828 func pathVersion(m Module) string { 829 versionStr := m.Version() 830 if m.Vendor() { 831 versionStr += "+vendor" 832 } 833 if versionStr == "" { 834 return m.Path() 835 } 836 return fmt.Sprintf("%s@%s", m.Path(), versionStr) 837 }