diff options
36 files changed, 901 insertions, 184 deletions
diff --git a/commands/commandeer.go b/commands/commandeer.go index b722991ab..75d69b251 100644 --- a/commands/commandeer.go +++ b/commands/commandeer.go @@ -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) diff --git a/commands/commands.go b/commands/commands.go index 0a6ea8860..0dfa10e67 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -14,6 +14,11 @@ package commands import ( + "os" + + "github.com/gohugoio/hugo/hugolib/paths" + + "github.com/gohugoio/hugo/common/hugo" "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/helpers" @@ -159,6 +164,7 @@ 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/") diff --git a/commands/commands_test.go b/commands/commands_test.go index 84afe4419..9b9806694 100644 --- a/commands/commands_test.go +++ b/commands/commands_test.go @@ -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 }} `) diff --git a/commands/hugo.go b/commands/hugo.go index 759efc17b..74173fa84 100644 --- a/commands/hugo.go +++ b/commands/hugo.go @@ -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 } diff --git a/commands/server.go b/commands/server.go index 58d1a60fb..c2bd76dae 100644 --- a/commands/server.go +++ b/commands/server.go @@ -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) diff --git a/commands/server_test.go b/commands/server_test.go index 438837a90..24f203ad0 100644 --- a/commands/server_test.go +++ b/commands/server_test.go @@ -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 diff --git a/common/herrors/error_locator.go b/common/herrors/error_locator.go index 88cb06c8c..3a72f4790 100644 --- a/common/herrors/error_locator.go +++ b/common/herrors/error_locator.go @@ -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) } diff --git a/common/hugo/hugo.go b/common/hugo/hugo.go index b93b10bf1..62d923bf0 100644 --- a/common/hugo/hugo.go +++ b/common/hugo/hugo.go @@ -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, } } diff --git a/common/hugo/hugo_test.go b/common/hugo/hugo_test.go index 18a9b594f..1769db587 100644 --- a/common/hugo/hugo_test.go +++ b/common/hugo/hugo_test.go @@ -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())) } diff --git a/common/hugo/version.go b/common/hugo/version.go index 204f8f747..e9deb6acf 100644 --- a/common/hugo/version.go +++ b/common/hugo/version.go @@ -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) } diff --git a/common/maps/maps.go b/common/maps/maps.go index a114b557c..e0d4f964d 100644 --- a/common/maps/maps.go +++ b/common/maps/maps.go @@ -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 + } + } +} diff --git a/common/maps/maps_test.go b/common/maps/maps_test.go index 37add5dc5..29bffa6bc 100644 --- a/common/maps/maps_test.go +++ b/common/maps/maps_test.go @@ -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 index 000000000..b60aa3fe5 --- /dev/null +++ b/config/configLoader.go @@ -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{}) { |