hugolib: Improve error and reload handling of hook templates in server mode
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Fri, 20 Dec 2019 07:11:36 +0000 (08:11 +0100)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Fri, 20 Dec 2019 10:38:44 +0000 (11:38 +0100)
Fixes #6635

commands/commandeer.go
commands/hugo.go
commands/server.go
hugolib/content_render_hooks_test.go
hugolib/hugo_sites.go
hugolib/page.go
hugolib/site.go
hugolib/testhelpers_test.go

index af711fdd68bd22a7baed01efa168481098e7b63d..c9059dd0c72da2d7ef410966e689af906d6376ec 100644 (file)
@@ -88,6 +88,7 @@ type commandeer struct {
        doLiveReload        bool
        fastRenderMode      bool
        showErrorInBrowser  bool
+       wasError            bool
 
        configured bool
        paused     bool
index 7c831db5648934c678b25b051ddf7ab02201a4c4..545daa83c1e070a95dd0e648894301fa6a826e67 100644 (file)
@@ -718,6 +718,9 @@ func (c *commandeer) handleBuildErr(err error, msg string) {
 
 func (c *commandeer) rebuildSites(events []fsnotify.Event) error {
        defer c.timeTrack(time.Now(), "Total")
+       defer func() {
+               c.wasError = false
+       }()
 
        c.buildErr = nil
        visited := c.visitedURLs.PeekAllSet()
@@ -734,16 +737,19 @@ func (c *commandeer) rebuildSites(events []fsnotify.Event) error {
                }
 
        }
-       return c.hugo().Build(hugolib.BuildCfg{RecentlyVisited: visited}, events...)
+       return c.hugo().Build(hugolib.BuildCfg{RecentlyVisited: visited, ErrRecovery: c.wasError}, events...)
 }
 
 func (c *commandeer) partialReRender(urls ...string) error {
+       defer func() {
+               c.wasError = false
+       }()
        c.buildErr = nil
        visited := make(map[string]bool)
        for _, url := range urls {
                visited[url] = true
        }
-       return c.hugo().Build(hugolib.BuildCfg{RecentlyVisited: visited, PartialReRender: true})
+       return c.hugo().Build(hugolib.BuildCfg{RecentlyVisited: visited, PartialReRender: true, ErrRecovery: c.wasError})
 }
 
 func (c *commandeer) fullRebuild(changeType string) {
index 7d884096c49e63ab12ca32457f349d2fd62cfa63..64409ee18540374ad05ddbc13e321cb5e03091ed 100644 (file)
@@ -334,6 +334,7 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, string, string, erro
                                // First check the error state
                                err := f.c.getErrorWithContext()
                                if err != nil {
+                                       f.c.wasError = true
                                        w.WriteHeader(500)
                                        r, err := f.errorTemplate(err)
                                        if err != nil {
index d206013ba8ffeb00b8dc09f86936222d2b50e43b..ee7a02074b9c29ac7a8fdf7a3b9a8b4f15c8ae95 100644 (file)
@@ -158,6 +158,60 @@ SHORT3|
 
 }
 
+func TestRenderHooksDeleteTemplate(t *testing.T) {
+       config := `
+baseURL="https://example.org"
+workingDir="/mywork"
+`
+       b := newTestSitesBuilder(t).WithWorkingDir("/mywork").WithConfigFile("toml", config).Running()
+       b.WithTemplatesAdded("_default/single.html", `{{ .Content }}`)
+       b.WithTemplatesAdded("_default/_markup/render-link.html", `html-render-link`)
+
+       b.WithContent("p1.md", `---
+title: P1
+---
+[First Link](https://www.google.com "Google's Homepage")
+
+`)
+       b.Build(BuildCfg{})
+
+       b.AssertFileContent("public/p1/index.html", `<p>html-render-link</p>`)
+
+       b.RemoveFiles(
+               "layouts/_default/_markup/render-link.html",
+       )
+
+       b.Build(BuildCfg{})
+       b.AssertFileContent("public/p1/index.html", `<p><a href="https://www.google.com" title="Google's Homepage">First Link</a></p>`)
+
+}
+
+func TestRenderHookAddTemplate(t *testing.T) {
+       config := `
+baseURL="https://example.org"
+workingDir="/mywork"
+`
+       b := newTestSitesBuilder(t).WithWorkingDir("/mywork").WithConfigFile("toml", config).Running()
+       b.WithTemplatesAdded("_default/single.html", `{{ .Content }}`)
+
+       b.WithContent("p1.md", `---
+title: P1
+---
+[First Link](https://www.google.com "Google's Homepage")
+
+`)
+       b.Build(BuildCfg{})
+
+       b.AssertFileContent("public/p1/index.html", `<p><a href="https://www.google.com" title="Google's Homepage">First Link</a></p>`)
+
+       b.EditFiles("layouts/_default/_markup/render-link.html", `html-render-link`)
+
+       b.Build(BuildCfg{})
+
+       b.AssertFileContent("public/p1/index.html", `<p>html-render-link</p>`)
+
+}
+
 func TestRenderHooksRSS(t *testing.T) {
 
        b := newTestSitesBuilder(t)
index 526f39fca9a0599cf5fc0b20e6cd3d78d5ec7cf4..8c29e2a88219708929419652efc86b6d67b5933a 100644 (file)
@@ -564,6 +564,9 @@ type BuildCfg struct {
        // we should skip most of the processing.
        PartialReRender bool
 
+       // Set in server mode when the last build failed for some reason.
+       ErrRecovery bool
+
        // Recently visited URLs. This is used for partial re-rendering.
        RecentlyVisited map[string]bool
 }
@@ -807,8 +810,20 @@ func (h *HugoSites) findPagesByKindIn(kind string, inPages page.Pages) page.Page
        return h.Sites[0].findPagesByKindIn(kind, inPages)
 }
 
-func (h *HugoSites) resetPageStateFromEvents(idset identity.Identities) {
+func (h *HugoSites) resetPageState() {
+       for _, s := range h.Sites {
+               for _, p := range s.rawAllPages {
+                       for _, po := range p.pageOutputs {
+                               if po.cp == nil {
+                                       continue
+                               }
+                               po.cp.Reset()
+                       }
+               }
+       }
+}
 
+func (h *HugoSites) resetPageStateFromEvents(idset identity.Identities) {
        for _, s := range h.Sites {
        PAGES:
                for _, p := range s.rawAllPages {
@@ -820,7 +835,6 @@ func (h *HugoSites) resetPageStateFromEvents(idset identity.Identities) {
                                for id, _ := range idset {
                                        if po.cp.dependencyTracker.Search(id) != nil {
                                                po.cp.Reset()
-                                               p.forceRender = true
                                                continue OUTPUTS
                                        }
                                }
@@ -834,7 +848,6 @@ func (h *HugoSites) resetPageStateFromEvents(idset identity.Identities) {
                                                                po.cp.Reset()
                                                        }
                                                }
-                                               p.forceRender = true
                                                continue PAGES
                                        }
                                }
index fb3b597be3b8fb3a3a22b0fa68c84c8d1cfcee79..af3deb59ffeafb058d8bcfc85b9350354793d8d3 100644 (file)
@@ -629,9 +629,12 @@ func (p *pageState) Render(layout ...string) (template.HTML, error) {
 
 }
 
-// wrapError adds some more context to the given error if possible
+// wrapError adds some more context to the given error if possible/needed
 func (p *pageState) wrapError(err error) error {
-
+       if _, ok := err.(*herrors.ErrorWithFileContext); ok {
+               // Preserve the first file context.
+               return err
+       }
        var filename string
        if !p.File().IsZero() {
                filename = p.File().Filename()
index 866ff56248c6c106406d99182b718682f01314d6..eb232c629eab55eca11883c6415527acf74a5db7 100644 (file)
@@ -909,6 +909,7 @@ func (s *Site) processPartial(config *BuildCfg, init func(config *BuildCfg) erro
                contentFilesChanged []string
 
                tmplChanged bool
+               tmplAdded   bool
                dataChanged bool
                i18nChanged bool
 
@@ -934,8 +935,16 @@ func (s *Site) processPartial(config *BuildCfg, init func(config *BuildCfg) erro
                                logger.Println("Source changed", ev)
                                sourceChanged = append(sourceChanged, ev)
                        case files.ComponentFolderLayouts:
-                               logger.Println("Template changed", ev)
                                tmplChanged = true
+                               if _, found := s.Tmpl.Lookup(id.Path); !found {
+                                       tmplAdded = true
+                               }
+                               if tmplAdded {
+                                       logger.Println("Template added", ev)
+                               } else {
+                                       logger.Println("Template changed", ev)
+                               }
+
                        case files.ComponentFolderData:
                                logger.Println("Data changed", ev)
                                dataChanged = true
@@ -1021,7 +1030,11 @@ func (s *Site) processPartial(config *BuildCfg, init func(config *BuildCfg) erro
                sourceFilesChanged[ev.Name] = true
        }
 
-       h.resetPageStateFromEvents(changeIdentities)
+       if config.ErrRecovery || tmplAdded {
+               h.resetPageState()
+       } else {
+               h.resetPageStateFromEvents(changeIdentities)
+       }
 
        if len(sourceReallyChanged) > 0 || len(contentFilesChanged) > 0 {
                var filenamesChanged []string
index 80aafe052eff7ea0b8b3d8f49225ab96711641a2..93ea9477cecb928116eacfa720e6a2dc69a8fc09 100644 (file)
@@ -68,6 +68,7 @@ type sitesBuilder struct {
 
        // Used to test partial rebuilds.
        changedFiles []string
+       removedFiles []string
 
        // Aka the Hugo server mode.
        running bool
@@ -386,16 +387,22 @@ func (s *sitesBuilder) WithI18nAdded(filenameContent ...string) *sitesBuilder {
 }
 
 func (s *sitesBuilder) EditFiles(filenameContent ...string) *sitesBuilder {
-       var changedFiles []string
        for i := 0; i < len(filenameContent); i += 2 {
                filename, content := filepath.FromSlash(filenameContent[i]), filenameContent[i+1]
                absFilename := s.absFilename(filename)
-               changedFiles = append(changedFiles, absFilename)
+               s.changedFiles = append(s.changedFiles, absFilename)
                writeSource(s.T, s.Fs, absFilename, content)
 
        }
-       s.changedFiles = changedFiles
+       return s
+}
 
+func (s *sitesBuilder) RemoveFiles(filenames ...string) *sitesBuilder {
+       for _, filename := range filenames {
+               absFilename := s.absFilename(filename)
+               s.removedFiles = append(s.removedFiles, absFilename)
+               s.Assert(s.Fs.Source.Remove(absFilename), qt.IsNil)
+       }
        return s
 }
 
@@ -523,17 +530,20 @@ func (s *sitesBuilder) BuildFail(cfg BuildCfg) *sitesBuilder {
 }
 
 func (s *sitesBuilder) changeEvents() []fsnotify.Event {
-       if len(s.changedFiles) == 0 {
-               return nil
-       }
 
-       events := make([]fsnotify.Event, len(s.changedFiles))
-       // TODO(bep) remove?
-       for i, v := range s.changedFiles {
-               events[i] = fsnotify.Event{
+       var events []fsnotify.Event
+
+       for _, v := range s.changedFiles {
+               events = append(events, fsnotify.Event{
                        Name: v,
                        Op:   fsnotify.Write,
-               }
+               })
+       }
+       for _, v := range s.removedFiles {
+               events = append(events, fsnotify.Event{
+                       Name: v,
+                       Op:   fsnotify.Remove,
+               })
        }
 
        return events