Repository: gohugoio/hugo (language: go)

Issue:
server: List page doesn't update after creating content
Reference: <https://discourse.gohugo.io/t/hugo-server-d-doesnt-show-new-posts-consistently/49266>

This worked as expected < v0.123.0.

####  archetypes/default.md

```text
---
title: {{ replace .File.ContentBaseName "-" " " }}
draft: false
---
```

#### layouts/_default/list.html

```
{{ range .Pages }}{{ .Title }}|{{ end }}
```
#### content

```text
content/
└── p1.md
```

#### actions

Run `hugo server` and look at the home page. You'll see `p1|` as expected.

In a new terminal, while the server is running, create a new page:

```text
hugo new p2.md
```

#### expected

The home page should display `p1|p2|`

#### actual 

The home displays `p1|` (no change)

#### test case

This isn't very helpful because it passes, but perhaps it will be useful as the basis for a different test.

<details>
<summary>integration test</summary>

```go
func TestFoo(t *testing.T) {
	t.Parallel()

	files := `
-- hugo.toml --
disableKinds = ['rss','section','sitemap','taxonomy','term']
-- layouts/_default/list.html --
{{ range .Pages }}{{ .Title }}|{{ end }}
-- layouts/_default/single.html --
{{ .Title }}
-- content/p1.md --
---
title: p1
---
`

	b := hugolib.Test(t, files, hugolib.TestOptRunning())
	b.AssertFileContent("public/index.html", "p1|")

	b.AddFiles("content/p2.md", "---\ntitle: p2\n---").Build()
	b.AssertFileContent("public/index.html", "p1|p2|") // this test passes, which doesn't match reality
}
```
</details>
<br>






Discussion / hints:
On what OS does this fail? Windows?
OP: hugo v0.124.1+extended linux/amd64 BuildDate=unknown
Me: hugo v0.124.1-db083b05f16c945fec04f745f0ca8640560cf1ec+extended linux/amd64
I have tested this again and cannot reproduce this on MacOS. I may eventually spin up a Linux VM and test this, but that may take some time.
I have a test repo that log the filesystem events for the different OSes. Looking at it now I see for created regular files:

MacOS:

```
CREATE        "/var/folders/h1/8hndypj13nsbj5pn4xsnv1tm0000gn/T/TestListEvents885546749/001/regularfile.txt"
```

Ubuntu:

```
CREATE        "/tmp/TestListEvents3296403036/001/regularfile.txt"
WRITE         "/tmp/TestListEvents3296403036/001/regularfile.txt"
```

In the `hugolib.TestRunning` we simulate these events (they doesn't get triggered with the mem fs, also they're very hard to get stable in CI).

But the above can explain what's seen above. I'll take another look.

Fix patch:
diff --git a/hugolib/hugo_sites_build.go b/hugolib/hugo_sites_build.go
index a77900e2753..411f90734e7 100644
--- a/hugolib/hugo_sites_build.go
+++ b/hugolib/hugo_sites_build.go
@@ -610,7 +610,7 @@ func (h *HugoSites) processPartial(ctx context.Context, l logg.LevelLogger, conf
 
 	// For a list of events for the different OSes, see the test output in https://github.com/bep/fsnotifyeventlister/.
 	events = h.fileEventsFilter(events)
-	events = h.fileEventsTranslate(events)
+	events = h.fileEventsTrim(events)
 	eventInfos := h.fileEventsApplyInfo(events)
 
 	logger := h.Log
diff --git a/hugolib/site.go b/hugolib/site.go
index 9ab361722c9..b66a1284b1a 100644
--- a/hugolib/site.go
+++ b/hugolib/site.go
@@ -424,7 +424,35 @@ func (h *HugoSites) fileEventsFilter(events []fsnotify.Event) []fsnotify.Event {
 		events[n] = ev
 		n++
 	}
-	return events[:n]
+	events = events[:n]
+
+	eventOrdinal := func(e fsnotify.Event) int {
+		// Pull the structural changes to the top.
+		if e.Op.Has(fsnotify.Create) {
+			return 1
+		}
+		if e.Op.Has(fsnotify.Remove) {
+			return 2
+		}
+		if e.Op.Has(fsnotify.Rename) {
+			return 3
+		}
+		if e.Op.Has(fsnotify.Write) {
+			return 4
+		}
+		return 5
+	}
+
+	sort.Slice(events, func(i, j int) bool {
+		// First sort by event type.
+		if eventOrdinal(events[i]) != eventOrdinal(events[j]) {
+			return eventOrdinal(events[i]) < eventOrdinal(events[j])
+		}
+		// Then sort by name.
+		return events[i].Name < events[j].Name
+	})
+
+	return events
 }
 
 type fileEventInfo struct {
@@ -494,41 +522,17 @@ func (h *HugoSites) fileEventsApplyInfo(events []fsnotify.Event) []fileEventInfo
 	return infos
 }
 
