aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorBjørn Erik Pedersen <[email protected]>2024-12-13 13:57:23 +0100
committerBjørn Erik Pedersen <[email protected]>2024-12-16 18:03:04 +0100
commita5e5be234c33016cd44a611ea4b8c6e57e2468e4 (patch)
treed950bc960d1e511ded3a8df1dafeed2eadfe8ebe
parent565c30eac9e00b2ebcbdbb8e05b5e8238a15fefb (diff)
downloadhugo-a5e5be234c33016cd44a611ea4b8c6e57e2468e4.tar.gz
hugo-a5e5be234c33016cd44a611ea4b8c6e57e2468e4.zip
Fix panic on server rebuilds when using both base templates and template.Defer
Fixes #12963
-rw-r--r--common/types/evictingqueue.go3
-rw-r--r--hugolib/integrationtest_builder.go27
-rw-r--r--internal/js/esbuild/batch_integration_test.go40
-rw-r--r--tpl/tplimpl/template.go56
-rw-r--r--tpl/tplimpl/tplimpl_integration_test.go49
5 files changed, 110 insertions, 65 deletions
diff --git a/common/types/evictingqueue.go b/common/types/evictingqueue.go
index 88add59d5..5ab715aa8 100644
--- a/common/types/evictingqueue.go
+++ b/common/types/evictingqueue.go
@@ -65,6 +65,9 @@ func (q *EvictingStringQueue) Len() int {
// Contains returns whether the queue contains v.
func (q *EvictingStringQueue) Contains(v string) bool {
+ if q == nil {
+ return false
+ }
q.mu.Lock()
defer q.mu.Unlock()
return q.set[v]
diff --git a/hugolib/integrationtest_builder.go b/hugolib/integrationtest_builder.go
index b593cd965..7a6c040b1 100644
--- a/hugolib/integrationtest_builder.go
+++ b/hugolib/integrationtest_builder.go
@@ -25,6 +25,7 @@ import (
"github.com/gohugoio/hugo/common/hexec"
"github.com/gohugoio/hugo/common/loggers"
"github.com/gohugoio/hugo/common/maps"
+ "github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/config/allconfig"
"github.com/gohugoio/hugo/config/security"
@@ -466,6 +467,28 @@ func (s *IntegrationTestBuilder) Build() *IntegrationTestBuilder {
return s
}
+func (s *IntegrationTestBuilder) BuildPartial(urls ...string) *IntegrationTestBuilder {
+ if _, err := s.BuildPartialE(urls...); err != nil {
+ s.Fatal(err)
+ }
+ return s
+}
+
+func (s *IntegrationTestBuilder) BuildPartialE(urls ...string) (*IntegrationTestBuilder, error) {
+ if s.buildCount == 0 {
+ panic("BuildPartial can only be used after a full build")
+ }
+ if !s.Cfg.Running {
+ panic("BuildPartial can only be used in server mode")
+ }
+ visited := types.NewEvictingStringQueue(len(urls))
+ for _, url := range urls {
+ visited.Add(url)
+ }
+ buildCfg := BuildCfg{RecentlyVisited: visited, PartialReRender: true}
+ return s, s.build(buildCfg)
+}
+
func (s *IntegrationTestBuilder) Close() {
s.Helper()
s.Assert(s.H.Close(), qt.IsNil)
@@ -747,10 +770,6 @@ func (s *IntegrationTestBuilder) build(cfg BuildCfg) error {
s.counters = &buildCounters{}
cfg.testCounters = s.counters
- if s.buildCount > 0 && (len(changeEvents) == 0) {
- return nil
- }
-
s.buildCount++
err := s.H.Build(cfg, changeEvents...)
diff --git a/internal/js/esbuild/batch_integration_test.go b/internal/js/esbuild/batch_integration_test.go
index 55528bdf0..b4a2454ac 100644
--- a/internal/js/esbuild/batch_integration_test.go
+++ b/internal/js/esbuild/batch_integration_test.go
@@ -721,43 +721,3 @@ console.log("config.params.id", id3);
b.EditFileReplaceAll("assets/other/bar.css", ".bar-edit {", ".bar-edit2 {").Build()
b.AssertFileContent("public/mybundle/reactbatch.css", ".bar-edit2 {")
}
-
-func TestEditBaseofManyTimes(t *testing.T) {
- files := `
--- hugo.toml --
-baseURL = "https://example.com"
-disableLiveReload = true
-disableKinds = ["taxonomy", "term"]
--- layouts/_default/baseof.html --
-Baseof.
-{{ block "main" . }}{{ end }}
-{{ with (templates.Defer (dict "key" "global")) }}
-Now. {{ now }}
-{{ end }}
--- layouts/_default/single.html --
-{{ define "main" }}
-Single.
-{{ end }}
---
--- layouts/_default/list.html --
-{{ define "main" }}
-List.
-{{ end }}
--- content/mybundle/index.md --
----
-title: "My Bundle"
----
--- content/_index.md --
----
-title: "Home"
----
-`
-
- b := hugolib.TestRunning(t, files)
- b.AssertFileContent("public/index.html", "Baseof.")
-
- for i := 0; i < 100; i++ {
- b.EditFileReplaceAll("layouts/_default/baseof.html", "Now", "Now.").Build()
- b.AssertFileContent("public/index.html", "Now..")
- }
-}
diff --git a/tpl/tplimpl/template.go b/tpl/tplimpl/template.go
index 9e2af046d..0a593593b 100644
--- a/tpl/tplimpl/template.go
+++ b/tpl/tplimpl/template.go
@@ -30,6 +30,7 @@ import (
"unicode"
"unicode/utf8"
+ "github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/output/layouts"
@@ -191,8 +192,10 @@ func newTemplateHandlers(d *deps.Deps) (*tpl.TemplateHandlers, error) {
func newTemplateNamespace(funcs map[string]any) *templateNamespace {
return &templateNamespace{
- prototypeHTML: htmltemplate.New("").Funcs(funcs),
- prototypeText: texttemplate.New("").Funcs(funcs),
+ prototypeHTML: htmltemplate.New("").Funcs(funcs),
+ prototypeText: texttemplate.New("").Funcs(funcs),
+ prototypeHTMLCloneCache: maps.NewCache[prototypeCloneID, *htmltemplate.Template](),
+ prototypeTextCloneCache: maps.NewCache[prototypeCloneID, *texttemplate.Template](),
templateStateMap: &templateStateMap{
templates: make(map[string]*templateState),
},
@@ -688,7 +691,7 @@ func (t *templateHandler) addTemplateTo(info templateInfo, to *templateNamespace
func (t *templateHandler) applyBaseTemplate(overlay, base templateInfo) (tpl.Template, error) {
if overlay.isText {
var (
- templ = t.main.prototypeTextClone.New(overlay.name)
+ templ = t.main.getPrototypeText(prototypeCloneIDBaseof).New(overlay.name)
err error
)
@@ -713,7 +716,7 @@ func (t *templateHandler) applyBaseTemplate(overlay, base templateInfo) (tpl.Tem
}
var (
- templ = t.main.prototypeHTMLClone.New(overlay.name)
+ templ = t.main.getPrototypeHTML(prototypeCloneIDBaseof).New(overlay.name)
err error
)
@@ -953,27 +956,37 @@ func (t *templateHandler) postTransform() error {
return nil
}
+type prototypeCloneID uint16
+
+const (
+ prototypeCloneIDBaseof prototypeCloneID = iota + 1
+ prototypeCloneIDDefer
+)
+
type templateNamespace struct {
- prototypeText *texttemplate.Template
- prototypeHTML *htmltemplate.Template
- prototypeTextClone *texttemplate.Template
- prototypeHTMLClone *htmltemplate.Template
+ prototypeText *texttemplate.Template
+ prototypeHTML *htmltemplate.Template
+
+ prototypeHTMLCloneCache *maps.Cache[prototypeCloneID, *htmltemplate.Template]
+ prototypeTextCloneCache *maps.Cache[prototypeCloneID, *texttemplate.Template]
*templateStateMap
}
-func (t *templateNamespace) getPrototypeText() *texttemplate.Template {
- if t.prototypeTextClone != nil {
- return t.prototypeTextClone
+func (t *templateNamespace) getPrototypeText(id prototypeCloneID) *texttemplate.Template {
+ v, ok := t.prototypeTextCloneCache.Get(id)
+ if !ok {
+ return t.prototypeText
}
- return t.prototypeText
+ return v
}
-func (t *templateNamespace) getPrototypeHTML() *htmltemplate.Template {
- if t.prototypeHTMLClone != nil {
- return t.prototypeHTMLClone
+func (t *templateNamespace) getPrototypeHTML(id prototypeCloneID) *htmltemplate.Template {
+ v, ok := t.prototypeHTMLCloneCache.Get(id)
+ if !ok {
+ return t.prototypeHTML
}
- return t.prototypeHTML
+ return v
}
func (t *templateNamespace) Lookup(name string) (tpl.Template, bool) {
@@ -989,9 +1002,10 @@ func (t *templateNamespace) Lookup(name string) (tpl.Template, bool) {
}
func (t *templateNamespace) createPrototypes() error {
- t.prototypeTextClone = texttemplate.Must(t.prototypeText.Clone())
- t.prototypeHTMLClone = htmltemplate.Must(t.prototypeHTML.Clone())
-
+ for _, id := range []prototypeCloneID{prototypeCloneIDBaseof, prototypeCloneIDDefer} {
+ t.prototypeHTMLCloneCache.Set(id, htmltemplate.Must(t.prototypeHTML.Clone()))
+ t.prototypeTextCloneCache.Set(id, texttemplate.Must(t.prototypeText.Clone()))
+ }
return nil
}
@@ -1021,7 +1035,7 @@ func (t *templateNamespace) addDeferredTemplate(owner *templateState, name strin
var templ tpl.Template
if owner.isText() {
- prototype := t.getPrototypeText()
+ prototype := t.getPrototypeText(prototypeCloneIDDefer)
tt, err := prototype.New(name).Parse("")
if err != nil {
return fmt.Errorf("failed to parse empty text template %q: %w", name, err)
@@ -1029,7 +1043,7 @@ func (t *templateNamespace) addDeferredTemplate(owner *templateState, name strin
tt.Tree.Root = n
templ = tt
} else {
- prototype := t.getPrototypeHTML()
+ prototype := t.getPrototypeHTML(prototypeCloneIDDefer)
tt, err := prototype.New(name).Parse("")
if err != nil {
return fmt.Errorf("failed to parse empty HTML template %q: %w", name, err)
diff --git a/tpl/tplimpl/tplimpl_integration_test.go b/tpl/tplimpl/tplimpl_integration_test.go
index 1e7aa3111..dbadece4e 100644
--- a/tpl/tplimpl/tplimpl_integration_test.go
+++ b/tpl/tplimpl/tplimpl_integration_test.go
@@ -649,3 +649,52 @@ E: An _emphasized_ word.
"<details>\n <summary>Details</summary>\n <p>D: An <em>emphasized</em> word.</p>\n</details>",
)
}
+
+// Issue 12963
+func TestEditBaseofParseAfterExecute(t *testing.T) {
+ files := `
+-- hugo.toml --
+baseURL = "https://example.com"
+disableLiveReload = true
+disableKinds = ["taxonomy", "term", "rss", "404", "sitemap"]
+[internal]
+fastRenderMode = true
+-- layouts/_default/baseof.html --
+Baseof!
+{{ block "main" . }}default{{ end }}
+{{ with (templates.Defer (dict "key" "global")) }}
+Now. {{ now }}
+{{ end }}
+-- layouts/_default/single.html --
+{{ define "main" }}
+Single.
+{{ end }}
+-- layouts/_default/list.html --
+{{ define "main" }}
+List.
+{{ .Content }}
+{{ range .Pages }}{{ .Title }}{{ end }}|
+{{ end }}
+-- content/mybundle1/index.md --
+---
+title: "My Bundle 1"
+---
+-- content/mybundle2/index.md --
+---
+title: "My Bundle 2"
+---
+-- content/_index.md --
+---
+title: "Home"
+---
+Home!
+`
+
+ b := hugolib.TestRunning(t, files)
+ b.AssertFileContent("public/index.html", "Home!")
+ b.EditFileReplaceAll("layouts/_default/baseof.html", "Baseof", "Baseof!").Build()
+ b.BuildPartial("/")
+ b.AssertFileContent("public/index.html", "Baseof!!")
+ b.BuildPartial("/mybundle1/")
+ b.AssertFileContent("public/mybundle1/index.html", "Baseof!!")
+}