From 07b2e535be469f0a95619edb2b0bd5ea569bca3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Fri, 15 Mar 2024 10:57:51 +0100 Subject: [PATCH] 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 --- hugolib/hugo_sites_build.go | 36 ++++---------------- hugolib/pages_capture.go | 2 +- hugolib/site.go | 68 +++++++++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 30 deletions(-) diff --git a/hugolib/hugo_sites_build.go b/hugolib/hugo_sites_build.go index ddc712a2d..7b8a6ef23 100644 --- a/hugolib/hugo_sites_build.go +++ b/hugolib/hugo_sites_build.go @@ -595,8 +595,10 @@ func (h *HugoSites) processPartial(ctx context.Context, l logg.LevelLogger, conf 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.fileEventsTranslate(events) + eventInfos := h.fileEventsApplyInfo(events) logger := h.Log @@ -631,36 +633,12 @@ func (h *HugoSites) processPartial(ctx context.Context, l logg.LevelLogger, conf addedContentPaths []*paths.Path ) - 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() - + for _, ev := range eventInfos { cpss := h.BaseFs.ResolvePaths(ev.Name) pss := make([]*paths.Path, len(cpss)) for i, cps := range cpss { p := cps.Path - if removed && !paths.HasExt(p) { + if ev.removed && !paths.HasExt(p) { // Assume this is a renamed/removed directory. // 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. @@ -671,7 +649,7 @@ func (h *HugoSites) processPartial(ctx context.Context, l logg.LevelLogger, conf } 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]) } @@ -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...) - } else if isChangedDir { + } else if ev.isChangedDir { changedPaths.changedDirs = append(changedPaths.changedDirs, pss...) } else { changedPaths.changedFiles = append(changedPaths.changedFiles, pss...) diff --git a/hugolib/pages_capture.go b/hugolib/pages_capture.go index 4328978db..231c2efad 100644 --- a/hugolib/pages_capture.go +++ b/hugolib/pages_capture.go @@ -161,7 +161,7 @@ func (c *pagesCollector) Collect() (collectErr error) { // We always start from a directory. collectErr = c.collectDir(id.p, id.isDir, func(fim hugofs.FileMetaInfo) bool { 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())) } diff --git a/hugolib/site.go b/hugolib/site.go index 117e10144..e7d170d09 100644 --- a/hugolib/site.go +++ b/hugolib/site.go @@ -19,6 +19,7 @@ import ( "io" "mime" "net/url" + "os" "path/filepath" "runtime" "sort" @@ -426,6 +427,73 @@ func (h *HugoSites) fileEventsFilter(events []fsnotify.Event) []fsnotify.Event { 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 { eventMap := make(map[string][]fsnotify.Event)