Add /config dir support
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Thu, 15 Nov 2018 08:28:02 +0000 (09:28 +0100)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Tue, 11 Dec 2018 12:08:36 +0000 (13:08 +0100)
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

36 files changed:
commands/commandeer.go
commands/commands.go
commands/commands_test.go
commands/hugo.go
commands/server.go
commands/server_test.go
common/herrors/error_locator.go
common/hugo/hugo.go
common/hugo/hugo_test.go
common/hugo/version.go
common/maps/maps.go
common/maps/maps_test.go
config/configLoader.go [new file with mode: 0644]
config/configProvider.go
go.mod
go.sum
goreleaser-extended.yml
goreleaser.yml
helpers/path.go
htesting/test_structs.go
htesting/testdata_builder.go [new file with mode: 0644]
hugolib/config.go
hugolib/config_test.go
hugolib/configdir_test.go [new file with mode: 0644]
hugolib/hugo_sites.go
hugolib/hugo_sites_build.go
hugolib/hugo_sites_build_test.go
hugolib/page.go
hugolib/page_test.go
hugolib/paths/themes.go
hugolib/site.go
hugolib/testhelpers_test.go
magefile.go
parser/metadecoders/decoder.go
parser/metadecoders/format.go
parser/metadecoders/format_test.go

index b722991ab94a1df53f21c85acf8d21659285b3b1..75d69b2517c63c33bdf758e6dbc37b8aa652cbae 100644 (file)
@@ -249,6 +249,8 @@ func (c *commandeer) loadConfig(mustHaveConfigFile, running bool) error {
                sourceFs = c.DepsCfg.Fs.Source
        }
 
