]> git.maquefel.me Git - brevno-suite/hugo/commitdiff
Fix panic on server rebuilds when using both base templates and template.Defer
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Fri, 13 Dec 2024 12:57:23 +0000 (13:57 +0100)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Mon, 16 Dec 2024 17:03:04 +0000 (18:03 +0100)
Fixes #12963

common/types/evictingqueue.go
hugolib/integrationtest_builder.go
internal/js/esbuild/batch_integration_test.go
tpl/tplimpl/template.go
tpl/tplimpl/tplimpl_integration_test.go

index 88add59d53929f4490be2f22e1f5a129de9dcdce..5ab715aa871da2c470bd8262074ed6916500c0a7 100644 (file)
@@ -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]
index b593cd965778376c83c644fd2b17bfe42c82214a..7a6c040b16abf8d14aefd37023aa33e0f661b8bc 100644 (file)
@@ -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...)
index 55528bdf0426029f1fac364645699cd96ff71408..b4a2454acf337d40bcf28ff03e61c2afe3ae9b31 100644 (file)
@@ -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..")
-       }
-}
index 9e2af046dde5044f32aa8ef20aa74ff0719e48c5..0a593593b88e0a5eb908fc95ec4a44db0c6ca589 100644 (file)
@@ -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)
index 1e7aa31113251cee603929950e2a2fc843fc1a52..dbadece4e52988119389ce570d185c5683fe0042 100644 (file)
@@ -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!!")
+}