diff --git a/commands/commandeer.go b/commands/commandeer.go index c55806980e4..41cb651180e 100644 --- a/commands/commandeer.go +++ b/commands/commandeer.go @@ -14,6 +14,11 @@ package commands import ( + "errors" + "io/ioutil" + + jww "github.com/spf13/jwalterweatherman" + "os" "path/filepath" "regexp" @@ -21,13 +26,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 +51,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 @@ -74,6 +81,25 @@ type commandeer struct { paused bool } +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() + + return m +} + func (c *commandeer) Set(key string, value interface{}) { if c.configured { panic("commandeer cannot be changed") @@ -105,6 +131,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) @@ -244,12 +272,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 54eb03b5b9a..8670d498303 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/hugo.go b/commands/hugo.go index 2e7353d5152..f20f78dadc5 100644 --- a/commands/hugo.go +++ b/commands/hugo.go @@ -21,13 +21,15 @@ import ( "os/signal" "sort" "sync/atomic" + + "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 +87,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 +120,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 +163,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) { @@ -277,7 +279,7 @@ func (c *commandeer) fullBuild() error { if !os.IsNotExist(err) { return fmt.Errorf("Error copying static files: %s", err) } - c.Logger.WARN.Println("No Static directory found") + c.logger.WARN.Println("No Static directory found") } langCount = cnt langCount = cnt @@ -345,8 +347,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 +390,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 +450,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 +482,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 +500,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 +513,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 +605,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}) } @@ -637,7 +639,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 +647,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 +683,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 +692,256 @@ 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 - } - } - - if c.paused { - // Wait for the server to get into a consistent state before - // we continue with processing. - continue + c.handleEvents(watcher, staticSyncer, evs, configSet) + if c.errCount() > 0 { + // TODO(bep) check if livereload enabled + other flag + // Need to reload browser to show the error + livereload.ForceRefresh() } - - 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() - } - } + c.changeDetector.PrepareNew() + if err := c.rebuildSites(dynamicEvents); err != nil { + c.logger.ERROR.Println("Rebuild failed:", err) + } + + 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. + return + } else if len(changed) == 1 { + pathToRefresh := c.firstPathSpec().RelURL(helpers.ToSlashTrimLeading(changed[0]), false) + livereload.RefreshPath(pathToRefresh) + } else { + livereload.ForceRefresh() + } + } + + if len(partitionedEvents.ContentEvents) > 0 { + + navigate := c.Cfg.GetBool("navigateToChanged") + // We have fetched the same page above, but it may have + // changed. + var p *hugolib.Page + + if navigate { + if onePageName != "" { + p = c.hugo.GetContentPage(onePageName) } } - case err := <-watcher.Errors: - if err != nil { - c.Logger.ERROR.Println(err) + + if p != nil { + livereload.NavigateToPathForPort(p.RelPermalink(), p.Site.ServerPort()) + } else { + livereload.ForceRefresh() } } } - }() - - return watcher, nil + } } // dynamicEvents contains events that is considered dynamic, as in "not static". diff --git a/commands/server.go b/commands/server.go index 27999fa6c2a..8387c58cfef 100644 --- a/commands/server.go +++ b/commands/server.go @@ -14,6 +14,7 @@ package commands import ( + "bytes" "fmt" "net" "net/http" @@ -21,6 +22,7 @@ import ( "os" "os/signal" "path/filepath" + "regexp" "runtime" "strconv" "strings" @@ -29,6 +31,7 @@ import ( "time" "github.com/gohugoio/hugo/livereload" + "github.com/gohugoio/hugo/tpl" "github.com/gohugoio/hugo/config" @@ -176,7 +179,7 @@ func (sc *serverCmd) server(cmd *cobra.Command, args []string) error { // port set explicitly by user -- he/she probably meant it! err = newSystemErrorF("Server startup failed: %s", err) } - jww.ERROR.Println("port", sc.serverPort, "already in use, attempting to use an available port") + c.logger.FEEDBACK.Println("port", sc.serverPort, "already in use, attempting to use an available port") sp, err := helpers.FindAvailablePort() if err != nil { err = newSystemError("Unable to find alternative port to use:", err) @@ -223,7 +226,7 @@ func (sc *serverCmd) server(cmd *cobra.Command, args []string) error { } if err := memStats(); err != nil { - jww.ERROR.Println("memstats error:", err) + jww.WARN.Println("memstats error:", err) } c, err := initializeConfig(true, true, &sc.hugoBuilderCommon, sc, cfgInit) @@ -271,10 +274,11 @@ func (sc *serverCmd) server(cmd *cobra.Command, args []string) error { } type fileServer struct { - baseURLs []string - roots []string - c *commandeer - s *serverCmd + baseURLs []string + roots []string + errorTemplate tpl.Template + c *commandeer + s *serverCmd } func (f *fileServer) createEndpoint(i int) (*http.ServeMux, string, string, error) { @@ -316,6 +320,18 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, string, string, erro decorate := func(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + + // First check the error state + err := f.c.getErrorWithContext() + if err != nil { + w.WriteHeader(500) + var b bytes.Buffer + f.errorTemplate.Execute(&b, err) + fmt.Fprint(w, injectLiveReloadScript(&b)) + + return + } + if f.s.noHTTPCache { w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0") w.Header().Set("Pragma", "no-cache") @@ -345,6 +361,11 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, string, string, erro return mu, u.String(), endpoint, nil } +var logErrorRe = regexp.MustCompile("(?s)ERROR \\d{4}/\\d{2}/\\d{2} \\d{2}:\\d{2}:\\d{2} ") + +func removeErrorPrefixFromLog(content string) string { + return logErrorRe.ReplaceAllLiteralString(content, "") +} func (c *commandeer) serve(s *serverCmd) error { isMultiHost := c.hugo.IsMultihost() @@ -365,11 +386,17 @@ func (c *commandeer) serve(s *serverCmd) error { roots = []string{""} } + templ, err := c.hugo.TextTmpl.Parse("__default_server_error", defaultBuildErrorTemplate) + if err != nil { + return err + } + srv := &fileServer{ - baseURLs: baseURLs, - roots: roots, - c: c, - s: s, + baseURLs: baseURLs, + roots: roots, + c: c, + s: s, + errorTemplate: templ, } doLiveReload := !c.Cfg.GetBool("disableLiveReload") @@ -392,7 +419,7 @@ func (c *commandeer) serve(s *serverCmd) error { go func() { err = http.ListenAndServe(endpoint, mu) if err != nil { - jww.ERROR.Printf("Error: %s\n", err.Error()) + c.logger.ERROR.Printf("Error: %s\n", err.Error()) os.Exit(1) } }() diff --git a/commands/server_errors.go b/commands/server_errors.go new file mode 100644 index 00000000000..ae8d2116d92 --- /dev/null +++ b/commands/server_errors.go @@ -0,0 +1,86 @@ +// 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 ( + "bytes" + "io" + + "github.com/gohugoio/hugo/transform" + "github.com/gohugoio/hugo/transform/livereloadinject" +) + +var defaultBuildErrorTemplate = ` + + + + Hugo Server: Error + + + +
+ {{ highlight .Error "apl" "noclasses=true,style=monokai" }} +

{{ .Version }}

+ Reload Page +
+ + +` + +func injectLiveReloadScript(src io.Reader) string { + // TODO(bep) errors port + var b bytes.Buffer + chain := transform.Chain{livereloadinject.New(1313)} + chain.Apply(&b, src) + + return b.String() +} + +//s.Cfg.GetInt("liveReloadPort") diff --git a/commands/server_test.go b/commands/server_test.go index 72d81d70db4..438837a90a8 100644 --- a/commands/server_test.go +++ b/commands/server_test.go @@ -18,6 +18,7 @@ import ( "net/http" "os" "runtime" + "strings" "testing" "time" @@ -113,6 +114,18 @@ func TestFixURL(t *testing.T) { } } +func TestRemoveErrorPrefixFromLog(t *testing.T) { + assert := require.New(t) + content := `ERROR 2018/10/07 13:11:12 Error while rendering "home": template: _default/baseof.html:4:3: executing "main" at : error calling partial: template: partials/logo.html:5:84: executing "partials/logo.html" at <$resized.AHeight>: can't evaluate field AHeight in type *resource.Image +ERROR 2018/10/07 13:11:12 Rebuild failed: logged 1 error(s) +` + + withoutError := removeErrorPrefixFromLog(content) + + assert.False(strings.Contains(withoutError, "ERROR"), withoutError) + +} + func isWindowsCI() bool { return runtime.GOOS == "windows" && os.Getenv("CI") != "" } diff --git a/commands/static_syncer.go b/commands/static_syncer.go index 1e73e7fc259..2374538683f 100644 --- a/commands/static_syncer.go +++ b/commands/static_syncer.go @@ -105,10 +105,10 @@ func (s *staticSyncer) syncsStaticEvents(staticEvents []fsnotify.Event) error { logger.Println("Syncing", relPath, "to", publishDir) if err := syncer.Sync(filepath.Join(publishDir, relPath), relPath); err != nil { - c.Logger.ERROR.Println(err) + c.logger.ERROR.Println(err) } } else { - c.Logger.ERROR.Println(err) + c.logger.ERROR.Println(err) } continue @@ -117,7 +117,7 @@ func (s *staticSyncer) syncsStaticEvents(staticEvents []fsnotify.Event) error { // For all other event operations Hugo will sync static. logger.Println("Syncing", relPath, "to", publishDir) if err := syncer.Sync(filepath.Join(publishDir, relPath), relPath); err != nil { - c.Logger.ERROR.Println(err) + c.logger.ERROR.Println(err) } } diff --git a/commands/version.go b/commands/version.go index ea4e4c926c0..b85f53725fe 100644 --- a/commands/version.go +++ b/commands/version.go @@ -14,14 +14,16 @@ package commands import ( + "fmt" "runtime" "strings" + jww "github.com/spf13/jwalterweatherman" + "github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/hugolib" "github.com/gohugoio/hugo/resource/tocss/scss" "github.com/spf13/cobra" - jww "github.com/spf13/jwalterweatherman" ) var _ cmder = (*versionCmd)(nil) @@ -45,6 +47,10 @@ func newVersionCmd() *versionCmd { } func printHugoVersion() { + jww.FEEDBACK.Println(hugoVersionString()) +} + +func hugoVersionString() string { program := "Hugo Static Site Generator" version := "v" + helpers.CurrentHugoVersion.String() @@ -64,5 +70,6 @@ func printHugoVersion() { buildDate = "unknown" } - jww.FEEDBACK.Println(program, version, osArch, "BuildDate:", buildDate) + return fmt.Sprintf("%s %s %s BuildDate: %s", program, version, osArch, buildDate) + } diff --git a/common/loggers/loggers.go b/common/loggers/loggers.go index 2f7f36b3440..a26cbd8ca9c 100644 --- a/common/loggers/loggers.go +++ b/common/loggers/loggers.go @@ -14,6 +14,8 @@ package loggers import ( + "bytes" + "io" "io/ioutil" "log" "os" @@ -21,17 +23,78 @@ import ( jww "github.com/spf13/jwalterweatherman" ) +var ( + // Counts ERROR logs to the global jww logger. + GlobalErrorCounter *jww.Counter +) + +func init() { + GlobalErrorCounter = &jww.Counter{} + jww.SetLogListeners(jww.LogCounter(GlobalErrorCounter, jww.LevelError)) +} + +// Logger wraps a *loggers.Logger and some other related logging state. +type Logger struct { + *jww.Notepad + ErrorCounter *jww.Counter + + // This is only set in server mode. + Errors *bytes.Buffer +} + +// Reset resets the logger's internal state. +func (l *Logger) Reset() { + l.ErrorCounter.Reset() + if l.Errors != nil { + l.Errors.Reset() + } +} + +// NewLogger creates a new Logger for the given thresholds +func NewLogger(stdoutThreshold, logThreshold jww.Threshold, outHandle, logHandle io.Writer, saveErrors bool) *Logger { + return newLogger(stdoutThreshold, logThreshold, outHandle, logHandle, saveErrors) +} + // NewDebugLogger is a convenience function to create a debug logger. -func NewDebugLogger() *jww.Notepad { - return jww.NewNotepad(jww.LevelDebug, jww.LevelError, os.Stdout, ioutil.Discard, "", log.Ldate|log.Ltime) +func NewDebugLogger() *Logger { + return newBasicLogger(jww.LevelDebug) } // NewWarningLogger is a convenience function to create a warning logger. -func NewWarningLogger() *jww.Notepad { - return jww.NewNotepad(jww.LevelWarn, jww.LevelError, os.Stdout, ioutil.Discard, "", log.Ldate|log.Ltime) +func NewWarningLogger() *Logger { + return newBasicLogger(jww.LevelWarn) } // NewErrorLogger is a convenience function to create an error logger. -func NewErrorLogger() *jww.Notepad { - return jww.NewNotepad(jww.LevelError, jww.LevelError, os.Stdout, ioutil.Discard, "", log.Ldate|log.Ltime) +func NewErrorLogger() *Logger { + return newBasicLogger(jww.LevelError) +} + +func newLogger(stdoutThreshold, logThreshold jww.Threshold, outHandle, logHandle io.Writer, saveErrors bool) *Logger { + errorCounter := &jww.Counter{} + listeners := []jww.LogListener{jww.LogCounter(errorCounter, jww.LevelError)} + var errorBuff *bytes.Buffer + if saveErrors { + errorBuff = new(bytes.Buffer) + errorCapture := func(t jww.Threshold) io.Writer { + if t != jww.LevelError { + // Only interested in ERROR + return nil + } + + return errorBuff + } + + listeners = append(listeners, errorCapture) + } + + return &Logger{ + Notepad: jww.NewNotepad(stdoutThreshold, logThreshold, outHandle, logHandle, "", log.Ldate|log.Ltime, listeners...), + ErrorCounter: errorCounter, + Errors: errorBuff, + } +} + +func newBasicLogger(t jww.Threshold) *Logger { + return newLogger(t, jww.LevelError, os.Stdout, ioutil.Discard, false) } diff --git a/deps/deps.go b/deps/deps.go index 2b66a153f4b..1e2686421dd 100644 --- a/deps/deps.go +++ b/deps/deps.go @@ -16,7 +16,6 @@ import ( "github.com/gohugoio/hugo/resource" "github.com/gohugoio/hugo/source" "github.com/gohugoio/hugo/tpl" - jww "github.com/spf13/jwalterweatherman" ) // Deps holds dependencies used by many. @@ -25,7 +24,7 @@ import ( type Deps struct { // The logger to use. - Log *jww.Notepad `json:"-"` + Log *loggers.Logger `json:"-"` // Used to log errors that may repeat itself many times. DistinctErrorLog *helpers.DistinctLogger @@ -122,10 +121,6 @@ func (d *Deps) LoadResources() error { return err } - if th, ok := d.Tmpl.(tpl.TemplateHandler); ok { - th.PrintErrors() - } - return nil } @@ -256,7 +251,7 @@ func (d Deps) ForLanguage(cfg DepsCfg) (*Deps, error) { type DepsCfg struct { // The Logger to use. - Logger *jww.Notepad + Logger *loggers.Logger // The file systems to use Fs *hugofs.Fs diff --git a/go.mod b/go.mod index 36fdf260b9a..5040282f5f5 100644 --- a/go.mod +++ b/go.mod @@ -38,7 +38,6 @@ require ( github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect github.com/nicksnyder/go-i18n v1.10.0 github.com/olekukonko/tablewriter v0.0.0-20180506121414-d4647c9c7a84 - github.com/pmezard/go-difflib v1.0.0 // indirect github.com/russross/blackfriday v0.0.0-20180804101149-46c73eb196ba github.com/sanity-io/litter v1.1.0 github.com/sergi/go-diff v1.0.0 // indirect @@ -47,7 +46,7 @@ require ( github.com/spf13/cast v1.2.0 github.com/spf13/cobra v0.0.3 github.com/spf13/fsync v0.0.0-20170320142552-12a01e648f05 - github.com/spf13/jwalterweatherman v1.0.0 + github.com/spf13/jwalterweatherman v1.0.1-0.20181005085228-103a6da826d0 github.com/spf13/nitro v0.0.0-20131003134307-24d7ef30a12d github.com/spf13/pflag v1.0.2 github.com/spf13/viper v1.2.0 @@ -60,6 +59,7 @@ require ( golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd // indirect golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f + golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e // indirect golang.org/x/text v0.3.0 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect gopkg.in/yaml.v2 v2.2.1 diff --git a/go.sum b/go.sum index 5a71e5d7690..9361ff1688e 100644 --- a/go.sum +++ b/go.sum @@ -107,6 +107,8 @@ github.com/spf13/fsync v0.0.0-20170320142552-12a01e648f05 h1:pQHm7pxjSgC54M1rtLS github.com/spf13/fsync v0.0.0-20170320142552-12a01e648f05/go.mod h1:jdsEoy1w+v0NpuwXZEaRAH6ADTDmzfRnE2eVwshwFrM= github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/jwalterweatherman v1.0.1-0.20181005085228-103a6da826d0 h1:kPJPXmEs6V1YyXfHFbp1NCpdqhvFVssh2FGx7+OoJLM= +github.com/spf13/jwalterweatherman v1.0.1-0.20181005085228-103a6da826d0/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/go.mod h1:jU8A+8xL+6n1OX4XaZtCj4B3mIa64tULUsD6YegdpFo= github.com/spf13/pflag v1.0.2 h1:Fy0orTDgHdbnzHcsOgfCN4LtHf0ec3wwtiwJqwvf3Gc= @@ -133,6 +135,8 @@ golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6Zh 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-20180909124046-d0be0721c37e h1:o3PsSEY8E4eXWkXrIP9YJALUkVZqzHJT5DOasTyn8Vs= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 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= diff --git a/hugolib/alias.go b/hugolib/alias.go index 73d8acafce7..bcf8f1963ec 100644 --- a/hugolib/alias.go +++ b/hugolib/alias.go @@ -22,12 +22,12 @@ import ( "runtime" "strings" + "github.com/gohugoio/hugo/common/loggers" + "github.com/gohugoio/hugo/output" "github.com/gohugoio/hugo/publisher" "github.com/gohugoio/hugo/tpl" - jww "github.com/spf13/jwalterweatherman" - "github.com/gohugoio/hugo/helpers" ) @@ -47,11 +47,11 @@ func init() { type aliasHandler struct { t tpl.TemplateFinder - log *jww.Notepad + log *loggers.Logger allowRoot bool } -func newAliasHandler(t tpl.TemplateFinder, l *jww.Notepad, allowRoot bool) aliasHandler { +func newAliasHandler(t tpl.TemplateFinder, l *loggers.Logger, allowRoot bool) aliasHandler { return aliasHandler{t, l, allowRoot} } diff --git a/hugolib/datafiles_test.go b/hugolib/datafiles_test.go index 8b2dc8c0fef..6685de4cc61 100644 --- a/hugolib/datafiles_test.go +++ b/hugolib/datafiles_test.go @@ -347,7 +347,7 @@ func doTestDataDirImpl(t *testing.T, dd dataDir, expected interface{}, configKey } }() - s := buildSingleSiteExpected(t, expectBuildError, depsCfg, BuildCfg{SkipRender: true}) + s := buildSingleSiteExpected(t, false, expectBuildError, depsCfg, BuildCfg{SkipRender: true}) if !expectBuildError && !reflect.DeepEqual(expected, s.Data) { // This disabled code detects the situation described in the WARNING message below. diff --git a/hugolib/hugo_sites.go b/hugolib/hugo_sites.go index 3ff31ece36a..d9eb9f57d1a 100644 --- a/hugolib/hugo_sites.go +++ b/hugolib/hugo_sites.go @@ -21,6 +21,7 @@ import ( "strings" "sync" + "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/langs" @@ -29,7 +30,6 @@ import ( "github.com/gohugoio/hugo/i18n" "github.com/gohugoio/hugo/tpl" "github.com/gohugoio/hugo/tpl/tplimpl" - jww "github.com/spf13/jwalterweatherman" ) // HugoSites represents the sites to build. Each site represents a language. @@ -69,7 +69,7 @@ func (h *HugoSites) NumLogErrors() int { if h == nil { return 0 } - return int(h.Log.LogCountForLevelsGreaterThanorEqualTo(jww.LevelError)) + return int(h.Log.ErrorCounter.Count()) } func (h *HugoSites) PrintProcessingStats(w io.Writer) { @@ -250,7 +250,9 @@ func NewHugoSites(cfg deps.DepsCfg) (*HugoSites, error) { func (s *Site) withSiteTemplates(withTemplates ...func(templ tpl.TemplateHandler) error) func(templ tpl.TemplateHandler) error { return func(templ tpl.TemplateHandler) error { - templ.LoadTemplates("") + if err := templ.LoadTemplates(""); err != nil { + return err + } for _, wt := range withTemplates { if wt == nil { @@ -301,7 +303,8 @@ func (h *HugoSites) reset() { // resetLogs resets the log counters etc. Used to do a new build on the same sites. func (h *HugoSites) resetLogs() { - h.Log.ResetLogCounters() + h.Log.Reset() + loggers.GlobalErrorCounter.Reset() for _, s := range h.Sites { s.Deps.DistinctErrorLog = helpers.NewDistinctLogger(h.Log.ERROR) } diff --git a/hugolib/hugo_sites_build.go b/hugolib/hugo_sites_build.go index 8ca2128a166..5bb328aa286 100644 --- a/hugolib/hugo_sites_build.go +++ b/hugolib/hugo_sites_build.go @@ -19,8 +19,6 @@ import ( "errors" - jww "github.com/spf13/jwalterweatherman" - "github.com/fsnotify/fsnotify" "github.com/gohugoio/hugo/helpers" ) @@ -79,7 +77,7 @@ func (h *HugoSites) Build(config BuildCfg, events ...fsnotify.Event) error { h.Log.FEEDBACK.Println() } - errorCount := h.Log.LogCountForLevel(jww.LevelError) + errorCount := h.Log.ErrorCounter.Count() if errorCount > 0 { return fmt.Errorf("logged %d error(s)", errorCount) } diff --git a/hugolib/page.go b/hugolib/page.go index 1fefd945a2e..f48d5f18dbf 100644 --- a/hugolib/page.go +++ b/hugolib/page.go @@ -989,11 +989,15 @@ func (s *Site) NewPage(name string) (*Page, error) { return p, nil } +func (p *Page) errorf(format string, a ...interface{}) error { + args := append([]interface{}{p.Lang(), p.pathOrTitle()}, a...) + return fmt.Errorf("[%s] Page %q: "+format, args...) +} + func (p *Page) ReadFrom(buf io.Reader) (int64, error) { // Parse for metadata & body if err := p.parse(buf); err != nil { - p.s.Log.ERROR.Printf("%s for %s", err, p.File.Path()) - return 0, err + return 0, p.errorf("parse failed: %s", err) } return int64(len(p.rawContent)), nil @@ -1751,6 +1755,7 @@ func (p *Page) shouldRenderTo(f output.Format) bool { func (p *Page) parse(reader io.Reader) error { psr, err := parser.ReadFrom(reader) + if err != nil { return err } @@ -1762,7 +1767,7 @@ func (p *Page) parse(reader io.Reader) error { meta, err := psr.Metadata() if err != nil { - return fmt.Errorf("failed to parse page metadata for %q: %s", p.File.Path(), err) + return fmt.Errorf("error in front matter: %s", err) } if meta == nil { // missing frontmatter equivalent to empty frontmatter @@ -2303,8 +2308,13 @@ func (p *Page) setValuesForKind(s *Site) { // Used in error logs. func (p *Page) pathOrTitle() string { - if p.Path() != "" { - return p.Path() + if p.Filename() != "" { + // Make a path relative to the working dir if possible. + filename := strings.TrimPrefix(p.Filename(), p.s.WorkingDir) + if filename != p.Filename() { + filename = strings.TrimPrefix(filename, helpers.FilePathSeparator) + } + return filename } return p.title } diff --git a/hugolib/page_bundler_capture.go b/hugolib/page_bundler_capture.go index fbfad0103a2..ca41df1fe71 100644 --- a/hugolib/page_bundler_capture.go +++ b/hugolib/page_bundler_capture.go @@ -20,6 +20,9 @@ import ( "path" "path/filepath" "runtime" + + "github.com/gohugoio/hugo/common/loggers" + "sort" "strings" "sync" @@ -33,7 +36,6 @@ import ( "golang.org/x/sync/errgroup" "github.com/gohugoio/hugo/source" - jww "github.com/spf13/jwalterweatherman" ) var errSkipCyclicDir = errors.New("skip potential cyclic dir") @@ -47,7 +49,7 @@ type capturer struct { sourceSpec *source.SourceSpec fs afero.Fs - logger *jww.Notepad + logger *loggers.Logger // Filenames limits the content to process to a list of filenames/directories. // This is used for partial building in server mode. @@ -61,7 +63,7 @@ type capturer struct { } func newCapturer( - logger *jww.Notepad, + logger *loggers.Logger, sourceSpec *source.SourceSpec, handler captureResultHandler, contentChanges *contentChangeMap, diff --git a/hugolib/page_bundler_capture_test.go b/hugolib/page_bundler_capture_test.go index ace96b633ad..d6128352c0a 100644 --- a/hugolib/page_bundler_capture_test.go +++ b/hugolib/page_bundler_capture_test.go @@ -22,8 +22,6 @@ import ( "github.com/gohugoio/hugo/common/loggers" - jww "github.com/spf13/jwalterweatherman" - "runtime" "strings" "sync" @@ -100,9 +98,6 @@ func TestPageBundlerCaptureSymlinks(t *testing.T) { assert.NoError(c.capture()) - // Symlink back to content skipped to prevent infinite recursion. - assert.Equal(uint64(3), logger.LogCountForLevelsGreaterThanorEqualTo(jww.LevelWarn)) - expected := ` F: /base/a/page_s.md diff --git a/hugolib/pagemeta/page_frontmatter.go b/hugolib/pagemeta/page_frontmatter.go index c1139bd907c..88f6f3a11e4 100644 --- a/hugolib/pagemeta/page_frontmatter.go +++ b/hugolib/pagemeta/page_frontmatter.go @@ -14,17 +14,14 @@ package pagemeta import ( - "io/ioutil" - "log" - "os" "strings" "time" + "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/config" "github.com/spf13/cast" - jww "github.com/spf13/jwalterweatherman" ) // FrontMatterHandler maps front matter into Page fields and .Params. @@ -40,7 +37,7 @@ type FrontMatterHandler struct { // A map of all date keys configured, including any custom. allDateKeys map[string]bool - logger *jww.Notepad + logger *loggers.Logger } // FrontMatterDescriptor describes how to handle front matter for a given Page. @@ -263,10 +260,10 @@ func toLowerSlice(in interface{}) []string { // NewFrontmatterHandler creates a new FrontMatterHandler with the given logger and configuration. // If no logger is provided, one will be created. -func NewFrontmatterHandler(logger *jww.Notepad, cfg config.Provider) (FrontMatterHandler, error) { +func NewFrontmatterHandler(logger *loggers.Logger, cfg config.Provider) (FrontMatterHandler, error) { if logger == nil { - logger = jww.NewNotepad(jww.LevelWarn, jww.LevelWarn, os.Stdout, ioutil.Discard, "", log.Ldate|log.Ltime) + logger = loggers.NewWarningLogger() } frontMatterConfig, err := newFrontmatterConfig(cfg) diff --git a/hugolib/shortcode.go b/hugolib/shortcode.go index c2fcf1b8d0f..9619b3e3b0e 100644 --- a/hugolib/shortcode.go +++ b/hugolib/shortcode.go @@ -524,7 +524,7 @@ Loop: // return that error, more specific continue } - return sc, fmt.Errorf("Shortcode '%s' in page '%s' has no .Inner, yet a closing tag was provided", next.val, p.FullFilePath()) + return sc, p.errorf("shortcode %q has no .Inner, yet a closing tag was provided", next.val) } if next.typ == tRightDelimScWithMarkup || next.typ == tRightDelimScNoMarkup { // self-closing @@ -542,13 +542,13 @@ Loop: // if more than one. It is "all inner or no inner". tmpl := getShortcodeTemplateForTemplateKey(scKey{}, sc.name, p.s.Tmpl) if tmpl == nil { - return sc, fmt.Errorf("Unable to locate template for shortcode %q in page %q", sc.name, p.Path()) + return sc, p.errorf("unable to locate template for shortcode %q", sc.name) } var err error isInner, err = isInnerShortcode(tmpl.(tpl.TemplateExecutor)) if err != nil { - return sc, fmt.Errorf("Failed to handle template for shortcode %q for page %q: %s", sc.name, p.Path(), err) + return sc, p.errorf("failed to handle template for shortcode %q: %s", sc.name, err) } case tScParam: diff --git a/hugolib/shortcode_test.go b/hugolib/shortcode_test.go index df7b7103f98..0f60f8e5ab7 100644 --- a/hugolib/shortcode_test.go +++ b/hugolib/shortcode_test.go @@ -24,8 +24,6 @@ import ( "github.com/spf13/viper" - jww "github.com/spf13/jwalterweatherman" - "github.com/spf13/afero" "github.com/gohugoio/hugo/output" @@ -777,7 +775,7 @@ NotFound: {{< thisDoesNotExist >}} "thisDoesNotExist", ) - require.Equal(t, uint64(1), s.Log.LogCountForLevel(jww.LevelError)) + require.Equal(t, uint64(1), s.Log.ErrorCounter.Count()) } diff --git a/hugolib/site.go b/hugolib/site.go index 1196496d303..fe539825d62 100644 --- a/hugolib/site.go +++ b/hugolib/site.go @@ -754,8 +754,6 @@ func (s *Site) processPartial(events []fsnotify.Event) (whatChanged, error) { return whatChanged{}, err } - s.TemplateHandler().PrintErrors() - for i := 1; i < len(sites); i++ { site := sites[i] var err error @@ -1759,7 +1757,7 @@ func (s *Site) renderForLayouts(name string, d interface{}, w io.Writer, layouts if err = templ.Execute(w, d); err != nil { // Behavior here should be dependent on if running in server or watch mode. if p, ok := d.(*PageOutput); ok { - if p.File != nil { + if p.File != nil && p.File.Dir() != "" { s.DistinctErrorLog.Printf("Error while rendering %q in %q: %s", name, p.File.Dir(), err) } else { s.DistinctErrorLog.Printf("Error while rendering %q: %s", name, err) diff --git a/hugolib/site_test.go b/hugolib/site_test.go index f775b0e7971..a5688c78ef4 100644 --- a/hugolib/site_test.go +++ b/hugolib/site_test.go @@ -54,7 +54,7 @@ func TestRenderWithInvalidTemplate(t *testing.T) { withTemplate := createWithTemplateFromNameValues("missing", templateMissingFunc) - buildSingleSiteExpected(t, true, deps.DepsCfg{Fs: fs, Cfg: cfg, WithTemplate: withTemplate}, BuildCfg{}) + buildSingleSiteExpected(t, true, false, deps.DepsCfg{Fs: fs, Cfg: cfg, WithTemplate: withTemplate}, BuildCfg{}) } diff --git a/hugolib/testhelpers_test.go b/hugolib/testhelpers_test.go index 27edf3fdd6b..1740deefdc0 100644 --- a/hugolib/testhelpers_test.go +++ b/hugolib/testhelpers_test.go @@ -14,7 +14,6 @@ import ( "github.com/gohugoio/hugo/langs" "github.com/sanity-io/litter" - jww "github.com/spf13/jwalterweatherman" "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/deps" @@ -26,6 +25,7 @@ import ( "os" + "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/hugofs" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -38,7 +38,7 @@ type sitesBuilder struct { Fs *hugofs.Fs T testing.TB - logger *jww.Notepad + logger *loggers.Logger dumper litter.Options @@ -103,7 +103,7 @@ func (s *sitesBuilder) Running() *sitesBuilder { return s } -func (s *sitesBuilder) WithLogger(logger *jww.Notepad) *sitesBuilder { +func (s *sitesBuilder) WithLogger(logger *loggers.Logger) *sitesBuilder { s.logger = logger return s } @@ -639,13 +639,19 @@ func createWithTemplateFromNameValues(additionalTemplates ...string) func(templ } func buildSingleSite(t testing.TB, depsCfg deps.DepsCfg, buildCfg BuildCfg) *Site { - return buildSingleSiteExpected(t, false, depsCfg, buildCfg) + return buildSingleSiteExpected(t, false, false, depsCfg, buildCfg) } -func buildSingleSiteExpected(t testing.TB, expectBuildError bool, depsCfg deps.DepsCfg, buildCfg BuildCfg) *Site { +func buildSingleSiteExpected(t testing.TB, expectSiteInitEror, expectBuildError bool, depsCfg deps.DepsCfg, buildCfg BuildCfg) *Site { h, err := NewHugoSites(depsCfg) - require.NoError(t, err) + if expectSiteInitEror { + require.Error(t, err) + return nil + } else { + require.NoError(t, err) + } + require.Len(t, h.Sites, 1) if expectBuildError { diff --git a/i18n/i18n.go b/i18n/i18n.go index 73417fb3240..d436ea58d37 100644 --- a/i18n/i18n.go +++ b/i18n/i18n.go @@ -14,6 +14,7 @@ package i18n import ( + "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/helpers" "github.com/nicksnyder/go-i18n/i18n/bundle" @@ -28,11 +29,11 @@ var ( type Translator struct { translateFuncs map[string]bundle.TranslateFunc cfg config.Provider - logger *jww.Notepad + logger *loggers.Logger } // NewTranslator creates a new Translator for the given language bundle and configuration. -func NewTranslator(b *bundle.Bundle, cfg config.Provider, logger *jww.Notepad) Translator { +func NewTranslator(b *bundle.Bundle, cfg config.Provider, logger *loggers.Logger) Translator { t := Translator{cfg: cfg, logger: logger, translateFuncs: make(map[string]bundle.TranslateFunc)} t.initFuncs(b) return t diff --git a/i18n/i18n_test.go b/i18n/i18n_test.go index 5075839ff2f..84b7384d075 100644 --- a/i18n/i18n_test.go +++ b/i18n/i18n_test.go @@ -19,24 +19,19 @@ import ( "github.com/gohugoio/hugo/tpl/tplimpl" + "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/langs" "github.com/spf13/afero" "github.com/gohugoio/hugo/deps" - "io/ioutil" - "os" - - "log" - "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/hugofs" - jww "github.com/spf13/jwalterweatherman" "github.com/spf13/viper" "github.com/stretchr/testify/require" ) -var logger = jww.NewNotepad(jww.LevelError, jww.LevelError, os.Stdout, ioutil.Discard, "", log.Ldate|log.Ltime) +var logger = loggers.NewErrorLogger() type i18nTest struct { data map[string][]byte diff --git a/resource/resource.go b/resource/resource.go index dd9cbbd4179..a18d03aabf3 100644 --- a/resource/resource.go +++ b/resource/resource.go @@ -32,8 +32,6 @@ import ( "github.com/gohugoio/hugo/common/hugio" "github.com/gohugoio/hugo/common/loggers" - jww "github.com/spf13/jwalterweatherman" - "github.com/spf13/afero" "github.com/gobwas/glob" @@ -273,7 +271,7 @@ type Spec struct { MediaTypes media.Types OutputFormats output.Formats - Logger *jww.Notepad + Logger *loggers.Logger TextTemplates tpl.TemplateParseFinder @@ -287,7 +285,7 @@ type Spec struct { GenAssetsPath string } -func NewSpec(s *helpers.PathSpec, logger *jww.Notepad, outputFormats output.Formats, mimeTypes media.Types) (*Spec, error) { +func NewSpec(s *helpers.PathSpec, logger *loggers.Logger, outputFormats output.Formats, mimeTypes media.Types) (*Spec, error) { imaging, err := decodeImaging(s.Cfg.GetStringMap("imaging")) if err != nil { @@ -542,7 +540,7 @@ type resourceHash struct { type publishOnce struct { publisherInit sync.Once publisherErr error - logger *jww.Notepad + logger *loggers.Logger } func (l *publishOnce) publish(s Source) error { diff --git a/tpl/collections/collections_test.go b/tpl/collections/collections_test.go index c8a7207ea4b..9a26100e090 100644 --- a/tpl/collections/collections_test.go +++ b/tpl/collections/collections_test.go @@ -17,20 +17,18 @@ import ( "errors" "fmt" "html/template" - "io/ioutil" - "log" "math/rand" - "os" "reflect" "testing" "time" + "github.com/gohugoio/hugo/common/collections" + "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/hugofs" "github.com/gohugoio/hugo/langs" - jww "github.com/spf13/jwalterweatherman" "github.com/spf13/viper" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -856,7 +854,7 @@ func newDeps(cfg config.Provider) *deps.Deps { Cfg: cfg, Fs: hugofs.NewMem(l), ContentSpec: cs, - Log: jww.NewNotepad(jww.LevelError, jww.LevelError, os.Stdout, ioutil.Discard, "", log.Ldate|log.Ltime), + Log: loggers.NewErrorLogger(), } } diff --git a/tpl/data/data_test.go b/tpl/data/data_test.go index 6bee0d52481..9ef969244a9 100644 --- a/tpl/data/data_test.go +++ b/tpl/data/data_test.go @@ -113,11 +113,11 @@ func TestGetCSV(t *testing.T) { require.NoError(t, err, msg) if _, ok := test.expect.(bool); ok { - require.Equal(t, 1, int(ns.deps.Log.LogCountForLevelsGreaterThanorEqualTo(jww.LevelError))) + require.Equal(t, 1, int(ns.deps.Log.ErrorCounter.Count())) require.Nil(t, got) continue } - require.Equal(t, 0, int(ns.deps.Log.LogCountForLevelsGreaterThanorEqualTo(jww.LevelError))) + require.Equal(t, 0, int(ns.deps.Log.ErrorCounter.Count())) require.NotNil(t, got, msg) assert.EqualValues(t, test.expect, got, msg) @@ -198,14 +198,14 @@ func TestGetJSON(t *testing.T) { continue } - if errLevel, ok := test.expect.(jww.Threshold); ok { - logCount := ns.deps.Log.LogCountForLevelsGreaterThanorEqualTo(errLevel) + if errLevel, ok := test.expect.(jww.Threshold); ok && errLevel >= jww.LevelError { + logCount := ns.deps.Log.ErrorCounter.Count() require.True(t, logCount >= 1, fmt.Sprintf("got log count %d", logCount)) continue } require.NoError(t, err, msg) - require.Equal(t, 0, int(ns.deps.Log.LogCountForLevelsGreaterThanorEqualTo(jww.LevelError)), msg) + require.Equal(t, 0, int(ns.deps.Log.ErrorCounter.Count()), msg) require.NotNil(t, got, msg) assert.EqualValues(t, test.expect, got, msg) diff --git a/tpl/fmt/fmt.go b/tpl/fmt/fmt.go index 0f4f906c289..09e4f5a405a 100644 --- a/tpl/fmt/fmt.go +++ b/tpl/fmt/fmt.go @@ -16,12 +16,13 @@ package fmt import ( _fmt "fmt" + "github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/helpers" ) // New returns a new instance of the fmt-namespaced template functions. -func New() *Namespace { - return &Namespace{helpers.NewDistinctErrorLogger()} +func New(d *deps.Deps) *Namespace { + return &Namespace{helpers.NewDistinctLogger(d.Log.ERROR)} } // Namespace provides template functions for the "fmt" namespace. diff --git a/tpl/fmt/init.go b/tpl/fmt/init.go index 76c68957aaa..1170558010b 100644 --- a/tpl/fmt/init.go +++ b/tpl/fmt/init.go @@ -22,7 +22,7 @@ const name = "fmt" func init() { f := func(d *deps.Deps) *internal.TemplateFuncsNamespace { - ctx := New() + ctx := New(d) ns := &internal.TemplateFuncsNamespace{ Name: name, diff --git a/tpl/partials/init_test.go b/tpl/partials/init_test.go index 4832e6b66bc..0513f1572a3 100644 --- a/tpl/partials/init_test.go +++ b/tpl/partials/init_test.go @@ -16,6 +16,7 @@ package partials import ( "testing" + "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/tpl/internal" "github.com/stretchr/testify/require" @@ -28,6 +29,7 @@ func TestInit(t *testing.T) { for _, nsf := range internal.TemplateFuncsNamespaceRegistry { ns = nsf(&deps.Deps{ BuildStartListeners: &deps.Listeners{}, + Log: loggers.NewErrorLogger(), }) if ns.Name == name { found = true diff --git a/tpl/template.go b/tpl/template.go index 2cef92bb225..461d06ce0d9 100644 --- a/tpl/template.go +++ b/tpl/template.go @@ -35,8 +35,7 @@ type TemplateHandler interface { TemplateFinder AddTemplate(name, tpl string) error AddLateTemplate(name, tpl string) error - LoadTemplates(prefix string) - PrintErrors() + LoadTemplates(prefix string) error NewTextTemplate() TemplateParseFinder diff --git a/tpl/tplimpl/template.go b/tpl/tplimpl/template.go index f19c312ec92..4888a384182 100644 --- a/tpl/tplimpl/template.go +++ b/tpl/tplimpl/template.go @@ -14,12 +14,14 @@ package tplimpl import ( + "errors" "fmt" "html/template" "path" "strings" texttemplate "text/template" + "github.com/gohugoio/hugo/hugofs" "github.com/gohugoio/hugo/tpl/tplimpl/embedded" "github.com/eknkc/amber" @@ -64,7 +66,7 @@ type templateErr struct { } type templateLoader interface { - handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (string, error)) error + handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (templateInfo, error)) error addTemplate(name, tpl string) error addLateTemplate(name, tpl string) error } @@ -114,22 +116,11 @@ func (t *templateHandler) NewTextTemplate() tpl.TemplateParseFinder { } -func (t *templateHandler) addError(name string, err error) { - t.errors = append(t.errors, &templateErr{name, err}) -} - func (t *templateHandler) Debug() { fmt.Println("HTML templates:\n", t.html.t.DefinedTemplates()) fmt.Println("\n\nText templates:\n", t.text.t.DefinedTemplates()) } -// PrintErrors prints the accumulated errors as ERROR to the log. -func (t *templateHandler) PrintErrors() { - for _, e := range t.errors { - t.Log.ERROR.Println(e.name, ":", e.err) - } -} - // Lookup tries to find a template with the given name in both template // collections: First HTML, then the plain text template collection. func (t *templateHandler) Lookup(name string) (tpl.Template, bool) { @@ -321,8 +312,8 @@ func (t *textTemplates) setFuncs(funcMap map[string]interface{}) { // LoadTemplates loads the templates from the layouts filesystem. // A prefix can be given to indicate a template namespace to load the templates // into, i.e. "_internal" etc. -func (t *templateHandler) LoadTemplates(prefix string) { - t.loadTemplates(prefix) +func (t *templateHandler) LoadTemplates(prefix string) error { + return t.loadTemplates(prefix) } @@ -423,7 +414,6 @@ func (t *templateHandler) addLateTemplate(name, tpl string) error { func (t *templateHandler) AddLateTemplate(name, tpl string) error { h := t.getTemplateHandler(name) if err := h.addLateTemplate(name, tpl); err != nil { - t.addError(name, err) return err } return nil @@ -435,7 +425,6 @@ func (t *templateHandler) AddLateTemplate(name, tpl string) error { func (t *templateHandler) AddTemplate(name, tpl string) error { h := t.getTemplateHandler(name) if err := h.addTemplate(name, tpl); err != nil { - t.addError(name, err) return err } return nil @@ -458,14 +447,21 @@ func (t *templateHandler) MarkReady() { // RebuildClone rebuilds the cloned templates. Used for live-reloads. func (t *templateHandler) RebuildClone() { - t.html.clone = template.Must(t.html.cloneClone.Clone()) - t.text.clone = texttemplate.Must(t.text.cloneClone.Clone()) + if t.html != nil && t.html.cloneClone != nil { + t.html.clone = template.Must(t.html.cloneClone.Clone()) + } + if t.text != nil && t.text.cloneClone != nil { + t.text.clone = texttemplate.Must(t.text.cloneClone.Clone()) + } } -func (t *templateHandler) loadTemplates(prefix string) { +func (t *templateHandler) loadTemplates(prefix string) error { + + var failed bool + walker := func(path string, fi os.FileInfo, err error) error { if err != nil || fi.IsDir() { - return nil + return err } if isDotFile(path) || isBackupFile(path) || isBaseTemplate(path) { @@ -490,21 +486,38 @@ func (t *templateHandler) loadTemplates(prefix string) { tplID, err := output.CreateTemplateNames(descriptor) if err != nil { t.Log.ERROR.Printf("Failed to resolve template in path %q: %s", path, err) - - return nil + failed = true } if err := t.addTemplateFile(tplID.Name, tplID.MasterFilename, tplID.OverlayFilename); err != nil { - t.Log.ERROR.Printf("Failed to add template %q in path %q: %s", tplID.Name, path, err) + + filename := path + // This layout can be overridden, so we need to give the user the full path in the error message, + // if possible + if realFi, ok := fi.(hugofs.RealFilenameInfo); ok { + filename = realFi.RealFilename() + } + + t.Log.ERROR.Printf("Failed to add %q: %s", filename, err) + failed = true } return nil } if err := helpers.SymbolicWalk(t.Layouts.Fs, "", walker); err != nil { - t.Log.ERROR.Printf("Failed to load templates: %s", err) + if !os.IsNotExist(err) { + return err + } + return nil } + if failed { + return errors.New("Failed to load templates") + } + + return nil + } func (t *templateHandler) initFuncs() { @@ -553,12 +566,18 @@ func (t *templateHandler) getTemplateHandler(name string) templateLoader { return t.html } -func (t *templateHandler) handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (string, error)) error { +type templateInfo struct { + template string + // The real filename (if possible). Used for logging. + filename string +} + +func (t *templateHandler) handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (templateInfo, error)) error { h := t.getTemplateHandler(name) return h.handleMaster(name, overlayFilename, masterFilename, onMissing) } -func (t *htmlTemplates) handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (string, error)) error { +func (t *htmlTemplates) handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (templateInfo, error)) error { masterTpl := t.lookup(masterFilename) @@ -570,7 +589,7 @@ func (t *htmlTemplates) handleMaster(name, overlayFilename, masterFilename strin masterTpl, err = t.t.New(overlayFilename).Parse(templ) if err != nil { - return err + return fmt.Errorf("failed to parse %q: %s", templ.filename, err) } } @@ -579,9 +598,9 @@ func (t *htmlTemplates) handleMaster(name, overlayFilename, masterFilename strin return err } - overlayTpl, err := template.Must(masterTpl.Clone()).Parse(templ) + overlayTpl, err := template.Must(masterTpl.Clone()).Parse(templ.template) if err != nil { - return err + return fmt.Errorf("failed to parse %q: %s", templ.filename, err) } // The extra lookup is a workaround, see @@ -598,7 +617,7 @@ func (t *htmlTemplates) handleMaster(name, overlayFilename, masterFilename strin } -func (t *textTemplates) handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (string, error)) error { +func (t *textTemplates) handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (templateInfo, error)) error { name = strings.TrimPrefix(name, textTmplNamePrefix) masterTpl := t.lookup(masterFilename) @@ -609,9 +628,9 @@ func (t *textTemplates) handleMaster(name, overlayFilename, masterFilename strin return err } - masterTpl, err = t.t.New(overlayFilename).Parse(templ) + masterTpl, err = t.t.New(masterFilename).Parse(templ.template) if err != nil { - return err + return fmt.Errorf("failed to parse %q: %s", templ.filename, err) } } @@ -620,9 +639,9 @@ func (t *textTemplates) handleMaster(name, overlayFilename, masterFilename strin return err } - overlayTpl, err := texttemplate.Must(masterTpl.Clone()).Parse(templ) + overlayTpl, err := texttemplate.Must(masterTpl.Clone()).Parse(templ.template) if err != nil { - return err + return fmt.Errorf("failed to parse %q: %s", templ.filename, err) } overlayTpl = overlayTpl.Lookup(overlayTpl.Name()) @@ -640,14 +659,20 @@ func (t *templateHandler) addTemplateFile(name, baseTemplatePath, path string) e t.Log.DEBUG.Printf("Add template file: name %q, baseTemplatePath %q, path %q", name, baseTemplatePath, path) - getTemplate := func(filename string) (string, error) { + getTemplate := func(filename string) (templateInfo, error) { b, err := afero.ReadFile(t.Layouts.Fs, filename) if err != nil { - return "", err + return templateInfo{filename: filename}, err } s := string(b) - return s, nil + if fi, err := t.Layouts.Fs.Stat(filename); err == nil { + if fir, ok := fi.(hugofs.RealFilenameInfo); ok { + filename = fir.RealFilename() + } + } + + return templateInfo{template: s, filename: filename}, nil } // get the suffix and switch on that @@ -712,7 +737,7 @@ func (t *templateHandler) addTemplateFile(name, baseTemplatePath, path string) e return err } - return t.AddTemplate(name, templ) + return t.AddTemplate(name, templ.template) } } @@ -720,19 +745,24 @@ var embeddedTemplatesAliases = map[string][]string{ "shortcodes/twitter.html": []string{"shortcodes/tweet.html"}, } -func (t *templateHandler) loadEmbedded() { +func (t *templateHandler) loadEmbedded() error { for _, kv := range embedded.EmbeddedTemplates { - // TODO(bep) error handling name, templ := kv[0], kv[1] - t.addInternalTemplate(name, templ) + if err := t.addInternalTemplate(name, templ); err != nil { + return err + } if aliases, found := embeddedTemplatesAliases[name]; found { for _, alias := range aliases { - t.addInternalTemplate(alias, templ) + if err := t.addInternalTemplate(alias, templ); err != nil { + return err + } } } } + return nil + } func (t *templateHandler) addInternalTemplate(name, tpl string) error { diff --git a/tpl/tplimpl/templateProvider.go b/tpl/tplimpl/templateProvider.go index df44e81a6e0..3a803f2da9c 100644 --- a/tpl/tplimpl/templateProvider.go +++ b/tpl/tplimpl/templateProvider.go @@ -33,12 +33,15 @@ func (*TemplateProvider) Update(deps *deps.Deps) error { deps.TextTmpl = newTmpl.NewTextTemplate() newTmpl.initFuncs() - newTmpl.loadEmbedded() + + if err := newTmpl.loadEmbedded(); err != nil { + return err + } if deps.WithTemplate != nil { err := deps.WithTemplate(newTmpl) if err != nil { - newTmpl.addError("init", err) + return err } } diff --git a/tpl/tplimpl/template_funcs_test.go b/tpl/tplimpl/template_funcs_test.go index 8594c67a455..04bb4941a7e 100644 --- a/tpl/tplimpl/template_funcs_test.go +++ b/tpl/tplimpl/template_funcs_test.go @@ -21,10 +21,7 @@ import ( "testing" "time" - "io/ioutil" - "log" - "os" - + "github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/helpers" @@ -35,13 +32,12 @@ import ( "github.com/gohugoio/hugo/tpl/internal" "github.com/gohugoio/hugo/tpl/partials" "github.com/spf13/afero" - jww "github.com/spf13/jwalterweatherman" "github.com/spf13/viper" "github.com/stretchr/testify/require" ) var ( - logger = jww.NewNotepad(jww.LevelFatal, jww.LevelFatal, os.Stdout, ioutil.Discard, "", log.Ldate|log.Ltime) + logger = loggers.NewErrorLogger() ) func newTestConfig() config.Provider {