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
This commit is contained in:
Bjørn Erik Pedersen 2018-11-15 09:28:02 +01:00
parent 256418917c
commit 7829474088
No known key found for this signature in database
GPG Key ID: 330E6E2BD4859D8F
36 changed files with 904 additions and 187 deletions

View File

@ -249,6 +249,8 @@ func (c *commandeer) loadConfig(mustHaveConfigFile, running bool) error {
sourceFs = c.DepsCfg.Fs.Source sourceFs = c.DepsCfg.Fs.Source
} }
environment := c.h.getEnvironment(running)
doWithConfig := func(cfg config.Provider) error { doWithConfig := func(cfg config.Provider) error {
if c.ftch != nil { if c.ftch != nil {
@ -256,7 +258,7 @@ func (c *commandeer) loadConfig(mustHaveConfigFile, running bool) error {
} }
cfg.Set("workingDir", dir) cfg.Set("workingDir", dir)
cfg.Set("environment", environment)
return nil return nil
} }
@ -269,8 +271,18 @@ func (c *commandeer) loadConfig(mustHaveConfigFile, running bool) error {
return err return err
} }
configPath := c.h.source
if configPath == "" {
configPath = dir
}
config, configFiles, err := hugolib.LoadConfig( 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, doWithCommandeer,
doWithConfig) doWithConfig)

View File

