Repository: gohugoio/hugo (language: go)

Issue:
Issue with merged static mounts
<!-- Please answer these questions before submitting your issue. Thanks! -->

### What version of Hugo are you using (`hugo version`)?

<pre>
$ hugo version
hugo v0.123.1-3f8434a62fc91a50f4e5bd43a4435d2d301844ff+extended darwin/arm64 BuildDate=2024-02-21T08:17:45Z VendorInfo=brew
</pre>

### Does this issue reproduce with the latest release?

Yes

# Description

Combined Mount setup like the following stopped working

Contents of the folders
* `static/images`
  *  `header.svg`
* `themes/PaperMod/images/`
  * `screenshot.png`

```
[module]

  [[module.mounts]]
    source = "themes/PaperMod/images/screenshot.png"
    target = "static/images/screenshot.png"

  [[module.mounts]]
    source = "static"
    target = "static"
```

See [Discourse #48374](https://discourse.gohugo.io/t/hugo-v0-123-0-not-all-contents-of-static-are-copied-to-publishdir/48374/6)

# Results

## 0.122.0

```
$ hugo-0.122.0
Start building sites …
hugo v0.122.0-b9a03bd59d5f71a529acb3e33f995e0ef332b3aa+extended darwin/arm64 BuildDate=2024-01-26T15:54:24Z

WARN  Dir layouts/partials/shortcodes/metadata not found

                   | EN  
-------------------+-----
  Pages            | 20  
  Paginator pages  |  0  
  Non-page files   |  0  
  Static files     |  2  
  Processed images |  0  
  Aliases          |  7  
  Sitemaps         |  1  
  Cleaned          |  0  

Total in 57 ms

$ ls public/images/
header.svg	screenshot.png

```

### 0.123.1

```
$ hugo
Start building sites …
hugo v0.123.1-3f8434a62fc91a50f4e5bd43a4435d2d301844ff+extended darwin/arm64 BuildDate=2024-02-21T08:17:45Z VendorInfo=brew

WARN  Dir layouts/partials/shortcodes/metadata not found

                   | EN  
-------------------+-----
  Pages            | 21  
  Paginator pages  |  0  
  Non-page files   |  0  
  Static files     |  1  
  Processed images |  0  
  Aliases          |  7  
  Cleaned          |  0  

Total in 54 ms
$ ls public/images/
screenshot.png
```

# Reproduceable example

See https://github.com/cmahnke/hugo-test/tree/0.123.0-static




# Further ideas
* Maybe it's now depending on the order of the mounts? Shouldn't really matter.


Discussion / hints:
@bep Please don't shoot me, if I'm wrong: IS it possible that this is also causing the remaining SCSS issues? There it's also possible that the search path for stylesheets is expected to be combined / merged from theme and site.

If not, please just ignore this comment.
>If not, please just ignore this comment.

Done :-)



Fix patch:
diff --git a/helpers/general.go b/helpers/general.go
index c7d9fcd85ed..af854041ddf 100644
--- a/helpers/general.go
+++ b/helpers/general.go
@@ -328,7 +328,14 @@ func PrintFs(fs afero.Fs, path string, w io.Writer) {
 	}
 
 	afero.Walk(fs, path, func(path string, info os.FileInfo, err error) error {
-		fmt.Fprintln(w, filepath.ToSlash(path))
+		if err != nil {
+			panic(fmt.Sprintf("error: path %q: %s", path, err))
+		}
+		path = filepath.ToSlash(path)
+		if path == "" {
+			path = "."
+		}
+		fmt.Fprintln(w, path, info.IsDir())
 		return nil
 	})
 }
diff --git a/hugofs/rootmapping_fs.go b/hugofs/rootmapping_fs.go
index 1efb8ee5fa1..ce4243fbb0e 100644
--- a/hugofs/rootmapping_fs.go
+++ b/hugofs/rootmapping_fs.go
@@ -323,7 +323,6 @@ func (fs *RootMappingFs) Stat(name string) (os.FileInfo, error) {
 	if err != nil {
 		return nil, err
 	}
-
 	return fis[0], nil
 }
 
@@ -403,16 +402,42 @@ func (fs *RootMappingFs) getRoot(key string) []RootMapping {
 }
 
 func (fs *RootMappingFs) getRoots(key string) (string, []RootMapping) {
-	return fs.getRootsIn(key, fs.rootMapToReal)
-}
+	tree := fs.rootMapToReal
+	levels := strings.Count(key, filepathSeparator)
+	seen := make(map[RootMapping]bool)
 
