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