Create a struct with all of Hugo's config options

Primary motivation is documentation, but it will also hopefully simplify the code.

Also,

* Lower case the default output format names; this is in line with the custom ones (map keys) and how
it's treated all the places. This avoids doing `stringds.EqualFold` everywhere.

Closes #10896
Closes #10620
This commit is contained in:
Bjørn Erik Pedersen
2023-01-04 18:24:36 +01:00
parent 6aededf6b4
commit 241b21b0fd
337 changed files with 13377 additions and 14898 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
// Copyright 2023 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -14,331 +14,28 @@
package commands
import (
"fmt"
"os"
"time"
"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"
"github.com/bep/simplecobra"
)
type commandsBuilder struct {
hugoBuilderCommon
commands []cmder
}
func newCommandsBuilder() *commandsBuilder {
return &commandsBuilder{}
}
func (b *commandsBuilder) addCommands(commands ...cmder) *commandsBuilder {
b.commands = append(b.commands, commands...)
return b
}
func (b *commandsBuilder) addAll() *commandsBuilder {
b.addCommands(
b.newServerCmd(),
newVersionCmd(),
newEnvCmd(),
b.newConfigCmd(),
b.newDeployCmd(),
b.newConvertCmd(),
b.newNewCmd(),
b.newListCmd(),
newImportCmd(),
newGenCmd(),
createReleaser(),
b.newModCmd(),
)
return b
}
func (b *commandsBuilder) build() *hugoCmd {
h := b.newHugoCmd()
addCommands(h.getCommand(), b.commands...)
return h
}
func addCommands(root *cobra.Command, commands ...cmder) {
for _, command := range commands {
cmd := command.getCommand()
if cmd == nil {
continue
}
root.AddCommand(cmd)
}
}
type baseCmd struct {
cmd *cobra.Command
}
var _ commandsBuilderGetter = (*baseBuilderCmd)(nil)
// Used in tests.
type commandsBuilderGetter interface {
getCommandsBuilder() *commandsBuilder
}
type baseBuilderCmd struct {
*baseCmd
*commandsBuilder
}
func (b *baseBuilderCmd) getCommandsBuilder() *commandsBuilder {
return b.commandsBuilder
}
func (c *baseCmd) getCommand() *cobra.Command {
return c.cmd
}
func newBaseCmd(cmd *cobra.Command) *baseCmd {
return &baseCmd{cmd: cmd}
}
func (b *commandsBuilder) newBuilderCmd(cmd *cobra.Command) *baseBuilderCmd {
bcmd := &baseBuilderCmd{commandsBuilder: b, baseCmd: &baseCmd{cmd: cmd}}
bcmd.hugoBuilderCommon.handleFlags(cmd)
return bcmd
}
func (b *commandsBuilder) newBuilderBasicCmd(cmd *cobra.Command) *baseBuilderCmd {
bcmd := &baseBuilderCmd{commandsBuilder: b, baseCmd: &baseCmd{cmd: cmd}}
bcmd.hugoBuilderCommon.handleCommonBuilderFlags(cmd)
return bcmd
}
func (c *baseCmd) flagsToConfig(cfg config.Provider) {
initializeFlags(c.cmd, cfg)
}
type hugoCmd struct {
*baseBuilderCmd
// Need to get the sites once built.
c *commandeer
}
var _ cmder = (*nilCommand)(nil)
type nilCommand struct{}
func (c *nilCommand) getCommand() *cobra.Command {
return nil
}
func (c *nilCommand) flagsToConfig(cfg config.Provider) {
}
func (b *commandsBuilder) newHugoCmd() *hugoCmd {
cc := &hugoCmd{}
cc.baseBuilderCmd = b.newBuilderCmd(&cobra.Command{
Use: "hugo",
Short: "hugo builds your site",
Long: `hugo is the main command, used to build your Hugo site.
Hugo is a Fast and Flexible Static Site Generator
built with love by spf13 and friends in Go.
Complete documentation is available at https://gohugo.io/.`,
RunE: func(cmd *cobra.Command, args []string) error {
defer cc.timeTrack(time.Now(), "Total")
cfgInit := func(c *commandeer) error {
if cc.buildWatch {
c.Set("disableLiveReload", true)
}
return nil
}
// prevent cobra printing error so it can be handled here (before the timeTrack prints)
cmd.SilenceErrors = true
c, err := initializeConfig(true, true, cc.buildWatch, &cc.hugoBuilderCommon, cc, cfgInit)
if err != nil {
cmd.PrintErrln("Error:", err.Error())
return err
}
cc.c = c
err = c.build()
if err != nil {
cmd.PrintErrln("Error:", err.Error())
}
return err
// newExec wires up all of Hugo's CLI.
func newExec() (*simplecobra.Exec, error) {
rootCmd := &rootCommand{
commands: []simplecobra.Commander{
newVersionCmd(),
newEnvCommand(),
newServerCommand(),
newDeployCommand(),
newConfigCommand(),
newNewCommand(),
newConvertCommand(),
newImportCommand(),
newListCommand(),
newModCommands(),
newGenCommand(),
newReleaseCommand(),
},
})
}
cc.cmd.PersistentFlags().StringVar(&cc.cfgFile, "config", "", "config file (default is hugo.yaml|json|toml)")
cc.cmd.PersistentFlags().StringVar(&cc.cfgDir, "configDir", "config", "config dir")
cc.cmd.PersistentFlags().BoolVar(&cc.quiet, "quiet", false, "build in quiet mode")
return simplecobra.New(rootCmd)
// Set bash-completion
_ = cc.cmd.PersistentFlags().SetAnnotation("config", cobra.BashCompFilenameExt, config.ValidConfigFileExtensions)
cc.cmd.PersistentFlags().BoolVarP(&cc.verbose, "verbose", "v", false, "verbose output")
cc.cmd.PersistentFlags().BoolVarP(&cc.debug, "debug", "", false, "debug output")
cc.cmd.PersistentFlags().BoolVar(&cc.logging, "log", false, "enable Logging")
cc.cmd.PersistentFlags().StringVar(&cc.logFile, "logFile", "", "log File path (if set, logging enabled automatically)")
cc.cmd.PersistentFlags().BoolVar(&cc.verboseLog, "verboseLog", false, "verbose logging")
cc.cmd.Flags().BoolVarP(&cc.buildWatch, "watch", "w", false, "watch filesystem for changes and recreate as needed")
cc.cmd.Flags().Bool("renderToMemory", false, "render to memory (only useful for benchmark testing)")
// Set bash-completion
_ = cc.cmd.PersistentFlags().SetAnnotation("logFile", cobra.BashCompFilenameExt, []string{})
cc.cmd.SetGlobalNormalizationFunc(helpers.NormalizeHugoFlags)
cc.cmd.SilenceUsage = true
return cc
}
type hugoBuilderCommon struct {
source string
baseURL string
environment string
buildWatch bool
panicOnWarning bool
poll string
clock string
gc bool
// Profile flags (for debugging of performance problems)
cpuprofile string
memprofile string
mutexprofile string
traceprofile string
printm bool
// TODO(bep) var vs string
logging bool
verbose bool
verboseLog bool
debug bool
quiet bool
cfgFile string
cfgDir string
logFile string
}
func (cc *hugoBuilderCommon) timeTrack(start time.Time, name string) {
if cc.quiet {
return
}
elapsed := time.Since(start)
fmt.Printf("%s in %v ms\n", name, int(1000*elapsed.Seconds()))
}
func (cc *hugoBuilderCommon) getConfigDir(baseDir string) string {
if cc.cfgDir != "" {
return hpaths.AbsPathify(baseDir, cc.cfgDir)
}
if v, found := os.LookupEnv("HUGO_CONFIGDIR"); found {
return hpaths.AbsPathify(baseDir, v)
}
return hpaths.AbsPathify(baseDir, "config")
}
func (cc *hugoBuilderCommon) getEnvironment(isServer bool) string {
if cc.environment != "" {
return cc.environment
}
if v, found := os.LookupEnv("HUGO_ENVIRONMENT"); found {
return v
}
// Used by Netlify and Forestry
if v, found := os.LookupEnv("HUGO_ENV"); found {
return v
}
if isServer {
return hugo.EnvironmentDevelopment
}
return hugo.EnvironmentProduction
}
func (cc *hugoBuilderCommon) handleCommonBuilderFlags(cmd *cobra.Command) {
cmd.PersistentFlags().StringVarP(&cc.source, "source", "s", "", "filesystem path to read files relative from")
cmd.PersistentFlags().SetAnnotation("source", cobra.BashCompSubdirsInDir, []string{})
cmd.PersistentFlags().StringVarP(&cc.environment, "environment", "e", "", "build environment")
cmd.PersistentFlags().StringP("themesDir", "", "", "filesystem path to themes directory")
cmd.PersistentFlags().StringP("ignoreVendorPaths", "", "", "ignores any _vendor for module paths matching the given Glob pattern")
cmd.PersistentFlags().StringVar(&cc.clock, "clock", "", "set the clock used by Hugo, e.g. --clock 2021-11-06T22:30:00.00+09:00")
}
func (cc *hugoBuilderCommon) handleFlags(cmd *cobra.Command) {
cc.handleCommonBuilderFlags(cmd)
cmd.Flags().Bool("cleanDestinationDir", false, "remove files from destination not found in static directories")
cmd.Flags().BoolP("buildDrafts", "D", false, "include content marked as draft")
cmd.Flags().BoolP("buildFuture", "F", false, "include content with publishdate in the future")
cmd.Flags().BoolP("buildExpired", "E", false, "include expired content")
cmd.Flags().StringP("contentDir", "c", "", "filesystem path to content directory")
cmd.Flags().StringP("layoutDir", "l", "", "filesystem path to layout directory")
cmd.Flags().StringP("cacheDir", "", "", "filesystem path to cache directory. Defaults: $TMPDIR/hugo_cache/")
cmd.Flags().BoolP("ignoreCache", "", false, "ignores the cache directory")
cmd.Flags().StringP("destination", "d", "", "filesystem path to write files to")
cmd.Flags().StringSliceP("theme", "t", []string{}, "themes to use (located in /themes/THEMENAME/)")
cmd.Flags().StringVarP(&cc.baseURL, "baseURL", "b", "", "hostname (and path) to the root, e.g. https://spf13.com/")
cmd.Flags().Bool("enableGitInfo", false, "add Git revision, date, author, and CODEOWNERS info to the pages")
cmd.Flags().BoolVar(&cc.gc, "gc", false, "enable to run some cleanup tasks (remove unused cache files) after the build")
cmd.Flags().StringVar(&cc.poll, "poll", "", "set this to a poll interval, e.g --poll 700ms, to use a poll based approach to watch for file system changes")
cmd.Flags().BoolVar(&cc.panicOnWarning, "panicOnWarning", false, "panic on first WARNING log")
cmd.Flags().Bool("templateMetrics", false, "display metrics about template executions")
cmd.Flags().Bool("templateMetricsHints", false, "calculate some improvement hints when combined with --templateMetrics")
cmd.Flags().BoolP("forceSyncStatic", "", false, "copy all files when static is changed.")
cmd.Flags().BoolP("noTimes", "", false, "don't sync modification time of files")
cmd.Flags().BoolP("noChmod", "", false, "don't sync permission mode of files")
cmd.Flags().BoolP("noBuildLock", "", false, "don't create .hugo_build.lock file")
cmd.Flags().BoolP("printI18nWarnings", "", false, "print missing translations")
cmd.Flags().BoolP("printPathWarnings", "", false, "print warnings on duplicate target paths etc.")
cmd.Flags().BoolP("printUnusedTemplates", "", false, "print warnings on unused templates.")
cmd.Flags().StringVarP(&cc.cpuprofile, "profile-cpu", "", "", "write cpu profile to `file`")
cmd.Flags().StringVarP(&cc.memprofile, "profile-mem", "", "", "write memory profile to `file`")
cmd.Flags().BoolVarP(&cc.printm, "printMemoryUsage", "", false, "print memory usage to screen at intervals")
cmd.Flags().StringVarP(&cc.mutexprofile, "profile-mutex", "", "", "write Mutex profile to `file`")
cmd.Flags().StringVarP(&cc.traceprofile, "trace", "", "", "write trace to `file` (not useful in general)")
// Hide these for now.
cmd.Flags().MarkHidden("profile-cpu")
cmd.Flags().MarkHidden("profile-mem")
cmd.Flags().MarkHidden("profile-mutex")
cmd.Flags().StringSlice("disableKinds", []string{}, "disable different kind of pages (home, RSS etc.)")
cmd.Flags().Bool("minify", false, "minify any supported output format (HTML, XML etc.)")
// Set bash-completion.
// Each flag must first be defined before using the SetAnnotation() call.
_ = cmd.Flags().SetAnnotation("source", cobra.BashCompSubdirsInDir, []string{})
_ = cmd.Flags().SetAnnotation("cacheDir", cobra.BashCompSubdirsInDir, []string{})
_ = cmd.Flags().SetAnnotation("destination", cobra.BashCompSubdirsInDir, []string{})
_ = cmd.Flags().SetAnnotation("theme", cobra.BashCompSubdirsInDir, []string{"themes"})
}
func checkErr(logger loggers.Logger, err error, s ...string) {
if err == nil {
return
}
for _, message := range s {
logger.Errorln(message)
}
logger.Errorln(err)
}

View File

@@ -1,411 +0,0 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package commands
import (
"fmt"
"os"
"path/filepath"
"testing"
"github.com/gohugoio/hugo/config"
"github.com/spf13/afero"
"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/common/types"
"github.com/spf13/cobra"
qt "github.com/frankban/quicktest"
)
func TestExecute(t *testing.T) {
c := qt.New(t)
createSite := func(c *qt.C) string {
dir := createSimpleTestSite(t, testSiteConfig{})
return dir
}
c.Run("hugo", func(c *qt.C) {
dir := createSite(c)
resp := Execute([]string{"-s=" + dir})
c.Assert(resp.Err, qt.IsNil)
result := resp.Result
c.Assert(len(result.Sites) == 1, qt.Equals, true)
c.Assert(len(result.Sites[0].RegularPages()) == 2, qt.Equals, true)
c.Assert(result.Sites[0].Info.Params()["myparam"], qt.Equals, "paramproduction")
})
c.Run("hugo, set environment", func(c *qt.C) {
dir := createSite(c)
resp := Execute([]string{"-s=" + dir, "-e=staging"})
c.Assert(resp.Err, qt.IsNil)
result := resp.Result
c.Assert(result.Sites[0].Info.Params()["myparam"], qt.Equals, "paramstaging")
})
c.Run("convert toJSON", func(c *qt.C) {
dir := createSite(c)
output := filepath.Join(dir, "myjson")
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"))
c.Assert(converted, qt.Equals, "{\n \"title\": \"P1\",\n \"weight\": 1\n}\n\nContent\n\n", qt.Commentf(converted))
})
c.Run("config, set environment", func(c *qt.C) {
dir := createSite(c)
out, err := captureStdout(func() error {
resp := Execute([]string{"config", "-s=" + dir, "-e=staging"})
return resp.Err
})
c.Assert(err, qt.IsNil)
c.Assert(out, qt.Contains, "params = map[myparam:paramstaging]", qt.Commentf(out))
})
c.Run("deploy, environment set", func(c *qt.C) {
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 := createSite(c)
out, err := captureStdout(func() error {
resp := Execute([]string{"list", "all", "-s=" + dir, "-e=staging"})
return resp.Err
})
c.Assert(err, qt.IsNil)
c.Assert(out, qt.Contains, "p1.md")
})
c.Run("new theme", func(c *qt.C) {
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)
themeTOML := readFileFrom(c, filepath.Join(themesDir, "mytheme", "theme.toml"))
c.Assert(themeTOML, qt.Contains, "name = \"Mytheme\"")
})
c.Run("new site", func(c *qt.C) {
dir := createSite(c)
siteDir := filepath.Join(dir, "mysite")
resp := Execute([]string{"new", "site", siteDir, "-e=staging"})
c.Assert(resp.Err, qt.IsNil)
config := readFileFrom(c, filepath.Join(siteDir, "config.toml"))
c.Assert(config, qt.Contains, "baseURL = 'http://example.org/'")
checkNewSiteInited(c, siteDir)
})
}
func checkNewSiteInited(c *qt.C, basepath string) {
paths := []string{
filepath.Join(basepath, "archetypes"),
filepath.Join(basepath, "assets"),
filepath.Join(basepath, "content"),
filepath.Join(basepath, "data"),
filepath.Join(basepath, "layouts"),
filepath.Join(basepath, "static"),
filepath.Join(basepath, "themes"),
filepath.Join(basepath, "config.toml"),
}
for _, path := range paths {
_, err := os.Stat(path)
c.Assert(err, qt.IsNil)
}
}
func readFileFrom(c *qt.C, filename string) string {
c.Helper()
filename = filepath.Clean(filename)
b, err := afero.ReadFile(hugofs.Os, filename)
c.Assert(err, qt.IsNil)
return string(b)
}
func TestFlags(t *testing.T) {
c := qt.New(t)
noOpRunE := func(cmd *cobra.Command, args []string) error {
return nil
}
tests := []struct {
name string
args []string
check func(c *qt.C, cmd *serverCmd)
}{
{
// https://github.com/gohugoio/hugo/issues/7642
name: "ignoreVendorPaths",
args: []string{"server", "--ignoreVendorPaths=github.com/**"},
check: func(c *qt.C, cmd *serverCmd) {
cfg := config.NewWithTestDefaults()
cmd.flagsToConfig(cfg)
c.Assert(cfg.Get("ignoreVendorPaths"), qt.Equals, "github.com/**")
},
},
{
name: "Persistent flags",
args: []string{
"server",
"--config=myconfig.toml",
"--configDir=myconfigdir",
"--contentDir=mycontent",
"--disableKinds=page,home",
"--environment=testing",
"--configDir=myconfigdir",
"--layoutDir=mylayouts",
"--theme=mytheme",
"--gc",
"--themesDir=mythemes",
"--cleanDestinationDir",
"--navigateToChanged",
"--disableLiveReload",
"--noHTTPCache",
"--printI18nWarnings",
"--destination=/tmp/mydestination",
"-b=https://example.com/b/",
"--port=1366",
"--renderToDisk",
"--source=mysource",
"--printPathWarnings",
"--printUnusedTemplates",
},
check: func(c *qt.C, sc *serverCmd) {
c.Assert(sc, qt.Not(qt.IsNil))
c.Assert(sc.navigateToChanged, qt.Equals, true)
c.Assert(sc.disableLiveReload, qt.Equals, true)
c.Assert(sc.noHTTPCache, qt.Equals, true)
c.Assert(sc.renderToDisk, qt.Equals, true)
c.Assert(sc.serverPort, qt.Equals, 1366)
c.Assert(sc.environment, qt.Equals, "testing")
cfg := config.NewWithTestDefaults()
sc.flagsToConfig(cfg)
c.Assert(cfg.GetString("publishDir"), qt.Equals, "/tmp/mydestination")
c.Assert(cfg.GetString("contentDir"), qt.Equals, "mycontent")
c.Assert(cfg.GetString("layoutDir"), qt.Equals, "mylayouts")
c.Assert(cfg.GetStringSlice("theme"), qt.DeepEquals, []string{"mytheme"})
c.Assert(cfg.GetString("themesDir"), qt.Equals, "mythemes")
c.Assert(cfg.GetString("baseURL"), qt.Equals, "https://example.com/b/")
c.Assert(cfg.Get("disableKinds"), qt.DeepEquals, []string{"page", "home"})
c.Assert(cfg.GetBool("gc"), qt.Equals, true)
// The flag is named printPathWarnings
c.Assert(cfg.GetBool("logPathWarnings"), qt.Equals, true)
// The flag is named printI18nWarnings
c.Assert(cfg.GetBool("logI18nWarnings"), qt.Equals, true)
},
},
}
for _, test := range tests {
c.Run(test.name, func(c *qt.C) {
b := newCommandsBuilder()
root := b.addAll().build()
for _, cmd := range b.commands {
if cmd.getCommand() == nil {
continue
}
// We are only interested in the flag handling here.
cmd.getCommand().RunE = noOpRunE
}
rootCmd := root.getCommand()
rootCmd.SetArgs(test.args)
c.Assert(rootCmd.Execute(), qt.IsNil)
test.check(c, b.commands[0].(*serverCmd))
})
}
}
func TestCommandsExecute(t *testing.T) {
c := qt.New(t)
dir := createSimpleTestSite(t, testSiteConfig{})
dirOut := t.TempDir()
sourceFlag := fmt.Sprintf("-s=%s", dir)
tests := []struct {
commands []string
flags []string
expectErrToContain string
}{
// TODO(bep) permission issue on my OSX? "operation not permitted" {[]string{"check", "ulimit"}, nil, false},
{[]string{"env"}, nil, ""},
{[]string{"version"}, nil, ""},
// no args = hugo build
{nil, []string{sourceFlag}, ""},
{nil, []string{sourceFlag, "--renderToMemory"}, ""},
{[]string{"completion", "bash"}, nil, ""},
{[]string{"completion", "fish"}, nil, ""},
{[]string{"completion", "powershell"}, nil, ""},
{[]string{"completion", "zsh"}, nil, ""},
{[]string{"config"}, []string{sourceFlag}, ""},
{[]string{"convert", "toTOML"}, []string{sourceFlag, "-o=" + filepath.Join(dirOut, "toml")}, ""},
{[]string{"convert", "toYAML"}, []string{sourceFlag, "-o=" + filepath.Join(dirOut, "yaml")}, ""},
{[]string{"convert", "toJSON"}, []string{sourceFlag, "-o=" + filepath.Join(dirOut, "json")}, ""},
{[]string{"gen", "chromastyles"}, []string{"--style=manni"}, ""},
{[]string{"gen", "doc"}, []string{"--dir=" + filepath.Join(dirOut, "doc")}, ""},
{[]string{"gen", "man"}, []string{"--dir=" + filepath.Join(dirOut, "man")}, ""},
{[]string{"list", "drafts"}, []string{sourceFlag}, ""},
{[]string{"list", "expired"}, []string{sourceFlag}, ""},
{[]string{"list", "future"}, []string{sourceFlag}, ""},
{[]string{"new", "new-page.md"}, []string{sourceFlag}, ""},
{[]string{"new", "site", filepath.Join(dirOut, "new-site")}, nil, ""},
{[]string{"unknowncommand"}, nil, "unknown command"},
// TODO(bep) cli refactor fix https://github.com/gohugoio/hugo/issues/4450
//{[]string{"new", "theme", filepath.Join(dirOut, "new-theme")}, nil,false},
}
for _, test := range tests {
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...))
// 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)
}
})
}
}
type testSiteConfig struct {
configTOML string
contentDir string
}
func createSimpleTestSite(t testing.TB, cfg testSiteConfig) string {
dir := t.TempDir()
cfgStr := `
baseURL = "https://example.org"
title = "Hugo Commands"
`
contentDir := "content"
if cfg.configTOML != "" {
cfgStr = cfg.configTOML
}
if cfg.contentDir != "" {
contentDir = cfg.contentDir
}
os.MkdirAll(filepath.Join(dir, "public"), 0777)
// Just the basic. These are for CLI tests, not site testing.
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(dir, "config", "testing", "params.toml"), `myparam="paramtesting"`)
writeFile(t, filepath.Join(dir, "config", "production", "params.toml"), `myparam="paramproduction"`)
writeFile(t, filepath.Join(dir, "static", "myfile.txt"), `Hello World!`)
writeFile(t, filepath.Join(dir, contentDir, "p1.md"), `
---
title: "P1"
weight: 1
---
Content
`)
writeFile(t, filepath.Join(dir, contentDir, "hügö.md"), `
---
weight: 2
---
This is hügö.
`)
writeFile(t, filepath.Join(dir, "layouts", "_default", "single.html"), `
Single: {{ .Title }}|{{ .Content }}
`)
writeFile(t, filepath.Join(dir, "layouts", "404.html"), `
404: {{ .Title }}|Not Found.
`)
writeFile(t, filepath.Join(dir, "layouts", "_default", "list.html"), `
List: {{ .Title }}
Environment: {{ hugo.Environment }}
For issue 9788:
{{ $foo :="abc" | resources.FromString "foo.css" | minify | resources.PostProcess }}
PostProcess: {{ $foo.RelPermalink }}
`)
return dir
}
func writeFile(t testing.TB, filename, content string) {
must(t, os.MkdirAll(filepath.Dir(filename), os.FileMode(0755)))
must(t, os.WriteFile(filename, []byte(content), os.FileMode(0755)))
}
func must(t testing.TB, err error) {
if err != nil {
t.Fatal(err)
}
}

