Create a struct with all of Hugo's config options

Primary motivation is documentation, but it will also hopefully simplify the code.

Also,

* Lower case the default output format names; this is in line with the custom ones (map keys) and how
it's treated all the places. This avoids doing `stringds.EqualFold` everywhere.

Closes #10896
Closes #10620
This commit is contained in:
Bjørn Erik Pedersen 2023-01-04 18:24:36 +01:00
parent 6aededf6b4
commit 241b21b0fd
337 changed files with 13377 additions and 14898 deletions

3
.gitignore vendored
View File

@ -1,2 +1,3 @@
*.test
*.test
imports.*

2
cache/docs.go vendored Normal file
View File

@ -0,0 +1,2 @@
// Package cache contains the differenct cache implementations.
package cache

View File

@ -35,7 +35,7 @@ import (
var ErrFatal = errors.New("fatal filecache error")
const (
filecacheRootDirname = "filecache"
FilecacheRootDirname = "filecache"
)
// Cache caches a set of files in a directory. This is usually a file on
@ -301,7 +301,7 @@ func (c *Cache) isExpired(modTime time.Time) bool {
}
// For testing
func (c *Cache) getString(id string) string {
func (c *Cache) GetString(id string) string {
id = cleanID(id)
c.nlocker.Lock(id)
@ -328,38 +328,24 @@ func (f Caches) Get(name string) *Cache {
// NewCaches creates a new set of file caches from the given
// configuration.
func NewCaches(p *helpers.PathSpec) (Caches, error) {
var dcfg Configs
if c, ok := p.Cfg.Get("filecacheConfigs").(Configs); ok {
dcfg = c
} else {
var err error
dcfg, err = DecodeConfig(p.Fs.Source, p.Cfg)
if err != nil {
return nil, err
}
}
dcfg := p.Cfg.GetConfigSection("caches").(Configs)
fs := p.Fs.Source
m := make(Caches)
for k, v := range dcfg {
var cfs afero.Fs
if v.isResourceDir {
if v.IsResourceDir {
cfs = p.BaseFs.ResourcesCache
} else {
cfs = fs
}
if cfs == nil {
// TODO(bep) we still have some places that do not initialize the
// full dependencies of a site, e.g. the import Jekyll command.
// That command does not need these caches, so let us just continue
// for now.
continue
panic("nil fs")
}
baseDir := v.Dir
baseDir := v.DirCompiled
if err := cfs.MkdirAll(baseDir, 0777); err != nil && !os.IsExist(err) {
return nil, err
@ -368,7 +354,7 @@ func NewCaches(p *helpers.PathSpec) (Caches, error) {
bfs := afero.NewBasePathFs(cfs, baseDir)
var pruneAllRootDir string
if k == cacheKeyModules {
if k == CacheKeyModules {
pruneAllRootDir = "pkg"
}

View File

@ -11,6 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// Package filecache provides a file based cache for Hugo.
package filecache
import (
@ -21,11 +22,8 @@ import (
"time"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/helpers"
"errors"
"github.com/mitchellh/mapstructure"
@ -33,98 +31,102 @@ import (
)
const (
cachesConfigKey = "caches"
resourcesGenDir = ":resourceDir/_gen"
cacheDirProject = ":cacheDir/:project"
)
var defaultCacheConfig = Config{
var defaultCacheConfig = FileCacheConfig{
MaxAge: -1, // Never expire
Dir: cacheDirProject,
}
const (
cacheKeyGetJSON = "getjson"
cacheKeyGetCSV = "getcsv"
cacheKeyImages = "images"
cacheKeyAssets = "assets"
cacheKeyModules = "modules"
cacheKeyGetResource = "getresource"
CacheKeyGetJSON = "getjson"
CacheKeyGetCSV = "getcsv"
CacheKeyImages = "images"
CacheKeyAssets = "assets"
CacheKeyModules = "modules"
CacheKeyGetResource = "getresource"
)
type Configs map[string]Config
type Configs map[string]FileCacheConfig
// For internal use.
func (c Configs) CacheDirModules() string {
return c[cacheKeyModules].Dir
return c[CacheKeyModules].DirCompiled
}
var defaultCacheConfigs = Configs{
cacheKeyModules: {
CacheKeyModules: {
MaxAge: -1,
Dir: ":cacheDir/modules",
},
cacheKeyGetJSON: defaultCacheConfig,
cacheKeyGetCSV: defaultCacheConfig,
cacheKeyImages: {
CacheKeyGetJSON: defaultCacheConfig,
CacheKeyGetCSV: defaultCacheConfig,
CacheKeyImages: {
MaxAge: -1,
Dir: resourcesGenDir,
},
cacheKeyAssets: {
CacheKeyAssets: {
MaxAge: -1,
Dir: resourcesGenDir,
},
cacheKeyGetResource: Config{
CacheKeyGetResource: FileCacheConfig{
MaxAge: -1, // Never expire
Dir: cacheDirProject,
},
}
type Config struct {
type FileCacheConfig struct {
// Max age of cache entries in this cache. Any items older than this will
// be removed and not returned from the cache.
// a negative value means forever, 0 means cache is disabled.
// A negative value means forever, 0 means cache is disabled.
// Hugo is leninent with what types it accepts here, but we recommend using
// a duration string, a sequence of decimal numbers, each with optional fraction and a unit suffix,
// such as "300ms", "1.5h" or "2h45m".
// Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h".
MaxAge time.Duration
// The directory where files are stored.
Dir string
Dir string
DirCompiled string `json:"-"`
// Will resources/_gen will get its own composite filesystem that
// also checks any theme.
isResourceDir bool
IsResourceDir bool
}
// GetJSONCache gets the file cache for getJSON.
func (f Caches) GetJSONCache() *Cache {
return f[cacheKeyGetJSON]
return f[CacheKeyGetJSON]
}
// GetCSVCache gets the file cache for getCSV.
func (f Caches) GetCSVCache() *Cache {
return f[cacheKeyGetCSV]
return f[CacheKeyGetCSV]
}
// ImageCache gets the file cache for processed images.
func (f Caches) ImageCache() *Cache {
return f[cacheKeyImages]
return f[CacheKeyImages]
}
// ModulesCache gets the file cache for Hugo Modules.
func (f Caches) ModulesCache() *Cache {
return f[cacheKeyModules]
return f[CacheKeyModules]
}
// AssetsCache gets the file cache for assets (processed resources, SCSS etc.).
func (f Caches) AssetsCache() *Cache {
return f[cacheKeyAssets]
return f[CacheKeyAssets]
}
// GetResourceCache gets the file cache for remote resources.
func (f Caches) GetResourceCache() *Cache {
return f[cacheKeyGetResource]
return f[CacheKeyGetResource]
}
func DecodeConfig(fs afero.Fs, cfg config.Provider) (Configs, error) {
func DecodeConfig(fs afero.Fs, bcfg config.BaseConfig, m map[string]any) (Configs, error) {
c := make(Configs)
valid := make(map[string]bool)
// Add defaults
@ -133,8 +135,6 @@ func DecodeConfig(fs afero.Fs, cfg config.Provider) (Configs, error) {
valid[k] = true
}
m := cfg.GetStringMap(cachesConfigKey)
_, isOsFs := fs.(*afero.OsFs)
for k, v := range m {
@ -170,9 +170,6 @@ func DecodeConfig(fs afero.Fs, cfg config.Provider) (Configs, error) {
c[name] = cc
}
// This is a very old flag in Hugo, but we need to respect it.
disabled := cfg.GetBool("ignoreCache")
for k, v := range c {
dir := filepath.ToSlash(filepath.Clean(v.Dir))
hadSlash := strings.HasPrefix(dir, "/")
@ -180,12 +177,12 @@ func DecodeConfig(fs afero.Fs, cfg config.Provider) (Configs, error) {
for i, part := range parts {
if strings.HasPrefix(part, ":") {
resolved, isResource, err := resolveDirPlaceholder(fs, cfg, part)
resolved, isResource, err := resolveDirPlaceholder(fs, bcfg, part)
if err != nil {
return c, err
}
if isResource {
v.isResourceDir = true
v.IsResourceDir = true
}
parts[i] = resolved
}
@ -195,33 +192,29 @@ func DecodeConfig(fs afero.Fs, cfg config.Provider) (Configs, error) {
if hadSlash {
dir = "/" + dir
}
v.Dir = filepath.Clean(filepath.FromSlash(dir))
v.DirCompiled = filepath.Clean(filepath.FromSlash(dir))
if !v.isResourceDir {
if isOsFs && !filepath.IsAbs(v.Dir) {
return c, fmt.Errorf("%q must resolve to an absolute directory", v.Dir)
if !v.IsResourceDir {
if isOsFs && !filepath.IsAbs(v.DirCompiled) {
return c, fmt.Errorf("%q must resolve to an absolute directory", v.DirCompiled)
}
// Avoid cache in root, e.g. / (Unix) or c:\ (Windows)
if len(strings.TrimPrefix(v.Dir, filepath.VolumeName(v.Dir))) == 1 {
return c, fmt.Errorf("%q is a root folder and not allowed as cache dir", v.Dir)
if len(strings.TrimPrefix(v.DirCompiled, filepath.VolumeName(v.DirCompiled))) == 1 {
return c, fmt.Errorf("%q is a root folder and not allowed as cache dir", v.DirCompiled)
}
}
if !strings.HasPrefix(v.Dir, "_gen") {
if !strings.HasPrefix(v.DirCompiled, "_gen") {
// We do cache eviction (file removes) and since the user can set
// his/hers own cache directory, we really want to make sure
// we do not delete any files that do not belong to this cache.
// We do add the cache name as the root, but this is an extra safe
// guard. We skip the files inside /resources/_gen/ because
// that would be breaking.
v.Dir = filepath.Join(v.Dir, filecacheRootDirname, k)
v.DirCompiled = filepath.Join(v.DirCompiled, FilecacheRootDirname, k)
} else {
v.Dir = filepath.Join(v.Dir, k)
}
if disabled {
v.MaxAge = 0
v.DirCompiled = filepath.Join(v.DirCompiled, k)
}
c[k] = v
@ -231,17 +224,15 @@ func DecodeConfig(fs afero.Fs, cfg config.Provider) (Configs, error) {
}
// Resolves :resourceDir => /myproject/resources etc., :cacheDir => ...
func resolveDirPlaceholder(fs afero.Fs, cfg config.Provider, placeholder string) (cacheDir string, isResource bool, err error) {
workingDir := cfg.GetString("workingDir")
func resolveDirPlaceholder(fs afero.Fs, bcfg config.BaseConfig, placeholder string) (cacheDir string, isResource bool, err error) {
switch strings.ToLower(placeholder) {
case ":resourcedir":
return "", true, nil
case ":cachedir":
d, err := helpers.GetCacheDir(fs, cfg)
return d, false, err
return bcfg.CacheDir, false, nil
case ":project":
return filepath.Base(workingDir), false, nil
return filepath.Base(bcfg.WorkingDir), false, nil
}
return "", false, fmt.Errorf("%q is not a valid placeholder (valid values are :cacheDir or :resourceDir)", placeholder)

View File

@ -11,18 +11,19 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package filecache
package filecache_test
import (
"path/filepath"
"runtime"
"strings"
"testing"
"time"
"github.com/spf13/afero"
"github.com/gohugoio/hugo/cache/filecache"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/config/testconfig"
qt "github.com/frankban/quicktest"
)
@ -57,22 +58,20 @@ dir = "/path/to/c4"
cfg, err := config.FromConfigString(configStr, "toml")
c.Assert(err, qt.IsNil)
fs := afero.NewMemMapFs()
decoded, err := DecodeConfig(fs, cfg)
c.Assert(err, qt.IsNil)
decoded := testconfig.GetTestConfigs(fs, cfg).Base.Caches
c.Assert(len(decoded), qt.Equals, 6)
c2 := decoded["getcsv"]
c.Assert(c2.MaxAge.String(), qt.Equals, "11h0m0s")
c.Assert(c2.Dir, qt.Equals, filepath.FromSlash("/path/to/c2/filecache/getcsv"))
c.Assert(c2.DirCompiled, qt.Equals, filepath.FromSlash("/path/to/c2/filecache/getcsv"))
c3 := decoded["images"]
c.Assert(c3.MaxAge, qt.Equals, time.Duration(-1))
c.Assert(c3.Dir, qt.Equals, filepath.FromSlash("/path/to/c3/filecache/images"))
c.Assert(c3.DirCompiled, qt.Equals, filepath.FromSlash("/path/to/c3/filecache/images"))
c4 := decoded["getresource"]
c.Assert(c4.MaxAge, qt.Equals, time.Duration(-1))
c.Assert(c4.Dir, qt.Equals, filepath.FromSlash("/path/to/c4/filecache/getresource"))
c.Assert(c4.DirCompiled, qt.Equals, filepath.FromSlash("/path/to/c4/filecache/getresource"))
}
func TestDecodeConfigIgnoreCache(t *testing.T) {
@ -106,9 +105,7 @@ dir = "/path/to/c4"
cfg, err := config.FromConfigString(configStr, "toml")
c.Assert(err, qt.IsNil)
fs := afero.NewMemMapFs()
decoded, err := DecodeConfig(fs, cfg)
c.Assert(err, qt.IsNil)
decoded := testconfig.GetTestConfigs(fs, cfg).Base.Caches
c.Assert(len(decoded), qt.Equals, 6)
for _, v := range decoded {
@ -118,7 +115,7 @@ dir = "/path/to/c4"
func TestDecodeConfigDefault(t *testing.T) {
c := qt.New(t)
cfg := newTestConfig()
cfg := config.New()
if runtime.GOOS == "windows" {
cfg.Set("resourceDir", "c:\\cache\\resources")
@ -128,71 +125,22 @@ func TestDecodeConfigDefault(t *testing.T) {
cfg.Set("resourceDir", "/cache/resources")
cfg.Set("cacheDir", "/cache/thecache")
}
cfg.Set("workingDir", filepath.FromSlash("/my/cool/hugoproject"))
fs := afero.NewMemMapFs()
decoded, err := DecodeConfig(fs, cfg)
c.Assert(err, qt.IsNil)
decoded := testconfig.GetTestConfigs(fs, cfg).Base.Caches
c.Assert(len(decoded), qt.Equals, 6)
imgConfig := decoded[cacheKeyImages]
jsonConfig := decoded[cacheKeyGetJSON]
imgConfig := decoded[filecache.CacheKeyImages]
jsonConfig := decoded[filecache.CacheKeyGetJSON]
if runtime.GOOS == "windows" {
c.Assert(imgConfig.Dir, qt.Equals, filepath.FromSlash("_gen/images"))
c.Assert(imgConfig.DirCompiled, qt.Equals, filepath.FromSlash("_gen/images"))
} else {
c.Assert(imgConfig.Dir, qt.Equals, "_gen/images")
c.Assert(jsonConfig.Dir, qt.Equals, "/cache/thecache/hugoproject/filecache/getjson")
c.Assert(imgConfig.DirCompiled, qt.Equals, "_gen/images")
c.Assert(jsonConfig.DirCompiled, qt.Equals, "/cache/thecache/hugoproject/filecache/getjson")
}
c.Assert(imgConfig.isResourceDir, qt.Equals, true)
c.Assert(jsonConfig.isResourceDir, qt.Equals, false)
}
func TestDecodeConfigInvalidDir(t *testing.T) {
t.Parallel()
c := qt.New(t)
configStr := `
resourceDir = "myresources"
contentDir = "content"
dataDir = "data"
i18nDir = "i18n"
layoutDir = "layouts"
assetDir = "assets"
archeTypedir = "archetypes"
[caches]
[caches.getJSON]
maxAge = "10m"
dir = "/"
`
if runtime.GOOS == "windows" {
configStr = strings.Replace(configStr, "/", "c:\\\\", 1)
}
cfg, err := config.FromConfigString(configStr, "toml")
c.Assert(err, qt.IsNil)
fs := afero.NewMemMapFs()
_, err = DecodeConfig(fs, cfg)
c.Assert(err, qt.Not(qt.IsNil))
}
func newTestConfig() config.Provider {
cfg := config.NewWithTestDefaults()
cfg.Set("workingDir", filepath.FromSlash("/my/cool/hugoproject"))
cfg.Set("contentDir", "content")
cfg.Set("dataDir", "data")
cfg.Set("resourceDir", "resources")
cfg.Set("i18nDir", "i18n")
cfg.Set("layoutDir", "layouts")
cfg.Set("archetypeDir", "archetypes")
cfg.Set("assetDir", "assets")
return cfg
c.Assert(imgConfig.IsResourceDir, qt.Equals, true)
c.Assert(jsonConfig.IsResourceDir, qt.Equals, false)
}

View File

@ -31,7 +31,6 @@ import (
func (c Caches) Prune() (int, error) {
counter := 0
for k, cache := range c {
count, err := cache.Prune(false)
counter += count
@ -58,6 +57,7 @@ func (c *Cache) Prune(force bool) (int, error) {
counter := 0
err := afero.Walk(c.Fs, "", func(name string, info os.FileInfo, err error) error {
if info == nil {
return nil
}

View File

@ -11,13 +11,14 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package filecache
package filecache_test
import (
"fmt"
"testing"
"time"
"github.com/gohugoio/hugo/cache/filecache"
"github.com/spf13/afero"
qt "github.com/frankban/quicktest"
@ -52,10 +53,10 @@ maxAge = "200ms"
dir = ":resourceDir/_gen"
`
for _, name := range []string{cacheKeyGetCSV, cacheKeyGetJSON, cacheKeyAssets, cacheKeyImages} {
for _, name := range []string{filecache.CacheKeyGetCSV, filecache.CacheKeyGetJSON, filecache.CacheKeyAssets, filecache.CacheKeyImages} {
msg := qt.Commentf("cache: %s", name)
p := newPathsSpec(t, afero.NewMemMapFs(), configStr)
caches, err := NewCaches(p)
caches, err := filecache.NewCaches(p)
c.Assert(err, qt.IsNil)
cache := caches[name]
for i := 0; i < 10; i++ {
@ -75,7 +76,7 @@ dir = ":resourceDir/_gen"
for i := 0; i < 10; i++ {
id := fmt.Sprintf("i%d", i)
v := cache.getString(id)
v := cache.GetString(id)
if i < 5 {
c.Assert(v, qt.Equals, "")
} else {
@ -83,7 +84,7 @@ dir = ":resourceDir/_gen"
}
}
caches, err = NewCaches(p)
caches, err = filecache.NewCaches(p)
c.Assert(err, qt.IsNil)
cache = caches[name]
// Touch one and then prune.
@ -98,7 +99,7 @@ dir = ":resourceDir/_gen"
// Now only the i5 should be left.
for i := 0; i < 10; i++ {
id := fmt.Sprintf("i%d", i)
v := cache.getString(id)
v := cache.GetString(id)
if i != 5 {
c.Assert(v, qt.Equals, "")
} else {

View File

@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package filecache
package filecache_test
import (
"errors"
@ -23,13 +23,10 @@ import (
"testing"
"time"
"github.com/gobwas/glob"
"github.com/gohugoio/hugo/langs"
"github.com/gohugoio/hugo/modules"
"github.com/gohugoio/hugo/cache/filecache"
"github.com/gohugoio/hugo/common/hugio"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/config/testconfig"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugofs"
@ -83,27 +80,19 @@ dir = ":cacheDir/c"
p := newPathsSpec(t, osfs, configStr)
caches, err := NewCaches(p)
caches, err := filecache.NewCaches(p)
c.Assert(err, qt.IsNil)
cache := caches.Get("GetJSON")
c.Assert(cache, qt.Not(qt.IsNil))
c.Assert(cache.maxAge.String(), qt.Equals, "10h0m0s")
bfs, ok := cache.Fs.(*afero.BasePathFs)
c.Assert(ok, qt.Equals, true)
filename, err := bfs.RealPath("key")
c.Assert(err, qt.IsNil)
if test.cacheDir != "" {
c.Assert(filename, qt.Equals, filepath.Join(test.cacheDir, "c/"+filecacheRootDirname+"/getjson/key"))
} else {
// Temp dir.
c.Assert(filename, qt.Matches, ".*hugo_cache.*"+filecacheRootDirname+".*key")
}
cache = caches.Get("Images")
c.Assert(cache, qt.Not(qt.IsNil))
c.Assert(cache.maxAge, qt.Equals, time.Duration(-1))
bfs, ok = cache.Fs.(*afero.BasePathFs)
c.Assert(ok, qt.Equals, true)
filename, _ = bfs.RealPath("key")
@ -125,7 +114,7 @@ dir = ":cacheDir/c"
return []byte("bcd"), nil
}
for _, ca := range []*Cache{caches.ImageCache(), caches.AssetsCache(), caches.GetJSONCache(), caches.GetCSVCache()} {
for _, ca := range []*filecache.Cache{caches.ImageCache(), caches.AssetsCache(), caches.GetJSONCache(), caches.GetCSVCache()} {
for i := 0; i < 2; i++ {
info, r, err := ca.GetOrCreate("a", rf("abc"))
c.Assert(err, qt.IsNil)
@ -160,7 +149,7 @@ dir = ":cacheDir/c"
c.Assert(info.Name, qt.Equals, "mykey")
io.WriteString(w, "Hugo is great!")
w.Close()
c.Assert(caches.ImageCache().getString("mykey"), qt.Equals, "Hugo is great!")
c.Assert(caches.ImageCache().GetString("mykey"), qt.Equals, "Hugo is great!")
info, r, err := caches.ImageCache().Get("mykey")
c.Assert(err, qt.IsNil)
@ -201,7 +190,7 @@ dir = "/cache/c"
p := newPathsSpec(t, afero.NewMemMapFs(), configStr)
caches, err := NewCaches(p)
caches, err := filecache.NewCaches(p)
c.Assert(err, qt.IsNil)
const cacheName = "getjson"
@ -244,11 +233,11 @@ func TestFileCacheReadOrCreateErrorInRead(t *testing.T) {
var result string
rf := func(failLevel int) func(info ItemInfo, r io.ReadSeeker) error {
return func(info ItemInfo, r io.ReadSeeker) error {
rf := func(failLevel int) func(info filecache.ItemInfo, r io.ReadSeeker) error {
return func(info filecache.ItemInfo, r io.ReadSeeker) error {
if failLevel > 0 {
if failLevel > 1 {
return ErrFatal
return filecache.ErrFatal
}
return errors.New("fail")
}
@ -260,8 +249,8 @@ func TestFileCacheReadOrCreateErrorInRead(t *testing.T) {
}
}
bf := func(s string) func(info ItemInfo, w io.WriteCloser) error {
return func(info ItemInfo, w io.WriteCloser) error {
bf := func(s string) func(info filecache.ItemInfo, w io.WriteCloser) error {
return func(info filecache.ItemInfo, w io.WriteCloser) error {
defer w.Close()
result = s
_, err := w.Write([]byte(s))
@ -269,7 +258,7 @@ func TestFileCacheReadOrCreateErrorInRead(t *testing.T) {
}
}
cache := NewCache(afero.NewMemMapFs(), 100*time.Hour, "")
cache := filecache.NewCache(afero.NewMemMapFs(), 100*time.Hour, "")
const id = "a32"
@ -283,60 +272,15 @@ func TestFileCacheReadOrCreateErrorInRead(t *testing.T) {
c.Assert(err, qt.IsNil)
c.Assert(result, qt.Equals, "v3")
_, err = cache.ReadOrCreate(id, rf(2), bf("v3"))
c.Assert(err, qt.Equals, ErrFatal)
}
func TestCleanID(t *testing.T) {
c := qt.New(t)
c.Assert(cleanID(filepath.FromSlash("/a/b//c.txt")), qt.Equals, filepath.FromSlash("a/b/c.txt"))
c.Assert(cleanID(filepath.FromSlash("a/b//c.txt")), qt.Equals, filepath.FromSlash("a/b/c.txt"))
}
func initConfig(fs afero.Fs, cfg config.Provider) error {
if _, err := langs.LoadLanguageSettings(cfg, nil); err != nil {
return err
}
modConfig, err := modules.DecodeConfig(cfg)
if err != nil {
return err
}
workingDir := cfg.GetString("workingDir")
themesDir := cfg.GetString("themesDir")
if !filepath.IsAbs(themesDir) {
themesDir = filepath.Join(workingDir, themesDir)
}
globAll := glob.MustCompile("**", '/')
modulesClient := modules.NewClient(modules.ClientConfig{
Fs: fs,
WorkingDir: workingDir,
ThemesDir: themesDir,
ModuleConfig: modConfig,
IgnoreVendor: globAll,
})
moduleConfig, err := modulesClient.Collect()
if err != nil {
return err
}
if err := modules.ApplyProjectConfigDefaults(cfg, moduleConfig.ActiveModules[len(moduleConfig.ActiveModules)-1]); err != nil {
return err
}
cfg.Set("allModules", moduleConfig.ActiveModules)
return nil
c.Assert(err, qt.Equals, filecache.ErrFatal)
}
func newPathsSpec(t *testing.T, fs afero.Fs, configStr string) *helpers.PathSpec {
c := qt.New(t)
cfg, err := config.FromConfigString(configStr, "toml")
c.Assert(err, qt.IsNil)
initConfig(fs, cfg)
config.SetBaseTestDefaults(cfg)
p, err := helpers.NewPathSpec(hugofs.NewFrom(fs, cfg), cfg, nil)
acfg := testconfig.GetTestConfig(fs, cfg)
p, err := helpers.NewPathSpec(hugofs.NewFrom(fs, acfg.BaseConfig()), acfg, nil)
c.Assert(err, qt.IsNil)
return p
}

View File

@ -15,6 +15,9 @@ package filecache_test
import (
"path/filepath"
jww "github.com/spf13/jwalterweatherman"
"testing"
"time"
@ -62,6 +65,7 @@ title: "Home"
-- assets/a/pixel.png --
iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==
-- layouts/index.html --
{{ warnf "HOME!" }}
{{ $img := resources.GetMatch "**.png" }}
{{ $img = $img.Resize "3x3" }}
{{ $img.RelPermalink }}
@ -71,10 +75,11 @@ iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAA
`
b := hugolib.NewIntegrationTestBuilder(
hugolib.IntegrationTestConfig{T: t, TxtarString: files, RunGC: true, NeedsOsFS: true},
hugolib.IntegrationTestConfig{T: t, TxtarString: files, Running: true, RunGC: true, NeedsOsFS: true, LogLevel: jww.LevelInfo},
).Build()
b.Assert(b.GCCount, qt.Equals, 0)
b.Assert(b.H, qt.IsNotNil)
imagesCacheDir := filepath.Join("_gen", "images")
_, err := b.H.BaseFs.ResourcesCache.Stat(imagesCacheDir)
@ -86,9 +91,11 @@ iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAA
time.Sleep(300 * time.Millisecond)
b.RenameFile("assets/a/pixel.png", "assets/b/pixel2.png").Build()
b.Assert(b.GCCount, qt.Equals, 1)
// Build it again to GC the empty a dir.
b.Build()
_, err = b.H.BaseFs.ResourcesCache.Stat(filepath.Join(imagesCacheDir, "a"))
b.Assert(err, qt.Not(qt.IsNil))
_, err = b.H.BaseFs.ResourcesCache.Stat(imagesCacheDir)

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
// Copyright 2023 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -14,331 +14,28 @@
package commands
import (
"fmt"
"os"
"time"
"github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/common/loggers"
hpaths "github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/helpers"
"github.com/spf13/cobra"
"github.com/bep/simplecobra"
)
type commandsBuilder struct {
hugoBuilderCommon
commands []cmder
}
func newCommandsBuilder() *commandsBuilder {
return &commandsBuilder{}
}
func (b *commandsBuilder) addCommands(commands ...cmder) *commandsBuilder {
b.commands = append(b.commands, commands...)
return b
}
func (b *commandsBuilder) addAll() *commandsBuilder {
b.addCommands(
b.newServerCmd(),
newVersionCmd(),
newEnvCmd(),
b.newConfigCmd(),
b.newDeployCmd(),
b.newConvertCmd(),
b.newNewCmd(),
b.newListCmd(),
newImportCmd(),
newGenCmd(),
createReleaser(),
b.newModCmd(),
)
return b
}
func (b *commandsBuilder) build() *hugoCmd {
h := b.newHugoCmd()
addCommands(h.getCommand(), b.commands...)
return h
}
func addCommands(root *cobra.Command, commands ...cmder) {
for _, command := range commands {
cmd := command.getCommand()
if cmd == nil {
continue
}
root.AddCommand(cmd)
}
}
type baseCmd struct {
cmd *cobra.Command
}
var _ commandsBuilderGetter = (*baseBuilderCmd)(nil)
// Used in tests.
type commandsBuilderGetter interface {
getCommandsBuilder() *commandsBuilder
}
type baseBuilderCmd struct {
*baseCmd
*commandsBuilder
}
func (b *baseBuilderCmd) getCommandsBuilder() *commandsBuilder {
return b.commandsBuilder
}
func (c *baseCmd) getCommand() *cobra.Command {
return c.cmd
}
func newBaseCmd(cmd *cobra.Command) *baseCmd {
return &baseCmd{cmd: cmd}
}
func (b *commandsBuilder) newBuilderCmd(cmd *cobra.Command) *baseBuilderCmd {
bcmd := &baseBuilderCmd{commandsBuilder: b, baseCmd: &baseCmd{cmd: cmd}}
bcmd.hugoBuilderCommon.handleFlags(cmd)
return bcmd
}
func (b *commandsBuilder) newBuilderBasicCmd(cmd *cobra.Command) *baseBuilderCmd {
bcmd := &baseBuilderCmd{commandsBuilder: b, baseCmd: &baseCmd{cmd: cmd}}
bcmd.hugoBuilderCommon.handleCommonBuilderFlags(cmd)
return bcmd
}
func (c *baseCmd) flagsToConfig(cfg config.Provider) {
initializeFlags(c.cmd, cfg)
}
type hugoCmd struct {
*baseBuilderCmd
// Need to get the sites once built.
c *commandeer
}
var _ cmder = (*nilCommand)(nil)
type nilCommand struct{}
func (c *nilCommand) getCommand() *cobra.Command {
return nil
}
func (c *nilCommand) flagsToConfig(cfg config.Provider) {
}
func (b *commandsBuilder) newHugoCmd() *hugoCmd {
cc := &hugoCmd{}
cc.baseBuilderCmd = b.newBuilderCmd(&cobra.Command{
Use: "hugo",
Short: "hugo builds your site",
Long: `hugo is the main command, used to build your Hugo site.
Hugo is a Fast and Flexible Static Site Generator
built with love by spf13 and friends in Go.
Complete documentation is available at https://gohugo.io/.`,
RunE: func(cmd *cobra.Command, args []string) error {
defer cc.timeTrack(time.Now(), "Total")
cfgInit := func(c *commandeer) error {
if cc.buildWatch {
c.Set("disableLiveReload", true)
}
return nil
}
// prevent cobra printing error so it can be handled here (before the timeTrack prints)
cmd.SilenceErrors = true
c, err := initializeConfig(true, true, cc.buildWatch, &cc.hugoBuilderCommon, cc, cfgInit)
if err != nil {
cmd.PrintErrln("Error:", err.Error())
return err
}
cc.c = c
err = c.build()
if err != nil {
cmd.PrintErrln("Error:", err.Error())
}
return err
// newExec wires up all of Hugo's CLI.
func newExec() (*simplecobra.Exec, error) {
rootCmd := &rootCommand{
commands: []simplecobra.Commander{
newVersionCmd(),
newEnvCommand(),
newServerCommand(),
newDeployCommand(),
newConfigCommand(),
newNewCommand(),
newConvertCommand(),
newImportCommand(),
newListCommand(),
newModCommands(),
newGenCommand(),
newReleaseCommand(),
},
})
}
cc.cmd.PersistentFlags().StringVar(&cc.cfgFile, "config", "", "config file (default is hugo.yaml|json|toml)")
cc.cmd.PersistentFlags().StringVar(&cc.cfgDir, "configDir", "config", "config dir")
cc.cmd.PersistentFlags().BoolVar(&cc.quiet, "quiet", false, "build in quiet mode")
return simplecobra.New(rootCmd)
// Set bash-completion
_ = cc.cmd.PersistentFlags().SetAnnotation("config", cobra.BashCompFilenameExt, config.ValidConfigFileExtensions)
cc.cmd.PersistentFlags().BoolVarP(&cc.verbose, "verbose", "v", false, "verbose output")
cc.cmd.PersistentFlags().BoolVarP(&cc.debug, "debug", "", false, "debug output")
cc.cmd.PersistentFlags().BoolVar(&cc.logging, "log", false, "enable Logging")
cc.cmd.PersistentFlags().StringVar(&cc.logFile, "logFile", "", "log File path (if set, logging enabled automatically)")
cc.cmd.PersistentFlags().BoolVar(&cc.verboseLog, "verboseLog", false, "verbose logging")
cc.cmd.Flags().BoolVarP(&cc.buildWatch, "watch", "w", false, "watch filesystem for changes and recreate as needed")
cc.cmd.Flags().Bool("renderToMemory", false, "render to memory (only useful for benchmark testing)")
// Set bash-completion
_ = cc.cmd.PersistentFlags().SetAnnotation("logFile", cobra.BashCompFilenameExt, []string{})
cc.cmd.SetGlobalNormalizationFunc(helpers.NormalizeHugoFlags)
cc.cmd.SilenceUsage = true
return cc
}
type hugoBuilderCommon struct {
source string
baseURL string
environment string
buildWatch bool
panicOnWarning bool
poll string
clock string
gc bool
// Profile flags (for debugging of performance problems)
cpuprofile string
memprofile string
mutexprofile string
traceprofile string
printm bool
// TODO(bep) var vs string
logging bool
verbose bool
verboseLog bool
debug bool
quiet bool
cfgFile string
cfgDir string
logFile string
}
func (cc *hugoBuilderCommon) timeTrack(start time.Time, name string) {
if cc.quiet {
return
}
elapsed := time.Since(start)
fmt.Printf("%s in %v ms\n", name, int(1000*elapsed.Seconds()))
}
func (cc *hugoBuilderCommon) getConfigDir(baseDir string) string {
if cc.cfgDir != "" {
return hpaths.AbsPathify(baseDir, cc.cfgDir)
}
if v, found := os.LookupEnv("HUGO_CONFIGDIR"); found {
return hpaths.AbsPathify(baseDir, v)
}
return hpaths.AbsPathify(baseDir, "config")
}
func (cc *hugoBuilderCommon) getEnvironment(isServer bool) string {
if cc.environment != "" {
return cc.environment
}
if v, found := os.LookupEnv("HUGO_ENVIRONMENT"); found {
return v
}
// Used by Netlify and Forestry
if v, found := os.LookupEnv("HUGO_ENV"); found {
return v
}
if isServer {
return hugo.EnvironmentDevelopment
}
return hugo.EnvironmentProduction
}
func (cc *hugoBuilderCommon) handleCommonBuilderFlags(cmd *cobra.Command) {
cmd.PersistentFlags().StringVarP(&cc.source, "source", "s", "", "filesystem path to read files relative from")
cmd.PersistentFlags().SetAnnotation("source", cobra.BashCompSubdirsInDir, []string{})
cmd.PersistentFlags().StringVarP(&cc.environment, "environment", "e", "", "build environment")
cmd.PersistentFlags().StringP("themesDir", "", "", "filesystem path to themes directory")
cmd.PersistentFlags().StringP("ignoreVendorPaths", "", "", "ignores any _vendor for module paths matching the given Glob pattern")
cmd.PersistentFlags().StringVar(&cc.clock, "clock", "", "set the clock used by Hugo, e.g. --clock 2021-11-06T22:30:00.00+09:00")
}
func (cc *hugoBuilderCommon) handleFlags(cmd *cobra.Command) {
cc.handleCommonBuilderFlags(cmd)
cmd.Flags().Bool("cleanDestinationDir", false, "remove files from destination not found in static directories")
cmd.Flags().BoolP("buildDrafts", "D", false, "include content marked as draft")
cmd.Flags().BoolP("buildFuture", "F", false, "include content with publishdate in the future")
cmd.Flags().BoolP("buildExpired", "E", false, "include expired content")
cmd.Flags().StringP("contentDir", "c", "", "filesystem path to content directory")
cmd.Flags().StringP("layoutDir", "l", "", "filesystem path to layout directory")
cmd.Flags().StringP("cacheDir", "", "", "filesystem path to cache directory. Defaults: $TMPDIR/hugo_cache/")
cmd.Flags().BoolP("ignoreCache", "", false, "ignores the cache directory")
cmd.Flags().StringP("destination", "d", "", "filesystem path to write files to")
cmd.Flags().StringSliceP("theme", "t", []string{}, "themes to use (located in /themes/THEMENAME/)")
cmd.Flags().StringVarP(&cc.baseURL, "baseURL", "b", "", "hostname (and path) to the root, e.g. https://spf13.com/")
cmd.Flags().Bool("enableGitInfo", false, "add Git revision, date, author, and CODEOWNERS info to the pages")
cmd.Flags().BoolVar(&cc.gc, "gc", false, "enable to run some cleanup tasks (remove unused cache files) after the build")
cmd.Flags().StringVar(&cc.poll, "poll", "", "set this to a poll interval, e.g --poll 700ms, to use a poll based approach to watch for file system changes")
cmd.Flags().BoolVar(&cc.panicOnWarning, "panicOnWarning", false, "panic on first WARNING log")
cmd.Flags().Bool("templateMetrics", false, "display metrics about template executions")
cmd.Flags().Bool("templateMetricsHints", false, "calculate some improvement hints when combined with --templateMetrics")
cmd.Flags().BoolP("forceSyncStatic", "", false, "copy all files when static is changed.")
cmd.Flags().BoolP("noTimes", "", false, "don't sync modification time of files")
cmd.Flags().BoolP("noChmod", "", false, "don't sync permission mode of files")
cmd.Flags().BoolP("noBuildLock", "", false, "don't create .hugo_build.lock file")
cmd.Flags().BoolP("printI18nWarnings", "", false, "print missing translations")
cmd.Flags().BoolP("printPathWarnings", "", false, "print warnings on duplicate target paths etc.")
cmd.Flags().BoolP("printUnusedTemplates", "", false, "print warnings on unused templates.")
cmd.Flags().StringVarP(&cc.cpuprofile, "profile-cpu", "", "", "write cpu profile to `file`")
cmd.Flags().StringVarP(&cc.memprofile, "profile-mem", "", "", "write memory profile to `file`")
cmd.Flags().BoolVarP(&cc.printm, "printMemoryUsage", "", false, "print memory usage to screen at intervals")
cmd.Flags().StringVarP(&cc.mutexprofile, "profile-mutex", "", "", "write Mutex profile to `file`")
cmd.Flags().StringVarP(&cc.traceprofile, "trace", "", "", "write trace to `file` (not useful in general)")
// Hide these for now.
cmd.Flags().MarkHidden("profile-cpu")
cmd.Flags().MarkHidden("profile-mem")
cmd.Flags().MarkHidden("profile-mutex")
cmd.Flags().StringSlice("disableKinds", []string{}, "disable different kind of pages (home, RSS etc.)")
cmd.Flags().Bool("minify", false, "minify any supported output format (HTML, XML etc.)")
// Set bash-completion.
// Each flag must first be defined before using the SetAnnotation() call.
_ = cmd.Flags().SetAnnotation("source", cobra.BashCompSubdirsInDir, []string{})
_ = cmd.Flags().SetAnnotation("cacheDir", cobra.BashCompSubdirsInDir, []string{})
_ = cmd.Flags().SetAnnotation("destination", cobra.BashCompSubdirsInDir, []string{})
_ = cmd.Flags().SetAnnotation("theme", cobra.BashCompSubdirsInDir, []string{"themes"})
}
func checkErr(logger loggers.Logger, err error, s ...string) {
if err == nil {
return
}
for _, message := range s {
logger.Errorln(message)
}
logger.Errorln(err)
}

View File

@ -1,411 +0,0 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package commands
import (
"fmt"
"os"
"path/filepath"
"testing"
"github.com/gohugoio/hugo/config"
"github.com/spf13/afero"
"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/common/types"
"github.com/spf13/cobra"
qt "github.com/frankban/quicktest"
)
func TestExecute(t *testing.T) {
c := qt.New(t)
createSite := func(c *qt.C) string {
dir := createSimpleTestSite(t, testSiteConfig{})
return dir
}
c.Run("hugo", func(c *qt.C) {
dir := createSite(c)
resp := Execute([]string{"-s=" + dir})
c.Assert(resp.Err, qt.IsNil)
result := resp.Result
c.Assert(len(result.Sites) == 1, qt.Equals, true)
c.Assert(len(result.Sites[0].RegularPages()) == 2, qt.Equals, true)
c.Assert(result.Sites[0].Info.Params()["myparam"], qt.Equals, "paramproduction")
})
c.Run("hugo, set environment", func(c *qt.C) {
dir := createSite(c)
resp := Execute([]string{"-s=" + dir, "-e=staging"})
c.Assert(resp.Err, qt.IsNil)
result := resp.Result
c.Assert(result.Sites[0].Info.Params()["myparam"], qt.Equals, "paramstaging")
})
c.Run("convert toJSON", func(c *qt.C) {
dir := createSite(c)
output := filepath.Join(dir, "myjson")
resp := Execute([]string{"convert", "toJSON", "-s=" + dir, "-e=staging", "-o=" + output})
c.Assert(resp.Err, qt.IsNil)
converted := readFileFrom(c, filepath.Join(output, "content", "p1.md"))
c.Assert(converted, qt.Equals, "{\n \"title\": \"P1\",\n \"weight\": 1\n}\n\nContent\n\n", qt.Commentf(converted))
})
c.Run("config, set environment", func(c *qt.C) {
dir := createSite(c)
out, err := captureStdout(func() error {
resp := Execute([]string{"config", "-s=" + dir, "-e=staging"})
return resp.Err
})
c.Assert(err, qt.IsNil)
c.Assert(out, qt.Contains, "params = map[myparam:paramstaging]", qt.Commentf(out))
})
c.Run("deploy, environment set", func(c *qt.C) {
dir := createSite(c)
resp := Execute([]string{"deploy", "-s=" + dir, "-e=staging", "--target=mydeployment", "--dryRun"})
c.Assert(resp.Err, qt.Not(qt.IsNil))
c.Assert(resp.Err.Error(), qt.Contains, `no driver registered for "hugocloud"`)
})
c.Run("list", func(c *qt.C) {
dir := createSite(c)
out, err := captureStdout(func() error {
resp := Execute([]string{"list", "all", "-s=" + dir, "-e=staging"})
return resp.Err
})
c.Assert(err, qt.IsNil)
c.Assert(out, qt.Contains, "p1.md")
})
c.Run("new theme", func(c *qt.C) {
dir := createSite(c)
themesDir := filepath.Join(dir, "mythemes")
resp := Execute([]string{"new", "theme", "mytheme", "-s=" + dir, "-e=staging", "--themesDir=" + themesDir})
c.Assert(resp.Err, qt.IsNil)
themeTOML := readFileFrom(c, filepath.Join(themesDir, "mytheme", "theme.toml"))
c.Assert(themeTOML, qt.Contains, "name = \"Mytheme\"")
})
c.Run("new site", func(c *qt.C) {
dir := createSite(c)
siteDir := filepath.Join(dir, "mysite")
resp := Execute([]string{"new", "site", siteDir, "-e=staging"})
c.Assert(resp.Err, qt.IsNil)
config := readFileFrom(c, filepath.Join(siteDir, "config.toml"))
c.Assert(config, qt.Contains, "baseURL = 'http://example.org/'")
checkNewSiteInited(c, siteDir)
})
}
func checkNewSiteInited(c *qt.C, basepath string) {
paths := []string{
filepath.Join(basepath, "archetypes"),
filepath.Join(basepath, "assets"),
filepath.Join(basepath, "content"),
filepath.Join(basepath, "data"),
filepath.Join(basepath, "layouts"),
filepath.Join(basepath, "static"),
filepath.Join(basepath, "themes"),
filepath.Join(basepath, "config.toml"),
}
for _, path := range paths {
_, err := os.Stat(path)
c.Assert(err, qt.IsNil)
}
}
func readFileFrom(c *qt.C, filename string) string {
c.Helper()
filename = filepath.Clean(filename)
b, err := afero.ReadFile(hugofs.Os, filename)
c.Assert(err, qt.IsNil)
return string(b)
}
func TestFlags(t *testing.T) {
c := qt.New(t)
noOpRunE := func(cmd *cobra.Command, args []string) error {
return nil
}
tests := []struct {
name string
args []string
check func(c *qt.C, cmd *serverCmd)
}{
{
// https://github.com/gohugoio/hugo/issues/7642
name: "ignoreVendorPaths",
args: []string{"server", "--ignoreVendorPaths=github.com/**"},
check: func(c *qt.C, cmd *serverCmd) {
cfg := config.NewWithTestDefaults()
cmd.flagsToConfig(cfg)
c.Assert(cfg.Get("ignoreVendorPaths"), qt.Equals, "github.com/**")
},
},
{
name: "Persistent flags",
args: []string{
"server",
"--config=myconfig.toml",
"--configDir=myconfigdir",
"--contentDir=mycontent",
"--disableKinds=page,home",
"--environment=testing",
"--configDir=myconfigdir",
"--layoutDir=mylayouts",
"--theme=mytheme",
"--gc",
"--themesDir=mythemes",
"--cleanDestinationDir",
"--navigateToChanged",
"--disableLiveReload",
"--noHTTPCache",
"--printI18nWarnings",
"--destination=/tmp/mydestination",
"-b=https://example.com/b/",
"--port=1366",
"--renderToDisk",
"--source=mysource",
"--printPathWarnings",
"--printUnusedTemplates",
},
check: func(c *qt.C, sc *serverCmd) {
c.Assert(sc, qt.Not(qt.IsNil))
c.Assert(sc.navigateToChanged, qt.Equals, true)
c.Assert(sc.disableLiveReload, qt.Equals, true)
c.Assert(sc.noHTTPCache, qt.Equals, true)
c.Assert(sc.renderToDisk, qt.Equals, true)
c.Assert(sc.serverPort, qt.Equals, 1366)
c.Assert(sc.environment, qt.Equals, "testing")
cfg := config.NewWithTestDefaults()
sc.flagsToConfig(cfg)
c.Assert(cfg.GetString("publishDir"), qt.Equals, "/tmp/mydestination")
c.Assert(cfg.GetString("contentDir"), qt.Equals, "mycontent")
c.Assert(cfg.GetString("layoutDir"), qt.Equals, "mylayouts")
c.Assert(cfg.GetStringSlice("theme"), qt.DeepEquals, []string{"mytheme"})
c.Assert(cfg.GetString("themesDir"), qt.Equals, "mythemes")
c.Assert(cfg.GetString("baseURL"), qt.Equals, "https://example.com/b/")
c.Assert(cfg.Get("disableKinds"), qt.DeepEquals, []string{"page", "home"})
c.Assert(cfg.GetBool("gc"), qt.Equals, true)
// The flag is named printPathWarnings
c.Assert(cfg.GetBool("logPathWarnings"), qt.Equals, true)
// The flag is named printI18nWarnings
c.Assert(cfg.GetBool("logI18nWarnings"), qt.Equals, true)
},
},
}
for _, test := range tests {
c.Run(test.name, func(c *qt.C) {
b := newCommandsBuilder()
root := b.addAll().build()
for _, cmd := range b.commands {
if cmd.getCommand() == nil {
continue
}
// We are only interested in the flag handling here.
cmd.getCommand().RunE = noOpRunE
}
rootCmd := root.getCommand()
rootCmd.SetArgs(test.args)
c.Assert(rootCmd.Execute(), qt.IsNil)
test.check(c, b.commands[0].(*serverCmd))
})
}
}
func TestCommandsExecute(t *testing.T) {
c := qt.New(t)
dir := createSimpleTestSite(t, testSiteConfig{})
dirOut := t.TempDir()
sourceFlag := fmt.Sprintf("-s=%s", dir)
tests := []struct {
commands []string
flags []string
expectErrToContain string
}{
// TODO(bep) permission issue on my OSX? "operation not permitted" {[]string{"check", "ulimit"}, nil, false},
{[]string{"env"}, nil, ""},
{[]string{"version"}, nil, ""},
// no args = hugo build
{nil, []string{sourceFlag}, ""},
{nil, []string{sourceFlag, "--renderToMemory"}, ""},
{[]string{"completion", "bash"}, nil, ""},
{[]string{"completion", "fish"}, nil, ""},
{[]string{"completion", "powershell"}, nil, ""},
{[]string{"completion", "zsh"}, nil, ""},
{[]string{"config"}, []string{sourceFlag}, ""},
{[]string{"convert", "toTOML"}, []string{sourceFlag, "-o=" + filepath.Join(dirOut, "toml")}, ""},
{[]string{"convert", "toYAML"}, []string{sourceFlag, "-o=" + filepath.Join(dirOut, "yaml")}, ""},
{[]string{"convert", "toJSON"}, []string{sourceFlag, "-o=" + filepath.Join(dirOut, "json")}, ""},
{[]string{"gen", "chromastyles"}, []string{"--style=manni"}, ""},
{[]string{"gen", "doc"}, []string{"--dir=" + filepath.Join(dirOut, "doc")}, ""},
{[]string{"gen", "man"}, []string{"--dir=" + filepath.Join(dirOut, "man")}, ""},
{[]string{"list", "drafts"}, []string{sourceFlag}, ""},
{[]string{"list", "expired"}, []string{sourceFlag}, ""},
{[]string{"list", "future"}, []string{sourceFlag}, ""},
{[]string{"new", "new-page.md"}, []string{sourceFlag}, ""},
{[]string{"new", "site", filepath.Join(dirOut, "new-site")}, nil, ""},
{[]string{"unknowncommand"}, nil, "unknown command"},
// TODO(bep) cli refactor fix https://github.com/gohugoio/hugo/issues/4450
//{[]string{"new", "theme", filepath.Join(dirOut, "new-theme")}, nil,false},
}
for _, test := range tests {
name := "hugo"
if len(test.commands) > 0 {
name = test.commands[0]
}
c.Run(name, func(c *qt.C) {
b := newCommandsBuilder().addAll().build()
hugoCmd := b.getCommand()
test.flags = append(test.flags, "--quiet")
hugoCmd.SetArgs(append(test.commands, test.flags...))
// TODO(bep) capture output and add some simple asserts
// TODO(bep) misspelled subcommands does not return an error. We should investigate this
// but before that, check for "Error: unknown command".
_, err := hugoCmd.ExecuteC()
if test.expectErrToContain != "" {
c.Assert(err, qt.Not(qt.IsNil))
c.Assert(err.Error(), qt.Contains, test.expectErrToContain)
} else {
c.Assert(err, qt.IsNil)
}
// Assert that we have not left any development debug artifacts in
// the code.
if b.c != nil {
_, ok := b.c.publishDirFs.(types.DevMarker)
c.Assert(ok, qt.Equals, false)
}
})
}
}
type testSiteConfig struct {
configTOML string
contentDir string
}
func createSimpleTestSite(t testing.TB, cfg testSiteConfig) string {
dir := t.TempDir()
cfgStr := `
baseURL = "https://example.org"
title = "Hugo Commands"
`
contentDir := "content"
if cfg.configTOML != "" {
cfgStr = cfg.configTOML
}
if cfg.contentDir != "" {
contentDir = cfg.contentDir
}
os.MkdirAll(filepath.Join(dir, "public"), 0777)
// Just the basic. These are for CLI tests, not site testing.
writeFile(t, filepath.Join(dir, "config.toml"), cfgStr)
writeFile(t, filepath.Join(dir, "config", "staging", "params.toml"), `myparam="paramstaging"`)
writeFile(t, filepath.Join(dir, "config", "staging", "deployment.toml"), `
[[targets]]
name = "mydeployment"
URL = "hugocloud://hugotestbucket"
`)
writeFile(t, filepath.Join(dir, "config", "testing", "params.toml"), `myparam="paramtesting"`)
writeFile(t, filepath.Join(dir, "config", "production", "params.toml"), `myparam="paramproduction"`)
writeFile(t, filepath.Join(dir, "static", "myfile.txt"), `Hello World!`)
writeFile(t, filepath.Join(dir, contentDir, "p1.md"), `
---
title: "P1"
weight: 1
---
Content
`)
writeFile(t, filepath.Join(dir, contentDir, "hügö.md"), `
---
weight: 2
---
This is hügö.
`)
writeFile(t, filepath.Join(dir, "layouts", "_default", "single.html"), `
Single: {{ .Title }}|{{ .Content }}
`)
writeFile(t, filepath.Join(dir, "layouts", "404.html"), `
404: {{ .Title }}|Not Found.
`)
writeFile(t, filepath.Join(dir, "layouts", "_default", "list.html"), `
List: {{ .Title }}
Environment: {{ hugo.Environment }}
For issue 9788:
{{ $foo :="abc" | resources.FromString "foo.css" | minify | resources.PostProcess }}
PostProcess: {{ $foo.RelPermalink }}
`)
return dir
}
func writeFile(t testing.TB, filename, content string) {
must(t, os.MkdirAll(filepath.Dir(filename), os.FileMode(0755)))
must(t, os.WriteFile(filename, []byte(content), os.FileMode(0755)))
}
func must(t testing.TB, err error) {
if err != nil {
t.Fatal(err)
}
}

View File

@ -1,4 +1,4 @@
// Copyright 2015 The Hugo Authors. All rights reserved.
// Copyright 2023 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -9,129 +9,93 @@
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.Print the version number of Hug
// limitations under the License.
package commands
import (
"context"
"encoding/json"
"fmt"
"os"
"reflect"
"regexp"
"sort"
"strings"
"time"
"github.com/gohugoio/hugo/common/maps"
"github.com/bep/simplecobra"
"github.com/gohugoio/hugo/modules"
"github.com/gohugoio/hugo/parser"
"github.com/gohugoio/hugo/parser/metadecoders"
"github.com/gohugoio/hugo/modules"
"github.com/spf13/cobra"
)
var _ cmder = (*configCmd)(nil)
type configCmd struct {
*baseBuilderCmd
}
func (b *commandsBuilder) newConfigCmd() *configCmd {
cc := &configCmd{}
cmd := &cobra.Command{
Use: "config",
Short: "Print the site configuration",
Long: `Print the site configuration, both default and custom settings.`,
RunE: cc.printConfig,
// newConfigCommand creates a new config command and its subcommands.
func newConfigCommand() *configCommand {
return &configCommand{
commands: []simplecobra.Commander{
&configMountsCommand{},
},
}
printMountsCmd := &cobra.Command{
Use: "mounts",
Short: "Print the configured file mounts",
RunE: cc.printMounts,
}
cmd.AddCommand(printMountsCmd)
cc.baseBuilderCmd = b.newBuilderBasicCmd(cmd)
return cc
}
func (c *configCmd) printMounts(cmd *cobra.Command, args []string) error {
cfg, err := initializeConfig(true, false, false, &c.hugoBuilderCommon, c, nil)
type configCommand struct {
r *rootCommand
commands []simplecobra.Commander
}
func (c *configCommand) Commands() []simplecobra.Commander {
return c.commands
}
func (c *configCommand) Name() string {
return "config"
}
func (c *configCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error {
conf, err := c.r.ConfigFromProvider(c.r.configVersionID.Load(), flagsToCfg(cd, nil))
if err != nil {
return err
}
config := conf.configs.Base
allModules := cfg.Cfg.Get("allmodules").(modules.Modules)
// Print it as JSON.
dec := json.NewEncoder(os.Stdout)
dec.SetIndent("", " ")
dec.SetEscapeHTML(false)
for _, m := range allModules {
if err := parser.InterfaceToConfig(&modMounts{m: m, verbose: c.verbose}, metadecoders.JSON, os.Stdout); err != nil {
return err
}
if err := dec.Encode(parser.ReplacingJSONMarshaller{Value: config, KeysToLower: true, OmitEmpty: true}); err != nil {
return err
}
return nil
}
func (c *configCmd) printConfig(cmd *cobra.Command, args []string) error {
cfg, err := initializeConfig(true, false, false, &c.hugoBuilderCommon, c, nil)
if err != nil {
return err
}
allSettings := cfg.Cfg.Get("").(maps.Params)
// We need to clean up this, but we store objects in the config that
// isn't really interesting to the end user, so filter these.
ignoreKeysRe := regexp.MustCompile("client|sorted|filecacheconfigs|allmodules|multilingual")
separator := ": "
if len(cfg.configFiles) > 0 && strings.HasSuffix(cfg.configFiles[0], ".toml") {
separator = " = "
}
var keys []string
for k := range allSettings {
if ignoreKeysRe.MatchString(k) {
continue
}
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
kv := reflect.ValueOf(allSettings[k])
if kv.Kind() == reflect.String {
fmt.Printf("%s%s\"%+v\"\n", k, separator, allSettings[k])
} else {
fmt.Printf("%s%s%+v\n", k, separator, allSettings[k])
}
}
func (c *configCommand) WithCobraCommand(cmd *cobra.Command) error {
cmd.Short = "Print the site configuration"
cmd.Long = `Print the site configuration, both default and custom settings.`
return nil
}
type modMounts struct {
verbose bool
m modules.Module
func (c *configCommand) Init(cd, runner *simplecobra.Commandeer) error {
c.r = cd.Root.Command.(*rootCommand)
return nil
}
type modMount struct {
type configModMount struct {
Source string `json:"source"`
Target string `json:"target"`
Lang string `json:"lang,omitempty"`
}
type configModMounts struct {
verbose bool
m modules.Module
}
// MarshalJSON is for internal use only.
func (m *modMounts) MarshalJSON() ([]byte, error) {
var mounts []modMount
func (m *configModMounts) MarshalJSON() ([]byte, error) {
var mounts []configModMount
for _, mount := range m.m.Mounts() {
mounts = append(mounts, modMount{
mounts = append(mounts, configModMount{
Source: mount.Source,
Target: mount.Target,
Lang: mount.Lang,
@ -154,7 +118,7 @@ func (m *modMounts) MarshalJSON() ([]byte, error) {
Meta map[string]any `json:"meta"`
HugoVersion modules.HugoVersion `json:"hugoVersion"`
Mounts []modMount `json:"mounts"`
Mounts []configModMount `json:"mounts"`
}{
Path: m.m.Path(),
Version: m.m.Version(),
@ -168,12 +132,12 @@ func (m *modMounts) MarshalJSON() ([]byte, error) {
}
return json.Marshal(&struct {
Path string `json:"path"`
Version string `json:"version"`
Time time.Time `json:"time"`
Owner string `json:"owner"`
Dir string `json:"dir"`
Mounts []modMount `json:"mounts"`
Path string `json:"path"`
Version string `json:"version"`
Time time.Time `json:"time"`
Owner string `json:"owner"`
Dir string `json:"dir"`
Mounts []configModMount `json:"mounts"`
}{
Path: m.m.Path(),
Version: m.m.Version(),
@ -184,3 +148,40 @@ func (m *modMounts) MarshalJSON() ([]byte, error) {
})
}
type configMountsCommand struct {
configCmd *configCommand
}
func (c *configMountsCommand) Commands() []simplecobra.Commander {
return nil
}
func (c *configMountsCommand) Name() string {
return "mounts"
}
func (c *configMountsCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error {
r := c.configCmd.r
conf, err := r.ConfigFromProvider(r.configVersionID.Load(), flagsToCfg(cd, nil))
if err != nil {
return err
}
for _, m := range conf.configs.Modules {
if err := parser.InterfaceToConfig(&configModMounts{m: m, verbose: r.verbose}, metadecoders.JSON, os.Stdout); err != nil {
return err
}
}
return nil
}
func (c *configMountsCommand) WithCobraCommand(cmd *cobra.Command) error {
cmd.Short = "Print the configured file mounts"
return nil
}
func (c *configMountsCommand) Init(cd, runner *simplecobra.Commandeer) error {
c.configCmd = cd.Parent.Command.(*configCommand)
return nil
}

View File

@ -1,4 +1,4 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
// Copyright 2023 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -15,122 +15,119 @@ package commands
import (
"bytes"
"context"
"fmt"
"path/filepath"
"strings"
"time"
"github.com/gohugoio/hugo/parser/pageparser"
"github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/hugofs"
"github.com/bep/simplecobra"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/hugolib"
"github.com/gohugoio/hugo/parser"
"github.com/gohugoio/hugo/parser/metadecoders"
"github.com/gohugoio/hugo/hugolib"
"github.com/gohugoio/hugo/parser/pageparser"
"github.com/gohugoio/hugo/resources/page"
"github.com/spf13/cobra"
)
var _ cmder = (*convertCmd)(nil)
func newConvertCommand() *convertCommand {
var c *convertCommand
c = &convertCommand{
commands: []simplecobra.Commander{
&simpleCommand{
name: "toJSON",
short: "Convert front matter to JSON",
long: `toJSON converts all front matter in the content directory
to use JSON for the front matter.`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
return c.convertContents(metadecoders.JSON)
},
withc: func(cmd *cobra.Command) {
},
},
&simpleCommand{
name: "toTOML",
short: "Convert front matter to TOML",
long: `toTOML converts all front matter in the content directory
to use TOML for the front matter.`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
return c.convertContents(metadecoders.TOML)
},
withc: func(cmd *cobra.Command) {
},
},
&simpleCommand{
name: "toYAML",
short: "Convert front matter to YAML",
long: `toYAML converts all front matter in the content directory
to use YAML for the front matter.`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
return c.convertContents(metadecoders.YAML)
},
withc: func(cmd *cobra.Command) {
},
},
},
}
return c
}
type convertCmd struct {
type convertCommand struct {
// Flags.
outputDir string
unsafe bool
*baseBuilderCmd
// Deps.
r *rootCommand
h *hugolib.HugoSites
// Commmands.
commands []simplecobra.Commander
}
func (b *commandsBuilder) newConvertCmd() *convertCmd {
cc := &convertCmd{}
cmd := &cobra.Command{
Use: "convert",
Short: "Convert your content to different formats",
Long: `Convert your content (e.g. front matter) to different formats.
See convert's subcommands toJSON, toTOML and toYAML for more information.`,
RunE: nil,
}
cmd.AddCommand(
&cobra.Command{
Use: "toJSON",
Short: "Convert front matter to JSON",
Long: `toJSON converts all front matter in the content directory
to use JSON for the front matter.`,
RunE: func(cmd *cobra.Command, args []string) error {
return cc.convertContents(metadecoders.JSON)
},
},
&cobra.Command{
Use: "toTOML",
Short: "Convert front matter to TOML",
Long: `toTOML converts all front matter in the content directory
to use TOML for the front matter.`,
RunE: func(cmd *cobra.Command, args []string) error {
return cc.convertContents(metadecoders.TOML)
},
},
&cobra.Command{
Use: "toYAML",
Short: "Convert front matter to YAML",
Long: `toYAML converts all front matter in the content directory
to use YAML for the front matter.`,
RunE: func(cmd *cobra.Command, args []string) error {
return cc.convertContents(metadecoders.YAML)
},
},
)
cmd.PersistentFlags().StringVarP(&cc.outputDir, "output", "o", "", "filesystem path to write files to")
cmd.PersistentFlags().BoolVar(&cc.unsafe, "unsafe", false, "enable less safe operations, please backup first")
cc.baseBuilderCmd = b.newBuilderBasicCmd(cmd)
return cc
func (c *convertCommand) Commands() []simplecobra.Commander {
return c.commands
}
func (cc *convertCmd) convertContents(format metadecoders.Format) error {
if cc.outputDir == "" && !cc.unsafe {
return newUserError("Unsafe operation not allowed, use --unsafe or set a different output path")
}
func (c *convertCommand) Name() string {
return "convert"
}
c, err := initializeConfig(true, false, false, &cc.hugoBuilderCommon, cc, nil)
if err != nil {
return err
}
c.Cfg.Set("buildDrafts", true)
h, err := hugolib.NewHugoSites(*c.DepsCfg)
if err != nil {
return err
}
if err := h.Build(hugolib.BuildCfg{SkipRender: true}); err != nil {
return err
}
site := h.Sites[0]
site.Log.Println("processing", len(site.AllPages()), "content files")
for _, p := range site.AllPages() {
if err := cc.convertAndSavePage(p, site, format); err != nil {
return err
}
}
func (c *convertCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error {
return nil
}
func (cc *convertCmd) convertAndSavePage(p page.Page, site *hugolib.Site, targetFormat metadecoders.Format) error {
func (c *convertCommand) WithCobraCommand(cmd *cobra.Command) error {
cmd.Short = "Convert your content to different formats"
cmd.Long = `Convert your content (e.g. front matter) to different formats.
See convert's subcommands toJSON, toTOML and toYAML for more information.`
cmd.PersistentFlags().StringVarP(&c.outputDir, "output", "o", "", "filesystem path to write files to")
cmd.PersistentFlags().BoolVar(&c.unsafe, "unsafe", false, "enable less safe operations, please backup first")
return nil
}
func (c *convertCommand) Init(cd, runner *simplecobra.Commandeer) error {
c.r = cd.Root.Command.(*rootCommand)
cfg := config.New()
cfg.Set("buildDrafts", true)
h, err := c.r.Hugo(flagsToCfg(cd, cfg))
if err != nil {
return err
}
c.h = h
return nil
}
func (c *convertCommand) convertAndSavePage(p page.Page, site *hugolib.Site, targetFormat metadecoders.Format) error {
// The resources are not in .Site.AllPages.
for _, r := range p.Resources().ByType("page") {
if err := cc.convertAndSavePage(r.(page.Page), site, targetFormat); err != nil {
if err := c.convertAndSavePage(r.(page.Page), site, targetFormat); err != nil {
return err
}
}
@ -140,9 +137,9 @@ func (cc *convertCmd) convertAndSavePage(p page.Page, site *hugolib.Site, target
return nil
}
errMsg := fmt.Errorf("Error processing file %q", p.File().Path())
errMsg := fmt.Errorf("error processing file %q", p.File().Path())
site.Log.Infoln("Attempting to convert", p.File().Filename())
site.Log.Infoln("ttempting to convert", p.File().Filename())
f := p.File()
file, err := f.FileInfo().Meta().Open()
@ -182,26 +179,45 @@ func (cc *convertCmd) convertAndSavePage(p page.Page, site *hugolib.Site, target
newFilename := p.File().Filename()
if cc.outputDir != "" {
if c.outputDir != "" {
contentDir := strings.TrimSuffix(newFilename, p.File().Path())
contentDir = filepath.Base(contentDir)
newFilename = filepath.Join(cc.outputDir, contentDir, p.File().Path())
newFilename = filepath.Join(c.outputDir, contentDir, p.File().Path())
}
fs := hugofs.Os
if err := helpers.WriteToDisk(newFilename, &newContent, fs); err != nil {
return fmt.Errorf("Failed to save file %q:: %w", newFilename, err)
return fmt.Errorf("failed to save file %q:: %w", newFilename, err)
}
return nil
}
type parsedFile struct {
frontMatterFormat metadecoders.Format
frontMatterSource []byte
frontMatter map[string]any
func (c *convertCommand) convertContents(format metadecoders.Format) error {
if c.outputDir == "" && !c.unsafe {
return newUserError("Unsafe operation not allowed, use --unsafe or set a different output path")
}
// Everything after Front Matter
content []byte
if err := c.h.Build(hugolib.BuildCfg{SkipRender: true}); err != nil {
return err
}
site := c.h.Sites[0]
var pagesBackedByFile page.Pages
for _, p := range site.AllPages() {
if p.File().IsZero() {
continue
}
pagesBackedByFile = append(pagesBackedByFile, p)
}
site.Log.Println("processing", len(pagesBackedByFile), "content files")
for _, p := range site.AllPages() {
if err := c.convertAndSavePage(p, site, format); err != nil {
return err
}
}
return nil
}

View File

@ -1,4 +1,4 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
// Copyright 2023 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -14,76 +14,58 @@
//go:build !nodeploy
// +build !nodeploy
// Copyright 2023 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package commands
import (
"context"
"github.com/bep/simplecobra"
"github.com/gohugoio/hugo/deploy"
"github.com/spf13/cobra"
)
var _ cmder = (*deployCmd)(nil)
func newDeployCommand() simplecobra.Commander {
// deployCmd supports deploying sites to Cloud providers.
type deployCmd struct {
*baseBuilderCmd
invalidateCDN bool
maxDeletes int
workers int
}
// TODO: In addition to the "deploy" command, consider adding a "--deploy"
// flag for the default command; this would build the site and then deploy it.
// It's not obvious how to do this; would all of the deploy-specific flags
// have to exist at the top level as well?
// TODO: The output files change every time "hugo" is executed, it looks
// like because of map order randomization. This means that you can
// run "hugo && hugo deploy" again and again and upload new stuff every time. Is
// this intended?
func (b *commandsBuilder) newDeployCmd() *deployCmd {
cc := &deployCmd{}
cmd := &cobra.Command{
Use: "deploy",
Short: "Deploy your site to a Cloud provider.",
Long: `Deploy your site to a Cloud provider.
return &simpleCommand{
name: "deploy",
short: "Deploy your site to a Cloud provider.",
long: `Deploy your site to a Cloud provider.
See https://gohugo.io/hosting-and-deployment/hugo-deploy/ for detailed
documentation.
`,
RunE: func(cmd *cobra.Command, args []string) error {
cfgInit := func(c *commandeer) error {
c.Set("invalidateCDN", cc.invalidateCDN)
c.Set("maxDeletes", cc.maxDeletes)
c.Set("workers", cc.workers)
return nil
}
comm, err := initializeConfig(true, true, false, &cc.hugoBuilderCommon, cc, cfgInit)
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
h, err := r.Hugo(flagsToCfgWithAdditionalConfigBase(cd, nil, "deployment"))
if err != nil {
return err
}
deployer, err := deploy.New(comm.Cfg, comm.hugo().PathSpec.PublishFs)
deployer, err := deploy.New(h.Configs.GetFirstLanguageConfig(), h.PathSpec.PublishFs)
if err != nil {
return err
}
return deployer.Deploy(context.Background())
return deployer.Deploy(ctx)
},
withc: func(cmd *cobra.Command) {
cmd.Flags().String("target", "", "target deployment from deployments section in config file; defaults to the first one")
cmd.Flags().Bool("confirm", false, "ask for confirmation before making changes to the target")
cmd.Flags().Bool("dryRun", false, "dry run")
cmd.Flags().Bool("force", false, "force upload of all files")
cmd.Flags().Bool("invalidateCDN", true, "invalidate the CDN cache listed in the deployment target")
cmd.Flags().Int("maxDeletes", 256, "maximum # of files to delete, or -1 to disable")
cmd.Flags().Int("workers", 10, "number of workers to transfer files. defaults to 10")
},
}
cmd.Flags().String("target", "", "target deployment from deployments section in config file; defaults to the first one")
cmd.Flags().Bool("confirm", false, "ask for confirmation before making changes to the target")
cmd.Flags().Bool("dryRun", false, "dry run")
cmd.Flags().Bool("force", false, "force upload of all files")
cmd.Flags().BoolVar(&cc.invalidateCDN, "invalidateCDN", true, "invalidate the CDN cache listed in the deployment target")
cmd.Flags().IntVar(&cc.maxDeletes, "maxDeletes", 256, "maximum # of files to delete, or -1 to disable")
cmd.Flags().IntVar(&cc.workers, "workers", 10, "number of workers to transfer files. defaults to 10")
cc.baseBuilderCmd = b.newBuilderBasicCmd(cmd)
return cc
}

48
commands/deploy_off.go Normal file
View File

@ -0,0 +1,48 @@
// Copyright 2023 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build nodeploy
// +build nodeploy
// Copyright 2023 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package commands
import (
"errors"
"github.com/spf13/cobra"
)
func newDeployCommand() simplecobra.Commander {
return &simpleCommand{
name: "deploy",
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
return nil
},
withc: func(cmd *cobra.Command) {
cmd.Hidden = true
},
}
}

View File

@ -1,4 +1,4 @@
// Copyright 2016 The Hugo Authors. All rights reserved.
// Copyright 2023 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -14,55 +14,50 @@
package commands
import (
"context"
"runtime"
"github.com/bep/simplecobra"
"github.com/gohugoio/hugo/common/hugo"
"github.com/spf13/cobra"
jww "github.com/spf13/jwalterweatherman"
)
var _ cmder = (*envCmd)(nil)
type envCmd struct {
*baseCmd
}
func newEnvCmd() *envCmd {
return &envCmd{
baseCmd: newBaseCmd(&cobra.Command{
Use: "env",
Short: "Print Hugo version and environment info",
Long: `Print Hugo version and environment info. This is useful in Hugo bug reports.
If you add the -v flag, you will get a full dependency list.
`,
RunE: func(cmd *cobra.Command, args []string) error {
printHugoVersion()
jww.FEEDBACK.Printf("GOOS=%q\n", runtime.GOOS)
jww.FEEDBACK.Printf("GOARCH=%q\n", runtime.GOARCH)
jww.FEEDBACK.Printf("GOVERSION=%q\n", runtime.Version())
isVerbose, _ := cmd.Flags().GetBool("verbose")
if isVerbose {
deps := hugo.GetDependencyList()
for _, dep := range deps {
jww.FEEDBACK.Printf("%s\n", dep)
}
} else {
// These are also included in the GetDependencyList above;
// always print these as these are most likely the most useful to know about.
deps := hugo.GetDependencyListNonGo()
for _, dep := range deps {
jww.FEEDBACK.Printf("%s\n", dep)
}
func newEnvCommand() simplecobra.Commander {
return &simpleCommand{
name: "env",
short: "Print Hugo version and environment info",
long: "Print Hugo version and environment info. This is useful in Hugo bug reports",
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
r.Printf("%s\n", hugo.BuildVersionString())
r.Printf("GOOS=%q\n", runtime.GOOS)
r.Printf("GOARCH=%q\n", runtime.GOARCH)
r.Printf("GOVERSION=%q\n", runtime.Version())
if r.verbose {
deps := hugo.GetDependencyList()
for _, dep := range deps {
r.Printf("%s\n", dep)
}
return nil
},
}),
} else {
// These are also included in the GetDependencyList above;
// always print these as these are most likely the most useful to know about.
deps := hugo.GetDependencyListNonGo()
for _, dep := range deps {
r.Printf("%s\n", dep)
}
}
return nil
},
}
}
func newVersionCmd() simplecobra.Commander {
return &simpleCommand{
name: "version",
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
r.Println(hugo.BuildVersionString())
return nil
},
short: "Print Hugo version and environment info",
long: "Print Hugo version and environment info. This is useful in Hugo bug reports.",
}
}

View File

@ -1,4 +1,4 @@
// Copyright 2015 The Hugo Authors. All rights reserved.
// Copyright 2023 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -14,27 +14,200 @@
package commands
import (
"context"
"fmt"
"os"
"path"
"path/filepath"
"strings"
"github.com/alecthomas/chroma/v2"
"github.com/alecthomas/chroma/v2/formatters/html"
"github.com/alecthomas/chroma/v2/styles"
"github.com/bep/simplecobra"
"github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugofs"
"github.com/spf13/cobra"
"github.com/spf13/cobra/doc"
)
var _ cmder = (*genCmd)(nil)
func newGenCommand() *genCommand {
var (
// Flags.
gendocdir string
genmandir string
// Chroma flags.
style string
highlightStyle string
linesStyle string
)
newChromaStyles := func() simplecobra.Commander {
return &simpleCommand{
name: "chromastyles",
short: "Generate CSS stylesheet for the Chroma code highlighter",
long: `Generate CSS stylesheet for the Chroma code highlighter for a given style. This stylesheet is needed if markup.highlight.noClasses is disabled in config.
See https://xyproto.github.io/splash/docs/all.html for a preview of the available styles`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
builder := styles.Get(style).Builder()
if highlightStyle != "" {
builder.Add(chroma.LineHighlight, highlightStyle)
}
if linesStyle != "" {
builder.Add(chroma.LineNumbers, linesStyle)
}
style, err := builder.Build()
if err != nil {
return err
}
formatter := html.New(html.WithAllClasses(true))
formatter.WriteCSS(os.Stdout, style)
return nil
},
withc: func(cmd *cobra.Command) {
cmd.PersistentFlags().StringVar(&style, "style", "friendly", "highlighter style (see https://xyproto.github.io/splash/docs/)")
cmd.PersistentFlags().StringVar(&highlightStyle, "highlightStyle", "bg:#ffffcc", "style used for highlighting lines (see https://github.com/alecthomas/chroma)")
cmd.PersistentFlags().StringVar(&linesStyle, "linesStyle", "", "style used for line numbers (see https://github.com/alecthomas/chroma)")
},
}
}
newMan := func() simplecobra.Commander {
return &simpleCommand{
name: "man",
short: "Generate man pages for the Hugo CLI",
long: `This command automatically generates up-to-date man pages of Hugo's
command-line interface. By default, it creates the man page files
in the "man" directory under the current directory.`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
header := &doc.GenManHeader{
Section: "1",
Manual: "Hugo Manual",
Source: fmt.Sprintf("Hugo %s", hugo.CurrentVersion),
}
if !strings.HasSuffix(genmandir, helpers.FilePathSeparator) {
genmandir += helpers.FilePathSeparator
}
if found, _ := helpers.Exists(genmandir, hugofs.Os); !found {
r.Println("Directory", genmandir, "does not exist, creating...")
if err := hugofs.Os.MkdirAll(genmandir, 0777); err != nil {
return err
}
}
cd.CobraCommand.Root().DisableAutoGenTag = true
r.Println("Generating Hugo man pages in", genmandir, "...")
doc.GenManTree(cd.CobraCommand.Root(), header, genmandir)
r.Println("Done.")
return nil
},
withc: func(cmd *cobra.Command) {
cmd.PersistentFlags().StringVar(&genmandir, "dir", "man/", "the directory to write the man pages.")
// For bash-completion
cmd.PersistentFlags().SetAnnotation("dir", cobra.BashCompSubdirsInDir, []string{})
},
}
}
newGen := func() simplecobra.Commander {
const gendocFrontmatterTemplate = `---
title: "%s"
slug: %s
url: %s
---
`
return &simpleCommand{
name: "doc",
short: "Generate Markdown documentation for the Hugo CLI.",
long: `Generate Markdown documentation for the Hugo CLI.
This command is, mostly, used to create up-to-date documentation
of Hugo's command-line interface for https://gohugo.io/.
It creates one Markdown file per command with front matter suitable
for rendering in Hugo.`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
cd.CobraCommand.VisitParents(func(c *cobra.Command) {
// Disable the "Auto generated by spf13/cobra on DATE"
// as it creates a lot of diffs.
c.DisableAutoGenTag = true
})
if !strings.HasSuffix(gendocdir, helpers.FilePathSeparator) {
gendocdir += helpers.FilePathSeparator
}
if found, _ := helpers.Exists(gendocdir, hugofs.Os); !found {
r.Println("Directory", gendocdir, "does not exist, creating...")
if err := hugofs.Os.MkdirAll(gendocdir, 0777); err != nil {
return err
}
}
prepender := func(filename string) string {
name := filepath.Base(filename)
base := strings.TrimSuffix(name, path.Ext(name))
url := "/commands/" + strings.ToLower(base) + "/"
return fmt.Sprintf(gendocFrontmatterTemplate, strings.Replace(base, "_", " ", -1), base, url)
}
linkHandler := func(name string) string {
base := strings.TrimSuffix(name, path.Ext(name))
return "/commands/" + strings.ToLower(base) + "/"
}
r.Println("Generating Hugo command-line documentation in", gendocdir, "...")
doc.GenMarkdownTreeCustom(cd.CobraCommand.Root(), gendocdir, prepender, linkHandler)
r.Println("Done.")
return nil
},
withc: func(cmd *cobra.Command) {
cmd.PersistentFlags().StringVar(&gendocdir, "dir", "/tmp/hugodoc/", "the directory to write the doc.")
// For bash-completion
cmd.PersistentFlags().SetAnnotation("dir", cobra.BashCompSubdirsInDir, []string{})
},
}
}
return &genCommand{
commands: []simplecobra.Commander{
newChromaStyles(),
newGen(),
newMan(),
},
}
type genCmd struct {
*baseCmd
}
func newGenCmd() *genCmd {
cc := &genCmd{}
cc.baseCmd = newBaseCmd(&cobra.Command{
Use: "gen",
Short: "A collection of several useful generators.",
})
type genCommand struct {
rootCmd *rootCommand
cc.cmd.AddCommand(
newGenDocCmd().getCommand(),
newGenManCmd().getCommand(),
createGenDocsHelper().getCommand(),
createGenChromaStyles().getCommand())
return cc
commands []simplecobra.Commander
}
func (c *genCommand) Commands() []simplecobra.Commander {
return c.commands
}
func (c *genCommand) Name() string {
return "gen"
}
func (c *genCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error {
return nil
}
func (c *genCommand) WithCobraCommand(cmd *cobra.Command) error {
cmd.Short = "A collection of several useful generators."
return nil
}
func (c *genCommand) Init(cd, runner *simplecobra.Commandeer) error {
c.rootCmd = cd.Root.Command.(*rootCommand)
return nil
}

View File

@ -1,72 +0,0 @@
// Copyright 2017-present The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package commands
import (
"os"
"github.com/alecthomas/chroma/v2"
"github.com/alecthomas/chroma/v2/formatters/html"
"github.com/alecthomas/chroma/v2/styles"
"github.com/spf13/cobra"
)
var _ cmder = (*genChromaStyles)(nil)
type genChromaStyles struct {
style string
highlightStyle string
linesStyle string
*baseCmd
}
// TODO(bep) highlight
func createGenChromaStyles() *genChromaStyles {
g := &genChromaStyles{
baseCmd: newBaseCmd(&cobra.Command{
Use: "chromastyles",
Short: "Generate CSS stylesheet for the Chroma code highlighter",
Long: `Generate CSS stylesheet for the Chroma code highlighter for a given style. This stylesheet is needed if markup.highlight.noClasses is disabled in config.
See https://xyproto.github.io/splash/docs/all.html for a preview of the available styles`,
}),
}
g.cmd.RunE = func(cmd *cobra.Command, args []string) error {
return g.generate()
}
g.cmd.PersistentFlags().StringVar(&g.style, "style", "friendly", "highlighter style (see https://xyproto.github.io/splash/docs/)")
g.cmd.PersistentFlags().StringVar(&g.highlightStyle, "highlightStyle", "bg:#ffffcc", "style used for highlighting lines (see https://github.com/alecthomas/chroma)")
g.cmd.PersistentFlags().StringVar(&g.linesStyle, "linesStyle", "", "style used for line numbers (see https://github.com/alecthomas/chroma)")
return g
}
func (g *genChromaStyles) generate() error {
builder := styles.Get(g.style).Builder()
if g.highlightStyle != "" {
builder.Add(chroma.LineHighlight, g.highlightStyle)
}
if g.linesStyle != "" {
builder.Add(chroma.LineNumbers, g.linesStyle)
}
style, err := builder.Build()
if err != nil {
return err
}
formatter := html.New(html.WithAllClasses(true))
formatter.WriteCSS(os.Stdout, style)
return nil
}

View File

@ -1,98 +0,0 @@
// Copyright 2016 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package commands
import (
"fmt"
"path"
"path/filepath"
"strings"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugofs"
"github.com/spf13/cobra"
"github.com/spf13/cobra/doc"
jww "github.com/spf13/jwalterweatherman"
)
var _ cmder = (*genDocCmd)(nil)
type genDocCmd struct {
gendocdir string
*baseCmd
}
func newGenDocCmd() *genDocCmd {
const gendocFrontmatterTemplate = `---
title: "%s"
slug: %s
url: %s
---
`
cc := &genDocCmd{}
cc.baseCmd = newBaseCmd(&cobra.Command{
Use: "doc",
Short: "Generate Markdown documentation for the Hugo CLI.",
Long: `Generate Markdown documentation for the Hugo CLI.
This command is, mostly, used to create up-to-date documentation
of Hugo's command-line interface for https://gohugo.io/.
It creates one Markdown file per command with front matter suitable
for rendering in Hugo.`,
RunE: func(cmd *cobra.Command, args []string) error {
cmd.VisitParents(func(c *cobra.Command) {
// Disable the "Auto generated by spf13/cobra on DATE"
// as it creates a lot of diffs.
c.DisableAutoGenTag = true
})
if !strings.HasSuffix(cc.gendocdir, helpers.FilePathSeparator) {
cc.gendocdir += helpers.FilePathSeparator
}
if found, _ := helpers.Exists(cc.gendocdir, hugofs.Os); !found {
jww.FEEDBACK.Println("Directory", cc.gendocdir, "does not exist, creating...")
if err := hugofs.Os.MkdirAll(cc.gendocdir, 0777); err != nil {
return err
}
}
prepender := func(filename string) string {
name := filepath.Base(filename)
base := strings.TrimSuffix(name, path.Ext(name))
url := "/commands/" + strings.ToLower(base) + "/"
return fmt.Sprintf(gendocFrontmatterTemplate, strings.Replace(base, "_", " ", -1), base, url)
}
linkHandler := func(name string) string {
base := strings.TrimSuffix(name, path.Ext(name))
return "/commands/" + strings.ToLower(base) + "/"
}
jww.FEEDBACK.Println("Generating Hugo command-line documentation in", cc.gendocdir, "...")
doc.GenMarkdownTreeCustom(cmd.Root(), cc.gendocdir, prepender, linkHandler)
jww.FEEDBACK.Println("Done.")
return nil
},
})
cc.cmd.PersistentFlags().StringVar(&cc.gendocdir, "dir", "/tmp/hugodoc/", "the directory to write the doc.")
// For bash-completion
cc.cmd.PersistentFlags().SetAnnotation("dir", cobra.BashCompSubdirsInDir, []string{})
return cc
}

View File

@ -1,71 +0,0 @@
// Copyright 2017-present The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package commands
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"github.com/gohugoio/hugo/docshelper"
"github.com/spf13/cobra"
)
var _ cmder = (*genDocsHelper)(nil)
type genDocsHelper struct {
target string
*baseCmd
}
func createGenDocsHelper() *genDocsHelper {
g := &genDocsHelper{
baseCmd: newBaseCmd(&cobra.Command{
Use: "docshelper",
Short: "Generate some data files for the Hugo docs.",
Hidden: true,
}),
}
g.cmd.RunE = func(cmd *cobra.Command, args []string) error {
return g.generate()
}
g.cmd.PersistentFlags().StringVarP(&g.target, "dir", "", "docs/data", "data dir")
return g
}
func (g *genDocsHelper) generate() error {
fmt.Println("Generate docs data to", g.target)
targetFile := filepath.Join(g.target, "docs.json")
f, err := os.Create(targetFile)
if err != nil {
return err
}
defer f.Close()
enc := json.NewEncoder(f)
enc.SetIndent("", " ")
if err := enc.Encode(docshelper.GetDocProvider()); err != nil {
return err
}
fmt.Println("Done!")
return nil
}

View File

@ -1,77 +0,0 @@
// Copyright 2016 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package commands
import (
"fmt"
"strings"
"github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugofs"
"github.com/spf13/cobra"
"github.com/spf13/cobra/doc"
jww "github.com/spf13/jwalterweatherman"
)
var _ cmder = (*genManCmd)(nil)
type genManCmd struct {
genmandir string
*baseCmd
}
func newGenManCmd() *genManCmd {
cc := &genManCmd{}
cc.baseCmd = newBaseCmd(&cobra.Command{
Use: "man",
Short: "Generate man pages for the Hugo CLI",
Long: `This command automatically generates up-to-date man pages of Hugo's
command-line interface. By default, it creates the man page files
in the "man" directory under the current directory.`,
RunE: func(cmd *cobra.Command, args []string) error {
header := &doc.GenManHeader{
Section: "1",
Manual: "Hugo Manual",
Source: fmt.Sprintf("Hugo %s", hugo.CurrentVersion),
}
if !strings.HasSuffix(cc.genmandir, helpers.FilePathSeparator) {
cc.genmandir += helpers.FilePathSeparator
}
if found, _ := helpers.Exists(cc.genmandir, hugofs.Os); !found {
jww.FEEDBACK.Println("Directory", cc.genmandir, "does not exist, creating...")
if err := hugofs.Os.MkdirAll(cc.genmandir, 0777); err != nil {
return err
}
}
cmd.Root().DisableAutoGenTag = true
jww.FEEDBACK.Println("Generating Hugo man pages in", cc.genmandir, "...")
doc.GenManTree(cmd.Root(), header, cc.genmandir)
jww.FEEDBACK.Println("Done.")
return nil
},
})
cc.cmd.PersistentFlags().StringVar(&cc.genmandir, "dir", "man/", "the directory to write the man pages.")
// For bash-completion
cc.cmd.PersistentFlags().SetAnnotation("dir", cobra.BashCompSubdirsInDir, []string{})
return cc
}

View File

@ -1,4 +1,4 @@
// Copyright 2018 The Hugo Authors. All rights reserved.
// Copyright 2023 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -11,16 +11,22 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// Package commands defines and implements command-line commands and flags
// used by Hugo. Commands and flags are implemented using Cobra.
package commands
import (
"bytes"
"errors"
"fmt"
"regexp"
"log"
"os"
"path/filepath"
"strings"
"github.com/bep/simplecobra"
"github.com/gohugoio/hugo/config"
"github.com/spf13/cobra"
"github.com/gohugoio/hugo/helpers"
"github.com/spf13/afero"
"github.com/spf13/pflag"
)
const (
@ -30,50 +36,101 @@ const (
showCursor = ansiEsc + "[?25h"
)
type flagsToConfigHandler interface {
flagsToConfig(cfg config.Provider)
func newUserError(a ...any) *simplecobra.CommandError {
return &simplecobra.CommandError{Err: errors.New(fmt.Sprint(a...))}
}
type cmder interface {
flagsToConfigHandler
getCommand() *cobra.Command
func setValueFromFlag(flags *pflag.FlagSet, key string, cfg config.Provider, targetKey string, force bool) {
key = strings.TrimSpace(key)
if (force && flags.Lookup(key) != nil) || flags.Changed(key) {
f := flags.Lookup(key)
configKey := key
if targetKey != "" {
configKey = targetKey
}
// Gotta love this API.
switch f.Value.Type() {
case "bool":
bv, _ := flags.GetBool(key)
cfg.Set(configKey, bv)
case "string":
cfg.Set(configKey, f.Value.String())
case "stringSlice":
bv, _ := flags.GetStringSlice(key)
cfg.Set(configKey, bv)
case "int":
iv, _ := flags.GetInt(key)
cfg.Set(configKey, iv)
default:
panic(fmt.Sprintf("update switch with %s", f.Value.Type()))
}
}
}
// commandError is an error used to signal different error situations in command handling.
type commandError struct {
s string
userError bool
func flagsToCfg(cd *simplecobra.Commandeer, cfg config.Provider) config.Provider {
return flagsToCfgWithAdditionalConfigBase(cd, cfg, "")
}
func (c commandError) Error() string {
return c.s
}
func (c commandError) isUserError() bool {
return c.userError
}
func newUserError(a ...any) commandError {
return commandError{s: fmt.Sprintln(a...), userError: true}
}
func newSystemError(a ...any) commandError {
return commandError{s: fmt.Sprintln(a...), userError: false}
}
func newSystemErrorF(format string, a ...any) commandError {
return commandError{s: fmt.Sprintf(format, a...), userError: false}
}
// Catch some of the obvious user errors from Cobra.
// We don't want to show the usage message for every error.
// The below may be to generic. Time will show.
var userErrorRegexp = regexp.MustCompile("unknown flag")
func isUserError(err error) bool {
if cErr, ok := err.(commandError); ok && cErr.isUserError() {
return true
func flagsToCfgWithAdditionalConfigBase(cd *simplecobra.Commandeer, cfg config.Provider, additionalConfigBase string) config.Provider {
if cfg == nil {
cfg = config.New()
}
return userErrorRegexp.MatchString(err.Error())
// Flags with a different name in the config.
keyMap := map[string]string{
"minify": "minifyOutput",
"destination": "publishDir",
"printI18nWarnings": "logI18nWarnings",
"printPathWarnings": "logPathWarnings",
"editor": "newContentEditor",
}
// Flags that we for some reason don't want to expose in the site config.
internalKeySet := map[string]bool{
"quiet": true,
"verbose": true,
"watch": true,
"disableLiveReload": true,
"liveReloadPort": true,
"renderToMemory": true,
"clock": true,
}
cmd := cd.CobraCommand
flags := cmd.Flags()
flags.VisitAll(func(f *pflag.Flag) {
if f.Changed {
targetKey := f.Name
if internalKeySet[targetKey] {
targetKey = "internal." + targetKey
} else if mapped, ok := keyMap[targetKey]; ok {
targetKey = mapped
}
setValueFromFlag(flags, f.Name, cfg, targetKey, false)
if additionalConfigBase != "" {
setValueFromFlag(flags, f.Name, cfg, additionalConfigBase+"."+targetKey, true)
}
}
})
return cfg
}
func mkdir(x ...string) {
p := filepath.Join(x...)
err := os.MkdirAll(p, 0777) // before umask
if err != nil {
log.Fatal(err)
}
}
func touchFile(fs afero.Fs, filename string) {
mkdir(filepath.Dir(filename))
err := helpers.WriteToDisk(filename, bytes.NewReader([]byte{}), fs)
if err != nil {
log.Fatal(err)
}
}

View File

@ -1,206 +0,0 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package commands
import (
"bytes"
"fmt"
"math/rand"
"path/filepath"
"strings"
"testing"
"github.com/bep/clock"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/common/htime"
"github.com/gohugoio/hugo/hugofs"
"github.com/spf13/afero"
"golang.org/x/tools/txtar"
)
// Issue #5662
func TestHugoWithContentDirOverride(t *testing.T) {
t.Parallel()
c := qt.New(t)
files := `
-- config.toml --
baseURL = "https://example.org"
title = "Hugo Commands"
-- mycontent/p1.md --
---
title: "P1"
---
-- layouts/_default/single.html --
Page: {{ .Title }}|
`
s := newTestHugoCmdBuilder(c, files, []string{"-c", "mycontent"}).Build()
s.AssertFileContent("public/p1/index.html", `Page: P1|`)
}
// Issue #9794
func TestHugoStaticFilesMultipleStaticAndManyFolders(t *testing.T) {
t.Parallel()
c := qt.New(t)
files := `
-- config.toml --
baseURL = "https://example.org"
theme = "mytheme"
-- layouts/index.html --
Home.
`
const (
numDirs = 33
numFilesMax = 12
)
r := rand.New(rand.NewSource(32))
for i := 0; i < numDirs; i++ {
for j := 0; j < r.Intn(numFilesMax); j++ {
if j%3 == 0 {
files += fmt.Sprintf("-- themes/mytheme/static/d%d/f%d.txt --\nHellot%d-%d\n", i, j, i, j)
files += fmt.Sprintf("-- themes/mytheme/static/d%d/ft%d.txt --\nHellot%d-%d\n", i, j, i, j)
}
files += fmt.Sprintf("-- static/d%d/f%d.txt --\nHello%d-%d\n", i, j, i, j)
}
}
r = rand.New(rand.NewSource(32))
s := newTestHugoCmdBuilder(c, files, []string{"-c", "mycontent"}).Build()
for i := 0; i < numDirs; i++ {
for j := 0; j < r.Intn(numFilesMax); j++ {
if j%3 == 0 {
if j%3 == 0 {
s.AssertFileContent(fmt.Sprintf("public/d%d/ft%d.txt", i, j), fmt.Sprintf("Hellot%d-%d", i, j))
}
s.AssertFileContent(fmt.Sprintf("public/d%d/f%d.txt", i, j), fmt.Sprintf("Hello%d-%d", i, j))
}
}
}
}
// Issue #8787
func TestHugoListCommandsWithClockFlag(t *testing.T) {
t.Cleanup(func() { htime.Clock = clock.System() })
c := qt.New(t)
files := `
-- config.toml --
baseURL = "https://example.org"
title = "Hugo Commands"
timeZone = "UTC"
-- content/past.md --
---
title: "Past"
date: 2000-11-06
---
-- content/future.md --
---
title: "Future"
date: 2200-11-06
---
-- layouts/_default/single.html --
Page: {{ .Title }}|
`
s := newTestHugoCmdBuilder(c, files, []string{"list", "future"})
s.captureOut = true
s.Build()
p := filepath.Join("content", "future.md")
s.AssertStdout(p + ",2200-11-06T00:00:00Z")
s = newTestHugoCmdBuilder(c, files, []string{"list", "future", "--clock", "2300-11-06"}).Build()
s.AssertStdout("")
}
type testHugoCmdBuilder struct {
*qt.C
fs afero.Fs
dir string
files string
args []string
captureOut bool
out string
}
func newTestHugoCmdBuilder(c *qt.C, files string, args []string) *testHugoCmdBuilder {
s := &testHugoCmdBuilder{C: c, files: files, args: args}
s.dir = s.TempDir()
s.fs = afero.NewBasePathFs(hugofs.Os, s.dir)
return s
}
func (s *testHugoCmdBuilder) Build() *testHugoCmdBuilder {
data := txtar.Parse([]byte(s.files))
for _, f := range data.Files {
filename := filepath.Clean(f.Name)
data := bytes.TrimSuffix(f.Data, []byte("\n"))
s.Assert(s.fs.MkdirAll(filepath.Dir(filename), 0777), qt.IsNil)
s.Assert(afero.WriteFile(s.fs, filename, data, 0666), qt.IsNil)
}
hugoCmd := newCommandsBuilder().addAll().build()
cmd := hugoCmd.getCommand()
args := append(s.args, "-s="+s.dir, "--quiet")
cmd.SetArgs(args)
if s.captureOut {
out, err := captureStdout(func() error {
_, err := cmd.ExecuteC()
return err
})
s.Assert(err, qt.IsNil)
s.out = out
} else {
_, err := cmd.ExecuteC()
s.Assert(err, qt.IsNil)
}
return s
}
func (s *testHugoCmdBuilder) AssertFileContent(filename string, matches ...string) {
s.Helper()
data, err := afero.ReadFile(s.fs, filename)
s.Assert(err, qt.IsNil)
content := strings.TrimSpace(string(data))
for _, m := range matches {
lines := strings.Split(m, "\n")
for _, match := range lines {
match = strings.TrimSpace(match)
if match == "" || strings.HasPrefix(match, "#") {
continue
}
s.Assert(content, qt.Contains, match, qt.Commentf(m))
}
}
}
func (s *testHugoCmdBuilder) AssertStdout(match string) {
s.Helper()
content := strings.TrimSpace(s.out)
s.Assert(content, qt.Contains, strings.TrimSpace(match))
}

View File

@ -1,4 +1,4 @@
// Copyright 2015 The Hugo Authors. All rights reserved.
// Copyright 2023 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
// Copyright 2023 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -15,252 +15,96 @@ package commands
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"regexp"
jww "github.com/spf13/jwalterweatherman"
"strconv"
"strings"
"time"
"unicode"
"github.com/gohugoio/hugo/parser/pageparser"
"github.com/bep/simplecobra"
"github.com/gohugoio/hugo/common/htime"
"github.com/gohugoio/hugo/common/hugio"
"github.com/gohugoio/hugo/parser/metadecoders"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/parser"
"github.com/gohugoio/hugo/parser/metadecoders"
"github.com/gohugoio/hugo/parser/pageparser"
"github.com/spf13/afero"
"github.com/spf13/cobra"
jww "github.com/spf13/jwalterweatherman"
)
var _ cmder = (*importCmd)(nil)
type importCmd struct {
*baseCmd
}
func newImportCmd() *importCmd {
cc := &importCmd{}
cc.baseCmd = newBaseCmd(&cobra.Command{
Use: "import",
Short: "Import your site from others.",
Long: `Import your site from other web site generators like Jekyll.
Import requires a subcommand, e.g. ` + "`hugo import jekyll jekyll_root_path target_path`.",
RunE: nil,
})
importJekyllCmd := &cobra.Command{
Use: "jekyll",
Short: "hugo import from Jekyll",
Long: `hugo import from Jekyll.
func newImportCommand() *importCommand {
var c *importCommand
c = &importCommand{
commands: []simplecobra.Commander{
&simpleCommand{
name: "jekyll",
short: "hugo import from Jekyll",
long: `hugo import from Jekyll.
Import from Jekyll requires two paths, e.g. ` + "`hugo import jekyll jekyll_root_path target_path`.",
RunE: cc.importFromJekyll,
}
importJekyllCmd.Flags().Bool("force", false, "allow import into non-empty target directory")
cc.cmd.AddCommand(importJekyllCmd)
return cc
}
func (i *importCmd) importFromJekyll(cmd *cobra.Command, args []string) error {
if len(args) < 2 {
return newUserError(`import from jekyll requires two paths, e.g. ` + "`hugo import jekyll jekyll_root_path target_path`.")
}
jekyllRoot, err := filepath.Abs(filepath.Clean(args[0]))
if err != nil {
return newUserError("path error:", args[0])
}
targetDir, err := filepath.Abs(filepath.Clean(args[1]))
if err != nil {
return newUserError("path error:", args[1])
}
jww.INFO.Println("Import Jekyll from:", jekyllRoot, "to:", targetDir)
if strings.HasPrefix(filepath.Dir(targetDir), jekyllRoot) {
return newUserError("abort: target path should not be inside the Jekyll root")
}
forceImport, _ := cmd.Flags().GetBool("force")
fs := afero.NewOsFs()
jekyllPostDirs, hasAnyPost := i.getJekyllDirInfo(fs, jekyllRoot)
if !hasAnyPost {
return errors.New("abort: jekyll root contains neither posts nor drafts")
}
err = i.createSiteFromJekyll(jekyllRoot, targetDir, jekyllPostDirs, forceImport)
if err != nil {
return newUserError(err)
}
jww.FEEDBACK.Println("Importing...")
fileCount := 0
callback := func(path string, fi hugofs.FileMetaInfo, err error) error {
if err != nil {
return err
}
if fi.IsDir() {
return nil
}
relPath, err := filepath.Rel(jekyllRoot, path)
if err != nil {
return newUserError("get rel path error:", path)
}
relPath = filepath.ToSlash(relPath)
draft := false
switch {
case strings.Contains(relPath, "_posts/"):
relPath = filepath.Join("content/post", strings.Replace(relPath, "_posts/", "", -1))
case strings.Contains(relPath, "_drafts/"):
relPath = filepath.Join("content/draft", strings.Replace(relPath, "_drafts/", "", -1))
draft = true
default:
return nil
}
fileCount++
return convertJekyllPost(path, relPath, targetDir, draft)
}
for jekyllPostDir, hasAnyPostInDir := range jekyllPostDirs {
if hasAnyPostInDir {
if err = helpers.SymbolicWalk(hugofs.Os, filepath.Join(jekyllRoot, jekyllPostDir), callback); err != nil {
return err
}
}
}
jww.FEEDBACK.Println("Congratulations!", fileCount, "post(s) imported!")
jww.FEEDBACK.Println("Now, start Hugo by yourself:\n" +
"$ git clone https://github.com/spf13/herring-cove.git " + args[1] + "/themes/herring-cove")
jww.FEEDBACK.Println("$ cd " + args[1] + "\n$ hugo server --theme=herring-cove")
return nil
}
func (i *importCmd) getJekyllDirInfo(fs afero.Fs, jekyllRoot string) (map[string]bool, bool) {
postDirs := make(map[string]bool)
hasAnyPost := false
if entries, err := os.ReadDir(jekyllRoot); err == nil {
for _, entry := range entries {
if entry.IsDir() {
subDir := filepath.Join(jekyllRoot, entry.Name())
if isPostDir, hasAnyPostInDir := i.retrieveJekyllPostDir(fs, subDir); isPostDir {
postDirs[entry.Name()] = hasAnyPostInDir
if hasAnyPostInDir {
hasAnyPost = true
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
if len(args) < 2 {
return newUserError(`import from jekyll requires two paths, e.g. ` + "`hugo import jekyll jekyll_root_path target_path`.")
}
}
}
}
}
return postDirs, hasAnyPost
}
func (i *importCmd) retrieveJekyllPostDir(fs afero.Fs, dir string) (bool, bool) {
if strings.HasSuffix(dir, "_posts") || strings.HasSuffix(dir, "_drafts") {
isEmpty, _ := helpers.IsEmpty(dir, fs)
return true, !isEmpty
}
if entries, err := os.ReadDir(dir); err == nil {
for _, entry := range entries {
if entry.IsDir() {
subDir := filepath.Join(dir, entry.Name())
if isPostDir, hasAnyPost := i.retrieveJekyllPostDir(fs, subDir); isPostDir {
return isPostDir, hasAnyPost
}
}
}
}
return false, true
}
func (i *importCmd) createSiteFromJekyll(jekyllRoot, targetDir string, jekyllPostDirs map[string]bool, force bool) error {
fs := &afero.OsFs{}
if exists, _ := helpers.Exists(targetDir, fs); exists {
if isDir, _ := helpers.IsDir(targetDir, fs); !isDir {
return errors.New("target path \"" + targetDir + "\" exists but is not a directory")
}
isEmpty, _ := helpers.IsEmpty(targetDir, fs)
if !isEmpty && !force {
return errors.New("target path \"" + targetDir + "\" exists and is not empty")
}
}
jekyllConfig := i.loadJekyllConfig(fs, jekyllRoot)
mkdir(targetDir, "layouts")
mkdir(targetDir, "content")
mkdir(targetDir, "archetypes")
mkdir(targetDir, "static")
mkdir(targetDir, "data")
mkdir(targetDir, "themes")
i.createConfigFromJekyll(fs, targetDir, "yaml", jekyllConfig)
i.copyJekyllFilesAndFolders(jekyllRoot, filepath.Join(targetDir, "static"), jekyllPostDirs)
return nil
}
func (i *importCmd) loadJekyllConfig(fs afero.Fs, jekyllRoot string) map[string]any {
path := filepath.Join(jekyllRoot, "_config.yml")
exists, err := helpers.Exists(path, fs)
if err != nil || !exists {
jww.WARN.Println("_config.yaml not found: Is the specified Jekyll root correct?")
return nil
}
f, err := fs.Open(path)
if err != nil {
return nil
}
defer f.Close()
b, err := io.ReadAll(f)
if err != nil {
return nil
}
c, err := metadecoders.Default.UnmarshalToMap(b, metadecoders.YAML)
if err != nil {
return nil
return c.importFromJekyll(args)
},
withc: func(cmd *cobra.Command) {
cmd.Flags().BoolVar(&c.force, "force", false, "allow import into non-empty target directory")
},
},
},
}
return c
}
func (i *importCmd) createConfigFromJekyll(fs afero.Fs, inpath string, kind metadecoders.Format, jekyllConfig map[string]any) (err error) {
type importCommand struct {
r *rootCommand
force bool
commands []simplecobra.Commander
}
func (c *importCommand) Commands() []simplecobra.Commander {
return c.commands
}
func (c *importCommand) Name() string {
return "import"
}
func (c *importCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error {
return nil
}
func (c *importCommand) WithCobraCommand(cmd *cobra.Command) error {
cmd.Short = "Import your site from others."
cmd.Long = `Import your site from other web site generators like Jekyll.
Import requires a subcommand, e.g. ` + "`hugo import jekyll jekyll_root_path target_path`."
return nil
}
func (c *importCommand) Init(cd, runner *simplecobra.Commandeer) error {
c.r = cd.Root.Command.(*rootCommand)
return nil
}
func (i *importCommand) createConfigFromJekyll(fs afero.Fs, inpath string, kind metadecoders.Format, jekyllConfig map[string]any) (err error) {
title := "My New Hugo Site"
baseURL := "http://example.org/"
@ -293,10 +137,209 @@ func (i *importCmd) createConfigFromJekyll(fs afero.Fs, inpath string, kind meta
return err
}
return helpers.WriteToDisk(filepath.Join(inpath, "config."+string(kind)), &buf, fs)
return helpers.WriteToDisk(filepath.Join(inpath, "hugo."+string(kind)), &buf, fs)
}
func (i *importCmd) copyJekyllFilesAndFolders(jekyllRoot, dest string, jekyllPostDirs map[string]bool) (err error) {
func (c *importCommand) getJekyllDirInfo(fs afero.Fs, jekyllRoot string) (map[string]bool, bool) {
postDirs := make(map[string]bool)
hasAnyPost := false
if entries, err := os.ReadDir(jekyllRoot); err == nil {
for _, entry := range entries {
if entry.IsDir() {
subDir := filepath.Join(jekyllRoot, entry.Name())
if isPostDir, hasAnyPostInDir := c.retrieveJekyllPostDir(fs, subDir); isPostDir {
postDirs[entry.Name()] = hasAnyPostInDir
if hasAnyPostInDir {
hasAnyPost = true
}
}
}
}
}
return postDirs, hasAnyPost
}
func (c *importCommand) createSiteFromJekyll(jekyllRoot, targetDir string, jekyllPostDirs map[string]bool) error {
fs := &afero.OsFs{}
if exists, _ := helpers.Exists(targetDir, fs); exists {
if isDir, _ := helpers.IsDir(targetDir, fs); !isDir {
return errors.New("target path \"" + targetDir + "\" exists but is not a directory")
}
isEmpty, _ := helpers.IsEmpty(targetDir, fs)
if !isEmpty && !c.force {
return errors.New("target path \"" + targetDir + "\" exists and is not empty")
}
}
jekyllConfig := c.loadJekyllConfig(fs, jekyllRoot)
mkdir(targetDir, "layouts")
mkdir(targetDir, "content")
mkdir(targetDir, "archetypes")
mkdir(targetDir, "static")
mkdir(targetDir, "data")
mkdir(targetDir, "themes")
c.createConfigFromJekyll(fs, targetDir, "yaml", jekyllConfig)
c.copyJekyllFilesAndFolders(jekyllRoot, filepath.Join(targetDir, "static"), jekyllPostDirs)
return nil
}
func (c *importCommand) convertJekyllContent(m any, content string) (string, error) {
metadata, _ := maps.ToStringMapE(m)
lines := strings.Split(content, "\n")
var resultLines []string
for _, line := range lines {
resultLines = append(resultLines, strings.Trim(line, "\r\n"))
}
content = strings.Join(resultLines, "\n")
excerptSep := "<!--more-->"
if value, ok := metadata["excerpt_separator"]; ok {
if str, strOk := value.(string); strOk {
content = strings.Replace(content, strings.TrimSpace(str), excerptSep, -1)
}
}
replaceList := []struct {
re *regexp.Regexp
replace string
}{
{regexp.MustCompile("(?i)<!-- more -->"), "<!--more-->"},
{regexp.MustCompile(`\{%\s*raw\s*%\}\s*(.*?)\s*\{%\s*endraw\s*%\}`), "$1"},
{regexp.MustCompile(`{%\s*endhighlight\s*%}`), "{{< / highlight >}}"},
}
for _, replace := range replaceList {
content = replace.re.ReplaceAllString(content, replace.replace)
}
replaceListFunc := []struct {
re *regexp.Regexp
replace func(string) string
}{
// Octopress image tag: http://octopress.org/docs/plugins/image-tag/
{regexp.MustCompile(`{%\s+img\s*(.*?)\s*%}`), c.replaceImageTag},
{regexp.MustCompile(`{%\s*highlight\s*(.*?)\s*%}`), c.replaceHighlightTag},
}
for _, replace := range replaceListFunc {
content = replace.re.ReplaceAllStringFunc(content, replace.replace)
}
var buf bytes.Buffer
if len(metadata) != 0 {
err := parser.InterfaceToFrontMatter(m, metadecoders.YAML, &buf)
if err != nil {
return "", err
}
}
buf.WriteString(content)
return buf.String(), nil
}
func (c *importCommand) convertJekyllMetaData(m any, postName string, postDate time.Time, draft bool) (any, error) {
metadata, err := maps.ToStringMapE(m)
if err != nil {
return nil, err
}
if draft {
metadata["draft"] = true
}
for key, value := range metadata {
lowerKey := strings.ToLower(key)
switch lowerKey {
case "layout":
delete(metadata, key)
case "permalink":
if str, ok := value.(string); ok {
metadata["url"] = str
}
delete(metadata, key)
case "category":
if str, ok := value.(string); ok {
metadata["categories"] = []string{str}
}
delete(metadata, key)
case "excerpt_separator":
if key != lowerKey {
delete(metadata, key)
metadata[lowerKey] = value
}
case "date":
if str, ok := value.(string); ok {
re := regexp.MustCompile(`(\d+):(\d+):(\d+)`)
r := re.FindAllStringSubmatch(str, -1)
if len(r) > 0 {
hour, _ := strconv.Atoi(r[0][1])
minute, _ := strconv.Atoi(r[0][2])
second, _ := strconv.Atoi(r[0][3])
postDate = time.Date(postDate.Year(), postDate.Month(), postDate.Day(), hour, minute, second, 0, time.UTC)
}
}
delete(metadata, key)
}
}
metadata["date"] = postDate.Format(time.RFC3339)
return metadata, nil
}
func (c *importCommand) convertJekyllPost(path, relPath, targetDir string, draft bool) error {
jww.TRACE.Println("Converting", path)
filename := filepath.Base(path)
postDate, postName, err := c.parseJekyllFilename(filename)
if err != nil {
c.r.Printf("Failed to parse filename '%s': %s. Skipping.", filename, err)
return nil
}
jww.TRACE.Println(filename, postDate, postName)
targetFile := filepath.Join(targetDir, relPath)
targetParentDir := filepath.Dir(targetFile)
os.MkdirAll(targetParentDir, 0777)
contentBytes, err := os.ReadFile(path)
if err != nil {
c.r.logger.Errorln("Read file error:", path)
return err
}
pf, err := pageparser.ParseFrontMatterAndContent(bytes.NewReader(contentBytes))
if err != nil {
return fmt.Errorf("failed to parse file %q: %s", filename, err)
}
newmetadata, err := c.convertJekyllMetaData(pf.FrontMatter, postName, postDate, draft)
if err != nil {
return fmt.Errorf("failed to convert metadata for file %q: %s", filename, err)
}
content, err := c.convertJekyllContent(newmetadata, string(pf.Content))
if err != nil {
return fmt.Errorf("failed to convert content for file %q: %s", filename, err)
}
fs := hugofs.Os
if err := helpers.WriteToDisk(targetFile, strings.NewReader(content), fs); err != nil {
return fmt.Errorf("failed to save file %q: %s", filename, err)
}
return nil
}
func (c *importCommand) copyJekyllFilesAndFolders(jekyllRoot, dest string, jekyllPostDirs map[string]bool) (err error) {
fs := hugofs.Os
fi, err := fs.Stat(jekyllRoot)
@ -353,7 +396,116 @@ func (i *importCmd) copyJekyllFilesAndFolders(jekyllRoot, dest string, jekyllPos
return nil
}
func parseJekyllFilename(filename string) (time.Time, string, error) {
func (c *importCommand) importFromJekyll(args []string) error {
jekyllRoot, err := filepath.Abs(filepath.Clean(args[0]))
if err != nil {
return newUserError("path error:", args[0])
}
targetDir, err := filepath.Abs(filepath.Clean(args[1]))
if err != nil {
return newUserError("path error:", args[1])
}
c.r.Println("Import Jekyll from:", jekyllRoot, "to:", targetDir)
if strings.HasPrefix(filepath.Dir(targetDir), jekyllRoot) {
return newUserError("abort: target path should not be inside the Jekyll root")
}
fs := afero.NewOsFs()
jekyllPostDirs, hasAnyPost := c.getJekyllDirInfo(fs, jekyllRoot)
if !hasAnyPost {
return errors.New("abort: jekyll root contains neither posts nor drafts")
}
err = c.createSiteFromJekyll(jekyllRoot, targetDir, jekyllPostDirs)
if err != nil {
return newUserError(err)
}
c.r.Println("Importing...")
fileCount := 0
callback := func(path string, fi hugofs.FileMetaInfo, err error) error {
if err != nil {
return err
}
if fi.IsDir() {
return nil
}
relPath, err := filepath.Rel(jekyllRoot, path)
if err != nil {
return newUserError("get rel path error:", path)
}
relPath = filepath.ToSlash(relPath)
draft := false
switch {
case strings.Contains(relPath, "_posts/"):
relPath = filepath.Join("content/post", strings.Replace(relPath, "_posts/", "", -1))
case strings.Contains(relPath, "_drafts/"):
relPath = filepath.Join("content/draft", strings.Replace(relPath, "_drafts/", "", -1))
draft = true
default:
return nil
}
fileCount++
return c.convertJekyllPost(path, relPath, targetDir, draft)
}
for jekyllPostDir, hasAnyPostInDir := range jekyllPostDirs {
if hasAnyPostInDir {
if err = helpers.SymbolicWalk(hugofs.Os, filepath.Join(jekyllRoot, jekyllPostDir), callback); err != nil {
return err
}
}
}
c.r.Println("Congratulations!", fileCount, "post(s) imported!")
c.r.Println("Now, start Hugo by yourself:\n" +
"$ git clone https://github.com/spf13/herring-cove.git " + args[1] + "/themes/herring-cove")
c.r.Println("$ cd " + args[1] + "\n$ hugo server --theme=herring-cove")
return nil
}
func (c *importCommand) loadJekyllConfig(fs afero.Fs, jekyllRoot string) map[string]any {
path := filepath.Join(jekyllRoot, "_config.yml")
exists, err := helpers.Exists(path, fs)
if err != nil || !exists {
c.r.Println("_config.yaml not found: Is the specified Jekyll root correct?")
return nil
}
f, err := fs.Open(path)
if err != nil {
return nil
}
defer f.Close()
b, err := io.ReadAll(f)
if err != nil {
return nil
}
m, err := metadecoders.Default.UnmarshalToMap(b, metadecoders.YAML)
if err != nil {
return nil
}
return m
}
func (c *importCommand) parseJekyllFilename(filename string) (time.Time, string, error) {
re := regexp.MustCompile(`(\d+-\d+-\d+)-(.+)\..*`)
r := re.FindAllStringSubmatch(filename, -1)
if len(r) == 0 {
@ -370,163 +522,7 @@ func parseJekyllFilename(filename string) (time.Time, string, error) {
return postDate, postName, nil
}
func convertJekyllPost(path, relPath, targetDir string, draft bool) error {
jww.TRACE.Println("Converting", path)
filename := filepath.Base(path)
postDate, postName, err := parseJekyllFilename(filename)
if err != nil {
jww.WARN.Printf("Failed to parse filename '%s': %s. Skipping.", filename, err)
return nil
}
jww.TRACE.Println(filename, postDate, postName)
targetFile := filepath.Join(targetDir, relPath)
targetParentDir := filepath.Dir(targetFile)
os.MkdirAll(targetParentDir, 0777)
contentBytes, err := os.ReadFile(path)
if err != nil {
jww.ERROR.Println("Read file error:", path)
return err
}
pf, err := pageparser.ParseFrontMatterAndContent(bytes.NewReader(contentBytes))
if err != nil {
jww.ERROR.Println("Parse file error:", path)
return err
}
newmetadata, err := convertJekyllMetaData(pf.FrontMatter, postName, postDate, draft)
if err != nil {
jww.ERROR.Println("Convert metadata error:", path)
return err
}
content, err := convertJekyllContent(newmetadata, string(pf.Content))
if err != nil {
jww.ERROR.Println("Converting Jekyll error:", path)
return err
}
fs := hugofs.Os
if err := helpers.WriteToDisk(targetFile, strings.NewReader(content), fs); err != nil {
return fmt.Errorf("failed to save file %q: %s", filename, err)
}
return nil
}
func convertJekyllMetaData(m any, postName string, postDate time.Time, draft bool) (any, error) {
metadata, err := maps.ToStringMapE(m)
if err != nil {
return nil, err
}
if draft {
metadata["draft"] = true
}
for key, value := range metadata {
lowerKey := strings.ToLower(key)
switch lowerKey {
case "layout":
delete(metadata, key)
case "permalink":
if str, ok := value.(string); ok {
metadata["url"] = str
}
delete(metadata, key)
case "category":
if str, ok := value.(string); ok {
metadata["categories"] = []string{str}
}
delete(metadata, key)
case "excerpt_separator":
if key != lowerKey {
delete(metadata, key)
metadata[lowerKey] = value
}
case "date":
if str, ok := value.(string); ok {
re := regexp.MustCompile(`(\d+):(\d+):(\d+)`)
r := re.FindAllStringSubmatch(str, -1)
if len(r) > 0 {
hour, _ := strconv.Atoi(r[0][1])
minute, _ := strconv.Atoi(r[0][2])
second, _ := strconv.Atoi(r[0][3])
postDate = time.Date(postDate.Year(), postDate.Month(), postDate.Day(), hour, minute, second, 0, time.UTC)
}
}
delete(metadata, key)
}
}
metadata["date"] = postDate.Format(time.RFC3339)
return metadata, nil
}
func convertJekyllContent(m any, content string) (string, error) {
metadata, _ := maps.ToStringMapE(m)
lines := strings.Split(content, "\n")
var resultLines []string
for _, line := range lines {
resultLines = append(resultLines, strings.Trim(line, "\r\n"))
}
content = strings.Join(resultLines, "\n")
excerptSep := "<!--more-->"
if value, ok := metadata["excerpt_separator"]; ok {
if str, strOk := value.(string); strOk {
content = strings.Replace(content, strings.TrimSpace(str), excerptSep, -1)
}
}
replaceList := []struct {
re *regexp.Regexp
replace string
}{
{regexp.MustCompile("(?i)<!-- more -->"), "<!--more-->"},
{regexp.MustCompile(`\{%\s*raw\s*%\}\s*(.*?)\s*\{%\s*endraw\s*%\}`), "$1"},
{regexp.MustCompile(`{%\s*endhighlight\s*%}`), "{{< / highlight >}}"},
}
for _, replace := range replaceList {
content = replace.re.ReplaceAllString(content, replace.replace)
}
replaceListFunc := []struct {
re *regexp.Regexp
replace func(string) string
}{
// Octopress image tag: http://octopress.org/docs/plugins/image-tag/
{regexp.MustCompile(`{%\s+img\s*(.*?)\s*%}`), replaceImageTag},
{regexp.MustCompile(`{%\s*highlight\s*(.*?)\s*%}`), replaceHighlightTag},
}
for _, replace := range replaceListFunc {
content = replace.re.ReplaceAllStringFunc(content, replace.replace)
}
var buf bytes.Buffer
if len(metadata) != 0 {
err := parser.InterfaceToFrontMatter(m, metadecoders.YAML, &buf)
if err != nil {
return "", err
}
}
buf.WriteString(content)
return buf.String(), nil
}
func replaceHighlightTag(match string) string {
func (c *importCommand) replaceHighlightTag(match string) string {
r := regexp.MustCompile(`{%\s*highlight\s*(.*?)\s*%}`)
parts := r.FindStringSubmatch(match)
lastQuote := rune(0)
@ -570,35 +566,55 @@ func replaceHighlightTag(match string) string {
return result.String()
}
func replaceImageTag(match string) string {
func (c *importCommand) replaceImageTag(match string) string {
r := regexp.MustCompile(`{%\s+img\s*(\p{L}*)\s+([\S]*/[\S]+)\s+(\d*)\s*(\d*)\s*(.*?)\s*%}`)
result := bytes.NewBufferString("{{< figure ")
parts := r.FindStringSubmatch(match)
// Index 0 is the entire string, ignore
replaceOptionalPart(result, "class", parts[1])
replaceOptionalPart(result, "src", parts[2])
replaceOptionalPart(result, "width", parts[3])
replaceOptionalPart(result, "height", parts[4])
c.replaceOptionalPart(result, "class", parts[1])
c.replaceOptionalPart(result, "src", parts[2])
c.replaceOptionalPart(result, "width", parts[3])
c.replaceOptionalPart(result, "height", parts[4])
// title + alt
part := parts[5]
if len(part) > 0 {
splits := strings.Split(part, "'")
lenSplits := len(splits)
if lenSplits == 1 {
replaceOptionalPart(result, "title", splits[0])
c.replaceOptionalPart(result, "title", splits[0])
} else if lenSplits == 3 {
replaceOptionalPart(result, "title", splits[1])
c.replaceOptionalPart(result, "title", splits[1])
} else if lenSplits == 5 {
replaceOptionalPart(result, "title", splits[1])
replaceOptionalPart(result, "alt", splits[3])
c.replaceOptionalPart(result, "title", splits[1])
c.replaceOptionalPart(result, "alt", splits[3])
}
}
result.WriteString(">}}")
return result.String()
}
func replaceOptionalPart(buffer *bytes.Buffer, partName string, part string) {
func (c *importCommand) replaceOptionalPart(buffer *bytes.Buffer, partName string, part string) {
if len(part) > 0 {
buffer.WriteString(partName + "=\"" + part + "\" ")
}
}
func (c *importCommand) retrieveJekyllPostDir(fs afero.Fs, dir string) (bool, bool) {
if strings.HasSuffix(dir, "_posts") || strings.HasSuffix(dir, "_drafts") {
isEmpty, _ := helpers.IsEmpty(dir, fs)
return true, !isEmpty
}
if entries, err := os.ReadDir(dir); err == nil {
for _, entry := range entries {
if entry.IsDir() {
subDir := filepath.Join(dir, entry.Name())
if isPostDir, hasAnyPost := c.retrieveJekyllPostDir(fs, subDir); isPostDir {
return isPostDir, hasAnyPost
}
}
}
}
return false, true
}

View File

@ -1,177 +0,0 @@
// Copyright 2015 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package commands
import (
"encoding/json"
"testing"
"time"
qt "github.com/frankban/quicktest"
)
func TestParseJekyllFilename(t *testing.T) {
c := qt.New(t)
filenameArray := []string{
"2015-01-02-test.md",
"2012-03-15-中文.markup",
}
expectResult := []struct {
postDate time.Time
postName string
}{
{time.Date(2015, time.January, 2, 0, 0, 0, 0, time.UTC), "test"},
{time.Date(2012, time.March, 15, 0, 0, 0, 0, time.UTC), "中文"},
}
for i, filename := range filenameArray {
postDate, postName, err := parseJekyllFilename(filename)
c.Assert(err, qt.IsNil)
c.Assert(expectResult[i].postDate.Format("2006-01-02"), qt.Equals, postDate.Format("2006-01-02"))
c.Assert(expectResult[i].postName, qt.Equals, postName)
}
}
func TestConvertJekyllMetadata(t *testing.T) {
c := qt.New(t)
testDataList := []struct {
metadata any
postName string
postDate time.Time
draft bool
expect string
}{
{
map[any]any{},
"testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), false,
`{"date":"2015-10-01T00:00:00Z"}`,
},
{
map[any]any{},
"testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), true,
`{"date":"2015-10-01T00:00:00Z","draft":true}`,
},
{
map[any]any{"Permalink": "/permalink.html", "layout": "post"},
"testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), false,
`{"date":"2015-10-01T00:00:00Z","url":"/permalink.html"}`,
},
{
map[any]any{"permalink": "/permalink.html"},
"testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), false,
`{"date":"2015-10-01T00:00:00Z","url":"/permalink.html"}`,
},
{
map[any]any{"category": nil, "permalink": 123},
"testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), false,
`{"date":"2015-10-01T00:00:00Z"}`,
},
{
map[any]any{"Excerpt_Separator": "sep"},
"testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), false,
`{"date":"2015-10-01T00:00:00Z","excerpt_separator":"sep"}`,
},
{
map[any]any{"category": "book", "layout": "post", "Others": "Goods", "Date": "2015-10-01 12:13:11"},
"testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), false,
`{"Others":"Goods","categories":["book"],"date":"2015-10-01T12:13:11Z"}`,
},
}
for _, data := range testDataList {
result, err := convertJekyllMetaData(data.metadata, data.postName, data.postDate, data.draft)
c.Assert(err, qt.IsNil)
jsonResult, err := json.Marshal(result)
c.Assert(err, qt.IsNil)
c.Assert(string(jsonResult), qt.Equals, data.expect)
}
}
func TestConvertJekyllContent(t *testing.T) {
c := qt.New(t)
testDataList := []struct {
metadata any
content string
expect string
}{
{
map[any]any{},
"Test content\r\n<!-- more -->\npart2 content", "Test content\n<!--more-->\npart2 content",
},
{
map[any]any{},
"Test content\n<!-- More -->\npart2 content", "Test content\n<!--more-->\npart2 content",
},
{
map[any]any{"excerpt_separator": "<!--sep-->"},
"Test content\n<!--sep-->\npart2 content",
"---\nexcerpt_separator: <!--sep-->\n---\nTest content\n<!--more-->\npart2 content",
},
{map[any]any{}, "{% raw %}text{% endraw %}", "text"},
{map[any]any{}, "{%raw%} text2 {%endraw %}", "text2"},
{
map[any]any{},
"{% highlight go %}\nvar s int\n{% endhighlight %}",
"{{< highlight go >}}\nvar s int\n{{< / highlight >}}",
},
{
map[any]any{},
"{% highlight go linenos hl_lines=\"1 2\" %}\nvar s string\nvar i int\n{% endhighlight %}",
"{{< highlight go \"linenos=table,hl_lines=1 2\" >}}\nvar s string\nvar i int\n{{< / highlight >}}",
},
// Octopress image tag
{
map[any]any{},
"{% img http://placekitten.com/890/280 %}",
"{{< figure src=\"http://placekitten.com/890/280\" >}}",
},
{
map[any]any{},
"{% img left http://placekitten.com/320/250 Place Kitten #2 %}",
"{{< figure class=\"left\" src=\"http://placekitten.com/320/250\" title=\"Place Kitten #2\" >}}",
},
{
map[any]any{},
"{% img right http://placekitten.com/300/500 150 250 'Place Kitten #3' %}",
"{{< figure class=\"right\" src=\"http://placekitten.com/300/500\" width=\"150\" height=\"250\" title=\"Place Kitten #3\" >}}",
},
{
map[any]any{},
"{% img right http://placekitten.com/300/500 150 250 'Place Kitten #4' 'An image of a very cute kitten' %}",
"{{< figure class=\"right\" src=\"http://placekitten.com/300/500\" width=\"150\" height=\"250\" title=\"Place Kitten #4\" alt=\"An image of a very cute kitten\" >}}",
},
{
map[any]any{},
"{% img http://placekitten.com/300/500 150 250 'Place Kitten #4' 'An image of a very cute kitten' %}",
"{{< figure src=\"http://placekitten.com/300/500\" width=\"150\" height=\"250\" title=\"Place Kitten #4\" alt=\"An image of a very cute kitten\" >}}",
},
{
map[any]any{},
"{% img right /placekitten/300/500 'Place Kitten #4' 'An image of a very cute kitten' %}",
"{{< figure class=\"right\" src=\"/placekitten/300/500\" title=\"Place Kitten #4\" alt=\"An image of a very cute kitten\" >}}",
},
{
map[any]any{"category": "book", "layout": "post", "Date": "2015-10-01 12:13:11"},
"somecontent",
"---\nDate: \"2015-10-01 12:13:11\"\ncategory: book\nlayout: post\n---\nsomecontent",
},
}
for _, data := range testDataList {
result, err := convertJekyllContent(data.metadata, data.content)
c.Assert(result, qt.Equals, data.expect)
c.Assert(err, qt.IsNil)
}
}

View File

@ -1,84 +0,0 @@
// Copyright 2018 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package commands
import (
"syscall"
"github.com/spf13/cobra"
jww "github.com/spf13/jwalterweatherman"
)
var _ cmder = (*limitCmd)(nil)
type limitCmd struct {
*baseCmd
}
func newLimitCmd() *limitCmd {
ccmd := &cobra.Command{
Use: "ulimit",
Short: "Check system ulimit settings",
Long: `Hugo will inspect the current ulimit settings on the system.
This is primarily to ensure that Hugo can watch enough files on some OSs`,
RunE: func(cmd *cobra.Command, args []string) error {
var rLimit syscall.Rlimit
err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit)
if err != nil {
return newSystemError("Error Getting rlimit ", err)
}
jww.FEEDBACK.Println("Current rLimit:", rLimit)
if rLimit.Cur >= newRlimit {
return nil
}
jww.FEEDBACK.Println("Attempting to increase limit")
rLimit.Cur = newRlimit
err = syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit)
if err != nil {
return newSystemError("Error Setting rLimit ", err)
}
err = syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit)
if err != nil {
return newSystemError("Error Getting rLimit ", err)
}
jww.FEEDBACK.Println("rLimit after change:", rLimit)
return nil
},
}
return &limitCmd{baseCmd: newBaseCmd(ccmd)}
}
const newRlimit = 10240
func tweakLimit() {
var rLimit syscall.Rlimit
err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit)
if err != nil {
jww.WARN.Println("Unable to get rlimit:", err)
return
}
if rLimit.Cur < newRlimit {
rLimit.Cur = newRlimit
err = syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit)
if err != nil {
// This may not succeed, see https://github.com/golang/go/issues/30401
jww.INFO.Println("Unable to increase number of open files limit:", err)
}
}
}

View File

@ -1,21 +0,0 @@
// Copyright 2018 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build !darwin
// +build !darwin
package commands
func tweakLimit() {
// nothing to do
}

View File

@ -1,4 +1,4 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
// Copyright 2023 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -14,197 +14,154 @@
package commands
import (
"context"
"encoding/csv"
"os"
"strconv"
"strings"
"time"
"github.com/bep/simplecobra"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/hugolib"
"github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/resources/resource"
"github.com/spf13/cobra"
jww "github.com/spf13/jwalterweatherman"
)
var _ cmder = (*listCmd)(nil)
// newListCommand creates a new list command and its subcommands.
func newListCommand() *listCommand {
type listCmd struct {
*baseBuilderCmd
}
func (lc *listCmd) buildSites(config map[string]any) (*hugolib.HugoSites, error) {
cfgInit := func(c *commandeer) error {
for key, value := range config {
c.Set(key, value)
list := func(cd *simplecobra.Commandeer, r *rootCommand, createRecord func(page.Page) []string, opts ...any) error {
bcfg := hugolib.BuildCfg{SkipRender: true}
cfg := config.New()
for i := 0; i < len(opts); i += 2 {
cfg.Set(opts[i].(string), opts[i+1])
}
h, err := r.Build(cd, bcfg, cfg)
if err != nil {
return err
}
writer := csv.NewWriter(r.Out)
defer writer.Flush()
for _, p := range h.Pages() {
if record := createRecord(p); record != nil {
if err := writer.Write(record); err != nil {
return err
}
if err != nil {
return err
}
}
}
return nil
}
c, err := initializeConfig(true, true, false, &lc.hugoBuilderCommon, lc, cfgInit)
if err != nil {
return nil, err
}
return &listCommand{
commands: []simplecobra.Commander{
&simpleCommand{
name: "drafts",
short: "List all drafts",
long: `List all of the drafts in your content directory.`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
createRecord := func(p page.Page) []string {
if !p.Draft() || p.File().IsZero() {
return nil
}
return []string{
p.File().Path(),
p.PublishDate().Format(time.RFC3339)}
sites, err := hugolib.NewHugoSites(*c.DepsCfg)
if err != nil {
return nil, newSystemError("Error creating sites", err)
}
if err := sites.Build(hugolib.BuildCfg{SkipRender: true}); err != nil {
return nil, newSystemError("Error Processing Source Content", err)
}
return sites, nil
}
func (b *commandsBuilder) newListCmd() *listCmd {
cc := &listCmd{}
cmd := &cobra.Command{
Use: "list",
Short: "Listing out various types of content",
Long: `Listing out various types of content.
List requires a subcommand, e.g. ` + "`hugo list drafts`.",
RunE: nil,
}
cmd.AddCommand(
&cobra.Command{
Use: "drafts",
Short: "List all drafts",
Long: `List all of the drafts in your content directory.`,
RunE: func(cmd *cobra.Command, args []string) error {
sites, err := cc.buildSites(map[string]any{"buildDrafts": true})
if err != nil {
return newSystemError("Error building sites", err)
}
for _, p := range sites.Pages() {
if p.Draft() {
jww.FEEDBACK.Println(strings.TrimPrefix(p.File().Filename(), sites.WorkingDir+string(os.PathSeparator)))
}
}
return nil
return list(cd, r, createRecord, "buildDrafts", true)
},
},
},
&cobra.Command{
Use: "future",
Short: "List all posts dated in the future",
Long: `List all of the posts in your content directory which will be posted in the future.`,
RunE: func(cmd *cobra.Command, args []string) error {
sites, err := cc.buildSites(map[string]any{"buildFuture": true})
if err != nil {
return newSystemError("Error building sites", err)
}
if err != nil {
return newSystemError("Error building sites", err)
}
writer := csv.NewWriter(os.Stdout)
defer writer.Flush()
for _, p := range sites.Pages() {
if resource.IsFuture(p) {
err := writer.Write([]string{
strings.TrimPrefix(p.File().Filename(), sites.WorkingDir+string(os.PathSeparator)),
&simpleCommand{
name: "future",
short: "List all posts dated in the future",
long: `List all of the posts in your content directory which will be posted in the future.`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
createRecord := func(p page.Page) []string {
if !resource.IsFuture(p) || p.File().IsZero() {
return nil
}
return []string{
p.File().Path(),
p.PublishDate().Format(time.RFC3339),
})
if err != nil {
return newSystemError("Error writing future posts to stdout", err)
}
}
}
return nil
return list(cd, r, createRecord, "buildFuture", true)
},
},
},
&cobra.Command{
Use: "expired",
Short: "List all posts already expired",
Long: `List all of the posts in your content directory which has already expired.`,
RunE: func(cmd *cobra.Command, args []string) error {
sites, err := cc.buildSites(map[string]any{"buildExpired": true})
if err != nil {
return newSystemError("Error building sites", err)
}
if err != nil {
return newSystemError("Error building sites", err)
}
writer := csv.NewWriter(os.Stdout)
defer writer.Flush()
for _, p := range sites.Pages() {
if resource.IsExpired(p) {
err := writer.Write([]string{
strings.TrimPrefix(p.File().Filename(), sites.WorkingDir+string(os.PathSeparator)),
p.ExpiryDate().Format(time.RFC3339),
})
if err != nil {
return newSystemError("Error writing expired posts to stdout", err)
&simpleCommand{
name: "expired",
short: "List all posts already expired",
long: `List all of the posts in your content directory which has already expired.`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
createRecord := func(p page.Page) []string {
if !resource.IsExpired(p) || p.File().IsZero() {
return nil
}
return []string{
p.File().Path(),
p.PublishDate().Format(time.RFC3339),
}
}
}
return nil
}
return list(cd, r, createRecord, "buildExpired", true)
},
},
&simpleCommand{
name: "all",
short: "List all posts",
long: `List all of the posts in your content directory, include drafts, future and expired pages.`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
createRecord := func(p page.Page) []string {
if p.File().IsZero() {
return nil
}
return []string{
p.File().Path(),
p.PublishDate().Format(time.RFC3339),
}
}
return list(cd, r, createRecord, "buildDrafts", true, "buildFuture", true, "buildExpired", true)
},
},
},
&cobra.Command{
Use: "all",
Short: "List all posts",
Long: `List all of the posts in your content directory, include drafts, future and expired pages.`,
RunE: func(cmd *cobra.Command, args []string) error {
sites, err := cc.buildSites(map[string]any{
"buildExpired": true,
"buildDrafts": true,
"buildFuture": true,
})
if err != nil {
return newSystemError("Error building sites", err)
}
}
writer := csv.NewWriter(os.Stdout)
defer writer.Flush()
writer.Write([]string{
"path",
"slug",
"title",
"date",
"expiryDate",
"publishDate",
"draft",
"permalink",
})
for _, p := range sites.Pages() {
if !p.IsPage() {
continue
}
err := writer.Write([]string{
strings.TrimPrefix(p.File().Filename(), sites.WorkingDir+string(os.PathSeparator)),
p.Slug(),
p.Title(),
p.Date().Format(time.RFC3339),
p.ExpiryDate().Format(time.RFC3339),
p.PublishDate().Format(time.RFC3339),
strconv.FormatBool(p.Draft()),
p.Permalink(),
})
if err != nil {
return newSystemError("Error writing posts to stdout", err)
}
}
return nil
},
},
)
cc.baseBuilderCmd = b.newBuilderBasicCmd(cmd)
return cc
}
type listCommand struct {
commands []simplecobra.Commander
}
func (c *listCommand) Commands() []simplecobra.Commander {
return c.commands
}
func (c *listCommand) Name() string {
return "list"
}
func (c *listCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error {
// Do nothing.
return nil
}
func (c *listCommand) WithCobraCommand(cmd *cobra.Command) error {
cmd.Short = "Listing out various types of content"
cmd.Long = `Listing out various types of content.
List requires a subcommand, e.g. hugo list drafts`
return nil
}
func (c *listCommand) Init(cd, runner *simplecobra.Commandeer) error {
return nil
}

View File

@ -1,68 +0,0 @@
package commands
import (
"bytes"
"encoding/csv"
"io"
"os"
"path/filepath"
"strings"
"testing"
qt "github.com/frankban/quicktest"
)
func captureStdout(f func() error) (string, error) {
old := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
err := f()
w.Close()
os.Stdout = old
var buf bytes.Buffer
io.Copy(&buf, r)
return buf.String(), err
}
func TestListAll(t *testing.T) {
c := qt.New(t)
dir := createSimpleTestSite(t, testSiteConfig{})
hugoCmd := newCommandsBuilder().addAll().build()
cmd := hugoCmd.getCommand()
t.Cleanup(func() {
os.RemoveAll(dir)
})
cmd.SetArgs([]string{"-s=" + dir, "list", "all"})
out, err := captureStdout(func() error {
_, err := cmd.ExecuteC()
return err
})
c.Assert(err, qt.IsNil)
r := csv.NewReader(strings.NewReader(out))
header, err := r.Read()
c.Assert(err, qt.IsNil)
c.Assert(header, qt.DeepEquals, []string{
"path", "slug", "title",
"date", "expiryDate", "publishDate",
"draft", "permalink",
})
record, err := r.Read()
c.Assert(err, qt.IsNil)
c.Assert(record, qt.DeepEquals, []string{
filepath.Join("content", "p1.md"), "", "P1",
"0001-01-01T00:00:00Z", "0001-01-01T00:00:00Z", "0001-01-01T00:00:00Z",
"false", "https://example.org/p1/",
})
}

View File

@ -1,4 +1,4 @@
// Copyright 2020 The Hugo Authors. All rights reserved.
// Copyright 2023 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -14,87 +14,18 @@
package commands
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"regexp"
"github.com/gohugoio/hugo/hugolib"
"github.com/gohugoio/hugo/modules"
"github.com/bep/simplecobra"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/modules/npm"
"github.com/spf13/cobra"
)
var _ cmder = (*modCmd)(nil)
type modCmd struct {
*baseBuilderCmd
}
func (c *modCmd) newVerifyCmd() *cobra.Command {
var clean bool
verifyCmd := &cobra.Command{
Use: "verify",
Short: "Verify dependencies.",
Long: `Verify checks that the dependencies of the current module, which are stored in a local downloaded source cache, have not been modified since being downloaded.
`,
RunE: func(cmd *cobra.Command, args []string) error {
return c.withModsClient(true, func(c *modules.Client) error {
return c.Verify(clean)
})
},
}
verifyCmd.Flags().BoolVarP(&clean, "clean", "", false, "delete module cache for dependencies that fail verification")
return verifyCmd
}
var moduleNotFoundRe = regexp.MustCompile("module.*not found")
func (c *modCmd) newCleanCmd() *cobra.Command {
var pattern string
var all bool
cmd := &cobra.Command{
Use: "clean",
Short: "Delete the Hugo Module cache for the current project.",
Long: `Delete the Hugo Module cache for the current project.
Note that after you run this command, all of your dependencies will be re-downloaded next time you run "hugo".
Also note that if you configure a positive maxAge for the "modules" file cache, it will also be cleaned as part of "hugo --gc".
`,
RunE: func(cmd *cobra.Command, args []string) error {
if all {
com, err := c.initConfig(false)
if err != nil && com == nil {
return err
}
count, err := com.hugo().FileCaches.ModulesCache().Prune(true)
com.logger.Printf("Deleted %d files from module cache.", count)
return err
}
return c.withModsClient(true, func(c *modules.Client) error {
return c.Clean(pattern)
})
},
}
cmd.Flags().StringVarP(&pattern, "pattern", "", "", `pattern matching module paths to clean (all if not set), e.g. "**hugo*"`)
cmd.Flags().BoolVarP(&all, "all", "", false, "clean entire module cache")
return cmd
}
func (b *commandsBuilder) newModCmd() *modCmd {
c := &modCmd{}
const commonUsage = `
const commonUsageMod = `
Note that Hugo will always start out by resolving the components defined in the site
configuration, provided by a _vendor directory (if no --ignoreVendorPaths flag provided),
Go Modules, or a folder inside the themes directory, in that order.
@ -103,27 +34,156 @@ See https://gohugo.io/hugo-modules/ for more information.
`
cmd := &cobra.Command{
Use: "mod",
Short: "Various Hugo Modules helpers.",
Long: `Various helpers to help manage the modules in your project's dependency graph.
// buildConfigCommands creates a new config command and its subcommands.
func newModCommands() *modCommands {
var (
clean bool
pattern string
all bool
)
Most operations here requires a Go version installed on your system (>= Go 1.12) and the relevant VCS client (typically Git).
This is not needed if you only operate on modules inside /themes or if you have vendored them via "hugo mod vendor".
npmCommand := &simpleCommand{
name: "npm",
short: "Various npm helpers.",
long: `Various npm (Node package manager) helpers.`,
commands: []simplecobra.Commander{
&simpleCommand{
name: "pack",
short: "Experimental: Prepares and writes a composite package.json file for your project.",
long: `Prepares and writes a composite package.json file for your project.
` + commonUsage,
On first run it creates a "package.hugo.json" in the project root if not already there. This file will be used as a template file
with the base dependency set.
RunE: nil,
This set will be merged with all "package.hugo.json" files found in the dependency tree, picking the version closest to the project.
This command is marked as 'Experimental'. We think it's a great idea, so it's not likely to be
removed from Hugo, but we need to test this out in "real life" to get a feel of it,
so this may/will change in future versions of Hugo.
`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
h, err := r.Hugo(flagsToCfg(cd, nil))
if err != nil {
return err
}
return npm.Pack(h.BaseFs.SourceFs, h.BaseFs.Assets.Dirs)
},
},
},
}
cmd.AddCommand(newModNPMCmd(c))
return &modCommands{
commands: []simplecobra.Commander{
&simpleCommand{
name: "init",
short: "Initialize this project as a Hugo Module.",
long: `Initialize this project as a Hugo Module.
It will try to guess the module path, but you may help by passing it as an argument, e.g:
hugo mod init github.com/gohugoio/testshortcodes
Note that Hugo Modules supports multi-module projects, so you can initialize a Hugo Module
inside a subfolder on GitHub, as one example.
`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
h, err := r.Hugo(flagsToCfg(cd, nil))
if err != nil {
return err
}
var initPath string
if len(args) >= 1 {
initPath = args[0]
}
return h.Configs.ModulesClient.Init(initPath)
},
},
&simpleCommand{
name: "verify",
short: "Verify dependencies.",
long: `Verify checks that the dependencies of the current module, which are stored in a local downloaded source cache, have not been modified since being downloaded.`,
withc: func(cmd *cobra.Command) {
cmd.Flags().BoolVarP(&clean, "clean", "", false, "delete module cache for dependencies that fail verification")
},
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
conf, err := r.ConfigFromProvider(r.configVersionID.Load(), flagsToCfg(cd, nil))
if err != nil {
return err
}
client := conf.configs.ModulesClient
return client.Verify(clean)
},
},
&simpleCommand{
name: "graph",
short: "Print a module dependency graph.",
long: `Print a module dependency graph with information about module status (disabled, vendored).
Note that for vendored modules, that is the version listed and not the one from go.mod.
`,
withc: func(cmd *cobra.Command) {
cmd.Flags().BoolVarP(&clean, "clean", "", false, "delete module cache for dependencies that fail verification")
},
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
conf, err := r.ConfigFromProvider(r.configVersionID.Load(), flagsToCfg(cd, nil))
if err != nil {
return err
}
client := conf.configs.ModulesClient
return client.Graph(os.Stdout)
},
},
&simpleCommand{
name: "clean",
short: "Delete the Hugo Module cache for the current project.",
long: `Delete the Hugo Module cache for the current project.`,
withc: func(cmd *cobra.Command) {
cmd.Flags().StringVarP(&pattern, "pattern", "", "", `pattern matching module paths to clean (all if not set), e.g. "**hugo*"`)
cmd.Flags().BoolVarP(&all, "all", "", false, "clean entire module cache")
},
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
h, err := r.Hugo(flagsToCfg(cd, nil))
if err != nil {
return err
}
if all {
modCache := h.ResourceSpec.FileCaches.ModulesCache()
count, err := modCache.Prune(true)
r.Printf("Deleted %d files from module cache.", count)
return err
}
cmd.AddCommand(
&cobra.Command{
Use: "get",
DisableFlagParsing: true,
Short: "Resolves dependencies in your current Hugo Project.",
Long: `
return h.Configs.ModulesClient.Clean(pattern)
},
},
&simpleCommand{
name: "tidy",
short: "Remove unused entries in go.mod and go.sum.",
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
h, err := r.Hugo(flagsToCfg(cd, nil))
if err != nil {
return err
}
return h.Configs.ModulesClient.Tidy()
},
},
&simpleCommand{
name: "vendor",
short: "Vendor all module dependencies into the _vendor directory.",
long: `Vendor all module dependencies into the _vendor directory.
If a module is vendored, that is where Hugo will look for it's dependencies.
`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
h, err := r.Hugo(flagsToCfg(cd, nil))
if err != nil {
return err
}
return h.Configs.ModulesClient.Vendor()
},
},
&simpleCommand{
name: "get",
short: "Resolves dependencies in your current Hugo Project.",
long: `
Resolves dependencies in your current Hugo Project.
Some examples:
@ -142,152 +202,109 @@ Install the latest versions of all module dependencies:
hugo mod get -u ./... (recursive)
Run "go help get" for more information. All flags available for "go get" is also relevant here.
` + commonUsage,
RunE: func(cmd *cobra.Command, args []string) error {
// We currently just pass on the flags we get to Go and
// need to do the flag handling manually.
if len(args) == 1 && args[0] == "-h" {
return cmd.Help()
}
var lastArg string
if len(args) != 0 {
lastArg = args[len(args)-1]
}
if lastArg == "./..." {
args = args[:len(args)-1]
// Do a recursive update.
dirname, err := os.Getwd()
if err != nil {
return err
` + commonUsageMod,
withc: func(cmd *cobra.Command) {
cmd.DisableFlagParsing = true
},
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
// We currently just pass on the flags we get to Go and
// need to do the flag handling manually.
if len(args) == 1 && args[0] == "-h" {
return errHelp
}
// Sanity check. We do recursive walking and want to avoid
// accidents.
if len(dirname) < 5 {
return errors.New("must not be run from the file system root")
var lastArg string
if len(args) != 0 {
lastArg = args[len(args)-1]
}
filepath.Walk(dirname, func(path string, info os.FileInfo, err error) error {
if info.IsDir() {
return nil
if lastArg == "./..." {
args = args[:len(args)-1]
// Do a recursive update.
dirname, err := os.Getwd()
if err != nil {
return err
}
if info.Name() == "go.mod" {
// Found a module.
dir := filepath.Dir(path)
fmt.Println("Update module in", dir)
c.source = dir
err := c.withModsClient(false, func(c *modules.Client) error {
if len(args) == 1 && args[0] == "-h" {
return cmd.Help()
}
return c.Get(args...)
})
if err != nil {
return err
// Sanity chesimplecobra. We do recursive walking and want to avoid
// accidents.
if len(dirname) < 5 {
return errors.New("must not be run from the file system root")
}
filepath.Walk(dirname, func(path string, info os.FileInfo, err error) error {
if info.IsDir() {
return nil
}
if info.Name() == "go.mod" {
// Found a module.
dir := filepath.Dir(path)
r.Println("Update module in", dir)
cfg := config.New()
cfg.Set("workingDir", dir)
conf, err := r.ConfigFromProvider(r.configVersionID.Load(), flagsToCfg(cd, cfg))
if err != nil {
return err
}
client := conf.configs.ModulesClient
return client.Get(args...)
}
}
return nil
})
return nil
})
return nil
}
return c.withModsClient(false, func(c *modules.Client) error {
return c.Get(args...)
})
} else {
conf, err := r.ConfigFromProvider(r.configVersionID.Load(), flagsToCfg(cd, nil))
if err != nil {
return err
}
client := conf.configs.ModulesClient
return client.Get(args...)
}
},
},
npmCommand,
},
&cobra.Command{
Use: "graph",
Short: "Print a module dependency graph.",
Long: `Print a module dependency graph with information about module status (disabled, vendored).
Note that for vendored modules, that is the version listed and not the one from go.mod.
`,
RunE: func(cmd *cobra.Command, args []string) error {
return c.withModsClient(true, func(c *modules.Client) error {
return c.Graph(os.Stdout)
})
},
},
&cobra.Command{
Use: "init",
Short: "Initialize this project as a Hugo Module.",
Long: `Initialize this project as a Hugo Module.
It will try to guess the module path, but you may help by passing it as an argument, e.g:
}
hugo mod init github.com/gohugoio/testshortcodes
Note that Hugo Modules supports multi-module projects, so you can initialize a Hugo Module
inside a subfolder on GitHub, as one example.
`,
RunE: func(cmd *cobra.Command, args []string) error {
var path string
if len(args) >= 1 {
path = args[0]
}
return c.withModsClient(false, func(c *modules.Client) error {
return c.Init(path)
})
},
},
&cobra.Command{
Use: "vendor",
Short: "Vendor all module dependencies into the _vendor directory.",
Long: `Vendor all module dependencies into the _vendor directory.
If a module is vendored, that is where Hugo will look for it's dependencies.
`,
RunE: func(cmd *cobra.Command, args []string) error {
return c.withModsClient(true, func(c *modules.Client) error {
return c.Vendor()
})
},
},
c.newVerifyCmd(),
&cobra.Command{
Use: "tidy",
Short: "Remove unused entries in go.mod and go.sum.",
RunE: func(cmd *cobra.Command, args []string) error {
return c.withModsClient(true, func(c *modules.Client) error {
return c.Tidy()
})
},
},
c.newCleanCmd(),
)
c.baseBuilderCmd = b.newBuilderCmd(cmd)
return c
}
func (c *modCmd) withModsClient(failOnMissingConfig bool, f func(*modules.Client) error) error {
com, err := c.initConfig(failOnMissingConfig)
type modCommands struct {
r *rootCommand
commands []simplecobra.Commander
}
func (c *modCommands) Commands() []simplecobra.Commander {
return c.commands
}
func (c *modCommands) Name() string {
return "mod"
}
func (c *modCommands) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error {
_, err := c.r.ConfigFromProvider(c.r.configVersionID.Load(), nil)
if err != nil {
return err
}
//config := conf.configs.Base
return f(com.hugo().ModulesClient)
return nil
}
func (c *modCmd) withHugo(f func(*hugolib.HugoSites) error) error {
com, err := c.initConfig(true)
if err != nil {
return err
}
func (c *modCommands) WithCobraCommand(cmd *cobra.Command) error {
cmd.Short = "Various Hugo Modules helpers."
cmd.Long = `Various helpers to help manage the modules in your project's dependency graph.
Most operations here requires a Go version installed on your system (>= Go 1.12) and the relevant VCS client (typically Git).
This is not needed if you only operate on modules inside /themes or if you have vendored them via "hugo mod vendor".
return f(com.hugo())
` + commonUsageMod
cmd.RunE = nil
return nil
}
func (c *modCmd) initConfig(failOnNoConfig bool) (*commandeer, error) {
com, err := initializeConfig(failOnNoConfig, false, false, &c.hugoBuilderCommon, c, nil)
if err != nil {
return nil, err
}
return com, nil
func (c *modCommands) Init(cd, runner *simplecobra.Commandeer) error {
c.r = cd.Root.Command.(*rootCommand)
return nil
}

View File

@ -1,56 +0,0 @@
// Copyright 2020 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package commands
import (
"github.com/gohugoio/hugo/hugolib"
"github.com/gohugoio/hugo/modules/npm"
"github.com/spf13/cobra"
)
func newModNPMCmd(c *modCmd) *cobra.Command {
cmd := &cobra.Command{
Use: "npm",
Short: "Various npm helpers.",
Long: `Various npm (Node package manager) helpers.`,
RunE: func(cmd *cobra.Command, args []string) error {
return c.withHugo(func(h *hugolib.HugoSites) error {
return nil
})
},
}
cmd.AddCommand(&cobra.Command{
Use: "pack",
Short: "Experimental: Prepares and writes a composite package.json file for your project.",
Long: `Prepares and writes a composite package.json file for your project.
On first run it creates a "package.hugo.json" in the project root if not already there. This file will be used as a template file
with the base dependency set.
This set will be merged with all "package.hugo.json" files found in the dependency tree, picking the version closest to the project.
This command is marked as 'Experimental'. We think it's a great idea, so it's not likely to be
removed from Hugo, but we need to test this out in "real life" to get a feel of it,
so this may/will change in future versions of Hugo.
`,
RunE: func(cmd *cobra.Command, args []string) error {
return c.withHugo(func(h *hugolib.HugoSites) error {
return npm.Pack(h.BaseFs.SourceFs, h.BaseFs.Assets.Dirs)
})
},
})
return cmd
}

View File

@ -1,4 +1,4 @@
// Copyright 2018 The Hugo Authors. All rights reserved.
// Copyright 2023 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -15,114 +15,351 @@ package commands
import (
"bytes"
"os"
"context"
"errors"
"fmt"
"path/filepath"
"strings"
"github.com/bep/simplecobra"
"github.com/gohugoio/hugo/common/htime"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/create"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugolib"
"github.com/gohugoio/hugo/parser"
"github.com/gohugoio/hugo/parser/metadecoders"
"github.com/spf13/afero"
"github.com/spf13/cobra"
jww "github.com/spf13/jwalterweatherman"
)
var _ cmder = (*newCmd)(nil)
func newNewCommand() *newCommand {
var (
configFormat string
force bool
contentType string
)
type newCmd struct {
contentEditor string
contentType string
force bool
var c *newCommand
c = &newCommand{
commands: []simplecobra.Commander{
&simpleCommand{
name: "content",
use: "content [path]",
short: "Create new content for your site",
long: `Create a new content file and automatically set the date and title.
It will guess which kind of file to create based on the path provided.
You can also specify the kind with ` + "`-k KIND`" + `.
If archetypes are provided in your theme or site, they will be used.
Ensure you run this within the root directory of your site.`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
if len(args) < 1 {
return errors.New("path needs to be provided")
}
h, err := r.Hugo(flagsToCfg(cd, nil))
if err != nil {
return err
}
return create.NewContent(h, contentType, args[0], force)
},
withc: func(cmd *cobra.Command) {
cmd.Flags().StringVarP(&contentType, "kind", "k", "", "content type to create")
cmd.Flags().String("editor", "", "edit new content with this editor, if provided")
cmd.Flags().BoolVarP(&force, "force", "f", false, "overwrite file if it already exists")
},
},
&simpleCommand{
name: "site",
use: "site [path]",
short: "Create a new site (skeleton)",
long: `Create a new site in the provided directory.
The new site will have the correct structure, but no content or theme yet.
Use ` + "`hugo new [contentPath]`" + ` to create new content.`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
if len(args) < 1 {
return errors.New("path needs to be provided")
}
createpath, err := filepath.Abs(filepath.Clean(args[0]))
if err != nil {
return err
}
cfg := config.New()
cfg.Set("workingDir", createpath)
cfg.Set("publishDir", "public")
conf, err := r.ConfigFromProvider(r.configVersionID.Load(), flagsToCfg(cd, cfg))
if err != nil {
return err
}
sourceFs := conf.fs.Source
archeTypePath := filepath.Join(createpath, "archetypes")
dirs := []string{
archeTypePath,
filepath.Join(createpath, "assets"),
filepath.Join(createpath, "content"),
filepath.Join(createpath, "data"),
filepath.Join(createpath, "layouts"),
filepath.Join(createpath, "static"),
filepath.Join(createpath, "themes"),
}
if exists, _ := helpers.Exists(createpath, sourceFs); exists {
if isDir, _ := helpers.IsDir(createpath, sourceFs); !isDir {
return errors.New(createpath + " already exists but not a directory")
}
isEmpty, _ := helpers.IsEmpty(createpath, sourceFs)
switch {
case !isEmpty && !force:
return errors.New(createpath + " already exists and is not empty. See --force.")
case !isEmpty && force:
all := append(dirs, filepath.Join(createpath, "hugo."+configFormat))
for _, path := range all {
if exists, _ := helpers.Exists(path, sourceFs); exists {
return errors.New(path + " already exists")
}
}
}
}
for _, dir := range dirs {
if err := sourceFs.MkdirAll(dir, 0777); err != nil {
return fmt.Errorf("failed to create dir: %w", err)
}
}
c.newSiteCreateConfig(sourceFs, createpath, configFormat)
// Create a default archetype file.
helpers.SafeWriteToDisk(filepath.Join(archeTypePath, "default.md"),
strings.NewReader(create.DefaultArchetypeTemplateTemplate), sourceFs)
r.Printf("Congratulations! Your new Hugo site is created in %s.\n\n", createpath)
r.Println(c.newSiteNextStepsText())
return nil
},
withc: func(cmd *cobra.Command) {
cmd.Flags().StringVarP(&configFormat, "format", "f", "toml", "config file format")
cmd.Flags().BoolVar(&force, "force", false, "init inside non-empty directory")
},
},
&simpleCommand{
name: "theme",
use: "theme [path]",
short: "Create a new site (skeleton)",
long: `Create a new site in the provided directory.
The new site will have the correct structure, but no content or theme yet.
Use ` + "`hugo new [contentPath]`" + ` to create new content.`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
h, err := r.Hugo(flagsToCfg(cd, nil))
if err != nil {
return err
}
ps := h.PathSpec
sourceFs := ps.Fs.Source
themesDir := h.Configs.LoadingInfo.BaseConfig.ThemesDir
createpath := ps.AbsPathify(filepath.Join(themesDir, args[0]))
r.Println("Creating theme at", createpath)
if x, _ := helpers.Exists(createpath, sourceFs); x {
return errors.New(createpath + " already exists")
}
for _, filename := range []string{
"index.html",
"404.html",
"_default/list.html",
"_default/single.html",
"partials/head.html",
"partials/header.html",
"partials/footer.html",
} {
touchFile(sourceFs, filepath.Join(createpath, "layouts", filename))
}
baseofDefault := []byte(`<!DOCTYPE html>
<html>
{{- partial "head.html" . -}}
<body>
{{- partial "header.html" . -}}
<div id="content">
{{- block "main" . }}{{- end }}
</div>
{{- partial "footer.html" . -}}
</body>
</html>
`)
err = helpers.WriteToDisk(filepath.Join(createpath, "layouts", "_default", "baseof.html"), bytes.NewReader(baseofDefault), sourceFs)
if err != nil {
return err
}
mkdir(createpath, "archetypes")
archDefault := []byte("+++\n+++\n")
err = helpers.WriteToDisk(filepath.Join(createpath, "archetypes", "default.md"), bytes.NewReader(archDefault), sourceFs)
if err != nil {
return err
}
mkdir(createpath, "static", "js")
mkdir(createpath, "static", "css")
by := []byte(`The MIT License (MIT)
Copyright (c) ` + htime.Now().Format("2006") + ` YOUR_NAME_HERE
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
`)
err = helpers.WriteToDisk(filepath.Join(createpath, "LICENSE"), bytes.NewReader(by), sourceFs)
if err != nil {
return err
}
c.createThemeMD(ps.Fs.Source, createpath)
return nil
},
},
},
}
return c
*baseBuilderCmd
}
func (b *commandsBuilder) newNewCmd() *newCmd {
cmd := &cobra.Command{
Use: "new [path]",
Short: "Create new content for your site",
Long: `Create a new content file and automatically set the date and title.
type newCommand struct {
rootCmd *rootCommand
commands []simplecobra.Commander
}
func (c *newCommand) Commands() []simplecobra.Commander {
return c.commands
}
func (c *newCommand) Name() string {
return "new"
}
func (c *newCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error {
return nil
}
func (c *newCommand) WithCobraCommand(cmd *cobra.Command) error {
cmd.Short = "Create new content for your site"
cmd.Long = `Create a new content file and automatically set the date and title.
It will guess which kind of file to create based on the path provided.
You can also specify the kind with ` + "`-k KIND`" + `.
If archetypes are provided in your theme or site, they will be used.
Ensure you run this within the root directory of your site.`,
}
cc := &newCmd{baseBuilderCmd: b.newBuilderCmd(cmd)}
cmd.Flags().StringVarP(&cc.contentType, "kind", "k", "", "content type to create")
cmd.Flags().StringVar(&cc.contentEditor, "editor", "", "edit new content with this editor, if provided")
cmd.Flags().BoolVarP(&cc.force, "force", "f", false, "overwrite file if it already exists")
cmd.AddCommand(b.newNewSiteCmd().getCommand())
cmd.AddCommand(b.newNewThemeCmd().getCommand())
cmd.RunE = cc.newContent
return cc
Ensure you run this within the root directory of your site.`
return nil
}
func (n *newCmd) newContent(cmd *cobra.Command, args []string) error {
cfgInit := func(c *commandeer) error {
if cmd.Flags().Changed("editor") {
c.Set("newContentEditor", n.contentEditor)
}
return nil
func (c *newCommand) Init(cd, runner *simplecobra.Commandeer) error {
c.rootCmd = cd.Root.Command.(*rootCommand)
return nil
}
func (c *newCommand) newSiteCreateConfig(fs afero.Fs, inpath string, kind string) (err error) {
in := map[string]string{
"baseURL": "http://example.org/",
"title": "My New Hugo Site",
"languageCode": "en-us",
}
c, err := initializeConfig(true, true, false, &n.hugoBuilderCommon, n, cfgInit)
var buf bytes.Buffer
err = parser.InterfaceToConfig(in, metadecoders.FormatFromString(kind), &buf)
if err != nil {
return err
}
if len(args) < 1 {
return newUserError("path needs to be provided")
}
return create.NewContent(c.hugo(), n.contentType, args[0], n.force)
return helpers.WriteToDisk(filepath.Join(inpath, "hugo."+kind), &buf, fs)
}
func mkdir(x ...string) {
p := filepath.Join(x...)
func (c *newCommand) newSiteNextStepsText() string {
var nextStepsText bytes.Buffer
err := os.MkdirAll(p, 0777) // before umask
nextStepsText.WriteString(`Just a few more steps and you're ready to go:
1. Download a theme into the same-named folder.
Choose a theme from https://themes.gohugo.io/ or
create your own with the "hugo new theme <THEMENAME>" command.
2. Perhaps you want to add some content. You can add single files
with "hugo new `)
nextStepsText.WriteString(filepath.Join("<SECTIONNAME>", "<FILENAME>.<FORMAT>"))
nextStepsText.WriteString(`".
3. Start the built-in live server via "hugo server".
Visit https://gohugo.io/ for quickstart guide and full documentation.`)
return nextStepsText.String()
}
func (c *newCommand) createThemeMD(fs afero.Fs, inpath string) (err error) {
by := []byte(`# theme.toml template for a Hugo theme
# See https://github.com/gohugoio/hugoThemes#themetoml for an example
name = "` + strings.Title(helpers.MakeTitle(filepath.Base(inpath))) + `"
license = "MIT"
licenselink = "https://github.com/yourname/yourtheme/blob/master/LICENSE"
description = ""
homepage = "http://example.com/"
tags = []
features = []
min_version = "0.41.0"
[author]
name = ""
homepage = ""
# If porting an existing theme
[original]
name = ""
homepage = ""
repo = ""
`)
err = helpers.WriteToDisk(filepath.Join(inpath, "theme.toml"), bytes.NewReader(by), fs)
if err != nil {
jww.FATAL.Fatalln(err)
return
}
}
func touchFile(fs afero.Fs, x ...string) {
inpath := filepath.Join(x...)
mkdir(filepath.Dir(inpath))
err := helpers.WriteToDisk(inpath, bytes.NewReader([]byte{}), fs)
err = helpers.WriteToDisk(filepath.Join(inpath, "hugo.toml"), strings.NewReader("# Theme config.\n"), fs)
if err != nil {
jww.FATAL.Fatalln(err)
return
}
}
func newContentPathSection(h *hugolib.HugoSites, path string) (string, string) {
// Forward slashes is used in all examples. Convert if needed.
// Issue #1133
createpath := filepath.FromSlash(path)
if h != nil {
for _, dir := range h.BaseFs.Content.Dirs {
createpath = strings.TrimPrefix(createpath, dir.Meta().Filename)
}
}
var section string
// assume the first directory is the section (kind)
if strings.Contains(createpath[1:], helpers.FilePathSeparator) {
parts := strings.Split(strings.TrimPrefix(createpath, helpers.FilePathSeparator), helpers.FilePathSeparator)
if len(parts) > 0 {
section = parts[0]
}
}
return createpath, section
return nil
}

View File

@ -1,29 +0,0 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package commands
import (
"path/filepath"
"testing"
qt "github.com/frankban/quicktest"
)
// Issue #1133
func TestNewContentPathSectionWithForwardSlashes(t *testing.T) {
c := qt.New(t)
p, s := newContentPathSection(nil, "/post/new.md")
c.Assert(p, qt.Equals, filepath.FromSlash("/post/new.md"))
c.Assert(s, qt.Equals, "post")
}

View File

@ -1,167 +0,0 @@
// Copyright 2018 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package commands
import (
"bytes"
"errors"
"fmt"
"path/filepath"
"strings"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/parser/metadecoders"
"github.com/gohugoio/hugo/create"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/parser"
"github.com/spf13/cobra"
jww "github.com/spf13/jwalterweatherman"
)
var _ cmder = (*newSiteCmd)(nil)
type newSiteCmd struct {
configFormat string
*baseBuilderCmd
}
func (b *commandsBuilder) newNewSiteCmd() *newSiteCmd {
cc := &newSiteCmd{}
cmd := &cobra.Command{
Use: "site [path]",
Short: "Create a new site (skeleton)",
Long: `Create a new site in the provided directory.
The new site will have the correct structure, but no content or theme yet.
Use ` + "`hugo new [contentPath]`" + ` to create new content.`,
RunE: cc.newSite,
}
cmd.Flags().StringVarP(&cc.configFormat, "format", "f", "toml", "config file format")
cmd.Flags().Bool("force", false, "init inside non-empty directory")
cc.baseBuilderCmd = b.newBuilderBasicCmd(cmd)
return cc
}
func (n *newSiteCmd) doNewSite(fs *hugofs.Fs, basepath string, force bool) error {
archeTypePath := filepath.Join(basepath, "archetypes")
dirs := []string{
archeTypePath,
filepath.Join(basepath, "assets"),
filepath.Join(basepath, "content"),
filepath.Join(basepath, "data"),
filepath.Join(basepath, "layouts"),
filepath.Join(basepath, "static"),
filepath.Join(basepath, "themes"),
}
if exists, _ := helpers.Exists(basepath, fs.Source); exists {
if isDir, _ := helpers.IsDir(basepath, fs.Source); !isDir {
return errors.New(basepath + " already exists but not a directory")
}
isEmpty, _ := helpers.IsEmpty(basepath, fs.Source)
switch {
case !isEmpty && !force:
return errors.New(basepath + " already exists and is not empty. See --force.")
case !isEmpty && force:
// TODO(bep) eventually rename this to hugo.
all := append(dirs, filepath.Join(basepath, "config."+n.configFormat))
for _, path := range all {
if exists, _ := helpers.Exists(path, fs.Source); exists {
return errors.New(path + " already exists")
}
}
}
}
for _, dir := range dirs {
if err := fs.Source.MkdirAll(dir, 0777); err != nil {
return fmt.Errorf("Failed to create dir: %w", err)
}
}
createConfig(fs, basepath, n.configFormat)
// Create a default archetype file.
helpers.SafeWriteToDisk(filepath.Join(archeTypePath, "default.md"),
strings.NewReader(create.DefaultArchetypeTemplateTemplate), fs.Source)
jww.FEEDBACK.Printf("Congratulations! Your new Hugo site is created in %s.\n\n", basepath)
jww.FEEDBACK.Println(nextStepsText())
return nil
}
// newSite creates a new Hugo site and initializes a structured Hugo directory.
func (n *newSiteCmd) newSite(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return newUserError("path needs to be provided")
}
createpath, err := filepath.Abs(filepath.Clean(args[0]))
if err != nil {
return newUserError(err)
}
forceNew, _ := cmd.Flags().GetBool("force")
cfg := config.New()
cfg.Set("workingDir", createpath)
cfg.Set("publishDir", "public")
return n.doNewSite(hugofs.NewDefault(cfg), createpath, forceNew)
}
func createConfig(fs *hugofs.Fs, inpath string, kind string) (err error) {
in := map[string]string{
"baseURL": "http://example.org/",
"title": "My New Hugo Site",
"languageCode": "en-us",
}
var buf bytes.Buffer
err = parser.InterfaceToConfig(in, metadecoders.FormatFromString(kind), &buf)
if err != nil {
return err
}
return helpers.WriteToDisk(filepath.Join(inpath, "config."+kind), &buf, fs.Source)
}
func nextStepsText() string {
var nextStepsText bytes.Buffer
nextStepsText.WriteString(`Just a few more steps and you're ready to go:
1. Download a theme into the same-named folder.
Choose a theme from https://themes.gohugo.io/ or
create your own with the "hugo new theme <THEMENAME>" command.
2. Perhaps you want to add some content. You can add single files
with "hugo new `)
nextStepsText.WriteString(filepath.Join("<SECTIONNAME>", "<FILENAME>.<FORMAT>"))
nextStepsText.WriteString(`".
3. Start the built-in live server via "hugo server".
Visit https://gohugo.io/ for quickstart guide and full documentation.`)
return nextStepsText.String()
}

View File

@ -1,176 +0,0 @@
// Copyright 2018 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package commands
import (
"bytes"
"errors"
"path/filepath"
"strings"
"github.com/gohugoio/hugo/common/htime"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugofs"
"github.com/spf13/cobra"
jww "github.com/spf13/jwalterweatherman"
)
var _ cmder = (*newThemeCmd)(nil)
type newThemeCmd struct {
*baseBuilderCmd
}
func (b *commandsBuilder) newNewThemeCmd() *newThemeCmd {
cc := &newThemeCmd{}
cmd := &cobra.Command{
Use: "theme [name]",
Short: "Create a new theme",
Long: `Create a new theme (skeleton) called [name] in ./themes.
New theme is a skeleton. Please add content to the touched files. Add your
name to the copyright line in the license and adjust the theme.toml file
as you see fit.`,
RunE: cc.newTheme,
}
cc.baseBuilderCmd = b.newBuilderBasicCmd(cmd)
return cc
}
// newTheme creates a new Hugo theme template
func (n *newThemeCmd) newTheme(cmd *cobra.Command, args []string) error {
c, err := initializeConfig(false, false, false, &n.hugoBuilderCommon, n, nil)
if err != nil {
return err
}
if len(args) < 1 {
return newUserError("theme name needs to be provided")
}
createpath := c.hugo().PathSpec.AbsPathify(filepath.Join(c.Cfg.GetString("themesDir"), args[0]))
jww.FEEDBACK.Println("Creating theme at", createpath)
cfg := c.DepsCfg
if x, _ := helpers.Exists(createpath, cfg.Fs.Source); x {
return errors.New(createpath + " already exists")
}
mkdir(createpath, "layouts", "_default")
mkdir(createpath, "layouts", "partials")
touchFile(cfg.Fs.Source, createpath, "layouts", "index.html")
touchFile(cfg.Fs.Source, createpath, "layouts", "404.html")
touchFile(cfg.Fs.Source, createpath, "layouts", "_default", "list.html")
touchFile(cfg.Fs.Source, createpath, "layouts", "_default", "single.html")
baseofDefault := []byte(`<!DOCTYPE html>
<html>
{{- partial "head.html" . -}}
<body>
{{- partial "header.html" . -}}
<div id="content">
{{- block "main" . }}{{- end }}
</div>
{{- partial "footer.html" . -}}
</body>
</html>
`)
err = helpers.WriteToDisk(filepath.Join(createpath, "layouts", "_default", "baseof.html"), bytes.NewReader(baseofDefault), cfg.Fs.Source)
if err != nil {
return err
}
touchFile(cfg.Fs.Source, createpath, "layouts", "partials", "head.html")
touchFile(cfg.Fs.Source, createpath, "layouts", "partials", "header.html")
touchFile(cfg.Fs.Source, createpath, "layouts", "partials", "footer.html")
mkdir(createpath, "archetypes")
archDefault := []byte("+++\n+++\n")
err = helpers.WriteToDisk(filepath.Join(createpath, "archetypes", "default.md"), bytes.NewReader(archDefault), cfg.Fs.Source)
if err != nil {
return err
}
mkdir(createpath, "static", "js")
mkdir(createpath, "static", "css")
by := []byte(`The MIT License (MIT)
Copyright (c) ` + htime.Now().Format("2006") + ` YOUR_NAME_HERE
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
`)
err = helpers.WriteToDisk(filepath.Join(createpath, "LICENSE"), bytes.NewReader(by), cfg.Fs.Source)
if err != nil {
return err
}
n.createThemeMD(cfg.Fs, createpath)
return nil
}
func (n *newThemeCmd) createThemeMD(fs *hugofs.Fs, inpath string) (err error) {
by := []byte(`# theme.toml template for a Hugo theme
# See https://github.com/gohugoio/hugoThemes#themetoml for an example
name = "` + strings.Title(helpers.MakeTitle(filepath.Base(inpath))) + `"
license = "MIT"
licenselink = "https://github.com/yourname/yourtheme/blob/master/LICENSE"
description = ""
homepage = "http://example.com/"
tags = []
features = []
min_version = "0.41.0"
[author]
name = ""
homepage = ""
# If porting an existing theme
[original]
name = ""
homepage = ""
repo = ""
`)
err = helpers.WriteToDisk(filepath.Join(inpath, "theme.toml"), bytes.NewReader(by), fs.Source)
if err != nil {
return
}
return nil
}

View File

@ -1,51 +0,0 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build nodeploy
// +build nodeploy
package commands
import (
"errors"
"github.com/spf13/cobra"
)
var _ cmder = (*deployCmd)(nil)
// deployCmd supports deploying sites to Cloud providers.
type deployCmd struct {
*baseBuilderCmd
}
func (b *commandsBuilder) newDeployCmd() *deployCmd {
cc := &deployCmd{}
cmd := &cobra.Command{
Use: "deploy",
Short: "Deploy your site to a Cloud provider.",
Long: `Deploy your site to a Cloud provider.
See https://gohugo.io/hosting-and-deployment/hugo-deploy/ for detailed
documentation.
`,
RunE: func(cmd *cobra.Command, args []string) error {
return errors.New("build without HUGO_BUILD_TAGS=nodeploy to use this command")
},
}
cc.baseBuilderCmd = b.newBuilderBasicCmd(cmd)
return cc
}

View File

@ -1,7 +1,4 @@
//go:build release
// +build release
// Copyright 2017-present The Hugo Authors. All rights reserved.
// Copyright 2023 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -17,55 +14,39 @@
package commands
import (
"github.com/gohugoio/hugo/config"
"context"
"github.com/bep/simplecobra"
"github.com/gohugoio/hugo/releaser"
"github.com/spf13/cobra"
)
var _ cmder = (*releaseCommandeer)(nil)
// Note: This is a command only meant for internal use and must be run
// via "go run -tags release main.go release" on the actual code base that is in the release.
func newReleaseCommand() simplecobra.Commander {
type releaseCommandeer struct {
cmd *cobra.Command
var (
step int
skipPush bool
try bool
)
step int
skipPush bool
try bool
}
return &simpleCommand{
name: "release",
short: "Release a new version of Hugo.",
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
rel, err := releaser.New(skipPush, try, step)
if err != nil {
return err
}
func createReleaser() cmder {
// Note: This is a command only meant for internal use and must be run
// via "go run -tags release main.go release" on the actual code base that is in the release.
r := &releaseCommandeer{
cmd: &cobra.Command{
Use: "release",
Short: "Release a new version of Hugo.",
Hidden: true,
return rel.Run()
},
withc: func(cmd *cobra.Command) {
cmd.Hidden = true
cmd.PersistentFlags().BoolVarP(&skipPush, "skip-push", "", false, "skip pushing to remote")
cmd.PersistentFlags().BoolVarP(&try, "try", "", false, "no changes")
cmd.PersistentFlags().IntVarP(&step, "step", "", 0, "step to run (1: set new version 2: prepare next dev version)")
},
}
r.cmd.RunE = func(cmd *cobra.Command, args []string) error {
return r.release()
}
r.cmd.PersistentFlags().BoolVarP(&r.skipPush, "skip-push", "", false, "skip pushing to remote")
r.cmd.PersistentFlags().BoolVarP(&r.try, "try", "", false, "no changes")
r.cmd.PersistentFlags().IntVarP(&r.step, "step", "", 0, "step to run (1: set new version 2: prepare next dev version)")
return r
}
func (c *releaseCommandeer) getCommand() *cobra.Command {
return c.cmd
}
func (c *releaseCommandeer) flagsToConfig(cfg config.Provider) {
}
func (r *releaseCommandeer) release() error {
rel, err := releaser.New(r.skipPush, r.try, r.step)
if err != nil {
return err
}
return rel.Run()
}

View File

@ -1,21 +0,0 @@
//go:build !release
// +build !release
// Copyright 2018 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package commands
func createReleaser() cmder {
return &nilCommand{}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,31 +0,0 @@
// Copyright 2018 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package commands
import (
"bytes"
"io"
"net/url"
"github.com/gohugoio/hugo/transform"
"github.com/gohugoio/hugo/transform/livereloadinject"
)
func injectLiveReloadScript(src io.Reader, baseURL url.URL) string {
var b bytes.Buffer
chain := transform.Chain{livereloadinject.New(baseURL)}
chain.Apply(&b, src)
return b.String()
}

View File

@ -1,429 +0,0 @@
// Copyright 2015 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package commands
import (
"context"
"fmt"
"net/http"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/htesting"
"golang.org/x/sync/errgroup"
qt "github.com/frankban/quicktest"
)
// Issue 9518
func TestServerPanicOnConfigError(t *testing.T) {
c := qt.New(t)
config := `
[markup]
[markup.highlight]
linenos='table'
`
r := runServerTest(c,
serverTestOptions{
config: config,
},
)
c.Assert(r.err, qt.IsNotNil)
c.Assert(r.err.Error(), qt.Contains, "cannot parse 'Highlight.LineNos' as bool:")
}
func TestServer404(t *testing.T) {
c := qt.New(t)
r := runServerTest(c,
serverTestOptions{
pathsToGet: []string{"this/does/not/exist"},
getNumHomes: 1,
},
)
c.Assert(r.err, qt.IsNil)
pr := r.pathsResults["this/does/not/exist"]
c.Assert(pr.statusCode, qt.Equals, http.StatusNotFound)
c.Assert(pr.body, qt.Contains, "404: 404 Page not found|Not Found.")
}
func TestServerPathEncodingIssues(t *testing.T) {
c := qt.New(t)
// Issue 10287
c.Run("Unicode paths", func(c *qt.C) {
r := runServerTest(c,
serverTestOptions{
pathsToGet: []string{"hügö/"},
getNumHomes: 1,
},
)
c.Assert(r.err, qt.IsNil)
c.Assert(r.pathsResults["hügö/"].body, qt.Contains, "This is hügö")
})
// Issue 10314
c.Run("Windows multilingual 404", func(c *qt.C) {
config := `
baseURL = 'https://example.org/'
title = 'Hugo Forum Topic #40568'
defaultContentLanguageInSubdir = true
[languages.en]
contentDir = 'content/en'
languageCode = 'en-US'
languageName = 'English'
weight = 1
[languages.es]
contentDir = 'content/es'
languageCode = 'es-ES'
languageName = 'Espanol'
weight = 2
[server]
[[server.redirects]]
from = '/en/**'
to = '/en/404.html'
status = 404
[[server.redirects]]
from = '/es/**'
to = '/es/404.html'
status = 404
`
r := runServerTest(c,
serverTestOptions{
config: config,
pathsToGet: []string{"en/this/does/not/exist", "es/this/does/not/exist"},
getNumHomes: 1,
},
)
c.Assert(r.err, qt.IsNil)
pr1 := r.pathsResults["en/this/does/not/exist"]
pr2 := r.pathsResults["es/this/does/not/exist"]
c.Assert(pr1.statusCode, qt.Equals, http.StatusNotFound)
c.Assert(pr2.statusCode, qt.Equals, http.StatusNotFound)
c.Assert(pr1.body, qt.Contains, "404: 404 Page not found|Not Found.")
c.Assert(pr2.body, qt.Contains, "404: 404 Page not found|Not Found.")
})
}
func TestServerFlags(t *testing.T) {
c := qt.New(t)
assertPublic := func(c *qt.C, r serverTestResult, renderStaticToDisk bool) {
c.Assert(r.err, qt.IsNil)
c.Assert(r.homesContent[0], qt.Contains, "Environment: development")
c.Assert(r.publicDirnames["myfile.txt"], qt.Equals, renderStaticToDisk)
}
for _, test := range []struct {
flag string
assert func(c *qt.C, r serverTestResult)
}{
{"", func(c *qt.C, r serverTestResult) {
assertPublic(c, r, false)
}},
{"--renderToDisk", func(c *qt.C, r serverTestResult) {
assertPublic(c, r, true)
}},
{"--renderStaticToDisk", func(c *qt.C, r serverTestResult) {
assertPublic(c, r, true)
}},
} {
c.Run(test.flag, func(c *qt.C) {
config := `
baseURL="https://example.org"
`
var args []string
if test.flag != "" {
args = strings.Split(test.flag, "=")
}
opts := serverTestOptions{
config: config,
args: args,
getNumHomes: 1,
}
r := runServerTest(c, opts)
test.assert(c, r)
})
}
}
func TestServerBugs(t *testing.T) {
// TODO(bep) this is flaky on Windows on GH Actions.
if htesting.IsGitHubAction() && runtime.GOOS == "windows" {
t.Skip("skipping on windows")
}
c := qt.New(t)
for _, test := range []struct {
name string
config string
flag string
numservers int
assert func(c *qt.C, r serverTestResult)
}{
{"PostProcess, memory", "", "", 1, func(c *qt.C, r serverTestResult) {
c.Assert(r.err, qt.IsNil)
c.Assert(r.homesContent[0], qt.Contains, "PostProcess: /foo.min.css")
}},
// Issue 9788
{"PostProcess, memory", "", "", 1, func(c *qt.C, r serverTestResult) {
c.Assert(r.err, qt.IsNil)
c.Assert(r.homesContent[0], qt.Contains, "PostProcess: /foo.min.css")
}},
{"PostProcess, disk", "", "--renderToDisk", 1, func(c *qt.C, r serverTestResult) {
c.Assert(r.err, qt.IsNil)
c.Assert(r.homesContent[0], qt.Contains, "PostProcess: /foo.min.css")
}},
// Issue 9901
{"Multihost", `
defaultContentLanguage = 'en'
[languages]
[languages.en]
baseURL = 'https://example.com'
title = 'My blog'
weight = 1
[languages.fr]
baseURL = 'https://example.fr'
title = 'Mon blogue'
weight = 2
`, "", 2, func(c *qt.C, r serverTestResult) {
c.Assert(r.err, qt.IsNil)
for i, s := range []string{"My blog", "Mon blogue"} {
c.Assert(r.homesContent[i], qt.Contains, s)
}
}},
} {
c.Run(test.name, func(c *qt.C) {
if test.config == "" {
test.config = `
baseURL="https://example.org"
`
}
var args []string
if test.flag != "" {
args = strings.Split(test.flag, "=")
}
opts := serverTestOptions{
config: test.config,
getNumHomes: test.numservers,
pathsToGet: []string{"this/does/not/exist"},
args: args,
}
r := runServerTest(c, opts)
pr := r.pathsResults["this/does/not/exist"]
c.Assert(pr.statusCode, qt.Equals, http.StatusNotFound)
c.Assert(pr.body, qt.Contains, "404: 404 Page not found|Not Found.")
test.assert(c, r)
})
}
}
type serverTestResult struct {
err error
homesContent []string
content404 string
publicDirnames map[string]bool
pathsResults map[string]pathResult
}
type pathResult struct {
statusCode int
body string
}
type serverTestOptions struct {
getNumHomes int
config string
pathsToGet []string
args []string
}
func runServerTest(c *qt.C, opts serverTestOptions) serverTestResult {
dir := createSimpleTestSite(c, testSiteConfig{configTOML: opts.config})
result := serverTestResult{
publicDirnames: make(map[string]bool),
pathsResults: make(map[string]pathResult),
}
sp, err := helpers.FindAvailablePort()
c.Assert(err, qt.IsNil)
port := sp.Port
defer func() {
os.RemoveAll(dir)
}()
stop := make(chan bool)
b := newCommandsBuilder()
scmd := b.newServerCmdSignaled(stop)
cmd := scmd.getCommand()
args := append([]string{"-s=" + dir, fmt.Sprintf("-p=%d", port)}, opts.args...)
cmd.SetArgs(args)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
wg, ctx := errgroup.WithContext(ctx)
wg.Go(func() error {
_, err := cmd.ExecuteC()
return err
})
if opts.getNumHomes > 0 {
// Esp. on slow CI machines, we need to wait a little before the web
// server is ready.
wait := 567 * time.Millisecond
if os.Getenv("CI") != "" {
wait = 2 * time.Second
}
time.Sleep(wait)
result.homesContent = make([]string, opts.getNumHomes)
for i := 0; i < opts.getNumHomes; i++ {
func() {
resp, err := http.Get(fmt.Sprintf("http://localhost:%d/", port+i))
c.Assert(err, qt.IsNil)
c.Assert(resp.StatusCode, qt.Equals, http.StatusOK)
if err == nil {
defer resp.Body.Close()
result.homesContent[i] = helpers.ReaderToString(resp.Body)
}
}()
}
}
for _, path := range opts.pathsToGet {
func() {
resp, err := http.Get(fmt.Sprintf("http://localhost:%d/%s", port, path))
c.Assert(err, qt.IsNil)
pr := pathResult{
statusCode: resp.StatusCode,
}
if err == nil {
defer resp.Body.Close()
pr.body = helpers.ReaderToString(resp.Body)
}
result.pathsResults[path] = pr
}()
}
time.Sleep(1 * time.Second)
select {
case <-stop:
case stop <- true:
}
pubFiles, err := os.ReadDir(filepath.Join(dir, "public"))
c.Assert(err, qt.IsNil)
for _, f := range pubFiles {
result.publicDirnames[f.Name()] = true
}
result.err = wg.Wait()
return result
}
func TestFixURL(t *testing.T) {
type data struct {
TestName string
CLIBaseURL string
CfgBaseURL string
AppendPort bool
Port int
Result string
}
tests := []data{
{"Basic http localhost", "", "http://foo.com", true, 1313, "http://localhost:1313/"},
{"Basic https production, http localhost", "", "https://foo.com", true, 1313, "http://localhost:1313/"},
{"Basic subdir", "", "http://foo.com/bar", true, 1313, "http://localhost:1313/bar/"},
{"Basic production", "http://foo.com", "http://foo.com", false, 80, "http://foo.com/"},
{"Production subdir", "http://foo.com/bar", "http://foo.com/bar", false, 80, "http://foo.com/bar/"},
{"No http", "", "foo.com", true, 1313, "//localhost:1313/"},
{"Override configured port", "", "foo.com:2020", true, 1313, "//localhost:1313/"},
{"No http production", "foo.com", "foo.com", false, 80, "//foo.com/"},
{"No http production with port", "foo.com", "foo.com", true, 2020, "//foo.com:2020/"},
{"No config", "", "", true, 1313, "//localhost:1313/"},
}
for _, test := range tests {
t.Run(test.TestName, func(t *testing.T) {
b := newCommandsBuilder()
s := b.newServerCmd()
v := config.NewWithTestDefaults()
baseURL := test.CLIBaseURL
v.Set("baseURL", test.CfgBaseURL)
s.serverAppend = test.AppendPort
s.serverPort = test.Port
result, err := s.fixURL(v, baseURL, s.serverPort)
if err != nil {
t.Errorf("Unexpected error %s", err)
}
if result != test.Result {
t.Errorf("Expected %q, got %q", test.Result, result)
}
})
}
}
func TestRemoveErrorPrefixFromLog(t *testing.T) {
c := qt.New(t)
content := `ERROR 2018/10/07 13:11:12 Error while rendering "home": template: _default/baseof.html:4:3: executing "main" at <partial "logo" .>: error calling partial: template: partials/logo.html:5:84: executing "partials/logo.html" at <$resized.AHeight>: can't evaluate field AHeight in type *resource.Image
ERROR 2018/10/07 13:11:12 Rebuild failed: logged 1 error(s)
`
withoutError := removeErrorPrefixFromLog(content)
c.Assert(strings.Contains(withoutError, "ERROR"), qt.Equals, false)
}
func isWindowsCI() bool {
return runtime.GOOS == "windows" && os.Getenv("CI") != ""
}

View File

@ -1,129 +0,0 @@
// Copyright 2017 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package commands
import (
"path/filepath"
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/hugolib/filesystems"
"github.com/fsnotify/fsnotify"
"github.com/gohugoio/hugo/helpers"
"github.com/spf13/fsync"
)
type staticSyncer struct {
c *commandeer
}
func newStaticSyncer(c *commandeer) (*staticSyncer, error) {
return &staticSyncer{c: c}, nil
}
func (s *staticSyncer) isStatic(filename string) bool {
return s.c.hugo().BaseFs.SourceFilesystems.IsStatic(filename)
}
func (s *staticSyncer) syncsStaticEvents(staticEvents []fsnotify.Event) error {
c := s.c
syncFn := func(sourceFs *filesystems.SourceFilesystem) (uint64, error) {
publishDir := helpers.FilePathSeparator
if sourceFs.PublishFolder != "" {
publishDir = filepath.Join(publishDir, sourceFs.PublishFolder)
}
syncer := fsync.NewSyncer()
syncer.NoTimes = c.Cfg.GetBool("noTimes")
syncer.NoChmod = c.Cfg.GetBool("noChmod")
syncer.ChmodFilter = chmodFilter
syncer.SrcFs = sourceFs.Fs
syncer.DestFs = c.Fs.PublishDir
if c.renderStaticToDisk {
syncer.DestFs = c.Fs.PublishDirStatic
}
// prevent spamming the log on changes
logger := helpers.NewDistinctErrorLogger()
for _, ev := range staticEvents {
// Due to our approach of layering both directories and the content's rendered output
// into one we can't accurately remove a file not in one of the source directories.
// If a file is in the local static dir and also in the theme static dir and we remove
// it from one of those locations we expect it to still exist in the destination
//
// If Hugo generates a file (from the content dir) over a static file
// the content generated file should take precedence.
//
// Because we are now watching and handling individual events it is possible that a static
// event that occupies the same path as a content generated file will take precedence
// until a regeneration of the content takes places.
//
// Hugo assumes that these cases are very rare and will permit this bad behavior
// The alternative is to track every single file and which pipeline rendered it
// and then to handle conflict resolution on every event.
fromPath := ev.Name
relPath, found := sourceFs.MakePathRelative(fromPath)
if !found {
// Not member of this virtual host.
continue
}
// Remove || rename is harder and will require an assumption.
// Hugo takes the following approach:
// If the static file exists in any of the static source directories after this event
// Hugo will re-sync it.
// If it does not exist in all of the static directories Hugo will remove it.
//
// This assumes that Hugo has not generated content on top of a static file and then removed
// the source of that static file. In this case Hugo will incorrectly remove that file
// from the published directory.
if ev.Op&fsnotify.Rename == fsnotify.Rename || ev.Op&fsnotify.Remove == fsnotify.Remove {
if _, err := sourceFs.Fs.Stat(relPath); herrors.IsNotExist(err) {
// If file doesn't exist in any static dir, remove it
logger.Println("File no longer exists in static dir, removing", relPath)
_ = c.Fs.PublishDirStatic.RemoveAll(relPath)
} else if err == nil {
// If file still exists, sync it
logger.Println("Syncing", relPath, "to", publishDir)
if err := syncer.Sync(relPath, relPath); err != nil {
c.logger.Errorln(err)
}
} else {
c.logger.Errorln(err)
}
continue
}
// For all other event operations Hugo will sync static.
logger.Println("Syncing", relPath, "to", publishDir)
if err := syncer.Sync(filepath.Join(publishDir, relPath), relPath); err != nil {
c.logger.Errorln(err)
}
}
return 0, nil
}
_, err := c.doWithPublishDirs(syncFn)
return err
}

View File

@ -1,44 +0,0 @@
// Copyright 2015 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package commands
import (
"github.com/gohugoio/hugo/common/hugo"
"github.com/spf13/cobra"
jww "github.com/spf13/jwalterweatherman"
)
var _ cmder = (*versionCmd)(nil)
type versionCmd struct {
*baseCmd
}
func newVersionCmd() *versionCmd {
return &versionCmd{
newBaseCmd(&cobra.Command{
Use: "version",
Short: "Print the version number of Hugo",
Long: `All software has versions. This is Hugo's.`,
RunE: func(cmd *cobra.Command, args []string) error {
printHugoVersion()
return nil
},
}),
}
}
func printHugoVersion() {
jww.FEEDBACK.Println(hugo.BuildVersionString())
}

View File

@ -0,0 +1,78 @@
// Copyright 2023 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package commands
import (
"context"
"fmt"
"github.com/bep/simplecobra"
"github.com/spf13/cobra"
)
func newSimpleTemplateCommand() simplecobra.Commander {
return &simpleCommand{
name: "template",
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
return nil
},
withc: func(cmd *cobra.Command) {
},
}
}
func newTemplateCommand() *templateCommand {
return &templateCommand{
commands: []simplecobra.Commander{},
}
}
type templateCommand struct {
r *rootCommand
commands []simplecobra.Commander
}
func (c *templateCommand) Commands() []simplecobra.Commander {
return c.commands
}
func (c *templateCommand) Name() string {
return "template"
}
func (c *templateCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error {
conf, err := c.r.ConfigFromProvider(c.r.configVersionID.Load(), flagsToCfg(cd, nil))
if err != nil {
return err
}
fmt.Println("templateCommand.Run", conf)
return nil
}
func (c *templateCommand) WithCobraCommand(cmd *cobra.Command) error {
cmd.Short = "Print the site configuration"
cmd.Long = `Print the site configuration, both default and custom settings.`
return nil
}
func (c *templateCommand) Init(cd, runner *simplecobra.Commandeer) error {
c.r = cd.Root.Command.(*rootCommand)
return nil
}

View File

@ -0,0 +1,57 @@
// Copyright 2023 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package hstrings
import (
"fmt"
"strings"
"github.com/gohugoio/hugo/compare"
)
var _ compare.Eqer = StringEqualFold("")
// StringEqualFold is a string that implements the compare.Eqer interface and considers
// two strings equal if they are equal when folded to lower case.
// The compare.Eqer interface is used in Hugo to compare values in templates (e.g. using the eq template function).
type StringEqualFold string
func (s StringEqualFold) EqualFold(s2 string) bool {
return strings.EqualFold(string(s), s2)
}
func (s StringEqualFold) String() string {
return string(s)
}
func (s StringEqualFold) Eq(s2 any) bool {
switch ss := s2.(type) {
case string:
return s.EqualFold(ss)
case fmt.Stringer:
return s.EqualFold(ss.String())
}
return false
}
// EqualAny returns whether a string is equal to any of the given strings.
func EqualAny(a string, b ...string) bool {
for _, s := range b {
if a == s {
return true
}
}
return false
}

View File

@ -1,4 +1,4 @@
// Copyright 2021 The Hugo Authors. All rights reserved.
// Copyright 2023 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package config
package hstrings
import (
"testing"
@ -19,22 +19,18 @@ import (
qt "github.com/frankban/quicktest"
)
func TestCompositeConfig(t *testing.T) {
func TestStringEqualFold(t *testing.T) {
c := qt.New(t)
c.Run("Set and get", func(c *qt.C) {
base, layer := New(), New()
cfg := NewCompositeConfig(base, layer)
s1 := "A"
s2 := "a"
layer.Set("a1", "av")
base.Set("b1", "bv")
cfg.Set("c1", "cv")
c.Assert(StringEqualFold(s1).EqualFold(s2), qt.Equals, true)
c.Assert(StringEqualFold(s1).EqualFold(s1), qt.Equals, true)
c.Assert(StringEqualFold(s2).EqualFold(s1), qt.Equals, true)
c.Assert(StringEqualFold(s2).EqualFold(s2), qt.Equals, true)
c.Assert(StringEqualFold(s1).EqualFold("b"), qt.Equals, false)
c.Assert(StringEqualFold(s1).Eq(s2), qt.Equals, true)
c.Assert(StringEqualFold(s1).Eq("b"), qt.Equals, false)
c.Assert(cfg.Get("a1"), qt.Equals, "av")
c.Assert(cfg.Get("b1"), qt.Equals, "bv")
c.Assert(cfg.Get("c1"), qt.Equals, "cv")
c.Assert(cfg.IsSet("c1"), qt.IsTrue)
c.Assert(layer.IsSet("c1"), qt.IsTrue)
c.Assert(base.IsSet("c1"), qt.IsFalse)
})
}

View File

@ -14,6 +14,7 @@
package htime
import (
"log"
"strings"
"time"
@ -163,3 +164,11 @@ func Since(t time.Time) time.Duration {
type AsTimeProvider interface {
AsTime(zone *time.Location) time.Time
}
// StopWatch is a simple helper to measure time during development.
func StopWatch(name string) func() {
start := time.Now()
return func() {
log.Printf("StopWatch %q took %s", name, time.Since(start))
}
}

View File

@ -46,8 +46,8 @@ var (
vendorInfo string
)
// Info contains information about the current Hugo environment
type Info struct {
// HugoInfo contains information about the current Hugo environment
type HugoInfo struct {
CommitHash string
BuildDate string
@ -64,30 +64,30 @@ type Info struct {
}
// Version returns the current version as a comparable version string.
func (i Info) Version() VersionString {
func (i HugoInfo) Version() VersionString {
return CurrentVersion.Version()
}
// Generator a Hugo meta generator HTML tag.
func (i Info) Generator() template.HTML {
func (i HugoInfo) Generator() template.HTML {
return template.HTML(fmt.Sprintf(`<meta name="generator" content="Hugo %s">`, CurrentVersion.String()))
}
func (i Info) IsProduction() bool {
func (i HugoInfo) IsProduction() bool {
return i.Environment == EnvironmentProduction
}
func (i Info) IsExtended() bool {
func (i HugoInfo) IsExtended() bool {
return IsExtended
}
// Deps gets a list of dependencies for this Hugo build.
func (i Info) Deps() []*Dependency {
func (i HugoInfo) Deps() []*Dependency {
return i.deps
}
// NewInfo creates a new Hugo Info object.
func NewInfo(environment string, deps []*Dependency) Info {
func NewInfo(environment string, deps []*Dependency) HugoInfo {
if environment == "" {
environment = EnvironmentProduction
}
@ -104,7 +104,7 @@ func NewInfo(environment string, deps []*Dependency) Info {
goVersion = bi.GoVersion
}
return Info{
return HugoInfo{
CommitHash: commitHash,
BuildDate: buildDate,
Environment: environment,
@ -115,7 +115,7 @@ func NewInfo(environment string, deps []*Dependency) Info {
// GetExecEnviron creates and gets the common os/exec environment used in the
// external programs we interact with via os/exec, e.g. postcss.
func GetExecEnviron(workDir string, cfg config.Provider, fs afero.Fs) []string {
func GetExecEnviron(workDir string, cfg config.AllProvider, fs afero.Fs) []string {
var env []string
nodepath := filepath.Join(workDir, "node_modules")
if np := os.Getenv("NODE_PATH"); np != "" {
@ -123,10 +123,9 @@ func GetExecEnviron(workDir string, cfg config.Provider, fs afero.Fs) []string {
}
config.SetEnvVars(&env, "NODE_PATH", nodepath)
config.SetEnvVars(&env, "PWD", workDir)
config.SetEnvVars(&env, "HUGO_ENVIRONMENT", cfg.GetString("environment"))
config.SetEnvVars(&env, "HUGO_ENV", cfg.GetString("environment"))
config.SetEnvVars(&env, "HUGO_PUBLISHDIR", filepath.Join(workDir, cfg.GetString("publishDirOrig")))
config.SetEnvVars(&env, "HUGO_ENVIRONMENT", cfg.Environment())
config.SetEnvVars(&env, "HUGO_ENV", cfg.Environment())
config.SetEnvVars(&env, "HUGO_PUBLISHDIR", filepath.Join(workDir, cfg.BaseConfig().PublishDir))
if fs != nil {
fis, err := afero.ReadDir(fs, files.FolderJSConfig)

View File

@ -15,7 +15,6 @@ package loggers
import (
"fmt"
"strings"
)
// IgnorableLogger is a logger that ignores certain log statements.
@ -31,14 +30,13 @@ type ignorableLogger struct {
}
// NewIgnorableLogger wraps the given logger and ignores the log statement IDs given.
func NewIgnorableLogger(logger Logger, statements ...string) IgnorableLogger {
statementsSet := make(map[string]bool)
for _, s := range statements {
statementsSet[strings.ToLower(s)] = true
func NewIgnorableLogger(logger Logger, statements map[string]bool) IgnorableLogger {
if statements == nil {
statements = make(map[string]bool)
}
return ignorableLogger{
Logger: logger,
statements: statementsSet,
statements: statements,
}
}

View File

@ -43,25 +43,25 @@ func ToStringMapE(in any) (map[string]any, error) {
// ToParamsAndPrepare converts in to Params and prepares it for use.
// If in is nil, an empty map is returned.
// See PrepareParams.
func ToParamsAndPrepare(in any) (Params, bool) {
func ToParamsAndPrepare(in any) (Params, error) {
if types.IsNil(in) {
return Params{}, true
return Params{}, nil
}
m, err := ToStringMapE(in)
if err != nil {
return nil, false
return nil, err
}
PrepareParams(m)
return m, true
return m, nil
}
// MustToParamsAndPrepare calls ToParamsAndPrepare and panics if it fails.
func MustToParamsAndPrepare(in any) Params {
if p, ok := ToParamsAndPrepare(in); ok {
return p
} else {
panic(fmt.Sprintf("cannot convert %T to maps.Params", in))
p, err := ToParamsAndPrepare(in)
if err != nil {
panic(fmt.Sprintf("cannot convert %T to maps.Params: %s", in, err))
}
return p
}
// ToStringMap converts in to map[string]interface{}.
@ -96,6 +96,8 @@ func ToSliceStringMap(in any) ([]map[string]any, error) {
switch v := in.(type) {
case []map[string]any:
return v, nil
case Params:
return []map[string]any{v}, nil
case []any:
var s []map[string]any
for _, entry := range v {
@ -123,6 +125,23 @@ func LookupEqualFold[T any | string](m map[string]T, key string) (T, bool) {
return s, false
}
// MergeShallow merges src into dst, but only if the key does not already exist in dst.
// The keys are compared case insensitively.
func MergeShallow(dst, src map[string]any) {
for k, v := range src {
found := false
for dk := range dst {
if strings.EqualFold(dk, k) {
found = true
break
}
}
if !found {
dst[k] = v
}
}
}
type keyRename struct {
pattern glob.Glob
newKey string

View File

@ -116,11 +116,11 @@ func TestToSliceStringMap(t *testing.T) {
func TestToParamsAndPrepare(t *testing.T) {
c := qt.New(t)
_, ok := ToParamsAndPrepare(map[string]any{"A": "av"})
c.Assert(ok, qt.IsTrue)
_, err := ToParamsAndPrepare(map[string]any{"A": "av"})
c.Assert(err, qt.IsNil)
params, ok := ToParamsAndPrepare(nil)
c.Assert(ok, qt.IsTrue)
params, err := ToParamsAndPrepare(nil)
c.Assert(err, qt.IsNil)
c.Assert(params, qt.DeepEquals, Params{})
}

View File

@ -23,30 +23,37 @@ import (
// Params is a map where all keys are lower case.
type Params map[string]any
// Get does a lower case and nested search in this map.
// KeyParams is an utility struct for the WalkParams method.
type KeyParams struct {
Key string
Params Params
}
// GetNested does a lower case and nested search in this map.
// It will return nil if none found.
func (p Params) Get(indices ...string) any {
// Make all of these methods internal somehow.
func (p Params) GetNested(indices ...string) any {
v, _, _ := getNested(p, indices)
return v
}
// Set overwrites values in p with values in pp for common or new keys.
// Set overwrites values in dst with values in src for common or new keys.
// This is done recursively.
func (p Params) Set(pp Params) {
for k, v := range pp {
vv, found := p[k]
func SetParams(dst, src Params) {
for k, v := range src {
vv, found := dst[k]
if !found {
p[k] = v
dst[k] = v
} else {
switch vvv := vv.(type) {
case Params:
if pv, ok := v.(Params); ok {
vvv.Set(pv)
SetParams(vvv, pv)
} else {
p[k] = v
dst[k] = v
}
default:
p[k] = v
dst[k] = v
}
}
}
@ -70,18 +77,17 @@ func (p Params) IsZero() bool {
}
// Merge transfers values from pp to p for new keys.
// MergeParamsWithStrategy transfers values from src to dst for new keys using the merge strategy given.
// This is done recursively.
func (p Params) Merge(pp Params) {
p.merge("", pp)
func MergeParamsWithStrategy(strategy string, dst, src Params) {
dst.merge(ParamsMergeStrategy(strategy), src)
}
// MergeRoot transfers values from pp to p for new keys where p is the
// root of the tree.
// MergeParamsWithStrategy transfers values from src to dst for new keys using the merge encoded in dst.
// This is done recursively.
func (p Params) MergeRoot(pp Params) {
ms, _ := p.GetMergeStrategy()
p.merge(ms, pp)
func MergeParams(dst, src Params) {
ms, _ := dst.GetMergeStrategy()
dst.merge(ms, src)
}
func (p Params) merge(ps ParamsMergeStrategy, pp Params) {
@ -116,6 +122,7 @@ func (p Params) merge(ps ParamsMergeStrategy, pp Params) {
}
}
// For internal use.
func (p Params) GetMergeStrategy() (ParamsMergeStrategy, bool) {
if v, found := p[mergeStrategyKey]; found {
if s, ok := v.(ParamsMergeStrategy); ok {
@ -125,6 +132,7 @@ func (p Params) GetMergeStrategy() (ParamsMergeStrategy, bool) {
return ParamsMergeStrategyShallow, false
}
// For internal use.
func (p Params) DeleteMergeStrategy() bool {
if _, found := p[mergeStrategyKey]; found {
delete(p, mergeStrategyKey)
@ -133,7 +141,8 @@ func (p Params) DeleteMergeStrategy() bool {
return false
}
func (p Params) SetDefaultMergeStrategy(s ParamsMergeStrategy) {
// For internal use.
func (p Params) SetMergeStrategy(s ParamsMergeStrategy) {
switch s {
case ParamsMergeStrategyDeep, ParamsMergeStrategyNone, ParamsMergeStrategyShallow:
default:
@ -187,7 +196,7 @@ func GetNestedParam(keyStr, separator string, candidates ...Params) (any, error)
keySegments := strings.Split(keyStr, separator)
for _, m := range candidates {
if v := m.Get(keySegments...); v != nil {
if v := m.GetNested(keySegments...); v != nil {
return v, nil
}
}
@ -236,6 +245,55 @@ const (
mergeStrategyKey = "_merge"
)
// CleanConfigStringMapString removes any processing instructions from m,
// m will never be modified.
func CleanConfigStringMapString(m map[string]string) map[string]string {
if m == nil || len(m) == 0 {
return m
}
if _, found := m[mergeStrategyKey]; !found {
return m
}
// Create a new map and copy all the keys except the merge strategy key.
m2 := make(map[string]string, len(m)-1)
for k, v := range m {
if k != mergeStrategyKey {
m2[k] = v
}
}
return m2
}
// CleanConfigStringMap is the same as CleanConfigStringMapString but for
// map[string]any.
func CleanConfigStringMap(m map[string]any) map[string]any {
if m == nil || len(m) == 0 {
return m
}
if _, found := m[mergeStrategyKey]; !found {
return m
}
// Create a new map and copy all the keys except the merge strategy key.
m2 := make(map[string]any, len(m)-1)
for k, v := range m {
if k != mergeStrategyKey {
m2[k] = v
}
switch v2 := v.(type) {
case map[string]any:
m2[k] = CleanConfigStringMap(v2)
case Params:
var p Params = CleanConfigStringMap(v2)
m2[k] = p
case map[string]string:
m2[k] = CleanConfigStringMapString(v2)
}
}
return m2
}
func toMergeStrategy(v any) ParamsMergeStrategy {
s := ParamsMergeStrategy(cast.ToString(v))
switch s {

View File

@ -81,7 +81,7 @@ func TestParamsSetAndMerge(t *testing.T) {
p1, p2 := createParamsPair()
p1.Set(p2)
SetParams(p1, p2)
c.Assert(p1, qt.DeepEquals, Params{
"a": "abv",
@ -97,7 +97,7 @@ func TestParamsSetAndMerge(t *testing.T) {
p1, p2 = createParamsPair()
p1.Merge(p2)
MergeParamsWithStrategy("", p1, p2)
// Default is to do a shallow merge.
c.Assert(p1, qt.DeepEquals, Params{
@ -111,8 +111,8 @@ func TestParamsSetAndMerge(t *testing.T) {
})
p1, p2 = createParamsPair()
p1.SetDefaultMergeStrategy(ParamsMergeStrategyNone)
p1.Merge(p2)
p1.SetMergeStrategy(ParamsMergeStrategyNone)
MergeParamsWithStrategy("", p1, p2)
p1.DeleteMergeStrategy()
c.Assert(p1, qt.DeepEquals, Params{
@ -125,8 +125,8 @@ func TestParamsSetAndMerge(t *testing.T) {
})
p1, p2 = createParamsPair()
p1.SetDefaultMergeStrategy(ParamsMergeStrategyShallow)
p1.Merge(p2)
p1.SetMergeStrategy(ParamsMergeStrategyShallow)
MergeParamsWithStrategy("", p1, p2)
p1.DeleteMergeStrategy()
c.Assert(p1, qt.DeepEquals, Params{
@ -140,8 +140,8 @@ func TestParamsSetAndMerge(t *testing.T) {
})
p1, p2 = createParamsPair()
p1.SetDefaultMergeStrategy(ParamsMergeStrategyDeep)
p1.Merge(p2)
p1.SetMergeStrategy(ParamsMergeStrategyDeep)
MergeParamsWithStrategy("", p1, p2)
p1.DeleteMergeStrategy()
c.Assert(p1, qt.DeepEquals, Params{

View File

@ -1,4 +1,4 @@
// Copyright 2018 The Hugo Authors. All rights reserved.
// Copyright 2023 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -11,32 +11,37 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package paths
package urls
import (
"fmt"
"net/url"
"strconv"
"strings"
)
// A BaseURL in Hugo is normally on the form scheme://path, but the
// form scheme: is also valid (mailto:hugo@rules.com).
type BaseURL struct {
url *url.URL
urlStr string
url *url.URL
WithPath string
WithoutPath string
BasePath string
}
func (b BaseURL) String() string {
if b.urlStr != "" {
return b.urlStr
}
return b.url.String()
return b.WithPath
}
func (b BaseURL) Path() string {
return b.url.Path
}
func (b BaseURL) Port() int {
p, _ := strconv.Atoi(b.url.Port())
return p
}
// HostURL returns the URL to the host root without any path elements.
func (b BaseURL) HostURL() string {
return strings.TrimSuffix(b.String(), b.Path())
@ -44,7 +49,7 @@ func (b BaseURL) HostURL() string {
// WithProtocol returns the BaseURL prefixed with the given protocol.
// The Protocol is normally of the form "scheme://", i.e. "webcal://".
func (b BaseURL) WithProtocol(protocol string) (string, error) {
func (b BaseURL) WithProtocol(protocol string) (BaseURL, error) {
u := b.URL()
scheme := protocol
@ -62,10 +67,16 @@ func (b BaseURL) WithProtocol(protocol string) (string, error) {
if isFullProtocol && u.Opaque != "" {
u.Opaque = "//" + u.Opaque
} else if isOpaqueProtocol && u.Opaque == "" {
return "", fmt.Errorf("cannot determine BaseURL for protocol %q", protocol)
return BaseURL{}, fmt.Errorf("cannot determine BaseURL for protocol %q", protocol)
}
return u.String(), nil
return newBaseURLFromURL(u)
}
func (b BaseURL) WithPort(port int) (BaseURL, error) {
u := b.URL()
u.Host = u.Hostname() + ":" + strconv.Itoa(port)
return newBaseURLFromURL(u)
}
// URL returns a copy of the internal URL.
@ -75,13 +86,25 @@ func (b BaseURL) URL() *url.URL {
return &c
}
func newBaseURLFromString(b string) (BaseURL, error) {
var result BaseURL
base, err := url.Parse(b)
func NewBaseURLFromString(b string) (BaseURL, error) {
u, err := url.Parse(b)
if err != nil {
return result, err
return BaseURL{}, err
}
return newBaseURLFromURL(u)
}
func newBaseURLFromURL(u *url.URL) (BaseURL, error) {
baseURL := BaseURL{url: u, WithPath: u.String()}
var baseURLNoPath = baseURL.URL()
baseURLNoPath.Path = ""
baseURL.WithoutPath = baseURLNoPath.String()
basePath := u.Path
if basePath != "" && basePath != "/" {
baseURL.BasePath = basePath
}
return BaseURL{url: base, urlStr: base.String()}, nil
return baseURL, nil
}

View File

@ -1,4 +1,4 @@
// Copyright 2018 The Hugo Authors. All rights reserved.
// Copyright 2023 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package paths
package urls
import (
"testing"
@ -21,46 +21,46 @@ import (
func TestBaseURL(t *testing.T) {
c := qt.New(t)
b, err := newBaseURLFromString("http://example.com")
b, err := NewBaseURLFromString("http://example.com")
c.Assert(err, qt.IsNil)
c.Assert(b.String(), qt.Equals, "http://example.com")
p, err := b.WithProtocol("webcal://")
c.Assert(err, qt.IsNil)
c.Assert(p, qt.Equals, "webcal://example.com")
c.Assert(p.String(), qt.Equals, "webcal://example.com")
p, err = b.WithProtocol("webcal")
c.Assert(err, qt.IsNil)
c.Assert(p, qt.Equals, "webcal://example.com")
c.Assert(p.String(), qt.Equals, "webcal://example.com")
_, err = b.WithProtocol("mailto:")
c.Assert(err, qt.Not(qt.IsNil))
b, err = newBaseURLFromString("mailto:hugo@rules.com")
b, err = NewBaseURLFromString("mailto:hugo@rules.com")
c.Assert(err, qt.IsNil)
c.Assert(b.String(), qt.Equals, "mailto:hugo@rules.com")
// These are pretty constructed
p, err = b.WithProtocol("webcal")
c.Assert(err, qt.IsNil)
c.Assert(p, qt.Equals, "webcal:hugo@rules.com")
c.Assert(p.String(), qt.Equals, "webcal:hugo@rules.com")
p, err = b.WithProtocol("webcal://")
c.Assert(err, qt.IsNil)
c.Assert(p, qt.Equals, "webcal://hugo@rules.com")
c.Assert(p.String(), qt.Equals, "webcal://hugo@rules.com")
// Test with "non-URLs". Some people will try to use these as a way to get
// relative URLs working etc.
b, err = newBaseURLFromString("/")
b, err = NewBaseURLFromString("/")
c.Assert(err, qt.IsNil)
c.Assert(b.String(), qt.Equals, "/")
b, err = newBaseURLFromString("")
b, err = NewBaseURLFromString("")
c.Assert(err, qt.IsNil)
c.Assert(b.String(), qt.Equals, "")
// BaseURL with sub path
b, err = newBaseURLFromString("http://example.com/sub")
b, err = NewBaseURLFromString("http://example.com/sub")
c.Assert(err, qt.IsNil)
c.Assert(b.String(), qt.Equals, "http://example.com/sub")
c.Assert(b.HostURL(), qt.Equals, "http://example.com")

View File

@ -0,0 +1,813 @@
// Copyright 2023 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package allconfig contains the full configuration for Hugo.
// <docsmeta>{ "name": "Configuration", "description": "This section holds all configiration options in Hugo." }</docsmeta>
package allconfig
import (
"errors"
"fmt"
"reflect"
"regexp"
"sort"
"strconv"
"strings"
"time"
"github.com/gohugoio/hugo/cache/filecache"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/urls"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/config/privacy"
"github.com/gohugoio/hugo/config/security"
"github.com/gohugoio/hugo/config/services"
"github.com/gohugoio/hugo/deploy"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/langs"
"github.com/gohugoio/hugo/markup/markup_config"
"github.com/gohugoio/hugo/media"
"github.com/gohugoio/hugo/minifiers"
"github.com/gohugoio/hugo/modules"
"github.com/gohugoio/hugo/navigation"
"github.com/gohugoio/hugo/output"
"github.com/gohugoio/hugo/related"
"github.com/gohugoio/hugo/resources/images"
"github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/resources/page/pagemeta"
"github.com/spf13/afero"
xmaps "golang.org/x/exp/maps"
)
// InternalConfig is the internal configuration for Hugo, not read from any user provided config file.
type InternalConfig struct {
// Server mode?
Running bool
Quiet bool
Verbose bool
Clock string
Watch bool
DisableLiveReload bool
LiveReloadPort int
}
type Config struct {
// For internal use only.
Internal InternalConfig `mapstructure:"-" json:"-"`
// For internal use only.
C ConfigCompiled `mapstructure:"-" json:"-"`
RootConfig
// Author information.
Author map[string]any
// Social links.
Social map[string]string
// The build configuration section contains build-related configuration options.
// <docsmeta>{"identifiers": ["build"] }</docsmeta>
Build config.BuildConfig `mapstructure:"-"`
// The caches configuration section contains cache-related configuration options.
// <docsmeta>{"identifiers": ["caches"] }</docsmeta>
Caches filecache.Configs `mapstructure:"-"`
// The markup configuration section contains markup-related configuration options.
// <docsmeta>{"identifiers": ["markup"] }</docsmeta>
Markup markup_config.Config `mapstructure:"-"`
// The mediatypes configuration section maps the MIME type (a string) to a configuration object for that type.
// <docsmeta>{"identifiers": ["mediatypes"], "refs": ["types:media:type"] }</docsmeta>
MediaTypes *config.ConfigNamespace[map[string]media.MediaTypeConfig, media.Types] `mapstructure:"-"`
Imaging *config.ConfigNamespace[images.ImagingConfig, images.ImagingConfigInternal] `mapstructure:"-"`
// The outputformats configuration sections maps a format name (a string) to a configuration object for that format.
OutputFormats *config.ConfigNamespace[map[string]output.OutputFormatConfig, output.Formats] `mapstructure:"-"`
// The outputs configuration section maps a Page Kind (a string) to a slice of output formats.
// This can be overridden in the front matter.
Outputs map[string][]string `mapstructure:"-"`
// The cascade configuration section contains the top level front matter cascade configuration options,
// a slice of page matcher and params to apply to those pages.
Cascade *config.ConfigNamespace[[]page.PageMatcherParamsConfig, map[page.PageMatcher]maps.Params] `mapstructure:"-"`
// Menu configuration.
// <docsmeta>{"refs": ["config:languages:menus"] }</docsmeta>
Menus *config.ConfigNamespace[map[string]navigation.MenuConfig, navigation.Menus] `mapstructure:"-"`
// The deployment configuration section contains for hugo deploy.
Deployment deploy.DeployConfig `mapstructure:"-"`
// Module configuration.
Module modules.Config `mapstructure:"-"`
// Front matter configuration.
Frontmatter pagemeta.FrontmatterConfig `mapstructure:"-"`
// Minification configuration.
Minify minifiers.MinifyConfig `mapstructure:"-"`
// Permalink configuration.
Permalinks map[string]string `mapstructure:"-"`
// Taxonomy configuration.
Taxonomies map[string]string `mapstructure:"-"`
// Sitemap configuration.
Sitemap config.SitemapConfig `mapstructure:"-"`
// Related content configuration.
Related related.Config `mapstructure:"-"`
// Server configuration.
Server config.Server `mapstructure:"-"`
// Privacy configuration.
Privacy privacy.Config `mapstructure:"-"`
// Security configuration.
Security security.Config `mapstructure:"-"`
// Services configuration.
Services services.Config `mapstructure:"-"`
// User provided parameters.
// <docsmeta>{"refs": ["config:languages:params"] }</docsmeta>
Params maps.Params `mapstructure:"-"`
// The languages configuration sections maps a language code (a string) to a configuration object for that language.
Languages map[string]langs.LanguageConfig `mapstructure:"-"`
// UglyURLs configuration. Either a boolean or a sections map.
UglyURLs any `mapstructure:"-"`
}
type configCompiler interface {
CompileConfig() error
}
func (c Config) cloneForLang() *Config {
x := c
// Collapse all static dirs to one.
x.StaticDir = x.staticDirs()
// These will go away soon ...
x.StaticDir0 = nil
x.StaticDir1 = nil
x.StaticDir2 = nil
x.StaticDir3 = nil
x.StaticDir4 = nil
x.StaticDir5 = nil
x.StaticDir6 = nil
x.StaticDir7 = nil
x.StaticDir8 = nil
x.StaticDir9 = nil
x.StaticDir10 = nil
return &x
}
func (c *Config) CompileConfig() error {
s := c.Timeout
if _, err := strconv.Atoi(s); err == nil {
// A number, assume seconds.
s = s + "s"
}
timeout, err := time.ParseDuration(s)
if err != nil {
return fmt.Errorf("failed to parse timeout: %s", err)
}
disabledKinds := make(map[string]bool)
for _, kind := range c.DisableKinds {
disabledKinds[strings.ToLower(kind)] = true
}
kindOutputFormats := make(map[string]output.Formats)
isRssDisabled := disabledKinds["rss"]
outputFormats := c.OutputFormats.Config
for kind, formats := range c.Outputs {
if disabledKinds[kind] {
continue
}
for _, format := range formats {
if isRssDisabled && format == "rss" {
// Legacy config.
continue
}
f, found := outputFormats.GetByName(format)
if !found {
return fmt.Errorf("unknown output format %q for kind %q", format, kind)
}
kindOutputFormats[kind] = append(kindOutputFormats[kind], f)
}
}
disabledLangs := make(map[string]bool)
for _, lang := range c.DisableLanguages {
if lang == c.DefaultContentLanguage {
return fmt.Errorf("cannot disable default content language %q", lang)
}
disabledLangs[lang] = true
}
ignoredErrors := make(map[string]bool)
for _, err := range c.IgnoreErrors {
ignoredErrors[strings.ToLower(err)] = true
}
baseURL, err := urls.NewBaseURLFromString(c.BaseURL)
if err != nil {
return err
}
isUglyURL := func(section string) bool {
switch v := c.UglyURLs.(type) {
case bool:
return v
case map[string]bool:
return v[section]
default:
return false
}
}
ignoreFile := func(s string) bool {
return false
}
if len(c.IgnoreFiles) > 0 {
regexps := make([]*regexp.Regexp, len(c.IgnoreFiles))
for i, pattern := range c.IgnoreFiles {
var err error
regexps[i], err = regexp.Compile(pattern)
if err != nil {
return fmt.Errorf("failed to compile ignoreFiles pattern %q: %s", pattern, err)
}
}
ignoreFile = func(s string) bool {
for _, r := range regexps {
if r.MatchString(s) {
return true
}
}
return false
}
}
var clock time.Time
if c.Internal.Clock != "" {
var err error
clock, err = time.Parse(time.RFC3339, c.Internal.Clock)
if err != nil {
return fmt.Errorf("failed to parse clock: %s", err)
}
}
c.C = ConfigCompiled{
Timeout: timeout,
BaseURL: baseURL,
BaseURLLiveReload: baseURL,
DisabledKinds: disabledKinds,
DisabledLanguages: disabledLangs,
IgnoredErrors: ignoredErrors,
KindOutputFormats: kindOutputFormats,
CreateTitle: helpers.GetTitleFunc(c.TitleCaseStyle),
IsUglyURLSection: isUglyURL,
IgnoreFile: ignoreFile,
MainSections: c.MainSections,
Clock: clock,
}
for _, s := range allDecoderSetups {
if getCompiler := s.getCompiler; getCompiler != nil {
if err := getCompiler(c).CompileConfig(); err != nil {
return err
}
}
}
return nil
}
func (c Config) IsKindEnabled(kind string) bool {
return !c.C.DisabledKinds[kind]
}
func (c Config) IsLangDisabled(lang string) bool {
return c.C.DisabledLanguages[lang]
}
// ConfigCompiled holds values and functions that are derived from the config.
type ConfigCompiled struct {
Timeout time.Duration
BaseURL urls.BaseURL
BaseURLLiveReload urls.BaseURL
KindOutputFormats map[string]output.Formats
DisabledKinds map[string]bool
DisabledLanguages map[string]bool
IgnoredErrors map[string]bool
CreateTitle func(s string) string
IsUglyURLSection func(section string) bool
IgnoreFile func(filename string) bool
MainSections []string
Clock time.Time
}
// This may be set after the config is compiled.
func (c *ConfigCompiled) SetMainSections(sections []string) {
c.MainSections = sections
}
// This is set after the config is compiled by the server command.
func (c *ConfigCompiled) SetBaseURL(baseURL, baseURLLiveReload urls.BaseURL) {
c.BaseURL = baseURL
c.BaseURLLiveReload = baseURLLiveReload
}
// RootConfig holds all the top-level configuration options in Hugo
type RootConfig struct {
// The base URL of the site.
// Note that the default value is empty, but Hugo requires a valid URL (e.g. "https://example.com/") to work properly.
// <docsmeta>{"identifiers": ["URL"] }</docsmeta>
BaseURL string
// Whether to build content marked as draft.X
// <docsmeta>{"identifiers": ["draft"] }</docsmeta>
BuildDrafts bool
// Whether to build content with expiryDate in the past.
// <docsmeta>{"identifiers": ["expiryDate"] }</docsmeta>
BuildExpired bool
// Whether to build content with publishDate in the future.
// <docsmeta>{"identifiers": ["publishDate"] }</docsmeta>
BuildFuture bool
// Copyright information.
Copyright string
// The language to apply to content without any Clolanguage indicator.
DefaultContentLanguage string
// By defefault, we put the default content language in the root and the others below their language ID, e.g. /no/.
// Set this to true to put all languages below their language ID.
DefaultContentLanguageInSubdir bool
// Disable creation of alias redirect pages.
DisableAliases bool
// Disable lower casing of path segments.
DisablePathToLower bool
// Disable page kinds from build.
DisableKinds []string
// A list of languages to disable.
DisableLanguages []string
// Disable the injection of the Hugo generator tag on the home page.
DisableHugoGeneratorInject bool
// Enable replacement in Pages' Content of Emoji shortcodes with their equivalent Unicode characters.
// <docsmeta>{"identifiers": ["Content", "Unicode"] }</docsmeta>
EnableEmoji bool
// THe main section(s) of the site.
// If not set, Hugo will try to guess this from the content.
MainSections []string
// Enable robots.txt generation.
EnableRobotsTXT bool
// When enabled, Hugo will apply Git version information to each Page if possible, which
// can be used to keep lastUpdated in synch and to print version information.
// <docsmeta>{"identifiers": ["Page"] }</docsmeta>
EnableGitInfo bool
// Enable to track, calculate and print metrics.
TemplateMetrics bool
// Enable to track, print and calculate metric hints.
TemplateMetricsHints bool
// Enable to disable the build lock file.
NoBuildLock bool
// A list of error IDs to ignore.
IgnoreErrors []string
// A list of regexps that match paths to ignore.
// Deprecated: Use the settings on module imports.
IgnoreFiles []string
// Ignore cache.
IgnoreCache bool
// Enable to print greppable placeholders (on the form "[i18n] TRANSLATIONID") for missing translation strings.
EnableMissingTranslationPlaceholders bool
// Enable to print warnings for missing translation strings.
LogI18nWarnings bool
// ENable to print warnings for multiple files published to the same destination.
LogPathWarnings bool
// The configured environment. Default is "development" for server and "production" for build.
Environment string
// The default language code.
LanguageCode string
// Enable if the site content has CJK language (Chinese, Japanese, or Korean). This affects how Hugo counts words.
HasCJKLanguage bool
// The default number of pages per page when paginating.
Paginate int
// The path to use when creating pagination URLs, e.g. "page" in /page/2/.
PaginatePath string
// Whether to pluralize default list titles.
// Note that this currently only works for English, but you can provide your own title in the content file's front matter.
PluralizeListTitles bool
// Make all relative URLs absolute using the baseURL.
// <docsmeta>{"identifiers": ["baseURL"] }</docsmeta>
CanonifyURLs bool
// Enable this to make all relative URLs relative to content root. Note that this does not affect absolute URLs.
RelativeURLs bool
// Removes non-spacing marks from composite characters in content paths.
RemovePathAccents bool
// Whether to track and print unused templates during the build.
PrintUnusedTemplates bool
// URL to be used as a placeholder when a page reference cannot be found in ref or relref. Is used as-is.
RefLinksNotFoundURL string
// When using ref or relref to resolve page links and a link cannot be resolved, it will be logged with this log level.
// Valid values are ERROR (default) or WARNING. Any ERROR will fail the build (exit -1).
RefLinksErrorLevel string
// This will create a menu with all the sections as menu items and all the sections pages as “shadow-members”.
SectionPagesMenu string
// The length of text in words to show in a .Summary.
SummaryLength int
// The site title.
Title string
// The theme(s) to use.
// See Modules for more a more flexible way to load themes.
Theme []string
// Timeout for generating page contents, specified as a duration or in milliseconds.
Timeout string
// The time zone (or location), e.g. Europe/Oslo, used to parse front matter dates without such information and in the time function.
TimeZone string
// Set titleCaseStyle to specify the title style used by the title template function and the automatic section titles in Hugo.
// It defaults to AP Stylebook for title casing, but you can also set it to Chicago or Go (every word starts with a capital letter).
TitleCaseStyle string
// The editor used for opening up new content.
NewContentEditor string
// Don't sync modification time of files for the static mounts.
NoTimes bool
// Don't sync modification time of files for the static mounts.
NoChmod bool
// Clean the destination folder before a new build.
// This currently only handles static files.
CleanDestinationDir bool
// A Glob pattern of module paths to ignore in the _vendor folder.
IgnoreVendorPaths string
config.CommonDirs `mapstructure:",squash"`
// The odd constructs below are kept for backwards compatibility.
// Deprecated: Use module mount config instead.
StaticDir []string
// Deprecated: Use module mount config instead.
StaticDir0 []string
// Deprecated: Use module mount config instead.
StaticDir1 []string
// Deprecated: Use module mount config instead.
StaticDir2 []string
// Deprecated: Use module mount config instead.
StaticDir3 []string
// Deprecated: Use module mount config instead.
StaticDir4 []string
// Deprecated: Use module mount config instead.
StaticDir5 []string
// Deprecated: Use module mount config instead.
StaticDir6 []string
// Deprecated: Use module mount config instead.
StaticDir7 []string
// Deprecated: Use module mount config instead.
StaticDir8 []string
// Deprecated: Use module mount config instead.
StaticDir9 []string
// Deprecated: Use module mount config instead.
StaticDir10 []string
}
func (c RootConfig) staticDirs() []string {
var dirs []string
dirs = append(dirs, c.StaticDir...)
dirs = append(dirs, c.StaticDir0...)
dirs = append(dirs, c.StaticDir1...)
dirs = append(dirs, c.StaticDir2...)
dirs = append(dirs, c.StaticDir3...)
dirs = append(dirs, c.StaticDir4...)
dirs = append(dirs, c.StaticDir5...)
dirs = append(dirs, c.StaticDir6...)
dirs = append(dirs, c.StaticDir7...)
dirs = append(dirs, c.StaticDir8...)
dirs = append(dirs, c.StaticDir9...)
dirs = append(dirs, c.StaticDir10...)
return helpers.UniqueStringsReuse(dirs)
}
type Configs struct {
Base *Config
LoadingInfo config.LoadConfigResult
LanguageConfigMap map[string]*Config
LanguageConfigSlice []*Config
IsMultihost bool
Languages langs.Languages
LanguagesDefaultFirst langs.Languages
Modules modules.Modules
ModulesClient *modules.Client
configLangs []config.AllProvider
}
func (c *Configs) IsZero() bool {
// A config always has at least one language.
return c == nil || len(c.Languages) == 0
}
func (c *Configs) Init() error {
c.configLangs = make([]config.AllProvider, len(c.Languages))
for i, l := range c.LanguagesDefaultFirst {
c.configLangs[i] = ConfigLanguage{
m: c,
config: c.LanguageConfigMap[l.Lang],
baseConfig: c.LoadingInfo.BaseConfig,
language: l,
}
}
if len(c.Modules) == 0 {
return errors.New("no modules loaded (ned at least the main module)")
}
// Apply default project mounts.
if err := modules.ApplyProjectConfigDefaults(c.Modules[0], c.configLangs...); err != nil {
return err
}
return nil
}
func (c Configs) ConfigLangs() []config.AllProvider {
return c.configLangs
}
func (c Configs) GetFirstLanguageConfig() config.AllProvider {
return c.configLangs[0]
}
func (c Configs) GetByLang(lang string) config.AllProvider {
for _, l := range c.configLangs {
if l.Language().Lang == lang {
return l
}
}
return nil
}
// FromLoadConfigResult creates a new Config from res.
func FromLoadConfigResult(fs afero.Fs, res config.LoadConfigResult) (*Configs, error) {
if !res.Cfg.IsSet("languages") {
// We need at least one
lang := res.Cfg.GetString("defaultContentLanguage")
res.Cfg.Set("languages", maps.Params{lang: maps.Params{}})
}
bcfg := res.BaseConfig
cfg := res.Cfg
all := &Config{}
err := decodeConfigFromParams(fs, bcfg, cfg, all, nil)
if err != nil {
return nil, err
}
langConfigMap := make(map[string]*Config)
var langConfigs []*Config
languagesConfig := cfg.GetStringMap("languages")
var isMultiHost bool
if err := all.CompileConfig(); err != nil {
return nil, err
}
for k, v := range languagesConfig {
mergedConfig := config.New()
var differentRootKeys []string
switch x := v.(type) {
case maps.Params:
for kk, vv := range x {
if kk == "baseurl" {
// baseURL configure don the language level is a multihost setup.
isMultiHost = true
}
mergedConfig.Set(kk, vv)
if cfg.IsSet(kk) {
rootv := cfg.Get(kk)
// This overrides a root key and potentially needs a merge.
if !reflect.DeepEqual(rootv, vv) {
switch vvv := vv.(type) {
case maps.Params:
differentRootKeys = append(differentRootKeys, kk)
// Use the language value as base.
mergedConfigEntry := xmaps.Clone(vvv)
// Merge in the root value.
maps.MergeParams(mergedConfigEntry, rootv.(maps.Params))
mergedConfig.Set(kk, mergedConfigEntry)
default:
// Apply new values to the root.
differentRootKeys = append(differentRootKeys, "")
}
}
} else {
// Apply new values to the root.
differentRootKeys = append(differentRootKeys, "")
}
}
differentRootKeys = helpers.UniqueStringsSorted(differentRootKeys)
if len(differentRootKeys) == 0 {
langConfigMap[k] = all
continue
}
// Create a copy of the complete config and replace the root keys with the language specific ones.
clone := all.cloneForLang()
if err := decodeConfigFromParams(fs, bcfg, mergedConfig, clone, differentRootKeys); err != nil {
return nil, fmt.Errorf("failed to decode config for language %q: %w", k, err)
}
if err := clone.CompileConfig(); err != nil {
return nil, err
}
langConfigMap[k] = clone
case maps.ParamsMergeStrategy:
default:
panic(fmt.Sprintf("unknown type in languages config: %T", v))
}
}
var languages langs.Languages
defaultContentLanguage := all.DefaultContentLanguage
for k, v := range langConfigMap {
languageConf := v.Languages[k]
language, err := langs.NewLanguage(k, defaultContentLanguage, v.TimeZone, languageConf)
if err != nil {
return nil, err
}
languages = append(languages, language)
}
// Sort the sites by language weight (if set) or lang.
sort.Slice(languages, func(i, j int) bool {
li := languages[i]
lj := languages[j]
if li.Weight != lj.Weight {
return li.Weight < lj.Weight
}
return li.Lang < lj.Lang
})
for _, l := range languages {
langConfigs = append(langConfigs, langConfigMap[l.Lang])
}
var languagesDefaultFirst langs.Languages
for _, l := range languages {
if l.Lang == defaultContentLanguage {
languagesDefaultFirst = append(languagesDefaultFirst, l)
}
}
for _, l := range languages {
if l.Lang != defaultContentLanguage {
languagesDefaultFirst = append(languagesDefaultFirst, l)
}
}
bcfg.PublishDir = all.PublishDir
res.BaseConfig = bcfg
cm := &Configs{
Base: all,
LanguageConfigMap: langConfigMap,
LanguageConfigSlice: langConfigs,
LoadingInfo: res,
IsMultihost: isMultiHost,
Languages: languages,
LanguagesDefaultFirst: languagesDefaultFirst,
}
return cm, nil
}
func decodeConfigFromParams(fs afero.Fs, bcfg config.BaseConfig, p config.Provider, target *Config, keys []string) error {
var decoderSetups []decodeWeight
if len(keys) == 0 {
for _, v := range allDecoderSetups {
decoderSetups = append(decoderSetups, v)
}
} else {
for _, key := range keys {
if v, found := allDecoderSetups[key]; found {
decoderSetups = append(decoderSetups, v)
} else {
return fmt.Errorf("unknown config key %q", key)
}
}
}
// Sort them to get the dependency order right.
sort.Slice(decoderSetups, func(i, j int) bool {
ki, kj := decoderSetups[i], decoderSetups[j]
if ki.weight == kj.weight {
return ki.key < kj.key
}
return ki.weight < kj.weight
})
for _, v := range decoderSetups {
p := decodeConfig{p: p, c: target, fs: fs, bcfg: bcfg}
if err := v.decode(v, p); err != nil {
return fmt.Errorf("failed to decode %q: %w", v.key, err)
}
}
return nil
}
func createDefaultOutputFormats(allFormats output.Formats) map[string][]string {
if len(allFormats) == 0 {
panic("no output formats")
}
rssOut, rssFound := allFormats.GetByName(output.RSSFormat.Name)
htmlOut, _ := allFormats.GetByName(output.HTMLFormat.Name)
defaultListTypes := []string{htmlOut.Name}
if rssFound {
defaultListTypes = append(defaultListTypes, rssOut.Name)
}
m := map[string][]string{
page.KindPage: {htmlOut.Name},
page.KindHome: defaultListTypes,
page.KindSection: defaultListTypes,
page.KindTerm: defaultListTypes,
page.KindTaxonomy: defaultListTypes,
}
// May be disabled
if rssFound {
m["rss"] = []string{rssOut.Name}
}
return m
}

View File

@ -0,0 +1,325 @@
// Copyright 2023 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package allconfig
import (
"fmt"
"strings"
"github.com/gohugoio/hugo/cache/filecache"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/config/privacy"
"github.com/gohugoio/hugo/config/security"
"github.com/gohugoio/hugo/config/services"
"github.com/gohugoio/hugo/deploy"
"github.com/gohugoio/hugo/langs"
"github.com/gohugoio/hugo/markup/markup_config"
"github.com/gohugoio/hugo/media"
"github.com/gohugoio/hugo/minifiers"
"github.com/gohugoio/hugo/modules"
"github.com/gohugoio/hugo/navigation"
"github.com/gohugoio/hugo/output"
"github.com/gohugoio/hugo/related"
"github.com/gohugoio/hugo/resources/images"
"github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/resources/page/pagemeta"
"github.com/mitchellh/mapstructure"
"github.com/spf13/afero"
"github.com/spf13/cast"
)
type decodeConfig struct {
p config.Provider
c *Config
fs afero.Fs
bcfg config.BaseConfig
}
type decodeWeight struct {
key string
decode func(decodeWeight, decodeConfig) error
getCompiler func(c *Config) configCompiler
weight int
}
var allDecoderSetups = map[string]decodeWeight{
"": {
key: "",
weight: -100, // Always first.
decode: func(d decodeWeight, p decodeConfig) error {
return mapstructure.WeakDecode(p.p.Get(""), &p.c.RootConfig)
},
},
"imaging": {
key: "imaging",
decode: func(d decodeWeight, p decodeConfig) error {
var err error
p.c.Imaging, err = images.DecodeConfig(p.p.GetStringMap(d.key))
return err
},
},
"caches": {
key: "caches",
decode: func(d decodeWeight, p decodeConfig) error {
var err error
p.c.Caches, err = filecache.DecodeConfig(p.fs, p.bcfg, p.p.GetStringMap(d.key))
if p.c.IgnoreCache {
// Set MaxAge in all caches to 0.
for k, cache := range p.c.Caches {
cache.MaxAge = 0
p.c.Caches[k] = cache
}
}
return err
},
},
"build": {
key: "build",
decode: func(d decodeWeight, p decodeConfig) error {
p.c.Build = config.DecodeBuildConfig(p.p)
return nil
},
},
"frontmatter": {
key: "frontmatter",
decode: func(d decodeWeight, p decodeConfig) error {
var err error
p.c.Frontmatter, err = pagemeta.DecodeFrontMatterConfig(p.p)
return err
},
},
"markup": {
key: "markup",
decode: func(d decodeWeight, p decodeConfig) error {
var err error
p.c.Markup, err = markup_config.Decode(p.p)
return err
},
},
"server": {
key: "server",
decode: func(d decodeWeight, p decodeConfig) error {
var err error
p.c.Server, err = config.DecodeServer(p.p)
return err
},
getCompiler: func(c *Config) configCompiler {
return &c.Server
},
},
"minify": {
key: "minify",
decode: func(d decodeWeight, p decodeConfig) error {
var err error
p.c.Minify, err = minifiers.DecodeConfig(p.p.Get(d.key))
return err
},
},
"mediaTypes": {
key: "mediaTypes",
decode: func(d decodeWeight, p decodeConfig) error {
var err error
p.c.MediaTypes, err = media.DecodeTypes(p.p.GetStringMap(d.key))
return err
},
},
"outputs": {
key: "outputs",
decode: func(d decodeWeight, p decodeConfig) error {
defaults := createDefaultOutputFormats(p.c.OutputFormats.Config)
m := p.p.GetStringMap("outputs")
p.c.Outputs = make(map[string][]string)
for k, v := range m {
s := types.ToStringSlicePreserveString(v)
for i, v := range s {
s[i] = strings.ToLower(v)
}
p.c.Outputs[k] = s
}
// Apply defaults.
for k, v := range defaults {
if _, found := p.c.Outputs[k]; !found {
p.c.Outputs[k] = v
}
}
return nil
},
},
"outputFormats": {
key: "outputFormats",
decode: func(d decodeWeight, p decodeConfig) error {
var err error
p.c.OutputFormats, err = output.DecodeConfig(p.c.MediaTypes.Config, p.p.Get(d.key))
return err
},
},
"params": {
key: "params",
decode: func(d decodeWeight, p decodeConfig) error {
p.c.Params = maps.CleanConfigStringMap(p.p.GetStringMap("params"))
if p.c.Params == nil {
p.c.Params = make(map[string]any)
}
// Before Hugo 0.112.0 this was configured via site Params.
if mainSections, found := p.c.Params["mainsections"]; found {
p.c.MainSections = types.ToStringSlicePreserveString(mainSections)
}
return nil
},
},
"module": {
key: "module",
decode: func(d decodeWeight, p decodeConfig) error {
var err error
p.c.Module, err = modules.DecodeConfig(p.p)
return err
},
},
"permalinks": {
key: "permalinks",
decode: func(d decodeWeight, p decodeConfig) error {
p.c.Permalinks = maps.CleanConfigStringMapString(p.p.GetStringMapString(d.key))
return nil
},
},
"sitemap": {
key: "sitemap",
decode: func(d decodeWeight, p decodeConfig) error {
var err error
p.c.Sitemap, err = config.DecodeSitemap(config.SitemapConfig{Priority: -1, Filename: "sitemap.xml"}, p.p.GetStringMap(d.key))
return err
},
},
"taxonomies": {
key: "taxonomies",
decode: func(d decodeWeight, p decodeConfig) error {
p.c.Taxonomies = maps.CleanConfigStringMapString(p.p.GetStringMapString(d.key))
return nil
},
},
"related": {
key: "related",
weight: 100, // This needs to be decoded after taxonomies.
decode: func(d decodeWeight, p decodeConfig) error {
if p.p.IsSet(d.key) {
var err error
p.c.Related, err = related.DecodeConfig(p.p.GetParams(d.key))
if err != nil {
return fmt.Errorf("failed to decode related config: %w", err)
}
} else {
p.c.Related = related.DefaultConfig
if _, found := p.c.Taxonomies["tag"]; found {
p.c.Related.Add(related.IndexConfig{Name: "tags", Weight: 80})
}
}
return nil
},
},
"languages": {
key: "languages",
decode: func(d decodeWeight, p decodeConfig) error {
var err error
p.c.Languages, err = langs.DecodeConfig(p.p.GetStringMap(d.key))
return err
},
},
"cascade": {
key: "cascade",
decode: func(d decodeWeight, p decodeConfig) error {
var err error
p.c.Cascade, err = page.DecodeCascadeConfig(p.p.Get(d.key))
return err
},
},
"menus": {
key: "menus",
decode: func(d decodeWeight, p decodeConfig) error {
var err error
p.c.Menus, err = navigation.DecodeConfig(p.p.Get(d.key))
return err
},
},
"privacy": {
key: "privacy",
decode: func(d decodeWeight, p decodeConfig) error {
var err error
p.c.Privacy, err = privacy.DecodeConfig(p.p)
return err
},
},
"security": {
key: "security",
decode: func(d decodeWeight, p decodeConfig) error {
var err error
p.c.Security, err = security.DecodeConfig(p.p)
return err
},
},
"services": {
key: "services",
decode: func(d decodeWeight, p decodeConfig) error {
var err error
p.c.Services, err = services.DecodeConfig(p.p)
return err
},
},
"deployment": {
key: "deployment",
decode: func(d decodeWeight, p decodeConfig) error {
var err error
p.c.Deployment, err = deploy.DecodeConfig(p.p)
return err
},
},
"author": {
key: "author",
decode: func(d decodeWeight, p decodeConfig) error {
p.c.Author = p.p.GetStringMap(d.key)
return nil
},
},
"social": {
key: "social",
decode: func(d decodeWeight, p decodeConfig) error {
p.c.Social = p.p.GetStringMapString(d.key)
return nil
},
},
"uglyurls": {
key: "uglyurls",
decode: func(d decodeWeight, p decodeConfig) error {
v := p.p.Get(d.key)
switch vv := v.(type) {
case bool:
p.c.UglyURLs = vv
case string:
p.c.UglyURLs = vv == "true"
default:
p.c.UglyURLs = cast.ToStringMapBool(v)
}
return nil
},
},
"internal": {
key: "internal",
decode: func(d decodeWeight, p decodeConfig) error {
return mapstructure.WeakDecode(p.p.GetStringMap(d.key), &p.c.Internal)
},
},
}

View File

@ -0,0 +1,216 @@
// Copyright 2023 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package allconfig
import (
"time"
"github.com/gohugoio/hugo/common/urls"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/langs"
)
type ConfigLanguage struct {
config *Config
baseConfig config.BaseConfig
m *Configs
language *langs.Language
}
func (c ConfigLanguage) Language() *langs.Language {
return c.language
}
func (c ConfigLanguage) Languages() langs.Languages {
return c.m.Languages
}
func (c ConfigLanguage) LanguagesDefaultFirst() langs.Languages {
return c.m.LanguagesDefaultFirst
}
func (c ConfigLanguage) BaseURL() urls.BaseURL {
return c.config.C.BaseURL
}
func (c ConfigLanguage) BaseURLLiveReload() urls.BaseURL {
return c.config.C.BaseURLLiveReload
}
func (c ConfigLanguage) Environment() string {
return c.config.Environment
}
func (c ConfigLanguage) IsMultihost() bool {
return c.m.IsMultihost
}
func (c ConfigLanguage) IsMultiLingual() bool {
return len(c.m.Languages) > 1
}
func (c ConfigLanguage) TemplateMetrics() bool {
return c.config.TemplateMetrics
}
func (c ConfigLanguage) TemplateMetricsHints() bool {
return c.config.TemplateMetricsHints
}
func (c ConfigLanguage) IsLangDisabled(lang string) bool {
return c.config.C.DisabledLanguages[lang]
}
func (c ConfigLanguage) IgnoredErrors() map[string]bool {
return c.config.C.IgnoredErrors
}
func (c ConfigLanguage) NoBuildLock() bool {
return c.config.NoBuildLock
}
func (c ConfigLanguage) NewContentEditor() string {
return c.config.NewContentEditor
}
func (c ConfigLanguage) Timeout() time.Duration {
return c.config.C.Timeout
}
func (c ConfigLanguage) BaseConfig() config.BaseConfig {
return c.baseConfig
}
func (c ConfigLanguage) Dirs() config.CommonDirs {
return c.config.CommonDirs
}
func (c ConfigLanguage) DirsBase() config.CommonDirs {
return c.m.Base.CommonDirs
}
func (c ConfigLanguage) Quiet() bool {
return c.m.Base.Internal.Quiet
}
// GetConfigSection is mostly used in tests. The switch statement isn't complete, but what's in use.
func (c ConfigLanguage) GetConfigSection(s string) any {
switch s {
case "security":
return c.config.Security
case "build":
return c.config.Build
case "frontmatter":
return c.config.Frontmatter
case "caches":
return c.config.Caches
case "markup":
return c.config.Markup
case "mediaTypes":
return c.config.MediaTypes.Config
case "outputFormats":
return c.config.OutputFormats.Config
case "permalinks":
return c.config.Permalinks
case "minify":
return c.config.Minify
case "activeModules":
return c.m.Modules
case "deployment":
return c.config.Deployment
default:
panic("not implemented: " + s)
}
}
func (c ConfigLanguage) GetConfig() any {
return c.config
}
func (c ConfigLanguage) CanonifyURLs() bool {
return c.config.CanonifyURLs
}
func (c ConfigLanguage) IsUglyURLs(section string) bool {
return c.config.C.IsUglyURLSection(section)
}
func (c ConfigLanguage) IgnoreFile(s string) bool {
return c.config.C.IgnoreFile(s)
}
func (c ConfigLanguage) DisablePathToLower() bool {
return c.config.DisablePathToLower
}
func (c ConfigLanguage) RemovePathAccents() bool {
return c.config.RemovePathAccents
}
func (c ConfigLanguage) DefaultContentLanguage() string {
return c.config.DefaultContentLanguage
}
func (c ConfigLanguage) DefaultContentLanguageInSubdir() bool {
return c.config.DefaultContentLanguageInSubdir
}
func (c ConfigLanguage) SummaryLength() int {
return c.config.SummaryLength
}
func (c ConfigLanguage) BuildExpired() bool {
return c.config.BuildExpired
}
func (c ConfigLanguage) BuildFuture() bool {
return c.config.BuildFuture
}
func (c ConfigLanguage) BuildDrafts() bool {
return c.config.BuildDrafts
}
func (c ConfigLanguage) Running() bool {
return c.config.Internal.Running
}
func (c ConfigLanguage) PrintUnusedTemplates() bool {
return c.config.PrintUnusedTemplates
}
func (c ConfigLanguage) EnableMissingTranslationPlaceholders() bool {
return c.config.EnableMissingTranslationPlaceholders
}
func (c ConfigLanguage) LogI18nWarnings() bool {
return c.config.LogI18nWarnings
}
func (c ConfigLanguage) CreateTitle(s string) string {
return c.config.C.CreateTitle(s)
}
func (c ConfigLanguage) Paginate() int {
return c.config.Paginate
}
func (c ConfigLanguage) PaginatePath() string {
return c.config.PaginatePath
}
func (c ConfigLanguage) StaticDirs() []string {
return c.config.staticDirs()
}

View File

@ -0,0 +1,71 @@
package allconfig_test
import (
"path/filepath"
"testing"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/config/allconfig"
"github.com/gohugoio/hugo/hugolib"
)
func TestDirsMount(t *testing.T) {
files := `
-- hugo.toml --
baseURL = "https://example.com"
disableKinds = ["taxonomy", "term"]
[languages]
[languages.en]
weight = 1
[languages.sv]
weight = 2
[[module.mounts]]
source = 'content/en'
target = 'content'
lang = 'en'
[[module.mounts]]
source = 'content/sv'
target = 'content'
lang = 'sv'
-- content/en/p1.md --
---
title: "p1"
---
-- content/sv/p1.md --
---
title: "p1"
---
-- layouts/_default/single.html --
Title: {{ .Title }}
`
b := hugolib.NewIntegrationTestBuilder(
hugolib.IntegrationTestConfig{T: t, TxtarString: files},
).Build()
//b.AssertFileContent("public/p1/index.html", "Title: p1")
sites := b.H.Sites
b.Assert(len(sites), qt.Equals, 2)
configs := b.H.Configs
mods := configs.Modules
b.Assert(len(mods), qt.Equals, 1)
mod := mods[0]
b.Assert(mod.Mounts(), qt.HasLen, 8)
enConcp := sites[0].Conf
enConf := enConcp.GetConfig().(*allconfig.Config)
b.Assert(enConcp.BaseURL().String(), qt.Equals, "https://example.com")
modConf := enConf.Module
b.Assert(modConf.Mounts, qt.HasLen, 2)
b.Assert(modConf.Mounts[0].Source, qt.Equals, filepath.FromSlash("content/en"))
b.Assert(modConf.Mounts[0].Target, qt.Equals, "content")
b.Assert(modConf.Mounts[0].Lang, qt.Equals, "en")
b.Assert(modConf.Mounts[1].Source, qt.Equals, filepath.FromSlash("content/sv"))
b.Assert(modConf.Mounts[1].Target, qt.Equals, "content")
b.Assert(modConf.Mounts[1].Lang, qt.Equals, "sv")
}

559
config/allconfig/load.go Normal file
View File

@ -0,0 +1,559 @@
// Copyright 2023 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package allconfig contains the full configuration for Hugo.
package allconfig
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/gobwas/glob"
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/hexec"
"github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/helpers"
hglob "github.com/gohugoio/hugo/hugofs/glob"
"github.com/gohugoio/hugo/modules"
"github.com/gohugoio/hugo/parser/metadecoders"
"github.com/spf13/afero"
)
var ErrNoConfigFile = errors.New("Unable to locate config file or config directory. Perhaps you need to create a new site.\n Run `hugo help new` for details.\n")
func LoadConfig(d ConfigSourceDescriptor) (*Configs, error) {
if len(d.Environ) == 0 && !hugo.IsRunningAsTest() {
d.Environ = os.Environ()
}
l := &configLoader{ConfigSourceDescriptor: d, cfg: config.New()}
// Make sure we always do this, even in error situations,
// as we have commands (e.g. "hugo mod init") that will
// use a partial configuration to do its job.
defer l.deleteMergeStrategies()
res, _, err := l.loadConfigMain(d)
if err != nil {
return nil, fmt.Errorf("failed to load config: %w", err)
}
configs, err := FromLoadConfigResult(d.Fs, res)
if err != nil {
return nil, fmt.Errorf("failed to create config from result: %w", err)
}
moduleConfig, modulesClient, err := l.loadModules(configs)
if err != nil {
return nil, fmt.Errorf("failed to load modules: %w", err)
}
if len(l.ModulesConfigFiles) > 0 {
// Config merged in from modules.
// Re-read the config.
configs, err = FromLoadConfigResult(d.Fs, res)
if err != nil {
return nil, fmt.Errorf("failed to create config: %w", err)
}
}
configs.Modules = moduleConfig.ActiveModules
configs.ModulesClient = modulesClient
if err := configs.Init(); err != nil {
return nil, fmt.Errorf("failed to init config: %w", err)
}
return configs, nil
}
// ConfigSourceDescriptor describes where to find the config (e.g. config.toml etc.).
type ConfigSourceDescriptor struct {
Fs afero.Fs
Logger loggers.Logger
// Config received from the command line.
// These will override any config file settings.
Flags config.Provider
// Path to the config file to use, e.g. /my/project/config.toml
Filename string
// The (optional) directory for additional configuration files.
ConfigDir string
// production, development
Environment string
// Defaults to os.Environ if not set.
Environ []string
}
func (d ConfigSourceDescriptor) configFilenames() []string {
if d.Filename == "" {
return nil
}
return strings.Split(d.Filename, ",")
}
type configLoader struct {
cfg config.Provider
BaseConfig config.BaseConfig
ConfigSourceDescriptor
// collected
ModulesConfig modules.ModulesConfig
ModulesConfigFiles []string
}
// Handle some legacy values.
func (l configLoader) applyConfigAliases() error {
aliases := []types.KeyValueStr{{Key: "taxonomies", Value: "indexes"}}
for _, alias := range aliases {
if l.cfg.IsSet(alias.Key) {
vv := l.cfg.Get(alias.Key)
l.cfg.Set(alias.Value, vv)
}
}
return nil
}
func (l configLoader) applyDefaultConfig() error {
defaultSettings := maps.Params{
"baseURL": "",
"cleanDestinationDir": false,
"watch": false,
"contentDir": "content",
"resourceDir": "resources",
"publishDir": "public",
"publishDirOrig": "public",
"themesDir": "themes",
"assetDir": "assets",
"layoutDir": "layouts",
"i18nDir": "i18n",
"dataDir": "data",
"archetypeDir": "archetypes",
"configDir": "config",
"staticDir": "static",
"buildDrafts": false,
"buildFuture": false,
"buildExpired": false,
"params": maps.Params{},
"environment": hugo.EnvironmentProduction,
"uglyURLs": false,
"verbose": false,
"ignoreCache": false,
"canonifyURLs": false,
"relativeURLs": false,
"removePathAccents": false,
"titleCaseStyle": "AP",
"taxonomies": maps.Params{"tag": "tags", "category": "categories"},
"permalinks": maps.Params{},
"sitemap": maps.Params{"priority": -1, "filename": "sitemap.xml"},
"menus": maps.Params{},
"disableLiveReload": false,
"pluralizeListTitles": true,
"forceSyncStatic": false,
"footnoteAnchorPrefix": "",
"footnoteReturnLinkContents": "",
"newContentEditor": "",
"paginate": 10,
"paginatePath": "page",
"summaryLength": 70,
"rssLimit": -1,
"sectionPagesMenu": "",
"disablePathToLower": false,
"hasCJKLanguage": false,
"enableEmoji": false,
"defaultContentLanguage": "en",
"defaultContentLanguageInSubdir": false,
"enableMissingTranslationPlaceholders": false,
"enableGitInfo": false,
"ignoreFiles": make([]string, 0),
"disableAliases": false,
"debug": false,
"disableFastRender": false,
"timeout": "30s",
"timeZone": "",
"enableInlineShortcodes": false,
}
l.cfg.SetDefaults(defaultSettings)
return nil
}
func (l configLoader) normalizeCfg(cfg config.Provider) error {
minify := cfg.Get("minify")
if b, ok := minify.(bool); ok && b {
cfg.Set("minify", maps.Params{"minifyOutput": true})
}
// Simplify later merge.
languages := cfg.GetStringMap("languages")
for _, v := range languages {
switch m := v.(type) {
case maps.Params:
// params have merge strategy deep by default.
// The languages config key has strategy none by default.
// This means that if these two sections does not exist on the left side,
// they will not get merged in, so just create some empty maps.
if _, ok := m["params"]; !ok {
m["params"] = maps.Params{}
}
}
}
return nil
}
func (l configLoader) cleanExternalConfig(cfg config.Provider) error {
if cfg.IsSet("internal") {
cfg.Set("internal", nil)
}
return nil
}
func (l configLoader) applyFlagsOverrides(cfg config.Provider) error {
for _, k := range cfg.Keys() {
l.cfg.Set(k, cfg.Get(k))
}
return nil
}
func (l configLoader) applyOsEnvOverrides(environ []string) error {
if len(environ) == 0 {
return nil
}
const delim = "__env__delim"
// Extract all that start with the HUGO prefix.
// The delimiter is the following rune, usually "_".
const hugoEnvPrefix = "HUGO"
var hugoEnv []types.KeyValueStr
for _, v := range environ {
key, val := config.SplitEnvVar(v)
if strings.HasPrefix(key, hugoEnvPrefix) {
delimiterAndKey := strings.TrimPrefix(key, hugoEnvPrefix)
if len(delimiterAndKey) < 2 {
continue
}
// Allow delimiters to be case sensitive.
// It turns out there isn't that many allowed special
// chars in environment variables when used in Bash and similar,
// so variables on the form HUGOxPARAMSxFOO=bar is one option.
key := strings.ReplaceAll(delimiterAndKey[1:], delimiterAndKey[:1], delim)
key = strings.ToLower(key)
hugoEnv = append(hugoEnv, types.KeyValueStr{
Key: key,
Value: val,
})
}
}
for _, env := range hugoEnv {
existing, nestedKey, owner, err := maps.GetNestedParamFn(env.Key, delim, l.cfg.Get)
if err != nil {
return err
}
if existing != nil {
val, err := metadecoders.Default.UnmarshalStringTo(env.Value, existing)
if err != nil {
continue
}
if owner != nil {
owner[nestedKey] = val
} else {
l.cfg.Set(env.Key, val)
}
} else if nestedKey != "" {
owner[nestedKey] = env.Value
} else {
// The container does not exist yet.
l.cfg.Set(strings.ReplaceAll(env.Key, delim, "."), env.Value)
}
}
return nil
}
func (l *configLoader) loadConfigMain(d ConfigSourceDescriptor) (config.LoadConfigResult, modules.ModulesConfig, error) {
var res config.LoadConfigResult
if d.Flags != nil {
if err := l.normalizeCfg(d.Flags); err != nil {
return res, l.ModulesConfig, err
}
}
if d.Fs == nil {
return res, l.ModulesConfig, errors.New("no filesystem provided")
}
if d.Flags != nil {
if err := l.applyFlagsOverrides(d.Flags); err != nil {
return res, l.ModulesConfig, err
}
workingDir := filepath.Clean(l.cfg.GetString("workingDir"))
l.BaseConfig = config.BaseConfig{
WorkingDir: workingDir,
ThemesDir: paths.AbsPathify(workingDir, l.cfg.GetString("themesDir")),
}
}
names := d.configFilenames()
if names != nil {
for _, name := range names {
var filename string
filename, err := l.loadConfig(name)
if err == nil {
res.ConfigFiles = append(res.ConfigFiles, filename)
} else if err != ErrNoConfigFile {
return res, l.ModulesConfig, l.wrapFileError(err, filename)
}
}
} else {
for _, name := range config.DefaultConfigNames {
var filename string
filename, err := l.loadConfig(name)
if err == nil {
res.ConfigFiles = append(res.ConfigFiles, filename)
break
} else if err != ErrNoConfigFile {
return res, l.ModulesConfig, l.wrapFileError(err, filename)
}
}
}
if d.ConfigDir != "" {
absConfigDir := paths.AbsPathify(l.BaseConfig.WorkingDir, d.ConfigDir)
dcfg, dirnames, err := config.LoadConfigFromDir(l.Fs, absConfigDir, l.Environment)
if err == nil {
if len(dirnames) > 0 {
if err := l.normalizeCfg(dcfg); err != nil {
return res, l.ModulesConfig, err
}
if err := l.cleanExternalConfig(dcfg); err != nil {
return res, l.ModulesConfig, err
}
l.cfg.Set("", dcfg.Get(""))
res.ConfigFiles = append(res.ConfigFiles, dirnames...)
}
} else if err != ErrNoConfigFile {
if len(dirnames) > 0 {
return res, l.ModulesConfig, l.wrapFileError(err, dirnames[0])
}
return res, l.ModulesConfig, err
}
}
res.Cfg = l.cfg
if err := l.applyDefaultConfig(); err != nil {
return res, l.ModulesConfig, err
}
// Some settings are used before we're done collecting all settings,
// so apply OS environment both before and after.
if err := l.applyOsEnvOverrides(d.Environ); err != nil {
return res, l.ModulesConfig, err
}
workingDir := filepath.Clean(l.cfg.GetString("workingDir"))
l.BaseConfig = config.BaseConfig{
WorkingDir: workingDir,
CacheDir: l.cfg.GetString("cacheDir"),
ThemesDir: paths.AbsPathify(workingDir, l.cfg.GetString("themesDir")),
}
var err error
l.BaseConfig.CacheDir, err = helpers.GetCacheDir(l.Fs, l.BaseConfig.CacheDir)
if err != nil {
return res, l.ModulesConfig, err
}
res.BaseConfig = l.BaseConfig
l.cfg.SetDefaultMergeStrategy()
res.ConfigFiles = append(res.ConfigFiles, l.ModulesConfigFiles...)
if d.Flags != nil {
if err := l.applyFlagsOverrides(d.Flags); err != nil {
return res, l.ModulesConfig, err
}
}
if err := l.applyOsEnvOverrides(d.Environ); err != nil {
return res, l.ModulesConfig, err
}
if err = l.applyConfigAliases(); err != nil {
return res, l.ModulesConfig, err
}
return res, l.ModulesConfig, err
}
func (l *configLoader) loadModules(configs *Configs) (modules.ModulesConfig, *modules.Client, error) {
bcfg := configs.LoadingInfo.BaseConfig
conf := configs.Base
workingDir := bcfg.WorkingDir
themesDir := bcfg.ThemesDir
cfg := configs.LoadingInfo.Cfg
var ignoreVendor glob.Glob
if s := conf.IgnoreVendorPaths; s != "" {
ignoreVendor, _ = hglob.GetGlob(hglob.NormalizePath(s))
}
ex := hexec.New(conf.Security)
hook := func(m *modules.ModulesConfig) error {
for _, tc := range m.ActiveModules {
if len(tc.ConfigFilenames()) > 0 {
if tc.Watch() {
l.ModulesConfigFiles = append(l.ModulesConfigFiles, tc.ConfigFilenames()...)
}
// Merge in the theme config using the configured
// merge strategy.
cfg.Merge("", tc.Cfg().Get(""))
}
}
return nil
}
modulesClient := modules.NewClient(modules.ClientConfig{
Fs: l.Fs,
Logger: l.Logger,
Exec: ex,
HookBeforeFinalize: hook,
WorkingDir: workingDir,
ThemesDir: themesDir,
Environment: l.Environment,
CacheDir: conf.Caches.CacheDirModules(),
ModuleConfig: conf.Module,
IgnoreVendor: ignoreVendor,
})
moduleConfig, err := modulesClient.Collect()
// We want to watch these for changes and trigger rebuild on version
// changes etc.
if moduleConfig.GoModulesFilename != "" {
l.ModulesConfigFiles = append(l.ModulesConfigFiles, moduleConfig.GoModulesFilename)
}
if moduleConfig.GoWorkspaceFilename != "" {
l.ModulesConfigFiles = append(l.ModulesConfigFiles, moduleConfig.GoWorkspaceFilename)
}
return moduleConfig, modulesClient, err
}
func (l configLoader) loadConfig(configName string) (string, error) {
baseDir := l.BaseConfig.WorkingDir
var baseFilename string
if filepath.IsAbs(configName) {
baseFilename = configName
} else {
baseFilename = filepath.Join(baseDir, configName)
}
var filename string
if paths.ExtNoDelimiter(configName) != "" {
exists, _ := helpers.Exists(baseFilename, l.Fs)
if exists {
filename = baseFilename
}
} else {
for _, ext := range config.ValidConfigFileExtensions {
filenameToCheck := baseFilename + "." + ext
exists, _ := helpers.Exists(filenameToCheck, l.Fs)
if exists {
filename = filenameToCheck
break
}
}
}
if filename == "" {
return "", ErrNoConfigFile
}
m, err := config.FromFileToMap(l.Fs, filename)
if err != nil {
return filename, err
}
// Set overwrites keys of the same name, recursively.
l.cfg.Set("", m)
if err := l.normalizeCfg(l.cfg); err != nil {
return filename, err
}
if err := l.cleanExternalConfig(l.cfg); err != nil {
return filename, err
}
return filename, nil
}
func (l configLoader) deleteMergeStrategies() {
l.cfg.WalkParams(func(params ...maps.KeyParams) bool {
params[len(params)-1].Params.DeleteMergeStrategy()
return false
})
}
func (l configLoader) loadModulesConfig() (modules.Config, error) {
modConfig, err := modules.DecodeConfig(l.cfg)
if err != nil {
return modules.Config{}, err
}
return modConfig, nil
}
func (l configLoader) wrapFileError(err error, filename string) error {
fe := herrors.UnwrapFileError(err)
if fe != nil {
pos := fe.Position()
pos.Filename = filename
fe.UpdatePosition(pos)
return err
}
return herrors.NewFileErrorFromFile(err, filename, l.Fs, nil)
}

View File

@ -0,0 +1,67 @@
package allconfig
import (
"os"
"path/filepath"
"testing"
"github.com/spf13/afero"
)
func BenchmarkLoad(b *testing.B) {
tempDir := b.TempDir()
configFilename := filepath.Join(tempDir, "hugo.toml")
config := `
baseURL = "https://example.com"
defaultContentLanguage = 'en'
[module]
[[module.mounts]]
source = 'content/en'
target = 'content/en'
lang = 'en'
[[module.mounts]]
source = 'content/nn'
target = 'content/nn'
lang = 'nn'
[[module.mounts]]
source = 'content/no'
target = 'content/no'
lang = 'no'
[[module.mounts]]
source = 'content/sv'
target = 'content/sv'
lang = 'sv'
[[module.mounts]]
source = 'layouts'
target = 'layouts'
[languages]
[languages.en]
title = "English"
weight = 1
[languages.nn]
title = "Nynorsk"
weight = 2
[languages.no]
title = "Norsk"
weight = 3
[languages.sv]
title = "Svenska"
weight = 4
`
if err := os.WriteFile(configFilename, []byte(config), 0666); err != nil {
b.Fatal(err)
}
d := ConfigSourceDescriptor{
Fs: afero.NewOsFs(),
Filename: configFilename,
}
for i := 0; i < b.N; i++ {
_, err := LoadConfig(d)
if err != nil {
b.Fatal(err)
}
}
}

View File

@ -17,7 +17,6 @@ import (
"fmt"
"sort"
"strings"
"sync"
"github.com/gohugoio/hugo/common/types"
@ -25,16 +24,66 @@ import (
"github.com/gohugoio/hugo/common/herrors"
"github.com/mitchellh/mapstructure"
"github.com/spf13/cast"
jww "github.com/spf13/jwalterweatherman"
)
var DefaultBuild = Build{
type BaseConfig struct {
WorkingDir string
CacheDir string
ThemesDir string
PublishDir string
}
type CommonDirs struct {
// The directory where Hugo will look for themes.
ThemesDir string
// Where to put the generated files.
PublishDir string
// The directory to put the generated resources files. This directory should in most situations be considered temporary
// and not be committed to version control. But there may be cached content in here that you want to keep,
// e.g. resources/_gen/images for performance reasons or CSS built from SASS when your CI server doesn't have the full setup.
ResourceDir string
// The project root directory.
WorkingDir string
// The root directory for all cache files.
CacheDir string
// The content source directory.
// Deprecated: Use module mounts.
ContentDir string
// Deprecated: Use module mounts.
// The data source directory.
DataDir string
// Deprecated: Use module mounts.
// The layout source directory.
LayoutDir string
// Deprecated: Use module mounts.
// The i18n source directory.
I18nDir string
// Deprecated: Use module mounts.
// The archetypes source directory.
ArcheTypeDir string
// Deprecated: Use module mounts.
// The assets source directory.
AssetDir string
}
type LoadConfigResult struct {
Cfg Provider
ConfigFiles []string
BaseConfig BaseConfig
}
var DefaultBuild = BuildConfig{
UseResourceCacheWhen: "fallback",
WriteStats: false,
}
// Build holds some build related configuration.
type Build struct {
// BuildConfig holds some build related configuration.
type BuildConfig struct {
UseResourceCacheWhen string // never, fallback, always. Default is fallback
// When enabled, will collect and write a hugo_stats.json with some build
@ -46,7 +95,7 @@ type Build struct {
NoJSConfigInAssets bool
}
func (b Build) UseResourceCache(err error) bool {
func (b BuildConfig) UseResourceCache(err error) bool {
if b.UseResourceCacheWhen == "never" {
return false
}
@ -58,7 +107,7 @@ func (b Build) UseResourceCache(err error) bool {
return true
}
func DecodeBuild(cfg Provider) Build {
func DecodeBuildConfig(cfg Provider) BuildConfig {
m := cfg.GetStringMap("build")
b := DefaultBuild
if m == nil {
@ -79,28 +128,19 @@ func DecodeBuild(cfg Provider) Build {
return b
}
// Sitemap configures the sitemap to be generated.
type Sitemap struct {
// SitemapConfig configures the sitemap to be generated.
type SitemapConfig struct {
// The page change frequency.
ChangeFreq string
Priority float64
Filename string
// The priority of the page.
Priority float64
// The sitemap filename.
Filename string
}
func DecodeSitemap(prototype Sitemap, input map[string]any) Sitemap {
for key, value := range input {
switch key {
case "changefreq":
prototype.ChangeFreq = cast.ToString(value)
case "priority":
prototype.Priority = cast.ToFloat64(value)
case "filename":
prototype.Filename = cast.ToString(value)
default:
jww.WARN.Printf("Unknown Sitemap field: %s\n", key)
}
}
return prototype
func DecodeSitemap(prototype SitemapConfig, input map[string]any) (SitemapConfig, error) {
err := mapstructure.WeakDecode(input, &prototype)
return prototype, err
}
// Config for the dev server.
@ -108,25 +148,24 @@ type Server struct {
Headers []Headers
Redirects []Redirect
compiledInit sync.Once
compiledHeaders []glob.Glob
compiledRedirects []glob.Glob
}
func (s *Server) init() {
s.compiledInit.Do(func() {
for _, h := range s.Headers {
s.compiledHeaders = append(s.compiledHeaders, glob.MustCompile(h.For))
}
for _, r := range s.Redirects {
s.compiledRedirects = append(s.compiledRedirects, glob.MustCompile(r.From))
}
})
func (s *Server) CompileConfig() error {
if s.compiledHeaders != nil {
return nil
}
for _, h := range s.Headers {
s.compiledHeaders = append(s.compiledHeaders, glob.MustCompile(h.For))
}
for _, r := range s.Redirects {
s.compiledRedirects = append(s.compiledRedirects, glob.MustCompile(r.From))
}
return nil
}
func (s *Server) MatchHeaders(pattern string) []types.KeyValueStr {
s.init()
if s.compiledHeaders == nil {
return nil
}
@ -150,8 +189,6 @@ func (s *Server) MatchHeaders(pattern string) []types.KeyValueStr {
}
func (s *Server) MatchRedirect(pattern string) Redirect {
s.init()
if s.compiledRedirects == nil {
return Redirect{}
}
@ -195,14 +232,10 @@ func (r Redirect) IsZero() bool {
return r.From == ""
}
func DecodeServer(cfg Provider) (*Server, error) {
m := cfg.GetStringMap("server")
func DecodeServer(cfg Provider) (Server, error) {
s := &Server{}
if m == nil {
return s, nil
}
_ = mapstructure.WeakDecode(m, s)
_ = mapstructure.WeakDecode(cfg.GetStringMap("server"), s)
for i, redir := range s.Redirects {
// Get it in line with the Hugo server for OK responses.
@ -213,7 +246,7 @@ func DecodeServer(cfg Provider) (*Server, error) {
// There are some tricky infinite loop situations when dealing
// when the target does not have a trailing slash.
// This can certainly be handled better, but not time for that now.
return nil, fmt.Errorf("unsupported redirect to value %q in server config; currently this must be either a remote destination or a local folder, e.g. \"/blog/\" or \"/blog/index.html\"", redir.To)
return Server{}, fmt.Errorf("unsupported redirect to value %q in server config; currently this must be either a remote destination or a local folder, e.g. \"/blog/\" or \"/blog/index.html\"", redir.To)
}
}
s.Redirects[i] = redir
@ -231,5 +264,5 @@ func DecodeServer(cfg Provider) (*Server, error) {
}
return s, nil
return *s, nil
}

View File

@ -31,7 +31,7 @@ func TestBuild(t *testing.T) {
"useResourceCacheWhen": "always",
})
b := DecodeBuild(v)
b := DecodeBuildConfig(v)
c.Assert(b.UseResourceCacheWhen, qt.Equals, "always")
@ -39,7 +39,7 @@ func TestBuild(t *testing.T) {
"useResourceCacheWhen": "foo",
})
b = DecodeBuild(v)
b = DecodeBuildConfig(v)
c.Assert(b.UseResourceCacheWhen, qt.Equals, "fallback")
@ -91,6 +91,7 @@ status = 301
s, err := DecodeServer(cfg)
c.Assert(err, qt.IsNil)
c.Assert(s.CompileConfig(), qt.IsNil)
c.Assert(s.MatchHeaders("/foo.jpg"), qt.DeepEquals, []types.KeyValueStr{
{Key: "X-Content-Type-Options", Value: "nosniff"},

View File

@ -1,117 +0,0 @@
// Copyright 2021 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package config
import (
"github.com/gohugoio/hugo/common/maps"
)
// NewCompositeConfig creates a new composite Provider with a read-only base
// and a writeable layer.
func NewCompositeConfig(base, layer Provider) Provider {
return &compositeConfig{
base: base,
layer: layer,
}
}
// compositeConfig contains a read only config base with
// a possibly writeable config layer on top.
type compositeConfig struct {
base Provider
layer Provider
}
func (c *compositeConfig) GetBool(key string) bool {
if c.layer.IsSet(key) {
return c.layer.GetBool(key)
}
return c.base.GetBool(key)
}
func (c *compositeConfig) GetInt(key string) int {
if c.layer.IsSet(key) {
return c.layer.GetInt(key)
}
return c.base.GetInt(key)
}
func (c *compositeConfig) Merge(key string, value any) {
c.layer.Merge(key, value)
}
func (c *compositeConfig) GetParams(key string) maps.Params {
if c.layer.IsSet(key) {
return c.layer.GetParams(key)
}
return c.base.GetParams(key)
}
func (c *compositeConfig) GetStringMap(key string) map[string]any {
if c.layer.IsSet(key) {
return c.layer.GetStringMap(key)
}
return c.base.GetStringMap(key)
}
func (c *compositeConfig) GetStringMapString(key string) map[string]string {
if c.layer.IsSet(key) {
return c.layer.GetStringMapString(key)
}
return c.base.GetStringMapString(key)
}
func (c *compositeConfig) GetStringSlice(key string) []string {
if c.layer.IsSet(key) {
return c.layer.GetStringSlice(key)
}
return c.base.GetStringSlice(key)
}
func (c *compositeConfig) Get(key string) any {
if c.layer.IsSet(key) {
return c.layer.Get(key)
}
return c.base.Get(key)
}
func (c *compositeConfig) IsSet(key string) bool {
if c.layer.IsSet(key) {
return true
}
return c.base.IsSet(key)
}
func (c *compositeConfig) GetString(key string) string {
if c.layer.IsSet(key) {
return c.layer.GetString(key)
}
return c.base.GetString(key)
}
func (c *compositeConfig) Set(key string, value any) {
c.layer.Set(key, value)
}
func (c *compositeConfig) SetDefaults(params maps.Params) {
c.layer.SetDefaults(params)
}
func (c *compositeConfig) WalkParams(walkFn func(params ...KeyParams) bool) {
panic("not supported")
}
func (c *compositeConfig) SetDefaultMergeStrategy() {
panic("not supported")
}

View File

@ -57,6 +57,14 @@ func IsValidConfigFilename(filename string) bool {
return validConfigFileExtensionsMap[ext]
}
func FromTOMLConfigString(config string) Provider {
cfg, err := FromConfigString(config, "toml")
if err != nil {
panic(err)
}
return cfg
}
// FromConfigString creates a config from the given YAML, JSON or TOML config. This is useful in tests.
func FromConfigString(config, configType string) (Provider, error) {
m, err := readConfig(metadecoders.FormatFromString(configType), []byte(config))

View File

@ -14,10 +14,58 @@
package config
import (
"time"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/common/urls"
"github.com/gohugoio/hugo/langs"
)
// AllProvider is a sub set of all config settings.
type AllProvider interface {
Language() *langs.Language
Languages() langs.Languages
LanguagesDefaultFirst() langs.Languages
BaseURL() urls.BaseURL
BaseURLLiveReload() urls.BaseURL
Environment() string
IsMultihost() bool
IsMultiLingual() bool
NoBuildLock() bool
BaseConfig() BaseConfig
Dirs() CommonDirs
Quiet() bool
DirsBase() CommonDirs
GetConfigSection(string) any
GetConfig() any
CanonifyURLs() bool
DisablePathToLower() bool
RemovePathAccents() bool
IsUglyURLs(section string) bool
DefaultContentLanguage() string
DefaultContentLanguageInSubdir() bool
IsLangDisabled(string) bool
SummaryLength() int
Paginate() int
PaginatePath() string
BuildExpired() bool
BuildFuture() bool
BuildDrafts() bool
Running() bool
PrintUnusedTemplates() bool
EnableMissingTranslationPlaceholders() bool
TemplateMetrics() bool
TemplateMetricsHints() bool
LogI18nWarnings() bool
CreateTitle(s string) string
IgnoreFile(s string) bool
NewContentEditor() string
Timeout() time.Duration
StaticDirs() []string
IgnoredErrors() map[string]bool
}
// Provider provides the configuration settings for Hugo.
type Provider interface {
GetString(key string) string
@ -29,10 +77,11 @@ type Provider interface {
GetStringSlice(key string) []string
Get(key string) any
Set(key string, value any)
Keys() []string
Merge(key string, value any)
SetDefaults(params maps.Params)
SetDefaultMergeStrategy()
WalkParams(walkFn func(params ...KeyParams) bool)
WalkParams(walkFn func(params ...maps.KeyParams) bool)
IsSet(key string) bool
}
@ -44,22 +93,6 @@ func GetStringSlicePreserveString(cfg Provider, key string) []string {
return types.ToStringSlicePreserveString(sd)
}
// SetBaseTestDefaults provides some common config defaults used in tests.
func SetBaseTestDefaults(cfg Provider) Provider {
setIfNotSet(cfg, "baseURL", "https://example.org")
setIfNotSet(cfg, "resourceDir", "resources")
setIfNotSet(cfg, "contentDir", "content")
setIfNotSet(cfg, "dataDir", "data")
setIfNotSet(cfg, "i18nDir", "i18n")
setIfNotSet(cfg, "layoutDir", "layouts")
setIfNotSet(cfg, "assetDir", "assets")
setIfNotSet(cfg, "archetypeDir", "archetypes")
setIfNotSet(cfg, "publishDir", "public")
setIfNotSet(cfg, "workingDir", "")
setIfNotSet(cfg, "defaultContentLanguage", "en")
return cfg
}
func setIfNotSet(cfg Provider, key string, value any) {
if !cfg.IsSet(key) {
cfg.Set(key, value)

View File

@ -19,6 +19,8 @@ import (
"strings"
"sync"
xmaps "golang.org/x/exp/maps"
"github.com/spf13/cast"
"github.com/gohugoio/hugo/common/maps"
@ -75,11 +77,6 @@ func NewFrom(params maps.Params) Provider {
}
}
// NewWithTestDefaults is used in tests only.
func NewWithTestDefaults() Provider {
return SetBaseTestDefaults(New())
}
// defaultConfigProvider is a Provider backed by a map where all keys are lower case.
// All methods are thread safe.
type defaultConfigProvider struct {
@ -160,9 +157,9 @@ func (c *defaultConfigProvider) Set(k string, v any) {
k = strings.ToLower(k)
if k == "" {
if p, ok := maps.ToParamsAndPrepare(v); ok {
if p, err := maps.ToParamsAndPrepare(v); err == nil {
// Set the values directly in root.
c.root.Set(p)
maps.SetParams(c.root, p)
} else {
c.root[k] = v
}
@ -184,7 +181,7 @@ func (c *defaultConfigProvider) Set(k string, v any) {
if existing, found := m[key]; found {
if p1, ok := existing.(maps.Params); ok {
if p2, ok := v.(maps.Params); ok {
p1.Set(p2)
maps.SetParams(p1, p2)
return
}
}
@ -208,12 +205,6 @@ func (c *defaultConfigProvider) Merge(k string, v any) {
defer c.mu.Unlock()
k = strings.ToLower(k)
const (
languagesKey = "languages"
paramsKey = "params"
menusKey = "menus"
)
if k == "" {
rs, f := c.root.GetMergeStrategy()
if f && rs == maps.ParamsMergeStrategyNone {
@ -222,7 +213,7 @@ func (c *defaultConfigProvider) Merge(k string, v any) {
return
}
if p, ok := maps.ToParamsAndPrepare(v); ok {
if p, err := maps.ToParamsAndPrepare(v); err == nil {
// As there may be keys in p not in root, we need to handle
// those as a special case.
var keysToDelete []string
@ -230,49 +221,14 @@ func (c *defaultConfigProvider) Merge(k string, v any) {
if pp, ok := vv.(maps.Params); ok {
if pppi, ok := c.root[kk]; ok {
ppp := pppi.(maps.Params)
if kk == languagesKey {
// Languages is currently a special case.
// We may have languages with menus or params in the
// right map that is not present in the left map.
// With the default merge strategy those items will not
// be passed over.
var hasParams, hasMenus bool
for _, rv := range pp {
if lkp, ok := rv.(maps.Params); ok {
_, hasMenus = lkp[menusKey]
_, hasParams = lkp[paramsKey]
}
}
if hasMenus || hasParams {
for _, lv := range ppp {
if lkp, ok := lv.(maps.Params); ok {
if hasMenus {
if _, ok := lkp[menusKey]; !ok {
p := maps.Params{}
p.SetDefaultMergeStrategy(maps.ParamsMergeStrategyShallow)
lkp[menusKey] = p
}
}
if hasParams {
if _, ok := lkp[paramsKey]; !ok {
p := maps.Params{}
p.SetDefaultMergeStrategy(maps.ParamsMergeStrategyShallow)
lkp[paramsKey] = p
}
}
}
}
}
}
ppp.Merge(pp)
maps.MergeParamsWithStrategy("", ppp, pp)
} else {
// We need to use the default merge strategy for
// this key.
np := make(maps.Params)
strategy := c.determineMergeStrategy(KeyParams{Key: "", Params: c.root}, KeyParams{Key: kk, Params: np})
np.SetDefaultMergeStrategy(strategy)
np.Merge(pp)
strategy := c.determineMergeStrategy(maps.KeyParams{Key: "", Params: c.root}, maps.KeyParams{Key: kk, Params: np})
np.SetMergeStrategy(strategy)
maps.MergeParamsWithStrategy("", np, pp)
c.root[kk] = np
if np.IsZero() {
// Just keep it until merge is done.
@ -282,7 +238,7 @@ func (c *defaultConfigProvider) Merge(k string, v any) {
}
}
// Merge the rest.
c.root.MergeRoot(p)
maps.MergeParams(c.root, p)
for _, k := range keysToDelete {
delete(c.root, k)
}
@ -307,7 +263,7 @@ func (c *defaultConfigProvider) Merge(k string, v any) {
if existing, found := m[key]; found {
if p1, ok := existing.(maps.Params); ok {
if p2, ok := v.(maps.Params); ok {
p1.Merge(p2)
maps.MergeParamsWithStrategy("", p1, p2)
}
}
} else {
@ -315,9 +271,15 @@ func (c *defaultConfigProvider) Merge(k string, v any) {
}
}
func (c *defaultConfigProvider) WalkParams(walkFn func(params ...KeyParams) bool) {
var walk func(params ...KeyParams)
walk = func(params ...KeyParams) {
func (c *defaultConfigProvider) Keys() []string {
c.mu.RLock()
defer c.mu.RUnlock()
return xmaps.Keys(c.root)
}
func (c *defaultConfigProvider) WalkParams(walkFn func(params ...maps.KeyParams) bool) {
var walk func(params ...maps.KeyParams)
walk = func(params ...maps.KeyParams) {
if walkFn(params...) {
return
}
@ -325,17 +287,17 @@ func (c *defaultConfigProvider) WalkParams(walkFn func(params ...KeyParams) bool
i := len(params)
for k, v := range p1.Params {
if p2, ok := v.(maps.Params); ok {
paramsplus1 := make([]KeyParams, i+1)
paramsplus1 := make([]maps.KeyParams, i+1)
copy(paramsplus1, params)
paramsplus1[i] = KeyParams{Key: k, Params: p2}
paramsplus1[i] = maps.KeyParams{Key: k, Params: p2}
walk(paramsplus1...)
}
}
}
walk(KeyParams{Key: "", Params: c.root})
walk(maps.KeyParams{Key: "", Params: c.root})
}
func (c *defaultConfigProvider) determineMergeStrategy(params ...KeyParams) maps.ParamsMergeStrategy {
func (c *defaultConfigProvider) determineMergeStrategy(params ...maps.KeyParams) maps.ParamsMergeStrategy {
if len(params) == 0 {
return maps.ParamsMergeStrategyNone
}
@ -391,13 +353,8 @@ func (c *defaultConfigProvider) determineMergeStrategy(params ...KeyParams) maps
return strategy
}
type KeyParams struct {
Key string
Params maps.Params
}
func (c *defaultConfigProvider) SetDefaultMergeStrategy() {
c.WalkParams(func(params ...KeyParams) bool {
c.WalkParams(func(params ...maps.KeyParams) bool {
if len(params) == 0 {
return false
}
@ -409,7 +366,7 @@ func (c *defaultConfigProvider) SetDefaultMergeStrategy() {
}
strategy := c.determineMergeStrategy(params...)
if strategy != "" {
p.SetDefaultMergeStrategy(strategy)
p.SetMergeStrategy(strategy)
}
return false
})

76
config/namespace.go Normal file
View File

@ -0,0 +1,76 @@
// Copyright 2023 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package config
import (
"encoding/json"
"github.com/gohugoio/hugo/identity"
)
func DecodeNamespace[S, C any](configSource any, buildConfig func(any) (C, any, error)) (*ConfigNamespace[S, C], error) {
// Calculate the hash of the input (not including any defaults applied later).
// This allows us to introduce new config options without breaking the hash.
h := identity.HashString(configSource)
// Build the config
c, ext, err := buildConfig(configSource)
if err != nil {
return nil, err
}
if ext == nil {
ext = configSource
}
if ext == nil {
panic("ext is nil")
}
ns := &ConfigNamespace[S, C]{
SourceStructure: ext,
SourceHash: h,
Config: c,
}
return ns, nil
}
// ConfigNamespace holds a Hugo configuration namespace.
// The construct looks a little odd, but it's built to make the configuration elements
// both self-documenting and contained in a common structure.
type ConfigNamespace[S, C any] struct {
// SourceStructure represents the source configuration with any defaults applied.
// This is used for documentation and printing of the configuration setup to the user.
SourceStructure any
// SourceHash is a hash of the source configuration before any defaults gets applied.
SourceHash string
// Config is the final configuration as used by Hugo.
Config C
}
// MarshalJSON marshals the source structure.
func (ns *ConfigNamespace[S, C]) MarshalJSON() ([]byte, error) {
return json.Marshal(ns.SourceStructure)
}
// Signature returns the signature of the source structure.
// Note that this is for documentation purposes only and SourceStructure may not always be cast to S (it's usually just a map).
func (ns *ConfigNamespace[S, C]) Signature() S {
var s S
return s
}

68
config/namespace_test.go Normal file
View File

@ -0,0 +1,68 @@
// Copyright 2023 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package config
import (
"strings"
"testing"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/common/maps"
"github.com/mitchellh/mapstructure"
)
func TestNamespace(t *testing.T) {
c := qt.New(t)
c.Assert(true, qt.Equals, true)
//ns, err := config.DecodeNamespace[map[string]DocsMediaTypeConfig](in, defaultMediaTypesConfig, buildConfig)
ns, err := DecodeNamespace[[]*tstNsExt](
map[string]interface{}{"foo": "bar"},
func(v any) (*tstNsExt, any, error) {
t := &tstNsExt{}
m, err := maps.ToStringMapE(v)
if err != nil {
return nil, nil, err
}
return t, nil, mapstructure.WeakDecode(m, t)
},
)
c.Assert(err, qt.IsNil)
c.Assert(ns, qt.Not(qt.IsNil))
c.Assert(ns.SourceStructure, qt.DeepEquals, map[string]interface{}{"foo": "bar"})
c.Assert(ns.SourceHash, qt.Equals, "14368731254619220105")
c.Assert(ns.Config, qt.DeepEquals, &tstNsExt{Foo: "bar"})
c.Assert(ns.Signature(), qt.DeepEquals, []*tstNsExt(nil))
}
type (
tstNsExt struct {
Foo string
}
tstNsInt struct {
Foo string
}
)
func (t *tstNsExt) Init() error {
t.Foo = strings.ToUpper(t.Foo)
return nil
}
func (t *tstNsInt) Compile(ext *tstNsExt) error {
t.Foo = ext.Foo + " qux"
return nil
}

View File

@ -54,14 +54,16 @@ var DefaultConfig = Config{
}
// Config is the top level security config.
// <docsmeta>{"name": "security", "description": "This section holds the top level security config.", "newIn": "0.91.0" }</docsmeta>
type Config struct {
// Restricts access to os.Exec.
// Restricts access to os.Exec....
// <docsmeta>{ "newIn": "0.91.0" }</docsmeta>
Exec Exec `json:"exec"`
// Restricts access to certain template funcs.
Funcs Funcs `json:"funcs"`
// Restricts access to resources.Get, getJSON, getCSV.
// Restricts access to resources.GetRemote, getJSON, getCSV.
HTTP HTTP `json:"http"`
// Allow inline shortcodes

View File

@ -54,7 +54,7 @@ disableInlineCSS = true
func TestUseSettingsFromRootIfSet(t *testing.T) {
c := qt.New(t)
cfg := config.NewWithTestDefaults()
cfg := config.New()
cfg.Set("disqusShortname", "root_short")
cfg.Set("googleAnalytics", "ga_root")

View File

@ -0,0 +1,84 @@
// Copyright 2023 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// This package should only be used for testing.
package testconfig
import (
_ "unsafe"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/config/allconfig"
"github.com/gohugoio/hugo/deps"
"github.com/gohugoio/hugo/hugofs"
toml "github.com/pelletier/go-toml/v2"
"github.com/spf13/afero"
)
func GetTestConfigs(fs afero.Fs, cfg config.Provider) *allconfig.Configs {
if fs == nil {
fs = afero.NewMemMapFs()
}
if cfg == nil {
cfg = config.New()
}
// Make sure that the workingDir exists.
workingDir := cfg.GetString("workingDir")
if workingDir != "" {
if err := fs.MkdirAll(workingDir, 0777); err != nil {
panic(err)
}
}
configs, err := allconfig.LoadConfig(allconfig.ConfigSourceDescriptor{Fs: fs, Flags: cfg})
if err != nil {
panic(err)
}
return configs
}
func GetTestConfig(fs afero.Fs, cfg config.Provider) config.AllProvider {
return GetTestConfigs(fs, cfg).GetFirstLanguageConfig()
}
func GetTestDeps(fs afero.Fs, cfg config.Provider, beforeInit ...func(*deps.Deps)) *deps.Deps {
if fs == nil {
fs = afero.NewMemMapFs()
}
conf := GetTestConfig(fs, cfg)
d := &deps.Deps{
Conf: conf,
Fs: hugofs.NewFrom(fs, conf.BaseConfig()),
}
for _, f := range beforeInit {
f(d)
}
if err := d.Init(); err != nil {
panic(err)
}
return d
}
func GetTestConfigSectionFromStruct(section string, v any) config.AllProvider {
data, err := toml.Marshal(v)
if err != nil {
panic(err)
}
p := maps.Params{
section: config.FromTOMLConfigString(string(data)).Get(""),
}
cfg := config.NewFrom(p)
return GetTestConfig(nil, cfg)
}

View File

@ -340,7 +340,7 @@ func (b *contentBuilder) mapArcheTypeDir() error {
}
func (b *contentBuilder) openInEditorIfConfigured(filename string) error {
editor := b.h.Cfg.GetString("newContentEditor")
editor := b.h.Conf.NewContentEditor()
if editor == "" {
return nil
}

View File

@ -21,6 +21,8 @@ import (
"testing"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/config/allconfig"
"github.com/gohugoio/hugo/config/testconfig"
"github.com/gohugoio/hugo/deps"
@ -80,7 +82,8 @@ func TestNewContentFromFile(t *testing.T) {
mm := afero.NewMemMapFs()
c.Assert(initFs(mm), qt.IsNil)
cfg, fs := newTestCfg(c, mm)
h, err := hugolib.NewHugoSites(deps.DepsCfg{Cfg: cfg, Fs: fs})
conf := testconfig.GetTestConfigs(fs.Source, cfg)
h, err := hugolib.NewHugoSites(deps.DepsCfg{Configs: conf, Fs: fs})
c.Assert(err, qt.IsNil)
err = create.NewContent(h, cas.kind, cas.path, false)
@ -141,7 +144,8 @@ i18n: {{ T "hugo" }}
c.Assert(initFs(mm), qt.IsNil)
cfg, fs := newTestCfg(c, mm)
h, err := hugolib.NewHugoSites(deps.DepsCfg{Cfg: cfg, Fs: fs})
conf := testconfig.GetTestConfigs(fs.Source, cfg)
h, err := hugolib.NewHugoSites(deps.DepsCfg{Configs: conf, Fs: fs})
c.Assert(err, qt.IsNil)
c.Assert(len(h.Sites), qt.Equals, 2)
@ -183,7 +187,8 @@ site RegularPages: {{ len site.RegularPages }}
c.Assert(initFs(mm), qt.IsNil)
cfg, fs := newTestCfg(c, mm)
h, err := hugolib.NewHugoSites(deps.DepsCfg{Cfg: cfg, Fs: fs})
conf := testconfig.GetTestConfigs(fs.Source, cfg)
h, err := hugolib.NewHugoSites(deps.DepsCfg{Configs: conf, Fs: fs})
c.Assert(err, qt.IsNil)
c.Assert(len(h.Sites), qt.Equals, 2)
@ -232,8 +237,8 @@ i18n: {{ T "hugo" }}
c.Assert(initFs(mm), qt.IsNil)
cfg, fs := newTestCfg(c, mm)
h, err := hugolib.NewHugoSites(deps.DepsCfg{Cfg: cfg, Fs: fs})
conf := testconfig.GetTestConfigs(fs.Source, cfg)
h, err := hugolib.NewHugoSites(deps.DepsCfg{Configs: conf, Fs: fs})
c.Assert(err, qt.IsNil)
c.Assert(len(h.Sites), qt.Equals, 2)
@ -264,7 +269,8 @@ func TestNewContentForce(t *testing.T) {
c.Assert(initFs(mm), qt.IsNil)
cfg, fs := newTestCfg(c, mm)
h, err := hugolib.NewHugoSites(deps.DepsCfg{Cfg: cfg, Fs: fs})
conf := testconfig.GetTestConfigs(fs.Source, cfg)
h, err := hugolib.NewHugoSites(deps.DepsCfg{Configs: conf, Fs: fs})
c.Assert(err, qt.IsNil)
c.Assert(len(h.Sites), qt.Equals, 2)
@ -461,8 +467,8 @@ other = "Hugo Rokkar!"`), 0o755), qt.IsNil)
c.Assert(afero.WriteFile(mm, "config.toml", []byte(cfg), 0o755), qt.IsNil)
v, _, err := hugolib.LoadConfig(hugolib.ConfigSourceDescriptor{Fs: mm, Filename: "config.toml"})
res, err := allconfig.LoadConfig(allconfig.ConfigSourceDescriptor{Fs: mm, Filename: "config.toml"})
c.Assert(err, qt.IsNil)
return v, hugofs.NewFrom(mm, v)
return res.LoadingInfo.Cfg, hugofs.NewFrom(mm, res.LoadingInfo.BaseConfig)
}

View File

@ -55,17 +55,12 @@ type Deployer struct {
localFs afero.Fs
bucket *blob.Bucket
target *target // the target to deploy to
matchers []*matcher // matchers to apply to uploaded files
mediaTypes media.Types // Hugo's MediaType to guess ContentType
ordering []*regexp.Regexp // orders uploads
quiet bool // true reduces STDOUT
confirm bool // true enables confirmation before making changes
dryRun bool // true skips conformations and prints changes instead of applying them
force bool // true forces upload of all files
invalidateCDN bool // true enables invalidate CDN cache (if possible)
maxDeletes int // caps the # of files to delete; -1 to disable
workers int // The number of workers to transfer files
mediaTypes media.Types // Hugo's MediaType to guess ContentType
quiet bool // true reduces STDOUT
cfg DeployConfig
target *Target // the target to deploy to
// For tests...
summary deploySummary // summary of latest Deploy results
@ -78,21 +73,18 @@ type deploySummary struct {
const metaMD5Hash = "md5chksum" // the meta key to store md5hash in
// New constructs a new *Deployer.
func New(cfg config.Provider, localFs afero.Fs) (*Deployer, error) {
targetName := cfg.GetString("target")
func New(cfg config.AllProvider, localFs afero.Fs) (*Deployer, error) {
// Load the [deployment] section of the config.
dcfg, err := decodeConfig(cfg)
if err != nil {
return nil, err
}
dcfg := cfg.GetConfigSection(deploymentConfigKey).(DeployConfig)
targetName := dcfg.Target
if len(dcfg.Targets) == 0 {
return nil, errors.New("no deployment targets found")
}
mediaTypes := cfg.GetConfigSection("mediaTypes").(media.Types)
// Find the target to deploy to.
var tgt *target
var tgt *Target
if targetName == "" {
// Default to the first target.
tgt = dcfg.Targets[0]
@ -108,18 +100,11 @@ func New(cfg config.Provider, localFs afero.Fs) (*Deployer, error) {
}
return &Deployer{
localFs: localFs,
target: tgt,
matchers: dcfg.Matchers,
ordering: dcfg.ordering,
mediaTypes: dcfg.mediaTypes,
quiet: cfg.GetBool("quiet"),
confirm: cfg.GetBool("confirm"),
dryRun: cfg.GetBool("dryRun"),
force: cfg.GetBool("force"),
invalidateCDN: cfg.GetBool("invalidateCDN"),
maxDeletes: cfg.GetInt("maxDeletes"),
workers: cfg.GetInt("workers"),
localFs: localFs,
target: tgt,
quiet: cfg.BuildExpired(),
mediaTypes: mediaTypes,
cfg: dcfg,
}, nil
}
@ -138,12 +123,16 @@ func (d *Deployer) Deploy(ctx context.Context) error {
return err
}
if d.cfg.Workers <= 0 {
d.cfg.Workers = 10
}
// Load local files from the source directory.
var include, exclude glob.Glob
if d.target != nil {
include, exclude = d.target.includeGlob, d.target.excludeGlob
}
local, err := walkLocal(d.localFs, d.matchers, include, exclude, d.mediaTypes)
local, err := walkLocal(d.localFs, d.cfg.Matchers, include, exclude, d.mediaTypes)
if err != nil {
return err
}
@ -159,7 +148,7 @@ func (d *Deployer) Deploy(ctx context.Context) error {
d.summary.NumRemote = len(remote)
// Diff local vs remote to see what changes need to be applied.
uploads, deletes := findDiffs(local, remote, d.force)
uploads, deletes := findDiffs(local, remote, d.cfg.Force)
d.summary.NumUploads = len(uploads)
d.summary.NumDeletes = len(deletes)
if len(uploads)+len(deletes) == 0 {
@ -173,7 +162,7 @@ func (d *Deployer) Deploy(ctx context.Context) error {
}
// Ask for confirmation before proceeding.
if d.confirm && !d.dryRun {
if d.cfg.Confirm && !d.cfg.DryRun {
fmt.Printf("Continue? (Y/n) ")
var confirm string
if _, err := fmt.Scanln(&confirm); err != nil {
@ -186,15 +175,9 @@ func (d *Deployer) Deploy(ctx context.Context) error {
// Order the uploads. They are organized in groups; all uploads in a group
// must be complete before moving on to the next group.
uploadGroups := applyOrdering(d.ordering, uploads)
uploadGroups := applyOrdering(d.cfg.ordering, uploads)
// Apply the changes in parallel, using an inverted worker
// pool (https://www.youtube.com/watch?v=5zXAHh5tJqQ&t=26m58s).
// sem prevents more than nParallel concurrent goroutines.
if d.workers <= 0 {
d.workers = 10
}
nParallel := d.workers
nParallel := d.cfg.Workers
var errs []error
var errMu sync.Mutex // protects errs
@ -207,7 +190,7 @@ func (d *Deployer) Deploy(ctx context.Context) error {
// Within the group, apply uploads in parallel.
sem := make(chan struct{}, nParallel)
for _, upload := range uploads {
if d.dryRun {
if d.cfg.DryRun {
if !d.quiet {
jww.FEEDBACK.Printf("[DRY RUN] Would upload: %v\n", upload)
}
@ -230,15 +213,15 @@ func (d *Deployer) Deploy(ctx context.Context) error {
}
}
if d.maxDeletes != -1 && len(deletes) > d.maxDeletes {
jww.WARN.Printf("Skipping %d deletes because it is more than --maxDeletes (%d). If this is expected, set --maxDeletes to a larger number, or -1 to disable this check.\n", len(deletes), d.maxDeletes)
if d.cfg.MaxDeletes != -1 && len(deletes) > d.cfg.MaxDeletes {
jww.WARN.Printf("Skipping %d deletes because it is more than --maxDeletes (%d). If this is expected, set --maxDeletes to a larger number, or -1 to disable this check.\n", len(deletes), d.cfg.MaxDeletes)
d.summary.NumDeletes = 0
} else {
// Apply deletes in parallel.
sort.Slice(deletes, func(i, j int) bool { return deletes[i] < deletes[j] })
sem := make(chan struct{}, nParallel)
for _, del := range deletes {
if d.dryRun {
if d.cfg.DryRun {
if !d.quiet {
jww.FEEDBACK.Printf("[DRY RUN] Would delete %s\n", del)
}
@ -264,6 +247,7 @@ func (d *Deployer) Deploy(ctx context.Context) error {
sem <- struct{}{}
}
}
if len(errs) > 0 {
if !d.quiet {
jww.FEEDBACK.Printf("Encountered %d errors.\n", len(errs))
@ -274,9 +258,9 @@ func (d *Deployer) Deploy(ctx context.Context) error {
jww.FEEDBACK.Println("Success!")
}
if d.invalidateCDN {
if d.cfg.InvalidateCDN {
if d.target.CloudFrontDistributionID != "" {
if d.dryRun {
if d.cfg.DryRun {
if !d.quiet {
jww.FEEDBACK.Printf("[DRY RUN] Would invalidate CloudFront CDN with ID %s\n", d.target.CloudFrontDistributionID)
}
@ -289,7 +273,7 @@ func (d *Deployer) Deploy(ctx context.Context) error {
}
}
if d.target.GoogleCloudCDNOrigin != "" {
if d.dryRun {
if d.cfg.DryRun {
if !d.quiet {
jww.FEEDBACK.Printf("[DRY RUN] Would invalidate Google Cloud CDN with origin %s\n", d.target.GoogleCloudCDNOrigin)
}
@ -356,14 +340,14 @@ type localFile struct {
UploadSize int64
fs afero.Fs
matcher *matcher
matcher *Matcher
md5 []byte // cache
gzipped bytes.Buffer // cached of gzipped contents if gzipping
mediaTypes media.Types
}
// newLocalFile initializes a *localFile.
func newLocalFile(fs afero.Fs, nativePath, slashpath string, m *matcher, mt media.Types) (*localFile, error) {
func newLocalFile(fs afero.Fs, nativePath, slashpath string, m *Matcher, mt media.Types) (*localFile, error) {
f, err := fs.Open(nativePath)
if err != nil {
return nil, err
@ -448,7 +432,7 @@ func (lf *localFile) ContentType() string {
ext := filepath.Ext(lf.NativePath)
if mimeType, _, found := lf.mediaTypes.GetFirstBySuffix(strings.TrimPrefix(ext, ".")); found {
return mimeType.Type()
return mimeType.Type
}
return mime.TypeByExtension(ext)
@ -495,7 +479,7 @@ func knownHiddenDirectory(name string) bool {
// walkLocal walks the source directory and returns a flat list of files,
// using localFile.SlashPath as the map keys.
func walkLocal(fs afero.Fs, matchers []*matcher, include, exclude glob.Glob, mediaTypes media.Types) (map[string]*localFile, error) {
func walkLocal(fs afero.Fs, matchers []*Matcher, include, exclude glob.Glob, mediaTypes media.Types) (map[string]*localFile, error) {
retval := map[string]*localFile{}
err := afero.Walk(fs, "", func(path string, info os.FileInfo, err error) error {
if err != nil {
@ -534,7 +518,7 @@ func walkLocal(fs afero.Fs, matchers []*matcher, include, exclude glob.Glob, med
}
// Find the first matching matcher (if any).
var m *matcher
var m *Matcher
for _, cur := range matchers {
if cur.Matches(slashpath) {
m = cur

View File

@ -25,23 +25,37 @@ import (
"github.com/gobwas/glob"
"github.com/gohugoio/hugo/config"
hglob "github.com/gohugoio/hugo/hugofs/glob"
"github.com/gohugoio/hugo/media"
"github.com/mitchellh/mapstructure"
)
const deploymentConfigKey = "deployment"
// deployConfig is the complete configuration for deployment.
type deployConfig struct {
Targets []*target
Matchers []*matcher
// DeployConfig is the complete configuration for deployment.
type DeployConfig struct {
Targets []*Target
Matchers []*Matcher
Order []string
ordering []*regexp.Regexp // compiled Order
mediaTypes media.Types
// Usually set via flags.
// Target deployment Name; defaults to the first one.
Target string
// Show a confirm prompt before deploying.
Confirm bool
// DryRun will try the deployment without any remote changes.
DryRun bool
// Force will re-upload all files.
Force bool
// Invalidate the CDN cache listed in the deployment target.
InvalidateCDN bool
// MaxDeletes is the maximum number of files to delete.
MaxDeletes int
// Number of concurrent workers to use when uploading files.
Workers int
ordering []*regexp.Regexp // compiled Order
}
type target struct {
type Target struct {
Name string
URL string
@ -61,7 +75,7 @@ type target struct {
excludeGlob glob.Glob
}
func (tgt *target) parseIncludeExclude() error {
func (tgt *Target) parseIncludeExclude() error {
var err error
if tgt.Include != "" {
tgt.includeGlob, err = hglob.GetGlob(tgt.Include)
@ -78,9 +92,9 @@ func (tgt *target) parseIncludeExclude() error {
return nil
}
// matcher represents configuration to be applied to files whose paths match
// Matcher represents configuration to be applied to files whose paths match
// a specified pattern.
type matcher struct {
type Matcher struct {
// Pattern is the string pattern to match against paths.
// Matching is done against paths converted to use / as the path separator.
Pattern string
@ -109,15 +123,14 @@ type matcher struct {
re *regexp.Regexp
}
func (m *matcher) Matches(path string) bool {
func (m *Matcher) Matches(path string) bool {
return m.re.MatchString(path)
}
// decode creates a config from a given Hugo configuration.
func decodeConfig(cfg config.Provider) (deployConfig, error) {
// DecodeConfig creates a config from a given Hugo configuration.
func DecodeConfig(cfg config.Provider) (DeployConfig, error) {
var (
mediaTypesConfig []map[string]any
dcfg deployConfig
dcfg DeployConfig
)
if !cfg.IsSet(deploymentConfigKey) {
@ -126,8 +139,13 @@ func decodeConfig(cfg config.Provider) (deployConfig, error) {
if err := mapstructure.WeakDecode(cfg.GetStringMap(deploymentConfigKey), &dcfg); err != nil {
return dcfg, err
}
if dcfg.Workers <= 0 {
dcfg.Workers = 10
}
for _, tgt := range dcfg.Targets {
if *tgt == (target{}) {
if *tgt == (Target{}) {
return dcfg, errors.New("empty deployment target")
}
if err := tgt.parseIncludeExclude(); err != nil {
@ -136,7 +154,7 @@ func decodeConfig(cfg config.Provider) (deployConfig, error) {
}
var err error
for _, m := range dcfg.Matchers {
if *m == (matcher{}) {
if *m == (Matcher{}) {
return dcfg, errors.New("empty deployment matcher")
}
m.re, err = regexp.Compile(m.Pattern)
@ -152,13 +170,5 @@ func decodeConfig(cfg config.Provider) (deployConfig, error) {
dcfg.ordering = append(dcfg.ordering, re)
}
if cfg.IsSet("mediaTypes") {
mediaTypesConfig = append(mediaTypesConfig, cfg.GetStringMap("mediaTypes"))
}
dcfg.mediaTypes, err = media.DecodeTypes(mediaTypesConfig...)
if err != nil {
return dcfg, err
}
return dcfg, nil
}

View File

@ -84,7 +84,7 @@ force = true
cfg, err := config.FromConfigString(tomlConfig, "toml")
c.Assert(err, qt.IsNil)
dcfg, err := decodeConfig(cfg)
dcfg, err := DecodeConfig(cfg)
c.Assert(err, qt.IsNil)
// Order.
@ -139,7 +139,7 @@ order = ["["] # invalid regular expression
cfg, err := config.FromConfigString(tomlConfig, "toml")
c.Assert(err, qt.IsNil)
_, err = decodeConfig(cfg)
_, err = DecodeConfig(cfg)
c.Assert(err, qt.Not(qt.IsNil))
}
@ -157,14 +157,14 @@ Pattern = "[" # invalid regular expression
cfg, err := config.FromConfigString(tomlConfig, "toml")
c.Assert(err, qt.IsNil)
_, err = decodeConfig(cfg)
_, err = DecodeConfig(cfg)
c.Assert(err, qt.Not(qt.IsNil))
}
func TestDecodeConfigDefault(t *testing.T) {
c := qt.New(t)
dcfg, err := decodeConfig(config.New())
dcfg, err := DecodeConfig(config.New())
c.Assert(err, qt.IsNil)
c.Assert(len(dcfg.Targets), qt.Equals, 0)
c.Assert(len(dcfg.Matchers), qt.Equals, 0)
@ -180,7 +180,7 @@ func TestEmptyTarget(t *testing.T) {
cfg, err := config.FromConfigString(tomlConfig, "toml")
c.Assert(err, qt.IsNil)
_, err = decodeConfig(cfg)
_, err = DecodeConfig(cfg)
c.Assert(err, qt.Not(qt.IsNil))
}
@ -194,6 +194,6 @@ func TestEmptyMatcher(t *testing.T) {
cfg, err := config.FromConfigString(tomlConfig, "toml")
c.Assert(err, qt.IsNil)
_, err = decodeConfig(cfg)
_, err = DecodeConfig(cfg)
c.Assert(err, qt.Not(qt.IsNil))
}

View File

@ -108,7 +108,7 @@ func TestFindDiffs(t *testing.T) {
{
Description: "local == remote with route.Force true -> diffs",
Local: []*localFile{
{NativePath: "aaa", SlashPath: "aaa", UploadSize: 1, matcher: &matcher{Force: true}, md5: hash1},
{NativePath: "aaa", SlashPath: "aaa", UploadSize: 1, matcher: &Matcher{Force: true}, md5: hash1},
makeLocal("bbb", 2, hash1),
},
Remote: []*blob.ListObject{
@ -289,8 +289,8 @@ func TestLocalFile(t *testing.T) {
tests := []struct {
Description string
Path string
Matcher *matcher
MediaTypesConfig []map[string]any
Matcher *Matcher
MediaTypesConfig map[string]any
WantContent []byte
WantSize int64
WantMD5 []byte
@ -315,7 +315,7 @@ func TestLocalFile(t *testing.T) {
{
Description: "CacheControl from matcher",
Path: "foo.txt",
Matcher: &matcher{CacheControl: "max-age=630720000"},
Matcher: &Matcher{CacheControl: "max-age=630720000"},
WantContent: contentBytes,
WantSize: contentLen,
WantMD5: contentMD5[:],
@ -324,7 +324,7 @@ func TestLocalFile(t *testing.T) {
{
Description: "ContentEncoding from matcher",
Path: "foo.txt",
Matcher: &matcher{ContentEncoding: "foobar"},
Matcher: &Matcher{ContentEncoding: "foobar"},
WantContent: contentBytes,
WantSize: contentLen,
WantMD5: contentMD5[:],
@ -333,7 +333,7 @@ func TestLocalFile(t *testing.T) {
{
Description: "ContentType from matcher",
Path: "foo.txt",
Matcher: &matcher{ContentType: "foo/bar"},
Matcher: &Matcher{ContentType: "foo/bar"},
WantContent: contentBytes,
WantSize: contentLen,
WantMD5: contentMD5[:],
@ -342,7 +342,7 @@ func TestLocalFile(t *testing.T) {
{
Description: "gzipped content",
Path: "foo.txt",
Matcher: &matcher{Gzip: true},
Matcher: &Matcher{Gzip: true},
WantContent: gzBytes,
WantSize: gzLen,
WantMD5: gzMD5[:],
@ -351,11 +351,9 @@ func TestLocalFile(t *testing.T) {
{
Description: "Custom MediaType",
Path: "foo.hugo",
MediaTypesConfig: []map[string]any{
{
"hugo/custom": map[string]any{
"suffixes": []string{"hugo"},
},
MediaTypesConfig: map[string]any{
"hugo/custom": map[string]any{
"suffixes": []string{"hugo"},
},
},
WantContent: contentBytes,
@ -373,11 +371,11 @@ func TestLocalFile(t *testing.T) {
}
mediaTypes := media.DefaultTypes
if len(tc.MediaTypesConfig) > 0 {
mt, err := media.DecodeTypes(tc.MediaTypesConfig...)
mt, err := media.DecodeTypes(tc.MediaTypesConfig)
if err != nil {
t.Fatal(err)
}
mediaTypes = mt
mediaTypes = mt.Config
}
lf, err := newLocalFile(fs, tc.Path, filepath.ToSlash(tc.Path), tc.Matcher, mediaTypes)
if err != nil {
@ -556,9 +554,9 @@ func TestEndToEndSync(t *testing.T) {
}
deployer := &Deployer{
localFs: test.fs,
maxDeletes: -1,
bucket: test.bucket,
mediaTypes: media.DefaultTypes,
cfg: DeployConfig{MaxDeletes: -1},
}
// Initial deployment should sync remote with local.
@ -639,9 +637,9 @@ func TestMaxDeletes(t *testing.T) {
}
deployer := &Deployer{
localFs: test.fs,
maxDeletes: -1,
bucket: test.bucket,
mediaTypes: media.DefaultTypes,
cfg: DeployConfig{MaxDeletes: -1},
}
// Sync remote with local.
@ -662,7 +660,7 @@ func TestMaxDeletes(t *testing.T) {
}
// A deployment with maxDeletes=0 shouldn't change anything.
deployer.maxDeletes = 0
deployer.cfg.MaxDeletes = 0
if err := deployer.Deploy(ctx); err != nil {
t.Errorf("deploy failed: %v", err)
}
@ -672,7 +670,7 @@ func TestMaxDeletes(t *testing.T) {
}
// A deployment with maxDeletes=1 shouldn't change anything either.
deployer.maxDeletes = 1
deployer.cfg.MaxDeletes = 1
if err := deployer.Deploy(ctx); err != nil {
t.Errorf("deploy failed: %v", err)
}
@ -682,7 +680,7 @@ func TestMaxDeletes(t *testing.T) {
}
// A deployment with maxDeletes=2 should make the changes.
deployer.maxDeletes = 2
deployer.cfg.MaxDeletes = 2
if err := deployer.Deploy(ctx); err != nil {
t.Errorf("deploy failed: %v", err)
}
@ -700,7 +698,7 @@ func TestMaxDeletes(t *testing.T) {
}
// A deployment with maxDeletes=-1 should make the changes.
deployer.maxDeletes = -1
deployer.cfg.MaxDeletes = -1
if err := deployer.Deploy(ctx); err != nil {
t.Errorf("deploy failed: %v", err)
}
@ -762,7 +760,7 @@ func TestIncludeExclude(t *testing.T) {
if err != nil {
t.Fatal(err)
}
tgt := &target{
tgt := &Target{
Include: test.Include,
Exclude: test.Exclude,
}
@ -770,9 +768,8 @@ func TestIncludeExclude(t *testing.T) {
t.Error(err)
}
deployer := &Deployer{
localFs: fsTest.fs,
maxDeletes: -1,
bucket: fsTest.bucket,
localFs: fsTest.fs,
cfg: DeployConfig{MaxDeletes: -1}, bucket: fsTest.bucket,
target: tgt,
mediaTypes: media.DefaultTypes,
}
@ -828,9 +825,8 @@ func TestIncludeExcludeRemoteDelete(t *testing.T) {
t.Fatal(err)
}
deployer := &Deployer{
localFs: fsTest.fs,
maxDeletes: -1,
bucket: fsTest.bucket,
localFs: fsTest.fs,
cfg: DeployConfig{MaxDeletes: -1}, bucket: fsTest.bucket,
mediaTypes: media.DefaultTypes,
}
@ -848,7 +844,7 @@ func TestIncludeExcludeRemoteDelete(t *testing.T) {
}
// Second sync
tgt := &target{
tgt := &Target{
Include: test.Include,
Exclude: test.Exclude,
}
@ -882,7 +878,7 @@ func TestCompression(t *testing.T) {
deployer := &Deployer{
localFs: test.fs,
bucket: test.bucket,
matchers: []*matcher{{Pattern: ".*", Gzip: true, re: regexp.MustCompile(".*")}},
cfg: DeployConfig{MaxDeletes: -1, Matchers: []*Matcher{{Pattern: ".*", Gzip: true, re: regexp.MustCompile(".*")}}},
mediaTypes: media.DefaultTypes,
}
@ -937,7 +933,7 @@ func TestMatching(t *testing.T) {
deployer := &Deployer{
localFs: test.fs,
bucket: test.bucket,
matchers: []*matcher{{Pattern: "^subdir/aaa$", Force: true, re: regexp.MustCompile("^subdir/aaa$")}},
cfg: DeployConfig{MaxDeletes: -1, Matchers: []*Matcher{{Pattern: "^subdir/aaa$", Force: true, re: regexp.MustCompile("^subdir/aaa$")}}},
mediaTypes: media.DefaultTypes,
}
@ -962,7 +958,7 @@ func TestMatching(t *testing.T) {
}
// Repeat with a matcher that should now match 3 files.
deployer.matchers = []*matcher{{Pattern: "aaa", Force: true, re: regexp.MustCompile("aaa")}}
deployer.cfg.Matchers = []*Matcher{{Pattern: "aaa", Force: true, re: regexp.MustCompile("aaa")}}
if err := deployer.Deploy(ctx); err != nil {
t.Errorf("no-op deploy with triple force matcher: %v", err)
}

450
deps/deps.go vendored
View File

@ -4,30 +4,27 @@ import (
"context"
"fmt"
"path/filepath"
"sort"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/gohugoio/hugo/cache/filecache"
"github.com/gohugoio/hugo/common/hexec"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/config/allconfig"
"github.com/gohugoio/hugo/config/security"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/langs"
"github.com/gohugoio/hugo/media"
"github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/resources/postpub"
"github.com/gohugoio/hugo/metrics"
"github.com/gohugoio/hugo/output"
"github.com/gohugoio/hugo/resources"
"github.com/gohugoio/hugo/source"
"github.com/gohugoio/hugo/tpl"
"github.com/spf13/cast"
"github.com/spf13/afero"
jww "github.com/spf13/jwalterweatherman"
)
@ -45,10 +42,7 @@ type Deps struct {
ExecHelper *hexec.Exec
// The templates to use. This will usually implement the full tpl.TemplateManager.
tmpl tpl.TemplateHandler
// We use this to parse and execute ad-hoc text templates.
textTmpl tpl.TemplateParseFinder
tmplHandlers *tpl.TemplateHandlers
// The file systems to use.
Fs *hugofs.Fs `json:"-"`
@ -66,56 +60,170 @@ type Deps struct {
ResourceSpec *resources.Spec
// The configuration to use
Cfg config.Provider `json:"-"`
// The file cache to use.
FileCaches filecache.Caches
Conf config.AllProvider `json:"-"`
// The translation func to use
Translate func(ctx context.Context, translationID string, templateData any) string `json:"-"`
// The language in use. TODO(bep) consolidate with site
Language *langs.Language
// The site building.
Site page.Site
// All the output formats available for the current site.
OutputFormatsConfig output.Formats
// FilenameHasPostProcessPrefix is a set of filenames in /public that
// contains a post-processing prefix.
FilenameHasPostProcessPrefix []string
templateProvider ResourceProvider
WithTemplate func(templ tpl.TemplateManager) error `json:"-"`
TemplateProvider ResourceProvider
// Used in tests
OverloadedTemplateFuncs map[string]any
translationProvider ResourceProvider
TranslationProvider ResourceProvider
Metrics metrics.Provider
// Timeout is configurable in site config.
Timeout time.Duration
// BuildStartListeners will be notified before a build starts.
BuildStartListeners *Listeners
// Resources that gets closed when the build is done or the server shuts down.
BuildClosers *Closers
// Atomic values set during a build.
// This is common/global for all sites.
BuildState *BuildState
// Whether we are in running (server) mode
Running bool
*globalErrHandler
}
func (d Deps) Clone(s page.Site, conf config.AllProvider) (*Deps, error) {
d.Conf = conf
d.Site = s
d.ExecHelper = nil
d.ContentSpec = nil
if err := d.Init(); err != nil {
return nil, err
}
return &d, nil
}
func (d *Deps) SetTempl(t *tpl.TemplateHandlers) {
d.tmplHandlers = t
}
func (d *Deps) Init() error {
if d.Conf == nil {
panic("conf is nil")
}
if d.Fs == nil {
// For tests.
d.Fs = hugofs.NewFrom(afero.NewMemMapFs(), d.Conf.BaseConfig())
}
if d.Log == nil {
d.Log = loggers.NewErrorLogger()
}
if d.LogDistinct == nil {
d.LogDistinct = helpers.NewDistinctLogger(d.Log)
}
if d.globalErrHandler == nil {
d.globalErrHandler = &globalErrHandler{}
}
if d.BuildState == nil {
d.BuildState = &BuildState{}
}
if d.BuildStartListeners == nil {
d.BuildStartListeners = &Listeners{}
}
if d.BuildClosers == nil {
d.BuildClosers = &Closers{}
}
if d.Metrics == nil && d.Conf.TemplateMetrics() {
d.Metrics = metrics.NewProvider(d.Conf.TemplateMetricsHints())
}
if d.ExecHelper == nil {
d.ExecHelper = hexec.New(d.Conf.GetConfigSection("security").(security.Config))
}
if d.PathSpec == nil {
hashBytesReceiverFunc := func(name string, match bool) {
if !match {
return
}
d.BuildState.AddFilenameWithPostPrefix(name)
}
// Skip binary files.
mediaTypes := d.Conf.GetConfigSection("mediaTypes").(media.Types)
hashBytesSHouldCheck := func(name string) bool {
ext := strings.TrimPrefix(filepath.Ext(name), ".")
return mediaTypes.IsTextSuffix(ext)
}
d.Fs.PublishDir = hugofs.NewHasBytesReceiver(d.Fs.PublishDir, hashBytesSHouldCheck, hashBytesReceiverFunc, []byte(postpub.PostProcessPrefix))
pathSpec, err := helpers.NewPathSpec(d.Fs, d.Conf, d.Log)
if err != nil {
return err
}
d.PathSpec = pathSpec
} else {
var err error
d.PathSpec, err = helpers.NewPathSpecWithBaseBaseFsProvided(d.Fs, d.Conf, d.Log, d.PathSpec.BaseFs)
if err != nil {
return err
}
}
if d.ContentSpec == nil {
contentSpec, err := helpers.NewContentSpec(d.Conf, d.Log, d.Content.Fs, d.ExecHelper)
if err != nil {
return err
}
d.ContentSpec = contentSpec
}
if d.SourceSpec == nil {
d.SourceSpec = source.NewSourceSpec(d.PathSpec, nil, d.Fs.Source)
}
var common *resources.SpecCommon
if d.ResourceSpec != nil {
common = d.ResourceSpec.SpecCommon
}
resourceSpec, err := resources.NewSpec(d.PathSpec, common, d.BuildState, d.Log, d, d.ExecHelper)
if err != nil {
return fmt.Errorf("failed to create resource spec: %w", err)
}
d.ResourceSpec = resourceSpec
return nil
}
func (d *Deps) Compile(prototype *Deps) error {
var err error
if prototype == nil {
if err = d.TemplateProvider.NewResource(d); err != nil {
return err
}
if err = d.TranslationProvider.NewResource(d); err != nil {
return err
}
return nil
}
if err = d.TemplateProvider.CloneResource(d, prototype); err != nil {
return err
}
if err = d.TranslationProvider.CloneResource(d, prototype); err != nil {
return err
}
return nil
}
type globalErrHandler struct {
// Channel for some "hard to get to" build errors
buildErrors chan error
@ -181,236 +289,22 @@ func (b *Listeners) Notify() {
// ResourceProvider is used to create and refresh, and clone resources needed.
type ResourceProvider interface {
Update(deps *Deps) error
Clone(deps *Deps) error
NewResource(dst *Deps) error
CloneResource(dst, src *Deps) error
}
func (d *Deps) Tmpl() tpl.TemplateHandler {
return d.tmpl
return d.tmplHandlers.Tmpl
}
func (d *Deps) TextTmpl() tpl.TemplateParseFinder {
return d.textTmpl
}
func (d *Deps) SetTmpl(tmpl tpl.TemplateHandler) {
d.tmpl = tmpl
}
func (d *Deps) SetTextTmpl(tmpl tpl.TemplateParseFinder) {
d.textTmpl = tmpl
}
// LoadResources loads translations and templates.
func (d *Deps) LoadResources() error {
// Note that the translations need to be loaded before the templates.
if err := d.translationProvider.Update(d); err != nil {
return fmt.Errorf("loading translations: %w", err)
}
if err := d.templateProvider.Update(d); err != nil {
return fmt.Errorf("loading templates: %w", err)
}
return nil
}
// New initializes a Dep struct.
// Defaults are set for nil values,
// but TemplateProvider, TranslationProvider and Language are always required.
func New(cfg DepsCfg) (*Deps, error) {
var (
logger = cfg.Logger
fs = cfg.Fs
d *Deps
)
if cfg.TemplateProvider == nil {
panic("Must have a TemplateProvider")
}
if cfg.TranslationProvider == nil {
panic("Must have a TranslationProvider")
}
if cfg.Language == nil {
panic("Must have a Language")
}
if logger == nil {
logger = loggers.NewErrorLogger()
}
if fs == nil {
// Default to the production file system.
fs = hugofs.NewDefault(cfg.Language)
}
if cfg.MediaTypes == nil {
cfg.MediaTypes = media.DefaultTypes
}
if cfg.OutputFormats == nil {
cfg.OutputFormats = output.DefaultFormats
}
securityConfig, err := security.DecodeConfig(cfg.Cfg)
if err != nil {
return nil, fmt.Errorf("failed to create security config from configuration: %w", err)
}
execHelper := hexec.New(securityConfig)
var filenameHasPostProcessPrefixMu sync.Mutex
hashBytesReceiverFunc := func(name string, match bool) {
if !match {
return
}
filenameHasPostProcessPrefixMu.Lock()
d.FilenameHasPostProcessPrefix = append(d.FilenameHasPostProcessPrefix, name)
filenameHasPostProcessPrefixMu.Unlock()
}
// Skip binary files.
hashBytesSHouldCheck := func(name string) bool {
ext := strings.TrimPrefix(filepath.Ext(name), ".")
mime, _, found := cfg.MediaTypes.GetBySuffix(ext)
if !found {
return false
}
switch mime.MainType {
case "text", "application":
return true
default:
return false
}
}
fs.PublishDir = hugofs.NewHasBytesReceiver(fs.PublishDir, hashBytesSHouldCheck, hashBytesReceiverFunc, []byte(postpub.PostProcessPrefix))
ps, err := helpers.NewPathSpec(fs, cfg.Language, logger)
if err != nil {
return nil, fmt.Errorf("create PathSpec: %w", err)
}
fileCaches, err := filecache.NewCaches(ps)
if err != nil {
return nil, fmt.Errorf("failed to create file caches from configuration: %w", err)
}
errorHandler := &globalErrHandler{}
buildState := &BuildState{}
resourceSpec, err := resources.NewSpec(ps, fileCaches, buildState, logger, errorHandler, execHelper, cfg.OutputFormats, cfg.MediaTypes)
if err != nil {
return nil, err
}
contentSpec, err := helpers.NewContentSpec(cfg.Language, logger, ps.BaseFs.Content.Fs, execHelper)
if err != nil {
return nil, err
}
sp := source.NewSourceSpec(ps, nil, fs.Source)
timeout := 30 * time.Second
if cfg.Cfg.IsSet("timeout") {
v := cfg.Cfg.Get("timeout")
d, err := types.ToDurationE(v)
if err == nil {
timeout = d
}
}
ignoreErrors := cast.ToStringSlice(cfg.Cfg.Get("ignoreErrors"))
ignorableLogger := loggers.NewIgnorableLogger(logger, ignoreErrors...)
logDistinct := helpers.NewDistinctLogger(logger)
d = &Deps{
Fs: fs,
Log: ignorableLogger,
LogDistinct: logDistinct,
ExecHelper: execHelper,
templateProvider: cfg.TemplateProvider,
translationProvider: cfg.TranslationProvider,
WithTemplate: cfg.WithTemplate,
OverloadedTemplateFuncs: cfg.OverloadedTemplateFuncs,
PathSpec: ps,
ContentSpec: contentSpec,
SourceSpec: sp,
ResourceSpec: resourceSpec,
Cfg: cfg.Language,
Language: cfg.Language,
Site: cfg.Site,
FileCaches: fileCaches,
BuildStartListeners: &Listeners{},
BuildClosers: &Closers{},
BuildState: buildState,
Running: cfg.Running,
Timeout: timeout,
globalErrHandler: errorHandler,
}
if cfg.Cfg.GetBool("templateMetrics") {
d.Metrics = metrics.NewProvider(cfg.Cfg.GetBool("templateMetricsHints"))
}
return d, nil
return d.tmplHandlers.TxtTmpl
}
func (d *Deps) Close() error {
return d.BuildClosers.Close()
}
// ForLanguage creates a copy of the Deps with the language dependent
// parts switched out.
func (d Deps) ForLanguage(cfg DepsCfg, onCreated func(d *Deps) error) (*Deps, error) {
l := cfg.Language
var err error
d.PathSpec, err = helpers.NewPathSpecWithBaseBaseFsProvided(d.Fs, l, d.Log, d.BaseFs)
if err != nil {
return nil, err
}
d.ContentSpec, err = helpers.NewContentSpec(l, d.Log, d.BaseFs.Content.Fs, d.ExecHelper)
if err != nil {
return nil, err
}
d.Site = cfg.Site
// These are common for all sites, so reuse.
// TODO(bep) clean up these inits.
resourceCache := d.ResourceSpec.ResourceCache
postBuildAssets := d.ResourceSpec.PostBuildAssets
d.ResourceSpec, err = resources.NewSpec(d.PathSpec, d.ResourceSpec.FileCaches, d.BuildState, d.Log, d.globalErrHandler, d.ExecHelper, cfg.OutputFormats, cfg.MediaTypes)
if err != nil {
return nil, err
}
d.ResourceSpec.ResourceCache = resourceCache
d.ResourceSpec.PostBuildAssets = postBuildAssets
d.Cfg = l
d.Language = l
if onCreated != nil {
if err = onCreated(&d); err != nil {
return nil, err
}
}
if err := d.translationProvider.Clone(&d); err != nil {
return nil, err
}
if err := d.templateProvider.Clone(&d); err != nil {
return nil, err
}
d.BuildStartListeners = &Listeners{}
return &d, nil
}
// DepsCfg contains configuration options that can be used to configure Hugo
// on a global level, i.e. logging etc.
// Nil values will be given default values.
@ -422,47 +316,53 @@ type DepsCfg struct {
// The file systems to use
Fs *hugofs.Fs
// The language to use.
Language *langs.Language
// The Site in use
Site page.Site
// The configuration to use.
Cfg config.Provider
// The media types configured.
MediaTypes media.Types
// The output formats configured.
OutputFormats output.Formats
Configs *allconfig.Configs
// Template handling.
TemplateProvider ResourceProvider
WithTemplate func(templ tpl.TemplateManager) error
// Used in tests
OverloadedTemplateFuncs map[string]any
// i18n handling.
TranslationProvider ResourceProvider
// Whether we are in running (server) mode
Running bool
}
// BuildState are flags that may be turned on during a build.
// BuildState are state used during a build.
type BuildState struct {
counter uint64
mu sync.Mutex // protects state below.
// A set of ilenames in /public that
// contains a post-processing prefix.
filenamesWithPostPrefix map[string]bool
}
func (b *BuildState) AddFilenameWithPostPrefix(filename string) {
b.mu.Lock()
defer b.mu.Unlock()
if b.filenamesWithPostPrefix == nil {
b.filenamesWithPostPrefix = make(map[string]bool)
}
b.filenamesWithPostPrefix[filename] = true
}
func (b *BuildState) GetFilenamesWithPostPrefix() []string {
b.mu.Lock()
defer b.mu.Unlock()
var filenames []string
for filename := range b.filenamesWithPostPrefix {
filenames = append(filenames, filename)
}
sort.Strings(filenames)
return filenames
}
func (b *BuildState) Incr() int {
return int(atomic.AddUint64(&b.counter, uint64(1)))
}
func NewBuildState() BuildState {
return BuildState{}
}
type Closer interface {
Close() error
}

5
deps/deps_test.go vendored
View File

@ -11,17 +11,18 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package deps
package deps_test
import (
"testing"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/deps"
)
func TestBuildFlags(t *testing.T) {
c := qt.New(t)
var bf BuildState
var bf deps.BuildState
bf.Incr()
bf.Incr()
bf.Incr()

9
go.mod
View File

@ -47,12 +47,12 @@ require (
github.com/niklasfasching/go-org v1.6.6
github.com/olekukonko/tablewriter v0.0.5
github.com/pelletier/go-toml/v2 v2.0.6
github.com/rogpeppe/go-internal v1.9.0
github.com/rogpeppe/go-internal v1.10.1-0.20230508101108-a4f6fabd84c5
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd
github.com/sanity-io/litter v1.5.5
github.com/spf13/afero v1.9.3
github.com/spf13/cast v1.5.1
github.com/spf13/cobra v1.6.1
github.com/spf13/cobra v1.7.0
github.com/spf13/fsync v0.9.0
github.com/spf13/jwalterweatherman v1.1.0
github.com/spf13/pflag v1.0.5
@ -94,6 +94,8 @@ require (
github.com/aws/aws-sdk-go-v2/service/sso v1.4.0 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.7.0 // indirect
github.com/aws/smithy-go v1.8.0 // indirect
github.com/bep/helpers v0.4.0 // indirect
github.com/bep/simplecobra v0.2.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
github.com/dlclark/regexp2 v1.7.0 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect
@ -106,7 +108,7 @@ require (
github.com/googleapis/gax-go/v2 v2.3.0 // indirect
github.com/googleapis/go-type-adapters v1.0.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.1 // indirect
github.com/inconshreveable/mousetrap v1.0.1 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/invopop/yaml v0.1.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
@ -119,6 +121,7 @@ require (
github.com/russross/blackfriday/v2 v2.1.0 // indirect
go.opencensus.io v0.24.0 // indirect
golang.org/x/crypto v0.3.0 // indirect
golang.org/x/mod v0.9.0 // indirect
golang.org/x/oauth2 v0.2.0 // indirect
golang.org/x/sys v0.5.0 // indirect
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect

12
go.sum
View File

@ -179,10 +179,14 @@ github.com/bep/golibsass v1.1.0 h1:pjtXr00IJZZaOdfryNa9wARTB3Q0BmxC3/V1KNcgyTw=
github.com/bep/golibsass v1.1.0/go.mod h1:DL87K8Un/+pWUS75ggYv41bliGiolxzDKWJAq3eJ1MA=
github.com/bep/gowebp v0.2.0 h1:ZVfK8i9PpZqKHEmthQSt3qCnnHycbLzBPEsVtk2ch2Q=
github.com/bep/gowebp v0.2.0/go.mod h1:ZhFodwdiFp8ehGJpF4LdPl6unxZm9lLFjxD3z2h2AgI=
github.com/bep/helpers v0.4.0 h1:ab9veaAiWY4ST48Oxp5usaqivDmYdB744fz+tcZ3Ifs=
github.com/bep/helpers v0.4.0/go.mod h1:/QpHdmcPagDw7+RjkLFCvnlUc8lQ5kg4KDrEkb2Yyco=
github.com/bep/lazycache v0.2.0 h1:HKrlZTrDxHIrNKqmnurH42ryxkngCMYLfBpyu40VcwY=
github.com/bep/lazycache v0.2.0/go.mod h1:xUIsoRD824Vx0Q/n57+ZO7kmbEhMBOnTjM/iPixNGbg=
github.com/bep/overlayfs v0.6.0 h1:sgLcq/qtIzbaQNl2TldGXOkHvqeZB025sPvHOQL+DYo=
github.com/bep/overlayfs v0.6.0/go.mod h1:NFjSmn3kCqG7KX2Lmz8qT8VhPPCwZap3UNogXawoQHM=
github.com/bep/simplecobra v0.2.0 h1:gfdZZ8QlPBMC9R9DRzUsxExR3FyuNtRkqMJqK98SBno=
github.com/bep/simplecobra v0.2.0/go.mod h1:EOp6bCKuuHmwA9bQcRC8LcDB60co2Cmht5X4xMIOwf0=
github.com/bep/tmc v0.5.1 h1:CsQnSC6MsomH64gw0cT5f+EwQDcvZz4AazKunFwTpuI=
github.com/bep/tmc v0.5.1/go.mod h1:tGYHN8fS85aJPhDLgXETVKp+PR382OvFi2+q2GkGsq0=
github.com/bep/workers v1.0.0 h1:U+H8YmEaBCEaFZBst7GcRVEoqeRC9dzH2dWOwGmOchg=
@ -408,6 +412,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc=
github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/invopop/yaml v0.1.0 h1:YW3WGUoJEXYfzWBjn00zIlrw7brGVD0fUKRYDPAPhrc=
github.com/invopop/yaml v0.1.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q=
github.com/jdkato/prose v1.2.1 h1:Fp3UnJmLVISmlc57BgKUzdjr0lOtjqTZicL3PaYy6cU=
@ -493,6 +499,8 @@ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFR
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.10.1-0.20230508101108-a4f6fabd84c5 h1:Tb1D114RozKzV2dDfarvSZn8lVYvjcGSCDaMQ+b4I+E=
github.com/rogpeppe/go-internal v1.10.1-0.20230508101108-a4f6fabd84c5/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd h1:CmH9+J6ZSsIjUK3dcGsnCnO41eRBOnY12zwkn5qVwgc=
@ -510,6 +518,8 @@ github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA=
github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48=
github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA=
github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/fsync v0.9.0 h1:f9CEt3DOB2mnHxZaftmEOFWjABEvKM/xpf3cUwJrGOY=
github.com/spf13/fsync v0.9.0/go.mod h1:fNtJEfG3HiltN3y4cPOz6MLjos9+2pIEqLIgszqhp/0=
github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk=
@ -628,6 +638,8 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=

View File

@ -50,30 +50,18 @@ type ContentSpec struct {
anchorNameSanitizer converter.AnchorNameSanitizer
getRenderer func(t hooks.RendererType, id any) any
// SummaryLength is the length of the summary that Hugo extracts from a content.
summaryLength int
BuildFuture bool
BuildExpired bool
BuildDrafts bool
Cfg config.Provider
Cfg config.AllProvider
}
// NewContentSpec returns a ContentSpec initialized
// with the appropriate fields from the given config.Provider.
func NewContentSpec(cfg config.Provider, logger loggers.Logger, contentFs afero.Fs, ex *hexec.Exec) (*ContentSpec, error) {
func NewContentSpec(cfg config.AllProvider, logger loggers.Logger, contentFs afero.Fs, ex *hexec.Exec) (*ContentSpec, error) {
spec := &ContentSpec{
summaryLength: cfg.GetInt("summaryLength"),
BuildFuture: cfg.GetBool("buildFuture"),
BuildExpired: cfg.GetBool("buildExpired"),
BuildDrafts: cfg.GetBool("buildDrafts"),
Cfg: cfg,
}
converterProvider, err := markup.NewConverterProvider(converter.ProviderConfig{
Cfg: cfg,
Conf: cfg,
ContentFs: contentFs,
Logger: logger,
Exec: ex,
@ -157,6 +145,9 @@ func (c *ContentSpec) SanitizeAnchorName(s string) string {
}
func (c *ContentSpec) ResolveMarkup(in string) string {
if c == nil {
panic("nil ContentSpec")
}
in = strings.ToLower(in)
switch in {
case "md", "markdown", "mdown":
@ -194,17 +185,17 @@ func (c *ContentSpec) TruncateWordsByRune(in []string) (string, bool) {
count := 0
for index, word := range words {
if count >= c.summaryLength {
if count >= c.Cfg.SummaryLength() {
return strings.Join(words[:index], " "), true
}
runeCount := utf8.RuneCountInString(word)
if len(word) == runeCount {
count++
} else if count+runeCount < c.summaryLength {
} else if count+runeCount < c.Cfg.SummaryLength() {
count += runeCount
} else {
for ri := range word {
if count >= c.summaryLength {
if count >= c.Cfg.SummaryLength() {
truncatedWords := append(words[:index], word[:ri])
return strings.Join(truncatedWords, " "), true
}
@ -229,7 +220,7 @@ func (c *ContentSpec) TruncateWordsToWholeSentence(s string) (string, bool) {
wordCount++
lastWordIndex = i
if wordCount >= c.summaryLength {
if wordCount >= c.Cfg.SummaryLength() {
break
}
@ -283,19 +274,19 @@ func isEndOfSentence(r rune) bool {
func (c *ContentSpec) truncateWordsToWholeSentenceOld(content string) (string, bool) {
words := strings.Fields(content)
if c.summaryLength >= len(words) {
if c.Cfg.SummaryLength() >= len(words) {
return strings.Join(words, " "), false
}
for counter, word := range words[c.summaryLength:] {
for counter, word := range words[c.Cfg.SummaryLength():] {
if strings.HasSuffix(word, ".") ||
strings.HasSuffix(word, "?") ||
strings.HasSuffix(word, ".\"") ||
strings.HasSuffix(word, "!") {
upper := c.summaryLength + counter + 1
upper := c.Cfg.SummaryLength() + counter + 1
return strings.Join(words[:upper], " "), (upper < len(words))
}
}
return strings.Join(words[:c.summaryLength], " "), true
return strings.Join(words[:c.Cfg.SummaryLength()], " "), true
}

View File

@ -1,4 +1,4 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
// Copyright 2023 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package helpers
package helpers_test
import (
"bytes"
@ -19,12 +19,9 @@ import (
"strings"
"testing"
"github.com/spf13/afero"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/config"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/helpers"
)
const tstHTMLContent = "<!DOCTYPE html><html><head><script src=\"http://two/foobar.js\"></script></head><body><nav><ul><li hugo-nav=\"section_0\"></li><li hugo-nav=\"section_1\"></li></ul></nav><article>content <a href=\"http://two/foobar\">foobar</a>. Follow up</article><p>This is some text.<br>And some more.</p></body></html>"
@ -43,7 +40,7 @@ func TestTrimShortHTML(t *testing.T) {
{[]byte("<p>Hello</p>\n<ul>\n<li>list1</li>\n<li>list2</li>\n</ul>"), []byte("<p>Hello</p>\n<ul>\n<li>list1</li>\n<li>list2</li>\n</ul>")},
}
c := newTestContentSpec()
c := newTestContentSpec(nil)
for i, test := range tests {
output := c.TrimShortHTML(test.input)
if !bytes.Equal(test.output, output) {
@ -52,55 +49,23 @@ func TestTrimShortHTML(t *testing.T) {
}
}
func TestStripEmptyNav(t *testing.T) {
c := qt.New(t)
cleaned := stripEmptyNav([]byte("do<nav>\n</nav>\n\nbedobedo"))
c.Assert(cleaned, qt.DeepEquals, []byte("dobedobedo"))
}
func TestBytesToHTML(t *testing.T) {
c := qt.New(t)
c.Assert(BytesToHTML([]byte("dobedobedo")), qt.Equals, template.HTML("dobedobedo"))
}
func TestNewContentSpec(t *testing.T) {
cfg := config.NewWithTestDefaults()
c := qt.New(t)
cfg.Set("summaryLength", 32)
cfg.Set("buildFuture", true)
cfg.Set("buildExpired", true)
cfg.Set("buildDrafts", true)
spec, err := NewContentSpec(cfg, loggers.NewErrorLogger(), afero.NewMemMapFs(), nil)
c.Assert(err, qt.IsNil)
c.Assert(spec.summaryLength, qt.Equals, 32)
c.Assert(spec.BuildFuture, qt.Equals, true)
c.Assert(spec.BuildExpired, qt.Equals, true)
c.Assert(spec.BuildDrafts, qt.Equals, true)
c.Assert(helpers.BytesToHTML([]byte("dobedobedo")), qt.Equals, template.HTML("dobedobedo"))
}
var benchmarkTruncateString = strings.Repeat("This is a sentence about nothing.", 20)
func BenchmarkTestTruncateWordsToWholeSentence(b *testing.B) {
c := newTestContentSpec()
c := newTestContentSpec(nil)
b.ResetTimer()
for i := 0; i < b.N; i++ {
c.TruncateWordsToWholeSentence(benchmarkTruncateString)
}
}
func BenchmarkTestTruncateWordsToWholeSentenceOld(b *testing.B) {
c := newTestContentSpec()
b.ResetTimer()
for i := 0; i < b.N; i++ {
c.truncateWordsToWholeSentenceOld(benchmarkTruncateString)
}
}
func TestTruncateWordsToWholeSentence(t *testing.T) {
c := newTestContentSpec()
type test struct {
input, expected string
max int
@ -118,7 +83,9 @@ func TestTruncateWordsToWholeSentence(t *testing.T) {
{"This... is a more difficult test?", "This... is a more difficult test?", 1, false},
}
for i, d := range data {
c.summaryLength = d.max
cfg := config.New()
cfg.Set("summaryLength", d.max)
c := newTestContentSpec(cfg)
output, truncated := c.TruncateWordsToWholeSentence(d.input)
if d.expected != output {
t.Errorf("Test %d failed. Expected %q got %q", i, d.expected, output)
@ -131,7 +98,7 @@ func TestTruncateWordsToWholeSentence(t *testing.T) {
}
func TestTruncateWordsByRune(t *testing.T) {
c := newTestContentSpec()
type test struct {
input, expected string
max int
@ -153,7 +120,9 @@ func TestTruncateWordsByRune(t *testing.T) {
{" \nThis is not a sentence\n ", "This is not", 3, true},
}
for i, d := range data {
c.summaryLength = d.max
cfg := config.New()
cfg.Set("summaryLength", d.max)
c := newTestContentSpec(cfg)
output, truncated := c.TruncateWordsByRune(strings.Fields(d.input))
if d.expected != output {
t.Errorf("Test %d failed. Expected %q got %q", i, d.expected, output)
@ -168,7 +137,7 @@ func TestTruncateWordsByRune(t *testing.T) {
func TestExtractTOCNormalContent(t *testing.T) {
content := []byte("<nav>\n<ul>\nTOC<li><a href=\"#")
actualTocLessContent, actualToc := ExtractTOC(content)
actualTocLessContent, actualToc := helpers.ExtractTOC(content)
expectedTocLess := []byte("TOC<li><a href=\"#")
expectedToc := []byte("<nav id=\"TableOfContents\">\n<ul>\n")
@ -184,7 +153,7 @@ func TestExtractTOCNormalContent(t *testing.T) {
func TestExtractTOCGreaterThanSeventy(t *testing.T) {
content := []byte("<nav>\n<ul>\nTOC This is a very long content which will definitely be greater than seventy, I promise you that.<li><a href=\"#")
actualTocLessContent, actualToc := ExtractTOC(content)
actualTocLessContent, actualToc := helpers.ExtractTOC(content)
// Because the start of Toc is greater than 70+startpoint of <li> content and empty TOC will be returned
expectedToc := []byte("")
@ -200,7 +169,7 @@ func TestExtractTOCGreaterThanSeventy(t *testing.T) {
func TestExtractNoTOC(t *testing.T) {
content := []byte("TOC")
actualTocLessContent, actualToc := ExtractTOC(content)
actualTocLessContent, actualToc := helpers.ExtractTOC(content)
expectedToc := []byte("")
if !bytes.Equal(actualTocLessContent, content) {
@ -225,7 +194,7 @@ func TestTotalWords(t *testing.T) {
{"One, Two, Three", 3},
{totalWordsBenchmarkString, 400},
} {
actualWordCount := TotalWords(this.s)
actualWordCount := helpers.TotalWords(this.s)
if actualWordCount != this.words {
t.Errorf("[%d] Actual word count (%d) for test string (%s) did not match %d", i, actualWordCount, this.s, this.words)
@ -236,7 +205,7 @@ func TestTotalWords(t *testing.T) {
func BenchmarkTotalWords(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
wordCount := TotalWords(totalWordsBenchmarkString)
wordCount := helpers.TotalWords(totalWordsBenchmarkString)
if wordCount != 400 {
b.Fatal("Wordcount error")
}

View File

@ -43,20 +43,6 @@ import (
// FilePathSeparator as defined by os.Separator.
const FilePathSeparator = string(filepath.Separator)
// FindAvailablePort returns an available and valid TCP port.
func FindAvailablePort() (*net.TCPAddr, error) {
l, err := net.Listen("tcp", ":0")
if err == nil {
defer l.Close()
addr := l.Addr()
if a, ok := addr.(*net.TCPAddr); ok {
return a, nil
}
return nil, fmt.Errorf("unable to obtain a valid tcp port: %v", addr)
}
return nil, err
}
// TCPListen starts listening on a valid TCP port.
func TCPListen() (net.Listener, *net.TCPAddr, error) {
l, err := net.Listen("tcp", ":0")

View File

@ -1,4 +1,4 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
// Copyright 2023 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package helpers
package helpers_test
import (
"fmt"
@ -21,17 +21,14 @@ import (
"time"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/helpers"
qt "github.com/frankban/quicktest"
"github.com/spf13/afero"
)
func TestResolveMarkup(t *testing.T) {
c := qt.New(t)
cfg := config.NewWithTestDefaults()
spec, err := NewContentSpec(cfg, loggers.NewErrorLogger(), afero.NewMemMapFs(), nil)
c.Assert(err, qt.IsNil)
spec := newTestContentSpec(nil)
for i, this := range []struct {
in string
@ -61,7 +58,7 @@ func TestResolveMarkup(t *testing.T) {
func TestDistinctLoggerDoesNotLockOnWarningPanic(t *testing.T) {
// Testing to make sure logger mutex doesn't lock if warnings cause panics.
// func Warnf() of DistinctLogger is defined in general.go
l := NewDistinctLogger(loggers.NewWarningLogger())
l := helpers.NewDistinctLogger(loggers.NewWarningLogger())
// Set PanicOnWarning to true to reproduce issue 9380
// Ensure global variable loggers.PanicOnWarning is reset to old value after test
@ -123,7 +120,7 @@ func TestFirstUpper(t *testing.T) {
{"", ""},
{"å", "Å"},
} {
result := FirstUpper(this.in)
result := helpers.FirstUpper(this.in)
if result != this.expect {
t.Errorf("[%d] got %s but expected %s", i, result, this.expect)
}
@ -143,7 +140,7 @@ func TestHasStringsPrefix(t *testing.T) {
{[]string{"abra", "ca", "dabra"}, []string{"abra", "ca"}, true},
{[]string{"abra", "ca"}, []string{"abra", "ca", "dabra"}, false},
} {
result := HasStringsPrefix(this.s, this.prefix)
result := helpers.HasStringsPrefix(this.s, this.prefix)
if result != this.expect {
t.Fatalf("[%d] got %t but expected %t", i, result, this.expect)
}
@ -162,7 +159,7 @@ func TestHasStringsSuffix(t *testing.T) {
{[]string{"abra", "ca", "dabra"}, []string{"abra", "ca"}, false},
{[]string{"abra", "ca", "dabra"}, []string{"ca", "dabra"}, true},
} {
result := HasStringsSuffix(this.s, this.suffix)
result := helpers.HasStringsSuffix(this.s, this.suffix)
if result != this.expect {
t.Fatalf("[%d] got %t but expected %t", i, result, this.expect)
}
@ -239,7 +236,7 @@ func TestSliceToLower(t *testing.T) {
}
for _, test := range tests {
res := SliceToLower(test.value)
res := helpers.SliceToLower(test.value)
for i, val := range res {
if val != test.expected[i] {
t.Errorf("Case mismatch. Expected %s, got %s", test.expected[i], res[i])
@ -251,34 +248,34 @@ func TestSliceToLower(t *testing.T) {
func TestReaderContains(t *testing.T) {
c := qt.New(t)
for i, this := range append(containsBenchTestData, containsAdditionalTestData...) {
result := ReaderContains(strings.NewReader(this.v1), this.v2)
result := helpers.ReaderContains(strings.NewReader(this.v1), this.v2)
if result != this.expect {
t.Errorf("[%d] got %t but expected %t", i, result, this.expect)
}
}
c.Assert(ReaderContains(nil, []byte("a")), qt.Equals, false)
c.Assert(ReaderContains(nil, nil), qt.Equals, false)
c.Assert(helpers.ReaderContains(nil, []byte("a")), qt.Equals, false)
c.Assert(helpers.ReaderContains(nil, nil), qt.Equals, false)
}
func TestGetTitleFunc(t *testing.T) {
title := "somewhere over the rainbow"
c := qt.New(t)
c.Assert(GetTitleFunc("go")(title), qt.Equals, "Somewhere Over The Rainbow")
c.Assert(GetTitleFunc("chicago")(title), qt.Equals, "Somewhere over the Rainbow")
c.Assert(GetTitleFunc("Chicago")(title), qt.Equals, "Somewhere over the Rainbow")
c.Assert(GetTitleFunc("ap")(title), qt.Equals, "Somewhere Over the Rainbow")
c.Assert(GetTitleFunc("ap")(title), qt.Equals, "Somewhere Over the Rainbow")
c.Assert(GetTitleFunc("")(title), qt.Equals, "Somewhere Over the Rainbow")
c.Assert(GetTitleFunc("unknown")(title), qt.Equals, "Somewhere Over the Rainbow")
c.Assert(helpers.GetTitleFunc("go")(title), qt.Equals, "Somewhere Over The Rainbow")
c.Assert(helpers.GetTitleFunc("chicago")(title), qt.Equals, "Somewhere over the Rainbow")
c.Assert(helpers.GetTitleFunc("Chicago")(title), qt.Equals, "Somewhere over the Rainbow")
c.Assert(helpers.GetTitleFunc("ap")(title), qt.Equals, "Somewhere Over the Rainbow")
c.Assert(helpers.GetTitleFunc("ap")(title), qt.Equals, "Somewhere Over the Rainbow")
c.Assert(helpers.GetTitleFunc("")(title), qt.Equals, "Somewhere Over the Rainbow")
c.Assert(helpers.GetTitleFunc("unknown")(title), qt.Equals, "Somewhere Over the Rainbow")
}
func BenchmarkReaderContains(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
for i, this := range containsBenchTestData {
result := ReaderContains(strings.NewReader(this.v1), this.v2)
result := helpers.ReaderContains(strings.NewReader(this.v1), this.v2)
if result != this.expect {
b.Errorf("[%d] got %t but expected %t", i, result, this.expect)
}
@ -288,7 +285,7 @@ func BenchmarkReaderContains(b *testing.B) {
func TestUniqueStrings(t *testing.T) {
in := []string{"a", "b", "a", "b", "c", "", "a", "", "d"}
output := UniqueStrings(in)
output := helpers.UniqueStrings(in)
expected := []string{"a", "b", "c", "", "d"}
if !reflect.DeepEqual(output, expected) {
t.Errorf("Expected %#v, got %#v\n", expected, output)
@ -297,7 +294,7 @@ func TestUniqueStrings(t *testing.T) {
func TestUniqueStringsReuse(t *testing.T) {
in := []string{"a", "b", "a", "b", "c", "", "a", "", "d"}
output := UniqueStringsReuse(in)
output := helpers.UniqueStringsReuse(in)
expected := []string{"a", "b", "c", "", "d"}
if !reflect.DeepEqual(output, expected) {
t.Errorf("Expected %#v, got %#v\n", expected, output)
@ -307,18 +304,10 @@ func TestUniqueStringsReuse(t *testing.T) {
func TestUniqueStringsSorted(t *testing.T) {
c := qt.New(t)
in := []string{"a", "a", "b", "c", "b", "", "a", "", "d"}
output := UniqueStringsSorted(in)
output := helpers.UniqueStringsSorted(in)
expected := []string{"", "a", "b", "c", "d"}
c.Assert(output, qt.DeepEquals, expected)
c.Assert(UniqueStringsSorted(nil), qt.IsNil)
}
func TestFindAvailablePort(t *testing.T) {
c := qt.New(t)
addr, err := FindAvailablePort()
c.Assert(err, qt.IsNil)
c.Assert(addr, qt.Not(qt.IsNil))
c.Assert(addr.Port > 0, qt.Equals, true)
c.Assert(helpers.UniqueStringsSorted(nil), qt.IsNil)
}
func TestFastMD5FromFile(t *testing.T) {
@ -357,23 +346,23 @@ func TestFastMD5FromFile(t *testing.T) {
defer bf1.Close()
defer bf2.Close()
m1, err := MD5FromFileFast(sf1)
m1, err := helpers.MD5FromFileFast(sf1)
c.Assert(err, qt.IsNil)
c.Assert(m1, qt.Equals, "e9c8989b64b71a88b4efb66ad05eea96")
m2, err := MD5FromFileFast(sf2)
m2, err := helpers.MD5FromFileFast(sf2)
c.Assert(err, qt.IsNil)
c.Assert(m2, qt.Not(qt.Equals), m1)
m3, err := MD5FromFileFast(bf1)
m3, err := helpers.MD5FromFileFast(bf1)
c.Assert(err, qt.IsNil)
c.Assert(m3, qt.Not(qt.Equals), m2)
m4, err := MD5FromFileFast(bf2)
m4, err := helpers.MD5FromFileFast(bf2)
c.Assert(err, qt.IsNil)
c.Assert(m4, qt.Not(qt.Equals), m3)
m5, err := MD5FromReader(bf2)
m5, err := helpers.MD5FromReader(bf2)
c.Assert(err, qt.IsNil)
c.Assert(m5, qt.Not(qt.Equals), m4)
}
@ -394,11 +383,11 @@ func BenchmarkMD5FromFileFast(b *testing.B) {
}
b.StartTimer()
if full {
if _, err := MD5FromReader(f); err != nil {
if _, err := helpers.MD5FromReader(f); err != nil {
b.Fatal(err)
}
} else {
if _, err := MD5FromFileFast(f); err != nil {
if _, err := helpers.MD5FromFileFast(f); err != nil {
b.Fatal(err)
}
}
@ -413,7 +402,7 @@ func BenchmarkUniqueStrings(b *testing.B) {
b.Run("Safe", func(b *testing.B) {
for i := 0; i < b.N; i++ {
result := UniqueStrings(input)
result := helpers.UniqueStrings(input)
if len(result) != 6 {
b.Fatal(fmt.Sprintf("invalid count: %d", len(result)))
}
@ -432,7 +421,7 @@ func BenchmarkUniqueStrings(b *testing.B) {
for i := 0; i < b.N; i++ {
inputc := inputs[i]
result := UniqueStringsReuse(inputc)
result := helpers.UniqueStringsReuse(inputc)
if len(result) != 6 {
b.Fatal(fmt.Sprintf("invalid count: %d", len(result)))
}
@ -451,7 +440,7 @@ func BenchmarkUniqueStrings(b *testing.B) {
for i := 0; i < b.N; i++ {
inputc := inputs[i]
result := UniqueStringsSorted(inputc)
result := helpers.UniqueStringsSorted(inputc)
if len(result) != 6 {
b.Fatal(fmt.Sprintf("invalid count: %d", len(result)))
}

View File

@ -1,4 +1,4 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
// Copyright 2023 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -28,8 +28,6 @@ import (
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/text"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/common/hugio"
@ -54,7 +52,7 @@ func (p *PathSpec) MakePathsSanitized(paths []string) {
// MakePathSanitized creates a Unicode-sanitized string, with the spaces replaced
func (p *PathSpec) MakePathSanitized(s string) string {
if p.DisablePathToLower {
if p.Cfg.DisablePathToLower() {
return p.MakePath(s)
}
return strings.ToLower(p.MakePath(s))
@ -91,7 +89,7 @@ func ishex(c rune) bool {
// Hyphens in the original input are maintained.
// Spaces will be replaced with a single hyphen, and sequential replacement hyphens will be reduced to one.
func (p *PathSpec) UnicodeSanitize(s string) string {
if p.RemovePathAccents {
if p.Cfg.RemovePathAccents() {
s = text.RemoveAccentsString(s)
}
@ -128,7 +126,7 @@ func (p *PathSpec) UnicodeSanitize(s string) string {
return string(target)
}
func makePathRelative(inPath string, possibleDirectories ...string) (string, error) {
func MakePathRelative(inPath string, possibleDirectories ...string) (string, error) {
for _, currentPath := range possibleDirectories {
if strings.HasPrefix(inPath, currentPath) {
return strings.TrimPrefix(inPath, currentPath), nil
@ -394,8 +392,8 @@ func OpenFileForWriting(fs afero.Fs, filename string) (afero.File, error) {
// GetCacheDir returns a cache dir from the given filesystem and config.
// The dir will be created if it does not exist.
func GetCacheDir(fs afero.Fs, cfg config.Provider) (string, error) {
cacheDir := getCacheDir(cfg)
func GetCacheDir(fs afero.Fs, cacheDir string) (string, error) {
cacheDir = cacheDirDefault(cacheDir)
if cacheDir != "" {
exists, err := DirExists(cacheDir, fs)
if err != nil {
@ -414,9 +412,8 @@ func GetCacheDir(fs afero.Fs, cfg config.Provider) (string, error) {
return GetTempDir("hugo_cache", fs), nil
}
func getCacheDir(cfg config.Provider) string {
func cacheDirDefault(cacheDir string) string {
// Always use the cacheDir config if set.
cacheDir := cfg.GetString("cacheDir")
if len(cacheDir) > 1 {
return addTrailingFileSeparator(cacheDir)
}

View File

@ -1,4 +1,4 @@
// Copyright 2015 The Hugo Authors. All rights reserved.
// Copyright 2023 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -11,7 +11,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package helpers
package helpers_test
import (
"fmt"
@ -24,16 +24,12 @@ import (
"testing"
"time"
"github.com/gohugoio/hugo/langs"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/helpers"
"github.com/spf13/afero"
)
func TestMakePath(t *testing.T) {
c := qt.New(t)
tests := []struct {
input string
expected string
@ -60,13 +56,7 @@ func TestMakePath(t *testing.T) {
}
for _, test := range tests {
v := newTestCfg()
v.Set("removePathAccents", test.removeAccents)
l := langs.NewDefaultLanguage(v)
p, err := NewPathSpec(hugofs.NewMem(v), l, nil)
c.Assert(err, qt.IsNil)
p := newTestPathSpec("removePathAccents", test.removeAccents)
output := p.MakePath(test.input)
if output != test.expected {
t.Errorf("Expected %#v, got %#v\n", test.expected, output)
@ -75,9 +65,7 @@ func TestMakePath(t *testing.T) {
}
func TestMakePathSanitized(t *testing.T) {
v := newTestCfg()
p, _ := NewPathSpec(hugofs.NewMem(v), v, nil)
p := newTestPathSpec()
tests := []struct {
input string
@ -100,12 +88,7 @@ func TestMakePathSanitized(t *testing.T) {
}
func TestMakePathSanitizedDisablePathToLower(t *testing.T) {
v := newTestCfg()
v.Set("disablePathToLower", true)
l := langs.NewDefaultLanguage(v)
p, _ := NewPathSpec(hugofs.NewMem(v), l, nil)
p := newTestPathSpec("disablePathToLower", true)
tests := []struct {
input string
@ -138,12 +121,12 @@ func TestMakePathRelative(t *testing.T) {
}
for i, d := range data {
output, _ := makePathRelative(d.inPath, d.path1, d.path2)
output, _ := helpers.MakePathRelative(d.inPath, d.path1, d.path2)
if d.output != output {
t.Errorf("Test #%d failed. Expected %q got %q", i, d.output, output)
}
}
_, error := makePathRelative("a/b/c.ss", "/a/c", "/d/c", "/e/f")
_, error := helpers.MakePathRelative("a/b/c.ss", "/a/c", "/d/c", "/e/f")
if error == nil {
t.Errorf("Test failed, expected error")
@ -181,7 +164,7 @@ func doTestGetDottedRelativePath(urlFixer func(string) string, t *testing.T) {
{"/404.html", "./"},
}
for i, d := range data {
output := GetDottedRelativePath(d.input)
output := helpers.GetDottedRelativePath(d.input)
if d.expected != output {
t.Errorf("Test %d failed. Expected %q got %q", i, d.expected, output)
}
@ -198,7 +181,7 @@ func TestMakeTitle(t *testing.T) {
{"make_title", "make_title"},
}
for i, d := range data {
output := MakeTitle(d.input)
output := helpers.MakeTitle(d.input)
if d.expected != output {
t.Errorf("Test %d failed. Expected %q got %q", i, d.expected, output)
}
@ -219,7 +202,7 @@ func TestDirExists(t *testing.T) {
{"./..", true},
{"./../", true},
{os.TempDir(), true},
{os.TempDir() + FilePathSeparator, true},
{os.TempDir() + helpers.FilePathSeparator, true},
{"/", true},
{"/some-really-random-directory-name", false},
{"/some/really/random/directory/name", false},
@ -228,7 +211,7 @@ func TestDirExists(t *testing.T) {
}
for i, d := range data {
exists, _ := DirExists(filepath.FromSlash(d.input), new(afero.OsFs))
exists, _ := helpers.DirExists(filepath.FromSlash(d.input), new(afero.OsFs))
if d.expected != exists {
t.Errorf("Test %d failed. Expected %t got %t", i, d.expected, exists)
}
@ -249,7 +232,7 @@ func TestIsDir(t *testing.T) {
for i, d := range data {
exists, _ := IsDir(d.input, new(afero.OsFs))
exists, _ := helpers.IsDir(d.input, new(afero.OsFs))
if d.expected != exists {
t.Errorf("Test %d failed. Expected %t got %t", i, d.expected, exists)
}
@ -310,7 +293,7 @@ func TestExists(t *testing.T) {
{nonExistentDir, false, nil},
}
for i, d := range data {
exists, err := Exists(d.input, new(afero.OsFs))
exists, err := helpers.Exists(d.input, new(afero.OsFs))
if d.expectedResult != exists {
t.Errorf("Test %d failed. Expected result %t got %t", i, d.expectedResult, exists)
}
@ -341,7 +324,7 @@ func TestAbsPathify(t *testing.T) {
for i, d := range data {
// todo see comment in AbsPathify
ps := newTestDefaultPathSpec("workingDir", d.workingDir)
ps := newTestPathSpec("workingDir", d.workingDir)
expected := ps.AbsPathify(d.inPath)
if d.expected != expected {
@ -351,7 +334,7 @@ func TestAbsPathify(t *testing.T) {
t.Logf("Running platform specific path tests for %s", runtime.GOOS)
if runtime.GOOS == "windows" {
for i, d := range windowsData {
ps := newTestDefaultPathSpec("workingDir", d.workingDir)
ps := newTestPathSpec("workingDir", d.workingDir)
expected := ps.AbsPathify(d.inPath)
if d.expected != expected {
@ -360,7 +343,7 @@ func TestAbsPathify(t *testing.T) {
}
} else {
for i, d := range unixData {
ps := newTestDefaultPathSpec("workingDir", d.workingDir)
ps := newTestPathSpec("workingDir", d.workingDir)
expected := ps.AbsPathify(d.inPath)
if d.expected != expected {
@ -383,7 +366,7 @@ func TestExtractAndGroupRootPaths(t *testing.T) {
inCopy := make([]string, len(in))
copy(inCopy, in)
result := ExtractAndGroupRootPaths(in)
result := helpers.ExtractAndGroupRootPaths(in)
c := qt.New(t)
c.Assert(fmt.Sprint(result), qt.Equals, filepath.FromSlash("[/a/b/{c,e} /c/d/e]"))
@ -405,7 +388,7 @@ func TestExtractRootPaths(t *testing.T) {
}}
for _, test := range tests {
output := ExtractRootPaths(test.input)
output := helpers.ExtractRootPaths(test.input)
if !reflect.DeepEqual(output, test.expected) {
t.Errorf("Expected %#v, got %#v\n", test.expected, output)
}
@ -426,7 +409,7 @@ func TestFindCWD(t *testing.T) {
// I really don't know a better way to test this function. - SPF 2014.11.04
}
for i, d := range data {
dir, err := FindCWD()
dir, err := helpers.FindCWD()
if d.expectedDir != dir {
t.Errorf("Test %d failed. Expected %q but got %q", i, d.expectedDir, dir)
}
@ -459,7 +442,7 @@ func TestSafeWriteToDisk(t *testing.T) {
}
for i, d := range data {
e := SafeWriteToDisk(d.filename, reader, new(afero.OsFs))
e := helpers.SafeWriteToDisk(d.filename, reader, new(afero.OsFs))
if d.expectedErr != nil {
if d.expectedErr.Error() != e.Error() {
t.Errorf("Test %d failed. Expected error %q but got %q", i, d.expectedErr.Error(), e.Error())
@ -498,7 +481,7 @@ func TestWriteToDisk(t *testing.T) {
}
for i, d := range data {
e := WriteToDisk(d.filename, reader, new(afero.OsFs))
e := helpers.WriteToDisk(d.filename, reader, new(afero.OsFs))
if d.expectedErr != e {
t.Errorf("Test %d failed. WriteToDisk Error Expected %q but got %q", i, d.expectedErr, e)
}
@ -515,27 +498,27 @@ func TestWriteToDisk(t *testing.T) {
func TestGetTempDir(t *testing.T) {
dir := os.TempDir()
if FilePathSeparator != dir[len(dir)-1:] {
dir = dir + FilePathSeparator
if helpers.FilePathSeparator != dir[len(dir)-1:] {
dir = dir + helpers.FilePathSeparator
}
testDir := "hugoTestFolder" + FilePathSeparator
testDir := "hugoTestFolder" + helpers.FilePathSeparator
tests := []struct {
input string
expected string
}{
{"", dir},
{testDir + " Foo bar ", dir + testDir + " Foo bar " + FilePathSeparator},
{testDir + "Foo.Bar/foo_Bar-Foo", dir + testDir + "Foo.Bar/foo_Bar-Foo" + FilePathSeparator},
{testDir + "fOO,bar:foo%bAR", dir + testDir + "fOObarfoo%bAR" + FilePathSeparator},
{testDir + "fOO,bar:foobAR", dir + testDir + "fOObarfoobAR" + FilePathSeparator},
{testDir + "FOo/BaR.html", dir + testDir + "FOo/BaR.html" + FilePathSeparator},
{testDir + "трям/трям", dir + testDir + "трям/трям" + FilePathSeparator},
{testDir + "은행", dir + testDir + "은행" + FilePathSeparator},
{testDir + "Банковский кассир", dir + testDir + "Банковский кассир" + FilePathSeparator},
{testDir + " Foo bar ", dir + testDir + " Foo bar " + helpers.FilePathSeparator},
{testDir + "Foo.Bar/foo_Bar-Foo", dir + testDir + "Foo.Bar/foo_Bar-Foo" + helpers.FilePathSeparator},
{testDir + "fOO,bar:foo%bAR", dir + testDir + "fOObarfoo%bAR" + helpers.FilePathSeparator},
{testDir + "fOO,bar:foobAR", dir + testDir + "fOObarfoobAR" + helpers.FilePathSeparator},
{testDir + "FOo/BaR.html", dir + testDir + "FOo/BaR.html" + helpers.FilePathSeparator},
{testDir + "трям/трям", dir + testDir + "трям/трям" + helpers.FilePathSeparator},
{testDir + "은행", dir + testDir + "은행" + helpers.FilePathSeparator},
{testDir + "Банковский кассир", dir + testDir + "Банковский кассир" + helpers.FilePathSeparator},
}
for _, test := range tests {
output := GetTempDir(test.input, new(afero.MemMapFs))
output := helpers.GetTempDir(test.input, new(afero.MemMapFs))
if output != test.expected {
t.Errorf("Expected %#v, got %#v\n", test.expected, output)
}

View File

@ -34,17 +34,17 @@ type PathSpec struct {
Fs *hugofs.Fs
// The config provider to use
Cfg config.Provider
Cfg config.AllProvider
}
// NewPathSpec creates a new PathSpec from the given filesystems and language.
func NewPathSpec(fs *hugofs.Fs, cfg config.Provider, logger loggers.Logger) (*PathSpec, error) {
func NewPathSpec(fs *hugofs.Fs, cfg config.AllProvider, logger loggers.Logger) (*PathSpec, error) {
return NewPathSpecWithBaseBaseFsProvided(fs, cfg, logger, nil)
}
// NewPathSpecWithBaseBaseFsProvided creates a new PathSpec from the given filesystems and language.
// If an existing BaseFs is provided, parts of that is reused.
func NewPathSpecWithBaseBaseFsProvided(fs *hugofs.Fs, cfg config.Provider, logger loggers.Logger, baseBaseFs *filesystems.BaseFs) (*PathSpec, error) {
func NewPathSpecWithBaseBaseFsProvided(fs *hugofs.Fs, cfg config.AllProvider, logger loggers.Logger, baseBaseFs *filesystems.BaseFs) (*PathSpec, error) {
p, err := paths.New(fs, cfg)
if err != nil {
return nil, err
@ -69,11 +69,6 @@ func NewPathSpecWithBaseBaseFsProvided(fs *hugofs.Fs, cfg config.Provider, logge
ProcessingStats: NewProcessingStats(p.Lang()),
}
basePath := ps.BaseURL.Path()
if basePath != "" && basePath != "/" {
ps.BasePath = basePath
}
return ps, nil
}

View File

@ -1,62 +0,0 @@
// Copyright 2018 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package helpers
import (
"path/filepath"
"testing"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/langs"
)
func TestNewPathSpecFromConfig(t *testing.T) {
c := qt.New(t)
v := newTestCfg()
l := langs.NewLanguage("no", v)
v.Set("disablePathToLower", true)
v.Set("removePathAccents", true)
v.Set("uglyURLs", true)
v.Set("canonifyURLs", true)
v.Set("paginatePath", "side")
v.Set("baseURL", "http://base.com/foo")
v.Set("themesDir", "thethemes")
v.Set("layoutDir", "thelayouts")
v.Set("workingDir", "thework")
v.Set("staticDir", "thestatic")
v.Set("theme", "thetheme")
langs.LoadLanguageSettings(v, nil)
fs := hugofs.NewMem(v)
fs.Source.MkdirAll(filepath.FromSlash("thework/thethemes/thetheme"), 0777)
p, err := NewPathSpec(fs, l, nil)
c.Assert(err, qt.IsNil)
c.Assert(p.CanonifyURLs, qt.Equals, true)
c.Assert(p.DisablePathToLower, qt.Equals, true)
c.Assert(p.RemovePathAccents, qt.Equals, true)
c.Assert(p.UglyURLs, qt.Equals, true)
c.Assert(p.Language.Lang, qt.Equals, "no")
c.Assert(p.PaginatePath, qt.Equals, "side")
c.Assert(p.BaseURL.String(), qt.Equals, "http://base.com/foo")
c.Assert(p.BaseURLString, qt.Equals, "http://base.com/foo")
c.Assert(p.BaseURLNoPathString, qt.Equals, "http://base.com")
c.Assert(p.ThemesDir, qt.Equals, "thethemes")
c.Assert(p.WorkingDir, qt.Equals, "thework")
}

View File

@ -1,47 +1,47 @@
package helpers
package helpers_test
import (
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/config"
"github.com/spf13/afero"
"github.com/gohugoio/hugo/config/testconfig"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/langs"
"github.com/gohugoio/hugo/modules"
"github.com/spf13/afero"
)
func newTestPathSpec(fs *hugofs.Fs, v config.Provider) *PathSpec {
l := langs.NewDefaultLanguage(v)
ps, _ := NewPathSpec(fs, l, nil)
return ps
}
func newTestPathSpecFromCfgAndLang(cfg config.Provider, lang string) *helpers.PathSpec {
mfs := afero.NewMemMapFs()
func newTestDefaultPathSpec(configKeyValues ...any) *PathSpec {
cfg := newTestCfg()
fs := hugofs.NewMem(cfg)
for i := 0; i < len(configKeyValues); i += 2 {
cfg.Set(configKeyValues[i].(string), configKeyValues[i+1])
configs := testconfig.GetTestConfigs(mfs, cfg)
var conf config.AllProvider
if lang == "" {
conf = configs.GetFirstLanguageConfig()
} else {
conf = configs.GetByLang(lang)
if conf == nil {
panic("no config for lang " + lang)
}
}
return newTestPathSpec(fs, cfg)
}
func newTestCfg() config.Provider {
v := config.NewWithTestDefaults()
langs.LoadLanguageSettings(v, nil)
langs.LoadLanguageSettings(v, nil)
mod, err := modules.CreateProjectModule(v)
fs := hugofs.NewFrom(mfs, conf.BaseConfig())
ps, err := helpers.NewPathSpec(fs, conf, loggers.NewErrorLogger())
if err != nil {
panic(err)
}
v.Set("allModules", modules.Modules{mod})
return v
return ps
}
func newTestContentSpec() *ContentSpec {
v := config.NewWithTestDefaults()
spec, err := NewContentSpec(v, loggers.NewErrorLogger(), afero.NewMemMapFs(), nil)
func newTestPathSpec(configKeyValues ...any) *helpers.PathSpec {
cfg := config.New()
for i := 0; i < len(configKeyValues); i += 2 {
cfg.Set(configKeyValues[i].(string), configKeyValues[i+1])
}
return newTestPathSpecFromCfgAndLang(cfg, "")
}
func newTestContentSpec(cfg config.Provider) *helpers.ContentSpec {
fs := afero.NewMemMapFs()
conf := testconfig.GetTestConfig(fs, cfg)
spec, err := helpers.NewContentSpec(conf, loggers.NewErrorLogger(), fs, nil)
if err != nil {
panic(err)
}

View File

@ -71,8 +71,9 @@ func SanitizeURLKeepTrailingSlash(in string) string {
// URLize is similar to MakePath, but with Unicode handling
// Example:
// uri: Vim (text editor)
// urlize: vim-text-editor
//
// uri: Vim (text editor)
// urlize: vim-text-editor
func (p *PathSpec) URLize(uri string) string {
return p.URLEscape(p.MakePathSanitized(uri))
}
@ -141,16 +142,16 @@ func (p *PathSpec) AbsURL(in string, addLanguage bool) string {
func (p *PathSpec) getBaseURLRoot(path string) string {
if strings.HasPrefix(path, "/") {
// Treat it as relative to the server root.
return p.BaseURLNoPathString
return p.Cfg.BaseURL().WithoutPath
} else {
// Treat it as relative to the baseURL.
return p.BaseURLString
return p.Cfg.BaseURL().WithPath
}
}
func (p *PathSpec) RelURL(in string, addLanguage bool) string {
baseURL := p.getBaseURLRoot(in)
canonifyURLs := p.CanonifyURLs
canonifyURLs := p.Cfg.CanonifyURLs()
if (!strings.HasPrefix(in, baseURL) && strings.HasPrefix(in, "http")) || strings.HasPrefix(in, "//") {
return in
}
@ -217,25 +218,3 @@ func (p *PathSpec) PrependBasePath(rel string, isAbs bool) string {
}
return rel
}
// URLizeAndPrep applies misc sanitation to the given URL to get it in line
// with the Hugo standard.
func (p *PathSpec) URLizeAndPrep(in string) string {
return p.URLPrep(p.URLize(in))
}
// URLPrep applies misc sanitation to the given URL.
func (p *PathSpec) URLPrep(in string) string {
if p.UglyURLs {
return paths.Uglify(SanitizeURL(in))
}
pretty := paths.PrettifyURL(SanitizeURL(in))
if path.Ext(pretty) == ".xml" {
return pretty
}
url, err := purell.NormalizeURLString(pretty, purell.FlagAddTrailingSlash)
if err != nil {
return pretty
}
return url
}

View File

@ -1,4 +1,4 @@
// Copyright 2015 The Hugo Authors. All rights reserved.
// Copyright 2023 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@ -11,21 +11,20 @@
// See the License for the specific language governing permissions and
// limitations under the License.
package helpers
package helpers_test
import (
"fmt"
"strings"
"testing"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/langs"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/helpers"
)
func TestURLize(t *testing.T) {
v := newTestCfg()
l := langs.NewDefaultLanguage(v)
p, _ := NewPathSpec(hugofs.NewMem(v), l, nil)
p := newTestPathSpec()
tests := []struct {
input string
@ -61,10 +60,6 @@ func TestAbsURL(t *testing.T) {
func doTestAbsURL(t *testing.T, defaultInSubDir, addLanguage, multilingual bool, lang string) {
c := qt.New(t)
v := newTestCfg()
v.Set("multilingual", multilingual)
v.Set("defaultContentLanguage", "en")
v.Set("defaultContentLanguageInSubdir", defaultInSubDir)
tests := []struct {
input string
@ -103,24 +98,42 @@ func doTestAbsURL(t *testing.T, defaultInSubDir, addLanguage, multilingual bool,
}
for _, test := range tests {
v.Set("baseURL", test.baseURL)
v.Set("contentDir", "content")
l := langs.NewLanguage(lang, v)
p, _ := NewPathSpec(hugofs.NewMem(v), l, nil)
output := p.AbsURL(test.input, addLanguage)
expected := test.expected
if multilingual && addLanguage {
if !defaultInSubDir && lang == "en" {
expected = strings.Replace(expected, "MULTI", "", 1)
} else {
expected = strings.Replace(expected, "MULTI", lang+"/", 1)
c.Run(fmt.Sprintf("%v/%t-%t-%t/%s", test, defaultInSubDir, addLanguage, multilingual, lang), func(c *qt.C) {
v := config.New()
if multilingual {
v.Set("languages", map[string]any{
"fr": map[string]interface{}{
"weight": 20,
},
"en": map[string]interface{}{
"weight": 10,
},
})
}
} else {
expected = strings.Replace(expected, "MULTI", "", 1)
}
v.Set("defaultContentLanguage", "en")
v.Set("defaultContentLanguageInSubdir", defaultInSubDir)
v.Set("baseURL", test.baseURL)
c.Assert(output, qt.Equals, expected)
var configLang string
if multilingual {
configLang = lang
}
p := newTestPathSpecFromCfgAndLang(v, configLang)
output := p.AbsURL(test.input, addLanguage)
expected := test.expected
if multilingual && addLanguage {
if !defaultInSubDir && lang == "en" {
expected = strings.Replace(expected, "MULTI", "", 1)
} else {
expected = strings.Replace(expected, "MULTI", lang+"/", 1)
}
} else {
expected = strings.Replace(expected, "MULTI", "", 1)
}
c.Assert(output, qt.Equals, expected)
})
}
}
@ -137,9 +150,19 @@ func TestRelURL(t *testing.T) {
}
func doTestRelURL(t *testing.T, defaultInSubDir, addLanguage, multilingual bool, lang string) {
t.Helper()
c := qt.New(t)
v := newTestCfg()
v.Set("multilingual", multilingual)
v := config.New()
if multilingual {
v.Set("languages", map[string]any{
"fr": map[string]interface{}{
"weight": 20,
},
"en": map[string]interface{}{
"weight": 10,
},
})
}
v.Set("defaultContentLanguage", "en")
v.Set("defaultContentLanguageInSubdir", defaultInSubDir)
@ -182,25 +205,31 @@ func doTestRelURL(t *testing.T, defaultInSubDir, addLanguage, multilingual bool,
}
for i, test := range tests {
v.Set("baseURL", test.baseURL)
v.Set("canonifyURLs", test.canonify)
l := langs.NewLanguage(lang, v)
p, _ := NewPathSpec(hugofs.NewMem(v), l, nil)
c.Run(fmt.Sprintf("%v/%t%t%t/%s", test, defaultInSubDir, addLanguage, multilingual, lang), func(c *qt.C) {
output := p.RelURL(test.input, addLanguage)
expected := test.expected
if multilingual && addLanguage {
if !defaultInSubDir && lang == "en" {
expected = strings.Replace(expected, "MULTI", "", 1)
} else {
expected = strings.Replace(expected, "MULTI", "/"+lang, 1)
v.Set("baseURL", test.baseURL)
v.Set("canonifyURLs", test.canonify)
var configLang string
if multilingual {
configLang = lang
}
} else {
expected = strings.Replace(expected, "MULTI", "", 1)
}
p := newTestPathSpecFromCfgAndLang(v, configLang)
c.Assert(output, qt.Equals, expected, qt.Commentf("[%d] %s", i, test.input))
output := p.RelURL(test.input, addLanguage)
expected := test.expected
if multilingual && addLanguage {
if !defaultInSubDir && lang == "en" {
expected = strings.Replace(expected, "MULTI", "", 1)
} else {
expected = strings.Replace(expected, "MULTI", "/"+lang, 1)
}
} else {
expected = strings.Replace(expected, "MULTI", "", 1)
}
c.Assert(output, qt.Equals, expected, qt.Commentf("[%d] %s", i, test.input))
})
}
}
@ -216,8 +245,8 @@ func TestSanitizeURL(t *testing.T) {
}
for i, test := range tests {
o1 := SanitizeURL(test.input)
o2 := SanitizeURLKeepTrailingSlash(test.input)
o1 := helpers.SanitizeURL(test.input)
o2 := helpers.SanitizeURLKeepTrailingSlash(test.input)
expected2 := test.expected
@ -233,28 +262,3 @@ func TestSanitizeURL(t *testing.T) {
}
}
}
func TestURLPrep(t *testing.T) {
type test struct {
ugly bool
input string
output string
}
data := []test{
{false, "/section/name.html", "/section/name/"},
{true, "/section/name/index.html", "/section/name.html"},
}
for i, d := range data {
v := newTestCfg()
v.Set("uglyURLs", d.ugly)
l := langs.NewDefaultLanguage(v)
p, _ := NewPathSpec(hugofs.NewMem(v), l, nil)
output := p.URLPrep(d.input)
if d.output != output {
t.Errorf("Test #%d failed. Expected %q got %q", i, d.output, output)
}
}
}

View File

@ -62,39 +62,55 @@ type Fs struct {
// NewDefault creates a new Fs with the OS file system
// as source and destination file systems.
func NewDefault(cfg config.Provider) *Fs {
func NewDefault(conf config.BaseConfig) *Fs {
fs := Os
return newFs(fs, fs, cfg)
return NewFrom(fs, conf)
}
// NewMem creates a new Fs with the MemMapFs
// as source and destination file systems.
// Useful for testing.
func NewMem(cfg config.Provider) *Fs {
fs := &afero.MemMapFs{}
return newFs(fs, fs, cfg)
func NewDefaultOld(cfg config.Provider) *Fs {
workingDir, publishDir := getWorkingPublishDir(cfg)
fs := Os
return newFs(fs, fs, workingDir, publishDir)
}
// NewFrom creates a new Fs based on the provided Afero Fs
// as source and destination file systems.
// Useful for testing.
func NewFrom(fs afero.Fs, cfg config.Provider) *Fs {
return newFs(fs, fs, cfg)
func NewFrom(fs afero.Fs, conf config.BaseConfig) *Fs {
return newFs(fs, fs, conf.WorkingDir, conf.PublishDir)
}
func NewFromOld(fs afero.Fs, cfg config.Provider) *Fs {
workingDir, publishDir := getWorkingPublishDir(cfg)
return newFs(fs, fs, workingDir, publishDir)
}
// NewFrom creates a new Fs based on the provided Afero Fss
// as the source and destination file systems.
func NewFromSourceAndDestination(source, destination afero.Fs, cfg config.Provider) *Fs {
return newFs(source, destination, cfg)
workingDir, publishDir := getWorkingPublishDir(cfg)
return newFs(source, destination, workingDir, publishDir)
}
func newFs(source, destination afero.Fs, cfg config.Provider) *Fs {
func getWorkingPublishDir(cfg config.Provider) (string, string) {
workingDir := cfg.GetString("workingDir")
publishDir := cfg.GetString("publishDir")
publishDir := cfg.GetString("publishDirDynamic")
if publishDir == "" {
publishDir = cfg.GetString("publishDir")
}
return workingDir, publishDir
}
func newFs(source, destination afero.Fs, workingDir, publishDir string) *Fs {
if publishDir == "" {
panic("publishDir is empty")
}
if workingDir == "." {
workingDir = ""
}
// Sanity check
if IsOsFs(source) && len(workingDir) < 2 {
panic("workingDir is too short")
@ -158,6 +174,7 @@ func MakeReadableAndRemoveAllModulePkgDir(fs afero.Fs, dir string) (int, error)
}
return nil
})
return counter, fs.RemoveAll(dir)
}

View File

@ -35,9 +35,10 @@ func TestIsOsFs(t *testing.T) {
func TestNewDefault(t *testing.T) {
c := qt.New(t)
v := config.NewWithTestDefaults()
v := config.New()
v.Set("workingDir", t.TempDir())
f := NewDefault(v)
v.Set("publishDir", "public")
f := NewDefaultOld(v)
c.Assert(f.Source, qt.IsNotNil)
c.Assert(f.Source, hqt.IsSameType, new(afero.OsFs))
@ -49,20 +50,3 @@ func TestNewDefault(t *testing.T) {
c.Assert(IsOsFs(f.PublishDir), qt.IsTrue)
c.Assert(IsOsFs(f.Os), qt.IsTrue)
}
func TestNewMem(t *testing.T) {
c := qt.New(t)
v := config.NewWithTestDefaults()
f := NewMem(v)
c.Assert(f.Source, qt.Not(qt.IsNil))
c.Assert(f.Source, hqt.IsSameType, new(afero.MemMapFs))
c.Assert(f.PublishDir, qt.Not(qt.IsNil))
c.Assert(f.PublishDir, hqt.IsSameType, new(afero.BasePathFs))
c.Assert(f.Os, hqt.IsSameType, new(afero.OsFs))
c.Assert(f.WorkingDirReadOnly, qt.IsNotNil)
c.Assert(IsOsFs(f.Source), qt.IsFalse)
c.Assert(IsOsFs(f.WorkingDirReadOnly), qt.IsFalse)
c.Assert(IsOsFs(f.PublishDir), qt.IsFalse)
c.Assert(IsOsFs(f.Os), qt.IsTrue)
}

View File

@ -34,7 +34,7 @@ type noOpFs struct {
}
func (fs noOpFs) Create(name string) (afero.File, error) {
return nil, errNoOp
panic(errNoOp)
}
func (fs noOpFs) Mkdir(name string, perm os.FileMode) error {
@ -62,7 +62,7 @@ func (fs noOpFs) RemoveAll(path string) error {
}
func (fs noOpFs) Rename(oldname string, newname string) error {
return errNoOp
panic(errNoOp)
}
func (fs noOpFs) Stat(name string) (os.FileInfo, error) {
@ -74,13 +74,13 @@ func (fs noOpFs) Name() string {
}
func (fs noOpFs) Chmod(name string, mode os.FileMode) error {
return errNoOp
panic(errNoOp)
}
func (fs noOpFs) Chtimes(name string, atime time.Time, mtime time.Time) error {
return errNoOp
panic(errNoOp)
}
func (fs *noOpFs) Chown(name string, uid int, gid int) error {
return errNoOp
panic(errNoOp)
}

View File

@ -30,7 +30,7 @@ import (
func TestLanguageRootMapping(t *testing.T) {
c := qt.New(t)
v := config.NewWithTestDefaults()
v := config.New()
v.Set("contentDir", "content")
fs := NewBaseFileDecorator(afero.NewMemMapFs())

Some files were not shown because too many files have changed in this diff Show More