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
This commit is contained in:
Bjørn Erik Pedersen 2018-10-03 14:58:09 +02:00
parent 3a3089121b
commit 35fbfb19a1
No known key found for this signature in database
GPG key ID: 330E6E2BD4859D8F
73 changed files with 1914 additions and 668 deletions

View file

@ -14,6 +14,15 @@
package commands package commands
import ( import (
"bytes"
"errors"
"github.com/gohugoio/hugo/common/herrors"
"io/ioutil"
jww "github.com/spf13/jwalterweatherman"
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
@ -21,13 +30,13 @@ import (
"sync" "sync"
"time" "time"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/config"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/afero"
"github.com/gohugoio/hugo/hugolib" "github.com/gohugoio/hugo/hugolib"
"github.com/spf13/afero"
"github.com/bep/debounce" "github.com/bep/debounce"
"github.com/gohugoio/hugo/common/types" "github.com/gohugoio/hugo/common/types"
@ -46,6 +55,8 @@ type commandeerHugoState struct {
type commandeer struct { type commandeer struct {
*commandeerHugoState *commandeerHugoState
logger *loggers.Logger
// Currently only set when in "fast render mode". But it seems to // 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. // be fast enough that we could maybe just add it for all server modes.
changeDetector *fileChangeDetector changeDetector *fileChangeDetector
@ -69,9 +80,45 @@ type commandeer struct {
serverPorts []int serverPorts []int
languagesConfigured bool languagesConfigured bool
languages langs.Languages languages langs.Languages
doLiveReload bool
fastRenderMode bool
showErrorInBrowser bool
configured bool configured bool
paused 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{}) { func (c *commandeer) Set(key string, value interface{}) {
@ -105,6 +152,8 @@ func newCommandeer(mustHaveConfigFile, running bool, h *hugoBuilderCommon, f fla
doWithCommandeer: doWithCommandeer, doWithCommandeer: doWithCommandeer,
visitedURLs: types.NewEvictingStringQueue(10), visitedURLs: types.NewEvictingStringQueue(10),
debounce: rebuildDebouncer, 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) return c, c.loadConfig(mustHaveConfigFile, running)
@ -236,6 +285,11 @@ func (c *commandeer) loadConfig(mustHaveConfigFile, running bool) error {
c.languages = l 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 // This is potentially double work, but we need to do this one more time now
// that all the languages have been configured. // that all the languages have been configured.
if c.doWithCommandeer != nil { 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 { if err != nil {
return err return err
} }
cfg.Logger = logger cfg.Logger = logger
c.logger = logger
createMemFs := config.GetBool("renderToMemory") createMemFs := config.GetBool("renderToMemory")

View file

@ -14,12 +14,10 @@
package commands package commands
import ( import (
"os" "github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/helpers"
"github.com/spf13/cobra" "github.com/spf13/cobra"
jww "github.com/spf13/jwalterweatherman"
"github.com/spf13/nitro" "github.com/spf13/nitro"
) )
@ -242,7 +240,7 @@ func (cc *hugoBuilderCommon) handleFlags(cmd *cobra.Command) {
_ = cmd.Flags().SetAnnotation("theme", cobra.BashCompSubdirsInDir, []string{"themes"}) _ = 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 { if err == nil {
return return
} }
@ -255,25 +253,3 @@ func checkErr(logger *jww.Notepad, err error, s ...string) {
} }
logger.ERROR.Println(err) 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)
}
}
}

View file

@ -14,10 +14,10 @@
package commands package commands
import ( import (
"fmt"
"time" "time"
src "github.com/gohugoio/hugo/source" src "github.com/gohugoio/hugo/source"
"github.com/pkg/errors"
"github.com/gohugoio/hugo/hugolib" "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 { 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 return nil

View file

@ -18,16 +18,22 @@ package commands
import ( import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os/signal" "os/signal"
"sort" "sort"
"sync/atomic" "sync/atomic"
"github.com/pkg/errors"
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/loggers"
"syscall" "syscall"
"github.com/gohugoio/hugo/hugolib/filesystems" "github.com/gohugoio/hugo/hugolib/filesystems"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
"log"
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
@ -85,7 +91,7 @@ func Execute(args []string) Response {
} }
if err == nil { if err == nil {
errCount := int(jww.LogCountForLevelsGreaterThanorEqualTo(jww.LevelError)) errCount := int(loggers.GlobalErrorCounter.Count())
if errCount > 0 { if errCount > 0 {
err = fmt.Errorf("logged %d errors", errCount) err = fmt.Errorf("logged %d errors", errCount)
} else if resp.Result != nil { } 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 ( var (
logHandle = ioutil.Discard logHandle = ioutil.Discard
logThreshold = jww.LevelWarn logThreshold = jww.LevelWarn
@ -161,7 +167,7 @@ func (c *commandeer) createLogger(cfg config.Provider) (*jww.Notepad, error) {
jww.SetStdoutThreshold(stdoutThreshold) jww.SetStdoutThreshold(stdoutThreshold)
helpers.InitLoggers() 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) { func initializeFlags(cmd *cobra.Command, cfg config.Provider) {
@ -275,9 +281,9 @@ func (c *commandeer) fullBuild() error {
cnt, err := c.copyStatic() cnt, err := c.copyStatic()
if err != nil { if err != nil {
if !os.IsNotExist(err) { 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
langCount = cnt langCount = cnt
@ -285,7 +291,7 @@ func (c *commandeer) fullBuild() error {
} }
buildSitesFunc := func() error { buildSitesFunc := func() error {
if err := c.buildSites(); err != nil { if err := c.buildSites(); err != nil {
return fmt.Errorf("Error building site: %s", err) return errors.Wrap(err, "Error building site")
} }
return nil return nil
} }
@ -345,8 +351,8 @@ func (c *commandeer) build() error {
if err != nil { if err != nil {
return err return err
} }
c.Logger.FEEDBACK.Println("Watching for changes in", c.hugo.PathSpec.AbsPathify(c.Cfg.GetString("contentDir"))) 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("Press Ctrl+C to stop")
watcher, err := c.newWatcher(watchDirs...) watcher, err := c.newWatcher(watchDirs...)
checkErr(c.Logger, err) checkErr(c.Logger, err)
defer watcher.Close() defer watcher.Close()
@ -388,7 +394,7 @@ func (c *commandeer) doWithPublishDirs(f func(sourceFs *filesystems.SourceFilesy
staticFilesystems := c.hugo.BaseFs.SourceFilesystems.Static staticFilesystems := c.hugo.BaseFs.SourceFilesystems.Static
if len(staticFilesystems) == 0 { 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 return langCount, nil
} }
@ -448,13 +454,13 @@ func (c *commandeer) copyStaticTo(sourceFs *filesystems.SourceFilesystem) (uint6
syncer.Delete = c.Cfg.GetBool("cleanDestinationDir") syncer.Delete = c.Cfg.GetBool("cleanDestinationDir")
if syncer.Delete { 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 { syncer.DeleteFilter = func(f os.FileInfo) bool {
return f.IsDir() && strings.HasPrefix(f.Name(), ".") 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 var err error
@ -480,7 +486,7 @@ func (c *commandeer) timeTrack(start time.Time, name string) {
return return
} }
elapsed := time.Since(start) 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. // getDirList provides NewWatcher() with a list of directories to watch for changes.
@ -498,7 +504,7 @@ func (c *commandeer) getDirList() ([]string, error) {
return nil return nil
} }
c.Logger.ERROR.Println("Walker: ", err) c.logger.ERROR.Println("Walker: ", err)
return nil return nil
} }
@ -511,16 +517,16 @@ func (c *commandeer) getDirList() ([]string, error) {
if fi.Mode()&os.ModeSymlink == os.ModeSymlink { if fi.Mode()&os.ModeSymlink == os.ModeSymlink {
link, err := filepath.EvalSymlinks(path) link, err := filepath.EvalSymlinks(path)
if err != nil { 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 return nil
} }
linkfi, err := helpers.LstatIfPossible(c.Fs.Source, link) linkfi, err := helpers.LstatIfPossible(c.Fs.Source, link)
if err != nil { 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 return nil
} }
if !allowSymbolicDirs && !linkfi.Mode().IsRegular() { 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 return nil
} }
@ -603,7 +609,7 @@ func (c *commandeer) getDirList() ([]string, error) {
func (c *commandeer) resetAndBuildSites() (err error) { func (c *commandeer) resetAndBuildSites() (err error) {
if !c.h.quiet { 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}) 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 { func (c *commandeer) rebuildSites(events []fsnotify.Event) error {
defer c.timeTrack(time.Now(), "Total") defer c.timeTrack(time.Now(), "Total")
c.buildErr = nil
visited := c.visitedURLs.PeekAllSet() visited := c.visitedURLs.PeekAllSet()
doLiveReload := !c.h.buildWatch && !c.Cfg.GetBool("disableLiveReload") doLiveReload := !c.h.buildWatch && !c.Cfg.GetBool("disableLiveReload")
if doLiveReload && !c.Cfg.GetBool("disableFastRender") { if doLiveReload && !c.Cfg.GetBool("disableFastRender") {
@ -637,7 +644,7 @@ func (c *commandeer) fullRebuild() {
c.commandeerHugoState = &commandeerHugoState{} c.commandeerHugoState = &commandeerHugoState{}
err := c.loadConfig(true, true) err := c.loadConfig(true, true)
if err != nil { 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. // Set the processing on pause until the state is recovered.
c.paused = true c.paused = true
} else { } else {
@ -645,8 +652,9 @@ func (c *commandeer) fullRebuild() {
} }
if !c.paused { if !c.paused {
if err := c.buildSites(); err != nil { err := c.buildSites()
jww.ERROR.Println(err) if err != nil {
c.logger.ERROR.Println(err)
} else if !c.h.buildWatch && !c.Cfg.GetBool("disableLiveReload") { } else if !c.h.buildWatch && !c.Cfg.GetBool("disableLiveReload") {
livereload.ForceRefresh() livereload.ForceRefresh()
} }
@ -680,7 +688,7 @@ func (c *commandeer) newWatcher(dirList ...string) (*watcher.Batcher, error) {
configSet := make(map[string]bool) configSet := make(map[string]bool)
for _, configFile := range c.configFiles { 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) watcher.Add(configFile)
configSet[configFile] = true configSet[configFile] = true
} }
@ -689,235 +697,14 @@ func (c *commandeer) newWatcher(dirList ...string) (*watcher.Batcher, error) {
for { for {
select { select {
case evs := <-watcher.Events: case evs := <-watcher.Events:
for _, ev := range evs { c.handleEvents(watcher, staticSyncer, evs, configSet)
if configSet[ev.Name] { if c.showErrorInBrowser && c.errCount() > 0 {
if ev.Op&fsnotify.Chmod == fsnotify.Chmod { // Need to reload browser to show the error
continue livereload.ForceRefresh()
}
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
}
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
}
c.Logger.INFO.Println("Received System Events:", evs)
staticEvents := []fsnotify.Event{}
dynamicEvents := []fsnotify.Event{}
// 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
}
// Check for any symbolic directory mapping.
dir, name := filepath.Split(ev.Name)
contentMapped = c.hugo.ContentChanges.GetSymbolicLinkMappings(dir)
if len(contentMapped) == 0 {
filtered = append(filtered, ev)
continue
}
for _, mapped := range contentMapped {
mappedFilename := filepath.Join(mapped, name)
filtered = append(filtered, fsnotify.Event{Name: mappedFilename, Op: ev.Op})
}
}
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
}
// 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
}
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
}
// 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 staticSyncer.isStatic(ev.Name) {
staticEvents = append(staticEvents, ev)
} else {
dynamicEvents = append(dynamicEvents, ev)
}
}
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))
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
}
}
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(dynamicEvents) > 0 {
partitionedEvents := partitionDynamicEvents(
c.firstPathSpec().BaseFs.SourceFilesystems,
dynamicEvents)
doLiveReload := !c.h.buildWatch && !c.Cfg.GetBool("disableLiveReload")
onePageName := pickOneWriteOrCreatePath(partitionedEvents.ContentEvents)
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))
c.changeDetector.PrepareNew()
if err := c.rebuildSites(dynamicEvents); err != nil {
c.Logger.ERROR.Println("Failed to rebuild site:", 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.
continue
} 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)
}
}
if p != nil {
livereload.NavigateToPathForPort(p.RelPermalink(), p.Site.ServerPort())
} else {
livereload.ForceRefresh()
}
}
}
} }
case err := <-watcher.Errors: case err := <-watcher.Errors:
if err != nil { if err != nil {
c.Logger.ERROR.Println(err) c.logger.ERROR.Println("Error while watching:", err)
} }
} }
} }
@ -926,6 +713,245 @@ func (c *commandeer) newWatcher(dirList ...string) (*watcher.Batcher, error) {
return watcher, nil return watcher, nil
} }
func (c *commandeer) handleEvents(watcher *watcher.Batcher,
staticSyncer *staticSyncer,
evs []fsnotify.Event,
configSet map[string]bool) {
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.
return
}
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
}
c.logger.INFO.Println("Received System Events:", evs)
staticEvents := []fsnotify.Event{}
dynamicEvents := []fsnotify.Event{}
// 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
}
// Check for any symbolic directory mapping.
dir, name := filepath.Split(ev.Name)
contentMapped = c.hugo.ContentChanges.GetSymbolicLinkMappings(dir)
if len(contentMapped) == 0 {
filtered = append(filtered, ev)
continue
}
for _, mapped := range contentMapped {
mappedFilename := filepath.Join(mapped, name)
filtered = append(filtered, fsnotify.Event{Name: mappedFilename, Op: ev.Op})
}
}
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
}
// 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
}
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
}
// 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 staticSyncer.isStatic(ev.Name) {
staticEvents = append(staticEvents, ev)
} else {
dynamicEvents = append(dynamicEvents, ev)
}
}
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))
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 !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(dynamicEvents) > 0 {
partitionedEvents := partitionDynamicEvents(
c.firstPathSpec().BaseFs.SourceFilesystems,
dynamicEvents)
doLiveReload := !c.h.buildWatch && !c.Cfg.GetBool("disableLiveReload")
onePageName := pickOneWriteOrCreatePath(partitionedEvents.ContentEvents)
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))
c.changeDetector.PrepareNew()
if err := c.rebuildSites(dynamicEvents); err != nil {
c.buildErr = err
c.logger.ERROR.Printf("Rebuild failed: %s", err)
if !c.h.quiet && c.h.verbose {
herrors.PrintStackTrace(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)
}
}
if p != nil {
livereload.NavigateToPathForPort(p.RelPermalink(), p.Site.ServerPort())
} else {
livereload.ForceRefresh()
}
}
}
}
}
// dynamicEvents contains events that is considered dynamic, as in "not static". // dynamicEvents contains events that is considered dynamic, as in "not static".
// Both of these categories will trigger a new build, but the asset events // Both of these categories will trigger a new build, but the asset events
// does not fit into the "navigate to changed" logic. // does not fit into the "navigate to changed" logic.

View file

