diff --git a/cache/filecache/filecache_config_test.go b/cache/filecache/filecache_config_test.go index 1ff3b8112..1ed020ef1 100644 --- a/cache/filecache/filecache_config_test.go +++ b/cache/filecache/filecache_config_test.go @@ -184,7 +184,7 @@ dir = "/" } func newTestConfig() config.Provider { - cfg := config.New() + cfg := config.NewWithTestDefaults() cfg.Set("workingDir", filepath.FromSlash("/my/cool/hugoproject")) cfg.Set("contentDir", "content") cfg.Set("dataDir", "data") diff --git a/cache/filecache/filecache_test.go b/cache/filecache/filecache_test.go index 6a051a264..47b5a7fcf 100644 --- a/cache/filecache/filecache_test.go +++ b/cache/filecache/filecache_test.go @@ -342,6 +342,7 @@ func newPathsSpec(t *testing.T, fs afero.Fs, configStr string) *helpers.PathSpec cfg, err := config.FromConfigString(configStr, "toml") c.Assert(err, qt.IsNil) initConfig(fs, cfg) + config.SetBaseTestDefaults(cfg) p, err := helpers.NewPathSpec(hugofs.NewFrom(fs, cfg), cfg, nil) c.Assert(err, qt.IsNil) return p diff --git a/commands/commandeer.go b/commands/commandeer.go index ced149e7a..1162a4b70 100644 --- a/commands/commandeer.go +++ b/commands/commandeer.go @@ -30,6 +30,7 @@ import ( "github.com/gohugoio/hugo/common/herrors" "github.com/gohugoio/hugo/common/hugo" + "github.com/gohugoio/hugo/common/paths" jww "github.com/spf13/jwalterweatherman" @@ -42,6 +43,7 @@ import ( "github.com/spf13/afero" "github.com/bep/debounce" + "github.com/bep/overlayfs" "github.com/gohugoio/hugo/common/types" "github.com/gohugoio/hugo/deps" "github.com/gohugoio/hugo/helpers" @@ -73,8 +75,10 @@ type commandeer struct { // be fast enough that we could maybe just add it for all server modes. changeDetector *fileChangeDetector - // We need to reuse this on server rebuilds. - destinationFs afero.Fs + // We need to reuse these on server rebuilds. + // These 2 will be different if --renderStaticToDisk is set. + publishDirFs afero.Fs + publishDirServerFs afero.Fs h *hugoBuilderCommon ftch flagsToConfigHandler @@ -162,7 +166,8 @@ func (c *commandeer) Set(key string, value any) { } func (c *commandeer) initFs(fs *hugofs.Fs) error { - c.destinationFs = fs.Destination + c.publishDirFs = fs.PublishDir + c.publishDirServerFs = fs.PublishDirServer c.DepsCfg.Fs = fs return nil @@ -378,28 +383,63 @@ func (c *commandeer) loadConfig() error { createMemFs := config.GetBool("renderToMemory") c.renderStaticToDisk = config.GetBool("renderStaticToDisk") - if createMemFs && !c.renderStaticToDisk { + if createMemFs { // Rendering to memoryFS, publish to Root regardless of publishDir. config.Set("publishDir", "/") + config.Set("publishDirStatic", "/") + } else if c.renderStaticToDisk { + // Hybrid, render dynamic content to Root. + config.Set("publishDirStatic", config.Get("publishDir")) + config.Set("publishDir", "/") + } c.fsCreate.Do(func() { fs := hugofs.NewFrom(sourceFs, config) - if c.destinationFs != nil { + if c.publishDirFs != nil { // Need to reuse the destination on server rebuilds. - fs.Destination = c.destinationFs - } else if createMemFs && c.renderStaticToDisk { - // Writes the dynamic output on memory, - // while serve others directly from publishDir + fs.PublishDir = c.publishDirFs + fs.PublishDirServer = c.publishDirServerFs + } else { publishDir := config.GetString("publishDir") - writableFs := afero.NewBasePathFs(afero.NewMemMapFs(), publishDir) - publicFs := afero.NewOsFs() - fs.Destination = afero.NewCopyOnWriteFs(afero.NewReadOnlyFs(publicFs), writableFs) - fs.DestinationStatic = publicFs - } else if createMemFs { - // Hugo writes the output to memory instead of the disk. - fs.Destination = new(afero.MemMapFs) + publishDirStatic := config.GetString("publishDirStatic") + workingDir := config.GetString("workingDir") + absPublishDir := paths.AbsPathify(workingDir, publishDir) + absPublishDirStatic := paths.AbsPathify(workingDir, publishDirStatic) + + if c.renderStaticToDisk { + // Writes the dynamic output oton memory, + // while serve others directly from /public on disk. + dynamicFs := afero.NewMemMapFs() + staticFs := afero.NewBasePathFs(afero.NewOsFs(), absPublishDirStatic) + + // Serve from both the static and dynamic fs, + // the first will take priority. + // THis is a read-only filesystem, + // we do all the writes to + // fs.Destination and fs.DestinationStatic. + fs.PublishDirServer = overlayfs.New( + overlayfs.Options{ + Fss: []afero.Fs{ + dynamicFs, + staticFs, + }, + }, + ) + fs.PublishDir = dynamicFs + fs.PublishDirStatic = staticFs + } else if createMemFs { + // Hugo writes the output to memory instead of the disk. + fs.PublishDir = new(afero.MemMapFs) + fs.PublishDirServer = fs.PublishDir + fs.PublishDirStatic = fs.PublishDir + } else { + // Write everything to disk. + fs.PublishDir = afero.NewBasePathFs(afero.NewOsFs(), absPublishDir) + fs.PublishDirServer = fs.PublishDir + fs.PublishDirStatic = fs.PublishDir + } } if c.fastRenderMode { @@ -413,15 +453,15 @@ func (c *commandeer) loadConfig() error { } changeDetector.PrepareNew() - fs.Destination = hugofs.NewHashingFs(fs.Destination, changeDetector) - fs.DestinationStatic = hugofs.NewHashingFs(fs.DestinationStatic, changeDetector) + fs.PublishDir = hugofs.NewHashingFs(fs.PublishDir, changeDetector) + fs.PublishDirStatic = hugofs.NewHashingFs(fs.PublishDirStatic, changeDetector) c.changeDetector = changeDetector } if c.Cfg.GetBool("logPathWarnings") { // Note that we only care about the "dynamic creates" here, // so skip the static fs. - fs.Destination = hugofs.NewCreateCountingFs(fs.Destination) + fs.PublishDir = hugofs.NewCreateCountingFs(fs.PublishDir) } // To debug hard-to-find path issues. diff --git a/commands/commands.go b/commands/commands.go index 01f076d1a..99b0866e5 100644 --- a/commands/commands.go +++ b/commands/commands.go @@ -18,10 +18,9 @@ import ( "os" "time" - "github.com/gohugoio/hugo/hugolib/paths" - "github.com/gohugoio/hugo/common/hugo" "github.com/gohugoio/hugo/common/loggers" + hpaths "github.com/gohugoio/hugo/common/paths" "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/helpers" "github.com/spf13/cobra" @@ -243,14 +242,14 @@ func (cc *hugoBuilderCommon) timeTrack(start time.Time, name string) { func (cc *hugoBuilderCommon) getConfigDir(baseDir string) string { if cc.cfgDir != "" { - return paths.AbsPathify(baseDir, cc.cfgDir) + return hpaths.AbsPathify(baseDir, cc.cfgDir) } if v, found := os.LookupEnv("HUGO_CONFIGDIR"); found { - return paths.AbsPathify(baseDir, v) + return hpaths.AbsPathify(baseDir, v) } - return paths.AbsPathify(baseDir, "config") + return hpaths.AbsPathify(baseDir, "config") } func (cc *hugoBuilderCommon) getEnvironment(isServer bool) string { diff --git a/commands/commands_test.go b/commands/commands_test.go index 43c7f8520..e3ec7bd99 100644 --- a/commands/commands_test.go +++ b/commands/commands_test.go @@ -22,8 +22,6 @@ import ( "github.com/gohugoio/hugo/config" - "github.com/gohugoio/hugo/htesting" - "github.com/spf13/afero" "github.com/gohugoio/hugo/hugofs" @@ -38,15 +36,13 @@ import ( func TestExecute(t *testing.T) { c := qt.New(t) - createSite := func(c *qt.C) (string, func()) { - dir, clean, err := createSimpleTestSite(t, testSiteConfig{}) - c.Assert(err, qt.IsNil) - return dir, clean + createSite := func(c *qt.C) string { + dir := createSimpleTestSite(t, testSiteConfig{}) + return dir } c.Run("hugo", func(c *qt.C) { - dir, clean := createSite(c) - defer clean() + dir := createSite(c) resp := Execute([]string{"-s=" + dir}) c.Assert(resp.Err, qt.IsNil) result := resp.Result @@ -56,8 +52,7 @@ func TestExecute(t *testing.T) { }) c.Run("hugo, set environment", func(c *qt.C) { - dir, clean := createSite(c) - defer clean() + dir := createSite(c) resp := Execute([]string{"-s=" + dir, "-e=staging"}) c.Assert(resp.Err, qt.IsNil) result := resp.Result @@ -65,9 +60,8 @@ func TestExecute(t *testing.T) { }) c.Run("convert toJSON", func(c *qt.C) { - dir, clean := createSite(c) + dir := createSite(c) output := filepath.Join(dir, "myjson") - defer clean() resp := Execute([]string{"convert", "toJSON", "-s=" + dir, "-e=staging", "-o=" + output}) c.Assert(resp.Err, qt.IsNil) converted := readFileFrom(c, filepath.Join(output, "content", "p1.md")) @@ -75,8 +69,7 @@ func TestExecute(t *testing.T) { }) c.Run("config, set environment", func(c *qt.C) { - dir, clean := createSite(c) - defer clean() + dir := createSite(c) out, err := captureStdout(func() error { resp := Execute([]string{"config", "-s=" + dir, "-e=staging"}) return resp.Err @@ -86,16 +79,14 @@ func TestExecute(t *testing.T) { }) c.Run("deploy, environment set", func(c *qt.C) { - dir, clean := createSite(c) - defer clean() + dir := createSite(c) resp := Execute([]string{"deploy", "-s=" + dir, "-e=staging", "--target=mydeployment", "--dryRun"}) c.Assert(resp.Err, qt.Not(qt.IsNil)) c.Assert(resp.Err.Error(), qt.Contains, `no driver registered for "hugocloud"`) }) c.Run("list", func(c *qt.C) { - dir, clean := createSite(c) - defer clean() + dir := createSite(c) out, err := captureStdout(func() error { resp := Execute([]string{"list", "all", "-s=" + dir, "-e=staging"}) return resp.Err @@ -105,8 +96,7 @@ func TestExecute(t *testing.T) { }) c.Run("new theme", func(c *qt.C) { - dir, clean := createSite(c) - defer clean() + dir := createSite(c) themesDir := filepath.Join(dir, "mythemes") resp := Execute([]string{"new", "theme", "mytheme", "-s=" + dir, "-e=staging", "--themesDir=" + themesDir}) c.Assert(resp.Err, qt.IsNil) @@ -115,8 +105,7 @@ func TestExecute(t *testing.T) { }) c.Run("new site", func(c *qt.C) { - dir, clean := createSite(c) - defer clean() + dir := createSite(c) siteDir := filepath.Join(dir, "mysite") resp := Execute([]string{"new", "site", siteDir, "-e=staging"}) c.Assert(resp.Err, qt.IsNil) @@ -167,7 +156,7 @@ func TestFlags(t *testing.T) { name: "ignoreVendorPaths", args: []string{"server", "--ignoreVendorPaths=github.com/**"}, check: func(c *qt.C, cmd *serverCmd) { - cfg := config.New() + cfg := config.NewWithTestDefaults() cmd.flagsToConfig(cfg) c.Assert(cfg.Get("ignoreVendorPaths"), qt.Equals, "github.com/**") }, @@ -208,7 +197,7 @@ func TestFlags(t *testing.T) { c.Assert(sc.serverPort, qt.Equals, 1366) c.Assert(sc.environment, qt.Equals, "testing") - cfg := config.New() + cfg := config.NewWithTestDefaults() sc.flagsToConfig(cfg) c.Assert(cfg.GetString("publishDir"), qt.Equals, "/tmp/mydestination") c.Assert(cfg.GetString("contentDir"), qt.Equals, "mycontent") @@ -253,14 +242,8 @@ func TestFlags(t *testing.T) { func TestCommandsExecute(t *testing.T) { c := qt.New(t) - dir, clean, err := createSimpleTestSite(t, testSiteConfig{}) - c.Assert(err, qt.IsNil) - - dirOut, clean2, err := htesting.CreateTempDir(hugofs.Os, "hugo-cli-out") - c.Assert(err, qt.IsNil) - - defer clean() - defer clean2() + dir := createSimpleTestSite(t, testSiteConfig{}) + dirOut := t.TempDir() sourceFlag := fmt.Sprintf("-s=%s", dir) @@ -297,29 +280,35 @@ func TestCommandsExecute(t *testing.T) { } for _, test := range tests { - b := newCommandsBuilder().addAll().build() - hugoCmd := b.getCommand() - test.flags = append(test.flags, "--quiet") - hugoCmd.SetArgs(append(test.commands, test.flags...)) - - // TODO(bep) capture output and add some simple asserts - // TODO(bep) misspelled subcommands does not return an error. We should investigate this - // but before that, check for "Error: unknown command". - - _, err := hugoCmd.ExecuteC() - if test.expectErrToContain != "" { - c.Assert(err, qt.Not(qt.IsNil)) - c.Assert(err.Error(), qt.Contains, test.expectErrToContain) - } else { - c.Assert(err, qt.IsNil) + name := "hugo" + if len(test.commands) > 0 { + name = test.commands[0] } + c.Run(name, func(c *qt.C) { + b := newCommandsBuilder().addAll().build() + hugoCmd := b.getCommand() + test.flags = append(test.flags, "--quiet") + hugoCmd.SetArgs(append(test.commands, test.flags...)) - // Assert that we have not left any development debug artifacts in - // the code. - if b.c != nil { - _, ok := b.c.destinationFs.(types.DevMarker) - c.Assert(ok, qt.Equals, false) - } + // TODO(bep) capture output and add some simple asserts + // TODO(bep) misspelled subcommands does not return an error. We should investigate this + // but before that, check for "Error: unknown command". + + _, err := hugoCmd.ExecuteC() + if test.expectErrToContain != "" { + c.Assert(err, qt.Not(qt.IsNil)) + c.Assert(err.Error(), qt.Contains, test.expectErrToContain) + } else { + c.Assert(err, qt.IsNil) + } + + // Assert that we have not left any development debug artifacts in + // the code. + if b.c != nil { + _, ok := b.c.publishDirFs.(types.DevMarker) + c.Assert(ok, qt.Equals, false) + } + }) } } @@ -329,11 +318,8 @@ type testSiteConfig struct { contentDir string } -func createSimpleTestSite(t testing.TB, cfg testSiteConfig) (string, func(), error) { - d, clean, e := htesting.CreateTempDir(hugofs.Os, "hugo-cli") - if e != nil { - return "", nil, e - } +func createSimpleTestSite(t testing.TB, cfg testSiteConfig) string { + dir := t.TempDir() cfgStr := ` @@ -352,23 +338,23 @@ title = "Hugo Commands" contentDir = cfg.contentDir } - os.MkdirAll(filepath.Join(d, "public"), 0777) + os.MkdirAll(filepath.Join(dir, "public"), 0777) // Just the basic. These are for CLI tests, not site testing. - writeFile(t, filepath.Join(d, "config.toml"), cfgStr) - writeFile(t, filepath.Join(d, "config", "staging", "params.toml"), `myparam="paramstaging"`) - writeFile(t, filepath.Join(d, "config", "staging", "deployment.toml"), ` + writeFile(t, filepath.Join(dir, "config.toml"), cfgStr) + writeFile(t, filepath.Join(dir, "config", "staging", "params.toml"), `myparam="paramstaging"`) + writeFile(t, filepath.Join(dir, "config", "staging", "deployment.toml"), ` [[targets]] name = "mydeployment" URL = "hugocloud://hugotestbucket" `) - writeFile(t, filepath.Join(d, "config", "testing", "params.toml"), `myparam="paramtesting"`) - writeFile(t, filepath.Join(d, "config", "production", "params.toml"), `myparam="paramproduction"`) + writeFile(t, filepath.Join(dir, "config", "testing", "params.toml"), `myparam="paramtesting"`) + writeFile(t, filepath.Join(dir, "config", "production", "params.toml"), `myparam="paramproduction"`) - writeFile(t, filepath.Join(d, "static", "myfile.txt"), `Hello World!`) + writeFile(t, filepath.Join(dir, "static", "myfile.txt"), `Hello World!`) - writeFile(t, filepath.Join(d, contentDir, "p1.md"), ` + writeFile(t, filepath.Join(dir, contentDir, "p1.md"), ` --- title: "P1" weight: 1 @@ -378,20 +364,20 @@ Content `) - writeFile(t, filepath.Join(d, "layouts", "_default", "single.html"), ` + writeFile(t, filepath.Join(dir, "layouts", "_default", "single.html"), ` Single: {{ .Title }} `) - writeFile(t, filepath.Join(d, "layouts", "_default", "list.html"), ` + writeFile(t, filepath.Join(dir, "layouts", "_default", "list.html"), ` List: {{ .Title }} Environment: {{ hugo.Environment }} `) - return d, clean, nil + return dir } func writeFile(t testing.TB, filename, content string) { diff --git a/commands/hugo.go b/commands/hugo.go index 21140fa43..9033fac90 100644 --- a/commands/hugo.go +++ b/commands/hugo.go @@ -508,7 +508,7 @@ func (c *commandeer) build() error { c.hugo().PrintProcessingStats(os.Stdout) fmt.Println() - if createCounter, ok := c.destinationFs.(hugofs.DuplicatesReporter); ok { + if createCounter, ok := c.publishDirFs.(hugofs.DuplicatesReporter); ok { dupes := createCounter.ReportDuplicates() if dupes != "" { c.logger.Warnln("Duplicate target paths:", dupes) @@ -634,11 +634,7 @@ func chmodFilter(dst, src os.FileInfo) bool { } func (c *commandeer) copyStaticTo(sourceFs *filesystems.SourceFilesystem) (uint64, error) { - publishDir := c.hugo().PathSpec.PublishDir - // If root, remove the second '/' - if publishDir == "//" { - publishDir = helpers.FilePathSeparator - } + publishDir := helpers.FilePathSeparator if sourceFs.PublishFolder != "" { publishDir = filepath.Join(publishDir, sourceFs.PublishFolder) @@ -651,9 +647,9 @@ func (c *commandeer) copyStaticTo(sourceFs *filesystems.SourceFilesystem) (uint6 syncer.NoChmod = c.Cfg.GetBool("noChmod") syncer.ChmodFilter = chmodFilter syncer.SrcFs = fs - syncer.DestFs = c.Fs.Destination + syncer.DestFs = c.Fs.PublishDir if c.renderStaticToDisk { - syncer.DestFs = c.Fs.DestinationStatic + syncer.DestFs = c.Fs.PublishDirStatic } // Now that we are using a unionFs for the static directories // We can effectively clean the publishDir on initial sync diff --git a/commands/hugo_test.go b/commands/hugo_test.go index 4bead09f0..aca3de2fd 100644 --- a/commands/hugo_test.go +++ b/commands/hugo_test.go @@ -36,12 +36,10 @@ title = "Hugo Commands" contentDir = "thisdoesnotexist" ` - dir, clean, err := createSimpleTestSite(t, testSiteConfig{configTOML: cfgStr, contentDir: contentDir}) - c.Assert(err, qt.IsNil) - defer clean() + dir := createSimpleTestSite(t, testSiteConfig{configTOML: cfgStr, contentDir: contentDir}) cmd.SetArgs([]string{"-s=" + dir, "-c=" + contentDir}) - _, err = cmd.ExecuteC() + _, err := cmd.ExecuteC() c.Assert(err, qt.IsNil) } diff --git a/commands/list_test.go b/commands/list_test.go index 6f3d6c74d..56a3ee9b9 100644 --- a/commands/list_test.go +++ b/commands/list_test.go @@ -29,10 +29,7 @@ func captureStdout(f func() error) (string, error) { func TestListAll(t *testing.T) { c := qt.New(t) - dir, clean, err := createSimpleTestSite(t, testSiteConfig{}) - defer clean() - - c.Assert(err, qt.IsNil) + dir := createSimpleTestSite(t, testSiteConfig{}) hugoCmd := newCommandsBuilder().addAll().build() cmd := hugoCmd.getCommand() diff --git a/commands/new_site.go b/commands/new_site.go index 1e3ed710b..e49a60202 100644 --- a/commands/new_site.go +++ b/commands/new_site.go @@ -122,8 +122,10 @@ func (n *newSiteCmd) newSite(cmd *cobra.Command, args []string) error { } forceNew, _ := cmd.Flags().GetBool("force") - - return n.doNewSite(hugofs.NewDefault(config.New()), createpath, forceNew) + cfg := config.New() + cfg.Set("workingDir", createpath) + cfg.Set("publishDir", "public") + return n.doNewSite(hugofs.NewDefault(cfg), createpath, forceNew) } func createConfig(fs *hugofs.Fs, inpath string, kind string) (err error) { diff --git a/commands/server.go b/commands/server.go index da1918ede..145a889bb 100644 --- a/commands/server.go +++ b/commands/server.go @@ -23,6 +23,7 @@ import ( "net/url" "os" "os/signal" + "path" "path/filepath" "regexp" "runtime" @@ -148,7 +149,7 @@ func (sc *serverCmd) server(cmd *cobra.Command, args []string) error { var serverCfgInit sync.Once cfgInit := func(c *commandeer) (rerr error) { - c.Set("renderToMemory", !sc.renderToDisk) + c.Set("renderToMemory", !(sc.renderToDisk || sc.renderStaticToDisk)) c.Set("renderStaticToDisk", sc.renderStaticToDisk) if cmd.Flags().Changed("navigateToChanged") { c.Set("navigateToChanged", sc.navigateToChanged) @@ -330,13 +331,18 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string port := f.c.serverPorts[i].p listener := f.c.serverPorts[i].ln + // For logging only. + // TODO(bep) consolidate. publishDir := f.c.Cfg.GetString("publishDir") + publishDirStatic := f.c.Cfg.GetString("publishDirStatic") + workingDir := f.c.Cfg.GetString("workingDir") if root != "" { publishDir = filepath.Join(publishDir, root) + publishDirStatic = filepath.Join(publishDirStatic, root) } - - absPublishDir := f.c.hugo().PathSpec.AbsPathify(publishDir) + absPublishDir := paths.AbsPathify(workingDir, publishDir) + absPublishDirStatic := paths.AbsPathify(workingDir, publishDirStatic) jww.FEEDBACK.Printf("Environment: %q", f.c.hugo().Deps.Site.Hugo().Environment) @@ -344,14 +350,14 @@ func (f *fileServer) createEndpoint(i int) (*http.ServeMux, net.Listener, string if f.s.renderToDisk { jww.FEEDBACK.Println("Serving pages from " + absPublishDir) } else if f.s.renderStaticToDisk { - jww.FEEDBACK.Println("Serving pages from memory and static files from " + absPublishDir) + jww.FEEDBACK.Println("Serving pages from memory and static files from " + absPublishDirStatic) } else { jww.FEEDBACK.Println("Serving pages from memory") } } - httpFs := afero.NewHttpFs(f.c.destinationFs) - fs := filesOnlyFs{httpFs.Dir(absPublishDir)} + httpFs := afero.NewHttpFs(f.c.publishDirServerFs) + fs := filesOnlyFs{httpFs.Dir(path.Join("/", root))} if i == 0 && f.c.fastRenderMode { jww.FEEDBACK.Println("Running in Fast Render Mode. For full rebuilds on change: hugo server --disableFastRender") diff --git a/commands/server_test.go b/commands/server_test.go index 6972bbe69..ea50afd94 100644 --- a/commands/server_test.go +++ b/commands/server_test.go @@ -77,6 +77,9 @@ func TestServerFlags(t *testing.T) { {"--renderToDisk", func(c *qt.C, r serverTestResult) { assertPublic(c, r, true) }}, + {"--renderStaticToDisk", func(c *qt.C, r serverTestResult) { + assertPublic(c, r, true) + }}, } { c.Run(test.flag, func(c *qt.C) { config := ` @@ -105,9 +108,7 @@ type serverTestResult struct { } func runServerTest(c *qt.C, getHome bool, config string, args ...string) (result serverTestResult) { - dir, clean, err := createSimpleTestSite(c, testSiteConfig{configTOML: config}) - defer clean() - c.Assert(err, qt.IsNil) + dir := createSimpleTestSite(c, testSiteConfig{configTOML: config}) sp, err := helpers.FindAvailablePort() c.Assert(err, qt.IsNil) @@ -141,12 +142,15 @@ func runServerTest(c *qt.C, getHome bool, config string, args ...string) (result time.Sleep(567 * time.Millisecond) resp, err := http.Get(fmt.Sprintf("http://localhost:%d/", port)) c.Check(err, qt.IsNil) + c.Check(resp.StatusCode, qt.Equals, http.StatusOK) if err == nil { defer resp.Body.Close() result.homeContent = helpers.ReaderToString(resp.Body) } } + time.Sleep(1 * time.Second) + select { case <-stop: case stop <- true: @@ -191,7 +195,7 @@ func TestFixURL(t *testing.T) { t.Run(test.TestName, func(t *testing.T) { b := newCommandsBuilder() s := b.newServerCmd() - v := config.New() + v := config.NewWithTestDefaults() baseURL := test.CLIBaseURL v.Set("baseURL", test.CfgBaseURL) s.serverAppend = test.AppendPort diff --git a/commands/static_syncer.go b/commands/static_syncer.go index 2eb2b6662..b97c4df7a 100644 --- a/commands/static_syncer.go +++ b/commands/static_syncer.go @@ -40,11 +40,7 @@ func (s *staticSyncer) syncsStaticEvents(staticEvents []fsnotify.Event) error { c := s.c syncFn := func(sourceFs *filesystems.SourceFilesystem) (uint64, error) { - publishDir := c.hugo().PathSpec.PublishDir - // If root, remove the second '/' - if publishDir == "//" { - publishDir = helpers.FilePathSeparator - } + publishDir := helpers.FilePathSeparator if sourceFs.PublishFolder != "" { publishDir = filepath.Join(publishDir, sourceFs.PublishFolder) @@ -55,9 +51,9 @@ func (s *staticSyncer) syncsStaticEvents(staticEvents []fsnotify.Event) error { syncer.NoChmod = c.Cfg.GetBool("noChmod") syncer.ChmodFilter = chmodFilter syncer.SrcFs = sourceFs.Fs - syncer.DestFs = c.Fs.Destination + syncer.DestFs = c.Fs.PublishDir if c.renderStaticToDisk { - syncer.DestFs = c.Fs.DestinationStatic + syncer.DestFs = c.Fs.PublishDirStatic } // prevent spamming the log on changes @@ -101,19 +97,14 @@ func (s *staticSyncer) syncsStaticEvents(staticEvents []fsnotify.Event) error { if ev.Op&fsnotify.Rename == fsnotify.Rename || ev.Op&fsnotify.Remove == fsnotify.Remove { if _, err := sourceFs.Fs.Stat(relPath); os.IsNotExist(err) { // If file doesn't exist in any static dir, remove it - toRemove := filepath.Join(publishDir, relPath) + logger.Println("File no longer exists in static dir, removing", relPath) + _ = c.Fs.PublishDirStatic.RemoveAll(relPath) - logger.Println("File no longer exists in static dir, removing", toRemove) - if c.renderStaticToDisk { - _ = c.Fs.DestinationStatic.RemoveAll(toRemove) - } else { - _ = c.Fs.Destination.RemoveAll(toRemove) - } } else if err == nil { // If file still exists, sync it logger.Println("Syncing", relPath, "to", publishDir) - if err := syncer.Sync(filepath.Join(publishDir, relPath), relPath); err != nil { + if err := syncer.Sync(relPath, relPath); err != nil { c.logger.Errorln(err) } } else { diff --git a/common/paths/path.go b/common/paths/path.go index 63e831ff6..3a7f3e790 100644 --- a/common/paths/path.go +++ b/common/paths/path.go @@ -63,6 +63,15 @@ func (filepathBridge) Separator() string { var fpb filepathBridge +// AbsPathify creates an absolute path if given a working dir and a relative path. +// If already absolute, the path is just cleaned. +func AbsPathify(workingDir, inPath string) string { + if filepath.IsAbs(inPath) { + return filepath.Clean(inPath) + } + return filepath.Join(workingDir, inPath) +} + // MakeTitle converts the path given to a suitable title, trimming whitespace // and replacing hyphens with whitespace. func MakeTitle(inpath string) string { diff --git a/config/configProvider.go b/config/configProvider.go index cf4c3dab3..01a2e8c54 100644 --- a/config/configProvider.go +++ b/config/configProvider.go @@ -45,13 +45,23 @@ func GetStringSlicePreserveString(cfg Provider, key string) []string { } // SetBaseTestDefaults provides some common config defaults used in tests. -func SetBaseTestDefaults(cfg Provider) { - cfg.Set("resourceDir", "resources") - cfg.Set("contentDir", "content") - cfg.Set("dataDir", "data") - cfg.Set("i18nDir", "i18n") - cfg.Set("layoutDir", "layouts") - cfg.Set("assetDir", "assets") - cfg.Set("archetypeDir", "archetypes") - cfg.Set("publishDir", "public") +func SetBaseTestDefaults(cfg Provider) Provider { + setIfNotSet(cfg, "baseURL", "https://example.org") + setIfNotSet(cfg, "resourceDir", "resources") + setIfNotSet(cfg, "contentDir", "content") + setIfNotSet(cfg, "dataDir", "data") + setIfNotSet(cfg, "i18nDir", "i18n") + setIfNotSet(cfg, "layoutDir", "layouts") + setIfNotSet(cfg, "assetDir", "assets") + setIfNotSet(cfg, "archetypeDir", "archetypes") + setIfNotSet(cfg, "publishDir", "public") + setIfNotSet(cfg, "workingDir", "") + setIfNotSet(cfg, "defaultContentLanguage", "en") + return cfg +} + +func setIfNotSet(cfg Provider, key string, value any) { + if !cfg.IsSet(key) { + cfg.Set(key, value) + } } diff --git a/config/defaultConfigProvider.go b/config/defaultConfigProvider.go index 46f083ed2..822f421fa 100644 --- a/config/defaultConfigProvider.go +++ b/config/defaultConfigProvider.go @@ -75,6 +75,11 @@ func NewFrom(params maps.Params) Provider { } } +// NewWithTestDefaults is used in tests only. +func NewWithTestDefaults() Provider { + return SetBaseTestDefaults(New()) +} + // defaultConfigProvider is a Provider backed by a map where all keys are lower case. // All methods are thread safe. type defaultConfigProvider struct { diff --git a/config/services/servicesConfig_test.go b/config/services/servicesConfig_test.go index 12b042a5a..826255e73 100644 --- a/config/services/servicesConfig_test.go +++ b/config/services/servicesConfig_test.go @@ -54,7 +54,7 @@ disableInlineCSS = true func TestUseSettingsFromRootIfSet(t *testing.T) { c := qt.New(t) - cfg := config.New() + cfg := config.NewWithTestDefaults() cfg.Set("disqusShortname", "root_short") cfg.Set("googleAnalytics", "ga_root") diff --git a/go.mod b/go.mod index 0d582cf76..a4308ae69 100644 --- a/go.mod +++ b/go.mod @@ -48,7 +48,7 @@ require ( github.com/russross/blackfriday v1.6.0 github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd github.com/sanity-io/litter v1.5.4 - github.com/spf13/afero v1.8.1 + github.com/spf13/afero v1.8.2 github.com/spf13/cast v1.4.1 github.com/spf13/cobra v1.4.0 github.com/spf13/fsync v0.9.0 @@ -98,6 +98,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/sso v1.11.3 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.16.3 // indirect github.com/aws/smithy-go v1.11.2 // indirect + github.com/bep/overlayfs v0.1.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect github.com/dlclark/regexp2 v1.4.0 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect diff --git a/go.sum b/go.sum index 3cb857003..0caef4d66 100644 --- a/go.sum +++ b/go.sum @@ -186,6 +186,8 @@ github.com/bep/golibsass v1.0.0 h1:gNguBMSDi5yZEZzVZP70YpuFQE3qogJIGUlrVILTmOw= github.com/bep/golibsass v1.0.0/go.mod h1:DL87K8Un/+pWUS75ggYv41bliGiolxzDKWJAq3eJ1MA= github.com/bep/gowebp v0.1.0 h1:4/iQpfnxHyXs3x/aTxMMdOpLEQQhFmF6G7EieWPTQyo= github.com/bep/gowebp v0.1.0/go.mod h1:ZhFodwdiFp8ehGJpF4LdPl6unxZm9lLFjxD3z2h2AgI= +github.com/bep/overlayfs v0.1.0 h1:1hOCrvS4E5Hf0qwxM7m+9oitqClD9mRjQ1d4pECsVcU= +github.com/bep/overlayfs v0.1.0/go.mod h1:NFjSmn3kCqG7KX2Lmz8qT8VhPPCwZap3UNogXawoQHM= github.com/bep/tmc v0.5.1 h1:CsQnSC6MsomH64gw0cT5f+EwQDcvZz4AazKunFwTpuI= github.com/bep/tmc v0.5.1/go.mod h1:tGYHN8fS85aJPhDLgXETVKp+PR382OvFi2+q2GkGsq0= github.com/bep/workers v1.0.0 h1:U+H8YmEaBCEaFZBst7GcRVEoqeRC9dzH2dWOwGmOchg= @@ -564,6 +566,8 @@ github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasO github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= github.com/spf13/afero v1.8.1 h1:izYHOT71f9iZ7iq37Uqjael60/vYC6vMtzedudZ0zEk= github.com/spf13/afero v1.8.1/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= +github.com/spf13/afero v1.8.2 h1:xehSyVa0YnHWsJ49JFljMpg1HX19V6NDZ1fkm1Xznbo= +github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q= diff --git a/helpers/content_test.go b/helpers/content_test.go index c1ff5c1d2..4b67b44f0 100644 --- a/helpers/content_test.go +++ b/helpers/content_test.go @@ -19,10 +19,10 @@ import ( "strings" "testing" - "github.com/gohugoio/hugo/config" "github.com/spf13/afero" "github.com/gohugoio/hugo/common/loggers" + "github.com/gohugoio/hugo/config" qt "github.com/frankban/quicktest" ) @@ -102,7 +102,7 @@ func TestBytesToHTML(t *testing.T) { } func TestNewContentSpec(t *testing.T) { - cfg := config.New() + cfg := config.NewWithTestDefaults() c := qt.New(t) cfg.Set("summaryLength", 32) diff --git a/helpers/general_test.go b/helpers/general_test.go index be9834d3f..75119f01d 100644 --- a/helpers/general_test.go +++ b/helpers/general_test.go @@ -20,9 +20,8 @@ import ( "testing" "time" - "github.com/gohugoio/hugo/config" - "github.com/gohugoio/hugo/common/loggers" + "github.com/gohugoio/hugo/config" qt "github.com/frankban/quicktest" "github.com/spf13/afero" @@ -30,7 +29,7 @@ import ( func TestResolveMarkup(t *testing.T) { c := qt.New(t) - cfg := config.New() + cfg := config.NewWithTestDefaults() spec, err := NewContentSpec(cfg, loggers.NewErrorLogger(), afero.NewMemMapFs(), nil) c.Assert(err, qt.IsNil) diff --git a/helpers/path.go b/helpers/path.go index b302b1569..73970d558 100644 --- a/helpers/path.go +++ b/helpers/path.go @@ -459,9 +459,17 @@ func IsDir(path string, fs afero.Fs) (bool, error) { return afero.IsDir(fs, path) } -// IsEmpty checks if a given path is empty. +// IsEmpty checks if a given path is empty, meaning it doesn't contain any regular files. func IsEmpty(path string, fs afero.Fs) (bool, error) { - return afero.IsEmpty(fs, path) + var hasFile bool + err := afero.Walk(fs, path, func(path string, info os.FileInfo, err error) error { + if info.IsDir() { + return nil + } + hasFile = true + return filepath.SkipDir + }) + return !hasFile, err } // Exists checks if a file or directory exists. diff --git a/helpers/path_test.go b/helpers/path_test.go index 6a119a741..3d0617f54 100644 --- a/helpers/path_test.go +++ b/helpers/path_test.go @@ -256,55 +256,6 @@ func TestIsDir(t *testing.T) { } } -func TestIsEmpty(t *testing.T) { - zeroSizedFile, _ := createZeroSizedFileInTempDir() - defer deleteFileInTempDir(zeroSizedFile) - nonZeroSizedFile, _ := createNonZeroSizedFileInTempDir() - defer deleteFileInTempDir(nonZeroSizedFile) - emptyDirectory, _ := createEmptyTempDir() - defer deleteTempDir(emptyDirectory) - nonEmptyZeroLengthFilesDirectory, _ := createTempDirWithZeroLengthFiles() - defer deleteTempDir(nonEmptyZeroLengthFilesDirectory) - nonEmptyNonZeroLengthFilesDirectory, _ := createTempDirWithNonZeroLengthFiles() - defer deleteTempDir(nonEmptyNonZeroLengthFilesDirectory) - nonExistentFile := os.TempDir() + "/this-file-does-not-exist.txt" - nonExistentDir := os.TempDir() + "/this/directory/does/not/exist/" - - fileDoesNotExist := fmt.Errorf("%q path does not exist", nonExistentFile) - dirDoesNotExist := fmt.Errorf("%q path does not exist", nonExistentDir) - - type test struct { - input string - expectedResult bool - expectedErr error - } - - data := []test{ - {zeroSizedFile.Name(), true, nil}, - {nonZeroSizedFile.Name(), false, nil}, - {emptyDirectory, true, nil}, - {nonEmptyZeroLengthFilesDirectory, false, nil}, - {nonEmptyNonZeroLengthFilesDirectory, false, nil}, - {nonExistentFile, false, fileDoesNotExist}, - {nonExistentDir, false, dirDoesNotExist}, - } - for i, d := range data { - exists, err := IsEmpty(d.input, new(afero.OsFs)) - if d.expectedResult != exists { - t.Errorf("Test %d failed. Expected result %t got %t", i, d.expectedResult, exists) - } - if d.expectedErr != nil { - if d.expectedErr.Error() != err.Error() { - t.Errorf("Test %d failed. Expected %q(%#v) got %q(%#v)", i, d.expectedErr, d.expectedErr, err, err) - } - } else { - if d.expectedErr != err { - t.Errorf("Test %d failed. Expected %q(%#v) got %q(%#v)", i, d.expectedErr, d.expectedErr, err, err) - } - } - } -} - func createZeroSizedFileInTempDir() (*os.File, error) { filePrefix := "_path_test_" f, e := ioutil.TempFile("", filePrefix) // dir is os.TempDir() @@ -346,51 +297,6 @@ func createEmptyTempDir() (string, error) { return d, nil } -func createTempDirWithZeroLengthFiles() (string, error) { - d, dirErr := createEmptyTempDir() - if dirErr != nil { - return "", dirErr - } - filePrefix := "_path_test_" - _, fileErr := ioutil.TempFile(d, filePrefix) // dir is os.TempDir() - if fileErr != nil { - // if there was an error no file was created. - // but we need to remove the directory to clean-up - deleteTempDir(d) - return "", fileErr - } - // the dir now has one, zero length file in it - return d, nil -} - -func createTempDirWithNonZeroLengthFiles() (string, error) { - d, dirErr := createEmptyTempDir() - if dirErr != nil { - return "", dirErr - } - filePrefix := "_path_test_" - f, fileErr := ioutil.TempFile(d, filePrefix) // dir is os.TempDir() - if fileErr != nil { - // if there was an error no file was created. - // but we need to remove the directory to clean-up - deleteTempDir(d) - return "", fileErr - } - byteString := []byte("byteString") - - fileErr = ioutil.WriteFile(f.Name(), byteString, 0644) - if fileErr != nil { - // delete the file - deleteFileInTempDir(f) - // also delete the directory - deleteTempDir(d) - return "", fileErr - } - - // the dir now has one, zero length file in it - return d, nil -} - func deleteTempDir(d string) { _ = os.RemoveAll(d) } diff --git a/helpers/testhelpers_test.go b/helpers/testhelpers_test.go index 1d1bf73ec..00be3db25 100644 --- a/helpers/testhelpers_test.go +++ b/helpers/testhelpers_test.go @@ -17,9 +17,8 @@ func newTestPathSpec(fs *hugofs.Fs, v config.Provider) *PathSpec { } func newTestDefaultPathSpec(configKeyValues ...any) *PathSpec { - v := config.New() - fs := hugofs.NewMem(v) cfg := newTestCfg() + fs := hugofs.NewMem(cfg) for i := 0; i < len(configKeyValues); i += 2 { cfg.Set(configKeyValues[i].(string), configKeyValues[i+1]) @@ -28,15 +27,7 @@ func newTestDefaultPathSpec(configKeyValues ...any) *PathSpec { } func newTestCfg() config.Provider { - v := config.New() - v.Set("contentDir", "content") - v.Set("dataDir", "data") - v.Set("i18nDir", "i18n") - v.Set("layoutDir", "layouts") - v.Set("assetDir", "assets") - v.Set("resourceDir", "resources") - v.Set("publishDir", "public") - v.Set("archetypeDir", "archetypes") + v := config.NewWithTestDefaults() langs.LoadLanguageSettings(v, nil) langs.LoadLanguageSettings(v, nil) mod, err := modules.CreateProjectModule(v) @@ -49,7 +40,7 @@ func newTestCfg() config.Provider { } func newTestContentSpec() *ContentSpec { - v := config.New() + v := config.NewWithTestDefaults() spec, err := NewContentSpec(v, loggers.NewErrorLogger(), afero.NewMemMapFs(), nil) if err != nil { panic(err) diff --git a/hugofs/createcounting_fs.go b/hugofs/createcounting_fs.go index 802806b7a..1737ad5ce 100644 --- a/hugofs/createcounting_fs.go +++ b/hugofs/createcounting_fs.go @@ -33,10 +33,18 @@ type DuplicatesReporter interface { ReportDuplicates() string } +var ( + _ FilesystemUnwrapper = (*createCountingFs)(nil) +) + func NewCreateCountingFs(fs afero.Fs) afero.Fs { return &createCountingFs{Fs: fs, fileCount: make(map[string]int)} } +func (fs *createCountingFs) UnwrapFilesystem() afero.Fs { + return fs.Fs +} + // ReportDuplicates reports filenames written more than once. func (c *createCountingFs) ReportDuplicates() string { c.mu.Lock() diff --git a/hugofs/decorators.go b/hugofs/decorators.go index 364a3e23e..be0ae495d 100644 --- a/hugofs/decorators.go +++ b/hugofs/decorators.go @@ -23,6 +23,10 @@ import ( "github.com/spf13/afero" ) +var ( + _ FilesystemUnwrapper = (*baseFileDecoratorFs)(nil) +) + func decorateDirs(fs afero.Fs, meta *FileMeta) afero.Fs { ffs := &baseFileDecoratorFs{Fs: fs} @@ -151,6 +155,10 @@ type baseFileDecoratorFs struct { decorate func(fi os.FileInfo, filename string) (os.FileInfo, error) } +func (fs *baseFileDecoratorFs) UnwrapFilesystem() afero.Fs { + return fs.Fs +} + func (fs *baseFileDecoratorFs) Stat(name string) (os.FileInfo, error) { fi, err := fs.Fs.Stat(name) if err != nil { diff --git a/hugofs/filename_filter_fs.go b/hugofs/filename_filter_fs.go index 2a11335a3..4ecd1f55a 100644 --- a/hugofs/filename_filter_fs.go +++ b/hugofs/filename_filter_fs.go @@ -23,6 +23,10 @@ import ( "github.com/spf13/afero" ) +var ( + _ FilesystemUnwrapper = (*filenameFilterFs)(nil) +) + func newFilenameFilterFs(fs afero.Fs, base string, filter *glob.FilenameFilter) afero.Fs { return &filenameFilterFs{ fs: fs, @@ -39,6 +43,10 @@ type filenameFilterFs struct { filter *glob.FilenameFilter } +func (fs *filenameFilterFs) UnwrapFilesystem() afero.Fs { + return fs.fs +} + func (fs *filenameFilterFs) LstatIfPossible(name string) (os.FileInfo, bool, error) { fi, b, err := fs.fs.(afero.Lstater).LstatIfPossible(name) if err != nil { diff --git a/hugofs/filter_fs.go b/hugofs/filter_fs.go index ec3897d9e..351b4d0f7 100644 --- a/hugofs/filter_fs.go +++ b/hugofs/filter_fs.go @@ -121,6 +121,10 @@ func NewFilterFs(fs afero.Fs) (afero.Fs, error) { return ffs, nil } +var ( + _ FilesystemUnwrapper = (*FilterFs)(nil) +) + // FilterFs is an ordered composite filesystem. type FilterFs struct { fs afero.Fs @@ -141,6 +145,10 @@ func (fs *FilterFs) Chown(n string, uid, gid int) error { return syscall.EPERM } +func (fs *FilterFs) UnwrapFilesystem() afero.Fs { + return fs.fs +} + func (fs *FilterFs) LstatIfPossible(name string) (os.FileInfo, bool, error) { fi, b, err := lstatIfPossible(fs.fs, name) if err != nil { diff --git a/hugofs/fs.go b/hugofs/fs.go index 95645204e..436387f13 100644 --- a/hugofs/fs.go +++ b/hugofs/fs.go @@ -19,6 +19,8 @@ import ( "os" "strings" + "github.com/bep/overlayfs" + "github.com/gohugoio/hugo/common/paths" "github.com/gohugoio/hugo/config" "github.com/spf13/afero" ) @@ -26,32 +28,43 @@ import ( // Os points to the (real) Os filesystem. var Os = &afero.OsFs{} -// Fs abstracts the file system to separate source and destination file systems -// and allows both to be mocked for testing. +// Fs holds the core filesystems used by Hugo. type Fs struct { // Source is Hugo's source file system. + // Note that this will always be a "plain" Afero filesystem: + // * afero.OsFs when running in production + // * afero.MemMapFs for many of the tests. Source afero.Fs - // Destination is Hugo's destination file system. - Destination afero.Fs + // PublishDir is where Hugo publishes its rendered content. + // It's mounted inside publishDir (default /public). + PublishDir afero.Fs - // Destination used for `renderStaticToDisk` - DestinationStatic afero.Fs + // PublishDirStatic is the file system used for static files when --renderStaticToDisk is set. + // When this is set, PublishDir is set to write to memory. + PublishDirStatic afero.Fs + + // PublishDirServer is the file system used for serving the public directory with Hugo's development server. + // This will typically be the same as PublishDir, but not if --renderStaticToDisk is set. + PublishDirServer afero.Fs // Os is an OS file system. // NOTE: Field is currently unused. Os afero.Fs - // WorkingDir is a read-only file system + // WorkingDirReadOnly is a read-only file system // restricted to the project working dir. - // TODO(bep) get rid of this (se BaseFs) - WorkingDir *afero.BasePathFs + WorkingDirReadOnly afero.Fs + + // WorkingDirWritable is a writable file system + // restricted to the project working dir. + WorkingDirWritable afero.Fs } // NewDefault creates a new Fs with the OS file system // as source and destination file systems. func NewDefault(cfg config.Provider) *Fs { - fs := &afero.OsFs{} + fs := Os return newFs(fs, cfg) } @@ -71,23 +84,49 @@ func NewFrom(fs afero.Fs, cfg config.Provider) *Fs { } func newFs(base afero.Fs, cfg config.Provider) *Fs { + workingDir := cfg.GetString("workingDir") + publishDir := cfg.GetString("publishDir") + if publishDir == "" { + panic("publishDir is empty") + } + + // Sanity check + if IsOsFs(base) && len(workingDir) < 2 { + panic("workingDir is too short") + } + + absPublishDir := paths.AbsPathify(workingDir, publishDir) + + // Make sure we always have the /public folder ready to use. + if err := base.MkdirAll(absPublishDir, 0777); err != nil && !os.IsExist(err) { + panic(err) + } + + pubFs := afero.NewBasePathFs(base, absPublishDir) + return &Fs{ - Source: base, - Destination: base, - DestinationStatic: base, - Os: &afero.OsFs{}, - WorkingDir: getWorkingDirFs(base, cfg), + Source: base, + PublishDir: pubFs, + PublishDirServer: pubFs, + PublishDirStatic: pubFs, + Os: &afero.OsFs{}, + WorkingDirReadOnly: getWorkingDirFsReadOnly(base, workingDir), + WorkingDirWritable: getWorkingDirFsWritable(base, workingDir), } } -func getWorkingDirFs(base afero.Fs, cfg config.Provider) *afero.BasePathFs { - workingDir := cfg.GetString("workingDir") - - if workingDir != "" { - return afero.NewBasePathFs(afero.NewReadOnlyFs(base), workingDir).(*afero.BasePathFs) +func getWorkingDirFsReadOnly(base afero.Fs, workingDir string) afero.Fs { + if workingDir == "" { + return afero.NewReadOnlyFs(base) } + return afero.NewBasePathFs(afero.NewReadOnlyFs(base), workingDir) +} - return nil +func getWorkingDirFsWritable(base afero.Fs, workingDir string) afero.Fs { + if workingDir == "" { + return base + } + return afero.NewBasePathFs(base, workingDir) } func isWrite(flag int) bool { @@ -117,3 +156,64 @@ func MakeReadableAndRemoveAllModulePkgDir(fs afero.Fs, dir string) (int, error) }) return counter, fs.RemoveAll(dir) } + +// HasOsFs returns whether fs is an OsFs or if it fs wraps an OsFs. +// TODO(bep) make this nore robust. +func IsOsFs(fs afero.Fs) bool { + var isOsFs bool + WalkFilesystems(fs, func(fs afero.Fs) bool { + switch base := fs.(type) { + case *afero.MemMapFs: + isOsFs = false + case *afero.OsFs: + isOsFs = true + case *afero.BasePathFs: + _, supportsLstat, _ := base.LstatIfPossible("asdfasdfasdf") + isOsFs = supportsLstat + } + return isOsFs + }) + return isOsFs +} + +// FilesystemsUnwrapper returns the underlying filesystems. +type FilesystemsUnwrapper interface { + UnwrapFilesystems() []afero.Fs +} + +// FilesystemsProvider returns the underlying filesystem. +type FilesystemUnwrapper interface { + UnwrapFilesystem() afero.Fs +} + +// WalkFn is the walk func for WalkFilesystems. +type WalkFn func(fs afero.Fs) bool + +// WalkFilesystems walks fs recursively and calls fn. +// If fn returns true, walking is stopped. +func WalkFilesystems(fs afero.Fs, fn WalkFn) bool { + if fn(fs) { + return true + } + + if afs, ok := fs.(FilesystemUnwrapper); ok { + if WalkFilesystems(afs.UnwrapFilesystem(), fn) { + return true + } + + } else if bfs, ok := fs.(FilesystemsUnwrapper); ok { + for _, sf := range bfs.UnwrapFilesystems() { + if WalkFilesystems(sf, fn) { + return true + } + } + } else if cfs, ok := fs.(overlayfs.FilesystemIterator); ok { + for i := 0; i < cfs.NumFilesystems(); i++ { + if WalkFilesystems(cfs.Filesystem(i), fn) { + return true + } + } + } + + return false +} diff --git a/hugofs/fs_test.go b/hugofs/fs_test.go index 8d52267af..f7203fac9 100644 --- a/hugofs/fs_test.go +++ b/hugofs/fs_test.go @@ -23,38 +23,46 @@ import ( "github.com/spf13/afero" ) +func TestIsOsFs(t *testing.T) { + c := qt.New(t) + + c.Assert(IsOsFs(Os), qt.Equals, true) + c.Assert(IsOsFs(&afero.MemMapFs{}), qt.Equals, false) + c.Assert(IsOsFs(afero.NewBasePathFs(&afero.MemMapFs{}, "/public")), qt.Equals, false) + c.Assert(IsOsFs(afero.NewBasePathFs(Os, t.TempDir())), qt.Equals, true) + +} + func TestNewDefault(t *testing.T) { c := qt.New(t) - v := config.New() + v := config.NewWithTestDefaults() + v.Set("workingDir", t.TempDir()) f := NewDefault(v) - c.Assert(f.Source, qt.Not(qt.IsNil)) + c.Assert(f.Source, qt.IsNotNil) c.Assert(f.Source, hqt.IsSameType, new(afero.OsFs)) - c.Assert(f.Os, qt.Not(qt.IsNil)) - c.Assert(f.WorkingDir, qt.IsNil) + c.Assert(f.Os, qt.IsNotNil) + c.Assert(f.WorkingDirReadOnly, qt.IsNotNil) + c.Assert(f.WorkingDirReadOnly, hqt.IsSameType, new(afero.BasePathFs)) + c.Assert(IsOsFs(f.Source), qt.IsTrue) + c.Assert(IsOsFs(f.WorkingDirReadOnly), qt.IsTrue) + c.Assert(IsOsFs(f.PublishDir), qt.IsTrue) + c.Assert(IsOsFs(f.Os), qt.IsTrue) } func TestNewMem(t *testing.T) { c := qt.New(t) - v := config.New() + v := config.NewWithTestDefaults() f := NewMem(v) c.Assert(f.Source, qt.Not(qt.IsNil)) c.Assert(f.Source, hqt.IsSameType, new(afero.MemMapFs)) - c.Assert(f.Destination, qt.Not(qt.IsNil)) - c.Assert(f.Destination, hqt.IsSameType, new(afero.MemMapFs)) + c.Assert(f.PublishDir, qt.Not(qt.IsNil)) + c.Assert(f.PublishDir, hqt.IsSameType, new(afero.BasePathFs)) c.Assert(f.Os, hqt.IsSameType, new(afero.OsFs)) - c.Assert(f.WorkingDir, qt.IsNil) -} - -func TestWorkingDir(t *testing.T) { - c := qt.New(t) - v := config.New() - - v.Set("workingDir", "/a/b/") - - f := NewMem(v) - - c.Assert(f.WorkingDir, qt.Not(qt.IsNil)) - c.Assert(f.WorkingDir, hqt.IsSameType, new(afero.BasePathFs)) + c.Assert(f.WorkingDirReadOnly, qt.IsNotNil) + c.Assert(IsOsFs(f.Source), qt.IsFalse) + c.Assert(IsOsFs(f.WorkingDirReadOnly), qt.IsFalse) + c.Assert(IsOsFs(f.PublishDir), qt.IsFalse) + c.Assert(IsOsFs(f.Os), qt.IsTrue) } diff --git a/hugofs/hashing_fs.go b/hugofs/hashing_fs.go index d7b6329c9..d15ba5863 100644 --- a/hugofs/hashing_fs.go +++ b/hugofs/hashing_fs.go @@ -22,7 +22,10 @@ import ( "github.com/spf13/afero" ) -var _ afero.Fs = (*md5HashingFs)(nil) +var ( + _ afero.Fs = (*md5HashingFs)(nil) + _ FilesystemUnwrapper = (*md5HashingFs)(nil) +) // FileHashReceiver will receive the filename an the content's MD5 sum on file close. type FileHashReceiver interface { @@ -45,6 +48,10 @@ func NewHashingFs(delegate afero.Fs, hashReceiver FileHashReceiver) afero.Fs { return &md5HashingFs{Fs: delegate, hashReceiver: hashReceiver} } +func (fs *md5HashingFs) UnwrapFilesystem() afero.Fs { + return fs.Fs +} + func (fs *md5HashingFs) Create(name string) (afero.File, error) { f, err := fs.Fs.Create(name) if err == nil { diff --git a/hugofs/language_composite_fs.go b/hugofs/language_composite_fs.go index 65bc89e71..9b4bc4cfd 100644 --- a/hugofs/language_composite_fs.go +++ b/hugofs/language_composite_fs.go @@ -20,11 +20,14 @@ import ( ) var ( - _ afero.Fs = (*languageCompositeFs)(nil) - _ afero.Lstater = (*languageCompositeFs)(nil) + _ afero.Fs = (*languageCompositeFs)(nil) + _ afero.Lstater = (*languageCompositeFs)(nil) + _ FilesystemsUnwrapper = (*languageCompositeFs)(nil) ) type languageCompositeFs struct { + base afero.Fs + overlay afero.Fs *afero.CopyOnWriteFs } @@ -33,7 +36,11 @@ type languageCompositeFs struct { // to the target filesystem. This information is available in Readdir, Stat etc. via the // special LanguageFileInfo FileInfo implementation. func NewLanguageCompositeFs(base, overlay afero.Fs) afero.Fs { - return &languageCompositeFs{afero.NewCopyOnWriteFs(base, overlay).(*afero.CopyOnWriteFs)} + return &languageCompositeFs{base, overlay, afero.NewCopyOnWriteFs(base, overlay).(*afero.CopyOnWriteFs)} +} + +func (fs *languageCompositeFs) UnwrapFilesystems() []afero.Fs { + return []afero.Fs{fs.base, fs.overlay} } // Open takes the full path to the file in the target filesystem. If it is a directory, it gets merged diff --git a/hugofs/nosymlink_fs.go b/hugofs/nosymlink_fs.go index ff9503257..d3cad5e74 100644 --- a/hugofs/nosymlink_fs.go +++ b/hugofs/nosymlink_fs.go @@ -30,6 +30,10 @@ func NewNoSymlinkFs(fs afero.Fs, logger loggers.Logger, allowFiles bool) afero.F return &noSymlinkFs{Fs: fs, logger: logger, allowFiles: allowFiles} } +var ( + _ FilesystemUnwrapper = (*noSymlinkFs)(nil) +) + // noSymlinkFs is a filesystem that prevents symlinking. type noSymlinkFs struct { allowFiles bool // block dirs only @@ -67,6 +71,10 @@ func (f *noSymlinkFile) Readdirnames(count int) ([]string, error) { return fileInfosToNames(dirs), nil } +func (fs *noSymlinkFs) UnwrapFilesystem() afero.Fs { + return fs.Fs +} + func (fs *noSymlinkFs) LstatIfPossible(name string) (os.FileInfo, bool, error) { return fs.stat(name) } diff --git a/hugofs/rootmapping_fs.go b/hugofs/rootmapping_fs.go index 28458155c..a891ba8de 100644 --- a/hugofs/rootmapping_fs.go +++ b/hugofs/rootmapping_fs.go @@ -151,6 +151,10 @@ func (r RootMapping) trimFrom(name string) string { return strings.TrimPrefix(name, r.From) } +var ( + _ FilesystemUnwrapper = (*RootMappingFs)(nil) +) + // A RootMappingFs maps several roots into one. Note that the root of this filesystem // is directories only, and they will be returned in Readdir and Readdirnames // in the order given. @@ -200,6 +204,10 @@ func (fs *RootMappingFs) Dirs(base string) ([]FileMetaInfo, error) { return fss, nil } +func (fs *RootMappingFs) UnwrapFilesystem() afero.Fs { + return fs.Fs +} + // Filter creates a copy of this filesystem with only mappings matching a filter. func (fs RootMappingFs) Filter(f func(m RootMapping) bool) *RootMappingFs { rootMapToReal := radix.New() diff --git a/hugofs/rootmapping_fs_test.go b/hugofs/rootmapping_fs_test.go index c650e8f11..c843866fc 100644 --- a/hugofs/rootmapping_fs_test.go +++ b/hugofs/rootmapping_fs_test.go @@ -20,9 +20,8 @@ import ( "sort" "testing" - "github.com/gohugoio/hugo/hugofs/glob" - "github.com/gohugoio/hugo/config" + "github.com/gohugoio/hugo/hugofs/glob" qt "github.com/frankban/quicktest" "github.com/gohugoio/hugo/htesting" @@ -31,7 +30,7 @@ import ( func TestLanguageRootMapping(t *testing.T) { c := qt.New(t) - v := config.New() + v := config.NewWithTestDefaults() v.Set("contentDir", "content") fs := NewBaseFileDecorator(afero.NewMemMapFs()) diff --git a/hugofs/slice_fs.go b/hugofs/slice_fs.go index 0f0d3850a..a9a3f1bbc 100644 --- a/hugofs/slice_fs.go +++ b/hugofs/slice_fs.go @@ -24,9 +24,10 @@ import ( ) var ( - _ afero.Fs = (*SliceFs)(nil) - _ afero.Lstater = (*SliceFs)(nil) - _ afero.File = (*sliceDir)(nil) + _ afero.Fs = (*SliceFs)(nil) + _ afero.Lstater = (*SliceFs)(nil) + _ FilesystemsUnwrapper = (*SliceFs)(nil) + _ afero.File = (*sliceDir)(nil) ) func NewSliceFs(dirs ...FileMetaInfo) (afero.Fs, error) { @@ -52,6 +53,14 @@ type SliceFs struct { dirs []FileMetaInfo } +func (fs *SliceFs) UnwrapFilesystems() []afero.Fs { + var fss []afero.Fs + for _, dir := range fs.dirs { + fss = append(fss, dir.Meta().Fs) + } + return fss +} + func (fs *SliceFs) Chmod(n string, m os.FileMode) error { return syscall.EPERM } diff --git a/hugofs/stacktracer_fs.go b/hugofs/stacktracer_fs.go index d3769f903..4411dbfde 100644 --- a/hugofs/stacktracer_fs.go +++ b/hugofs/stacktracer_fs.go @@ -24,8 +24,11 @@ import ( "github.com/spf13/afero" ) -// Make sure we don't accidentally use this in the real Hugo. -var _ types.DevMarker = (*stacktracerFs)(nil) +var ( + // Make sure we don't accidentally use this in the real Hugo. + _ types.DevMarker = (*stacktracerFs)(nil) + _ FilesystemUnwrapper = (*stacktracerFs)(nil) +) // NewStacktracerFs wraps the given fs printing stack traces for file creates // matching the given regexp pattern. @@ -45,6 +48,10 @@ type stacktracerFs struct { func (fs *stacktracerFs) DevOnly() { } +func (fs *stacktracerFs) UnwrapFilesystem() afero.Fs { + return fs.Fs +} + func (fs *stacktracerFs) onCreate(filename string) { if fs.re.MatchString(filename) { trace := make([]byte, 1500) diff --git a/hugolib/config.go b/hugolib/config.go index b2479cbfe..5d2c6ddf7 100644 --- a/hugolib/config.go +++ b/hugolib/config.go @@ -35,7 +35,6 @@ import ( "github.com/gohugoio/hugo/common/herrors" "github.com/gohugoio/hugo/common/hugo" - "github.com/gohugoio/hugo/hugolib/paths" "github.com/gohugoio/hugo/langs" "github.com/gohugoio/hugo/modules" "github.com/pkg/errors" @@ -359,7 +358,7 @@ func (l configLoader) collectModules(modConfig modules.Config, v1 config.Provide workingDir = v1.GetString("workingDir") } - themesDir := paths.AbsPathify(l.WorkingDir, v1.GetString("themesDir")) + themesDir := cpaths.AbsPathify(l.WorkingDir, v1.GetString("themesDir")) var ignoreVendor glob.Glob if s := v1.GetString("ignoreVendorPaths"); s != "" { diff --git a/hugolib/filesystems/basefs.go b/hugolib/filesystems/basefs.go index 0290d2e1c..693dd8575 100644 --- a/hugolib/filesystems/basefs.go +++ b/hugolib/filesystems/basefs.go @@ -38,8 +38,8 @@ import ( "github.com/gohugoio/hugo/modules" + hpaths "github.com/gohugoio/hugo/common/paths" "github.com/gohugoio/hugo/hugofs" - "github.com/gohugoio/hugo/hugolib/paths" "github.com/spf13/afero" ) @@ -68,12 +68,12 @@ type BaseFs struct { // This usually maps to /my-project/public. PublishFs afero.Fs - // A read-only filesystem starting from the project workDir. - WorkDir afero.Fs - // The filesystem used for renderStaticToDisk. PublishFsStatic afero.Fs + // A read-only filesystem starting from the project workDir. + WorkDir afero.Fs + theBigFs *filesystemsCollector // Locks. @@ -434,21 +434,13 @@ func NewBase(p *paths.Paths, logger loggers.Logger, options ...func(*BaseFs) err logger = loggers.NewWarningLogger() } - // Make sure we always have the /public folder ready to use. - if err := fs.Destination.MkdirAll(p.AbsPublishDir, 0777); err != nil && !os.IsExist(err) { - return nil, err - } - - publishFs := hugofs.NewBaseFileDecorator(afero.NewBasePathFs(fs.Destination, p.AbsPublishDir)) + publishFs := hugofs.NewBaseFileDecorator(fs.PublishDir) sourceFs := hugofs.NewBaseFileDecorator(afero.NewBasePathFs(fs.Source, p.WorkingDir)) - publishFsStatic := afero.NewBasePathFs(fs.Source, p.AbsPublishDir) - - // Same as sourceFs, but no decoration. This is what's used by os.ReadDir etc. - workDir := afero.NewBasePathFs(afero.NewReadOnlyFs(fs.Source), p.WorkingDir) + publishFsStatic := fs.PublishDirStatic b := &BaseFs{ SourceFs: sourceFs, - WorkDir: workDir, + WorkDir: fs.WorkingDirReadOnly, PublishFs: publishFs, PublishFsStatic: publishFsStatic, buildMu: lockedfile.MutexAt(filepath.Join(p.WorkingDir, lockFileBuild)), @@ -638,7 +630,7 @@ func (b *sourceFilesystemsBuilder) createModFs( if filepath.IsAbs(path) { return "", path } - return md.dir, paths.AbsPathify(md.dir, path) + return md.dir, hpaths.AbsPathify(md.dir, path) } for i, mount := range md.Mounts() { diff --git a/hugolib/filesystems/basefs_test.go b/hugolib/filesystems/basefs_test.go index 32d1eef71..a729e63b1 100644 --- a/hugolib/filesystems/basefs_test.go +++ b/hugolib/filesystems/basefs_test.go @@ -75,7 +75,7 @@ func initConfig(fs afero.Fs, cfg config.Provider) error { func TestNewBaseFs(t *testing.T) { c := qt.New(t) - v := config.New() + v := config.NewWithTestDefaults() fs := hugofs.NewMem(v) @@ -181,7 +181,7 @@ theme = ["atheme"] } func createConfig() config.Provider { - v := config.New() + v := config.NewWithTestDefaults() v.Set("contentDir", "mycontent") v.Set("i18nDir", "myi18n") v.Set("staticDir", "mystatic") @@ -219,22 +219,19 @@ func TestNewBaseFsEmpty(t *testing.T) { func TestRealDirs(t *testing.T) { c := qt.New(t) v := createConfig() + root, themesDir := t.TempDir(), t.TempDir() + v.Set("workingDir", root) + v.Set("themesDir", themesDir) + v.Set("theme", "mytheme") + fs := hugofs.NewDefault(v) sfs := fs.Source - root, err := afero.TempDir(sfs, "", "realdir") - c.Assert(err, qt.IsNil) - themesDir, err := afero.TempDir(sfs, "", "themesDir") - c.Assert(err, qt.IsNil) defer func() { os.RemoveAll(root) os.RemoveAll(themesDir) }() - v.Set("workingDir", root) - v.Set("themesDir", themesDir) - v.Set("theme", "mytheme") - c.Assert(sfs.MkdirAll(filepath.Join(root, "myassets", "scss", "sf1"), 0755), qt.IsNil) c.Assert(sfs.MkdirAll(filepath.Join(root, "myassets", "scss", "sf2"), 0755), qt.IsNil) c.Assert(sfs.MkdirAll(filepath.Join(themesDir, "mytheme", "assets", "scss", "sf2"), 0755), qt.IsNil) diff --git a/hugolib/hugo_modules_test.go b/hugolib/hugo_modules_test.go index eb6b7433b..358286495 100644 --- a/hugolib/hugo_modules_test.go +++ b/hugolib/hugo_modules_test.go @@ -59,13 +59,14 @@ path="github.com/gohugoio/hugoTestModule2" return fmt.Sprintf(tomlConfig, workingDir, moduleOpts) } - newTestBuilder := func(t testing.TB, moduleOpts string) (*sitesBuilder, func()) { + newTestBuilder := func(t testing.TB, moduleOpts string) *sitesBuilder { b := newTestSitesBuilder(t) - tempDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugo-modules-variants") - b.Assert(err, qt.IsNil) + tempDir := t.TempDir() workingDir := filepath.Join(tempDir, "myhugosite") b.Assert(os.MkdirAll(workingDir, 0777), qt.IsNil) - b.Fs = hugofs.NewDefault(config.New()) + cfg := config.NewWithTestDefaults() + cfg.Set("workingDir", workingDir) + b.Fs = hugofs.NewDefault(cfg) b.WithWorkingDir(workingDir).WithConfigFile("toml", createConfig(workingDir, moduleOpts)) b.WithTemplates( "index.html", ` @@ -92,22 +93,18 @@ github.com/gohugoio/hugoTestModule2 v0.0.0-20200131160637-9657d7697877 h1:WLM2bQ github.com/gohugoio/hugoTestModule2 v0.0.0-20200131160637-9657d7697877/go.mod h1:CBFZS3khIAXKxReMwq0le8sEl/D8hcXmixlOHVv+Gd0= `) - return b, clean + return b } t.Run("Target in subfolder", func(t *testing.T) { - b, clean := newTestBuilder(t, "ignoreImports=true") - defer clean() - + b := newTestBuilder(t, "ignoreImports=true") b.Build(BuildCfg{}) b.AssertFileContent("public/p1/index.html", `
Page|https://bep.is|Title: |Text: A link|END
`) }) t.Run("Ignore config", func(t *testing.T) { - b, clean := newTestBuilder(t, "ignoreConfig=true") - defer clean() - + b := newTestBuilder(t, "ignoreConfig=true") b.Build(BuildCfg{}) b.AssertFileContent("public/index.html", ` @@ -117,9 +114,7 @@ JS imported in module: | }) t.Run("Ignore imports", func(t *testing.T) { - b, clean := newTestBuilder(t, "ignoreImports=true") - defer clean() - + b := newTestBuilder(t, "ignoreImports=true") b.Build(BuildCfg{}) b.AssertFileContent("public/index.html", ` @@ -129,8 +124,7 @@ JS imported in module: | }) t.Run("Create package.json", func(t *testing.T) { - b, clean := newTestBuilder(t, "") - defer clean() + b := newTestBuilder(t, "") b.WithSourceFile("package.json", `{ "name": "mypack", @@ -205,8 +199,7 @@ JS imported in module: | }) t.Run("Create package.json, no default", func(t *testing.T) { - b, clean := newTestBuilder(t, "") - defer clean() + b := newTestBuilder(t, "") const origPackageJSON = `{ "name": "mypack", @@ -268,8 +261,7 @@ JS imported in module: | }) t.Run("Create package.json, no default, no package.json", func(t *testing.T) { - b, clean := newTestBuilder(t, "") - defer clean() + b := newTestBuilder(t, "") b.Build(BuildCfg{}) b.Assert(npm.Pack(b.H.BaseFs.SourceFs, b.H.BaseFs.Assets.Dirs), qt.IsNil) @@ -333,12 +325,13 @@ func TestHugoModulesMatrix(t *testing.T) { for _, m := range testmods[:2] { c := qt.New(t) - v := config.New() - workingDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugo-modules-test") c.Assert(err, qt.IsNil) defer clean() + v := config.NewWithTestDefaults() + v.Set("workingDir", workingDir) + configTemplate := ` baseURL = "https://example.com" title = "My Modular Site" @@ -670,13 +663,14 @@ func TestModulesSymlinks(t *testing.T) { }() c := qt.New(t) - // We need to use the OS fs for this. - cfg := config.New() - fs := hugofs.NewFrom(hugofs.Os, cfg) - - workDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugo-mod-sym") + workingDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugo-mod-sym") c.Assert(err, qt.IsNil) + // We need to use the OS fs for this. + cfg := config.NewWithTestDefaults() + cfg.Set("workingDir", workingDir) + fs := hugofs.NewFrom(hugofs.Os, cfg) + defer clean() const homeTemplate = ` @@ -694,9 +688,9 @@ Data: {{ .Site.Data }} } // Create project dirs and files. - createDirsAndFiles(workDir) + createDirsAndFiles(workingDir) // Create one module inside the default themes folder. - themeDir := filepath.Join(workDir, "themes", "mymod") + themeDir := filepath.Join(workingDir, "themes", "mymod") createDirsAndFiles(themeDir) createSymlinks := func(baseDir, id string) { @@ -711,7 +705,7 @@ Data: {{ .Site.Data }} } } - createSymlinks(workDir, "project") + createSymlinks(workingDir, "project") createSymlinks(themeDir, "mod") config := ` @@ -729,12 +723,12 @@ weight = 2 ` - b := newTestSitesBuilder(t).WithNothingAdded().WithWorkingDir(workDir) + b := newTestSitesBuilder(t).WithNothingAdded().WithWorkingDir(workingDir) b.WithLogger(loggers.NewErrorLogger()) b.Fs = fs b.WithConfigFile("toml", config) - c.Assert(os.Chdir(workDir), qt.IsNil) + c.Assert(os.Chdir(workingDir), qt.IsNil) b.Build(BuildCfg{}) @@ -846,7 +840,10 @@ workingDir = %q b := newTestSitesBuilder(t).Running() - b.Fs = hugofs.NewDefault(config.New()) + cfg := config.NewWithTestDefaults() + cfg.Set("workingDir", workingDir) + + b.Fs = hugofs.NewDefault(cfg) b.WithWorkingDir(workingDir).WithConfigFile("toml", tomlConfig) b.WithTemplatesAdded("index.html", ` @@ -968,7 +965,9 @@ workingDir = %q b := newTestSitesBuilder(c).Running() - b.Fs = hugofs.NewDefault(config.New()) + cfg := config.NewWithTestDefaults() + cfg.Set("workingDir", workingDir) + b.Fs = hugofs.NewDefault(cfg) os.MkdirAll(filepath.Join(workingDir, "content", "blog"), 0777) @@ -1067,7 +1066,7 @@ func TestSiteWithGoModButNoModules(t *testing.T) { workDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugo-no-mod") c.Assert(err, qt.IsNil) - cfg := config.New() + cfg := config.NewWithTestDefaults() cfg.Set("workingDir", workDir) fs := hugofs.NewFrom(hugofs.Os, cfg) @@ -1093,7 +1092,7 @@ func TestModuleAbsMount(t *testing.T) { absContentDir, clean2, err := htesting.CreateTempDir(hugofs.Os, "hugo-content") c.Assert(err, qt.IsNil) - cfg := config.New() + cfg := config.NewWithTestDefaults() cfg.Set("workingDir", workDir) fs := hugofs.NewFrom(hugofs.Os, cfg) diff --git a/hugolib/hugo_sites.go b/hugolib/hugo_sites.go index 09e1a331a..d67652dab 100644 --- a/hugolib/hugo_sites.go +++ b/hugolib/hugo_sites.go @@ -597,7 +597,7 @@ func (h *HugoSites) reset(config *BuildCfg) { if config.ResetState { for i, s := range h.Sites { h.Sites[i] = s.reset() - if r, ok := s.Fs.Destination.(hugofs.Reseter); ok { + if r, ok := s.Fs.PublishDir.(hugofs.Reseter); ok { r.Reset() } } diff --git a/hugolib/hugo_sites_build.go b/hugolib/hugo_sites_build.go index bf52277a9..4616b6dbb 100644 --- a/hugolib/hugo_sites_build.go +++ b/hugolib/hugo_sites_build.go @@ -496,9 +496,9 @@ func (h *HugoSites) writeBuildStats() error { return err } - // Write to the destination, too, if a mem fs is in play. - if h.Fs.Source != hugofs.Os { - if err := afero.WriteFile(h.Fs.Destination, filename, js, 0666); err != nil { + // Write to the destination as well if it's a in-memory fs. + if !hugofs.IsOsFs(h.Fs.Source) { + if err := afero.WriteFile(h.Fs.WorkingDirWritable, filename, js, 0666); err != nil { return err } } diff --git a/hugolib/hugo_sites_build_test.go b/hugolib/hugo_sites_build_test.go index d71e7c7a4..4a629eedd 100644 --- a/hugolib/hugo_sites_build_test.go +++ b/hugolib/hugo_sites_build_test.go @@ -489,7 +489,7 @@ func TestMultiSitesRebuild(t *testing.T) { c.Assert(enSite.RegularPages()[0].Title(), qt.Equals, "new_en_2") c.Assert(enSite.RegularPages()[1].Title(), qt.Equals, "new_en_1") - rendered := readDestination(t, fs, "public/en/new1/index.html") + rendered := readWorkingDir(t, fs, "public/en/new1/index.html") c.Assert(strings.Contains(rendered, "new_en_1"), qt.Equals, true) }, }, @@ -503,7 +503,7 @@ func TestMultiSitesRebuild(t *testing.T) { []fsnotify.Event{{Name: filepath.FromSlash("content/sect/doc1.en.md"), Op: fsnotify.Write}}, func(t *testing.T) { c.Assert(len(enSite.RegularPages()), qt.Equals, 6) - doc1 := readDestination(t, fs, "public/en/sect/doc1-slug/index.html") + doc1 := readWorkingDir(t, fs, "public/en/sect/doc1-slug/index.html") c.Assert(strings.Contains(doc1, "CHANGED"), qt.Equals, true) }, }, @@ -521,7 +521,7 @@ func TestMultiSitesRebuild(t *testing.T) { func(t *testing.T) { c.Assert(len(enSite.RegularPages()), qt.Equals, 6, qt.Commentf("Rename")) c.Assert(enSite.RegularPages()[1].Title(), qt.Equals, "new_en_1") - rendered := readDestination(t, fs, "public/en/new1renamed/index.html") + rendered := readWorkingDir(t, fs, "public/en/new1renamed/index.html") c.Assert(rendered, qt.Contains, "new_en_1") }, }, @@ -538,7 +538,7 @@ func TestMultiSitesRebuild(t *testing.T) { c.Assert(len(enSite.RegularPages()), qt.Equals, 6) c.Assert(len(enSite.AllPages()), qt.Equals, 34) c.Assert(len(frSite.RegularPages()), qt.Equals, 5) - doc1 := readDestination(t, fs, "public/en/sect/doc1-slug/index.html") + doc1 := readWorkingDir(t, fs, "public/en/sect/doc1-slug/index.html") c.Assert(strings.Contains(doc1, "Template Changed"), qt.Equals, true) }, }, @@ -555,9 +555,9 @@ func TestMultiSitesRebuild(t *testing.T) { c.Assert(len(enSite.RegularPages()), qt.Equals, 6) c.Assert(len(enSite.AllPages()), qt.Equals, 34) c.Assert(len(frSite.RegularPages()), qt.Equals, 5) - docEn := readDestination(t, fs, "public/en/sect/doc1-slug/index.html") + docEn := readWorkingDir(t, fs, "public/en/sect/doc1-slug/index.html") c.Assert(strings.Contains(docEn, "Hello"), qt.Equals, true) - docFr := readDestination(t, fs, "public/fr/sect/doc1/index.html") + docFr := readWorkingDir(t, fs, "public/fr/sect/doc1/index.html") c.Assert(strings.Contains(docFr, "Salut"), qt.Equals, true) homeEn := enSite.getPage(page.KindHome) @@ -700,7 +700,7 @@ END func checkContent(s *sitesBuilder, filename string, matches ...string) { s.T.Helper() - content := readDestination(s.T, s.Fs, filename) + content := readWorkingDir(s.T, s.Fs, filename) for _, match := range matches { if !strings.Contains(content, match) { s.Fatalf("No match for\n%q\nin content for %s\n%q\nDiff:\n%s", match, filename, content, htesting.DiffStrings(content, match)) @@ -1170,13 +1170,13 @@ func writeToFs(t testing.TB, fs afero.Fs, filename, content string) { } } -func readDestination(t testing.TB, fs *hugofs.Fs, filename string) string { +func readWorkingDir(t testing.TB, fs *hugofs.Fs, filename string) string { t.Helper() - return readFileFromFs(t, fs.Destination, filename) + return readFileFromFs(t, fs.WorkingDirReadOnly, filename) } -func destinationExists(fs *hugofs.Fs, filename string) bool { - b, err := helpers.Exists(filename, fs.Destination) +func workingDirExists(fs *hugofs.Fs, filename string) bool { + b, err := helpers.Exists(filename, fs.WorkingDirReadOnly) if err != nil { panic(err) } diff --git a/hugolib/image_test.go b/hugolib/image_test.go index 5f056e4ad..ac18b9423 100644 --- a/hugolib/image_test.go +++ b/hugolib/image_test.go @@ -38,7 +38,7 @@ func TestImageOps(t *testing.T) { defer clean() newBuilder := func(timeout any) *sitesBuilder { - v := config.New() + v := config.NewWithTestDefaults() v.Set("workingDir", workDir) v.Set("baseURL", "https://example.org") v.Set("timeout", timeout) @@ -141,7 +141,7 @@ IMG SHORTCODE: /images/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_129x239_r assertImages := func() { b.Helper() - b.AssertFileContent(filepath.Join(workDir, "public/index.html"), imgExpect) + b.AssertFileContent("public/index.html", imgExpect) b.AssertImage(350, 219, "public/images/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_350x0_resize_q75_box.a86fe88d894e5db613f6aa8a80538fefc25b20fa24ba0d782c057adcef616f56.jpg") b.AssertImage(129, 239, "public/images/sunset_hu59e56ffff1bc1d8d122b1403d34e039f_90587_129x239_resize_q75_box.jpg") } diff --git a/hugolib/integrationtest_builder.go b/hugolib/integrationtest_builder.go index d49e29763..e5c9a6856 100644 --- a/hugolib/integrationtest_builder.go +++ b/hugolib/integrationtest_builder.go @@ -47,6 +47,8 @@ func NewIntegrationTestBuilder(conf IntegrationTestConfig) *IntegrationTestBuild if doClean { c.Cleanup(clean) } + } else if conf.WorkingDir == "" { + conf.WorkingDir = helpers.FilePathSeparator } return &IntegrationTestBuilder{ @@ -157,7 +159,7 @@ func (s *IntegrationTestBuilder) AssertDestinationExists(filename string, b bool } func (s *IntegrationTestBuilder) destinationExists(filename string) bool { - b, err := helpers.Exists(filename, s.fs.Destination) + b, err := helpers.Exists(filename, s.fs.PublishDir) if err != nil { panic(err) } @@ -258,11 +260,7 @@ func (s *IntegrationTestBuilder) RenameFile(old, new string) *IntegrationTestBui func (s *IntegrationTestBuilder) FileContent(filename string) string { s.Helper() - filename = filepath.FromSlash(filename) - if !strings.HasPrefix(filename, s.Cfg.WorkingDir) { - filename = filepath.Join(s.Cfg.WorkingDir, filename) - } - return s.readDestination(s, s.fs, filename) + return s.readWorkingDir(s, s.fs, filepath.FromSlash(filename)) } func (s *IntegrationTestBuilder) initBuilder() { @@ -280,8 +278,6 @@ func (s *IntegrationTestBuilder) initBuilder() { logger := loggers.NewBasicLoggerForWriter(s.Cfg.LogLevel, &s.logBuff) - fs := hugofs.NewFrom(afs, config.New()) - for _, f := range s.data.Files { filename := filepath.Join(s.Cfg.WorkingDir, f.Name) s.Assert(afs.MkdirAll(filepath.Dir(filename), 0777), qt.IsNil) @@ -301,10 +297,12 @@ func (s *IntegrationTestBuilder) initBuilder() { }, ) - s.Assert(err, qt.IsNil) - cfg.Set("workingDir", s.Cfg.WorkingDir) + fs := hugofs.NewFrom(afs, cfg) + + s.Assert(err, qt.IsNil) + depsCfg := deps.DepsCfg{Cfg: cfg, Fs: fs, Running: s.Cfg.Running, Logger: logger} sites, err := NewHugoSites(depsCfg) s.Assert(err, qt.IsNil) @@ -400,9 +398,9 @@ func (s *IntegrationTestBuilder) changeEvents() []fsnotify.Event { return events } -func (s *IntegrationTestBuilder) readDestination(t testing.TB, fs *hugofs.Fs, filename string) string { +func (s *IntegrationTestBuilder) readWorkingDir(t testing.TB, fs *hugofs.Fs, filename string) string { t.Helper() - return s.readFileFromFs(t, fs.Destination, filename) + return s.readFileFromFs(t, fs.WorkingDirReadOnly, filename) } func (s *IntegrationTestBuilder) readFileFromFs(t testing.TB, fs afero.Fs, filename string) string { diff --git a/hugolib/language_content_dir_test.go b/hugolib/language_content_dir_test.go index 541878220..57cdab67b 100644 --- a/hugolib/language_content_dir_test.go +++ b/hugolib/language_content_dir_test.go @@ -224,8 +224,8 @@ Content. nnSite := b.H.Sites[1] svSite := b.H.Sites[2] - b.AssertFileContent("/my/project/public/en/mystatic/file1.yaml", "en") - b.AssertFileContent("/my/project/public/nn/mystatic/file1.yaml", "nn") + b.AssertFileContent("public/en/mystatic/file1.yaml", "en") + b.AssertFileContent("public/nn/mystatic/file1.yaml", "nn") // dumpPages(nnSite.RegularPages()...) @@ -300,16 +300,16 @@ Content. c.Assert(len(bundleSv.Resources()), qt.Equals, 4) c.Assert(len(bundleEn.Resources()), qt.Equals, 4) - b.AssertFileContent("/my/project/public/en/sect/mybundle/index.html", "image/png: /en/sect/mybundle/logo.png") - b.AssertFileContent("/my/project/public/nn/sect/mybundle/index.html", "image/png: /nn/sect/mybundle/logo.png") - b.AssertFileContent("/my/project/public/sv/sect/mybundle/index.html", "image/png: /sv/sect/mybundle/logo.png") + b.AssertFileContent("public/en/sect/mybundle/index.html", "image/png: /en/sect/mybundle/logo.png") + b.AssertFileContent("public/nn/sect/mybundle/index.html", "image/png: /nn/sect/mybundle/logo.png") + b.AssertFileContent("public/sv/sect/mybundle/index.html", "image/png: /sv/sect/mybundle/logo.png") - b.AssertFileContent("/my/project/public/sv/sect/mybundle/featured.png", "PNG Data for sv") - b.AssertFileContent("/my/project/public/nn/sect/mybundle/featured.png", "PNG Data for nn") - b.AssertFileContent("/my/project/public/en/sect/mybundle/featured.png", "PNG Data for en") - b.AssertFileContent("/my/project/public/en/sect/mybundle/logo.png", "PNG Data") - b.AssertFileContent("/my/project/public/sv/sect/mybundle/logo.png", "PNG Data") - b.AssertFileContent("/my/project/public/nn/sect/mybundle/logo.png", "PNG Data") + b.AssertFileContent("public/sv/sect/mybundle/featured.png", "PNG Data for sv") + b.AssertFileContent("public/nn/sect/mybundle/featured.png", "PNG Data for nn") + b.AssertFileContent("public/en/sect/mybundle/featured.png", "PNG Data for en") + b.AssertFileContent("public/en/sect/mybundle/logo.png", "PNG Data") + b.AssertFileContent("public/sv/sect/mybundle/logo.png", "PNG Data") + b.AssertFileContent("public/nn/sect/mybundle/logo.png", "PNG Data") nnSect := nnSite.getPage(page.KindSection, "sect") c.Assert(nnSect, qt.Not(qt.IsNil)) diff --git a/hugolib/minify_publisher_test.go b/hugolib/minify_publisher_test.go index ef460efa2..03b46a5fe 100644 --- a/hugolib/minify_publisher_test.go +++ b/hugolib/minify_publisher_test.go @@ -22,7 +22,7 @@ import ( func TestMinifyPublisher(t *testing.T) { t.Parallel() - v := config.New() + v := config.NewWithTestDefaults() v.Set("minify", true) v.Set("baseURL", "https://example.org/") diff --git a/hugolib/mount_filters_test.go b/hugolib/mount_filters_test.go index 5f4157715..688cf2558 100644 --- a/hugolib/mount_filters_test.go +++ b/hugolib/mount_filters_test.go @@ -101,13 +101,13 @@ Resources: {{ resources.Match "**.js" }} assertExists := func(name string, shouldExist bool) { b.Helper() - b.Assert(b.CheckExists(filepath.Join(workingDir, name)), qt.Equals, shouldExist) + b.Assert(b.CheckExists(name), qt.Equals, shouldExist) } assertExists("public/a/b/p1/index.html", true) assertExists("public/a/c/p2/index.html", false) - b.AssertFileContent(filepath.Join(workingDir, "public", "index.html"), ` + b.AssertFileContent(filepath.Join("public", "index.html"), ` Data: map[mydata:map[b:map[b1:bval]]]:END Template: false Resource1: js/include.js:END diff --git a/hugolib/page_test.go b/hugolib/page_test.go index d29a4f865..c16754cd9 100644 --- a/hugolib/page_test.go +++ b/hugolib/page_test.go @@ -23,7 +23,6 @@ import ( "time" "github.com/gohugoio/hugo/htesting" - "github.com/gohugoio/hugo/markup/asciidocext" "github.com/gohugoio/hugo/markup/rst" @@ -35,7 +34,6 @@ import ( "github.com/gohugoio/hugo/resources/page" "github.com/gohugoio/hugo/resources/resource" - "github.com/spf13/afero" "github.com/spf13/jwalterweatherman" qt "github.com/frankban/quicktest" @@ -1031,14 +1029,14 @@ func TestPageWithLastmodFromGitInfo(t *testing.T) { } c := qt.New(t) - // We need to use the OS fs for this. - cfg := config.New() - fs := hugofs.NewFrom(hugofs.Os, cfg) - fs.Destination = &afero.MemMapFs{} - wd, err := os.Getwd() c.Assert(err, qt.IsNil) + // We need to use the OS fs for this. + cfg := config.NewWithTestDefaults() + cfg.Set("workingDir", filepath.Join(wd, "testsite")) + fs := hugofs.NewFrom(hugofs.Os, cfg) + cfg.Set("frontmatter", map[string]any{ "lastmod": []string{":git", "lastmod"}, }) @@ -1060,8 +1058,6 @@ func TestPageWithLastmodFromGitInfo(t *testing.T) { cfg.Set("languages", langConfig) cfg.Set("enableGitInfo", true) - cfg.Set("workingDir", filepath.Join(wd, "testsite")) - b := newTestSitesBuilderFromDepsCfg(t, deps.DepsCfg{Fs: fs, Cfg: cfg}).WithNothingAdded() b.Build(BuildCfg{SkipRender: true}) @@ -1314,7 +1310,7 @@ func TestChompBOM(t *testing.T) { func TestPageWithEmoji(t *testing.T) { for _, enableEmoji := range []bool{true, false} { - v := config.New() + v := config.NewWithTestDefaults() v.Set("enableEmoji", enableEmoji) b := newTestSitesBuilder(t).WithViper(v) diff --git a/hugolib/pagebundler_test.go b/hugolib/pagebundler_test.go index cbad36520..f88d2e4d2 100644 --- a/hugolib/pagebundler_test.go +++ b/hugolib/pagebundler_test.go @@ -127,22 +127,22 @@ func TestPageBundlerSiteRegular(t *testing.T) { // Check both output formats rel, filename := relFilename("/a/1/", "index.html") - b.AssertFileContent(filepath.Join("/work/public", filename), + b.AssertFileContent(filepath.Join("public", filename), "TheContent", "Single RelPermalink: "+rel, ) rel, filename = relFilename("/cpath/a/1/", "cindex.html") - b.AssertFileContent(filepath.Join("/work/public", filename), + b.AssertFileContent(filepath.Join("public", filename), "TheContent", "Single RelPermalink: "+rel, ) - b.AssertFileContent(filepath.FromSlash("/work/public/images/hugo-logo.png"), "content") + b.AssertFileContent(filepath.FromSlash("public/images/hugo-logo.png"), "content") // This should be just copied to destination. - b.AssertFileContent(filepath.FromSlash("/work/public/assets/pic1.png"), "content") + b.AssertFileContent(filepath.FromSlash("public/assets/pic1.png"), "content") leafBundle1 := s.getPage(page.KindPage, "b/my-bundle/index.md") c.Assert(leafBundle1, qt.Not(qt.IsNil)) @@ -159,8 +159,8 @@ func TestPageBundlerSiteRegular(t *testing.T) { c.Assert(rootBundle, qt.Not(qt.IsNil)) c.Assert(rootBundle.Parent().IsHome(), qt.Equals, true) if !ugly { - b.AssertFileContent(filepath.FromSlash("/work/public/root/index.html"), "Single RelPermalink: "+relURLBase+"/root/") - b.AssertFileContent(filepath.FromSlash("/work/public/cpath/root/cindex.html"), "Single RelPermalink: "+relURLBase+"/cpath/root/") + b.AssertFileContent(filepath.FromSlash("public/root/index.html"), "Single RelPermalink: "+relURLBase+"/root/") + b.AssertFileContent(filepath.FromSlash("public/cpath/root/cindex.html"), "Single RelPermalink: "+relURLBase+"/cpath/root/") } leafBundle2 := s.getPage(page.KindPage, "a/b/index.md") @@ -202,17 +202,17 @@ func TestPageBundlerSiteRegular(t *testing.T) { } if ugly { - b.AssertFileContent("/work/public/2017/pageslug.html", + b.AssertFileContent("public/2017/pageslug.html", relPermalinker("Single RelPermalink: %s/2017/pageslug.html"), permalinker("Single Permalink: %s/2017/pageslug.html"), relPermalinker("Sunset RelPermalink: %s/2017/pageslug/sunset1.jpg"), permalinker("Sunset Permalink: %s/2017/pageslug/sunset1.jpg")) } else { - b.AssertFileContent("/work/public/2017/pageslug/index.html", + b.AssertFileContent("public/2017/pageslug/index.html", relPermalinker("Sunset RelPermalink: %s/2017/pageslug/sunset1.jpg"), permalinker("Sunset Permalink: %s/2017/pageslug/sunset1.jpg")) - b.AssertFileContent("/work/public/cpath/2017/pageslug/cindex.html", + b.AssertFileContent("public/cpath/2017/pageslug/cindex.html", relPermalinker("Single RelPermalink: %s/cpath/2017/pageslug/"), relPermalinker("Short Sunset RelPermalink: %s/cpath/2017/pageslug/sunset2.jpg"), relPermalinker("Sunset RelPermalink: %s/cpath/2017/pageslug/sunset1.jpg"), @@ -220,15 +220,15 @@ func TestPageBundlerSiteRegular(t *testing.T) { ) } - b.AssertFileContent(filepath.FromSlash("/work/public/2017/pageslug/c/logo.png"), "content") - b.AssertFileContent(filepath.FromSlash("/work/public/cpath/2017/pageslug/c/logo.png"), "content") - c.Assert(b.CheckExists("/work/public/cpath/cpath/2017/pageslug/c/logo.png"), qt.Equals, false) + b.AssertFileContent(filepath.FromSlash("public/2017/pageslug/c/logo.png"), "content") + b.AssertFileContent(filepath.FromSlash("public/cpath/2017/pageslug/c/logo.png"), "content") + c.Assert(b.CheckExists("public/cpath/cpath/2017/pageslug/c/logo.png"), qt.Equals, false) // Custom media type defined in site config. c.Assert(len(leafBundle1.Resources().ByType("bepsays")), qt.Equals, 1) if ugly { - b.AssertFileContent(filepath.FromSlash("/work/public/2017/pageslug.html"), + b.AssertFileContent(filepath.FromSlash("public/2017/pageslug.html"), "TheContent", relPermalinker("Sunset RelPermalink: %s/2017/pageslug/sunset1.jpg"), permalinker("Sunset Permalink: %s/2017/pageslug/sunset1.jpg"), @@ -247,18 +247,18 @@ func TestPageBundlerSiteRegular(t *testing.T) { // https://github.com/gohugoio/hugo/issues/5882 b.AssertFileContent( - filepath.FromSlash("/work/public/2017/pageslug.html"), "0: Page RelPermalink: |") + filepath.FromSlash("public/2017/pageslug.html"), "0: Page RelPermalink: |") - b.AssertFileContent(filepath.FromSlash("/work/public/cpath/2017/pageslug.html"), "TheContent") + b.AssertFileContent(filepath.FromSlash("public/cpath/2017/pageslug.html"), "TheContent") // 은행 - b.AssertFileContent(filepath.FromSlash("/work/public/c/은행/logo-은행.png"), "은행 PNG") + b.AssertFileContent(filepath.FromSlash("public/c/은행/logo-은행.png"), "은행 PNG") } else { - b.AssertFileContent(filepath.FromSlash("/work/public/2017/pageslug/index.html"), "TheContent") - b.AssertFileContent(filepath.FromSlash("/work/public/cpath/2017/pageslug/cindex.html"), "TheContent") - b.AssertFileContent(filepath.FromSlash("/work/public/2017/pageslug/index.html"), "Single Title") - b.AssertFileContent(filepath.FromSlash("/work/public/root/index.html"), "Single Title") + b.AssertFileContent(filepath.FromSlash("public/2017/pageslug/index.html"), "TheContent") + b.AssertFileContent(filepath.FromSlash("public/cpath/2017/pageslug/cindex.html"), "TheContent") + b.AssertFileContent(filepath.FromSlash("public/2017/pageslug/index.html"), "Single Title") + b.AssertFileContent(filepath.FromSlash("public/root/index.html"), "Single Title") } }) @@ -397,23 +397,24 @@ func TestPageBundlerSiteWitSymbolicLinksInContent(t *testing.T) { }() c := qt.New(t) - // We need to use the OS fs for this. - cfg := config.New() - fs := hugofs.NewFrom(hugofs.Os, cfg) - workDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugosym") + // We need to use the OS fs for this. + workingDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugosym") c.Assert(err, qt.IsNil) + cfg := config.NewWithTestDefaults() + cfg.Set("workingDir", workingDir) + fs := hugofs.NewFrom(hugofs.Os, cfg) contentDirName := "content" - contentDir := filepath.Join(workDir, contentDirName) + contentDir := filepath.Join(workingDir, contentDirName) c.Assert(os.MkdirAll(filepath.Join(contentDir, "a"), 0777), qt.IsNil) for i := 1; i <= 3; i++ { - c.Assert(os.MkdirAll(filepath.Join(workDir, fmt.Sprintf("symcontent%d", i)), 0777), qt.IsNil) + c.Assert(os.MkdirAll(filepath.Join(workingDir, fmt.Sprintf("symcontent%d", i)), 0777), qt.IsNil) } - c.Assert(os.MkdirAll(filepath.Join(workDir, "symcontent2", "a1"), 0777), qt.IsNil) + c.Assert(os.MkdirAll(filepath.Join(workingDir, "symcontent2", "a1"), 0777), qt.IsNil) // Symlinked sections inside content. os.Chdir(contentDir) @@ -431,11 +432,11 @@ func TestPageBundlerSiteWitSymbolicLinksInContent(t *testing.T) { // Create a circular symlink. Will print some warnings. c.Assert(os.Symlink(filepath.Join("..", contentDirName), filepath.FromSlash("circus")), qt.IsNil) - c.Assert(os.Chdir(workDir), qt.IsNil) + c.Assert(os.Chdir(workingDir), qt.IsNil) defer clean() - cfg.Set("workingDir", workDir) + cfg.Set("workingDir", workingDir) cfg.Set("contentDir", contentDirName) cfg.Set("baseURL", "https://example.com") @@ -488,9 +489,9 @@ TheContent. c.Assert(len(a1Bundle.Resources()), qt.Equals, 2) c.Assert(len(a1Bundle.Resources().ByType(pageResourceType)), qt.Equals, 1) - b.AssertFileContent(filepath.FromSlash(workDir+"/public/a/page/index.html"), "TheContent") - b.AssertFileContent(filepath.FromSlash(workDir+"/public/symbolic1/s1/index.html"), "TheContent") - b.AssertFileContent(filepath.FromSlash(workDir+"/public/symbolic2/a1/index.html"), "TheContent") + b.AssertFileContent(filepath.FromSlash("public/a/page/index.html"), "TheContent") + b.AssertFileContent(filepath.FromSlash("public/symbolic1/s1/index.html"), "TheContent") + b.AssertFileContent(filepath.FromSlash("public/symbolic2/a1/index.html"), "TheContent") } func TestPageBundlerHeadless(t *testing.T) { @@ -563,12 +564,12 @@ HEADLESS {{< myShort >}} th := newTestHelper(s.Cfg, s.Fs, t) - th.assertFileContent(filepath.FromSlash(workDir+"/public/s1/index.html"), "TheContent") - th.assertFileContent(filepath.FromSlash(workDir+"/public/s1/l1.png"), "PNG") + th.assertFileContent(filepath.FromSlash("public/s1/index.html"), "TheContent") + th.assertFileContent(filepath.FromSlash("public/s1/l1.png"), "PNG") - th.assertFileNotExist(workDir + "/public/s2/index.html") + th.assertFileNotExist("public/s2/index.html") // But the bundled resources needs to be published - th.assertFileContent(filepath.FromSlash(workDir+"/public/s2/l1.png"), "PNG") + th.assertFileContent(filepath.FromSlash("public/s2/l1.png"), "PNG") // No headless bundles here, please. // https://github.com/gohugoio/hugo/issues/6492 @@ -1321,7 +1322,7 @@ func TestPageBundlerHome(t *testing.T) { workDir, clean, err := htesting.CreateTempDir(hugofs.Os, "hugo-bundler-home") c.Assert(err, qt.IsNil) - cfg := config.New() + cfg := config.NewWithTestDefaults() cfg.Set("workingDir", workDir) fs := hugofs.NewFrom(hugofs.Os, cfg) diff --git a/hugolib/paths/paths.go b/hugolib/paths/paths.go index 1ab7ae87e..f6e7b1a76 100644 --- a/hugolib/paths/paths.go +++ b/hugolib/paths/paths.go @@ -18,6 +18,8 @@ import ( "path/filepath" "strings" + hpaths "github.com/gohugoio/hugo/common/paths" + "github.com/gohugoio/hugo/config" "github.com/gohugoio/hugo/langs" "github.com/gohugoio/hugo/modules" @@ -51,6 +53,7 @@ type Paths struct { // pagination path handling PaginatePath string + // TODO1 check usage PublishDir string // When in multihost mode, this returns a list of base paths below PublishDir @@ -123,7 +126,7 @@ func New(fs *hugofs.Fs, cfg config.Provider) (*Paths, error) { languages = langs.Languages{&langs.Language{Lang: "en", Cfg: cfg, ContentDir: contentDir}} } - absPublishDir := AbsPathify(workingDir, publishDir) + absPublishDir := hpaths.AbsPathify(workingDir, publishDir) if !strings.HasSuffix(absPublishDir, FilePathSeparator) { absPublishDir += FilePathSeparator } @@ -131,7 +134,7 @@ func New(fs *hugofs.Fs, cfg config.Provider) (*Paths, error) { if absPublishDir == "//" { absPublishDir = FilePathSeparator } - absResourcesDir := AbsPathify(workingDir, resourceDir) + absResourcesDir := hpaths.AbsPathify(workingDir, resourceDir) if !strings.HasSuffix(absResourcesDir, FilePathSeparator) { absResourcesDir += FilePathSeparator } @@ -254,7 +257,7 @@ func (p *Paths) GetLangSubDir(lang string) string { // AbsPathify creates an absolute path if given a relative path. If already // absolute, the path is just cleaned. func (p *Paths) AbsPathify(inPath string) string { - return AbsPathify(p.WorkingDir, inPath) + return hpaths.AbsPathify(p.WorkingDir, inPath) } // RelPathify trims any WorkingDir prefix from the given filename. If @@ -267,12 +270,3 @@ func (p *Paths) RelPathify(filename string) string { return strings.TrimPrefix(strings.TrimPrefix(filename, p.WorkingDir), FilePathSeparator) } - -// AbsPathify creates an absolute path if given a working dir and a relative path. -// If already absolute, the path is just cleaned. -func AbsPathify(workingDir, inPath string) string { - if filepath.IsAbs(inPath) { - return filepath.Clean(inPath) - } - return filepath.Join(workingDir, inPath) -} diff --git a/hugolib/paths/paths_test.go b/hugolib/paths/paths_test.go index 4e68acafb..cd9d0593f 100644 --- a/hugolib/paths/paths_test.go +++ b/hugolib/paths/paths_test.go @@ -25,7 +25,7 @@ import ( func TestNewPaths(t *testing.T) { c := qt.New(t) - v := config.New() + v := config.NewWithTestDefaults() fs := hugofs.NewMem(v) v.Set("languages", map[string]any{ diff --git a/hugolib/resource_chain_test.go b/hugolib/resource_chain_test.go index c7bf8a68a..d94d389a7 100644 --- a/hugolib/resource_chain_test.go +++ b/hugolib/resource_chain_test.go @@ -137,7 +137,7 @@ Edited content. `) - b.Assert(b.Fs.Destination.Remove("public"), qt.IsNil) + b.Assert(b.Fs.WorkingDirWritable.Remove("public"), qt.IsNil) b.H.ResourceSpec.ClearCaches() } diff --git a/hugolib/robotstxt_test.go b/hugolib/robotstxt_test.go index 2035c235f..c58795ca4 100644 --- a/hugolib/robotstxt_test.go +++ b/hugolib/robotstxt_test.go @@ -28,7 +28,7 @@ const robotTxtTemplate = `User-agent: Googlebot func TestRobotsTXTOutput(t *testing.T) { t.Parallel() - cfg := config.New() + cfg := config.NewWithTestDefaults() cfg.Set("baseURL", "http://auth/bub/") cfg.Set("enableRobotsTXT", true) diff --git a/hugolib/rss_test.go b/hugolib/rss_test.go index 634843e3d..5da8ea0d6 100644 --- a/hugolib/rss_test.go +++ b/hugolib/rss_test.go @@ -50,7 +50,7 @@ func TestRSSOutput(t *testing.T) { th.assertFileContent(filepath.Join("public", "categories", "hugo", rssURI), "") if c != rssLimit { t.Errorf("incorrect RSS item count: expected %d, got %d", rssLimit, c) diff --git a/hugolib/shortcode_test.go b/hugolib/shortcode_test.go index 1f2a71bc9..c2c5abe87 100644 --- a/hugolib/shortcode_test.go +++ b/hugolib/shortcode_test.go @@ -1212,7 +1212,7 @@ title: "Hugo Rocks!" func TestShortcodeEmoji(t *testing.T) { t.Parallel() - v := config.New() + v := config.NewWithTestDefaults() v.Set("enableEmoji", true) builder := newTestSitesBuilder(t).WithViper(v) @@ -1277,7 +1277,7 @@ func TestShortcodeRef(t *testing.T) { t.Run(fmt.Sprintf("plainIDAnchors=%t", plainIDAnchors), func(t *testing.T) { t.Parallel() - v := config.New() + v := config.NewWithTestDefaults() v.Set("baseURL", "https://example.org") v.Set("blackfriday", map[string]any{ "plainIDAnchors": plainIDAnchors, diff --git a/hugolib/site_output_test.go b/hugolib/site_output_test.go index 843a13248..1a8bbadec 100644 --- a/hugolib/site_output_test.go +++ b/hugolib/site_output_test.go @@ -363,7 +363,7 @@ func TestCreateSiteOutputFormats(t *testing.T) { page.KindSection: []string{"JSON"}, } - cfg := config.New() + cfg := config.NewWithTestDefaults() cfg.Set("outputs", outputsConfig) outputs, err := createSiteOutputFormats(output.DefaultFormats, cfg.GetStringMap("outputs"), false) @@ -388,7 +388,7 @@ func TestCreateSiteOutputFormats(t *testing.T) { // Issue #4528 t.Run("Mixed case", func(t *testing.T) { c := qt.New(t) - cfg := config.New() + cfg := config.NewWithTestDefaults() outputsConfig := map[string]any{ // Note that we in Hugo 0.53.0 renamed this Kind to "taxonomy", @@ -410,7 +410,7 @@ func TestCreateSiteOutputFormatsInvalidConfig(t *testing.T) { page.KindHome: []string{"FOO", "JSON"}, } - cfg := config.New() + cfg := config.NewWithTestDefaults() cfg.Set("outputs", outputsConfig) _, err := createSiteOutputFormats(output.DefaultFormats, cfg.GetStringMap("outputs"), false) @@ -424,7 +424,7 @@ func TestCreateSiteOutputFormatsEmptyConfig(t *testing.T) { page.KindHome: []string{}, } - cfg := config.New() + cfg := config.NewWithTestDefaults() cfg.Set("outputs", outputsConfig) outputs, err := createSiteOutputFormats(output.DefaultFormats, cfg.GetStringMap("outputs"), false) @@ -439,7 +439,7 @@ func TestCreateSiteOutputFormatsCustomFormats(t *testing.T) { page.KindHome: []string{}, } - cfg := config.New() + cfg := config.NewWithTestDefaults() cfg.Set("outputs", outputsConfig) var ( diff --git a/hugolib/site_test.go b/hugolib/site_test.go index 1012144fb..012e824ba 100644 --- a/hugolib/site_test.go +++ b/hugolib/site_test.go @@ -336,7 +336,7 @@ func doTestShouldAlwaysHaveUglyURLs(t *testing.T, uglyURLs bool) { } for _, test := range tests { - content := readDestination(t, fs, test.doc) + content := readWorkingDir(t, fs, test.doc) if content != test.expected { t.Errorf("%s content expected:\n%q\ngot:\n%q", test.doc, test.expected, content) @@ -362,7 +362,7 @@ func TestMainSections(t *testing.T) { c := qt.New(t) for _, paramSet := range []bool{false, true} { c.Run(fmt.Sprintf("param-%t", paramSet), func(c *qt.C) { - v := config.New() + v := config.NewWithTestDefaults() if paramSet { v.Set("params", map[string]any{ "mainSections": []string{"a1", "a2"}, diff --git a/hugolib/site_url_test.go b/hugolib/site_url_test.go index d668095b9..ec68d21fc 100644 --- a/hugolib/site_url_test.go +++ b/hugolib/site_url_test.go @@ -76,7 +76,7 @@ func TestPageCount(t *testing.T) { writeSourcesToSource(t, "", fs, urlFakeSource...) s := buildSingleSite(t, deps.DepsCfg{Fs: fs, Cfg: cfg}, BuildCfg{}) - _, err := s.Fs.Destination.Open("public/blue") + _, err := s.Fs.WorkingDirReadOnly.Open("public/blue") if err != nil { t.Errorf("No indexed rendered.") } @@ -87,7 +87,7 @@ func TestPageCount(t *testing.T) { "public/sd3/index.html", "public/sd4.html", } { - if _, err := s.Fs.Destination.Open(filepath.FromSlash(pth)); err != nil { + if _, err := s.Fs.WorkingDirReadOnly.Open(filepath.FromSlash(pth)); err != nil { t.Errorf("No alias rendered: %s", pth) } } diff --git a/hugolib/sitemap_test.go b/hugolib/sitemap_test.go index 28d7d6eb5..cb4eea234 100644 --- a/hugolib/sitemap_test.go +++ b/hugolib/sitemap_test.go @@ -80,7 +80,7 @@ func doTestSitemapOutput(t *testing.T, internal bool) { "