diff options
-rw-r--r-- | Gopkg.lock | 6 | ||||
-rw-r--r-- | Gopkg.toml | 2 | ||||
-rw-r--r-- | commands/commandeer.go | 3 | ||||
-rw-r--r-- | commands/hugo.go | 241 | ||||
-rw-r--r-- | commands/server.go | 71 | ||||
-rw-r--r-- | commands/static_syncer.go | 135 | ||||
-rw-r--r-- | helpers/path.go | 2 | ||||
-rw-r--r-- | helpers/path_test.go | 3 | ||||
-rw-r--r-- | helpers/pathspec.go | 46 | ||||
-rw-r--r-- | helpers/pathspec_test.go | 2 | ||||
-rw-r--r-- | hugolib/config.go | 54 | ||||
-rw-r--r-- | hugolib/hugo_sites.go | 33 | ||||
-rw-r--r-- | hugolib/hugo_sites_build_test.go | 2 | ||||
-rw-r--r-- | hugolib/hugo_sites_multihost_test.go | 6 | ||||
-rw-r--r-- | hugolib/page.go | 1 | ||||
-rw-r--r-- | hugolib/page_output.go | 2 | ||||
-rw-r--r-- | hugolib/page_paths.go | 12 | ||||
-rw-r--r-- | hugolib/pagination.go | 14 | ||||
-rw-r--r-- | hugolib/site.go | 17 | ||||
-rw-r--r-- | hugolib/site_render.go | 2 | ||||
-rw-r--r-- | livereload/livereload.go | 57 | ||||
-rw-r--r-- | source/dirs.go | 191 | ||||
-rw-r--r-- | source/dirs_test.go | 177 | ||||
-rw-r--r-- | tpl/urls/init_test.go | 3 | ||||
-rw-r--r-- | tpl/urls/urls.go | 10 |
25 files changed, 822 insertions, 270 deletions
diff --git a/Gopkg.lock b/Gopkg.lock index 82698a6bb..dc63e7bd4 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -193,10 +193,10 @@ revision = "86672fcb3f950f35f2e675df2240550f2a50762f" [[projects]] - branch = "master" name = "github.com/spf13/afero" packages = [".","mem"] - revision = "5660eeed305fe5f69c8fc6cf899132a459a97064" + revision = "8d919cbe7e2627e417f3e45c3c0e489a5b7e2536" + version = "v1.0.0" [[projects]] name = "github.com/spf13/cast" @@ -285,6 +285,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "271e5ca84d4f9c63392ca282b940207c0c96995efb3a0a9fbc43114b0669bfa0" + inputs-digest = "a7cec7b1df49f84fdd4073cc70139d56c62c5fffcc7e3fcea5ca29615d4b9568" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index e51766330..cf12080cc 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -81,8 +81,8 @@ version = "1.5.0" [[constraint]] - branch = "master" name = "github.com/spf13/afero" + version = "1.0.0" [[constraint]] name = "github.com/spf13/cast" diff --git a/commands/commandeer.go b/commands/commandeer.go index 63fc0a663..b08566613 100644 --- a/commands/commandeer.go +++ b/commands/commandeer.go @@ -24,7 +24,8 @@ type commandeer struct { *deps.DepsCfg pathSpec *helpers.PathSpec visitedURLs *types.EvictingStringQueue - configured bool + + configured bool } func (c *commandeer) Set(key string, value interface{}) { diff --git a/commands/hugo.go b/commands/hugo.go index 1714c8035..7b50d0bb3 100644 --- a/commands/hugo.go +++ b/commands/hugo.go @@ -22,7 +22,6 @@ import ( "github.com/gohugoio/hugo/hugofs" "log" - "net/http" "os" "path/filepath" "runtime" @@ -30,6 +29,8 @@ import ( "sync" "time" + src "github.com/gohugoio/hugo/source" + "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/parser" @@ -526,8 +527,7 @@ func (c *commandeer) watchConfig() { func (c *commandeer) build(watches ...bool) error { if err := c.copyStatic(); err != nil { - // TODO(bep) multihost - return fmt.Errorf("Error copying static files to %s: %s", c.PathSpec().AbsPathify(c.Cfg.GetString("publishDir")), err) + return fmt.Errorf("Error copying static files: %s", err) } watch := false if len(watches) > 0 && watches[0] { @@ -538,88 +538,64 @@ func (c *commandeer) build(watches ...bool) error { } if buildWatch { + watchDirs, err := c.getDirList() + if err != nil { + return err + } c.Logger.FEEDBACK.Println("Watching for changes in", c.PathSpec().AbsPathify(c.Cfg.GetString("contentDir"))) c.Logger.FEEDBACK.Println("Press Ctrl+C to stop") - utils.CheckErr(c.Logger, c.newWatcher(0)) + utils.CheckErr(c.Logger, c.newWatcher(false, watchDirs...)) } return nil } -func (c *commandeer) getStaticSourceFs() afero.Fs { - source := c.Fs.Source - themeDir, err := c.PathSpec().GetThemeStaticDirPath() - staticDir := c.PathSpec().GetStaticDirPath() + helpers.FilePathSeparator - useTheme := true - useStatic := true - - if err != nil { - if err != helpers.ErrThemeUndefined { - c.Logger.WARN.Println(err) - } - useTheme = false - } else { - if _, err := source.Stat(themeDir); os.IsNotExist(err) { - c.Logger.WARN.Println("Unable to find Theme Static Directory:", themeDir) - useTheme = false - } - } - - if _, err := source.Stat(staticDir); os.IsNotExist(err) { - c.Logger.WARN.Println("Unable to find Static Directory:", staticDir) - useStatic = false - } - - if !useStatic && !useTheme { - return nil - } - - if !useStatic { - c.Logger.INFO.Println(themeDir, "is the only static directory available to sync from") - return afero.NewReadOnlyFs(afero.NewBasePathFs(source, themeDir)) - } - - if !useTheme { - c.Logger.INFO.Println(staticDir, "is the only static directory available to sync from") - return afero.NewReadOnlyFs(afero.NewBasePathFs(source, staticDir)) - } - - c.Logger.INFO.Println("using a UnionFS for static directory comprised of:") - c.Logger.INFO.Println("Base:", themeDir) - c.Logger.INFO.Println("Overlay:", staticDir) - base := afero.NewReadOnlyFs(afero.NewBasePathFs(source, themeDir)) - overlay := afero.NewReadOnlyFs(afero.NewBasePathFs(source, staticDir)) - return afero.NewCopyOnWriteFs(base, overlay) +func (c *commandeer) copyStatic() error { + return c.doWithPublishDirs(c.copyStaticTo) } -func (c *commandeer) copyStatic() error { +func (c *commandeer) doWithPublishDirs(f func(dirs *src.Dirs, publishDir string) error) error { publishDir := c.PathSpec().AbsPathify(c.Cfg.GetString("publishDir")) + helpers.FilePathSeparator - roots := c.roots() + // If root, remove the second '/' + if publishDir == "//" { + publishDir = helpers.FilePathSeparator + } - if len(roots) == 0 { - return c.copyStaticTo(publishDir) + languages := c.languages() + + if !languages.IsMultihost() { + dirs, err := src.NewDirs(c.Fs, c.Cfg, c.DepsCfg.Logger) + if err != nil { + return err + } + return f(dirs, publishDir) } - for _, root := range roots { - dir := filepath.Join(publishDir, root) - if err := c.copyStaticTo(dir); err != nil { + for _, l := range languages { + dir := filepath.Join(publishDir, l.Lang) + dirs, err := src.NewDirs(c.Fs, l, c.DepsCfg.Logger) + if err != nil { + return err + } + if err := f(dirs, dir); err != nil { return err } } return nil - } -func (c *commandeer) copyStaticTo(publishDir string) error { +func (c *commandeer) copyStaticTo(dirs *src.Dirs, publishDir string) error { // If root, remove the second '/' if publishDir == "//" { publishDir = helpers.FilePathSeparator } - // Includes both theme/static & /static - staticSourceFs := c.getStaticSourceFs() + staticSourceFs, err := dirs.CreateStaticFs() + if err != nil { + return err + } if staticSourceFs == nil { c.Logger.WARN.Println("No static directories found to sync") @@ -650,12 +626,17 @@ func (c *commandeer) copyStaticTo(publishDir string) error { } // getDirList provides NewWatcher() with a list of directories to watch for changes. -func (c *commandeer) getDirList() []string { +func (c *commandeer) getDirList() ([]string, error) { var a []string dataDir := c.PathSpec().AbsPathify(c.Cfg.GetString("dataDir")) i18nDir := c.PathSpec().AbsPathify(c.Cfg.GetString("i18nDir")) + staticSyncer, err := newStaticSyncer(c) + if err != nil { + return nil, err + } + layoutDir := c.PathSpec().GetLayoutDirPath() - staticDir := c.PathSpec().GetStaticDirPath() + staticDirs := staticSyncer.d.AbsStaticDirs walker := func(path string, fi os.FileInfo, err error) error { if err != nil { @@ -674,12 +655,12 @@ func (c *commandeer) getDirList() []string { return nil } - if path == staticDir && os.IsNotExist(err) { - c.Logger.WARN.Println("Skip staticDir:", err) - return nil - } - if os.IsNotExist(err) { + for _, staticDir := range staticDirs { + if path == staticDir && os.IsNotExist(err) { + c.Logger.WARN.Println("Skip staticDir:", err) + } + } // Ignore. return nil } @@ -726,17 +707,18 @@ func (c *commandeer) getDirList() []string { _ = helpers.SymbolicWalk(c.Fs.Source, c.PathSpec().AbsPathify(c.Cfg.GetString("contentDir")), walker) _ = helpers.SymbolicWalk(c.Fs.Source, i18nDir, walker) _ = helpers.SymbolicWalk(c.Fs.Source, layoutDir, walker) - _ = helpers.SymbolicWalk(c.Fs.Source, staticDir, walker) + for _, staticDir := range staticDirs { + _ = helpers.SymbolicWalk(c.Fs.Source, staticDir, walker) + } if c.PathSpec().ThemeSet() { themesDir := c.PathSpec().GetThemeDir() _ = helpers.SymbolicWalk(c.Fs.Source, filepath.Join(themesDir, "layouts"), walker) - _ = helpers.SymbolicWalk(c.Fs.Source, filepath.Join(themesDir, "static"), walker) _ = helpers.SymbolicWalk(c.Fs.Source, filepath.Join(themesDir, "i18n"), walker) _ = helpers.SymbolicWalk(c.Fs.Source, filepath.Join(themesDir, "data"), walker) } - return a + return a, nil } func (c *commandeer) recreateAndBuildSites(watching bool) (err error) { @@ -798,11 +780,18 @@ func (c *commandeer) rebuildSites(events []fsnotify.Event) error { } // newWatcher creates a new watcher to watch filesystem events. -func (c *commandeer) newWatcher(port int) error { +// if serve is set it will also start one or more HTTP servers to serve those +// files. +func (c *commandeer) newWatcher(serve bool, dirList ...string) error { if runtime.GOOS == "darwin" { tweakLimit() } + staticSyncer, err := newStaticSyncer(c) + if err != nil { + return err + } + watcher, err := watcher.New(1 * time.Second) var wg sync.WaitGroup @@ -814,7 +803,7 @@ func (c *commandeer) newWatcher(port int) error { wg.Add(1) - for _, d := range c.getDirList() { + for _, d := range dirList { if d != "" { _ = watcher.Add(d) } @@ -874,7 +863,7 @@ func (c *commandeer) newWatcher(port int) error { if err := watcher.Add(path); err != nil { return err } - } else if !c.isStatic(path) { + } else if !staticSyncer.isStatic(path) { // Hugo's rebuilding logic is entirely file based. When you drop a new folder into // /content on OSX, the above logic will handle future watching of those files, // but the initial CREATE is lost. @@ -891,7 +880,7 @@ func (c *commandeer) newWatcher(port int) error { } } - if c.isStatic(ev.Name) { + if staticSyncer.isStatic(ev.Name) { staticEvents = append(staticEvents, ev) } else { dynamicEvents = append(dynamicEvents, ev) @@ -899,100 +888,20 @@ func (c *commandeer) newWatcher(port int) error { } if len(staticEvents) > 0 { - publishDir := c.PathSpec().AbsPathify(c.Cfg.GetString("publishDir")) + helpers.FilePathSeparator - - // If root, remove the second '/' - if publishDir == "//" { - publishDir = helpers.FilePathSeparator - } - c.Logger.FEEDBACK.Println("\nStatic file changes detected") const layout = "2006-01-02 15:04:05.000 -0700" c.Logger.FEEDBACK.Println(time.Now().Format(layout)) if c.Cfg.GetBool("forceSyncStatic") { c.Logger.FEEDBACK.Printf("Syncing all static files\n") - // TODO(bep) multihost err := c.copyStatic() if err != nil { - utils.StopOnErr(c.Logger, err, fmt.Sprintf("Error copying static files to %s", publishDir)) + utils.StopOnErr(c.Logger, err, "Error copying static files to publish dir") } } else { - staticSourceFs := c.getStaticSourceFs() - - if staticSourceFs == nil { - c.Logger.WARN.Println("No static directories found to sync") - return - } - - syncer := fsync.NewSyncer() - syncer.NoTimes = c.Cfg.GetBool("noTimes") - syncer.NoChmod = c.Cfg.GetBool("noChmod") - syncer.SrcFs = staticSourceFs - syncer.DestFs = c.Fs.Destination - - // prevent spamming the log on changes - logger := helpers.NewDistinctFeedbackLogger() - - for _, ev := range staticEvents { - // Due to our approach of layering both directories and the content's rendered output - // into one we can't accurately remove a file not in one of the source directories. - // If a file is in the local static dir and also in the theme static dir and we remove - // it from one of those locations we expect it to still exist in the destination - // - // If Hugo generates a file (from the content dir) over a static file - // the content generated file should take precedence. - // - // Because we are now watching and handling individual events it is possible that a static - // event that occupies the same path as a content generated file will take precedence - // until a regeneration of the content takes places. - // - // Hugo assumes that these cases are very rare and will permit this bad behavior - // The alternative is to track every single file and which pipeline rendered it - // and then to handle conflict resolution on every event. - - fromPath := ev.Name - - // If we are here we already know the event took place in a static dir - relPath, err := c.PathSpec().MakeStaticPathRelative(fromPath) - if err != nil { - c.Logger.ERROR.Println(err) - continue - } - - // Remove || rename is harder and will require an assumption. - // Hugo takes the following approach: - // If the static file exists in any of the static source directories after this event - // Hugo will re-sync it. - // If it does not exist in all of the static directories Hugo will remove it. - // - // This assumes that Hugo has not generated content on top of a static file and then removed - // the source of that static file. In this case Hugo will incorrectly remove that file - // from the published directory. - if ev.Op&fsnotify.Rename == fsnotify.Rename || ev.Op&fsnotify.Remove == fsnotify.Remove { - if _, err := staticSourceFs.Stat(relPath); os.IsNotExist(err) { - // If file doesn't exist in any static dir, remove it - toRemove := filepath.Join(publishDir, relPath) - logger.Println("File no longer exists in static dir, removing", toRemove) - _ = c.Fs.Destination.RemoveAll(toRemove) - } else if err == nil { - // If file still exists, sync it - logger.Println("Syncing", relPath, "to", publishDir) - if err := syncer.Sync(filepath.Join(publishDir, relPath), relPath); err != nil { - c.Logger.ERROR.Println(err) - } - } else { - c.Logger.ERROR.Println(err) - } - - continue - } - - // For all other event operations Hugo will sync static. - logger.Println("Syncing", relPath, "to", publishDir) - if err := syncer.Sync(filepath.Join(publishDir, relPath), relPath); err != nil { - c.Logger.ERROR.Println(err) - } + if err := staticSyncer.syncsStaticEvents(staticEvents); err != nil { + c.Logger.ERROR.Println(err) + continue } } @@ -1002,7 +911,7 @@ func (c *commandeer) newWatcher(port int) error { // force refresh when more than one file if len(staticEvents) > 0 { for _, ev := range staticEvents { - path, _ := c.PathSpec().MakeStaticPathRelative(ev.Name) + path := staticSyncer.d.MakeStaticPathRelative(ev.Name) livereload.RefreshPath(path) } @@ -1044,7 +953,7 @@ func (c *commandeer) newWatcher(port int) error { } if p != nil { - livereload.NavigateToPath(p.RelPermalink()) + livereload.NavigateToPathForPort(p.RelPermalink(), p.Site.ServerPort()) } else { livereload.ForceRefresh() } @@ -1058,14 +967,8 @@ func (c *commandeer) newWatcher(port int) error { } }() - if port > 0 { - if !c.Cfg.GetBool("disableLiveReload") { - livereload.Initialize() - http.HandleFunc("/livereload.js", livereload.ServeJS) - http.HandleFunc("/livereload", livereload.Handler) - } - - go c.serve(port) + if serve { + go c.serve() } wg.Wait() @@ -1084,10 +987,6 @@ func pickOneWriteOrCreatePath(events []fsnotify.Event) string { return name } -func (c *commandeer) isStatic(path string) bool { - return strings.HasPrefix(path, c.PathSpec().GetStaticDirPath()) || (len(c.PathSpec().GetThemesDirPath()) > 0 && strings.HasPrefix(path, c.PathSpec().GetThemesDirPath())) -} - // isThemeVsHugoVersionMismatch returns whether the current Hugo version is // less than the theme's min_version. func (c *commandeer) isThemeVsHugoVersionMismatch() (mismatch bool, requiredMinVersion string) { diff --git a/commands/server.go b/commands/server.go index bd45e7054..666f255e3 100644 --- a/commands/server.go +++ b/commands/server.go @@ -25,6 +25,8 @@ import ( "strings" "time" + "github.com/gohugoio/hugo/livereload" + "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/helpers" @@ -189,7 +191,7 @@ func server(cmd *cobra.Command, args []string) error { if err != nil { return err } - c.Cfg.Set("baseURL", baseURL) + c.Set("baseURL", baseURL) } if err := memStats(); err != nil { @@ -218,16 +220,22 @@ func server(cmd *cobra.Command, args []string) error { // Watch runs its own server as part of the routine if serverWatch { - watchDirs := c.getDirList() + + watchDirs, err := c.getDirList() + if err != nil { + return err + } + baseWatchDir := c.Cfg.GetString("workingDir") + relWatchDirs := make([]string, len(watchDirs)) for i, dir := range watchDirs { - watchDirs[i], _ = helpers.GetRelativePath(dir, baseWatchDir) + relWatchDirs[i], _ = helpers.GetRelativePath(dir, baseWatchDir) } - rootWatchDirs := strings.Join(helpers.UniqueStrings(helpers.ExtractRootPaths(watchDirs)), ",") + rootWatchDirs := strings.Join(helpers.UniqueStrings(helpers.ExtractRootPaths(relWatchDirs)), ",") jww.FEEDBACK.Printf("Watching for changes in %s%s{%s}\n", baseWatchDir, helpers.FilePathSeparator, rootWatchDirs) - err := c.newWatcher(serverPort) + err = c.newWatcher(true, watchDirs...) if err != nil { return err @@ -238,7 +246,7 @@ func server(cmd *cobra.Command, args []string) error { } type fileServer struct { - basePort int + ports []int baseURLs []string roots []string c *commandeer @@ -247,7 +255,7 @@ type fileServer struct { func (f *fileServer) createEndpoint(i int) (*http.ServeMux, string, error) { baseURL := f.baseURLs[i] root := f.roots[i] - port := f.basePort + i + port := f.ports[i] publishDir := f.c.Cfg.GetString("publishDir") @@ -257,11 +265,12 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, string, error) { absPublishDir := f.c.PathSpec().AbsPathify(publishDir) - // TODO(bep) multihost unify feedback - if renderToDisk { - jww.FEEDBACK.Println("Serving pages from " + absPublishDir) - } else { - jww.FEEDBACK.Println("Serving pages from memory") + if i == 0 { + if renderToDisk { + jww.FEEDBACK.Println("Serving pages from " + absPublishDir) + } else { + jww.FEEDBACK.Println("Serving pages from memory") + } } httpFs := afero.NewHttpFs(f.c.Fs.Destination) @@ -270,7 +279,7 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, string, error) { doLiveReload := !buildWatch && !f.c.Cfg.GetBool("disableLiveReload") fastRenderMode := doLiveReload && !f.c.Cfg.GetBool("disableFastRender") - if fastRenderMode { + if i == 0 && fastRenderMode { jww.FEEDBACK.Println("Running in Fast Render Mode. For full rebuilds on change: hugo server --disableFastRender") } @@ -311,49 +320,50 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, string, error) { return mu, endpoint, nil } -func (c *commandeer) roots() []string { - var roots []string - languages := c.languages() - isMultiHost := languages.IsMultihost() - if !isMultiHost { - return roots - } - - for _, l := range languages { - roots = append(roots, l.Lang) - } - return roots -} +func (c *commandeer) serve() { -func (c *commandeer) serve(port int) { - // TODO(bep) multihost isMultiHost := Hugo.IsMultihost() var ( baseURLs []string roots []string + ports []int ) if isMultiHost { for _, s := range Hugo.Sites { baseURLs = append(baseURLs, s.BaseURL.String()) roots = append(roots, s.Language.Lang) + ports = append(ports, s.Info.ServerPort()) } } else { - baseURLs = []string{Hugo.Sites[0].BaseURL.String()} + s := Hugo.Sites[0] + baseURLs = []string{s.BaseURL.String()} roots = []string{""} + ports = append(ports, s.Info.ServerPort()) } srv := &fileServer{ - basePort: port, + ports: ports, baseURLs: baseURLs, roots: roots, c: c, } + doLiveReload := !c.Cfg.GetBool("disableLiveReload") + + if doLiveReload { + livereload.Initialize() + } + for i, _ := range baseURLs { mu, endpoint, err := srv.createEndpoint(i) + if doLiveReload { + mu.HandleFunc("/livereload.js", livereload.ServeJS) + mu.HandleFunc("/livereload", livereload.Handler) + } + jww.FEEDBACK.Printf("Web Server is available at %s (bind address %s)\n", endpoint, serverInterface) go func() { err = http.ListenAndServe(endpoint, mu) if err != nil { @@ -363,7 +373,6 @@ func (c *commandeer) serve(port int) { }() } - // TODO(bep) multihost jww.FEEDBACK.Printf("Web Server is available at %s (bind address %s)\n", u.String(), serverInterface) jww.FEEDBACK.Println("Press Ctrl+C to stop") } diff --git a/commands/static_syncer.go b/commands/static_syncer.go new file mode 100644 index 000000000..98b745e4c --- /dev/null +++ b/commands/static_syncer.go @@ -0,0 +1,135 @@ +// Copyright 2017 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package commands + +import ( + "os" + "path/filepath" + + "github.com/fsnotify/fsnotify" + "github.com/gohugoio/hugo/helpers" + src "github.com/gohugoio/hugo/source" + "github.com/spf13/fsync" +) + +type staticSyncer struct { + c *commandeer + d *src.Dirs +} + +func newStaticSyncer(c *commandeer) (*staticSyncer, error) { + dirs, err := src.NewDirs(c.Fs, c.Cfg, c.DepsCfg.Logger) + if err != nil { + return nil, err + } + + return &staticSyncer{c: c, d: dirs}, nil +} + +func (s *staticSyncer) isStatic(path string) bool { + return s.d.IsStatic(path) +} + +func (s *staticSyncer) syncsStaticEvents(staticEvents []fsnotify.Event) error { + c := s.c + + syncFn := func(dirs *src.Dirs, publishDir string) error { + staticSourceFs, err := dirs.CreateStaticFs() + if err != nil { + return err + } + + if staticSourceFs == nil { + c.Logger.WARN.Println("No static directories found to sync") + return nil + } + + syncer := fsync.NewSyncer() + syncer.NoTimes = c.Cfg.GetBool("noTimes") + syncer.NoChmod = c.Cfg.GetBool("noChmod") + syncer.SrcFs = staticSourceFs + syncer.DestFs = c.Fs.Destination + + // prevent spamming the log on changes + logger := helpers.NewDistinctFeedbackLogger() + + for _, ev := range staticEvents { + // Due to our approach of layering both directories and the content's rendered output + // into one we can't accurately remove a file not in one of the source directories. + // If a file is in the local static dir and also in the theme static dir and we remove + // it from one of those locations we expect it to still exist in the destination + // + // If Hugo generates a file (from the content dir) over a static file + // the content generated file should take precedence. + // + // Because we are now watching and handling individual events it is possible that a static + // event that occupies the same path as a content generated file will take precedence + // until a regeneration of the content takes places. + // + // Hugo assumes that these cases are very rare and will permit this bad behavior + // The alternative is to track every single file and which pipeline rendered it + // and then to handle conflict resolution on every event. + + fromPath := ev.Name + + // If we are here we already know the event took place in a static dir + relPath := dirs.MakeStaticPathRelative(fromPath) + if relPath == "" { + // Not member of this virtual host. + continue + } + + // Remove || rename is harder and will require an assumption. + // Hugo takes the following approach: + // If the static file exists in any of the static source directories after this event + // Hugo will re-sync it. + // If it does not exist in all of the static directories Hugo will remove it. + // + // This assumes that Hugo has not generated content on top of a static file and then removed + // the source of that static file. In this case Hugo will incorrectly remove that file + // from the published directory. + if ev.Op&fsnotify.Rename == fsnotify.Rename || ev.Op&fsnotify.Remove == fsnotify.Remove { + if _, err := staticSourceFs.Stat(relPath); os.IsNotExist(err) { + // If file doesn't exist in any static dir, re |