hugo/commands/hugo.go
Bjørn Erik Pedersen dea71670c0
Add Hugo Piper with SCSS support and much more
Before this commit, you would have to use page bundles to do image processing etc. in Hugo.

This commit adds

* A new `/assets` top-level project or theme dir (configurable via `assetDir`)
* A new template func, `resources.Get` which can be used to "get a resource" that can be further processed.

This means that you can now do this in your templates (or shortcodes):

```bash
{{ $sunset := (resources.Get "images/sunset.jpg").Fill "300x200" }}
```

This also adds a new `extended` build tag that enables powerful SCSS/SASS support with source maps. To compile this from source, you will also need a C compiler installed:

```
HUGO_BUILD_TAGS=extended mage install
```

Note that you can use output of the SCSS processing later in a non-SCSSS-enabled Hugo.

The `SCSS` processor is a _Resource transformation step_ and it can be chained with the many others in a pipeline:

```bash
{{ $css := resources.Get "styles.scss" | resources.ToCSS | resources.PostCSS | resources.Minify | resources.Fingerprint }}
<link rel="stylesheet" href="{{ $styles.RelPermalink }}" integrity="{{ $styles.Data.Digest }}" media="screen">
```

The transformation funcs above have aliases, so it can be shortened to:

```bash
{{ $css := resources.Get "styles.scss" | toCSS | postCSS | minify | fingerprint }}
<link rel="stylesheet" href="{{ $styles.RelPermalink }}" integrity="{{ $styles.Data.Digest }}" media="screen">
```

A quick tip would be to avoid the fingerprinting part, and possibly also the not-superfast `postCSS` when you're doing development, as it allows Hugo to be smarter about the rebuilding.

Documentation will follow, but have a look at the demo repo in https://github.com/bep/hugo-sass-test

New functions to create `Resource` objects:

* `resources.Get` (see above)
* `resources.FromString`: Create a Resource from a string.

New `Resource` transformation funcs:

* `resources.ToCSS`: Compile `SCSS` or `SASS` into `CSS`.
* `resources.PostCSS`: Process your CSS with PostCSS. Config file support (project or theme or passed as an option).
* `resources.Minify`: Currently supports `css`, `js`, `json`, `html`, `svg`, `xml`.
* `resources.Fingerprint`: Creates a fingerprinted version of the given Resource with Subresource Integrity..
* `resources.Concat`: Concatenates a list of Resource objects. Think of this as a poor man's bundler.
* `resources.ExecuteAsTemplate`: Parses and executes the given Resource and data context (e.g. .Site) as a Go template.

Fixes #4381
Fixes #4903
Fixes #4858
2018-07-06 11:46:12 +02:00

973 lines
25 KiB
Go

// 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/utils"
"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
}
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",
}
for _, key := range persFlagKeys {
setValueFromFlag(cmd.PersistentFlags(), key, cfg, "")
}
for _, key := range flagKeys {
setValueFromFlag(cmd.Flags(), key, cfg, "")
}
// Set some "config aliases"
setValueFromFlag(cmd.Flags(), "destination", cfg, "publishDir")
setValueFromFlag(cmd.Flags(), "i18n-warnings", cfg, "logI18nWarnings")
}
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) {
key = strings.TrimSpace(key)
if 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...)
utils.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{}
if err := c.loadConfig(true, true); err != nil {
jww.ERROR.Println("Failed to reload config:", err)
} else 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:
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 {
if configSet[ev.Name] {
if ev.Op&fsnotify.Chmod == fsnotify.Chmod {
continue
}
// Config file changed. Need full rebuild.
c.fullRebuild()
break
}
// 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 {
utils.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) (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 {
if helpers.CompareVersion(minVersion) > 0 {
return true, fmt.Sprint(minVersion)
}
}
}
return
}