-func (fs *RootMappingFs) getRootsReverse(key string) (string, []RootMapping) {
-	return fs.getRootsIn(key, fs.realMapToRoot)
+	var roots []RootMapping
+	var s string
+
+	for {
+		var found bool
+		ss, vv, found := tree.LongestPrefix(key)
+		if !found || (levels < 2 && ss == key) {
+			break
+		}
+
+		for _, rm := range vv.([]RootMapping) {
+			if !seen[rm] {
+				seen[rm] = true
+				roots = append(roots, rm)
+			}
+		}
+		s = ss
+
+		// We may have more than one root for this key, so walk up.
+		oldKey := key
+		key = filepath.Dir(key)
+		if key == oldKey {
+			break
+		}
+	}
+
+	return s, roots
 }
 
-func (fs *RootMappingFs) getRootsIn(key string, tree *radix.Tree) (string, []RootMapping) {
+func (fs *RootMappingFs) getRootsReverse(key string) (string, []RootMapping) {
+	tree := fs.realMapToRoot
 	s, v, found := tree.LongestPrefix(key)
-
 	if !found {
 		return "", nil
 	}


Test patch:
diff --git a/hugolib/filesystems/basefs_test.go b/hugolib/filesystems/basefs_test.go
index ebbb378d317..f5b7b6170e5 100644
--- a/hugolib/filesystems/basefs_test.go
+++ b/hugolib/filesystems/basefs_test.go
@@ -478,11 +478,47 @@ Home.
 	_ = stat("blog/b1.md")
 }
 
+func TestStaticComposite(t *testing.T) {
+	files := `
+-- hugo.toml --
+disableKinds = ["taxonomy", "term"]
+[module]
+[[module.mounts]]
+source = "myfiles/f1.txt"
+target = "static/files/f1.txt"
+[[module.mounts]]
+source = "f3.txt"
+target = "static/f3.txt"
+[[module.mounts]]
+source = "static"
+target = "static"
+-- static/files/f2.txt --
+f2
+-- myfiles/f1.txt --
+f1
+-- f3.txt --
+f3
+-- layouts/home.html --
+Home.
+
+`
+	b := hugolib.Test(t, files)
+
+	b.AssertFs(b.H.BaseFs.StaticFs(""), `
+. true
+f3.txt false
+files true
+files/f1.txt false
+files/f2.txt false
+`)
+}
+
 func checkFileCount(fs afero.Fs, dirname string, c *qt.C, expected int) {
 	c.Helper()
-	count, _, err := countFilesAndGetFilenames(fs, dirname)
-	c.Assert(err, qt.IsNil)
-	c.Assert(count, qt.Equals, expected)
+	count, names, err := countFilesAndGetFilenames(fs, dirname)
+	namesComment := qt.Commentf("filenames: %v", names)
+	c.Assert(err, qt.IsNil, namesComment)
+	c.Assert(count, qt.Equals, expected, namesComment)
 }
 
 func checkFileContent(fs afero.Fs, filename string, c *qt.C, expected ...string) {
diff --git a/hugolib/integrationtest_builder.go b/hugolib/integrationtest_builder.go
index 194b79c6871..8c7017a8723 100644
--- a/hugolib/integrationtest_builder.go
+++ b/hugolib/integrationtest_builder.go
@@ -275,9 +275,13 @@ func (s *IntegrationTestBuilder) AssertFileContentExact(filename string, matches
 }
 
 func (s *IntegrationTestBuilder) AssertPublishDir(matches ...string) {
+	s.AssertFs(s.fs.PublishDir, matches...)
+}
+
+func (s *IntegrationTestBuilder) AssertFs(fs afero.Fs, matches ...string) {
 	s.Helper()
 	var buff bytes.Buffer
-	helpers.PrintFs(s.H.Fs.PublishDir, "", &buff)
+	helpers.PrintFs(fs, "", &buff)
 	printFsLines := strings.Split(buff.String(), "\n")
 	sort.Strings(printFsLines)
 	content := strings.TrimSpace((strings.Join(printFsLines, "\n")))
diff --git a/testscripts/commands/hugo__static_composite.txt b/testscripts/commands/hugo__static_composite.txt
new file mode 100644
index 00000000000..bf73f6abbab
--- /dev/null
+++ b/testscripts/commands/hugo__static_composite.txt
@@ -0,0 +1,27 @@
+hugo
+ls public/files
+checkfile public/files/f1.txt
+checkfile public/files/f2.txt
+checkfile public/f3.txt
+
+-- hugo.toml --
+disableKinds = ["taxonomy", "term"]
+[module]
+[[module.mounts]]
+source = "myfiles/f1.txt"
+target = "static/files/f1.txt"
+[[module.mounts]]
+source = "f3.txt"
+target = "static/f3.txt"
+[[module.mounts]]
+source = "static"
+target = "static"
+-- static/files/f2.txt --
+f2
+-- myfiles/f1.txt --
+f1
+-- f3.txt --
+f3
+-- layouts/home.html --
+Home.
+