From 173187e2633f3fc037c83e1e3de2902ae3c93b92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Thu, 29 Oct 2020 17:14:04 +0100 Subject: [PATCH] Add module.replacements Fixes #7904 Fixes #7908 --- modules/client.go | 9 ++++++ modules/client_test.go | 28 ++++++++++++++++ modules/collect.go | 13 +++++--- modules/config.go | 49 ++++++++++++++++++++++++++++ modules/config_test.go | 72 ++++++++++++++++++++++++++++++------------ 5 files changed, 146 insertions(+), 25 deletions(-) diff --git a/modules/client.go b/modules/client.go index d07483d36a2..c6f43298da6 100644 --- a/modules/client.go +++ b/modules/client.go @@ -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 diff --git a/modules/client_test.go b/modules/client_test.go index 41509a9ed99..7354f15e81d 100644 --- a/modules/client_test.go +++ b/modules/client_test.go @@ -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("**") diff --git a/modules/collect.go b/modules/collect.go index 3059d3f99f5..e00fa540ff3 100644 --- a/modules/collect.go +++ b/modules/collect.go @@ -274,10 +274,14 @@ func (c *collector) add(owner *moduleAdapter, moduleImport Import, disabled bool } } - // Fall back to /themes/ + // Fall back to project/themes/ 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) { diff --git a/modules/config.go b/modules/config.go index e0a0ea060cd..1ce8c9f02be 100644 --- a/modules/config.go +++ b/modules/config.go @@ -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. diff --git a/modules/config_test.go b/modules/config_test.go index 60fa9586ea3..dd9dbc22f86 100644 --- a/modules/config_test.go +++ b/modules/config_test.go @@ -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") + } + }) }