-func (h *HugoSites) fileEventsTranslate(events []fsnotify.Event) []fsnotify.Event {
-	eventMap := make(map[string][]fsnotify.Event)
-
-	// We often get a Remove etc. followed by a Create, a Create followed by a Write.
-	// Remove the superfluous events to make the update logic simpler.
-	for _, ev := range events {
-		eventMap[ev.Name] = append(eventMap[ev.Name], ev)
-	}
-
+func (h *HugoSites) fileEventsTrim(events []fsnotify.Event) []fsnotify.Event {
+	seen := make(map[string]bool)
 	n := 0
 	for _, ev := range events {
-		mapped := eventMap[ev.Name]
-
-		// Keep one
-		found := false
-		var kept fsnotify.Event
-		for i, ev2 := range mapped {
-			if i == 0 {
-				kept = ev2
-			}
-
-			if ev2.Op&fsnotify.Write == fsnotify.Write {
-				kept = ev2
-				found = true
-			}
-
-			if !found && ev2.Op&fsnotify.Create == fsnotify.Create {
-				kept = ev2
-			}
+		if seen[ev.Name] {
+			continue
 		}
-
-		events[n] = kept
+		seen[ev.Name] = true
+		events[n] = ev
 		n++
 	}
-
 	return events
 }
 


Test patch:
diff --git a/hugolib/integrationtest_builder.go b/hugolib/integrationtest_builder.go
index be11c18d692..8daf7b1caff 100644
--- a/hugolib/integrationtest_builder.go
+++ b/hugolib/integrationtest_builder.go
@@ -10,6 +10,7 @@ import (
 	"os"
 	"path/filepath"
 	"regexp"
+	"runtime"
 	"sort"
 	"strings"
 	"sync"
@@ -685,8 +686,17 @@ func (s *IntegrationTestBuilder) build(cfg BuildCfg) error {
 	return nil
 }
 
+// We simulate the fsnotify events.
+// See the test output in https://github.com/bep/fsnotifyeventlister for what events gets produced
+// by the different OSes.
 func (s *IntegrationTestBuilder) changeEvents() []fsnotify.Event {
-	var events []fsnotify.Event
+	var (
+		events    []fsnotify.Event
+		isLinux   = runtime.GOOS == "linux"
+		isMacOs   = runtime.GOOS == "darwin"
+		isWindows = runtime.GOOS == "windows"
+	)
+
 	for _, v := range s.removedFiles {
 		events = append(events, fsnotify.Event{
 			Name: v,
@@ -713,12 +723,32 @@ func (s *IntegrationTestBuilder) changeEvents() []fsnotify.Event {
 			Name: v,
 			Op:   fsnotify.Write,
 		})
+		if isLinux || isWindows {
+			// Duplicate write events, for some reason.
+			events = append(events, fsnotify.Event{
+				Name: v,
+				Op:   fsnotify.Write,
+			})
+		}
+		if isMacOs {
+			events = append(events, fsnotify.Event{
+				Name: v,
+				Op:   fsnotify.Chmod,
+			})
+		}
 	}
 	for _, v := range s.createdFiles {
 		events = append(events, fsnotify.Event{
 			Name: v,
 			Op:   fsnotify.Create,
 		})
+		if isLinux || isWindows {
+			events = append(events, fsnotify.Event{
+				Name: v,
+				Op:   fsnotify.Write,
+			})
+		}
+
 	}
 
 	// Shuffle events.
diff --git a/hugolib/rebuild_test.go b/hugolib/rebuild_test.go
index 23e3e053208..ea7efcb6fa6 100644
--- a/hugolib/rebuild_test.go
+++ b/hugolib/rebuild_test.go
@@ -1553,3 +1553,27 @@ Single: {{ .Title }}|{{ .Content }}|
 	b.AssertRenderCountPage(1)
 	b.AssertRenderCountContent(1)
 }
+
+func TestRebuildEditSingleListChangeUbuntuIssue12362(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- hugo.toml --
+disableKinds = ['rss','section','sitemap','taxonomy','term']
+disableLiveReload = true
+-- layouts/_default/list.html --
+{{ range .Pages }}{{ .Title }}|{{ end }}
+-- layouts/_default/single.html --
+{{ .Title }}
+-- content/p1.md --
+---
+title: p1
+---
+`
+
+	b := TestRunning(t, files)
+	b.AssertFileContent("public/index.html", "p1|")
+
+	b.AddFiles("content/p2.md", "---\ntitle: p2\n---").Build()
+	b.AssertFileContent("public/index.html", "p1|p2|") // this test passes, which doesn't match reality
+}