mirror of
				https://github.com/gohugoio/hugo.git
				synced 2024-05-11 05:54:58 +00:00 
			
		
		
		
	The main motivation of this commit is to add a `page.Page` interface to replace the very file-oriented `hugolib.Page` struct. This is all a preparation step for issue #5074, "pages from other data sources". But this also fixes a set of annoying limitations, especially related to custom output formats, and shortcodes. Most notable changes: * The inner content of shortcodes using the `{{%` as the outer-most delimiter will now be sent to the content renderer, e.g. Blackfriday. This means that any markdown will partake in the global ToC and footnote context etc. * The Custom Output formats are now "fully virtualized". This removes many of the current limitations. * The taxonomy list type now has a reference to the `Page` object. This improves the taxonomy template `.Title` situation and make common template constructs much simpler. See #5074 Fixes #5763 Fixes #5758 Fixes #5090 Fixes #5204 Fixes #4695 Fixes #5607 Fixes #5707 Fixes #5719 Fixes #3113 Fixes #5706 Fixes #5767 Fixes #5723 Fixes #5769 Fixes #5770 Fixes #5771 Fixes #5759 Fixes #5776 Fixes #5777 Fixes #5778
		
			
				
	
	
		
			1199 lines
		
	
	
		
			29 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			1199 lines
		
	
	
		
			29 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
// Copyright 2019 The Hugo Authors. All rights reserved.
 | 
						|
//
 | 
						|
// Licensed under the Apache License, Version 2.0 (the "License");
 | 
						|
// you may not use this file except in compliance with the License.
 | 
						|
// You may obtain a copy of the License at
 | 
						|
// http://www.apache.org/licenses/LICENSE-2.0
 | 
						|
//
 | 
						|
// Unless required by applicable law or agreed to in writing, software
 | 
						|
// distributed under the License is distributed on an "AS IS" BASIS,
 | 
						|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | 
						|
// See the License for the specific language governing permissions and
 | 
						|
// limitations under the License.
 | 
						|
 | 
						|
// Package commands defines and implements command-line commands and flags
 | 
						|
// used by Hugo. Commands and flags are implemented using Cobra.
 | 
						|
package commands
 | 
						|
 | 
						|
import (
 | 
						|
	"fmt"
 | 
						|
	"io/ioutil"
 | 
						|
	"os/signal"
 | 
						|
	"runtime/pprof"
 | 
						|
	"runtime/trace"
 | 
						|
	"sort"
 | 
						|
	"sync/atomic"
 | 
						|
 | 
						|
	"github.com/gohugoio/hugo/hugofs"
 | 
						|
 | 
						|
	"github.com/gohugoio/hugo/resources/page"
 | 
						|
 | 
						|
	"github.com/gohugoio/hugo/common/hugo"
 | 
						|
	"github.com/pkg/errors"
 | 
						|
 | 
						|
	"github.com/gohugoio/hugo/common/herrors"
 | 
						|
	"github.com/gohugoio/hugo/common/loggers"
 | 
						|
	"github.com/gohugoio/hugo/common/terminal"
 | 
						|
 | 
						|
	"syscall"
 | 
						|
 | 
						|
	"github.com/gohugoio/hugo/hugolib/filesystems"
 | 
						|
 | 
						|
	"golang.org/x/sync/errgroup"
 | 
						|
 | 
						|
	"os"
 | 
						|
	"path/filepath"
 | 
						|
	"runtime"
 | 
						|
	"strings"
 | 
						|
	"time"
 | 
						|
 | 
						|
	"github.com/gohugoio/hugo/config"
 | 
						|
 | 
						|
	"github.com/gohugoio/hugo/parser/metadecoders"
 | 
						|
	flag "github.com/spf13/pflag"
 | 
						|
 | 
						|
	"github.com/fsnotify/fsnotify"
 | 
						|
	"github.com/gohugoio/hugo/helpers"
 | 
						|
	"github.com/gohugoio/hugo/hugolib"
 | 
						|
	"github.com/gohugoio/hugo/livereload"
 | 
						|
	"github.com/gohugoio/hugo/watcher"
 | 
						|
	"github.com/spf13/afero"
 | 
						|
	"github.com/spf13/cobra"
 | 
						|
	"github.com/spf13/fsync"
 | 
						|
	jww "github.com/spf13/jwalterweatherman"
 | 
						|
)
 | 
						|
 | 
						|
// The Response value from Execute.
 | 
						|
type Response struct {
 | 
						|
	// The build Result will only be set in the hugo build command.
 | 
						|
	Result *hugolib.HugoSites
 | 
						|
 | 
						|
	// Err is set when the command failed to execute.
 | 
						|
	Err error
 | 
						|
 | 
						|
	// The command that was executed.
 | 
						|
	Cmd *cobra.Command
 | 
						|
}
 | 
						|
 | 
						|
// IsUserError returns true is the Response error is a user error rather than a
 | 
						|
// system error.
 | 
						|
func (r Response) IsUserError() bool {
 | 
						|
	return r.Err != nil && isUserError(r.Err)
 | 
						|
}
 | 
						|
 | 
						|
// Execute adds all child commands to the root command HugoCmd and sets flags appropriately.
 | 
						|
// The args are usually filled with os.Args[1:].
 | 
						|
