summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--Gopkg.lock6
-rw-r--r--Gopkg.toml2
-rw-r--r--commands/commandeer.go3
-rw-r--r--commands/hugo.go241
-rw-r--r--commands/server.go71
-rw-r--r--commands/static_syncer.go135
-rw-r--r--helpers/path.go2
-rw-r--r--helpers/path_test.go3
-rw-r--r--helpers/pathspec.go46
-rw-r--r--helpers/pathspec_test.go2
-rw-r--r--hugolib/config.go54
-rw-r--r--hugolib/hugo_sites.go33
-rw-r--r--hugolib/hugo_sites_build_test.go2
-rw-r--r--hugolib/hugo_sites_multihost_test.go6
-rw-r--r--hugolib/page.go1
-rw-r--r--hugolib/page_output.go2
-rw-r--r--hugolib/page_paths.go12
-rw-r--r--hugolib/pagination.go14
-rw-r--r--hugolib/site.go17
-rw-r--r--hugolib/site_render.go2
-rw-r--r--livereload/livereload.go57
-rw-r--r--source/dirs.go191
-rw-r--r--source/dirs_test.go177
-rw-r--r--tpl/urls/init_test.go3
-rw-r--r--tpl/urls/urls.go10
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