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