@ -14,6 +14,11 @@
package commands package commands
import ( import (
"os"
"github.com/gohugoio/hugo/hugolib/paths"
"github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/helpers" "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.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") cc.cmd.PersistentFlags().BoolVar(&cc.quiet, "quiet", false, "build in quiet mode")
// Set bash-completion // Set bash-completion
@ -185,8 +191,9 @@ Complete documentation is available at http://gohugo.io/.`,
} }
type hugoBuilderCommon struct { type hugoBuilderCommon struct {
source string source string
baseURL string baseURL string
environment string
buildWatch bool buildWatch bool
@ -200,15 +207,45 @@ type hugoBuilderCommon struct {
quiet bool quiet bool
cfgFile string cfgFile string
cfgDir string
logFile 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) { func (cc *hugoBuilderCommon) handleFlags(cmd *cobra.Command) {
cmd.Flags().Bool("cleanDestinationDir", false, "remove files from destination not found in static directories") 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("buildDrafts", "D", false, "include content marked as draft")
cmd.Flags().BoolP("buildFuture", "F", false, "include content with publishdate in the future") cmd.Flags().BoolP("buildFuture", "F", false, "include content with publishdate in the future")
cmd.Flags().BoolP("buildExpired", "E", false, "include expired content") 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.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("contentDir", "c", "", "filesystem path to content directory")
cmd.Flags().StringP("layoutDir", "l", "", "filesystem path to layout directory") cmd.Flags().StringP("layoutDir", "l", "", "filesystem path to layout directory")
cmd.Flags().StringP("cacheDir", "", "", "filesystem path to cache directory. Defaults: $TMPDIR/hugo_cache/") cmd.Flags().StringP("cacheDir", "", "", "filesystem path to cache directory. Defaults: $TMPDIR/hugo_cache/")

View File

@ -56,8 +56,11 @@ func TestCommandsPersistentFlags(t *testing.T) {
check func(command []cmder) check func(command []cmder)
}{{[]string{"server", }{{[]string{"server",
"--config=myconfig.toml", "--config=myconfig.toml",
"--configDir=myconfigdir",
"--contentDir=mycontent", "--contentDir=mycontent",
"--disableKinds=page,home", "--disableKinds=page,home",
"--environment=testing",
"--configDir=myconfigdir",
"--layoutDir=mylayouts", "--layoutDir=mylayouts",
"--theme=mytheme", "--theme=mytheme",
"--gc", "--gc",
@ -78,6 +81,7 @@ func TestCommandsPersistentFlags(t *testing.T) {
if b, ok := command.(commandsBuilderGetter); ok { if b, ok := command.(commandsBuilderGetter); ok {
v := b.getCommandsBuilder().hugoBuilderCommon v := b.getCommandsBuilder().hugoBuilderCommon
assert.Equal("myconfig.toml", v.cfgFile) assert.Equal("myconfig.toml", v.cfgFile)
assert.Equal("myconfigdir", v.cfgDir)
assert.Equal("mysource", v.source) assert.Equal("mysource", v.source)
assert.Equal("https://example.com/b/", v.baseURL) assert.Equal("https://example.com/b/", v.baseURL)
} }
@ -93,6 +97,7 @@ func TestCommandsPersistentFlags(t *testing.T) {
assert.True(sc.noHTTPCache) assert.True(sc.noHTTPCache)
assert.True(sc.renderToDisk) assert.True(sc.renderToDisk)
assert.Equal(1366, sc.serverPort) assert.Equal(1366, sc.serverPort)
assert.Equal("testing", sc.environment)
cfg := viper.New() cfg := viper.New()
sc.flagsToConfig(cfg) sc.flagsToConfig(cfg)
@ -233,6 +238,7 @@ Single: {{ .Title }}
writeFile(t, filepath.Join(d, "layouts", "_default", "list.html"), ` writeFile(t, filepath.Join(d, "layouts", "_default", "list.html"), `
List: {{ .Title }} List: {{ .Title }}
Environment: {{ hugo.Environment }}
`) `)

View File

@ -718,8 +718,8 @@ func (c *commandeer) newWatcher(dirList ...string) (*watcher.Batcher, error) {
// Identifies changes to config (config.toml) files. // Identifies changes to config (config.toml) files.
configSet := make(map[string]bool) configSet := make(map[string]bool)
c.logger.FEEDBACK.Println("Watching for config changes in", strings.Join(c.configFiles, ", "))
for _, configFile := range c.configFiles { for _, configFile := range c.configFiles {
c.logger.FEEDBACK.Println("Watching for config changes in", configFile)
watcher.Add(configFile) watcher.Add(configFile)
configSet[configFile] = true configSet[configFile] = true
} }
@ -750,7 +750,17 @@ func (c *commandeer) handleEvents(watcher *watcher.Batcher,
configSet map[string]bool) { configSet map[string]bool) {
for _, ev := range evs { 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 { if ev.Op&fsnotify.Chmod == fsnotify.Chmod {
continue 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() c.fullRebuild()
break break
} }

View File

@ -36,7 +36,6 @@ import (
"github.com/gohugoio/hugo/tpl" "github.com/gohugoio/hugo/tpl"
"github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/helpers"
"github.com/spf13/afero" "github.com/spf13/afero"
"github.com/spf13/cobra" "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) absPublishDir := f.c.hugo.PathSpec.AbsPathify(publishDir)
jww.FEEDBACK.Printf("Environment: %q", f.c.hugo.Deps.Site.Hugo().Environment)
if i == 0 { if i == 0 {
if f.s.renderToDisk { if f.s.renderToDisk {
jww.FEEDBACK.Println("Serving pages from " + absPublishDir) jww.FEEDBACK.Println("Serving pages from " + absPublishDir)

View File

@ -68,6 +68,7 @@ func TestServer(t *testing.T) {
homeContent := helpers.ReaderToString(resp.Body) homeContent := helpers.ReaderToString(resp.Body)
assert.Contains(homeContent, "List: Hugo Commands") assert.Contains(homeContent, "List: Hugo Commands")
assert.Contains(homeContent, "Environment: development")
// Stop the server. // Stop the server.
stop <- true stop <- true

View File

@ -17,10 +17,10 @@ package herrors
import ( import (
"io" "io"
"io/ioutil" "io/ioutil"
"path/filepath"
"strings" "strings"
"github.com/gohugoio/hugo/common/text" "github.com/gohugoio/hugo/common/text"
"github.com/gohugoio/hugo/helpers"
"github.com/spf13/afero" "github.com/spf13/afero"
) )
@ -172,12 +172,16 @@ func chromaLexerFromType(fileType string) string {
return fileType return fileType
} }
func extNoDelimiter(filename string) string {
return strings.TrimPrefix(".", filepath.Ext(filename))
}
func chromaLexerFromFilename(filename string) string { func chromaLexerFromFilename(filename string) string {
if strings.Contains(filename, "layouts") { if strings.Contains(filename, "layouts") {
return "go-html-template" return "go-html-template"
} }
ext := helpers.ExtNoDelimiter(filename) ext := extNoDelimiter(filename)
return chromaLexerFromType(ext) return chromaLexerFromType(ext)
} }

View File

@ -18,28 +18,50 @@ import (
"html/template" "html/template"
) )
var ( const (
// CommitHash contains the current Git revision. Use make to build to make EnvironmentDevelopment = "development"
// sure this gets set. EnvironmentProduction = "production"
CommitHash string )
// BuildDate contains the date of the current build. var (
BuildDate string // commitHash contains the current Git revision. Use make to build to make
// sure this gets set.
commitHash string
// buildDate contains the date of the current build.
buildDate string
) )
// Info contains information about the current Hugo environment // Info contains information about the current Hugo environment
type Info struct { type Info struct {
Version VersionString
Generator template.HTML
CommitHash string CommitHash string
BuildDate 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{ return Info{
Version: CurrentVersion.Version(), CommitHash: commitHash,
CommitHash: CommitHash, BuildDate: buildDate,
BuildDate: BuildDate, Environment: environment,
Generator: template.HTML(fmt.Sprintf(`<meta name="generator" content="Hugo %s" />`, CurrentVersion.String())),
} }
} }

View File

@ -23,12 +23,13 @@ import (
func TestHugoInfo(t *testing.T) { func TestHugoInfo(t *testing.T) {
assert := require.New(t) assert := require.New(t)
hugoInfo := NewInfo() hugoInfo := NewInfo("")
assert.Equal(CurrentVersion.Version(), hugoInfo.Version) assert.Equal(CurrentVersion.Version(), hugoInfo.Version())
assert.IsType(VersionString(""), hugoInfo.Version) assert.IsType(VersionString(""), hugoInfo.Version())
assert.Equal(CommitHash, hugoInfo.CommitHash) assert.Equal(commitHash, hugoInfo.CommitHash)
assert.Equal(BuildDate, hugoInfo.BuildDate) assert.Equal(buildDate, hugoInfo.BuildDate)
assert.Contains(hugoInfo.Generator, fmt.Sprintf("Hugo %s", hugoInfo.Version)) assert.Equal("production", hugoInfo.Environment)
assert.Contains(hugoInfo.Generator(), fmt.Sprintf("Hugo %s", hugoInfo.Version()))
} }

View File

@ -130,8 +130,8 @@ func BuildVersionString() string {
program := "Hugo Static Site Generator" program := "Hugo Static Site Generator"
version := "v" + CurrentVersion.String() version := "v" + CurrentVersion.String()
if CommitHash != "" { if commitHash != "" {
version += "-" + strings.ToUpper(CommitHash) version += "-" + strings.ToUpper(commitHash)
} }
if isExtended { if isExtended {
version += "/extended" version += "/extended"
@ -139,14 +139,12 @@ func BuildVersionString() string {
osArch := runtime.GOOS + "/" + runtime.GOARCH osArch := runtime.GOOS + "/" + runtime.GOARCH
var buildDate string date := buildDate
if BuildDate != "" { if date == "" {
buildDate = BuildDate date = "unknown"
} else {
buildDate = "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)
} }

View File

@ -16,6 +16,8 @@ package maps
import ( import (
"strings" "strings"
"github.com/gobwas/glob"
"github.com/spf13/cast" "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
}
}
}

View File

@ -16,6 +16,8 @@ package maps
import ( import (
"reflect" "reflect"
"testing" "testing"
"github.com/stretchr/testify/require"
) )
func TestToLower(t *testing.T) { 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)
}
}

106
config/configLoader.go Normal file
View File

@ -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
}

View File

@ -14,11 +14,7 @@
package config package config
import ( import (
"strings"
"github.com/spf13/cast" "github.com/spf13/cast"
"github.com/spf13/viper"
) )
// Provider provides the configuration settings for Hugo. // Provider provides the configuration settings for Hugo.
@ -34,16 +30,6 @@ type Provider interface {
IsSet(key string) bool 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. // 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, // It differs from the GetStringSlice method in that if the config value is a string,
// we do not attempt to split it into fields. // we do not attempt to split it into fields.

8
go.mod
View File

@ -33,7 +33,7 @@ require (
github.com/mattn/go-runewidth v0.0.3 // indirect github.com/mattn/go-runewidth v0.0.3 // indirect
github.com/miekg/mmark v1.3.6 github.com/miekg/mmark v1.3.6
github.com/mitchellh/hashstructure v1.0.0 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/muesli/smartcrop v0.0.0-20180228075044-f6ebaa786a12
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
github.com/nicksnyder/go-i18n v1.10.0 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/jwalterweatherman v1.0.1-0.20181028145347-94f6ae3ed3bc
github.com/spf13/nitro v0.0.0-20131003134307-24d7ef30a12d github.com/spf13/nitro v0.0.0-20131003134307-24d7ef30a12d
github.com/spf13/pflag v1.0.3 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/stretchr/testify v1.2.3-0.20181014000028-04af85275a5c
github.com/tdewolff/minify/v2 v2.3.7 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 github.com/yosssi/ace v0.0.5
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81 golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd // indirect golang.org/x/net v0.0.0-20180906233101-161cd47e91fd // indirect
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f 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 golang.org/x/text v0.3.0
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect 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 exclude github.com/chaseadamsio/goorgeous v2.0.0+incompatible

32
go.sum
View File

@ -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/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 h1:p9Sln00KOTlrYkxI1zYWl1QLnEqAqEARBEYa8FQnQcY=
github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= 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 h1:6ocXeW2iZ/7vAzgXz82J00tYxncMiEEBExPftTtOQzk=
github.com/bep/debounce v1.1.0/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0= github.com/bep/debounce v1.1.0/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
github.com/bep/gitmap v1.0.0 h1:cTTZwq7vpGuhwefKCBDV9UrHnZAPVJTvoWobimrqkUc= 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/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 h1:SKI1/fuSdodxmNNyVBR8d7X/HuLnRpvvFO0AgyQk764=
github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927/go.mod h1:h/aW8ynjgkuj+NQRlZcDbAbM1ORAbXjXX77sX7T289U= 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 h1:DwoNytLphI8hzS2Af4D0dfaEaiSq2bN05mEm4R6vf8M=
github.com/cpuguy83/go-md2man v1.0.8/go.mod h1:N6JayAiVKtlHSnuTCeuLSQVs75hb8q+dYQLjr7cDsKY= github.com/cpuguy83/go-md2man v1.0.8/go.mod h1:N6JayAiVKtlHSnuTCeuLSQVs75hb8q+dYQLjr7cDsKY=
github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ= 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/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 h1:ZkRJX1CyOoTkar7p/mLS5TZU4nJ1Rn/F8u9dGS02Q3Y=
github.com/mitchellh/hashstructure v1.0.0/go.mod h1:QjSHrPWS+BGUVBYkbTZWEnOh3G1DutKwClXU/ABz6AQ= 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.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE=
github.com/mitchellh/mapstructure v1.0.0/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= 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 h1:l0X/8IDy2UoK+oXcQFMRSIOcyuYb5iEPytPGplnM41Y=
github.com/muesli/smartcrop v0.0.0-20180228075044-f6ebaa786a12/go.mod h1:i2fCI/UorTfgEpPPLWiFBv4pye+YAG78RwcQLUkocpI= 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= 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/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 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= 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 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8= 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/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 h1:ihvj2nmx8eqWjlgNgdW6h0DyGJuq5GiwHadJkG0wXtQ=
github.com/spf13/nitro v0.0.0-20131003134307-24d7ef30a12d/go.mod h1:jU8A+8xL+6n1OX4XaZtCj4B3mIa64tULUsD6YegdpFo= 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 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 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.3.0 h1:cO6QlTTeK9RQDhFAbGLV5e3fHXbRpin/Gi8qfL4rdLk=
github.com/spf13/viper v1.2.0/go.mod h1:P4AexN0a+C9tGAnUFNwDMYYZv3pjFuvmeiMyKRaNVlI= 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 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 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= 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/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 h1:jOwzqCXr5ePXEPGJaq2ivoR6HOCi+D5TPfpoyg8yvmU=
github.com/tdewolff/test v1.0.0/go.mod h1:DiQUlutnqlEvdvhSn2LPGy4TFwRauAaYDsL+683RNX4= 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 h1:9vG9vvVNVupO4Y7uwFkRgIMNe9rdaJMCINDe8vhAhLo=
github.com/wellington/go-libsass v0.9.3-0.20181113175235-c63644206701/go.mod h1:mxgxgam0N0E+NAUMHLcu20Ccfc3mVpDkyrLDayqfiTs= 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 h1:tUkIP/BLdKqrlrPwcmH0shwEEhTRHoGnc1wFIWmaBUA=
github.com/yosssi/ace v0.0.5/go.mod h1:ALfIzm2vT7t5ZE7uoIZqF3TQ7SAOyupFZnkrF5id+K0= 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 h1:00VmoueYNlNz/aHIilyyQz/MHSqGoWJzpFv/HW8xpzI=
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= 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 h1:nTDtHvHSdCn1m6ITfMRqtOd/9+7a3s8RBNOZ3eYZzJA=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 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 h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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 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-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 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 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 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 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 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.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=

View File

@ -2,7 +2,7 @@ project_name: hugo_extended
builds: builds:
- binary: hugo - binary: hugo
ldflags: 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'" - "-extldflags '-static'"
env: env:
- CGO_ENABLED=1 - CGO_ENABLED=1

View File

@ -2,7 +2,7 @@ project_name: hugo
build: build:
main: main.go main: main.go
binary: hugo 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: env:
- CGO_ENABLED=0 - CGO_ENABLED=0
goos: goos:

View File

@ -274,6 +274,13 @@ func FileAndExt(in string) (string, string) {
return fileAndExt(in, fpb) 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, // Filename takes a path, strips out the extension,
// and returns the name of the file. // and returns the name of the file.
func Filename(in string) (name string) { 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 // FindCWD returns the current working directory from where the Hugo
// executable is run. // executable is run.
func FindCWD() (string, error) { func FindCWD() (string, error) {

View File

@ -39,7 +39,7 @@ func (t testSite) Language() *langs.Language {
// NewTestHugoSite creates a new minimal test site. // NewTestHugoSite creates a new minimal test site.
func NewTestHugoSite() hugo.Site { func NewTestHugoSite() hugo.Site {
return testSite{ return testSite{
h: hugo.NewInfo(), h: hugo.NewInfo(hugo.EnvironmentProduction),
l: langs.NewLanguage("en", newTestConfig()), l: langs.NewLanguage("en", newTestConfig()),
} }
} }

View File

@ -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
}

View File

@ -14,14 +14,19 @@
package hugolib package hugolib
import ( import (
"errors"
"fmt" "fmt"
"io"
"os"
"path/filepath"
"strings" "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/gohugoio/hugo/hugolib/paths"
"github.com/pkg/errors"
_errors "github.com/pkg/errors" _errors "github.com/pkg/errors"
"github.com/gohugoio/hugo/langs" "github.com/gohugoio/hugo/langs"
@ -65,96 +70,84 @@ func loadSiteConfig(cfg config.Provider) (scfg SiteConfig, err error) {
type ConfigSourceDescriptor struct { type ConfigSourceDescriptor struct {
Fs afero.Fs 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 Filename string
// The path to the directory to look for configuration. Is used if Filename is not // 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 Path string
// The project's working dir. Is used to look for additional theme config. // The project's working dir. Is used to look for additional theme config.
WorkingDir string WorkingDir string
// The (optional) directory for additional configuration files.
AbsConfigDir string
// production, development
Environment string
} }
func (d ConfigSourceDescriptor) configFilenames() []string { func (d ConfigSourceDescriptor) configFilenames() []string {
if d.Filename == "" {
return []string{"config"}
}
return strings.Split(d.Filename, ",") 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. // LoadConfigDefault is a convenience method to load the default "config.toml" config.
func LoadConfigDefault(fs afero.Fs) (*viper.Viper, error) { func LoadConfigDefault(fs afero.Fs) (*viper.Viper, error) {
v, _, err := LoadConfig(ConfigSourceDescriptor{Fs: fs, Filename: "config.toml"}) v, _, err := LoadConfig(ConfigSourceDescriptor{Fs: fs, Filename: "config.toml"})
return v, err 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 // LoadConfig loads Hugo configuration into a new Viper and then adds
// a set of defaults. // a set of defaults.
func LoadConfig(d ConfigSourceDescriptor, doWithConfig ...func(cfg config.Provider) error) (*viper.Viper, []string, error) { func LoadConfig(d ConfigSourceDescriptor, doWithConfig ...func(cfg config.Provider) error) (*viper.Viper, []string, error) {
if d.Environment == "" {
d.Environment = hugo.EnvironmentProduction
}
var configFiles []string var configFiles []string
fs := d.Fs
v := viper.New() v := viper.New()
v.SetFs(fs) l := configLoader{ConfigSourceDescriptor: d}
if d.Path == "" {
d.Path = "."
}
configFilenames := d.configFilenames()
v.AutomaticEnv() v.AutomaticEnv()
v.SetEnvPrefix("hugo") v.SetEnvPrefix("hugo")
v.SetConfigFile(configFilenames[0])
v.AddConfigPath(d.Path)
applyFileContext := func(filename string, err error) error { var cerr error
err, _ = herrors.WithFileContextForFile(
err,
filename,
filename,
fs,
herrors.SimpleLineMatcher)
return err for _, name := range d.configFilenames() {
var filename string
if filename, cerr = l.loadConfig(name, v); cerr != nil && cerr != ErrNoConfigFile {
return nil, nil, cerr
}
configFiles = append(configFiles, filename)
} }
var configFileErr error if d.AbsConfigDir != "" {
dirnames, err := l.loadConfigFromConfigDir(v)
err := v.ReadInConfig() if err == nil {
if err != nil { configFiles = append(configFiles, dirnames...)
if _, ok := err.(viper.ConfigParseError); ok {
return nil, configFiles, applyFileContext(v.ConfigFileUsed(), err)
} }
configFileErr = ErrNoConfigFile cerr = err
}
if configFileErr == nil {
if cf := v.ConfigFileUsed(); cf != "" {
configFiles = append(configFiles, cf)
}
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)
}
} }
if err := loadDefaultSettingsFor(v); err != nil { if err := loadDefaultSettingsFor(v); err != nil {
return v, configFiles, err return v, configFiles, err
} }
if configFileErr == nil { if cerr == nil {
themeConfigFiles, err := l.loadThemeConfig(v)
themeConfigFiles, err := loadThemeConfig(d, v)
if err != nil { if err != nil {
return v, configFiles, err return v, configFiles, err
} }
@ -176,10 +169,181 @@ func LoadConfig(d ConfigSourceDescriptor, doWithConfig ...func(cfg config.Provid
return v, configFiles, err 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 { func loadLanguageSettings(cfg config.Provider, oldLangs langs.Languages) error {
defaultLang := cfg.GetString("defaultContentLanguage") defaultLang := cfg.GetString("defaultContentLanguage")
@ -289,12 +453,11 @@ func loadLanguageSettings(cfg config.Provider, oldLangs langs.Languages) error {
return nil return nil
} }
func loadThemeConfig(d ConfigSourceDescriptor, v1 *viper.Viper) ([]string, error) { func (l configLoader) loadThemeConfig(v1 *viper.Viper) ([]string, error) {
themesDir := paths.AbsPathify(d.WorkingDir, v1.GetString("themesDir")) themesDir := paths.AbsPathify(l.WorkingDir, v1.GetString("themesDir"))
themes := config.GetStringSlicePreserveString(v1, "theme") themes := config.GetStringSlicePreserveString(v1, "theme")
// CollectThemes(fs afero.Fs, themesDir string, themes []strin themeConfigs, err := paths.CollectThemes(l.Fs, themesDir, themes)
themeConfigs, err := paths.CollectThemes(d.Fs, themesDir, themes)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -309,7 +472,7 @@ func loadThemeConfig(d ConfigSourceDescriptor, v1 *viper.Viper) ([]string, error
for _, tc := range themeConfigs { for _, tc := range themeConfigs {
if tc.ConfigFilename != "" { if tc.ConfigFilename != "" {
configFilenames = append(configFilenames, 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 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 ( const (
paramsKey = "params" paramsKey = "params"
languagesKey = "languages" languagesKey = "languages"
menuKey = "menu" menuKey = "menus"
) )
v2 := theme.Cfg v2 := theme.Cfg
for _, key := range []string{paramsKey, "outputformats", "mediatypes"} { for _, key := range []string{paramsKey, "outputformats", "mediatypes"} {
mergeStringMapKeepLeft("", key, v1, v2) l.mergeStringMapKeepLeft("", key, v1, v2)
} }
themeLower := strings.ToLower(theme.Name) themeLower := strings.ToLower(theme.Name)
@ -348,7 +511,7 @@ func applyThemeConfig(v1 *viper.Viper, theme paths.ThemeConfig) error {
v1Langs := v1.GetStringMap(languagesKey) v1Langs := v1.GetStringMap(languagesKey)
for k := range v1Langs { for k := range v1Langs {
langParamsKey := languagesKey + "." + k + "." + paramsKey langParamsKey := languagesKey + "." + k + "." + paramsKey
mergeStringMapKeepLeft(paramsKey, langParamsKey, v1, v2) l.mergeStringMapKeepLeft(paramsKey, langParamsKey, v1, v2)
} }
v2Langs := v2.GetStringMap(languagesKey) v2Langs := v2.GetStringMap(languagesKey)
for k := range v2Langs { 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 // Add menu definitions from theme not found in project
if v2.IsSet("menu") { if v2.IsSet(menuKey) {
v2menus := v2.GetStringMap(menuKey) v2menus := v2.GetStringMap(menuKey)
for k, v := range v2menus { for k, v := range v2menus {
menuEntry := menuKey + "." + k 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) { if !v2.IsSet(key) {
return return
} }
@ -440,6 +603,7 @@ func loadDefaultSettingsFor(v *viper.Viper) error {
v.SetDefault("buildDrafts", false) v.SetDefault("buildDrafts", false)
v.SetDefault("buildFuture", false) v.SetDefault("buildFuture", false)
v.SetDefault("buildExpired", false) v.SetDefault("buildExpired", false)
v.SetDefault("environment", hugo.EnvironmentProduction)
v.SetDefault("uglyURLs", false) v.SetDefault("uglyURLs", false)
v.SetDefault("verbose", false) v.SetDefault("verbose", false)
v.SetDefault("ignoreCache", false) v.SetDefault("ignoreCache", false)

View File

@ -247,8 +247,8 @@ map[string]interface {}{
b.AssertObject(`map[string]interface {}{ b.AssertObject(`map[string]interface {}{
"en": map[string]interface {}{ "en": map[string]interface {}{
"languagename": "English", "languagename": "English",
"menu": map[string]interface {}{ "menus": map[string]interface {}{
"theme": []interface {}{ "theme": []map[string]interface {}{
map[string]interface {}{ map[string]interface {}{
"name": "menu-lang-en-theme", "name": "menu-lang-en-theme",
}, },
@ -265,8 +265,8 @@ map[string]interface {}{
}, },
"nb": map[string]interface {}{ "nb": map[string]interface {}{
"languagename": "Norsk", "languagename": "Norsk",
"menu": map[string]interface {}{ "menus": map[string]interface {}{
"theme": []interface {}{ "theme": []map[string]interface {}{
map[string]interface {}{ map[string]interface {}{
"name": "menu-lang-nb-theme", "name": "menu-lang-nb-theme",
}, },
@ -287,23 +287,23 @@ map[string]interface {}{
b.AssertObject(` b.AssertObject(`
map[string]interface {}{ map[string]interface {}{
"main": []interface {}{ "main": []map[string]interface {}{
map[string]interface {}{ map[string]interface {}{
"name": "menu-main-main", "name": "menu-main-main",
}, },
}, },
"thememenu": []interface {}{ "thememenu": []map[string]interface {}{
map[string]interface {}{ map[string]interface {}{
"name": "menu-theme", "name": "menu-theme",
}, },
}, },
"top": []interface {}{ "top": []map[string]interface {}{
map[string]interface {}{ map[string]interface {}{
"name": "menu-top-main", "name": "menu-top-main",
}, },
}, },
} }
`, got["menu"]) `, got["menus"])
assert.Equal("https://example.com/", got["baseurl"]) assert.Equal("https://example.com/", got["baseurl"])

152
hugolib/configdir_test.go Normal file
View File

@ -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)
}

View File

@ -21,6 +21,8 @@ import (
"strings" "strings"
"sync" "sync"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/publisher" "github.com/gohugoio/hugo/publisher"
"github.com/gohugoio/hugo/common/herrors" "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) oldLangs, _ := h.Cfg.Get("languagesSorted").(langs.Languages)
if err := loadLanguageSettings(h.Cfg, oldLangs); err != nil { if err := loadLanguageSettings(h.Cfg, oldLangs); err != nil {
return err return err
} }
depsCfg := deps.DepsCfg{Fs: h.Fs, Cfg: h.Cfg} depsCfg := deps.DepsCfg{Fs: h.Fs, Cfg: cfg}
sites, err := createSitesFromConfig(depsCfg) sites, err := createSitesFromConfig(depsCfg)
@ -412,9 +414,9 @@ func (h *HugoSites) toSiteInfos() []*SiteInfo {
type BuildCfg struct { type BuildCfg struct {
// Reset site state before build. Use to force full rebuilds. // Reset site state before build. Use to force full rebuilds.
ResetState bool 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. // This is needed if new languages are added.
CreateSitesFromConfig bool NewConfig config.Provider
// Skip rendering. Useful for testing. // Skip rendering. Useful for testing.
SkipRender bool SkipRender bool
// Use this to indicate what changed (for rebuilds). // Use this to indicate what changed (for rebuilds).

View File

@ -144,8 +144,8 @@ func (h *HugoSites) init(config *BuildCfg) error {
h.reset() h.reset()
} }
if config.CreateSitesFromConfig { if config.NewConfig != nil {
if err := h.createSitesFromConfig(); err != nil { if err := h.createSitesFromConfig(config.NewConfig); err != nil {
return err return err
} }
} }
@ -154,8 +154,8 @@ func (h *HugoSites) init(config *BuildCfg) error {
} }
func (h *HugoSites) initRebuild(config *BuildCfg) error { func (h *HugoSites) initRebuild(config *BuildCfg) error {
if config.CreateSitesFromConfig { if config.NewConfig != nil {
return errors.New("Rebuild does not support 'CreateSitesFromConfig'.") return errors.New("Rebuild does not support 'NewConfig'.")
} }
if config.ResetState { if config.ResetState {

View File

@ -11,14 +11,11 @@ import (
"path/filepath" "path/filepath"
"time" "time"
"github.com/gohugoio/hugo/langs"
"github.com/fortytw2/leaktest" "github.com/fortytw2/leaktest"
"github.com/fsnotify/fsnotify" "github.com/fsnotify/fsnotify"
"github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugofs" "github.com/gohugoio/hugo/hugofs"
"github.com/spf13/afero" "github.com/spf13/afero"
"github.com/spf13/viper"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -661,9 +658,8 @@ title = "Svenska"
sites := b.H sites := b.H
// Watching does not work with in-memory fs, so we trigger a reload manually assert.NoError(b.LoadConfig())
assert.NoError(sites.Cfg.(*langs.Language).Cfg.(*viper.Viper).ReadInConfig()) err := b.H.Build(BuildCfg{NewConfig: b.Cfg})
err := b.H.Build(BuildCfg{CreateSitesFromConfig: true})
if err != nil { if err != nil {
t.Fatalf("Failed to rebuild sites: %s", err) t.Fatalf("Failed to rebuild sites: %s", err)
@ -723,10 +719,9 @@ func TestChangeDefaultLanguage(t *testing.T) {
"DefaultContentLanguageInSubdir": false, "DefaultContentLanguageInSubdir": false,
}) })
// Watching does not work with in-memory fs, so we trigger a reload manually assert.NoError(b.LoadConfig())
// This does not look pretty, so we should think of something else. err := b.H.Build(BuildCfg{NewConfig: b.Cfg})
assert.NoError(b.H.Cfg.(*langs.Language).Cfg.(*viper.Viper).ReadInConfig())
err := b.H.Build(BuildCfg{CreateSitesFromConfig: true})
if err != nil { if err != nil {
t.Fatalf("Failed to rebuild sites: %s", err) t.Fatalf("Failed to rebuild sites: %s", err)
} }

View File

@ -1647,7 +1647,12 @@ func (p *Page) Menus() PageMenus {
p.pageMenusInit.Do(func() { p.pageMenusInit.Do(func() {
p.pageMenus = PageMenus{} 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() link := p.RelPermalink()
me := MenuEntry{Page: p, Name: p.LinkTitle(), Weight: p.Weight, URL: link} me := MenuEntry{Page: p, Name: p.LinkTitle(), Weight: p.Weight, URL: link}

View File

@ -1436,7 +1436,7 @@ func TestIndexPageSimpleMethods(t *testing.T) {
{func(n *Page) bool { return n.IsNode() }}, {func(n *Page) bool { return n.IsNode() }},
{func(n *Page) bool { return !n.IsPage() }}, {func(n *Page) bool { return !n.IsPage() }},
{func(n *Page) bool { return n.Scratch() != nil }}, {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() n := s.newHomePage()

View File

@ -20,7 +20,6 @@ import (
"github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/config"
"github.com/spf13/afero" "github.com/spf13/afero"
"github.com/spf13/cast" "github.com/spf13/cast"
"github.com/spf13/viper"
) )
type ThemeConfig struct { type ThemeConfig struct {
@ -73,18 +72,11 @@ func (c *themesCollector) add(name, configFilename string) (ThemeConfig, error)
var tc ThemeConfig var tc ThemeConfig
if configFilename != "" { if configFilename != "" {
v := viper.New() var err error
v.SetFs(c.fs) cfg, err = config.FromFile(c.fs, configFilename)
v.AutomaticEnv()
v.SetEnvPrefix("hugo")
v.SetConfigFile(configFilename)
err := v.ReadInConfig()
if err != nil { if err != nil {
return tc, err return tc, nil
} }
cfg = v
} }
tc = ThemeConfig{Name: name, ConfigFilename: configFilename, Cfg: cfg} tc = ThemeConfig{Name: name, ConfigFilename: configFilename, Cfg: cfg}

View File

@ -1226,7 +1226,7 @@ func (s *Site) initializeSiteInfo() error {
Data: &s.Data, Data: &s.Data,
owner: s.owner, owner: s.owner,
s: s, s: s,
hugoInfo: hugo.NewInfo(), hugoInfo: hugo.NewInfo(s.Cfg.GetString("environment")),
// TODO(bep) make this Menu and similar into delegate methods on SiteInfo // TODO(bep) make this Menu and similar into delegate methods on SiteInfo
Taxonomies: s.Taxonomies, Taxonomies: s.Taxonomies,
} }
@ -1370,7 +1370,7 @@ func (s *Site) getMenusFromConfig() Menus {
ret := Menus{} ret := Menus{}
if menus := s.Language.GetStringMap("menu"); menus != nil { if menus := s.Language.GetStringMap("menus"); menus != nil {
for name, menu := range menus { for name, menu := range menus {
m, err := cast.ToSliceE(menu) m, err := cast.ToSliceE(menu)
if err != nil { if err != nil {

View File

@ -322,6 +322,15 @@ func (s *sitesBuilder) CreateSites() *sitesBuilder {
return s 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 { func (s *sitesBuilder) CreateSitesE() error {
s.addDefaults() s.addDefaults()
s.writeFilePairs("content", s.contentFilePairs) s.writeFilePairs("content", s.contentFilePairs)
@ -334,18 +343,9 @@ func (s *sitesBuilder) CreateSitesE() error {
s.writeFilePairs("i18n", s.i18nFilePairsAdded) s.writeFilePairs("i18n", s.i18nFilePairsAdded)
if s.Cfg == nil { if s.Cfg == nil {
cfg, _, err := LoadConfig(ConfigSourceDescriptor{Fs: s.Fs.Source, Filename: "config." + s.configFormat}) if err := s.LoadConfig(); err != nil {
if err != nil {
return err 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}) sites, err := NewHugoSites(deps.DepsCfg{Fs: s.Fs, Cfg: s.Cfg, Logger: s.logger, Running: s.running})

View File

@ -21,10 +21,10 @@ import (
const ( const (
packageName = "github.com/gohugoio/hugo" 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 // allow user to override go executable by running as GOEXE=xxx make ... on unix-like systems
var goexe = "go" var goexe = "go"

View File

@ -22,6 +22,7 @@ import (
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
"github.com/chaseadamsio/goorgeous" "github.com/chaseadamsio/goorgeous"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/afero"
"github.com/spf13/cast" "github.com/spf13/cast"
yaml "gopkg.in/yaml.v2" yaml "gopkg.in/yaml.v2"
) )
@ -37,7 +38,21 @@ func UnmarshalToMap(data []byte, f Format) (map[string]interface{}, error) {
err := unmarshal(data, f, &m) err := unmarshal(data, f, &m)
return m, err 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{}. // Unmarshal will unmarshall data in format f into an interface{}.

View File

@ -14,6 +14,7 @@
package metadecoders package metadecoders
import ( import (
"path/filepath"
"strings" "strings"
"github.com/gohugoio/hugo/parser/pageparser" "github.com/gohugoio/hugo/parser/pageparser"
@ -34,6 +35,11 @@ const (
// into a Format. It returns an empty string for unknown formats. // into a Format. It returns an empty string for unknown formats.
func FormatFromString(formatStr string) Format { func FormatFromString(formatStr string) Format {
formatStr = strings.ToLower(formatStr) formatStr = strings.ToLower(formatStr)
if strings.Contains(formatStr, ".") {
// Assume a filename
formatStr = strings.TrimPrefix(filepath.Ext(formatStr), ".")
}
switch formatStr { switch formatStr {
case "yaml", "yml": case "yaml", "yml":
return YAML return YAML

View File

@ -32,6 +32,7 @@ func TestFormatFromString(t *testing.T) {
{"yaml", YAML}, {"yaml", YAML},
{"yml", YAML}, {"yml", YAML},
{"toml", TOML}, {"toml", TOML},
{"config.toml", TOML},
{"tOMl", TOML}, {"tOMl", TOML},
{"org", ORG}, {"org", ORG},
{"foo", ""}, {"foo", ""},