1// Copyright 2019 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
14package hugolib
15
16import (
17	"context"
18	"io"
19	"path/filepath"
20	"sort"
21	"strings"
22	"sync"
23	"sync/atomic"
24
25	"github.com/gohugoio/hugo/hugofs/glob"
26
27	"github.com/fsnotify/fsnotify"
28
29	"github.com/gohugoio/hugo/identity"
30
31	radix "github.com/armon/go-radix"
32
33	"github.com/gohugoio/hugo/output"
34	"github.com/gohugoio/hugo/parser/metadecoders"
35
36	"github.com/gohugoio/hugo/common/para"
37	"github.com/gohugoio/hugo/hugofs"
38	"github.com/pkg/errors"
39
40	"github.com/gohugoio/hugo/source"
41
42	"github.com/bep/gitmap"
43	"github.com/gohugoio/hugo/config"
44
45	"github.com/gohugoio/hugo/publisher"
46
47	"github.com/gohugoio/hugo/common/herrors"
48	"github.com/gohugoio/hugo/common/loggers"
49	"github.com/gohugoio/hugo/deps"
50	"github.com/gohugoio/hugo/helpers"
51	"github.com/gohugoio/hugo/langs"
52	"github.com/gohugoio/hugo/lazy"
53
54	"github.com/gohugoio/hugo/langs/i18n"
55	"github.com/gohugoio/hugo/resources/page"
56	"github.com/gohugoio/hugo/resources/page/pagemeta"
57	"github.com/gohugoio/hugo/tpl"
58	"github.com/gohugoio/hugo/tpl/tplimpl"
59)
60
61// HugoSites represents the sites to build. Each site represents a language.
62type HugoSites struct {
63	Sites []*Site
64
65	multilingual *Multilingual
66
67	// Multihost is set if multilingual and baseURL set on the language level.
68	multihost bool
69
70	// If this is running in the dev server.
71	running bool
72
73	// Render output formats for all sites.
74	renderFormats output.Formats
75
76	*deps.Deps
77
78	gitInfo *gitInfo
79
80	// As loaded from the /data dirs
81	data map[string]interface{}
82
83	contentInit sync.Once
84	content     *pageMaps
85
86	// Keeps track of bundle directories and symlinks to enable partial rebuilding.
87	ContentChanges *contentChangeMap
88
89	// File change events with filename stored in this map will be skipped.
90	skipRebuildForFilenamesMu sync.Mutex
91	skipRebuildForFilenames   map[string]bool
92
93	init *hugoSitesInit
94
95	workers    *para.Workers
96	numWorkers int
97
98	*fatalErrorHandler
99	*testCounters
100}
101
102// ShouldSkipFileChangeEvent allows skipping filesystem event early before
103// the build is started.
104func (h *HugoSites) ShouldSkipFileChangeEvent(ev fsnotify.Event) bool {
105	h.skipRebuildForFilenamesMu.Lock()
106	defer h.skipRebuildForFilenamesMu.Unlock()
107	return h.skipRebuildForFilenames[ev.Name]
108}
109
110func (h *HugoSites) getContentMaps() *pageMaps {
111	h.contentInit.Do(func() {
112		h.content = newPageMaps(h)
113	})
114	return h.content
115}
116
117// Only used in tests.
118type testCounters struct {
119	contentRenderCounter uint64
120}
121
122func (h *testCounters) IncrContentRender() {
123	if h == nil {
124		return
125	}
126	atomic.AddUint64(&h.contentRenderCounter, 1)
127}
128
129type fatalErrorHandler struct {
130	mu sync.Mutex
131
132	h *HugoSites
133
134	err error
135
136	done  bool
137	donec chan bool // will be closed when done
138}
139
140// FatalError error is used in some rare situations where it does not make sense to
141// continue processing, to abort as soon as possible and log the error.
142func (f *fatalErrorHandler) FatalError(err error) {
143	f.mu.Lock()
144	defer f.mu.Unlock()
145	if !f.done {
146		f.done = true
147		close(f.donec)
148	}
149	f.err = err
150}
151
152func (f *fatalErrorHandler) getErr() error {
153	f.mu.Lock()
154	defer f.mu.Unlock()
155	return f.err
156}
157
158func (f *fatalErrorHandler) Done() <-chan bool {
159	return f.donec
160}
161
162type hugoSitesInit struct {
163	// Loads the data from all of the /data folders.
164	data *lazy.Init
165
166	// Performs late initialization (before render) of the templates.
167	layouts *lazy.Init
168
169	// Loads the Git info for all the pages if enabled.
170	gitInfo *lazy.Init
171
172	// Maps page translations.
173	translations *lazy.Init
174}
175
176func (h *hugoSitesInit) Reset() {
177	h.data.Reset()
178	h.layouts.Reset()
179	h.gitInfo.Reset()
180	h.translations.Reset()
181}
182
183func (h *HugoSites) Data() map[string]interface{} {
184	if _, err := h.init.data.Do(); err != nil {
185		h.SendError(errors.Wrap(err, "failed to load data"))
186		return nil
187	}
188	return h.data
189}
190
191func (h *HugoSites) gitInfoForPage(p page.Page) (*gitmap.GitInfo, error) {
192	if _, err := h.init.gitInfo.Do(); err != nil {
193		return nil, err
194	}
195
196	if h.gitInfo == nil {
197		return nil, nil
198	}
199
200	return h.gitInfo.forPage(p), nil
201}
202
203func (h *HugoSites) siteInfos() page.Sites {
204	infos := make(page.Sites, len(h.Sites))
205	for i, site := range h.Sites {
206		infos[i] = site.Info
207	}
208	return infos
209}
210
211func (h *HugoSites) pickOneAndLogTheRest(errors []error) error {
212	if len(errors) == 0 {
213		return nil
214	}
215
216	var i int
217
218	for j, err := range errors {
219		// If this is in server mode, we want to return an error to the client
220		// with a file context, if possible.
221		if herrors.UnwrapErrorWithFileContext(err) != nil {
222			i = j
223			break
224		}
225	}
226
227	// Log the rest, but add a threshold to avoid flooding the log.
228	const errLogThreshold = 5
229
230	for j, err := range errors {
231		if j == i || err == nil {
232			continue
233		}
234
235		if j >= errLogThreshold {
236			break
237		}
238
239		h.Log.Errorln(err)
240	}
241
242	return errors[i]
243}
244
245func (h *HugoSites) IsMultihost() bool {
246	return h != nil && h.multihost
247}
248
249// TODO(bep) consolidate
250func (h *HugoSites) LanguageSet() map[string]int {
251	set := make(map[string]int)
252	for i, s := range h.Sites {
253		set[s.language.Lang] = i
254	}
255	return set
256}
257
258func (h *HugoSites) NumLogErrors() int {
259	if h == nil {
260		return 0
261	}
262	return int(h.Log.LogCounters().ErrorCounter.Count())
263}
264
265func (h *HugoSites) PrintProcessingStats(w io.Writer) {
266	stats := make([]*helpers.ProcessingStats, len(h.Sites))
267	for i := 0; i < len(h.Sites); i++ {
268		stats[i] = h.Sites[i].PathSpec.ProcessingStats
269	}
270	helpers.ProcessingStatsTable(w, stats...)
271}
272
273// GetContentPage finds a Page with content given the absolute filename.
274// Returns nil if none found.
275func (h *HugoSites) GetContentPage(filename string) page.Page {
276	var p page.Page
277
278	h.getContentMaps().walkBundles(func(b *contentNode) bool {
279		if b.p == nil || b.fi == nil {
280			return false
281		}
282
283		if b.fi.Meta().Filename == filename {
284			p = b.p
285			return true
286		}
287
288		return false
289	})
290
291	return p
292}
293
294// NewHugoSites creates a new collection of sites given the input sites, building
295// a language configuration based on those.
296func newHugoSites(cfg deps.DepsCfg, sites ...*Site) (*HugoSites, error) {
297	if cfg.Language != nil {
298		return nil, errors.New("Cannot provide Language in Cfg when sites are provided")
299	}
300
301	// Return error at the end. Make the caller decide if it's fatal or not.
302	var initErr error
303
304	langConfig, err := newMultiLingualFromSites(cfg.Cfg, sites...)
305	if err != nil {
306		return nil, errors.Wrap(err, "failed to create language config")
307	}
308
309	var contentChangeTracker *contentChangeMap
310
311	numWorkers := config.GetNumWorkerMultiplier()
312	if numWorkers > len(sites) {
313		numWorkers = len(sites)
314	}
315	var workers *para.Workers
316	if numWorkers > 1 {
317		workers = para.New(numWorkers)
318	}
319
320	h := &HugoSites{
321		running:                 cfg.Running,
322		multilingual:            langConfig,
323		multihost:               cfg.Cfg.GetBool("multihost"),
324		Sites:                   sites,
325		workers:                 workers,
326		numWorkers:              numWorkers,
327		skipRebuildForFilenames: make(map[string]bool),
328		init: &hugoSitesInit{
329			data:         lazy.New(),
330			layouts:      lazy.New(),
331			gitInfo:      lazy.New(),
332			translations: lazy.New(),
333		},
334	}
335
336	h.fatalErrorHandler = &fatalErrorHandler{
337		h:     h,
338		donec: make(chan bool),
339	}
340
341	h.init.data.Add(func() (interface{}, error) {
342		err := h.loadData(h.PathSpec.BaseFs.Data.Dirs)
343		if err != nil {
344			return nil, errors.Wrap(err, "failed to load data")
345		}
346		return nil, nil
347	})
348
349	h.init.layouts.Add(func() (interface{}, error) {
350		for _, s := range h.Sites {
351			if err := s.Tmpl().(tpl.TemplateManager).MarkReady(); err != nil {
352				return nil, err
353			}
354		}
355		return nil, nil
356	})
357
358	h.init.translations.Add(func() (interface{}, error) {
359		if len(h.Sites) > 1 {
360			allTranslations := pagesToTranslationsMap(h.Sites)
361			assignTranslationsToPages(allTranslations, h.Sites)
362		}
363
364		return nil, nil
365	})
366
367	h.init.gitInfo.Add(func() (interface{}, error) {
368		err := h.loadGitInfo()
369		if err != nil {
370			return nil, errors.Wrap(err, "failed to load Git info")
371		}
372		return nil, nil
373	})
374
375	for _, s := range sites {
376		s.h = h
377	}
378
379	var l configLoader
380	if err := l.applyDeps(cfg, sites...); err != nil {
381		initErr = errors.Wrap(err, "add site dependencies")
382	}
383
384	h.Deps = sites[0].Deps
385
386	// Only needed in server mode.
387	// TODO(bep) clean up the running vs watching terms
388	if cfg.Running {
389		contentChangeTracker = &contentChangeMap{
390			pathSpec:      h.PathSpec,
391			symContent:    make(map[string]map[string]bool),
392			leafBundles:   radix.New(),
393			branchBundles: make(map[string]bool),
394		}
395		h.ContentChanges = contentChangeTracker
396	}
397
398	return h, initErr
399}
400
401func (h *HugoSites) loadGitInfo() error {
402	if h.Cfg.GetBool("enableGitInfo") {
403		gi, err := newGitInfo(h.Cfg)
404		if err != nil {
405			h.Log.Errorln("Failed to read Git log:", err)
406		} else {
407			h.gitInfo = gi
408		}
409	}
410	return nil
411}
412
413func (l configLoader) applyDeps(cfg deps.DepsCfg, sites ...*Site) error {
414	if cfg.TemplateProvider == nil {
415		cfg.TemplateProvider = tplimpl.DefaultTemplateProvider
416	}
417
418	if cfg.TranslationProvider == nil {
419		cfg.TranslationProvider = i18n.NewTranslationProvider()
420	}
421
422	var (
423		d   *deps.Deps
424		err error
425	)
426
427	for _, s := range sites {
428		if s.Deps != nil {
429			continue
430		}
431
432		onCreated := func(d *deps.Deps) error {
433			s.Deps = d
434
435			// Set up the main publishing chain.
436			pub, err := publisher.NewDestinationPublisher(
437				d.ResourceSpec,
438				s.outputFormatsConfig,
439				s.mediaTypesConfig,
440			)
441			if err != nil {
442				return err
443			}
444			s.publisher = pub
445
446			if err := s.initializeSiteInfo(); err != nil {
447				return err
448			}
449
450			d.Site = s.Info
451
452			siteConfig, err := l.loadSiteConfig(s.language)
453			if err != nil {
454				return errors.Wrap(err, "load site config")
455			}
456			s.siteConfigConfig = siteConfig
457
458			pm := &pageMap{
459				contentMap: newContentMap(contentMapConfig{
460					lang:                 s.Lang(),
461					taxonomyConfig:       s.siteCfg.taxonomiesConfig.Values(),
462					taxonomyDisabled:     !s.isEnabled(page.KindTerm),
463					taxonomyTermDisabled: !s.isEnabled(page.KindTaxonomy),
464					pageDisabled:         !s.isEnabled(page.KindPage),
465				}),
466				s: s,
467			}
468
469			s.PageCollections = newPageCollections(pm)
470
471			s.siteRefLinker, err = newSiteRefLinker(s.language, s)
472			return err
473		}
474
475		cfg.Language = s.language
476		cfg.MediaTypes = s.mediaTypesConfig
477		cfg.OutputFormats = s.outputFormatsConfig
478
479		if d == nil {
480			cfg.WithTemplate = s.withSiteTemplates(cfg.WithTemplate)
481
482			var err error
483			d, err = deps.New(cfg)
484			if err != nil {
485				return errors.Wrap(err, "create deps")
486			}
487
488			d.OutputFormatsConfig = s.outputFormatsConfig
489
490			if err := onCreated(d); err != nil {
491				return errors.Wrap(err, "on created")
492			}
493
494			if err = d.LoadResources(); err != nil {
495				return errors.Wrap(err, "load resources")
496			}
497
498		} else {
499			d, err = d.ForLanguage(cfg, onCreated)
500			if err != nil {
501				return err
502			}
503			d.OutputFormatsConfig = s.outputFormatsConfig
504		}
505	}
506
507	return nil
508}
509
510// NewHugoSites creates HugoSites from the given config.
511func NewHugoSites(cfg deps.DepsCfg) (*HugoSites, error) {
512	if cfg.Logger == nil {
513		cfg.Logger = loggers.NewErrorLogger()
514	}
515	sites, err := createSitesFromConfig(cfg)
516	if err != nil {
517		return nil, errors.Wrap(err, "from config")
518	}
519	return newHugoSites(cfg, sites...)
520}
521
522func (s *Site) withSiteTemplates(withTemplates ...func(templ tpl.TemplateManager) error) func(templ tpl.TemplateManager) error {
523	return func(templ tpl.TemplateManager) error {
524		for _, wt := range withTemplates {
525			if wt == nil {
526				continue
527			}
528			if err := wt(templ); err != nil {
529				return err
530			}
531		}
532
533		return nil
534	}
535}
536
537func createSitesFromConfig(cfg deps.DepsCfg) ([]*Site, error) {
538	var sites []*Site
539
540	languages := getLanguages(cfg.Cfg)
541
542	for _, lang := range languages {
543		if lang.Disabled {
544			continue
545		}
546		var s *Site
547		var err error
548		cfg.Language = lang
549		s, err = newSite(cfg)
550
551		if err != nil {
552			return nil, err
553		}
554
555		sites = append(sites, s)
556	}
557
558	return sites, nil
559}
560
561// Reset resets the sites and template caches etc., making it ready for a full rebuild.
562func (h *HugoSites) reset(config *BuildCfg) {
563	if config.ResetState {
564		for i, s := range h.Sites {
565			h.Sites[i] = s.reset()
566			if r, ok := s.Fs.Destination.(hugofs.Reseter); ok {
567				r.Reset()
568			}
569		}
570	}
571
572	h.fatalErrorHandler = &fatalErrorHandler{
573		h:     h,
574		donec: make(chan bool),
575	}
576
577	h.init.Reset()
578}
579
580// resetLogs resets the log counters etc. Used to do a new build on the same sites.
581func (h *HugoSites) resetLogs() {
582	h.Log.Reset()
583	loggers.GlobalErrorCounter.Reset()
584	for _, s := range h.Sites {
585		s.Deps.Log.Reset()
586		s.Deps.LogDistinct.Reset()
587	}
588}
589
590func (h *HugoSites) withSite(fn func(s *Site) error) error {
591	if h.workers == nil {
592		for _, s := range h.Sites {
593			if err := fn(s); err != nil {
594				return err
595			}
596		}
597		return nil
598	}
599
600	g, _ := h.workers.Start(context.Background())
601	for _, s := range h.Sites {
602		s := s
603		g.Run(func() error {
604			return fn(s)
605		})
606	}
607	return g.Wait()
608}
609
610func (h *HugoSites) createSitesFromConfig(cfg config.Provider) error {
611	oldLangs, _ := h.Cfg.Get("languagesSorted").(langs.Languages)
612
613	l := configLoader{cfg: h.Cfg}
614	if err := l.loadLanguageSettings(oldLangs); err != nil {
615		return err
616	}
617
618	depsCfg := deps.DepsCfg{Fs: h.Fs, Cfg: l.cfg}
619
620	sites, err := createSitesFromConfig(depsCfg)
621	if err != nil {
622		return err
623	}
624
625	langConfig, err := newMultiLingualFromSites(depsCfg.Cfg, sites...)
626	if err != nil {
627		return err
628	}
629
630	h.Sites = sites
631
632	for _, s := range sites {
633		s.h = h
634	}
635
636	var cl configLoader
637	if err := cl.applyDeps(depsCfg, sites...); err != nil {
638		return err
639	}
640
641	h.Deps = sites[0].Deps
642
643	h.multilingual = langConfig
644	h.multihost = h.Deps.Cfg.GetBool("multihost")
645
646	return nil
647}
648
649func (h *HugoSites) toSiteInfos() []*SiteInfo {
650	infos := make([]*SiteInfo, len(h.Sites))
651	for i, s := range h.Sites {
652		infos[i] = s.Info
653	}
654	return infos
655}
656
657// BuildCfg holds build options used to, as an example, skip the render step.
658type BuildCfg struct {
659	// Reset site state before build. Use to force full rebuilds.
660	ResetState bool
661	// If set, we re-create the sites from the given configuration before a build.
662	// This is needed if new languages are added.
663	NewConfig config.Provider
664	// Skip rendering. Useful for testing.
665	SkipRender bool
666	// Use this to indicate what changed (for rebuilds).
667	whatChanged *whatChanged
668
669	// This is a partial re-render of some selected pages. This means
670	// we should skip most of the processing.
671	PartialReRender bool
672
673	// Set in server mode when the last build failed for some reason.
674	ErrRecovery bool
675
676	// Recently visited URLs. This is used for partial re-rendering.
677	RecentlyVisited map[string]bool
678
679	// Can be set to build only with a sub set of the content source.
680	ContentInclusionFilter *glob.FilenameFilter
681
682	// Set when the buildlock is already acquired (e.g. the archetype content builder).
683	NoBuildLock bool
684
685	testCounters *testCounters
686}
687
688// shouldRender is used in the Fast Render Mode to determine if we need to re-render
689// a Page: If it is recently visited (the home pages will always be in this set) or changed.
690// Note that a page does not have to have a content page / file.
691// For regular builds, this will allways return true.
692// TODO(bep) rename/work this.
693func (cfg *BuildCfg) shouldRender(p *pageState) bool {
694	if p.forceRender {
695		return true
696	}
697
698	if len(cfg.RecentlyVisited) == 0 {
699		return true
700	}
701
702	if cfg.RecentlyVisited[p.RelPermalink()] {
703		return true
704	}
705
706	if cfg.whatChanged != nil && !p.File().IsZero() {
707		return cfg.whatChanged.files[p.File().Filename()]
708	}
709
710	return false
711}
712
713func (h *HugoSites) renderCrossSitesSitemap() error {
714	if !h.multilingual.enabled() || h.IsMultihost() {
715		return nil
716	}
717
718	sitemapEnabled := false
719	for _, s := range h.Sites {
720		if s.isEnabled(kindSitemap) {
721			sitemapEnabled = true
722			break
723		}
724	}
725
726	if !sitemapEnabled {
727		return nil
728	}
729
730	s := h.Sites[0]
731
732	templ := s.lookupLayouts("sitemapindex.xml", "_default/sitemapindex.xml", "_internal/_default/sitemapindex.xml")
733
734	return s.renderAndWriteXML(&s.PathSpec.ProcessingStats.Sitemaps, "sitemapindex",
735		s.siteCfg.sitemap.Filename, h.toSiteInfos(), templ)
736}
737
738func (h *HugoSites) renderCrossSitesRobotsTXT() error {
739	if h.multihost {
740		return nil
741	}
742	if !h.Cfg.GetBool("enableRobotsTXT") {
743		return nil
744	}
745
746	s := h.Sites[0]
747
748	p, err := newPageStandalone(&pageMeta{
749		s:    s,
750		kind: kindRobotsTXT,
751		urlPaths: pagemeta.URLPath{
752			URL: "robots.txt",
753		},
754	},
755		output.RobotsTxtFormat)
756	if err != nil {
757		return err
758	}
759
760	if !p.render {
761		return nil
762	}
763
764	templ := s.lookupLayouts("robots.txt", "_default/robots.txt", "_internal/_default/robots.txt")
765
766	return s.renderAndWritePage(&s.PathSpec.ProcessingStats.Pages, "Robots Txt", "robots.txt", p, templ)
767}
768
769func (h *HugoSites) removePageByFilename(filename string) {
770	h.getContentMaps().withMaps(func(m *pageMap) error {
771		m.deleteBundleMatching(func(b *contentNode) bool {
772			if b.p == nil {
773				return false
774			}
775
776			if b.fi == nil {
777				return false
778			}
779
780			return b.fi.Meta().Filename == filename
781		})
782		return nil
783	})
784}
785
786func (h *HugoSites) createPageCollections() error {
787	allPages := newLazyPagesFactory(func() page.Pages {
788		var pages page.Pages
789		for _, s := range h.Sites {
790			pages = append(pages, s.Pages()...)
791		}
792
793		page.SortByDefault(pages)
794
795		return pages
796	})
797
798	allRegularPages := newLazyPagesFactory(func() page.Pages {
799		return h.findPagesByKindIn(page.KindPage, allPages.get())
800	})
801
802	for _, s := range h.Sites {
803		s.PageCollections.allPages = allPages
804		s.PageCollections.allRegularPages = allRegularPages
805	}
806
807	return nil
808}
809
810func (s *Site) preparePagesForRender(isRenderingSite bool, idx int) error {
811	var err error
812	s.pageMap.withEveryBundlePage(func(p *pageState) bool {
813		if err = p.initOutputFormat(isRenderingSite, idx); err != nil {
814			return true
815		}
816		return false
817	})
818	return nil
819}
820
821// Pages returns all pages for all sites.
822func (h *HugoSites) Pages() page.Pages {
823	return h.Sites[0].AllPages()
824}
825
826func (h *HugoSites) loadData(fis []hugofs.FileMetaInfo) (err error) {
827	spec := source.NewSourceSpec(h.PathSpec, nil, nil)
828
829	h.data = make(map[string]interface{})
830	for _, fi := range fis {
831		fileSystem := spec.NewFilesystemFromFileMetaInfo(fi)
832		files, err := fileSystem.Files()
833		if err != nil {
834			return err
835		}
836		for _, r := range files {
837			if err := h.handleDataFile(r); err != nil {
838				return err
839			}
840		}
841	}
842
843	return
844}
845
846func (h *HugoSites) handleDataFile(r source.File) error {
847	var current map[string]interface{}
848
849	f, err := r.FileInfo().Meta().Open()
850	if err != nil {
851		return errors.Wrapf(err, "data: failed to open %q:", r.LogicalName())
852	}
853	defer f.Close()
854
855	// Crawl in data tree to insert data
856	current = h.data
857	keyParts := strings.Split(r.Dir(), helpers.FilePathSeparator)
858
859	for _, key := range keyParts {
860		if key != "" {
861			if _, ok := current[key]; !ok {
862				current[key] = make(map[string]interface{})
863			}
864			current = current[key].(map[string]interface{})
865		}
866	}
867
868	data, err := h.readData(r)
869	if err != nil {
870		return h.errWithFileContext(err, r)
871	}
872
873	if data == nil {
874		return nil
875	}
876
877	// filepath.Walk walks the files in lexical order, '/' comes before '.'
878	higherPrecedentData := current[r.BaseFileName()]
879
880	switch data.(type) {
881	case nil:
882	case map[string]interface{}:
883
884		switch higherPrecedentData.(type) {
885		case nil:
886			current[r.BaseFileName()] = data
887		case map[string]interface{}:
888			// merge maps: insert entries from data for keys that
889			// don't already exist in higherPrecedentData
890			higherPrecedentMap := higherPrecedentData.(map[string]interface{})
891			for key, value := range data.(map[string]interface{}) {
892				if _, exists := higherPrecedentMap[key]; exists {
893					// this warning could happen if
894					// 1. A theme uses the same key; the main data folder wins
895					// 2. A sub folder uses the same key: the sub folder wins
896					// TODO(bep) figure out a way to detect 2) above and make that a WARN
897					h.Log.Infof("Data for key '%s' in path '%s' is overridden by higher precedence data already in the data tree", key, r.Path())
898				} else {
899					higherPrecedentMap[key] = value
900				}
901			}
902		default:
903			// can't merge: higherPrecedentData is not a map
904			h.Log.Warnf("The %T data from '%s' overridden by "+
905				"higher precedence %T data already in the data tree", data, r.Path(), higherPrecedentData)
906		}
907
908	case []interface{}:
909		if higherPrecedentData == nil {
910			current[r.BaseFileName()] = data
911		} else {
912			// we don't merge array data
913			h.Log.Warnf("The %T data from '%s' overridden by "+
914				"higher precedence %T data already in the data tree", data, r.Path(), higherPrecedentData)
915		}
916
917	default:
918		h.Log.Errorf("unexpected data type %T in file %s", data, r.LogicalName())
919	}
920
921	return nil
922}
923
924func (h *HugoSites) errWithFileContext(err error, f source.File) error {
925	fim, ok := f.FileInfo().(hugofs.FileMetaInfo)
926	if !ok {
927		return err
928	}
929
930	realFilename := fim.Meta().Filename
931
932	err, _ = herrors.WithFileContextForFile(
933		err,
934		realFilename,
935		realFilename,
936		h.SourceSpec.Fs.Source,
937		herrors.SimpleLineMatcher)
938
939	return err
940}
941
942func (h *HugoSites) readData(f source.File) (interface{}, error) {
943	file, err := f.FileInfo().Meta().Open()
944	if err != nil {
945		return nil, errors.Wrap(err, "readData: failed to open data file")
946	}
947	defer file.Close()
948	content := helpers.ReaderToBytes(file)
949
950	format := metadecoders.FormatFromString(f.Extension())
951	return metadecoders.Default.Unmarshal(content, format)
952}
953
954func (h *HugoSites) findPagesByKindIn(kind string, inPages page.Pages) page.Pages {
955	return h.Sites[0].findPagesByKindIn(kind, inPages)
956}
957
958func (h *HugoSites) resetPageState() {
959	h.getContentMaps().walkBundles(func(n *contentNode) bool {
960		if n.p == nil {
961			return false
962		}
963		p := n.p
964		for _, po := range p.pageOutputs {
965			if po.cp == nil {
966				continue
967			}
968			po.cp.Reset()
969		}
970
971		return false
972	})
973}
974
975func (h *HugoSites) resetPageStateFromEvents(idset identity.Identities) {
976	h.getContentMaps().walkBundles(func(n *contentNode) bool {
977		if n.p == nil {
978			return false
979		}
980		p := n.p
981	OUTPUTS:
982		for _, po := range p.pageOutputs {
983			if po.cp == nil {
984				continue
985			}
986			for id := range idset {
987				if po.cp.dependencyTracker.Search(id) != nil {
988					po.cp.Reset()
989					continue OUTPUTS
990				}
991			}
992		}
993
994		if p.shortcodeState == nil {
995			return false
996		}
997
998		for _, s := range p.shortcodeState.shortcodes {
999			for _, templ := range s.templs {
1000				sid := templ.(identity.Manager)
1001				for id := range idset {
1002					if sid.Search(id) != nil {
1003						for _, po := range p.pageOutputs {
1004							if po.cp != nil {
1005								po.cp.Reset()
1006							}
1007						}
1008						return false
1009					}
1010				}
1011			}
1012		}
1013		return false
1014	})
1015}
1016
1017// Used in partial reloading to determine if the change is in a bundle.
1018type contentChangeMap struct {
1019	mu sync.RWMutex
1020
1021	// Holds directories with leaf bundles.
1022	leafBundles *radix.Tree
1023
1024	// Holds directories with branch bundles.
1025	branchBundles map[string]bool
1026
1027	pathSpec *helpers.PathSpec
1028
1029	// Hugo supports symlinked content (both directories and files). This
1030	// can lead to situations where the same file can be referenced from several
1031	// locations in /content -- which is really cool, but also means we have to
1032	// go an extra mile to handle changes.
1033	// This map is only used in watch mode.
1034	// It maps either file to files or the real dir to a set of content directories
1035	// where it is in use.
1036	symContentMu sync.Mutex
1037	symContent   map[string]map[string]bool
1038}
1039
1040func (m *contentChangeMap) add(dirname string, tp bundleDirType) {
1041	m.mu.Lock()
1042	if !strings.HasSuffix(dirname, helpers.FilePathSeparator) {
1043		dirname += helpers.FilePathSeparator
1044	}
1045	switch tp {
1046	case bundleBranch:
1047		m.branchBundles[dirname] = true
1048	case bundleLeaf:
1049		m.leafBundles.Insert(dirname, true)
1050	default:
1051		m.mu.Unlock()
1052		panic("invalid bundle type")
1053	}
1054	m.mu.Unlock()
1055}
1056
1057func (m *contentChangeMap) resolveAndRemove(filename string) (string, bundleDirType) {
1058	m.mu.RLock()
1059	defer m.mu.RUnlock()
1060
1061	// Bundles share resources, so we need to start from the virtual root.
1062	relFilename := m.pathSpec.RelContentDir(filename)
1063	dir, name := filepath.Split(relFilename)
1064	if !strings.HasSuffix(dir, helpers.FilePathSeparator) {
1065		dir += helpers.FilePathSeparator
1066	}
1067
1068	if _, found := m.branchBundles[dir]; found {
1069		delete(m.branchBundles, dir)
1070		return dir, bundleBranch
1071	}
1072
1073	if key, _, found := m.leafBundles.LongestPrefix(dir); found {
1074		m.leafBundles.Delete(key)
1075		dir = string(key)
1076		return dir, bundleLeaf
1077	}
1078
1079	fileTp, isContent := classifyBundledFile(name)
1080	if isContent && fileTp != bundleNot {
1081		// A new bundle.
1082		return dir, fileTp
1083	}
1084
1085	return dir, bundleNot
1086}
1087
1088func (m *contentChangeMap) addSymbolicLinkMapping(fim hugofs.FileMetaInfo) {
1089	meta := fim.Meta()
1090	if !meta.IsSymlink {
1091		return
1092	}
1093	m.symContentMu.Lock()
1094
1095	from, to := meta.Filename, meta.OriginalFilename
1096	if fim.IsDir() {
1097		if !strings.HasSuffix(from, helpers.FilePathSeparator) {
1098			from += helpers.FilePathSeparator
1099		}
1100	}
1101
1102	mm, found := m.symContent[from]
1103
1104	if !found {
1105		mm = make(map[string]bool)
1106		m.symContent[from] = mm
1107	}
1108	mm[to] = true
1109	m.symContentMu.Unlock()
1110}
1111
1112func (m *contentChangeMap) GetSymbolicLinkMappings(dir string) []string {
1113	mm, found := m.symContent[dir]
1114	if !found {
1115		return nil
1116	}
1117	dirs := make([]string, len(mm))
1118	i := 0
1119	for dir := range mm {
1120		dirs[i] = dir
1121		i++
1122	}
1123
1124	sort.Strings(dirs)
1125
1126	return dirs
1127}
1128