View File

@@ -1,4 +1,4 @@
// Copyright 2015 The Hugo Authors. All rights reserved.
// Copyright 2023 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -9,129 +9,93 @@
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.Print the version number of Hug
// limitations under the License.
package commands
import (
"context"
"encoding/json"
"fmt"
"os"
"reflect"
"regexp"
"sort"
"strings"
"time"
"github.com/gohugoio/hugo/common/maps"
"github.com/bep/simplecobra"
"github.com/gohugoio/hugo/modules"
"github.com/gohugoio/hugo/parser"
"github.com/gohugoio/hugo/parser/metadecoders"
"github.com/gohugoio/hugo/modules"
"github.com/spf13/cobra"
)
var _ cmder = (*configCmd)(nil)
type configCmd struct {
*baseBuilderCmd
}
func (b *commandsBuilder) newConfigCmd() *configCmd {
cc := &configCmd{}
cmd := &cobra.Command{
Use: "config",
Short: "Print the site configuration",
Long: `Print the site configuration, both default and custom settings.`,
RunE: cc.printConfig,
// newConfigCommand creates a new config command and its subcommands.
func newConfigCommand() *configCommand {
return &configCommand{
commands: []simplecobra.Commander{
&configMountsCommand{},
},
}
printMountsCmd := &cobra.Command{
Use: "mounts",
Short: "Print the configured file mounts",
RunE: cc.printMounts,
}
cmd.AddCommand(printMountsCmd)
cc.baseBuilderCmd = b.newBuilderBasicCmd(cmd)
return cc
}
func (c *configCmd) printMounts(cmd *cobra.Command, args []string) error {
cfg, err := initializeConfig(true, false, false, &c.hugoBuilderCommon, c, nil)
type configCommand struct {
r *rootCommand
commands []simplecobra.Commander
}
func (c *configCommand) Commands() []simplecobra.Commander {
return c.commands
}
func (c *configCommand) Name() string {
return "config"
}
func (c *configCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error {
conf, err := c.r.ConfigFromProvider(c.r.configVersionID.Load(), flagsToCfg(cd, nil))
if err != nil {
return err
}
config := conf.configs.Base
allModules := cfg.Cfg.Get("allmodules").(modules.Modules)
// Print it as JSON.
dec := json.NewEncoder(os.Stdout)
dec.SetIndent("", " ")
dec.SetEscapeHTML(false)
for _, m := range allModules {
if err := parser.InterfaceToConfig(&modMounts{m: m, verbose: c.verbose}, metadecoders.JSON, os.Stdout); err != nil {
return err
}
if err := dec.Encode(parser.ReplacingJSONMarshaller{Value: config, KeysToLower: true, OmitEmpty: true}); err != nil {
return err
}
return nil
}
func (c *configCmd) printConfig(cmd *cobra.Command, args []string) error {
cfg, err := initializeConfig(true, false, false, &c.hugoBuilderCommon, c, nil)
if err != nil {
return err
}
allSettings := cfg.Cfg.Get("").(maps.Params)
// We need to clean up this, but we store objects in the config that
// isn't really interesting to the end user, so filter these.
ignoreKeysRe := regexp.MustCompile("client|sorted|filecacheconfigs|allmodules|multilingual")
separator := ": "
if len(cfg.configFiles) > 0 && strings.HasSuffix(cfg.configFiles[0], ".toml") {
separator = " = "
}
var keys []string
for k := range allSettings {
if ignoreKeysRe.MatchString(k) {
continue
}
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
kv := reflect.ValueOf(allSettings[k])
if kv.Kind() == reflect.String {
fmt.Printf("%s%s\"%+v\"\n", k, separator, allSettings[k])
} else {
fmt.Printf("%s%s%+v\n", k, separator, allSettings[k])
}
}
func (c *configCommand) WithCobraCommand(cmd *cobra.Command) error {
cmd.Short = "Print the site configuration"
cmd.Long = `Print the site configuration, both default and custom settings.`
return nil
}
type modMounts struct {
verbose bool
m modules.Module
func (c *configCommand) Init(cd, runner *simplecobra.Commandeer) error {
c.r = cd.Root.Command.(*rootCommand)
return nil
}
type modMount struct {
type configModMount struct {
Source string `json:"source"`
Target string `json:"target"`
Lang string `json:"lang,omitempty"`
}
type configModMounts struct {
verbose bool
m modules.Module
}
// MarshalJSON is for internal use only.
func (m *modMounts) MarshalJSON() ([]byte, error) {
var mounts []modMount
func (m *configModMounts) MarshalJSON() ([]byte, error) {
var mounts []configModMount
for _, mount := range m.m.Mounts() {
mounts = append(mounts, modMount{
mounts = append(mounts, configModMount{
Source: mount.Source,
Target: mount.Target,
Lang: mount.Lang,
@@ -154,7 +118,7 @@ func (m *modMounts) MarshalJSON() ([]byte, error) {
Meta map[string]any `json:"meta"`
HugoVersion modules.HugoVersion `json:"hugoVersion"`
Mounts []modMount `json:"mounts"`
Mounts []configModMount `json:"mounts"`
}{
Path: m.m.Path(),
Version: m.m.Version(),
@@ -168,12 +132,12 @@ func (m *modMounts) MarshalJSON() ([]byte, error) {
}
return json.Marshal(&struct {
Path string `json:"path"`
Version string `json:"version"`
Time time.Time `json:"time"`
Owner string `json:"owner"`
Dir string `json:"dir"`
Mounts []modMount `json:"mounts"`
Path string `json:"path"`
Version string `json:"version"`
Time time.Time `json:"time"`
Owner string `json:"owner"`
Dir string `json:"dir"`
Mounts []configModMount `json:"mounts"`
}{
Path: m.m.Path(),
Version: m.m.Version(),
@@ -184,3 +148,40 @@ func (m *modMounts) MarshalJSON() ([]byte, error) {
})
}
type configMountsCommand struct {
configCmd *configCommand
}
func (c *configMountsCommand) Commands() []simplecobra.Commander {
return nil
}
func (c *configMountsCommand) Name() string {
return "mounts"
}
func (c *configMountsCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error {
r := c.configCmd.r
conf, err := r.ConfigFromProvider(r.configVersionID.Load(), flagsToCfg(cd, nil))
if err != nil {
return err
}
for _, m := range conf.configs.Modules {
if err := parser.InterfaceToConfig(&configModMounts{m: m, verbose: r.verbose}, metadecoders.JSON, os.Stdout); err != nil {
return err
}
}
return nil
}
func (c *configMountsCommand) WithCobraCommand(cmd *cobra.Command) error {
cmd.Short = "Print the configured file mounts"
return nil
}
func (c *configMountsCommand) Init(cd, runner *simplecobra.Commandeer) error {
c.configCmd = cd.Parent.Command.(*configCommand)
return nil
}

View File

@@ -1,4 +1,4 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
// Copyright 2023 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -15,122 +15,119 @@ package commands
import (
"bytes"
"context"
"fmt"
"path/filepath"
"strings"
"time"
"github.com/gohugoio/hugo/parser/pageparser"
"github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/hugofs"
"github.com/bep/simplecobra"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/hugolib"
"github.com/gohugoio/hugo/parser"
"github.com/gohugoio/hugo/parser/metadecoders"
"github.com/gohugoio/hugo/hugolib"
"github.com/gohugoio/hugo/parser/pageparser"
"github.com/gohugoio/hugo/resources/page"
"github.com/spf13/cobra"
)
var _ cmder = (*convertCmd)(nil)
func newConvertCommand() *convertCommand {
var c *convertCommand
c = &convertCommand{
commands: []simplecobra.Commander{
&simpleCommand{
name: "toJSON",
short: "Convert front matter to JSON",
long: `toJSON converts all front matter in the content directory
to use JSON for the front matter.`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
return c.convertContents(metadecoders.JSON)
},
withc: func(cmd *cobra.Command) {
},
},
&simpleCommand{
name: "toTOML",
short: "Convert front matter to TOML",
long: `toTOML converts all front matter in the content directory
to use TOML for the front matter.`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
return c.convertContents(metadecoders.TOML)
},
withc: func(cmd *cobra.Command) {
},
},
&simpleCommand{
name: "toYAML",
short: "Convert front matter to YAML",
long: `toYAML converts all front matter in the content directory
to use YAML for the front matter.`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
return c.convertContents(metadecoders.YAML)
},
withc: func(cmd *cobra.Command) {
},
},
},
}
return c
}
type convertCmd struct {
type convertCommand struct {
// Flags.
outputDir string
unsafe bool
*baseBuilderCmd
// Deps.
r *rootCommand
h *hugolib.HugoSites
// Commmands.
commands []simplecobra.Commander
}
func (b *commandsBuilder) newConvertCmd() *convertCmd {
cc := &convertCmd{}
cmd := &cobra.Command{
Use: "convert",
Short: "Convert your content to different formats",
Long: `Convert your content (e.g. front matter) to different formats.
See convert's subcommands toJSON, toTOML and toYAML for more information.`,
RunE: nil,
}
cmd.AddCommand(
&cobra.Command{
Use: "toJSON",
Short: "Convert front matter to JSON",
Long: `toJSON converts all front matter in the content directory
to use JSON for the front matter.`,
RunE: func(cmd *cobra.Command, args []string) error {
return cc.convertContents(metadecoders.JSON)
},
},
&cobra.Command{
Use: "toTOML",
Short: "Convert front matter to TOML",
Long: `toTOML converts all front matter in the content directory
to use TOML for the front matter.`,
RunE: func(cmd *cobra.Command, args []string) error {
return cc.convertContents(metadecoders.TOML)
},
},
&cobra.Command{
Use: "toYAML",
Short: "Convert front matter to YAML",
Long: `toYAML converts all front matter in the content directory
to use YAML for the front matter.`,
RunE: func(cmd *cobra.Command, args []string) error {
return cc.convertContents(metadecoders.YAML)
},
},
)
cmd.PersistentFlags().StringVarP(&cc.outputDir, "output", "o", "", "filesystem path to write files to")
cmd.PersistentFlags().BoolVar(&cc.unsafe, "unsafe", false, "enable less safe operations, please backup first")
cc.baseBuilderCmd = b.newBuilderBasicCmd(cmd)
return cc
func (c *convertCommand) Commands() []simplecobra.Commander {
return c.commands
}
func (cc *convertCmd) convertContents(format metadecoders.Format) error {
if cc.outputDir == "" && !cc.unsafe {
return newUserError("Unsafe operation not allowed, use --unsafe or set a different output path")
}
func (c *convertCommand) Name() string {
return "convert"
}
c, err := initializeConfig(true, false, false, &cc.hugoBuilderCommon, cc, nil)
if err != nil {
return err
}
c.Cfg.Set("buildDrafts", true)
h, err := hugolib.NewHugoSites(*c.DepsCfg)
if err != nil {
return err
}
if err := h.Build(hugolib.BuildCfg{SkipRender: true}); err != nil {
return err
}
site := h.Sites[0]
site.Log.Println("processing", len(site.AllPages()), "content files")
for _, p := range site.AllPages() {
if err := cc.convertAndSavePage(p, site, format); err != nil {
return err
}
}
func (c *convertCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error {
return nil
}
func (cc *convertCmd) convertAndSavePage(p page.Page, site *hugolib.Site, targetFormat metadecoders.Format) error {
func (c *convertCommand) WithCobraCommand(cmd *cobra.Command) error {
cmd.Short = "Convert your content to different formats"
cmd.Long = `Convert your content (e.g. front matter) to different formats.
See convert's subcommands toJSON, toTOML and toYAML for more information.`
cmd.PersistentFlags().StringVarP(&c.outputDir, "output", "o", "", "filesystem path to write files to")
cmd.PersistentFlags().BoolVar(&c.unsafe, "unsafe", false, "enable less safe operations, please backup first")
return nil
}
func (c *convertCommand) Init(cd, runner *simplecobra.Commandeer) error {
c.r = cd.Root.Command.(*rootCommand)
cfg := config.New()
cfg.Set("buildDrafts", true)
h, err := c.r.Hugo(flagsToCfg(cd, cfg))
if err != nil {
return err
}
c.h = h
return nil
}
func (c *convertCommand) convertAndSavePage(p page.Page, site *hugolib.Site, targetFormat metadecoders.Format) error {
// The resources are not in .Site.AllPages.
for _, r := range p.Resources().ByType("page") {
if err := cc.convertAndSavePage(r.(page.Page), site, targetFormat); err != nil {
if err := c.convertAndSavePage(r.(page.Page), site, targetFormat); err != nil {
return err
}
}
@@ -140,9 +137,9 @@ func (cc *convertCmd) convertAndSavePage(p page.Page, site *hugolib.Site, target
return nil
}
errMsg := fmt.Errorf("Error processing file %q", p.File().Path())
errMsg := fmt.Errorf("error processing file %q", p.File().Path())
site.Log.Infoln("Attempting to convert", p.File().Filename())
site.Log.Infoln("ttempting to convert", p.File().Filename())
f := p.File()
file, err := f.FileInfo().Meta().Open()
@@ -182,26 +179,45 @@ func (cc *convertCmd) convertAndSavePage(p page.Page, site *hugolib.Site, target
newFilename := p.File().Filename()
if cc.outputDir != "" {
if c.outputDir != "" {
contentDir := strings.TrimSuffix(newFilename, p.File().Path())
contentDir = filepath.Base(contentDir)
newFilename = filepath.Join(cc.outputDir, contentDir, p.File().Path())
newFilename = filepath.Join(c.outputDir, contentDir, p.File().Path())
}
fs := hugofs.Os
if err := helpers.WriteToDisk(newFilename, &newContent, fs); err != nil {
return fmt.Errorf("Failed to save file %q:: %w", newFilename, err)
return fmt.Errorf("failed to save file %q:: %w", newFilename, err)
}
return nil
}
type parsedFile struct {
frontMatterFormat metadecoders.Format
frontMatterSource []byte
frontMatter map[string]any
func (c *convertCommand) convertContents(format metadecoders.Format) error {
if c.outputDir == "" && !c.unsafe {
return newUserError("Unsafe operation not allowed, use --unsafe or set a different output path")
}
// Everything after Front Matter
content []byte
if err := c.h.Build(hugolib.BuildCfg{SkipRender: true}); err != nil {
return err
}
site := c.h.Sites[0]
var pagesBackedByFile page.Pages
for _, p := range site.AllPages() {
if p.File().IsZero() {
continue
}
pagesBackedByFile = append(pagesBackedByFile, p)
}
site.Log.Println("processing", len(pagesBackedByFile), "content files")
for _, p := range site.AllPages() {
if err := c.convertAndSavePage(p, site, format); err != nil {
return err
}
}
return nil
}

View File

@@ -1,4 +1,4 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
// Copyright 2023 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -14,76 +14,58 @@
//go:build !nodeploy
// +build !nodeploy
// Copyright 2023 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package commands
import (
"context"
"github.com/bep/simplecobra"
"github.com/gohugoio/hugo/deploy"
"github.com/spf13/cobra"
)
var _ cmder = (*deployCmd)(nil)
func newDeployCommand() simplecobra.Commander {
// deployCmd supports deploying sites to Cloud providers.
type deployCmd struct {
*baseBuilderCmd
invalidateCDN bool
maxDeletes int
workers int
}
// TODO: In addition to the "deploy" command, consider adding a "--deploy"
// flag for the default command; this would build the site and then deploy it.
// It's not obvious how to do this; would all of the deploy-specific flags
// have to exist at the top level as well?
// TODO: The output files change every time "hugo" is executed, it looks
// like because of map order randomization. This means that you can
// run "hugo && hugo deploy" again and again and upload new stuff every time. Is
// this intended?
func (b *commandsBuilder) newDeployCmd() *deployCmd {
cc := &deployCmd{}
cmd := &cobra.Command{
Use: "deploy",
Short: "Deploy your site to a Cloud provider.",
Long: `Deploy your site to a Cloud provider.
return &simpleCommand{
name: "deploy",
short: "Deploy your site to a Cloud provider.",
long: `Deploy your site to a Cloud provider.
See https://gohugo.io/hosting-and-deployment/hugo-deploy/ for detailed
documentation.
`,
RunE: func(cmd *cobra.Command, args []string) error {
cfgInit := func(c *commandeer) error {
c.Set("invalidateCDN", cc.invalidateCDN)
c.Set("maxDeletes", cc.maxDeletes)
c.Set("workers", cc.workers)
return nil
}
comm, err := initializeConfig(true, true, false, &cc.hugoBuilderCommon, cc, cfgInit)
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
h, err := r.Hugo(flagsToCfgWithAdditionalConfigBase(cd, nil, "deployment"))
if err != nil {
return err
}
deployer, err := deploy.New(comm.Cfg, comm.hugo().PathSpec.PublishFs)
deployer, err := deploy.New(h.Configs.GetFirstLanguageConfig(), h.PathSpec.PublishFs)
if err != nil {
return err
}
return deployer.Deploy(context.Background())
return deployer.Deploy(ctx)
},
withc: func(cmd *cobra.Command) {
cmd.Flags().String("target", "", "target deployment from deployments section in config file; defaults to the first one")
cmd.Flags().Bool("confirm", false, "ask for confirmation before making changes to the target")
cmd.Flags().Bool("dryRun", false, "dry run")
cmd.Flags().Bool("force", false, "force upload of all files")
cmd.Flags().Bool("invalidateCDN", true, "invalidate the CDN cache listed in the deployment target")
cmd.Flags().Int("maxDeletes", 256, "maximum # of files to delete, or -1 to disable")
cmd.Flags().Int("workers", 10, "number of workers to transfer files. defaults to 10")
},
}
cmd.Flags().String("target", "", "target deployment from deployments section in config file; defaults to the first one")
cmd.Flags().Bool("confirm", false, "ask for confirmation before making changes to the target")
cmd.Flags().Bool("dryRun", false, "dry run")
cmd.Flags().Bool("force", false, "force upload of all files")
cmd.Flags().BoolVar(&cc.invalidateCDN, "invalidateCDN", true, "invalidate the CDN cache listed in the deployment target")
cmd.Flags().IntVar(&cc.maxDeletes, "maxDeletes", 256, "maximum # of files to delete, or -1 to disable")
cmd.Flags().IntVar(&cc.workers, "workers", 10, "number of workers to transfer files. defaults to 10")
cc.baseBuilderCmd = b.newBuilderBasicCmd(cmd)
return cc
}

48
commands/deploy_off.go Normal file
View File

@@ -0,0 +1,48 @@
// Copyright 2023 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build nodeploy
// +build nodeploy
// Copyright 2023 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package commands
import (
"errors"
"github.com/spf13/cobra"
)
func newDeployCommand() simplecobra.Commander {
return &simpleCommand{
name: "deploy",
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
return nil
},
withc: func(cmd *cobra.Command) {
cmd.Hidden = true
},
}
}

View File

@@ -1,4 +1,4 @@
// Copyright 2016 The Hugo Authors. All rights reserved.
// Copyright 2023 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -14,55 +14,50 @@
package commands
import (
"context"
"runtime"
"github.com/bep/simplecobra"
"github.com/gohugoio/hugo/common/hugo"
"github.com/spf13/cobra"
jww "github.com/spf13/jwalterweatherman"
)
var _ cmder = (*envCmd)(nil)
type envCmd struct {
*baseCmd
}
func newEnvCmd() *envCmd {
return &envCmd{
baseCmd: newBaseCmd(&cobra.Command{
Use: "env",
Short: "Print Hugo version and environment info",
Long: `Print Hugo version and environment info. This is useful in Hugo bug reports.
If you add the -v flag, you will get a full dependency list.
`,
RunE: func(cmd *cobra.Command, args []string) error {
printHugoVersion()
jww.FEEDBACK.Printf("GOOS=%q\n", runtime.GOOS)
jww.FEEDBACK.Printf("GOARCH=%q\n", runtime.GOARCH)
jww.FEEDBACK.Printf("GOVERSION=%q\n", runtime.Version())
isVerbose, _ := cmd.Flags().GetBool("verbose")
if isVerbose {
deps := hugo.GetDependencyList()
for _, dep := range deps {
jww.FEEDBACK.Printf("%s\n", dep)
}
} else {
// These are also included in the GetDependencyList above;
// always print these as these are most likely the most useful to know about.
deps := hugo.GetDependencyListNonGo()
for _, dep := range deps {
jww.FEEDBACK.Printf("%s\n", dep)
}
func newEnvCommand() simplecobra.Commander {
return &simpleCommand{
name: "env",
short: "Print Hugo version and environment info",
long: "Print Hugo version and environment info. This is useful in Hugo bug reports",
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
r.Printf("%s\n", hugo.BuildVersionString())
r.Printf("GOOS=%q\n", runtime.GOOS)
r.Printf("GOARCH=%q\n", runtime.GOARCH)
r.Printf("GOVERSION=%q\n", runtime.Version())
if r.verbose {
deps := hugo.GetDependencyList()
for _, dep := range deps {
r.Printf("%s\n", dep)
}
return nil
},
}),
} else {
// These are also included in the GetDependencyList above;
// always print these as these are most likely the most useful to know about.
deps := hugo.GetDependencyListNonGo()
for _, dep := range deps {
r.Printf("%s\n", dep)
}
}
return nil
},
}
}
func newVersionCmd() simplecobra.Commander {
return &simpleCommand{
name: "version",
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
r.Println(hugo.BuildVersionString())
return nil
},
short: "Print Hugo version and environment info",
long: "Print Hugo version and environment info. This is useful in Hugo bug reports.",
}
}

View File

@@ -1,4 +1,4 @@
// Copyright 2015 The Hugo Authors. All rights reserved.
// Copyright 2023 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -14,27 +14,200 @@
package commands
import (
"context"
"fmt"
"os"
"path"
"path/filepath"
"strings"
"github.com/alecthomas/chroma/v2"
"github.com/alecthomas/chroma/v2/formatters/html"
"github.com/alecthomas/chroma/v2/styles"
"github.com/bep/simplecobra"
"github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugofs"
"github.com/spf13/cobra"
"github.com/spf13/cobra/doc"
)
var _ cmder = (*genCmd)(nil)
func newGenCommand() *genCommand {
var (
// Flags.
gendocdir string
genmandir string
// Chroma flags.
style string
highlightStyle string
linesStyle string
)
newChromaStyles := func() simplecobra.Commander {
return &simpleCommand{
name: "chromastyles",
short: "Generate CSS stylesheet for the Chroma code highlighter",
long: `Generate CSS stylesheet for the Chroma code highlighter for a given style. This stylesheet is needed if markup.highlight.noClasses is disabled in config.
See https://xyproto.github.io/splash/docs/all.html for a preview of the available styles`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
builder := styles.Get(style).Builder()
if highlightStyle != "" {
builder.Add(chroma.LineHighlight, highlightStyle)
}
if linesStyle != "" {
builder.Add(chroma.LineNumbers, linesStyle)
}
style, err := builder.Build()
if err != nil {
return err
}
formatter := html.New(html.WithAllClasses(true))
formatter.WriteCSS(os.Stdout, style)
return nil
},
withc: func(cmd *cobra.Command) {
cmd.PersistentFlags().StringVar(&style, "style", "friendly", "highlighter style (see https://xyproto.github.io/splash/docs/)")
cmd.PersistentFlags().StringVar(&highlightStyle, "highlightStyle", "bg:#ffffcc", "style used for highlighting lines (see https://github.com/alecthomas/chroma)")
cmd.PersistentFlags().StringVar(&linesStyle, "linesStyle", "", "style used for line numbers (see https://github.com/alecthomas/chroma)")
},
}
}
newMan := func() simplecobra.Commander {
return &simpleCommand{
name: "man",
short: "Generate man pages for the Hugo CLI",
long: `This command automatically generates up-to-date man pages of Hugo's
command-line interface. By default, it creates the man page files
in the "man" directory under the current directory.`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
header := &doc.GenManHeader{
Section: "1",
Manual: "Hugo Manual",
Source: fmt.Sprintf("Hugo %s", hugo.CurrentVersion),
}
if !strings.HasSuffix(genmandir, helpers.FilePathSeparator) {
genmandir += helpers.FilePathSeparator
}
if found, _ := helpers.Exists(genmandir, hugofs.Os); !found {
r.Println("Directory", genmandir, "does not exist, creating...")
if err := hugofs.Os.MkdirAll(genmandir, 0777); err != nil {
return err
}
}
cd.CobraCommand.Root().DisableAutoGenTag = true
r.Println("Generating Hugo man pages in", genmandir, "...")
doc.GenManTree(cd.CobraCommand.Root(), header, genmandir)
r.Println("Done.")
return nil
},
withc: func(cmd *cobra.Command) {
cmd.PersistentFlags().StringVar(&genmandir, "dir", "man/", "the directory to write the man pages.")
// For bash-completion
cmd.PersistentFlags().SetAnnotation("dir", cobra.BashCompSubdirsInDir, []string{})
},
}
}
newGen := func() simplecobra.Commander {
const gendocFrontmatterTemplate = `---
title: "%s"
slug: %s
url: %s
---
`
return &simpleCommand{
name: "doc",
short: "Generate Markdown documentation for the Hugo CLI.",
long: `Generate Markdown documentation for the Hugo CLI.
This command is, mostly, used to create up-to-date documentation
of Hugo's command-line interface for https://gohugo.io/.
It creates one Markdown file per command with front matter suitable
for rendering in Hugo.`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
cd.CobraCommand.VisitParents(func(c *cobra.Command) {
// Disable the "Auto generated by spf13/cobra on DATE"
// as it creates a lot of diffs.
c.DisableAutoGenTag = true
})
if !strings.HasSuffix(gendocdir, helpers.FilePathSeparator) {
gendocdir += helpers.FilePathSeparator
}
if found, _ := helpers.Exists(gendocdir, hugofs.Os); !found {
r.Println("Directory", gendocdir, "does not exist, creating...")
if err := hugofs.Os.MkdirAll(gendocdir, 0777); err != nil {
return err
}
}
prepender := func(filename string) string {
name := filepath.Base(filename)
base := strings.TrimSuffix(name, path.Ext(name))
url := "/commands/" + strings.ToLower(base) + "/"
return fmt.Sprintf(gendocFrontmatterTemplate, strings.Replace(base, "_", " ", -1), base, url)
}
linkHandler := func(name string) string {
base := strings.TrimSuffix(name, path.Ext(name))
return "/commands/" + strings.ToLower(base) + "/"
}
r.Println("Generating Hugo command-line documentation in", gendocdir, "...")
doc.GenMarkdownTreeCustom(cd.CobraCommand.Root(), gendocdir, prepender, linkHandler)
r.Println("Done.")
return nil
},
withc: func(cmd *cobra.Command) {
cmd.PersistentFlags().StringVar(&gendocdir, "dir", "/tmp/hugodoc/", "the directory to write the doc.")
// For bash-completion
cmd.PersistentFlags().SetAnnotation("dir", cobra.BashCompSubdirsInDir, []string{})
},
}
}
return &genCommand{
commands: []simplecobra.Commander{
newChromaStyles(),
newGen(),
newMan(),
},
}
type genCmd struct {
*baseCmd
}
func newGenCmd() *genCmd {
cc := &genCmd{}
cc.baseCmd = newBaseCmd(&cobra.Command{
Use: "gen",
Short: "A collection of several useful generators.",
})
type genCommand struct {
rootCmd *rootCommand
cc.cmd.AddCommand(
newGenDocCmd().getCommand(),
newGenManCmd().getCommand(),
createGenDocsHelper().getCommand(),
createGenChromaStyles().getCommand())
return cc
commands []simplecobra.Commander
}
func (c *genCommand) Commands() []simplecobra.Commander {
return c.commands
}
func (c *genCommand) Name() string {
return "gen"
}
func (c *genCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error {
return nil
}
func (c *genCommand) WithCobraCommand(cmd *cobra.Command) error {
cmd.Short = "A collection of several useful generators."
return nil
}
func (c *genCommand) Init(cd, runner *simplecobra.Commandeer) error {
c.rootCmd = cd.Root.Command.(*rootCommand)
return nil
}

View File

@@ -1,72 +0,0 @@
// Copyright 2017-present The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package commands
import (
"os"
"github.com/alecthomas/chroma/v2"
"github.com/alecthomas/chroma/v2/formatters/html"
"github.com/alecthomas/chroma/v2/styles"
"github.com/spf13/cobra"
)
var _ cmder = (*genChromaStyles)(nil)
type genChromaStyles struct {
style string
highlightStyle string
linesStyle string
*baseCmd
}
// TODO(bep) highlight
func createGenChromaStyles() *genChromaStyles {
g := &genChromaStyles{
baseCmd: newBaseCmd(&cobra.Command{
Use: "chromastyles",
Short: "Generate CSS stylesheet for the Chroma code highlighter",
Long: `Generate CSS stylesheet for the Chroma code highlighter for a given style. This stylesheet is needed if markup.highlight.noClasses is disabled in config.
See https://xyproto.github.io/splash/docs/all.html for a preview of the available styles`,
}),
}
g.cmd.RunE = func(cmd *cobra.Command, args []string) error {
return g.generate()
}
g.cmd.PersistentFlags().StringVar(&g.style, "style", "friendly", "highlighter style (see https://xyproto.github.io/splash/docs/)")
g.cmd.PersistentFlags().StringVar(&g.highlightStyle, "highlightStyle", "bg:#ffffcc", "style used for highlighting lines (see https://github.com/alecthomas/chroma)")
g.cmd.PersistentFlags().StringVar(&g.linesStyle, "linesStyle", "", "style used for line numbers (see https://github.com/alecthomas/chroma)")
return g
}
func (g *genChromaStyles) generate() error {
builder := styles.Get(g.style).Builder()
if g.highlightStyle != "" {
builder.Add(chroma.LineHighlight, g.highlightStyle)
}
if g.linesStyle != "" {
builder.Add(chroma.LineNumbers, g.linesStyle)
}
style, err := builder.Build()
if err != nil {
return err
}
formatter := html.New(html.WithAllClasses(true))
formatter.WriteCSS(os.Stdout, style)
return nil
}

View File

@@ -1,98 +0,0 @@
// Copyright 2016 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package commands
import (
"fmt"
"path"
"path/filepath"
"strings"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugofs"
"github.com/spf13/cobra"
"github.com/spf13/cobra/doc"
jww "github.com/spf13/jwalterweatherman"
)
var _ cmder = (*genDocCmd)(nil)
type genDocCmd struct {
gendocdir string
*baseCmd
}
func newGenDocCmd() *genDocCmd {
const gendocFrontmatterTemplate = `---
title: "%s"
slug: %s
url: %s
---
`
cc := &genDocCmd{}
cc.baseCmd = newBaseCmd(&cobra.Command{
Use: "doc",
Short: "Generate Markdown documentation for the Hugo CLI.",
Long: `Generate Markdown documentation for the Hugo CLI.
This command is, mostly, used to create up-to-date documentation
of Hugo's command-line interface for https://gohugo.io/.
It creates one Markdown file per command with front matter suitable
for rendering in Hugo.`,
RunE: func(cmd *cobra.Command, args []string) error {
cmd.VisitParents(func(c *cobra.Command) {
// Disable the "Auto generated by spf13/cobra on DATE"
// as it creates a lot of diffs.
c.DisableAutoGenTag = true
})
if !strings.HasSuffix(cc.gendocdir, helpers.FilePathSeparator) {
cc.gendocdir += helpers.FilePathSeparator
}
if found, _ := helpers.Exists(cc.gendocdir, hugofs.Os); !found {
jww.FEEDBACK.Println("Directory", cc.gendocdir, "does not exist, creating...")
if err := hugofs.Os.MkdirAll(cc.gendocdir, 0777); err != nil {
return err
}
}
prepender := func(filename string) string {
name := filepath.Base(filename)
base := strings.TrimSuffix(name, path.Ext(name))
url := "/commands/" + strings.ToLower(base) + "/"
return fmt.Sprintf(gendocFrontmatterTemplate, strings.Replace(base, "_", " ", -1), base, url)
}
linkHandler := func(name string) string {
base := strings.TrimSuffix(name, path.Ext(name))
return "/commands/" + strings.ToLower(base) + "/"
}
jww.FEEDBACK.Println("Generating Hugo command-line documentation in", cc.gendocdir, "...")
doc.GenMarkdownTreeCustom(cmd.Root(), cc.gendocdir, prepender, linkHandler)
jww.FEEDBACK.Println("Done.")
return nil
},
})
cc.cmd.PersistentFlags().StringVar(&cc.gendocdir, "dir", "/tmp/hugodoc/", "the directory to write the doc.")
// For bash-completion
cc.cmd.PersistentFlags().SetAnnotation("dir", cobra.BashCompSubdirsInDir, []string{})
return cc
}

View File

@@ -1,71 +0,0 @@
// Copyright 2017-present The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package commands
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"github.com/gohugoio/hugo/docshelper"
"github.com/spf13/cobra"
)
var _ cmder = (*genDocsHelper)(nil)
type genDocsHelper struct {
target string
*baseCmd
}
func createGenDocsHelper() *genDocsHelper {
g := &genDocsHelper{
baseCmd: newBaseCmd(&cobra.Command{
Use: "docshelper",
Short: "Generate some data files for the Hugo docs.",
Hidden: true,
}),
}
g.cmd.RunE = func(cmd *cobra.Command, args []string) error {
return g.generate()
}
g.cmd.PersistentFlags().StringVarP(&g.target, "dir", "", "docs/data", "data dir")
return g
}
func (g *genDocsHelper) generate() error {
fmt.Println("Generate docs data to", g.target)
targetFile := filepath.Join(g.target, "docs.json")
f, err := os.Create(targetFile)
if err != nil {
return err
}
defer f.Close()
enc := json.NewEncoder(f)
enc.SetIndent("", " ")
if err := enc.Encode(docshelper.GetDocProvider()); err != nil {
return err
}
fmt.Println("Done!")
return nil
}

View File

@@ -1,77 +0,0 @@
// Copyright 2016 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package commands
import (
"fmt"
"strings"
"github.com/gohugoio/hugo/common/hugo"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugofs"
"github.com/spf13/cobra"
"github.com/spf13/cobra/doc"
jww "github.com/spf13/jwalterweatherman"
)
var _ cmder = (*genManCmd)(nil)
type genManCmd struct {
genmandir string
*baseCmd
}
func newGenManCmd() *genManCmd {
cc := &genManCmd{}
cc.baseCmd = newBaseCmd(&cobra.Command{
Use: "man",
Short: "Generate man pages for the Hugo CLI",
Long: `This command automatically generates up-to-date man pages of Hugo's
command-line interface. By default, it creates the man page files
in the "man" directory under the current directory.`,
RunE: func(cmd *cobra.Command, args []string) error {
header := &doc.GenManHeader{
Section: "1",
Manual: "Hugo Manual",
Source: fmt.Sprintf("Hugo %s", hugo.CurrentVersion),
}
if !strings.HasSuffix(cc.genmandir, helpers.FilePathSeparator) {
cc.genmandir += helpers.FilePathSeparator
}
if found, _ := helpers.Exists(cc.genmandir, hugofs.Os); !found {
jww.FEEDBACK.Println("Directory", cc.genmandir, "does not exist, creating...")
if err := hugofs.Os.MkdirAll(cc.genmandir, 0777); err != nil {
return err
}
}
cmd.Root().DisableAutoGenTag = true
jww.FEEDBACK.Println("Generating Hugo man pages in", cc.genmandir, "...")
doc.GenManTree(cmd.Root(), header, cc.genmandir)
jww.FEEDBACK.Println("Done.")
return nil
},
})
cc.cmd.PersistentFlags().StringVar(&cc.genmandir, "dir", "man/", "the directory to write the man pages.")
// For bash-completion
cc.cmd.PersistentFlags().SetAnnotation("dir", cobra.BashCompSubdirsInDir, []string{})
return cc
}

View File

@@ -1,4 +1,4 @@
// Copyright 2018 The Hugo Authors. All rights reserved.
// Copyright 2023 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -11,16 +11,22 @@
// See the License for the specific language governing permissions and
// limitations under the License.
// Package commands defines and implements command-line commands and flags
// used by Hugo. Commands and flags are implemented using Cobra.
package commands
import (
"bytes"
"errors"
"fmt"
"regexp"
"log"
"os"
"path/filepath"
"strings"
"github.com/bep/simplecobra"
"github.com/gohugoio/hugo/config"
"github.com/spf13/cobra"
"github.com/gohugoio/hugo/helpers"
"github.com/spf13/afero"
"github.com/spf13/pflag"
)
const (
@@ -30,50 +36,101 @@ const (
showCursor = ansiEsc + "[?25h"
)
type flagsToConfigHandler interface {
flagsToConfig(cfg config.Provider)
func newUserError(a ...any) *simplecobra.CommandError {
return &simplecobra.CommandError{Err: errors.New(fmt.Sprint(a...))}
}
type cmder interface {
flagsToConfigHandler
getCommand() *cobra.Command
func setValueFromFlag(flags *pflag.FlagSet, key string, cfg config.Provider, targetKey string, force bool) {
key = strings.TrimSpace(key)
if (force && flags.Lookup(key) != nil) || flags.Changed(key) {
f := flags.Lookup(key)
configKey := key
if targetKey != "" {
configKey = targetKey
}
// Gotta love this API.
switch f.Value.Type() {
case "bool":
bv, _ := flags.GetBool(key)
cfg.Set(configKey, bv)
case "string":
cfg.Set(configKey, f.Value.String())
case "stringSlice":
bv, _ := flags.GetStringSlice(key)
cfg.Set(configKey, bv)
case "int":
iv, _ := flags.GetInt(key)
cfg.Set(configKey, iv)
default:
panic(fmt.Sprintf("update switch with %s", f.Value.Type()))
}
}
}
// commandError is an error used to signal different error situations in command handling.
type commandError struct {
s string
userError bool
func flagsToCfg(cd *simplecobra.Commandeer, cfg config.Provider) config.Provider {
return flagsToCfgWithAdditionalConfigBase(cd, cfg, "")
}
func (c commandError) Error() string {
return c.s
}
func (c commandError) isUserError() bool {
return c.userError
}
func newUserError(a ...any) commandError {
return commandError{s: fmt.Sprintln(a...), userError: true}
}
func newSystemError(a ...any) commandError {
return commandError{s: fmt.Sprintln(a...), userError: false}
}
func newSystemErrorF(format string, a ...any) commandError {
return commandError{s: fmt.Sprintf(format, a...), userError: false}
}
// Catch some of the obvious user errors from Cobra.
// We don't want to show the usage message for every error.
// The below may be to generic. Time will show.
var userErrorRegexp = regexp.MustCompile("unknown flag")
func isUserError(err error) bool {
if cErr, ok := err.(commandError); ok && cErr.isUserError() {
return true
func flagsToCfgWithAdditionalConfigBase(cd *simplecobra.Commandeer, cfg config.Provider, additionalConfigBase string) config.Provider {
if cfg == nil {
cfg = config.New()
}
return userErrorRegexp.MatchString(err.Error())
// Flags with a different name in the config.
keyMap := map[string]string{
"minify": "minifyOutput",
"destination": "publishDir",
"printI18nWarnings": "logI18nWarnings",
"printPathWarnings": "logPathWarnings",
"editor": "newContentEditor",
}
// Flags that we for some reason don't want to expose in the site config.
internalKeySet := map[string]bool{
"quiet": true,
"verbose": true,
"watch": true,
"disableLiveReload": true,
"liveReloadPort": true,
"renderToMemory": true,
"clock": true,
}
cmd := cd.CobraCommand
flags := cmd.Flags()
flags.VisitAll(func(f *pflag.Flag) {
if f.Changed {
targetKey := f.Name
if internalKeySet[targetKey] {
targetKey = "internal." + targetKey
} else if mapped, ok := keyMap[targetKey]; ok {
targetKey = mapped
}
setValueFromFlag(flags, f.Name, cfg, targetKey, false)
if additionalConfigBase != "" {
setValueFromFlag(flags, f.Name, cfg, additionalConfigBase+"."+targetKey, true)
}
}
})
return cfg
}
func mkdir(x ...string) {
p := filepath.Join(x...)
err := os.MkdirAll(p, 0777) // before umask
if err != nil {
log.Fatal(err)
}
}
func touchFile(fs afero.Fs, filename string) {
mkdir(filepath.Dir(filename))
err := helpers.WriteToDisk(filename, bytes.NewReader([]byte{}), fs)
if err != nil {
log.Fatal(err)
}
}

View File

@@ -1,206 +0,0 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package commands
import (
"bytes"
"fmt"
"math/rand"
"path/filepath"
"strings"
"testing"
"github.com/bep/clock"
qt "github.com/frankban/quicktest"
"github.com/gohugoio/hugo/common/htime"
"github.com/gohugoio/hugo/hugofs"
"github.com/spf13/afero"
"golang.org/x/tools/txtar"
)
// Issue #5662
func TestHugoWithContentDirOverride(t *testing.T) {
t.Parallel()
c := qt.New(t)
files := `
-- config.toml --
baseURL = "https://example.org"
title = "Hugo Commands"
-- mycontent/p1.md --
---
title: "P1"
---
-- layouts/_default/single.html --
Page: {{ .Title }}|
`
s := newTestHugoCmdBuilder(c, files, []string{"-c", "mycontent"}).Build()
s.AssertFileContent("public/p1/index.html", `Page: P1|`)
}
// Issue #9794
func TestHugoStaticFilesMultipleStaticAndManyFolders(t *testing.T) {
t.Parallel()
c := qt.New(t)
files := `
-- config.toml --
baseURL = "https://example.org"
theme = "mytheme"
-- layouts/index.html --
Home.
`
const (
numDirs = 33
numFilesMax = 12
)
r := rand.New(rand.NewSource(32))
for i := 0; i < numDirs; i++ {
for j := 0; j < r.Intn(numFilesMax); j++ {
if j%3 == 0 {
files += fmt.Sprintf("-- themes/mytheme/static/d%d/f%d.txt --\nHellot%d-%d\n", i, j, i, j)
files += fmt.Sprintf("-- themes/mytheme/static/d%d/ft%d.txt --\nHellot%d-%d\n", i, j, i, j)
}
files += fmt.Sprintf("-- static/d%d/f%d.txt --\nHello%d-%d\n", i, j, i, j)
}
}
r = rand.New(rand.NewSource(32))
s := newTestHugoCmdBuilder(c, files, []string{"-c", "mycontent"}).Build()
for i := 0; i < numDirs; i++ {
for j := 0; j < r.Intn(numFilesMax); j++ {
if j%3 == 0 {
if j%3 == 0 {
s.AssertFileContent(fmt.Sprintf("public/d%d/ft%d.txt", i, j), fmt.Sprintf("Hellot%d-%d", i, j))
}
s.AssertFileContent(fmt.Sprintf("public/d%d/f%d.txt", i, j), fmt.Sprintf("Hello%d-%d", i, j))
}
}
}
}
// Issue #8787
func TestHugoListCommandsWithClockFlag(t *testing.T) {
t.Cleanup(func() { htime.Clock = clock.System() })
c := qt.New(t)
files := `
-- config.toml --
baseURL = "https://example.org"
title = "Hugo Commands"
timeZone = "UTC"
-- content/past.md --
---
title: "Past"
date: 2000-11-06
---
-- content/future.md --
---
title: "Future"
date: 2200-11-06
---
-- layouts/_default/single.html --
Page: {{ .Title }}|
`
s := newTestHugoCmdBuilder(c, files, []string{"list", "future"})
s.captureOut = true
s.Build()
p := filepath.Join("content", "future.md")
s.AssertStdout(p + ",2200-11-06T00:00:00Z")
s = newTestHugoCmdBuilder(c, files, []string{"list", "future", "--clock", "2300-11-06"}).Build()
s.AssertStdout("")
}
type testHugoCmdBuilder struct {
*qt.C
fs afero.Fs
dir string
files string
args []string
captureOut bool
out string
}
func newTestHugoCmdBuilder(c *qt.C, files string, args []string) *testHugoCmdBuilder {
s := &testHugoCmdBuilder{C: c, files: files, args: args}
s.dir = s.TempDir()
s.fs = afero.NewBasePathFs(hugofs.Os, s.dir)
return s
}
func (s *testHugoCmdBuilder) Build() *testHugoCmdBuilder {
data := txtar.Parse([]byte(s.files))
for _, f := range data.Files {
filename := filepath.Clean(f.Name)
data := bytes.TrimSuffix(f.Data, []byte("\n"))
s.Assert(s.fs.MkdirAll(filepath.Dir(filename), 0777), qt.IsNil)
s.Assert(afero.WriteFile(s.fs, filename, data, 0666), qt.IsNil)
}
hugoCmd := newCommandsBuilder().addAll().build()
cmd := hugoCmd.getCommand()
args := append(s.args, "-s="+s.dir, "--quiet")
cmd.SetArgs(args)
if s.captureOut {
out, err := captureStdout(func() error {
_, err := cmd.ExecuteC()
return err
})
s.Assert(err, qt.IsNil)
s.out = out
} else {
_, err := cmd.ExecuteC()
s.Assert(err, qt.IsNil)
}
return s
}
func (s *testHugoCmdBuilder) AssertFileContent(filename string, matches ...string) {
s.Helper()
data, err := afero.ReadFile(s.fs, filename)
s.Assert(err, qt.IsNil)
content := strings.TrimSpace(string(data))
for _, m := range matches {
lines := strings.Split(m, "\n")
for _, match := range lines {
match = strings.TrimSpace(match)
if match == "" || strings.HasPrefix(match, "#") {
continue
}
s.Assert(content, qt.Contains, match, qt.Commentf(m))
}
}
}
func (s *testHugoCmdBuilder) AssertStdout(match string) {
s.Helper()
content := strings.TrimSpace(s.out)
s.Assert(content, qt.Contains, strings.TrimSpace(match))
}

View File

@@ -1,4 +1,4 @@
// Copyright 2015 The Hugo Authors. All rights reserved.
// Copyright 2023 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
// Copyright 2023 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -15,252 +15,96 @@ package commands
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"regexp"
jww "github.com/spf13/jwalterweatherman"
"strconv"
"strings"
"time"
"unicode"
"github.com/gohugoio/hugo/parser/pageparser"
"github.com/bep/simplecobra"
"github.com/gohugoio/hugo/common/htime"
"github.com/gohugoio/hugo/common/hugio"
"github.com/gohugoio/hugo/parser/metadecoders"
"github.com/gohugoio/hugo/common/maps"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/parser"
"github.com/gohugoio/hugo/parser/metadecoders"
"github.com/gohugoio/hugo/parser/pageparser"
"github.com/spf13/afero"
"github.com/spf13/cobra"
jww "github.com/spf13/jwalterweatherman"
)
var _ cmder = (*importCmd)(nil)
type importCmd struct {
*baseCmd
}
func newImportCmd() *importCmd {
cc := &importCmd{}
cc.baseCmd = newBaseCmd(&cobra.Command{
Use: "import",
Short: "Import your site from others.",
Long: `Import your site from other web site generators like Jekyll.
Import requires a subcommand, e.g. ` + "`hugo import jekyll jekyll_root_path target_path`.",
RunE: nil,
})
importJekyllCmd := &cobra.Command{
Use: "jekyll",
Short: "hugo import from Jekyll",
Long: `hugo import from Jekyll.
func newImportCommand() *importCommand {
var c *importCommand
c = &importCommand{
commands: []simplecobra.Commander{
&simpleCommand{
name: "jekyll",
short: "hugo import from Jekyll",
long: `hugo import from Jekyll.
Import from Jekyll requires two paths, e.g. ` + "`hugo import jekyll jekyll_root_path target_path`.",
RunE: cc.importFromJekyll,
}
importJekyllCmd.Flags().Bool("force", false, "allow import into non-empty target directory")
cc.cmd.AddCommand(importJekyllCmd)
return cc
}
func (i *importCmd) importFromJekyll(cmd *cobra.Command, args []string) error {
if len(args) < 2 {
return newUserError(`import from jekyll requires two paths, e.g. ` + "`hugo import jekyll jekyll_root_path target_path`.")
}
jekyllRoot, err := filepath.Abs(filepath.Clean(args[0]))
if err != nil {
return newUserError("path error:", args[0])
}
targetDir, err := filepath.Abs(filepath.Clean(args[1]))
if err != nil {
return newUserError("path error:", args[1])
}
jww.INFO.Println("Import Jekyll from:", jekyllRoot, "to:", targetDir)
if strings.HasPrefix(filepath.Dir(targetDir), jekyllRoot) {
return newUserError("abort: target path should not be inside the Jekyll root")
}
forceImport, _ := cmd.Flags().GetBool("force")
fs := afero.NewOsFs()
jekyllPostDirs, hasAnyPost := i.getJekyllDirInfo(fs, jekyllRoot)
if !hasAnyPost {
return errors.New("abort: jekyll root contains neither posts nor drafts")
}
err = i.createSiteFromJekyll(jekyllRoot, targetDir, jekyllPostDirs, forceImport)
if err != nil {
return newUserError(err)
}
jww.FEEDBACK.Println("Importing...")
fileCount := 0
callback := func(path string, fi hugofs.FileMetaInfo, err error) error {
if err != nil {
return err
}
if fi.IsDir() {
return nil
}
relPath, err := filepath.Rel(jekyllRoot, path)
if err != nil {
return newUserError("get rel path error:", path)
}
relPath = filepath.ToSlash(relPath)
draft := false
switch {
case strings.Contains(relPath, "_posts/"):
relPath = filepath.Join("content/post", strings.Replace(relPath, "_posts/", "", -1))
case strings.Contains(relPath, "_drafts/"):
relPath = filepath.Join("content/draft", strings.Replace(relPath, "_drafts/", "", -1))
draft = true
default:
return nil
}
fileCount++
return convertJekyllPost(path, relPath, targetDir, draft)
}
for jekyllPostDir, hasAnyPostInDir := range jekyllPostDirs {
if hasAnyPostInDir {
if err = helpers.SymbolicWalk(hugofs.Os, filepath.Join(jekyllRoot, jekyllPostDir), callback); err != nil {
return err
}
}
}
jww.FEEDBACK.Println("Congratulations!", fileCount, "post(s) imported!")
jww.FEEDBACK.Println("Now, start Hugo by yourself:\n" +
"$ git clone https://github.com/spf13/herring-cove.git " + args[1] + "/themes/herring-cove")
jww.FEEDBACK.Println("$ cd " + args[1] + "\n$ hugo server --theme=herring-cove")
return nil
}
func (i *importCmd) getJekyllDirInfo(fs afero.Fs, jekyllRoot string) (map[string]bool, bool) {
postDirs := make(map[string]bool)
hasAnyPost := false
if entries, err := os.ReadDir(jekyllRoot); err == nil {
for _, entry := range entries {
if entry.IsDir() {
subDir := filepath.Join(jekyllRoot, entry.Name())
if isPostDir, hasAnyPostInDir := i.retrieveJekyllPostDir(fs, subDir); isPostDir {
postDirs[entry.Name()] = hasAnyPostInDir
if hasAnyPostInDir {
hasAnyPost = true
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
if len(args) < 2 {
return newUserError(`import from jekyll requires two paths, e.g. ` + "`hugo import jekyll jekyll_root_path target_path`.")
}
}
}
}
}
return postDirs, hasAnyPost
}
func (i *importCmd) retrieveJekyllPostDir(fs afero.Fs, dir string) (bool, bool) {
if strings.HasSuffix(dir, "_posts") || strings.HasSuffix(dir, "_drafts") {
isEmpty, _ := helpers.IsEmpty(dir, fs)
return true, !isEmpty
}
if entries, err := os.ReadDir(dir); err == nil {
for _, entry := range entries {
if entry.IsDir() {
subDir := filepath.Join(dir, entry.Name())
if isPostDir, hasAnyPost := i.retrieveJekyllPostDir(fs, subDir); isPostDir {
return isPostDir, hasAnyPost
}
}
}
}
return false, true
}
func (i *importCmd) createSiteFromJekyll(jekyllRoot, targetDir string, jekyllPostDirs map[string]bool, force bool) error {
fs := &afero.OsFs{}
if exists, _ := helpers.Exists(targetDir, fs); exists {
if isDir, _ := helpers.IsDir(targetDir, fs); !isDir {
return errors.New("target path \"" + targetDir + "\" exists but is not a directory")
}
isEmpty, _ := helpers.IsEmpty(targetDir, fs)
if !isEmpty && !force {
return errors.New("target path \"" + targetDir + "\" exists and is not empty")
}
}
jekyllConfig := i.loadJekyllConfig(fs, jekyllRoot)
mkdir(targetDir, "layouts")
mkdir(targetDir, "content")
mkdir(targetDir, "archetypes")
mkdir(targetDir, "static")
mkdir(targetDir, "data")
mkdir(targetDir, "themes")
i.createConfigFromJekyll(fs, targetDir, "yaml", jekyllConfig)
i.copyJekyllFilesAndFolders(jekyllRoot, filepath.Join(targetDir, "static"), jekyllPostDirs)
return nil
}
func (i *importCmd) loadJekyllConfig(fs afero.Fs, jekyllRoot string) map[string]any {
path := filepath.Join(jekyllRoot, "_config.yml")
exists, err := helpers.Exists(path, fs)
if err != nil || !exists {
jww.WARN.Println("_config.yaml not found: Is the specified Jekyll root correct?")
return nil
}
f, err := fs.Open(path)
if err != nil {
return nil
}
defer f.Close()
b, err := io.ReadAll(f)
if err != nil {
return nil
}
c, err := metadecoders.Default.UnmarshalToMap(b, metadecoders.YAML)
if err != nil {
return nil
return c.importFromJekyll(args)
},
withc: func(cmd *cobra.Command) {
cmd.Flags().BoolVar(&c.force, "force", false, "allow import into non-empty target directory")
},
},
},
}
return c
}
func (i *importCmd) createConfigFromJekyll(fs afero.Fs, inpath string, kind metadecoders.Format, jekyllConfig map[string]any) (err error) {
type importCommand struct {
r *rootCommand
force bool
commands []simplecobra.Commander
}
func (c *importCommand) Commands() []simplecobra.Commander {
return c.commands
}
func (c *importCommand) Name() string {
return "import"
}
func (c *importCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error {
return nil
}
func (c *importCommand) WithCobraCommand(cmd *cobra.Command) error {
cmd.Short = "Import your site from others."
cmd.Long = `Import your site from other web site generators like Jekyll.
Import requires a subcommand, e.g. ` + "`hugo import jekyll jekyll_root_path target_path`."
return nil
}
func (c *importCommand) Init(cd, runner *simplecobra.Commandeer) error {
c.r = cd.Root.Command.(*rootCommand)
return nil
}
func (i *importCommand) createConfigFromJekyll(fs afero.Fs, inpath string, kind metadecoders.Format, jekyllConfig map[string]any) (err error) {
title := "My New Hugo Site"
baseURL := "http://example.org/"
@@ -293,10 +137,209 @@ func (i *importCmd) createConfigFromJekyll(fs afero.Fs, inpath string, kind meta
return err
}
return helpers.WriteToDisk(filepath.Join(inpath, "config."+string(kind)), &buf, fs)
return helpers.WriteToDisk(filepath.Join(inpath, "hugo."+string(kind)), &buf, fs)
}
func (i *importCmd) copyJekyllFilesAndFolders(jekyllRoot, dest string, jekyllPostDirs map[string]bool) (err error) {
func (c *importCommand) getJekyllDirInfo(fs afero.Fs, jekyllRoot string) (map[string]bool, bool) {
postDirs := make(map[string]bool)
hasAnyPost := false
if entries, err := os.ReadDir(jekyllRoot); err == nil {
for _, entry := range entries {
if entry.IsDir() {
subDir := filepath.Join(jekyllRoot, entry.Name())
if isPostDir, hasAnyPostInDir := c.retrieveJekyllPostDir(fs, subDir); isPostDir {
postDirs[entry.Name()] = hasAnyPostInDir
if hasAnyPostInDir {
hasAnyPost = true
}
}
}
}
}
return postDirs, hasAnyPost
}
func (c *importCommand) createSiteFromJekyll(jekyllRoot, targetDir string, jekyllPostDirs map[string]bool) error {
fs := &afero.OsFs{}
if exists, _ := helpers.Exists(targetDir, fs); exists {
if isDir, _ := helpers.IsDir(targetDir, fs); !isDir {
return errors.New("target path \"" + targetDir + "\" exists but is not a directory")
}
isEmpty, _ := helpers.IsEmpty(targetDir, fs)
if !isEmpty && !c.force {
return errors.New("target path \"" + targetDir + "\" exists and is not empty")
}
}
jekyllConfig := c.loadJekyllConfig(fs, jekyllRoot)
mkdir(targetDir, "layouts")
mkdir(targetDir, "content")
mkdir(targetDir, "archetypes")
mkdir(targetDir, "static")
mkdir(targetDir, "data")
mkdir(targetDir, "themes")
c.createConfigFromJekyll(fs, targetDir, "yaml", jekyllConfig)
c.copyJekyllFilesAndFolders(jekyllRoot, filepath.Join(targetDir, "static"), jekyllPostDirs)
return nil
}
func (c *importCommand) convertJekyllContent(m any, content string) (string, error) {
metadata, _ := maps.ToStringMapE(m)
lines := strings.Split(content, "\n")
var resultLines []string
for _, line := range lines {
resultLines = append(resultLines, strings.Trim(line, "\r\n"))
}
content = strings.Join(resultLines, "\n")
excerptSep := "<!--more-->"
if value, ok := metadata["excerpt_separator"]; ok {
if str, strOk := value.(string); strOk {
content = strings.Replace(content, strings.TrimSpace(str), excerptSep, -1)
}
}
replaceList := []struct {
re *regexp.Regexp
replace string
}{
{regexp.MustCompile("(?i)<!-- more -->"), "<!--more-->"},
{regexp.MustCompile(`\{%\s*raw\s*%\}\s*(.*?)\s*\{%\s*endraw\s*%\}`), "$1"},
{regexp.MustCompile(`{%\s*endhighlight\s*%}`), "{{< / highlight >}}"},
}
for _, replace := range replaceList {
content = replace.re.ReplaceAllString(content, replace.replace)
}
replaceListFunc := []struct {
re *regexp.Regexp
replace func(string) string
}{
// Octopress image tag: http://octopress.org/docs/plugins/image-tag/
{regexp.MustCompile(`{%\s+img\s*(.*?)\s*%}`), c.replaceImageTag},
{regexp.MustCompile(`{%\s*highlight\s*(.*?)\s*%}`), c.replaceHighlightTag},
}
for _, replace := range replaceListFunc {
content = replace.re.ReplaceAllStringFunc(content, replace.replace)
}
var buf bytes.Buffer
if len(metadata) != 0 {
err := parser.InterfaceToFrontMatter(m, metadecoders.YAML, &buf)
if err != nil {
return "", err
}
}
buf.WriteString(content)
return buf.String(), nil
}
func (c *importCommand) convertJekyllMetaData(m any, postName string, postDate time.Time, draft bool) (any, error) {
metadata, err := maps.ToStringMapE(m)
if err != nil {
return nil, err
}
if draft {
metadata["draft"] = true
}
for key, value := range metadata {
lowerKey := strings.ToLower(key)
switch lowerKey {
case "layout":
delete(metadata, key)
case "permalink":
if str, ok := value.(string); ok {
metadata["url"] = str
}
delete(metadata, key)
case "category":
if str, ok := value.(string); ok {
metadata["categories"] = []string{str}
}
delete(metadata, key)
case "excerpt_separator":
if key != lowerKey {
delete(metadata, key)
metadata[lowerKey] = value
}
case "date":
if str, ok := value.(string); ok {
re := regexp.MustCompile(`(\d+):(\d+):(\d+)`)
r := re.FindAllStringSubmatch(str, -1)
if len(r) > 0 {
hour, _ := strconv.Atoi(r[0][1])
minute, _ := strconv.Atoi(r[0][2])
second, _ := strconv.Atoi(r[0][3])
postDate = time.Date(postDate.Year(), postDate.Month(), postDate.Day(), hour, minute, second, 0, time.UTC)
}
}
delete(metadata, key)
}
}
metadata["date"] = postDate.Format(time.RFC3339)
return metadata, nil
}
func (c *importCommand) convertJekyllPost(path, relPath, targetDir string, draft bool) error {
jww.TRACE.Println("Converting", path)
filename := filepath.Base(path)
postDate, postName, err := c.parseJekyllFilename(filename)
if err != nil {
c.r.Printf("Failed to parse filename '%s': %s. Skipping.", filename, err)
return nil
}
jww.TRACE.Println(filename, postDate, postName)
targetFile := filepath.Join(targetDir, relPath)
targetParentDir := filepath.Dir(targetFile)
os.MkdirAll(targetParentDir, 0777)
contentBytes, err := os.ReadFile(path)
if err != nil {
c.r.logger.Errorln("Read file error:", path)
return err
}
pf, err := pageparser.ParseFrontMatterAndContent(bytes.NewReader(contentBytes))
if err != nil {
return fmt.Errorf("failed to parse file %q: %s", filename, err)
}
newmetadata, err := c.convertJekyllMetaData(pf.FrontMatter, postName, postDate, draft)
if err != nil {
return fmt.Errorf("failed to convert metadata for file %q: %s", filename, err)
}
content, err := c.convertJekyllContent(newmetadata, string(pf.Content))
if err != nil {
return fmt.Errorf("failed to convert content for file %q: %s", filename, err)
}
fs := hugofs.Os
if err := helpers.WriteToDisk(targetFile, strings.NewReader(content), fs); err != nil {
return fmt.Errorf("failed to save file %q: %s", filename, err)
}
return nil
}
func (c *importCommand) copyJekyllFilesAndFolders(jekyllRoot, dest string, jekyllPostDirs map[string]bool) (err error) {
fs := hugofs.Os
fi, err := fs.Stat(jekyllRoot)
@@ -353,7 +396,116 @@ func (i *importCmd) copyJekyllFilesAndFolders(jekyllRoot, dest string, jekyllPos
return nil
}
func parseJekyllFilename(filename string) (time.Time, string, error) {
func (c *importCommand) importFromJekyll(args []string) error {
jekyllRoot, err := filepath.Abs(filepath.Clean(args[0]))
if err != nil {
return newUserError("path error:", args[0])
}
targetDir, err := filepath.Abs(filepath.Clean(args[1]))
if err != nil {
return newUserError("path error:", args[1])
}
c.r.Println("Import Jekyll from:", jekyllRoot, "to:", targetDir)
if strings.HasPrefix(filepath.Dir(targetDir), jekyllRoot) {
return newUserError("abort: target path should not be inside the Jekyll root")
}
fs := afero.NewOsFs()
jekyllPostDirs, hasAnyPost := c.getJekyllDirInfo(fs, jekyllRoot)
if !hasAnyPost {
return errors.New("abort: jekyll root contains neither posts nor drafts")
}
err = c.createSiteFromJekyll(jekyllRoot, targetDir, jekyllPostDirs)
if err != nil {
return newUserError(err)
}
c.r.Println("Importing...")
fileCount := 0
callback := func(path string, fi hugofs.FileMetaInfo, err error) error {
if err != nil {
return err
}
if fi.IsDir() {
return nil
}
relPath, err := filepath.Rel(jekyllRoot, path)
if err != nil {
return newUserError("get rel path error:", path)
}
relPath = filepath.ToSlash(relPath)
draft := false
switch {
case strings.Contains(relPath, "_posts/"):
relPath = filepath.Join("content/post", strings.Replace(relPath, "_posts/", "", -1))
case strings.Contains(relPath, "_drafts/"):
relPath = filepath.Join("content/draft", strings.Replace(relPath, "_drafts/", "", -1))
draft = true
default:
return nil
}
fileCount++
return c.convertJekyllPost(path, relPath, targetDir, draft)
}
for jekyllPostDir, hasAnyPostInDir := range jekyllPostDirs {
if hasAnyPostInDir {
if err = helpers.SymbolicWalk(hugofs.Os, filepath.Join(jekyllRoot, jekyllPostDir), callback); err != nil {
return err
}
}
}
c.r.Println("Congratulations!", fileCount, "post(s) imported!")
c.r.Println("Now, start Hugo by yourself:\n" +
"$ git clone https://github.com/spf13/herring-cove.git " + args[1] + "/themes/herring-cove")
c.r.Println("$ cd " + args[1] + "\n$ hugo server --theme=herring-cove")
return nil
}
func (c *importCommand) loadJekyllConfig(fs afero.Fs, jekyllRoot string) map[string]any {
path := filepath.Join(jekyllRoot, "_config.yml")
exists, err := helpers.Exists(path, fs)
if err != nil || !exists {
c.r.Println("_config.yaml not found: Is the specified Jekyll root correct?")
return nil
}
f, err := fs.Open(path)
if err != nil {
return nil
}
defer f.Close()
b, err := io.ReadAll(f)
if err != nil {
return nil
}
m, err := metadecoders.Default.UnmarshalToMap(b, metadecoders.YAML)
if err != nil {
return nil
}
return m
}
func (c *importCommand) parseJekyllFilename(filename string) (time.Time, string, error) {
re := regexp.MustCompile(`(\d+-\d+-\d+)-(.+)\..*`)
r := re.FindAllStringSubmatch(filename, -1)
if len(r) == 0 {
@@ -370,163 +522,7 @@ func parseJekyllFilename(filename string) (time.Time, string, error) {
return postDate, postName, nil
}
func convertJekyllPost(path, relPath, targetDir string, draft bool) error {
jww.TRACE.Println("Converting", path)
filename := filepath.Base(path)
postDate, postName, err := parseJekyllFilename(filename)
if err != nil {
jww.WARN.Printf("Failed to parse filename '%s': %s. Skipping.", filename, err)
return nil
}
jww.TRACE.Println(filename, postDate, postName)
targetFile := filepath.Join(targetDir, relPath)
targetParentDir := filepath.Dir(targetFile)
os.MkdirAll(targetParentDir, 0777)
contentBytes, err := os.ReadFile(path)
if err != nil {
jww.ERROR.Println("Read file error:", path)
return err
}
pf, err := pageparser.ParseFrontMatterAndContent(bytes.NewReader(contentBytes))
if err != nil {
jww.ERROR.Println("Parse file error:", path)
return err
}
newmetadata, err := convertJekyllMetaData(pf.FrontMatter, postName, postDate, draft)
if err != nil {
jww.ERROR.Println("Convert metadata error:", path)
return err
}
content, err := convertJekyllContent(newmetadata, string(pf.Content))
if err != nil {
jww.ERROR.Println("Converting Jekyll error:", path)
return err
}
fs := hugofs.Os
if err := helpers.WriteToDisk(targetFile, strings.NewReader(content), fs); err != nil {
return fmt.Errorf("failed to save file %q: %s", filename, err)
}
return nil
}
func convertJekyllMetaData(m any, postName string, postDate time.Time, draft bool) (any, error) {
metadata, err := maps.ToStringMapE(m)
if err != nil {
return nil, err
}
if draft {
metadata["draft"] = true
}
for key, value := range metadata {
lowerKey := strings.ToLower(key)
switch lowerKey {
case "layout":
delete(metadata, key)
case "permalink":
if str, ok := value.(string); ok {
metadata["url"] = str
}
delete(metadata, key)
case "category":
if str, ok := value.(string); ok {
metadata["categories"] = []string{str}
}
delete(metadata, key)
case "excerpt_separator":
if key != lowerKey {
delete(metadata, key)
metadata[lowerKey] = value
}
case "date":
if str, ok := value.(string); ok {
re := regexp.MustCompile(`(\d+):(\d+):(\d+)`)
r := re.FindAllStringSubmatch(str, -1)
if len(r) > 0 {
hour, _ := strconv.Atoi(r[0][1])
minute, _ := strconv.Atoi(r[0][2])
second, _ := strconv.Atoi(r[0][3])
postDate = time.Date(postDate.Year(), postDate.Month(), postDate.Day(), hour, minute, second, 0, time.UTC)
}
}
delete(metadata, key)
}
}
metadata["date"] = postDate.Format(time.RFC3339)
return metadata, nil
}
func convertJekyllContent(m any, content string) (string, error) {
metadata, _ := maps.ToStringMapE(m)
lines := strings.Split(content, "\n")
var resultLines []string
for _, line := range lines {
resultLines = append(resultLines, strings.Trim(line, "\r\n"))
}
content = strings.Join(resultLines, "\n")
excerptSep := "<!--more-->"
if value, ok := metadata["excerpt_separator"]; ok {
if str, strOk := value.(string); strOk {
content = strings.Replace(content, strings.TrimSpace(str), excerptSep, -1)
}
}
replaceList := []struct {
re *regexp.Regexp
replace string
}{
{regexp.MustCompile("(?i)<!-- more -->"), "<!--more-->"},
{regexp.MustCompile(`\{%\s*raw\s*%\}\s*(.*?)\s*\{%\s*endraw\s*%\}`), "$1"},
{regexp.MustCompile(`{%\s*endhighlight\s*%}`), "{{< / highlight >}}"},
}
for _, replace := range replaceList {
content = replace.re.ReplaceAllString(content, replace.replace)
}
replaceListFunc := []struct {
re *regexp.Regexp
replace func(string) string
}{
// Octopress image tag: http://octopress.org/docs/plugins/image-tag/
{regexp.MustCompile(`{%\s+img\s*(.*?)\s*%}`), replaceImageTag},
{regexp.MustCompile(`{%\s*highlight\s*(.*?)\s*%}`), replaceHighlightTag},
}
for _, replace := range replaceListFunc {
content = replace.re.ReplaceAllStringFunc(content, replace.replace)
}
var buf bytes.Buffer
if len(metadata) != 0 {
err := parser.InterfaceToFrontMatter(m, metadecoders.YAML, &buf)
if err != nil {
return "", err
}
}
buf.WriteString(content)
return buf.String(), nil
}
func replaceHighlightTag(match string) string {
func (c *importCommand) replaceHighlightTag(match string) string {
r := regexp.MustCompile(`{%\s*highlight\s*(.*?)\s*%}`)
parts := r.FindStringSubmatch(match)
lastQuote := rune(0)
@@ -570,35 +566,55 @@ func replaceHighlightTag(match string) string {
return result.String()
}
func replaceImageTag(match string) string {
func (c *importCommand) replaceImageTag(match string) string {
r := regexp.MustCompile(`{%\s+img\s*(\p{L}*)\s+([\S]*/[\S]+)\s+(\d*)\s*(\d*)\s*(.*?)\s*%}`)
result := bytes.NewBufferString("{{< figure ")
parts := r.FindStringSubmatch(match)
// Index 0 is the entire string, ignore
replaceOptionalPart(result, "class", parts[1])
replaceOptionalPart(result, "src", parts[2])
replaceOptionalPart(result, "width", parts[3])
replaceOptionalPart(result, "height", parts[4])
c.replaceOptionalPart(result, "class", parts[1])
c.replaceOptionalPart(result, "src", parts[2])
c.replaceOptionalPart(result, "width", parts[3])
c.replaceOptionalPart(result, "height", parts[4])
// title + alt
part := parts[5]
if len(part) > 0 {
splits := strings.Split(part, "'")
lenSplits := len(splits)
if lenSplits == 1 {
replaceOptionalPart(result, "title", splits[0])
c.replaceOptionalPart(result, "title", splits[0])
} else if lenSplits == 3 {
replaceOptionalPart(result, "title", splits[1])
c.replaceOptionalPart(result, "title", splits[1])
} else if lenSplits == 5 {
replaceOptionalPart(result, "title", splits[1])
replaceOptionalPart(result, "alt", splits[3])
c.replaceOptionalPart(result, "title", splits[1])
c.replaceOptionalPart(result, "alt", splits[3])
}
}
result.WriteString(">}}")
return result.String()
}
func replaceOptionalPart(buffer *bytes.Buffer, partName string, part string) {
func (c *importCommand) replaceOptionalPart(buffer *bytes.Buffer, partName string, part string) {
if len(part) > 0 {
buffer.WriteString(partName + "=\"" + part + "\" ")
}
}
func (c *importCommand) retrieveJekyllPostDir(fs afero.Fs, dir string) (bool, bool) {
if strings.HasSuffix(dir, "_posts") || strings.HasSuffix(dir, "_drafts") {
isEmpty, _ := helpers.IsEmpty(dir, fs)
return true, !isEmpty
}
if entries, err := os.ReadDir(dir); err == nil {
for _, entry := range entries {
if entry.IsDir() {
subDir := filepath.Join(dir, entry.Name())
if isPostDir, hasAnyPost := c.retrieveJekyllPostDir(fs, subDir); isPostDir {
return isPostDir, hasAnyPost
}
}
}
}
return false, true
}

View File

@@ -1,177 +0,0 @@
// Copyright 2015 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package commands
import (
"encoding/json"
"testing"
"time"
qt "github.com/frankban/quicktest"
)
func TestParseJekyllFilename(t *testing.T) {
c := qt.New(t)
filenameArray := []string{
"2015-01-02-test.md",
"2012-03-15-中文.markup",
}
expectResult := []struct {
postDate time.Time
postName string
}{
{time.Date(2015, time.January, 2, 0, 0, 0, 0, time.UTC), "test"},
{time.Date(2012, time.March, 15, 0, 0, 0, 0, time.UTC), "中文"},
}
for i, filename := range filenameArray {
postDate, postName, err := parseJekyllFilename(filename)
c.Assert(err, qt.IsNil)
c.Assert(expectResult[i].postDate.Format("2006-01-02"), qt.Equals, postDate.Format("2006-01-02"))
c.Assert(expectResult[i].postName, qt.Equals, postName)
}
}
func TestConvertJekyllMetadata(t *testing.T) {
c := qt.New(t)
testDataList := []struct {
metadata any
postName string
postDate time.Time
draft bool
expect string
}{
{
map[any]any{},
"testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), false,
`{"date":"2015-10-01T00:00:00Z"}`,
},
{
map[any]any{},
"testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), true,
`{"date":"2015-10-01T00:00:00Z","draft":true}`,
},
{
map[any]any{"Permalink": "/permalink.html", "layout": "post"},
"testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), false,
`{"date":"2015-10-01T00:00:00Z","url":"/permalink.html"}`,
},
{
map[any]any{"permalink": "/permalink.html"},
"testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), false,
`{"date":"2015-10-01T00:00:00Z","url":"/permalink.html"}`,
},
{
map[any]any{"category": nil, "permalink": 123},
"testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), false,
`{"date":"2015-10-01T00:00:00Z"}`,
},
{
map[any]any{"Excerpt_Separator": "sep"},
"testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), false,
`{"date":"2015-10-01T00:00:00Z","excerpt_separator":"sep"}`,
},
{
map[any]any{"category": "book", "layout": "post", "Others": "Goods", "Date": "2015-10-01 12:13:11"},
"testPost", time.Date(2015, 10, 1, 0, 0, 0, 0, time.UTC), false,
`{"Others":"Goods","categories":["book"],"date":"2015-10-01T12:13:11Z"}`,
},
}
for _, data := range testDataList {
result, err := convertJekyllMetaData(data.metadata, data.postName, data.postDate, data.draft)
c.Assert(err, qt.IsNil)
jsonResult, err := json.Marshal(result)
c.Assert(err, qt.IsNil)
c.Assert(string(jsonResult), qt.Equals, data.expect)
}
}
func TestConvertJekyllContent(t *testing.T) {
c := qt.New(t)
testDataList := []struct {
metadata any
content string
expect string
}{
{
map[any]any{},
"Test content\r\n<!-- more -->\npart2 content", "Test content\n<!--more-->\npart2 content",
},
{
map[any]any{},
"Test content\n<!-- More -->\npart2 content", "Test content\n<!--more-->\npart2 content",
},
{
map[any]any{"excerpt_separator": "<!--sep-->"},
"Test content\n<!--sep-->\npart2 content",
"---\nexcerpt_separator: <!--sep-->\n---\nTest content\n<!--more-->\npart2 content",
},
{map[any]any{}, "{% raw %}text{% endraw %}", "text"},
{map[any]any{}, "{%raw%} text2 {%endraw %}", "text2"},
{
map[any]any{},
"{% highlight go %}\nvar s int\n{% endhighlight %}",
"{{< highlight go >}}\nvar s int\n{{< / highlight >}}",
},
{
map[any]any{},
"{% highlight go linenos hl_lines=\"1 2\" %}\nvar s string\nvar i int\n{% endhighlight %}",
"{{< highlight go \"linenos=table,hl_lines=1 2\" >}}\nvar s string\nvar i int\n{{< / highlight >}}",
},
// Octopress image tag
{
map[any]any{},
"{% img http://placekitten.com/890/280 %}",
"{{< figure src=\"http://placekitten.com/890/280\" >}}",
},
{
map[any]any{},
"{% img left http://placekitten.com/320/250 Place Kitten #2 %}",
"{{< figure class=\"left\" src=\"http://placekitten.com/320/250\" title=\"Place Kitten #2\" >}}",
},
{
map[any]any{},
"{% img right http://placekitten.com/300/500 150 250 'Place Kitten #3' %}",
"{{< figure class=\"right\" src=\"http://placekitten.com/300/500\" width=\"150\" height=\"250\" title=\"Place Kitten #3\" >}}",
},
{
map[any]any{},
"{% img right http://placekitten.com/300/500 150 250 'Place Kitten #4' 'An image of a very cute kitten' %}",
"{{< figure class=\"right\" src=\"http://placekitten.com/300/500\" width=\"150\" height=\"250\" title=\"Place Kitten #4\" alt=\"An image of a very cute kitten\" >}}",
},
{
map[any]any{},
"{% img http://placekitten.com/300/500 150 250 'Place Kitten #4' 'An image of a very cute kitten' %}",
"{{< figure src=\"http://placekitten.com/300/500\" width=\"150\" height=\"250\" title=\"Place Kitten #4\" alt=\"An image of a very cute kitten\" >}}",
},
{
map[any]any{},
"{% img right /placekitten/300/500 'Place Kitten #4' 'An image of a very cute kitten' %}",
"{{< figure class=\"right\" src=\"/placekitten/300/500\" title=\"Place Kitten #4\" alt=\"An image of a very cute kitten\" >}}",
},
{
map[any]any{"category": "book", "layout": "post", "Date": "2015-10-01 12:13:11"},
"somecontent",
"---\nDate: \"2015-10-01 12:13:11\"\ncategory: book\nlayout: post\n---\nsomecontent",
},
}
for _, data := range testDataList {
result, err := convertJekyllContent(data.metadata, data.content)
c.Assert(result, qt.Equals, data.expect)
c.Assert(err, qt.IsNil)
}
}

View File

@@ -1,84 +0,0 @@
// Copyright 2018 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package commands
import (
"syscall"
"github.com/spf13/cobra"
jww "github.com/spf13/jwalterweatherman"
)
var _ cmder = (*limitCmd)(nil)
type limitCmd struct {
*baseCmd
}
func newLimitCmd() *limitCmd {
ccmd := &cobra.Command{
Use: "ulimit",
Short: "Check system ulimit settings",
Long: `Hugo will inspect the current ulimit settings on the system.
This is primarily to ensure that Hugo can watch enough files on some OSs`,
RunE: func(cmd *cobra.Command, args []string) error {
var rLimit syscall.Rlimit
err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit)
if err != nil {
return newSystemError("Error Getting rlimit ", err)
}
jww.FEEDBACK.Println("Current rLimit:", rLimit)
if rLimit.Cur >= newRlimit {
return nil
}
jww.FEEDBACK.Println("Attempting to increase limit")
rLimit.Cur = newRlimit
err = syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit)
if err != nil {
return newSystemError("Error Setting rLimit ", err)
}
err = syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit)
if err != nil {
return newSystemError("Error Getting rLimit ", err)
}
jww.FEEDBACK.Println("rLimit after change:", rLimit)
return nil
},
}
return &limitCmd{baseCmd: newBaseCmd(ccmd)}
}
const newRlimit = 10240
func tweakLimit() {
var rLimit syscall.Rlimit
err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit)
if err != nil {
jww.WARN.Println("Unable to get rlimit:", err)
return
}
if rLimit.Cur < newRlimit {
rLimit.Cur = newRlimit
err = syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit)
if err != nil {
// This may not succeed, see https://github.com/golang/go/issues/30401
jww.INFO.Println("Unable to increase number of open files limit:", err)
}
}
}

View File

@@ -1,21 +0,0 @@
// Copyright 2018 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build !darwin
// +build !darwin
package commands
func tweakLimit() {
// nothing to do
}

View File

@@ -1,4 +1,4 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
// Copyright 2023 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -14,197 +14,154 @@
package commands
import (
"context"
"encoding/csv"
"os"
"strconv"
"strings"
"time"
"github.com/bep/simplecobra"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/hugolib"
"github.com/gohugoio/hugo/resources/page"
"github.com/gohugoio/hugo/resources/resource"
"github.com/spf13/cobra"
jww "github.com/spf13/jwalterweatherman"
)
var _ cmder = (*listCmd)(nil)
// newListCommand creates a new list command and its subcommands.
func newListCommand() *listCommand {
type listCmd struct {
*baseBuilderCmd
}
func (lc *listCmd) buildSites(config map[string]any) (*hugolib.HugoSites, error) {
cfgInit := func(c *commandeer) error {
for key, value := range config {
c.Set(key, value)
list := func(cd *simplecobra.Commandeer, r *rootCommand, createRecord func(page.Page) []string, opts ...any) error {
bcfg := hugolib.BuildCfg{SkipRender: true}
cfg := config.New()
for i := 0; i < len(opts); i += 2 {
cfg.Set(opts[i].(string), opts[i+1])
}
h, err := r.Build(cd, bcfg, cfg)
if err != nil {
return err
}
writer := csv.NewWriter(r.Out)
defer writer.Flush()
for _, p := range h.Pages() {
if record := createRecord(p); record != nil {
if err := writer.Write(record); err != nil {
return err
}
if err != nil {
return err
}
}
}
return nil
}
c, err := initializeConfig(true, true, false, &lc.hugoBuilderCommon, lc, cfgInit)
if err != nil {
return nil, err
}
return &listCommand{
commands: []simplecobra.Commander{
&simpleCommand{
name: "drafts",
short: "List all drafts",
long: `List all of the drafts in your content directory.`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
createRecord := func(p page.Page) []string {
if !p.Draft() || p.File().IsZero() {
return nil
}
return []string{
p.File().Path(),
p.PublishDate().Format(time.RFC3339)}
sites, err := hugolib.NewHugoSites(*c.DepsCfg)
if err != nil {
return nil, newSystemError("Error creating sites", err)
}
if err := sites.Build(hugolib.BuildCfg{SkipRender: true}); err != nil {
return nil, newSystemError("Error Processing Source Content", err)
}
return sites, nil
}
func (b *commandsBuilder) newListCmd() *listCmd {
cc := &listCmd{}
cmd := &cobra.Command{
Use: "list",
Short: "Listing out various types of content",
Long: `Listing out various types of content.
List requires a subcommand, e.g. ` + "`hugo list drafts`.",
RunE: nil,
}
cmd.AddCommand(
&cobra.Command{
Use: "drafts",
Short: "List all drafts",
Long: `List all of the drafts in your content directory.`,
RunE: func(cmd *cobra.Command, args []string) error {
sites, err := cc.buildSites(map[string]any{"buildDrafts": true})
if err != nil {
return newSystemError("Error building sites", err)
}
for _, p := range sites.Pages() {
if p.Draft() {
jww.FEEDBACK.Println(strings.TrimPrefix(p.File().Filename(), sites.WorkingDir+string(os.PathSeparator)))
}
}
return nil
return list(cd, r, createRecord, "buildDrafts", true)
},
},
},
&cobra.Command{
Use: "future",
Short: "List all posts dated in the future",
Long: `List all of the posts in your content directory which will be posted in the future.`,
RunE: func(cmd *cobra.Command, args []string) error {
sites, err := cc.buildSites(map[string]any{"buildFuture": true})
if err != nil {
return newSystemError("Error building sites", err)
}
if err != nil {
return newSystemError("Error building sites", err)
}
writer := csv.NewWriter(os.Stdout)
defer writer.Flush()
for _, p := range sites.Pages() {
if resource.IsFuture(p) {
err := writer.Write([]string{
strings.TrimPrefix(p.File().Filename(), sites.WorkingDir+string(os.PathSeparator)),
&simpleCommand{
name: "future",
short: "List all posts dated in the future",
long: `List all of the posts in your content directory which will be posted in the future.`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
createRecord := func(p page.Page) []string {
if !resource.IsFuture(p) || p.File().IsZero() {
return nil
}
return []string{
p.File().Path(),
p.PublishDate().Format(time.RFC3339),
})
if err != nil {
return newSystemError("Error writing future posts to stdout", err)
}
}
}
return nil
return list(cd, r, createRecord, "buildFuture", true)
},
},
},
&cobra.Command{
Use: "expired",
Short: "List all posts already expired",
Long: `List all of the posts in your content directory which has already expired.`,
RunE: func(cmd *cobra.Command, args []string) error {
sites, err := cc.buildSites(map[string]any{"buildExpired": true})
if err != nil {
return newSystemError("Error building sites", err)
}
if err != nil {
return newSystemError("Error building sites", err)
}
writer := csv.NewWriter(os.Stdout)
defer writer.Flush()
for _, p := range sites.Pages() {
if resource.IsExpired(p) {
err := writer.Write([]string{
strings.TrimPrefix(p.File().Filename(), sites.WorkingDir+string(os.PathSeparator)),
p.ExpiryDate().Format(time.RFC3339),
})
if err != nil {
return newSystemError("Error writing expired posts to stdout", err)
&simpleCommand{
name: "expired",
short: "List all posts already expired",
long: `List all of the posts in your content directory which has already expired.`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
createRecord := func(p page.Page) []string {
if !resource.IsExpired(p) || p.File().IsZero() {
return nil
}
return []string{
p.File().Path(),
p.PublishDate().Format(time.RFC3339),
}
}
}
return nil
}
return list(cd, r, createRecord, "buildExpired", true)
},
},
&simpleCommand{
name: "all",
short: "List all posts",
long: `List all of the posts in your content directory, include drafts, future and expired pages.`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
createRecord := func(p page.Page) []string {
if p.File().IsZero() {
return nil
}
return []string{
p.File().Path(),
p.PublishDate().Format(time.RFC3339),
}
}
return list(cd, r, createRecord, "buildDrafts", true, "buildFuture", true, "buildExpired", true)
},
},
},
&cobra.Command{
Use: "all",
Short: "List all posts",
Long: `List all of the posts in your content directory, include drafts, future and expired pages.`,
RunE: func(cmd *cobra.Command, args []string) error {
sites, err := cc.buildSites(map[string]any{
"buildExpired": true,
"buildDrafts": true,
"buildFuture": true,
})
if err != nil {
return newSystemError("Error building sites", err)
}
}
writer := csv.NewWriter(os.Stdout)
defer writer.Flush()
writer.Write([]string{
"path",
"slug",
"title",
"date",
"expiryDate",
"publishDate",
"draft",
"permalink",
})
for _, p := range sites.Pages() {
if !p.IsPage() {
continue
}
err := writer.Write([]string{
strings.TrimPrefix(p.File().Filename(), sites.WorkingDir+string(os.PathSeparator)),
p.Slug(),
p.Title(),
p.Date().Format(time.RFC3339),
p.ExpiryDate().Format(time.RFC3339),
p.PublishDate().Format(time.RFC3339),
strconv.FormatBool(p.Draft()),
p.Permalink(),
})
if err != nil {
return newSystemError("Error writing posts to stdout", err)
}
}
return nil
},
},
)
cc.baseBuilderCmd = b.newBuilderBasicCmd(cmd)
return cc
}
type listCommand struct {
commands []simplecobra.Commander
}
func (c *listCommand) Commands() []simplecobra.Commander {
return c.commands
}
func (c *listCommand) Name() string {
return "list"
}
func (c *listCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error {
// Do nothing.
return nil
}
func (c *listCommand) WithCobraCommand(cmd *cobra.Command) error {
cmd.Short = "Listing out various types of content"
cmd.Long = `Listing out various types of content.
List requires a subcommand, e.g. hugo list drafts`
return nil
}
func (c *listCommand) Init(cd, runner *simplecobra.Commandeer) error {
return nil
}

View File

@@ -1,68 +0,0 @@
package commands
import (
"bytes"
"encoding/csv"
"io"
"os"
"path/filepath"
"strings"
"testing"
qt "github.com/frankban/quicktest"
)
func captureStdout(f func() error) (string, error) {
old := os.Stdout
r, w, _ := os.Pipe()
os.Stdout = w
err := f()
w.Close()
os.Stdout = old
var buf bytes.Buffer
io.Copy(&buf, r)
return buf.String(), err
}
func TestListAll(t *testing.T) {
c := qt.New(t)
dir := createSimpleTestSite(t, testSiteConfig{})
hugoCmd := newCommandsBuilder().addAll().build()
cmd := hugoCmd.getCommand()
t.Cleanup(func() {
os.RemoveAll(dir)
})
cmd.SetArgs([]string{"-s=" + dir, "list", "all"})
out, err := captureStdout(func() error {
_, err := cmd.ExecuteC()
return err
})
c.Assert(err, qt.IsNil)
r := csv.NewReader(strings.NewReader(out))
header, err := r.Read()
c.Assert(err, qt.IsNil)
c.Assert(header, qt.DeepEquals, []string{
"path", "slug", "title",
"date", "expiryDate", "publishDate",
"draft", "permalink",
})
record, err := r.Read()
c.Assert(err, qt.IsNil)
c.Assert(record, qt.DeepEquals, []string{
filepath.Join("content", "p1.md"), "", "P1",
"0001-01-01T00:00:00Z", "0001-01-01T00:00:00Z", "0001-01-01T00:00:00Z",
"false", "https://example.org/p1/",
})
}

View File

@@ -1,4 +1,4 @@
// Copyright 2020 The Hugo Authors. All rights reserved.
// Copyright 2023 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -14,87 +14,18 @@
package commands
import (
"context"
"errors"
"fmt"
"os"
"path/filepath"
"regexp"
"github.com/gohugoio/hugo/hugolib"
"github.com/gohugoio/hugo/modules"
"github.com/bep/simplecobra"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/modules/npm"
"github.com/spf13/cobra"
)
var _ cmder = (*modCmd)(nil)
type modCmd struct {
*baseBuilderCmd
}
func (c *modCmd) newVerifyCmd() *cobra.Command {
var clean bool
verifyCmd := &cobra.Command{
Use: "verify",
Short: "Verify dependencies.",
Long: `Verify checks that the dependencies of the current module, which are stored in a local downloaded source cache, have not been modified since being downloaded.
`,
RunE: func(cmd *cobra.Command, args []string) error {
return c.withModsClient(true, func(c *modules.Client) error {
return c.Verify(clean)
})
},
}
verifyCmd.Flags().BoolVarP(&clean, "clean", "", false, "delete module cache for dependencies that fail verification")
return verifyCmd
}
var moduleNotFoundRe = regexp.MustCompile("module.*not found")
func (c *modCmd) newCleanCmd() *cobra.Command {
var pattern string
var all bool
cmd := &cobra.Command{
Use: "clean",
Short: "Delete the Hugo Module cache for the current project.",
Long: `Delete the Hugo Module cache for the current project.
Note that after you run this command, all of your dependencies will be re-downloaded next time you run "hugo".
Also note that if you configure a positive maxAge for the "modules" file cache, it will also be cleaned as part of "hugo --gc".
`,
RunE: func(cmd *cobra.Command, args []string) error {
if all {
com, err := c.initConfig(false)
if err != nil && com == nil {
return err
}
count, err := com.hugo().FileCaches.ModulesCache().Prune(true)
com.logger.Printf("Deleted %d files from module cache.", count)
return err
}
return c.withModsClient(true, func(c *modules.Client) error {
return c.Clean(pattern)
})
},
}
cmd.Flags().StringVarP(&pattern, "pattern", "", "", `pattern matching module paths to clean (all if not set), e.g. "**hugo*"`)
cmd.Flags().BoolVarP(&all, "all", "", false, "clean entire module cache")
return cmd
}
func (b *commandsBuilder) newModCmd() *modCmd {
c := &modCmd{}
const commonUsage = `
const commonUsageMod = `
Note that Hugo will always start out by resolving the components defined in the site
configuration, provided by a _vendor directory (if no --ignoreVendorPaths flag provided),
Go Modules, or a folder inside the themes directory, in that order.
@@ -103,27 +34,156 @@ See https://gohugo.io/hugo-modules/ for more information.
`
cmd := &cobra.Command{
Use: "mod",
Short: "Various Hugo Modules helpers.",
Long: `Various helpers to help manage the modules in your project's dependency graph.
// buildConfigCommands creates a new config command and its subcommands.
func newModCommands() *modCommands {
var (
clean bool
pattern string
all bool
)
Most operations here requires a Go version installed on your system (>= Go 1.12) and the relevant VCS client (typically Git).
This is not needed if you only operate on modules inside /themes or if you have vendored them via "hugo mod vendor".
npmCommand := &simpleCommand{
name: "npm",
short: "Various npm helpers.",
long: `Various npm (Node package manager) helpers.`,
commands: []simplecobra.Commander{
&simpleCommand{
name: "pack",
short: "Experimental: Prepares and writes a composite package.json file for your project.",
long: `Prepares and writes a composite package.json file for your project.
` + commonUsage,
On first run it creates a "package.hugo.json" in the project root if not already there. This file will be used as a template file
with the base dependency set.
RunE: nil,
This set will be merged with all "package.hugo.json" files found in the dependency tree, picking the version closest to the project.
This command is marked as 'Experimental'. We think it's a great idea, so it's not likely to be
removed from Hugo, but we need to test this out in "real life" to get a feel of it,
so this may/will change in future versions of Hugo.
`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
h, err := r.Hugo(flagsToCfg(cd, nil))
if err != nil {
return err
}
return npm.Pack(h.BaseFs.SourceFs, h.BaseFs.Assets.Dirs)
},
},
},
}
cmd.AddCommand(newModNPMCmd(c))
return &modCommands{
commands: []simplecobra.Commander{
&simpleCommand{
name: "init",
short: "Initialize this project as a Hugo Module.",
long: `Initialize this project as a Hugo Module.
It will try to guess the module path, but you may help by passing it as an argument, e.g:
hugo mod init github.com/gohugoio/testshortcodes
Note that Hugo Modules supports multi-module projects, so you can initialize a Hugo Module
inside a subfolder on GitHub, as one example.
`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
h, err := r.Hugo(flagsToCfg(cd, nil))
if err != nil {
return err
}
var initPath string
if len(args) >= 1 {
initPath = args[0]
}
return h.Configs.ModulesClient.Init(initPath)
},
},
&simpleCommand{
name: "verify",
short: "Verify dependencies.",
long: `Verify checks that the dependencies of the current module, which are stored in a local downloaded source cache, have not been modified since being downloaded.`,
withc: func(cmd *cobra.Command) {
cmd.Flags().BoolVarP(&clean, "clean", "", false, "delete module cache for dependencies that fail verification")
},
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
conf, err := r.ConfigFromProvider(r.configVersionID.Load(), flagsToCfg(cd, nil))
if err != nil {
return err
}
client := conf.configs.ModulesClient
return client.Verify(clean)
},
},
&simpleCommand{
name: "graph",
short: "Print a module dependency graph.",
long: `Print a module dependency graph with information about module status (disabled, vendored).
Note that for vendored modules, that is the version listed and not the one from go.mod.
`,
withc: func(cmd *cobra.Command) {
cmd.Flags().BoolVarP(&clean, "clean", "", false, "delete module cache for dependencies that fail verification")
},
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
conf, err := r.ConfigFromProvider(r.configVersionID.Load(), flagsToCfg(cd, nil))
if err != nil {
return err
}
client := conf.configs.ModulesClient
return client.Graph(os.Stdout)
},
},
&simpleCommand{
name: "clean",
short: "Delete the Hugo Module cache for the current project.",
long: `Delete the Hugo Module cache for the current project.`,
withc: func(cmd *cobra.Command) {
cmd.Flags().StringVarP(&pattern, "pattern", "", "", `pattern matching module paths to clean (all if not set), e.g. "**hugo*"`)
cmd.Flags().BoolVarP(&all, "all", "", false, "clean entire module cache")
},
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
h, err := r.Hugo(flagsToCfg(cd, nil))
if err != nil {
return err
}
if all {
modCache := h.ResourceSpec.FileCaches.ModulesCache()
count, err := modCache.Prune(true)
r.Printf("Deleted %d files from module cache.", count)
return err
}
cmd.AddCommand(
&cobra.Command{
Use: "get",
DisableFlagParsing: true,
Short: "Resolves dependencies in your current Hugo Project.",
Long: `
return h.Configs.ModulesClient.Clean(pattern)
},
},
&simpleCommand{
name: "tidy",
short: "Remove unused entries in go.mod and go.sum.",
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
h, err := r.Hugo(flagsToCfg(cd, nil))
if err != nil {
return err
}
return h.Configs.ModulesClient.Tidy()
},
},
&simpleCommand{
name: "vendor",
short: "Vendor all module dependencies into the _vendor directory.",
long: `Vendor all module dependencies into the _vendor directory.
If a module is vendored, that is where Hugo will look for it's dependencies.
`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
h, err := r.Hugo(flagsToCfg(cd, nil))
if err != nil {
return err
}
return h.Configs.ModulesClient.Vendor()
},
},
&simpleCommand{
name: "get",
short: "Resolves dependencies in your current Hugo Project.",
long: `
Resolves dependencies in your current Hugo Project.
Some examples:
@@ -142,152 +202,109 @@ Install the latest versions of all module dependencies:
hugo mod get -u ./... (recursive)
Run "go help get" for more information. All flags available for "go get" is also relevant here.
` + commonUsage,
RunE: func(cmd *cobra.Command, args []string) error {
// We currently just pass on the flags we get to Go and
// need to do the flag handling manually.
if len(args) == 1 && args[0] == "-h" {
return cmd.Help()
}
var lastArg string
if len(args) != 0 {
lastArg = args[len(args)-1]
}
if lastArg == "./..." {
args = args[:len(args)-1]
// Do a recursive update.
dirname, err := os.Getwd()
if err != nil {
return err
` + commonUsageMod,
withc: func(cmd *cobra.Command) {
cmd.DisableFlagParsing = true
},
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
// We currently just pass on the flags we get to Go and
// need to do the flag handling manually.
if len(args) == 1 && args[0] == "-h" {
return errHelp
}
// Sanity check. We do recursive walking and want to avoid
// accidents.
if len(dirname) < 5 {
return errors.New("must not be run from the file system root")
var lastArg string
if len(args) != 0 {
lastArg = args[len(args)-1]
}
filepath.Walk(dirname, func(path string, info os.FileInfo, err error) error {
if info.IsDir() {
return nil
if lastArg == "./..." {
args = args[:len(args)-1]
// Do a recursive update.
dirname, err := os.Getwd()
if err != nil {
return err
}
if info.Name() == "go.mod" {
// Found a module.
dir := filepath.Dir(path)
fmt.Println("Update module in", dir)
c.source = dir
err := c.withModsClient(false, func(c *modules.Client) error {
if len(args) == 1 && args[0] == "-h" {
return cmd.Help()
}
return c.Get(args...)
})
if err != nil {
return err
// Sanity chesimplecobra. We do recursive walking and want to avoid
// accidents.
if len(dirname) < 5 {
return errors.New("must not be run from the file system root")
}
filepath.Walk(dirname, func(path string, info os.FileInfo, err error) error {
if info.IsDir() {
return nil
}
if info.Name() == "go.mod" {
// Found a module.
dir := filepath.Dir(path)
r.Println("Update module in", dir)
cfg := config.New()
cfg.Set("workingDir", dir)
conf, err := r.ConfigFromProvider(r.configVersionID.Load(), flagsToCfg(cd, cfg))
if err != nil {
return err
}
client := conf.configs.ModulesClient
return client.Get(args...)
}
}
return nil
})
return nil
})
return nil
}
return c.withModsClient(false, func(c *modules.Client) error {
return c.Get(args...)
})
} else {
conf, err := r.ConfigFromProvider(r.configVersionID.Load(), flagsToCfg(cd, nil))
if err != nil {
return err
}
client := conf.configs.ModulesClient
return client.Get(args...)
}
},
},
npmCommand,
},
&cobra.Command{
Use: "graph",
Short: "Print a module dependency graph.",
Long: `Print a module dependency graph with information about module status (disabled, vendored).
Note that for vendored modules, that is the version listed and not the one from go.mod.
`,
RunE: func(cmd *cobra.Command, args []string) error {
return c.withModsClient(true, func(c *modules.Client) error {
return c.Graph(os.Stdout)
})
},
},
&cobra.Command{
Use: "init",
Short: "Initialize this project as a Hugo Module.",
Long: `Initialize this project as a Hugo Module.
It will try to guess the module path, but you may help by passing it as an argument, e.g:
}
hugo mod init github.com/gohugoio/testshortcodes
Note that Hugo Modules supports multi-module projects, so you can initialize a Hugo Module
inside a subfolder on GitHub, as one example.
`,
RunE: func(cmd *cobra.Command, args []string) error {
var path string
if len(args) >= 1 {
path = args[0]
}
return c.withModsClient(false, func(c *modules.Client) error {
return c.Init(path)
})
},
},
&cobra.Command{
Use: "vendor",
Short: "Vendor all module dependencies into the _vendor directory.",
Long: `Vendor all module dependencies into the _vendor directory.
If a module is vendored, that is where Hugo will look for it's dependencies.
`,
RunE: func(cmd *cobra.Command, args []string) error {
return c.withModsClient(true, func(c *modules.Client) error {
return c.Vendor()
})
},
},
c.newVerifyCmd(),
&cobra.Command{
Use: "tidy",
Short: "Remove unused entries in go.mod and go.sum.",
RunE: func(cmd *cobra.Command, args []string) error {
return c.withModsClient(true, func(c *modules.Client) error {
return c.Tidy()
})
},
},
c.newCleanCmd(),
)
c.baseBuilderCmd = b.newBuilderCmd(cmd)
return c
}
func (c *modCmd) withModsClient(failOnMissingConfig bool, f func(*modules.Client) error) error {
com, err := c.initConfig(failOnMissingConfig)
type modCommands struct {
r *rootCommand
commands []simplecobra.Commander
}
func (c *modCommands) Commands() []simplecobra.Commander {
return c.commands
}
func (c *modCommands) Name() string {
return "mod"
}
func (c *modCommands) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error {
_, err := c.r.ConfigFromProvider(c.r.configVersionID.Load(), nil)
if err != nil {
return err
}
//config := conf.configs.Base
return f(com.hugo().ModulesClient)
return nil
}
func (c *modCmd) withHugo(f func(*hugolib.HugoSites) error) error {
com, err := c.initConfig(true)
if err != nil {
return err
}
func (c *modCommands) WithCobraCommand(cmd *cobra.Command) error {
cmd.Short = "Various Hugo Modules helpers."
cmd.Long = `Various helpers to help manage the modules in your project's dependency graph.
Most operations here requires a Go version installed on your system (>= Go 1.12) and the relevant VCS client (typically Git).
This is not needed if you only operate on modules inside /themes or if you have vendored them via "hugo mod vendor".
return f(com.hugo())
` + commonUsageMod
cmd.RunE = nil
return nil
}
func (c *modCmd) initConfig(failOnNoConfig bool) (*commandeer, error) {
com, err := initializeConfig(failOnNoConfig, false, false, &c.hugoBuilderCommon, c, nil)
if err != nil {
return nil, err
}
return com, nil
func (c *modCommands) Init(cd, runner *simplecobra.Commandeer) error {
c.r = cd.Root.Command.(*rootCommand)
return nil
}

View File

@@ -1,56 +0,0 @@
// Copyright 2020 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package commands
import (
"github.com/gohugoio/hugo/hugolib"
"github.com/gohugoio/hugo/modules/npm"
"github.com/spf13/cobra"
)
func newModNPMCmd(c *modCmd) *cobra.Command {
cmd := &cobra.Command{
Use: "npm",
Short: "Various npm helpers.",
Long: `Various npm (Node package manager) helpers.`,
RunE: func(cmd *cobra.Command, args []string) error {
return c.withHugo(func(h *hugolib.HugoSites) error {
return nil
})
},
}
cmd.AddCommand(&cobra.Command{
Use: "pack",
Short: "Experimental: Prepares and writes a composite package.json file for your project.",
Long: `Prepares and writes a composite package.json file for your project.
On first run it creates a "package.hugo.json" in the project root if not already there. This file will be used as a template file
with the base dependency set.
This set will be merged with all "package.hugo.json" files found in the dependency tree, picking the version closest to the project.
This command is marked as 'Experimental'. We think it's a great idea, so it's not likely to be
removed from Hugo, but we need to test this out in "real life" to get a feel of it,
so this may/will change in future versions of Hugo.
`,
RunE: func(cmd *cobra.Command, args []string) error {
return c.withHugo(func(h *hugolib.HugoSites) error {
return npm.Pack(h.BaseFs.SourceFs, h.BaseFs.Assets.Dirs)
})
},
})
return cmd
}

View File

@@ -1,4 +1,4 @@
// Copyright 2018 The Hugo Authors. All rights reserved.
// Copyright 2023 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -15,114 +15,351 @@ package commands
import (
"bytes"
"os"
"context"
"errors"
"fmt"
"path/filepath"
"strings"
"github.com/bep/simplecobra"
"github.com/gohugoio/hugo/common/htime"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/create"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugolib"
"github.com/gohugoio/hugo/parser"
"github.com/gohugoio/hugo/parser/metadecoders"
"github.com/spf13/afero"
"github.com/spf13/cobra"
jww "github.com/spf13/jwalterweatherman"
)
var _ cmder = (*newCmd)(nil)
func newNewCommand() *newCommand {
var (
configFormat string
force bool
contentType string
)
type newCmd struct {
contentEditor string
contentType string
force bool
var c *newCommand
c = &newCommand{
commands: []simplecobra.Commander{
&simpleCommand{
name: "content",
use: "content [path]",
short: "Create new content for your site",
long: `Create a new content file and automatically set the date and title.
It will guess which kind of file to create based on the path provided.
You can also specify the kind with ` + "`-k KIND`" + `.
If archetypes are provided in your theme or site, they will be used.
Ensure you run this within the root directory of your site.`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
if len(args) < 1 {
return errors.New("path needs to be provided")
}
h, err := r.Hugo(flagsToCfg(cd, nil))
if err != nil {
return err
}
return create.NewContent(h, contentType, args[0], force)
},
withc: func(cmd *cobra.Command) {
cmd.Flags().StringVarP(&contentType, "kind", "k", "", "content type to create")
cmd.Flags().String("editor", "", "edit new content with this editor, if provided")
cmd.Flags().BoolVarP(&force, "force", "f", false, "overwrite file if it already exists")
},
},
&simpleCommand{
name: "site",
use: "site [path]",
short: "Create a new site (skeleton)",
long: `Create a new site in the provided directory.
The new site will have the correct structure, but no content or theme yet.
Use ` + "`hugo new [contentPath]`" + ` to create new content.`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
if len(args) < 1 {
return errors.New("path needs to be provided")
}
createpath, err := filepath.Abs(filepath.Clean(args[0]))
if err != nil {
return err
}
cfg := config.New()
cfg.Set("workingDir", createpath)
cfg.Set("publishDir", "public")
conf, err := r.ConfigFromProvider(r.configVersionID.Load(), flagsToCfg(cd, cfg))
if err != nil {
return err
}
sourceFs := conf.fs.Source
archeTypePath := filepath.Join(createpath, "archetypes")
dirs := []string{
archeTypePath,
filepath.Join(createpath, "assets"),
filepath.Join(createpath, "content"),
filepath.Join(createpath, "data"),
filepath.Join(createpath, "layouts"),
filepath.Join(createpath, "static"),
filepath.Join(createpath, "themes"),
}
if exists, _ := helpers.Exists(createpath, sourceFs); exists {
if isDir, _ := helpers.IsDir(createpath, sourceFs); !isDir {
return errors.New(createpath + " already exists but not a directory")
}
isEmpty, _ := helpers.IsEmpty(createpath, sourceFs)
switch {
case !isEmpty && !force:
return errors.New(createpath + " already exists and is not empty. See --force.")
case !isEmpty && force:
all := append(dirs, filepath.Join(createpath, "hugo."+configFormat))
for _, path := range all {
if exists, _ := helpers.Exists(path, sourceFs); exists {
return errors.New(path + " already exists")
}
}
}
}
for _, dir := range dirs {
if err := sourceFs.MkdirAll(dir, 0777); err != nil {
return fmt.Errorf("failed to create dir: %w", err)
}
}
c.newSiteCreateConfig(sourceFs, createpath, configFormat)
// Create a default archetype file.
helpers.SafeWriteToDisk(filepath.Join(archeTypePath, "default.md"),
strings.NewReader(create.DefaultArchetypeTemplateTemplate), sourceFs)
r.Printf("Congratulations! Your new Hugo site is created in %s.\n\n", createpath)
r.Println(c.newSiteNextStepsText())
return nil
},
withc: func(cmd *cobra.Command) {
cmd.Flags().StringVarP(&configFormat, "format", "f", "toml", "config file format")
cmd.Flags().BoolVar(&force, "force", false, "init inside non-empty directory")
},
},
&simpleCommand{
name: "theme",
use: "theme [path]",
short: "Create a new site (skeleton)",
long: `Create a new site in the provided directory.
The new site will have the correct structure, but no content or theme yet.
Use ` + "`hugo new [contentPath]`" + ` to create new content.`,
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
h, err := r.Hugo(flagsToCfg(cd, nil))
if err != nil {
return err
}
ps := h.PathSpec
sourceFs := ps.Fs.Source
themesDir := h.Configs.LoadingInfo.BaseConfig.ThemesDir
createpath := ps.AbsPathify(filepath.Join(themesDir, args[0]))
r.Println("Creating theme at", createpath)
if x, _ := helpers.Exists(createpath, sourceFs); x {
return errors.New(createpath + " already exists")
}
for _, filename := range []string{
"index.html",
"404.html",
"_default/list.html",
"_default/single.html",
"partials/head.html",
"partials/header.html",
"partials/footer.html",
} {
touchFile(sourceFs, filepath.Join(createpath, "layouts", filename))
}
baseofDefault := []byte(`<!DOCTYPE html>
<html>
{{- partial "head.html" . -}}
<body>
{{- partial "header.html" . -}}
<div id="content">
{{- block "main" . }}{{- end }}
</div>
{{- partial "footer.html" . -}}
</body>
</html>
`)
err = helpers.WriteToDisk(filepath.Join(createpath, "layouts", "_default", "baseof.html"), bytes.NewReader(baseofDefault), sourceFs)
if err != nil {
return err
}
mkdir(createpath, "archetypes")
archDefault := []byte("+++\n+++\n")
err = helpers.WriteToDisk(filepath.Join(createpath, "archetypes", "default.md"), bytes.NewReader(archDefault), sourceFs)
if err != nil {
return err
}
mkdir(createpath, "static", "js")
mkdir(createpath, "static", "css")
by := []byte(`The MIT License (MIT)
Copyright (c) ` + htime.Now().Format("2006") + ` YOUR_NAME_HERE
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
`)
err = helpers.WriteToDisk(filepath.Join(createpath, "LICENSE"), bytes.NewReader(by), sourceFs)
if err != nil {
return err
}
c.createThemeMD(ps.Fs.Source, createpath)
return nil
},
},
},
}
return c
*baseBuilderCmd
}
func (b *commandsBuilder) newNewCmd() *newCmd {
cmd := &cobra.Command{
Use: "new [path]",
Short: "Create new content for your site",
Long: `Create a new content file and automatically set the date and title.
type newCommand struct {
rootCmd *rootCommand
commands []simplecobra.Commander
}
func (c *newCommand) Commands() []simplecobra.Commander {
return c.commands
}
func (c *newCommand) Name() string {
return "new"
}
func (c *newCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error {
return nil
}
func (c *newCommand) WithCobraCommand(cmd *cobra.Command) error {
cmd.Short = "Create new content for your site"
cmd.Long = `Create a new content file and automatically set the date and title.
It will guess which kind of file to create based on the path provided.
You can also specify the kind with ` + "`-k KIND`" + `.
If archetypes are provided in your theme or site, they will be used.
Ensure you run this within the root directory of your site.`,
}
cc := &newCmd{baseBuilderCmd: b.newBuilderCmd(cmd)}
cmd.Flags().StringVarP(&cc.contentType, "kind", "k", "", "content type to create")
cmd.Flags().StringVar(&cc.contentEditor, "editor", "", "edit new content with this editor, if provided")
cmd.Flags().BoolVarP(&cc.force, "force", "f", false, "overwrite file if it already exists")
cmd.AddCommand(b.newNewSiteCmd().getCommand())
cmd.AddCommand(b.newNewThemeCmd().getCommand())
cmd.RunE = cc.newContent
return cc
Ensure you run this within the root directory of your site.`
return nil
}
func (n *newCmd) newContent(cmd *cobra.Command, args []string) error {
cfgInit := func(c *commandeer) error {
if cmd.Flags().Changed("editor") {
c.Set("newContentEditor", n.contentEditor)
}
return nil
func (c *newCommand) Init(cd, runner *simplecobra.Commandeer) error {
c.rootCmd = cd.Root.Command.(*rootCommand)
return nil
}
func (c *newCommand) newSiteCreateConfig(fs afero.Fs, inpath string, kind string) (err error) {
in := map[string]string{
"baseURL": "http://example.org/",
"title": "My New Hugo Site",
"languageCode": "en-us",
}
c, err := initializeConfig(true, true, false, &n.hugoBuilderCommon, n, cfgInit)
var buf bytes.Buffer
err = parser.InterfaceToConfig(in, metadecoders.FormatFromString(kind), &buf)
if err != nil {
return err
}
if len(args) < 1 {
return newUserError("path needs to be provided")
}
return create.NewContent(c.hugo(), n.contentType, args[0], n.force)
return helpers.WriteToDisk(filepath.Join(inpath, "hugo."+kind), &buf, fs)
}
func mkdir(x ...string) {
p := filepath.Join(x...)
func (c *newCommand) newSiteNextStepsText() string {
var nextStepsText bytes.Buffer
err := os.MkdirAll(p, 0777) // before umask
nextStepsText.WriteString(`Just a few more steps and you're ready to go:
1. Download a theme into the same-named folder.
Choose a theme from https://themes.gohugo.io/ or
create your own with the "hugo new theme <THEMENAME>" command.
2. Perhaps you want to add some content. You can add single files
with "hugo new `)
nextStepsText.WriteString(filepath.Join("<SECTIONNAME>", "<FILENAME>.<FORMAT>"))
nextStepsText.WriteString(`".
3. Start the built-in live server via "hugo server".
Visit https://gohugo.io/ for quickstart guide and full documentation.`)
return nextStepsText.String()
}
func (c *newCommand) createThemeMD(fs afero.Fs, inpath string) (err error) {
by := []byte(`# theme.toml template for a Hugo theme
# See https://github.com/gohugoio/hugoThemes#themetoml for an example
name = "` + strings.Title(helpers.MakeTitle(filepath.Base(inpath))) + `"
license = "MIT"
licenselink = "https://github.com/yourname/yourtheme/blob/master/LICENSE"
description = ""
homepage = "http://example.com/"
tags = []
features = []
min_version = "0.41.0"
[author]
name = ""
homepage = ""
# If porting an existing theme
[original]
name = ""
homepage = ""
repo = ""
`)
err = helpers.WriteToDisk(filepath.Join(inpath, "theme.toml"), bytes.NewReader(by), fs)
if err != nil {
jww.FATAL.Fatalln(err)
return
}
}
func touchFile(fs afero.Fs, x ...string) {
inpath := filepath.Join(x...)
mkdir(filepath.Dir(inpath))
err := helpers.WriteToDisk(inpath, bytes.NewReader([]byte{}), fs)
err = helpers.WriteToDisk(filepath.Join(inpath, "hugo.toml"), strings.NewReader("# Theme config.\n"), fs)
if err != nil {
jww.FATAL.Fatalln(err)
return
}
}
func newContentPathSection(h *hugolib.HugoSites, path string) (string, string) {
// Forward slashes is used in all examples. Convert if needed.
// Issue #1133
createpath := filepath.FromSlash(path)
if h != nil {
for _, dir := range h.BaseFs.Content.Dirs {
createpath = strings.TrimPrefix(createpath, dir.Meta().Filename)
}
}
var section string
// assume the first directory is the section (kind)
if strings.Contains(createpath[1:], helpers.FilePathSeparator) {
parts := strings.Split(strings.TrimPrefix(createpath, helpers.FilePathSeparator), helpers.FilePathSeparator)
if len(parts) > 0 {
section = parts[0]
}
}
return createpath, section
return nil
}

View File

@@ -1,29 +0,0 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package commands
import (
"path/filepath"
"testing"
qt "github.com/frankban/quicktest"
)
// Issue #1133
func TestNewContentPathSectionWithForwardSlashes(t *testing.T) {
c := qt.New(t)
p, s := newContentPathSection(nil, "/post/new.md")
c.Assert(p, qt.Equals, filepath.FromSlash("/post/new.md"))
c.Assert(s, qt.Equals, "post")
}

View File

@@ -1,167 +0,0 @@
// Copyright 2018 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package commands
import (
"bytes"
"errors"
"fmt"
"path/filepath"
"strings"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/parser/metadecoders"
"github.com/gohugoio/hugo/create"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugofs"
"github.com/gohugoio/hugo/parser"
"github.com/spf13/cobra"
jww "github.com/spf13/jwalterweatherman"
)
var _ cmder = (*newSiteCmd)(nil)
type newSiteCmd struct {
configFormat string
*baseBuilderCmd
}
func (b *commandsBuilder) newNewSiteCmd() *newSiteCmd {
cc := &newSiteCmd{}
cmd := &cobra.Command{
Use: "site [path]",
Short: "Create a new site (skeleton)",
Long: `Create a new site in the provided directory.
The new site will have the correct structure, but no content or theme yet.
Use ` + "`hugo new [contentPath]`" + ` to create new content.`,
RunE: cc.newSite,
}
cmd.Flags().StringVarP(&cc.configFormat, "format", "f", "toml", "config file format")
cmd.Flags().Bool("force", false, "init inside non-empty directory")
cc.baseBuilderCmd = b.newBuilderBasicCmd(cmd)
return cc
}
func (n *newSiteCmd) doNewSite(fs *hugofs.Fs, basepath string, force bool) error {
archeTypePath := filepath.Join(basepath, "archetypes")
dirs := []string{
archeTypePath,
filepath.Join(basepath, "assets"),
filepath.Join(basepath, "content"),
filepath.Join(basepath, "data"),
filepath.Join(basepath, "layouts"),
filepath.Join(basepath, "static"),
filepath.Join(basepath, "themes"),
}
if exists, _ := helpers.Exists(basepath, fs.Source); exists {
if isDir, _ := helpers.IsDir(basepath, fs.Source); !isDir {
return errors.New(basepath + " already exists but not a directory")
}
isEmpty, _ := helpers.IsEmpty(basepath, fs.Source)
switch {
case !isEmpty && !force:
return errors.New(basepath + " already exists and is not empty. See --force.")
case !isEmpty && force:
// TODO(bep) eventually rename this to hugo.
all := append(dirs, filepath.Join(basepath, "config."+n.configFormat))
for _, path := range all {
if exists, _ := helpers.Exists(path, fs.Source); exists {
return errors.New(path + " already exists")
}
}
}
}
for _, dir := range dirs {
if err := fs.Source.MkdirAll(dir, 0777); err != nil {
return fmt.Errorf("Failed to create dir: %w", err)
}
}
createConfig(fs, basepath, n.configFormat)
// Create a default archetype file.
helpers.SafeWriteToDisk(filepath.Join(archeTypePath, "default.md"),
strings.NewReader(create.DefaultArchetypeTemplateTemplate), fs.Source)
jww.FEEDBACK.Printf("Congratulations! Your new Hugo site is created in %s.\n\n", basepath)
jww.FEEDBACK.Println(nextStepsText())
return nil
}
// newSite creates a new Hugo site and initializes a structured Hugo directory.
func (n *newSiteCmd) newSite(cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return newUserError("path needs to be provided")
}
createpath, err := filepath.Abs(filepath.Clean(args[0]))
if err != nil {
return newUserError(err)
}
forceNew, _ := cmd.Flags().GetBool("force")
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) {
in := map[string]string{
"baseURL": "http://example.org/",
"title": "My New Hugo Site",
"languageCode": "en-us",
}
var buf bytes.Buffer
err = parser.InterfaceToConfig(in, metadecoders.FormatFromString(kind), &buf)
if err != nil {
return err
}
return helpers.WriteToDisk(filepath.Join(inpath, "config."+kind), &buf, fs.Source)
}
func nextStepsText() string {
var nextStepsText bytes.Buffer
nextStepsText.WriteString(`Just a few more steps and you're ready to go:
1. Download a theme into the same-named folder.
Choose a theme from https://themes.gohugo.io/ or
create your own with the "hugo new theme <THEMENAME>" command.
2. Perhaps you want to add some content. You can add single files
with "hugo new `)
nextStepsText.WriteString(filepath.Join("<SECTIONNAME>", "<FILENAME>.<FORMAT>"))
nextStepsText.WriteString(`".
3. Start the built-in live server via "hugo server".
Visit https://gohugo.io/ for quickstart guide and full documentation.`)
return nextStepsText.String()
}

View File

@@ -1,176 +0,0 @@
// Copyright 2018 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package commands
import (
"bytes"
"errors"
"path/filepath"
"strings"
"github.com/gohugoio/hugo/common/htime"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/hugofs"
"github.com/spf13/cobra"
jww "github.com/spf13/jwalterweatherman"
)
var _ cmder = (*newThemeCmd)(nil)
type newThemeCmd struct {
*baseBuilderCmd
}
func (b *commandsBuilder) newNewThemeCmd() *newThemeCmd {
cc := &newThemeCmd{}
cmd := &cobra.Command{
Use: "theme [name]",
Short: "Create a new theme",
Long: `Create a new theme (skeleton) called [name] in ./themes.
New theme is a skeleton. Please add content to the touched files. Add your
name to the copyright line in the license and adjust the theme.toml file
as you see fit.`,
RunE: cc.newTheme,
}
cc.baseBuilderCmd = b.newBuilderBasicCmd(cmd)
return cc
}
// newTheme creates a new Hugo theme template
func (n *newThemeCmd) newTheme(cmd *cobra.Command, args []string) error {
c, err := initializeConfig(false, false, false, &n.hugoBuilderCommon, n, nil)
if err != nil {
return err
}
if len(args) < 1 {
return newUserError("theme name needs to be provided")
}
createpath := c.hugo().PathSpec.AbsPathify(filepath.Join(c.Cfg.GetString("themesDir"), args[0]))
jww.FEEDBACK.Println("Creating theme at", createpath)
cfg := c.DepsCfg
if x, _ := helpers.Exists(createpath, cfg.Fs.Source); x {
return errors.New(createpath + " already exists")
}
mkdir(createpath, "layouts", "_default")
mkdir(createpath, "layouts", "partials")
touchFile(cfg.Fs.Source, createpath, "layouts", "index.html")
touchFile(cfg.Fs.Source, createpath, "layouts", "404.html")
touchFile(cfg.Fs.Source, createpath, "layouts", "_default", "list.html")
touchFile(cfg.Fs.Source, createpath, "layouts", "_default", "single.html")
baseofDefault := []byte(`<!DOCTYPE html>
<html>
{{- partial "head.html" . -}}
<body>
{{- partial "header.html" . -}}
<div id="content">
{{- block "main" . }}{{- end }}
</div>
{{- partial "footer.html" . -}}
</body>
</html>
`)
err = helpers.WriteToDisk(filepath.Join(createpath, "layouts", "_default", "baseof.html"), bytes.NewReader(baseofDefault), cfg.Fs.Source)
if err != nil {
return err
}
touchFile(cfg.Fs.Source, createpath, "layouts", "partials", "head.html")
touchFile(cfg.Fs.Source, createpath, "layouts", "partials", "header.html")
touchFile(cfg.Fs.Source, createpath, "layouts", "partials", "footer.html")
mkdir(createpath, "archetypes")
archDefault := []byte("+++\n+++\n")
err = helpers.WriteToDisk(filepath.Join(createpath, "archetypes", "default.md"), bytes.NewReader(archDefault), cfg.Fs.Source)
if err != nil {
return err
}
mkdir(createpath, "static", "js")
mkdir(createpath, "static", "css")
by := []byte(`The MIT License (MIT)
Copyright (c) ` + htime.Now().Format("2006") + ` YOUR_NAME_HERE
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
`)
err = helpers.WriteToDisk(filepath.Join(createpath, "LICENSE"), bytes.NewReader(by), cfg.Fs.Source)
if err != nil {
return err
}
n.createThemeMD(cfg.Fs, createpath)
return nil
}
func (n *newThemeCmd) createThemeMD(fs *hugofs.Fs, inpath string) (err error) {
by := []byte(`# theme.toml template for a Hugo theme
# See https://github.com/gohugoio/hugoThemes#themetoml for an example
name = "` + strings.Title(helpers.MakeTitle(filepath.Base(inpath))) + `"
license = "MIT"
licenselink = "https://github.com/yourname/yourtheme/blob/master/LICENSE"
description = ""
homepage = "http://example.com/"
tags = []
features = []
min_version = "0.41.0"
[author]
name = ""
homepage = ""
# If porting an existing theme
[original]
name = ""
homepage = ""
repo = ""
`)
err = helpers.WriteToDisk(filepath.Join(inpath, "theme.toml"), bytes.NewReader(by), fs.Source)
if err != nil {
return
}
return nil
}

View File

@@ -1,51 +0,0 @@
// Copyright 2019 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//go:build nodeploy
// +build nodeploy
package commands
import (
"errors"
"github.com/spf13/cobra"
)
var _ cmder = (*deployCmd)(nil)
// deployCmd supports deploying sites to Cloud providers.
type deployCmd struct {
*baseBuilderCmd
}
func (b *commandsBuilder) newDeployCmd() *deployCmd {
cc := &deployCmd{}
cmd := &cobra.Command{
Use: "deploy",
Short: "Deploy your site to a Cloud provider.",
Long: `Deploy your site to a Cloud provider.
See https://gohugo.io/hosting-and-deployment/hugo-deploy/ for detailed
documentation.
`,
RunE: func(cmd *cobra.Command, args []string) error {
return errors.New("build without HUGO_BUILD_TAGS=nodeploy to use this command")
},
}
cc.baseBuilderCmd = b.newBuilderBasicCmd(cmd)
return cc
}

View File

@@ -1,7 +1,4 @@
//go:build release
// +build release
// Copyright 2017-present The Hugo Authors. All rights reserved.
// Copyright 2023 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
@@ -17,55 +14,39 @@
package commands
import (
"github.com/gohugoio/hugo/config"
"context"
"github.com/bep/simplecobra"
"github.com/gohugoio/hugo/releaser"
"github.com/spf13/cobra"
)
var _ cmder = (*releaseCommandeer)(nil)
// Note: This is a command only meant for internal use and must be run
// via "go run -tags release main.go release" on the actual code base that is in the release.
func newReleaseCommand() simplecobra.Commander {
type releaseCommandeer struct {
cmd *cobra.Command
var (
step int
skipPush bool
try bool
)
step int
skipPush bool
try bool
}
return &simpleCommand{
name: "release",
short: "Release a new version of Hugo.",
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
rel, err := releaser.New(skipPush, try, step)
if err != nil {
return err
}
func createReleaser() cmder {
// Note: This is a command only meant for internal use and must be run
// via "go run -tags release main.go release" on the actual code base that is in the release.
r := &releaseCommandeer{
cmd: &cobra.Command{
Use: "release",
Short: "Release a new version of Hugo.",
Hidden: true,
return rel.Run()
},
withc: func(cmd *cobra.Command) {
cmd.Hidden = true
cmd.PersistentFlags().BoolVarP(&skipPush, "skip-push", "", false, "skip pushing to remote")
cmd.PersistentFlags().BoolVarP(&try, "try", "", false, "no changes")
cmd.PersistentFlags().IntVarP(&step, "step", "", 0, "step to run (1: set new version 2: prepare next dev version)")
},
}
r.cmd.RunE = func(cmd *cobra.Command, args []string) error {
return r.release()
}
r.cmd.PersistentFlags().BoolVarP(&r.skipPush, "skip-push", "", false, "skip pushing to remote")
r.cmd.PersistentFlags().BoolVarP(&r.try, "try", "", false, "no changes")
r.cmd.PersistentFlags().IntVarP(&r.step, "step", "", 0, "step to run (1: set new version 2: prepare next dev version)")
return r
}
func (c *releaseCommandeer) getCommand() *cobra.Command {
return c.cmd
}
func (c *releaseCommandeer) flagsToConfig(cfg config.Provider) {
}
func (r *releaseCommandeer) release() error {
rel, err := releaser.New(r.skipPush, r.try, r.step)
if err != nil {
return err
}
return rel.Run()
}

View File

@@ -1,21 +0,0 @@
//go:build !release
// +build !release
// Copyright 2018 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package commands
func createReleaser() cmder {
return &nilCommand{}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,31 +0,0 @@
// Copyright 2018 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package commands
import (
"bytes"
"io"
"net/url"
"github.com/gohugoio/hugo/transform"
"github.com/gohugoio/hugo/transform/livereloadinject"
)
func injectLiveReloadScript(src io.Reader, baseURL url.URL) string {
var b bytes.Buffer
chain := transform.Chain{livereloadinject.New(baseURL)}
chain.Apply(&b, src)
return b.String()
}

View File

@@ -1,429 +0,0 @@
// Copyright 2015 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package commands
import (
"context"
"fmt"
"net/http"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
"github.com/gohugoio/hugo/config"
"github.com/gohugoio/hugo/helpers"
"github.com/gohugoio/hugo/htesting"
"golang.org/x/sync/errgroup"
qt "github.com/frankban/quicktest"
)
// Issue 9518
func TestServerPanicOnConfigError(t *testing.T) {
c := qt.New(t)
config := `
[markup]
[markup.highlight]
linenos='table'
`
r := runServerTest(c,
serverTestOptions{
config: config,
},
)
c.Assert(r.err, qt.IsNotNil)
c.Assert(r.err.Error(), qt.Contains, "cannot parse 'Highlight.LineNos' as bool:")
}
func TestServer404(t *testing.T) {
c := qt.New(t)
r := runServerTest(c,
serverTestOptions{
pathsToGet: []string{"this/does/not/exist"},
getNumHomes: 1,
},
)
c.Assert(r.err, qt.IsNil)
pr := r.pathsResults["this/does/not/exist"]
c.Assert(pr.statusCode, qt.Equals, http.StatusNotFound)
c.Assert(pr.body, qt.Contains, "404: 404 Page not found|Not Found.")
}
func TestServerPathEncodingIssues(t *testing.T) {
c := qt.New(t)
// Issue 10287
c.Run("Unicode paths", func(c *qt.C) {
r := runServerTest(c,
serverTestOptions{
pathsToGet: []string{"hügö/"},
getNumHomes: 1,
},
)
c.Assert(r.err, qt.IsNil)
c.Assert(r.pathsResults["hügö/"].body, qt.Contains, "This is hügö")
})
// Issue 10314
c.Run("Windows multilingual 404", func(c *qt.C) {
config := `
baseURL = 'https://example.org/'
title = 'Hugo Forum Topic #40568'
defaultContentLanguageInSubdir = true
[languages.en]
contentDir = 'content/en'
languageCode = 'en-US'
languageName = 'English'
weight = 1
[languages.es]
contentDir = 'content/es'
languageCode = 'es-ES'
languageName = 'Espanol'
weight = 2
[server]
[[server.redirects]]
from = '/en/**'
to = '/en/404.html'
status = 404
[[server.redirects]]
from = '/es/**'
to = '/es/404.html'
status = 404
`
r := runServerTest(c,
serverTestOptions{
config: config,
pathsToGet: []string{"en/this/does/not/exist", "es/this/does/not/exist"},
getNumHomes: 1,
},
)
c.Assert(r.err, qt.IsNil)
pr1 := r.pathsResults["en/this/does/not/exist"]
pr2 := r.pathsResults["es/this/does/not/exist"]
c.Assert(pr1.statusCode, qt.Equals, http.StatusNotFound)
c.Assert(pr2.statusCode, qt.Equals, http.StatusNotFound)
c.Assert(pr1.body, qt.Contains, "404: 404 Page not found|Not Found.")
c.Assert(pr2.body, qt.Contains, "404: 404 Page not found|Not Found.")
})
}
func TestServerFlags(t *testing.T) {
c := qt.New(t)
assertPublic := func(c *qt.C, r serverTestResult, renderStaticToDisk bool) {
c.Assert(r.err, qt.IsNil)
c.Assert(r.homesContent[0], qt.Contains, "Environment: development")
c.Assert(r.publicDirnames["myfile.txt"], qt.Equals, renderStaticToDisk)
}
for _, test := range []struct {
flag string
assert func(c *qt.C, r serverTestResult)
}{
{"", func(c *qt.C, r serverTestResult) {
assertPublic(c, r, false)
}},
{"--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 := `
baseURL="https://example.org"
`
var args []string
if test.flag != "" {
args = strings.Split(test.flag, "=")
}
opts := serverTestOptions{
config: config,
args: args,
getNumHomes: 1,
}
r := runServerTest(c, opts)
test.assert(c, r)
})
}
}
func TestServerBugs(t *testing.T) {
// TODO(bep) this is flaky on Windows on GH Actions.
if htesting.IsGitHubAction() && runtime.GOOS == "windows" {
t.Skip("skipping on windows")
}
c := qt.New(t)
for _, test := range []struct {
name string
config string
flag string
numservers int
assert func(c *qt.C, r serverTestResult)
}{
{"PostProcess, memory", "", "", 1, func(c *qt.C, r serverTestResult) {
c.Assert(r.err, qt.IsNil)
c.Assert(r.homesContent[0], qt.Contains, "PostProcess: /foo.min.css")
}},
// Issue 9788
{"PostProcess, memory", "", "", 1, func(c *qt.C, r serverTestResult) {
c.Assert(r.err, qt.IsNil)
c.Assert(r.homesContent[0], qt.Contains, "PostProcess: /foo.min.css")
}},
{"PostProcess, disk", "", "--renderToDisk", 1, func(c *qt.C, r serverTestResult) {
c.Assert(r.err, qt.IsNil)
c.Assert(r.homesContent[0], qt.Contains, "PostProcess: /foo.min.css")
}},
// Issue 9901
{"Multihost", `
defaultContentLanguage = 'en'
[languages]
[languages.en]
baseURL = 'https://example.com'
title = 'My blog'
weight = 1
[languages.fr]
baseURL = 'https://example.fr'
title = 'Mon blogue'
weight = 2
`, "", 2, func(c *qt.C, r serverTestResult) {
c.Assert(r.err, qt.IsNil)
for i, s := range []string{"My blog", "Mon blogue"} {
c.Assert(r.homesContent[i], qt.Contains, s)
}
}},
} {
c.Run(test.name, func(c *qt.C) {
if test.config == "" {
test.config = `
baseURL="https://example.org"
`
}
var args []string
if test.flag != "" {
args = strings.Split(test.flag, "=")
}
opts := serverTestOptions{
config: test.config,
getNumHomes: test.numservers,
pathsToGet: []string{"this/does/not/exist"},
args: args,
}
r := runServerTest(c, opts)
pr := r.pathsResults["this/does/not/exist"]
c.Assert(pr.statusCode, qt.Equals, http.StatusNotFound)
c.Assert(pr.body, qt.Contains, "404: 404 Page not found|Not Found.")
test.assert(c, r)
})
}
}
type serverTestResult struct {
err error
homesContent []string
content404 string
publicDirnames map[string]bool
pathsResults map[string]pathResult
}
type pathResult struct {
statusCode int
body string
}
type serverTestOptions struct {
getNumHomes int
config string
pathsToGet []string
args []string
}
func runServerTest(c *qt.C, opts serverTestOptions) serverTestResult {
dir := createSimpleTestSite(c, testSiteConfig{configTOML: opts.config})
result := serverTestResult{
publicDirnames: make(map[string]bool),
pathsResults: make(map[string]pathResult),
}
sp, err := helpers.FindAvailablePort()
c.Assert(err, qt.IsNil)
port := sp.Port
defer func() {
os.RemoveAll(dir)
}()
stop := make(chan bool)
b := newCommandsBuilder()
scmd := b.newServerCmdSignaled(stop)
cmd := scmd.getCommand()
args := append([]string{"-s=" + dir, fmt.Sprintf("-p=%d", port)}, opts.args...)
cmd.SetArgs(args)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
wg, ctx := errgroup.WithContext(ctx)
wg.Go(func() error {
_, err := cmd.ExecuteC()
return err
})
if opts.getNumHomes > 0 {
// Esp. on slow CI machines, we need to wait a little before the web
// server is ready.
wait := 567 * time.Millisecond
if os.Getenv("CI") != "" {
wait = 2 * time.Second
}
time.Sleep(wait)
result.homesContent = make([]string, opts.getNumHomes)
for i := 0; i < opts.getNumHomes; i++ {
func() {
resp, err := http.Get(fmt.Sprintf("http://localhost:%d/", port+i))
c.Assert(err, qt.IsNil)
c.Assert(resp.StatusCode, qt.Equals, http.StatusOK)
if err == nil {
defer resp.Body.Close()
result.homesContent[i] = helpers.ReaderToString(resp.Body)
}
}()
}
}
for _, path := range opts.pathsToGet {
func() {
resp, err := http.Get(fmt.Sprintf("http://localhost:%d/%s", port, path))
c.Assert(err, qt.IsNil)
pr := pathResult{
statusCode: resp.StatusCode,
}
if err == nil {
defer resp.Body.Close()
pr.body = helpers.ReaderToString(resp.Body)
}
result.pathsResults[path] = pr
}()
}
time.Sleep(1 * time.Second)
select {
case <-stop:
case stop <- true:
}
pubFiles, err := os.ReadDir(filepath.Join(dir, "public"))
c.Assert(err, qt.IsNil)
for _, f := range pubFiles {
result.publicDirnames[f.Name()] = true
}
result.err = wg.Wait()
return result
}
func TestFixURL(t *testing.T) {
type data struct {
TestName string
CLIBaseURL string
CfgBaseURL string
AppendPort bool
Port int
Result string
}
tests := []data{
{"Basic http localhost", "", "http://foo.com", true, 1313, "http://localhost:1313/"},
{"Basic https production, http localhost", "", "https://foo.com", true, 1313, "http://localhost:1313/"},
{"Basic subdir", "", "http://foo.com/bar", true, 1313, "http://localhost:1313/bar/"},
{"Basic production", "http://foo.com", "http://foo.com", false, 80, "http://foo.com/"},
{"Production subdir", "http://foo.com/bar", "http://foo.com/bar", false, 80, "http://foo.com/bar/"},
{"No http", "", "foo.com", true, 1313, "//localhost:1313/"},
{"Override configured port", "", "foo.com:2020", true, 1313, "//localhost:1313/"},
{"No http production", "foo.com", "foo.com", false, 80, "//foo.com/"},
{"No http production with port", "foo.com", "foo.com", true, 2020, "//foo.com:2020/"},
{"No config", "", "", true, 1313, "//localhost:1313/"},
}
for _, test := range tests {
t.Run(test.TestName, func(t *testing.T) {
b := newCommandsBuilder()
s := b.newServerCmd()
v := config.NewWithTestDefaults()
baseURL := test.CLIBaseURL
v.Set("baseURL", test.CfgBaseURL)
s.serverAppend = test.AppendPort
s.serverPort = test.Port
result, err := s.fixURL(v, baseURL, s.serverPort)
if err != nil {
t.Errorf("Unexpected error %s", err)
}
if result != test.Result {
t.Errorf("Expected %q, got %q", test.Result, result)
}
})
}
}
func TestRemoveErrorPrefixFromLog(t *testing.T) {
c := qt.New(t)
content := `ERROR 2018/10/07 13:11:12 Error while rendering "home": template: _default/baseof.html:4:3: executing "main" at <partial "logo" .>: error calling partial: template: partials/logo.html:5:84: executing "partials/logo.html" at <$resized.AHeight>: can't evaluate field AHeight in type *resource.Image
ERROR 2018/10/07 13:11:12 Rebuild failed: logged 1 error(s)
`
withoutError := removeErrorPrefixFromLog(content)
c.Assert(strings.Contains(withoutError, "ERROR"), qt.Equals, false)
}
func isWindowsCI() bool {
return runtime.GOOS == "windows" && os.Getenv("CI") != ""
}

View File

@@ -1,129 +0,0 @@
// Copyright 2017 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package commands
import (
"path/filepath"
"github.com/gohugoio/hugo/common/herrors"
"github.com/gohugoio/hugo/hugolib/filesystems"
"github.com/fsnotify/fsnotify"
"github.com/gohugoio/hugo/helpers"
"github.com/spf13/fsync"
)
type staticSyncer struct {
c *commandeer
}
func newStaticSyncer(c *commandeer) (*staticSyncer, error) {
return &staticSyncer{c: c}, nil
}
func (s *staticSyncer) isStatic(filename string) bool {
return s.c.hugo().BaseFs.SourceFilesystems.IsStatic(filename)
}
func (s *staticSyncer) syncsStaticEvents(staticEvents []fsnotify.Event) error {
c := s.c
syncFn := func(sourceFs *filesystems.SourceFilesystem) (uint64, error) {
publishDir := helpers.FilePathSeparator
if sourceFs.PublishFolder != "" {
publishDir = filepath.Join(publishDir, sourceFs.PublishFolder)
}
syncer := fsync.NewSyncer()
syncer.NoTimes = c.Cfg.GetBool("noTimes")
syncer.NoChmod = c.Cfg.GetBool("noChmod")
syncer.ChmodFilter = chmodFilter
syncer.SrcFs = sourceFs.Fs
syncer.DestFs = c.Fs.PublishDir
if c.renderStaticToDisk {
syncer.DestFs = c.Fs.PublishDirStatic
}
// prevent spamming the log on changes
logger := helpers.NewDistinctErrorLogger()
for _, ev := range staticEvents {
// Due to our approach of layering both directories and the content's rendered output
// into one we can't accurately remove a file not in one of the source directories.
// If a file is in the local static dir and also in the theme static dir and we remove
// it from one of those locations we expect it to still exist in the destination
//
// If Hugo generates a file (from the content dir) over a static file
// the content generated file should take precedence.
//
// Because we are now watching and handling individual events it is possible that a static
// event that occupies the same path as a content generated file will take precedence
// until a regeneration of the content takes places.
//
// Hugo assumes that these cases are very rare and will permit this bad behavior
// The alternative is to track every single file and which pipeline rendered it
// and then to handle conflict resolution on every event.
fromPath := ev.Name
relPath, found := sourceFs.MakePathRelative(fromPath)
if !found {
// Not member of this virtual host.
continue
}
// Remove || rename is harder and will require an assumption.
// Hugo takes the following approach:
// If the static file exists in any of the static source directories after this event
// Hugo will re-sync it.
// If it does not exist in all of the static directories Hugo will remove it.
//
// This assumes that Hugo has not generated content on top of a static file and then removed
// the source of that static file. In this case Hugo will incorrectly remove that file
// from the published directory.
if ev.Op&fsnotify.Rename == fsnotify.Rename || ev.Op&fsnotify.Remove == fsnotify.Remove {
if _, err := sourceFs.Fs.Stat(relPath); herrors.IsNotExist(err) {
// If file doesn't exist in any static dir, remove it
logger.Println("File no longer exists in static dir, removing", relPath)
_ = c.Fs.PublishDirStatic.RemoveAll(relPath)
} else if err == nil {
// If file still exists, sync it
logger.Println("Syncing", relPath, "to", publishDir)
if err := syncer.Sync(relPath, relPath); err != nil {
c.logger.Errorln(err)
}
} else {
c.logger.Errorln(err)
}
continue
}
// For all other event operations Hugo will sync static.
logger.Println("Syncing", relPath, "to", publishDir)
if err := syncer.Sync(filepath.Join(publishDir, relPath), relPath); err != nil {
c.logger.Errorln(err)
}
}
return 0, nil
}
_, err := c.doWithPublishDirs(syncFn)
return err
}

View File

@@ -1,44 +0,0 @@
// Copyright 2015 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package commands
import (
"github.com/gohugoio/hugo/common/hugo"
"github.com/spf13/cobra"
jww "github.com/spf13/jwalterweatherman"
)
var _ cmder = (*versionCmd)(nil)
type versionCmd struct {
*baseCmd
}
func newVersionCmd() *versionCmd {
return &versionCmd{
newBaseCmd(&cobra.Command{
Use: "version",
Short: "Print the version number of Hugo",
Long: `All software has versions. This is Hugo's.`,
RunE: func(cmd *cobra.Command, args []string) error {
printHugoVersion()
return nil
},
}),
}
}
func printHugoVersion() {
jww.FEEDBACK.Println(hugo.BuildVersionString())
}

View File

@@ -0,0 +1,78 @@
// Copyright 2023 The Hugo Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package commands
import (
"context"
"fmt"
"github.com/bep/simplecobra"
"github.com/spf13/cobra"
)
func newSimpleTemplateCommand() simplecobra.Commander {
return &simpleCommand{
name: "template",
run: func(ctx context.Context, cd *simplecobra.Commandeer, r *rootCommand, args []string) error {
return nil
},
withc: func(cmd *cobra.Command) {
},
}
}
func newTemplateCommand() *templateCommand {
return &templateCommand{
commands: []simplecobra.Commander{},
}
}
type templateCommand struct {
r *rootCommand
commands []simplecobra.Commander
}
func (c *templateCommand) Commands() []simplecobra.Commander {
return c.commands
}
func (c *templateCommand) Name() string {
return "template"
}
func (c *templateCommand) Run(ctx context.Context, cd *simplecobra.Commandeer, args []string) error {
conf, err := c.r.ConfigFromProvider(c.r.configVersionID.Load(), flagsToCfg(cd, nil))
if err != nil {
return err
}
fmt.Println("templateCommand.Run", conf)
return nil
}
func (c *templateCommand) WithCobraCommand(cmd *cobra.Command) error {
cmd.Short = "Print the site configuration"
cmd.Long = `Print the site configuration, both default and custom settings.`
return nil
}
func (c *templateCommand) Init(cd, runner *simplecobra.Commandeer) error {
c.r = cd.Root.Command.(*rootCommand)
return nil
}