Fix rebuild with resources.Concat

Fixes #12017
This commit is contained in:
Bjørn Erik Pedersen 2024-02-09 13:52:36 +02:00
parent 0672b5c766
commit 57a1893999
No known key found for this signature in database
18 changed files with 229 additions and 120 deletions

View file

@ -18,9 +18,11 @@ import (
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings" "strings"
"sync"
"github.com/gohugoio/hugo/common/types" "github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/hugofs/files" "github.com/gohugoio/hugo/hugofs/files"
"github.com/gohugoio/hugo/identity"
) )
var defaultPathParser PathParser var defaultPathParser PathParser
@ -50,19 +52,42 @@ func NormalizePathStringBasic(s string) string {
return s 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. // Parse parses component c with path s into Path using Hugo's content path rules.
func (parser PathParser) Parse(c, s string) *Path { func (pp *PathParser) Parse(c, s string) *Path {
p, err := parser.parse(c, s) p, err := pp.parse(c, s)
if err != nil { if err != nil {
panic(err) panic(err)
} }
return p 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) { func (pp *PathParser) parse(component, s string) (*Path, error) {
ss := NormalizePathStringBasic(s) ss := NormalizePathStringBasic(s)
p, err := pp.doParse(component, ss) p, err := pp.doParse(component, ss, pp.newPath(component))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -70,7 +95,7 @@ func (pp *PathParser) parse(component, s string) (*Path, error) {
if s != ss { if s != ss {
var err error var err error
// Preserve the original case for titles etc. // 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 { if err != nil {
return nil, err return nil, err
@ -82,15 +107,7 @@ func (pp *PathParser) parse(component, s string) (*Path, error) {
return p, nil return p, nil
} }
func (pp *PathParser) doParse(component, s string) (*Path, error) { func (pp *PathParser) doParse(component, s string, p *Path) (*Path, error) {
p := &Path{
component: component,
posContainerLow: -1,
posContainerHigh: -1,
posSectionHigh: -1,
posIdentifierLanguage: -1,
}
hasLang := pp.LanguageIndex != nil hasLang := pp.LanguageIndex != nil
hasLang = hasLang && (component == files.ComponentFolderContent || component == files.ComponentFolderLayouts) hasLang = hasLang && (component == files.ComponentFolderContent || component == files.ComponentFolderLayouts)
@ -220,6 +237,7 @@ const (
) )
type Path struct { type Path struct {
// Note: Any additions to this struct should also be added to the pathPool.
s string s string
posContainerLow int posContainerLow int
@ -239,6 +257,31 @@ type Path struct {
unnormalized *Path 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. // TrimLeadingSlash returns a copy of the Path with the leading slash removed.
func (p Path) TrimLeadingSlash() *Path { func (p Path) TrimLeadingSlash() *Path {
p.trimLeadingSlash = true p.trimLeadingSlash = true
@ -254,7 +297,7 @@ func (p *Path) norm(s string) string {
// IdentifierBase satifies identity.Identity. // IdentifierBase satifies identity.Identity.
func (p *Path) IdentifierBase() string { func (p *Path) IdentifierBase() string {
return p.Base()[1:] return p.Base()
} }
// Component returns the component for this path (e.g. "content"). // Component returns the component for this path (e.g. "content").

View file

@ -349,3 +349,9 @@ func TestHasExt(t *testing.T) {
c.Assert(HasExt("/a/b/c"), qt.IsFalse) c.Assert(HasExt("/a/b/c"), qt.IsFalse)
c.Assert(HasExt("/a/b.c/d"), 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")
}
}

View file

@ -663,7 +663,7 @@ type Configs struct {
// All below is set in Init. // All below is set in Init.
Languages langs.Languages Languages langs.Languages
LanguagesDefaultFirst langs.Languages LanguagesDefaultFirst langs.Languages
ContentPathParser paths.PathParser ContentPathParser *paths.PathParser
configLangs []config.AllProvider configLangs []config.AllProvider
} }
@ -735,7 +735,7 @@ func (c *Configs) Init() error {
c.Languages = languages c.Languages = languages
c.LanguagesDefaultFirst = languagesDefaultFirst 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)) c.configLangs = make([]config.AllProvider, len(c.Languages))
for i, l := range c.LanguagesDefaultFirst { for i, l := range c.LanguagesDefaultFirst {

View file

@ -19,6 +19,7 @@ import (
"github.com/gohugoio/hugo/common/paths" "github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/common/urls" "github.com/gohugoio/hugo/common/urls"
"github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/langs" "github.com/gohugoio/hugo/langs"
) )
@ -42,7 +43,7 @@ func (c ConfigLanguage) LanguagesDefaultFirst() langs.Languages {
return c.m.LanguagesDefaultFirst return c.m.LanguagesDefaultFirst
} }
func (c ConfigLanguage) PathParser() paths.PathParser { func (c ConfigLanguage) PathParser() *paths.PathParser {
return c.m.ContentPathParser return c.m.ContentPathParser
} }
@ -133,6 +134,13 @@ func (c ConfigLanguage) Watching() bool {
return c.m.Base.Internal.Watch 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. // GetConfigSection is mostly used in tests. The switch statement isn't complete, but what's in use.
func (c ConfigLanguage) GetConfigSection(s string) any { func (c ConfigLanguage) GetConfigSection(s string) any {
switch s { switch s {

View file

@ -20,6 +20,7 @@ import (
"github.com/gohugoio/hugo/common/paths" "github.com/gohugoio/hugo/common/paths"
"github.com/gohugoio/hugo/common/types" "github.com/gohugoio/hugo/common/types"
"github.com/gohugoio/hugo/common/urls" "github.com/gohugoio/hugo/common/urls"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/langs" "github.com/gohugoio/hugo/langs"
) )
@ -31,7 +32,7 @@ type AllProvider interface {
LanguagePrefix() string LanguagePrefix() string
BaseURL() urls.BaseURL BaseURL() urls.BaseURL
BaseURLLiveReload() urls.BaseURL BaseURLLiveReload() urls.BaseURL
PathParser() paths.PathParser PathParser() *paths.PathParser
Environment() string Environment() string
IsMultihost() bool IsMultihost() bool
IsMultiLingual() bool IsMultiLingual() bool
@ -57,6 +58,7 @@ type AllProvider interface {
BuildDrafts() bool BuildDrafts() bool
Running() bool Running() bool
Watching() bool Watching() bool
NewIdentityManager(name string) identity.Manager
FastRenderMode() bool FastRenderMode() bool
PrintUnusedTemplates() bool PrintUnusedTemplates() bool
EnableMissingTranslationPlaceholders() bool EnableMissingTranslationPlaceholders() bool

View file

@ -241,7 +241,7 @@ type ComponentFsOptions struct {
DefaultContentLanguage string DefaultContentLanguage string
// The parser used to parse paths provided by this filesystem. // 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) { func (fs *componentFs) Open(name string) (afero.File, error) {

View file

@ -93,8 +93,8 @@ func (r *resourceSource) GetIdentity() identity.Identity {
return r.path return r.path
} }
func (r *resourceSource) ForEeachIdentity(f func(identity.Identity) bool) { func (r *resourceSource) ForEeachIdentity(f func(identity.Identity) bool) bool {
f(r.GetIdentity()) return f(r.GetIdentity())
} }
func (r *resourceSource) Path() string { func (r *resourceSource) Path() string {
@ -142,14 +142,15 @@ func (n resourceSources) GetIdentity() identity.Identity {
return nil 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 { for _, r := range n {
if r != nil { if r != nil {
if f(r.GetIdentity()) { if f(r.GetIdentity()) {
return return true
} }
} }
} }
return false
} }
func (cfg contentMapConfig) getTaxonomyConfig(s string) (v viewName) { func (cfg contentMapConfig) getTaxonomyConfig(s string) (v viewName) {

View file

@ -605,13 +605,16 @@ func (n contentNodeIs) GetIdentity() identity.Identity {
return n[0].GetIdentity() 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 { for _, nn := range n {
if nn != nil { if nn != nil {
nn.ForEeachIdentity(f) if nn.ForEeachIdentity(f) {
return true
} }
} }
} }
return false
}
func (n contentNodeIs) resetBuildState() { func (n contentNodeIs) resetBuildState() {
for _, nn := range n { for _, nn := range n {
@ -1151,7 +1154,7 @@ func (h *HugoSites) resolveAndResetDependententPageOutputs(ctx context.Context,
// First check the top level dependency manager. // First check the top level dependency manager.
for _, id := range changes { for _, id := range changes {
checkedCounter.Add(1) 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 { for _, po := range p.pageOutputs {
resetPo(po, r) resetPo(po, r)
} }
@ -1167,7 +1170,7 @@ func (h *HugoSites) resolveAndResetDependententPageOutputs(ctx context.Context,
} }
for _, id := range changes { for _, id := range changes {
checkedCounter.Add(1) 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) resetPo(po, r)
continue OUTPUTS continue OUTPUTS
} }

View file

@ -120,8 +120,8 @@ func (p *pageState) GetIdentity() identity.Identity {
return p return p
} }
func (p *pageState) ForEeachIdentity(f func(identity.Identity) bool) { func (p *pageState) ForEeachIdentity(f func(identity.Identity) bool) bool {
f(p) return f(p)
} }
func (p *pageState) GetDependencyManager() identity.Manager { func (p *pageState) GetDependencyManager() identity.Manager {

View file

@ -20,7 +20,6 @@ import (
"sync/atomic" "sync/atomic"
"github.com/gohugoio/hugo/hugofs/files" "github.com/gohugoio/hugo/hugofs/files"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/resources" "github.com/gohugoio/hugo/resources"
"github.com/gohugoio/hugo/common/maps" "github.com/gohugoio/hugo/common/maps"
@ -160,12 +159,6 @@ func (h *HugoSites) newPage(m *pageMeta) (*pageState, *paths.Path, error) {
return nil, nil 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. // Parse the rest of the page content.
m.content, err = m.newCachedContent(h, pi) m.content, err = m.newCachedContent(h, pi)
if err != nil { if err != nil {
@ -178,7 +171,7 @@ func (h *HugoSites) newPage(m *pageMeta) (*pageState, *paths.Path, error) {
pageOutputTemplateVariationsState: &atomic.Uint32{}, pageOutputTemplateVariationsState: &atomic.Uint32{},
resourcesPublishInit: &sync.Once{}, resourcesPublishInit: &sync.Once{},
Staler: m, Staler: m,
dependencyManager: dependencyManager, dependencyManager: m.s.Conf.NewIdentityManager(m.Path()),
pageCommon: &pageCommon{ pageCommon: &pageCommon{
FileProvider: m, FileProvider: m,
AuthorProvider: m, AuthorProvider: m,

View file

@ -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 { providers := struct {
page.PaginatorProvider page.PaginatorProvider
resource.ResourceLinksProvider resource.ResourceLinksProvider
@ -75,7 +70,7 @@ func newPageOutput(
TableOfContentsProvider: page.NopPage, TableOfContentsProvider: page.NopPage,
render: render, render: render,
paginator: pag, paginator: pag,
dependencyManagerOutput: dependencyManager, dependencyManagerOutput: ps.s.Conf.NewIdentityManager((ps.Path() + "/" + f.Name)),
} }
return po return po

View file

@ -1131,7 +1131,7 @@ Single.
Running: true, Running: true,
NeedsOsFS: true, NeedsOsFS: true,
NeedsNpmInstall: true, NeedsNpmInstall: true,
// LogLevel: logg.LevelTrace, // LogLevel: logg.LevelDebug,
}, },
).Build() ).Build()
@ -1261,3 +1261,35 @@ func BenchmarkRebuildContentFileChange(b *testing.B) {
// fmt.Println(bb.LogString()) // 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")
}

View file

@ -169,12 +169,7 @@ func (f *Finder) checkManager(sid *searchID, m Manager, level int) FinderResult
return r return r
} }
ids := m.getIdentities() r = f.search(sid, m, level)
if len(ids) == 0 {
r = FinderNotFound
} else {
r = f.search(sid, ids, level)
}
if r == FinderFoundOneOfMany { if r == FinderFoundOneOfMany {
// Don't cache this one. // 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. // search searches for id in ids.
func (f *Finder) search(sid *searchID, ids Identities, depth int) FinderResult { func (f *Finder) search(sid *searchID, m Manager, depth int) FinderResult {
if len(ids) == 0 {
return FinderNotFound
}
id := sid.id id := sid.id
if id == Anonymous { if id == Anonymous {
@ -285,20 +276,25 @@ func (f *Finder) search(sid *searchID, ids Identities, depth int) FinderResult {
return FinderNotFound return FinderNotFound
} }
for v := range ids { var r FinderResult
r := f.checkOne(sid, v, depth) m.forEeachIdentity(
func(v Identity) bool {
if r > 0 { if r > 0 {
return r panic("should be terminated")
}
r = f.checkOne(sid, v, depth)
if r > 0 {
return true
} }
m := GetDependencyManager(v) m := GetDependencyManager(v)
if r := f.checkManager(sid, m, depth+1); r > 0 { if r = f.checkManager(sid, m, depth+1); r > 0 {
return true
}
return false
},
)
return r return r
} }
}
return FinderNotFound
}
// FinderConfig provides configuration for the Finder. // FinderConfig provides configuration for the Finder.
// Note that we by default will use a strategy where probable matches are // Note that we by default will use a strategy where probable matches are

View file

@ -55,8 +55,8 @@ func NewManager(name string, opts ...ManagerOption) Manager {
// CleanString cleans s to be suitable as an identifier. // CleanString cleans s to be suitable as an identifier.
func CleanString(s string) string { func CleanString(s string) string {
s = strings.ToLower(s) s = strings.ToLower(s)
s = strings.TrimPrefix(filepath.ToSlash(s), "/") s = strings.Trim(filepath.ToSlash(s), "/")
return path.Clean(s) return "/" + path.Clean(s)
} }
// CleanStringIdentity cleans s to be suitable as an identifier and wraps it in a StringIdentity. // 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 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 // FirstIdentity returns the first Identity in v, Anonymous if none found
func FirstIdentity(v any) Identity { func FirstIdentity(v any) Identity {
var result Identity = Anonymous var result Identity = Anonymous
@ -169,7 +152,15 @@ type DependencyManagerScopedProvider interface {
type ForEeachIdentityProvider interface { type ForEeachIdentityProvider interface {
// ForEeachIdentityProvider calls cb for each Identity. // ForEeachIdentityProvider calls cb for each Identity.
// If cb returns true, the iteration is terminated. // 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. // ForEeachIdentityByNameProvider provides a way to look up identities by name.
@ -279,9 +270,10 @@ type IsProbablyDependencyProvider interface {
type Manager interface { type Manager interface {
Identity Identity
AddIdentity(ids ...Identity) AddIdentity(ids ...Identity)
AddIdentityForEach(ids ...ForEeachIdentityProvider)
GetIdentity() Identity GetIdentity() Identity
Reset() Reset()
getIdentities() Identities forEeachIdentity(func(id Identity) bool) bool
} }
type ManagerOption func(m *identityManager) type ManagerOption func(m *identityManager)
@ -303,6 +295,7 @@ type identityManager struct {
// reads currently assumes no concurrent writes. // reads currently assumes no concurrent writes.
mu sync.RWMutex mu sync.RWMutex
ids Identities ids Identities
forEachIds []ForEeachIdentityProvider
// Hooks used in debugging. // Hooks used in debugging.
onAddIdentity func(id Identity) onAddIdentity func(id Identity)
@ -312,7 +305,7 @@ func (im *identityManager) AddIdentity(ids ...Identity) {
im.mu.Lock() im.mu.Lock()
for _, id := range ids { for _, id := range ids {
if id == Anonymous { if id == nil || id == Anonymous {
continue continue
} }
if _, found := im.ids[id]; !found { if _, found := im.ids[id]; !found {
@ -325,6 +318,12 @@ func (im *identityManager) AddIdentity(ids ...Identity) {
im.mu.Unlock() 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 { func (im *identityManager) ContainsIdentity(id Identity) FinderResult {
if im.Identity != Anonymous && id == im.Identity { if im.Identity != Anonymous && id == im.Identity {
return FinderFound return FinderFound
@ -355,10 +354,20 @@ func (im *identityManager) String() string {
return fmt.Sprintf("IdentityManager(%s)", im.name) return fmt.Sprintf("IdentityManager(%s)", im.name)
} }
// TODO(bep) these identities are currently only read on server reloads func (im *identityManager) forEeachIdentity(fn func(id Identity) bool) bool {
// so there should be no concurrency issues, but that may change. // The absense of a lock here is debliberate. This is currently opnly used on server reloads
func (im *identityManager) getIdentities() Identities { // in a single-threaded context.
return im.ids 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 type nopManager int
@ -366,6 +375,9 @@ type nopManager int
func (m *nopManager) AddIdentity(ids ...Identity) { func (m *nopManager) AddIdentity(ids ...Identity) {
} }
func (m *nopManager) AddIdentityForEach(ids ...ForEeachIdentityProvider) {
}
func (m *nopManager) IdentifierBase() string { func (m *nopManager) IdentifierBase() string {
return "" return ""
} }
@ -377,8 +389,8 @@ func (m *nopManager) GetIdentity() Identity {
func (m *nopManager) Reset() { func (m *nopManager) Reset() {
} }
func (m *nopManager) getIdentities() Identities { func (m *nopManager) forEeachIdentity(func(id Identity) bool) bool {
return nil return false
} }
// returns whether further walking should be terminated. // 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 deep {
if m := GetDependencyManager(id); m != nil { if m := GetDependencyManager(id); m != nil {
for id2 := range m.getIdentities() { m.forEeachIdentity(func(id2 Identity) bool {
if walkIdentitiesShallow(id2, level+1, cbRecursive) { return walkIdentitiesShallow(id2, level+1, cbRecursive)
return true })
}
}
} }
} }
return false return false
@ -420,6 +430,9 @@ func walkIdentitiesShallow(v any, level int, cb func(level int, id Identity) boo
if id == Anonymous { if id == Anonymous {
return false return false
} }
if id == nil {
return false
}
return cb(level, id) return cb(level, id)
} }

View file

@ -171,7 +171,7 @@ func (fd *ResourceSourceDescriptor) init(r *Spec) error {
fd.MediaType = mediaType fd.MediaType = mediaType
if fd.DependencyManager == nil { if fd.DependencyManager == nil {
fd.DependencyManager = identity.NopManager fd.DependencyManager = r.Cfg.NewIdentityManager("resource")
} }
return nil return nil

View file

@ -39,7 +39,7 @@ func newResourceCache(rs *Spec, memCache *dynacache.Cache) *ResourceCache {
cacheResources: dynacache.GetOrCreatePartition[string, resource.Resources]( cacheResources: dynacache.GetOrCreatePartition[string, resource.Resources](
memCache, memCache,
"/ress", "/ress",
dynacache.OptionsPartition{ClearWhen: dynacache.ClearOnChange, Weight: 40}, dynacache.OptionsPartition{ClearWhen: dynacache.ClearOnRebuild, Weight: 40},
), ),
cacheResourceTransformation: dynacache.GetOrCreatePartition[string, *resourceAdapterInner]( cacheResourceTransformation: dynacache.GetOrCreatePartition[string, *resourceAdapterInner](
memCache, memCache,

View file

@ -20,6 +20,7 @@ import (
"path" "path"
"github.com/gohugoio/hugo/common/hugio" "github.com/gohugoio/hugo/common/hugio"
"github.com/gohugoio/hugo/identity"
"github.com/gohugoio/hugo/media" "github.com/gohugoio/hugo/media"
"github.com/gohugoio/hugo/resources" "github.com/gohugoio/hugo/resources"
"github.com/gohugoio/hugo/resources/resource" "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. // 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. // We may improve on that in the future, but then we need to know more.
for i, r := range r { for i, rr := range r {
if i > 0 && r.MediaType().Type != resolvedm.Type { 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", r.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) { concatr := func() (hugio.ReadSeekCloser, error) {
var rcsources []hugio.ReadSeekCloser var rcsources []hugio.ReadSeekCloser
for _, s := range r { for _, s := range r {
@ -136,6 +156,7 @@ func (c *Client) Concat(targetPath string, r resource.Resources) (resource.Resou
LazyPublish: true, LazyPublish: true,
OpenReadSeekCloser: concatr, OpenReadSeekCloser: concatr,
TargetPath: targetPath, TargetPath: targetPath,
DependencyManager: idm,
}) })
if err != nil { if err != nil {
return nil, err return nil, err

View file

@ -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. // Get creates a new Resource by opening the given pathname in the assets filesystem.
func (c *Client) Get(pathname string) (resource.Resource, error) { func (c *Client) Get(pathname string) (resource.Resource, error) {
pathname = path.Clean(pathname) 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), // 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. // so we need to check that the file exists here.
filename := filepath.FromSlash(pathname) 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) { if os.IsNotExist(err) {
return nil, nil return nil, nil
} }
@ -87,13 +81,15 @@ func (c *Client) Get(pathname string) (resource.Resource, error) {
return nil, err return nil, err
} }
pi := fi.(hugofs.FileMetaInfo).Meta().PathInfo
return c.rs.NewResource(resources.ResourceSourceDescriptor{ return c.rs.NewResource(resources.ResourceSourceDescriptor{
LazyPublish: true, LazyPublish: true,
OpenReadSeekCloser: func() (hugio.ReadSeekCloser, error) { OpenReadSeekCloser: func() (hugio.ReadSeekCloser, error) {
return c.rs.BaseFs.Assets.Fs.Open(filename) return c.rs.BaseFs.Assets.Fs.Open(filename)
}, },
GroupIdentity: identity.StringIdentity(key), Path: pi,
DependencyManager: c.newDependencyManager(), GroupIdentity: pi,
TargetPath: pathname, TargetPath: pathname,
}) })
}) })