]> git.maquefel.me Git - brevno-suite/hugo/commitdiff
resources: Re-publish on transformation cache hit
authorBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Sun, 15 Mar 2026 10:38:18 +0000 (11:38 +0100)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Sun, 15 Mar 2026 17:32:41 +0000 (18:32 +0100)
When a resource transformation result was served from cache (same
options as a previous build), the output file was not re-written to
disk. This caused incorrect output when toggling transformation
options (e.g. minify) back to a previously seen value in server mode.

Fixes #14629

deps/deps.go
hugolib/hugo_sites.go
hugolib/hugo_sites_build.go
hugolib/page.go
hugolib/site_render.go
identity/identity.go
resources/resource_cache.go
resources/resource_spec.go
resources/transform.go
tpl/css/build_integration_test.go

index 85e808527961bfd1acc91d7a116076e011b3d289..81dacab2efdc07e7be7624c75845a0b5909374ad 100644 (file)
@@ -441,6 +441,9 @@ type DepsCfg struct {
 type BuildState struct {
        counter uint64
 
+       // Tracks invocations of the Build method.
+       BuildCounter atomic.Uint64
+
        mu sync.Mutex // protects state below.
 
        OnSignalRebuild func(ids ...identity.Identity)
@@ -472,6 +475,11 @@ type DeferredExecutions struct {
 
 var _ identity.SignalRebuilder = (*BuildState)(nil)
 
+// IsRebuild reports whether this is a rebuild.
+func (b *BuildState) IsRebuild() bool {
+       return b.BuildCounter.Load() > 0
+}
+
 // StartStageRender will be called before a stage is rendered.
 func (b *BuildState) StartStageRender(stage tpl.RenderingContext) {
 }
index 694dd705176aa021775a5e193498818e01684d92..50ae2f2d92cc433a716ab0223695ac939e5ce684 100644 (file)
@@ -123,8 +123,6 @@ type HugoSites struct {
        *progressReporter
        *fatalErrorHandler
        *buildCounters
-       // Tracks invocations of the Build method.
-       buildCounter atomic.Uint64
 }
 
 // hugoSitesSitesProvider is a wrapper that implements page.SitesProvider.
@@ -253,7 +251,7 @@ func (h *HugoSites) Close() error {
 }
 
 func (h *HugoSites) isRebuild() bool {
-       return h.buildCounter.Load() > 0
+       return h.BuildState.IsRebuild()
 }
 
 func (h *HugoSites) resolveFirstSite(matrix sitesmatrix.VectorStore) *Site {
@@ -650,7 +648,7 @@ func (cfg *BuildCfg) shouldRender(infol logg.LevelLogger, p *pageState) bool {
 
        fastRenderMode := p.s.Conf.FastRenderMode()
 
-       if !fastRenderMode || p.s.h.buildCounter.Load() == 0 {
+       if !fastRenderMode || !p.s.h.BuildState.IsRebuild() {
                return shouldRender
        }
 
index 84b070d138df670c7f9280d949d94a8518977efe..98d1d68c74405026ff34c7787017ae28ac1c37c8 100644 (file)
@@ -83,7 +83,7 @@ func (h *HugoSites) Build(config BuildCfg, events ...fsnotify.Event) error {
                h.reportProgress(func() (state terminal.ProgressState, progress float64) {
                        return terminal.ProgressHidden, 1.0
                })
-               h.buildCounter.Add(1)
+               h.BuildState.BuildCounter.Add(1)
        }()
 
        if h.Deps == nil {
index ef2bd2132c80ba80299dadf9412864d5610ce433..759e4fe36ece9430c05f6896245543e3edc816fc 100644 (file)
@@ -721,7 +721,7 @@ func (ps *pageState) initPage() error {
 func (ps *pageState) renderResources() error {
        for _, r := range ps.Resources() {
                if _, ok := r.(page.Page); ok {
-                       if ps.s.h.buildCounter.Load() == 0 {
+                       if !ps.s.h.BuildState.IsRebuild() {
                                // Pages gets rendered with the owning page but we count them here.
                                ps.s.PathSpec.ProcessingStats.Incr(&ps.s.PathSpec.ProcessingStats.Pages)
                        }
index 2da329255405154b4507c1f9c467d891989eb990..b69e3dcbc58d8e8f754cb63f6f1236cf825e419e 100644 (file)
@@ -154,7 +154,7 @@ func pageRenderer(
                        }
                }
 
-               if !s.conf.DisableAliases && s.h.buildCounter.Load() == 0 {
+               if !s.conf.DisableAliases && !s.h.BuildState.IsRebuild() {
                        of := p.outputFormat()
                        if of.IsHTML && of.Permalinkable {
                                // Render any aliases for this page.
index 7cc303ffa56f7e161c3d9181ce47cb2187c8dc90..d0823c57f5d1f4ce74ae75262e1d6fbd1aacad5b 100644 (file)
@@ -245,6 +245,11 @@ type SignalRebuilder interface {
        SignalRebuild(ids ...Identity)
 }
 
+// IsRebuildProvider signals if we're in a rebuild or not.
+type IsRebuildProvider interface {
+       IsRebuild() bool
+}
+
 // IncrementByOne implements Incrementer adding 1 every time Incr is called.
 type IncrementByOne struct {
        counter uint64
index e91b0ea7e7f1745c3d9acf7e4c4ab11bb65fc8f2..dbdd829d2bceed4d6d09c808240858a2ec2f51c3 100644 (file)
@@ -22,6 +22,7 @@ import (
        "strings"
        "sync"
 
+       "github.com/gohugoio/hugo/common/hmaps"
        "github.com/gohugoio/hugo/resources/resource"
 
        "github.com/gohugoio/hugo/cache/dynacache"
@@ -56,6 +57,8 @@ func newResourceCache(rs *Spec, memCache *dynacache.Cache) *ResourceCache {
                        "/res1/tra",
                        dynacache.OptionsPartition{ClearWhen: dynacache.ClearOnChange, Weight: 40},
                ),
+
+               cacheResourceTransformationPublished: hmaps.NewMap[string, string](),
        }
 }
 
@@ -68,6 +71,9 @@ type ResourceCache struct {
        cacheResources              *dynacache.Partition[string, resource.Resources]
        cacheResourceTransformation *dynacache.Partition[string, *resourceAdapterInner]
 
+       // Used in rebuilds. Maps the target path to the last published transformation key.
+       cacheResourceTransformationPublished *hmaps.Map[string, string]
+
        fileCache *filecache.Cache
 }
 
index ac2e0c30da3b684d5fc67cabd1046403dc781335..2ffa4f9f229d2fa40934af9420cd22d4ac6ecefe 100644 (file)
@@ -57,7 +57,7 @@ func NewSpec(
        errorHandler herrors.ErrorSender,
        execHelper *hexec.Exec,
        buildClosers types.CloseAdder,
-       rebuilder identity.SignalRebuilder,
+       rebuilder Rebuilder,
 ) (*Spec, error) {
        conf := s.Cfg.GetConfig().(*allconfig.Config)
        imgConfig := conf.Imaging
@@ -118,13 +118,18 @@ func NewSpec(
        return rs, nil
 }
 
+type Rebuilder interface {
+       identity.SignalRebuilder
+       identity.IsRebuildProvider
+}
+
 type Spec struct {
        *helpers.PathSpec
 
        Logger       loggers.Logger
        ErrorSender  herrors.ErrorSender
        BuildClosers types.CloseAdder
-       Rebuilder    identity.SignalRebuilder
+       Rebuilder    Rebuilder
 
        Permalinks page.PermalinkExpander
 
index 50f8de30c92999b4db25086d28dd9e92bae32c15..5dc281f7175256662b395f6635f05bdf6f6c353b 100644 (file)
@@ -455,7 +455,10 @@ func (r *resourceAdapter) TransformationKey() string {
 
 func (r *resourceAdapter) getOrTransform(publish, setContent bool) error {
        key := r.TransformationKey()
+
+       var created bool
        res, err := r.spec.ResourceCache.cacheResourceTransformation.GetOrCreate(key, func(string) (*resourceAdapterInner, error) {
+               created = true
                return r.transform(key, publish, setContent)
        })
        if err != nil {
@@ -463,6 +466,40 @@ func (r *resourceAdapter) getOrTransform(publish, setContent bool) error {
        }
 
        r.resourceAdapterInner = res
+
+       if publish && r.spec.Rebuilder.IsRebuild() {
+               targetPath := r.target.TargetPath()
+               var republish bool
+
+               r.spec.ResourceCache.cacheResourceTransformationPublished.WithWriteLock(func(m map[string]string) error {
+                       if created {
+                               m[targetPath] = key
+                       } else {
+                               key2, found := m[targetPath]
+                               republish = !found || key2 != key
+                               m[targetPath] = key
+                       }
+                       return nil
+               })
+
+               if !created && republish {
+                       src, err := contentReadSeekerCloser(r.target)
+                       if err != nil {
+                               return err
+                       }
+                       defer src.Close()
+                       dest, err := r.target.openPublishFileForWriting(targetPath)
+                       if err != nil {
+                               return err
+                       }
+                       defer dest.Close()
+                       _, err = io.Copy(dest, src)
+                       if err != nil {
+                               return err
+                       }
+               }
+       }
+
        return nil
 }
 
index f3bf8117a8f590596bd0f3ebf344fdf324110dde..78682ff8352491428391f7524e20042918b53f37 100644 (file)
@@ -169,6 +169,43 @@ Home.
        b.AssertFileContent("public/css/main.css", `{background:red}`)
 }
 
+func TestCSSBuildEditOptionsMultiple(t *testing.T) {
+       t.Parallel()
+
+       files := `
+-- hugo.toml --
+disableKinds = ["taxonomy", "term", "rss"]
+disableLiveReload = true
+-- assets/css/main.css --
+body {
+ background: red;
+}
+-- layouts/_partials/css.html --
+{{ with resources.Get "css/main.css"  }}
+{{ $opts := dict "minify" false }}
+{{ with . | css.Build $opts  }}
+ <link rel="stylesheet" href="{{ .RelPermalink }}" />
+{{ end }}
+{{ end }}
+-- layouts/all.html --
+All. {{ partial "css.html" . }}
+-- content/p1.md --
+-- content/p2.md --
+-- content/p3.md --
+
+
+`
+
+       b := hugolib.TestRunning(t, files, hugolib.TestOptOsFs())
+
+       for range 3 {
+               b.AssertFileContent("public/css/main.css", `  background: red;`)
+               b.EditFileReplaceAll("layouts/_partials/css.html", `"minify" false`, `"minify" true`).Build()
+               b.AssertFileContent("public/css/main.css", `{background:red}`)
+               b.EditFileReplaceAll("layouts/_partials/css.html", `"minify" true`, `"minify" false`).Build()
+       }
+}
+
 func TestCSSBuildSourceMaps(t *testing.T) {
        t.Parallel()