diff options
author | Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com> | 2017-07-24 09:00:23 +0200 |
---|---|---|
committer | Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com> | 2017-12-27 18:44:47 +0100 |
commit | 3cdf19e9b7e46c57a9bb43ff02199177feb55768 (patch) | |
tree | d05e3dc15824c8eeef3e5455193d2d6328621f47 /commands/hugo.go | |
parent | 02f2735f68e1bb2e2c412698755d52c4d396f237 (diff) |
:sparkles: Implement Page bundling and image handling
This commit is not the smallest in Hugo's history.
Some hightlights include:
* Page bundles (for complete articles, keeping images and content together etc.).
* Bundled images can be processed in as many versions/sizes as you need with the three methods `Resize`, `Fill` and `Fit`.
* Processed images are cached inside `resources/_gen/images` (default) in your project.
* Symbolic links (both files and dirs) are now allowed anywhere inside /content
* A new table based build summary
* The "Total in nn ms" now reports the total including the handling of the files inside /static. So if it now reports more than you're used to, it is just **more real** and probably faster than before (see below).
A site building benchmark run compared to `v0.31.1` shows that this should be slightly faster and use less memory:
```bash
▶ ./benchSite.sh "TOML,num_langs=.*,num_root_sections=5,num_pages=(500|1000),tags_per_page=5,shortcodes,render"
benchmark old ns/op new ns/op delta
BenchmarkSiteBuilding/TOML,num_langs=1,num_root_sections=5,num_pages=500,tags_per_page=5,shortcodes,render-4 101785785 78067944 -23.30%
BenchmarkSiteBuilding/TOML,num_langs=1,num_root_sections=5,num_pages=1000,tags_per_page=5,shortcodes,render-4 185481057 149159919 -19.58%
BenchmarkSiteBuilding/TOML,num_langs=3,num_root_sections=5,num_pages=500,tags_per_page=5,shortcodes,render-4 103149918 85679409 -16.94%
BenchmarkSiteBuilding/TOML,num_langs=3,num_root_sections=5,num_pages=1000,tags_per_page=5,shortcodes,render-4 203515478 169208775 -16.86%
benchmark old allocs new allocs delta
BenchmarkSiteBuilding/TOML,num_langs=1,num_root_sections=5,num_pages=500,tags_per_page=5,shortcodes,render-4 532464 391539 -26.47%
BenchmarkSiteBuilding/TOML,num_langs=1,num_root_sections=5,num_pages=1000,tags_per_page=5,shortcodes,render-4 1056549 772702 -26.87%
BenchmarkSiteBuilding/TOML,num_langs=3,num_root_sections=5,num_pages=500,tags_per_page=5,shortcodes,render-4 555974 406630 -26.86%
BenchmarkSiteBuilding/TOML,num_langs=3,num_root_sections=5,num_pages=1000,tags_per_page=5,shortcodes,render-4 1086545 789922 -27.30%
benchmark old bytes new bytes delta
BenchmarkSiteBuilding/TOML,num_langs=1,num_root_sections=5,num_pages=500,tags_per_page=5,shortcodes,render-4 53243246 43598155 -18.12%
BenchmarkSiteBuilding/TOML,num_langs=1,num_root_sections=5,num_pages=1000,tags_per_page=5,shortcodes,render-4 105811617 86087116 -18.64%
BenchmarkSiteBuilding/TOML,num_langs=3,num_root_sections=5,num_pages=500,tags_per_page=5,shortcodes,render-4 54558852 44545097 -18.35%
BenchmarkSiteBuilding/TOML,num_langs=3,num_root_sections=5,num_pages=1000,tags_per_page=5,shortcodes,render-4 106903858 86978413 -18.64%
```
Fixes #3651
Closes #3158
Fixes #1014
Closes #2021
Fixes #1240
Updates #3757
Diffstat (limited to 'commands/hugo.go')
-rw-r--r-- | commands/hugo.go | 462 |
1 files changed, 333 insertions, 129 deletions
diff --git a/commands/hugo.go b/commands/hugo.go index 7b50d0bb3..200a5e1c3 100644 --- a/commands/hugo.go +++ b/commands/hugo.go @@ -18,6 +18,10 @@ package commands import ( "fmt" "io/ioutil" + "sort" + "sync/atomic" + + "golang.org/x/sync/errgroup" "github.com/gohugoio/hugo/hugofs" @@ -58,6 +62,13 @@ import ( // provide a cleaner external API, but until then, this is it. var Hugo *hugolib.HugoSites +const ( + ansiEsc = "\u001B" + clearLine = "\r\033[K" + hideCursor = ansiEsc + "[?25l" + showCursor = ansiEsc + "[?25h" +) + // Reset resets Hugo ready for a new full build. This is mainly only useful // for benchmark testing etc. via the CLI commands. func Reset() error { @@ -116,18 +127,20 @@ built with love by spf13 and friends in Go. Complete documentation is available at http://gohugo.io/.`, RunE: func(cmd *cobra.Command, args []string) error { - cfg, err := InitializeConfig() - if err != nil { - return err + + cfgInit := func(c *commandeer) error { + if buildWatch { + c.Set("disableLiveReload", true) + } + return nil } - c, err := newCommandeer(cfg) + c, err := InitializeConfig(buildWatch, cfgInit) if err != nil { return err } if buildWatch { - cfg.Cfg.Set("disableLiveReload", true) c.watchConfig() } @@ -149,6 +162,7 @@ var ( ) var ( + gc bool baseURL string cacheDir string contentDir string @@ -201,6 +215,7 @@ func AddCommands() { genCmd.AddCommand(genmanCmd) genCmd.AddCommand(createGenDocsHelper().cmd) genCmd.AddCommand(createGenChromaStyles().cmd) + } // initHugoBuilderFlags initializes all common flags, typically used by the @@ -240,6 +255,7 @@ func initHugoBuildCommonFlags(cmd *cobra.Command) { cmd.Flags().Bool("canonifyURLs", false, "if true, all relative URLs will be canonicalized using baseURL") cmd.Flags().StringVarP(&baseURL, "baseURL", "b", "", "hostname (and path) to the root, e.g. http://spf13.com/") cmd.Flags().Bool("enableGitInfo", false, "add Git revision, date and author info to the pages") + cmd.Flags().BoolVar(&gc, "gc", false, "enable to run some cleanup tasks (remove unused cache files) after the build") cmd.Flags().BoolVar(&nitro.AnalysisOn, "stepAnalysis", false, "display memory and timing of different steps of the program") cmd.Flags().Bool("templateMetrics", false, "display metrics about template executions") @@ -285,7 +301,7 @@ func init() { } // InitializeConfig initializes a config file with sensible default configuration flags. -func InitializeConfig(subCmdVs ...*cobra.Command) (*deps.DepsCfg, error) { +func InitializeConfig(running bool, doWithCommandeer func(c *commandeer) error, subCmdVs ...*cobra.Command) (*commandeer, error) { var cfg *deps.DepsCfg = &deps.DepsCfg{} @@ -294,13 +310,13 @@ func InitializeConfig(subCmdVs ...*cobra.Command) (*deps.DepsCfg, error) { config, err := hugolib.LoadConfig(osFs, source, cfgFile) if err != nil { - return cfg, err + return nil, err } // Init file systems. This may be changed at a later point. cfg.Cfg = config - c, err := newCommandeer(cfg) + c, err := newCommandeer(cfg, running) if err != nil { return nil, err } @@ -309,23 +325,29 @@ func InitializeConfig(subCmdVs ...*cobra.Command) (*deps.DepsCfg, error) { c.initializeFlags(cmdV) } + if baseURL != "" { + config.Set("baseURL", baseURL) + } + + if doWithCommandeer != nil { + if err := doWithCommandeer(c); err != nil { + return nil, err + } + } + if len(disableKinds) > 0 { c.Set("disableKinds", disableKinds) } logger, err := createLogger(cfg.Cfg) if err != nil { - return cfg, err + return nil, err } cfg.Logger = logger config.Set("logI18nWarnings", logI18nWarnings) - if baseURL != "" { - config.Set("baseURL", baseURL) - } - if !config.GetBool("relativeURLs") && config.GetString("baseURL") == "" { cfg.Logger.ERROR.Println("No 'baseURL' set in configuration or as a flag. Features like page menus will not work without one.") } @@ -350,17 +372,6 @@ func InitializeConfig(subCmdVs ...*cobra.Command) (*deps.DepsCfg, error) { } config.Set("workingDir", dir) - fs := hugofs.NewFrom(osFs, config) - - // Hugo writes the output to memory instead of the disk. - // This is only used for benchmark testing. Cause the content is only visible - // in memory. - if renderToMemory { - fs.Destination = new(afero.MemMapFs) - // Rendering to memoryFS, publish to Root regardless of publishDir. - c.Set("publishDir", "/") - } - if contentDir != "" { config.Set("contentDir", contentDir) } @@ -373,6 +384,17 @@ func InitializeConfig(subCmdVs ...*cobra.Command) (*deps.DepsCfg, error) { config.Set("cacheDir", cacheDir) } + fs := hugofs.NewFrom(osFs, config) + + // Hugo writes the output to memory instead of the disk. + // This is only used for benchmark testing. Cause the content is only visible + // in memory. + if config.GetBool("renderToMemory") { + fs.Destination = new(afero.MemMapFs) + // Rendering to memoryFS, publish to Root regardless of publishDir. + config.Set("publishDir", "/") + } + cacheDir = config.GetString("cacheDir") if cacheDir != "" { if helpers.FilePathSeparator != cacheDir[len(cacheDir)-1:] { @@ -397,7 +419,7 @@ func InitializeConfig(subCmdVs ...*cobra.Command) (*deps.DepsCfg, error) { themeDir := c.PathSpec().GetThemeDir() if themeDir != "" { if _, err := cfg.Fs.Source.Stat(themeDir); os.IsNotExist(err) { - return cfg, newSystemError("Unable to find theme Directory:", themeDir) + return nil, newSystemError("Unable to find theme Directory:", themeDir) } } @@ -408,7 +430,7 @@ func InitializeConfig(subCmdVs ...*cobra.Command) (*deps.DepsCfg, error) { helpers.CurrentHugoVersion.ReleaseVersion(), minVersion) } - return cfg, nil + return c, nil } @@ -482,17 +504,17 @@ func (c *commandeer) initializeFlags(cmd *cobra.Command) { "templateMetricsHints", } - // Remove these in Hugo 0.23. + // Remove these in Hugo 0.33. if cmd.Flags().Changed("disable404") { - helpers.Deprecated("command line", "--disable404", "Use --disableKinds=404", false) + helpers.Deprecated("command line", "--disable404", "Use --disableKinds=404", true) } if cmd.Flags().Changed("disableRSS") { - helpers.Deprecated("command line", "--disableRSS", "Use --disableKinds=RSS", false) + helpers.Deprecated("command line", "--disableRSS", "Use --disableKinds=RSS", true) } if cmd.Flags().Changed("disableSitemap") { - helpers.Deprecated("command line", "--disableSitemap", "Use --disableKinds=sitemap", false) + helpers.Deprecated("command line", "--disableSitemap", "Use --disableKinds=sitemap", true) } for _, key := range persFlagKeys { @@ -525,16 +547,71 @@ func (c *commandeer) watchConfig() { }) } -func (c *commandeer) build(watches ...bool) error { - if err := c.copyStatic(); err != nil { - return fmt.Errorf("Error copying static files: %s", err) +func (c *commandeer) fullBuild(watches ...bool) error { + var ( + g errgroup.Group + langCount map[string]uint64 + ) + + if !quiet { + fmt.Print(hideCursor + "Building sites … ") + defer func() { + fmt.Print(showCursor + clearLine) + }() } - watch := false - if len(watches) > 0 && watches[0] { - watch = true + + g.Go(func() error { + cnt, err := c.copyStatic() + if err != nil { + return fmt.Errorf("Error copying static files: %s", err) + } + langCount = cnt + return nil + }) + + g.Go(func() error { + if err := c.buildSites(); err != nil { + return fmt.Errorf("Error building site: %s", err) + } + + return nil + }) + + if err := g.Wait(); err != nil { + return err } - if err := c.buildSites(buildWatch || watch); err != nil { - return fmt.Errorf("Error building site: %s", err) + + for _, s := range Hugo.Sites { + s.ProcessingStats.Static = langCount[s.Language.Lang] + } + + if gc { + count, err := Hugo.GC() + if err != nil { + return err + } + for _, s := range 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(watches ...bool) error { + defer c.timeTrack(time.Now(), "Total") + + if err := c.fullBuild(watches...); err != nil { + return err + } + + // TODO(bep) Feedback? + if !quiet { + fmt.Println() + Hugo.PrintProcessingStats(os.Stdout) + fmt.Println() } if buildWatch { @@ -550,62 +627,101 @@ func (c *commandeer) build(watches ...bool) error { return nil } -func (c *commandeer) copyStatic() error { +func (c *commandeer) copyStatic() (map[string]uint64, error) { return c.doWithPublishDirs(c.copyStaticTo) } -func (c *commandeer) doWithPublishDirs(f func(dirs *src.Dirs, publishDir string) error) error { - publishDir := c.PathSpec().AbsPathify(c.Cfg.GetString("publishDir")) + helpers.FilePathSeparator - // If root, remove the second '/' - if publishDir == "//" { - publishDir = helpers.FilePathSeparator - } - - languages := c.languages() +func (c *commandeer) createStaticDirsConfig() ([]*src.Dirs, error) { + var dirsConfig []*src.Dirs - if !languages.IsMultihost() { + if !c.languages.IsMultihost() { dirs, err := src.NewDirs(c.Fs, c.Cfg, c.DepsCfg.Logger) if err != nil { - return err + return nil, err + } + dirsConfig = append(dirsConfig, dirs) + } else { + for _, l := range c.languages { + dirs, err := src.NewDirs(c.Fs, l, c.DepsCfg.Logger) + if err != nil { + return nil, err + } + dirsConfig = append(dirsConfig, dirs) } - return f(dirs, publishDir) } - for _, l := range languages { - dir := filepath.Join(publishDir, l.Lang) - dirs, err := src.NewDirs(c.Fs, l, c.DepsCfg.Logger) + return dirsConfig, nil + +} + +func (c *commandeer) doWithPublishDirs(f func(dirs *src.Dirs, publishDir string) (uint64, error)) (map[string]uint64, error) { + + langCount := make(map[string]uint64) + + for _, dirs := range c.staticDirsConfig { + + cnt, err := f(dirs, c.pathSpec.PublishDir) if err != nil { - return err + return langCount, err } - if err := f(dirs, dir); err != nil { - return err + + if dirs.Language == nil { + // Not multihost + for _, l := range c.languages { + langCount[l.Lang] = cnt + } + } else { + langCount[dirs.Language.Lang] = cnt } + } - return nil + return langCount, nil +} + +type countingStatFs struct { + afero.Fs + statCounter uint64 } -func (c *commandeer) copyStaticTo(dirs *src.Dirs, publishDir string) error { +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(dirs *src.Dirs, publishDir string) (uint64, error) { // If root, remove the second '/' if publishDir == "//" { publishDir = helpers.FilePathSeparator } + if dirs.Language != nil { + // Multihost setup. + publishDir = filepath.Join(publishDir, dirs.Language.Lang) + } + staticSourceFs, err := dirs.CreateStaticFs() if err != nil { - return err + return 0, err } if staticSourceFs == nil { c.Logger.WARN.Println("No static directories found to sync") - return nil + return 0, nil } + fs := &countingStatFs{Fs: staticSourceFs} + syncer := fsync.NewSyncer() syncer.NoTimes = c.Cfg.GetBool("noTimes") syncer.NoChmod = c.Cfg.GetBool("noChmod") - syncer.SrcFs = staticSourceFs + 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 @@ -622,12 +738,30 @@ func (c *commandeer) copyStaticTo(dirs *src.Dirs, publishDir string) error { // because we are using a baseFs (to get the union right). // set sync src to root - return syncer.Sync(publishDir, helpers.FilePathSeparator) + 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) timeTrack(start time.Time, name string) { + 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 + dataDir := c.PathSpec().AbsPathify(c.Cfg.GetString("dataDir")) i18nDir := c.PathSpec().AbsPathify(c.Cfg.GetString("i18nDir")) staticSyncer, err := newStaticSyncer(c) @@ -638,86 +772,121 @@ func (c *commandeer) getDirList() ([]string, error) { layoutDir := c.PathSpec().GetLayoutDirPath() staticDirs := staticSyncer.d.AbsStaticDirs - walker := func(path string, fi os.FileInfo, err error) error { - if err != nil { - if path == dataDir && os.IsNotExist(err) { - c.Logger.WARN.Println("Skip dataDir:", err) - return nil - } + 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 path == dataDir && os.IsNotExist(err) { + c.Logger.WARN.Println("Skip dataDir:", err) + return nil + } - if path == i18nDir && os.IsNotExist(err) { - c.Logger.WARN.Println("Skip i18nDir:", err) - return nil - } + if path == i18nDir && os.IsNotExist(err) { + c.Logger.WARN.Println("Skip i18nDir:", err) + return nil + } - if path == layoutDir && os.IsNotExist(err) { - c.Logger.WARN.Println("Skip layoutDir:", err) - return nil - } + if path == layoutDir && os.IsNotExist(err) { + c.Logger.WARN.Println("Skip layoutDir:", err) + return nil + } - if os.IsNotExist(err) { - for _, staticDir := range staticDirs { - if path == staticDir && os.IsNotExist(err) { - c.Logger.WARN.Println("Skip staticDir:", err) + if os.IsNotExist(err) { + for _, staticDir := range staticDirs { + if path == staticDir && os.IsNotExist(err) { + c.Logger.WARN.Println("Skip staticDir:", err) + } } + // Ignore. + return nil } - // Ignore. - 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) + c.Logger.ERROR.Println("Walker: ", err) return nil } - linkfi, err := c.Fs.Source.Stat(link) - if err != nil { - c.Logger.ERROR.Printf("Cannot stat '%s', error was: %s", link, err) + + // Skip .git directories. + // Related to https://github.com/gohugoio/hugo/issues/3468. + if fi.Name() == ".git" { return nil } - if !linkfi.Mode().IsRegular() { - c.Logger.ERROR.Printf("Symbolic links for directories not supported, skipping '%s'", path) + + 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.LstatIfOs(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 } - return nil - } - if fi.IsDir() { - if fi.Name() == ".git" || - fi.Name() == "node_modules" || fi.Name() == "bower_components" { - return filepath.SkipDir + if fi.IsDir() { + if fi.Name() == ".git" || + fi.Name() == "node_modules" || fi.Name() == "bower_components" { + return filepath.SkipDir + } + a = append(a, path) } - a = append(a, path) + return nil } - return nil } + symLinkWalker := newWalker(true) + regularWalker := newWalker(false) + // SymbolicWalk will log anny ERRORs - _ = helpers.SymbolicWalk(c.Fs.Source, dataDir, walker) - _ = helpers.SymbolicWalk(c.Fs.Source, c.PathSpec().AbsPathify(c.Cfg.GetString("contentDir")), walker) - _ = helpers.SymbolicWalk(c.Fs.Source, i18nDir, walker) - _ = helpers.SymbolicWalk(c.Fs.Source, layoutDir, walker) + _ = helpers.SymbolicWalk(c.Fs.Source, dataDir, regularWalker) + _ = helpers.SymbolicWalk(c.Fs.Source, c.PathSpec().AbsPathify(c.Cfg.GetString("contentDir")), symLinkWalker) + _ = helpers.SymbolicWalk(c.Fs.Source, i18nDir, regularWalker) + _ = helpers.SymbolicWalk(c.Fs.Source, layoutDir, regularWalker) for _, staticDir := range staticDirs { - _ = helpers.SymbolicWalk(c.Fs.Source, staticDir, walker) + _ = helpers.SymbolicWalk(c.Fs.Source, staticDir, regularWalker) } if c.PathSpec().ThemeSet() { themesDir := c.PathSpec().GetThemeDir() - _ = helpers.SymbolicWalk(c.Fs.Source, filepath.Join(themesDir, "layouts"), walker) - _ = helpers.SymbolicWalk(c.Fs.Source, filepath.Join(themesDir, "i18n"), walker) - _ = helpers.SymbolicWalk(c.Fs.Source, filepath.Join(themesDir, "data"), walker) + _ = helpers.SymbolicWalk(c.Fs.Source, filepath.Join(themesDir, "layouts"), regularWalker) + _ = helpers.SymbolicWalk(c.Fs.Source, filepath.Join(themesDir, "i18n"), regularWalker) + _ = helpers.SymbolicWalk(c.Fs.Source, filepath.Join(themesDir, "data"), 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 } @@ -728,17 +897,17 @@ func (c *commandeer) recreateAndBuildSites(watching bool) (err error) { if !quiet { c.Logger.FEEDBACK.Println("Started building sites ...") } - return Hugo.Build(hugolib.BuildCfg{CreateSitesFromConfig: true, Watching: watching, PrintStats: !quiet}) + return Hugo.Build(hugolib.BuildCfg{CreateSitesFromConfig: true}) } -func (c *commandeer) resetAndBuildSites(watching bool) (err error) { +func (c *commandeer) resetAndBuildSites() (err error) { if err = c.initSites(); err != nil { return } if !quiet { c.Logger.FEEDBACK.Println("Started building sites ...") } - return Hugo.Build(hugolib.BuildCfg{ResetState: true, Watching: watching, PrintStats: !quiet}) + return Hugo.Build(hugolib.BuildCfg{ResetState: true}) } func (c *commandeer) initSites() error { @@ -755,17 +924,16 @@ func (c *commandeer) initSites() error { return nil } -func (c *commandeer) buildSites(watching bool) (err error) { +func (c *commandeer) buildSites() (err error) { if err := c.initSites(); err != nil { return err } - if !quiet { - c.Logger.FEEDBACK.Println("Started building sites ...") - } - return Hugo.Build(hugolib.BuildCfg{Watching: watching, PrintStats: !quiet}) + return Hugo.Build(hugolib.BuildCfg{}) } func (c *commandeer) rebuildSites(events []fsnotify.Event) error { + defer c.timeTrack(time.Now(), "Total") + if err := c.initSites(); err != nil { return err } @@ -776,7 +944,7 @@ func (c *commandeer) rebuildSites(events []fsnotify.Event) error { // Make sure we always render the home page visited[home] = true } - return Hugo.Build(hugolib.BuildCfg{PrintStats: !quiet, Watching: true, RecentlyVisited: visited}, events...) + return Hugo.Build(hugolib.BuildCfg{RecentlyVisited: visited}, events...) } // newWatcher creates a new watcher to watch filesystem events. @@ -818,6 +986,37 @@ func (c *commandeer) newWatcher(serve bool, dirList ...string) error { staticEvents := []fsnotify.Event{} dynamicEvents := []fsnotify.Event{} + // Special handling for symbolic links inside /content. + filtered := []fsnotify.Event{} + for _, ev := range evs { + // Check the most specific first, i.e. files. + contentMapped := 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 = 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) @@ -894,7 +1093,7 @@ func (c *commandeer) newWatcher(serve bool, dirList ...string) error { if c.Cfg.GetBool("forceSyncStatic") { c.Logger.FEEDBACK.Printf("Syncing all static files\n") - err := c.copyStatic() + _, err := c.copyStatic() if err != nil { utils.StopOnErr(c.Logger, err, "Error copying static files to publish dir") } @@ -932,8 +1131,9 @@ func (c *commandeer) newWatcher(serve bool, dirList ...string) error { } } + c.Logger.FEEDBACK.Println("\nChange detected, rebuilding site") - const layout = "2006-01-02 15:04 -0700" + const layout = "2006-01-02 15:04:05.000 -0700" c.Logger.FEEDBACK.Println(time.Now().Format(layout)) if err := c.rebuildSites(dynamicEvents); err != nil { @@ -950,6 +1150,7 @@ func (c *commandeer) newWatcher(serve bool, dirList ...string) error { if onePageName != "" { p = Hugo.GetContentPage(onePageName) } + } if p != nil { @@ -978,6 +1179,9 @@ func (c *commandeer) newWatcher(serve bool, dirList ...string) error { 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 |