diff options
Diffstat (limited to 'commands/hugo.go')
-rw-r--r-- | commands/hugo.go | 1011 |
1 files changed, 1011 insertions, 0 deletions
diff --git a/commands/hugo.go b/commands/hugo.go new file mode 100644 index 000000000..3f1697ea9 --- /dev/null +++ b/commands/hugo.go @@ -0,0 +1,1011 @@ +// Copyright 2018 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" + "sort" + "sync/atomic" + "syscall" + + "github.com/gohugoio/hugo/hugolib/filesystems" + + "golang.org/x/sync/errgroup" + + "log" + "os" + "path/filepath" + "runtime" + "strings" + "time" + + "github.com/gohugoio/hugo/config" + + "github.com/gohugoio/hugo/parser" + 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(jww.LogCountForLevelsGreaterThanorEqualTo(jww.LevelError)) + 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) (*jww.Notepad, error) { + var ( + logHandle = ioutil.Discard + logThreshold = jww.LevelWarn + logFile = cfg.GetString("logFile") + outHandle = os.Stdout + stdoutThreshold = jww.LevelError + ) + + 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 + } + } + + // The global logger is used in some few cases. + jww.SetLogOutput(logHandle) + jww.SetLogThreshold(logThreshold) + jww.SetStdoutThreshold(stdoutThreshold) + helpers.InitLoggers() + + return jww.NewNotepad(stdoutThreshold, logThreshold, outHandle, logHandle, "", log.Ldate|log.Ltime), 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", + } + + // 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) + +} + +var deprecatedFlags = map[string]bool{ + strings.ToLower("uglyURLs"): true, + strings.ToLower("pluralizeListTitles"): true, + strings.ToLower("preserveTaxonomyNames"): true, + strings.ToLower("canonifyURLs"): true, +} + +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) { + if _, deprecated := deprecatedFlags[strings.ToLower(key)]; deprecated { + msg := fmt.Sprintf(`Set "%s = true" in your config.toml. +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)) + // Remove in Hugo 0.38 + helpers.Deprecated("hugo", "--"+key+" flag", msg, true) + } + 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 (c *commandeer) fullBuild() error { + var ( + g errgroup.Group + langCount map[string]uint64 + ) + + if !c.h.quiet { + fmt.Print(hideCursor + "Building sites … ") + defer func() { + fmt.Print(showCursor + clearLine) + }() + } + + copyStaticFunc := func() error { + cnt, err := c.copyStatic() + if err != nil { + if !os.IsNotExist(err) { + return fmt.Errorf("Error copying static files: %s", err) + } + c.Logger.WARN.Println("No Static directory found") + } + langCount = cnt + langCount = cnt + return nil + } + buildSitesFunc := func() error { + if err := c.buildSites(); err != nil { + return fmt.Errorf("Error building site: %s", err) + } + 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) build() error { + defer c.timeTrack(time.Now(), "Total") + + 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 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) + signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) + + <-sigs + } + + return nil +} + +func (c *commandeer) serverBuild() error { + defer c.timeTrack(time.Now(), "Total") + + 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.WARN.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) + + var err error + + // 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) resetAndBuildSites() (err error) { + if !c.h.quiet { + c.Logger.FEEDBACK.Println("Started building sites ...") + } + return c.hugo.Build(hugolib.BuildCfg{ResetState: true}) +} + +func (c *commandeer) buildSites() (err error) { + return c.hugo.Build(hugolib.BuildCfg{}) +} + +func (c *commandeer) rebuildSites(events []fsnotify.Event) error { + defer c.timeTrack(time.Now(), "Total") + + visited := c.visitedURLs.PeekAllSet() + doLiveReload := !c.h.buildWatch && !c.Cfg.GetBool("disableLiveReload") + if doLiveReload && !c.Cfg.GetBool("disableFastRender") { + + // 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) + visited[home] = true + } + + } + return c.hugo.Build(hugolib.BuildCfg{RecentlyVisited: visited}, events...) +} + +func (c *commandeer) fullRebuild() { + c.commandeerHugoState = &commandeerHugoState{} + err := c.loadConfig(true, true) + if err != nil { + jww.ERROR.Println("Failed to reload config:", err) + // Set the processing on pause until the state is recovered. + c.paused = true + } else { + c.paused = false + } + + if !c.paused { + if err := c.buildSites(); err != nil { + jww.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) + + for _, configFile := range c.configFiles { + c.Logger.FEEDBACK.Println("Watching for config changes in", configFile) + watcher.Add(configFile) + configSet[configFile] = true + } + + go func() { + for { + select { + case evs := <-watcher.Events: + for _, ev := range evs { + if configSet[ev.Name] { + if ev.Op&fsnotify.Chmod == fsnotify.Chmod { + continue + } + if ev.Op&fsnotify.Remove == fsnotify.Remove { + for _, configFile := range c.configFiles { + counter := 0 + for watcher.Add(configFile) != nil { + counter++ + if counter >= 100 { + break + } + time.Sleep(100 * time.Millisecond) + } + } + } + // Config file 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. + continue + } + + 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) + continue + } + + 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 + } + // 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 { + stopOnErr(c.Logger, err, "Error copying static files to publish dir") + } + } else { + if err := staticSyncer.syncsStaticEvents(staticEvents); err != nil { + c.Logger.ERROR.Println(err) + continue + } + } + + 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.Logger.ERROR.Println("Failed to rebuild site:", err) + } + + 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. + continue + } 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 *hugolib.Page + + if navigate { + if onePageName != "" { + p = c.hugo.GetContentPage(onePageName) + } + } + + if p != nil { + livereload.NavigateToPathForPort(p.RelPermalink(), p.Site.ServerPort()) + } else { + livereload.ForceRefresh() + } + } + } + } + case err := <-watcher.Errors: + if err != nil { + c.Logger.ERROR.Println(err) + } + } + } + }() + + return watcher, nil +} + +// 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) + + tomlMeta, err := parser.HandleTOMLMetaData(b) + + if err != nil { + continue + } + + if minVersion, ok := tomlMeta["min_version"]; ok { |