shithub: hugo

Download patch

ref: 173187e2633f3fc037c83e1e3de2902ae3c93b92
parent: 8a1c637c4494751046142e0ef345fce38fc1431b
author: Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
date: Thu Oct 29 13:14:04 EDT 2020

Add module.replacements

Fixes #7904
Fixes #7908

--- a/modules/client.go
+++ b/modules/client.go
@@ -613,6 +613,15 @@
 	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
--- a/modules/client_test.go
+++ b/modules/client_test.go
@@ -15,6 +15,8 @@
 
 import (
 	"bytes"
+	"os"
+	"path/filepath"
 	"testing"
 
 	"github.com/gohugoio/hugo/hugofs/glob"
@@ -41,10 +43,14 @@
 
 		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)
@@ -129,6 +135,28 @@
 		var graphb bytes.Buffer
 		c.Assert(client.Graph(&graphb), qt.IsNil)
 		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))
+
 	})
 
 }
--- a/modules/collect.go
+++ b/modules/collect.go
@@ -274,10 +274,14 @@
 				}
 			}
 
-			// 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 @@
 		tc.cfg = cfg
 	}
 
-	config, err := DecodeConfig(cfg)
+	config, err := decodeConfig(cfg, c.moduleConfig.replacementsMap)
 	if err != nil {
 		return err
 	}
@@ -605,7 +609,6 @@
 
 		mnt.Source = filepath.Clean(mnt.Source)
 		mnt.Target = filepath.Clean(mnt.Target)
-
 		var sourceDir string
 
 		if owner.projectMod && filepath.IsAbs(mnt.Source) {
--- a/modules/config.go
+++ b/modules/config.go
@@ -18,6 +18,8 @@
 	"path/filepath"
 	"strings"
 
+	"github.com/pkg/errors"
+
 	"github.com/gohugoio/hugo/common/hugo"
 
 	"github.com/gohugoio/hugo/config"
@@ -40,6 +42,14 @@
 	// 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 @@
 
 // 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 @@
 			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)
@@ -232,6 +278,9 @@
 	// A optional Glob pattern matching module paths to skip when vendoring, e.g.
 	// "github.com/**".
 	NoVendor string
+
+	Replacements    []string
+	replacementsMap map[string]string
 
 	// Configures GOPROXY.
 	Proxy string
--- a/modules/config_test.go
+++ b/modules/config_test.go
@@ -41,7 +41,9 @@
 
 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 @@
 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.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")
+
+		}
+	})
 
 }