func Execute(args []string) Response {
 | 
						|
	hugoCmd := newCommandsBuilder().addAll().build()
 | 
						|
	cmd := hugoCmd.getCommand()
 | 
						|
	cmd.SetArgs(args)
 | 
						|
 | 
						|
	c, err := cmd.ExecuteC()
 | 
						|
 | 
						|
	var resp Response
 | 
						|
 | 
						|
	if c == cmd && hugoCmd.c != nil {
 | 
						|
		// Root command executed
 | 
						|
		resp.Result = hugoCmd.c.hugo
 | 
						|
	}
 | 
						|
 | 
						|
	if err == nil {
 | 
						|
		errCount := int(loggers.GlobalErrorCounter.Count())
 | 
						|
		if errCount > 0 {
 | 
						|
			err = fmt.Errorf("logged %d errors", errCount)
 | 
						|
		} else if resp.Result != nil {
 | 
						|
			errCount = resp.Result.NumLogErrors()
 | 
						|
			if errCount > 0 {
 | 
						|
				err = fmt.Errorf("logged %d errors", errCount)
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
	}
 | 
						|
 | 
						|
	resp.Err = err
 | 
						|
	resp.Cmd = c
 | 
						|
 | 
						|
	return resp
 | 
						|
}
 | 
						|
 | 
						|
// InitializeConfig initializes a config file with sensible default configuration flags.
 | 
						|
func initializeConfig(mustHaveConfigFile, running bool,
 | 
						|
	h *hugoBuilderCommon,
 | 
						|
	f flagsToConfigHandler,
 | 
						|
	doWithCommandeer func(c *commandeer) error) (*commandeer, error) {
 | 
						|
 | 
						|
	c, err := newCommandeer(mustHaveConfigFile, running, h, f, doWithCommandeer)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	return c, nil
 | 
						|
 | 
						|
}
 | 
						|
 | 
						|
func (c *commandeer) createLogger(cfg config.Provider, running bool) (*loggers.Logger, error) {
 | 
						|
	var (
 | 
						|
		logHandle       = ioutil.Discard
 | 
						|
		logThreshold    = jww.LevelWarn
 | 
						|
		logFile         = cfg.GetString("logFile")
 | 
						|
		outHandle       = os.Stdout
 | 
						|
		stdoutThreshold = jww.LevelWarn
 | 
						|
	)
 | 
						|
 | 
						|
	if c.h.verboseLog || c.h.logging || (c.h.logFile != "") {
 | 
						|
		var err error
 | 
						|
		if logFile != "" {
 | 
						|
			logHandle, err = os.OpenFile(logFile, os.O_RDWR|os.O_APPEND|os.O_CREATE, 0666)
 | 
						|
			if err != nil {
 | 
						|
				return nil, newSystemError("Failed to open log file:", logFile, err)
 | 
						|
			}
 | 
						|
		} else {
 | 
						|
			logHandle, err = ioutil.TempFile("", "hugo")
 | 
						|
			if err != nil {
 | 
						|
				return nil, newSystemError(err)
 | 
						|
			}
 | 
						|
		}
 | 
						|
	} else if !c.h.quiet && cfg.GetBool("verbose") {
 | 
						|
		stdoutThreshold = jww.LevelInfo
 | 
						|
	}
 | 
						|
 | 
						|
	if cfg.GetBool("debug") {
 | 
						|
		stdoutThreshold = jww.LevelDebug
 | 
						|
	}
 | 
						|
 | 
						|
	if c.h.verboseLog {
 | 
						|
		logThreshold = jww.LevelInfo
 | 
						|
		if cfg.GetBool("debug") {
 | 
						|
			logThreshold = jww.LevelDebug
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	loggers.InitGlobalLogger(stdoutThreshold, logThreshold, outHandle, logHandle)
 | 
						|
	helpers.InitLoggers()
 | 
						|
 | 
						|
	return loggers.NewLogger(stdoutThreshold, logThreshold, outHandle, logHandle, running), nil
 | 
						|
}
 | 
						|
 | 
						|
func initializeFlags(cmd *cobra.Command, cfg config.Provider) {
 | 
						|
	persFlagKeys := []string{
 | 
						|
		"debug",
 | 
						|
		"verbose",
 | 
						|
		"logFile",
 | 
						|
		// Moved from vars
 | 
						|
	}
 | 
						|
	flagKeys := []string{
 | 
						|
		"cleanDestinationDir",
 | 
						|
		"buildDrafts",
 | 
						|
		"buildFuture",
 | 
						|
		"buildExpired",
 | 
						|
		"uglyURLs",
 | 
						|
		"canonifyURLs",
 | 
						|
		"enableRobotsTXT",
 | 
						|
		"enableGitInfo",
 | 
						|
		"pluralizeListTitles",
 | 
						|
		"preserveTaxonomyNames",
 | 
						|
		"ignoreCache",
 | 
						|
		"forceSyncStatic",
 | 
						|
		"noTimes",
 | 
						|
		"noChmod",
 | 
						|
		"templateMetrics",
 | 
						|
		"templateMetricsHints",
 | 
						|
 | 
						|
		// Moved from vars.
 | 
						|
		"baseURL",
 | 
						|
		"buildWatch",
 | 
						|
		"cacheDir",
 | 
						|
		"cfgFile",
 | 
						|
		"contentDir",
 | 
						|
		"debug",
 | 
						|
		"destination",
 | 
						|
		"disableKinds",
 | 
						|
		"gc",
 | 
						|
		"layoutDir",
 | 
						|
		"logFile",
 | 
						|
		"i18n-warnings",
 | 
						|
		"quiet",
 | 
						|
		"renderToMemory",
 | 
						|
		"source",
 | 
						|
		"theme",
 | 
						|
		"themesDir",
 | 
						|
		"verbose",
 | 
						|
		"verboseLog",
 | 
						|
		"duplicateTargetPaths",
 | 
						|
	}
 | 
						|
 | 
						|
	// Will set a value even if it is the default.
 | 
						|
	flagKeysForced := []string{
 | 
						|
		"minify",
 | 
						|
	}
 | 
						|
 | 
						|
	for _, key := range persFlagKeys {
 | 
						|
		setValueFromFlag(cmd.PersistentFlags(), key, cfg, "", false)
 | 
						|
	}
 | 
						|
	for _, key := range flagKeys {
 | 
						|
		setValueFromFlag(cmd.Flags(), key, cfg, "", false)
 | 
						|
	}
 | 
						|
 | 
						|
	for _, key := range flagKeysForced {
 | 
						|
		setValueFromFlag(cmd.Flags(), key, cfg, "", true)
 | 
						|
	}
 | 
						|
 | 
						|
	// Set some "config aliases"
 | 
						|
	setValueFromFlag(cmd.Flags(), "destination", cfg, "publishDir", false)
 | 
						|
	setValueFromFlag(cmd.Flags(), "i18n-warnings", cfg, "logI18nWarnings", false)
 | 
						|
	setValueFromFlag(cmd.Flags(), "path-warnings", cfg, "logPathWarnings", false)
 | 
						|
 | 
						|
}
 | 
						|
 | 
						|
func setValueFromFlag(flags *flag.FlagSet, key string, cfg config.Provider, targetKey string, force bool) {
 | 
						|
	key = strings.TrimSpace(key)
 | 
						|
	if (force && flags.Lookup(key) != nil) || flags.Changed(key) {
 | 
						|
		f := flags.Lookup(key)
 | 
						|
		configKey := key
 | 
						|
		if targetKey != "" {
 | 
						|
			configKey = targetKey
 | 
						|
		}
 | 
						|
		// Gotta love this API.
 | 
						|
		switch f.Value.Type() {
 | 
						|
		case "bool":
 | 
						|
			bv, _ := flags.GetBool(key)
 | 
						|
			cfg.Set(configKey, bv)
 | 
						|
		case "string":
 | 
						|
			cfg.Set(configKey, f.Value.String())
 | 
						|
		case "stringSlice":
 | 
						|
			bv, _ := flags.GetStringSlice(key)
 | 
						|
			cfg.Set(configKey, bv)
 | 
						|
		default:
 | 
						|
			panic(fmt.Sprintf("update switch with %s", f.Value.Type()))
 | 
						|
		}
 | 
						|
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func isTerminal() bool {
 | 
						|
	return terminal.IsTerminal(os.Stdout)
 | 
						|
 | 
						|
}
 | 
						|
func ifTerminal(s string) string {
 | 
						|
	if !isTerminal() {
 | 
						|
		return ""
 | 
						|
	}
 | 
						|
	return s
 | 
						|
}
 | 
						|
 | 
						|
func (c *commandeer) fullBuild() error {
 | 
						|
	var (
 | 
						|
		g         errgroup.Group
 | 
						|
		langCount map[string]uint64
 | 
						|
	)
 | 
						|
 | 
						|
	if !c.h.quiet {
 | 
						|
		fmt.Print(ifTerminal(hideCursor) + "Building sites … ")
 | 
						|
		if isTerminal() {
 | 
						|
			defer func() {
 | 
						|
				fmt.Print(showCursor + clearLine)
 | 
						|
			}()
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	copyStaticFunc := func() error {
 | 
						|
 | 
						|
		cnt, err := c.copyStatic()
 | 
						|
		if err != nil {
 | 
						|
			if !os.IsNotExist(err) {
 | 
						|
				return errors.Wrap(err, "Error copying static files")
 | 
						|
			}
 | 
						|
			c.logger.INFO.Println("No Static directory found")
 | 
						|
		}
 | 
						|
		langCount = cnt
 | 
						|
		langCount = cnt
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
	buildSitesFunc := func() error {
 | 
						|
		if err := c.buildSites(); err != nil {
 | 
						|
			return errors.Wrap(err, "Error building site")
 | 
						|
		}
 | 
						|
		return nil
 | 
						|
	}
 | 
						|
	// Do not copy static files and build sites in parallel if cleanDestinationDir is enabled.
 | 
						|
	// This flag deletes all static resources in /public folder that are missing in /static,
 | 
						|
	// and it does so at the end of copyStatic() call.
 | 
						|
	if c.Cfg.GetBool("cleanDestinationDir") {
 | 
						|
		if err := copyStaticFunc(); err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
		if err := buildSitesFunc(); err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
	} else {
 | 
						|
		g.Go(copyStaticFunc)
 | 
						|
		g.Go(buildSitesFunc)
 | 
						|
		if err := g.Wait(); err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	for _, s := range c.hugo.Sites {
 | 
						|
		s.ProcessingStats.Static = langCount[s.Language().Lang]
 | 
						|
	}
 | 
						|
 | 
						|
	if c.h.gc {
 | 
						|
		count, err := c.hugo.GC()
 | 
						|
		if err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
		for _, s := range c.hugo.Sites {
 | 
						|
			// We have no way of knowing what site the garbage belonged to.
 | 
						|
			s.ProcessingStats.Cleaned = uint64(count)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return nil
 | 
						|
 | 
						|
}
 | 
						|
 | 
						|
func (c *commandeer) initCPUProfile() (func(), error) {
 | 
						|
	if c.h.cpuprofile == "" {
 | 
						|
		return nil, nil
 | 
						|
	}
 | 
						|
 | 
						|
	f, err := os.Create(c.h.cpuprofile)
 | 
						|
	if err != nil {
 | 
						|
		return nil, errors.Wrap(err, "failed to create CPU profile")
 | 
						|
	}
 | 
						|
	if err := pprof.StartCPUProfile(f); err != nil {
 | 
						|
		return nil, errors.Wrap(err, "failed to start CPU profile")
 | 
						|
	}
 | 
						|
	return func() {
 | 
						|
		pprof.StopCPUProfile()
 | 
						|
		f.Close()
 | 
						|
	}, nil
 | 
						|
}
 | 
						|
 | 
						|
func (c *commandeer) initMemProfile() {
 | 
						|
	if c.h.memprofile == "" {
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	f, err := os.Create(c.h.memprofile)
 | 
						|
	if err != nil {
 | 
						|
		c.logger.ERROR.Println("could not create memory profile: ", err)
 | 
						|
	}
 | 
						|
	defer f.Close()
 | 
						|
	runtime.GC() // get up-to-date statistics
 | 
						|
	if err := pprof.WriteHeapProfile(f); err != nil {
 | 
						|
		c.logger.ERROR.Println("could not write memory profile: ", err)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func (c *commandeer) initTraceProfile() (func(), error) {
 | 
						|
	if c.h.traceprofile == "" {
 | 
						|
		return nil, nil
 | 
						|
	}
 | 
						|
 | 
						|
	f, err := os.Create(c.h.traceprofile)
 | 
						|
	if err != nil {
 | 
						|
		return nil, errors.Wrap(err, "failed to create trace file")
 | 
						|
	}
 | 
						|
 | 
						|
	if err := trace.Start(f); err != nil {
 | 
						|
		return nil, errors.Wrap(err, "failed to start trace")
 | 
						|
	}
 | 
						|
 | 
						|
	return func() {
 | 
						|
		trace.Stop()
 | 
						|
		f.Close()
 | 
						|
	}, nil
 | 
						|
}
 | 
						|
 | 
						|
func (c *commandeer) initMutexProfile() (func(), error) {
 | 
						|
	if c.h.mutexprofile == "" {
 | 
						|
		return nil, nil
 | 
						|
	}
 | 
						|
 | 
						|
	f, err := os.Create(c.h.mutexprofile)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	runtime.SetMutexProfileFraction(1)
 | 
						|
 | 
						|
	return func() {
 | 
						|
		pprof.Lookup("mutex").WriteTo(f, 0)
 | 
						|
		f.Close()
 | 
						|
	}, nil
 | 
						|
 | 
						|
}
 | 
						|
 | 
						|
func (c *commandeer) initProfiling() (func(), error) {
 | 
						|
	stopCPUProf, err := c.initCPUProfile()
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	defer c.initMemProfile()
 | 
						|
 | 
						|
	stopMutexProf, err := c.initMutexProfile()
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	stopTraceProf, err := c.initTraceProfile()
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	return func() {
 | 
						|
		if stopCPUProf != nil {
 | 
						|
			stopCPUProf()
 | 
						|
		}
 | 
						|
		if stopMutexProf != nil {
 | 
						|
			stopMutexProf()
 | 
						|
		}
 | 
						|
 | 
						|
		if stopTraceProf != nil {
 | 
						|
			stopTraceProf()
 | 
						|
		}
 | 
						|
	}, nil
 | 
						|
}
 | 
						|
 | 
						|
func (c *commandeer) build() error {
 | 
						|
	defer c.timeTrack(time.Now(), "Total")
 | 
						|
 | 
						|
	stopProfiling, err := c.initProfiling()
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	defer func() {
 | 
						|
		if stopProfiling != nil {
 | 
						|
			stopProfiling()
 | 
						|
		}
 | 
						|
	}()
 | 
						|
 | 
						|
	if err := c.fullBuild(); err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	// TODO(bep) Feedback?
 | 
						|
	if !c.h.quiet {
 | 
						|
		fmt.Println()
 | 
						|
		c.hugo.PrintProcessingStats(os.Stdout)
 | 
						|
		fmt.Println()
 | 
						|
 | 
						|
		if createCounter, ok := c.destinationFs.(hugofs.DuplicatesReporter); ok {
 | 
						|
			dupes := createCounter.ReportDuplicates()
 | 
						|
			if dupes != "" {
 | 
						|
				c.logger.WARN.Println("Duplicate target paths:", dupes)
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if c.h.buildWatch {
 | 
						|
		watchDirs, err := c.getDirList()
 | 
						|
		if err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
		c.logger.FEEDBACK.Println("Watching for changes in", c.hugo.PathSpec.AbsPathify(c.Cfg.GetString("contentDir")))
 | 
						|
		c.logger.FEEDBACK.Println("Press Ctrl+C to stop")
 | 
						|
		watcher, err := c.newWatcher(watchDirs...)
 | 
						|
		checkErr(c.Logger, err)
 | 
						|
		defer watcher.Close()
 | 
						|
 | 
						|
		var sigs = make(chan os.Signal, 1)
 | 
						|
		signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
 | 
						|
 | 
						|
		<-sigs
 | 
						|
	}
 | 
						|
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func (c *commandeer) serverBuild() error {
 | 
						|
	defer c.timeTrack(time.Now(), "Total")
 | 
						|
 | 
						|
	stopProfiling, err := c.initProfiling()
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	defer func() {
 | 
						|
		if stopProfiling != nil {
 | 
						|
			stopProfiling()
 | 
						|
		}
 | 
						|
	}()
 | 
						|
 | 
						|
	if err := c.fullBuild(); err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	// TODO(bep) Feedback?
 | 
						|
	if !c.h.quiet {
 | 
						|
		fmt.Println()
 | 
						|
		c.hugo.PrintProcessingStats(os.Stdout)
 | 
						|
		fmt.Println()
 | 
						|
	}
 | 
						|
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
func (c *commandeer) copyStatic() (map[string]uint64, error) {
 | 
						|
	return c.doWithPublishDirs(c.copyStaticTo)
 | 
						|
}
 | 
						|
 | 
						|
func (c *commandeer) doWithPublishDirs(f func(sourceFs *filesystems.SourceFilesystem) (uint64, error)) (map[string]uint64, error) {
 | 
						|
 | 
						|
	langCount := make(map[string]uint64)
 | 
						|
 | 
						|
	staticFilesystems := c.hugo.BaseFs.SourceFilesystems.Static
 | 
						|
 | 
						|
	if len(staticFilesystems) == 0 {
 | 
						|
		c.logger.INFO.Println("No static directories found to sync")
 | 
						|
		return langCount, nil
 | 
						|
	}
 | 
						|
 | 
						|
	for lang, fs := range staticFilesystems {
 | 
						|
		cnt, err := f(fs)
 | 
						|
		if err != nil {
 | 
						|
			return langCount, err
 | 
						|
		}
 | 
						|
		if lang == "" {
 | 
						|
			// Not multihost
 | 
						|
			for _, l := range c.languages {
 | 
						|
				langCount[l.Lang] = cnt
 | 
						|
			}
 | 
						|
		} else {
 | 
						|
			langCount[lang] = cnt
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return langCount, nil
 | 
						|
}
 | 
						|
 | 
						|
type countingStatFs struct {
 | 
						|
	afero.Fs
 | 
						|
	statCounter uint64
 | 
						|
}
 | 
						|
 | 
						|
func (fs *countingStatFs) Stat(name string) (os.FileInfo, error) {
 | 
						|
	f, err := fs.Fs.Stat(name)
 | 
						|
	if err == nil {
 | 
						|
		if !f.IsDir() {
 | 
						|
			atomic.AddUint64(&fs.statCounter, 1)
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return f, err
 | 
						|
}
 | 
						|
 | 
						|
func (c *commandeer) copyStaticTo(sourceFs *filesystems.SourceFilesystem) (uint64, error) {
 | 
						|
	publishDir := c.hugo.PathSpec.PublishDir
 | 
						|
	// If root, remove the second '/'
 | 
						|
	if publishDir == "//" {
 | 
						|
		publishDir = helpers.FilePathSeparator
 | 
						|
	}
 | 
						|
 | 
						|
	if sourceFs.PublishFolder != "" {
 | 
						|
		publishDir = filepath.Join(publishDir, sourceFs.PublishFolder)
 | 
						|
	}
 | 
						|
 | 
						|
	fs := &countingStatFs{Fs: sourceFs.Fs}
 | 
						|
 | 
						|
	syncer := fsync.NewSyncer()
 | 
						|
	syncer.NoTimes = c.Cfg.GetBool("noTimes")
 | 
						|
	syncer.NoChmod = c.Cfg.GetBool("noChmod")
 | 
						|
	syncer.SrcFs = fs
 | 
						|
	syncer.DestFs = c.Fs.Destination
 | 
						|
	// Now that we are using a unionFs for the static directories
 | 
						|
	// We can effectively clean the publishDir on initial sync
 | 
						|
	syncer.Delete = c.Cfg.GetBool("cleanDestinationDir")
 | 
						|
 | 
						|
	if syncer.Delete {
 | 
						|
		c.logger.INFO.Println("removing all files from destination that don't exist in static dirs")
 | 
						|
 | 
						|
		syncer.DeleteFilter = func(f os.FileInfo) bool {
 | 
						|
			return f.IsDir() && strings.HasPrefix(f.Name(), ".")
 | 
						|
		}
 | 
						|
	}
 | 
						|
	c.logger.INFO.Println("syncing static files to", publishDir)
 | 
						|
 | 
						|
	// because we are using a baseFs (to get the union right).
 | 
						|
	// set sync src to root
 | 
						|
	err := syncer.Sync(publishDir, helpers.FilePathSeparator)
 | 
						|
	if err != nil {
 | 
						|
		return 0, err
 | 
						|
	}
 | 
						|
 | 
						|
	// Sync runs Stat 3 times for every source file (which sounds much)
 | 
						|
	numFiles := fs.statCounter / 3
 | 
						|
 | 
						|
	return numFiles, err
 | 
						|
}
 | 
						|
 | 
						|
func (c *commandeer) firstPathSpec() *helpers.PathSpec {
 | 
						|
	return c.hugo.Sites[0].PathSpec
 | 
						|
}
 | 
						|
 | 
						|
func (c *commandeer) timeTrack(start time.Time, name string) {
 | 
						|
	if c.h.quiet {
 | 
						|
		return
 | 
						|
	}
 | 
						|
	elapsed := time.Since(start)
 | 
						|
	c.logger.FEEDBACK.Printf("%s in %v ms", name, int(1000*elapsed.Seconds()))
 | 
						|
}
 | 
						|
 | 
						|
// getDirList provides NewWatcher() with a list of directories to watch for changes.
 | 
						|
func (c *commandeer) getDirList() ([]string, error) {
 | 
						|
	var a []string
 | 
						|
 | 
						|
	// To handle nested symlinked content dirs
 | 
						|
	var seen = make(map[string]bool)
 | 
						|
	var nested []string
 | 
						|
 | 
						|
	newWalker := func(allowSymbolicDirs bool) func(path string, fi os.FileInfo, err error) error {
 | 
						|
		return func(path string, fi os.FileInfo, err error) error {
 | 
						|
			if err != nil {
 | 
						|
				if os.IsNotExist(err) {
 | 
						|
					return nil
 | 
						|
				}
 | 
						|
 | 
						|
				c.logger.ERROR.Println("Walker: ", err)
 | 
						|
				return nil
 | 
						|
			}
 | 
						|
 | 
						|
			// Skip .git directories.
 | 
						|
			// Related to https://github.com/gohugoio/hugo/issues/3468.
 | 
						|
			if fi.Name() == ".git" {
 | 
						|
				return nil
 | 
						|
			}
 | 
						|
 | 
						|
			if fi.Mode()&os.ModeSymlink == os.ModeSymlink {
 | 
						|
				link, err := filepath.EvalSymlinks(path)
 | 
						|
				if err != nil {
 | 
						|
					c.logger.ERROR.Printf("Cannot read symbolic link '%s', error was: %s", path, err)
 | 
						|
					return nil
 | 
						|
				}
 | 
						|
				linkfi, err := helpers.LstatIfPossible(c.Fs.Source, link)
 | 
						|
				if err != nil {
 | 
						|
					c.logger.ERROR.Printf("Cannot stat %q: %s", link, err)
 | 
						|
					return nil
 | 
						|
				}
 | 
						|
				if !allowSymbolicDirs && !linkfi.Mode().IsRegular() {
 | 
						|
					c.logger.ERROR.Printf("Symbolic links for directories not supported, skipping %q", path)
 | 
						|
					return nil
 | 
						|
				}
 | 
						|
 | 
						|
				if allowSymbolicDirs && linkfi.IsDir() {
 | 
						|
					// afero.Walk will not walk symbolic links, so wee need to do it.
 | 
						|
					if !seen[path] {
 | 
						|
						seen[path] = true
 | 
						|
						nested = append(nested, path)
 | 
						|
					}
 | 
						|
					return nil
 | 
						|
				}
 | 
						|
 | 
						|
				fi = linkfi
 | 
						|
			}
 | 
						|
 | 
						|
			if fi.IsDir() {
 | 
						|
				if fi.Name() == ".git" ||
 | 
						|
					fi.Name() == "node_modules" || fi.Name() == "bower_components" {
 | 
						|
					return filepath.SkipDir
 | 
						|
				}
 | 
						|
				a = append(a, path)
 | 
						|
			}
 | 
						|
			return nil
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	symLinkWalker := newWalker(true)
 | 
						|
	regularWalker := newWalker(false)
 | 
						|
 | 
						|
	// SymbolicWalk will log anny ERRORs
 | 
						|
	// Also note that the Dirnames fetched below will contain any relevant theme
 | 
						|
	// directories.
 | 
						|
	for _, contentDir := range c.hugo.PathSpec.BaseFs.Content.Dirnames {
 | 
						|
		_ = helpers.SymbolicWalk(c.Fs.Source, contentDir, symLinkWalker)
 | 
						|
	}
 | 
						|
 | 
						|
	for _, staticDir := range c.hugo.PathSpec.BaseFs.Data.Dirnames {
 | 
						|
		_ = helpers.SymbolicWalk(c.Fs.Source, staticDir, regularWalker)
 | 
						|
	}
 | 
						|
 | 
						|
	for _, staticDir := range c.hugo.PathSpec.BaseFs.I18n.Dirnames {
 | 
						|
		_ = helpers.SymbolicWalk(c.Fs.Source, staticDir, regularWalker)
 | 
						|
	}
 | 
						|
 | 
						|
	for _, staticDir := range c.hugo.PathSpec.BaseFs.Layouts.Dirnames {
 | 
						|
		_ = helpers.SymbolicWalk(c.Fs.Source, staticDir, regularWalker)
 | 
						|
	}
 | 
						|
 | 
						|
	for _, staticFilesystem := range c.hugo.PathSpec.BaseFs.Static {
 | 
						|
		for _, staticDir := range staticFilesystem.Dirnames {
 | 
						|
			_ = helpers.SymbolicWalk(c.Fs.Source, staticDir, regularWalker)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	for _, assetDir := range c.hugo.PathSpec.BaseFs.Assets.Dirnames {
 | 
						|
		_ = helpers.SymbolicWalk(c.Fs.Source, assetDir, regularWalker)
 | 
						|
	}
 | 
						|
 | 
						|
	if len(nested) > 0 {
 | 
						|
		for {
 | 
						|
 | 
						|
			toWalk := nested
 | 
						|
			nested = nested[:0]
 | 
						|
 | 
						|
			for _, d := range toWalk {
 | 
						|
				_ = helpers.SymbolicWalk(c.Fs.Source, d, symLinkWalker)
 | 
						|
			}
 | 
						|
 | 
						|
			if len(nested) == 0 {
 | 
						|
				break
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	a = helpers.UniqueStrings(a)
 | 
						|
	sort.Strings(a)
 | 
						|
 | 
						|
	return a, nil
 | 
						|
}
 | 
						|
 | 
						|
func (c *commandeer) buildSites() (err error) {
 | 
						|
	return c.hugo.Build(hugolib.BuildCfg{})
 | 
						|
}
 | 
						|
 | 
						|
func (c *commandeer) handleBuildErr(err error, msg string) {
 | 
						|
	c.buildErr = err
 | 
						|
 | 
						|
	c.logger.ERROR.Print(msg + ":\n\n")
 | 
						|
	c.logger.ERROR.Println(helpers.FirstUpper(err.Error()))
 | 
						|
	if !c.h.quiet && c.h.verbose {
 | 
						|
		herrors.PrintStackTrace(err)
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
func (c *commandeer) rebuildSites(events []fsnotify.Event) error {
 | 
						|
	defer c.timeTrack(time.Now(), "Total")
 | 
						|
 | 
						|
	c.buildErr = nil
 | 
						|
	visited := c.visitedURLs.PeekAllSet()
 | 
						|
	if c.fastRenderMode {
 | 
						|
 | 
						|
		// Make sure we always render the home pages
 | 
						|
		for _, l := range c.languages {
 | 
						|
			langPath := c.hugo.PathSpec.GetLangSubDir(l.Lang)
 | 
						|
			if langPath != "" {
 | 
						|
				langPath = langPath + "/"
 | 
						|
			}
 | 
						|
			home := c.hugo.PathSpec.PrependBasePath("/"+langPath, false)
 | 
						|
			visited[home] = true
 | 
						|
		}
 | 
						|
 | 
						|
	}
 | 
						|
	return c.hugo.Build(hugolib.BuildCfg{RecentlyVisited: visited}, events...)
 | 
						|
}
 | 
						|
 | 
						|
func (c *commandeer) partialReRender(urls ...string) error {
 | 
						|
	c.buildErr = nil
 | 
						|
	visited := make(map[string]bool)
 | 
						|
	for _, url := range urls {
 | 
						|
		visited[url] = true
 | 
						|
	}
 | 
						|
	return c.hugo.Build(hugolib.BuildCfg{RecentlyVisited: visited, PartialReRender: true})
 | 
						|
}
 | 
						|
 | 
						|
func (c *commandeer) fullRebuild() {
 | 
						|
	c.commandeerHugoState = &commandeerHugoState{}
 | 
						|
	err := c.loadConfig(true, true)
 | 
						|
	if err != nil {
 | 
						|
		// Set the processing on pause until the state is recovered.
 | 
						|
		c.paused = true
 | 
						|
		c.handleBuildErr(err, "Failed to reload config")
 | 
						|
 | 
						|
	} else {
 | 
						|
		c.paused = false
 | 
						|
	}
 | 
						|
 | 
						|
	if !c.paused {
 | 
						|
		err := c.buildSites()
 | 
						|
		if err != nil {
 | 
						|
			c.logger.ERROR.Println(err)
 | 
						|
		} else if !c.h.buildWatch && !c.Cfg.GetBool("disableLiveReload") {
 | 
						|
			livereload.ForceRefresh()
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// newWatcher creates a new watcher to watch filesystem events.
 | 
						|
func (c *commandeer) newWatcher(dirList ...string) (*watcher.Batcher, error) {
 | 
						|
	if runtime.GOOS == "darwin" {
 | 
						|
		tweakLimit()
 | 
						|
	}
 | 
						|
 | 
						|
	staticSyncer, err := newStaticSyncer(c)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	watcher, err := watcher.New(1 * time.Second)
 | 
						|
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	for _, d := range dirList {
 | 
						|
		if d != "" {
 | 
						|
			_ = watcher.Add(d)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	// Identifies changes to config (config.toml) files.
 | 
						|
	configSet := make(map[string]bool)
 | 
						|
 | 
						|
	c.logger.FEEDBACK.Println("Watching for config changes in", strings.Join(c.configFiles, ", "))
 | 
						|
	for _, configFile := range c.configFiles {
 | 
						|
		watcher.Add(configFile)
 | 
						|
		configSet[configFile] = true
 | 
						|
	}
 | 
						|
 | 
						|
	go func() {
 | 
						|
		for {
 | 
						|
			select {
 | 
						|
			case evs := <-watcher.Events:
 | 
						|
				c.handleEvents(watcher, staticSyncer, evs, configSet)
 | 
						|
				if c.showErrorInBrowser && c.errCount() > 0 {
 | 
						|
					// Need to reload browser to show the error
 | 
						|
					livereload.ForceRefresh()
 | 
						|
				}
 | 
						|
			case err := <-watcher.Errors:
 | 
						|
				if err != nil {
 | 
						|
					c.logger.ERROR.Println("Error while watching:", err)
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}()
 | 
						|
 | 
						|
	return watcher, nil
 | 
						|
}
 | 
						|
 | 
						|
func (c *commandeer) handleEvents(watcher *watcher.Batcher,
 | 
						|
	staticSyncer *staticSyncer,
 | 
						|
	evs []fsnotify.Event,
 | 
						|
	configSet map[string]bool) {
 | 
						|
 | 
						|
	for _, ev := range evs {
 | 
						|
		isConfig := configSet[ev.Name]
 | 
						|
		if !isConfig {
 | 
						|
			// It may be one of the /config folders
 | 
						|
			dirname := filepath.Dir(ev.Name)
 | 
						|
			if dirname != "." && configSet[dirname] {
 | 
						|
				isConfig = true
 | 
						|
			}
 | 
						|
 | 
						|
		}
 | 
						|
 | 
						|
		if isConfig {
 | 
						|
			if ev.Op&fsnotify.Chmod == fsnotify.Chmod {
 | 
						|
				continue
 | 
						|
			}
 | 
						|
			if ev.Op&fsnotify.Remove == fsnotify.Remove || ev.Op&fsnotify.Rename == fsnotify.Rename {
 | 
						|
				for _, configFile := range c.configFiles {
 | 
						|
					counter := 0
 | 
						|
					for watcher.Add(configFile) != nil {
 | 
						|
						counter++
 | 
						|
						if counter >= 100 {
 | 
						|
							break
 | 
						|
						}
 | 
						|
						time.Sleep(100 * time.Millisecond)
 | 
						|
					}
 | 
						|
				}
 | 
						|
			}
 | 
						|
			// Config file(s) changed. Need full rebuild.
 | 
						|
			c.fullRebuild()
 | 
						|
			break
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if c.paused {
 | 
						|
		// Wait for the server to get into a consistent state before
 | 
						|
		// we continue with processing.
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	if len(evs) > 50 {
 | 
						|
		// This is probably a mass edit of the content dir.
 | 
						|
		// Schedule a full rebuild for when it slows down.
 | 
						|
		c.debounce(c.fullRebuild)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	c.logger.INFO.Println("Received System Events:", evs)
 | 
						|
 | 
						|
	staticEvents := []fsnotify.Event{}
 | 
						|
	dynamicEvents := []fsnotify.Event{}
 | 
						|
 | 
						|
	// Special handling for symbolic links inside /content.
 | 
						|
	filtered := []fsnotify.Event{}
 | 
						|
	for _, ev := range evs {
 | 
						|
		// Check the most specific first, i.e. files.
 | 
						|
		contentMapped := c.hugo.ContentChanges.GetSymbolicLinkMappings(ev.Name)
 | 
						|
		if len(contentMapped) > 0 {
 | 
						|
			for _, mapped := range contentMapped {
 | 
						|
				filtered = append(filtered, fsnotify.Event{Name: mapped, Op: ev.Op})
 | 
						|
			}
 | 
						|
			continue
 | 
						|
		}
 | 
						|
 | 
						|
		// Check for any symbolic directory mapping.
 | 
						|
 | 
						|
		dir, name := filepath.Split(ev.Name)
 | 
						|
 | 
						|
		contentMapped = c.hugo.ContentChanges.GetSymbolicLinkMappings(dir)
 | 
						|
 | 
						|
		if len(contentMapped) == 0 {
 | 
						|
			filtered = append(filtered, ev)
 | 
						|
			continue
 | 
						|
		}
 | 
						|
 | 
						|
		for _, mapped := range contentMapped {
 | 
						|
			mappedFilename := filepath.Join(mapped, name)
 | 
						|
			filtered = append(filtered, fsnotify.Event{Name: mappedFilename, Op: ev.Op})
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	evs = filtered
 | 
						|
 | 
						|
	for _, ev := range evs {
 | 
						|
		ext := filepath.Ext(ev.Name)
 | 
						|
		baseName := filepath.Base(ev.Name)
 | 
						|
		istemp := strings.HasSuffix(ext, "~") ||
 | 
						|
			(ext == ".swp") || // vim
 | 
						|
			(ext == ".swx") || // vim
 | 
						|
			(ext == ".tmp") || // generic temp file
 | 
						|
			(ext == ".DS_Store") || // OSX Thumbnail
 | 
						|
			baseName == "4913" || // vim
 | 
						|
			strings.HasPrefix(ext, ".goutputstream") || // gnome
 | 
						|
			strings.HasSuffix(ext, "jb_old___") || // intelliJ
 | 
						|
			strings.HasSuffix(ext, "jb_tmp___") || // intelliJ
 | 
						|
			strings.HasSuffix(ext, "jb_bak___") || // intelliJ
 | 
						|
			strings.HasPrefix(ext, ".sb-") || // byword
 | 
						|
			strings.HasPrefix(baseName, ".#") || // emacs
 | 
						|
			strings.HasPrefix(baseName, "#") // emacs
 | 
						|
		if istemp {
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		if c.hugo.Deps.SourceSpec.IgnoreFile(ev.Name) {
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		// Sometimes during rm -rf operations a '"": REMOVE' is triggered. Just ignore these
 | 
						|
		if ev.Name == "" {
 | 
						|
			continue
 | 
						|
		}
 | 
						|
 | 
						|
		// Write and rename operations are often followed by CHMOD.
 | 
						|
		// There may be valid use cases for rebuilding the site on CHMOD,
 | 
						|
		// but that will require more complex logic than this simple conditional.
 | 
						|
		// On OS X this seems to be related to Spotlight, see:
 | 
						|
		// https://github.com/go-fsnotify/fsnotify/issues/15
 | 
						|
		// A workaround is to put your site(s) on the Spotlight exception list,
 | 
						|
		// but that may be a little mysterious for most end users.
 | 
						|
		// So, for now, we skip reload on CHMOD.
 | 
						|
		// We do have to check for WRITE though. On slower laptops a Chmod
 | 
						|
		// could be aggregated with other important events, and we still want
 | 
						|
		// to rebuild on those
 | 
						|
		if ev.Op&(fsnotify.Chmod|fsnotify.Write|fsnotify.Create) == fsnotify.Chmod {
 | 
						|
			continue
 | 
						|
		}
 | 
						|
 | 
						|
		walkAdder := func(path string, f os.FileInfo, err error) error {
 | 
						|
			if f.IsDir() {
 | 
						|
				c.logger.FEEDBACK.Println("adding created directory to watchlist", path)
 | 
						|
				if err := watcher.Add(path); err != nil {
 | 
						|
					return err
 | 
						|
				}
 | 
						|
			} else if !staticSyncer.isStatic(path) {
 | 
						|
				// Hugo's rebuilding logic is entirely file based. When you drop a new folder into
 | 
						|
				// /content on OSX, the above logic will handle future watching of those files,
 | 
						|
				// but the initial CREATE is lost.
 | 
						|
				dynamicEvents = append(dynamicEvents, fsnotify.Event{Name: path, Op: fsnotify.Create})
 | 
						|
			}
 | 
						|
			return nil
 | 
						|
		}
 | 
						|
 | 
						|
		// recursively add new directories to watch list
 | 
						|
		// When mkdir -p is used, only the top directory triggers an event (at least on OSX)
 | 
						|
		if ev.Op&fsnotify.Create == fsnotify.Create {
 | 
						|
			if s, err := c.Fs.Source.Stat(ev.Name); err == nil && s.Mode().IsDir() {
 | 
						|
				_ = helpers.SymbolicWalk(c.Fs.Source, ev.Name, walkAdder)
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		if staticSyncer.isStatic(ev.Name) {
 | 
						|
			staticEvents = append(staticEvents, ev)
 | 
						|
		} else {
 | 
						|
			dynamicEvents = append(dynamicEvents, ev)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if len(staticEvents) > 0 {
 | 
						|
		c.logger.FEEDBACK.Println("\nStatic file changes detected")
 | 
						|
		const layout = "2006-01-02 15:04:05.000 -0700"
 | 
						|
		c.logger.FEEDBACK.Println(time.Now().Format(layout))
 | 
						|
 | 
						|
		if c.Cfg.GetBool("forceSyncStatic") {
 | 
						|
			c.logger.FEEDBACK.Printf("Syncing all static files\n")
 | 
						|
			_, err := c.copyStatic()
 | 
						|
			if err != nil {
 | 
						|
				c.logger.ERROR.Println("Error copying static files to publish dir:", err)
 | 
						|
				return
 | 
						|
			}
 | 
						|
		} else {
 | 
						|
			if err := staticSyncer.syncsStaticEvents(staticEvents); err != nil {
 | 
						|
				c.logger.ERROR.Println("Error syncing static files to publish dir:", err)
 | 
						|
				return
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		if !c.h.buildWatch && !c.Cfg.GetBool("disableLiveReload") {
 | 
						|
			// Will block forever trying to write to a channel that nobody is reading if livereload isn't initialized
 | 
						|
 | 
						|
			// force refresh when more than one file
 | 
						|
			if len(staticEvents) == 1 {
 | 
						|
				ev := staticEvents[0]
 | 
						|
				path := c.hugo.BaseFs.SourceFilesystems.MakeStaticPathRelative(ev.Name)
 | 
						|
				path = c.firstPathSpec().RelURL(helpers.ToSlashTrimLeading(path), false)
 | 
						|
				livereload.RefreshPath(path)
 | 
						|
			} else {
 | 
						|
				livereload.ForceRefresh()
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if len(dynamicEvents) > 0 {
 | 
						|
		partitionedEvents := partitionDynamicEvents(
 | 
						|
			c.firstPathSpec().BaseFs.SourceFilesystems,
 | 
						|
			dynamicEvents)
 | 
						|
 | 
						|
		doLiveReload := !c.h.buildWatch && !c.Cfg.GetBool("disableLiveReload")
 | 
						|
		onePageName := pickOneWriteOrCreatePath(partitionedEvents.ContentEvents)
 | 
						|
 | 
						|
		c.logger.FEEDBACK.Println("\nChange detected, rebuilding site")
 | 
						|
		const layout = "2006-01-02 15:04:05.000 -0700"
 | 
						|
		c.logger.FEEDBACK.Println(time.Now().Format(layout))
 | 
						|
 | 
						|
		c.changeDetector.PrepareNew()
 | 
						|
		if err := c.rebuildSites(dynamicEvents); err != nil {
 | 
						|
			c.handleBuildErr(err, "Rebuild failed")
 | 
						|
		}
 | 
						|
 | 
						|
		if doLiveReload {
 | 
						|
			if len(partitionedEvents.ContentEvents) == 0 && len(partitionedEvents.AssetEvents) > 0 {
 | 
						|
				changed := c.changeDetector.changed()
 | 
						|
				if c.changeDetector != nil && len(changed) == 0 {
 | 
						|
					// Nothing has changed.
 | 
						|
					return
 | 
						|
				} else if len(changed) == 1 {
 | 
						|
					pathToRefresh := c.firstPathSpec().RelURL(helpers.ToSlashTrimLeading(changed[0]), false)
 | 
						|
					livereload.RefreshPath(pathToRefresh)
 | 
						|
				} else {
 | 
						|
					livereload.ForceRefresh()
 | 
						|
				}
 | 
						|
			}
 | 
						|
 | 
						|
			if len(partitionedEvents.ContentEvents) > 0 {
 | 
						|
 | 
						|
				navigate := c.Cfg.GetBool("navigateToChanged")
 | 
						|
				// We have fetched the same page above, but it may have
 | 
						|
				// changed.
 | 
						|
				var p page.Page
 | 
						|
 | 
						|
				if navigate {
 | 
						|
					if onePageName != "" {
 | 
						|
						p = c.hugo.GetContentPage(onePageName)
 | 
						|
					}
 | 
						|
				}
 | 
						|
 | 
						|
				if p != nil {
 | 
						|
					livereload.NavigateToPathForPort(p.RelPermalink(), p.Site().ServerPort())
 | 
						|
				} else {
 | 
						|
					livereload.ForceRefresh()
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
// dynamicEvents contains events that is considered dynamic, as in "not static".
 | 
						|
// Both of these categories will trigger a new build, but the asset events
 | 
						|
// does not fit into the "navigate to changed" logic.
 | 
						|
type dynamicEvents struct {
 | 
						|
	ContentEvents []fsnotify.Event
 | 
						|
	AssetEvents   []fsnotify.Event
 | 
						|
}
 | 
						|
 | 
						|
func partitionDynamicEvents(sourceFs *filesystems.SourceFilesystems, events []fsnotify.Event) (de dynamicEvents) {
 | 
						|
	for _, e := range events {
 | 
						|
		if sourceFs.IsAsset(e.Name) {
 | 
						|
			de.AssetEvents = append(de.AssetEvents, e)
 | 
						|
		} else {
 | 
						|
			de.ContentEvents = append(de.ContentEvents, e)
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return
 | 
						|
 | 
						|
}
 | 
						|
 | 
						|
func pickOneWriteOrCreatePath(events []fsnotify.Event) string {
 | 
						|
	name := ""
 | 
						|
 | 
						|
	// Some editors (for example notepad.exe on Windows) triggers a change
 | 
						|
	// both for directory and file. So we pick the longest path, which should
 | 
						|
	// be the file itself.
 | 
						|
	for _, ev := range events {
 | 
						|
		if (ev.Op&fsnotify.Write == fsnotify.Write || ev.Op&fsnotify.Create == fsnotify.Create) && len(ev.Name) > len(name) {
 | 
						|
			name = ev.Name
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return name
 | 
						|
}
 | 
						|
 | 
						|
// isThemeVsHugoVersionMismatch returns whether the current Hugo version is
 | 
						|
// less than any of the themes' min_version.
 | 
						|
func (c *commandeer) isThemeVsHugoVersionMismatch(fs afero.Fs) (dir string, mismatch bool, requiredMinVersion string) {
 | 
						|
	if !c.hugo.PathSpec.ThemeSet() {
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	for _, absThemeDir := range c.hugo.BaseFs.AbsThemeDirs {
 | 
						|
 | 
						|
		path := filepath.Join(absThemeDir, "theme.toml")
 | 
						|
 | 
						|
		exists, err := helpers.Exists(path, fs)
 | 
						|
 | 
						|
		if err != nil || !exists {
 | 
						|
			continue
 | 
						|
		}
 | 
						|
 | 
						|
		b, err := afero.ReadFile(fs, path)
 | 
						|
		if err != nil {
 | 
						|
			continue
 | 
						|
		}
 | 
						|
 | 
						|
		tomlMeta, err := metadecoders.Default.UnmarshalToMap(b, metadecoders.TOML)
 | 
						|
		if err != nil {
 | 
						|
			continue
 | 
						|
		}
 | 
						|
 | 
						|
		if minVersion, ok := tomlMeta["min_version"]; ok {
 | 
						|
			if hugo.CompareVersion(minVersion) > 0 {
 | 
						|
				return absThemeDir, true, fmt.Sprint(minVersion)
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
	}
 | 
						|
 | 
						|
	return
 | 
						|
}
 |