ref: 7829474088f835251f04caa1121d47e35fe89f7e
parent: 256418917c6642f7e5b3d3206ff4b6fa03b1cb28
author: Bjørn Erik Pedersen <[email protected]>
date: Thu Nov 15 04:28:02 EST 2018
Add /config dir support This commit adds support for a configuration directory (default `config`). The different pieces in this puzzle are: * A new `--environment` (or `-e`) flag. This can also be set with the `HUGO_ENVIRONMENT` OS environment variable. The value for `environment` defaults to `production` when running `hugo` and `development` when running `hugo server`. You can set it to any value you want (e.g. `hugo server -e "Sensible Environment"`), but as it is used to load configuration from the file system, the letter case may be important. You can get this value in your templates with `{{ hugo.Environment }}`. * A new `--configDir` flag (defaults to `config` below your project). This can also be set with `HUGO_CONFIGDIR` OS environment variable. If the `configDir` exists, the configuration files will be read and merged on top of each other from left to right; the right-most value will win on duplicates. Given the example tree below: If `environment` is `production`, the left-most `config.toml` would be the one directly below the project (this can now be omitted if you want), and then `_default/config.toml` and finally `production/config.toml`. And since these will be merged, you can just provide the environment specific configuration setting in you production config, e.g. `enableGitInfo = true`. The order within the directories will be lexical (`config.toml` and then `params.toml`). ```bash config ├── _default │ ├── config.toml │ ├── languages.toml │ ├── menus │ │ ├── menus.en.toml │ │ └── menus.zh.toml │ └── params.toml ├── development │ └── params.toml └── production ├── config.toml └── params.toml ``` Some configuration maps support the language code in the filename (e.g. `menus.en.toml`): `menus` (`menu` also works) and `params`. Also note that the only folders with "a meaning" in the above listing is the top level directories below `config`. The `menus` sub folder is just added for better organization. We use `TOML` in the example above, but Hugo also supports `JSON` and `YAML` as configuration formats. These can be mixed. Fixes #5422
--- a/commands/commandeer.go
+++ b/commands/commandeer.go
@@ -249,6 +249,8 @@
sourceFs = c.DepsCfg.Fs.Source
}
+ environment := c.h.getEnvironment(running)
+
doWithConfig := func(cfg config.Provider) error {
if c.ftch != nil {
@@ -256,7 +258,7 @@
}
cfg.Set("workingDir", dir)
-
+ cfg.Set("environment", environment)
return nil
}
@@ -269,8 +271,18 @@
return err
}
+ configPath := c.h.source
+ if configPath == "" {
+ configPath = dir
+ }
config, configFiles, err := hugolib.LoadConfig(
- hugolib.ConfigSourceDescriptor{Fs: sourceFs, Path: c.h.source, WorkingDir: dir, Filename: c.h.cfgFile},
+ hugolib.ConfigSourceDescriptor{
+ Fs: sourceFs,
+ Path: configPath,
+ WorkingDir: dir,
+ Filename: c.h.cfgFile,
+ AbsConfigDir: c.h.getConfigDir(dir),
+ Environment: environment},
doWithCommandeer,
doWithConfig)
--- a/commands/commands.go
+++ b/commands/commands.go
@@ -14,6 +14,11 @@
package commands
import (
+ "os"
+
+ "github.com/gohugoio/hugo/hugolib/paths"
+
+ "github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/helpers"
@@ -159,6 +164,7 @@
})
cc.cmd.PersistentFlags().StringVar(&cc.cfgFile, "config", "", "config file (default is path/config.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")
// Set bash-completion
@@ -185,8 +191,9 @@
}
type hugoBuilderCommon struct {
- source string
- baseURL string
+ source string
+ baseURL string
+ environment string
buildWatch bool
@@ -200,9 +207,38 @@
quiet bool
cfgFile string
+ cfgDir string
logFile string
}
+func (cc *hugoBuilderCommon) getConfigDir(baseDir string) string {
+ if cc.cfgDir != "" {
+ return paths.AbsPathify(baseDir, cc.cfgDir)
+ }
+
+ if v, found := os.LookupEnv("HUGO_CONFIGDIR"); found {
+ return paths.AbsPathify(baseDir, v)
+ }
+
+ return paths.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
+ }
+
+ if isServer {
+ return hugo.EnvironmentDevelopment
+ }
+
+ return hugo.EnvironmentProduction
+}
+
func (cc *hugoBuilderCommon) handleFlags(cmd *cobra.Command) {
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")
@@ -209,6 +245,7 @@
cmd.Flags().BoolP("buildFuture", "F", false, "include content with publishdate in the future")
cmd.Flags().BoolP("buildExpired", "E", false, "include expired content")
cmd.Flags().StringVarP(&cc.source, "source", "s", "", "filesystem path to read files relative from")
+ cmd.Flags().StringVarP(&cc.environment, "environment", "e", "", "build environment")
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/")
--- a/commands/commands_test.go
+++ b/commands/commands_test.go
@@ -56,8 +56,11 @@
check func(command []cmder)
}{{[]string{"server",
"--config=myconfig.toml",
+ "--configDir=myconfigdir",
"--contentDir=mycontent",
"--disableKinds=page,home",
+ "--environment=testing",
+ "--configDir=myconfigdir",
"--layoutDir=mylayouts",
"--theme=mytheme",
"--gc",
@@ -78,6 +81,7 @@
if b, ok := command.(commandsBuilderGetter); ok {
v := b.getCommandsBuilder().hugoBuilderCommon
assert.Equal("myconfig.toml", v.cfgFile)
+ assert.Equal("myconfigdir", v.cfgDir)
assert.Equal("mysource", v.source)
assert.Equal("https://example.com/b/", v.baseURL)
}
@@ -93,6 +97,7 @@
assert.True(sc.noHTTPCache)
assert.True(sc.renderToDisk)
assert.Equal(1366, sc.serverPort)
+ assert.Equal("testing", sc.environment)
cfg := viper.New()
sc.flagsToConfig(cfg)
@@ -233,6 +238,7 @@
writeFile(t, filepath.Join(d, "layouts", "_default", "list.html"), `
List: {{ .Title }}
+Environment: {{ hugo.Environment }}
`)
--- a/commands/hugo.go
+++ b/commands/hugo.go
@@ -718,8 +718,8 @@
// Identifies changes to config (config.toml) files.
configSet := make(map[string]bool)
+ c.logger.FEEDBACK.Println("Watching for config changes in", strings.Join(c.configFiles, ", "))
for _, configFile := range c.configFiles {
- c.logger.FEEDBACK.Println("Watching for config changes in", configFile)
watcher.Add(configFile)
configSet[configFile] = true
}
@@ -750,7 +750,17 @@
configSet map[string]bool) {
for _, ev := range evs {
- if configSet[ev.Name] {
+ isConfig := configSet[ev.Name]
+ if !isConfig {
+ // It may be one of the /config folders
+ dirname := filepath.Dir(ev.Name)
+ if dirname != "." && configSet[dirname] {
+ isConfig = true
+ }
+
+ }
+
+ if isConfig {
if ev.Op&fsnotify.Chmod == fsnotify.Chmod {
continue
}
@@ -766,7 +776,7 @@
}
}
}
- // Config file changed. Need full rebuild.
+ // Config file(s) changed. Need full rebuild.
c.fullRebuild()
break
}
--- a/commands/server.go
+++ b/commands/server.go
@@ -36,7 +36,6 @@
"github.com/gohugoio/hugo/tpl"
"github.com/gohugoio/hugo/config"
-
"github.com/gohugoio/hugo/helpers"
"github.com/spf13/afero"
"github.com/spf13/cobra"
@@ -300,6 +299,8 @@
}
absPublishDir := f.c.hugo.PathSpec.AbsPathify(publishDir)
+
+ jww.FEEDBACK.Printf("Environment: %q", f.c.hugo.Deps.Site.Hugo().Environment)
if i == 0 {
if f.s.renderToDisk {
--- a/commands/server_test.go
+++ b/commands/server_test.go
@@ -68,6 +68,7 @@
homeContent := helpers.ReaderToString(resp.Body)
assert.Contains(homeContent, "List: Hugo Commands")
+ assert.Contains(homeContent, "Environment: development")
// Stop the server.
stop <- true
--- a/common/herrors/error_locator.go
+++ b/common/herrors/error_locator.go
@@ -17,10 +17,10 @@
import (
"io"
"io/ioutil"
+ "path/filepath"
"strings"
"github.com/gohugoio/hugo/common/text"
- "github.com/gohugoio/hugo/helpers"
"github.com/spf13/afero"
)
@@ -172,12 +172,16 @@
return fileType
}
+func extNoDelimiter(filename string) string {
+ return strings.TrimPrefix(".", filepath.Ext(filename))
+}
+
func chromaLexerFromFilename(filename string) string {
if strings.Contains(filename, "layouts") {
return "go-html-template"
}
- ext := helpers.ExtNoDelimiter(filename)
+ ext := extNoDelimiter(filename)
return chromaLexerFromType(ext)
}
--- a/common/hugo/hugo.go
+++ b/common/hugo/hugo.go
@@ -18,28 +18,50 @@
"html/template"
)
+const (
+ EnvironmentDevelopment = "development"
+ EnvironmentProduction = "production"
+)
+
var (
- // CommitHash contains the current Git revision. Use make to build to make
+ // commitHash contains the current Git revision. Use make to build to make
// sure this gets set.
- CommitHash string
+ commitHash string
- // BuildDate contains the date of the current build.
- BuildDate string
+ // buildDate contains the date of the current build.
+ buildDate string
)
// Info contains information about the current Hugo environment
type Info struct {
- Version VersionString
- Generator template.HTML
CommitHash string
BuildDate string
+
+ // The build environment.
+ // Defaults are "production" (hugo) and "development" (hugo server).
+ // This can also be set by the user.
+ // It can be any string, but it will be all lower case.
+ Environment string
}
-func NewInfo() Info {
+// Version returns the current version as a comparable version string.
+func (i Info) Version() VersionString {
+ return CurrentVersion.Version()
+}
+
+// Generator a Hugo meta generator HTML tag.
+func (i Info) Generator() template.HTML {
+ return template.HTML(fmt.Sprintf(`<meta name="generator" content="Hugo %s" />`, CurrentVersion.String()))
+}
+
+// NewInfo creates a new Hugo Info object.
+func NewInfo(environment string) Info {
+ if environment == "" {
+ environment = EnvironmentProduction
+ }
return Info{
- Version: CurrentVersion.Version(),
- CommitHash: CommitHash,
- BuildDate: BuildDate,
- Generator: template.HTML(fmt.Sprintf(`<meta name="generator" content="Hugo %s" />`, CurrentVersion.String())),
+ CommitHash: commitHash,
+ BuildDate: buildDate,
+ Environment: environment,
}
}
--- a/common/hugo/hugo_test.go
+++ b/common/hugo/hugo_test.go
@@ -23,12 +23,13 @@
func TestHugoInfo(t *testing.T) {
assert := require.New(t)
- hugoInfo := NewInfo()
+ hugoInfo := NewInfo("")
- assert.Equal(CurrentVersion.Version(), hugoInfo.Version)
- assert.IsType(VersionString(""), hugoInfo.Version)
- assert.Equal(CommitHash, hugoInfo.CommitHash)
- assert.Equal(BuildDate, hugoInfo.BuildDate)
- assert.Contains(hugoInfo.Generator, fmt.Sprintf("Hugo %s", hugoInfo.Version))
+ assert.Equal(CurrentVersion.Version(), hugoInfo.Version())
+ assert.IsType(VersionString(""), hugoInfo.Version())
+ assert.Equal(commitHash, hugoInfo.CommitHash)
+ assert.Equal(buildDate, hugoInfo.BuildDate)
+ assert.Equal("production", hugoInfo.Environment)
+ assert.Contains(hugoInfo.Generator(), fmt.Sprintf("Hugo %s", hugoInfo.Version()))
}
--- a/common/hugo/version.go
+++ b/common/hugo/version.go
@@ -130,8 +130,8 @@
program := "Hugo Static Site Generator"
version := "v" + CurrentVersion.String()
- if CommitHash != "" {
- version += "-" + strings.ToUpper(CommitHash)
+ if commitHash != "" {
+ version += "-" + strings.ToUpper(commitHash)
}
if isExtended {
version += "/extended"
@@ -139,14 +139,12 @@
osArch := runtime.GOOS + "/" + runtime.GOARCH
- var buildDate string
- if BuildDate != "" {
- buildDate = BuildDate
- } else {
- buildDate = "unknown"
+ date := buildDate
+ if date == "" {
+ date = "unknown"
}
- return fmt.Sprintf("%s %s %s BuildDate: %s", program, version, osArch, buildDate)
+ return fmt.Sprintf("%s %s %s BuildDate: %s", program, version, osArch, date)
}
--- a/common/maps/maps.go
+++ b/common/maps/maps.go
@@ -16,6 +16,8 @@
import (
"strings"
+ "github.com/gobwas/glob"
+
"github.com/spf13/cast"
)
@@ -40,5 +42,75 @@
m[lKey] = v
}
+ }
+}
+
+type keyRename struct {
+ pattern glob.Glob
+ newKey string
+}
+
+// KeyRenamer supports renaming of keys in a map.
+type KeyRenamer struct {
+ renames []keyRename
+}
+
+// NewKeyRenamer creates a new KeyRenamer given a list of pattern and new key
+// value pairs.
+func NewKeyRenamer(patternKeys ...string) (KeyRenamer, error) {
+ var renames []keyRename
+ for i := 0; i < len(patternKeys); i += 2 {
+ g, err := glob.Compile(strings.ToLower(patternKeys[i]), '/')
+ if err != nil {
+ return KeyRenamer{}, err
+ }
+ renames = append(renames, keyRename{pattern: g, newKey: patternKeys[i+1]})
+ }
+
+ return KeyRenamer{renames: renames}, nil
+}
+
+func (r KeyRenamer) getNewKey(keyPath string) string {
+ for _, matcher := range r.renames {
+ if matcher.pattern.Match(keyPath) {
+ return matcher.newKey
+ }
+ }
+
+ return ""
+}
+
+// Rename renames the keys in the given map according
+// to the patterns in the current KeyRenamer.
+func (r KeyRenamer) Rename(m map[string]interface{}) {
+ r.renamePath("", m)
+}
+
+func (KeyRenamer) keyPath(k1, k2 string) string {
+ k1, k2 = strings.ToLower(k1), strings.ToLower(k2)
+ if k1 == "" {
+ return k2
+ } else {
+ return k1 + "/" + k2
+ }
+}
+
+func (r KeyRenamer) renamePath(parentKeyPath string, m map[string]interface{}) {
+ for key, val := range m {
+ keyPath := r.keyPath(parentKeyPath, key)
+ switch val.(type) {
+ case map[interface{}]interface{}:
+ val = cast.ToStringMap(val)
+ r.renamePath(keyPath, val.(map[string]interface{}))
+ case map[string]interface{}:
+ r.renamePath(keyPath, val.(map[string]interface{}))
+ }
+
+ newKey := r.getNewKey(keyPath)
+
+ if newKey != "" {
+ delete(m, key)
+ m[newKey] = val
+ }
}
}
--- a/common/maps/maps_test.go
+++ b/common/maps/maps_test.go
@@ -16,6 +16,8 @@
import (
"reflect"
"testing"
+
+ "github.com/stretchr/testify/require"
)
func TestToLower(t *testing.T) {
@@ -69,4 +71,53 @@
t.Errorf("[%d] Expected\n%#v, got\n%#v\n", i, test.expected, test.input)
}
}
+}
+
+func TestRenameKeys(t *testing.T) {
+ assert := require.New(t)
+
+ m := map[string]interface{}{
+ "a": 32,
+ "ren1": "m1",
+ "ren2": "m1_2",
+ "sub": map[string]interface{}{
+ "subsub": map[string]interface{}{
+ "REN1": "m2",
+ "ren2": "m2_2",
+ },
+ },
+ "no": map[string]interface{}{
+ "ren1": "m2",
+ "ren2": "m2_2",
+ },
+ }
+
+ expected := map[string]interface{}{
+ "a": 32,
+ "new1": "m1",
+ "new2": "m1_2",
+ "sub": map[string]interface{}{
+ "subsub": map[string]interface{}{
+ "new1": "m2",
+ "ren2": "m2_2",
+ },
+ },
+ "no": map[string]interface{}{
+ "ren1": "m2",
+ "ren2": "m2_2",
+ },
+ }
+
+ renamer, err := NewKeyRenamer(
+ "{ren1,sub/*/ren1}", "new1",
+ "{Ren2,sub/ren2}", "new2",
+ )
+ assert.NoError(err)
+
+ renamer.Rename(m)
+
+ if !reflect.DeepEqual(expected, m) {
+ t.Errorf("Expected\n%#v, got\n%#v\n", expected, m)
+ }
+
}
--- /dev/null
+++ b/config/configLoader.go
@@ -1,0 +1,106 @@
+// 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 config
+
+import (
+ "github.com/gohugoio/hugo/common/maps"
+ "github.com/gohugoio/hugo/parser/metadecoders"
+ "github.com/spf13/afero"
+ "github.com/spf13/viper"
+)
+
+// FromConfigString creates a config from the given YAML, JSON or TOML config. This is useful in tests.
+func FromConfigString(config, configType string) (Provider, error) {
+ v := newViper()
+ m, err := readConfig(metadecoders.FormatFromString(configType), []byte(config))
+ if err != nil {
+ return nil, err
+ }
+
+ v.MergeConfigMap(m)
+
+ return v, nil
+}
+
+// FromFile loads the configuration from the given filename.
+func FromFile(fs afero.Fs, filename string) (Provider, error) {
+ m, err := loadConfigFromFile(fs, filename)
+ if err != nil {
+ return nil, err
+ }
+
+ v := newViper()
+
+ err = v.MergeConfigMap(m)
+ if err != nil {
+ return nil, err
+ }
+
+ return v, nil
+}
+
+// FromFileToMap is the same as FromFile, but it returns the config values
+// as a simple map.
+func FromFileToMap(fs afero.Fs, filename string) (map[string]interface{}, error) {
+ return loadConfigFromFile(fs, filename)
+}
+
+func readConfig(format metadecoders.Format, data []byte) (map[string]interface{}, error) {
+ m, err := metadecoders.UnmarshalToMap(data, format)
+ if err != nil {
+ return nil, err
+ }
+
+ RenameKeys(m)
+
+ return m, nil
+
+}
+
+func loadConfigFromFile(fs afero.Fs, filename string) (map[string]interface{}, error) {
+ m, err := metadecoders.UnmarshalFileToMap(fs, filename)
+ if err != nil {
+ return nil, err
+ }
+ RenameKeys(m)
+ return m, nil
+}
+
+var keyAliases maps.KeyRenamer
+
+func init() {
+ var err error
+ keyAliases, err = maps.NewKeyRenamer(
+ // Before 0.53 we used singular for "menu".
+ "{menu,languages/*/menu}", "menus",
+ )
+
+ if err != nil {
+ panic(err)
+ }
+}
+
+// RenameKeys renames config keys in m recursively according to a global Hugo
+// alias definition.
+func RenameKeys(m map[string]interface{}) {
+ keyAliases.Rename(m)
+}
+
+func newViper() *viper.Viper {
+ v := viper.New()
+ v.AutomaticEnv()
+ v.SetEnvPrefix("hugo")
+
+ return v
+}
--- a/config/configProvider.go
+++ b/config/configProvider.go
@@ -14,11 +14,7 @@
package config
import (
- "strings"
-
"github.com/spf13/cast"
-
- "github.com/spf13/viper"
)
// Provider provides the configuration settings for Hugo.
@@ -32,16 +28,6 @@
Get(key string) interface{}
Set(key string, value interface{})
IsSet(key string) bool
-}
-
-// FromConfigString creates a config from the given YAML, JSON or TOML config. This is useful in tests.
-func FromConfigString(config, configType string) (Provider, error) {
- v := viper.New()
- v.SetConfigType(configType)
- if err := v.ReadConfig(strings.NewReader(config)); err != nil {
- return nil, err
- }
- return v, nil
}
// GetStringSlicePreserveString returns a string slice from the given config and key.
--- a/go.mod
+++ b/go.mod
@@ -33,7 +33,7 @@
github.com/mattn/go-runewidth v0.0.3 // indirect
github.com/miekg/mmark v1.3.6
github.com/mitchellh/hashstructure v1.0.0
- github.com/mitchellh/mapstructure v1.0.0
+ github.com/mitchellh/mapstructure v1.1.2
github.com/muesli/smartcrop v0.0.0-20180228075044-f6ebaa786a12
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
github.com/nicksnyder/go-i18n v1.10.0
@@ -50,16 +50,18 @@
github.com/spf13/jwalterweatherman v1.0.1-0.20181028145347-94f6ae3ed3bc
github.com/spf13/nitro v0.0.0-20131003134307-24d7ef30a12d
github.com/spf13/pflag v1.0.3
- github.com/spf13/viper v1.2.0
+ github.com/spf13/viper v1.3.1
github.com/stretchr/testify v1.2.3-0.20181014000028-04af85275a5c
github.com/tdewolff/minify/v2 v2.3.7
+ github.com/ugorji/go/codec v0.0.0-20181206144755-e72634d4d386 // indirect
github.com/yosssi/ace v0.0.5
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd // indirect
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f
+ golang.org/x/sys v0.0.0-20181206074257-70b957f3b65e // indirect
golang.org/x/text v0.3.0
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
- gopkg.in/yaml.v2 v2.2.1
+ gopkg.in/yaml.v2 v2.2.2
)
exclude github.com/chaseadamsio/goorgeous v2.0.0+incompatible
--- a/go.sum
+++ b/go.sum
@@ -14,6 +14,7 @@
github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0=
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897 h1:p9Sln00KOTlrYkxI1zYWl1QLnEqAqEARBEYa8FQnQcY=
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
+github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
github.com/bep/debounce v1.1.0 h1:6ocXeW2iZ/7vAzgXz82J00tYxncMiEEBExPftTtOQzk=
github.com/bep/debounce v1.1.0/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
github.com/bep/gitmap v1.0.0 h1:cTTZwq7vpGuhwefKCBDV9UrHnZAPVJTvoWobimrqkUc=
@@ -24,6 +25,9 @@
github.com/chaseadamsio/goorgeous v1.1.0/go.mod h1:6QaC0vFoKWYDth94dHFNgRT2YkT5FHdQp/Yx15aAAi0=
github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927 h1:SKI1/fuSdodxmNNyVBR8d7X/HuLnRpvvFO0AgyQk764=
github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927/go.mod h1:h/aW8ynjgkuj+NQRlZcDbAbM1ORAbXjXX77sX7T289U=
+github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
+github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
+github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/cpuguy83/go-md2man v1.0.8 h1:DwoNytLphI8hzS2Af4D0dfaEaiSq2bN05mEm4R6vf8M=
github.com/cpuguy83/go-md2man v1.0.8/go.mod h1:N6JayAiVKtlHSnuTCeuLSQVs75hb8q+dYQLjr7cDsKY=
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ=
@@ -79,8 +83,8 @@
github.com/miekg/mmark v1.3.6/go.mod h1:w7r9mkTvpS55jlfyn22qJ618itLryxXBhA7Jp3FIlkw=
github.com/mitchellh/hashstructure v1.0.0 h1:ZkRJX1CyOoTkar7p/mLS5TZU4nJ1Rn/F8u9dGS02Q3Y=
github.com/mitchellh/hashstructure v1.0.0/go.mod h1:QjSHrPWS+BGUVBYkbTZWEnOh3G1DutKwClXU/ABz6AQ=
-github.com/mitchellh/mapstructure v1.0.0 h1:vVpGvMXJPqSDh2VYHF7gsfQj8Ncx+Xw5Y1KHeTRY+7I=
-github.com/mitchellh/mapstructure v1.0.0/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
+github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/muesli/smartcrop v0.0.0-20180228075044-f6ebaa786a12 h1:l0X/8IDy2UoK+oXcQFMRSIOcyuYb5iEPytPGplnM41Y=
github.com/muesli/smartcrop v0.0.0-20180228075044-f6ebaa786a12/go.mod h1:i2fCI/UorTfgEpPPLWiFBv4pye+YAG78RwcQLUkocpI=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
@@ -105,8 +109,6 @@
github.com/shurcooL/sanitized_anchor_name v0.0.0-20170918181015-86672fcb3f95/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
-github.com/spf13/cast v1.2.0 h1:HHl1DSRbEQN2i8tJmtS6ViPyHx35+p51amrdsiTCrkg=
-github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8=
@@ -119,12 +121,12 @@
github.com/spf13/jwalterweatherman v1.0.1-0.20181028145347-94f6ae3ed3bc/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
github.com/spf13/nitro v0.0.0-20131003134307-24d7ef30a12d h1:ihvj2nmx8eqWjlgNgdW6h0DyGJuq5GiwHadJkG0wXtQ=
github.com/spf13/nitro v0.0.0-20131003134307-24d7ef30a12d/go.mod h1:jU8A+8xL+6n1OX4XaZtCj4B3mIa64tULUsD6YegdpFo=
-github.com/spf13/pflag v1.0.2 h1:Fy0orTDgHdbnzHcsOgfCN4LtHf0ec3wwtiwJqwvf3Gc=
-github.com/spf13/pflag v1.0.2/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
-github.com/spf13/viper v1.2.0 h1:M4Rzxlu+RgU4pyBRKhKaVN1VeYOm8h2jgyXnAseDgCc=
-github.com/spf13/viper v1.2.0/go.mod h1:P4AexN0a+C9tGAnUFNwDMYYZv3pjFuvmeiMyKRaNVlI=
+github.com/spf13/viper v1.3.0 h1:cO6QlTTeK9RQDhFAbGLV5e3fHXbRpin/Gi8qfL4rdLk=
+github.com/spf13/viper v1.3.0/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
+github.com/spf13/viper v1.3.1 h1:5+8j8FTpnFV4nEImW/ofkzEt8VoOiLXxdYIDsB73T38=
+github.com/spf13/viper v1.3.1/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.2.3-0.20181014000028-04af85275a5c h1:03OmljzZYsezlgAfa+f/cY8E8XXPiFh5bgANMhUlDI4=
@@ -135,10 +137,14 @@
github.com/tdewolff/parse/v2 v2.3.5/go.mod h1:HansaqmN4I/U7L6/tUp0NcwT2tFO0F4EAWYGSDzkYNk=
github.com/tdewolff/test v1.0.0 h1:jOwzqCXr5ePXEPGJaq2ivoR6HOCi+D5TPfpoyg8yvmU=
github.com/tdewolff/test v1.0.0/go.mod h1:DiQUlutnqlEvdvhSn2LPGy4TFwRauAaYDsL+683RNX4=
+github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
+github.com/ugorji/go/codec v0.0.0-20181206144755-e72634d4d386/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
github.com/wellington/go-libsass v0.9.3-0.20181113175235-c63644206701 h1:9vG9vvVNVupO4Y7uwFkRgIMNe9rdaJMCINDe8vhAhLo=
github.com/wellington/go-libsass v0.9.3-0.20181113175235-c63644206701/go.mod h1:mxgxgam0N0E+NAUMHLcu20Ccfc3mVpDkyrLDayqfiTs=
+github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
github.com/yosssi/ace v0.0.5 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA=
github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0=
+golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81 h1:00VmoueYNlNz/aHIilyyQz/MHSqGoWJzpFv/HW8xpzI=
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA=
@@ -145,14 +151,16 @@
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sys v0.0.0-20180906133057-8cf3aee42992 h1:BH3eQWeGbwRU2+wxxuuPOdFBmaiBH81O8BugSjHeTFg=
-golang.org/x/sys v0.0.0-20180906133057-8cf3aee42992/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181031143558-9b800f95dbbc h1:SdCq5U4J+PpbSDIl9bM0V1e1Ug1jsnBkAFvTs1htn7U=
golang.org/x/sys v0.0.0-20181031143558-9b800f95dbbc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a h1:1n5lsVfiQW3yfsRGu98756EH1YthsFqr/5mxHduZW2A=
+golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181206074257-70b957f3b65e h1:njOxP/wVblhCLIUhjHXf6X+dzTt5OQ3vMQo9mkOIKIo=
+golang.org/x/sys v0.0.0-20181206074257-70b957f3b65e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
-gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
--- a/goreleaser-extended.yml
+++ b/goreleaser-extended.yml
@@ -2,7 +2,7 @@
builds:
- binary: hugo
ldflags:
- - -s -w -X github.com/gohugoio/hugo/common/hugo.BuildDate={{.Date}} -X github.com/gohugoio/hugo/common/hugo.CommitHash={{ .ShortCommit }}
+ - -s -w -X github.com/gohugoio/hugo/common/hugo.buildDate={{.Date}} -X github.com/gohugoio/hugo/common/hugo.commitHash={{ .ShortCommit }}
- "-extldflags '-static'"
env:
- CGO_ENABLED=1
--- a/goreleaser.yml
+++ b/goreleaser.yml
@@ -2,7 +2,7 @@
build:
main: main.go
binary: hugo
- ldflags: -s -w -X github.com/gohugoio/hugo/common/hugo.BuildDate={{.Date}} -X github.com/gohugoio/hugo/common/hugo.CommitHash={{ .ShortCommit }}
+ ldflags: -s -w -X github.com/gohugoio/hugo/common/hugo.buildDate={{.Date}} -X github.com/gohugoio/hugo/common/hugo.commitHash={{ .ShortCommit }}
env:
- CGO_ENABLED=0
goos:
--- a/helpers/path.go
+++ b/helpers/path.go
@@ -274,6 +274,13 @@
return fileAndExt(in, fpb)
}
+// FileAndExtNoDelimiter takes a path and returns the file and extension separated,
+// the extension excluding the delmiter, e.g "md".
+func FileAndExtNoDelimiter(in string) (string, string) {
+ file, ext := fileAndExt(in, fpb)
+ return file, strings.TrimPrefix(ext, ".")
+}
+
// Filename takes a path, strips out the extension,
// and returns the name of the file.
func Filename(in string) (name string) {
@@ -399,6 +406,8 @@
return r
}
+
+var numInPathRe = regexp.MustCompile("\\.(\\d+)\\.")
// FindCWD returns the current working directory from where the Hugo
// executable is run.
--- a/htesting/test_structs.go
+++ b/htesting/test_structs.go
@@ -39,7 +39,7 @@
// NewTestHugoSite creates a new minimal test site.
func NewTestHugoSite() hugo.Site {
return testSite{
- h: hugo.NewInfo(),
+ h: hugo.NewInfo(hugo.EnvironmentProduction),
l: langs.NewLanguage("en", newTestConfig()),
}
}
--- /dev/null
+++ b/htesting/testdata_builder.go
@@ -1,0 +1,59 @@
+// 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 htesting
+
+import (
+ "path/filepath"
+ "testing"
+
+ "github.com/spf13/afero"
+)
+
+type testFile struct {
+ name string
+ content string
+}
+
+type testdataBuilder struct {
+ t testing.TB
+ fs afero.Fs
+ workingDir string
+
+ files []testFile
+}
+
+func NewTestdataBuilder(fs afero.Fs, workingDir string, t testing.TB) *testdataBuilder {
+ workingDir = filepath.Clean(workingDir)
+ return &testdataBuilder{fs: fs, workingDir: workingDir, t: t}
+}
+
+func (b *testdataBuilder) Add(filename, content string) *testdataBuilder {
+ b.files = append(b.files, testFile{name: filename, content: content})
+ return b
+}
+
+func (b *testdataBuilder) Build() *testdataBuilder {
+ for _, f := range b.files {
+ if err := afero.WriteFile(b.fs, filepath.Join(b.workingDir, f.name), []byte(f.content), 0666); err != nil {
+ b.t.Fatalf("failed to add %q: %s", f.name, err)
+ }
+ }
+ return b
+}
+
+func (b testdataBuilder) WithWorkingDir(dir string) *testdataBuilder {
+ b.workingDir = filepath.Clean(dir)
+ b.files = make([]testFile, 0)
+ return &b
+}
--- a/hugolib/config.go
+++ b/hugolib/config.go
@@ -14,14 +14,19 @@
package hugolib
import (
- "errors"
"fmt"
- "io"
+
+ "os"
+ "path/filepath"
"strings"
- "github.com/gohugoio/hugo/common/herrors"
+ "github.com/gohugoio/hugo/parser/metadecoders"
+ "github.com/gohugoio/hugo/common/herrors"
+ "github.com/gohugoio/hugo/common/hugo"
+ "github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/hugolib/paths"
+ "github.com/pkg/errors"
_errors "github.com/pkg/errors"
"github.com/gohugoio/hugo/langs"
@@ -65,21 +70,37 @@
type ConfigSourceDescriptor struct {
Fs afero.Fs
- // Full path to the config file to use, i.e. /my/project/config.toml
+ // Path to the config file to use, e.g. /my/project/config.toml
Filename string
// The path to the directory to look for configuration. Is used if Filename is not
- // set.
+ // set or if it is set to a relative filename.
Path string
// The project's working dir. Is used to look for additional theme config.
WorkingDir string
+
+ // The (optional) directory for additional configuration files.
+ AbsConfigDir string
+
+ // production, development
+ Environment string
}
func (d ConfigSourceDescriptor) configFilenames() []string {
+ if d.Filename == "" {
+ return []string{"config"}
+ }
return strings.Split(d.Filename, ",")
}
+func (d ConfigSourceDescriptor) configFileDir() string {
+ if d.Path != "" {
+ return d.Path
+ }
+ return d.WorkingDir
+}
+
// LoadConfigDefault is a convenience method to load the default "config.toml" config.
func LoadConfigDefault(fs afero.Fs) (*viper.Viper, error) {
v, _, err := LoadConfig(ConfigSourceDescriptor{Fs: fs, Filename: "config.toml"})
@@ -86,66 +107,39 @@
return v, err
}
-var ErrNoConfigFile = errors.New("Unable to locate Config file. Perhaps you need to create a new site.\n Run `hugo help new` for details.\n")
+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")
// LoadConfig loads Hugo configuration into a new Viper and then adds
// a set of defaults.
func LoadConfig(d ConfigSourceDescriptor, doWithConfig ...func(cfg config.Provider) error) (*viper.Viper, []string, error) {
+ if d.Environment == "" {
+ d.Environment = hugo.EnvironmentProduction
+ }
+
var configFiles []string
- fs := d.Fs
v := viper.New()
- v.SetFs(fs)
+ l := configLoader{ConfigSourceDescriptor: d}
- if d.Path == "" {
- d.Path = "."
- }
-
- configFilenames := d.configFilenames()
v.AutomaticEnv()
v.SetEnvPrefix("hugo")
- v.SetConfigFile(configFilenames[0])
- v.AddConfigPath(d.Path)
- applyFileContext := func(filename string, err error) error {
- err, _ = herrors.WithFileContextForFile(
- err,
- filename,
- filename,
- fs,
- herrors.SimpleLineMatcher)
+ var cerr error
- return err
- }
-
- var configFileErr error
-
- err := v.ReadInConfig()
- if err != nil {
- if _, ok := err.(viper.ConfigParseError); ok {
- return nil, configFiles, applyFileContext(v.ConfigFileUsed(), err)
+ for _, name := range d.configFilenames() {
+ var filename string
+ if filename, cerr = l.loadConfig(name, v); cerr != nil && cerr != ErrNoConfigFile {
+ return nil, nil, cerr
}
- configFileErr = ErrNoConfigFile
+ configFiles = append(configFiles, filename)
}
- if configFileErr == nil {
-
- if cf := v.ConfigFileUsed(); cf != "" {
- configFiles = append(configFiles, cf)
+ if d.AbsConfigDir != "" {
+ dirnames, err := l.loadConfigFromConfigDir(v)
+ if err == nil {
+ configFiles = append(configFiles, dirnames...)
}
-
- for _, configFile := range configFilenames[1:] {
- var r io.Reader
- var err error
- if r, err = fs.Open(configFile); err != nil {
- return nil, configFiles, fmt.Errorf("Unable to open Config file.\n (%s)\n", err)
- }
- if err = v.MergeConfig(r); err != nil {
- return nil, configFiles, applyFileContext(configFile, err)
- }
- configFiles = append(configFiles, configFile)
- }
-
+ cerr = err
}
if err := loadDefaultSettingsFor(v); err != nil {
@@ -152,9 +146,8 @@
return v, configFiles, err
}
- if configFileErr == nil {
-
- themeConfigFiles, err := loadThemeConfig(d, v)
+ if cerr == nil {
+ themeConfigFiles, err := l.loadThemeConfig(v)
if err != nil {
return v, configFiles, err
}
@@ -176,10 +169,181 @@
return v, configFiles, err
}
- return v, configFiles, configFileErr
+ return v, configFiles, cerr
}
+type configLoader struct {
+ ConfigSourceDescriptor
+}
+
+func (l configLoader) wrapFileInfoError(err error, fi os.FileInfo) error {
+ rfi, ok := fi.(hugofs.RealFilenameInfo)
+ if !ok {
+ return err
+ }
+ return l.wrapFileError(err, rfi.RealFilename())
+}
+
+func (l configLoader) loadConfig(configName string, v *viper.Viper) (string, error) {
+ baseDir := l.configFileDir()
+ var baseFilename string
+ if filepath.IsAbs(configName) {
+ baseFilename = configName
+ } else {
+ baseFilename = filepath.Join(baseDir, configName)
+ }
+
+ var filename string
+ fileExt := helpers.ExtNoDelimiter(configName)
+ if fileExt != "" {
+ exists, _ := helpers.Exists(baseFilename, l.Fs)
+ if exists {
+ filename = baseFilename
+ }
+ } else {
+ for _, ext := range []string{"toml", "yaml", "yml", "json"} {
+ filenameToCheck := baseFilename + "." + ext
+ exists, _ := helpers.Exists(filenameToCheck, l.Fs)
+ if exists {
+ filename = filenameToCheck
+ fileExt = ext
+ break
+ }
+ }
+ }
+
+ if filename == "" {
+ return "", ErrNoConfigFile
+ }
+
+ m, err := config.FromFileToMap(l.Fs, filename)
+ if err != nil {
+ return "", l.wrapFileError(err, filename)
+ }
+
+ if err = v.MergeConfigMap(m); err != nil {
+ return "", l.wrapFileError(err, filename)
+ }
+
+ return filename, nil
+
+}
+
+func (l configLoader) wrapFileError(err error, filename string) error {
+ err, _ = herrors.WithFileContextForFile(
+ err,
+ filename,
+ filename,
+ l.Fs,
+ herrors.SimpleLineMatcher)
+ return err
+}
+
+func (l configLoader) newRealBaseFs(path string) afero.Fs {
+ return hugofs.NewBasePathRealFilenameFs(afero.NewBasePathFs(l.Fs, path).(*afero.BasePathFs))
+
+}
+
+func (l configLoader) loadConfigFromConfigDir(v *viper.Viper) ([]string, error) {
+ sourceFs := l.Fs
+ configDir := l.AbsConfigDir
+
+ if _, err := sourceFs.Stat(configDir); err != nil {
+ // Config dir does not exist.
+ return nil, nil
+ }
+
+ defaultConfigDir := filepath.Join(configDir, "_default")
+ environmentConfigDir := filepath.Join(configDir, l.Environment)
+
+ var configDirs []string
+ // Merge from least to most specific.
+ for _, dir := range []string{defaultConfigDir, environmentConfigDir} {
+ if _, err := sourceFs.Stat(dir); err == nil {
+ configDirs = append(configDirs, dir)
+ }
+ }
+
+ if len(configDirs) == 0 {
+ return nil, nil
+ }
+
+ // Keep track of these so we can watch them for changes.
+ var dirnames []string
+
+ for _, configDir := range configDirs {
+ err := afero.Walk(sourceFs, configDir, func(path string, fi os.FileInfo, err error) error {
+ if fi == nil {
+ return nil
+ }
+
+ if fi.IsDir() {
+ dirnames = append(dirnames, path)
+ return nil
+ }
+
+ name := helpers.Filename(filepath.Base(path))
+
+ item, err := metadecoders.UnmarshalFileToMap(sourceFs, path)
+ if err != nil {
+ return l.wrapFileError(err, path)
+ }
+
+ var keyPath []string
+
+ if name != "config" {
+ // Can be params.jp, menus.en etc.
+ name, lang := helpers.FileAndExtNoDelimiter(name)
+
+ keyPath = []string{name}
+
+ if lang != "" {
+ keyPath = []string{"languages", lang}
+ switch name {
+ case "menu", "menus":
+ keyPath = append(keyPath, "menus")
+ case "params":
+ keyPath = append(keyPath, "params")
+ }
+ }
+ }
+
+ root := item
+ if len(keyPath) > 0 {
+ root = make(map[string]interface{})
+ m := root
+ for i, key := range keyPath {
+ if i >= len(keyPath)-1 {
+ m[key] = item
+ } else {
+ nm := make(map[string]interface{})
+ m[key] = nm
+ m = nm
+ }
+ }
+ }
+
+ // Migrate menu => menus etc.
+ config.RenameKeys(root)
+
+ if err := v.MergeConfigMap(root); err != nil {
+ return l.wrapFileError(err, path)
+ }
+
+ return nil
+
+ })
+
+ if err != nil {
+ return nil, err
+ }
+
+ }
+
+ return dirnames, nil
+}
+
func loadLanguageSettings(cfg config.Provider, oldLangs langs.Languages) error {
defaultLang := cfg.GetString("defaultContentLanguage")
@@ -289,12 +453,11 @@
return nil
}
-func loadThemeConfig(d ConfigSourceDescriptor, v1 *viper.Viper) ([]string, error) {
- themesDir := paths.AbsPathify(d.WorkingDir, v1.GetString("themesDir"))
+func (l configLoader) loadThemeConfig(v1 *viper.Viper) ([]string, error) {
+ themesDir := paths.AbsPathify(l.WorkingDir, v1.GetString("themesDir"))
themes := config.GetStringSlicePreserveString(v1, "theme")
- // CollectThemes(fs afero.Fs, themesDir string, themes []strin
- themeConfigs, err := paths.CollectThemes(d.Fs, themesDir, themes)
+ themeConfigs, err := paths.CollectThemes(l.Fs, themesDir, themes)
if err != nil {
return nil, err
}
@@ -309,7 +472,7 @@
for _, tc := range themeConfigs {
if tc.ConfigFilename != "" {
configFilenames = append(configFilenames, tc.ConfigFilename)
- if err := applyThemeConfig(v1, tc); err != nil {
+ if err := l.applyThemeConfig(v1, tc); err != nil {
return nil, err
}
}
@@ -319,18 +482,18 @@
}
-func applyThemeConfig(v1 *viper.Viper, theme paths.ThemeConfig) error {
+func (l configLoader) applyThemeConfig(v1 *viper.Viper, theme paths.ThemeConfig) error {
const (
paramsKey = "params"
languagesKey = "languages"
- menuKey = "menu"
+ menuKey = "menus"
)
v2 := theme.Cfg
for _, key := range []string{paramsKey, "outputformats", "mediatypes"} {
- mergeStringMapKeepLeft("", key, v1, v2)
+ l.mergeStringMapKeepLeft("", key, v1, v2)
}
themeLower := strings.ToLower(theme.Name)
@@ -348,7 +511,7 @@
v1Langs := v1.GetStringMap(languagesKey)
for k := range v1Langs {
langParamsKey := languagesKey + "." + k + "." + paramsKey
- mergeStringMapKeepLeft(paramsKey, langParamsKey, v1, v2)
+ l.mergeStringMapKeepLeft(paramsKey, langParamsKey, v1, v2)
}
v2Langs := v2.GetStringMap(languagesKey)
for k := range v2Langs {
@@ -378,7 +541,7 @@
}
// Add menu definitions from theme not found in project
- if v2.IsSet("menu") {
+ if v2.IsSet(menuKey) {
v2menus := v2.GetStringMap(menuKey)
for k, v := range v2menus {
menuEntry := menuKey + "." + k
@@ -392,7 +555,7 @@
}
-func mergeStringMapKeepLeft(rootKey, key string, v1, v2 config.Provider) {
+func (configLoader) mergeStringMapKeepLeft(rootKey, key string, v1, v2 config.Provider) {
if !v2.IsSet(key) {
return
}
@@ -440,6 +603,7 @@
v.SetDefault("buildDrafts", false)
v.SetDefault("buildFuture", false)
v.SetDefault("buildExpired", false)
+ v.SetDefault("environment", hugo.EnvironmentProduction)
v.SetDefault("uglyURLs", false)
v.SetDefault("verbose", false)
v.SetDefault("ignoreCache", false)
--- a/hugolib/config_test.go
+++ b/hugolib/config_test.go
@@ -247,8 +247,8 @@
b.AssertObject(`map[string]interface {}{
"en": map[string]interface {}{
"languagename": "English",
- "menu": map[string]interface {}{
- "theme": []interface {}{
+ "menus": map[string]interface {}{
+ "theme": []map[string]interface {}{
map[string]interface {}{
"name": "menu-lang-en-theme",
},
@@ -265,8 +265,8 @@
},
"nb": map[string]interface {}{
"languagename": "Norsk",
- "menu": map[string]interface {}{
- "theme": []interface {}{
+ "menus": map[string]interface {}{
+ "theme": []map[string]interface {}{
map[string]interface {}{
"name": "menu-lang-nb-theme",
},
@@ -287,23 +287,23 @@
b.AssertObject(`
map[string]interface {}{
- "main": []interface {}{
+ "main": []map[string]interface {}{
map[string]interface {}{
"name": "menu-main-main",
},
},
- "thememenu": []interface {}{
+ "thememenu": []map[string]interface {}{
map[string]interface {}{
"name": "menu-theme",
},
},
- "top": []interface {}{
+ "top": []map[string]interface {}{
map[string]interface {}{
"name": "menu-top-main",
},
},
}
-`, got["menu"])
+`, got["menus"])
assert.Equal("https://example.com/", got["baseurl"])
--- /dev/null
+++ b/hugolib/configdir_test.go
@@ -1,0 +1,152 @@
+// 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 hugolib
+
+import (
+ "path/filepath"
+ "testing"
+
+ "github.com/gohugoio/hugo/common/herrors"
+
+ "github.com/gohugoio/hugo/htesting"
+ "github.com/spf13/afero"
+ "github.com/stretchr/testify/require"
+)
+
+func TestLoadConfigDir(t *testing.T) {
+ t.Parallel()
+
+ assert := require.New(t)
+
+ configContent := `
+baseURL = "https://example.org"
+paginagePath = "pag_root"
+
+[languages.en]
+weight = 0
+languageName = "English"
+
+[languages.no]
+weight = 10
+languageName = "FOO"
+
+[params]
+p1 = "p1_base"
+
+`
+
+ mm := afero.NewMemMapFs()
+
+ writeToFs(t, mm, "hugo.toml", configContent)
+
+ fb := htesting.NewTestdataBuilder(mm, "config/_default", t)
+
+ fb.Add("config.toml", `paginatePath = "pag_default"`)
+
+ fb.Add("params.yaml", `
+p2: "p2params_default"
+p3: "p3params_default"
+p4: "p4params_default"
+`)
+ fb.Add("menus.toml", `
+[[docs]]
+name = "About Hugo"
+weight = 1
+[[docs]]
+name = "Home"
+weight = 2
+ `)
+
+ fb.Add("menus.no.toml", `
+ [[docs]]
+ name = "Om Hugo"
+ weight = 1
+ `)
+
+ fb.Add("params.no.toml",
+ `
+p3 = "p3params_no_default"
+p4 = "p4params_no_default"`,
+ )
+ fb.Add("languages.no.toml", `languageName = "Norsk_no_default"`)
+
+ fb.Build()
+
+ fb = fb.WithWorkingDir("config/production")
+
+ fb.Add("config.toml", `paginatePath = "pag_production"`)
+
+ fb.Add("params.no.toml", `
+p2 = "p2params_no_production"
+p3 = "p3params_no_production"
+`)
+
+ fb.Build()
+
+ fb = fb.WithWorkingDir("config/development")
+
+ // This is set in all the config.toml variants above, but this will win.
+ fb.Add("config.toml", `paginatePath = "pag_development"`)
+
+ fb.Add("params.no.toml", `p3 = "p3params_no_development"`)
+ fb.Add("params.toml", `p3 = "p3params_development"`)
+
+ fb.Build()
+
+ cfg, _, err := LoadConfig(ConfigSourceDescriptor{Fs: mm, Environment: "development", Filename: "hugo.toml", AbsConfigDir: "config"})
+ assert.NoError(err)
+
+ assert.Equal("pag_development", cfg.GetString("paginatePath")) // /config/development/config.toml
+
+ assert.Equal(10, cfg.GetInt("languages.no.weight")) // /config.toml
+ assert.Equal("Norsk_no_default", cfg.GetString("languages.no.languageName")) // /config/_default/languages.no.toml
+
+ assert.Equal("p1_base", cfg.GetString("params.p1"))
+ assert.Equal("p2params_default", cfg.GetString("params.p2")) // Is in both _default and production
+ assert.Equal("p3params_development", cfg.GetString("params.p3"))
+ assert.Equal("p3params_no_development", cfg.GetString("languages.no.params.p3"))
+
+ assert.Equal(2, len(cfg.Get("menus.docs").(([]map[string]interface{}))))
+ noMenus := cfg.Get("languages.no.menus.docs")
+ assert.NotNil(noMenus)
+ assert.Equal(1, len(noMenus.(([]map[string]interface{}))))
+
+}
+
+func TestLoadConfigDirError(t *testing.T) {
+ t.Parallel()
+
+ assert := require.New(t)
+
+ configContent := `
+baseURL = "https://example.org"
+
+`
+
+ mm := afero.NewMemMapFs()
+
+ writeToFs(t, mm, "hugo.toml", configContent)
+
+ fb := htesting.NewTestdataBuilder(mm, "config/development", t)
+
+ fb.Add("config.toml", `invalid & syntax`).Build()
+
+ _, _, err := LoadConfig(ConfigSourceDescriptor{Fs: mm, Environment: "development", Filename: "hugo.toml", AbsConfigDir: "config"})
+ assert.Error(err)
+
+ fe := herrors.UnwrapErrorWithFileContext(err)
+ assert.NotNil(fe)
+ assert.Equal(filepath.FromSlash("config/development/config.toml"), fe.Position().Filename)
+
+}
--- a/hugolib/hugo_sites.go
+++ b/hugolib/hugo_sites.go
@@ -21,6 +21,8 @@
"strings"
"sync"
+ "github.com/gohugoio/hugo/config"
+
"github.com/gohugoio/hugo/publisher"
"github.com/gohugoio/hugo/common/herrors"
@@ -361,7 +363,7 @@
}
}
-func (h *HugoSites) createSitesFromConfig() error {
+func (h *HugoSites) createSitesFromConfig(cfg config.Provider) error {
oldLangs, _ := h.Cfg.Get("languagesSorted").(langs.Languages)
if err := loadLanguageSettings(h.Cfg, oldLangs); err != nil {
@@ -368,7 +370,7 @@
return err
}
- depsCfg := deps.DepsCfg{Fs: h.Fs, Cfg: h.Cfg}
+ depsCfg := deps.DepsCfg{Fs: h.Fs, Cfg: cfg}
sites, err := createSitesFromConfig(depsCfg)
@@ -412,9 +414,9 @@
type BuildCfg struct {
// Reset site state before build. Use to force full rebuilds.
ResetState bool
- // Re-creates the sites from configuration before a build.
+ // If set, we re-create the sites from the given configuration before a build.
// This is needed if new languages are added.
- CreateSitesFromConfig bool
+ NewConfig config.Provider
// Skip rendering. Useful for testing.
SkipRender bool
// Use this to indicate what changed (for rebuilds).
--- a/hugolib/hugo_sites_build.go
+++ b/hugolib/hugo_sites_build.go
@@ -144,8 +144,8 @@
h.reset()
}
- if config.CreateSitesFromConfig {
- if err := h.createSitesFromConfig(); err != nil {
+ if config.NewConfig != nil {
+ if err := h.createSitesFromConfig(config.NewConfig); err != nil {
return err
}
}
@@ -154,8 +154,8 @@
}
func (h *HugoSites) initRebuild(config *BuildCfg) error {
- if config.CreateSitesFromConfig {
- return errors.New("Rebuild does not support 'CreateSitesFromConfig'.")
+ if config.NewConfig != nil {
+ return errors.New("Rebuild does not support 'NewConfig'.")
}
if config.ResetState {
--- a/hugolib/hugo_sites_build_test.go
+++ b/hugolib/hugo_sites_build_test.go
@@ -11,14 +11,11 @@
"path/filepath"
"time"
- "github.com/gohugoio/hugo/langs"
-
"github.com/fortytw2/leaktest"
"github.com/fsnotify/fsnotify"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugofs"
"github.com/spf13/afero"
- "github.com/spf13/viper"
"github.com/stretchr/testify/require"
)
@@ -661,9 +658,8 @@
sites := b.H
- // Watching does not work with in-memory fs, so we trigger a reload manually
- assert.NoError(sites.Cfg.(*langs.Language).Cfg.(*viper.Viper).ReadInConfig())
- err := b.H.Build(BuildCfg{CreateSitesFromConfig: true})
+ assert.NoError(b.LoadConfig())
+ err := b.H.Build(BuildCfg{NewConfig: b.Cfg})
if err != nil {
t.Fatalf("Failed to rebuild sites: %s", err)
@@ -723,10 +719,9 @@
"DefaultContentLanguageInSubdir": false,
})
- // Watching does not work with in-memory fs, so we trigger a reload manually
- // This does not look pretty, so we should think of something else.
- assert.NoError(b.H.Cfg.(*langs.Language).Cfg.(*viper.Viper).ReadInConfig())
- err := b.H.Build(BuildCfg{CreateSitesFromConfig: true})
+ assert.NoError(b.LoadConfig())
+ err := b.H.Build(BuildCfg{NewConfig: b.Cfg})
+
if err != nil {
t.Fatalf("Failed to rebuild sites: %s", err)
}
--- a/hugolib/page.go
+++ b/hugolib/page.go
@@ -1647,7 +1647,12 @@
p.pageMenusInit.Do(func() {
p.pageMenus = PageMenus{}
- if ms, ok := p.params["menu"]; ok {
+ ms, ok := p.params["menus"]
+ if !ok {
+ ms, ok = p.params["menu"]
+ }
+
+ if ok {
link := p.RelPermalink()
me := MenuEntry{Page: p, Name: p.LinkTitle(), Weight: p.Weight, URL: link}
--- a/hugolib/page_test.go
+++ b/hugolib/page_test.go
@@ -1436,7 +1436,7 @@
{func(n *Page) bool { return n.IsNode() }},
{func(n *Page) bool { return !n.IsPage() }},
{func(n *Page) bool { return n.Scratch() != nil }},
- {func(n *Page) bool { return n.Hugo().Version != "" }},
+ {func(n *Page) bool { return n.Hugo().Version() != "" }},
} {
n := s.newHomePage()
--- a/hugolib/paths/themes.go
+++ b/hugolib/paths/themes.go
@@ -20,7 +20,6 @@
"github.com/gohugoio/hugo/config"
"github.com/spf13/afero"
"github.com/spf13/cast"
- "github.com/spf13/viper"
)
type ThemeConfig struct {
@@ -73,18 +72,11 @@
var tc ThemeConfig
if configFilename != "" {
- v := viper.New()
- v.SetFs(c.fs)
- v.AutomaticEnv()
- v.SetEnvPrefix("hugo")
- v.SetConfigFile(configFilename)
-
- err := v.ReadInConfig()
+ var err error
+ cfg, err = config.FromFile(c.fs, configFilename)
if err != nil {
- return tc, err
+ return tc, nil
}
- cfg = v
-
}
tc = ThemeConfig{Name: name, ConfigFilename: configFilename, Cfg: cfg}
--- a/hugolib/site.go
+++ b/hugolib/site.go
@@ -1226,7 +1226,7 @@
Data: &s.Data,
owner: s.owner,
s: s,
- hugoInfo: hugo.NewInfo(),
+ hugoInfo: hugo.NewInfo(s.Cfg.GetString("environment")),
// TODO(bep) make this Menu and similar into delegate methods on SiteInfo
Taxonomies: s.Taxonomies,
}
@@ -1370,7 +1370,7 @@
ret := Menus{}
- if menus := s.Language.GetStringMap("menu"); menus != nil {
+ if menus := s.Language.GetStringMap("menus"); menus != nil {
for name, menu := range menus {
m, err := cast.ToSliceE(menu)
if err != nil {
--- a/hugolib/testhelpers_test.go
+++ b/hugolib/testhelpers_test.go
@@ -322,6 +322,15 @@
return s
}
+func (s *sitesBuilder) LoadConfig() error {
+ cfg, _, err := LoadConfig(ConfigSourceDescriptor{Fs: s.Fs.Source, Filename: "config." + s.configFormat})
+ if err != nil {
+ return err
+ }
+ s.Cfg = cfg
+ return nil
+}
+
func (s *sitesBuilder) CreateSitesE() error {
s.addDefaults()
s.writeFilePairs("content", s.contentFilePairs)
@@ -334,18 +343,9 @@
s.writeFilePairs("i18n", s.i18nFilePairsAdded)
if s.Cfg == nil {
- cfg, _, err := LoadConfig(ConfigSourceDescriptor{Fs: s.Fs.Source, Filename: "config." + s.configFormat})
- if err != nil {
+ if err := s.LoadConfig(); err != nil {
return err
}
- // TODO(bep)
- /* expectedConfigs := 1
- if s.theme != "" {
- expectedConfigs = 2
- }
- require.Equal(s.T, expectedConfigs, len(configFiles), fmt.Sprintf("Configs: %v", configFiles))
- */
- s.Cfg = cfg
}
sites, err := NewHugoSites(deps.DepsCfg{Fs: s.Fs, Cfg: s.Cfg, Logger: s.logger, Running: s.running})
--- a/magefile.go
+++ b/magefile.go
@@ -21,10 +21,10 @@
const (
packageName = "github.com/gohugoio/hugo"
- noGitLdflags = "-X $PACKAGE/common/hugo.BuildDate=$BUILD_DATE"
+ noGitLdflags = "-X $PACKAGE/common/hugo.buildDate=$BUILD_DATE"
)
-var ldflags = "-X $PACKAGE/common/hugo.CommitHash=$COMMIT_HASH -X $PACKAGE/common/hugo.BuildDate=$BUILD_DATE"
+var ldflags = "-X $PACKAGE/common/hugo.commitHash=$COMMIT_HASH -X $PACKAGE/common/hugo.buildDate=$BUILD_DATE"
// allow user to override go executable by running as GOEXE=xxx make ... on unix-like systems
var goexe = "go"
--- a/parser/metadecoders/decoder.go
+++ b/parser/metadecoders/decoder.go
@@ -22,6 +22,7 @@
"github.com/BurntSushi/toml"
"github.com/chaseadamsio/goorgeous"
"github.com/pkg/errors"
+ "github.com/spf13/afero"
"github.com/spf13/cast"
yaml "gopkg.in/yaml.v2"
)
@@ -37,7 +38,21 @@
err := unmarshal(data, f, &m)
return m, err
+}
+// UnmarshalFileToMap is the same as UnmarshalToMap, but reads the data from
+// the given filename.
+func UnmarshalFileToMap(fs afero.Fs, filename string) (map[string]interface{}, error) {
+ format := FormatFromString(filename)
+ if format == "" {
+ return nil, errors.Errorf("%q is not a valid configuration format", filename)
+ }
+
+ data, err := afero.ReadFile(fs, filename)
+ if err != nil {
+ return nil, err
+ }
+ return UnmarshalToMap(data, format)
}
// Unmarshal will unmarshall data in format f into an interface{}.
--- a/parser/metadecoders/format.go
+++ b/parser/metadecoders/format.go
@@ -14,6 +14,7 @@
package metadecoders
import (
+ "path/filepath"
"strings"
"github.com/gohugoio/hugo/parser/pageparser"
@@ -34,6 +35,11 @@
// into a Format. It returns an empty string for unknown formats.
func FormatFromString(formatStr string) Format {
formatStr = strings.ToLower(formatStr)
+ if strings.Contains(formatStr, ".") {
+ // Assume a filename
+ formatStr = strings.TrimPrefix(filepath.Ext(formatStr), ".")
+
+ }
switch formatStr {
case "yaml", "yml":
return YAML
--- a/parser/metadecoders/format_test.go
+++ b/parser/metadecoders/format_test.go
@@ -32,6 +32,7 @@
{"yaml", YAML},
{"yml", YAML},
{"toml", TOML},
+ {"config.toml", TOML},
{"tOMl", TOML},
{"org", ORG},
{"foo", ""},