diff options
author | Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com> | 2023-01-04 18:24:36 +0100 |
---|---|---|
committer | Bjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com> | 2023-05-16 18:01:29 +0200 |
commit | 241b21b0fd34d91fccb2ce69874110dceae6f926 (patch) | |
tree | d4e0118eac7e9c42f065815447a70805f8d6ad3e /commands/hugobuilder.go | |
parent | 6aededf6b42011c3039f5f66487a89a8dd65e0e7 (diff) |
Create a struct with all of Hugo's config options
Primary motivation is documentation, but it will also hopefully simplify the code.
Also,
* Lower case the default output format names; this is in line with the custom ones (map keys) and how
it's treated all the places. This avoids doing `stringds.EqualFold` everywhere.
Closes #10896
Closes #10620
Diffstat (limited to 'commands/hugobuilder.go')
-rw-r--r-- | commands/hugobuilder.go | 993 |
1 files changed, 993 insertions, 0 deletions
diff --git a/commands/hugobuilder.go b/commands/hugobuilder.go new file mode 100644 index 000000000..7c6dbee35 --- /dev/null +++ b/commands/hugobuilder.go @@ -0,0 +1,993 @@ +// Copyright 2023 The Hugo Authors. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package commands + +import ( + "context" + "errors" + "fmt" + "os" + "path/filepath" + "runtime" + "runtime/pprof" + "runtime/trace" + "strings" + "sync" + "time" + + "github.com/bep/simplecobra" + "github.com/fsnotify/fsnotify" + "github.com/gohugoio/hugo/common/herrors" + "github.com/gohugoio/hugo/common/htime" + "github.com/gohugoio/hugo/common/hugo" + "github.com/gohugoio/hugo/common/maps" + "github.com/gohugoio/hugo/common/terminal" + "github.com/gohugoio/hugo/common/types" + "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/helpers" + "github.com/gohugoio/hugo/hugofs" + "github.com/gohugoio/hugo/hugolib" + "github.com/gohugoio/hugo/hugolib/filesystems" + "github.com/gohugoio/hugo/livereload" + "github.com/gohugoio/hugo/resources/page" + "github.com/gohugoio/hugo/watcher" + "github.com/spf13/fsync" + "golang.org/x/sync/errgroup" + "golang.org/x/sync/semaphore" +) + +type hugoBuilder struct { + r *rootCommand + + cunfMu sync.Mutex + conf_ *commonConfig + + // May be nil. + s *serverCommand + + // Currently only set when in "fast render mode". + changeDetector *fileChangeDetector + visitedURLs *types.EvictingStringQueue + + fullRebuildSem *semaphore.Weighted + debounce func(f func()) + + onConfigLoaded func(reloaded bool) error + + fastRenderMode bool + buildWatch bool + showErrorInBrowser bool + + errState hugoBuilderErrState +} + +func (c *hugoBuilder) conf() *commonConfig { + c.cunfMu.Lock() + defer c.cunfMu.Unlock() + return c.conf_ +} + +func (c *hugoBuilder) setConf(conf *commonConfig) { + c.cunfMu.Lock() + defer c.cunfMu.Unlock() + c.conf_ = conf +} + +type hugoBuilderErrState struct { + mu sync.Mutex + paused bool + builderr error + waserr bool +} + +func (e *hugoBuilderErrState) setPaused(p bool) { + e.mu.Lock() + defer e.mu.Unlock() + e.paused = p +} + +func (e *hugoBuilderErrState) isPaused() bool { + e.mu.Lock() + defer e.mu.Unlock() + return e.paused +} + +func (e *hugoBuilderErrState) setBuildErr(err error) { + e.mu.Lock() + defer e.mu.Unlock() + e.builderr = err +} + +func (e *hugoBuilderErrState) buildErr() error { + e.mu.Lock() + defer e.mu.Unlock() + return e.builderr +} + +func (e *hugoBuilderErrState) setWasErr(w bool) { + e.mu.Lock() + defer e.mu.Unlock() + e.waserr = w +} + +func (e *hugoBuilderErrState) wasErr() bool { + e.mu.Lock() + defer e.mu.Unlock() + return e.waserr +} + +func (c *hugoBuilder) errCount() int { + return int(c.r.logger.LogCounters().ErrorCounter.Count()) +} + +// getDirList provides NewWatcher() with a list of directories to watch for changes. +func (c *hugoBuilder) getDirList() ([]string, error) { + var filenames []string + + walkFn := func(path string, fi hugofs.FileMetaInfo, err error) error { + if err != nil { + c.r.logger.Errorln("walker: ", err) + return nil + } + + if fi.IsDir() { + if fi.Name() == ".git" || + fi.Name() == "node_modules" || fi.Name() == "bower_components" { + return filepath.SkipDir + } + + filenames = append(filenames, fi.Meta().Filename) + } + + return nil + } + + watchFiles := c.hugo().PathSpec.BaseFs.WatchDirs() + for _, fi := range watchFiles { + if !fi.IsDir() { + filenames = append(filenames, fi.Meta().Filename) + continue + } + + w := hugofs.NewWalkway(hugofs.WalkwayConfig{Logger: c.r.logger, Info: fi, WalkFn: walkFn}) + if err := w.Walk(); err != nil { + c.r.logger.Errorln("walker: ", err) + } + } + + filenames = helpers.UniqueStringsSorted(filenames) + + return filenames, nil +} + +func (c *hugoBuilder) initCPUProfile() (func(), error) { + if c.r.cpuprofile == "" { + return nil, nil + } + + f, err := os.Create(c.r.cpuprofile) + if err != nil { + return nil, fmt.Errorf("failed to create CPU profile: %w", err) + } + if err := pprof.StartCPUProfile(f); err != nil { + return nil, fmt.Errorf("failed to start CPU profile: %w", err) + } + return func() { + pprof.StopCPUProfile() + f.Close() + }, nil +} + +func (c *hugoBuilder) initMemProfile() { + if c.r.memprofile == "" { + return + } + + f, err := os.Create(c.r.memprofile) + if err != nil { + c.r.logger.Errorf("could not create memory profile: ", err) + } + defer f.Close() + runtime.GC() // get up-to-date statistics + if err := pprof.WriteHeapProfile(f); err != nil { + c.r.logger.Errorf("could not write memory profile: ", err) + } +} + +func (c *hugoBuilder) initMemTicker() func() { + memticker := time.NewTicker(5 * time.Second) + quit := make(chan struct{}) + printMem := func() { + var m runtime.MemStats + runtime.ReadMemStats(&m) + fmt.Printf("\n\nAlloc = %v\nTotalAlloc = %v\nSys = %v\nNumGC = %v\n\n", formatByteCount(m.Alloc), formatByteCount(m.TotalAlloc), formatByteCount(m.Sys), m.NumGC) + } + + go func() { + for { + select { + case <-memticker.C: + printMem() + case <-quit: + memticker.Stop() + printMem() + return + } + } + }() + + return func() { + close(quit) + } +} + +func (c *hugoBuilder) initMutexProfile() (func(), error) { + if c.r.mutexprofile == "" { + return nil, nil + } + + f, err := os.Create(c.r.mutexprofile) + if err != nil { + return nil, err + } + + runtime.SetMutexProfileFraction(1) + + return func() { + pprof.Lookup("mutex").WriteTo(f, 0) + f.Close() + }, nil +} + +func (c *hugoBuilder) initProfiling() (func(), error) { + stopCPUProf, err := c.initCPUProfile() + if err != nil { + return nil, err + } + + stopMutexProf, err := c.initMutexProfile() + if err != nil { + return nil, err + } + + stopTraceProf, err := c.initTraceProfile() + if err != nil { + return nil, err + } + + var stopMemTicker func() + if c.r.printm { + stopMemTicker = c.initMemTicker() + } + + return func() { + c.initMemProfile() + + if stopCPUProf != nil { + stopCPUProf() + } + if stopMutexProf != nil { + stopMutexProf() + } + + if stopTraceProf != nil { + stopTraceProf() + } + + if stopMemTicker != nil { + stopMemTicker() + } + }, nil +} + +func (c *hugoBuilder) initTraceProfile() (func(), error) { + if c.r.traceprofile == "" { + return nil, nil + } + + f, err := os.Create(c.r.traceprofile) + if err != nil { + return nil, fmt.Errorf("failed to create trace file: %w", err) + } + + if err := trace.Start(f); err != nil { + return nil, fmt.Errorf("failed to start trace: %w", err) + } + + return func() { + trace.Stop() + f.Close() + }, nil +} + +// newWatcher creates a new watcher to watch filesystem events. +func (c *hugoBuilder) newWatcher(pollIntervalStr string, dirList ...string) (*watcher.Batcher, error) { + staticSyncer := &staticSyncer{c: c} + + var pollInterval time.Duration + poll := pollIntervalStr != "" + if poll { + pollInterval, err := types.ToDurationE(pollIntervalStr) + if err != nil { + return nil, fmt.Errorf("invalid value for flag poll: %s", err) + } + c.r.logger.Printf("Use watcher with poll interval %v", pollInterval) + } + + if pollInterval == 0 { + pollInterval = 500 * time.Millisecond + } + + watcher, err := watcher.New(500*time.Millisecond, pollInterval, poll) + if err != nil { + return nil, err + } + + spec := c.hugo().Deps.SourceSpec + + for _, d := range dirList { + if d != "" { + if spec.IgnoreFile(d) { + continue + } + _ = watcher.Add(d) + } + } + + // Identifies changes to config (config.toml) files. + configSet := make(map[string]bool) + configFiles := c.conf().configs.LoadingInfo.ConfigFiles + + c.r.logger.Println("Watching for config changes in", strings.Join(configFiles, ", ")) + for _, configFile := range configFiles { + watcher.Add(configFile) + configSet[configFile] = true + } + + go func() { + for { + select { + case evs := <-watcher.Events: + unlock, err := c.hugo().LockBuild() + if err != nil { + c.r.logger.Errorln("Failed to acquire a build lock: %s", err) + return + } + c.handleEvents(watcher, staticSyncer, evs, configSet) + if c.showErrorInBrowser && c.errCount() > 0 { + // Need to reload browser to show the error + livereload.ForceRefresh() + } + unlock() + case err := <-watcher.Errors(): + if err != nil && !herrors.IsNotExist(err) { + c.r.logger.Errorln("Error while watching:", err) + } + } + } + }() + + return watcher, nil +} + +func (c *hugoBuilder) build() error { + stopProfiling, err := c.initProfiling() + if err != nil { + return err + } + + defer func() { + if stopProfiling != nil { + stopProfiling() + } + }() + + if err := c.fullBuild(false); err != nil { + return err + } + + if !c.r.quiet { + c.r.Println() + c.hugo().PrintProcessingStats(os.Stdout) + c.r.Println() + } + + return nil +} + +func (c *hugoBuilder) buildSites(noBuildLock bool) (err error) { + return c.hugo().Build(hugolib.BuildCfg{NoBuildLock: noBuildLock}) +} + +func (c *hugoBuilder) copyStatic() (map[string]uint64, error) { + m, err := c.doWithPublishDirs(c.copyStaticTo) + if err == nil || herrors.IsNotExist(err) { + return m, nil + } + return m, err +} + +func (c *hugoBuilder) copyStaticTo(sourceFs *filesystems.SourceFilesystem) (uint64, error) { + publishDir := helpers.FilePathSeparator + + if sourceFs.PublishFolder != "" { + publishDir = filepath.Join(publishDir, sourceFs.PublishFolder) + } + + fs := &countingStatFs{Fs: sourceFs.Fs} + + syncer := fsync.NewSyncer() + syncer.NoTimes = c.conf().configs.Base.NoTimes + syncer.NoChmod = c.conf().configs.Base.NoChmod + syncer.ChmodFilter = chmodFilter + syncer.SrcFs = fs + syncer.DestFs = c.conf().fs.PublishDirStatic + // Now that we are using a unionFs for the static directories + // We can effectively clean the publishDir on initial sync + syncer.Delete = c.conf().configs.Base.CleanDestinationDir + + if syncer.Delete { + c.r.logger.Infoln("removing all files from destination that don't exist in static dirs") + + syncer.DeleteFilter = func(f os.FileInfo) bool { + return f.IsDir() && strings.HasPrefix(f.Name(), ".") + } + } + c.r.logger.Infoln("syncing static files to", publishDir) + + // because we are using a baseFs (to get the union right). + // set sync src to root + err := syncer.Sync(publishDir, helpers.FilePathSeparator) + if err != nil { + return 0, err + } + + // Sync runs Stat 3 times for every source file (which sounds much) + numFiles := fs.statCounter / 3 + + return numFiles, err +} + +func (c *hugoBuilder) doWithPublishDirs(f func(sourceFs *filesystems.SourceFilesystem) (uint64, error)) (map[string]uint64, error) { + langCount := make(map[string]uint64) + + staticFilesystems := c.hugo().BaseFs.SourceFilesystems.Static + + if len(staticFilesystems) == 0 { + c.r.logger.Infoln("No static directories found to sync") + return langCount, nil + } + + for lang, fs := range staticFilesystems { + cnt, err := f(fs) + if err != nil { + return langCount, err + } + if lang == "" { + // Not multihost + for _, l := range c.conf().configs.Languages { + langCount[l.Lang] = cnt + } + } else { + langCount[lang] = cnt + } + } + + return langCount, nil +} + +func (c *hugoBuilder) fullBuild(noBuildLock bool) error { + var ( + g errgroup.Group + langCount map[string]uint64 + ) + + if !c.r.quiet { + fmt.Println("Start building sites … ") + fmt.Println(hugo.BuildVersionString()) + if terminal.IsTerminal(os.Stdout) { + defer func() { + fmt.Print(showCursor + clearLine) + }() + } + } + + copyStaticFunc := func() error { + cnt, err := c.copyStatic() + if err != nil { + return fmt.Errorf("error copying static files: %w", err) + } + langCount = cnt + return nil + } + buildSitesFunc := func() error { + if err := c.buildSites(noBuildLock); err != nil { + return fmt.Errorf("error building site: %w", err) + } + return nil + } + // Do not copy static files and build sites in parallel if cleanDestinationDir is enabled. + // This flag deletes all static resources in /public folder that are missing in /static, + // and it does so at the end of copyStatic() call. + if c.conf().configs.Base.CleanDestinationDir { + if err := copyStaticFunc(); err != nil { + return err + } + if err := buildSitesFunc(); err != nil { + return err + } + } else { + g.Go(copyStaticFunc) + g.Go(buildSitesFunc) + if err := g.Wait(); err != nil { + return err + } + } + + for _, s := range c.hugo().Sites { + s.ProcessingStats.Static = langCount[s.Language().Lang] + } + + if c.r.gc { + count, err := c.hugo().GC() + if err != nil { + return err + } + for _, s := range c.hugo().Sites { + // We have no way of knowing what site the garbage belonged to. + s.ProcessingStats.Cleaned = uint64(count) + } + } + + return nil +} + +func (c *hugoBuilder) fullRebuild(changeType string) { + if changeType == configChangeGoMod { + // go.mod may be changed during the build itself, and + // we really want to prevent superfluous builds. + if !c.fullRebuildSem.TryAcquire(1) { + return + } + c.fullRebuildSem.Release(1) + } + + c.fullRebuildSem.Acquire(context.Background(), 1) + + go func() { + defer c.fullRebuildSem.Release(1) + + c.printChangeDetected(changeType) + + defer func() { + // Allow any file system events to arrive basimplecobra. + // This will block any rebuild on config changes for the + // duration of the sleep. + time.Sleep(2 * time.Second) + }() + + defer c.r.timeTrack(time.Now(), "Rebuilt") + + err := c.reloadConfig() + if err != nil { + // Set the processing on pause until the state is recovered. + c.errState.setPaused(true) + c.handleBuildErr(err, "Failed to reload config") + } else { + c.errState.setPaused(false) + } + + if !c.errState.isPaused() { + _, err := c.copyStatic() + if err != nil { + c.r.logger.Errorln(err) + return + } + err = c.buildSites(false) + if err != nil { + c.r.logger.Errorln(err) + } else if c.s != nil && c.s.doLiveReload { + livereload.ForceRefresh() + } + } + }() +} + +func (c *hugoBuilder) handleBuildErr(err error, msg string) { + c.errState.setBuildErr(err) + c.r.logger.Errorln(msg + ": " + cleanErrorLog(err.Error())) +} + +func (c *hugoBuilder) handleEvents(watcher *watcher.Batcher, + staticSyncer *staticSyncer, + evs []fsnotify.Event, + configSet map[string]bool) { + defer func() { + c.errState.setWasErr(false) + }() + + var isHandled bool + + for _, ev := range evs { + isConfig := configSet[ev.Name] + configChangeType := configChangeConfig + if isConfig { + if strings.Contains(ev.Name, "go.mod") { + configChangeType = configChangeGoMod + } + if strings.Contains(ev.Name, ".work") { + configChangeType = configChangeGoWork + } + } + if !isConfig { + // It may be one of the /config folders + dirname := filepath.Dir(ev.Name) + if dirname != "." && configSet[dirname] { + isConfig = true + } + } + + if isConfig { + isHandled = true + + if ev.Op&fsnotify.Chmod == fsnotify.Chmod { + continue + } + + if ev.Op&fsnotify.Remove == fsnotify.Remove || ev.Op&fsnotify.Rename == fsnotify.Rename { + configFiles := c.conf().configs.LoadingInfo.ConfigFiles + for _, configFile := range configFiles { + counter := 0 + for watcher.Add(configFile) != nil { + counter++ + if counter >= 100 { + break + } + time.Sleep(100 * time.Millisecond) + } + } + } + + // Config file(s) changed. Need full rebuild. + c.fullRebuild(configChangeType) + + return + } + } + + if isHandled { + return + } + + if c.errState.isPaused() { + // Wait for the server to get into a consistent state before + // we continue with processing. + return + } + + if len(evs) > 50 { + // This is probably a mass edit of the content dir. + // Schedule a full rebuild for when it slows down. + c.debounce(func() { + c.fullRebuild("") + }) + return + } + + c.r.logger.Infoln("Received System Events:", evs) + + staticEvents := []fsnotify.Event{} + dynamicEvents := []fsnotify.Event{} + + filtered := []fsnotify.Event{} + for _, ev := range evs { + if c.hugo().ShouldSkipFileChangeEvent(ev) { + continue + } + // Check the most specific first, i.e. files. + contentMapped := c.hugo().ContentChanges.GetSymbolicLinkMappings(ev.Name) + if len(contentMapped) > 0 { + for _, mapped := range contentMapped { + filtered = append(filtered, fsnotify.Event{Name: mapped, Op: ev.Op}) + } + continue + } + + // Check for any symbolic directory mapping. + + dir, name := filepath.Split(ev.Name) + + contentMapped = c.hugo().ContentChanges.GetSymbolicLinkMappings(dir) + + if len(contentMapped) == 0 { + filtered = append(filtered, ev) + continue + } + + for _, mapped := range contentMapped { + mappedFilename := filepath.Join(mapped, name) + filtered = append(filtered, fsnotify.Event{Name: mappedFilename, Op: ev.Op}) + } + } + + evs = filtered + + for _, ev := range evs { + ext := filepath.Ext(ev.Name) + baseName := filepath.Base(ev.Name) + istemp := strings.HasSuffix(ext, "~") || + (ext == ".swp") || // vim + (ext == ".swx") || // vim + (ext == ".tmp") || // generic temp file + (ext == ".DS_Store") || // OSX Thumbnail + baseName == "4913" || // vim + strings.HasPrefix(ext, ".goutputstream") || // gnome + strings.HasSuffix(ext, "jb_old___") || // intelliJ + strings.HasSuffix(ext, "jb_tmp___") || // intelliJ + strings.HasSuffix(ext, "jb_bak___") || // intelliJ + strings.HasPrefix(ext, ".sb-") || // byword + strings.HasPrefix(baseName, ".#") || // emacs + strings.HasPrefix(baseName, "#") // emacs + if istemp { + continue + } + if c.hugo().Deps.SourceSpec.IgnoreFile(ev.Name) { + continue + } + // Sometimes during rm -rf operations a '"": REMOVE' is triggered. Just ignore these + if ev.Name == "" { + continue + } + + // Write and rename operations are often followed by CHMOD. + // There may be valid use cases for rebuilding the site on CHMOD, + // but that will require more complex logic than this simple conditional. + // On OS X this seems to be related to Spotlight, see: + // https://github.com/go-fsnotify/fsnotify/issues/15 + // A workaround is to put your site(s) on the Spotlight exception list, + // but that may be a little mysterious for most end users. + // So, for now, we skip reload on CHMOD. + // We do have to check for WRITE though. On slower laptops a Chmod + // could be aggregated with other important events, and we still want + // to rebuild on those + if ev.Op&(fsnotify.Chmod|fsnotify.Write|fsnotify.Create) == fsnotify.Chmod { + continue + } + + walkAdder := func(path string, f hugofs.FileMetaInfo, err error) error { + if f.IsDir() { + c.r.logger.Println("adding created directory to watchlist", path) + if err := watcher.Add(path); err != nil { + return err + } + } else if !staticSyncer.isStatic(path) { + // Hugo's rebuilding logic is entirely file based. When you drop a new folder into + // /content on OSX, the above logic will handle future watching of those files, + // but the initial CREATE is lost. + dynamicEvents = append(dynamicEvents, fsnotify.Event{Name: path, Op: fsnotify.Create}) + } + return nil + } + + // recursively add new directories to watch list + // When mkdir -p is used, only the top directory triggers an event (at least on OSX) + if ev.Op&fsnotify.Create == fsnotify.Create { + if s, err := c.conf().fs.Source.Stat(ev.Name); err == nil && s.Mode().IsDir() { + _ = helpers.SymbolicWalk(c.conf().fs.Source, ev.Name, walkAdder) + } + } + + if staticSyncer.isStatic(ev.Name) { + staticEvents = append(staticEvents, ev) + } else { + dynamicEvents = append(dynamicEvents, ev) + } + } + + if len(staticEvents) > 0 { + c.printChangeDetected("Static files") + + if c.r.forceSyncStatic { + c.r.logger.Printf("Syncing all static files\n") + _, err := c.copyStatic() + if err != nil { + c.r.logger.Errorln("Error copying static files to publish dir:", err) + return + } + } else { + if err := staticSyncer.syncsStaticEvents(staticEvents); err != nil { + c.r.logger.Errorln("Error syncing static files to publish dir:", err) + return + } + } + + if c.s != nil && c.s.doLiveReload { + // Will block forever trying to write to a channel that nobody is reading if livereload isn't initialized + + // force refresh when more than one file + if !c.errState.wasErr() && len(staticEvents) == 1 { + ev := staticEvents[0] + h := c.hugo() + path := h.BaseFs.SourceFilesystems.MakeStaticPathRelative(ev.Name) + path = h.RelURL(helpers.ToSlashTrimLeading(path), false) + + livereload.RefreshPath(path) + } else { + livereload.ForceRefresh() + } + } + } + + if len(dynamicEvents) > 0 { + partitionedEvents := partitionDynamicEvents( + c.hugo().BaseFs.SourceFilesystems, + dynamicEvents) + + onePageName := pickOneWriteOrCreatePath(partitionedEvents.ContentEvents) + + c.printChangeDetected("") + c.changeDetector.PrepareNew() + + func() { + defer c.r.timeTrack(time.Now(), "Total") + if err := c.rebuildSites(dynamicEvents); err != nil { + c.handleBuildErr(err, "Rebuild failed") + } + }() + + if c.s != nil && c.s.doLiveReload { + if len(partitionedEvents.ContentEvents) == 0 && len(partitionedEvents.AssetEvents) > 0 { + if c.errState.wasErr() { + livereload.ForceRefresh() + return + } + changed := c.changeDetector.changed() + if c.changeDetector != nil && len(changed) == 0 { + // Nothing has changed. + return + } else if len(changed) == 1 { + pathToRefresh := c.hugo().PathSpec.RelURL(helpers.ToSlashTrimLeading(changed[0]), false) + livereload.RefreshPath(pathToRefresh) + } else { + livereload.ForceRefresh() + } + } + + if len(partitionedEvents.ContentEvents) > 0 { + navigate := c.s != nil && c.s.navigateToChanged + // We have fetched the same page above, but it may have + // changed. + var p page.Page + + if navigate { + if onePageName != "" { + p = c.hugo().GetContentPage(onePageName) + } + } + + if p != nil { + livereload.NavigateToPathForPort(p.RelPermalink(), p.Site().ServerPort()) + } else { + livereload.ForceRefresh() + } + } + } + } +} + +func (c *hugoBuilder) hugo() *hugolib.HugoSites { + h, err := c.r.HugFromConfig(c.conf()) + if err != nil { + panic(err) + } + if c.s != nil { + // A running server, register the media types. + for _, s := range h.Sites { + s.RegisterMediaTypes() + } + } + return h +} + +func (c *hugoBuilder) hugoTry() *hugolib.HugoSites { + h, _ := c.r.HugFromConfig(c.conf()) + return h +} + +func (c *hugoBuilder) loadConfig(cd *simplecobra.Commandeer, running bool) error { + cfg := config.New() + cfg.Set("renderToDisk", (c.s == nil && !c.r.renderToMemory) || (c.s != nil && c.s.renderToDisk)) + watch := c.r.buildWatch || (c.s != nil && c.s.serverWatch) + cfg.Set("environment", c.r.environment) + + cfg.Set("internal", maps.Params{ + "running": running, + "watch": watch, + "verbose": c.r.verbose, + }) + + conf, err := c.r.ConfigFromProvider(c.r.configVersionID.Load(), flagsToCfg(cd, cfg)) + if err != nil { + return err + } + c.setConf(conf) + if c.onConfigLoaded != nil { + if err := c.onConfigLoaded(false); err != nil { + return err + } + } + + return nil + +} + +func (c *hugoBuilder) printChangeDetected(typ string) { + msg := "\nChange" + if typ != "" { + msg += " of " + typ + } + msg += " detected, rebuilding site." + + c.r.logger.Println(msg) + const layout = "2006-01-02 15:04:05.000 -0700" + c.r.logger.Println(htime.Now().Format(layout)) +} + +func (c *hugoBuilder) rebuildSites(events []fsnotify.Event) error { + if err := c.errState.buildErr(); err != nil { + ferrs := herrors.UnwrapFileErrorsWithErrorContext(err) + for _, err := range ferrs { + events = append(events, fsnotify.Event{Name: err.Position().Filename, Op: fsnotify.Write}) + } + } + c.errState.setBuildErr(nil) + visited := c.visitedURLs.PeekAllSet() + h := c.hugo() + if c.fastRenderMode { + // Make sure we always render the home pages + for _, l := range c.conf().configs.Languages { + langPath := h.GetLangSubDir(l.Lang) + if langPath != "" { + langPath = langPath + "/" + } + home := h.PrependBasePath("/"+langPath, false) + visited[home] = true + } + } + return h.Build(hugolib.BuildCfg{NoBuildLock: true, RecentlyVisited: visited, ErrRecovery: c.errState.wasErr()}, events...) +} + +func (c *hugoBuilder) reloadConfig() error { + c.r.Reset() + c.r.configVersionID.Add(1) + oldConf := c.conf() + conf, err := c.r.ConfigFromConfig(c.r.configVersionID.Load(), c.conf()) + if err != nil { + return err + } + sameLen := len(oldConf.configs.Languages) == len(conf.configs.Languages) + if !sameLen { + if oldConf.configs.IsMultihost || conf.configs.IsMultihost { + return errors.New("multihost change detected, please restart server") + } + } + c.setConf(conf) + if c.onConfigLoaded != nil { + if err := c.onConfigLoaded(true); err != nil { |