summaryrefslogtreecommitdiffstats
path: root/commands
diff options
context:
space:
mode:
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2018-10-03 14:58:09 +0200
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>2018-10-16 22:10:56 +0200
commit35fbfb19a173b01bc881f2bbc5d104136633a7ec (patch)
tree636d0d51fa262dc808eb3c5cc9cf92ad977a0c6a /commands
parent3a3089121b852332b5744d1f566959c8cf93cef4 (diff)
commands: Show server error info in browser
The main item in this commit is showing of errors with a file context when running `hugo server`. This can be turned off: `hugo server --disableBrowserError` (can also be set in `config.toml`). But to get there, the error handling in Hugo needed a revision. There are some items left TODO for commits soon to follow, most notable errors in content and config files. Fixes #5284 Fixes #5290 See #5325 See #5324
Diffstat (limited to 'commands')
-rw-r--r--commands/commandeer.go61
-rw-r--r--commands/commands.go28
-rw-r--r--commands/convert.go4
-rw-r--r--commands/hugo.go470
-rw-r--r--commands/new_site.go5
-rw-r--r--commands/server.go73
-rw-r--r--commands/server_errors.go95
-rw-r--r--commands/server_test.go13
-rw-r--r--commands/static_syncer.go6
-rw-r--r--commands/version.go11
10 files changed, 487 insertions, 279 deletions
diff --git a/commands/commandeer.go b/commands/commandeer.go
index c55806980..2b76462fe 100644
--- a/commands/commandeer.go
+++ b/commands/commandeer.go
@@ -14,6 +14,15 @@
package commands
import (
+ "bytes"
+ "errors"
+
+ "github.com/gohugoio/hugo/common/herrors"
+
+ "io/ioutil"
+
+ jww "github.com/spf13/jwalterweatherman"
+
"os"
"path/filepath"
"regexp"
@@ -21,13 +30,13 @@ import (
"sync"
"time"
+ "github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/config"
"github.com/spf13/cobra"
- "github.com/spf13/afero"
-
"github.com/gohugoio/hugo/hugolib"
+ "github.com/spf13/afero"
"github.com/bep/debounce"
"github.com/gohugoio/hugo/common/types"
@@ -46,6 +55,8 @@ type commandeerHugoState struct {
type commandeer struct {
*commandeerHugoState
+ logger *loggers.Logger
+
// 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
@@ -69,9 +80,45 @@ type commandeer struct {
serverPorts []int
languagesConfigured bool
languages langs.Languages
+ doLiveReload bool
+ fastRenderMode bool
+ showErrorInBrowser bool
configured bool
paused bool
+
+ // Any error from the last build.
+ buildErr error
+}
+
+func (c *commandeer) errCount() int {
+ return int(c.logger.ErrorCounter.Count())
+}
+
+func (c *commandeer) getErrorWithContext() interface{} {
+ errCount := c.errCount()
+
+ if errCount == 0 {
+ return nil
+ }
+
+ m := make(map[string]interface{})
+
+ m["Error"] = errors.New(removeErrorPrefixFromLog(c.logger.Errors.String()))
+ m["Version"] = hugoVersionString()
+
+ fe := herrors.UnwrapErrorWithFileContext(c.buildErr)
+ if fe != nil {
+ m["File"] = fe
+ }
+
+ if c.h.verbose {
+ var b bytes.Buffer
+ herrors.FprintStackTrace(&b, c.buildErr)
+ m["StackTrace"] = b.String()
+ }
+
+ return m
}
func (c *commandeer) Set(key string, value interface{}) {
@@ -105,6 +152,8 @@ func newCommandeer(mustHaveConfigFile, running bool, h *hugoBuilderCommon, f fla
doWithCommandeer: doWithCommandeer,
visitedURLs: types.NewEvictingStringQueue(10),
debounce: rebuildDebouncer,
+ // This will be replaced later, but we need something to log to before the configuration is read.
+ logger: loggers.NewLogger(jww.LevelError, jww.LevelError, os.Stdout, ioutil.Discard, running),
}
return c, c.loadConfig(mustHaveConfigFile, running)
@@ -236,6 +285,11 @@ func (c *commandeer) loadConfig(mustHaveConfigFile, running bool) error {
c.languages = l
}
+ // Set some commonly used flags
+ c.doLiveReload = !c.h.buildWatch && !c.Cfg.GetBool("disableLiveReload")
+ c.fastRenderMode = c.doLiveReload && !c.Cfg.GetBool("disableFastRender")
+ c.showErrorInBrowser = c.doLiveReload && !c.Cfg.GetBool("disableBrowserError")
+
// 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 {
@@ -244,12 +298,13 @@ func (c *commandeer) loadConfig(mustHaveConfigFile, running bool) error {
}
}
- logger, err := c.createLogger(config)
+ logger, err := c.createLogger(config, running)
if err != nil {
return err
}
cfg.Logger = logger
+ c.logger = logger
createMemFs := config.GetBool("renderToMemory")
diff --git a/commands/commands.go b/commands/commands.go
index 54eb03b5b..8670d4983 100644
--- a/commands/commands.go
+++ b/commands/commands.go
@@ -14,12 +14,10 @@
package commands
import (
- "os"
-
+ "github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/helpers"
"github.com/spf13/cobra"
- jww "github.com/spf13/jwalterweatherman"
"github.com/spf13/nitro"
)
@@ -242,7 +240,7 @@ func (cc *hugoBuilderCommon) handleFlags(cmd *cobra.Command) {
_ = cmd.Flags().SetAnnotation("theme", cobra.BashCompSubdirsInDir, []string{"themes"})
}
-func checkErr(logger *jww.Notepad, err error, s ...string) {
+func checkErr(logger *loggers.Logger, err error, s ...string) {
if err == nil {
return
}
@@ -255,25 +253,3 @@ func checkErr(logger *jww.Notepad, err error, s ...string) {
}
logger.ERROR.Println(err)
}
-
-func stopOnErr(logger *jww.Notepad, err error, s ...string) {
- if err == nil {
- return
- }
-
- defer os.Exit(-1)
-
- if len(s) == 0 {
- newMessage := err.Error()
- // Printing an empty string results in a error with
- // no message, no bueno.
- if newMessage != "" {
- logger.CRITICAL.Println(newMessage)
- }
- }
- for _, message := range s {
- if message != "" {
- logger.CRITICAL.Println(message)
- }
- }
-}
diff --git a/commands/convert.go b/commands/convert.go
index 8de155e9b..dc6b8fe15 100644
--- a/commands/convert.go
+++ b/commands/convert.go
@@ -14,10 +14,10 @@
package commands
import (
- "fmt"
"time"
src "github.com/gohugoio/hugo/source"
+ "github.com/pkg/errors"
"github.com/gohugoio/hugo/hugolib"
@@ -187,7 +187,7 @@ func (cc *convertCmd) convertAndSavePage(p *hugolib.Page, site *hugolib.Site, ma
}
if err = newPage.SaveSourceAs(newFilename); err != nil {
- return fmt.Errorf("Failed to save file %q: %s", newFilename, err)
+ return errors.Wrapf(err, "Failed to save file %q:", newFilename)
}
return nil
diff --git a/commands/hugo.go b/commands/hugo.go
index 2e7353d51..6cb2ec012 100644
--- a/commands/hugo.go
+++ b/commands/hugo.go
@@ -18,16 +18,22 @@ package commands
import (
"fmt"
"io/ioutil"
+
"os/signal"
"sort"
"sync/atomic"
+
+ "github.com/pkg/errors"
+
+ "github.com/gohugoio/hugo/common/herrors"
+ "github.com/gohugoio/hugo/common/loggers"
+
"syscall"
"github.com/gohugoio/hugo/hugolib/filesystems"
"golang.org/x/sync/errgroup"
- "log"
"os"
"path/filepath"
"runtime"
@@ -85,7 +91,7 @@ func Execute(args []string) Response {
}
if err == nil {
- errCount := int(jww.LogCountForLevelsGreaterThanorEqualTo(jww.LevelError))
+ errCount := int(loggers.GlobalErrorCounter.Count())
if errCount > 0 {
err = fmt.Errorf("logged %d errors", errCount)
} else if resp.Result != nil {
@@ -118,7 +124,7 @@ func initializeConfig(mustHaveConfigFile, running bool,
}
-func (c *commandeer) createLogger(cfg config.Provider) (*jww.Notepad, error) {
+func (c *commandeer) createLogger(cfg config.Provider, running bool) (*loggers.Logger, error) {
var (
logHandle = ioutil.Discard
logThreshold = jww.LevelWarn
@@ -161,7 +167,7 @@ func (c *commandeer) createLogger(cfg config.Provider) (*jww.Notepad, error) {
jww.SetStdoutThreshold(stdoutThreshold)
helpers.InitLoggers()
- return jww.NewNotepad(stdoutThreshold, logThreshold, outHandle, logHandle, "", log.Ldate|log.Ltime), nil
+ return loggers.NewLogger(stdoutThreshold, logThreshold, outHandle, logHandle, running), nil
}
func initializeFlags(cmd *cobra.Command, cfg config.Provider) {
@@ -275,9 +281,9 @@ func (c *commandeer) fullBuild() error {
cnt, err := c.copyStatic()
if err != nil {
if !os.IsNotExist(err) {
- return fmt.Errorf("Error copying static files: %s", err)
+ return errors.Wrap(err, "Error copying static files")
}
- c.Logger.WARN.Println("No Static directory found")
+ c.logger.WARN.Println("No Static directory found")
}
langCount = cnt
langCount = cnt
@@ -285,7 +291,7 @@ func (c *commandeer) fullBuild() error {
}
buildSitesFunc := func() error {
if err := c.buildSites(); err != nil {
- return fmt.Errorf("Error building site: %s", err)
+ return errors.Wrap(err, "Error building site")
}
return nil
}
@@ -345,8 +351,8 @@ func (c *commandeer) build() error {
if err != nil {
return err
}
- c.Logger.FEEDBACK.Println("Watching for changes in", c.hugo.PathSpec.AbsPathify(c.Cfg.GetString("contentDir")))
- c.Logger.FEEDBACK.Println("Press Ctrl+C to stop")
+ c.logger.FEEDBACK.Println("Watching for changes in", c.hugo.PathSpec.AbsPathify(c.Cfg.GetString("contentDir")))
+ c.logger.FEEDBACK.Println("Press Ctrl+C to stop")
watcher, err := c.newWatcher(watchDirs...)
checkErr(c.Logger, err)
defer watcher.Close()
@@ -388,7 +394,7 @@ func (c *commandeer) doWithPublishDirs(f func(sourceFs *filesystems.SourceFilesy
staticFilesystems := c.hugo.BaseFs.SourceFilesystems.Static
if len(staticFilesystems) == 0 {
- c.Logger.WARN.Println("No static directories found to sync")
+ c.logger.WARN.Println("No static directories found to sync")
return langCount, nil
}
@@ -448,13 +454,13 @@ func (c *commandeer) copyStaticTo(sourceFs *filesystems.SourceFilesystem) (uint6
syncer.Delete = c.Cfg.GetBool("cleanDestinationDir")
if syncer.Delete {
- c.Logger.INFO.Println("removing all files from destination that don't exist in static dirs")
+ c.logger.INFO.Println("removing all files from destination that don't exist in static dirs")
syncer.DeleteFilter = func(f os.FileInfo) bool {
return f.IsDir() && strings.HasPrefix(f.Name(), ".")
}
}
- c.Logger.INFO.Println("syncing static files to", publishDir)
+ c.logger.INFO.Println("syncing static files to", publishDir)
var err error
@@ -480,7 +486,7 @@ func (c *commandeer) timeTrack(start time.Time, name string) {
return
}
elapsed := time.Since(start)
- c.Logger.FEEDBACK.Printf("%s in %v ms", name, int(1000*elapsed.Seconds()))
+ c.logger.FEEDBACK.Printf("%s in %v ms", name, int(1000*elapsed.Seconds()))
}
// getDirList provides NewWatcher() with a list of directories to watch for changes.
@@ -498,7 +504,7 @@ func (c *commandeer) getDirList() ([]string, error) {
return nil
}
- c.Logger.ERROR.Println("Walker: ", err)
+ c.logger.ERROR.Println("Walker: ", err)
return nil
}
@@ -511,16 +517,16 @@ func (c *commandeer) getDirList() ([]string, error) {
if fi.Mode()&os.ModeSymlink == os.ModeSymlink {
link, err := filepath.EvalSymlinks(path)
if err != nil {
- c.Logger.ERROR.Printf("Cannot read symbolic link '%s', error was: %s", path, err)
+ c.logger.ERROR.Printf("Cannot read symbolic link '%s', error was: %s", path, err)
return nil
}
linkfi, err := helpers.LstatIfPossible(c.Fs.Source, link)
if err != nil {
- c.Logger.ERROR.Printf("Cannot stat %q: %s", link, err)
+ c.logger.ERROR.Printf("Cannot stat %q: %s", link, err)
return nil
}
if !allowSymbolicDirs && !linkfi.Mode().IsRegular() {
- c.Logger.ERROR.Printf("Symbolic links for directories not supported, skipping %q", path)
+ c.logger.ERROR.Printf("Symbolic links for directories not supported, skipping %q", path)
return nil
}
@@ -603,7 +609,7 @@ func (c *commandeer) getDirList() ([]string, error) {
func (c *commandeer) resetAndBuildSites() (err error) {
if !c.h.quiet {
- c.Logger.FEEDBACK.Println("Started building sites ...")
+ c.logger.FEEDBACK.Println("Started building sites ...")
}
return c.hugo.Build(hugolib.BuildCfg{ResetState: true})
}
@@ -615,6 +621,7 @@ func (c *commandeer) buildSites() (err error) {
func (c *commandeer) rebuildSites(events []fsnotify.Event) error {
defer c.timeTrack(time.Now(), "Total")
+ c.buildErr = nil
visited := c.visitedURLs.PeekAllSet()
doLiveReload := !c.h.buildWatch && !c.Cfg.GetBool("disableLiveReload")
if doLiveReload && !c.Cfg.GetBool("disableFastRender") {
@@ -637,7 +644,7 @@ func (c *commandeer) fullRebuild() {
c.commandeerHugoState = &commandeerHugoState{}
err := c.loadConfig(true, true)
if err != nil {
- jww.ERROR.Println("Failed to reload config:", err)
+ c.logger.ERROR.Println("Failed to reload config:", err)
// Set the processing on pause until the state is recovered.
c.paused = true
} else {
@@ -645,8 +652,9 @@ func (c *commandeer) fullRebuild() {
}
if !c.paused {
- if err := c.buildSites(); err != nil {
- jww.ERROR.Println(err)
+ err := c.buildSites()
+ if err != nil {
+ c.logger.ERROR.Println(err)
} else if !c.h.buildWatch && !c.Cfg.GetBool("disableLiveReload") {
livereload.ForceRefresh()
}
@@ -680,7 +688,7 @@ func (c *commandeer) newWatcher(dirList ...string) (*watcher.Batcher, error) {
configSet := make(map[string]bool)
for _, configFile := range c.configFiles {
- c.Logger.FEEDBACK.Println("Watching for config changes in", configFile)
+ c.logger.FEEDBACK.Println("Watching for config changes in", configFile)
watcher.Add(configFile)
configSet[configFile] = true
}
@@ -689,241 +697,259 @@ func (c *commandeer) newWatcher(dirList ...string) (*watcher.Batcher, error) {
for {
select {
case evs := <-watcher.Events:
- for _, ev := range evs {
- if configSet[ev.Name] {
- if ev.Op&fsnotify.Chmod == fsnotify.Chmod {
- continue
- }
- if ev.Op&fsnotify.Remove == fsnotify.Remove {
- for _, configFile := range c.configFiles {
- counter := 0
- for watcher.Add(configFile) != nil {
- counter++
- if counter >= 100 {
- break
- }
- time.Sleep(100 * time.Millisecond)
- }
- }
- }
- // Config file changed. Need full rebuild.
- c.fullRebuild()
- break
- }
+ c.handleEvents(watcher, staticSyncer, evs, configSet)
+ if c.showErrorInBrowser && c.errCount() > 0 {
+ // Need to reload browser to show the error
+ livereload.ForceRefresh()
}
-
- if c.paused {
- // Wait for the server to get into a consistent state before
- // we continue with processing.
- continue
- }
-
- if len(evs) > 50 {
- // This is probably a mass edit of the content dir.
- // Schedule a full rebuild for when it slows down.
- c.debounce(c.fullRebuild)
- continue
+ case err := <-watcher.Errors:
+ if err != nil {
+ c.logger.ERROR.Println("Error while watching:", err)
}
+ }
+ }
+ }()
- c.Logger.INFO.Println("Received System Events:", evs)
+ return watcher, nil
+}
- staticEvents := []fsnotify.Event{}
- dynamicEvents := []fsnotify.Event{}
+func (c *commandeer) handleEvents(watcher *watcher.Batcher,
+ staticSyncer *staticSyncer,
+ evs []fsnotify.Event,
+ configSet map[string]bool) {
- // Special handling for symbolic links inside /content.
- filtered := []fsnotify.Event{}
- for _, ev := range evs {
- // Check the most specific first, i.e. files.
- contentMapped := c.hugo.ContentChanges.GetSymbolicLinkMappings(ev.Name)
- if len(contentMapped) > 0 {
- for _, mapped := range contentMapped {
- filtered = append(filtered, fsnotify.Event{Name: mapped, Op: ev.Op})
+ for _, ev := range evs {
+ if configSet[ev.Name] {
+ if ev.Op&fsnotify.Chmod == fsnotify.Chmod {
+ continue
+ }
+ if ev.Op&fsnotify.Remove == fsnotify.Remove {
+ for _, configFile := range c.configFiles {
+ counter := 0
+ for watcher.Add(configFile) != nil {
+ counter++
+ if counter >= 100 {
+ break
}
- continue
+ time.Sleep(100 * time.Millisecond)
}
+ }
+ }
+ // Config file changed. Need full rebuild.
+ c.fullRebuild()
+ break
+ }
+ }
- // Check for any symbolic directory mapping.
+ if c.paused {
+ // Wait for the server to get into a consistent state before
+ // we continue with processing.
+ return
+ }
- dir, name := filepath.Split(ev.Name)
+ if len(evs) > 50 {
+ // This is probably a mass edit of the content dir.
+ // Schedule a full rebuild for when it slows down.
+ c.debounce(c.fullRebuild)
+ return
+ }
- contentMapped = c.hugo.ContentChanges.GetSymbolicLinkMappings(dir)
+ c.logger.INFO.Println("Received System Events:", evs)
- if len(contentMapped) == 0 {
- filtered = append(filtered, ev)
- continue
- }
+ staticEvents := []fsnotify.Event{}
+ dynamicEvents := []fsnotify.Event{}
- for _, mapped := range contentMapped {
- mappedFilename := filepath.Join(mapped, name)
- filtered = append(filtered, fsnotify.Event{Name: mappedFilename, Op: ev.Op})
- }
- }
+ // Special handling for symbolic links inside /content.
+ filtered := []fsnotify.Event{}
+ for _, ev := range evs {
+ // Check the most specific first, i.e. files.
+ contentMapped := c.hugo.ContentChanges.GetSymbolicLinkMappings(ev.Name)
+ if len(contentMapped) > 0 {
+ for _, mapped := range contentMapped {
+ filtered = append(filtered, fsnotify.Event{Name: mapped, Op: ev.Op})
+ }
+ continue
+ }
- evs = filtered
-
- for _, ev := range evs {
- ext := filepath.Ext(ev.Name)
- baseName := filepath.Base(ev.Name)
- istemp := strings.HasSuffix(ext, "~") ||
- (ext == ".swp") || // vim
- (ext == ".swx") || // vim
- (ext == ".tmp") || // generic temp file
- (ext == ".DS_Store") || // OSX Thumbnail
- baseName == "4913" || // vim
- strings.HasPrefix(ext, ".goutputstream") || // gnome
- strings.HasSuffix(ext, "jb_old___") || // intelliJ
- strings.HasSuffix(ext, "jb_tmp___") || // intelliJ
- strings.HasSuffix(ext, "jb_bak___") || // intelliJ
- strings.HasPrefix(ext, ".sb-") || // byword
- strings.HasPrefix(baseName, ".#") || // emacs
- strings.HasPrefix(baseName, "#") // emacs
- if istemp {
- continue
- }
- // Sometimes during rm -rf operations a '"": REMOVE' is triggered. Just ignore these
- if ev.Name == "" {
- continue
- }
+ // Check for any symbolic directory mapping.
- // Write and rename operations are often followed by CHMOD.
- // There may be valid use cases for rebuilding the site on CHMOD,
- // but that will require more complex logic than this simple conditional.
- // On OS X this seems to be related to Spotlight, see:
- // https://github.com/go-fsnotify/fsnotify/issues/15
- // A workaround is to put your site(s) on the Spotlight exception list,
- // but that may be a little mysterious for most end users.
- // So, for now, we skip reload on CHMOD.
- // We do have to check for WRITE though. On slower laptops a Chmod
- // could be aggregated with other important events, and we still want
- // to rebuild on those
- if ev.Op&(fsnotify.Chmod|fsnotify.Write|fsnotify.Create) == fsnotify.Chmod {
- continue
- }
+ dir, name := filepath.Split(ev.Name)
- walkAdder := func(path string, f os.FileInfo, err error) error {
- if f.IsDir() {
- c.Logger.FEEDBACK.Println("adding created directory to watchlist", path)
- if err := watcher.Add(path); err != nil {
- return err
- }
- } else if !staticSyncer.isStatic(path) {
- // Hugo's rebuilding logic is entirely file based. When you drop a new folder into
- // /content on OSX, the above logic will handle future watching of those files,
- // but the initial CREATE is lost.
- dynamicEvents = append(dynamicEvents, fsnotify.Event{Name: path, Op: fsnotify.Create})
- }
- return nil
- }
+ contentMapped = c.hugo.ContentChanges.GetSymbolicLinkMappings(dir)
- // recursively add new directories to watch list
- // When mkdir -p is used, only the top directory triggers an event (at least on OSX)
- if ev.Op&fsnotify.Create == fsnotify.Create {
- if s, err := c.Fs.Source.Stat(ev.Name); err == nil && s.Mode().IsDir() {
- _ = helpers.SymbolicWalk(c.Fs.Source, ev.Name, walkAdder)
- }
- }
+ if len(contentMapped) == 0 {
+ filtered = append(filtered, ev)
+ continue
+ }
- if staticSyncer.isStatic(ev.Name) {
- staticEvents = append(staticEvents, ev)
- } else {
- dynamicEvents = append(dynamicEvents, ev)
- }
- }
+ for _, mapped := range contentMapped {
+ mappedFilename := filepath.Join(mapped, name)
+ filtered = append(filtered, fsnotify.Event{Name: mappedFilename, Op: ev.Op})
+ }
+ }
- if len(staticEvents) > 0 {
- c.Logger.FEEDBACK.Println("\nStatic file changes detected")
- const layout = "2006-01-02 15:04:05.000 -0700"
- c.Logger.FEEDBACK.Println(time.Now().Format(layout))
+ evs = filtered
+
+ for _, ev := range evs {
+ ext := filepath.Ext(ev.Name)
+ baseName := filepath.Base(ev.Name)
+ istemp := strings.HasSuffix(ext, "~") ||
+ (ext == ".swp") || // vim
+ (ext == ".swx") || // vim
+ (ext == ".tmp") || // generic temp file
+ (ext == ".DS_Store") || // OSX Thumbnail
+ baseName == "4913" || // vim
+ strings.HasPrefix(ext, ".goutputstream") || // gnome
+ strings.HasSuffix(ext, "jb_old___") || // intelliJ
+ strings.HasSuffix(ext, "jb_tmp___") || // intelliJ
+ strings.HasSuffix(ext, "jb_bak___") || // intelliJ
+ strings.HasPrefix(ext, ".sb-") || // byword
+ strings.HasPrefix(baseName, ".#") || // emacs
+ strings.HasPrefix(baseName, "#") // emacs
+ if istemp {
+ continue
+ }
+ // Sometimes during rm -rf operations a '"": REMOVE' is triggered. Just ignore these
+ if ev.Name == "" {
+ continue
+ }
- if c.Cfg.GetBool("forceSyncStatic") {
- c.Logger.FEEDBACK.Printf("Syncing all static files\n")
- _, err := c.copyStatic()
- if err != nil {
- stopOnErr(c.Logger, err, "Error copying static files to publish dir")
- }
- } else {
- if err := staticSyncer.syncsStaticEvents(staticEvents); err != nil {
- c.Logger.ERROR.Println(err)
- continue
- }
- }
+ // Write and rename operations are often followed by CHMOD.
+ // There may be valid use cases for rebuilding the site on CHMOD,
+ // but that will require more complex logic than this simple conditional.
+ // On OS X this seems to be related to Spotlight, see:
+ // https://github.com/go-fsnotify/fsnotify/issues/15
+ // A workaround is to put your site(s) on the Spotlight exception list,
+ // but that may be a little mysterious for most end users.
+ // So, for now, we skip reload on CHMOD.
+ // We do have to check for WRITE though. On slower laptops a Chmod
+ // could be aggregated with other important events, and we still want
+ // to rebuild on those
+ if ev.Op&(fsnotify.Chmod|fsnotify.Write|fsnotify.Create) == fsnotify.Chmod {
+ continue
+ }
- if !c.h.buildWatch && !c.Cfg.GetBool("disableLiveReload") {
- // Will block forever trying to write to a channel that nobody is reading if livereload isn't initialized
-
- // force refresh when more than one file
- if len(staticEvents) == 1 {
- ev := staticEvents[0]
- path := c.hugo.BaseFs.SourceFilesystems.MakeStaticPathRelative(ev.Name)
- path = c.firstPathSpec().RelURL(helpers.ToSlashTrimLeading(path), false)
- livereload.RefreshPath(path)
- } else {
- livereload.ForceRefresh()
- }
- }
+ walkAdder := func(path string, f os.FileInfo, err error) error {
+ if f.IsDir() {
+ c.logger.FEEDBACK.Println("adding created directory to watchlist", path)
+ if err := watcher.Add(path); err != nil {
+ return err
}
+ } else if !staticSyncer.isStatic(path) {
+ // Hugo's rebuilding logic is entirely file based. When you drop a new folder into
+ // /content on OSX, the above logic will handle future watching of those files,
+ // but the initial CREATE is lost.
+ dynamicEvents = append(dynamicEvents, fsnotify.Event{Name: path, Op: fsnotify.Create})
+ }
+ return nil
+ }
- if len(dynamicEvents) > 0 {
- partitionedEvents := partitionDynamicEvents(
- c.firstPathSpec().BaseFs.SourceFilesystems,
- dynamicEvents)
+ // recursively add new directories to watch list
+ // When mkdir -p is used, only the top directory triggers an event (at least on OSX)
+ if ev.Op&fsnotify.Create == fsnotify.Create {
+ if s, err := c.Fs.Source.Stat(ev.Name); err == nil && s.Mode().IsDir() {
+ _ = helpers.SymbolicWalk(c.Fs.Source, ev.Name, walkAdder)
+ }
+ }
- doLiveReload := !c.h.buildWatch && !c.Cfg.GetBool("disableLiveReload")
- onePageName := pickOneWriteOrCreatePath(partitionedEvents.ContentEvents)
+ if staticSyncer.isStatic(ev.Name) {
+ staticEvents = append(staticEvents, ev)
+ } else {
+ dynamicEvents = append(dynamicEvents, ev)
+ }
+ }
- c.Logger.FEEDBACK.Println("\nChange detected, rebuilding site")
- const layout = "2006-01-02 15:04:05.000 -0700"
- c.Logger.FEEDBACK.Println(time.Now().Format(layout))
+ if len(staticEvents) > 0 {
+ c.logger.FEEDBACK.Println("\nStatic file changes detected")
+ const layout = "2006-01-02 15:04:05.000 -0700"
+ c.logger.FEEDBACK.Println(time.Now().Format(layout))
- c.changeDetector.PrepareNew()
- if err := c.rebuildSites(dynamicEvents); err != nil {
- c.Logger.ERROR.Println("Failed to rebuild site:", err)
- }
+ if c.Cfg.GetBool("forceSyncStatic") {
+ c.logger.FEEDBACK.Printf("Syncing all static files\n")
+ _, err := c.copyStatic()
+ if err != nil {
+ c.logger.ERROR.Println("Error copying static files to publish dir:", err)
+ return
+ }
+ } else {
+ if err := staticSyncer.syncsStaticEvents(staticEvents); err != nil {
+ c.logger.ERROR.Println("Error syncing static files to publish dir:", err)
+ return
+ }
+ }
- if doLiveReload {
- if len(partitionedEvents.ContentEvents) == 0 && len(partitionedEvents.AssetEvents) > 0 {
- changed := c.changeDetector.changed()
- if c.changeDetector != nil && len(changed) == 0 {
- // Nothing has changed.
- continue
- } else if len(changed) == 1 {
- pathToRefresh := c.firstPathSpec().RelURL(helpers.ToSlashTrimLeading(changed[0]), false)
- livereload.RefreshPath(pathToRefresh)
- } else {
- livereload.ForceRefresh()
- }
- }
+ if !c.h.buildWatch && !c.Cfg.GetBool("disableLiveReload") {
+ // Will block forever trying to write to a channel that nobody is reading if livereload isn't initialized
+
+ // force refresh when more than one file
+ if len(staticEvents) == 1 {
+ ev := staticEvents[0]
+ path := c.hugo.BaseFs.SourceFilesystems.MakeStaticPathRelative(ev.Name)
+ path = c.firstPathSpec().RelURL(helpers.ToSlashTrimLeading(path), false)
+ livereload.RefreshPath(path)
+ } else {
+ livereload.ForceRefresh()
+ }
+ }
+ }
- if len(partitionedEvents.ContentEvents) > 0 {
+ if len(dynamicEvents) > 0 {
+ partitionedEvents := partitionDynamicEvents(
+ c.firstPathSpec().BaseFs.SourceFilesystems,
+ dynamicEvents)
- navigate := c.Cfg.GetBool("navigateToChanged")
- // We have fetched the same page above, but it may have
- // changed.
- var p *hugolib.Page
+ doLiveReload := !c.h.buildWatch && !c.Cfg.GetBool("disableLiveReload")
+ onePageName := pickOneWriteOrCreatePath(partitionedEvents.ContentEvents)
- if navigate {
- if onePageName != "" {
- p = c.hugo.GetContentPage(onePageName)
- }
- }
+ c.logger.FEEDBACK.Println("\nChange detected, rebuilding site")
+ const layout = "2006-01-02 15:04:05.000 -0700"
+ c.logger.FEEDBACK.Println(time.Now().Format(layout))
- if p != nil {
- livereload.NavigateToPathForPort(p.RelPermalink(), p.Site.ServerPort())
- } else {
- livereload.ForceRefresh()
- }