github.com/gohugoio/hugo@v0.88.1/commands/hugo.go (about)

     1  // Copyright 2019 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  	"context"
    20  	"fmt"
    21  	"io/ioutil"
    22  	"os"
    23  	"os/signal"
    24  	"path/filepath"
    25  	"runtime"
    26  	"runtime/pprof"
    27  	"runtime/trace"
    28  	"strings"
    29  	"sync/atomic"
    30  	"syscall"
    31  	"time"
    32  
    33  	"github.com/gohugoio/hugo/common/types"
    34  
    35  	"github.com/gohugoio/hugo/hugofs"
    36  
    37  	"github.com/gohugoio/hugo/resources/page"
    38  
    39  	"github.com/pkg/errors"
    40  
    41  	"github.com/gohugoio/hugo/common/herrors"
    42  	"github.com/gohugoio/hugo/common/hugo"
    43  	"github.com/gohugoio/hugo/common/loggers"
    44  	"github.com/gohugoio/hugo/common/terminal"
    45  
    46  	"github.com/gohugoio/hugo/hugolib/filesystems"
    47  
    48  	"golang.org/x/sync/errgroup"
    49  
    50  	"github.com/gohugoio/hugo/config"
    51  
    52  	flag "github.com/spf13/pflag"
    53  
    54  	"github.com/fsnotify/fsnotify"
    55  	"github.com/gohugoio/hugo/helpers"
    56  	"github.com/gohugoio/hugo/hugolib"
    57  	"github.com/gohugoio/hugo/livereload"
    58  	"github.com/gohugoio/hugo/watcher"
    59  	"github.com/spf13/afero"
    60  	"github.com/spf13/cobra"
    61  	"github.com/spf13/fsync"
    62  	jww "github.com/spf13/jwalterweatherman"
    63  )
    64  
    65  // The Response value from Execute.
    66  type Response struct {
    67  	// The build Result will only be set in the hugo build command.
    68  	Result *hugolib.HugoSites
    69  
    70  	// Err is set when the command failed to execute.
    71  	Err error
    72  
    73  	// The command that was executed.
    74  	Cmd *cobra.Command
    75  }
    76  
    77  // IsUserError returns true is the Response error is a user error rather than a
    78  // system error.
    79  func (r Response) IsUserError() bool {
    80  	return r.Err != nil && isUserError(r.Err)
    81  }
    82  
    83  // Execute adds all child commands to the root command HugoCmd and sets flags appropriately.
    84  // The args are usually filled with os.Args[1:].
    85  func Execute(args []string) Response {
    86  	hugoCmd := newCommandsBuilder().addAll().build()
    87  	cmd := hugoCmd.getCommand()
    88  	cmd.SetArgs(args)
    89  
    90  	c, err := cmd.ExecuteC()
    91  
    92  	var resp Response
    93  
    94  	if c == cmd && hugoCmd.c != nil {
    95  		// Root command executed
    96  		resp.Result = hugoCmd.c.hugo()
    97  	}
    98  
    99  	if err == nil {
   100  		errCount := int(loggers.GlobalErrorCounter.Count())
   101  		if errCount > 0 {
   102  			err = fmt.Errorf("logged %d errors", errCount)
   103  		} else if resp.Result != nil {
   104  			errCount = resp.Result.NumLogErrors()
   105  			if errCount > 0 {
   106  				err = fmt.Errorf("logged %d errors", errCount)
   107  			}
   108  		}
   109  
   110  	}
   111  
   112  	resp.Err = err
   113  	resp.Cmd = c
   114  
   115  	return resp
   116  }
   117  
   118  // InitializeConfig initializes a config file with sensible default configuration flags.
   119  func initializeConfig(mustHaveConfigFile, failOnInitErr, running bool,
   120  	h *hugoBuilderCommon,
   121  	f flagsToConfigHandler,
   122  	cfgInit func(c *commandeer) error) (*commandeer, error) {
   123  	c, err := newCommandeer(mustHaveConfigFile, failOnInitErr, running, h, f, cfgInit)
   124  	if err != nil {
   125  		return nil, err
   126  	}
   127  
   128  	return c, nil
   129  }
   130  
   131  func (c *commandeer) createLogger(cfg config.Provider) (loggers.Logger, error) {
   132  	var (
   133  		logHandle       = ioutil.Discard
   134  		logThreshold    = jww.LevelWarn
   135  		logFile         = cfg.GetString("logFile")
   136  		outHandle       = ioutil.Discard
   137  		stdoutThreshold = jww.LevelWarn
   138  	)
   139  
   140  	if !c.h.quiet {
   141  		outHandle = os.Stdout
   142  	}
   143  
   144  	if c.h.verboseLog || c.h.logging || (c.h.logFile != "") {
   145  		var err error
   146  		if logFile != "" {
   147  			logHandle, err = os.OpenFile(logFile, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0666)
   148  			if err != nil {
   149  				return nil, newSystemError("Failed to open log file:", logFile, err)
   150  			}
   151  		} else {
   152  			logHandle, err = ioutil.TempFile("", "hugo")
   153  			if err != nil {
   154  				return nil, newSystemError(err)
   155  			}
   156  		}
   157  	} else if !c.h.quiet && cfg.GetBool("verbose") {
   158  		stdoutThreshold = jww.LevelInfo
   159  	}
   160  
   161  	if cfg.GetBool("debug") {
   162  		stdoutThreshold = jww.LevelDebug
   163  	}
   164  
   165  	if c.h.verboseLog {
   166  		logThreshold = jww.LevelInfo
   167  		if cfg.GetBool("debug") {
   168  			logThreshold = jww.LevelDebug
   169  		}
   170  	}
   171  
   172  	loggers.InitGlobalLogger(stdoutThreshold, logThreshold, outHandle, logHandle)
   173  	helpers.InitLoggers()
   174  
   175  	return loggers.NewLogger(stdoutThreshold, logThreshold, outHandle, logHandle, c.running), nil
   176  }
   177  
   178  func initializeFlags(cmd *cobra.Command, cfg config.Provider) {
   179  	persFlagKeys := []string{
   180  		"debug",
   181  		"verbose",
   182  		"logFile",
   183  		// Moved from vars
   184  	}
   185  	flagKeys := []string{
   186  		"cleanDestinationDir",
   187  		"buildDrafts",
   188  		"buildFuture",
   189  		"buildExpired",
   190  		"uglyURLs",
   191  		"canonifyURLs",
   192  		"enableRobotsTXT",
   193  		"enableGitInfo",
   194  		"pluralizeListTitles",
   195  		"preserveTaxonomyNames",
   196  		"ignoreCache",
   197  		"forceSyncStatic",
   198  		"noTimes",
   199  		"noChmod",
   200  		"ignoreVendor",
   201  		"ignoreVendorPaths",
   202  		"templateMetrics",
   203  		"templateMetricsHints",
   204  
   205  		// Moved from vars.
   206  		"baseURL",
   207  		"buildWatch",
   208  		"cacheDir",
   209  		"cfgFile",
   210  		"confirm",
   211  		"contentDir",
   212  		"debug",
   213  		"destination",
   214  		"disableKinds",
   215  		"dryRun",
   216  		"force",
   217  		"gc",
   218  		"i18n-warnings",
   219  		"invalidateCDN",
   220  		"layoutDir",
   221  		"logFile",
   222  		"maxDeletes",
   223  		"quiet",
   224  		"renderToMemory",
   225  		"source",
   226  		"target",
   227  		"theme",
   228  		"themesDir",
   229  		"verbose",
   230  		"verboseLog",
   231  		"duplicateTargetPaths",
   232  	}
   233  
   234  	for _, key := range persFlagKeys {
   235  		setValueFromFlag(cmd.PersistentFlags(), key, cfg, "", false)
   236  	}
   237  	for _, key := range flagKeys {
   238  		setValueFromFlag(cmd.Flags(), key, cfg, "", false)
   239  	}
   240  
   241  	setValueFromFlag(cmd.Flags(), "minify", cfg, "minifyOutput", true)
   242  
   243  	// Set some "config aliases"
   244  	setValueFromFlag(cmd.Flags(), "destination", cfg, "publishDir", false)
   245  	setValueFromFlag(cmd.Flags(), "i18n-warnings", cfg, "logI18nWarnings", false)
   246  	setValueFromFlag(cmd.Flags(), "path-warnings", cfg, "logPathWarnings", false)
   247  }
   248  
   249  func setValueFromFlag(flags *flag.FlagSet, key string, cfg config.Provider, targetKey string, force bool) {
   250  	key = strings.TrimSpace(key)
   251  	if (force && flags.Lookup(key) != nil) || flags.Changed(key) {
   252  		f := flags.Lookup(key)
   253  		configKey := key
   254  		if targetKey != "" {
   255  			configKey = targetKey
   256  		}
   257  		// Gotta love this API.
   258  		switch f.Value.Type() {
   259  		case "bool":
   260  			bv, _ := flags.GetBool(key)
   261  			cfg.Set(configKey, bv)
   262  		case "string":
   263  			cfg.Set(configKey, f.Value.String())
   264  		case "stringSlice":
   265  			bv, _ := flags.GetStringSlice(key)
   266  			cfg.Set(configKey, bv)
   267  		case "int":
   268  			iv, _ := flags.GetInt(key)
   269  			cfg.Set(configKey, iv)
   270  		default:
   271  			panic(fmt.Sprintf("update switch with %s", f.Value.Type()))
   272  		}
   273  
   274  	}
   275  }
   276  
   277  func isTerminal() bool {
   278  	return terminal.IsTerminal(os.Stdout)
   279  }
   280  
   281  func (c *commandeer) fullBuild() error {
   282  	var (
   283  		g         errgroup.Group
   284  		langCount map[string]uint64
   285  	)
   286  
   287  	if !c.h.quiet {
   288  		fmt.Println("Start building sites … ")
   289  		fmt.Println(hugo.BuildVersionString())
   290  		if isTerminal() {
   291  			defer func() {
   292  				fmt.Print(showCursor + clearLine)
   293  			}()
   294  		}
   295  	}
   296  
   297  	copyStaticFunc := func() error {
   298  		cnt, err := c.copyStatic()
   299  		if err != nil {
   300  			return errors.Wrap(err, "Error copying static files")
   301  		}
   302  		langCount = cnt
   303  		return nil
   304  	}
   305  	buildSitesFunc := func() error {
   306  		if err := c.buildSites(); err != nil {
   307  			return errors.Wrap(err, "Error building site")
   308  		}
   309  		return nil
   310  	}
   311  	// Do not copy static files and build sites in parallel if cleanDestinationDir is enabled.
   312  	// This flag deletes all static resources in /public folder that are missing in /static,
   313  	// and it does so at the end of copyStatic() call.
   314  	if c.Cfg.GetBool("cleanDestinationDir") {
   315  		if err := copyStaticFunc(); err != nil {
   316  			return err
   317  		}
   318  		if err := buildSitesFunc(); err != nil {
   319  			return err
   320  		}
   321  	} else {
   322  		g.Go(copyStaticFunc)
   323  		g.Go(buildSitesFunc)
   324  		if err := g.Wait(); err != nil {
   325  			return err
   326  		}
   327  	}
   328  
   329  	for _, s := range c.hugo().Sites {
   330  		s.ProcessingStats.Static = langCount[s.Language().Lang]
   331  	}
   332  
   333  	if c.h.gc {
   334  		count, err := c.hugo().GC()
   335  		if err != nil {
   336  			return err
   337  		}
   338  		for _, s := range c.hugo().Sites {
   339  			// We have no way of knowing what site the garbage belonged to.
   340  			s.ProcessingStats.Cleaned = uint64(count)
   341  		}
   342  	}
   343  
   344  	return nil
   345  }
   346  
   347  func (c *commandeer) initCPUProfile() (func(), error) {
   348  	if c.h.cpuprofile == "" {
   349  		return nil, nil
   350  	}
   351  
   352  	f, err := os.Create(c.h.cpuprofile)
   353  	if err != nil {
   354  		return nil, errors.Wrap(err, "failed to create CPU profile")
   355  	}
   356  	if err := pprof.StartCPUProfile(f); err != nil {
   357  		return nil, errors.Wrap(err, "failed to start CPU profile")
   358  	}
   359  	return func() {
   360  		pprof.StopCPUProfile()
   361  		f.Close()
   362  	}, nil
   363  }
   364  
   365  func (c *commandeer) initMemProfile() {
   366  	if c.h.memprofile == "" {
   367  		return
   368  	}
   369  
   370  	f, err := os.Create(c.h.memprofile)
   371  	if err != nil {
   372  		c.logger.Errorf("could not create memory profile: ", err)
   373  	}
   374  	defer f.Close()
   375  	runtime.GC() // get up-to-date statistics
   376  	if err := pprof.WriteHeapProfile(f); err != nil {
   377  		c.logger.Errorf("could not write memory profile: ", err)
   378  	}
   379  }
   380  
   381  func (c *commandeer) initTraceProfile() (func(), error) {
   382  	if c.h.traceprofile == "" {
   383  		return nil, nil
   384  	}
   385  
   386  	f, err := os.Create(c.h.traceprofile)
   387  	if err != nil {
   388  		return nil, errors.Wrap(err, "failed to create trace file")
   389  	}
   390  
   391  	if err := trace.Start(f); err != nil {
   392  		return nil, errors.Wrap(err, "failed to start trace")
   393  	}
   394  
   395  	return func() {
   396  		trace.Stop()
   397  		f.Close()
   398  	}, nil
   399  }
   400  
   401  func (c *commandeer) initMutexProfile() (func(), error) {
   402  	if c.h.mutexprofile == "" {
   403  		return nil, nil
   404  	}
   405  
   406  	f, err := os.Create(c.h.mutexprofile)
   407  	if err != nil {
   408  		return nil, err
   409  	}
   410  
   411  	runtime.SetMutexProfileFraction(1)
   412  
   413  	return func() {
   414  		pprof.Lookup("mutex").WriteTo(f, 0)
   415  		f.Close()
   416  	}, nil
   417  }
   418  
   419  func (c *commandeer) initMemTicker() func() {
   420  	memticker := time.NewTicker(5 * time.Second)
   421  	quit := make(chan struct{})
   422  	printMem := func() {
   423  		var m runtime.MemStats
   424  		runtime.ReadMemStats(&m)
   425  		fmt.Printf("\n\nAlloc = %v\nTotalAlloc = %v\nSys = %v\nNumGC = %v\n\n", formatByteCount(m.Alloc), formatByteCount(m.TotalAlloc), formatByteCount(m.Sys), m.NumGC)
   426  	}
   427  
   428  	go func() {
   429  		for {
   430  			select {
   431  			case <-memticker.C:
   432  				printMem()
   433  			case <-quit:
   434  				memticker.Stop()
   435  				printMem()
   436  				return
   437  			}
   438  		}
   439  	}()
   440  
   441  	return func() {
   442  		close(quit)
   443  	}
   444  }
   445  
   446  func (c *commandeer) initProfiling() (func(), error) {
   447  	stopCPUProf, err := c.initCPUProfile()
   448  	if err != nil {
   449  		return nil, err
   450  	}
   451  
   452  	stopMutexProf, err := c.initMutexProfile()
   453  	if err != nil {
   454  		return nil, err
   455  	}
   456  
   457  	stopTraceProf, err := c.initTraceProfile()
   458  	if err != nil {
   459  		return nil, err
   460  	}
   461  
   462  	var stopMemTicker func()
   463  	if c.h.printm {
   464  		stopMemTicker = c.initMemTicker()
   465  	}
   466  
   467  	return func() {
   468  		c.initMemProfile()
   469  
   470  		if stopCPUProf != nil {
   471  			stopCPUProf()
   472  		}
   473  		if stopMutexProf != nil {
   474  			stopMutexProf()
   475  		}
   476  
   477  		if stopTraceProf != nil {
   478  			stopTraceProf()
   479  		}
   480  
   481  		if stopMemTicker != nil {
   482  			stopMemTicker()
   483  		}
   484  	}, nil
   485  }
   486  
   487  func (c *commandeer) build() error {
   488  	stopProfiling, err := c.initProfiling()
   489  	if err != nil {
   490  		return err
   491  	}
   492  
   493  	defer func() {
   494  		if stopProfiling != nil {
   495  			stopProfiling()
   496  		}
   497  	}()
   498  
   499  	if err := c.fullBuild(); err != nil {
   500  		return err
   501  	}
   502  
   503  	// TODO(bep) Feedback?
   504  	if !c.h.quiet {
   505  		fmt.Println()
   506  		c.hugo().PrintProcessingStats(os.Stdout)
   507  		fmt.Println()
   508  
   509  		if createCounter, ok := c.destinationFs.(hugofs.DuplicatesReporter); ok {
   510  			dupes := createCounter.ReportDuplicates()
   511  			if dupes != "" {
   512  				c.logger.Warnln("Duplicate target paths:", dupes)
   513  			}
   514  		}
   515  	}
   516  
   517  	if c.h.buildWatch {
   518  		watchDirs, err := c.getDirList()
   519  		if err != nil {
   520  			return err
   521  		}
   522  
   523  		baseWatchDir := c.Cfg.GetString("workingDir")
   524  		rootWatchDirs := getRootWatchDirsStr(baseWatchDir, watchDirs)
   525  
   526  		c.logger.Printf("Watching for changes in %s%s{%s}\n", baseWatchDir, helpers.FilePathSeparator, rootWatchDirs)
   527  		c.logger.Println("Press Ctrl+C to stop")
   528  		watcher, err := c.newWatcher(c.h.poll, watchDirs...)
   529  		checkErr(c.Logger, err)
   530  		defer watcher.Close()
   531  
   532  		sigs := make(chan os.Signal, 1)
   533  		signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
   534  
   535  		<-sigs
   536  	}
   537  
   538  	return nil
   539  }
   540  
   541  func (c *commandeer) serverBuild() error {
   542  
   543  	stopProfiling, err := c.initProfiling()
   544  	if err != nil {
   545  		return err
   546  	}
   547  
   548  	defer func() {
   549  		if stopProfiling != nil {
   550  			stopProfiling()
   551  		}
   552  	}()
   553  
   554  	if err := c.fullBuild(); err != nil {
   555  		return err
   556  	}
   557  
   558  	// TODO(bep) Feedback?
   559  	if !c.h.quiet {
   560  		fmt.Println()
   561  		c.hugo().PrintProcessingStats(os.Stdout)
   562  		fmt.Println()
   563  	}
   564  
   565  	return nil
   566  }
   567  
   568  func (c *commandeer) copyStatic() (map[string]uint64, error) {
   569  	m, err := c.doWithPublishDirs(c.copyStaticTo)
   570  	if err == nil || os.IsNotExist(err) {
   571  		return m, nil
   572  	}
   573  	return m, err
   574  }
   575  
   576  func (c *commandeer) doWithPublishDirs(f func(sourceFs *filesystems.SourceFilesystem) (uint64, error)) (map[string]uint64, error) {
   577  	langCount := make(map[string]uint64)
   578  
   579  	staticFilesystems := c.hugo().BaseFs.SourceFilesystems.Static
   580  
   581  	if len(staticFilesystems) == 0 {
   582  		c.logger.Infoln("No static directories found to sync")
   583  		return langCount, nil
   584  	}
   585  
   586  	for lang, fs := range staticFilesystems {
   587  		cnt, err := f(fs)
   588  		if err != nil {
   589  			return langCount, err
   590  		}
   591  
   592  		if lang == "" {
   593  			// Not multihost
   594  			for _, l := range c.languages {
   595  				langCount[l.Lang] = cnt
   596  			}
   597  		} else {
   598  			langCount[lang] = cnt
   599  		}
   600  	}
   601  
   602  	return langCount, nil
   603  }
   604  
   605  type countingStatFs struct {
   606  	afero.Fs
   607  	statCounter uint64
   608  }
   609  
   610  func (fs *countingStatFs) Stat(name string) (os.FileInfo, error) {
   611  	f, err := fs.Fs.Stat(name)
   612  	if err == nil {
   613  		if !f.IsDir() {
   614  			atomic.AddUint64(&fs.statCounter, 1)
   615  		}
   616  	}
   617  	return f, err
   618  }
   619  
   620  func chmodFilter(dst, src os.FileInfo) bool {
   621  	// Hugo publishes data from multiple sources, potentially
   622  	// with overlapping directory structures. We cannot sync permissions
   623  	// for directories as that would mean that we might end up with write-protected
   624  	// directories inside /public.
   625  	// One example of this would be syncing from the Go Module cache,
   626  	// which have 0555 directories.
   627  	return src.IsDir()
   628  }
   629  
   630  func (c *commandeer) copyStaticTo(sourceFs *filesystems.SourceFilesystem) (uint64, error) {
   631  	publishDir := c.hugo().PathSpec.PublishDir
   632  	// If root, remove the second '/'
   633  	if publishDir == "//" {
   634  		publishDir = helpers.FilePathSeparator
   635  	}
   636  
   637  	if sourceFs.PublishFolder != "" {
   638  		publishDir = filepath.Join(publishDir, sourceFs.PublishFolder)
   639  	}
   640  
   641  	fs := &countingStatFs{Fs: sourceFs.Fs}
   642  
   643  	syncer := fsync.NewSyncer()
   644  	syncer.NoTimes = c.Cfg.GetBool("noTimes")
   645  	syncer.NoChmod = c.Cfg.GetBool("noChmod")
   646  	syncer.ChmodFilter = chmodFilter
   647  	syncer.SrcFs = fs
   648  	syncer.DestFs = c.Fs.Destination
   649  	// Now that we are using a unionFs for the static directories
   650  	// We can effectively clean the publishDir on initial sync
   651  	syncer.Delete = c.Cfg.GetBool("cleanDestinationDir")
   652  
   653  	if syncer.Delete {
   654  		c.logger.Infoln("removing all files from destination that don't exist in static dirs")
   655  
   656  		syncer.DeleteFilter = func(f os.FileInfo) bool {
   657  			return f.IsDir() && strings.HasPrefix(f.Name(), ".")
   658  		}
   659  	}
   660  	c.logger.Infoln("syncing static files to", publishDir)
   661  
   662  	// because we are using a baseFs (to get the union right).
   663  	// set sync src to root
   664  	err := syncer.Sync(publishDir, helpers.FilePathSeparator)
   665  	if err != nil {
   666  		return 0, err
   667  	}
   668  
   669  	// Sync runs Stat 3 times for every source file (which sounds much)
   670  	numFiles := fs.statCounter / 3
   671  
   672  	return numFiles, err
   673  }
   674  
   675  func (c *commandeer) firstPathSpec() *helpers.PathSpec {
   676  	return c.hugo().Sites[0].PathSpec
   677  }
   678  
   679  func (c *commandeer) timeTrack(start time.Time, name string) {
   680  	elapsed := time.Since(start)
   681  	c.logger.Printf("%s in %v ms", name, int(1000*elapsed.Seconds()))
   682  }
   683  
   684  // getDirList provides NewWatcher() with a list of directories to watch for changes.
   685  func (c *commandeer) getDirList() ([]string, error) {
   686  	var filenames []string
   687  
   688  	walkFn := func(path string, fi hugofs.FileMetaInfo, err error) error {
   689  		if err != nil {
   690  			c.logger.Errorln("walker: ", err)
   691  			return nil
   692  		}
   693  
   694  		if fi.IsDir() {
   695  			if fi.Name() == ".git" ||
   696  				fi.Name() == "node_modules" || fi.Name() == "bower_components" {
   697  				return filepath.SkipDir
   698  			}
   699  
   700  			filenames = append(filenames, fi.Meta().Filename)
   701  		}
   702  
   703  		return nil
   704  	}
   705  
   706  	watchFiles := c.hugo().PathSpec.BaseFs.WatchDirs()
   707  	for _, fi := range watchFiles {
   708  		if !fi.IsDir() {
   709  			filenames = append(filenames, fi.Meta().Filename)
   710  			continue
   711  		}
   712  
   713  		w := hugofs.NewWalkway(hugofs.WalkwayConfig{Logger: c.logger, Info: fi, WalkFn: walkFn})
   714  		if err := w.Walk(); err != nil {
   715  			c.logger.Errorln("walker: ", err)
   716  		}
   717  	}
   718  
   719  	filenames = helpers.UniqueStringsSorted(filenames)
   720  
   721  	return filenames, nil
   722  }
   723  
   724  func (c *commandeer) buildSites() (err error) {
   725  	return c.hugo().Build(hugolib.BuildCfg{})
   726  }
   727  
   728  func (c *commandeer) handleBuildErr(err error, msg string) {
   729  	c.buildErr = err
   730  
   731  	c.logger.Errorln(msg + ":\n")
   732  	c.logger.Errorln(helpers.FirstUpper(err.Error()))
   733  	if !c.h.quiet && c.h.verbose {
   734  		herrors.PrintStackTraceFromErr(err)
   735  	}
   736  }
   737  
   738  func (c *commandeer) rebuildSites(events []fsnotify.Event) error {
   739  
   740  	c.buildErr = nil
   741  	visited := c.visitedURLs.PeekAllSet()
   742  	if c.fastRenderMode {
   743  		// Make sure we always render the home pages
   744  		for _, l := range c.languages {
   745  			langPath := c.hugo().PathSpec.GetLangSubDir(l.Lang)
   746  			if langPath != "" {
   747  				langPath = langPath + "/"
   748  			}
   749  			home := c.hugo().PathSpec.PrependBasePath("/"+langPath, false)
   750  			visited[home] = true
   751  		}
   752  	}
   753  	return c.hugo().Build(hugolib.BuildCfg{RecentlyVisited: visited, ErrRecovery: c.wasError}, events...)
   754  }
   755  
   756  func (c *commandeer) partialReRender(urls ...string) error {
   757  	defer func() {
   758  		c.wasError = false
   759  	}()
   760  	c.buildErr = nil
   761  	visited := make(map[string]bool)
   762  	for _, url := range urls {
   763  		visited[url] = true
   764  	}
   765  	return c.hugo().Build(hugolib.BuildCfg{RecentlyVisited: visited, PartialReRender: true, ErrRecovery: c.wasError})
   766  }
   767  
   768  func (c *commandeer) fullRebuild(changeType string) {
   769  	if changeType == configChangeGoMod {
   770  		// go.mod may be changed during the build itself, and
   771  		// we really want to prevent superfluous builds.
   772  		if !c.fullRebuildSem.TryAcquire(1) {
   773  			return
   774  		}
   775  		c.fullRebuildSem.Release(1)
   776  	}
   777  
   778  	c.fullRebuildSem.Acquire(context.Background(), 1)
   779  
   780  	go func() {
   781  		defer c.fullRebuildSem.Release(1)
   782  
   783  		c.printChangeDetected(changeType)
   784  
   785  		defer func() {
   786  			// Allow any file system events to arrive back.
   787  			// This will block any rebuild on config changes for the
   788  			// duration of the sleep.
   789  			time.Sleep(2 * time.Second)
   790  		}()
   791  
   792  		defer c.timeTrack(time.Now(), "Rebuilt")
   793  
   794  		c.commandeerHugoState = newCommandeerHugoState()
   795  		err := c.loadConfig()
   796  		if err != nil {
   797  			// Set the processing on pause until the state is recovered.
   798  			c.paused = true
   799  			c.handleBuildErr(err, "Failed to reload config")
   800  
   801  		} else {
   802  			c.paused = false
   803  		}
   804  
   805  		if !c.paused {
   806  			_, err := c.copyStatic()
   807  			if err != nil {
   808  				c.logger.Errorln(err)
   809  				return
   810  			}
   811  
   812  			err = c.buildSites()
   813  			if err != nil {
   814  				c.logger.Errorln(err)
   815  			} else if !c.h.buildWatch && !c.Cfg.GetBool("disableLiveReload") {
   816  				livereload.ForceRefresh()
   817  			}
   818  		}
   819  	}()
   820  }
   821  
   822  // newWatcher creates a new watcher to watch filesystem events.
   823  func (c *commandeer) newWatcher(pollIntervalStr string, dirList ...string) (*watcher.Batcher, error) {
   824  	if runtime.GOOS == "darwin" {
   825  		tweakLimit()
   826  	}
   827  
   828  	staticSyncer, err := newStaticSyncer(c)
   829  	if err != nil {
   830  		return nil, err
   831  	}
   832  
   833  	var pollInterval time.Duration
   834  	poll := pollIntervalStr != ""
   835  	if poll {
   836  		pollInterval, err = types.ToDurationE(pollIntervalStr)
   837  		if err != nil {
   838  			return nil, fmt.Errorf("invalid value for flag poll: %s", err)
   839  		}
   840  		c.logger.Printf("Use watcher with poll interval %v", pollInterval)
   841  	}
   842  
   843  	watcher, err := watcher.New(500*time.Millisecond, pollInterval, poll)
   844  	if err != nil {
   845  		return nil, err
   846  	}
   847  
   848  	for _, d := range dirList {
   849  		if d != "" {
   850  			_ = watcher.Add(d)
   851  		}
   852  	}
   853  
   854  	// Identifies changes to config (config.toml) files.
   855  	configSet := make(map[string]bool)
   856  
   857  	c.logger.Println("Watching for config changes in", strings.Join(c.configFiles, ", "))
   858  	for _, configFile := range c.configFiles {
   859  		watcher.Add(configFile)
   860  		configSet[configFile] = true
   861  	}
   862  
   863  	go func() {
   864  		for {
   865  			select {
   866  			case evs := <-watcher.Events:
   867  				c.handleEvents(watcher, staticSyncer, evs, configSet)
   868  				if c.showErrorInBrowser && c.errCount() > 0 {
   869  					// Need to reload browser to show the error
   870  					livereload.ForceRefresh()
   871  				}
   872  			case err := <-watcher.Errors():
   873  				if err != nil {
   874  					c.logger.Errorln("Error while watching:", err)
   875  				}
   876  			}
   877  		}
   878  	}()
   879  
   880  	return watcher, nil
   881  }
   882  
   883  func (c *commandeer) printChangeDetected(typ string) {
   884  	msg := "\nChange"
   885  	if typ != "" {
   886  		msg += " of " + typ
   887  	}
   888  	msg += " detected, rebuilding site."
   889  
   890  	c.logger.Println(msg)
   891  	const layout = "2006-01-02 15:04:05.000 -0700"
   892  	c.logger.Println(time.Now().Format(layout))
   893  }
   894  
   895  const (
   896  	configChangeConfig = "config file"
   897  	configChangeGoMod  = "go.mod file"
   898  )
   899  
   900  func (c *commandeer) handleEvents(watcher *watcher.Batcher,
   901  	staticSyncer *staticSyncer,
   902  	evs []fsnotify.Event,
   903  	configSet map[string]bool) {
   904  	defer func() {
   905  		c.wasError = false
   906  	}()
   907  
   908  	var isHandled bool
   909  
   910  	for _, ev := range evs {
   911  		isConfig := configSet[ev.Name]
   912  		configChangeType := configChangeConfig
   913  		if isConfig {
   914  			if strings.Contains(ev.Name, "go.mod") {
   915  				configChangeType = configChangeGoMod
   916  			}
   917  		}
   918  		if !isConfig {
   919  			// It may be one of the /config folders
   920  			dirname := filepath.Dir(ev.Name)
   921  			if dirname != "." && configSet[dirname] {
   922  				isConfig = true
   923  			}
   924  		}
   925  
   926  		if isConfig {
   927  			isHandled = true
   928  
   929  			if ev.Op&fsnotify.Chmod == fsnotify.Chmod {
   930  				continue
   931  			}
   932  
   933  			if ev.Op&fsnotify.Remove == fsnotify.Remove || ev.Op&fsnotify.Rename == fsnotify.Rename {
   934  				for _, configFile := range c.configFiles {
   935  					counter := 0
   936  					for watcher.Add(configFile) != nil {
   937  						counter++
   938  						if counter >= 100 {
   939  							break
   940  						}
   941  						time.Sleep(100 * time.Millisecond)
   942  					}
   943  				}
   944  			}
   945  
   946  			// Config file(s) changed. Need full rebuild.
   947  			c.fullRebuild(configChangeType)
   948  
   949  			return
   950  		}
   951  	}
   952  
   953  	if isHandled {
   954  		return
   955  	}
   956  
   957  	if c.paused {
   958  		// Wait for the server to get into a consistent state before
   959  		// we continue with processing.
   960  		return
   961  	}
   962  
   963  	if len(evs) > 50 {
   964  		// This is probably a mass edit of the content dir.
   965  		// Schedule a full rebuild for when it slows down.
   966  		c.debounce(func() {
   967  			c.fullRebuild("")
   968  		})
   969  		return
   970  	}
   971  
   972  	c.logger.Infoln("Received System Events:", evs)
   973  
   974  	staticEvents := []fsnotify.Event{}
   975  	dynamicEvents := []fsnotify.Event{}
   976  
   977  	filtered := []fsnotify.Event{}
   978  	for _, ev := range evs {
   979  		if c.hugo().ShouldSkipFileChangeEvent(ev) {
   980  			continue
   981  		}
   982  		// Check the most specific first, i.e. files.
   983  		contentMapped := c.hugo().ContentChanges.GetSymbolicLinkMappings(ev.Name)
   984  		if len(contentMapped) > 0 {
   985  			for _, mapped := range contentMapped {
   986  				filtered = append(filtered, fsnotify.Event{Name: mapped, Op: ev.Op})
   987  			}
   988  			continue
   989  		}
   990  
   991  		// Check for any symbolic directory mapping.
   992  
   993  		dir, name := filepath.Split(ev.Name)
   994  
   995  		contentMapped = c.hugo().ContentChanges.GetSymbolicLinkMappings(dir)
   996  
   997  		if len(contentMapped) == 0 {
   998  			filtered = append(filtered, ev)
   999  			continue
  1000  		}
  1001  
  1002  		for _, mapped := range contentMapped {
  1003  			mappedFilename := filepath.Join(mapped, name)
  1004  			filtered = append(filtered, fsnotify.Event{Name: mappedFilename, Op: ev.Op})
  1005  		}
  1006  	}
  1007  
  1008  	evs = filtered
  1009  
  1010  	for _, ev := range evs {
  1011  		ext := filepath.Ext(ev.Name)
  1012  		baseName := filepath.Base(ev.Name)
  1013  		istemp := strings.HasSuffix(ext, "~") ||
  1014  			(ext == ".swp") || // vim
  1015  			(ext == ".swx") || // vim
  1016  			(ext == ".tmp") || // generic temp file
  1017  			(ext == ".DS_Store") || // OSX Thumbnail
  1018  			baseName == "4913" || // vim
  1019  			strings.HasPrefix(ext, ".goutputstream") || // gnome
  1020  			strings.HasSuffix(ext, "jb_old___") || // intelliJ
  1021  			strings.HasSuffix(ext, "jb_tmp___") || // intelliJ
  1022  			strings.HasSuffix(ext, "jb_bak___") || // intelliJ
  1023  			strings.HasPrefix(ext, ".sb-") || // byword
  1024  			strings.HasPrefix(baseName, ".#") || // emacs
  1025  			strings.HasPrefix(baseName, "#") // emacs
  1026  		if istemp {
  1027  			continue
  1028  		}
  1029  		if c.hugo().Deps.SourceSpec.IgnoreFile(ev.Name) {
  1030  			continue
  1031  		}
  1032  		// Sometimes during rm -rf operations a '"": REMOVE' is triggered. Just ignore these
  1033  		if ev.Name == "" {
  1034  			continue
  1035  		}
  1036  
  1037  		// Write and rename operations are often followed by CHMOD.
  1038  		// There may be valid use cases for rebuilding the site on CHMOD,
  1039  		// but that will require more complex logic than this simple conditional.
  1040  		// On OS X this seems to be related to Spotlight, see:
  1041  		// https://github.com/go-fsnotify/fsnotify/issues/15
  1042  		// A workaround is to put your site(s) on the Spotlight exception list,
  1043  		// but that may be a little mysterious for most end users.
  1044  		// So, for now, we skip reload on CHMOD.
  1045  		// We do have to check for WRITE though. On slower laptops a Chmod
  1046  		// could be aggregated with other important events, and we still want
  1047  		// to rebuild on those
  1048  		if ev.Op&(fsnotify.Chmod|fsnotify.Write|fsnotify.Create) == fsnotify.Chmod {
  1049  			continue
  1050  		}
  1051  
  1052  		walkAdder := func(path string, f hugofs.FileMetaInfo, err error) error {
  1053  			if f.IsDir() {
  1054  				c.logger.Println("adding created directory to watchlist", path)
  1055  				if err := watcher.Add(path); err != nil {
  1056  					return err
  1057  				}
  1058  			} else if !staticSyncer.isStatic(path) {
  1059  				// Hugo's rebuilding logic is entirely file based. When you drop a new folder into
  1060  				// /content on OSX, the above logic will handle future watching of those files,
  1061  				// but the initial CREATE is lost.
  1062  				dynamicEvents = append(dynamicEvents, fsnotify.Event{Name: path, Op: fsnotify.Create})
  1063  			}
  1064  			return nil
  1065  		}
  1066  
  1067  		// recursively add new directories to watch list
  1068  		// When mkdir -p is used, only the top directory triggers an event (at least on OSX)
  1069  		if ev.Op&fsnotify.Create == fsnotify.Create {
  1070  			if s, err := c.Fs.Source.Stat(ev.Name); err == nil && s.Mode().IsDir() {
  1071  				_ = helpers.SymbolicWalk(c.Fs.Source, ev.Name, walkAdder)
  1072  			}
  1073  		}
  1074  
  1075  		if staticSyncer.isStatic(ev.Name) {
  1076  			staticEvents = append(staticEvents, ev)
  1077  		} else {
  1078  			dynamicEvents = append(dynamicEvents, ev)
  1079  		}
  1080  	}
  1081  
  1082  	if len(staticEvents) > 0 {
  1083  		c.printChangeDetected("Static files")
  1084  
  1085  		if c.Cfg.GetBool("forceSyncStatic") {
  1086  			c.logger.Printf("Syncing all static files\n")
  1087  			_, err := c.copyStatic()
  1088  			if err != nil {
  1089  				c.logger.Errorln("Error copying static files to publish dir:", err)
  1090  				return
  1091  			}
  1092  		} else {
  1093  			if err := staticSyncer.syncsStaticEvents(staticEvents); err != nil {
  1094  				c.logger.Errorln("Error syncing static files to publish dir:", err)
  1095  				return
  1096  			}
  1097  		}
  1098  
  1099  		if !c.h.buildWatch && !c.Cfg.GetBool("disableLiveReload") {
  1100  			// Will block forever trying to write to a channel that nobody is reading if livereload isn't initialized
  1101  
  1102  			// force refresh when more than one file
  1103  			if !c.wasError && len(staticEvents) == 1 {
  1104  				ev := staticEvents[0]
  1105  				path := c.hugo().BaseFs.SourceFilesystems.MakeStaticPathRelative(ev.Name)
  1106  				path = c.firstPathSpec().RelURL(helpers.ToSlashTrimLeading(path), false)
  1107  
  1108  				livereload.RefreshPath(path)
  1109  			} else {
  1110  				livereload.ForceRefresh()
  1111  			}
  1112  		}
  1113  	}
  1114  
  1115  	if len(dynamicEvents) > 0 {
  1116  		partitionedEvents := partitionDynamicEvents(
  1117  			c.firstPathSpec().BaseFs.SourceFilesystems,
  1118  			dynamicEvents)
  1119  
  1120  		doLiveReload := !c.h.buildWatch && !c.Cfg.GetBool("disableLiveReload")
  1121  		onePageName := pickOneWriteOrCreatePath(partitionedEvents.ContentEvents)
  1122  
  1123  		c.printChangeDetected("")
  1124  		c.changeDetector.PrepareNew()
  1125  
  1126  		func() {
  1127  			defer c.timeTrack(time.Now(), "Total")
  1128  			if err := c.rebuildSites(dynamicEvents); err != nil {
  1129  				c.handleBuildErr(err, "Rebuild failed")
  1130  			}
  1131  		}()
  1132  
  1133  		if doLiveReload {
  1134  			if len(partitionedEvents.ContentEvents) == 0 && len(partitionedEvents.AssetEvents) > 0 {
  1135  				if c.wasError {
  1136  					livereload.ForceRefresh()
  1137  					return
  1138  				}
  1139  				changed := c.changeDetector.changed()
  1140  				if c.changeDetector != nil && len(changed) == 0 {
  1141  					// Nothing has changed.
  1142  					return
  1143  				} else if len(changed) == 1 {
  1144  					pathToRefresh := c.firstPathSpec().RelURL(helpers.ToSlashTrimLeading(changed[0]), false)
  1145  					livereload.RefreshPath(pathToRefresh)
  1146  				} else {
  1147  					livereload.ForceRefresh()
  1148  				}
  1149  			}
  1150  
  1151  			if len(partitionedEvents.ContentEvents) > 0 {
  1152  
  1153  				navigate := c.Cfg.GetBool("navigateToChanged")
  1154  				// We have fetched the same page above, but it may have
  1155  				// changed.
  1156  				var p page.Page
  1157  
  1158  				if navigate {
  1159  					if onePageName != "" {
  1160  						p = c.hugo().GetContentPage(onePageName)
  1161  					}
  1162  				}
  1163  
  1164  				if p != nil {
  1165  					livereload.NavigateToPathForPort(p.RelPermalink(), p.Site().ServerPort())
  1166  				} else {
  1167  					livereload.ForceRefresh()
  1168  				}
  1169  			}
  1170  		}
  1171  	}
  1172  }
  1173  
  1174  // dynamicEvents contains events that is considered dynamic, as in "not static".
  1175  // Both of these categories will trigger a new build, but the asset events
  1176  // does not fit into the "navigate to changed" logic.
  1177  type dynamicEvents struct {
  1178  	ContentEvents []fsnotify.Event
  1179  	AssetEvents   []fsnotify.Event
  1180  }
  1181  
  1182  func partitionDynamicEvents(sourceFs *filesystems.SourceFilesystems, events []fsnotify.Event) (de dynamicEvents) {
  1183  	for _, e := range events {
  1184  		if sourceFs.IsAsset(e.Name) {
  1185  			de.AssetEvents = append(de.AssetEvents, e)
  1186  		} else {
  1187  			de.ContentEvents = append(de.ContentEvents, e)
  1188  		}
  1189  	}
  1190  	return
  1191  }
  1192  
  1193  func pickOneWriteOrCreatePath(events []fsnotify.Event) string {
  1194  	name := ""
  1195  
  1196  	// Some editors (for example notepad.exe on Windows) triggers a change
  1197  	// both for directory and file. So we pick the longest path, which should
  1198  	// be the file itself.
  1199  	for _, ev := range events {
  1200  		if (ev.Op&fsnotify.Write == fsnotify.Write || ev.Op&fsnotify.Create == fsnotify.Create) && len(ev.Name) > len(name) {
  1201  			name = ev.Name
  1202  		}
  1203  	}
  1204  
  1205  	return name
  1206  }
  1207  
  1208  func formatByteCount(b uint64) string {
  1209  	const unit = 1000
  1210  	if b < unit {
  1211  		return fmt.Sprintf("%d B", b)
  1212  	}
  1213  	div, exp := int64(unit), 0
  1214  	for n := b / unit; n >= unit; n /= unit {
  1215  		div *= unit
  1216  		exp++
  1217  	}
  1218  	return fmt.Sprintf("%.1f %cB",
  1219  		float64(b)/float64(div), "kMGTPE"[exp])
  1220  }