Repository: gohugoio/hugo
Pull request #13633 (2025-04-21 09:29:47)

build(deps): bump github.com/bep/imagemeta from 0.11.0 to 0.12.0
Bumps [github.com/bep/imagemeta](https://github.com/bep/imagemeta) from 0.11.0 to 0.12.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/bep/imagemeta/releases">github.com/bep/imagemeta's releases</a>.</em></p>
<blockquote>
<h2>v0.12.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Fix test for corrupt files by <a href="https://github.com/fkleon"><code>@​fkleon</code></a> in <a href="https://redirect.github.com/bep/imagemeta/pull/44">bep/imagemeta#44</a></li>
<li>Allow invalid encoding in EXIF UserComment field by <a href="https://github.com/fkleon"><code>@​fkleon</code></a> in <a href="https://redirect.github.com/bep/imagemeta/pull/45">bep/imagemeta#45</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/fkleon"><code>@​fkleon</code></a> made their first contribution in <a href="https://redirect.github.com/bep/imagemeta/pull/44">bep/imagemeta#44</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a href="https://github.com/bep/imagemeta/compare/v0.11.0...v0.12.0">https://github.com/bep/imagemeta/compare/v0.11.0...v0.12.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/bep/imagemeta/commit/741c616e1a506004793eff78284c88b9d2279f76"><code>741c616</code></a> Allow invalid encoding in EXIF UserComment field</li>
<li><a href="https://github.com/bep/imagemeta/commit/b26d2ec72a799b078c8d7639022447e718224f11"><code>b26d2ec</code></a> Fix test for corrupt files</li>
<li>See full diff in <a href="https://github.com/bep/imagemeta/compare/v0.11.0...v0.12.0">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/bep/imagemeta&package-manager=go_modules&previous-version=0.11.0&new-version=0.12.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


Patch:
diff --git a/config/allconfig/load.go b/config/allconfig/load.go
index f224009ac..9289cf294 100644
--- a/config/allconfig/load.go
+++ b/config/allconfig/load.go
@@ -213,7 +213,7 @@ func (l configLoader) applyDefaultConfig() error {
 		"disableAliases":                       false,
 		"debug":                                false,
 		"disableFastRender":                    false,
-		"timeout":                              "30s",
+		"timeout":                              "60s",
 		"timeZone":                             "",
 		"enableInlineShortcodes":               false,
 	}
diff --git a/go.mod b/go.mod
index 7ac1780d5..346a79f18 100644
--- a/go.mod
+++ b/go.mod
@@ -15,7 +15,7 @@ require (
 	github.com/bep/goportabletext v0.1.0
 	github.com/bep/gowebp v0.3.0
 	github.com/bep/helpers v0.5.0
-	github.com/bep/imagemeta v0.11.0
+	github.com/bep/imagemeta v0.12.0
 	github.com/bep/lazycache v0.8.0
 	github.com/bep/logg v0.4.0
 	github.com/bep/mclib v1.20400.20402
@@ -69,7 +69,7 @@ require (
 	github.com/tdewolff/minify/v2 v2.20.37
 	github.com/tdewolff/parse/v2 v2.7.15
 	github.com/tetratelabs/wazero v1.9.0
-	github.com/yuin/goldmark v1.7.9
+	github.com/yuin/goldmark v1.7.10
 	github.com/yuin/goldmark-emoji v1.0.5
 	go.uber.org/automaxprocs v1.5.3
 	gocloud.dev v0.40.0
diff --git a/go.sum b/go.sum
index 234dc465f..10bad9333 100644
--- a/go.sum
+++ b/go.sum
@@ -139,8 +139,8 @@ github.com/bep/gowebp v0.3.0 h1:MhmMrcf88pUY7/PsEhMgEP0T6fDUnRTMpN8OclDrbrY=
 github.com/bep/gowebp v0.3.0/go.mod h1:ZhFodwdiFp8ehGJpF4LdPl6unxZm9lLFjxD3z2h2AgI=
 github.com/bep/helpers v0.5.0 h1:rneezhnG7GzLFlsEWO/EnleaBRuluBDGFimalO6Y50o=
 github.com/bep/helpers v0.5.0/go.mod h1:dSqCzIvHbzsk5YOesp1M7sKAq5xUcvANsRoKdawxH4Q=
-github.com/bep/imagemeta v0.11.0 h1:jL92HhL1H70NC+f8OVVn5D/nC3FmdxTnM3R+csj54mE=
-github.com/bep/imagemeta v0.11.0/go.mod h1:23AF6O+4fUi9avjiydpKLStUNtJr5hJB4rarG18JpN8=
+github.com/bep/imagemeta v0.12.0 h1:ARf+igs5B7pf079LrqRnwzQ/wEB8Q9v4NSDRZO1/F5k=
+github.com/bep/imagemeta v0.12.0/go.mod h1:23AF6O+4fUi9avjiydpKLStUNtJr5hJB4rarG18JpN8=
 github.com/bep/lazycache v0.8.0 h1:lE5frnRjxaOFbkPZ1YL6nijzOPPz6zeXasJq8WpG4L8=
 github.com/bep/lazycache v0.8.0/go.mod h1:BQ5WZepss7Ko91CGdWz8GQZi/fFnCcyWupv8gyTeKwk=
 github.com/bep/logg v0.4.0 h1:luAo5mO4ZkhA5M1iDVDqDqnBBnlHjmtZF6VAyTp+nCQ=
@@ -467,8 +467,8 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec
 github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
-github.com/yuin/goldmark v1.7.9 h1:rVSeT+f7/lAM+bJHVm5YHGwNrnd40i1Ch2DEocEjHQ0=
-github.com/yuin/goldmark v1.7.9/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
+github.com/yuin/goldmark v1.7.10 h1:S+LrtBjRmqMac2UdtB6yyCEJm+UILZ2fefI4p7o0QpI=
+github.com/yuin/goldmark v1.7.10/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
 github.com/yuin/goldmark-emoji v1.0.5 h1:EMVWyCGPlXJfUXBXpuMu+ii3TIaxbVBnEX9uaDC4cIk=
 github.com/yuin/goldmark-emoji v1.0.5/go.mod h1:tTkZEbwu5wkPmgTcitqddVxY9osFZiavD+r4AzQrh1U=
 go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
diff --git a/hugolib/page__content.go b/hugolib/page__content.go
index 5f7d6f930..20abb7884 100644
--- a/hugolib/page__content.go
+++ b/hugolib/page__content.go
@@ -850,7 +850,7 @@ func (c *cachedContentScope) contentPlain(ctx context.Context) (contentPlainPlai
 	})
 	if err != nil {
 		if herrors.IsTimeoutError(err) {
-			err = fmt.Errorf("timed out rendering the page content. You may have a circular loop in a shortcode, or your site may have resources that take longer to build than the `timeout` limit in your Hugo config file: %w", err)
+			err = fmt.Errorf("timed out rendering the page content. Extend the `timeout` limit in your Hugo config file: %w", err)
 		}
 		return contentPlainPlainWords{}, err
 	}
diff --git a/tpl/collections/where.go b/tpl/collections/where.go
index b15cfe781..ee49d0bbb 100644
--- a/tpl/collections/where.go
+++ b/tpl/collections/where.go
@@ -138,6 +138,9 @@ func (ns *Namespace) checkCondition(v, mv reflect.Value, op string) (bool, error
 		}
 
 		if mv.Len() == 0 {
+			if op == "not in" {
+				return true, nil
+			}
 			return false, nil
 		}
 
diff --git a/tpl/partials/partials.go b/tpl/partials/partials.go
index b9ef4b244..19882e36a 100644
--- a/tpl/partials/partials.go
+++ b/tpl/partials/partials.go
@@ -24,13 +24,13 @@ import (
 	"time"
 
 	"github.com/bep/lazycache"
-
 	"github.com/gohugoio/hugo/common/constants"
 	"github.com/gohugoio/hugo/common/hashing"
 	"github.com/gohugoio/hugo/identity"
+	texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
 
 	"github.com/gohugoio/hugo/tpl"
-	texttemplate "github.com/gohugoio/hugo/tpl/internal/go_templates/texttemplate"
+	"github.com/gohugoio/hugo/tpl/tplimpl"
 
 	bp "github.com/gohugoio/hugo/bufferpool"
 	"github.com/gohugoio/hugo/deps"
@@ -109,7 +109,7 @@ func (c *contextWrapper) Set(in any) string {
 // A string if the partial is a text/template, or template.HTML when html/template.
 // Note that ctx is provided by Hugo, not the end user.
 func (ns *Namespace) Include(ctx context.Context, name string, contextList ...any) (any, error) {
-	res := ns.includWithTimeout(ctx, name, contextList...)
+	res := ns.include(ctx, name, contextList...)
 	if res.err != nil {
 		return nil, res.err
 	}
@@ -121,49 +121,36 @@ func (ns *Namespace) Include(ctx context.Context, name string, contextList ...an
 	return res.result, nil
 }
 
-func (ns *Namespace) includWithTimeout(ctx context.Context, name string, dataList ...any) includeResult {
+func (ns *Namespace) include(ctx context.Context, name string, dataList ...any) includeResult {
+	v, err := ns.lookup(name)
+	if err != nil {
+		return includeResult{err: err}
+	}
+	return ns.doInclude(ctx, v, dataList...)
+}
+
+func (ns *Namespace) lookup(name string) (*tplimpl.TemplInfo, error) {
 	if strings.HasPrefix(name, "partials/") {
 		// This is most likely not what the user intended.
 		// This worked before Hugo 0.146.0.
 		ns.deps.Log.Warnidf(constants.WarnPartialSuperfluousPrefix, "Partial name %q starting with 'partials/' (as in {{ partial \"%s\"}}) is most likely not what you want. Before 0.146.0 we did a double lookup in this situation.", name, name)
 	}
-	// Create a new context with a timeout not connected to the incoming context.
-	timeoutCtx, cancel := context.WithTimeout(context.Background(), ns.deps.Conf.Timeout())
-	defer cancel()
-
-	res := make(chan includeResult, 1)
-
-	go func() {
-		res <- ns.include(ctx, name, dataList...)
-	}()
-
-	select {
-	case r := <-res:
-		return r
-	case <-timeoutCtx.Done():
-		err := timeoutCtx.Err()
-		if err == context.DeadlineExceeded {
-			//lint:ignore ST1005 end user message.
-			err = fmt.Errorf("partial %q timed out after %s. This is most likely due to infinite recursion. If this is just a slow template, you can try to increase the 'timeout' config setting.", name, ns.deps.Conf.Timeout())
-		}
-		return includeResult{err: err}
+	v := ns.deps.TemplateStore.LookupPartial(name)
+	if v == nil {
+		return nil, fmt.Errorf("partial %q not found", name)
 	}
+	return v, nil
 }
 
 // include is a helper function that lookups and executes the named partial.
 // Returns the final template name and the rendered output.
-func (ns *Namespace) include(ctx context.Context, name string, dataList ...any) includeResult {
+func (ns *Namespace) doInclude(ctx context.Context, templ *tplimpl.TemplInfo, dataList ...any) includeResult {
 	var data any
 	if len(dataList) > 0 {
 		data = dataList[0]
 	}
-	v := ns.deps.TemplateStore.LookupPartial(name)
-	if v == nil {
-		return includeResult{err: fmt.Errorf("partial %q not found", name)}
-	}
 
-	templ := v
-	info := v.ParseInfo
+	info := templ.ParseInfo
 
 	var w io.Writer
 
@@ -212,6 +199,20 @@ func (ns *Namespace) IncludeCached(ctx context.Context, name string, context any
 		Variants: variants,
 	}
 	depsManagerIn := tpl.Context.GetDependencyManagerInCurrentScope(ctx)
+	ti, err := ns.lookup(name)
+	if err != nil {
+		return nil, err
+	}
+
+	if parent := tpl.Context.CurrentTemplate.Get(ctx); parent != nil {
+		for parent != nil {
+			if parent.CurrentTemplateInfoOps == ti {
+				// This will deadlock if we continue.
+				return nil, fmt.Errorf("circular call stack detected in partial %q", ti.Filename())
+			}
+			parent = parent.Parent
+		}
+	}
 
 	r, found, err := ns.cachedPartials.cache.GetOrCreate(key.Key(), func(string) (includeResult, error) {
 		var depsManagerShared identity.Manager
@@ -221,7 +222,7 @@ func (ns *Namespace) IncludeCached(ctx context.Context, name string, context any
 			depsManagerShared = identity.NewManager("partials")
 			ctx = tpl.Context.DependencyManagerScopedProvider.Set(ctx, depsManagerShared.(identity.DependencyManagerScopedProvider))
 		}
-		r := ns.includWithTimeout(ctx, key.Name, context)
+		r := ns.doInclude(ctx, ti, context)
 		if ns.deps.Conf.Watching() {
 			r.mangager = depsManagerShared
 		}
diff --git a/tpl/template.go b/tpl/template.go
index f69ae2210..877422123 100644
--- a/tpl/template.go
+++ b/tpl/template.go
@@ -160,6 +160,7 @@ type CurrentTemplateInfoCommonOps interface {
 // CurrentTemplateInfo as returned in templates.Current.
 type CurrentTemplateInfo struct {
 	Parent *CurrentTemplateInfo
+	Level  int
 	CurrentTemplateInfoOps
 }
 
diff --git a/tpl/tplimpl/templatedescriptor.go b/tpl/tplimpl/templatedescriptor.go
index ca73c9f78..8b50605ac 100644
--- a/tpl/tplimpl/templatedescriptor.go
+++ b/tpl/tplimpl/templatedescriptor.go
@@ -94,14 +94,10 @@ func (this TemplateDescriptor) doCompare(category Category, isEmbedded bool, oth
 	}
 
 	if other.LayoutFromTemplate != "" && other.LayoutFromTemplate != layoutAll {
-		if this.LayoutFromUser == "" {
+		if this.LayoutFromUser == "" || this.LayoutFromUser != other.LayoutFromTemplate {
 			if other.LayoutFromTemplate != this.LayoutFromTemplate {
 				return w
 			}
-		} else if isLayoutStandard(this.LayoutFromUser) {
-			if other.LayoutFromTemplate != this.LayoutFromUser {
-				return w
-			}
 		}
 	}
 


Test patch:
diff --git a/tpl/collections/collections_integration_test.go b/tpl/collections/collections_integration_test.go
index cc60770f9..b60aaea87 100644
--- a/tpl/collections/collections_integration_test.go
+++ b/tpl/collections/collections_integration_test.go
@@ -278,3 +278,23 @@ disableKinds = ['rss','sitemap', 'taxonomy', 'term', 'page']
 
 	b.AssertFileContentExact("public/index.html", "0: /a3_b1.html\n\n1: /b2.html\n\n2: /a1.html\n\n3: /a2.html\n$")
 }
+
+// Issue 13621.
+func TestWhereNotInEmptySlice(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- hugo.toml --
+-- layouts/home.html --
+{{- $pages := where site.RegularPages "Kind" "not in" (slice) -}}
+Len: {{  $pages | len }}|
+-- layouts/all.html --
+All|{{ .Title }}|
+-- content/p1.md --
+
+`
+
+	b := hugolib.Test(t, files)
+
+	b.AssertFileContent("public/index.html", "Len: 1|")
+}
diff --git a/tpl/collections/where_test.go b/tpl/collections/where_test.go
index 60f97e607..ecf748f93 100644
--- a/tpl/collections/where_test.go
+++ b/tpl/collections/where_test.go
@@ -761,6 +761,7 @@ func TestCheckCondition(t *testing.T) {
 			expect{true, false},
 		},
 		{reflect.ValueOf(123), reflect.ValueOf([]int{45, 678}), "not in", expect{true, false}},
+		{reflect.ValueOf(123), reflect.ValueOf([]int{}), "not in", expect{true, false}},
 		{reflect.ValueOf("foo"), reflect.ValueOf([]string{"bar", "baz"}), "not in", expect{true, false}},
 		{
 			reflect.ValueOf(time.Date(2015, time.May, 26, 19, 18, 56, 12345, time.UTC)),
diff --git a/tpl/partials/partials_integration_test.go b/tpl/partials/partials_integration_test.go
index 6fab3abd8..0fa47104d 100644
--- a/tpl/partials/partials_integration_test.go
+++ b/tpl/partials/partials_integration_test.go
@@ -256,7 +256,6 @@ func TestIncludeTimeout(t *testing.T) {
 	files := `
 -- config.toml --
 baseURL = 'http://example.com/'
-timeout = '200ms'
 -- layouts/index.html --
 {{ partials.Include "foo.html" . }}
 -- layouts/partials/foo.html --
@@ -271,7 +270,7 @@ timeout = '200ms'
 	).BuildE()
 
 	b.Assert(err, qt.Not(qt.IsNil))
-	b.Assert(err.Error(), qt.Contains, "timed out")
+	b.Assert(err.Error(), qt.Contains, "maximum template call stack size exceeded")
 }
 
 func TestIncludeCachedTimeout(t *testing.T) {
@@ -284,6 +283,8 @@ timeout = '200ms'
 -- layouts/index.html --
 {{ partials.IncludeCached "foo.html" . }}
 -- layouts/partials/foo.html --
+{{ partialCached "bar.html" . }}
+-- layouts/partials/bar.html --
 {{ partialCached "foo.html" . }}
   `
 
@@ -295,7 +296,7 @@ timeout = '200ms'
 	).BuildE()
 
 	b.Assert(err, qt.Not(qt.IsNil))
-	b.Assert(err.Error(), qt.Contains, "timed out")
+	b.Assert(err.Error(), qt.Contains, `error calling partialCached: circular call stack detected in partial`)
 }
 
 // See Issue #10789
diff --git a/tpl/tplimpl/templatestore.go b/tpl/tplimpl/templatestore.go
index 4266e274a..7770d053b 100644
--- a/tpl/tplimpl/templatestore.go
+++ b/tpl/tplimpl/templatestore.go
@@ -484,13 +484,24 @@ func (t *TemplateStore) ExecuteWithContext(ctx context.Context, ti *TemplInfo, w
 
 	templ := ti.Template
 
+	parent := tpl.Context.CurrentTemplate.Get(ctx)
+	var level int
+	if parent != nil {
+		level = parent.Level + 1
+	}
 	currentTi := &tpl.CurrentTemplateInfo{
-		Parent:                 tpl.Context.CurrentTemplate.Get(ctx),
+		Parent:                 parent,
+		Level:                  level,
 		CurrentTemplateInfoOps: ti,
 	}
 
 	ctx = tpl.Context.CurrentTemplate.Set(ctx, currentTi)
 
+	const levelThreshold = 999
+	if level > levelThreshold {
+		return fmt.Errorf("maximum template call stack size exceeded in %q", ti.Filename())
+	}
+
 	if t.opts.Metrics != nil {
 		defer t.opts.Metrics.MeasureSince(templ.Name(), time.Now())
 	}
diff --git a/tpl/tplimpl/templatestore_integration_test.go b/tpl/tplimpl/templatestore_integration_test.go
index e59dad33a..51b28754f 100644
--- a/tpl/tplimpl/templatestore_integration_test.go
+++ b/tpl/tplimpl/templatestore_integration_test.go
@@ -1229,3 +1229,44 @@ layouts/list.html
 		b.AssertFileContent("public/p1/index.html", "layouts/single.html")
 	}
 }
+
+func TestTemplateLoop(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- hugo.toml --
+-- layouts/_partials/p.html --
+p: {{ partial "p.html" . }}
+-- layouts/all.html --
+{{ partial "p.html" . }}
+
+`
+	b, err := hugolib.TestE(t, files)
+	b.Assert(err, qt.IsNotNil)
+	b.Assert(err.Error(), qt.Contains, "error calling partial: maximum template call stack size exceeded")
+}
+
+func TestIssue13630(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- hugo.toml --
+disableKinds = ['rss','sitemap']
+-- content/p1.md --
+---
+title: p1
+layout: foo
+---
+-- layouts/list.html --
+layouts/list.html
+-- layouts/taxononmy.html.html --
+layouts/taxononmy.html.html
+`
+
+	var b *hugolib.IntegrationTestBuilder
+
+	for range 3 {
+		b = hugolib.Test(t, files)
+		b.AssertFileExists("public/p1/index.html", false)
+	}
+}