From 725eff51761749810057dc7d6f98fa3409e9136a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?=
 <bjorn.erik.pedersen@gmail.com>
Date: Sat, 20 Apr 2024 11:05:35 +0200
Subject: [PATCH] Fix partial rebuilds for SCSS fetched with GetMatch and
 similar Fixes #12395

---
 cache/dynacache/dynacache.go                  | 16 ++++--
 cache/dynacache/dynacache_test.go             |  2 +-
 commands/commandeer.go                        | 50 +++++++++++--------
 hugolib/content_map_page.go                   |  2 +-
 hugolib/hugo_sites_build.go                   | 46 ++++++++++++++---
 .../tocss/scss/scss_integration_test.go       | 31 ++++++++++++
 6 files changed, 114 insertions(+), 33 deletions(-)

diff --git a/cache/dynacache/dynacache.go b/cache/dynacache/dynacache.go
index eab251e5d..e79de5a5b 100644
--- a/cache/dynacache/dynacache.go
+++ b/cache/dynacache/dynacache.go
@@ -140,16 +140,25 @@ func (c *Cache) DrainEvictedIdentities() []identity.Identity {
 }
 
 // ClearMatching clears all partition for which the predicate returns true.
-func (c *Cache) ClearMatching(predicate func(k, v any) bool) {
+func (c *Cache) ClearMatching(predicatePartition func(k string, p PartitionManager) bool, predicateValue func(k, v any) bool) {
+	if predicatePartition == nil {
+		predicatePartition = func(k string, p PartitionManager) bool { return true }
+	}
+	if predicateValue == nil {
+		panic("nil predicateValue")
+	}
 	g := rungroup.Run[PartitionManager](context.Background(), rungroup.Config[PartitionManager]{
 		NumWorkers: len(c.partitions),
 		Handle: func(ctx context.Context, partition PartitionManager) error {
-			partition.clearMatching(predicate)
+			partition.clearMatching(predicateValue)
 			return nil
 		},
 	})
 
-	for _, p := range c.partitions {
+	for k, p := range c.partitions {
+		if !predicatePartition(k, p) {
+			continue
+		}
 		g.Enqueue(p)
 	}
 
@@ -356,6 +365,7 @@ func GetOrCreatePartition[K comparable, V any](c *Cache, name string, opts Optio
 		trace:   c.opts.Log.Logger().WithLevel(logg.LevelTrace).WithField("partition", name),
 		opts:    opts,
 	}
+
 	c.partitions[name] = partition
 
 	return partition
diff --git a/cache/dynacache/dynacache_test.go b/cache/dynacache/dynacache_test.go
index 53de2385e..275e63f0b 100644
--- a/cache/dynacache/dynacache_test.go
+++ b/cache/dynacache/dynacache_test.go
@@ -156,7 +156,7 @@ func TestClear(t *testing.T) {
 
 	cache = newTestCache(t)
 
-	cache.ClearMatching(func(k, v any) bool {
+	cache.ClearMatching(nil, func(k, v any) bool {
 		return k.(string) == "clearOnRebuild"
 	})
 
diff --git a/commands/commandeer.go b/commands/commandeer.go
index f18c3f813..616a3c867 100644
--- a/commands/commandeer.go
+++ b/commands/commandeer.go
@@ -128,6 +128,7 @@ type rootCommand struct {
 	verbose bool
 	debug   bool
 	quiet   bool
+	devMode bool // Hidden flag.
 
 	renderToMemory bool
 
@@ -423,29 +424,33 @@ func (r *rootCommand) PreRun(cd, runner *simplecobra.Commandeer) error {
 func (r *rootCommand) createLogger(running bool) (loggers.Logger, error) {
 	level := logg.LevelWarn
 
-	if r.logLevel != "" {
-		switch strings.ToLower(r.logLevel) {
-		case "debug":
-			level = logg.LevelDebug
-		case "info":
-			level = logg.LevelInfo
-		case "warn", "warning":
-			level = logg.LevelWarn
-		case "error":
-			level = logg.LevelError
-		default:
-			return nil, fmt.Errorf("invalid log level: %q, must be one of debug, warn, info or error", r.logLevel)
-		}
+	if r.devMode {
+		level = logg.LevelTrace
 	} else {
-		if r.verbose {
-			hugo.Deprecate("--verbose", "use --logLevel info", "v0.114.0")
-			hugo.Deprecate("--verbose", "use --logLevel info", "v0.114.0")
-			level = logg.LevelInfo
-		}
+		if r.logLevel != "" {
+			switch strings.ToLower(r.logLevel) {
+			case "debug":
+				level = logg.LevelDebug
+			case "info":
+				level = logg.LevelInfo
+			case "warn", "warning":
+				level = logg.LevelWarn
+			case "error":
+				level = logg.LevelError
+			default:
+				return nil, fmt.Errorf("invalid log level: %q, must be one of debug, warn, info or error", r.logLevel)
+			}
+		} else {
+			if r.verbose {
+				hugo.Deprecate("--verbose", "use --logLevel info", "v0.114.0")
+				hugo.Deprecate("--verbose", "use --logLevel info", "v0.114.0")
+				level = logg.LevelInfo
+			}
 
-		if r.debug {
-			hugo.Deprecate("--debug", "use --logLevel debug", "v0.114.0")
-			level = logg.LevelDebug
+			if r.debug {
+				hugo.Deprecate("--debug", "use --logLevel debug", "v0.114.0")
+				level = logg.LevelDebug
+			}
 		}
 	}
 
@@ -505,10 +510,13 @@ Complete documentation is available at https://gohugo.io/.`
 
 	cmd.PersistentFlags().BoolVarP(&r.verbose, "verbose", "v", false, "verbose output")
 	cmd.PersistentFlags().BoolVarP(&r.debug, "debug", "", false, "debug output")
+	cmd.PersistentFlags().BoolVarP(&r.devMode, "devMode", "", false, "only used for internal testing, flag hidden.")
 	cmd.PersistentFlags().StringVar(&r.logLevel, "logLevel", "", "log level (debug|info|warn|error)")
 	_ = cmd.RegisterFlagCompletionFunc("logLevel", cobra.FixedCompletions([]string{"debug", "info", "warn", "error"}, cobra.ShellCompDirectiveNoFileComp))
 	cmd.Flags().BoolVarP(&r.buildWatch, "watch", "w", false, "watch filesystem for changes and recreate as needed")
 
+	cmd.PersistentFlags().MarkHidden("devMode")
+
 	// Configure local flags
 	applyLocalFlagsBuild(cmd, r)
 
diff --git a/hugolib/content_map_page.go b/hugolib/content_map_page.go
index aa32b5320..50e1bc35d 100644
--- a/hugolib/content_map_page.go
+++ b/hugolib/content_map_page.go
@@ -1084,7 +1084,7 @@ func (h *HugoSites) resolveAndClearStateForIdentities(
 				return b
 			}
 
-			h.MemCache.ClearMatching(shouldDelete)
+			h.MemCache.ClearMatching(nil, shouldDelete)
 
 			return ll, nil
 		}); err != nil {
diff --git a/hugolib/hugo_sites_build.go b/hugolib/hugo_sites_build.go
index 411f90734..3beb072e3 100644
--- a/hugolib/hugo_sites_build.go
+++ b/hugolib/hugo_sites_build.go
@@ -23,6 +23,7 @@ import (
 	"path"
 	"path/filepath"
 	"strings"
+	"sync"
 	"time"
 
 	"github.com/bep/logg"
@@ -46,6 +47,7 @@ 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"
 
@@ -758,15 +760,45 @@ func (h *HugoSites) processPartial(ctx context.Context, l logg.LevelLogger, conf
 				}
 			}
 		case files.ComponentFolderAssets:
-			logger.Println("Asset changed", pathInfo.Path())
+			p := pathInfo.Path()
+			logger.Println("Asset changed", p)
+
+			var matches []any
+			var mu sync.Mutex
+
+			h.MemCache.ClearMatching(
+				func(k string, pm dynacache.PartitionManager) bool {
+					// Avoid going through everything.
+					return strings.HasPrefix(k, "/res")
+				},
+				func(k, v any) bool {
+					if strings.Contains(k.(string), p) {
+						mu.Lock()
+						defer mu.Unlock()
+						switch vv := v.(type) {
+						case resource.Resources:
+							// GetMatch/Match.
+							for _, r := range vv {
+								matches = append(matches, r)
+							}
+							return true
+						default:
+							matches = append(matches, vv)
+							return true
+
+						}
+					}
+					return false
+				})
 
 			var hasID bool
-			r, _ := h.ResourceSpec.ResourceCache.Get(context.Background(), dynacache.CleanKey(pathInfo.Base()))
-			identity.WalkIdentitiesShallow(r, func(level int, rid identity.Identity) bool {
-				hasID = true
-				changes = append(changes, rid)
-				return false
-			})
+			for _, r := range matches {
+				identity.WalkIdentitiesShallow(r, func(level int, rid identity.Identity) bool {
+					hasID = true
+					changes = append(changes, rid)
+					return false
+				})
+			}
 			if !hasID {
 				changes = append(changes, pathInfo)
 			}
diff --git a/resources/resource_transformers/tocss/scss/scss_integration_test.go b/resources/resource_transformers/tocss/scss/scss_integration_test.go
index c193ca8af..02e2b9200 100644
--- a/resources/resource_transformers/tocss/scss/scss_integration_test.go
+++ b/resources/resource_transformers/tocss/scss/scss_integration_test.go
@@ -327,3 +327,34 @@ Styles: {{ $r.RelPermalink }}
 
 	b.AssertFileContent("public/index.html", "Styles: /scss/main.css")
 }
+
+func TestRebuildAssetGetMatch(t *testing.T) {
+	t.Parallel()
+	if !scss.Supports() {
+		t.Skip()
+	}
+
+	files := `
+-- assets/scss/main.scss --
+b {
+	color: red;
+}
+-- layouts/index.html --
+{{ $r := resources.GetMatch "scss/main.scss" |  toCSS  }}
+T1: {{ $r.Content }}
+	`
+
+	b := hugolib.NewIntegrationTestBuilder(
+		hugolib.IntegrationTestConfig{
+			T:           t,
+			TxtarString: files,
+			NeedsOsFS:   true,
+			Running:     true,
+		}).Build()
+
+	b.AssertFileContent("public/index.html", `color: red`)
+
+	b.EditFiles("assets/scss/main.scss", `b { color: blue; }`).Build()
+
+	b.AssertFileContent("public/index.html", `color: blue`)
+}