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