From 766a2e7868454225b176468ee7df412a7373324e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Wed, 28 May 2025 09:18:16 +0200 Subject: [PATCH] Make sure that unreferenced but changed bundle resources gets republished Fixes #13748 --- hugolib/content_map_page.go | 2 -- hugolib/hugo_sites_build.go | 1 - hugolib/page.go | 61 +++++++++++++++++++------------------ hugolib/page__new.go | 2 -- hugolib/rebuild_test.go | 20 ++++++++++++ resources/resource.go | 20 ++++++++++-- resources/resource_spec.go | 3 +- resources/transform.go | 6 ++++ 8 files changed, 77 insertions(+), 38 deletions(-) diff --git a/hugolib/content_map_page.go b/hugolib/content_map_page.go index ef2daca74..47637d3e8 100644 --- a/hugolib/content_map_page.go +++ b/hugolib/content_map_page.go @@ -21,7 +21,6 @@ import ( "sort" "strconv" "strings" - "sync" "sync/atomic" "time" @@ -1310,7 +1309,6 @@ func (h *HugoSites) resolveAndResetDependententPageOutputs(ctx context.Context, } po.renderState = 0 - po.p.resourcesPublishInit = &sync.Once{} if r == identity.FinderFoundOneOfMany || po.f.Name == output.HTTPStatus404HTMLFormat.Name { // Will force a re-render even in fast render mode. po.renderOnce = false diff --git a/hugolib/hugo_sites_build.go b/hugolib/hugo_sites_build.go index ce8ddd143..b1fe279f4 100644 --- a/hugolib/hugo_sites_build.go +++ b/hugolib/hugo_sites_build.go @@ -905,7 +905,6 @@ func (h *HugoSites) processPartialFileEvents(ctx context.Context, l logg.LevelLo handleChange := func(pathInfo *paths.Path, delete, isDir bool) { switch pathInfo.Component() { case files.ComponentFolderContent: - logger.Println("Source changed", pathInfo.Path()) isContentDataFile := pathInfo.IsContentData() if !isContentDataFile { if ids := h.pageTrees.collectAndMarkStaleIdentities(pathInfo); len(ids) > 0 { diff --git a/hugolib/page.go b/hugolib/page.go index 84d4d1ea8..bdfc66e87 100644 --- a/hugolib/page.go +++ b/hugolib/page.go @@ -19,7 +19,6 @@ import ( "path/filepath" "strconv" "strings" - "sync" "sync/atomic" "github.com/gohugoio/hugo/hugofs" @@ -29,6 +28,7 @@ import ( "github.com/gohugoio/hugo/media" "github.com/gohugoio/hugo/output" "github.com/gohugoio/hugo/related" + "github.com/gohugoio/hugo/resources" "github.com/gohugoio/hugo/tpl/tplimpl" "github.com/spf13/afero" @@ -110,8 +110,7 @@ type pageState struct { *pageCommon resource.Staler - dependencyManager identity.Manager - resourcesPublishInit *sync.Once + dependencyManager identity.Manager } func (p *pageState) incrPageOutputTemplateVariation() { @@ -522,39 +521,41 @@ func (p *pageState) initPage() error { } func (p *pageState) renderResources() error { - var initErr error + for _, r := range p.Resources() { - p.resourcesPublishInit.Do(func() { - for _, r := range p.Resources() { - if _, ok := r.(page.Page); ok { + if _, ok := r.(page.Page); ok { + if p.s.h.buildCounter.Load() == 0 { // Pages gets rendered with the owning page but we count them here. p.s.PathSpec.ProcessingStats.Incr(&p.s.PathSpec.ProcessingStats.Pages) - continue - } - - if _, isWrapper := r.(resource.ResourceWrapper); isWrapper { - // Skip resources that are wrapped. - // These gets published on its own. - continue - } - - src, ok := r.(resource.Source) - if !ok { - initErr = fmt.Errorf("resource %T does not support resource.Source", r) - return - } - - if err := src.Publish(); err != nil { - if !herrors.IsNotExist(err) { - p.s.Log.Errorf("Failed to publish Resource for page %q: %s", p.pathOrTitle(), err) - } - } else { - p.s.PathSpec.ProcessingStats.Incr(&p.s.PathSpec.ProcessingStats.Files) } + continue } - }) - return initErr + if resources.IsPublished(r) { + continue + } + + if _, isWrapper := r.(resource.ResourceWrapper); isWrapper { + // Skip resources that are wrapped. + // These gets published on its own. + continue + } + + src, ok := r.(resource.Source) + if !ok { + return fmt.Errorf("resource %T does not support resource.Source", r) + } + + if err := src.Publish(); err != nil { + if !herrors.IsNotExist(err) { + p.s.Log.Errorf("Failed to publish Resource for page %q: %s", p.pathOrTitle(), err) + } + } else { + p.s.PathSpec.ProcessingStats.Incr(&p.s.PathSpec.ProcessingStats.Files) + } + } + + return nil } func (p *pageState) AlternativeOutputFormats() page.OutputFormats { diff --git a/hugolib/page__new.go b/hugolib/page__new.go index 10336e255..9d05abea6 100644 --- a/hugolib/page__new.go +++ b/hugolib/page__new.go @@ -16,7 +16,6 @@ package hugolib import ( "fmt" "strings" - "sync" "sync/atomic" "github.com/gohugoio/hugo/hugofs/files" @@ -190,7 +189,6 @@ func (h *HugoSites) doNewPage(m *pageMeta) (*pageState, *paths.Path, error) { pid: pid, pageOutput: nopPageOutput, pageOutputTemplateVariationsState: &atomic.Uint32{}, - resourcesPublishInit: &sync.Once{}, Staler: m, dependencyManager: m.s.Conf.NewIdentityManager(m.Path()), pageCommon: &pageCommon{ diff --git a/hugolib/rebuild_test.go b/hugolib/rebuild_test.go index 0b22b9aa7..d4a15fb5b 100644 --- a/hugolib/rebuild_test.go +++ b/hugolib/rebuild_test.go @@ -1946,3 +1946,23 @@ tags: ["tag1"] // But that is a harder problem to tackle. b.AssertFileContent("public/tags/index.html", "All. Tag1|Tag2|") } + +func TestRebuildEditNonReferencedResourceIssue13748(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +baseURL = "https://example.com" +disableLiveReload = true +-- content/mybundle/index.md -- +-- content/mybundle/resource.txt -- +This is a resource file. +-- layouts/all.html -- +All. +` + b := TestRunning(t, files) + + b.AssertFileContent("public/mybundle/resource.txt", "This is a resource file.") + b.EditFileReplaceAll("content/mybundle/resource.txt", "This is a resource file.", "This is an edited resource file.").Build() + b.AssertFileContent("public/mybundle/resource.txt", "This is an edited resource file.") +} diff --git a/resources/resource.go b/resources/resource.go index 6ef9bdae0..f6e5b9d73 100644 --- a/resources/resource.go +++ b/resources/resource.go @@ -24,6 +24,7 @@ import ( "sync/atomic" "github.com/gohugoio/hugo/identity" + "github.com/gohugoio/hugo/lazy" "github.com/gohugoio/hugo/resources/internal" "github.com/gohugoio/hugo/common/hashing" @@ -54,6 +55,7 @@ var ( _ identity.DependencyManagerProvider = (*genericResource)(nil) _ identity.Identity = (*genericResource)(nil) _ fileInfo = (*genericResource)(nil) + _ isPublishedProvider = (*genericResource)(nil) ) type ResourceSourceDescriptor struct { @@ -242,6 +244,7 @@ type baseResourceInternal interface { fileInfo mediaTypeAssigner targetPather + isPublishedProvider ReadSeekCloser() (hugio.ReadSeekCloser, error) @@ -355,7 +358,7 @@ func GetTestInfoForResource(r resource.Resource) GenericResourceTestInfo { // genericResource represents a generic linkable resource. type genericResource struct { - publishInit *sync.Once + publishInit *lazy.OnceMore key string keyInit *sync.Once @@ -536,6 +539,10 @@ func (l *genericResource) Publish() error { return err } +func (l *genericResource) isPublished() bool { + return l.publishInit.Done() +} + func (l *genericResource) RelPermalink() string { return l.spec.PathSpec.GetBasePath(false) + paths.PathEscape(l.paths.TargetLink()) } @@ -629,7 +636,7 @@ func (rc *genericResource) cloneWithUpdates(u *transformationUpdate) (baseResour } func (l genericResource) clone() *genericResource { - l.publishInit = &sync.Once{} + l.publishInit = &lazy.OnceMore{} l.keyInit = &sync.Once{} return &l } @@ -643,6 +650,10 @@ type targetPather interface { TargetPath() string } +type isPublishedProvider interface { + isPublished() bool +} + type resourceHash struct { value uint64 size int64 @@ -702,6 +713,11 @@ func InternalResourceSourcePathBestEffort(r resource.Resource) string { return InternalResourceTargetPath(r) } +// isPublished returns true if the resource is published. +func IsPublished(r resource.Resource) bool { + return r.(isPublishedProvider).isPublished() +} + type targetPathProvider interface { // targetPath is the relative path to this resource. // In most cases this will be the same as the RelPermalink(), diff --git a/resources/resource_spec.go b/resources/resource_spec.go index 56ad9a27d..806a5ff8d 100644 --- a/resources/resource_spec.go +++ b/resources/resource_spec.go @@ -20,6 +20,7 @@ import ( "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/config/allconfig" + "github.com/gohugoio/hugo/lazy" "github.com/gohugoio/hugo/output" "github.com/gohugoio/hugo/resources/internal" "github.com/gohugoio/hugo/resources/jsconfig" @@ -189,7 +190,7 @@ func (r *Spec) NewResource(rd ResourceSourceDescriptor) (resource.Resource, erro gr := &genericResource{ Staler: &AtomicStaler{}, h: &resourceHash{}, - publishInit: &sync.Once{}, + publishInit: &lazy.OnceMore{}, keyInit: &sync.Once{}, includeHashInKey: isImage, paths: rp, diff --git a/resources/transform.go b/resources/transform.go index abb55f3cc..572143d49 100644 --- a/resources/transform.go +++ b/resources/transform.go @@ -61,6 +61,7 @@ var ( _ identity.DependencyManagerProvider = (*resourceAdapter)(nil) _ identity.IdentityGroupProvider = (*resourceAdapter)(nil) _ resource.NameNormalizedProvider = (*resourceAdapter)(nil) + _ isPublishedProvider = (*resourceAdapter)(nil) ) // These are transformations that need special support in Hugo that may not @@ -325,6 +326,11 @@ func (r *resourceAdapter) Publish() error { return r.target.Publish() } +func (r *resourceAdapter) isPublished() bool { + r.init(false, false) + return r.target.isPublished() +} + func (r *resourceAdapter) ReadSeekCloser() (hugio.ReadSeekCloser, error) { r.init(false, false) return r.target.ReadSeekCloser()