hugo

Unnamed repository; edit this file 'description' to name the repository.

git clone git://git.shimmy1996.com/hugo.git
commit 8a58ebb311fd079f65068e7e37725e4d43f17ab5
parent 0453683816cfbc94e1e19c644f5f84213bb8cf35
Author: Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Date:   Fri, 20 Dec 2019 08:11:36 +0100

hugolib: Improve error and reload handling  of hook templates in server mode

Fixes #6635

Diffstat:
Mcommands/commandeer.go | 1+
Mcommands/hugo.go | 10++++++++--
Mcommands/server.go | 1+
Mhugolib/content_render_hooks_test.go | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mhugolib/hugo_sites.go | 19++++++++++++++++---
Mhugolib/page.go | 7+++++--
Mhugolib/site.go | 17+++++++++++++++--
Mhugolib/testhelpers_test.go | 32+++++++++++++++++++++-----------
8 files changed, 121 insertions(+), 20 deletions(-)
diff --git a/commands/commandeer.go b/commands/commandeer.go
@@ -88,6 +88,7 @@ type commandeer struct {
 	doLiveReload        bool
 	fastRenderMode      bool
 	showErrorInBrowser  bool
+	wasError            bool
 
 	configured bool
 	paused     bool
diff --git a/commands/hugo.go b/commands/hugo.go
@@ -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) {
diff --git a/commands/server.go b/commands/server.go
@@ -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 {
diff --git a/hugolib/content_render_hooks_test.go b/hugolib/content_render_hooks_test.go
@@ -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)
diff --git a/hugolib/hugo_sites.go b/hugolib/hugo_sites.go
@@ -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
 					}
 				}
diff --git a/hugolib/page.go b/hugolib/page.go
@@ -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()
diff --git a/hugolib/site.go b/hugolib/site.go
@@ -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
diff --git a/hugolib/testhelpers_test.go b/hugolib/testhelpers_test.go
@@ -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