summaryrefslogtreecommitdiffstats
path: root/commands
diff options
context:
space:
mode:
Diffstat (limited to 'commands')
-rw-r--r--commands/commandeer.go84
-rw-r--r--commands/hugo.go92
2 files changed, 153 insertions, 23 deletions
diff --git a/commands/commandeer.go b/commands/commandeer.go
index 4ca0c4be9..051787f6e 100644
--- a/commands/commandeer.go
+++ b/commands/commandeer.go
@@ -16,6 +16,7 @@ package commands
import (
"os"
"path/filepath"
+ "regexp"
"sync"
"time"
@@ -46,6 +47,10 @@ type commandeerHugoState struct {
type commandeer struct {
*commandeerHugoState
+ // Currently only set when in "fast render mode". But it seems to
+ // be fast enough that we could maybe just add it for all server modes.
+ changeDetector *fileChangeDetector
+
// We need to reuse this on server rebuilds.
destinationFs afero.Fs
@@ -105,6 +110,68 @@ func newCommandeer(mustHaveConfigFile, running bool, h *hugoBuilderCommon, f fla
return c, c.loadConfig(mustHaveConfigFile, running)
}
+type fileChangeDetector struct {
+ sync.Mutex
+ current map[string]string
+ prev map[string]string
+
+ irrelevantRe *regexp.Regexp
+}
+
+func (f *fileChangeDetector) OnFileClose(name, md5sum string) {
+ f.Lock()
+ defer f.Unlock()
+ f.current[name] = md5sum
+}
+
+func (f *fileChangeDetector) changed() []string {
+ if f == nil {
+ return nil
+ }
+ f.Lock()
+ defer f.Unlock()
+ var c []string
+ for k, v := range f.current {
+ vv, found := f.prev[k]
+ if !found || v != vv {
+ c = append(c, k)
+ }
+ }
+
+ return f.filterIrrelevant(c)
+}
+
+func (f *fileChangeDetector) filterIrrelevant(in []string) []string {
+ var filtered []string
+ for _, v := range in {
+ if !f.irrelevantRe.MatchString(v) {
+ filtered = append(filtered, v)
+ }
+ }
+ return filtered
+}
+
+func (f *fileChangeDetector) PrepareNew() {
+ if f == nil {
+ return
+ }
+
+ f.Lock()
+ defer f.Unlock()
+
+ if f.current == nil {
+ f.current = make(map[string]string)
+ f.prev = make(map[string]string)
+ return
+ }
+
+ f.prev = make(map[string]string)
+ for k, v := range f.current {
+ f.prev[k] = v
+ }
+ f.current = make(map[string]string)
+}
+
func (c *commandeer) loadConfig(mustHaveConfigFile, running bool) error {
if c.DepsCfg == nil {
@@ -202,6 +269,23 @@ func (c *commandeer) loadConfig(mustHaveConfigFile, running bool) error {
fs.Destination = new(afero.MemMapFs)
}
+ doLiveReload := !c.h.buildWatch && !config.GetBool("disableLiveReload")
+ fastRenderMode := doLiveReload && !config.GetBool("disableFastRender")
+
+ if fastRenderMode {
+ // For now, fast render mode only. It should, however, be fast enough
+ // for the full variant, too.
+ changeDetector := &fileChangeDetector{
+ // We use this detector to decide to do a Hot reload of a single path or not.
+ // We need to filter out source maps and possibly some other to be able
+ // to make that decision.
+ irrelevantRe: regexp.MustCompile(`\.map$`),
+ }
+ changeDetector.PrepareNew()
+ fs.Destination = hugofs.NewHashingFs(fs.Destination, changeDetector)
+ c.changeDetector = changeDetector
+ }
+
err = c.initFs(fs)
if err != nil {
return
diff --git a/commands/hugo.go b/commands/hugo.go
index 2b847ec95..980189c47 100644
--- a/commands/hugo.go
+++ b/commands/hugo.go
@@ -474,6 +474,10 @@ func (c *commandeer) copyStaticTo(sourceFs *filesystems.SourceFilesystem) (uint6
return numFiles, err
}
+func (c *commandeer) firstPathSpec() *helpers.PathSpec {
+ return c.hugo.Sites[0].PathSpec
+}
+
func (c *commandeer) timeTrack(start time.Time, name string) {
if c.h.quiet {
return
@@ -552,8 +556,8 @@ func (c *commandeer) getDirList() ([]string, error) {
// SymbolicWalk will log anny ERRORs
// Also note that the Dirnames fetched below will contain any relevant theme
// directories.
- for _, contentDir := range c.hugo.PathSpec.BaseFs.AbsContentDirs {
- _ = helpers.SymbolicWalk(c.Fs.Source, contentDir.Value, symLinkWalker)
+ for _, contentDir := range c.hugo.PathSpec.BaseFs.Content.Dirnames {
+ _ = helpers.SymbolicWalk(c.Fs.Source, contentDir, symLinkWalker)
}
for _, staticDir := range c.hugo.PathSpec.BaseFs.Data.Dirnames {
@@ -574,6 +578,10 @@ func (c *commandeer) getDirList() ([]string, error) {
}
}
+ for _, assetDir := range c.hugo.PathSpec.BaseFs.Assets.Dirnames {
+ _ = helpers.SymbolicWalk(c.Fs.Source, assetDir, regularWalker)
+ }
+
if len(nested) > 0 {
for {
@@ -818,13 +826,11 @@ func (c *commandeer) newWatcher(dirList ...string) (*watcher.Batcher, error) {
// Will block forever trying to write to a channel that nobody is reading if livereload isn't initialized
// force refresh when more than one file
- if len(staticEvents) > 0 {
- for _, ev := range staticEvents {
-
- path := c.hugo.BaseFs.SourceFilesystems.MakeStaticPathRelative(ev.Name)
- livereload.RefreshPath(path)
- }
-
+ if len(staticEvents) == 1 {
+ ev := staticEvents[0]
+ path := c.hugo.BaseFs.SourceFilesystems.MakeStaticPathRelative(ev.Name)
+ path = c.firstPathSpec().RelURL(helpers.ToSlashTrimLeading(path), false)
+ livereload.RefreshPath(path)
} else {
livereload.ForceRefresh()
}
@@ -832,34 +838,54 @@ func (c *commandeer) newWatcher(dirList ...string) (*watcher.Batcher, error) {
}
if len(dynamicEvents) > 0 {
+ partitionedEvents := partitionDynamicEvents(
+ c.firstPathSpec().BaseFs.SourceFilesystems,
+ dynamicEvents)
+
doLiveReload := !c.h.buildWatch && !c.Cfg.GetBool("disableLiveReload")
- onePageName := pickOneWriteOrCreatePath(dynamicEvents)
+ onePageName := pickOneWriteOrCreatePath(partitionedEvents.ContentEvents)
c.Logger.FEEDBACK.Println("\nChange detected, rebuilding site")
const layout = "2006-01-02 15:04:05.000 -0700"
c.Logger.FEEDBACK.Println(time.Now().Format(layout))
+ c.changeDetector.PrepareNew()
if err := c.rebuildSites(dynamicEvents); err != nil {
c.Logger.ERROR.Println("Failed to rebuild site:", err)
}
if doLiveReload {
- navigate := c.Cfg.GetBool("navigateToChanged")
- // We have fetched the same page above, but it may have
- // changed.
- var p *hugolib.Page
-
- if navigate {
- if onePageName != "" {
- p = c.hugo.GetContentPage(onePageName)
+ if len(partitionedEvents.ContentEvents) == 0 && len(partitionedEvents.AssetEvents) > 0 {
+ changed := c.changeDetector.changed()
+ if c.changeDetector != nil && len(changed) == 0 {
+ // Nothing has changed.
+ continue
+ } else if len(changed) == 1 {
+ pathToRefresh := c.firstPathSpec().RelURL(helpers.ToSlashTrimLeading(changed[0]), false)
+ livereload.RefreshPath(pathToRefresh)
+ } else {
+ livereload.ForceRefresh()
}
-
}
- if p != nil {
- livereload.NavigateToPathForPort(p.RelPermalink(), p.Site.ServerPort())
- } else {
- livereload.ForceRefresh()
+ if len(partitionedEvents.ContentEvents) > 0 {
+
+ navigate := c.Cfg.GetBool("navigateToChanged")
+ // We have fetched the same page above, but it may have
+ // changed.
+ var p *hugolib.Page
+
+ if navigate {
+ if onePageName != "" {
+ p = c.hugo.GetContentPage(onePageName)
+ }
+ }
+
+ if p != nil {
+ livereload.NavigateToPathForPort(p.RelPermalink(), p.Site.ServerPort())
+ } else {
+ livereload.ForceRefresh()
+ }
}
}
}
@@ -874,6 +900,26 @@ func (c *commandeer) newWatcher(dirList ...string) (*watcher.Batcher, error) {
return watcher, nil
}
+// dynamicEvents contains events that is considered dynamic, as in "not static".
+// Both of these categories will trigger a new build, but the asset events
+// does not fit into the "navigate to changed" logic.
+type dynamicEvents struct {
+ ContentEvents []fsnotify.Event
+ AssetEvents []fsnotify.Event
+}
+
+func partitionDynamicEvents(sourceFs *filesystems.SourceFilesystems, events []fsnotify.Event) (de dynamicEvents) {
+ for _, e := range events {
+ if sourceFs.IsAsset(e.Name) {
+ de.AssetEvents = append(de.AssetEvents, e)
+ } else {
+ de.ContentEvents = append(de.ContentEvents, e)
+ }
+ }
+ return
+
+}
+
func pickOneWriteOrCreatePath(events []fsnotify.Event) string {
name := ""