diff options
45 files changed, 631 insertions, 186 deletions
@@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2022 The Hugo Authors. + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/SECURITY.md b/SECURITY.md index 320b2ff54..6ac90f072 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,4 +4,4 @@ Please report (suspected) security vulnerabilities to **[bjorn.erik.pedersen@gmail.com](mailto:bjorn.erik.pedersen@gmail.com)**. You will receive a response from us within 48 hours. If we can confirm the issue, we will release a patch as soon as possible depending on the complexity of the issue but historically within days. -Also see [Hugo's Security Model](https://gohugo.io/about/security-model/). +Also see [Hugo's Security Model](https://gohugo.io/about/security/). diff --git a/cache/dynacache/dynacache.go b/cache/dynacache/dynacache.go index e79de5a5b..6190dd234 100644 --- a/cache/dynacache/dynacache.go +++ b/cache/dynacache/dynacache.go @@ -67,7 +67,7 @@ func New(opts Options) *Cache { evictedIdentities := collections.NewStack[identity.Identity]() onEvict := func(k, v any) { - if !opts.Running { + if !opts.Watching { return } identity.WalkIdentitiesShallow(v, func(level int, id identity.Identity) bool { @@ -97,7 +97,7 @@ type Options struct { CheckInterval time.Duration MaxSize int MinMaxSize int - Running bool + Watching bool } // Options for a partition. @@ -385,13 +385,37 @@ type Partition[K comparable, V any] struct { // GetOrCreate gets or creates a value for the given key. func (p *Partition[K, V]) GetOrCreate(key K, create func(key K) (V, error)) (V, error) { + v, err := p.doGetOrCreate(key, create) + if err != nil { + return p.zero, err + } + if resource.StaleVersion(v) > 0 { + p.c.Delete(key) + return p.doGetOrCreate(key, create) + } + return v, err +} + +func (p *Partition[K, V]) doGetOrCreate(key K, create func(key K) (V, error)) (V, error) { v, _, err := p.c.GetOrCreate(key, create) return v, err } +func (p *Partition[K, V]) GetOrCreateWitTimeout(key K, duration time.Duration, create func(key K) (V, error)) (V, error) { + v, err := p.doGetOrCreateWitTimeout(key, duration, create) + if err != nil { + return p.zero, err + } + if resource.StaleVersion(v) > 0 { + p.c.Delete(key) + return p.doGetOrCreateWitTimeout(key, duration, create) + } + return v, err +} + // GetOrCreateWitTimeout gets or creates a value for the given key and times out if the create function // takes too long. -func (p *Partition[K, V]) GetOrCreateWitTimeout(key K, duration time.Duration, create func(key K) (V, error)) (V, error) { +func (p *Partition[K, V]) doGetOrCreateWitTimeout(key K, duration time.Duration, create func(key K) (V, error)) (V, error) { resultch := make(chan V, 1) errch := make(chan error, 1) @@ -448,7 +472,7 @@ func (p *Partition[K, V]) clearOnRebuild(changeset ...identity.Identity) { shouldDelete := func(key K, v V) bool { // We always clear elements marked as stale. - if resource.IsStaleAny(v) { + if resource.StaleVersion(v) > 0 { return true } @@ -503,8 +527,8 @@ func (p *Partition[K, V]) Keys() []K { func (p *Partition[K, V]) clearStale() { p.c.DeleteFunc(func(key K, v V) bool { - isStale := resource.IsStaleAny(v) - if isStale { + staleVersion := resource.StaleVersion(v) + if staleVersion > 0 { p.trace.Log( logg.StringFunc( func() string { @@ -514,7 +538,7 @@ func (p *Partition[K, V]) clearStale() { ) } - return isStale + return staleVersion > 0 }) } diff --git a/cache/dynacache/dynacache_test.go b/cache/dynacache/dynacache_test.go index 275e63f0b..a58a8d94b 100644 --- a/cache/dynacache/dynacache_test.go +++ b/cache/dynacache/dynacache_test.go @@ -29,12 +29,12 @@ var ( ) type testItem struct { - name string - isStale bool + name string + staleVersion uint32 } -func (t testItem) IsStale() bool { - return t.isStale +func (t testItem) StaleVersion() uint32 { + return t.staleVersion } func (t testItem) IdentifierBase() string { @@ -109,7 +109,7 @@ func newTestCache(t *testing.T) *Cache { p2.GetOrCreate("clearBecauseStale", func(string) (testItem, error) { return testItem{ - isStale: true, + staleVersion: 32, }, nil }) @@ -121,7 +121,7 @@ func newTestCache(t *testing.T) *Cache { p2.GetOrCreate("clearNever", func(string) (testItem, error) { return testItem{ - isStale: false, + staleVersion: 0, }, nil }) diff --git a/commands/commandeer.go b/commands/commandeer.go index 616a3c867..5d4f02a4d 100644 --- a/commands/commandeer.go +++ b/commands/commandeer.go @@ -327,12 +327,12 @@ func (r *rootCommand) Name() string { } func (r *rootCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error { + b := newHugoBuilder(r, nil) + if !r.buildWatch { - defer r.timeTrack(time.Now(), "Total") + defer b.postBuild("Total", time.Now()) } - b := newHugoBuilder(r, nil) - if err := b.loadConfig(cd, false); err != nil { return err } diff --git a/commands/hugobuilder.go b/commands/hugobuilder.go index f5bc73087..657048d48 100644 --- a/commands/hugobuilder.go +++ b/commands/hugobuilder.go @@ -75,9 +75,14 @@ type hugoBuilder struct { errState hugoBuilderErrState } +var errConfigNotSet = errors.New("config not set") + func (c *hugoBuilder) withConfE(fn func(conf *commonConfig) error) error { c.confmu.Lock() defer c.confmu.Unlock() + if c.conf == nil { + return errConfigNotSet + } return fn(c.conf) } @@ -585,7 +590,7 @@ func (c *hugoBuilder) fullRebuild(changeType string) { time.Sleep(2 * time.Second) }() - defer c.r.timeTrack(time.Now(), "Rebuilt") + defer c.postBuild("Rebuilt", time.Now()) err := c.reloadConfig() if err != nil { @@ -855,7 +860,7 @@ func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher, c.changeDetector.PrepareNew() func() { - defer c.r.timeTrack(time.Now(), "Total") + defer c.postBuild("Total", time.Now()) if err := c.rebuildSites(dynamicEvents); err != nil { c.handleBuildErr(err, "Rebuild failed") } @@ -901,6 +906,13 @@ func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher, } } +func (c *hugoBuilder) postBuild(what string, start time.Time) { + if h, err := c.hugo(); err == nil && h.Conf.Running() { + h.LogServerAddresses() + } + c.r.timeTrack(start, what) +} + func (c *hugoBuilder) hugo() (*hugolib.HugoSites, error) { var h *hugolib.HugoSites if err := c.withConfE(func(conf *commonConfig) error { diff --git a/commands/new.go b/commands/new.go index 045a920a3..4bad0059f 100644 --- a/commands/new.go +++ b/commands/new.go @@ -209,7 +209,7 @@ func (c *newCommand) newSiteNextStepsText(path string, format string) string { 1. Change the current directory to ` + path + `. 2. Create or install a theme: - Create a new theme with the command "hugo new theme <THEMENAME>" - - Install a theme from https://themes.gohugo.io/ + - Or, install a theme from https://themes.gohugo.io/ 3. Edit hugo.` + format + `, setting the "theme" property to the theme name. 4. Create new content with the command "hugo new content `) diff --git a/common/hugo/version_current.go b/common/hugo/version_current.go index 5f4e104ff..b71bdeec1 100644 --- a/common/hugo/version_current.go +++ b/common/hugo/version_current.go @@ -17,7 +17,7 @@ package hugo // This should be the only one. var CurrentVersion = Version{ Major: 0, - Minor: 126, - PatchLevel: 0, - Suffix: "-DEV", + Minor: 125, + PatchLevel: 6, + Suffix: "", } diff --git a/config/allconfig/configlanguage.go b/config/allconfig/configlanguage.go index 900db75ce..c7f1c276a 100644 --- a/config/allconfig/configlanguage.go +++ b/config/allconfig/configlanguage.go @@ -71,6 +71,9 @@ func (c ConfigLanguage) Environment() string { } func (c ConfigLanguage) IsMultihost() bool { + if len(c.m.Languages)-len(c.config.C.DisabledLanguages) <= 1 { + return false + } return c.m.IsMultihost } diff --git a/deps/deps.go b/deps/deps.go index 78a5330b7..41a8ecb3e 100644 --- a/deps/deps.go +++ b/deps/deps.go @@ -155,7 +155,7 @@ func (d *Deps) Init() error { } if d.MemCache == nil { - d.MemCache = dynacache.New(dynacache.Options{Running: d.Conf.Running(), Log: d.Log}) + d.MemCache = dynacache.New(dynacache.Options{Watching: d.Conf.Watching(), Log: d.Log}) } if d.PathSpec == nil { @@ -55,7 +55,7 @@ require ( github.com/niklasfasching/go-org v1.7.0 github.com/olekukonko/tablewriter v0.0.5 github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 - github.com/pelletier/go-toml/v2 v2.2.1 + github.com/pelletier/go-toml/v2 v2.2.2 github.com/rogpeppe/go-internal v1.12.0 github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd github.com/sanity-io/litter v1.5.5 @@ -64,8 +64,8 @@ require ( github.com/spf13/cobra v1.8.0 github.com/spf13/fsync v0.10.1 github.com/spf13/pflag v1.0.5 - github.com/tdewolff/minify/v2 v2.20.19 - github.com/tdewolff/parse/v2 v2.7.12 + github.com/tdewolff/minify/v2 v2.20.20 + github.com/tdewolff/parse/v2 v2.7.13 github.com/yuin/goldmark v1.7.1 github.com/yuin/goldmark-emoji v1.0.2 go.uber.org/automaxprocs v1.5.3 @@ -376,8 +376,8 @@ github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2DcNVpwGmV9E1BkGknEliJkfwQj0= github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= -github.com/pelletier/go-toml/v2 v2.2.1 h1:9TA9+T8+8CUCO2+WYnDLCgrYi9+omqKXyjDtosvtEhg= -github.com/pelletier/go-toml/v2 v2.2.1/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= @@ -429,10 +429,10 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/tdewolff/minify/v2 v2.20.19 h1:tX0SR0LUrIqGoLjXnkIzRSIbKJ7PaNnSENLD4CyH6Xo= -github.com/tdewolff/minify/v2 v2.20.19/go.mod h1:ulkFoeAVWMLEyjuDz1ZIWOA31g5aWOawCFRp9R/MudM= -github.com/tdewolff/parse/v2 v2.7.12 h1:tgavkHc2ZDEQVKy1oWxwIyh5bP4F5fEh/JmBwPP/3LQ= -github.com/tdewolff/parse/v2 v2.7.12/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA= +github.com/tdewolff/minify/v2 v2.20.20 h1:vhULb+VsW2twkplgsawAoUY957efb+EdiZ7zu5fUhhk= +github.com/tdewolff/minify/v2 v2.20.20/go.mod h1:GYaLXFpIIwsX99apQHXfGdISUdlA98wmaoWxjT9C37k= +github.com/tdewolff/parse/v2 v2.7.13 h1:iSiwOUkCYLNfapHoqdLcqZVgvQ0jrsao8YYKP/UJYTI= +github.com/tdewolff/parse/v2 v2.7.13/go.mod h1:3FbJWZp3XT9OWVN3Hmfp0p/a08v4h8J9W1aghka0soA= github.com/tdewolff/test v1.0.11-0.20231101010635-f1265d231d52/go.mod h1:6DAvZliBAAnD7rhVgwaM7DE5/d9NMOAJ09SqYqeK4QE= github.com/tdewolff/test v1.0.11-0.20240106005702-7de5f7df4739 h1:IkjBCtQOOjIn03u/dMQK9g+Iw9ewps4mCl1nB8Sscbo= github.com/ugorji/go/codec v1.2.7 h1:YPXUKf7fYbp/y8xloBqZOw2qaVggbfwMlI8WM3wZUJ0= diff --git a/hugolib/cascade_test.go b/hugolib/cascade_test.go index 8075f190f..7a4b6e6be 100644 --- a/hugolib/cascade_test.go +++ b/hugolib/cascade_test.go @@ -329,7 +329,7 @@ cascade: counters := &buildCounters{} b.Build(BuildCfg{testCounters: counters}) - b.Assert(int(counters.contentRenderCounter.Load()), qt.Equals, 2) + b.Assert(int(counters.contentRenderCounter.Load()), qt.Equals, 1) b.AssertFileContent("public/post/index.html", `Banner: post.jpg|Layout: postlayout|Type: posttype|Content: <p>content edit</p>`) b.AssertFileContent("public/post/dir/p1/index.html", `Banner: post.jpg|Layout: postlayout|`) @@ -672,6 +672,55 @@ S1|p1:|p2:p2| }) } +func TestCascadeEditIssue12449(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- +baseURL = "https://example.com" +disableKinds = ['sitemap','rss', 'home', 'taxonomy','term'] +disableLiveReload = true +-- layouts/_default/list.html -- +Title: {{ .Title }}|{{ .Content }}|cascadeparam: {{ .Params.cascadeparam }}| +-- layouts/_default/single.html -- +Title: {{ .Title }}|{{ .Content }}|cascadeparam: {{ .Params.cascadeparam }}| +-- content/mysect/_index.md -- +--- +title: mysect +cascade: + description: descriptionvalue + params: + cascadeparam: cascadeparamvalue +--- +mysect-content| +-- content/mysect/p1/index.md -- +--- +slug: p1 +--- +p1-content| +-- content/mysect/subsect/_index.md -- +--- +slug: subsect +--- +subsect-content| +` + + b := TestRunning(t, files) + + // Make the cascade set the title. + b.EditFileReplaceAll("content/mysect/_index.md", "description: descriptionvalue", "title: cascadetitle").Build() + b.AssertFileContent("public/mysect/subsect/index.html", "Title: cascadetitle|") + + // Edit cascade title. + b.EditFileReplaceAll("content/mysect/_index.md", "title: cascadetitle", "title: cascadetitle-edit").Build() + b.AssertFileContent("public/mysect/subsect/index.html", "Title: cascadetitle-edit|") + + // Revert title change. + // The step below failed in #12449. + b.EditFileReplaceAll("content/mysect/_index.md", "title: cascadetitle-edit", "description: descriptionvalue").Build() + b.AssertFileContent("public/mysect/subsect/index.html", "Title: |") +} + // Issue 11977. func TestCascadeExtensionInPath(t *testing.T) { t.Parallel() diff --git a/hugolib/content_map_page.go b/hugolib/content_map_page.go index 50e1bc35d..a0bff7472 100644 --- a/hugolib/content_map_page.go +++ b/hugolib/content_map_page.go @@ -825,6 +825,9 @@ func (s *contentNodeShifter) Insert(old, new contentNodeI) contentNodeI { panic(fmt.Sprintf("unknown type %T", new)) } if vv.s.languagei == newp.s.languagei { + if newp != old { + resource.MarkStale(old) + } return new } is := make(contentNodeIs, s.numLanguages) @@ -836,7 +839,10 @@ func (s *contentNodeShifter) Insert(old, new contentNodeI) contentNodeI { if !ok { panic(fmt.Sprintf("unknown type %T", new)) } - resource.MarkStale(vv[newp.s.languagei]) + oldp := vv[newp.s.languagei] + if oldp != newp { + resource.MarkStale(oldp) + } vv[newp.s.languagei] = new return vv case *resourceSource: @@ -845,6 +851,9 @@ func (s *contentNodeShifter) Insert(old, new contentNodeI) contentNodeI { panic(fmt.Sprintf("unknown type %T", new)) } if vv.LangIndex() == newp.LangIndex() { + if vv != newp { + resource.MarkStale(vv) + } return new } rs := make(resourceSources, s.numLanguages) @@ -856,7 +865,10 @@ func (s *contentNodeShifter) Insert(old, new contentNodeI) contentNodeI { if !ok { panic(fmt.Sprintf("unknown type %T", new)) } - resource.MarkStale(vv[newp.LangIndex()]) + oldp := vv[newp.LangIndex()] + if oldp != newp { + resource.MarkStale(oldp) + } vv[newp.LangIndex()] = newp return vv default: @@ -1054,7 +1066,7 @@ func (h *HugoSites) resolveAndClearStateForIdentities( ) for _, id := range changes { - if staler, ok := id.(resource.Staler); ok && !staler.IsStale() { + if staler, ok := id.(resource.Staler); ok { var msgDetail string if p, ok := id.(*pageState); ok && p.File() != nil { msgDetail = fmt.Sprintf(" (%s)", p.File().Filename()) diff --git a/hugolib/content_render_hooks_test.go b/hugolib/content_render_hooks_test.go index 36d1e626f..abe305762 100644 --- a/hugolib/content_render_hooks_test.go +++ b/hugolib/content_render_hooks_test.go @@ -14,6 +14,7 @@ package hugolib import ( + "fmt" "strings" "testing" ) @@ -241,3 +242,52 @@ iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAA "p1|<p><a href=\"p2\">P2</a>", "<img src=\"pixel.png\" alt=\"Pixel\">") }) } + +func TestRenderHooksDefaultEscape(t *testing.T) { + files := ` +-- hugo.toml -- +[markup.goldmark.renderHooks] +[markup.goldmark.renderHooks.image] + enableDefault = ENABLE +[markup.goldmark.renderHooks.link] +enableDefault = ENABLE +[markup.goldmark.parser] +wrapStandAloneImageWithinParagraph = false +[markup.goldmark.parser.attribute] +block = true +title = true +-- content/_index.md -- +--- +title: "Home" +--- +Link: [text-"<>&](/destination-"<> 'title-"<>&') + +Image: ![alt-"<>&](/destination-"<> 'title-"<>&') +{class="><script>alert()</script>" id="baz"} + +-- layouts/index.html -- +{{ .Content }} +` + + for _, enabled := range []bool{true, false} { + enabled := enabled + t.Run(fmt.Sprint(enabled), func(t *testing.T) { + t.Parallel() + b := Test(t, strings.ReplaceAll(files, "ENABLE", fmt.Sprint(enabled))) + + // The escaping is slightly different between the two. + if enabled { + b.AssertFileContent("public/index.html", + "Link: <a href=\"/destination-%22%3C%3E\" title=\"title-"<>&\">text-"<>&</a>", + "img alt=\"alt-"<>&\" src=\"/destination-%22%3C%3E\" title=\"title-"<>&\">", + "><script>", + ) + } else { + b.AssertFileContent("public/index.html", + "Link: <a href=\"/destination-%22%3C%3E\" title=\"title-"<>&\">text-"<>&</a>", + "Image: <img src=\"/destination-%22%3C%3E\" alt=\"alt-"<>&\" title=\"title-"<>&\">", + ) + } + }) + } +} diff --git a/hugolib/hugo_sites_build.go b/hugolib/hugo_sites_build.go index 3beb072e3..6a9afee99 100644 --- a/hugolib/hugo_sites_build.go +++ b/hugolib/hugo_sites_build.go @@ -23,11 +23,9 @@ import ( "path" "path/filepath" "strings" - "sync" "time" "github.com/bep/logg" - "github.com/gohugoio/hugo/cache/dynacache" "github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/hugofs" "github.com/gohugoio/hugo/hugofs/files" @@ -47,7 +45,6 @@ import ( "github.com/gohugoio/hugo/resources/page" "github.com/gohugoio/hugo/resources/page/siteidentities" "github.com/gohugoio/hugo/resources/postpub" - "github.com/gohugoio/hugo/resources/resource" "github.com/spf13/afero" @@ -277,7 +274,7 @@ func (h *HugoSites) assemble(ctx context.Context, l logg.LevelLogger, bcfg *Buil changes := assembleChanges.Changes() - // Changes from the assemble step (e.g. lastMod, cascase) needs a re-calculation + // Changes from the assemble step (e.g. lastMod, cascade) needs a re-calculation // of what needs to be re-built. if len(changes) > 0 { if err := h.resolveAndClearStateForIdentities(ctx, l, nil, changes); err != nil { @@ -598,6 +595,10 @@ type pathChange struct { isDir bool } +func (p pathChange) isStructuralChange() bool { + return p.delete || p.isDir +} + // processPartial prepares the Sites' sources for a partial rebuild. func (h *HugoSites) processPartial(ctx context.Context, l logg.LevelLogger, config *BuildCfg, init func(config *BuildCfg) error, events []fsnotify.Event) error { |