github.com/shohhei1126/hugo@v0.42.2-0.20180623210752-3d5928889ad7/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) recreateAndBuildSites(watching bool) (err error) {
   600  	defer c.timeTrack(time.Now(), "Total")
   601  	if !c.h.quiet {
   602  		c.Logger.FEEDBACK.Println("Started building sites ...")
   603  	}
   604  	return c.hugo.Build(hugolib.BuildCfg{CreateSitesFromConfig: true})
   605  }
   606  
   607  func (c *commandeer) resetAndBuildSites() (err error) {
   608  	if !c.h.quiet {
   609  		c.Logger.FEEDBACK.Println("Started building sites ...")
   610  	}
   611  	return c.hugo.Build(hugolib.BuildCfg{ResetState: true})
   612  }
   613  
   614  func (c *commandeer) buildSites() (err error) {
   615  	return c.hugo.Build(hugolib.BuildCfg{})
   616  }
   617  
   618  func (c *commandeer) rebuildSites(events []fsnotify.Event) error {
   619  	defer c.timeTrack(time.Now(), "Total")
   620  
   621  	visited := c.visitedURLs.PeekAllSet()
   622  	doLiveReload := !c.h.buildWatch && !c.Cfg.GetBool("disableLiveReload")
   623  	if doLiveReload && !c.Cfg.GetBool("disableFastRender") {
   624  
   625  		// Make sure we always render the home pages
   626  		for _, l := range c.languages {
   627  			langPath := c.hugo.PathSpec.GetLangSubDir(l.Lang)
   628  			if langPath != "" {
   629  				langPath = langPath + "/"
   630  			}
   631  			home := c.hugo.PathSpec.PrependBasePath("/" + langPath)
   632  			visited[home] = true
   633  		}
   634  
   635  	}
   636  	return c.hugo.Build(hugolib.BuildCfg{RecentlyVisited: visited}, events...)
   637  }
   638  
   639  func (c *commandeer) fullRebuild() {
   640  	if err := c.loadConfig(true, true); err != nil {
   641  		jww.ERROR.Println("Failed to reload config:", err)
   642  	} else if err := c.recreateAndBuildSites(true); err != nil {
   643  		jww.ERROR.Println(err)
   644  	} else if !c.h.buildWatch && !c.Cfg.GetBool("disableLiveReload") {
   645  		livereload.ForceRefresh()
   646  	}
   647  }
   648  
   649  // newWatcher creates a new watcher to watch filesystem events.
   650  func (c *commandeer) newWatcher(dirList ...string) (*watcher.Batcher, error) {
   651  	if runtime.GOOS == "darwin" {
   652  		tweakLimit()
   653  	}
   654  
   655  	staticSyncer, err := newStaticSyncer(c)
   656  	if err != nil {
   657  		return nil, err
   658  	}
   659  
   660  	watcher, err := watcher.New(1 * time.Second)
   661  
   662  	if err != nil {
   663  		return nil, err
   664  	}
   665  
   666  	for _, d := range dirList {
   667  		if d != "" {
   668  			_ = watcher.Add(d)
   669  		}
   670  	}
   671  
   672  	// Identifies changes to config (config.toml) files.
   673  	configSet := make(map[string]bool)
   674  
   675  	for _, configFile := range c.configFiles {
   676  		c.Logger.FEEDBACK.Println("Watching for config changes in", configFile)
   677  		watcher.Add(configFile)
   678  		configSet[configFile] = true
   679  	}
   680  
   681  	go func() {
   682  		for {
   683  			select {
   684  			case evs := <-watcher.Events:
   685  				if len(evs) > 50 {
   686  					// This is probably a mass edit of the content dir.
   687  					// Schedule a full rebuild for when it slows down.
   688  					c.debounce(c.fullRebuild)
   689  					continue
   690  				}
   691  
   692  				c.Logger.INFO.Println("Received System Events:", evs)
   693  
   694  				staticEvents := []fsnotify.Event{}
   695  				dynamicEvents := []fsnotify.Event{}
   696  
   697  				// Special handling for symbolic links inside /content.
   698  				filtered := []fsnotify.Event{}
   699  				for _, ev := range evs {
   700  					if configSet[ev.Name] {
   701  						if ev.Op&fsnotify.Chmod == fsnotify.Chmod {
   702  							continue
   703  						}
   704  						// Config file changed. Need full rebuild.
   705  						c.fullRebuild()
   706  						break
   707  					}
   708  
   709  					// Check the most specific first, i.e. files.
   710  					contentMapped := c.hugo.ContentChanges.GetSymbolicLinkMappings(ev.Name)
   711  					if len(contentMapped) > 0 {
   712  						for _, mapped := range contentMapped {
   713  							filtered = append(filtered, fsnotify.Event{Name: mapped, Op: ev.Op})
   714  						}
   715  						continue
   716  					}
   717  
   718  					// Check for any symbolic directory mapping.
   719  
   720  					dir, name := filepath.Split(ev.Name)
   721  
   722  					contentMapped = c.hugo.ContentChanges.GetSymbolicLinkMappings(dir)
   723  
   724  					if len(contentMapped) == 0 {
   725  						filtered = append(filtered, ev)
   726  						continue
   727  					}
   728  
   729  					for _, mapped := range contentMapped {
   730  						mappedFilename := filepath.Join(mapped, name)
   731  						filtered = append(filtered, fsnotify.Event{Name: mappedFilename, Op: ev.Op})
   732  					}
   733  				}
   734  
   735  				evs = filtered
   736  
   737  				for _, ev := range evs {
   738  					ext := filepath.Ext(ev.Name)
   739  					baseName := filepath.Base(ev.Name)
   740  					istemp := strings.HasSuffix(ext, "~") ||
   741  						(ext == ".swp") || // vim
   742  						(ext == ".swx") || // vim
   743  						(ext == ".tmp") || // generic temp file
   744  						(ext == ".DS_Store") || // OSX Thumbnail
   745  						baseName == "4913" || // vim
   746  						strings.HasPrefix(ext, ".goutputstream") || // gnome
   747  						strings.HasSuffix(ext, "jb_old___") || // intelliJ
   748  						strings.HasSuffix(ext, "jb_tmp___") || // intelliJ
   749  						strings.HasSuffix(ext, "jb_bak___") || // intelliJ
   750  						strings.HasPrefix(ext, ".sb-") || // byword
   751  						strings.HasPrefix(baseName, ".#") || // emacs
   752  						strings.HasPrefix(baseName, "#") // emacs
   753  					if istemp {
   754  						continue
   755  					}
   756  					// Sometimes during rm -rf operations a '"": REMOVE' is triggered. Just ignore these
   757  					if ev.Name == "" {
   758  						continue
   759  					}
   760  
   761  					// Write and rename operations are often followed by CHMOD.
   762  					// There may be valid use cases for rebuilding the site on CHMOD,
   763  					// but that will require more complex logic than this simple conditional.
   764  					// On OS X this seems to be related to Spotlight, see:
   765  					// https://github.com/go-fsnotify/fsnotify/issues/15
   766  					// A workaround is to put your site(s) on the Spotlight exception list,
   767  					// but that may be a little mysterious for most end users.
   768  					// So, for now, we skip reload on CHMOD.
   769  					// We do have to check for WRITE though. On slower laptops a Chmod
   770  					// could be aggregated with other important events, and we still want
   771  					// to rebuild on those
   772  					if ev.Op&(fsnotify.Chmod|fsnotify.Write|fsnotify.Create) == fsnotify.Chmod {
   773  						continue
   774  					}
   775  
   776  					walkAdder := func(path string, f os.FileInfo, err error) error {
   777  						if f.IsDir() {
   778  							c.Logger.FEEDBACK.Println("adding created directory to watchlist", path)
   779  							if err := watcher.Add(path); err != nil {
   780  								return err
   781  							}
   782  						} else if !staticSyncer.isStatic(path) {
   783  							// Hugo's rebuilding logic is entirely file based. When you drop a new folder into
   784  							// /content on OSX, the above logic will handle future watching of those files,
   785  							// but the initial CREATE is lost.
   786  							dynamicEvents = append(dynamicEvents, fsnotify.Event{Name: path, Op: fsnotify.Create})
   787  						}
   788  						return nil
   789  					}
   790  
   791  					// recursively add new directories to watch list
   792  					// When mkdir -p is used, only the top directory triggers an event (at least on OSX)
   793  					if ev.Op&fsnotify.Create == fsnotify.Create {
   794  						if s, err := c.Fs.Source.Stat(ev.Name); err == nil && s.Mode().IsDir() {
   795  							_ = helpers.SymbolicWalk(c.Fs.Source, ev.Name, walkAdder)
   796  						}
   797  					}
   798  
   799  					if staticSyncer.isStatic(ev.Name) {
   800  						staticEvents = append(staticEvents, ev)
   801  					} else {
   802  						dynamicEvents = append(dynamicEvents, ev)
   803  					}
   804  				}
   805  
   806  				if len(staticEvents) > 0 {
   807  					c.Logger.FEEDBACK.Println("\nStatic file changes detected")
   808  					const layout = "2006-01-02 15:04:05.000 -0700"
   809  					c.Logger.FEEDBACK.Println(time.Now().Format(layout))
   810  
   811  					if c.Cfg.GetBool("forceSyncStatic") {
   812  						c.Logger.FEEDBACK.Printf("Syncing all static files\n")
   813  						_, err := c.copyStatic()
   814  						if err != nil {
   815  							utils.StopOnErr(c.Logger, err, "Error copying static files to publish dir")
   816  						}
   817  					} else {
   818  						if err := staticSyncer.syncsStaticEvents(staticEvents); err != nil {
   819  							c.Logger.ERROR.Println(err)
   820  							continue
   821  						}
   822  					}
   823  
   824  					if !c.h.buildWatch && !c.Cfg.GetBool("disableLiveReload") {
   825  						// Will block forever trying to write to a channel that nobody is reading if livereload isn't initialized
   826  
   827  						// force refresh when more than one file
   828  						if len(staticEvents) > 0 {
   829  							for _, ev := range staticEvents {
   830  
   831  								path := c.hugo.BaseFs.SourceFilesystems.MakeStaticPathRelative(ev.Name)
   832  								livereload.RefreshPath(path)
   833  							}
   834  
   835  						} else {
   836  							livereload.ForceRefresh()
   837  						}
   838  					}
   839  				}
   840  
   841  				if len(dynamicEvents) > 0 {
   842  					doLiveReload := !c.h.buildWatch && !c.Cfg.GetBool("disableLiveReload")
   843  					onePageName := pickOneWriteOrCreatePath(dynamicEvents)
   844  
   845  					c.Logger.FEEDBACK.Println("\nChange detected, rebuilding site")
   846  					const layout = "2006-01-02 15:04:05.000 -0700"
   847  					c.Logger.FEEDBACK.Println(time.Now().Format(layout))
   848  
   849  					if err := c.rebuildSites(dynamicEvents); err != nil {
   850  						c.Logger.ERROR.Println("Failed to rebuild site:", err)
   851  					}
   852  
   853  					if doLiveReload {
   854  						navigate := c.Cfg.GetBool("navigateToChanged")
   855  						// We have fetched the same page above, but it may have
   856  						// changed.
   857  						var p *hugolib.Page
   858  
   859  						if navigate {
   860  							if onePageName != "" {
   861  								p = c.hugo.GetContentPage(onePageName)
   862  							}
   863  
   864  						}
   865  
   866  						if p != nil {
   867  							livereload.NavigateToPathForPort(p.RelPermalink(), p.Site.ServerPort())
   868  						} else {
   869  							livereload.ForceRefresh()
   870  						}
   871  					}
   872  				}
   873  			case err := <-watcher.Errors:
   874  				if err != nil {
   875  					c.Logger.ERROR.Println(err)
   876  				}
   877  			}
   878  		}
   879  	}()
   880  
   881  	return watcher, nil
   882  }
   883  
   884  func pickOneWriteOrCreatePath(events []fsnotify.Event) string {
   885  	name := ""
   886  
   887  	// Some editors (for example notepad.exe on Windows) triggers a change
   888  	// both for directory and file. So we pick the longest path, which should
   889  	// be the file itself.
   890  	for _, ev := range events {
   891  		if (ev.Op&fsnotify.Write == fsnotify.Write || ev.Op&fsnotify.Create == fsnotify.Create) && len(ev.Name) > len(name) {
   892  			name = ev.Name
   893  		}
   894  	}
   895  
   896  	return name
   897  }
   898  
   899  // isThemeVsHugoVersionMismatch returns whether the current Hugo version is
   900  // less than any of the themes' min_version.
   901  func (c *commandeer) isThemeVsHugoVersionMismatch(fs afero.Fs) (mismatch bool, requiredMinVersion string) {
   902  	if !c.hugo.PathSpec.ThemeSet() {
   903  		return
   904  	}
   905  
   906  	for _, absThemeDir := range c.hugo.BaseFs.AbsThemeDirs {
   907  
   908  		path := filepath.Join(absThemeDir, "theme.toml")
   909  
   910  		exists, err := helpers.Exists(path, fs)
   911  
   912  		if err != nil || !exists {
   913  			continue
   914  		}
   915  
   916  		b, err := afero.ReadFile(fs, path)
   917  
   918  		tomlMeta, err := parser.HandleTOMLMetaData(b)
   919  
   920  		if err != nil {
   921  			continue
   922  		}
   923  
   924  		if minVersion, ok := tomlMeta["min_version"]; ok {
   925  			if helpers.CompareVersion(minVersion) > 0 {
   926  				return true, fmt.Sprint(minVersion)
   927  			}
   928  		}
   929  
   930  	}
   931  
   932  	return
   933  }