summaryrefslogtreecommitdiffstats
path: root/commands/hugo.go
diff options
context:
space:
mode:
Diffstat (limited to 'commands/hugo.go')
-rw-r--r--commands/hugo.go1231
1 files changed, 1231 insertions, 0 deletions
diff --git a/commands/hugo.go b/commands/hugo.go
new file mode 100644
index 000000000..5442c32d7
--- /dev/null
+++ b/commands/hugo.go
@@ -0,0 +1,1231 @@
+// Copyright 2019 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 defines and implements command-line commands and flags
+// used by Hugo. Commands and flags are implemented using Cobra.
+package commands
+
+import (
+ "context"
+ "fmt"
+ "io/ioutil"
+ "os/signal"
+ "runtime/pprof"
+ "runtime/trace"
+ "sync/atomic"
+
+ "github.com/gohugoio/hugo/hugofs"
+
+ "github.com/gohugoio/hugo/resources/page"
+
+ "github.com/pkg/errors"
+
+ "github.com/gohugoio/hugo/common/herrors"
+ "github.com/gohugoio/hugo/common/loggers"
+ "github.com/gohugoio/hugo/common/terminal"
+
+ "syscall"
+
+ "github.com/gohugoio/hugo/hugolib/filesystems"
+
+ "golang.org/x/sync/errgroup"
+
+ "os"
+ "path/filepath"
+ "runtime"
+ "strings"
+ "time"
+
+ "github.com/gohugoio/hugo/config"
+
+ flag "github.com/spf13/pflag"
+
+ "github.com/fsnotify/fsnotify"
+ "github.com/gohugoio/hugo/helpers"
+ "github.com/gohugoio/hugo/hugolib"
+ "github.com/gohugoio/hugo/livereload"
+ "github.com/gohugoio/hugo/watcher"
+ "github.com/spf13/afero"
+ "github.com/spf13/cobra"
+ "github.com/spf13/fsync"
+ jww "github.com/spf13/jwalterweatherman"
+)
+
+// The Response value from Execute.
+type Response struct {
+ // The build Result will only be set in the hugo build command.
+ Result *hugolib.HugoSites
+
+ // Err is set when the command failed to execute.
+ Err error
+
+ // The command that was executed.
+ Cmd *cobra.Command
+}
+
+// IsUserError returns true is the Response error is a user error rather than a
+// system error.
+func (r Response) IsUserError() bool {
+ return r.Err != nil && isUserError(r.Err)
+}
+
+// Execute adds all child commands to the root command HugoCmd and sets flags appropriately.
+// The args are usually filled with os.Args[1:].
+func Execute(args []string) Response {
+
+ hugoCmd := newCommandsBuilder().addAll().build()
+ cmd := hugoCmd.getCommand()
+ cmd.SetArgs(args)
+
+ c, err := cmd.ExecuteC()
+
+ var resp Response
+
+ if c == cmd && hugoCmd.c != nil {
+ // Root command executed
+ resp.Result = hugoCmd.c.hugo()
+ }
+
+ if err == nil {
+ errCount := int(loggers.GlobalErrorCounter.Count())
+ if errCount > 0 {
+ err = fmt.Errorf("logged %d errors", errCount)
+ } else if resp.Result != nil {
+ errCount = resp.Result.NumLogErrors()
+ if errCount > 0 {
+ err = fmt.Errorf("logged %d errors", errCount)
+ }
+ }
+
+ }
+
+ resp.Err = err
+ resp.Cmd = c
+
+ return resp
+}
+
+// InitializeConfig initializes a config file with sensible default configuration flags.
+func initializeConfig(mustHaveConfigFile, running bool,
+ h *hugoBuilderCommon,
+ f flagsToConfigHandler,
+ cfgInit func(c *commandeer) error) (*commandeer, error) {
+
+ c, err := newCommandeer(mustHaveConfigFile, running, h, f, cfgInit)
+ if err != nil {
+ return nil, err
+ }
+
+ return c, nil
+
+}
+
+func (c *commandeer) createLogger(cfg config.Provider, running bool) (*loggers.Logger, error) {
+ var (
+ logHandle = ioutil.Discard
+ logThreshold = jww.LevelWarn
+ logFile = cfg.GetString("logFile")
+ outHandle = ioutil.Discard
+ stdoutThreshold = jww.LevelWarn
+ )
+
+ if !c.h.quiet {
+ outHandle = os.Stdout
+ }
+
+ if c.h.verboseLog || c.h.logging || (c.h.logFile != "") {
+ var err error
+ if logFile != "" {
+ logHandle, err = os.OpenFile(logFile, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0666)
+ if err != nil {
+ return nil, newSystemError("Failed to open log file:", logFile, err)
+ }
+ } else {
+ logHandle, err = ioutil.TempFile("", "hugo")
+ if err != nil {
+ return nil, newSystemError(err)
+ }
+ }
+ } else if !c.h.quiet && cfg.GetBool("verbose") {
+ stdoutThreshold = jww.LevelInfo
+ }
+
+ if cfg.GetBool("debug") {
+ stdoutThreshold = jww.LevelDebug
+ }
+
+ if c.h.verboseLog {
+ logThreshold = jww.LevelInfo
+ if cfg.GetBool("debug") {
+ logThreshold = jww.LevelDebug
+ }
+ }
+
+ loggers.InitGlobalLogger(stdoutThreshold, logThreshold, outHandle, logHandle)
+ helpers.InitLoggers()
+
+ return loggers.NewLogger(stdoutThreshold, logThreshold, outHandle, logHandle, running), nil
+}
+
+func initializeFlags(cmd *cobra.Command, cfg config.Provider) {
+ persFlagKeys := []string{
+ "debug",
+ "verbose",
+ "logFile",
+ // Moved from vars
+ }
+ flagKeys := []string{
+ "cleanDestinationDir",
+ "buildDrafts",
+ "buildFuture",
+ "buildExpired",
+ "uglyURLs",
+ "canonifyURLs",
+ "enableRobotsTXT",
+ "enableGitInfo",
+ "pluralizeListTitles",
+ "preserveTaxonomyNames",
+ "ignoreCache",
+ "forceSyncStatic",
+ "noTimes",
+ "noChmod",
+ "ignoreVendor",
+ "templateMetrics",
+ "templateMetricsHints",
+
+ // Moved from vars.
+ "baseURL",
+ "buildWatch",
+ "cacheDir",
+ "cfgFile",
+ "confirm",
+ "contentDir",
+ "debug",
+ "destination",
+ "disableKinds",
+ "dryRun",
+ "force",
+ "gc",
+ "i18n-warnings",
+ "invalidateCDN",
+ "layoutDir",
+ "logFile",
+ "maxDeletes",
+ "quiet",
+ "renderToMemory",
+ "source",
+ "target",
+ "theme",
+ "themesDir",
+ "verbose",
+ "verboseLog",
+ "duplicateTargetPaths",
+ }
+
+ for _, key := range persFlagKeys {
+ setValueFromFlag(cmd.PersistentFlags(), key, cfg, "", false)
+ }
+ for _, key := range flagKeys {
+ setValueFromFlag(cmd.Flags(), key, cfg, "", false)
+ }
+
+ setValueFromFlag(cmd.Flags(), "minify", cfg, "minifyOutput", true)
+
+ // Set some "config aliases"
+ setValueFromFlag(cmd.Flags(), "destination", cfg, "publishDir", false)
+ setValueFromFlag(cmd.Flags(), "i18n-warnings", cfg, "logI18nWarnings", false)
+ setValueFromFlag(cmd.Flags(), "path-warnings", cfg, "logPathWarnings", false)
+
+}
+
+func setValueFromFlag(flags *flag.FlagSet, key string, cfg config.Provider, targetKey string, force bool) {
+ key = strings.TrimSpace(key)
+ if (force && flags.Lookup(key) != nil) || flags.Changed(key) {
+ f := flags.Lookup(key)
+ configKey := key
+ if targetKey != "" {
+ configKey = targetKey
+ }
+ // Gotta love this API.
+ switch f.Value.Type() {
+ case "bool":
+ bv, _ := flags.GetBool(key)
+ cfg.Set(configKey, bv)
+ case "string":
+ cfg.Set(configKey, f.Value.String())
+ case "stringSlice":
+ bv, _ := flags.GetStringSlice(key)
+ cfg.Set(configKey, bv)
+ case "int":
+ iv, _ := flags.GetInt(key)
+ cfg.Set(configKey, iv)
+ default:
+ panic(fmt.Sprintf("update switch with %s", f.Value.Type()))
+ }
+
+ }
+}
+
+func isTerminal() bool {
+ return terminal.IsTerminal(os.Stdout)
+
+}
+func ifTerminal(s string) string {
+ if !isTerminal() {
+ return ""
+ }
+ return s
+}
+
+func (c *commandeer) fullBuild() error {
+
+ var (
+ g errgroup.Group
+ langCount map[string]uint64
+ )
+
+ if !c.h.quiet {
+ fmt.Print(ifTerminal(hideCursor) + "Building sites … ")
+ if isTerminal() {
+ defer func() {
+ fmt.Print(showCursor + clearLine)
+ }()
+ }
+ }
+
+ copyStaticFunc := func() error {
+
+ cnt, err := c.copyStatic()
+ if err != nil {
+ return errors.Wrap(err, "Error copying static files")
+ }
+ langCount = cnt
+ return nil
+ }
+ buildSitesFunc := func() error {
+ if err := c.buildSites(); err != nil {
+ return errors.Wrap(err, "Error building site")
+ }
+ return nil
+ }
+ // Do not copy static files and build sites in parallel if cleanDestinationDir is enabled.
+ // This flag deletes all static resources in /public folder that are missing in /static,
+ // and it does so at the end of copyStatic() call.
+ if c.Cfg.GetBool("cleanDestinationDir") {
+ if err := copyStaticFunc(); err != nil {
+ return err
+ }
+ if err := buildSitesFunc(); err != nil {
+ return err
+ }
+ } else {
+ g.Go(copyStaticFunc)
+ g.Go(buildSitesFunc)
+ if err := g.Wait(); err != nil {
+ return err
+ }
+ }
+
+ for _, s := range c.hugo().Sites {
+ s.ProcessingStats.Static = langCount[s.Language().Lang]
+ }
+
+ if c.h.gc {
+ count, err := c.hugo().GC()
+ if err != nil {
+ return err
+ }
+ for _, s := range c.hugo().Sites {
+ // We have no way of knowing what site the garbage belonged to.
+ s.ProcessingStats.Cleaned = uint64(count)
+ }
+ }
+
+ return nil
+
+}
+
+func (c *commandeer) initCPUProfile() (func(), error) {
+ if c.h.cpuprofile == "" {
+ return nil, nil
+ }
+
+ f, err := os.Create(c.h.cpuprofile)
+ if err != nil {
+ return nil, errors.Wrap(err, "failed to create CPU profile")
+ }
+ if err := pprof.StartCPUProfile(f); err != nil {
+ return nil, errors.Wrap(err, "failed to start CPU profile")
+ }
+ return func() {
+ pprof.StopCPUProfile()
+ f.Close()
+ }, nil
+}
+
+func (c *commandeer) initMemProfile() {
+ if c.h.memprofile == "" {
+ return
+ }
+
+ f, err := os.Create(c.h.memprofile)
+ if err != nil {
+ c.logger.ERROR.Println("could not create memory profile: ", err)
+ }
+ defer f.Close()
+ runtime.GC() // get up-to-date statistics
+ if err := pprof.WriteHeapProfile(f); err != nil {
+ c.logger.ERROR.Println("could not write memory profile: ", err)
+ }
+}
+
+func (c *commandeer) initTraceProfile() (func(), error) {
+ if c.h.traceprofile == "" {
+ return nil, nil
+ }
+
+ f, err := os.Create(c.h.traceprofile)
+ if err != nil {
+ return nil, errors.Wrap(err, "failed to create trace file")
+ }
+
+ if err := trace.Start(f); err != nil {
+ return nil, errors.Wrap(err, "failed to start trace")
+ }
+
+ return func() {
+ trace.Stop()
+ f.Close()
+ }, nil
+}
+
+func (c *commandeer) initMutexProfile() (func(), error) {
+ if c.h.mutexprofile == "" {
+ return nil, nil
+ }
+
+ f, err := os.Create(c.h.mutexprofile)
+ if err != nil {
+ return nil, err
+ }
+
+ runtime.SetMutexProfileFraction(1)
+
+ return func() {
+ pprof.Lookup("mutex").WriteTo(f, 0)
+ f.Close()
+ }, nil
+
+}
+
+func (c *commandeer) initMemTicker() func() {
+ memticker := time.NewTicker(5 * time.Second)
+ quit := make(chan struct{})
+ printMem := func() {
+ var m runtime.MemStats
+ runtime.ReadMemStats(&m)
+ fmt.Printf("\n\nAlloc = %v\nTotalAlloc = %v\nSys = %v\nNumGC = %v\n\n", formatByteCount(m.Alloc), formatByteCount(m.TotalAlloc), formatByteCount(m.Sys), m.NumGC)
+
+ }
+
+ go func() {
+ for {
+ select {
+ case <-memticker.C:
+ printMem()
+ case <-quit:
+ memticker.Stop()
+ printMem()
+ return
+ }
+
+ }
+ }()
+
+ return func() {
+ close(quit)
+ }
+}
+
+func (c *commandeer) initProfiling() (func(), error) {
+
+ stopCPUProf, err := c.initCPUProfile()
+ if err != nil {
+ return nil, err
+ }
+
+ stopMutexProf, err := c.initMutexProfile()
+ if err != nil {
+ return nil, err
+ }
+
+ stopTraceProf, err := c.initTraceProfile()
+ if err != nil {
+ return nil, err
+ }
+
+ var stopMemTicker func()
+ if c.h.printm {
+ stopMemTicker = c.initMemTicker()
+ }
+
+ return func() {
+ c.initMemProfile()
+
+ if stopCPUProf != nil {
+ stopCPUProf()
+ }
+ if stopMutexProf != nil {
+ stopMutexProf()
+ }
+
+ if stopTraceProf != nil {
+ stopTraceProf()
+ }
+
+ if stopMemTicker != nil {
+ stopMemTicker()
+ }
+ }, nil
+}
+
+func (c *commandeer) build() error {
+ stopProfiling, err := c.initProfiling()
+ if err != nil {
+ return err
+ }
+
+ defer func() {
+ if stopProfiling != nil {
+ stopProfiling()
+ }
+ }()
+
+ if err := c.fullBuild(); err != nil {
+ return err
+ }
+
+ // TODO(bep) Feedback?
+ if !c.h.quiet {
+ fmt.Println()
+ c.hugo().PrintProcessingStats(os.Stdout)
+ fmt.Println()
+
+ if createCounter, ok := c.destinationFs.(hugofs.DuplicatesReporter); ok {
+ dupes := createCounter.ReportDuplicates()
+ if dupes != "" {
+ c.logger.WARN.Println("Duplicate target paths:", dupes)
+ }
+ }
+ }
+
+ if c.h.buildWatch {
+ watchDirs, err := c.getDirList()
+ if err != nil {
+ return err
+ }
+
+ baseWatchDir := c.Cfg.GetString("workingDir")
+ rootWatchDirs := getRootWatchDirsStr(baseWatchDir, watchDirs)
+
+ c.logger.FEEDBACK.Printf("Watching for changes in %s%s{%s}\n", baseWatchDir, helpers.FilePathSeparator, rootWatchDirs)
+ c.logger.FEEDBACK.Println("Press Ctrl+C to stop")
+ watcher, err := c.newWatcher(watchDirs...)
+ checkErr(c.Logger, err)
+ defer watcher.Close()
+
+ var sigs = make(chan os.Signal, 1)
+ signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
+
+ <-sigs
+ }
+
+ return nil
+}
+
+func (c *commandeer) serverBuild() error {
+ defer c.timeTrack(time.Now(), "Built")
+
+ stopProfiling, err := c.initProfiling()
+ if err != nil {
+ return err
+ }
+
+ defer func() {
+ if stopProfiling != nil {
+ stopProfiling()
+ }
+ }()
+
+ if err := c.fullBuild(); err != nil {
+ return err
+ }
+
+ // TODO(bep) Feedback?
+ if !c.h.quiet {
+ fmt.Println()
+ c.hugo().PrintProcessingStats(os.Stdout)
+ fmt.Println()
+ }
+
+ return nil
+}
+
+func (c *commandeer) copyStatic() (map[string]uint64, error) {
+ m, err := c.doWithPublishDirs(c.copyStaticTo)
+ if err == nil || os.IsNotExist(err) {
+ return m, nil
+ }
+ return m, err
+}
+
+func (c *commandeer) doWithPublishDirs(f func(sourceFs *filesystems.SourceFilesystem) (uint64, error)) (map[string]uint64, error) {
+
+ langCount := make(map[string]uint64)
+
+ staticFilesystems := c.hugo().BaseFs.SourceFilesystems.Static
+
+ if len(staticFilesystems) == 0 {
+ c.logger.INFO.Println("No static directories found to sync")
+ return langCount, nil
+ }
+
+ for lang, fs := range staticFilesystems {
+ cnt, err := f(fs)
+ if err != nil {
+ return langCount, err
+ }
+
+ if lang == "" {
+ // Not multihost
+ for _, l := range c.languages {
+ langCount[l.Lang] = cnt
+ }
+ } else {
+ langCount[lang] = cnt
+ }
+ }
+
+ return langCount, nil
+}
+
+type countingStatFs struct {
+ afero.Fs
+ statCounter uint64
+}
+
+func (fs *countingStatFs) Stat(name string) (os.FileInfo, error) {
+ f, err := fs.Fs.Stat(name)
+ if err == nil {
+ if !f.IsDir() {
+ atomic.AddUint64(&fs.statCounter, 1)
+ }
+ }
+ return f, err
+}
+
+func chmodFilter(dst, src os.FileInfo) bool {
+ // Hugo publishes data from multiple sources, potentially
+ // with overlapping directory structures. We cannot sync permissions
+ // for directories as that would mean that we might end up with write-protected
+ // directories inside /public.
+ // One example of this would be syncing from the Go Module cache,
+ // which have 0555 directories.
+ return src.IsDir()
+}
+
+func (c *commandeer) copyStaticTo(sourceFs *filesystems.SourceFilesystem) (uint64, error) {
+ publishDir := c.hugo().PathSpec.PublishDir
+ // If root, remove the second '/'
+ if publishDir == "//" {
+ publishDir = helpers.FilePathSeparator
+ }
+
+ if sourceFs.PublishFolder != "" {
+ publishDir = filepath.Join(publishDir, sourceFs.PublishFolder)
+ }
+
+ fs := &countingStatFs{Fs: sourceFs.Fs}
+
+ syncer := fsync.NewSyncer()
+ syncer.NoTimes = c.Cfg.GetBool("noTimes")
+ syncer.NoChmod = c.Cfg.GetBool("noChmod")
+ syncer.ChmodFilter = chmodFilter
+ syncer.SrcFs = fs
+ syncer.DestFs = c.Fs.Destination
+ // Now that we are using a unionFs for the static directories
+ // We can effectively clean the publishDir on initial sync
+ syncer.Delete = c.Cfg.GetBool("cleanDestinationDir")
+
+ if syncer.Delete {
+ c.logger.INFO.Println("removing all files from destination that don't exist in static dirs")
+
+ syncer.DeleteFilter = func(f os.FileInfo) bool {
+ return f.IsDir() && strings.HasPrefix(f.Name(), ".")
+ }
+ }
+ c.logger.INFO.Println("syncing static files to", publishDir)
+
+ // because we are using a baseFs (to get the union right).
+ // set sync src to root
+ err := syncer.Sync(publishDir, helpers.FilePathSeparator)
+ if err != nil {
+ return 0, err
+ }
+
+ // Sync runs Stat 3 times for every source file (which sounds much)
+ numFiles := fs.statCounter / 3
+
+ return numFiles, err
+}
+
+func (c *commandeer) firstPathSpec() *helpers.PathSpec {
+ return c.hugo().Sites[0].PathSpec
+}
+
+func (c *commandeer) timeTrack(start time.Time, name string) {
+ elapsed := time.Since(start)
+ c.logger.FEEDBACK.Printf("%s in %v ms", name, int(1000*elapsed.Seconds()))
+}
+
+// getDirList provides NewWatcher() with a list of directories to watch for changes.
+func (c *commandeer) getDirList() ([]string, error) {
+ var filenames []string
+
+ walkFn := func(path string, fi hugofs.FileMetaInfo, err error) error {
+ if err != nil {
+ c.logger.ERROR.Println("walker: ", err)
+ return nil
+ }
+
+ if fi.IsDir() {
+ if fi.Name() == ".git" ||
+ fi.Name() == "node_modules" || fi.Name() == "bower_components" {
+ return filepath.SkipDir
+ }
+
+ filenames = append(filenames, fi.Meta().Filename())
+ }
+
+ return nil
+
+ }
+
+ watchFiles := c.hugo().PathSpec.BaseFs.WatchDirs()
+ for _, fi := range watchFiles {
+ if !fi.IsDir() {
+ filenames = append(filenames, fi.Meta().Filename())
+ continue
+ }
+
+ w := hugofs.NewWalkway(hugofs.WalkwayConfig{Logger: c.logger, Info: fi, WalkFn: walkFn})
+ if err := w.Walk(); err != nil {
+ c.logger.ERROR.Println("walker: ", err)
+ }
+ }
+
+ filenames = helpers.UniqueStringsSorted(filenames)
+
+ return filenames, nil
+}
+
+func (c *commandeer) buildSites() (err error) {
+ return c.hugo().Build(hugolib.BuildCfg{})
+}
+
+func (c *commandeer) handleBuildErr(err error, msg string) {
+ c.buildErr = err
+
+ c.logger.ERROR.Print(msg + ":\n\n")
+ c.logger.ERROR.Println(helpers.FirstUpper(err.Error()))
+ if !c.h.quiet && c.h.verbose {
+ herrors.PrintStackTraceFromErr(err)
+ }
+}
+
+func (c *commandeer) rebuildSites(events []fsnotify.Event) error {
+ defer c.timeTrack(time.Now(), "Total")
+
+ c.buildErr = nil
+ visited := c.visitedURLs.PeekAllSet()
+ if c.fastRenderMode {
+
+ // Make sure we always render the home pages
+ for _, l := range c.languages {
+ langPath := c.hugo().PathSpec.GetLangSubDir(l.Lang)
+ if langPath != "" {
+ langPath = langPath + "/"
+ }
+ home := c.hugo().PathSpec.PrependBasePath("/"+langPath, false)
+ visited[home] = true
+ }
+
+ }
+ return c.hugo().Build(hugolib.BuildCfg{RecentlyVisited: visited, ErrRecovery: c.wasError}, events...)
+}
+
+func (c *commandeer) partialReRender(urls ...string) error {
+ defer func() {
+ c.wasError = false
+ }()
+ c.buildErr = nil
+ visited := make(map[string]bool)
+ for _, url := range urls {
+ visited[url] = true
+ }
+ return c.hugo().Build(hugolib.BuildCfg{RecentlyVisited: visited, PartialReRender: true, ErrRecovery: c.wasError})
+}
+
+func (c *commandeer) fullRebuild(changeType string) {
+ if changeType == configChangeGoMod {
+ // go.mod may be changed during the build itself, and
+ // we really want to prevent superfluous builds.
+ if !c.fullRebuildSem.TryAcquire(1) {
+ return
+ }
+ c.fullRebuildSem.Release(1)
+ }
+
+ c.fullRebuildSem.Acquire(context.Background(), 1)
+
+ go func() {
+
+ defer c.fullRebuildSem.Release(1)
+
+ c.printChangeDetected(changeType)
+
+ defer func() {
+
+ // Allow any file system events to arrive back.
+ // This will block any rebuild on config changes for the
+ // duration of the sleep.
+ time.Sleep(2 * time.Second)
+ }()
+
+ defer c.timeTrack(time.Now(), "Rebuilt")
+
+ c.commandeerHugoState = newCommandeerHugoState()
+ err := c.loadConfig(true, true)
+ if err != nil {
+ // Set the processing on pause until the state is recovered.
+ c.paused = true
+ c.handleBuildErr(err, "Failed to reload config")
+
+ } else {
+ c.paused = false
+ }
+
+ if !c.paused {
+ _, err := c.copyStatic()
+ if err != nil {
+ c.logger.ERROR.Println(err)
+ return
+ }
+
+ err = c.buildSites()
+ if err != nil {
+ c.logger.ERROR.Println(err)
+ } else if !c.h.buildWatch && !c.Cfg.GetBool("disableLiveReload") {
+ livereload.ForceRefresh()
+ }
+ }
+ }()
+}
+
+// newWatcher creates a new watcher to watch filesystem events.
+func (c *commandeer) newWatcher(dirList ...string) (*watcher.Batcher, error) {
+ if runtime.GOOS == "darwin" {
+ tweakLimit()
+ }
+
+ staticSyncer, err := newStaticSyncer(c)
+ if err != nil {
+ return nil, err
+ }
+
+ watcher, err := watcher.New(1 * time.Second)
+
+ if err != nil {
+ return nil, err
+ }
+
+ for _, d := range dirList {
+ if d != "" {
+ _ = watcher.Add(d)
+ }
+ }
+
+ // Identifies changes to config (config.toml) files.
+ configSet := make(map[string]bool)
+
+ c.logger.FEEDBACK.Println("Watching for config changes in", strings.Join(c.configFiles, ", "))
+ for _, configFile := range c.configFiles {
+ watcher.Add(configFile)
+ configSet[configFile] = true
+ }
+
+ go func() {
+ for {
+ select {
+ case evs := <-watcher.Events:
+ c.handleEvents(watcher, staticSyncer, evs, configSet)
+ if c.showErrorInBrowser && c.errCount() > 0 {
+ // Need to reload browser to show the error
+ livereload.ForceRefresh()
+ }
+ case err := <-watcher.Errors:
+ if err != nil {
+ c.logger.ERROR.Println("Error while watching:", err)
+ }
+ }
+ }
+ }()
+
+ return watcher, nil
+}
+
+func (c *commandeer) printChangeDetected(typ string) {
+ msg := "\nChange"
+ if typ != "" {
+ msg += " of " + typ
+ }
+ msg += " detected, rebuilding site."
+
+ c.logger.FEEDBACK.Println(msg)
+ const layout = "2006-01-02 15:04:05.000 -0700"
+ c.logger.FEEDBACK.Println(time.Now().Format(layout))
+}
+
+const (
+ configChangeConfig = "config file"
+ configChangeGoMod = "go.mod file"
+)
+
+func (c *commandeer) handleEvents(watcher *watcher.Batcher,
+ staticSyncer *staticSyncer,
+ evs []fsnotify.Event,
+ configSet map[string]bool) {
+
+ defer func() {
+ c.wasError = false
+ }()
+
+ var isHandled bool
+
+ for _, ev := range evs {
+ isConfig := configSet[ev.Name]
+ configChangeType := configChangeConfig
+ if isConfig {
+ if strings.Contains(ev.Name, "go.mod") {
+ configChangeType = configChangeGoMod
+ }
+ }
+ if !isConfig {
+ // It may be one of the /config folders
+ dirname := filepath.Dir(ev.Name)
+ if dirname != "." && configSet[dirname] {
+ isConfig = true
+ }
+ }
+
+ if isConfig {
+ isHandled = true
+
+ if ev.Op&fsnotify.Chmod == fsnotify.Chmod {
+ continue
+ }
+
+ if ev.Op&fsnotify.Remove == fsnotify.Remove || ev.Op&fsnotify.Rename == fsnotify.Rename {
+ for _, configFile := range c.configFiles {
+ counter := 0
+ for watcher.Add(configFile) != nil {
+ counter++
+ if counter >= 100 {
+ break
+ }
+ time.Sleep(100 * time.Millisecond)
+ }
+ }
+
+ }
+
+ // Config file(s) changed. Need full rebuild.
+ c.fullRebuild(configChangeType)
+
+ return
+ }
+ }
+
+ if isHandled {
+ return
+ }
+
+ if c.paused {
+ // Wait for the server to get into a consistent state before
+ // we continue with processing.
+ return
+ }
+
+ if len(evs) > 50 {
+ // This is probably a mass edit of the content dir.
+ // Schedule a full rebuild for when it slows down.
+ c.debounce(func() {
+ c.fullRebuild("")
+ })
+ return
+ }
+
+ c.logger.INFO.Println("Received System Events:", evs)
+
+ staticEvents := []fsnotify.Event{}
+ dynamicEvents := []fsnotify.Event{}
+
+ // Special handling for symbolic links inside /content.
+ filtered := []fsnotify.Event{}
+ for _, ev := range evs {
+ // Check the most specific first, i.e. files.
+ contentMapped := c.hugo().ContentChanges.GetSymbolicLinkMappings(ev.Name)
+ if len(contentMapped) > 0 {
+ for _, mapped := range contentMapped {
+ filtered = append(filtered, fsnotify.Event{Name: mapped, Op: ev.Op})
+ }
+ continue
+ }
+
+ // Check for any symbolic directory mapping.
+
+ dir, name := filepath.Split(ev.Name)
+
+ contentMapped = c.hugo().ContentChanges.GetSymbolicLinkMappings(dir)
+
+ if len(contentMapped) == 0 {
+ filtered = append(filtered, ev)
+ continue
+ }
+
+ for _, mapped := range contentMapped {
+ mappedFilename := filepath.Join(mapped, name)
+ filtered = append(filtered, fsnotify.Event{Name: mappedFilename, Op: ev.Op})
+ }
+ }
+
+ evs = filtered
+
+ for _, ev := range evs {
+ ext := filepath.Ext(ev.Name)
+ baseName := filepath.Base(ev.Name)
+ istemp := strings.HasSuffix(ext, "~") ||
+ (ext == ".swp") || // vim
+ (ext == ".swx") || // vim
+ (ext == ".tmp") || // generic temp file
+ (ext == ".DS_Store") || // OSX Thumbnail
+ baseName == "4913" || // vim
+ strings.HasPrefix(ext, ".goutputstream") || // gnome
+ strings.HasSuffix(ext, "jb_old___") || // intelliJ
+ strings.HasSuffix(ext, "jb_tmp___") || // intelliJ
+ strings.HasSuffix(ext, "jb_bak___") || // intelliJ
+ strings.HasPrefix(ext, ".sb-") || // byword
+ strings.HasPrefix(baseName, ".#") || // emacs
+ strings.HasPrefix(baseName, "#") // emacs
+ if istemp {
+ continue
+ }
+ if c.hugo().Deps.SourceSpec.IgnoreFile(ev.Name) {
+ continue
+ }
+ // Sometimes during rm -rf operations a '"": REMOVE' is triggered. Just ignore these
+ if ev.Name == "" {
+ continue
+ }
+
+ // Write and rename operations are often followed by CHMOD.
+ // There may be valid use cases for rebuilding the site on CHMOD,
+ // but that will require more complex logic than this simple conditional.
+ // On OS X this seems to be related to Spotlight, see:
+ // https://github.com/go-fsnotify/fsnotify/issues/15
+ // A workaround is to put your site(s) on the Spotlight exception list,
+ // but that may be a little mysterious for most end users.
+ // So, for now, we skip reload on CHMOD.
+ // We do have to check for WRITE though. On slower laptops a Chmod
+ // could be aggregated with other important events, and we still want
+ // to rebuild on those
+ if ev.Op&(fsnotify.Chmod|fsnotify.Write|fsnotify.Create) == fsnotify.Chmod {
+ continue
+ }
+
+ walkAdder := func(path string, f hugofs.FileMetaInfo, err error) error {
+ if f.IsDir() {
+ c.logger.FEEDBACK.Println("adding created directory to watchlist", path)
+ if err := watcher.Add(path); err != nil {
+ return err
+ }
+ } 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.