summaryrefslogtreecommitdiffstats
path: root/commands/commandeer.go
diff options
context:
space:
mode:
Diffstat (limited to 'commands/commandeer.go')
-rw-r--r--commands/commandeer.go339
1 files changed, 339 insertions, 0 deletions
diff --git a/commands/commandeer.go b/commands/commandeer.go
new file mode 100644
index 000000000..c55806980
--- /dev/null
+++ b/commands/commandeer.go
@@ -0,0 +1,339 @@
+// 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 commands
+
+import (
+ "os"
+ "path/filepath"
+ "regexp"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/gohugoio/hugo/config"
+
+ "github.com/spf13/cobra"
+
+ "github.com/spf13/afero"
+
+ "github.com/gohugoio/hugo/hugolib"
+
+ "github.com/bep/debounce"
+ "github.com/gohugoio/hugo/common/types"
+ "github.com/gohugoio/hugo/deps"
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/gohugoio/hugo/hugofs"
+ "github.com/gohugoio/hugo/langs"
+)
+
+type commandeerHugoState struct {
+ *deps.DepsCfg
+ hugo *hugolib.HugoSites
+ fsCreate sync.Once
+}
+
+type commandeer struct {
+ *commandeerHugoState
+
+ // Currently only set when in "fast render mode". But it seems to
+ // be fast enough that we could maybe just add it for all server modes.
+ changeDetector *fileChangeDetector
+
+ // We need to reuse this on server rebuilds.
+ destinationFs afero.Fs
+
+ h *hugoBuilderCommon
+ ftch flagsToConfigHandler
+
+ visitedURLs *types.EvictingStringQueue
+
+ doWithCommandeer func(c *commandeer) error
+
+ // We watch these for changes.
+ configFiles []string
+
+ // Used in cases where we get flooded with events in server mode.
+ debounce func(f func())
+
+ serverPorts []int
+ languagesConfigured bool
+ languages langs.Languages
+
+ configured bool
+ paused bool
+}
+
+func (c *commandeer) Set(key string, value interface{}) {
+ if c.configured {
+ panic("commandeer cannot be changed")
+ }
+ c.Cfg.Set(key, value)
+}
+
+func (c *commandeer) initFs(fs *hugofs.Fs) error {
+ c.destinationFs = fs.Destination
+ c.DepsCfg.Fs = fs
+
+ return nil
+}
+
+func newCommandeer(mustHaveConfigFile, running bool, h *hugoBuilderCommon, f flagsToConfigHandler, doWithCommandeer func(c *commandeer) error, subCmdVs ...*cobra.Command) (*commandeer, error) {
+
+ var rebuildDebouncer func(f func())
+ if running {
+ // The time value used is tested with mass content replacements in a fairly big Hugo site.
+ // It is better to wait for some seconds in those cases rather than get flooded
+ // with rebuilds.
+ rebuildDebouncer, _, _ = debounce.New(4 * time.Second)
+ }
+
+ c := &commandeer{
+ h: h,
+ ftch: f,
+ commandeerHugoState: &commandeerHugoState{},
+ doWithCommandeer: doWithCommandeer,
+ visitedURLs: types.NewEvictingStringQueue(10),
+ debounce: rebuildDebouncer,
+ }
+
+ return c, c.loadConfig(mustHaveConfigFile, running)
+}
+
+type fileChangeDetector struct {
+ sync.Mutex
+ current map[string]string
+ prev map[string]string
+
+ irrelevantRe *regexp.Regexp
+}
+
+func (f *fileChangeDetector) OnFileClose(name, md5sum string) {
+ f.Lock()
+ defer f.Unlock()
+ f.current[name] = md5sum
+}
+
+func (f *fileChangeDetector) changed() []string {
+ if f == nil {
+ return nil
+ }
+ f.Lock()
+ defer f.Unlock()
+ var c []string
+ for k, v := range f.current {
+ vv, found := f.prev[k]
+ if !found || v != vv {
+ c = append(c, k)
+ }
+ }
+
+ return f.filterIrrelevant(c)
+}
+
+func (f *fileChangeDetector) filterIrrelevant(in []string) []string {
+ var filtered []string
+ for _, v := range in {
+ if !f.irrelevantRe.MatchString(v) {
+ filtered = append(filtered, v)
+ }
+ }
+ return filtered
+}
+
+func (f *fileChangeDetector) PrepareNew() {
+ if f == nil {
+ return
+ }
+
+ f.Lock()
+ defer f.Unlock()
+
+ if f.current == nil {
+ f.current = make(map[string]string)
+ f.prev = make(map[string]string)
+ return
+ }
+
+ f.prev = make(map[string]string)
+ for k, v := range f.current {
+ f.prev[k] = v
+ }
+ f.current = make(map[string]string)
+}
+
+func (c *commandeer) loadConfig(mustHaveConfigFile, running bool) error {
+
+ if c.DepsCfg == nil {
+ c.DepsCfg = &deps.DepsCfg{}
+ }
+
+ cfg := c.DepsCfg
+ c.configured = false
+ cfg.Running = running
+
+ var dir string
+ if c.h.source != "" {
+ dir, _ = filepath.Abs(c.h.source)
+ } else {
+ dir, _ = os.Getwd()
+ }
+
+ var sourceFs afero.Fs = hugofs.Os
+ if c.DepsCfg.Fs != nil {
+ sourceFs = c.DepsCfg.Fs.Source
+ }
+
+ doWithConfig := func(cfg config.Provider) error {
+
+ if c.ftch != nil {
+ c.ftch.flagsToConfig(cfg)
+ }
+
+ cfg.Set("workingDir", dir)
+
+ return nil
+ }
+
+ doWithCommandeer := func(cfg config.Provider) error {
+ c.Cfg = cfg
+ if c.doWithCommandeer == nil {
+ return nil
+ }
+ err := c.doWithCommandeer(c)
+ return err
+ }
+
+ config, configFiles, err := hugolib.LoadConfig(
+ hugolib.ConfigSourceDescriptor{Fs: sourceFs, Path: c.h.source, WorkingDir: dir, Filename: c.h.cfgFile},
+ doWithCommandeer,
+ doWithConfig)
+
+ if err != nil {
+ if mustHaveConfigFile {
+ return err
+ }
+ if err != hugolib.ErrNoConfigFile {
+ return err
+ }
+
+ }
+
+ c.configFiles = configFiles
+
+ if l, ok := c.Cfg.Get("languagesSorted").(langs.Languages); ok {
+ c.languagesConfigured = true
+ c.languages = l
+ }
+
+ // This is potentially double work, but we need to do this one more time now
+ // that all the languages have been configured.
+ if c.doWithCommandeer != nil {
+ if err := c.doWithCommandeer(c); err != nil {
+ return err
+ }
+ }
+
+ logger, err := c.createLogger(config)
+ if err != nil {
+ return err
+ }
+
+ cfg.Logger = logger
+
+ createMemFs := config.GetBool("renderToMemory")
+
+ if createMemFs {
+ // Rendering to memoryFS, publish to Root regardless of publishDir.
+ config.Set("publishDir", "/")
+ }
+
+ c.fsCreate.Do(func() {
+ fs := hugofs.NewFrom(sourceFs, config)
+
+ if c.destinationFs != nil {
+ // Need to reuse the destination on server rebuilds.
+ fs.Destination = c.destinationFs
+ } else if createMemFs {
+ // Hugo writes the output to memory instead of the disk.
+ fs.Destination = new(afero.MemMapFs)
+ }
+
+ doLiveReload := !c.h.buildWatch && !config.GetBool("disableLiveReload")
+ fastRenderMode := doLiveReload && !config.GetBool("disableFastRender")
+
+ if fastRenderMode {
+ // For now, fast render mode only. It should, however, be fast enough
+ // for the full variant, too.
+ changeDetector := &fileChangeDetector{
+ // We use this detector to decide to do a Hot reload of a single path or not.
+ // We need to filter out source maps and possibly some other to be able
+ // to make that decision.
+ irrelevantRe: regexp.MustCompile(`\.map$`),
+ }
+ changeDetector.PrepareNew()
+ fs.Destination = hugofs.NewHashingFs(fs.Destination, changeDetector)
+ c.changeDetector = changeDetector
+ }
+
+ err = c.initFs(fs)
+ if err != nil {
+ return
+ }
+
+ var h *hugolib.HugoSites
+
+ h, err = hugolib.NewHugoSites(*c.DepsCfg)
+ c.hugo = h
+
+ })
+
+ if err != nil {
+ return err
+ }
+
+ cacheDir := config.GetString("cacheDir")
+ if cacheDir != "" {
+ if helpers.FilePathSeparator != cacheDir[len(cacheDir)-1:] {
+ cacheDir = cacheDir + helpers.FilePathSeparator
+ }
+ isDir, err := helpers.DirExists(cacheDir, sourceFs)
+ checkErr(cfg.Logger, err)
+ if !isDir {
+ mkdir(cacheDir)
+ }
+ config.Set("cacheDir", cacheDir)
+ } else {
+ config.Set("cacheDir", helpers.GetTempDir("hugo_cache", sourceFs))
+ }
+
+ cfg.Logger.INFO.Println("Using config file:", config.ConfigFileUsed())
+
+ themeDir := c.hugo.PathSpec.GetFirstThemeDir()
+ if themeDir != "" {
+ if _, err := sourceFs.Stat(themeDir); os.IsNotExist(err) {
+ return newSystemError("Unable to find theme Directory:", themeDir)
+ }
+ }
+
+ dir, themeVersionMismatch, minVersion := c.isThemeVsHugoVersionMismatch(sourceFs)
+
+ if themeVersionMismatch {
+ name := filepath.Base(dir)
+ cfg.Logger.ERROR.Printf("%s theme does not support Hugo version %s. Minimum version required is %s\n",
+ strings.ToUpper(name), helpers.CurrentHugoVersion.ReleaseVersion(), minVersion)
+ }
+
+ return nil
+
+}