Add module.replacements
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Thu, 29 Oct 2020 16:14:04 +0000 (17:14 +0100)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Fri, 30 Oct 2020 08:41:05 +0000 (09:41 +0100)
Fixes #7904
Fixes #7908

modules/client.go
modules/client_test.go
modules/collect.go
modules/config.go
modules/config_test.go

index d07483d36a25b7fe433d1db343c92275da14b698..c6f43298da6837c6e9a28196ddd78e4f61e36076 100644 (file)
@@ -613,6 +613,15 @@ func (c *Client) shouldVendor(path string) bool {
        return c.noVendor == nil || !c.noVendor.Match(path)
 }
 
+func (c *Client) createThemeDirname(modulePath string, isProjectMod bool) (string, error) {
+       modulePath = filepath.Clean(modulePath)
+       moduleDir := filepath.Join(c.ccfg.ThemesDir, modulePath)
+       if !isProjectMod && !strings.HasPrefix(moduleDir, c.ccfg.ThemesDir) {
+               return "", errors.Errorf("invalid module path %q; must be relative to themesDir when defined outside of the project", modulePath)
+       }
+       return moduleDir, nil
+}
+
 // ClientConfig configures the module Client.
 type ClientConfig struct {
        Fs     afero.Fs
index 41509a9ed99ebf7644496ef46c855175007bf0d5..7354f15e81d96b23479843413994c684c43986f7 100644 (file)
@@ -15,6 +15,8 @@ package modules
 
 import (
        "bytes"
+       "os"
+       "path/filepath"
        "testing"
 
        "github.com/gohugoio/hugo/hugofs/glob"
@@ -41,10 +43,14 @@ github.com/gohugoio/hugoTestModules1_darwin/modh2_2@v1.4.0 github.com/gohugoio/h
 
                workingDir, clean, err := htesting.CreateTempDir(hugofs.Os, modName)
                c.Assert(err, qt.IsNil)
+               themesDir := filepath.Join(workingDir, "themes")
+               err = os.Mkdir(themesDir, 0777)
+               c.Assert(err, qt.IsNil)
 
                ccfg := ClientConfig{
                        Fs:         hugofs.Os,
                        WorkingDir: workingDir,
+                       ThemesDir:  themesDir,
                }
 
                withConfig(&ccfg)
@@ -131,6 +137,28 @@ project github.com/gohugoio/hugoTestModules1_darwin/modh2_2_2@v1.3.0+vendor
                c.Assert(graphb.String(), qt.Equals, expect)
        })
 
+       // https://github.com/gohugoio/hugo/issues/7908
+       c.Run("createThemeDirname", func(c *qt.C) {
+               mcfg := DefaultModuleConfig
+               client, clean := newClient(
+                       c, func(cfg *ClientConfig) {
+                               cfg.ModuleConfig = mcfg
+                       })
+               defer clean()
+
+               dirname, err := client.createThemeDirname("foo", false)
+               c.Assert(err, qt.IsNil)
+               c.Assert(dirname, qt.Equals, filepath.Join(client.ccfg.ThemesDir, "foo"))
+
+               dirname, err = client.createThemeDirname("../../foo", true)
+               c.Assert(err, qt.IsNil)
+               c.Assert(dirname, qt.Equals, filepath.Join(client.ccfg.ThemesDir, "../../foo"))
+
+               dirname, err = client.createThemeDirname("../../foo", false)
+               c.Assert(err, qt.Not(qt.IsNil))
+
+       })
+
 }
 
 var globAll, _ = glob.GetGlob("**")
index 3059d3f99f540ff390887261f9e4b2368e500577..e00fa540ff3367e1988bd5338326bd9171f387f7 100644 (file)
@@ -274,10 +274,14 @@ func (c *collector) add(owner *moduleAdapter, moduleImport Import, disabled bool
                                }
                        }
 
