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 }