From 639073e4fee8fd4235c1002b076e110fad4c82f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Fri, 9 Feb 2024 13:52:36 +0200 Subject: [PATCH] Fix rebuild with resources.Concat Fixes #12017 --- common/paths/pathparser.go | 71 ++++++++++++---- common/paths/pathparser_test.go | 6 ++ config/allconfig/allconfig.go | 4 +- config/allconfig/configlanguage.go | 10 ++- config/configProvider.go | 4 +- hugofs/component_fs.go | 2 +- hugolib/content_map.go | 9 +- hugolib/content_map_page.go | 11 ++- hugolib/page.go | 4 +- hugolib/page__new.go | 9 +- hugolib/page__output.go | 7 +- hugolib/rebuild_test.go | 34 +++++++- identity/finder.go | 44 +++++----- identity/identity.go | 83 +++++++++++-------- resources/resource.go | 2 +- resources/resource_cache.go | 2 +- .../resource_factories/bundler/bundler.go | 29 ++++++- resources/resource_factories/create/create.go | 18 ++-- 18 files changed, 229 insertions(+), 120 deletions(-) diff --git a/common/paths/pathparser.go b/common/paths/pathparser.go index eceb46b3d..898eee341 100644 --- a/common/paths/pathparser.go +++ b/common/paths/pathparser.go @@ -18,9 +18,11 @@ import ( "path/filepath" "runtime" "strings" + "sync" "github.com/gohugoio/hugo/common/types" "github.com/gohugoio/hugo/hugofs/files" + "github.com/gohugoio/hugo/identity" ) var defaultPathParser PathParser @@ -50,19 +52,42 @@ func NormalizePathStringBasic(s string) string { return s } +// ParseIdentity parses component c with path s into a StringIdentity. +func (pp *PathParser) ParseIdentity(c, s string) identity.StringIdentity { + s = NormalizePathStringBasic(s) + p := getPath() + p.component = c + defer putPath(p) + p, err := pp.doParse(c, s, p) + if err != nil { + panic(err) + } + return identity.StringIdentity(p.IdentifierBase()) +} + // Parse parses component c with path s into Path using Hugo's content path rules. -func (parser PathParser) Parse(c, s string) *Path { - p, err := parser.parse(c, s) +func (pp *PathParser) Parse(c, s string) *Path { + p, err := pp.parse(c, s) if err != nil { panic(err) } return p } +func (pp *PathParser) newPath(component string) *Path { + return &Path{ + component: component, + posContainerLow: -1, + posContainerHigh: -1, + posSectionHigh: -1, + posIdentifierLanguage: -1, + } +} + func (pp *PathParser) parse(component, s string) (*Path, error) { ss := NormalizePathStringBasic(s) - p, err := pp.doParse(component, ss) + p, err := pp.doParse(component, ss, pp.newPath(component)) if err != nil { return nil, err } @@ -70,7 +95,7 @@ func (pp *PathParser) parse(component, s string) (*Path, error) { if s != ss { var err error // Preserve the original case for titles etc. - p.unnormalized, err = pp.doParse(component, s) + p.unnormalized, err = pp.doParse(component, s, pp.newPath(component)) if err != nil { return nil, err @@ -82,15 +107,7 @@ func (pp *PathParser) parse(component, s string) (*Path, error) { return p, nil } -func (pp *PathParser) doParse(component, s string) (*Path, error) { - p := &Path{ - component: component, - posContainerLow: -1, - posContainerHigh: -1, - posSectionHigh: -1, - posIdentifierLanguage: -1, - } - +func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) { hasLang := pp.LanguageIndex != nil hasLang = hasLang && (component == files.ComponentFolderContent || component == files.ComponentFolderLayouts) @@ -220,6 +237,7 @@ const ( ) type Path struct { + // Note: Any additions to this struct should also be added to the pathPool. s string posContainerLow int @@ -239,6 +257,31 @@ type Path struct { unnormalized *Path } +var pathPool = &sync.Pool{ + New: func() any { + return &Path{} + }, +} + +func getPath() *Path { + return pathPool.Get().(*Path) +} + +func putPath(p *Path) { + p.s = "" + p.posContainerLow = -1 + p.posContainerHigh = -1 + p.posSectionHigh = -1 + p.component = "" + p.bundleType = 0 + p.identifiers = p.identifiers[:0] + p.posIdentifierLanguage = -1 + p.disabled = false + p.trimLeadingSlash = false + p.unnormalized = nil + pathPool.Put(p) +} + // TrimLeadingSlash returns a copy of the Path with the leading slash removed. func (p Path) TrimLeadingSlash() *Path { p.trimLeadingSlash = true @@ -254,7 +297,7 @@ func (p *Path) norm(s string) string { // IdentifierBase satifies identity.Identity. func (p *Path) IdentifierBase() string { - return p.Base()[1:] + return p.Base() } // Component returns the component for this path (e.g. "content"). diff --git a/common/paths/pathparser_test.go b/common/paths/pathparser_test.go index 27d0b45e9..8c89ddd41 100644 --- a/common/paths/pathparser_test.go +++ b/common/paths/pathparser_test.go @@ -349,3 +349,9 @@ func TestHasExt(t *testing.T) { c.Assert(HasExt("/a/b/c"), qt.IsFalse) c.Assert(HasExt("/a/b.c/d"), qt.IsFalse) } + +func BenchmarkParseIdentity(b *testing.B) { + for i := 0; i < b.N; i++ { + testParser.ParseIdentity(files.ComponentFolderAssets, "/a/b.css") + } +} diff --git a/config/allconfig/allconfig.go b/config/allconfig/allconfig.go index f22fe8a7e..21d12b0c7 100644 --- a/config/allconfig/allconfig.go +++ b/config/allconfig/allconfig.go @@ -663,7 +663,7 @@ type Configs struct { // All below is set in Init. Languages langs.Languages LanguagesDefaultFirst langs.Languages - ContentPathParser paths.PathParser + ContentPathParser *paths.PathParser configLangs []config.AllProvider } @@ -735,7 +735,7 @@ func (c *Configs) Init() error { c.Languages = languages c.LanguagesDefaultFirst = languagesDefaultFirst - c.ContentPathParser = paths.PathParser{LanguageIndex: languagesDefaultFirst.AsIndexSet(), IsLangDisabled: c.Base.IsLangDisabled} + c.ContentPathParser = &paths.PathParser{LanguageIndex: languagesDefaultFirst.AsIndexSet(), IsLangDisabled: c.Base.IsLangDisabled} c.configLangs = make([]config.AllProvider, len(c.Languages)) for i, l := range c.LanguagesDefaultFirst { diff --git a/config/allconfig/configlanguage.go b/config/allconfig/configlanguage.go index 2cc80caa8..9971be65f 100644 --- a/config/allconfig/configlanguage.go +++ b/config/allconfig/configlanguage.go @@ -19,6 +19,7 @@ import ( "github.com/gohugoio/hugo/common/paths" "github.com/gohugoio/hugo/common/urls" "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/identity" "github.com/gohugoio/hugo/langs" ) @@ -42,7 +43,7 @@ func (c ConfigLanguage) LanguagesDefaultFirst() langs.Languages { return c.m.LanguagesDefaultFirst } -func (c ConfigLanguage) PathParser() paths.PathParser { +func (c ConfigLanguage) PathParser() *paths.PathParser { return c.m.ContentPathParser } @@ -133,6 +134,13 @@ func (c ConfigLanguage) Watching() bool { return c.m.Base.Internal.Watch } +func (c ConfigLanguage) NewIdentityManager(name string) identity.Manager { + if !c.Watching() { + return identity.NopManager + } + return identity.NewManager(name) +} + // GetConfigSection is mostly used in tests. The switch statement isn't complete, but what's in use. func (c ConfigLanguage) GetConfigSection(s string) any { switch s { diff --git a/config/configProvider.go b/config/configProvider.go index 38dde3bb4..586a9b758 100644 --- a/config/configProvider.go +++ b/config/configProvider.go @@ -20,6 +20,7 @@ import ( "github.com/gohugoio/hugo/common/paths" "github.com/gohugoio/hugo/common/types" "github.com/gohugoio/hugo/common/urls" + "github.com/gohugoio/hugo/identity" "github.com/gohugoio/hugo/langs" ) @@ -31,7 +32,7 @@ type AllProvider interface { LanguagePrefix() string BaseURL() urls.BaseURL BaseURLLiveReload() urls.BaseURL - PathParser() paths.PathParser + PathParser() *paths.PathParser Environment() string IsMultihost() bool IsMultiLingual() bool @@ -57,6 +58,7 @@ type AllProvider interface { BuildDrafts() bool Running() bool Watching() bool + NewIdentityManager(name string) identity.Manager FastRenderMode() bool PrintUnusedTemplates() bool EnableMissingTranslationPlaceholders() bool diff --git a/hugofs/component_fs.go b/hugofs/component_fs.go index c3c64b18f..b6eeb89e5 100644 --- a/hugofs/component_fs.go +++ b/hugofs/component_fs.go @@ -241,7 +241,7 @@ type ComponentFsOptions struct { DefaultContentLanguage string // The parser used to parse paths provided by this filesystem. - PathParser paths.PathParser + PathParser *paths.PathParser } func (fs *componentFs) Open(name string) (afero.File, error) { diff --git a/hugolib/content_map.go b/hugolib/content_map.go index 85300e3db..23b74e1c7 100644 --- a/hugolib/content_map.go +++ b/hugolib/content_map.go @@ -93,8 +93,8 @@ func (r *resourceSource) GetIdentity() identity.Identity { return r.path } -func (r *resourceSource) ForEeachIdentity(f func(identity.Identity) bool) { - f(r.GetIdentity()) +func (r *resourceSource) ForEeachIdentity(f func(identity.Identity) bool) bool { + return f(r.GetIdentity()) } func (r *resourceSource) Path() string { @@ -142,14 +142,15 @@ func (n resourceSources) GetIdentity() identity.Identity { return nil } -func (n resourceSources) ForEeachIdentity(f func(identity.Identity) bool) { +func (n resourceSources) ForEeachIdentity(f func(identity.Identity) bool) bool { for _, r := range n { if r != nil { if f(r.GetIdentity()) { - return + return true } } } + return false } func (cfg contentMapConfig) getTaxonomyConfig(s string) (v viewName) { diff --git a/hugolib/content_map_page.go b/hugolib/content_map_page.go index 6273870b7..576ee5e08 100644 --- a/hugolib/content_map_page.go +++ b/hugolib/content_map_page.go @@ -605,12 +605,15 @@ func (n contentNodeIs) GetIdentity() identity.Identity { return n[0].GetIdentity() } -func (n contentNodeIs) ForEeachIdentity(f func(identity.Identity) bool) { +func (n contentNodeIs) ForEeachIdentity(f func(identity.Identity) bool) bool { for _, nn := range n { if nn != nil { - nn.ForEeachIdentity(f) + if nn.ForEeachIdentity(f) { + return true + } } } + return false } func (n contentNodeIs) resetBuildState() { @@ -1151,7 +1154,7 @@ func (h *HugoSites) resolveAndResetDependententPageOutputs(ctx context.Context, // First check the top level dependency manager. for _, id := range changes { checkedCounter.Add(1) - if r := depsFinder.Contains(id, p.dependencyManager, 100); r > identity.FinderFoundOneOfManyRepetition { + if r := depsFinder.Contains(id, p.dependencyManager, 2); r > identity.FinderFoundOneOfManyRepetition { for _, po := range p.pageOutputs { resetPo(po, r) } @@ -1167,7 +1170,7 @@ func (h *HugoSites) resolveAndResetDependententPageOutputs(ctx context.Context, } for _, id := range changes { checkedCounter.Add(1) - if r := depsFinder.Contains(id, po.dependencyManagerOutput, 2); r > identity.FinderFoundOneOfManyRepetition { + if r := depsFinder.Contains(id, po.dependencyManagerOutput, 50); r > identity.FinderFoundOneOfManyRepetition { resetPo(po, r) continue OUTPUTS } diff --git a/hugolib/page.go b/hugolib/page.go index 10ecdcad2..c743d4282 100644 --- a/hugolib/page.go +++ b/hugolib/page.go @@ -120,8 +120,8 @@ func (p *pageState) GetIdentity() identity.Identity { return p } -func (p *pageState) ForEeachIdentity(f func(identity.Identity) bool) { - f(p) +func (p *pageState) ForEeachIdentity(f func(identity.Identity) bool) bool { + return f(p) } func (p *pageState) GetDependencyManager() identity.Manager { diff --git a/hugolib/page__new.go b/hugolib/page__new.go index 2c2d92ab8..9dd2f2cdf 100644 --- a/hugolib/page__new.go +++ b/hugolib/page__new.go @@ -20,7 +20,6 @@ import ( "sync/atomic" "github.com/gohugoio/hugo/hugofs/files" - "github.com/gohugoio/hugo/identity" "github.com/gohugoio/hugo/resources" "github.com/gohugoio/hugo/common/maps" @@ -160,12 +159,6 @@ func (h *HugoSites) newPage(m *pageMeta) (*pageState, *paths.Path, error) { return nil, nil } - var dependencyManager identity.Manager = identity.NopManager - - if m.s.conf.Internal.Watch { - dependencyManager = identity.NewManager(m.Path()) - } - // Parse the rest of the page content. m.content, err = m.newCachedContent(h, pi) if err != nil { @@ -178,7 +171,7 @@ func (h *HugoSites) newPage(m *pageMeta) (*pageState, *paths.Path, error) { pageOutputTemplateVariationsState: &atomic.Uint32{}, resourcesPublishInit: &sync.Once{}, Staler: m, - dependencyManager: dependencyManager, + dependencyManager: m.s.Conf.NewIdentityManager(m.Path()), pageCommon: &pageCommon{ FileProvider: m, AuthorProvider: m, diff --git a/hugolib/page__output.go b/hugolib/page__output.go index 9050766d1..2f4d6c205 100644 --- a/hugolib/page__output.go +++ b/hugolib/page__output.go @@ -51,11 +51,6 @@ func newPageOutput( }) } - var dependencyManager identity.Manager = identity.NopManager - if ps.s.conf.Internal.Watch { - dependencyManager = identity.NewManager(ps.Path() + "/" + f.Name) - } - providers := struct { page.PaginatorProvider resource.ResourceLinksProvider @@ -75,7 +70,7 @@ func newPageOutput( TableOfContentsProvider: page.NopPage, render: render, paginator: pag, - dependencyManagerOutput: dependencyManager, + dependencyManagerOutput: ps.s.Conf.NewIdentityManager((ps.Path() + "/" + f.Name)), } return po diff --git a/hugolib/rebuild_test.go b/hugolib/rebuild_test.go index b1ebe14d5..9b15fc3e2 100644 --- a/hugolib/rebuild_test.go +++ b/hugolib/rebuild_test.go @@ -1131,7 +1131,7 @@ Single. Running: true, NeedsOsFS: true, NeedsNpmInstall: true, - // LogLevel: logg.LevelTrace, + // LogLevel: logg.LevelDebug, }, ).Build() @@ -1261,3 +1261,35 @@ func BenchmarkRebuildContentFileChange(b *testing.B) { // fmt.Println(bb.LogString()) } } + +func TestRebuildConcat(t *testing.T) { + files := ` +-- hugo.toml -- +baseURL = "https://example.com" +disableLiveReload = true +disableKinds = ["taxonomy", "term", "sitemap", "robotsTXT", "404", "rss"] +-- assets/a.css -- +a +-- assets/b.css -- +b +-- assets/c.css -- +c +-- assets/common/c1.css -- +c1 +-- assets/common/c2.css -- +c2 +-- layouts/index.html -- +{{ $a := resources.Get "a.css" }} +{{ $b := resources.Get "b.css" }} +{{ $common := resources.Match "common/*.css" | resources.Concat "common.css" | minify }} +{{ $ab := slice $a $b $common | resources.Concat "ab.css" }} +all: {{ $ab.RelPermalink }} +` + b := TestRunning(t, files) + + b.AssertFileContent("public/ab.css", "abc1c2") + b.EditFileReplaceAll("assets/common/c2.css", "c2", "c2 edited").Build() + b.AssertFileContent("public/ab.css", "abc1c2 edited") + b.AddFiles("assets/common/c3.css", "c3").Build() + b.AssertFileContent("public/ab.css", "abc1c2 editedc3") +} diff --git a/identity/finder.go b/identity/finder.go index bd23d698e..91fac7237 100644 --- a/identity/finder.go +++ b/identity/finder.go @@ -169,12 +169,7 @@ func (f *Finder) checkManager(sid *searchID, m Manager, level int) FinderResult return r } - ids := m.getIdentities() - if len(ids) == 0 { - r = FinderNotFound - } else { - r = f.search(sid, ids, level) - } + r = f.search(sid, m, level) if r == FinderFoundOneOfMany { // Don't cache this one. @@ -270,11 +265,7 @@ func (f *Finder) doCheckOne(sid *searchID, v Identity, depth int) FinderResult { } // search searches for id in ids. -func (f *Finder) search(sid *searchID, ids Identities, depth int) FinderResult { - if len(ids) == 0 { - return FinderNotFound - } - +func (f *Finder) search(sid *searchID, m Manager, depth int) FinderResult { id := sid.id if id == Anonymous { @@ -285,19 +276,24 @@ func (f *Finder) search(sid *searchID, ids Identities, depth int) FinderResult { return FinderNotFound } - for v := range ids { - r := f.checkOne(sid, v, depth) - if r > 0 { - return r - } - - m := GetDependencyManager(v) - if r := f.checkManager(sid, m, depth+1); r > 0 { - return r - } - } - - return FinderNotFound + var r FinderResult + m.forEeachIdentity( + func(v Identity) bool { + if r > 0 { + panic("should be terminated") + } + r = f.checkOne(sid, v, depth) + if r > 0 { + return true + } + m := GetDependencyManager(v) + if r = f.checkManager(sid, m, depth+1); r > 0 { + return true + } + return false + }, + ) + return r } // FinderConfig provides configuration for the Finder. diff --git a/identity/identity.go b/identity/identity.go index ccb2f6e79..c799759df 100644 --- a/identity/identity.go +++ b/identity/identity.go @@ -55,8 +55,8 @@ func NewManager(name string, opts ...ManagerOption) Manager { // CleanString cleans s to be suitable as an identifier. func CleanString(s string) string { s = strings.ToLower(s) - s = strings.TrimPrefix(filepath.ToSlash(s), "/") - return path.Clean(s) + s = strings.Trim(filepath.ToSlash(s), "/") + return "/" + path.Clean(s) } // CleanStringIdentity cleans s to be suitable as an identifier and wraps it in a StringIdentity. @@ -77,23 +77,6 @@ func GetDependencyManager(v any) Manager { return nil } -// GetDependencyManagerForScope returns the DependencyManager for the given scope from v or nil if none found. -// Note that it will fall back to an unscoped manager if none found for the given scope. -func GetDependencyManagerForScope(v any, scope int) Manager { - switch vv := v.(type) { - case DependencyManagerScopedProvider: - return vv.GetDependencyManagerForScope(scope) - case types.Unwrapper: - return GetDependencyManagerForScope(vv.Unwrapv(), scope) - case Manager: - return vv - case DependencyManagerProvider: - return vv.GetDependencyManager() - - } - return nil -} - // FirstIdentity returns the first Identity in v, Anonymous if none found func FirstIdentity(v any) Identity { var result Identity = Anonymous @@ -169,7 +152,15 @@ type DependencyManagerScopedProvider interface { type ForEeachIdentityProvider interface { // ForEeachIdentityProvider calls cb for each Identity. // If cb returns true, the iteration is terminated. - ForEeachIdentity(cb func(id Identity) bool) + // The return value is whether the iteration was terminated. + ForEeachIdentity(cb func(id Identity) bool) bool +} + +// ForEeachIdentityProviderFunc is a function that implements the ForEeachIdentityProvider interface. +type ForEeachIdentityProviderFunc func(func(id Identity) bool) bool + +func (f ForEeachIdentityProviderFunc) ForEeachIdentity(cb func(id Identity) bool) bool { + return f(cb) } // ForEeachIdentityByNameProvider provides a way to look up identities by name. @@ -279,9 +270,10 @@ type IsProbablyDependencyProvider interface { type Manager interface { Identity AddIdentity(ids ...Identity) + AddIdentityForEach(ids ...ForEeachIdentityProvider) GetIdentity() Identity Reset() - getIdentities() Identities + forEeachIdentity(func(id Identity) bool) bool } type ManagerOption func(m *identityManager) @@ -301,8 +293,9 @@ type identityManager struct { // mu protects _changes_ to this manager, // reads currently assumes no concurrent writes. - mu sync.RWMutex - ids Identities + mu sync.RWMutex + ids Identities + forEachIds []ForEeachIdentityProvider // Hooks used in debugging. onAddIdentity func(id Identity) @@ -312,7 +305,7 @@ func (im *identityManager) AddIdentity(ids ...Identity) { im.mu.Lock() for _, id := range ids { - if id == Anonymous { + if id == nil || id == Anonymous { continue } if _, found := im.ids[id]; !found { @@ -325,6 +318,12 @@ func (im *identityManager) AddIdentity(ids ...Identity) { im.mu.Unlock() } +func (im *identityManager) AddIdentityForEach(ids ...ForEeachIdentityProvider) { + im.mu.Lock() + im.forEachIds = append(im.forEachIds, ids...) + im.mu.Unlock() +} + func (im *identityManager) ContainsIdentity(id Identity) FinderResult { if im.Identity != Anonymous && id == im.Identity { return FinderFound @@ -355,10 +354,20 @@ func (im *identityManager) String() string { return fmt.Sprintf("IdentityManager(%s)", im.name) } -// TODO(bep) these identities are currently only read on server reloads -// so there should be no concurrency issues, but that may change. -func (im *identityManager) getIdentities() Identities { - return im.ids +func (im *identityManager) forEeachIdentity(fn func(id Identity) bool) bool { + // The absense of a lock here is debliberate. This is currently opnly used on server reloads + // in a single-threaded context. + for id := range im.ids { + if fn(id) { + return true + } + } + for _, fe := range im.forEachIds { + if fe.ForEeachIdentity(fn) { + return true + } + } + return false } type nopManager int @@ -366,6 +375,9 @@ type nopManager int func (m *nopManager) AddIdentity(ids ...Identity) { } +func (m *nopManager) AddIdentityForEach(ids ...ForEeachIdentityProvider) { +} + func (m *nopManager) IdentifierBase() string { return "" } @@ -377,8 +389,8 @@ func (m *nopManager) GetIdentity() Identity { func (m *nopManager) Reset() { } -func (m *nopManager) getIdentities() Identities { - return nil +func (m *nopManager) forEeachIdentity(func(id Identity) bool) bool { + return false } // returns whether further walking should be terminated. @@ -401,11 +413,9 @@ func walkIdentities(v any, level int, deep bool, seen map[Identity]bool, cb func if deep { if m := GetDependencyManager(id); m != nil { - for id2 := range m.getIdentities() { - if walkIdentitiesShallow(id2, level+1, cbRecursive) { - return true - } - } + m.forEeachIdentity(func(id2 Identity) bool { + return walkIdentitiesShallow(id2, level+1, cbRecursive) + }) } } return false @@ -420,6 +430,9 @@ func walkIdentitiesShallow(v any, level int, cb func(level int, id Identity) boo if id == Anonymous { return false } + if id == nil { + return false + } return cb(level, id) } diff --git a/resources/resource.go b/resources/resource.go index 900b05a5f..73a0ad56e 100644 --- a/resources/resource.go +++ b/resources/resource.go @@ -171,7 +171,7 @@ func (fd *ResourceSourceDescriptor) init(r *Spec) error { fd.MediaType = mediaType if fd.DependencyManager == nil { - fd.DependencyManager = identity.NopManager + fd.DependencyManager = r.Cfg.NewIdentityManager("resource") } return nil diff --git a/resources/resource_cache.go b/resources/resource_cache.go index a76a51b1c..bf930c71d 100644 --- a/resources/resource_cache.go +++ b/resources/resource_cache.go @@ -39,7 +39,7 @@ func newResourceCache(rs *Spec, memCache *dynacache.Cache) *ResourceCache { cacheResources: dynacache.GetOrCreatePartition[string, resource.Resources]( memCache, "/ress", - dynacache.OptionsPartition{ClearWhen: dynacache.ClearOnChange, Weight: 40}, + dynacache.OptionsPartition{ClearWhen: dynacache.ClearOnRebuild, Weight: 40}, ), cacheResourceTransformation: dynacache.GetOrCreatePartition[string, *resourceAdapterInner]( memCache, diff --git a/resources/resource_factories/bundler/bundler.go b/resources/resource_factories/bundler/bundler.go index c255da601..dd0f1a4e1 100644 --- a/resources/resource_factories/bundler/bundler.go +++ b/resources/resource_factories/bundler/bundler.go @@ -20,6 +20,7 @@ import ( "path" "github.com/gohugoio/hugo/common/hugio" + "github.com/gohugoio/hugo/identity" "github.com/gohugoio/hugo/media" "github.com/gohugoio/hugo/resources" "github.com/gohugoio/hugo/resources/resource" @@ -86,13 +87,32 @@ func (c *Client) Concat(targetPath string, r resource.Resources) (resource.Resou // The given set of resources must be of the same Media Type. // We may improve on that in the future, but then we need to know more. - for i, r := range r { - if i > 0 && r.MediaType().Type != resolvedm.Type { - return nil, fmt.Errorf("resources in Concat must be of the same Media Type, got %q and %q", r.MediaType().Type, resolvedm.Type) + for i, rr := range r { + if i > 0 && rr.MediaType().Type != resolvedm.Type { + return nil, fmt.Errorf("resources in Concat must be of the same Media Type, got %q and %q", rr.MediaType().Type, resolvedm.Type) } - resolvedm = r.MediaType() + resolvedm = rr.MediaType() } + idm := c.rs.Cfg.NewIdentityManager("concat") + // Add the concatenated resources as dependencies to the composite resource + // so that we can track changes to the individual resources. + idm.AddIdentityForEach(identity.ForEeachIdentityProviderFunc( + func(f func(identity.Identity) bool) bool { + var terminate bool + for _, rr := range r { + identity.WalkIdentitiesShallow(rr, func(depth int, id identity.Identity) bool { + terminate = f(id) + return terminate + }) + if terminate { + break + } + } + return terminate + }, + )) + concatr := func() (hugio.ReadSeekCloser, error) { var rcsources []hugio.ReadSeekCloser for _, s := range r { @@ -136,6 +156,7 @@ func (c *Client) Concat(targetPath string, r resource.Resources) (resource.Resou LazyPublish: true, OpenReadSeekCloser: concatr, TargetPath: targetPath, + DependencyManager: idm, }) if err != nil { return nil, err diff --git a/resources/resource_factories/create/create.go b/resources/resource_factories/create/create.go index e98eb7425..0cf84c49b 100644 --- a/resources/resource_factories/create/create.go +++ b/resources/resource_factories/create/create.go @@ -63,13 +63,6 @@ func (c *Client) Copy(r resource.Resource, targetPath string) (resource.Resource }) } -func (c *Client) newDependencyManager() identity.Manager { - if c.rs.Cfg.Running() { - return identity.NewManager("resources") - } - return identity.NopManager -} - // Get creates a new Resource by opening the given pathname in the assets filesystem. func (c *Client) Get(pathname string) (resource.Resource, error) { pathname = path.Clean(pathname) @@ -79,7 +72,8 @@ func (c *Client) Get(pathname string) (resource.Resource, error) { // The resource file will not be read before it gets used (e.g. in .Content), // so we need to check that the file exists here. filename := filepath.FromSlash(pathname) - if _, err := c.rs.BaseFs.Assets.Fs.Stat(filename); err != nil { + fi, err := c.rs.BaseFs.Assets.Fs.Stat(filename) + if err != nil { if os.IsNotExist(err) { return nil, nil } @@ -87,14 +81,16 @@ func (c *Client) Get(pathname string) (resource.Resource, error) { return nil, err } + pi := fi.(hugofs.FileMetaInfo).Meta().PathInfo + return c.rs.NewResource(resources.ResourceSourceDescriptor{ LazyPublish: true, OpenReadSeekCloser: func() (hugio.ReadSeekCloser, error) { return c.rs.BaseFs.Assets.Fs.Open(filename) }, - GroupIdentity: identity.StringIdentity(key), - DependencyManager: c.newDependencyManager(), - TargetPath: pathname, + Path: pi, + GroupIdentity: pi, + TargetPath: pathname, }) }) }