@ -16,10 +16,11 @@ package commands
import ( import (
"bytes" "bytes"
"errors" "errors"
"fmt"
"path/filepath" "path/filepath"
"strings" "strings"
_errors "github.com/pkg/errors"
"github.com/gohugoio/hugo/create" "github.com/gohugoio/hugo/create"
"github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugofs" "github.com/gohugoio/hugo/hugofs"
@ -92,7 +93,7 @@ func (n *newSiteCmd) doNewSite(fs *hugofs.Fs, basepath string, force bool) error
for _, dir := range dirs { for _, dir := range dirs {
if err := fs.Source.MkdirAll(dir, 0777); err != nil { if err := fs.Source.MkdirAll(dir, 0777); err != nil {
return fmt.Errorf("Failed to create dir: %s", err) return _errors.Wrap(err, "Failed to create dir")
} }
} }

View file

@ -14,6 +14,7 @@
package commands package commands
import ( import (
"bytes"
"fmt" "fmt"
"net" "net"
"net/http" "net/http"
@ -21,6 +22,7 @@ import (
"os" "os"
"os/signal" "os/signal"
"path/filepath" "path/filepath"
"regexp"
"runtime" "runtime"
"strconv" "strconv"
"strings" "strings"
@ -28,7 +30,10 @@ import (
"syscall" "syscall"
"time" "time"
"github.com/pkg/errors"
"github.com/gohugoio/hugo/livereload" "github.com/gohugoio/hugo/livereload"
"github.com/gohugoio/hugo/tpl"
"github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/config"
@ -52,7 +57,8 @@ type serverCmd struct {
serverWatch bool serverWatch bool
noHTTPCache bool noHTTPCache bool
disableFastRender bool disableFastRender bool
disableBrowserError bool
*baseBuilderCmd *baseBuilderCmd
} }
@ -93,6 +99,7 @@ of a second, you will be able to save and see your changes nearly instantly.`,
cc.cmd.Flags().BoolVar(&cc.navigateToChanged, "navigateToChanged", false, "navigate to changed content file on live browser reload") cc.cmd.Flags().BoolVar(&cc.navigateToChanged, "navigateToChanged", false, "navigate to changed content file on live browser reload")
cc.cmd.Flags().BoolVar(&cc.renderToDisk, "renderToDisk", false, "render to Destination path (default is render to memory & serve from there)") cc.cmd.Flags().BoolVar(&cc.renderToDisk, "renderToDisk", false, "render to Destination path (default is render to memory & serve from there)")
cc.cmd.Flags().BoolVar(&cc.disableFastRender, "disableFastRender", false, "enables full re-renders on changes") cc.cmd.Flags().BoolVar(&cc.disableFastRender, "disableFastRender", false, "enables full re-renders on changes")
cc.cmd.Flags().BoolVar(&cc.disableBrowserError, "disableBrowserError", false, "do not show build errors in the browser")
cc.cmd.Flags().String("memstats", "", "log memory usage to this file") cc.cmd.Flags().String("memstats", "", "log memory usage to this file")
cc.cmd.Flags().String("meminterval", "100ms", "interval to poll memory usage (requires --memstats), valid time units are \"ns\", \"us\" (or \"µs\"), \"ms\", \"s\", \"m\", \"h\".") cc.cmd.Flags().String("meminterval", "100ms", "interval to poll memory usage (requires --memstats), valid time units are \"ns\", \"us\" (or \"µs\"), \"ms\", \"s\", \"m\", \"h\".")
@ -142,6 +149,9 @@ func (sc *serverCmd) server(cmd *cobra.Command, args []string) error {
if cmd.Flags().Changed("disableFastRender") { if cmd.Flags().Changed("disableFastRender") {
c.Set("disableFastRender", sc.disableFastRender) c.Set("disableFastRender", sc.disableFastRender)
} }
if cmd.Flags().Changed("disableBrowserError") {
c.Set("disableBrowserError", sc.disableBrowserError)
}
if sc.serverWatch { if sc.serverWatch {
c.Set("watch", true) c.Set("watch", true)
} }
@ -176,7 +186,7 @@ func (sc *serverCmd) server(cmd *cobra.Command, args []string) error {
// port set explicitly by user -- he/she probably meant it! // port set explicitly by user -- he/she probably meant it!
err = newSystemErrorF("Server startup failed: %s", err) 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() sp, err := helpers.FindAvailablePort()
if err != nil { if err != nil {
err = newSystemError("Unable to find alternative port to use:", err) err = newSystemError("Unable to find alternative port to use:", err)
@ -223,7 +233,7 @@ func (sc *serverCmd) server(cmd *cobra.Command, args []string) error {
} }
if err := memStats(); err != nil { 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) c, err := initializeConfig(true, true, &sc.hugoBuilderCommon, sc, cfgInit)
@ -271,10 +281,11 @@ func (sc *serverCmd) server(cmd *cobra.Command, args []string) error {
} }
type fileServer struct { type fileServer struct {
baseURLs []string baseURLs []string
roots []string roots []string
c *commandeer errorTemplate tpl.Template
s *serverCmd c *commandeer
s *serverCmd
} }
func (f *fileServer) createEndpoint(i int) (*http.ServeMux, string, string, error) { func (f *fileServer) createEndpoint(i int) (*http.ServeMux, string, string, error) {
@ -301,27 +312,40 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, string, string, erro
httpFs := afero.NewHttpFs(f.c.destinationFs) httpFs := afero.NewHttpFs(f.c.destinationFs)
fs := filesOnlyFs{httpFs.Dir(absPublishDir)} fs := filesOnlyFs{httpFs.Dir(absPublishDir)}
doLiveReload := !f.s.buildWatch && !f.c.Cfg.GetBool("disableLiveReload") if i == 0 && f.c.fastRenderMode {
fastRenderMode := doLiveReload && !f.c.Cfg.GetBool("disableFastRender")
if i == 0 && fastRenderMode {
jww.FEEDBACK.Println("Running in Fast Render Mode. For full rebuilds on change: hugo server --disableFastRender") jww.FEEDBACK.Println("Running in Fast Render Mode. For full rebuilds on change: hugo server --disableFastRender")
} }
// We're only interested in the path // We're only interested in the path
u, err := url.Parse(baseURL) u, err := url.Parse(baseURL)
if err != nil { if err != nil {
return nil, "", "", fmt.Errorf("Invalid baseURL: %s", err) return nil, "", "", errors.Wrap(err, "Invalid baseURL")
} }
decorate := func(h http.Handler) http.Handler { decorate := func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if f.c.showErrorInBrowser {
// First check the error state
err := f.c.getErrorWithContext()
if err != nil {
w.WriteHeader(500)
var b bytes.Buffer
err := f.errorTemplate.Execute(&b, err)
if err != nil {
f.c.logger.ERROR.Println(err)
}
fmt.Fprint(w, injectLiveReloadScript(&b, f.c.Cfg.GetInt("liveReloadPort")))
return
}
}
if f.s.noHTTPCache { if f.s.noHTTPCache {
w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0") w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0")
w.Header().Set("Pragma", "no-cache") w.Header().Set("Pragma", "no-cache")
} }
if fastRenderMode { if f.c.fastRenderMode {
p := r.RequestURI p := r.RequestURI
if strings.HasSuffix(p, "/") || strings.HasSuffix(p, "html") || strings.HasSuffix(p, "htm") { if strings.HasSuffix(p, "/") || strings.HasSuffix(p, "html") || strings.HasSuffix(p, "htm") {
f.c.visitedURLs.Add(p) f.c.visitedURLs.Add(p)
@ -345,6 +369,11 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, string, string, erro
return mu, u.String(), endpoint, nil 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 { func (c *commandeer) serve(s *serverCmd) error {
isMultiHost := c.hugo.IsMultihost() isMultiHost := c.hugo.IsMultihost()
@ -365,11 +394,17 @@ func (c *commandeer) serve(s *serverCmd) error {
roots = []string{""} roots = []string{""}
} }
templ, err := c.hugo.TextTmpl.Parse("__default_server_error", buildErrorTemplate)
if err != nil {
return err
}
srv := &fileServer{ srv := &fileServer{
baseURLs: baseURLs, baseURLs: baseURLs,
roots: roots, roots: roots,
c: c, c: c,
s: s, s: s,
errorTemplate: templ,
} }
doLiveReload := !c.Cfg.GetBool("disableLiveReload") doLiveReload := !c.Cfg.GetBool("disableLiveReload")
@ -392,7 +427,7 @@ func (c *commandeer) serve(s *serverCmd) error {
go func() { go func() {
err = http.ListenAndServe(endpoint, mu) err = http.ListenAndServe(endpoint, mu)
if err != nil { if err != nil {
jww.ERROR.Printf("Error: %s\n", err.Error()) c.logger.ERROR.Printf("Error: %s\n", err.Error())
os.Exit(1) os.Exit(1)
} }
}() }()
@ -453,7 +488,7 @@ func (sc *serverCmd) fixURL(cfg config.Provider, s string, port int) (string, er
if strings.Contains(u.Host, ":") { if strings.Contains(u.Host, ":") {
u.Host, _, err = net.SplitHostPort(u.Host) u.Host, _, err = net.SplitHostPort(u.Host)
if err != nil { if err != nil {
return "", fmt.Errorf("Failed to split baseURL hostpost: %s", err) return "", errors.Wrap(err, "Failed to split baseURL hostpost")
} }
} }
u.Host += fmt.Sprintf(":%d", port) u.Host += fmt.Sprintf(":%d", port)

95
commands/server_errors.go Normal file
View file

@ -0,0 +1,95 @@
// 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 buildErrorTemplate = `<!doctype html>
<html class="no-js" lang="">
<head>
<meta charset="utf-8">
<title>Hugo Server: Error</title>
<style type="text/css">
body {
font-family: "Muli",avenir, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-size: 16px;
background-color: black;
color: rgba(255, 255, 255, 0.9);
}
main {
margin: auto;
width: 95%;
padding: 1rem;
}
.version {
color: #ccc;
padding: 1rem 0;
}
.stack {
margin-top: 6rem;
}
pre {
white-space: pre-wrap;
white-space: -moz-pre-wrap;
white-space: -pre-wrap;
white-space: -o-pre-wrap;
word-wrap: break-word;
}
.highlight {
overflow-x: scroll;
padding: 0.75rem;
margin-bottom: 1rem;
background-color: #272822;
border: 1px solid black;
}
a {
color: #0594cb;
text-decoration: none;
}
a:hover {
color: #ccc;
}
</style>
</head>
<body>
<main>
{{ highlight .Error "apl" "noclasses=true,style=monokai" }}
{{ with .File }}
{{ $params := printf "noclasses=true,style=monokai,linenos=table,hl_lines=%d,linenostart=%d" (add .Pos 1) .LineNumber }}
{{ $lexer := .ChromaLexer | default "go-html-template" }}
{{ highlight (delimit .Lines "\n") $lexer $params }}
{{ end }}
{{ with .StackTrace }}
{{ highlight . "apl" "noclasses=true,style=monokai" }}
{{ end }}
<p class="version">{{ .Version }}</p>
<a href="">Reload Page</a>
</main>
</body>
</html>
`
func injectLiveReloadScript(src io.Reader, port int) string {
var b bytes.Buffer
chain := transform.Chain{livereloadinject.New(port)}
chain.Apply(&b, src)
return b.String()
}

View file

@ -18,6 +18,7 @@ import (
"net/http" "net/http"
"os" "os"
"runtime" "runtime"
"strings"
"testing" "testing"
"time" "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 <partial "logo" .>: 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 { func isWindowsCI() bool {
return runtime.GOOS == "windows" && os.Getenv("CI") != "" return runtime.GOOS == "windows" && os.Getenv("CI") != ""
} }

View file

@ -105,10 +105,10 @@ func (s *staticSyncer) syncsStaticEvents(staticEvents []fsnotify.Event) error {
logger.Println("Syncing", relPath, "to", publishDir) logger.Println("Syncing", relPath, "to", publishDir)
if err := syncer.Sync(filepath.Join(publishDir, relPath), relPath); err != nil { if err := syncer.Sync(filepath.Join(publishDir, relPath), relPath); err != nil {
c.Logger.ERROR.Println(err) c.logger.ERROR.Println(err)
} }
} else { } else {
c.Logger.ERROR.Println(err) c.logger.ERROR.Println(err)
} }
continue continue
@ -117,7 +117,7 @@ func (s *staticSyncer) syncsStaticEvents(staticEvents []fsnotify.Event) error {
// For all other event operations Hugo will sync static. // For all other event operations Hugo will sync static.
logger.Println("Syncing", relPath, "to", publishDir) logger.Println("Syncing", relPath, "to", publishDir)
if err := syncer.Sync(filepath.Join(publishDir, relPath), relPath); err != nil { if err := syncer.Sync(filepath.Join(publishDir, relPath), relPath); err != nil {
c.Logger.ERROR.Println(err) c.logger.ERROR.Println(err)
} }
} }

View file

@ -14,14 +14,16 @@
package commands package commands
import ( import (
"fmt"
"runtime" "runtime"
"strings" "strings"
jww "github.com/spf13/jwalterweatherman"
"github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugolib" "github.com/gohugoio/hugo/hugolib"
"github.com/gohugoio/hugo/resource/tocss/scss" "github.com/gohugoio/hugo/resource/tocss/scss"
"github.com/spf13/cobra" "github.com/spf13/cobra"
jww "github.com/spf13/jwalterweatherman"
) )
var _ cmder = (*versionCmd)(nil) var _ cmder = (*versionCmd)(nil)
@ -45,6 +47,10 @@ func newVersionCmd() *versionCmd {
} }
func printHugoVersion() { func printHugoVersion() {
jww.FEEDBACK.Println(hugoVersionString())
}
func hugoVersionString() string {
program := "Hugo Static Site Generator" program := "Hugo Static Site Generator"
version := "v" + helpers.CurrentHugoVersion.String() version := "v" + helpers.CurrentHugoVersion.String()
@ -64,5 +70,6 @@ func printHugoVersion() {
buildDate = "unknown" buildDate = "unknown"
} }
jww.FEEDBACK.Println(program, version, osArch, "BuildDate:", buildDate) return fmt.Sprintf("%s %s %s BuildDate: %s", program, version, osArch, buildDate)
} }

View file

@ -0,0 +1,194 @@
// 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 errors contains common Hugo errors and error related utilities.
package herrors
import (
"bufio"
"io"
"strings"
"github.com/spf13/afero"
)
// LineMatcher is used to match a line with an error.
type LineMatcher func(le FileError, lineNumber int, line string) bool
// SimpleLineMatcher matches if the current line number matches the line number
// in the error.
var SimpleLineMatcher = func(le FileError, lineNumber int, line string) bool {
return le.LineNumber() == lineNumber
}
// ErrorContext contains contextual information about an error. This will
// typically be the lines surrounding some problem in a file.
type ErrorContext struct {
// If a match will contain the matched line and up to 2 lines before and after.
// Will be empty if no match.
Lines []string
// The position of the error in the Lines above. 0 based.
Pos int
// The linenumber in the source file from where the Lines start. Starting at 1.
LineNumber int
// The lexer to use for syntax highlighting.
// https://gohugo.io/content-management/syntax-highlighting/#list-of-chroma-highlighting-languages
ChromaLexer string
}
var _ causer = (*ErrorWithFileContext)(nil)
// ErrorWithFileContext is an error with some additional file context related
// to that error.
type ErrorWithFileContext struct {
cause error
ErrorContext
}
func (e *ErrorWithFileContext) Error() string {
return e.cause.Error()
}
func (e *ErrorWithFileContext) Cause() error {
return e.cause
}
// WithFileContextForFile will try to add a file context with lines matching the given matcher.
// If no match could be found, the original error is returned with false as the second return value.
func WithFileContextForFile(e error, filename string, fs afero.Fs, chromaLexer string, matcher LineMatcher) (error, bool) {
f, err := fs.Open(filename)
if err != nil {
return e, false
}
defer f.Close()
return WithFileContext(e, f, chromaLexer, matcher)
}
// WithFileContextForFile will try to add a file context with lines matching the given matcher.
// If no match could be found, the original error is returned with false as the second return value.
func WithFileContext(e error, r io.Reader, chromaLexer string, matcher LineMatcher) (error, bool) {
if e == nil {
panic("error missing")
}
le := UnwrapFileError(e)
if le == nil {
var ok bool
if le, ok = ToFileError("bash", e).(FileError); !ok {
return e, false
}
}
errCtx := locateError(r, le, matcher)
if errCtx.LineNumber == -1 {
return e, false
}
if chromaLexer != "" {
errCtx.ChromaLexer = chromaLexer
} else {
errCtx.ChromaLexer = chromaLexerFromType(le.Type())
}
return &ErrorWithFileContext{cause: e, ErrorContext: errCtx}, true
}
// UnwrapErrorWithFileContext tries to unwrap an ErrorWithFileContext from err.
// It returns nil if this is not possible.
func UnwrapErrorWithFileContext(err error) *ErrorWithFileContext {
for err != nil {
switch v := err.(type) {
case *ErrorWithFileContext:
return v
case causer:
err = v.Cause()
default:
return nil
}
}
return nil
}
func chromaLexerFromType(fileType string) string {
return fileType
}
func locateErrorInString(le FileError, src string, matcher LineMatcher) ErrorContext {
return locateError(strings.NewReader(src), nil, matcher)
}
func locateError(r io.Reader, le FileError, matches LineMatcher) ErrorContext {
var errCtx ErrorContext
s := bufio.NewScanner(r)
lineNo := 0
var buff [6]string
i := 0
errCtx.Pos = -1
for s.Scan() {
lineNo++
txt := s.Text()
buff[i] = txt
if errCtx.Pos != -1 && i >= 5 {
break
}
if errCtx.Pos == -1 && matches(le, lineNo, txt) {
errCtx.Pos = i
errCtx.LineNumber = lineNo - i
}
if errCtx.Pos == -1 && i == 2 {
// Shift left
buff[0], buff[1] = buff[i-1], buff[i]
} else {
i++
}
}
// Go's template parser will typically report "unexpected EOF" errors on the
// empty last line that is supressed by the scanner.
// Do an explicit check for that.
if errCtx.Pos == -1 {
lineNo++
if matches(le, lineNo, "") {
buff[i] = ""
errCtx.Pos = i
errCtx.LineNumber = lineNo - 1
i++
}
}
if errCtx.Pos != -1 {
low := errCtx.Pos - 2
if low < 0 {
low = 0
}
high := i
errCtx.Lines = buff[low:high]
} else {
errCtx.Pos = -1
errCtx.LineNumber = -1
}
return errCtx
}

View file

@ -0,0 +1,112 @@
// 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 errors contains common Hugo errors and error related utilities.
package herrors
import (
"strings"
"testing"
"github.com/stretchr/testify/require"
)
func TestErrorLocator(t *testing.T) {
assert := require.New(t)
lineMatcher := func(le FileError, lineno int, line string) bool {
return strings.Contains(line, "THEONE")
}
lines := `LINE 1
LINE 2
LINE 3
LINE 4
This is THEONE
LINE 6
LINE 7
LINE 8
`
location := locateErrorInString(nil, lines, lineMatcher)
assert.Equal([]string{"LINE 3", "LINE 4", "This is THEONE", "LINE 6", "LINE 7"}, location.Lines)
assert.Equal(3, location.LineNumber)
assert.Equal(2, location.Pos)
assert.Equal([]string{"This is THEONE"}, locateErrorInString(nil, `This is THEONE`, lineMatcher).Lines)
location = locateErrorInString(nil, `L1
This is THEONE
L2
`, lineMatcher)
assert.Equal(1, location.Pos)
assert.Equal([]string{"L1", "This is THEONE", "L2"}, location.Lines)
location = locateErrorInString(nil, `This is THEONE
L2
`, lineMatcher)
assert.Equal(0, location.Pos)
assert.Equal([]string{"This is THEONE", "L2"}, location.Lines)
location = locateErrorInString(nil, `L1
This THEONE
`, lineMatcher)
assert.Equal([]string{"L1", "This THEONE"}, location.Lines)
assert.Equal(1, location.Pos)
location = locateErrorInString(nil, `L1
L2
This THEONE
`, lineMatcher)
assert.Equal([]string{"L1", "L2", "This THEONE"}, location.Lines)
assert.Equal(2, location.Pos)
location = locateErrorInString(nil, "NO MATCH", lineMatcher)
assert.Equal(-1, location.LineNumber)
assert.Equal(-1, location.Pos)
assert.Equal(0, len(location.Lines))
lineMatcher = func(le FileError, lineno int, line string) bool {
return lineno == 6
}
location = locateErrorInString(nil, `A
B
C
D
E
F
G
H
I
J`, lineMatcher)
assert.Equal([]string{"D", "E", "F", "G", "H"}, location.Lines)
assert.Equal(4, location.LineNumber)
assert.Equal(2, location.Pos)
// Test match EOF
lineMatcher = func(le FileError, lineno int, line string) bool {
return lineno == 4
}
location = locateErrorInString(nil, `A
B
C
`, lineMatcher)
assert.Equal([]string{"B", "C", ""}, location.Lines)
assert.Equal(3, location.LineNumber)
assert.Equal(2, location.Pos)
}

View file

@ -11,13 +11,41 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
// Package errors contains common Hugo errors and error related utilities. // Package herrors contains common Hugo errors and error related utilities.
package errors package herrors
import ( import (
"errors" "errors"
"fmt"
"io"
"os"
_errors "github.com/pkg/errors"
) )
// As defined in https://godoc.org/github.com/pkg/errors
type causer interface {
Cause() error
}
type stackTracer interface {
StackTrace() _errors.StackTrace
}
// PrintStackTrace prints the error's stack trace to stdoud.
func PrintStackTrace(err error) {
FprintStackTrace(os.Stdout, err)
}
// FprintStackTrace prints the error's stack trace to w.
func FprintStackTrace(w io.Writer, err error) {
if err, ok := err.(stackTracer); ok {
for _, f := range err.StackTrace() {
fmt.Fprintf(w, "%+s:%d\n", f, f)
}
}
}
// ErrFeatureNotAvailable denotes that a feature is unavailable. // ErrFeatureNotAvailable denotes that a feature is unavailable.
// //
// We will, at least to begin with, make some Hugo features (SCSS with libsass) optional, // We will, at least to begin with, make some Hugo features (SCSS with libsass) optional,

View file

@ -0,0 +1,111 @@
// 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
// limitatio ns under the License.
package herrors
import (
"fmt"
)
var _ causer = (*fileError)(nil)
// FileError represents an error when handling a file: Parsing a config file,
// execute a template etc.
type FileError interface {
error
// LineNumber gets the error location, starting at line 1.
LineNumber() int
// A string identifying the type of file, e.g. JSON, TOML, markdown etc.
Type() string
}
var _ FileError = (*fileError)(nil)
type fileError struct {
lineNumber int
fileType string
msg string
cause error
}
func (e *fileError) LineNumber() int {
return e.lineNumber
}
func (e *fileError) Type() string {
return e.fileType
}
func (e *fileError) Error() string {
return e.msg
}
func (f *fileError) Cause() error {
return f.cause
}
func (e *fileError) Format(s fmt.State, verb rune) {
switch verb {
case 'v':
fallthrough
case 's':
fmt.Fprintf(s, "%s:%d: %s:%s", e.fileType, e.lineNumber, e.msg, e.cause)
case 'q':
fmt.Fprintf(s, "%q:%d: %q:%q", e.fileType, e.lineNumber, e.msg, e.cause)
}
}
// NewFileError creates a new FileError.
func NewFileError(fileType string, lineNumber int, msg string, err error) FileError {
return &fileError{cause: err, fileType: fileType, lineNumber: lineNumber, msg: msg}
}
// UnwrapFileError tries to unwrap a FileError from err.
// It returns nil if this is not possible.
func UnwrapFileError(err error) FileError {
for err != nil {
switch v := err.(type) {
case FileError:
return v
case causer:
err = v.Cause()
default:
return nil
}
}
return nil
}
// ToFileError will try to convert the given error to an error supporting
// the FileError interface.
// If will fall back to returning the original error if a line number cannot be extracted.
func ToFileError(fileType string, err error) error {
return ToFileErrorWithOffset(fileType, err, 0)
}
// ToFileErrorWithOffset will try to convert the given error to an error supporting
// the FileError interface. It will take any line number offset given into account.
// If will fall back to returning the original error if a line number cannot be extracted.
func ToFileErrorWithOffset(fileType string, err error, offset int) error {
for _, handle := range lineNumberExtractors {
lno, msg := handle(err, offset)
if lno > 0 {
return NewFileError(fileType, lno, msg, err)
}
}
// Fall back to the original.
return err
}

View file

@ -0,0 +1,56 @@
// 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 herrors
import (
"errors"
"fmt"
"strconv"
"testing"
"github.com/stretchr/testify/require"
)
func TestToLineNumberError(t *testing.T) {
t.Parallel()
assert := require.New(t)
for i, test := range []struct {
in error
offset int
lineNumber int
}{
{errors.New("no line number for you"), 0, -1},
{errors.New(`template: _default/single.html:2:15: executing "_default/single.html" at <.Titles>: can't evaluate field`), 0, 2},
{errors.New("parse failed: template: _default/bundle-resource-meta.html:11: unexpected in operand"), 0, 11},
{errors.New(`failed:: template: _default/bundle-resource-meta.html:2:7: executing "main" at <.Titles>`), 0, 2},
{errors.New("error in front matter: Near line 32 (last key parsed 'title')"), 0, 32},
{errors.New("error in front matter: Near line 32 (last key parsed 'title')"), 2, 34},
} {
got := ToFileErrorWithOffset("template", test.in, test.offset)
errMsg := fmt.Sprintf("[%d][%T]", i, got)
le, ok := got.(FileError)
if test.lineNumber > 0 {
assert.True(ok)
assert.Equal(test.lineNumber, le.LineNumber(), errMsg)
assert.Contains(got.Error(), strconv.Itoa(le.LineNumber()))
} else {
assert.False(ok)
}
}
}

View file

@ -0,0 +1,59 @@
// Copyright 2018 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitatio ns under the License.
package herrors
import (
"fmt"
"regexp"
"strconv"
)
var lineNumberExtractors = []lineNumberExtractor{
// Template/shortcode parse errors
newLineNumberErrHandlerFromRegexp("(.*?:)(\\d+)(:.*)"),
// TOML parse errors
newLineNumberErrHandlerFromRegexp("(.*Near line )(\\d+)(\\s.*)"),
// YAML parse errors
newLineNumberErrHandlerFromRegexp("(line )(\\d+)(:)"),
}
type lineNumberExtractor func(e error, offset int) (int, string)
func newLineNumberErrHandlerFromRegexp(expression string) lineNumberExtractor {
re := regexp.MustCompile(expression)
return extractLineNo(re)
}
func extractLineNo(re *regexp.Regexp) lineNumberExtractor {
return func(e error, offset int) (int, string) {
if e == nil {
panic("no error")
}
s := e.Error()
m := re.FindStringSubmatch(s)
if len(m) == 4 {
i, _ := strconv.Atoi(m[2])
msg := e.Error()
if offset != 0 {
i = i + offset
msg = re.ReplaceAllString(s, fmt.Sprintf("${1}%d${3}", i))
}
return i, msg
}
return -1, ""
}
}

View file

@ -14,6 +14,8 @@
package loggers package loggers
import ( import (
"bytes"
"io"
"io/ioutil" "io/ioutil"
"log" "log"
"os" "os"
@ -21,17 +23,78 @@ import (
jww "github.com/spf13/jwalterweatherman" 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. // NewDebugLogger is a convenience function to create a debug logger.
func NewDebugLogger() *jww.Notepad { func NewDebugLogger() *Logger {
return jww.NewNotepad(jww.LevelDebug, jww.LevelError, os.Stdout, ioutil.Discard, "", log.Ldate|log.Ltime) return newBasicLogger(jww.LevelDebug)
} }
// NewWarningLogger is a convenience function to create a warning logger. // NewWarningLogger is a convenience function to create a warning logger.
func NewWarningLogger() *jww.Notepad { func NewWarningLogger() *Logger {
return jww.NewNotepad(jww.LevelWarn, jww.LevelError, os.Stdout, ioutil.Discard, "", log.Ldate|log.Ltime) return newBasicLogger(jww.LevelWarn)
} }
// NewErrorLogger is a convenience function to create an error logger. // NewErrorLogger is a convenience function to create an error logger.
func NewErrorLogger() *jww.Notepad { func NewErrorLogger() *Logger {
return jww.NewNotepad(jww.LevelError, jww.LevelError, os.Stdout, ioutil.Discard, "", log.Ldate|log.Ltime) 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)
} }

View file

@ -16,7 +16,9 @@ package create
import ( import (
"bytes" "bytes"
"fmt"
"github.com/pkg/errors"
"io" "io"
"os" "os"
"os/exec" "os/exec"
@ -135,7 +137,7 @@ func newContentFromDir(
targetDir := filepath.Dir(targetFilename) targetDir := filepath.Dir(targetFilename)
if err := targetFs.MkdirAll(targetDir, 0777); err != nil && !os.IsExist(err) { if err := targetFs.MkdirAll(targetDir, 0777); err != nil && !os.IsExist(err) {
return fmt.Errorf("failed to create target directory for %s: %s", targetDir, err) return errors.Wrapf(err, "failed to create target directory for %s:", targetDir)
} }
out, err := targetFs.Create(targetFilename) out, err := targetFs.Create(targetFilename)
@ -223,7 +225,7 @@ func mapArcheTypeDir(
func usesSiteVar(fs afero.Fs, filename string) (bool, error) { func usesSiteVar(fs afero.Fs, filename string) (bool, error) {
f, err := fs.Open(filename) f, err := fs.Open(filename)
if err != nil { if err != nil {
return false, fmt.Errorf("failed to open archetype file: %s", err) return false, errors.Wrap(err, "failed to open archetype file")
} }
defer f.Close() defer f.Close()
return helpers.ReaderContains(f, []byte(".Site")), nil return helpers.ReaderContains(f, []byte(".Site")), nil

View file

@ -20,6 +20,8 @@ import (
"strings" "strings"
"time" "time"
"github.com/pkg/errors"
"github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/source" "github.com/gohugoio/hugo/source"
@ -127,14 +129,14 @@ func executeArcheTypeAsTemplate(s *hugolib.Site, name, kind, targetPath, archety
templateHandler := s.Deps.Tmpl.(tpl.TemplateHandler) templateHandler := s.Deps.Tmpl.(tpl.TemplateHandler)
templateName := "_text/" + helpers.Filename(archetypeFilename) templateName := "_text/" + helpers.Filename(archetypeFilename)
if err := templateHandler.AddTemplate(templateName, string(archetypeTemplate)); err != nil { if err := templateHandler.AddTemplate(templateName, string(archetypeTemplate)); err != nil {
return nil, fmt.Errorf("Failed to parse archetype file %q: %s", archetypeFilename, err) return nil, errors.Wrapf(err, "Failed to parse archetype file %q:", archetypeFilename)
} }
templ, _ := templateHandler.Lookup(templateName) templ, _ := templateHandler.Lookup(templateName)
var buff bytes.Buffer var buff bytes.Buffer
if err := templ.Execute(&buff, data); err != nil { if err := templ.Execute(&buff, data); err != nil {
return nil, fmt.Errorf("Failed to process archetype file %q: %s", archetypeFilename, err) return nil, errors.Wrapf(err, "Failed to process archetype file %q:", archetypeFilename)
} }
archetypeContent = []byte(archetypeShortcodeReplacementsPost.Replace(buff.String())) archetypeContent = []byte(archetypeShortcodeReplacementsPost.Replace(buff.String()))

9
deps/deps.go vendored
View file

@ -16,7 +16,6 @@ import (
"github.com/gohugoio/hugo/resource" "github.com/gohugoio/hugo/resource"
"github.com/gohugoio/hugo/source" "github.com/gohugoio/hugo/source"
"github.com/gohugoio/hugo/tpl" "github.com/gohugoio/hugo/tpl"
jww "github.com/spf13/jwalterweatherman"
) )
// Deps holds dependencies used by many. // Deps holds dependencies used by many.
@ -25,7 +24,7 @@ import (
type Deps struct { type Deps struct {
// The logger to use. // The logger to use.
Log *jww.Notepad `json:"-"` Log *loggers.Logger `json:"-"`
// Used to log errors that may repeat itself many times. // Used to log errors that may repeat itself many times.
DistinctErrorLog *helpers.DistinctLogger DistinctErrorLog *helpers.DistinctLogger
@ -122,10 +121,6 @@ func (d *Deps) LoadResources() error {
return err return err
} }
if th, ok := d.Tmpl.(tpl.TemplateHandler); ok {
th.PrintErrors()
}
return nil return nil
} }
@ -256,7 +251,7 @@ func (d Deps) ForLanguage(cfg DepsCfg) (*Deps, error) {
type DepsCfg struct { type DepsCfg struct {
// The Logger to use. // The Logger to use.
Logger *jww.Notepad Logger *loggers.Logger
// The file systems to use // The file systems to use
Fs *hugofs.Fs Fs *hugofs.Fs

5
go.mod
View file

@ -38,7 +38,7 @@ require (
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
github.com/nicksnyder/go-i18n v1.10.0 github.com/nicksnyder/go-i18n v1.10.0
github.com/olekukonko/tablewriter v0.0.0-20180506121414-d4647c9c7a84 github.com/olekukonko/tablewriter v0.0.0-20180506121414-d4647c9c7a84
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pkg/errors v0.8.0
github.com/russross/blackfriday v0.0.0-20180804101149-46c73eb196ba github.com/russross/blackfriday v0.0.0-20180804101149-46c73eb196ba
github.com/sanity-io/litter v1.1.0 github.com/sanity-io/litter v1.1.0
github.com/sergi/go-diff v1.0.0 // indirect github.com/sergi/go-diff v1.0.0 // indirect
@ -47,7 +47,7 @@ require (
github.com/spf13/cast v1.2.0 github.com/spf13/cast v1.2.0
github.com/spf13/cobra v0.0.3 github.com/spf13/cobra v0.0.3
github.com/spf13/fsync v0.0.0-20170320142552-12a01e648f05 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/nitro v0.0.0-20131003134307-24d7ef30a12d
github.com/spf13/pflag v1.0.2 github.com/spf13/pflag v1.0.2
github.com/spf13/viper v1.2.0 github.com/spf13/viper v1.2.0
@ -60,6 +60,7 @@ require (
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81 golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd // indirect golang.org/x/net v0.0.0-20180906233101-161cd47e91fd // indirect
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e // indirect
golang.org/x/text v0.3.0 golang.org/x/text v0.3.0
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
gopkg.in/yaml.v2 v2.2.1 gopkg.in/yaml.v2 v2.2.1

6
go.sum
View file

@ -87,6 +87,8 @@ github.com/olekukonko/tablewriter v0.0.0-20180506121414-d4647c9c7a84 h1:fiKJgB4J
github.com/olekukonko/tablewriter v0.0.0-20180506121414-d4647c9c7a84/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= github.com/olekukonko/tablewriter v0.0.0-20180506121414-d4647c9c7a84/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo=
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday v0.0.0-20180804101149-46c73eb196ba h1:8Vzt8HxRjy7hp1eqPKVoAEPK9npQFW2510qlobGzvi0= github.com/russross/blackfriday v0.0.0-20180804101149-46c73eb196ba h1:8Vzt8HxRjy7hp1eqPKVoAEPK9npQFW2510qlobGzvi0=
@ -107,6 +109,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/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 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= 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 h1:ihvj2nmx8eqWjlgNgdW6h0DyGJuq5GiwHadJkG0wXtQ=
github.com/spf13/nitro v0.0.0-20131003134307-24d7ef30a12d/go.mod h1:jU8A+8xL+6n1OX4XaZtCj4B3mIa64tULUsD6YegdpFo= github.com/spf13/nitro v0.0.0-20131003134307-24d7ef30a12d/go.mod h1:jU8A+8xL+6n1OX4XaZtCj4B3mIa64tULUsD6YegdpFo=
github.com/spf13/pflag v1.0.2 h1:Fy0orTDgHdbnzHcsOgfCN4LtHf0ec3wwtiwJqwvf3Gc= github.com/spf13/pflag v1.0.2 h1:Fy0orTDgHdbnzHcsOgfCN4LtHf0ec3wwtiwJqwvf3Gc=
@ -133,6 +137,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/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 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-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 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View file

@ -25,6 +25,7 @@ import (
"unicode" "unicode"
"github.com/gohugoio/hugo/common/hugio" "github.com/gohugoio/hugo/common/hugio"
_errors "github.com/pkg/errors"
"github.com/spf13/afero" "github.com/spf13/afero"
"golang.org/x/text/transform" "golang.org/x/text/transform"
"golang.org/x/text/unicode/norm" "golang.org/x/text/unicode/norm"
@ -493,11 +494,11 @@ func getRealFileInfo(fs afero.Fs, path string) (os.FileInfo, string, error) {
if fileInfo.Mode()&os.ModeSymlink == os.ModeSymlink { if fileInfo.Mode()&os.ModeSymlink == os.ModeSymlink {
link, err := filepath.EvalSymlinks(path) link, err := filepath.EvalSymlinks(path)
if err != nil { if err != nil {
return nil, "", fmt.Errorf("Cannot read symbolic link '%s', error was: %s", path, err) return nil, "", _errors.Wrapf(err, "Cannot read symbolic link %q", path)
} }
fileInfo, err = LstatIfPossible(fs, link) fileInfo, err = LstatIfPossible(fs, link)
if err != nil { if err != nil {
return nil, "", fmt.Errorf("Cannot stat '%s', error was: %s", link, err) return nil, "", _errors.Wrapf(err, "Cannot stat %q", link)
} }
realPath = link realPath = link
} }

View file

@ -22,12 +22,12 @@ import (
"runtime" "runtime"
"strings" "strings"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/output" "github.com/gohugoio/hugo/output"
"github.com/gohugoio/hugo/publisher" "github.com/gohugoio/hugo/publisher"
"github.com/gohugoio/hugo/tpl" "github.com/gohugoio/hugo/tpl"
jww "github.com/spf13/jwalterweatherman"
"github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/helpers"
) )
@ -47,11 +47,11 @@ func init() {
type aliasHandler struct { type aliasHandler struct {
t tpl.TemplateFinder t tpl.TemplateFinder
log *jww.Notepad log *loggers.Logger
allowRoot bool 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} return aliasHandler{t, l, allowRoot}
} }

View file

@ -17,11 +17,12 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/gohugoio/hugo/hugolib/paths"
"io" "io"
"strings" "strings"
"github.com/gohugoio/hugo/hugolib/paths"
_errors "github.com/pkg/errors"
"github.com/gohugoio/hugo/langs" "github.com/gohugoio/hugo/langs"
"github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/config"
@ -205,7 +206,7 @@ func loadLanguageSettings(cfg config.Provider, oldLangs langs.Languages) error {
} else { } else {
languages2, err = toSortedLanguages(cfg, languages) languages2, err = toSortedLanguages(cfg, languages)
if err != nil { if err != nil {
return fmt.Errorf("Failed to parse multilingual config: %s", err) return _errors.Wrap(err, "Failed to parse multilingual config")
} }
} }

View file

@ -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) { if !expectBuildError && !reflect.DeepEqual(expected, s.Data) {
// This disabled code detects the situation described in the WARNING message below. // This disabled code detects the situation described in the WARNING message below.

View file

@ -54,6 +54,9 @@ func (fi *fileInfo) Lang() string {
} }
func (fi *fileInfo) Filename() string { func (fi *fileInfo) Filename() string {
if fi == nil || fi.basePather == nil {
return ""
}
return fi.basePather.Filename() return fi.basePather.Filename()
} }

View file

@ -21,6 +21,7 @@ import (
"strings" "strings"
"sync" "sync"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/deps"
"github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/langs" "github.com/gohugoio/hugo/langs"
@ -29,7 +30,6 @@ import (
"github.com/gohugoio/hugo/i18n" "github.com/gohugoio/hugo/i18n"
"github.com/gohugoio/hugo/tpl" "github.com/gohugoio/hugo/tpl"
"github.com/gohugoio/hugo/tpl/tplimpl" "github.com/gohugoio/hugo/tpl/tplimpl"
jww "github.com/spf13/jwalterweatherman"
) )
// HugoSites represents the sites to build. Each site represents a language. // HugoSites represents the sites to build. Each site represents a language.
@ -69,7 +69,7 @@ func (h *HugoSites) NumLogErrors() int {
if h == nil { if h == nil {
return 0 return 0
} }
return int(h.Log.LogCountForLevelsGreaterThanorEqualTo(jww.LevelError)) return int(h.Log.ErrorCounter.Count())
} }
func (h *HugoSites) PrintProcessingStats(w io.Writer) { 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 { func (s *Site) withSiteTemplates(withTemplates ...func(templ tpl.TemplateHandler) error) func(templ tpl.TemplateHandler) error {
return 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 { for _, wt := range withTemplates {
if wt == nil { 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. // resetLogs resets the log counters etc. Used to do a new build on the same sites.
func (h *HugoSites) resetLogs() { func (h *HugoSites) resetLogs() {
h.Log.ResetLogCounters() h.Log.Reset()
loggers.GlobalErrorCounter.Reset()
for _, s := range h.Sites { for _, s := range h.Sites {
s.Deps.DistinctErrorLog = helpers.NewDistinctLogger(h.Log.ERROR) s.Deps.DistinctErrorLog = helpers.NewDistinctLogger(h.Log.ERROR)
} }

View file

@ -19,8 +19,6 @@ import (
"errors" "errors"
jww "github.com/spf13/jwalterweatherman"
"github.com/fsnotify/fsnotify" "github.com/fsnotify/fsnotify"
"github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/helpers"
) )
@ -79,7 +77,7 @@ func (h *HugoSites) Build(config BuildCfg, events ...fsnotify.Event) error {
h.Log.FEEDBACK.Println() h.Log.FEEDBACK.Println()
} }
errorCount := h.Log.LogCountForLevel(jww.LevelError) errorCount := h.Log.ErrorCounter.Count()
if errorCount > 0 { if errorCount > 0 {
return fmt.Errorf("logged %d error(s)", errorCount) return fmt.Errorf("logged %d error(s)", errorCount)
} }

View file

@ -0,0 +1,182 @@
package hugolib
import (
"fmt"
"strings"
"testing"
"github.com/gohugoio/hugo/common/herrors"
"github.com/stretchr/testify/require"
)
type testSiteBuildErrorAsserter struct {
name string
assert *require.Assertions
}
func (t testSiteBuildErrorAsserter) getFileError(err error) *herrors.ErrorWithFileContext {
t.assert.NotNil(err, t.name)
ferr := herrors.UnwrapErrorWithFileContext(err)
t.assert.NotNil(ferr, fmt.Sprintf("[%s] got %T: %+v", t.name, err, err))
return ferr
}
func (t testSiteBuildErrorAsserter) assertLineNumber(lineNumber int, err error) {
fe := t.getFileError(err)
t.assert.Equal(lineNumber, fe.LineNumber, fmt.Sprintf("[%s] got => %s", t.name, fe))
}
func TestSiteBuildErrors(t *testing.T) {
t.Parallel()
assert := require.New(t)
const (
yamlcontent = "yamlcontent"
shortcode = "shortcode"
base = "base"
single = "single"
)
// TODO(bep) add content tests after https://github.com/gohugoio/hugo/issues/5324
// is implemented.
tests := []struct {
name string
fileType string
fileFixer func(content string) string
assertCreateError func(a testSiteBuildErrorAsserter, err error)
assertBuildError func(a testSiteBuildErrorAsserter, err error)
}{
{
name: "Base template parse failed",
fileType: base,
fileFixer: func(content string) string {
return strings.Replace(content, ".Title }}", ".Title }", 1)
},
assertCreateError: func(a testSiteBuildErrorAsserter, err error) {
a.assertLineNumber(2, err)
},
},
{
name: "Base template execute failed",
fileType: base,
fileFixer: func(content string) string {
return strings.Replace(content, ".Title", ".Titles", 1)
},
assertBuildError: func(a testSiteBuildErrorAsserter, err error) {
a.assertLineNumber(2, err)
},
},
{
name: "Single template parse failed",
fileType: single,
fileFixer: func(content string) string {
return strings.Replace(content, ".Title }}", ".Title }", 1)
},
assertCreateError: func(a testSiteBuildErrorAsserter, err error) {
a.assertLineNumber(3, err)
},
},
{
name: "Single template execute failed",
fileType: single,
fileFixer: func(content string) string {
return strings.Replace(content, ".Title", ".Titles", 1)
},
assertBuildError: func(a testSiteBuildErrorAsserter, err error) {
a.assertLineNumber(3, err)
},
},
{
name: "Shortcode parse failed",
fileType: shortcode,
fileFixer: func(content string) string {
return strings.Replace(content, ".Title }}", ".Title }", 1)
},
assertCreateError: func(a testSiteBuildErrorAsserter, err error) {
a.assertLineNumber(2, err)
},
},
// TODO(bep) 2errors
/* {
name: "Shortode execute failed",
fileType: shortcode,
fileFixer: func(content string) string {
return strings.Replace(content, ".Title", ".Titles", 1)
},
assertBuildError: func(a testSiteBuildErrorAsserter, err error) {
a.assertLineNumber(2, err)
},
},*/
}
for _, test := range tests {
errorAsserter := testSiteBuildErrorAsserter{
assert: assert,
name: test.name,
}
b := newTestSitesBuilder(t).WithSimpleConfigFile()
f := func(fileType, content string) string {
if fileType != test.fileType {
return content
}
return test.fileFixer(content)
}
b.WithTemplatesAdded("layouts/shortcodes/sc.html", f(shortcode, `SHORTCODE L1
SHORTCODE L2
SHORTCODE L3:
SHORTCODE L4: {{ .Page.Title }}
`))
b.WithTemplatesAdded("layouts/_default/baseof.html", f(base, `BASEOF L1
BASEOF L2
BASEOF L3
BASEOF L4{{ if .Title }}{{ end }}
{{block "main" .}}This is the main content.{{end}}
BASEOF L6
`))
b.WithTemplatesAdded("layouts/_default/single.html", f(single, `{{ define "main" }}
SINGLE L2:
SINGLE L3:
SINGLE L4:
SINGLE L5: {{ .Title }} {{ .Content }}
{{ end }}
`))
b.WithContent("myyaml.md", f(yamlcontent, `---
title: "The YAML"
---
Some content.
{{< sc >}}
Some more text.
The end.
`))
createErr := b.CreateSitesE()
if test.assertCreateError != nil {
test.assertCreateError(errorAsserter, createErr)
} else {
assert.NoError(createErr)
}
if createErr == nil {
buildErr := b.BuildE(BuildCfg{})
if test.assertBuildError != nil {
test.assertBuildError(errorAsserter, buildErr)
} else {
assert.NoError(buildErr)
}
}
}
}

View file

@ -1,42 +0,0 @@
package hugolib
import (
"fmt"
"testing"
)
// https://github.com/gohugoio/hugo/issues/4526
func TestSiteBuildFailureInvalidPageMetadata(t *testing.T) {
t.Parallel()
validContentFile := `
---
title = "This is good"
---
Some content.
`
invalidContentFile := `
---
title = "PDF EPUB: Anne Bradstreet: Poems "The Prologue Summary And Analysis EBook Full Text "
---
Some content.
`
var contentFiles []string
for i := 0; i <= 30; i++ {
name := fmt.Sprintf("valid%d.md", i)
contentFiles = append(contentFiles, name, validContentFile)
if i%5 == 0 {
name = fmt.Sprintf("invalid%d.md", i)
contentFiles = append(contentFiles, name, invalidContentFile)
}
}
b := newTestSitesBuilder(t)
b.WithSimpleConfigFile().WithContent(contentFiles...)
b.CreateSites().BuildFail(BuildCfg{})
}

View file

@ -22,6 +22,7 @@ import (
"unicode" "unicode"
"github.com/gohugoio/hugo/media" "github.com/gohugoio/hugo/media"
_errors "github.com/pkg/errors"
"github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/common/maps"
@ -307,13 +308,13 @@ func (p *Page) initContent() {
err = p.prepareForRender() err = p.prepareForRender()
if err != nil { if err != nil {
p.s.Log.ERROR.Printf("Failed to prepare page %q for render: %s", p.Path(), err) c <- err
return return
} }
if len(p.summary) == 0 { if len(p.summary) == 0 {
if err = p.setAutoSummary(); err != nil { if err = p.setAutoSummary(); err != nil {
err = fmt.Errorf("Failed to set user auto summary for page %q: %s", p.pathOrTitle(), err) err = _errors.Wrapf(err, "Failed to set user auto summary for page %q:", p.pathOrTitle())
} }
} }
c <- err c <- err
@ -324,11 +325,11 @@ func (p *Page) initContent() {
p.s.Log.WARN.Printf("WARNING: Timed out creating content for page %q (.Content will be empty). This is most likely a circular shortcode content loop that should be fixed. If this is just a shortcode calling a slow remote service, try to set \"timeout=20000\" (or higher, value is in milliseconds) in config.toml.\n", p.pathOrTitle()) p.s.Log.WARN.Printf("WARNING: Timed out creating content for page %q (.Content will be empty). This is most likely a circular shortcode content loop that should be fixed. If this is just a shortcode calling a slow remote service, try to set \"timeout=20000\" (or higher, value is in milliseconds) in config.toml.\n", p.pathOrTitle())
case err := <-c: case err := <-c:
if err != nil { if err != nil {
// TODO(bep) 2errors needs to be transported to the caller.
p.s.Log.ERROR.Println(err) p.s.Log.ERROR.Println(err)
} }
} }
}) })
} }
// This is sent to the shortcodes for this page. Not doing that will create an infinite regress. So, // This is sent to the shortcodes for this page. Not doing that will create an infinite regress. So,
@ -989,11 +990,20 @@ func (s *Site) NewPage(name string) (*Page, error) {
return p, nil return p, nil
} }
func (p *Page) errorf(err error, format string, a ...interface{}) error {
args := append([]interface{}{p.Lang(), p.pathOrTitle()}, a...)
format = "[%s] Page %q: " + format
if err == nil {
return fmt.Errorf(format, args...)
}
return _errors.Wrapf(err, format, args...)
}
func (p *Page) ReadFrom(buf io.Reader) (int64, error) { func (p *Page) ReadFrom(buf io.Reader) (int64, error) {
// Parse for metadata & body // Parse for metadata & body
if err := p.parse(buf); err != nil { if err := p.parse(buf); err != nil {
p.s.Log.ERROR.Printf("%s for %s", err, p.File.Path()) return 0, p.errorf(err, "parse failed")
return 0, err
} }
return int64(len(p.rawContent)), nil return int64(len(p.rawContent)), nil
@ -1205,7 +1215,7 @@ func (p *Page) initMainOutputFormat() error {
pageOutput, err := newPageOutput(p, false, false, outFormat) pageOutput, err := newPageOutput(p, false, false, outFormat)
if err != nil { if err != nil {
return fmt.Errorf("Failed to create output page for type %q for page %q: %s", outFormat.Name, p.pathOrTitle(), err) return _errors.Wrapf(err, "Failed to create output page for type %q for page %q:", outFormat.Name, p.pathOrTitle())
} }
p.mainPageOutput = pageOutput p.mainPageOutput = pageOutput
@ -1271,7 +1281,7 @@ func (p *Page) prepareForRender() error {
// Note: The shortcodes in a page cannot access the page content it lives in, // Note: The shortcodes in a page cannot access the page content it lives in,
// hence the withoutContent(). // hence the withoutContent().
if workContentCopy, err = handleShortcodes(p.withoutContent(), workContentCopy); err != nil { if workContentCopy, err = handleShortcodes(p.withoutContent(), workContentCopy); err != nil {
s.Log.ERROR.Printf("Failed to handle shortcodes for page %s: %s", p.BaseFileName(), err) return err
} }
if p.Markup != "html" { if p.Markup != "html" {
@ -1294,8 +1304,6 @@ func (p *Page) prepareForRender() error {
return nil return nil
} }
var ErrHasDraftAndPublished = errors.New("both draft and published parameters were found in page's frontmatter")
func (p *Page) update(frontmatter map[string]interface{}) error { func (p *Page) update(frontmatter map[string]interface{}) error {
if frontmatter == nil { if frontmatter == nil {
return errors.New("missing frontmatter data") return errors.New("missing frontmatter data")
@ -1512,8 +1520,7 @@ func (p *Page) update(frontmatter map[string]interface{}) error {
if draft != nil && published != nil { if draft != nil && published != nil {
p.Draft = *draft p.Draft = *draft
p.s.Log.ERROR.Printf("page %s has both draft and published settings in its frontmatter. Using draft.", p.File.Path()) p.s.Log.WARN.Printf("page %q has both draft and published settings in its frontmatter. Using draft.", p.File.Path())
return ErrHasDraftAndPublished
} else if draft != nil { } else if draft != nil {
p.Draft = *draft p.Draft = *draft
} else if published != nil { } else if published != nil {
@ -1751,6 +1758,7 @@ func (p *Page) shouldRenderTo(f output.Format) bool {
func (p *Page) parse(reader io.Reader) error { func (p *Page) parse(reader io.Reader) error {
psr, err := parser.ReadFrom(reader) psr, err := parser.ReadFrom(reader)
if err != nil { if err != nil {
return err return err
} }
@ -1762,7 +1770,7 @@ func (p *Page) parse(reader io.Reader) error {
meta, err := psr.Metadata() meta, err := psr.Metadata()
if err != nil { if err != nil {
return fmt.Errorf("failed to parse page metadata for %q: %s", p.File.Path(), err) return _errors.Wrap(err, "error in front matter")
} }
if meta == nil { if meta == nil {
// missing frontmatter equivalent to empty frontmatter // missing frontmatter equivalent to empty frontmatter
@ -2079,7 +2087,7 @@ func (p *Page) decodeRefArgs(args map[string]interface{}) (refArgs, *SiteInfo, e
func (p *Page) Ref(argsm map[string]interface{}) (string, error) { func (p *Page) Ref(argsm map[string]interface{}) (string, error) {
args, s, err := p.decodeRefArgs(argsm) args, s, err := p.decodeRefArgs(argsm)
if err != nil { if err != nil {
return "", fmt.Errorf("invalid arguments to Ref: %s", err) return "", _errors.Wrap(err, "invalid arguments to Ref")
} }
if s == nil { if s == nil {
@ -2099,7 +2107,7 @@ func (p *Page) Ref(argsm map[string]interface{}) (string, error) {
func (p *Page) RelRef(argsm map[string]interface{}) (string, error) { func (p *Page) RelRef(argsm map[string]interface{}) (string, error) {
args, s, err := p.decodeRefArgs(argsm) args, s, err := p.decodeRefArgs(argsm)
if err != nil { if err != nil {
return "", fmt.Errorf("invalid arguments to Ref: %s", err) return "", _errors.Wrap(err, "invalid arguments to Ref")
} }
if s == nil { if s == nil {
@ -2303,8 +2311,13 @@ func (p *Page) setValuesForKind(s *Site) {
// Used in error logs. // Used in error logs.
func (p *Page) pathOrTitle() string { func (p *Page) pathOrTitle() string {
if p.Path() != "" { if p.Filename() != "" {
return p.Path() // 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 return p.title
} }

View file

@ -19,6 +19,8 @@ import (
"math" "math"
"runtime" "runtime"
_errors "github.com/pkg/errors"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
) )
@ -145,7 +147,7 @@ func (s *siteContentProcessor) process(ctx context.Context) error {
for _, file := range files { for _, file := range files {
f, err := s.site.BaseFs.Content.Fs.Open(file.Filename()) f, err := s.site.BaseFs.Content.Fs.Open(file.Filename())
if err != nil { if err != nil {
return fmt.Errorf("failed to open assets file: %s", err) return _errors.Wrap(err, "failed to open assets file")
} }
err = s.site.publish(&s.site.PathSpec.ProcessingStats.Files, file.Path(), f) err = s.site.publish(&s.site.PathSpec.ProcessingStats.Files, file.Path(), f)
f.Close() f.Close()

View file

@ -20,6 +20,10 @@ import (
"path" "path"
"path/filepath" "path/filepath"
"runtime" "runtime"
"github.com/gohugoio/hugo/common/loggers"
_errors "github.com/pkg/errors"
"sort" "sort"
"strings" "strings"
"sync" "sync"
@ -33,7 +37,6 @@ import (
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
"github.com/gohugoio/hugo/source" "github.com/gohugoio/hugo/source"
jww "github.com/spf13/jwalterweatherman"
) )
var errSkipCyclicDir = errors.New("skip potential cyclic dir") var errSkipCyclicDir = errors.New("skip potential cyclic dir")
@ -47,7 +50,7 @@ type capturer struct {
sourceSpec *source.SourceSpec sourceSpec *source.SourceSpec
fs afero.Fs fs afero.Fs
logger *jww.Notepad logger *loggers.Logger
// Filenames limits the content to process to a list of filenames/directories. // Filenames limits the content to process to a list of filenames/directories.
// This is used for partial building in server mode. // This is used for partial building in server mode.
@ -61,7 +64,7 @@ type capturer struct {
} }
func newCapturer( func newCapturer(
logger *jww.Notepad, logger *loggers.Logger,
sourceSpec *source.SourceSpec, sourceSpec *source.SourceSpec,
handler captureResultHandler, handler captureResultHandler,
contentChanges *contentChangeMap, contentChanges *contentChangeMap,
@ -701,13 +704,13 @@ func (c *capturer) resolveRealPathIn(fileInfo pathLangFileFi) error {
if fileInfo.Mode()&os.ModeSymlink == os.ModeSymlink { if fileInfo.Mode()&os.ModeSymlink == os.ModeSymlink {
link, err := filepath.EvalSymlinks(path) link, err := filepath.EvalSymlinks(path)
if err != nil { if err != nil {
return fmt.Errorf("Cannot read symbolic link %q, error was: %s", path, err) return _errors.Wrapf(err, "Cannot read symbolic link %q, error was:", path)
} }
// This is a file on the outside of any base fs, so we have to use the os package. // This is a file on the outside of any base fs, so we have to use the os package.
sfi, err := os.Stat(link) sfi, err := os.Stat(link)
if err != nil { if err != nil {
return fmt.Errorf("Cannot stat %q, error was: %s", link, err) return _errors.Wrapf(err, "Cannot stat %q, error was:", link)
} }
// TODO(bep) improve all of this. // TODO(bep) improve all of this.

View file

@ -22,8 +22,6 @@ import (
"github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/common/loggers"
jww "github.com/spf13/jwalterweatherman"
"runtime" "runtime"
"strings" "strings"
"sync" "sync"
@ -100,9 +98,6 @@ func TestPageBundlerCaptureSymlinks(t *testing.T) {
assert.NoError(c.capture()) assert.NoError(c.capture())
// Symlink back to content skipped to prevent infinite recursion.
assert.Equal(uint64(3), logger.LogCountForLevelsGreaterThanorEqualTo(jww.LevelWarn))
expected := ` expected := `
F: F:
/base/a/page_s.md /base/a/page_s.md

View file

@ -132,7 +132,7 @@ func TestPageBundlerSiteRegular(t *testing.T) {
assert.Len(pageResources, 2) assert.Len(pageResources, 2)
firstPage := pageResources[0].(*Page) firstPage := pageResources[0].(*Page)
secondPage := pageResources[1].(*Page) secondPage := pageResources[1].(*Page)
assert.Equal(filepath.FromSlash("b/my-bundle/1.md"), firstPage.pathOrTitle(), secondPage.pathOrTitle()) assert.Equal(filepath.FromSlash("base/b/my-bundle/1.md"), firstPage.pathOrTitle(), secondPage.pathOrTitle())
assert.Contains(firstPage.content(), "TheContent") assert.Contains(firstPage.content(), "TheContent")
assert.Equal(6, len(leafBundle1.Resources)) assert.Equal(6, len(leafBundle1.Resources))

View file

@ -1361,23 +1361,6 @@ func TestPagePaths(t *testing.T) {
} }
} }
var pageWithDraftAndPublished = `---
title: broken
published: false
draft: true
---
some content
`
func TestDraftAndPublishedFrontMatterError(t *testing.T) {
t.Parallel()
s := newTestSite(t)
_, err := s.NewPageFrom(strings.NewReader(pageWithDraftAndPublished), "content/post/broken.md")
if err != ErrHasDraftAndPublished {
t.Errorf("expected ErrHasDraftAndPublished, was %#v", err)
}
}
var pagesWithPublishedFalse = `--- var pagesWithPublishedFalse = `---
title: okay title: okay
published: false published: false

View file

@ -14,17 +14,14 @@
package pagemeta package pagemeta
import ( import (
"io/ioutil"
"log"
"os"
"strings" "strings"
"time" "time"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/config"
"github.com/spf13/cast" "github.com/spf13/cast"
jww "github.com/spf13/jwalterweatherman"
) )
// FrontMatterHandler maps front matter into Page fields and .Params. // 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. // A map of all date keys configured, including any custom.
allDateKeys map[string]bool allDateKeys map[string]bool
logger *jww.Notepad logger *loggers.Logger
} }
// FrontMatterDescriptor describes how to handle front matter for a given Page. // 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. // NewFrontmatterHandler creates a new FrontMatterHandler with the given logger and configuration.
// If no logger is provided, one will be created. // 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 { 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) frontMatterConfig, err := newFrontmatterConfig(cfg)

View file

@ -20,6 +20,7 @@ import (
"github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/langs" "github.com/gohugoio/hugo/langs"
"github.com/pkg/errors"
"github.com/gohugoio/hugo/hugofs" "github.com/gohugoio/hugo/hugofs"
) )
@ -83,13 +84,13 @@ func New(fs *hugofs.Fs, cfg config.Provider) (*Paths, error) {
baseURL, err := newBaseURLFromString(baseURLstr) baseURL, err := newBaseURLFromString(baseURLstr)
if err != nil { if err != nil {
return nil, fmt.Errorf("Failed to create baseURL from %q: %s", baseURLstr, err) return nil, errors.Wrapf(err, "Failed to create baseURL from %q:", baseURLstr)
} }
contentDir := cfg.GetString("contentDir") contentDir := filepath.Clean(cfg.GetString("contentDir"))
workingDir := cfg.GetString("workingDir") workingDir := filepath.Clean(cfg.GetString("workingDir"))
resourceDir := cfg.GetString("resourceDir") resourceDir := filepath.Clean(cfg.GetString("resourceDir"))
publishDir := cfg.GetString("publishDir") publishDir := filepath.Clean(cfg.GetString("publishDir"))
if contentDir == "" { if contentDir == "" {
return nil, fmt.Errorf("contentDir not set") return nil, fmt.Errorf("contentDir not set")

View file

@ -21,6 +21,9 @@ import (
"reflect" "reflect"
"regexp" "regexp"
"sort" "sort"
_errors "github.com/pkg/errors"
"strings" "strings"
"sync" "sync"
@ -278,7 +281,7 @@ func prepareShortcodeForPage(placeholder string, sc *shortcode, parent *Shortcod
// The most specific template will win. // The most specific template will win.
key := newScKeyFromLangAndOutputFormat(lang, f, placeholder) key := newScKeyFromLangAndOutputFormat(lang, f, placeholder)
m[key] = func() (string, error) { m[key] = func() (string, error) {
return renderShortcode(key, sc, nil, p), nil return renderShortcode(key, sc, nil, p)
} }
} }
@ -289,12 +292,12 @@ func renderShortcode(
tmplKey scKey, tmplKey scKey,
sc *shortcode, sc *shortcode,
parent *ShortcodeWithPage, parent *ShortcodeWithPage,
p *PageWithoutContent) string { p *PageWithoutContent) (string, error) {
tmpl := getShortcodeTemplateForTemplateKey(tmplKey, sc.name, p.s.Tmpl) tmpl := getShortcodeTemplateForTemplateKey(tmplKey, sc.name, p.s.Tmpl)
if tmpl == nil { if tmpl == nil {
p.s.Log.ERROR.Printf("Unable to locate template for shortcode %q in page %q", sc.name, p.Path()) p.s.Log.ERROR.Printf("Unable to locate template for shortcode %q in page %q", sc.name, p.Path())
return "" return "", nil
} }
data := &ShortcodeWithPage{Ordinal: sc.ordinal, Params: sc.params, Page: p, Parent: parent} data := &ShortcodeWithPage{Ordinal: sc.ordinal, Params: sc.params, Page: p, Parent: parent}
@ -309,11 +312,15 @@ func renderShortcode(
case string: case string:
inner += innerData.(string) inner += innerData.(string)
case *shortcode: case *shortcode:
inner += renderShortcode(tmplKey, innerData.(*shortcode), data, p) s, err := renderShortcode(tmplKey, innerData.(*shortcode), data, p)
if err != nil {
return "", err
}
inner += s
default: default:
p.s.Log.ERROR.Printf("Illegal state on shortcode rendering of %q in page %q. Illegal type in inner data: %s ", p.s.Log.ERROR.Printf("Illegal state on shortcode rendering of %q in page %q. Illegal type in inner data: %s ",
sc.name, p.Path(), reflect.TypeOf(innerData)) sc.name, p.Path(), reflect.TypeOf(innerData))
return "" return "", nil
} }
} }
@ -441,7 +448,7 @@ func (s *shortcodeHandler) executeShortcodesForDelta(p *PageWithoutContent) erro
render := s.contentShortcodesDelta.getShortcodeRenderer(k) render := s.contentShortcodesDelta.getShortcodeRenderer(k)
renderedShortcode, err := render() renderedShortcode, err := render()
if err != nil { if err != nil {
return fmt.Errorf("Failed to execute shortcode in page %q: %s", p.Path(), err) return _errors.Wrapf(err, "Failed to execute shortcode in page %q:", p.Path())
} }
s.renderedShortcodes[k.(scKey).ShortcodePlaceholder] = renderedShortcode s.renderedShortcodes[k.(scKey).ShortcodePlaceholder] = renderedShortcode
@ -479,6 +486,16 @@ func (s *shortcodeHandler) extractShortcode(ordinal int, pt *pageTokens, p *Page
var cnt = 0 var cnt = 0
var nestedOrdinal = 0 var nestedOrdinal = 0
// TODO(bep) 2errors revisit after https://github.com/gohugoio/hugo/issues/5324
msgf := func(i item, format string, args ...interface{}) string {
format = format + ":%d:"
c1 := strings.Count(pt.lexer.input[:i.pos], "\n") + 1
c2 := bytes.Count(p.frontmatter, []byte{'\n'})
args = append(args, c1+c2)
return fmt.Sprintf(format, args...)
}
Loop: Loop:
for { for {
currItem = pt.next() currItem = pt.next()
@ -524,7 +541,7 @@ Loop:
// return that error, more specific // return that error, more specific
continue 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, errors.New(msgf(next, "shortcode %q has no .Inner, yet a closing tag was provided", next.val))
} }
if next.typ == tRightDelimScWithMarkup || next.typ == tRightDelimScNoMarkup { if next.typ == tRightDelimScWithMarkup || next.typ == tRightDelimScNoMarkup {
// self-closing // self-closing
@ -542,13 +559,13 @@ Loop:
// if more than one. It is "all inner or no inner". // if more than one. It is "all inner or no inner".
tmpl := getShortcodeTemplateForTemplateKey(scKey{}, sc.name, p.s.Tmpl) tmpl := getShortcodeTemplateForTemplateKey(scKey{}, sc.name, p.s.Tmpl)
if tmpl == nil { if tmpl == nil {
return sc, fmt.Errorf("Unable to locate template for shortcode %q in page %q", sc.name, p.Path()) return sc, errors.New(msgf(currItem, "unable to locate template for shortcode %q", sc.name))
} }
var err error var err error
isInner, err = isInnerShortcode(tmpl.(tpl.TemplateExecutor)) isInner, err = isInnerShortcode(tmpl.(tpl.TemplateExecutor))
if err != nil { 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, _errors.Wrap(err, msgf(currItem, "failed to handle template for shortcode %q", sc.name))
} }
case tScParam: case tScParam:
@ -651,8 +668,8 @@ Loop:
case tEOF: case tEOF:
break Loop break Loop
case tError: case tError:
err := fmt.Errorf("%s:%d: %s", err := fmt.Errorf("%s:shortcode:%d: %s",
p.FullFilePath(), (p.lineNumRawContentStart() + pt.lexer.lineNum() - 1), currItem) p.pathOrTitle(), (p.lineNumRawContentStart() + pt.lexer.lineNum() - 1), currItem)
currShortcode.err = err currShortcode.err = err
return result.String(), err return result.String(), err
} }
@ -750,7 +767,7 @@ func getShortcodeTemplateForTemplateKey(key scKey, shortcodeName string, t tpl.T
return nil return nil
} }
func renderShortcodeWithPage(tmpl tpl.Template, data *ShortcodeWithPage) string { func renderShortcodeWithPage(tmpl tpl.Template, data *ShortcodeWithPage) (string, error) {
buffer := bp.GetBuffer() buffer := bp.GetBuffer()
defer bp.PutBuffer(buffer) defer bp.PutBuffer(buffer)
@ -758,7 +775,7 @@ func renderShortcodeWithPage(tmpl tpl.Template, data *ShortcodeWithPage) string
err := tmpl.Execute(buffer, data) err := tmpl.Execute(buffer, data)
isInnerShortcodeCache.RUnlock() isInnerShortcodeCache.RUnlock()
if err != nil { if err != nil {
data.Page.s.Log.ERROR.Printf("error processing shortcode %q for page %q: %s", tmpl.Name(), data.Page.Path(), err) return "", data.Page.errorf(err, "failed to process shortcode")
} }
return buffer.String() return buffer.String(), nil
} }

View file

@ -24,8 +24,6 @@ import (
"github.com/spf13/viper" "github.com/spf13/viper"
jww "github.com/spf13/jwalterweatherman"
"github.com/spf13/afero" "github.com/spf13/afero"
"github.com/gohugoio/hugo/output" "github.com/gohugoio/hugo/output"
@ -367,11 +365,11 @@ func TestExtractShortcodes(t *testing.T) {
expectErrorMsg string expectErrorMsg string
}{ }{
{"text", "Some text.", "map[]", "Some text.", ""}, {"text", "Some text.", "map[]", "Some text.", ""},
{"invalid right delim", "{{< tag }}", "", false, "simple.md:4:.*unrecognized character.*}"}, {"invalid right delim", "{{< tag }}", "", false, ":4:.*unrecognized character.*}"},
{"invalid close", "\n{{< /tag >}}", "", false, "simple.md:5:.*got closing shortcode, but none is open"}, {"invalid close", "\n{{< /tag >}}", "", false, ":5:.*got closing shortcode, but none is open"},
{"invalid close2", "\n\n{{< tag >}}{{< /anotherTag >}}", "", false, "simple.md:6: closing tag for shortcode 'anotherTag' does not match start tag"}, {"invalid close2", "\n\n{{< tag >}}{{< /anotherTag >}}", "", false, ":6: closing tag for shortcode 'anotherTag' does not match start tag"},
{"unterminated quote 1", `{{< figure src="im caption="S" >}}`, "", false, "simple.md:4:.got pos.*"}, {"unterminated quote 1", `{{< figure src="im caption="S" >}}`, "", false, ":4:.got pos.*"},
{"unterminated quote 1", `{{< figure src="im" caption="S >}}`, "", false, "simple.md:4:.*unterm.*}"}, {"unterminated quote 1", `{{< figure src="im" caption="S >}}`, "", false, ":4:.*unterm.*}"},
{"one shortcode, no markup", "{{< tag >}}", "", testScPlaceholderRegexp, ""}, {"one shortcode, no markup", "{{< tag >}}", "", testScPlaceholderRegexp, ""},
{"one shortcode, markup", "{{% tag %}}", "", testScPlaceholderRegexp, ""}, {"one shortcode, markup", "{{% tag %}}", "", testScPlaceholderRegexp, ""},
{"one pos param", "{{% tag param1 %}}", `tag([\"param1\"], true){[]}"]`, testScPlaceholderRegexp, ""}, {"one pos param", "{{% tag param1 %}}", `tag([\"param1\"], true){[]}"]`, testScPlaceholderRegexp, ""},
@ -384,7 +382,7 @@ func TestExtractShortcodes(t *testing.T) {
// issue #934 // issue #934
{"inner self-closing", `Some text. {{< inner />}}. Some more text.`, `inner([], false){[]}`, {"inner self-closing", `Some text. {{< inner />}}. Some more text.`, `inner([], false){[]}`,
fmt.Sprintf("Some text. %s. Some more text.", testScPlaceholderRegexp), ""}, fmt.Sprintf("Some text. %s. Some more text.", testScPlaceholderRegexp), ""},
{"close, but not inner", "{{< tag >}}foo{{< /tag >}}", "", false, "Shortcode 'tag' in page 'simple.md' has no .Inner.*"}, {"close, but not inner", "{{< tag >}}foo{{< /tag >}}", "", false, `shortcode "tag" has no .Inner, yet a closing tag was provided`},
{"nested inner", `Inner->{{< inner >}}Inner Content->{{% inner2 param1 %}}inner2txt{{% /inner2 %}}Inner close->{{< / inner >}}<-done`, {"nested inner", `Inner->{{< inner >}}Inner Content->{{% inner2 param1 %}}inner2txt{{% /inner2 %}}Inner close->{{< / inner >}}<-done`,
`inner([], false){[Inner Content-> inner2([\"param1\"], true){[inner2txt]} Inner close->]}`, `inner([], false){[Inner Content-> inner2([\"param1\"], true){[inner2txt]} Inner close->]}`,
fmt.Sprintf("Inner->%s<-done", testScPlaceholderRegexp), ""}, fmt.Sprintf("Inner->%s<-done", testScPlaceholderRegexp), ""},
@ -434,7 +432,7 @@ func TestExtractShortcodes(t *testing.T) {
} else { } else {
r, _ := regexp.Compile(this.expectErrorMsg) r, _ := regexp.Compile(this.expectErrorMsg)
if !r.MatchString(err.Error()) { if !r.MatchString(err.Error()) {
t.Fatalf("[%d] %s: ExtractShortcodes didn't return an expected error message, got %s but expected %s", t.Fatalf("[%d] %s: ExtractShortcodes didn't return an expected error message, got\n%s but expected\n%s",
i, this.name, err.Error(), this.expectErrorMsg) i, this.name, err.Error(), this.expectErrorMsg)
} }
} }
@ -777,7 +775,7 @@ NotFound: {{< thisDoesNotExist >}}
"thisDoesNotExist", "thisDoesNotExist",
) )
require.Equal(t, uint64(1), s.Log.LogCountForLevel(jww.LevelError)) require.Equal(t, uint64(1), s.Log.ErrorCounter.Count())
} }

View file

@ -15,7 +15,6 @@ package hugolib
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"html/template" "html/template"
"io" "io"
@ -29,6 +28,9 @@ import (
"strings" "strings"
"time" "time"
_errors "github.com/pkg/errors"
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/publisher" "github.com/gohugoio/hugo/publisher"
"github.com/gohugoio/hugo/resource" "github.com/gohugoio/hugo/resource"
@ -754,8 +756,6 @@ func (s *Site) processPartial(events []fsnotify.Event) (whatChanged, error) {
return whatChanged{}, err return whatChanged{}, err
} }
s.TemplateHandler().PrintErrors()
for i := 1; i < len(sites); i++ { for i := 1; i < len(sites); i++ {
site := sites[i] site := sites[i]
var err error var err error
@ -861,7 +861,7 @@ func (s *Site) handleDataFile(r source.ReadableFile) error {
f, err := r.Open() f, err := r.Open()
if err != nil { if err != nil {
return fmt.Errorf("Failed to open data file %q: %s", r.LogicalName(), err) return _errors.Wrapf(err, "Failed to open data file %q:", r.LogicalName())
} }
defer f.Close() defer f.Close()
@ -942,7 +942,7 @@ func (s *Site) handleDataFile(r source.ReadableFile) error {
func (s *Site) readData(f source.ReadableFile) (interface{}, error) { func (s *Site) readData(f source.ReadableFile) (interface{}, error) {
file, err := f.Open() file, err := f.Open()
if err != nil { if err != nil {
return nil, fmt.Errorf("readData: failed to open data file: %s", err) return nil, _errors.Wrap(err, "readData: failed to open data file")
} }
defer file.Close() defer file.Close()
content := helpers.ReaderToBytes(file) content := helpers.ReaderToBytes(file)
@ -1558,26 +1558,52 @@ func (s *Site) preparePages() error {
} }
} }
if len(errors) != 0 { return s.pickOneAndLogTheRest(errors)
return fmt.Errorf("Prepare pages failed: %.100q…", errors)
}
return nil
} }
func errorCollator(results <-chan error, errs chan<- error) { func (s *Site) errorCollator(results <-chan error, errs chan<- error) {
errMsgs := []string{} var errors []error
for err := range results { for e := range results {
if err != nil { errors = append(errors, e)
errMsgs = append(errMsgs, err.Error()) }
errs <- s.pickOneAndLogTheRest(errors)
close(errs)
}
func (s *Site) pickOneAndLogTheRest(errors []error) error {
if len(errors) == 0 {
return nil
}
var i int
for j, err := range errors {
// If this is in server mode, we want to return an error to the client
// with a file context, if possible.
if herrors.UnwrapErrorWithFileContext(err) != nil {
i = j
break
} }
} }
if len(errMsgs) == 0 {
errs <- nil // Log the rest, but add a threshold to avoid flooding the log.
} else { const errLogThreshold = 5
errs <- errors.New(strings.Join(errMsgs, "\n"))
for j, err := range errors {
if j == i {
continue
}
if j >= errLogThreshold {
break
}
s.Log.ERROR.Println(err)
} }
close(errs)
return errors[i]
} }
func (s *Site) appendThemeTemplates(in []string) []string { func (s *Site) appendThemeTemplates(in []string) []string {
@ -1650,8 +1676,7 @@ func (s *Site) renderAndWriteXML(statCounter *uint64, name string, targetPath st
renderBuffer.WriteString("<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\" ?>\n") renderBuffer.WriteString("<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\" ?>\n")
if err := s.renderForLayouts(name, d, renderBuffer, layouts...); err != nil { if err := s.renderForLayouts(name, d, renderBuffer, layouts...); err != nil {
helpers.DistinctWarnLog.Println(err) return err
return nil
} }
var path string var path string
@ -1684,8 +1709,8 @@ func (s *Site) renderAndWritePage(statCounter *uint64, name string, targetPath s
defer bp.PutBuffer(renderBuffer) defer bp.PutBuffer(renderBuffer)
if err := s.renderForLayouts(p.Kind, p, renderBuffer, layouts...); err != nil { if err := s.renderForLayouts(p.Kind, p, renderBuffer, layouts...); err != nil {
helpers.DistinctWarnLog.Println(err)
return nil return err
} }
if renderBuffer.Len() == 0 { if renderBuffer.Len() == 0 {
@ -1735,46 +1760,18 @@ func (s *Site) renderAndWritePage(statCounter *uint64, name string, targetPath s
func (s *Site) renderForLayouts(name string, d interface{}, w io.Writer, layouts ...string) (err error) { func (s *Site) renderForLayouts(name string, d interface{}, w io.Writer, layouts ...string) (err error) {
var templ tpl.Template var templ tpl.Template
defer func() {
if r := recover(); r != nil {
templName := ""
if templ != nil {
templName = templ.Name()
}
s.DistinctErrorLog.Printf("Failed to render %q: %s", templName, r)
s.DistinctErrorLog.Printf("Stack Trace:\n%s", stackTrace(1200))
// TOD(bep) we really need to fix this. Also see below.
if !s.running() && !testMode {
os.Exit(-1)
}
}
}()
templ = s.findFirstTemplate(layouts...) templ = s.findFirstTemplate(layouts...)
if templ == nil { if templ == nil {
return fmt.Errorf("[%s] Unable to locate layout for %q: %s\n", s.Language.Lang, name, layouts) s.Log.WARN.Printf("[%s] Unable to locate layout for %q: %s\n", s.Language.Lang, name, layouts)
return nil
} }
if err = templ.Execute(w, d); err != nil { 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, ok := d.(*PageOutput); ok {
if p.File != nil { return p.errorf(err, "render of %q failed", name)
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)
}
} else {
s.DistinctErrorLog.Printf("Error while rendering %q: %s", name, err)
}
if !s.running() && !testMode {
// TODO(bep) check if this can be propagated
os.Exit(-1)
} else if testMode {
return
} }
return _errors.Wrapf(err, "render of %q failed", name)
} }
return return
} }

View file

@ -19,6 +19,8 @@ import (
"strings" "strings"
"sync" "sync"
"github.com/pkg/errors"
"github.com/gohugoio/hugo/output" "github.com/gohugoio/hugo/output"
) )
@ -30,7 +32,7 @@ func (s *Site) renderPages(cfg *BuildCfg) error {
pages := make(chan *Page) pages := make(chan *Page)
errs := make(chan error) errs := make(chan error)
go errorCollator(results, errs) go s.errorCollator(results, errs)
numWorkers := getGoMaxProcs() * 4 numWorkers := getGoMaxProcs() * 4
@ -60,7 +62,7 @@ func (s *Site) renderPages(cfg *BuildCfg) error {
err := <-errs err := <-errs
if err != nil { if err != nil {
return fmt.Errorf("Error(s) rendering pages: %s", err) return errors.Wrap(err, "failed to render pages")
} }
return nil return nil
} }
@ -132,6 +134,7 @@ func pageRenderer(s *Site, pages <-chan *Page, results chan<- error, wg *sync.Wa
if shouldRender { if shouldRender {
if err := pageOutput.renderResources(); err != nil { if err := pageOutput.renderResources(); err != nil {
// TODO(bep) 2errors
s.Log.ERROR.Printf("Failed to render resources for page %q: %s", page, err) s.Log.ERROR.Printf("Failed to render resources for page %q: %s", page, err)
continue continue
} }

View file

@ -54,7 +54,7 @@ func TestRenderWithInvalidTemplate(t *testing.T) {
withTemplate := createWithTemplateFromNameValues("missing", templateMissingFunc) 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{})
} }

View file

@ -14,7 +14,6 @@ import (
"github.com/gohugoio/hugo/langs" "github.com/gohugoio/hugo/langs"
"github.com/sanity-io/litter" "github.com/sanity-io/litter"
jww "github.com/spf13/jwalterweatherman"
"github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/deps"
@ -26,6 +25,7 @@ import (
"os" "os"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/hugofs" "github.com/gohugoio/hugo/hugofs"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -38,7 +38,7 @@ type sitesBuilder struct {
Fs *hugofs.Fs Fs *hugofs.Fs
T testing.TB T testing.TB
logger *jww.Notepad logger *loggers.Logger
dumper litter.Options dumper litter.Options
@ -103,7 +103,7 @@ func (s *sitesBuilder) Running() *sitesBuilder {
return s return s
} }
func (s *sitesBuilder) WithLogger(logger *jww.Notepad) *sitesBuilder { func (s *sitesBuilder) WithLogger(logger *loggers.Logger) *sitesBuilder {
s.logger = logger s.logger = logger
return s return s
} }
@ -312,6 +312,14 @@ func (s *sitesBuilder) writeFilePairs(folder string, filenameContent []string) *
} }
func (s *sitesBuilder) CreateSites() *sitesBuilder { func (s *sitesBuilder) CreateSites() *sitesBuilder {
if err := s.CreateSitesE(); err != nil {
s.Fatalf("Failed to create sites: %s", err)
}
return s
}
func (s *sitesBuilder) CreateSitesE() error {
s.addDefaults() s.addDefaults()
s.writeFilePairs("content", s.contentFilePairs) s.writeFilePairs("content", s.contentFilePairs)
s.writeFilePairs("content", s.contentFilePairsAdded) s.writeFilePairs("content", s.contentFilePairsAdded)
@ -325,7 +333,7 @@ func (s *sitesBuilder) CreateSites() *sitesBuilder {
if s.Cfg == nil { if s.Cfg == nil {
cfg, _, err := LoadConfig(ConfigSourceDescriptor{Fs: s.Fs.Source, Filename: "config." + s.configFormat}) cfg, _, err := LoadConfig(ConfigSourceDescriptor{Fs: s.Fs.Source, Filename: "config." + s.configFormat})
if err != nil { if err != nil {
s.Fatalf("Failed to load config: %s", err) return err
} }
// TODO(bep) // TODO(bep)
/* expectedConfigs := 1 /* expectedConfigs := 1
@ -339,11 +347,19 @@ func (s *sitesBuilder) CreateSites() *sitesBuilder {
sites, err := NewHugoSites(deps.DepsCfg{Fs: s.Fs, Cfg: s.Cfg, Logger: s.logger, Running: s.running}) sites, err := NewHugoSites(deps.DepsCfg{Fs: s.Fs, Cfg: s.Cfg, Logger: s.logger, Running: s.running})
if err != nil { if err != nil {
s.Fatalf("Failed to create sites: %s", err) return err
} }
s.H = sites s.H = sites
return s return nil
}
func (s *sitesBuilder) BuildE(cfg BuildCfg) error {
if s.H == nil {
s.CreateSites()
}
return s.H.Build(cfg)
} }
func (s *sitesBuilder) Build(cfg BuildCfg) *sitesBuilder { func (s *sitesBuilder) Build(cfg BuildCfg) *sitesBuilder {
@ -360,6 +376,7 @@ func (s *sitesBuilder) build(cfg BuildCfg, shouldFail bool) *sitesBuilder {
} }
err := s.H.Build(cfg) err := s.H.Build(cfg)
if err == nil { if err == nil {
logErrorCount := s.H.NumLogErrors() logErrorCount := s.H.NumLogErrors()
if logErrorCount > 0 { if logErrorCount > 0 {
@ -639,13 +656,19 @@ func createWithTemplateFromNameValues(additionalTemplates ...string) func(templ
} }
func buildSingleSite(t testing.TB, depsCfg deps.DepsCfg, buildCfg BuildCfg) *Site { 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) 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) require.Len(t, h.Sites, 1)
if expectBuildError { if expectBuildError {

View file

@ -14,8 +14,10 @@
package i18n package i18n
import ( import (
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/helpers"
"github.com/nicksnyder/go-i18n/i18n/bundle" "github.com/nicksnyder/go-i18n/i18n/bundle"
jww "github.com/spf13/jwalterweatherman" jww "github.com/spf13/jwalterweatherman"
) )
@ -28,11 +30,11 @@ var (
type Translator struct { type Translator struct {
translateFuncs map[string]bundle.TranslateFunc translateFuncs map[string]bundle.TranslateFunc
cfg config.Provider cfg config.Provider
logger *jww.Notepad logger *loggers.Logger
} }
// NewTranslator creates a new Translator for the given language bundle and configuration. // 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 := Translator{cfg: cfg, logger: logger, translateFuncs: make(map[string]bundle.TranslateFunc)}
t.initFuncs(b) t.initFuncs(b)
return t return t

View file

@ -19,24 +19,19 @@ import (
"github.com/gohugoio/hugo/tpl/tplimpl" "github.com/gohugoio/hugo/tpl/tplimpl"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/langs" "github.com/gohugoio/hugo/langs"
"github.com/spf13/afero" "github.com/spf13/afero"
"github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/deps"
"io/ioutil"
"os"
"log"
"github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/hugofs" "github.com/gohugoio/hugo/hugofs"
jww "github.com/spf13/jwalterweatherman"
"github.com/spf13/viper" "github.com/spf13/viper"
"github.com/stretchr/testify/require" "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 { type i18nTest struct {
data map[string][]byte data map[string][]byte

View file

@ -15,14 +15,13 @@ package i18n
import ( import (
"errors" "errors"
"fmt"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/deps"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/source" "github.com/gohugoio/hugo/source"
"github.com/nicksnyder/go-i18n/i18n/bundle" "github.com/nicksnyder/go-i18n/i18n/bundle"
"github.com/nicksnyder/go-i18n/i18n/language" "github.com/nicksnyder/go-i18n/i18n/language"
_errors "github.com/pkg/errors"
) )
// TranslationProvider provides translation handling, i.e. loading // TranslationProvider provides translation handling, i.e. loading
@ -82,12 +81,12 @@ func (tp *TranslationProvider) Update(d *deps.Deps) error {
func addTranslationFile(bundle *bundle.Bundle, r source.ReadableFile) error { func addTranslationFile(bundle *bundle.Bundle, r source.ReadableFile) error {
f, err := r.Open() f, err := r.Open()
if err != nil { if err != nil {
return fmt.Errorf("Failed to open translations file %q: %s", r.LogicalName(), err) return _errors.Wrapf(err, "Failed to open translations file %q:", r.LogicalName())
} }
defer f.Close() defer f.Close()
err = bundle.ParseTranslationFileBytes(r.LogicalName(), helpers.ReaderToBytes(f)) err = bundle.ParseTranslationFileBytes(r.LogicalName(), helpers.ReaderToBytes(f))
if err != nil { if err != nil {
return fmt.Errorf("Failed to load translations in file %q: %s", r.LogicalName(), err) return _errors.Wrapf(err, "Failed to load translations in file %q:", r.LogicalName())
} }
return nil return nil
} }

View file

@ -16,7 +16,6 @@
package releaser package releaser
import ( import (
"errors"
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"log" "log"
@ -26,6 +25,8 @@ import (
"regexp" "regexp"
"strings" "strings"
"github.com/pkg/errors"
"github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/helpers"
) )
@ -255,7 +256,7 @@ func (r *ReleaseHandler) release(releaseNotesFile string) error {
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
err := cmd.Run() err := cmd.Run()
if err != nil { if err != nil {
return fmt.Errorf("goreleaser failed: %s", err) return errors.Wrap(err, "goreleaser failed")
} }
return nil return nil
} }

View file

@ -26,6 +26,8 @@ import (
"strings" "strings"
"sync" "sync"
_errors "github.com/pkg/errors"
"github.com/disintegration/imaging" "github.com/disintegration/imaging"
"github.com/gohugoio/hugo/common/hugio" "github.com/gohugoio/hugo/common/hugio"
"github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/helpers"
@ -430,7 +432,7 @@ func (i *Image) initConfig() error {
}) })
if err != nil { if err != nil {
return fmt.Errorf("failed to load image config: %s", err) return _errors.Wrap(err, "failed to load image config")
} }
return nil return nil
@ -439,7 +441,7 @@ func (i *Image) initConfig() error {
func (i *Image) decodeSource() (image.Image, error) { func (i *Image) decodeSource() (image.Image, error) {
f, err := i.ReadSeekCloser() f, err := i.ReadSeekCloser()
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to open image for decode: %s", err) return nil, _errors.Wrap(err, "failed to open image for decode")
} }
defer f.Close() defer f.Close()
img, _, err := image.Decode(f) img, _, err := image.Decode(f)

View file

@ -14,19 +14,18 @@
package postcss package postcss
import ( import (
"fmt"
"io" "io"
"path/filepath" "path/filepath"
"github.com/gohugoio/hugo/hugofs" "github.com/gohugoio/hugo/hugofs"
"github.com/pkg/errors"
"github.com/mitchellh/mapstructure"
// "io/ioutil"
"os" "os"
"os/exec" "os/exec"
"github.com/gohugoio/hugo/common/errors" "github.com/mitchellh/mapstructure"
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/resource" "github.com/gohugoio/hugo/resource"
) )
@ -111,7 +110,7 @@ func (t *postcssTransformation) Transform(ctx *resource.ResourceTransformationCt
binary = binaryName binary = binaryName
if _, err := exec.LookPath(binary); err != nil { if _, err := exec.LookPath(binary); err != nil {
// This may be on a CI server etc. Will fall back to pre-built assets. // This may be on a CI server etc. Will fall back to pre-built assets.
return errors.ErrFeatureNotAvailable return herrors.ErrFeatureNotAvailable
} }
} }
@ -134,7 +133,7 @@ func (t *postcssTransformation) Transform(ctx *resource.ResourceTransformationCt
if err != nil { if err != nil {
if t.options.Config != "" { if t.options.Config != "" {
// Only fail if the user specificed config file is not found. // Only fail if the user specificed config file is not found.
return fmt.Errorf("postcss config %q not found: %s", configFile, err) return errors.Wrapf(err, "postcss config %q not found:", configFile)
} }
configFile = "" configFile = ""
} else { } else {

View file

@ -14,7 +14,6 @@
package resource package resource
import ( import (
"errors"
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
@ -27,13 +26,12 @@ import (
"github.com/gohugoio/hugo/output" "github.com/gohugoio/hugo/output"
"github.com/gohugoio/hugo/tpl" "github.com/gohugoio/hugo/tpl"
"github.com/pkg/errors"
"github.com/gohugoio/hugo/common/collections" "github.com/gohugoio/hugo/common/collections"
"github.com/gohugoio/hugo/common/hugio" "github.com/gohugoio/hugo/common/hugio"
"github.com/gohugoio/hugo/common/loggers" "github.com/gohugoio/hugo/common/loggers"
jww "github.com/spf13/jwalterweatherman"
"github.com/spf13/afero" "github.com/spf13/afero"
"github.com/gobwas/glob" "github.com/gobwas/glob"
@ -273,7 +271,7 @@ type Spec struct {
MediaTypes media.Types MediaTypes media.Types
OutputFormats output.Formats OutputFormats output.Formats
Logger *jww.Notepad Logger *loggers.Logger
TextTemplates tpl.TemplateParseFinder TextTemplates tpl.TemplateParseFinder
@ -287,7 +285,7 @@ type Spec struct {
GenAssetsPath string 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")) imaging, err := decodeImaging(s.Cfg.GetStringMap("imaging"))
if err != nil { if err != nil {
@ -542,7 +540,7 @@ type resourceHash struct {
type publishOnce struct { type publishOnce struct {
publisherInit sync.Once publisherInit sync.Once
publisherErr error publisherErr error
logger *jww.Notepad logger *loggers.Logger
} }
func (l *publishOnce) publish(s Source) error { func (l *publishOnce) publish(s Source) error {
@ -660,7 +658,7 @@ func (l *genericResource) initHash() error {
var f hugio.ReadSeekCloser var f hugio.ReadSeekCloser
f, err = l.ReadSeekCloser() f, err = l.ReadSeekCloser()
if err != nil { if err != nil {
err = fmt.Errorf("failed to open source file: %s", err) err = errors.Wrap(err, "failed to open source file")
return return
} }
defer f.Close() defer f.Close()

View file

@ -17,6 +17,7 @@ import (
"fmt" "fmt"
"strconv" "strconv"
"github.com/pkg/errors"
"github.com/spf13/cast" "github.com/spf13/cast"
"strings" "strings"
@ -69,7 +70,7 @@ func AssignMetadata(metadata []map[string]interface{}, resources ...Resource) er
glob, err := getGlob(srcKey) glob, err := getGlob(srcKey)
if err != nil { if err != nil {
return fmt.Errorf("failed to match resource with metadata: %s", err) return errors.Wrap(err, "failed to match resource with metadata")
} }
match := glob.Match(resourceSrcKey) match := glob.Match(resourceSrcKey)

View file

@ -15,11 +15,10 @@
package templates package templates
import ( import (
"fmt"
"github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/resource" "github.com/gohugoio/hugo/resource"
"github.com/gohugoio/hugo/tpl" "github.com/gohugoio/hugo/tpl"
"github.com/pkg/errors"
) )
// Client contains methods to perform template processing of Resource objects. // Client contains methods to perform template processing of Resource objects.
@ -55,7 +54,7 @@ func (t *executeAsTemplateTransform) Transform(ctx *resource.ResourceTransformat
tplStr := helpers.ReaderToString(ctx.From) tplStr := helpers.ReaderToString(ctx.From)
templ, err := t.textTemplate.Parse(ctx.InPath, tplStr) templ, err := t.textTemplate.Parse(ctx.InPath, tplStr)
if err != nil { if err != nil {
return fmt.Errorf("failed to parse Resource %q as Template: %s", ctx.InPath, err) return errors.Wrapf(err, "failed to parse Resource %q as Template:", ctx.InPath)
} }
ctx.OutPath = t.targetPath ctx.OutPath = t.targetPath

View file

@ -29,6 +29,7 @@ import (
"github.com/gohugoio/hugo/hugofs" "github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/media" "github.com/gohugoio/hugo/media"
"github.com/gohugoio/hugo/resource" "github.com/gohugoio/hugo/resource"
"github.com/pkg/errors"
) )
// Used in tests. This feature requires Hugo to be built with the extended tag. // Used in tests. This feature requires Hugo to be built with the extended tag.
@ -165,7 +166,7 @@ func (c *Client) toCSS(options scss.Options, dst io.Writer, src io.Reader) (tocs
res, err = transpiler.Execute(dst, src) res, err = transpiler.Execute(dst, src)
if err != nil { if err != nil {
return res, fmt.Errorf("SCSS processing failed: %s", err) return res, errors.Wrap(err, "SCSS processing failed")
} }
return res, nil return res, nil

View file

@ -16,7 +16,7 @@
package scss package scss
import ( import (
"github.com/gohugoio/hugo/common/errors" "github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/resource" "github.com/gohugoio/hugo/resource"
) )
@ -26,5 +26,5 @@ func Supports() bool {
} }
func (t *toCSSTransformation) Transform(ctx *resource.ResourceTransformationCtx) error { func (t *toCSSTransformation) Transform(ctx *resource.ResourceTransformationCtx) error {
return errors.ErrFeatureNotAvailable return herrors.ErrFeatureNotAvailable
} }

View file

@ -20,7 +20,7 @@ import (
"strings" "strings"
"github.com/gohugoio/hugo/common/collections" "github.com/gohugoio/hugo/common/collections"
"github.com/gohugoio/hugo/common/errors" "github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/common/hugio" "github.com/gohugoio/hugo/common/hugio"
"github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/helpers"
"github.com/mitchellh/hashstructure" "github.com/mitchellh/hashstructure"
@ -390,7 +390,7 @@ func (r *transformedResource) transform(setContent bool) (err error) {
} }
if err := tr.transformation.Transform(tctx); err != nil { if err := tr.transformation.Transform(tctx); err != nil {
if err == errors.ErrFeatureNotAvailable { if err == herrors.ErrFeatureNotAvailable {
// This transformation is not available in this // This transformation is not available in this
// Hugo installation (scss not compiled in, PostCSS not available etc.) // Hugo installation (scss not compiled in, PostCSS not available etc.)
// If a prepared bundle for this transformation chain is available, use that. // If a prepared bundle for this transformation chain is available, use that.

View file

@ -17,20 +17,17 @@ import (
"errors" "errors"
"fmt" "fmt"
"html/template" "html/template"
"io/ioutil"
"log"
"math/rand" "math/rand"
"os"
"reflect" "reflect"
"testing" "testing"
"time" "time"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/deps"
"github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugofs" "github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/langs" "github.com/gohugoio/hugo/langs"
jww "github.com/spf13/jwalterweatherman"
"github.com/spf13/viper" "github.com/spf13/viper"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -856,7 +853,7 @@ func newDeps(cfg config.Provider) *deps.Deps {
Cfg: cfg, Cfg: cfg,
Fs: hugofs.NewMem(l), Fs: hugofs.NewMem(l),
ContentSpec: cs, ContentSpec: cs,
Log: jww.NewNotepad(jww.LevelError, jww.LevelError, os.Stdout, ioutil.Discard, "", log.Ldate|log.Ltime), Log: loggers.NewErrorLogger(),
} }
} }

View file

@ -18,12 +18,12 @@ import (
"encoding/csv" "encoding/csv"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"net/http" "net/http"
"strings" "strings"
"time" "time"
"github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/deps"
_errors "github.com/pkg/errors"
) )
// New returns a new instance of the data-namespaced template functions. // New returns a new instance of the data-namespaced template functions.
@ -59,7 +59,7 @@ func (ns *Namespace) GetCSV(sep string, urlParts ...string) (d [][]string, err e
var req *http.Request var req *http.Request
req, err = http.NewRequest("GET", url, nil) req, err = http.NewRequest("GET", url, nil)
if err != nil { if err != nil {
return nil, fmt.Errorf("Failed to create request for getCSV for resource %s: %s", url, err) return nil, _errors.Wrapf(err, "Failed to create request for getCSV for resource %s:", url)
} }
req.Header.Add("Accept", "text/csv") req.Header.Add("Accept", "text/csv")
@ -103,7 +103,7 @@ func (ns *Namespace) GetJSON(urlParts ...string) (v interface{}, err error) {
var req *http.Request var req *http.Request
req, err = http.NewRequest("GET", url, nil) req, err = http.NewRequest("GET", url, nil)
if err != nil { if err != nil {
return nil, fmt.Errorf("Failed to create request for getJSON resource %s: %s", url, err) return nil, _errors.Wrapf(err, "Failed to create request for getJSON resource %s:", url)
} }
req.Header.Add("Accept", "application/json") req.Header.Add("Accept", "application/json")

View file

@ -113,11 +113,11 @@ func TestGetCSV(t *testing.T) {
require.NoError(t, err, msg) require.NoError(t, err, msg)
if _, ok := test.expect.(bool); ok { 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) require.Nil(t, got)
continue 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) require.NotNil(t, got, msg)
assert.EqualValues(t, test.expect, got, msg) assert.EqualValues(t, test.expect, got, msg)
@ -198,14 +198,14 @@ func TestGetJSON(t *testing.T) {
continue continue
} }
if errLevel, ok := test.expect.(jww.Threshold); ok { if errLevel, ok := test.expect.(jww.Threshold); ok && errLevel >= jww.LevelError {
logCount := ns.deps.Log.LogCountForLevelsGreaterThanorEqualTo(errLevel) logCount := ns.deps.Log.ErrorCounter.Count()
require.True(t, logCount >= 1, fmt.Sprintf("got log count %d", logCount)) require.True(t, logCount >= 1, fmt.Sprintf("got log count %d", logCount))
continue continue
} }
require.NoError(t, err, msg) 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) require.NotNil(t, got, msg)
assert.EqualValues(t, test.expect, got, msg) assert.EqualValues(t, test.expect, got, msg)

View file

@ -16,12 +16,13 @@ package fmt
import ( import (
_fmt "fmt" _fmt "fmt"
"github.com/gohugoio/hugo/deps"
"github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/helpers"
) )
// New returns a new instance of the fmt-namespaced template functions. // New returns a new instance of the fmt-namespaced template functions.
func New() *Namespace { func New(d *deps.Deps) *Namespace {
return &Namespace{helpers.NewDistinctErrorLogger()} return &Namespace{helpers.NewDistinctLogger(d.Log.ERROR)}
} }
// Namespace provides template functions for the "fmt" namespace. // Namespace provides template functions for the "fmt" namespace.

View file

@ -22,7 +22,7 @@ const name = "fmt"
func init() { func init() {
f := func(d *deps.Deps) *internal.TemplateFuncsNamespace { f := func(d *deps.Deps) *internal.TemplateFuncsNamespace {
ctx := New() ctx := New(d)
ns := &internal.TemplateFuncsNamespace{ ns := &internal.TemplateFuncsNamespace{
Name: name, Name: name,

View file

@ -16,6 +16,7 @@ package fmt
import ( import (
"testing" "testing"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/deps"
"github.com/gohugoio/hugo/tpl/internal" "github.com/gohugoio/hugo/tpl/internal"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -26,7 +27,7 @@ func TestInit(t *testing.T) {
var ns *internal.TemplateFuncsNamespace var ns *internal.TemplateFuncsNamespace
for _, nsf := range internal.TemplateFuncsNamespaceRegistry { for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
ns = nsf(&deps.Deps{}) ns = nsf(&deps.Deps{Log: loggers.NewErrorLogger()})
if ns.Name == name { if ns.Name == name {
found = true found = true
break break

View file

@ -16,6 +16,7 @@ package partials
import ( import (
"testing" "testing"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/deps"
"github.com/gohugoio/hugo/tpl/internal" "github.com/gohugoio/hugo/tpl/internal"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -28,6 +29,7 @@ func TestInit(t *testing.T) {
for _, nsf := range internal.TemplateFuncsNamespaceRegistry { for _, nsf := range internal.TemplateFuncsNamespaceRegistry {
ns = nsf(&deps.Deps{ ns = nsf(&deps.Deps{
BuildStartListeners: &deps.Listeners{}, BuildStartListeners: &deps.Listeners{},
Log: loggers.NewErrorLogger(),
}) })
if ns.Name == name { if ns.Name == name {
found = true found = true

View file

@ -18,6 +18,8 @@ import (
"fmt" "fmt"
"path/filepath" "path/filepath"
_errors "github.com/pkg/errors"
"github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/deps"
"github.com/gohugoio/hugo/resource" "github.com/gohugoio/hugo/resource"
"github.com/gohugoio/hugo/resource/bundler" "github.com/gohugoio/hugo/resource/bundler"
@ -256,7 +258,7 @@ func (ns *Namespace) resolveArgs(args []interface{}) (resource.Resource, map[str
m, err := cast.ToStringMapE(args[0]) m, err := cast.ToStringMapE(args[0])
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("invalid options type: %s", err) return nil, nil, _errors.Wrap(err, "invalid options type")
} }
return r, m, nil return r, m, nil

View file

@ -20,6 +20,8 @@ import (
_strings "strings" _strings "strings"
"unicode/utf8" "unicode/utf8"
_errors "github.com/pkg/errors"
"github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/deps"
"github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/helpers"
"github.com/spf13/cast" "github.com/spf13/cast"
@ -44,7 +46,7 @@ type Namespace struct {
func (ns *Namespace) CountRunes(s interface{}) (int, error) { func (ns *Namespace) CountRunes(s interface{}) (int, error) {
ss, err := cast.ToStringE(s) ss, err := cast.ToStringE(s)
if err != nil { if err != nil {
return 0, fmt.Errorf("Failed to convert content to string: %s", err) return 0, _errors.Wrap(err, "Failed to convert content to string")
} }
counter := 0 counter := 0
@ -61,7 +63,7 @@ func (ns *Namespace) CountRunes(s interface{}) (int, error) {
func (ns *Namespace) RuneCount(s interface{}) (int, error) { func (ns *Namespace) RuneCount(s interface{}) (int, error) {
ss, err := cast.ToStringE(s) ss, err := cast.ToStringE(s)
if err != nil { if err != nil {
return 0, fmt.Errorf("Failed to convert content to string: %s", err) return 0, _errors.Wrap(err, "Failed to convert content to string")
} }
return utf8.RuneCountInString(ss), nil return utf8.RuneCountInString(ss), nil
} }
@ -70,7 +72,7 @@ func (ns *Namespace) RuneCount(s interface{}) (int, error) {
func (ns *Namespace) CountWords(s interface{}) (int, error) { func (ns *Namespace) CountWords(s interface{}) (int, error) {
ss, err := cast.ToStringE(s) ss, err := cast.ToStringE(s)
if err != nil { if err != nil {
return 0, fmt.Errorf("Failed to convert content to string: %s", err) return 0, _errors.Wrap(err, "Failed to convert content to string")
} }
counter := 0 counter := 0

View file

@ -1,4 +1,4 @@
// Copyright 2017-present The Hugo Authors. All rights reserved. // Copyright 2018 The Hugo Authors. All rights reserved.
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -14,16 +14,26 @@
package tpl package tpl
import ( import (
"fmt"
"io" "io"
"path/filepath"
"regexp"
"strings"
"time" "time"
"text/template/parse" "github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/hugofs"
"github.com/spf13/afero"
"html/template" "html/template"
texttemplate "text/template" texttemplate "text/template"
"text/template/parse"
bp "github.com/gohugoio/hugo/bufferpool" bp "github.com/gohugoio/hugo/bufferpool"
"github.com/gohugoio/hugo/metrics" "github.com/gohugoio/hugo/metrics"
"github.com/pkg/errors"
) )
var ( var (
@ -35,8 +45,7 @@ type TemplateHandler interface {
TemplateFinder TemplateFinder
AddTemplate(name, tpl string) error AddTemplate(name, tpl string) error
AddLateTemplate(name, tpl string) error AddLateTemplate(name, tpl string) error
LoadTemplates(prefix string) LoadTemplates(prefix string) error
PrintErrors()
NewTextTemplate() TemplateParseFinder NewTextTemplate() TemplateParseFinder
@ -82,16 +91,122 @@ type TemplateDebugger interface {
type TemplateAdapter struct { type TemplateAdapter struct {
Template Template
Metrics metrics.Provider Metrics metrics.Provider
// The filesystem where the templates are stored.
Fs afero.Fs
// Maps to base template if relevant.
NameBaseTemplateName map[string]string
}
var baseOfRe = regexp.MustCompile("template: (.*?):")
func extractBaseOf(err string) string {
m := baseOfRe.FindStringSubmatch(err)
if len(m) == 2 {
return m[1]
}
return ""
} }
// Execute executes the current template. The actual execution is performed // Execute executes the current template. The actual execution is performed
// by the embedded text or html template, but we add an implementation here so // by the embedded text or html template, but we add an implementation here so
// we can add a timer for some metrics. // we can add a timer for some metrics.
func (t *TemplateAdapter) Execute(w io.Writer, data interface{}) error { func (t *TemplateAdapter) Execute(w io.Writer, data interface{}) (execErr error) {
defer func() {
// Panics in templates are a little bit too common (nil pointers etc.)
if r := recover(); r != nil {
execErr = t.addFileContext(t.Name(), fmt.Errorf("panic in Execute: %s", r))
}
}()
if t.Metrics != nil { if t.Metrics != nil {
defer t.Metrics.MeasureSince(t.Name(), time.Now()) defer t.Metrics.MeasureSince(t.Name(), time.Now())
} }
return t.Template.Execute(w, data)
execErr = t.Template.Execute(w, data)
if execErr != nil {
execErr = t.addFileContext(t.Name(), execErr)
}
return
}
var identifiersRe = regexp.MustCompile("at \\<(.*?)\\>:")
func (t *TemplateAdapter) extractIdentifiers(line string) []string {
m := identifiersRe.FindAllStringSubmatch(line, -1)
identifiers := make([]string, len(m))
for i := 0; i < len(m); i++ {
identifiers[i] = m[i][1]
}
return identifiers
}
func (t *TemplateAdapter) addFileContext(name string, inerr error) error {
f, realFilename, err := t.fileAndFilename(t.Name())
if err != nil {
return err
}
defer f.Close()
master, hasMaster := t.NameBaseTemplateName[name]
ferr := errors.Wrapf(inerr, "execute of template %q failed", realFilename)
// Since this can be a composite of multiple template files (single.html + baseof.html etc.)
// we potentially need to look in both -- and cannot rely on line number alone.
lineMatcher := func(le herrors.FileError, lineNumber int, line string) bool {
if le.LineNumber() != lineNumber {
return false
}
if !hasMaster {
return true
}
identifiers := t.extractIdentifiers(le.Error())
for _, id := range identifiers {
if strings.Contains(line, id) {
return true
}
}
return false
}
// TODO(bep) 2errors text vs HTML
fe, ok := herrors.WithFileContext(ferr, f, "go-html-template", lineMatcher)
if ok || !hasMaster {
return fe
}
// Try the base template if relevant
f, realFilename, err = t.fileAndFilename(master)
if err != nil {
return err
}
defer f.Close()
ferr = errors.Wrapf(inerr, "execute of template %q failed", realFilename)
fe, _ = herrors.WithFileContext(ferr, f, "go-html-template", lineMatcher)
return fe
}
func (t *TemplateAdapter) fileAndFilename(name string) (afero.File, string, error) {
fs := t.Fs
filename := filepath.FromSlash(name)
fi, err := fs.Stat(filename)
if err != nil {
return nil, "", errors.Wrapf(err, "failed to Stat %q", filename)
}
f, err := fs.Open(filename)
if err != nil {
return nil, "", errors.Wrapf(err, "failed to open template file %q:", filename)
}
return f, fi.(hugofs.RealFilenameInfo).RealFilename(), nil
} }
// ExecuteToString executes the current template and returns the result as a // ExecuteToString executes the current template and returns the result as a

31
tpl/template_test.go Normal file
View file

@ -0,0 +1,31 @@
// 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 tpl
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestExtractBaseof(t *testing.T) {
assert := require.New(t)
replaced := extractBaseOf(`failed: template: _default/baseof.html:37:11: executing "_default/baseof.html" at <.Parents>: can't evaluate field Parents in type *hugolib.PageOutput`)
assert.Equal("_default/baseof.html", replaced)
assert.Equal("", extractBaseOf("not baseof for you"))
assert.Equal("blog/baseof.html", extractBaseOf("template: blog/baseof.html:23:11:"))
assert.Equal("blog/baseof.ace", extractBaseOf("template: blog/baseof.ace:23:11:"))
}

View file

@ -1,4 +1,4 @@
// Copyright 2017-present The Hugo Authors. All rights reserved. // Copyright 2018 The Hugo Authors. All rights reserved.
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -20,7 +20,9 @@ import (
"strings" "strings"
texttemplate "text/template" texttemplate "text/template"
"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/tpl/tplimpl/embedded" "github.com/gohugoio/hugo/tpl/tplimpl/embedded"
"github.com/pkg/errors"
"github.com/eknkc/amber" "github.com/eknkc/amber"
@ -64,7 +66,7 @@ type templateErr struct {
} }
type templateLoader interface { 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 addTemplate(name, tpl string) error
addLateTemplate(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() { func (t *templateHandler) Debug() {
fmt.Println("HTML templates:\n", t.html.t.DefinedTemplates()) fmt.Println("HTML templates:\n", t.html.t.DefinedTemplates())
fmt.Println("\n\nText templates:\n", t.text.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 // Lookup tries to find a template with the given name in both template
// collections: First HTML, then the plain text template collection. // collections: First HTML, then the plain text template collection.
func (t *templateHandler) Lookup(name string) (tpl.Template, bool) { func (t *templateHandler) Lookup(name string) (tpl.Template, bool) {
@ -156,8 +147,8 @@ func (t *templateHandler) clone(d *deps.Deps) *templateHandler {
c := &templateHandler{ c := &templateHandler{
Deps: d, Deps: d,
layoutsFs: d.BaseFs.Layouts.Fs, layoutsFs: d.BaseFs.Layouts.Fs,
html: &htmlTemplates{t: template.Must(t.html.t.Clone()), overlays: make(map[string]*template.Template)}, html: &htmlTemplates{t: template.Must(t.html.t.Clone()), overlays: make(map[string]*template.Template), templatesCommon: t.html.templatesCommon},
text: &textTemplates{textTemplate: &textTemplate{t: texttemplate.Must(t.text.t.Clone())}, overlays: make(map[string]*texttemplate.Template)}, text: &textTemplates{textTemplate: &textTemplate{t: texttemplate.Must(t.text.t.Clone())}, overlays: make(map[string]*texttemplate.Template), templatesCommon: t.text.templatesCommon},
errors: make([]*templateErr, 0), errors: make([]*templateErr, 0),
} }
@ -187,15 +178,21 @@ func (t *templateHandler) clone(d *deps.Deps) *templateHandler {
} }
func newTemplateAdapter(deps *deps.Deps) *templateHandler { func newTemplateAdapter(deps *deps.Deps) *templateHandler {
common := &templatesCommon{
nameBaseTemplateName: make(map[string]string),
}
htmlT := &htmlTemplates{ htmlT := &htmlTemplates{
t: template.New(""), t: template.New(""),
overlays: make(map[string]*template.Template), overlays: make(map[string]*template.Template),
templatesCommon: common,
} }
textT := &textTemplates{ textT := &textTemplates{
textTemplate: &textTemplate{t: texttemplate.New("")}, textTemplate: &textTemplate{t: texttemplate.New("")},
overlays: make(map[string]*texttemplate.Template), overlays: make(map[string]*texttemplate.Template),
templatesCommon: common,
} }
return &templateHandler{ h := &templateHandler{
Deps: deps, Deps: deps,
layoutsFs: deps.BaseFs.Layouts.Fs, layoutsFs: deps.BaseFs.Layouts.Fs,
html: htmlT, html: htmlT,
@ -203,11 +200,23 @@ func newTemplateAdapter(deps *deps.Deps) *templateHandler {
errors: make([]*templateErr, 0), errors: make([]*templateErr, 0),
} }
common.handler = h
return h
} }
type htmlTemplates struct { // Shared by both HTML and text templates.
type templatesCommon struct {
handler *templateHandler
funcster *templateFuncster funcster *templateFuncster
// Used to get proper filenames in errors
nameBaseTemplateName map[string]string
}
type htmlTemplates struct {
*templatesCommon
t *template.Template t *template.Template
// This looks, and is, strange. // This looks, and is, strange.
@ -231,7 +240,8 @@ func (t *htmlTemplates) Lookup(name string) (tpl.Template, bool) {
if templ == nil { if templ == nil {
return nil, false return nil, false
} }
return &tpl.TemplateAdapter{Template: templ, Metrics: t.funcster.Deps.Metrics}, true
return &tpl.TemplateAdapter{Template: templ, Metrics: t.funcster.Deps.Metrics, Fs: t.handler.layoutsFs, NameBaseTemplateName: t.nameBaseTemplateName}, true
} }
func (t *htmlTemplates) lookup(name string) *template.Template { func (t *htmlTemplates) lookup(name string) *template.Template {
@ -259,8 +269,8 @@ func (t *textTemplates) setTemplateFuncster(f *templateFuncster) {
} }
type textTemplates struct { type textTemplates struct {
*templatesCommon
*textTemplate *textTemplate
funcster *templateFuncster
clone *texttemplate.Template clone *texttemplate.Template
cloneClone *texttemplate.Template cloneClone *texttemplate.Template
@ -272,7 +282,7 @@ func (t *textTemplates) Lookup(name string) (tpl.Template, bool) {
if templ == nil { if templ == nil {
return nil, false return nil, false
} }
return &tpl.TemplateAdapter{Template: templ, Metrics: t.funcster.Deps.Metrics}, true return &tpl.TemplateAdapter{Template: templ, Metrics: t.funcster.Deps.Metrics, Fs: t.handler.layoutsFs, NameBaseTemplateName: t.nameBaseTemplateName}, true
} }
func (t *textTemplates) lookup(name string) *texttemplate.Template { func (t *textTemplates) lookup(name string) *texttemplate.Template {
@ -321,8 +331,8 @@ func (t *textTemplates) setFuncs(funcMap map[string]interface{}) {
// LoadTemplates loads the templates from the layouts filesystem. // LoadTemplates loads the templates from the layouts filesystem.
// A prefix can be given to indicate a template namespace to load the templates // A prefix can be given to indicate a template namespace to load the templates
// into, i.e. "_internal" etc. // into, i.e. "_internal" etc.
func (t *templateHandler) LoadTemplates(prefix string) { func (t *templateHandler) LoadTemplates(prefix string) error {
t.loadTemplates(prefix) return t.loadTemplates(prefix)
} }
@ -423,7 +433,6 @@ func (t *templateHandler) addLateTemplate(name, tpl string) error {
func (t *templateHandler) AddLateTemplate(name, tpl string) error { func (t *templateHandler) AddLateTemplate(name, tpl string) error {
h := t.getTemplateHandler(name) h := t.getTemplateHandler(name)
if err := h.addLateTemplate(name, tpl); err != nil { if err := h.addLateTemplate(name, tpl); err != nil {
t.addError(name, err)
return err return err
} }
return nil return nil
@ -435,7 +444,6 @@ func (t *templateHandler) AddLateTemplate(name, tpl string) error {
func (t *templateHandler) AddTemplate(name, tpl string) error { func (t *templateHandler) AddTemplate(name, tpl string) error {
h := t.getTemplateHandler(name) h := t.getTemplateHandler(name)
if err := h.addTemplate(name, tpl); err != nil { if err := h.addTemplate(name, tpl); err != nil {
t.addError(name, err)
return err return err
} }
return nil return nil
@ -458,14 +466,19 @@ func (t *templateHandler) MarkReady() {
// RebuildClone rebuilds the cloned templates. Used for live-reloads. // RebuildClone rebuilds the cloned templates. Used for live-reloads.
func (t *templateHandler) RebuildClone() { func (t *templateHandler) RebuildClone() {
t.html.clone = template.Must(t.html.cloneClone.Clone()) if t.html != nil && t.html.cloneClone != nil {
t.text.clone = texttemplate.Must(t.text.cloneClone.Clone()) 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 {
walker := func(path string, fi os.FileInfo, err error) error { walker := func(path string, fi os.FileInfo, err error) error {
if err != nil || fi.IsDir() { if err != nil || fi.IsDir() {
return nil return err
} }
if isDotFile(path) || isBackupFile(path) || isBaseTemplate(path) { if isDotFile(path) || isBackupFile(path) || isBaseTemplate(path) {
@ -490,21 +503,25 @@ func (t *templateHandler) loadTemplates(prefix string) {
tplID, err := output.CreateTemplateNames(descriptor) tplID, err := output.CreateTemplateNames(descriptor)
if err != nil { if err != nil {
t.Log.ERROR.Printf("Failed to resolve template in path %q: %s", path, err) t.Log.ERROR.Printf("Failed to resolve template in path %q: %s", path, err)
return nil return nil
} }
if err := t.addTemplateFile(tplID.Name, tplID.MasterFilename, tplID.OverlayFilename); err != nil { 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) return err
} }
return nil return nil
} }
if err := helpers.SymbolicWalk(t.Layouts.Fs, "", walker); err != 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
} }
return nil
} }
func (t *templateHandler) initFuncs() { func (t *templateHandler) initFuncs() {
@ -553,12 +570,12 @@ func (t *templateHandler) getTemplateHandler(name string) templateLoader {
return t.html return t.html
} }
func (t *templateHandler) handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (string, error)) error { func (t *templateHandler) handleMaster(name, overlayFilename, masterFilename string, onMissing func(filename string) (templateInfo, error)) error {
h := t.getTemplateHandler(name) h := t.getTemplateHandler(name)
return h.handleMaster(name, overlayFilename, masterFilename, onMissing) 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) masterTpl := t.lookup(masterFilename)
@ -568,9 +585,9 @@ func (t *htmlTemplates) handleMaster(name, overlayFilename, masterFilename strin
return err return err
} }
masterTpl, err = t.t.New(overlayFilename).Parse(templ) masterTpl, err = t.t.New(overlayFilename).Parse(templ.template)
if err != nil { if err != nil {
return err return templ.errWithFileContext("parse master failed", err)
} }
} }
@ -579,9 +596,9 @@ func (t *htmlTemplates) handleMaster(name, overlayFilename, masterFilename strin
return err return err
} }
overlayTpl, err := template.Must(masterTpl.Clone()).Parse(templ) overlayTpl, err := template.Must(masterTpl.Clone()).Parse(templ.template)
if err != nil { if err != nil {
return err return templ.errWithFileContext("parse failed", err)
} }
// The extra lookup is a workaround, see // The extra lookup is a workaround, see
@ -593,12 +610,13 @@ func (t *htmlTemplates) handleMaster(name, overlayFilename, masterFilename strin
} }
t.overlays[name] = overlayTpl t.overlays[name] = overlayTpl
t.nameBaseTemplateName[name] = masterFilename
return err return err
} }
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) name = strings.TrimPrefix(name, textTmplNamePrefix)
masterTpl := t.lookup(masterFilename) masterTpl := t.lookup(masterFilename)
@ -609,10 +627,11 @@ func (t *textTemplates) handleMaster(name, overlayFilename, masterFilename strin
return err return err
} }
masterTpl, err = t.t.New(overlayFilename).Parse(templ) masterTpl, err = t.t.New(masterFilename).Parse(templ.template)
if err != nil { if err != nil {
return err return errors.Wrapf(err, "failed to parse %q:", templ.filename)
} }
t.nameBaseTemplateName[masterFilename] = templ.filename
} }
templ, err := onMissing(overlayFilename) templ, err := onMissing(overlayFilename)
@ -620,9 +639,9 @@ func (t *textTemplates) handleMaster(name, overlayFilename, masterFilename strin
return err return err
} }
overlayTpl, err := texttemplate.Must(masterTpl.Clone()).Parse(templ) overlayTpl, err := texttemplate.Must(masterTpl.Clone()).Parse(templ.template)
if err != nil { if err != nil {
return err return errors.Wrapf(err, "failed to parse %q:", templ.filename)
} }
overlayTpl = overlayTpl.Lookup(overlayTpl.Name()) overlayTpl = overlayTpl.Lookup(overlayTpl.Name())
@ -630,6 +649,7 @@ func (t *textTemplates) handleMaster(name, overlayFilename, masterFilename strin
return err return err
} }
t.overlays[name] = overlayTpl t.overlays[name] = overlayTpl
t.nameBaseTemplateName[name] = templ.filename
return err return err
@ -640,14 +660,22 @@ 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) 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) fs := t.Layouts.Fs
b, err := afero.ReadFile(fs, filename)
if err != nil { if err != nil {
return "", err return templateInfo{filename: filename, fs: fs}, err
} }
s := string(b) s := string(b)
return s, nil realFilename := filename
if fi, err := fs.Stat(filename); err == nil {
if fir, ok := fi.(hugofs.RealFilenameInfo); ok {
realFilename = fir.RealFilename()
}
}
return templateInfo{template: s, filename: filename, realFilename: realFilename, fs: fs}, nil
} }
// get the suffix and switch on that // get the suffix and switch on that
@ -712,7 +740,11 @@ func (t *templateHandler) addTemplateFile(name, baseTemplatePath, path string) e
return err return err
} }
return t.AddTemplate(name, templ) err = t.AddTemplate(name, templ.template)
if err != nil {
return templ.errWithFileContext("parse failed", err)
}
return nil
} }
} }
@ -720,19 +752,24 @@ var embeddedTemplatesAliases = map[string][]string{
"shortcodes/twitter.html": []string{"shortcodes/tweet.html"}, "shortcodes/twitter.html": []string{"shortcodes/tweet.html"},
} }
func (t *templateHandler) loadEmbedded() { func (t *templateHandler) loadEmbedded() error {
for _, kv := range embedded.EmbeddedTemplates { for _, kv := range embedded.EmbeddedTemplates {
// TODO(bep) error handling
name, templ := kv[0], kv[1] 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 { if aliases, found := embeddedTemplatesAliases[name]; found {
for _, alias := range aliases { 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 { func (t *templateHandler) addInternalTemplate(name, tpl string) error {

View file

@ -33,12 +33,15 @@ func (*TemplateProvider) Update(deps *deps.Deps) error {
deps.TextTmpl = newTmpl.NewTextTemplate() deps.TextTmpl = newTmpl.NewTextTemplate()
newTmpl.initFuncs() newTmpl.initFuncs()
newTmpl.loadEmbedded()
if err := newTmpl.loadEmbedded(); err != nil {
return err
}
if deps.WithTemplate != nil { if deps.WithTemplate != nil {
err := deps.WithTemplate(newTmpl) err := deps.WithTemplate(newTmpl)
if err != nil { if err != nil {
newTmpl.addError("init", err) return err
} }
} }

View file

@ -0,0 +1,46 @@
// 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 tplimpl
import (
"github.com/gohugoio/hugo/common/herrors"
"github.com/pkg/errors"
"github.com/spf13/afero"
)
type templateInfo struct {
template string
// Used to create some error context in error situations
fs afero.Fs
// The filename relative to the fs above.
filename string
// The real filename (if possible). Used for logging.
realFilename string
}
func (info templateInfo) errWithFileContext(what string, err error) error {
err = errors.Wrapf(err, "file %q: %s:", info.realFilename, what)
err, _ = herrors.WithFileContextForFile(
err,
info.filename,
info.fs,
"go-html-template",
herrors.SimpleLineMatcher)
return err
}

View file

@ -21,10 +21,7 @@ import (
"testing" "testing"
"time" "time"
"io/ioutil" "github.com/gohugoio/hugo/common/loggers"
"log"
"os"
"github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/deps"
"github.com/gohugoio/hugo/helpers" "github.com/gohugoio/hugo/helpers"
@ -35,13 +32,12 @@ import (
"github.com/gohugoio/hugo/tpl/internal" "github.com/gohugoio/hugo/tpl/internal"
"github.com/gohugoio/hugo/tpl/partials" "github.com/gohugoio/hugo/tpl/partials"
"github.com/spf13/afero" "github.com/spf13/afero"
jww "github.com/spf13/jwalterweatherman"
"github.com/spf13/viper" "github.com/spf13/viper"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
var ( var (
logger = jww.NewNotepad(jww.LevelFatal, jww.LevelFatal, os.Stdout, ioutil.Discard, "", log.Ldate|log.Ltime) logger = loggers.NewErrorLogger()
) )
func newTestConfig() config.Provider { func newTestConfig() config.Provider {

View file

@ -17,11 +17,12 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/russross/blackfriday"
"html/template" "html/template"
"net/url" "net/url"
_errors "github.com/pkg/errors"
"github.com/russross/blackfriday"
"github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/deps"
"github.com/spf13/cast" "github.com/spf13/cast"
) )
@ -55,7 +56,7 @@ func (ns *Namespace) AbsURL(a interface{}) (template.HTML, error) {
func (ns *Namespace) Parse(rawurl interface{}) (*url.URL, error) { func (ns *Namespace) Parse(rawurl interface{}) (*url.URL, error) {
s, err := cast.ToStringE(rawurl) s, err := cast.ToStringE(rawurl)
if err != nil { if err != nil {
return nil, fmt.Errorf("Error in Parse: %s", err) return nil, _errors.Wrap(err, "Error in Parse")
} }
return url.Parse(s) return url.Parse(s)