-                       // Fall back to /themes/<mymodule>
+                       // Fall back to project/themes/<mymodule>
                        if moduleDir == "" {
-                               moduleDir = filepath.Join(c.ccfg.ThemesDir, modulePath)
-
+                               var err error
+                               moduleDir, err = c.createThemeDirname(modulePath, owner.projectMod)
+                               if err != nil {
+                                       c.err = err
+                                       return nil, nil
+                               }
                                if found, _ := afero.Exists(c.fs, moduleDir); !found {
                                        c.err = c.wrapModuleNotFound(errors.Errorf(`module %q not found; either add it as a Hugo Module or store it in %q.`, modulePath, c.ccfg.ThemesDir))
                                        return nil, nil
@@ -441,7 +445,7 @@ func (c *collector) applyThemeConfig(tc *moduleAdapter) error {
                tc.cfg = cfg
        }
 
-       config, err := DecodeConfig(cfg)
+       config, err := decodeConfig(cfg, c.moduleConfig.replacementsMap)
        if err != nil {
                return err
        }
@@ -605,7 +609,6 @@ func (c *collector) normalizeMounts(owner *moduleAdapter, mounts []Mount) ([]Mou
 
                mnt.Source = filepath.Clean(mnt.Source)
                mnt.Target = filepath.Clean(mnt.Target)
-
                var sourceDir string
 
                if owner.projectMod && filepath.IsAbs(mnt.Source) {
index e0a0ea060cde0d7d85545bb33090a44125c5b43f..1ce8c9f02be28df92d27acac95918a59dd59cd3d 100644 (file)
@@ -18,6 +18,8 @@ import (
        "path/filepath"
        "strings"
 
+       "github.com/pkg/errors"
+
        "github.com/gohugoio/hugo/common/hugo"
 
        "github.com/gohugoio/hugo/config"
@@ -40,6 +42,14 @@ var DefaultModuleConfig = Config{
        // Comma separated glob list matching paths that should be
        // treated as private.
        Private: "*.*",
+
+       // A list of replacement directives mapping a module path to a directory
+       // or a theme component in the themes folder.
+       // Note that this will turn the component into a traditional theme component
+       // that does not partake in vendoring etc.
+       // The syntax is the similar to the replacement directives used in go.mod, e.g:
+       //    github.com/mod1 -> ../mod1,github.com/mod2 -> ../mod2
+       Replacements: nil,
 }
 
 // ApplyProjectConfigDefaults applies default/missing module configuration for
@@ -182,7 +192,12 @@ func ApplyProjectConfigDefaults(cfg config.Provider, mod Module) error {
 
 // DecodeConfig creates a modules Config from a given Hugo configuration.
 func DecodeConfig(cfg config.Provider) (Config, error) {
+       return decodeConfig(cfg, nil)
+}
+
+func decodeConfig(cfg config.Provider, pathReplacements map[string]string) (Config, error) {
        c := DefaultModuleConfig
+       c.replacementsMap = pathReplacements
 
        if cfg == nil {
                return c, nil
@@ -197,6 +212,37 @@ func DecodeConfig(cfg config.Provider) (Config, error) {
                        return c, err
                }
 
+               if c.replacementsMap == nil {
+
+                       if len(c.Replacements) == 1 {
+                               c.Replacements = strings.Split(c.Replacements[0], ",")
+                       }
+
+                       for i, repl := range c.Replacements {
+                               c.Replacements[i] = strings.TrimSpace(repl)
+                       }
+
+                       c.replacementsMap = make(map[string]string)
+                       for _, repl := range c.Replacements {
+                               parts := strings.Split(repl, "->")
+                               if len(parts) != 2 {
+                                       return c, errors.Errorf(`invalid module.replacements: %q; configure replacement pairs on the form "oldpath->newpath" `, repl)
+                               }
+
+                               c.replacementsMap[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1])
+                       }
+               }
+
+               if c.replacementsMap != nil && c.Imports != nil {
+                       for i, imp := range c.Imports {
+                               if newImp, found := c.replacementsMap[imp.Path]; found {
+                                       imp.Path = newImp
+                                       c.Imports[i] = imp
+                               }
+                       }
+
+               }
+
                for i, mnt := range c.Mounts {
                        mnt.Source = filepath.Clean(mnt.Source)
                        mnt.Target = filepath.Clean(mnt.Target)
@@ -233,6 +279,9 @@ type Config struct {
        // "github.com/**".
        NoVendor string
 
+       Replacements    []string
+       replacementsMap map[string]string
+
        // Configures GOPROXY.
        Proxy string
        // Configures GONOPROXY.
index 60fa9586ea394585b59b3294993213663418e801..dd9dbc22f867475f4da80bca48141af898e505be 100644 (file)
@@ -41,7 +41,9 @@ func TestConfigHugoVersionIsValid(t *testing.T) {
 
 func TestDecodeConfig(t *testing.T) {
        c := qt.New(t)
-       tomlConfig := `
+
+       c.Run("Basic", func(c *qt.C) {
+               tomlConfig := `
 [module]
 
 [module.hugoVersion]
@@ -63,31 +65,61 @@ source="src/markdown/blog"
 target="content/blog"
 lang="en"
 `
-       cfg, err := config.FromConfigString(tomlConfig, "toml")
-       c.Assert(err, qt.IsNil)
+               cfg, err := config.FromConfigString(tomlConfig, "toml")
+               c.Assert(err, qt.IsNil)
 
-       mcfg, err := DecodeConfig(cfg)
-       c.Assert(err, qt.IsNil)
+               mcfg, err := DecodeConfig(cfg)
+               c.Assert(err, qt.IsNil)
 
-       v056 := hugo.VersionString("0.56.0")
+               v056 := hugo.VersionString("0.56.0")
 
-       hv := mcfg.HugoVersion
+               hv := mcfg.HugoVersion
 
-       c.Assert(v056.Compare(hv.Min), qt.Equals, -1)
-       c.Assert(v056.Compare(hv.Max), qt.Equals, 1)
-       c.Assert(hv.Extended, qt.Equals, true)
+               c.Assert(v056.Compare(hv.Min), qt.Equals, -1)
+               c.Assert(v056.Compare(hv.Max), qt.Equals, 1)
+               c.Assert(hv.Extended, qt.Equals, true)
 
-       if hugo.IsExtended {
-               c.Assert(hv.IsValid(), qt.Equals, true)
-       }
+               if hugo.IsExtended {
+                       c.Assert(hv.IsValid(), qt.Equals, true)
+               }
+
+               c.Assert(len(mcfg.Mounts), qt.Equals, 1)
+               c.Assert(len(mcfg.Imports), qt.Equals, 1)
+               imp := mcfg.Imports[0]
+               imp.Path = "github.com/bep/mycomponent"
+               c.Assert(imp.Mounts[1].Source, qt.Equals, "src/markdown/blog")
+               c.Assert(imp.Mounts[1].Target, qt.Equals, "content/blog")
+               c.Assert(imp.Mounts[1].Lang, qt.Equals, "en")
+       })
+
+       c.Run("Replacements", func(c *qt.C) {
+               for _, tomlConfig := range []string{`
+[module]
+replacements="a->b,github.com/bep/mycomponent->c"
+[[module.imports]]
+path="github.com/bep/mycomponent"
+`, `
+[module]
+replacements=["a->b","github.com/bep/mycomponent->c"]
+[[module.imports]]
+path="github.com/bep/mycomponent"
+`} {
+
+                       cfg, err := config.FromConfigString(tomlConfig, "toml")
+                       c.Assert(err, qt.IsNil)
+
+                       mcfg, err := DecodeConfig(cfg)
+                       c.Assert(err, qt.IsNil)
+                       c.Assert(mcfg.Replacements, qt.DeepEquals, []string{"a->b", "github.com/bep/mycomponent->c"})
+                       c.Assert(mcfg.replacementsMap, qt.DeepEquals, map[string]string{
+                               "a":                          "b",
+                               "github.com/bep/mycomponent": "c",
+                       })
+
+                       c.Assert(mcfg.Imports[0].Path, qt.Equals, "c")
 
-       c.Assert(len(mcfg.Mounts), qt.Equals, 1)
-       c.Assert(len(mcfg.Imports), qt.Equals, 1)
-       imp := mcfg.Imports[0]
-       imp.Path = "github.com/bep/mycomponent"
-       c.Assert(imp.Mounts[1].Source, qt.Equals, "src/markdown/blog")
-       c.Assert(imp.Mounts[1].Target, qt.Equals, "content/blog")
-       c.Assert(imp.Mounts[1].Lang, qt.Equals, "en")
+               }
+       })
 
 }