github.com/olliephillips/hugo@v0.42.2/commands/hugo.go (about)

     1  // Copyright 2018 The Hugo Authors. All rights reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  // http://www.apache.org/licenses/LICENSE-2.0
     7  //
     8  // Unless required by applicable law or agreed to in writing, software
     9  // distributed under the License is distributed on an "AS IS" BASIS,
    10  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  // Package commands defines and implements command-line commands and flags
    15  // used by Hugo. Commands and flags are implemented using Cobra.
    16  package commands
    17  
    18  import (
    19  	"fmt"
    20  	"io/ioutil"
    21  	"os/signal"
    22  	"sort"
    23  	"sync/atomic"
    24  	"syscall"
    25  
    26  	"github.com/gohugoio/hugo/hugolib/filesystems"
    27  
    28  	"golang.org/x/sync/errgroup"
    29  
    30  	"log"
    31  	"os"
    32  	"path/filepath"
    33  	"runtime"
    34  	"strings"
    35  	"time"
    36  
    37  	"github.com/gohugoio/hugo/config"
    38  
    39  	"github.com/gohugoio/hugo/parser"
    40  	flag "github.com/spf13/pflag"
    41  
    42  	"github.com/fsnotify/fsnotify"
    43  	"github.com/gohugoio/hugo/helpers"
    44  	"github.com/gohugoio/hugo/hugolib"
    45  	"github.com/gohugoio/hugo/livereload"
    46  	"github.com/gohugoio/hugo/utils"
    47  	"github.com/gohugoio/hugo/watcher"
    48  	"github.com/spf13/afero"
    49  	"github.com/spf13/cobra"
    50  	"github.com/spf13/fsync"
    51  	jww "github.com/spf13/jwalterweatherman"
    52  )
    53  
    54  // The Response value from Execute.
    55  type Response struct {
    56  	// The build Result will only be set in the hugo build command.
    57  	Result *hugolib.HugoSites
    58  
    59  	// Err is set when the command failed to execute.
    60  	Err error
    61  
    62  	// The command that was executed.
    63  	Cmd *cobra.Command
    64  }
    65  
    66  func (r Response) IsUserError() bool {
    67  	return r.Err != nil && isUserError(r.Err)
    68  }
    69  
    70  // Execute adds all child commands to the root command HugoCmd and sets flags appropriately.
    71  // The args are usually filled with os.Args[1:].
    72  func Execute(args []string) Response {
    73  	hugoCmd := newCommandsBuilder().addAll().build()
    74  	cmd := hugoCmd.getCommand()
    75  	cmd.SetArgs(args)
    76  
    77  	c, err := cmd.ExecuteC()
    78  
    79  	var resp Response
    80  
    81  	if c == cmd && hugoCmd.c != nil {
    82  		// Root command executed
    83  		resp.Result = hugoCmd.c.hugo
    84  	}
    85  
    86  	if err == nil {
    87  		errCount := int(jww.LogCountForLevelsGreaterThanorEqualTo(jww.LevelError))
    88  		if errCount > 0 {
    89  			err = fmt.Errorf("logged %d errors", errCount)
    90  		} else if resp.Result != nil {
    91  			errCount = resp.Result.NumLogErrors()
    92  			if errCount > 0 {
    93  				err = fmt.Errorf("logged %d errors", errCount)
    94  			}
    95  		}
    96  
    97  	}
    98  
    99  	resp.Err = err
   100  	resp.Cmd = c
   101  
   102  	return resp
   103  }
   104  
   105  // InitializeConfig initializes a config file with sensible default configuration flags.
   106  func initializeConfig(mustHaveConfigFile, running bool,
   107  	h *hugoBuilderCommon,
   108  	f flagsToConfigHandler,
   109  	doWithCommandeer func(c *commandeer) error) (*commandeer, error) {
   110  
   111  	c, err := newCommandeer(mustHaveConfigFile, running, h, f, doWithCommandeer)
   112  	if err != nil {
   113  		return nil, err
   114  	}
   115  
   116  	return c, nil
   117  
   118  }
   119  
   120  func (c *commandeer) createLogger(cfg config.Provider) (*jww.Notepad, error) {
   121  	var (
   122  		logHandle       = ioutil.Discard
   123  		logThreshold    = jww.LevelWarn
   124  		logFile         = cfg.GetString("logFile")
   125  		outHandle       = os.Stdout
   126  		stdoutThreshold = jww.LevelError
   127  	)
   128  
   129  	if c.h.verboseLog || c.h.logging || (c.h.logFile != "") {
   130  		var err error
   131  		if logFile != "" {
   132  			logHandle, err = os.OpenFile(logFile, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0666)
   133  			if err != nil {
   134  				return nil, newSystemError("Failed to open log file:", logFile, err)
   135  			}
   136  		} else {
   137  			logHandle, err = ioutil.TempFile("", "hugo")
   138  			if err != nil {
   139  				return nil, newSystemError(err)
   140  			}
   141  		}
   142  	} else if !c.h.quiet && cfg.GetBool("verbose") {
   143  		stdoutThreshold = jww.LevelInfo
   144  	}
   145  
   146  	if cfg.GetBool("debug") {
   147  		stdoutThreshold = jww.LevelDebug
   148  	}
   149  
   150  	if c.h.verboseLog {
   151  		logThreshold = jww.LevelInfo
   152  		if cfg.GetBool("debug") {
   153  			logThreshold = jww.LevelDebug
   154  		}
   155  	}
   156  
   157  	// The global logger is used in some few cases.
   158  	jww.SetLogOutput(logHandle)
   159  	jww.SetLogThreshold(logThreshold)
   160  	jww.SetStdoutThreshold(stdoutThreshold)
   161  	helpers.InitLoggers()
   162  
   163  	return jww.NewNotepad(stdoutThreshold, logThreshold, outHandle, logHandle, "", log.Ldate|log.Ltime), nil
   164  }
   165  
   166  func initializeFlags(cmd *cobra.Command, cfg config.Provider) {
   167  	persFlagKeys := []string{
   168  		"debug",
   169  		"verbose",
   170  		"logFile",
   171  		// Moved from vars
   172  	}
   173  	flagKeys := []string{
   174  		"cleanDestinationDir",
   175  		"buildDrafts",
   176  		"buildFuture",
   177  		"buildExpired",
   178  		"uglyURLs",
   179  		"canonifyURLs",
   180  		"enableRobotsTXT",
   181  		"enableGitInfo",
   182  		"pluralizeListTitles",
   183  		"preserveTaxonomyNames",
   184  		"ignoreCache",
   185  		"forceSyncStatic",
   186  		"noTimes",
   187  		"noChmod",
   188  		"templateMetrics",
   189  		"templateMetricsHints",
   190  
   191  		// Moved from vars.
   192  		"baseURL",
   193  		"buildWatch",
   194  		"cacheDir",
   195  		"cfgFile",
   196  		"contentDir",
   197  		"debug",
   198  		"destination",
   199  		"disableKinds",
   200  		"gc",
   201  		"layoutDir",
   202  		"logFile",
   203  		"i18n-warnings",
   204  		"quiet",
   205  		"renderToMemory",
   206  		"source",
   207  		"theme",
   208  		"themesDir",
   209  		"verbose",
   210  		"verboseLog",
   211  	}
   212  
   213  	for _, key := range persFlagKeys {
   214  		setValueFromFlag(cmd.PersistentFlags(), key, cfg, "")
   215  	}
   216  	for _, key := range flagKeys {
   217  		setValueFromFlag(cmd.Flags(), key, cfg, "")
   218  	}
   219  
   220  	// Set some "config aliases"
   221  	setValueFromFlag(cmd.Flags(), "destination", cfg, "publishDir")
   222  	setValueFromFlag(cmd.Flags(), "i18n-warnings", cfg, "logI18nWarnings")
   223  
   224  }
   225  
   226  var deprecatedFlags = map[string]bool{
   227  	strings.ToLower("uglyURLs"):              true,
   228  	strings.ToLower("pluralizeListTitles"):   true,
   229  	strings.ToLower("preserveTaxonomyNames"): true,
   230  	strings.ToLower("canonifyURLs"):          true,
   231  }
   232  
   233  func setValueFromFlag(flags *flag.FlagSet, key string, cfg config.Provider, targetKey string) {
   234  	key = strings.TrimSpace(key)
   235  	if flags.Changed(key) {
   236  		if _, deprecated := deprecatedFlags[strings.ToLower(key)]; deprecated {
   237  			msg := fmt.Sprintf(`Set "%s = true" in your config.toml.
   238  If you need to set this configuration value from the command line, set it via an OS environment variable: "HUGO_%s=true hugo"`, key, strings.ToUpper(key))
   239  			// Remove in Hugo 0.38
   240  			helpers.Deprecated("hugo", "--"+key+" flag", msg, true)
   241  		}
   242  		f := flags.Lookup(key)
   243  		configKey := key
   244  		if targetKey != "" {
   245  			configKey = targetKey
   246  		}
   247  		// Gotta love this API.
   248  		switch f.Value.Type() {
   249  		case "bool":
   250  			bv, _ := flags.GetBool(key)
   251  			cfg.Set(configKey, bv)
   252  		case "string":
   253  			cfg.Set(configKey, f.Value.String())
   254  		case "stringSlice":
   255  			bv, _ := flags.GetStringSlice(key)
   256  			cfg.Set(configKey, bv)
   257  		default:
   258  			panic(fmt.Sprintf("update switch with %s", f.Value.Type()))
   259  		}
   260  
   261  	}
   262  }
   263  
   264  func (c *commandeer) fullBuild() error {
   265  	var (
   266  		g         errgroup.Group
   267  		langCount map[string]uint64
   268  	)
   269  
   270  	if !c.h.quiet {
   271  		fmt.Print(hideCursor + "Building sites … ")
   272  		defer func() {
   273  			fmt.Print(showCursor + clearLine)
   274  		}()
   275  	}
   276  
   277  	copyStaticFunc := func() error {
   278  		cnt, err := c.copyStatic()
   279  		if err != nil {
   280  			if !os.IsNotExist(err) {
   281  				return fmt.Errorf("Error copying static files: %s", err)
   282  			}
   283  			c.Logger.WARN.Println("No Static directory found")
   284  		}
   285  		langCount = cnt
   286  		langCount = cnt
   287  		return nil
   288  	}
   289  	buildSitesFunc := func() error {
   290  		if err := c.buildSites(); err != nil {
   291  			return fmt.Errorf("Error building site: %s", err)
   292  		}
   293  		return nil
   294  	}
   295  	// Do not copy static files and build sites in parallel if cleanDestinationDir is enabled.
   296  	// This flag deletes all static resources in /public folder that are missing in /static,
   297  	// and it does so at the end of copyStatic() call.
   298  	if c.Cfg.GetBool("cleanDestinationDir") {
   299  		if err := copyStaticFunc(); err != nil {
   300  			return err
   301  		}
   302  		if err := buildSitesFunc(); err != nil {
   303  			return err
   304  		}
   305  	} else {
   306  		g.Go(copyStaticFunc)
   307  		g.Go(buildSitesFunc)
   308  		if err := g.Wait(); err != nil {
   309  			return err
   310  		}
   311  	}
   312  
   313  	for _, s := range c.hugo.Sites {
   314  		s.ProcessingStats.Static = langCount[s.Language.Lang]
   315  	}
   316  
   317  	if c.h.gc {
   318  		count, err := c.hugo.GC()
   319  		if err != nil {
   320  			return err
   321  		}
   322  		for _, s := range c.hugo.Sites {
   323  			// We have no way of knowing what site the garbage belonged to.
   324  			s.ProcessingStats.Cleaned = uint64(count)
   325  		}
   326  	}
   327  
   328  	return nil
   329  
   330  }
   331  
   332  func (c *commandeer) build() error {
   333  	defer c.timeTrack(time.Now(), "Total")
   334  
   335  	if err := c.fullBuild(); err != nil {
   336  		return err
   337  	}
   338  
   339  	// TODO(bep) Feedback?
   340  	if !c.h.quiet {
   341  		fmt.Println()
   342  		c.hugo.PrintProcessingStats(os.Stdout)
   343  		fmt.Println()
   344  	}
   345  
   346  	if c.h.buildWatch {
   347  		watchDirs, err := c.getDirList()
   348  		if err != nil {
   349  			return err
   350  		}
   351  		c.Logger.FEEDBACK.Println("Watching for changes in", c.hugo.PathSpec.AbsPathify(c.Cfg.GetString("contentDir")))
   352  		c.Logger.FEEDBACK.Println("Press Ctrl+C to stop")
   353  		watcher, err := c.newWatcher(watchDirs...)
   354  		utils.CheckErr(c.Logger, err)
   355  		defer watcher.Close()
   356  
   357  		var sigs = make(chan os.Signal)
   358  		signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
   359  
   360  		<-sigs
   361  	}
   362  
   363  	return nil
   364  }
   365  
   366  func (c *commandeer) serverBuild() error {
   367  	defer c.timeTrack(time.Now(), "Total")
   368  
   369  	if err := c.fullBuild(); err != nil {
   370  		return err
   371  	}
   372  
   373  	// TODO(bep) Feedback?
   374  	if !c.h.quiet {
   375  		fmt.Println()
   376  		c.hugo.PrintProcessingStats(os.Stdout)
   377  		fmt.Println()
   378  	}
   379  
   380  	return nil
   381  }
   382  
   383  func (c *commandeer) copyStatic() (map[string]uint64, error) {
   384  	return c.doWithPublishDirs(c.copyStaticTo)
   385  }
   386  
   387  func (c *commandeer) doWithPublishDirs(f func(sourceFs *filesystems.SourceFilesystem) (uint64, error)) (map[string]uint64, error) {
   388  
   389  	langCount := make(map[string]uint64)
   390  
   391  	staticFilesystems := c.hugo.BaseFs.SourceFilesystems.Static
   392  
   393  	if len(staticFilesystems) == 0 {
   394  		c.Logger.WARN.Println("No static directories found to sync")
   395  		return langCount, nil
   396  	}
   397  
   398  	for lang, fs := range staticFilesystems {
   399  		cnt, err := f(fs)
   400  		if err != nil {
   401  			return langCount, err
   402  		}
   403  		if lang == "" {
   404  			// Not multihost
   405  			for _, l := range c.languages {
   406  				langCount[l.Lang] = cnt
   407  			}
   408  		} else {
   409  			langCount[lang] = cnt
   410  		}
   411  	}
   412  
   413  	return langCount, nil
   414  }
   415  
   416  type countingStatFs struct {
   417  	afero.Fs
   418  	statCounter uint64
   419  }
   420  
   421  func (fs *countingStatFs) Stat(name string) (os.FileInfo, error) {
   422  	f, err := fs.Fs.Stat(name)
   423  	if err == nil {
   424  		if !f.IsDir() {
   425  			atomic.AddUint64(&fs.statCounter, 1)
   426  		}
   427  	}
   428  	return f, err
   429  }
   430  
   431  func (c *commandeer) copyStaticTo(sourceFs *filesystems.SourceFilesystem) (uint64, error) {
   432  	publishDir := c.hugo.PathSpec.PublishDir
   433  	// If root, remove the second '/'
   434  	if publishDir == "//" {
   435  		publishDir = helpers.FilePathSeparator
   436  	}
   437  
   438  	if sourceFs.PublishFolder != "" {
   439  		publishDir = filepath.Join(publishDir, sourceFs.PublishFolder)
   440  	}
   441  
   442  	fs := &countingStatFs{Fs: sourceFs.Fs}
   443  
   444  	syncer := fsync.NewSyncer()
   445  	syncer.NoTimes = c.Cfg.GetBool("noTimes")
   446  	syncer.NoChmod = c.Cfg.GetBool("noChmod")
   447  	syncer.SrcFs = fs
   448  	syncer.DestFs = c.Fs.Destination
   449  	// Now that we are using a unionFs for the static directories
   450  	// We can effectively clean the publishDir on initial sync
   451  	syncer.Delete = c.Cfg.GetBool("cleanDestinationDir")
   452  
   453  	if syncer.Delete {
   454  		c.Logger.INFO.Println("removing all files from destination that don't exist in static dirs")
   455  
   456  		syncer.DeleteFilter = func(f os.FileInfo) bool {
   457  			return f.IsDir() && strings.HasPrefix(f.Name(), ".")
   458  		}
   459  	}
   460  	c.Logger.INFO.Println("syncing static files to", publishDir)
   461  
   462  	var err error
   463  
   464  	// because we are using a baseFs (to get the union right).
   465  	// set sync src to root
   466  	err = syncer.Sync(publishDir, helpers.FilePathSeparator)
   467  	if err != nil {
   468  		return 0, err
   469  	}
   470  
   471  	// Sync runs Stat 3 times for every source file (which sounds much)
   472  	numFiles := fs.statCounter / 3
   473  
   474  	return numFiles, err
   475  }
   476  
   477  func (c *commandeer) timeTrack(start time.Time, name string) {
   478  	if c.h.quiet {
   479  		return
   480  	}
   481  	elapsed := time.Since(start)
   482  	c.Logger.FEEDBACK.Printf("%s in %v ms", name, int(1000*elapsed.Seconds()))
   483  }
   484  
   485  // getDirList provides NewWatcher() with a list of directories to watch for changes.
   486  func (c *commandeer) getDirList() ([]string, error) {
   487  	var a []string
   488  
   489  	// To handle nested symlinked content dirs
   490  	var seen = make(map[string]bool)
   491  	var nested []string
   492  
   493  	newWalker := func(allowSymbolicDirs bool) func(path string, fi os.FileInfo, err error) error {
   494  		return func(path string, fi os.FileInfo, err error) error {
   495  			if err != nil {
   496  				if os.IsNotExist(err) {
   497  					return nil
   498  				}
   499  
   500  				c.Logger.ERROR.Println("Walker: ", err)
   501  				return nil
   502  			}
   503  
   504  			// Skip .git directories.
   505  			// Related to https://github.com/gohugoio/hugo/issues/3468.
   506  			if fi.Name() == ".git" {
   507  				return nil
   508  			}
   509  
   510  			if fi.Mode()&os.ModeSymlink == os.ModeSymlink {
   511  				link, err := filepath.EvalSymlinks(path)
   512  				if err != nil {
   513  					c.Logger.ERROR.Printf("Cannot read symbolic link '%s', error was: %s", path, err)
   514  					return nil
   515  				}
   516  				linkfi, err := helpers.LstatIfPossible(c.Fs.Source, link)
   517  				if err != nil {
   518  					c.Logger.ERROR.Printf("Cannot stat %q: %s", link, err)
   519  					return nil
   520  				}
   521  				if !allowSymbolicDirs && !linkfi.Mode().IsRegular() {
   522  					c.Logger.ERROR.Printf("Symbolic links for directories not supported, skipping %q", path)
   523  					return nil
   524  				}
   525  
   526  				if allowSymbolicDirs && linkfi.IsDir() {
   527  					// afero.Walk will not walk symbolic links, so wee need to do it.
   528  					if !seen[path] {
   529  						seen[path] = true
   530  						nested = append(nested, path)
   531  					}
   532  					return nil
   533  				}
   534  
   535  				fi = linkfi
   536  			}
   537  
   538  			if fi.IsDir() {
   539  				if fi.Name() == ".git" ||
   540  					fi.Name() == "node_modules" || fi.Name() == "bower_components" {
   541  					return filepath.SkipDir
   542  				}
   543  				a = append(a, path)
   544  			}
   545  			return nil
   546  		}
   547  	}
   548  
   549  	symLinkWalker := newWalker(true)
   550  	regularWalker := newWalker(false)
   551  
   552  	// SymbolicWalk will log anny ERRORs
   553  	// Also note that the Dirnames fetched below will contain any relevant theme
   554  	// directories.
   555  	for _, contentDir := range c.hugo.PathSpec.BaseFs.AbsContentDirs {
   556  		_ = helpers.SymbolicWalk(c.Fs.Source, contentDir.Value, symLinkWalker)
   557  	}
   558  
   559  	for _, staticDir := range c.hugo.PathSpec.BaseFs.Data.Dirnames {
   560  		_ = helpers.SymbolicWalk(c.Fs.Source, staticDir, regularWalker)
   561  	}
   562  
   563  	for _, staticDir := range c.hugo.PathSpec.BaseFs.I18n.Dirnames {
   564  		_ = helpers.SymbolicWalk(c.Fs.Source, staticDir, regularWalker)
   565  	}
   566  
   567  	for _, staticDir := range c.hugo.PathSpec.BaseFs.Layouts.Dirnames {
   568  		_ = helpers.SymbolicWalk(c.Fs.Source, staticDir, regularWalker)
   569  	}
   570  
   571  	for _, staticFilesystem := range c.hugo.PathSpec.BaseFs.Static {
   572  		for _, staticDir := range staticFilesystem.Dirnames {
   573  			_ = helpers.SymbolicWalk(c.Fs.Source, staticDir, regularWalker)
   574  		}
   575  	}
   576  
   577  	if len(nested) > 0 {
   578  		for {
   579  
   580  			toWalk := nested
   581  			nested = nested[:0]
   582  
   583  			for _, d := range toWalk {
   584  				_ = helpers.SymbolicWalk(c.Fs.Source, d, symLinkWalker)
   585  			}
   586  
   587  			if len(nested) == 0 {
   588  				break
   589  			}
   590  		}
   591  	}
   592  
   593  	a = helpers.UniqueStrings(a)
   594  	sort.Strings(a)
   595  
   596  	return a, nil
   597  }
   598  
   599  func (c *commandeer) resetAndBuildSites() (err error) {
   600  	if !c.h.quiet {
   601  		c.Logger.FEEDBACK.Println("Started building sites ...")
   602  	}
   603  	return c.hugo.Build(hugolib.BuildCfg{ResetState: true})
   604  }
   605  
   606  func (c *commandeer) buildSites() (err error) {
   607  	return c.hugo.Build(hugolib.BuildCfg{})
   608  }
   609  
   610  func (c *commandeer) rebuildSites(events []fsnotify.Event) error {
   611  	defer c.timeTrack(time.Now(), "Total")
   612  
   613  	visited := c.visitedURLs.PeekAllSet()
   614  	doLiveReload := !c.h.buildWatch && !c.Cfg.GetBool("disableLiveReload")
   615  	if doLiveReload && !c.Cfg.GetBool("disableFastRender") {
   616  
   617  		// Make sure we always render the home pages
   618  		for _, l := range c.languages {
   619  			langPath := c.hugo.PathSpec.GetLangSubDir(l.Lang)
   620  			if langPath != "" {
   621  				langPath = langPath + "/"
   622  			}
   623  			home := c.hugo.PathSpec.PrependBasePath("/" + langPath)
   624  			visited[home] = true
   625  		}
   626  
   627  	}
   628  	return c.hugo.Build(hugolib.BuildCfg{RecentlyVisited: visited}, events...)
   629  }
   630  
   631  func (c *commandeer) fullRebuild() {
   632  	c.commandeerHugoState = &commandeerHugoState{}
   633  	if err := c.loadConfig(true, true); err != nil {
   634  		jww.ERROR.Println("Failed to reload config:", err)
   635  	} else if err := c.buildSites(); err != nil {
   636  		jww.ERROR.Println(err)
   637  	} else if !c.h.buildWatch && !c.Cfg.GetBool("disableLiveReload") {
   638  		livereload.ForceRefresh()
   639  	}
   640  }
   641  
   642  // newWatcher creates a new watcher to watch filesystem events.
   643  func (c *commandeer) newWatcher(dirList ...string) (*watcher.Batcher, error) {
   644  	if runtime.GOOS == "darwin" {
   645  		tweakLimit()
   646  	}
   647  
   648  	staticSyncer, err := newStaticSyncer(c)
   649  	if err != nil {
   650  		return nil, err
   651  	}
   652  
   653  	watcher, err := watcher.New(1 * time.Second)
   654  
   655  	if err != nil {
   656  		return nil, err
   657  	}
   658  
   659  	for _, d := range dirList {
   660  		if d != "" {
   661  			_ = watcher.Add(d)
   662  		}
   663  	}
   664  
   665  	// Identifies changes to config (config.toml) files.
   666  	configSet := make(map[string]bool)
   667  
   668  	for _, configFile := range c.configFiles {
   669  		c.Logger.FEEDBACK.Println("Watching for config changes in", configFile)
   670  		watcher.Add(configFile)
   671  		configSet[configFile] = true
   672  	}
   673  
   674  	go func() {
   675  		for {
   676  			select {
   677  			case evs := <-watcher.Events:
   678  				if len(evs) > 50 {
   679  					// This is probably a mass edit of the content dir.
   680  					// Schedule a full rebuild for when it slows down.
   681  					c.debounce(c.fullRebuild)
   682  					continue
   683  				}
   684  
   685  				c.Logger.INFO.Println("Received System Events:", evs)
   686  
   687  				staticEvents := []fsnotify.Event{}
   688  				dynamicEvents := []fsnotify.Event{}
   689  
   690  				// Special handling for symbolic links inside /content.
   691  				filtered := []fsnotify.Event{}
   692  				for _, ev := range evs {
   693  					if configSet[ev.Name] {
   694  						if ev.Op&fsnotify.Chmod == fsnotify.Chmod {
   695  							continue
   696  						}
   697  						// Config file changed. Need full rebuild.
   698  						c.fullRebuild()
   699  						break
   700  					}
   701  
   702  					// Check the most specific first, i.e. files.
   703  					contentMapped := c.hugo.ContentChanges.GetSymbolicLinkMappings(ev.Name)
   704  					if len(contentMapped) > 0 {
   705  						for _, mapped := range contentMapped {
   706  							filtered = append(filtered, fsnotify.Event{Name: mapped, Op: ev.Op})
   707  						}
   708  						continue
   709  					}
   710  
   711  					// Check for any symbolic directory mapping.
   712  
   713  					dir, name := filepath.Split(ev.Name)
   714  
   715  					contentMapped = c.hugo.ContentChanges.GetSymbolicLinkMappings(dir)
   716  
   717  					if len(contentMapped) == 0 {
   718  						filtered = append(filtered, ev)
   719  						continue
   720  					}
   721  
   722  					for _, mapped := range contentMapped {
   723  						mappedFilename := filepath.Join(mapped, name)
   724  						filtered = append(filtered, fsnotify.Event{Name: mappedFilename, Op: ev.Op})
   725  					}
   726  				}
   727  
   728  				evs = filtered
   729  
   730  				for _, ev := range evs {
   731  					ext := filepath.Ext(ev.Name)
   732  					baseName := filepath.Base(ev.Name)
   733  					istemp := strings.HasSuffix(ext, "~") ||
   734  						(ext == ".swp") || // vim
   735  						(ext == ".swx") || // vim
   736  						(ext == ".tmp") || // generic temp file
   737  						(ext == ".DS_Store") || // OSX Thumbnail
   738  						baseName == "4913" || // vim
   739  						strings.HasPrefix(ext, ".goutputstream") || // gnome
   740  						strings.HasSuffix(ext, "jb_old___") || // intelliJ
   741  						strings.HasSuffix(ext, "jb_tmp___") || // intelliJ
   742  						strings.HasSuffix(ext, "jb_bak___") || // intelliJ
   743  						strings.HasPrefix(ext, ".sb-") || // byword
   744  						strings.HasPrefix(baseName, ".#") || // emacs
   745  						strings.HasPrefix(baseName, "#") // emacs
   746  					if istemp {
   747  						continue
   748  					}
   749  					// Sometimes during rm -rf operations a '"": REMOVE' is triggered. Just ignore these
   750  					if ev.Name == "" {
   751  						continue
   752  					}
   753  
   754  					// Write and rename operations are often followed by CHMOD.
   755  					// There may be valid use cases for rebuilding the site on CHMOD,
   756  					// but that will require more complex logic than this simple conditional.
   757  					// On OS X this seems to be related to Spotlight, see:
   758  					// https://github.com/go-fsnotify/fsnotify/issues/15
   759  					// A workaround is to put your site(s) on the Spotlight exception list,
   760  					// but that may be a little mysterious for most end users.
   761  					// So, for now, we skip reload on CHMOD.
   762  					// We do have to check for WRITE though. On slower laptops a Chmod
   763  					// could be aggregated with other important events, and we still want
   764  					// to rebuild on those
   765  					if ev.Op&(fsnotify.Chmod|fsnotify.Write|fsnotify.Create) == fsnotify.Chmod {
   766  						continue
   767  					}
   768  
   769  					walkAdder := func(path string, f os.FileInfo, err error) error {
   770  						if f.IsDir() {
   771  							c.Logger.FEEDBACK.Println("adding created directory to watchlist", path)
   772  							if err := watcher.Add(path); err != nil {
   773  								return err
   774  							}
   775  						} else if !staticSyncer.isStatic(path) {
   776  							// Hugo's rebuilding logic is entirely file based. When you drop a new folder into
   777  							// /content on OSX, the above logic will handle future watching of those files,
   778  							// but the initial CREATE is lost.
   779  							dynamicEvents = append(dynamicEvents, fsnotify.Event{Name: path, Op: fsnotify.Create})
   780  						}
   781  						return nil
   782  					}
   783  
   784  					// recursively add new directories to watch list
   785  					// When mkdir -p is used, only the top directory triggers an event (at least on OSX)
   786  					if ev.Op&fsnotify.Create == fsnotify.Create {
   787  						if s, err := c.Fs.Source.Stat(ev.Name); err == nil && s.Mode().IsDir() {
   788  							_ = helpers.SymbolicWalk(c.Fs.Source, ev.Name, walkAdder)
   789  						}
   790  					}
   791  
   792  					if staticSyncer.isStatic(ev.Name) {
   793  						staticEvents = append(staticEvents, ev)
   794  					} else {
   795  						dynamicEvents = append(dynamicEvents, ev)
   796  					}
   797  				}
   798  
   799  				if len(staticEvents) > 0 {
   800  					c.Logger.FEEDBACK.Println("\nStatic file changes detected")
   801  					const layout = "2006-01-02 15:04:05.000 -0700"
   802  					c.Logger.FEEDBACK.Println(time.Now().Format(layout))
   803  
   804  					if c.Cfg.GetBool("forceSyncStatic") {
   805  						c.Logger.FEEDBACK.Printf("Syncing all static files\n")
   806  						_, err := c.copyStatic()
   807  						if err != nil {
   808  							utils.StopOnErr(c.Logger, err, "Error copying static files to publish dir")
   809  						}
   810  					} else {
   811  						if err := staticSyncer.syncsStaticEvents(staticEvents); err != nil {
   812  							c.Logger.ERROR.Println(err)
   813  							continue
   814  						}
   815  					}
   816  
   817  					if !c.h.buildWatch && !c.Cfg.GetBool("disableLiveReload") {
   818  						// Will block forever trying to write to a channel that nobody is reading if livereload isn't initialized
   819  
   820  						// force refresh when more than one file
   821  						if len(staticEvents) > 0 {
   822  							for _, ev := range staticEvents {
   823  
   824  								path := c.hugo.BaseFs.SourceFilesystems.MakeStaticPathRelative(ev.Name)
   825  								livereload.RefreshPath(path)
   826  							}
   827  
   828  						} else {
   829  							livereload.ForceRefresh()
   830  						}
   831  					}
   832  				}
   833  
   834  				if len(dynamicEvents) > 0 {
   835  					doLiveReload := !c.h.buildWatch && !c.Cfg.GetBool("disableLiveReload")
   836  					onePageName := pickOneWriteOrCreatePath(dynamicEvents)
   837  
   838  					c.Logger.FEEDBACK.Println("\nChange detected, rebuilding site")
   839  					const layout = "2006-01-02 15:04:05.000 -0700"
   840  					c.Logger.FEEDBACK.Println(time.Now().Format(layout))
   841  
   842  					if err := c.rebuildSites(dynamicEvents); err != nil {
   843  						c.Logger.ERROR.Println("Failed to rebuild site:", err)
   844  					}
   845  
   846  					if doLiveReload {
   847  						navigate := c.Cfg.GetBool("navigateToChanged")
   848  						// We have fetched the same page above, but it may have
   849  						// changed.
   850  						var p *hugolib.Page
   851  
   852  						if navigate {
   853  							if onePageName != "" {
   854  								p = c.hugo.GetContentPage(onePageName)
   855  							}
   856  
   857  						}
   858  
   859  						if p != nil {
   860  							livereload.NavigateToPathForPort(p.RelPermalink(), p.Site.ServerPort())
   861  						} else {
   862  							livereload.ForceRefresh()
   863  						}
   864  					}
   865  				}
   866  			case err := <-watcher.Errors:
   867  				if err != nil {
   868  					c.Logger.ERROR.Println(err)
   869  				}
   870  			}
   871  		}
   872  	}()
   873  
   874  	return watcher, nil
   875  }
   876  
   877  func pickOneWriteOrCreatePath(events []fsnotify.Event) string {
   878  	name := ""
   879  
   880  	// Some editors (for example notepad.exe on Windows) triggers a change
   881  	// both for directory and file. So we pick the longest path, which should
   882  	// be the file itself.
   883  	for _, ev := range events {
   884  		if (ev.Op&fsnotify.Write == fsnotify.Write || ev.Op&fsnotify.Create == fsnotify.Create) && len(ev.Name) > len(name) {
   885  			name = ev.Name
   886  		}
   887  	}
   888  
   889  	return name
   890  }
   891  
   892  // isThemeVsHugoVersionMismatch returns whether the current Hugo version is
   893  // less than any of the themes' min_version.
   894  func (c *commandeer) isThemeVsHugoVersionMismatch(fs afero.Fs) (mismatch bool, requiredMinVersion string) {
   895  	if !c.hugo.PathSpec.ThemeSet() {
   896  		return
   897  	}
   898  
   899  	for _, absThemeDir := range c.hugo.BaseFs.AbsThemeDirs {
   900  
   901  		path := filepath.Join(absThemeDir, "theme.toml")
   902  
   903  		exists, err := helpers.Exists(path, fs)
   904  
   905  		if err != nil || !exists {
   906  			continue
   907  		}
   908  
   909  		b, err := afero.ReadFile(fs, path)
   910  
   911  		tomlMeta, err := parser.HandleTOMLMetaData(b)
   912  
   913  		if err != nil {
   914  			continue
   915  		}
   916  
   917  		if minVersion, ok := tomlMeta["min_version"]; ok {
   918  			if helpers.CompareVersion(minVersion) > 0 {
   919  				return true, fmt.Sprint(minVersion)
   920  			}
   921  		}
   922  
   923  	}
   924  
   925  	return
   926  }