Fix server rebuilds when adding sub sections especially on Windows

This commit also optimizes for the case where change events for both file (e.g. `_index.md`) and the container directory comes in the same event batch.

While testing this on Windows 11 (ARM64), I notice that Windows behaves a little oddly when dumping a folder of files into the content tree; it works (at least after this commit), but it seems like the event batching behaves differently compared to other OSes (even older Win versions).

A related tip would be to try starting the server with polling, to see if that improves the situation, e.g.:

```
hugo server --poll 700ms
```

Fixes #12230
This commit is contained in:
Bjørn Erik Pedersen 2024-03-15 10:57:51 +01:00
parent f038a51b3e
commit 07b2e535be
3 changed files with 76 additions and 30 deletions

View file

@ -595,8 +595,10 @@ func (h *HugoSites) processPartial(ctx context.Context, l logg.LevelLogger, conf
return sb.String() return sb.String()
})) }))
// 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.fileEventsFilter(events)
events = h.fileEventsTranslate(events) events = h.fileEventsTranslate(events)
eventInfos := h.fileEventsApplyInfo(events)
logger := h.Log logger := h.Log
@ -631,36 +633,12 @@ func (h *HugoSites) processPartial(ctx context.Context, l logg.LevelLogger, conf
addedContentPaths []*paths.Path addedContentPaths []*paths.Path
) )
for _, ev := range events { for _, ev := range eventInfos {
removed := false
added := false
if ev.Op&fsnotify.Remove == fsnotify.Remove {
removed = true
}
fi, statErr := h.Fs.Source.Stat(ev.Name)
// Some editors (Vim) sometimes issue only a Rename operation when writing an existing file
// Sometimes a rename operation means that file has been renamed other times it means
// it's been updated.
if ev.Op.Has(fsnotify.Rename) {
// If the file is still on disk, it's only been updated, if it's not, it's been moved
if statErr != nil {
removed = true
}
}
if ev.Op.Has(fsnotify.Create) {
added = true
}
isChangedDir := statErr == nil && fi.IsDir()
cpss := h.BaseFs.ResolvePaths(ev.Name) cpss := h.BaseFs.ResolvePaths(ev.Name)
pss := make([]*paths.Path, len(cpss)) pss := make([]*paths.Path, len(cpss))
for i, cps := range cpss { for i, cps := range cpss {
p := cps.Path p := cps.Path
if removed && !paths.HasExt(p) { if ev.removed && !paths.HasExt(p) {
// Assume this is a renamed/removed directory. // Assume this is a renamed/removed directory.
// For deletes, we walk up the tree to find the container (e.g. branch bundle), // For deletes, we walk up the tree to find the container (e.g. branch bundle),
// so we will catch this even if it is a file without extension. // so we will catch this even if it is a file without extension.
@ -671,7 +649,7 @@ func (h *HugoSites) processPartial(ctx context.Context, l logg.LevelLogger, conf
} }
pss[i] = h.Configs.ContentPathParser.Parse(cps.Component, p) pss[i] = h.Configs.ContentPathParser.Parse(cps.Component, p)
if added && !isChangedDir && cps.Component == files.ComponentFolderContent { if ev.added && !ev.isChangedDir && cps.Component == files.ComponentFolderContent {
addedContentPaths = append(addedContentPaths, pss[i]) addedContentPaths = append(addedContentPaths, pss[i])
} }
@ -683,9 +661,9 @@ func (h *HugoSites) processPartial(ctx context.Context, l logg.LevelLogger, conf
} }
} }
if removed { if ev.removed {
changedPaths.deleted = append(changedPaths.deleted, pss...) changedPaths.deleted = append(changedPaths.deleted, pss...)
} else if isChangedDir { } else if ev.isChangedDir {
changedPaths.changedDirs = append(changedPaths.changedDirs, pss...) changedPaths.changedDirs = append(changedPaths.changedDirs, pss...)
} else { } else {
changedPaths.changedFiles = append(changedPaths.changedFiles, pss...) changedPaths.changedFiles = append(changedPaths.changedFiles, pss...)

View file

@ -161,7 +161,7 @@ func (c *pagesCollector) Collect() (collectErr error) {
// We always start from a directory. // We always start from a directory.
collectErr = c.collectDir(id.p, id.isDir, func(fim hugofs.FileMetaInfo) bool { collectErr = c.collectDir(id.p, id.isDir, func(fim hugofs.FileMetaInfo) bool {
if id.delete || id.isDir { if id.delete || id.isDir {
if id.isDir { if id.isDir && fim.Meta().PathInfo.IsLeafBundle() {
return strings.HasPrefix(fim.Meta().PathInfo.Path(), paths.AddTrailingSlash(id.p.Path())) return strings.HasPrefix(fim.Meta().PathInfo.Path(), paths.AddTrailingSlash(id.p.Path()))
} }

View file

@ -19,6 +19,7 @@ import (
"io" "io"
"mime" "mime"
"net/url" "net/url"
"os"
"path/filepath" "path/filepath"
"runtime" "runtime"
"sort" "sort"
@ -426,6 +427,73 @@ func (h *HugoSites) fileEventsFilter(events []fsnotify.Event) []fsnotify.Event {
return events[:n] return events[:n]
} }
type fileEventInfo struct {
fsnotify.Event
fi os.FileInfo
added bool
removed bool
isChangedDir bool
}
func (h *HugoSites) fileEventsApplyInfo(events []fsnotify.Event) []fileEventInfo {
var infos []fileEventInfo
for _, ev := range events {
removed := false
added := false
if ev.Op&fsnotify.Remove == fsnotify.Remove {
removed = true
}
fi, statErr := h.Fs.Source.Stat(ev.Name)
// Some editors (Vim) sometimes issue only a Rename operation when writing an existing file
// Sometimes a rename operation means that file has been renamed other times it means
// it's been updated.
if ev.Op.Has(fsnotify.Rename) {
// If the file is still on disk, it's only been updated, if it's not, it's been moved
if statErr != nil {
removed = true
}
}
if ev.Op.Has(fsnotify.Create) {
added = true
}
isChangedDir := statErr == nil && fi.IsDir()
infos = append(infos, fileEventInfo{
Event: ev,
fi: fi,
added: added,
removed: removed,
isChangedDir: isChangedDir,
})
}
n := 0
for _, ev := range infos {
// Remove any directories that's also represented by a file.
keep := true
if ev.isChangedDir {
for _, ev2 := range infos {
if ev2.fi != nil && !ev2.fi.IsDir() && filepath.Dir(ev2.Name) == ev.Name {
keep = false
break
}
}
}
if keep {
infos[n] = ev
n++
}
}
infos = infos[:n]
return infos
}
func (h *HugoSites) fileEventsTranslate(events []fsnotify.Event) []fsnotify.Event { func (h *HugoSites) fileEventsTranslate(events []fsnotify.Event) []fsnotify.Event {
eventMap := make(map[string][]fsnotify.Event) eventMap := make(map[string][]fsnotify.Event)