summaryrefslogtreecommitdiffstats
path: root/hugolib/config.go
diff options
context:
space:
mode:
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2018-11-15 09:28:02 +0100
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2018-12-11 13:08:36 +0100
commit7829474088f835251f04caa1121d47e35fe89f7e (patch)
treef9b91d601befc966162036559e4418bebf46d643 /hugolib/config.go
parent256418917c6642f7e5b3d3206ff4b6fa03b1cb28 (diff)
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
Diffstat (limited to 'hugolib/config.go')
-rw-r--r--hugolib/config.go292
1 files changed, 228 insertions, 64 deletions
diff --git a/hugolib/config.go b/hugolib/config.go
index 77ebb42ae..3a452d5fd 100644
--- 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,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)