github.com/shohhei1126/hugo@v0.42.2-0.20180623210752-3d5928889ad7/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) recreateAndBuildSites(watching bool) (err error) { 600 defer c.timeTrack(time.Now(), "Total") 601 if !c.h.quiet { 602 c.Logger.FEEDBACK.Println("Started building sites ...") 603 } 604 return c.hugo.Build(hugolib.BuildCfg{CreateSitesFromConfig: true}) 605 } 606 607 func (c *commandeer) resetAndBuildSites() (err error) { 608 if !c.h.quiet { 609 c.Logger.FEEDBACK.Println("Started building sites ...") 610 } 611 return c.hugo.Build(hugolib.BuildCfg{ResetState: true}) 612 } 613 614 func (c *commandeer) buildSites() (err error) { 615 return c.hugo.Build(hugolib.BuildCfg{}) 616 } 617 618 func (c *commandeer) rebuildSites(events []fsnotify.Event) error { 619 defer c.timeTrack(time.Now(), "Total") 620 621 visited := c.visitedURLs.PeekAllSet() 622 doLiveReload := !c.h.buildWatch && !c.Cfg.GetBool("disableLiveReload") 623 if doLiveReload && !c.Cfg.GetBool("disableFastRender") { 624 625 // Make sure we always render the home pages 626 for _, l := range c.languages { 627 langPath := c.hugo.PathSpec.GetLangSubDir(l.Lang) 628 if langPath != "" { 629 langPath = langPath + "/" 630 } 631 home := c.hugo.PathSpec.PrependBasePath("/" + langPath) 632 visited[home] = true 633 } 634 635 } 636 return c.hugo.Build(hugolib.BuildCfg{RecentlyVisited: visited}, events...) 637 } 638 639 func (c *commandeer) fullRebuild() { 640 if err := c.loadConfig(true, true); err != nil { 641 jww.ERROR.Println("Failed to reload config:", err) 642 } else if err := c.recreateAndBuildSites(true); err != nil { 643 jww.ERROR.Println(err) 644 } else if !c.h.buildWatch && !c.Cfg.GetBool("disableLiveReload") { 645 livereload.ForceRefresh() 646 } 647 } 648 649 // newWatcher creates a new watcher to watch filesystem events. 650 func (c *commandeer) newWatcher(dirList ...string) (*watcher.Batcher, error) { 651 if runtime.GOOS == "darwin" { 652 tweakLimit() 653 } 654 655 staticSyncer, err := newStaticSyncer(c) 656 if err != nil { 657 return nil, err 658 } 659 660 watcher, err := watcher.New(1 * time.Second) 661 662 if err != nil { 663 return nil, err 664 } 665 666 for _, d := range dirList { 667 if d != "" { 668 _ = watcher.Add(d) 669 } 670 } 671 672 // Identifies changes to config (config.toml) files. 673 configSet := make(map[string]bool) 674 675 for _, configFile := range c.configFiles { 676 c.Logger.FEEDBACK.Println("Watching for config changes in", configFile) 677 watcher.Add(configFile) 678 configSet[configFile] = true 679 } 680 681 go func() { 682 for { 683 select { 684 case evs := <-watcher.Events: 685 if len(evs) > 50 { 686 // This is probably a mass edit of the content dir. 687 // Schedule a full rebuild for when it slows down. 688 c.debounce(c.fullRebuild) 689 continue 690 } 691 692 c.Logger.INFO.Println("Received System Events:", evs) 693 694 staticEvents := []fsnotify.Event{} 695 dynamicEvents := []fsnotify.Event{} 696 697 // Special handling for symbolic links inside /content. 698 filtered := []fsnotify.Event{} 699 for _, ev := range evs { 700 if configSet[ev.Name] { 701 if ev.Op&fsnotify.Chmod == fsnotify.Chmod { 702 continue 703 } 704 // Config file changed. Need full rebuild. 705 c.fullRebuild() 706 break 707 } 708 709 // Check the most specific first, i.e. files. 710 contentMapped := c.hugo.ContentChanges.GetSymbolicLinkMappings(ev.Name) 711 if len(contentMapped) > 0 { 712 for _, mapped := range contentMapped { 713 filtered = append(filtered, fsnotify.Event{Name: mapped, Op: ev.Op}) 714 } 715 continue 716 } 717 718 // Check for any symbolic directory mapping. 719 720 dir, name := filepath.Split(ev.Name) 721 722 contentMapped = c.hugo.ContentChanges.GetSymbolicLinkMappings(dir) 723 724 if len(contentMapped) == 0 { 725 filtered = append(filtered, ev) 726 continue 727 } 728 729 for _, mapped := range contentMapped { 730 mappedFilename := filepath.Join(mapped, name) 731 filtered = append(filtered, fsnotify.Event{Name: mappedFilename, Op: ev.Op}) 732 } 733 } 734 735 evs = filtered 736 737 for _, ev := range evs { 738 ext := filepath.Ext(ev.Name) 739 baseName := filepath.Base(ev.Name) 740 istemp := strings.HasSuffix(ext, "~") || 741 (ext == ".swp") || // vim 742 (ext == ".swx") || // vim 743 (ext == ".tmp") || // generic temp file 744 (ext == ".DS_Store") || // OSX Thumbnail 745 baseName == "4913" || // vim 746 strings.HasPrefix(ext, ".goutputstream") || // gnome 747 strings.HasSuffix(ext, "jb_old___") || // intelliJ 748 strings.HasSuffix(ext, "jb_tmp___") || // intelliJ 749 strings.HasSuffix(ext, "jb_bak___") || // intelliJ 750 strings.HasPrefix(ext, ".sb-") || // byword 751 strings.HasPrefix(baseName, ".#") || // emacs 752 strings.HasPrefix(baseName, "#") // emacs 753 if istemp { 754 continue 755 } 756 // Sometimes during rm -rf operations a '"": REMOVE' is triggered. Just ignore these 757 if ev.Name == "" { 758 continue 759 } 760 761 // Write and rename operations are often followed by CHMOD. 762 // There may be valid use cases for rebuilding the site on CHMOD, 763 // but that will require more complex logic than this simple conditional. 764 // On OS X this seems to be related to Spotlight, see: 765 // https://github.com/go-fsnotify/fsnotify/issues/15 766 // A workaround is to put your site(s) on the Spotlight exception list, 767 // but that may be a little mysterious for most end users. 768 // So, for now, we skip reload on CHMOD. 769 // We do have to check for WRITE though. On slower laptops a Chmod 770 // could be aggregated with other important events, and we still want 771 // to rebuild on those 772 if ev.Op&(fsnotify.Chmod|fsnotify.Write|fsnotify.Create) == fsnotify.Chmod { 773 continue 774 } 775 776 walkAdder := func(path string, f os.FileInfo, err error) error { 777 if f.IsDir() { 778 c.Logger.FEEDBACK.Println("adding created directory to watchlist", path) 779 if err := watcher.Add(path); err != nil { 780 return err 781 } 782 } else if !staticSyncer.isStatic(path) { 783 // Hugo's rebuilding logic is entirely file based. When you drop a new folder into 784 // /content on OSX, the above logic will handle future watching of those files, 785 // but the initial CREATE is lost. 786 dynamicEvents = append(dynamicEvents, fsnotify.Event{Name: path, Op: fsnotify.Create}) 787 } 788 return nil 789 } 790 791 // recursively add new directories to watch list 792 // When mkdir -p is used, only the top directory triggers an event (at least on OSX) 793 if ev.Op&fsnotify.Create == fsnotify.Create { 794 if s, err := c.Fs.Source.Stat(ev.Name); err == nil && s.Mode().IsDir() { 795 _ = helpers.SymbolicWalk(c.Fs.Source, ev.Name, walkAdder) 796 } 797 } 798 799 if staticSyncer.isStatic(ev.Name) { 800 staticEvents = append(staticEvents, ev) 801 } else { 802 dynamicEvents = append(dynamicEvents, ev) 803 } 804 } 805 806 if len(staticEvents) > 0 { 807 c.Logger.FEEDBACK.Println("\nStatic file changes detected") 808 const layout = "2006-01-02 15:04:05.000 -0700" 809 c.Logger.FEEDBACK.Println(time.Now().Format(layout)) 810 811 if c.Cfg.GetBool("forceSyncStatic") { 812 c.Logger.FEEDBACK.Printf("Syncing all static files\n") 813 _, err := c.copyStatic() 814 if err != nil { 815 utils.StopOnErr(c.Logger, err, "Error copying static files to publish dir") 816 } 817 } else { 818 if err := staticSyncer.syncsStaticEvents(staticEvents); err != nil { 819 c.Logger.ERROR.Println(err) 820 continue 821 } 822 } 823 824 if !c.h.buildWatch && !c.Cfg.GetBool("disableLiveReload") { 825 // Will block forever trying to write to a channel that nobody is reading if livereload isn't initialized 826 827 // force refresh when more than one file 828 if len(staticEvents) > 0 { 829 for _, ev := range staticEvents { 830 831 path := c.hugo.BaseFs.SourceFilesystems.MakeStaticPathRelative(ev.Name) 832 livereload.RefreshPath(path) 833 } 834 835 } else { 836 livereload.ForceRefresh() 837 } 838 } 839 } 840 841 if len(dynamicEvents) > 0 { 842 doLiveReload := !c.h.buildWatch && !c.Cfg.GetBool("disableLiveReload") 843 onePageName := pickOneWriteOrCreatePath(dynamicEvents) 844 845 c.Logger.FEEDBACK.Println("\nChange detected, rebuilding site") 846 const layout = "2006-01-02 15:04:05.000 -0700" 847 c.Logger.FEEDBACK.Println(time.Now().Format(layout)) 848 849 if err := c.rebuildSites(dynamicEvents); err != nil { 850 c.Logger.ERROR.Println("Failed to rebuild site:", err) 851 } 852 853 if doLiveReload { 854 navigate := c.Cfg.GetBool("navigateToChanged") 855 // We have fetched the same page above, but it may have 856 // changed. 857 var p *hugolib.Page 858 859 if navigate { 860 if onePageName != "" { 861 p = c.hugo.GetContentPage(onePageName) 862 } 863 864 } 865 866 if p != nil { 867 livereload.NavigateToPathForPort(p.RelPermalink(), p.Site.ServerPort()) 868 } else { 869 livereload.ForceRefresh() 870 } 871 } 872 } 873 case err := <-watcher.Errors: 874 if err != nil { 875 c.Logger.ERROR.Println(err) 876 } 877 } 878 } 879 }() 880 881 return watcher, nil 882 } 883 884 func pickOneWriteOrCreatePath(events []fsnotify.Event) string { 885 name := "" 886 887 // Some editors (for example notepad.exe on Windows) triggers a change 888 // both for directory and file. So we pick the longest path, which should 889 // be the file itself. 890 for _, ev := range events { 891 if (ev.Op&fsnotify.Write == fsnotify.Write || ev.Op&fsnotify.Create == fsnotify.Create) && len(ev.Name) > len(name) { 892 name = ev.Name 893 } 894 } 895 896 return name 897 } 898 899 // isThemeVsHugoVersionMismatch returns whether the current Hugo version is 900 // less than any of the themes' min_version. 901 func (c *commandeer) isThemeVsHugoVersionMismatch(fs afero.Fs) (mismatch bool, requiredMinVersion string) { 902 if !c.hugo.PathSpec.ThemeSet() { 903 return 904 } 905 906 for _, absThemeDir := range c.hugo.BaseFs.AbsThemeDirs { 907 908 path := filepath.Join(absThemeDir, "theme.toml") 909 910 exists, err := helpers.Exists(path, fs) 911 912 if err != nil || !exists { 913 continue 914 } 915 916 b, err := afero.ReadFile(fs, path) 917 918 tomlMeta, err := parser.HandleTOMLMetaData(b) 919 920 if err != nil { 921 continue 922 } 923 924 if minVersion, ok := tomlMeta["min_version"]; ok { 925 if helpers.CompareVersion(minVersion) > 0 { 926 return true, fmt.Sprint(minVersion) 927 } 928 } 929 930 } 931 932 return 933 }