Skip to content

Commit a5e5be2

Browse files
committedDec 16, 2024·
Fix panic on server rebuilds when using both base templates and template.Defer
Fixes #12963
1 parent 565c30e commit a5e5be2

File tree

5 files changed

+110
-65
lines changed

5 files changed

+110
-65
lines changed
 

‎common/types/evictingqueue.go

+3
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ func (q *EvictingStringQueue) Len() int {
6565

6666
// Contains returns whether the queue contains v.
6767
func (q *EvictingStringQueue) Contains(v string) bool {
68+
if q == nil {
69+
return false
70+
}
6871
q.mu.Lock()
6972
defer q.mu.Unlock()
7073
return q.set[v]

‎hugolib/integrationtest_builder.go

+23-4
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"github.com/gohugoio/hugo/common/hexec"
2626
"github.com/gohugoio/hugo/common/loggers"
2727
"github.com/gohugoio/hugo/common/maps"
28+
"github.com/gohugoio/hugo/common/types"
2829
"github.com/gohugoio/hugo/config"
2930
"github.com/gohugoio/hugo/config/allconfig"
3031
"github.com/gohugoio/hugo/config/security"
@@ -466,6 +467,28 @@ func (s *IntegrationTestBuilder) Build() *IntegrationTestBuilder {
466467
return s
467468
}
468469

470+
func (s *IntegrationTestBuilder) BuildPartial(urls ...string) *IntegrationTestBuilder {
471+
if _, err := s.BuildPartialE(urls...); err != nil {
472+
s.Fatal(err)
473+
}
474+
return s
475+
}
476+
477+
func (s *IntegrationTestBuilder) BuildPartialE(urls ...string) (*IntegrationTestBuilder, error) {
478+
if s.buildCount == 0 {
479+
panic("BuildPartial can only be used after a full build")
480+
}
481+
if !s.Cfg.Running {
482+
panic("BuildPartial can only be used in server mode")
483+
}
484+
visited := types.NewEvictingStringQueue(len(urls))
485+
for _, url := range urls {
486+
visited.Add(url)
487+
}
488+
buildCfg := BuildCfg{RecentlyVisited: visited, PartialReRender: true}
489+
return s, s.build(buildCfg)
490+
}
491+
469492
func (s *IntegrationTestBuilder) Close() {
470493
s.Helper()
471494
s.Assert(s.H.Close(), qt.IsNil)
@@ -747,10 +770,6 @@ func (s *IntegrationTestBuilder) build(cfg BuildCfg) error {
747770
s.counters = &buildCounters{}
748771
cfg.testCounters = s.counters
749772

750-
if s.buildCount > 0 && (len(changeEvents) == 0) {
751-
return nil
752-
}
753-
754773
s.buildCount++
755774

756775
err := s.H.Build(cfg, changeEvents...)

‎internal/js/esbuild/batch_integration_test.go

-40
Original file line numberDiff line numberDiff line change
@@ -721,43 +721,3 @@ console.log("config.params.id", id3);
721721
b.EditFileReplaceAll("assets/other/bar.css", ".bar-edit {", ".bar-edit2 {").Build()
722722
b.AssertFileContent("public/mybundle/reactbatch.css", ".bar-edit2 {")
723723
}
724-
725-
func TestEditBaseofManyTimes(t *testing.T) {
726-
files := `
727-
-- hugo.toml --
728-
baseURL = "https://example.com"
729-
disableLiveReload = true
730-
disableKinds = ["taxonomy", "term"]
731-
-- layouts/_default/baseof.html --
732-
Baseof.
733-
{{ block "main" . }}{{ end }}
734-
{{ with (templates.Defer (dict "key" "global")) }}
735-
Now. {{ now }}
736-
{{ end }}
737-
-- layouts/_default/single.html --
738-
{{ define "main" }}
739-
Single.
740-
{{ end }}
741-
--
742-
-- layouts/_default/list.html --
743-
{{ define "main" }}
744-
List.
745-
{{ end }}
746-
-- content/mybundle/index.md --
747-
---
748-
title: "My Bundle"
749-
---
750-
-- content/_index.md --
751-
---
752-
title: "Home"
753-
---
754-
`
755-
756-
b := hugolib.TestRunning(t, files)
757-
b.AssertFileContent("public/index.html", "Baseof.")
758-
759-
for i := 0; i < 100; i++ {
760-
b.EditFileReplaceAll("layouts/_default/baseof.html", "Now", "Now.").Build()
761-
b.AssertFileContent("public/index.html", "Now..")
762-
}
763-
}

‎tpl/tplimpl/template.go

+35-21
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030
"unicode"
3131
"unicode/utf8"
3232

33+
"github.com/gohugoio/hugo/common/maps"
3334
"github.com/gohugoio/hugo/common/types"
3435
"github.com/gohugoio/hugo/output/layouts"
3536

@@ -191,8 +192,10 @@ func newTemplateHandlers(d *deps.Deps) (*tpl.TemplateHandlers, error) {
191192

192193
func newTemplateNamespace(funcs map[string]any) *templateNamespace {
193194
return &templateNamespace{
194-
prototypeHTML: htmltemplate.New("").Funcs(funcs),
195-
prototypeText: texttemplate.New("").Funcs(funcs),
195+
prototypeHTML: htmltemplate.New("").Funcs(funcs),
196+
prototypeText: texttemplate.New("").Funcs(funcs),
197+
prototypeHTMLCloneCache: maps.NewCache[prototypeCloneID, *htmltemplate.Template](),
198+
prototypeTextCloneCache: maps.NewCache[prototypeCloneID, *texttemplate.Template](),
196199
templateStateMap: &templateStateMap{
197200
templates: make(map[string]*templateState),
198201
},
@@ -688,7 +691,7 @@ func (t *templateHandler) addTemplateTo(info templateInfo, to *templateNamespace
688691
func (t *templateHandler) applyBaseTemplate(overlay, base templateInfo) (tpl.Template, error) {
689692
if overlay.isText {
690693
var (
691-
templ = t.main.prototypeTextClone.New(overlay.name)
694+
templ = t.main.getPrototypeText(prototypeCloneIDBaseof).New(overlay.name)
692695
err error
693696
)
694697

@@ -713,7 +716,7 @@ func (t *templateHandler) applyBaseTemplate(overlay, base templateInfo) (tpl.Tem
713716
}
714717

715718
var (
716-
templ = t.main.prototypeHTMLClone.New(overlay.name)
719+
templ = t.main.getPrototypeHTML(prototypeCloneIDBaseof).New(overlay.name)
717720
err error
718721
)
719722

@@ -953,27 +956,37 @@ func (t *templateHandler) postTransform() error {
953956
return nil
954957
}
955958

959+
type prototypeCloneID uint16
960+
961+
const (
962+
prototypeCloneIDBaseof prototypeCloneID = iota + 1
963+
prototypeCloneIDDefer
964+
)
965+
956966
type templateNamespace struct {
957-
prototypeText *texttemplate.Template
958-
prototypeHTML *htmltemplate.Template
959-
prototypeTextClone *texttemplate.Template
960-
prototypeHTMLClone *htmltemplate.Template
967+
prototypeText *texttemplate.Template
968+
prototypeHTML *htmltemplate.Template
969+
970+
prototypeHTMLCloneCache *maps.Cache[prototypeCloneID, *htmltemplate.Template]
971+
prototypeTextCloneCache *maps.Cache[prototypeCloneID, *texttemplate.Template]
961972

962973
*templateStateMap
963974
}
964975

965-
func (t *templateNamespace) getPrototypeText() *texttemplate.Template {
966-
if t.prototypeTextClone != nil {
967-
return t.prototypeTextClone
976+
func (t *templateNamespace) getPrototypeText(id prototypeCloneID) *texttemplate.Template {
977+
v, ok := t.prototypeTextCloneCache.Get(id)
978+
if !ok {
979+
return t.prototypeText
968980
}
969-
return t.prototypeText
981+
return v
970982
}
971983

972-
func (t *templateNamespace) getPrototypeHTML() *htmltemplate.Template {
973-
if t.prototypeHTMLClone != nil {
974-
return t.prototypeHTMLClone
984+
func (t *templateNamespace) getPrototypeHTML(id prototypeCloneID) *htmltemplate.Template {
985+
v, ok := t.prototypeHTMLCloneCache.Get(id)
986+
if !ok {
987+
return t.prototypeHTML
975988
}
976-
return t.prototypeHTML
989+
return v
977990
}
978991

979992
func (t *templateNamespace) Lookup(name string) (tpl.Template, bool) {
@@ -989,9 +1002,10 @@ func (t *templateNamespace) Lookup(name string) (tpl.Template, bool) {
9891002
}
9901003

9911004
func (t *templateNamespace) createPrototypes() error {
992-
t.prototypeTextClone = texttemplate.Must(t.prototypeText.Clone())
993-
t.prototypeHTMLClone = htmltemplate.Must(t.prototypeHTML.Clone())
994-
1005+
for _, id := range []prototypeCloneID{prototypeCloneIDBaseof, prototypeCloneIDDefer} {
1006+
t.prototypeHTMLCloneCache.Set(id, htmltemplate.Must(t.prototypeHTML.Clone()))
1007+
t.prototypeTextCloneCache.Set(id, texttemplate.Must(t.prototypeText.Clone()))
1008+
}
9951009
return nil
9961010
}
9971011

@@ -1021,15 +1035,15 @@ func (t *templateNamespace) addDeferredTemplate(owner *templateState, name strin
10211035
var templ tpl.Template
10221036

10231037
if owner.isText() {
1024-
prototype := t.getPrototypeText()
1038+
prototype := t.getPrototypeText(prototypeCloneIDDefer)
10251039
tt, err := prototype.New(name).Parse("")
10261040
if err != nil {
10271041
return fmt.Errorf("failed to parse empty text template %q: %w", name, err)
10281042
}
10291043
tt.Tree.Root = n
10301044
templ = tt
10311045
} else {
1032-
prototype := t.getPrototypeHTML()
1046+
prototype := t.getPrototypeHTML(prototypeCloneIDDefer)
10331047
tt, err := prototype.New(name).Parse("")
10341048
if err != nil {
10351049
return fmt.Errorf("failed to parse empty HTML template %q: %w", name, err)

‎tpl/tplimpl/tplimpl_integration_test.go

+49
Original file line numberDiff line numberDiff line change
@@ -649,3 +649,52 @@ E: An _emphasized_ word.
649649
"<details>\n <summary>Details</summary>\n <p>D: An <em>emphasized</em> word.</p>\n</details>",
650650
)
651651
}
652+
653+
// Issue 12963
654+
func TestEditBaseofParseAfterExecute(t *testing.T) {
655+
files := `
656+
-- hugo.toml --
657+
baseURL = "https://example.com"
658+
disableLiveReload = true
659+
disableKinds = ["taxonomy", "term", "rss", "404", "sitemap"]
660+
[internal]
661+
fastRenderMode = true
662+
-- layouts/_default/baseof.html --
663+
Baseof!
664+
{{ block "main" . }}default{{ end }}
665+
{{ with (templates.Defer (dict "key" "global")) }}
666+
Now. {{ now }}
667+
{{ end }}
668+
-- layouts/_default/single.html --
669+
{{ define "main" }}
670+
Single.
671+
{{ end }}
672+
-- layouts/_default/list.html --
673+
{{ define "main" }}
674+
List.
675+
{{ .Content }}
676+
{{ range .Pages }}{{ .Title }}{{ end }}|
677+
{{ end }}
678+
-- content/mybundle1/index.md --
679+
---
680+
title: "My Bundle 1"
681+
---
682+
-- content/mybundle2/index.md --
683+
---
684+
title: "My Bundle 2"
685+
---
686+
-- content/_index.md --
687+
---
688+
title: "Home"
689+
---
690+
Home!
691+
`
692+
693+
b := hugolib.TestRunning(t, files)
694+
b.AssertFileContent("public/index.html", "Home!")
695+
b.EditFileReplaceAll("layouts/_default/baseof.html", "Baseof", "Baseof!").Build()
696+
b.BuildPartial("/")
697+
b.AssertFileContent("public/index.html", "Baseof!!")
698+
b.BuildPartial("/mybundle1/")
699+
b.AssertFileContent("public/mybundle1/index.html", "Baseof!!")
700+
}

0 commit comments

Comments
 (0)
Please sign in to comment.