+       environment := c.h.getEnvironment(running)
+
        doWithConfig := func(cfg config.Provider) error {
 
                if c.ftch != nil {
@@ -256,7 +258,7 @@ func (c *commandeer) loadConfig(mustHaveConfigFile, running bool) error {
                }
 
                cfg.Set("workingDir", dir)
-
+               cfg.Set("environment", environment)
                return nil
        }
 
@@ -269,8 +271,18 @@ func (c *commandeer) loadConfig(mustHaveConfigFile, running bool) error {
                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)
 
index 0a6ea8860a9a91b47d92873dd9d9135c345f27bb..0dfa10e6766e4a48570cdaa3fdfdc9153ee6b604 100644 (file)
 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 @@ Complete documentation is available at http://gohugo.io/.`,
        })
 
        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 @@ Complete documentation is available at http://gohugo.io/.`,
 }
 
 type hugoBuilderCommon struct {
-       source  string
-       baseURL string
+       source      string
+       baseURL     string
+       environment string
 
        buildWatch bool
 
@@ -200,15 +207,45 @@ type hugoBuilderCommon struct {
        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")
        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/")
index 84afe4419719fadf6ba357f7aaf524003a55e609..9b9806694291fdc452054beff98b421e9c52cf6d 100644 (file)
@@ -56,8 +56,11 @@ func TestCommandsPersistentFlags(t *testing.T) {
                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 @@ func TestCommandsPersistentFlags(t *testing.T) {
                        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 @@ func TestCommandsPersistentFlags(t *testing.T) {
                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 @@ Single: {{ .Title }}
        writeFile(t, filepath.Join(d, "layouts", "_default", "list.html"), `
 
 List: {{ .Title }}
+Environment: {{ hugo.Environment }}
 
 `)
 
index 759efc17bd0f6a53b9021698861089f7eab20854..74173fa8472f4f81710c901b546e2fe77f42f410 100644 (file)
@@ -718,8 +718,8 @@ func (c *commandeer) newWatcher(dirList ...string) (*watcher.Batcher, error) {
        // 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 @@ func (c *commandeer) handleEvents(watcher *watcher.Batcher,
        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 @@ func (c *commandeer) handleEvents(watcher *watcher.Batcher,
                                        }
                                }
                        }
-                       // Config file changed. Need full rebuild.
+                       // Config file(s) changed. Need full rebuild.
                        c.fullRebuild()
                        break
                }
index 58d1a60fb76a424d5ee4c4b9e51006e7ce0be9d2..c2bd76dae54eec3ad87b5ca5c46aa17454966dc1 100644 (file)
@@ -36,7 +36,6 @@ import (
        "github.com/gohugoio/hugo/tpl"
 
        "github.com/gohugoio/hugo/config"
-
        "github.com/gohugoio/hugo/helpers"
        "github.com/spf13/afero"
        "github.com/spf13/cobra"
@@ -301,6 +300,8 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, string, string, erro
 
        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 {
                        jww.FEEDBACK.Println("Serving pages from " + absPublishDir)
index 438837a90a8f2dcdd98e265abbd21440bd9b960d..24f203ad0f096bc0cc65a26eabcb23d9c3e607f3 100644 (file)
@@ -68,6 +68,7 @@ func TestServer(t *testing.T) {
        homeContent := helpers.ReaderToString(resp.Body)
 
        assert.Contains(homeContent, "List: Hugo Commands")
+       assert.Contains(homeContent, "Environment: development")
 
        // Stop the server.
        stop <- true
index 88cb06c8cf921b8e01dee1fdf51f4aaa4f73ce0a..3a72f47908369fef0628177ffbf46981d64fc3b7 100644 (file)
@@ -17,10 +17,10 @@ package herrors
 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 @@ func chromaLexerFromType(fileType string) string {
        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)
 }
 
index b93b10bf15e1d6130a923ecf95da50f5b68dfc7a..62d923bf0f657001d07d77461c24477c993a9ea8 100644 (file)
@@ -18,28 +18,50 @@ import (
        "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,
        }
 }
index 18a9b594ff1b5a49fa727eaaf0d8c6bb7fd037d0..1769db5873eaf2f24489e70f92eec67a0231cd08 100644 (file)
@@ -23,12 +23,13 @@ import (
 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()))
 
 }
index 204f8f7470a9d0804da96c1506c85d76190c7b77..e9deb6acf97935e96dc6e66a57e5187e120a7654 100644 (file)
@@ -130,8 +130,8 @@ func BuildVersionString() string {
        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 @@ func BuildVersionString() string {
 
        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)
 
 }
 
index a114b557caac5b4363942a47ac8cdcf7693dae2a..e0d4f964df8692334d3c415cae609c01fe32fce6 100644 (file)
@@ -16,6 +16,8 @@ package maps
 import (
        "strings"
 
+       "github.com/gobwas/glob"
+
        "github.com/spf13/cast"
 )
 
@@ -42,3 +44,73 @@ func ToLower(m map[string]interface{}) {
 
        }
 }
+
+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
+               }
+       }
+}
index 37add5dc50baebd7414b36fab9fbbc4d12cde7a8..29bffa6bc3c50fae726d9d2246bb2ada20170e46 100644 (file)
@@ -16,6 +16,8 @@ package maps
 import (
        "reflect"
        "testing"
+
+       "github.com/stretchr/testify/require"
 )
 
 func TestToLower(t *testing.T) {
@@ -70,3 +72,52 @@ func TestToLower(t *testing.T) {
                }
        }
 }
+
+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)
+       }
+
+}
diff --git a/config/configLoader.go b/config/configLoader.go
new file mode 100644 (file)
index 0000000..b60aa3f
--- /dev/null
@@ -0,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
+}
index 432948d7462be4cb8f8a38ff3c2d7dc0d7653726..bc0dd950d7ad6e1e037b3aba9b3dc3fb5fa50984 100644 (file)
 package config
 
 import (
-       "strings"
-
        "github.com/spf13/cast"
-
-       "github.com/spf13/viper"
 )
 
 // Provider provides the configuration settings for Hugo.
@@ -34,16 +30,6 @@ type Provider 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.
 // It differs from the GetStringSlice method in that if the config value is a string,
 // we do not attempt to split it into fields.
diff --git a/go.mod b/go.mod
index 9628a2853a97f96ef9a09d64c33fdd470d04568a..29ece00e9d5d50a38ea9f0b3888b58de30d90e26 100644 (file)
--- a/go.mod
+++ b/go.mod
@@ -33,7 +33,7 @@ require (
        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 @@ require (
        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
diff --git a/go.sum b/go.sum
index 951e4c6f924e2341f7a36e07e544e6fd98489d81..84bc927264f60042e16df924d19b27c51f1cca9c 100644 (file)
--- a/go.sum
+++ b/go.sum
@@ -14,6 +14,7 @@ github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 h1:JHZL0hZKJ1VEN
 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 h1:J9UrYDhzucUMHXsCKG+kICvpR5dT1cqZdVFT
 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 h1:t47x5vThdwgLJzofNsbsAl7gmIiJ7kbDQN5BxwBmwvY=
 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 h1:
 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 h1:Iwxhe
 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,24 +137,30 @@ github.com/tdewolff/parse/v2 v2.3.5 h1:/uS8JfhwVJsNkEh769GM5ENv6L9LOh2Z9uW3tCdlh
 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=
 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=
index 004e949539b3f436f34eaafcdb2ffd67a7cd2cc7..e7fe2779b56f48f57e60365c3b021477e8f92636 100644 (file)
@@ -2,7 +2,7 @@ project_name: hugo_extended
 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
index 0e900601932d1b90207c4861dafca6b1fb99bbb4..5f3e444cce24a749d6007b64d44cdf845adae710 100644 (file)
@@ -2,7 +2,7 @@ project_name: hugo
 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:
index 92b58de8470a7638f9cc5dc7663fd57569a8c81f..2d0e8aa645d9c95b8313df8e52a618900cf010f6 100644 (file)
@@ -274,6 +274,13 @@ func FileAndExt(in string) (string, string) {
        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) {
@@ -400,6 +407,8 @@ func ExtractRootPaths(paths []string) []string {
 
 }
 
+var numInPathRe = regexp.MustCompile("\\.(\\d+)\\.")
+
 // FindCWD returns the current working directory from where the Hugo
 // executable is run.
 func FindCWD() (string, error) {
index 168c848aa274f01d12e8ba04717bc435506ab43d..f5aa6ff2513e88171c9cef599279f5e030f09170 100644 (file)
@@ -39,7 +39,7 @@ func (t testSite) Language() *langs.Language {
 // 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()),
        }
 }
diff --git a/htesting/testdata_builder.go b/htesting/testdata_builder.go
new file mode 100644 (file)
index 0000000..d7ba185
--- /dev/null
@@ -0,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
+}
index 77ebb42ae6f8764df831d7d3877410144757ddc9..3a452d5fd05d08d7310012c8fd1e63a6a9be5c34 100644 (file)
 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,96 +70,84 @@ func loadSiteConfig(cfg config.Provider) (scfg SiteConfig, err error) {
 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"})
        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)
-
-       if d.Path == "" {
-               d.Path = "."
-       }
+       l := configLoader{ConfigSourceDescriptor: d}
 
-       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 {
                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,8 +169,179 @@ func LoadConfig(d ConfigSourceDescriptor, doWithConfig ...func(cfg config.Provid
                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 {
@@ -289,12 +453,11 @@ func loadLanguageSettings(cfg config.Provider, oldLangs langs.Languages) error {
        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 @@ func loadThemeConfig(d ConfigSourceDescriptor, v1 *viper.Viper) ([]string, error
        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 loadThemeConfig(d ConfigSourceDescriptor, v1 *viper.Viper) ([]string, error
 
 }
 
-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 @@ func applyThemeConfig(v1 *viper.Viper, theme paths.ThemeConfig) error {
                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 @@ func applyThemeConfig(v1 *viper.Viper, theme paths.ThemeConfig) error {
        }
 
        // 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 applyThemeConfig(v1 *viper.Viper, theme paths.ThemeConfig) error {
 
 }
 
-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 @@ func loadDefaultSettingsFor(v *viper.Viper) error {
        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)
index 1f9e7377c0d5b6affb14745b73a79e2ac29fc4aa..885a07ee9519f348600bbb230ea36ff7770d3014 100644 (file)
@@ -247,8 +247,8 @@ map[string]interface {}{
        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 @@ map[string]interface {}{
   },
   "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 @@ map[string]interface {}{
 
        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"])
 
diff --git a/hugolib/configdir_test.go b/hugolib/configdir_test.go
new file mode 100644 (file)
index 0000000..80fcda6
--- /dev/null
@@ -0,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)
+
+}
index 0bb3b43621b3486fd39c3212a9abe071dc4ab473..5e75135c0fa03989ab89c228a5b06a61fb555913 100644 (file)
@@ -21,6 +21,8 @@ import (
        "strings"
        "sync"
 
+       "github.com/gohugoio/hugo/config"
+
        "github.com/gohugoio/hugo/publisher"
 
        "github.com/gohugoio/hugo/common/herrors"
@@ -361,14 +363,14 @@ func (h *HugoSites) resetLogs() {
        }
 }
 
-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 {
                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 @@ func (h *HugoSites) toSiteInfos() []*SiteInfo {
 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).
index 4c275f55bab0ce6572cd75c13fd60acbbb66c56a..ec5070fa814afd264d2c2ea17f5a68d4e8b0a427 100644 (file)
@@ -144,8 +144,8 @@ func (h *HugoSites) init(config *BuildCfg) error {
                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) init(config *BuildCfg) error {
 }
 
 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 {
index f1e317f5967a2874f3d6f25faa4c2ae545eb6dd2..91ae8434d9bec8cf8f77bab661ce11fce1fbe69e 100644 (file)
@@ -11,14 +11,11 @@ import (
        "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 @@ title = "Svenska"
 
        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 @@ func TestChangeDefaultLanguage(t *testing.T) {
                "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)
        }
index 81880023a4c88e60b6bd2de267194f1bb5b96281..15ed631c176c5064d1f97da692671e5cd2328ae7 100644 (file)
@@ -1647,7 +1647,12 @@ func (p *Page) Menus() PageMenus {
        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}
index 177ed7fb1b96b67964d2aa0132e830843e851349..b8273ce28c3f0e5fc50ddaa19a54c2c3bdbba573 100644 (file)
@@ -1436,7 +1436,7 @@ func TestIndexPageSimpleMethods(t *testing.T) {
                {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()
index c6dae5e628463a6bdf83d50efd78d93efb145cab..1ce8e7997a693a18201d277769db628aea5597fd 100644 (file)
@@ -20,7 +20,6 @@ import (
        "github.com/gohugoio/hugo/config"
        "github.com/spf13/afero"
        "github.com/spf13/cast"
-       "github.com/spf13/viper"
 )
 
 type ThemeConfig struct {
@@ -73,18 +72,11 @@ func (c *themesCollector) add(name, configFilename string) (ThemeConfig, error)
        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}
index 0579edf6edfe23b1e9ce0ec49c2211ddd977c7a4..7882d517fa5725b60721ae20d4880b1fc1655fe7 100644 (file)
@@ -1226,7 +1226,7 @@ func (s *Site) initializeSiteInfo() error {
                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 @@ func (s *Site) getMenusFromConfig() Menus {
 
        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 {
index 33f973e96e415357edd8825b23c6e6de4caf8d4f..a60ca29059641ffa9e9ba2dbc91128108608daa1 100644 (file)
@@ -322,6 +322,15 @@ func (s *sitesBuilder) CreateSites() *sitesBuilder {
        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 @@ func (s *sitesBuilder) CreateSitesE() error {
        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})
index cd873dd770b906f035a7bad12ebf9335ec760a50..19485b2bebb840e4146bb480b606e86daee2dc9c 100644 (file)
@@ -21,10 +21,10 @@ import (
 
 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"
index e03a6aacd0c53d672d093a17ed4ced1421e1fb4f..6da791c73a583a4875f6f20fa919600428baf73d 100644 (file)
@@ -22,6 +22,7 @@ import (
        "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 @@ func UnmarshalToMap(data []byte, f Format) (map[string]interface{}, error) {
        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{}.
index b9f7f691905e56f7d15f4eb31876be221d28cd3c..3f5a8a5c179cda63bbe12f0b866934f13a75147a 100644 (file)
@@ -14,6 +14,7 @@
 package metadecoders
 
 import (
+       "path/filepath"
        "strings"
 
        "github.com/gohugoio/hugo/parser/pageparser"
@@ -34,6 +35,11 @@ const (
 // 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
index 46b4e434af6ed35b78f532bc4b36f382f1393a40..a22e84f981f14af42b378349f6aa074493a73789 100644 (file)
@@ -32,6 +32,7 @@ func TestFormatFromString(t *testing.T) {
                {"yaml", YAML},
                {"yml", YAML},
                {"toml", TOML},
+               {"config.toml", TOML},
                {"tOMl", TOML},
                {"org", ORG},
                {"foo", ""},