Repository: gohugoio/hugo (language: go)

Issue:
force flag for hugo new (not hugo new site)
We already have `--force` for `hugo new site` but it seems this flag is not available for `hugo new` to create content.

I have looked at all the issues there and I can not see any relevant issue or PR:
https://github.com/gohugoio/hugo/search?p=15&q=force&type=issues

I have the following usecase:

A specific content type as template. `ENV_VAR=value hugo new my-new-file-name -k my-template-type` to populate the frontmatter with custom keys and values and create the new file.

But if I want to update the content in a specific environment I want to run `ENV_VAR=other-value hugo new my-new-file-name -k my-template-type` to recreate it for compilation.

And if my template-file changes, then I want to just run it with the old `ENV_VAR=value hugo new my-new-file-name -k my-template-type` but get the new template in the resulting file.

Currently this does not work as Hugo says "file-name already exists".
Even the `--force` option for sites does not overwrite files but only folders. See https://github.com/gohugoio/hugo/blob/master/commands/new_site.go#L89 (seems to only add new files but not overwrite existing files).

Maybe such an option to overwrite files should be added for the site command too.

My current workaround is to do a `rm file-name` and then run `ENV_VAR=other-value hugo new my-new-file-name -k my-template-type` again.


Discussion / hints:
Yes, a way to overwrite existing files with `hugo new` would be very useful and it has been bugging me also [for a while](https://discourse.gohugo.io/t/hugo-new-how-to-overwrite-existing-content-files/24971).

Fix patch:
diff --git a/commands/new.go b/commands/new.go
index c5b5cd18271..a6c2c8ca1ca 100644
--- a/commands/new.go
+++ b/commands/new.go
@@ -32,6 +32,7 @@ var _ cmder = (*newCmd)(nil)
 type newCmd struct {
 	contentEditor string
 	contentType   string
+	force         bool
 
 	*baseBuilderCmd
 }
@@ -54,6 +55,7 @@ Ensure you run this within the root directory of your site.`,
 
 	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())
@@ -80,7 +82,7 @@ func (n *newCmd) newContent(cmd *cobra.Command, args []string) error {
 		return newUserError("path needs to be provided")
 	}
 
-	return create.NewContent(c.hugo(), n.contentType, args[0])
+	return create.NewContent(c.hugo(), n.contentType, args[0], n.force)
 }
 
 func mkdir(x ...string) {
diff --git a/create/content.go b/create/content.go
index 5f05e25f9d9..0b9f277970c 100644
--- a/create/content.go
+++ b/create/content.go
@@ -52,7 +52,7 @@ draft: true
 
 // NewContent creates a new content file in h (or a full bundle if the archetype is a directory)
 // in targetPath.
-func NewContent(h *hugolib.HugoSites, kind, targetPath string) error {
+func NewContent(h *hugolib.HugoSites, kind, targetPath string, force bool) error {
 	if h.BaseFs.Content.Dirs == nil {
 		return errors.New("no existing content directory configured for this project")
 	}
@@ -76,6 +76,7 @@ func NewContent(h *hugolib.HugoSites, kind, targetPath string) error {
 
 		kind:       kind,
 		targetPath: targetPath,
+		force:      force,
 	}
 
 	ext := paths.Ext(targetPath)
@@ -132,6 +133,7 @@ type contentBuilder struct {
 	kind              string
 	isDir             bool
 	dirMap            archetypeMap
+	force             bool
 }
 
 func (b *contentBuilder) buildDir() error {
@@ -145,7 +147,7 @@ func (b *contentBuilder) buildDir() error {
 
 	for _, fi := range b.dirMap.contentFiles {
 		targetFilename := filepath.Join(b.targetPath, strings.TrimPrefix(fi.Meta().Path, b.archetypeFilename))
-		abs, err := b.cf.CreateContentPlaceHolder(targetFilename)
+		abs, err := b.cf.CreateContentPlaceHolder(targetFilename, b.force)
 		if err != nil {
 			return err
 		}
@@ -218,7 +220,7 @@ func (b *contentBuilder) buildDir() error {
 }
 
 func (b *contentBuilder) buildFile() (string, error) {
-	contentPlaceholderAbsFilename, err := b.cf.CreateContentPlaceHolder(b.targetPath)
+	contentPlaceholderAbsFilename, err := b.cf.CreateContentPlaceHolder(b.targetPath, b.force)
 	if err != nil {
 		return "", err
 	}
diff --git a/hugolib/content_factory.go b/hugolib/content_factory.go
index 0a4d0aa0aa8..017a0bc979c 100644
--- a/hugolib/content_factory.go
+++ b/hugolib/content_factory.go
@@ -110,7 +110,7 @@ func (f ContentFactory) SectionFromFilename(filename string) (string, error) {
 
 // CreateContentPlaceHolder creates a content placeholder file inside the
 // best matching content directory.
-func (f ContentFactory) CreateContentPlaceHolder(filename string) (string, error) {
+func (f ContentFactory) CreateContentPlaceHolder(filename string, force bool) (string, error) {
 	filename = filepath.Clean(filename)
 	_, abs, err := f.h.AbsProjectContentDir(filename)
 
@@ -130,6 +130,9 @@ _build:
 
 `
 
+	if force {
+		return abs, afero.WriteReader(f.h.Fs.Source, abs, strings.NewReader(placeholder))
+	}
 	return abs, afero.SafeWriteReader(f.h.Fs.Source, abs, strings.NewReader(placeholder))
 }
 


Test patch:
diff --git a/create/content_test.go b/create/content_test.go
index 80a66609367..fdfee6e68c4 100644
--- a/create/content_test.go
+++ b/create/content_test.go
@@ -82,7 +82,7 @@ func TestNewContentFromFile(t *testing.T) {
 			cfg, fs := newTestCfg(c, mm)
 			h, err := hugolib.NewHugoSites(deps.DepsCfg{Cfg: cfg, Fs: fs})
 			c.Assert(err, qt.IsNil)
-			err = create.NewContent(h, cas.kind, cas.path)
+			err = create.NewContent(h, cas.kind, cas.path, false)
 
 			if b, ok := cas.expected.(bool); ok && !b {
 				if !b {
@@ -145,7 +145,7 @@ i18n: {{ T "hugo" }}
 	c.Assert(err, qt.IsNil)
 	c.Assert(len(h.Sites), qt.Equals, 2)
 
-	c.Assert(create.NewContent(h, "my-bundle", "post/my-post"), qt.IsNil)
+	c.Assert(create.NewContent(h, "my-bundle", "post/my-post", false), qt.IsNil)
 
 	cContains(c, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-post/resources/hugo1.json")), `hugo1: {{ printf "no template handling in here" }}`)
 	cContains(c, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-post/resources/hugo2.xml")), `hugo2: {{ printf "no template handling in here" }}`)
@@ -157,7 +157,7 @@ i18n: {{ T "hugo" }}
 
 	cContains(c, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-post/pages/bio.md")), `File: bio.md`, `Site Lang: en`, `Name: Bio`)
 
-	c.Assert(create.NewContent(h, "my-theme-bundle", "post/my-theme-post"), qt.IsNil)
+	c.Assert(create.NewContent(h, "my-theme-bundle", "post/my-theme-post", false), qt.IsNil)
 	cContains(c, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-theme-post/index.md")), `File: index.md`, `Site Lang: en`, `Name: My Theme Post`, `i18n: Hugo Rocks!`)
 	cContains(c, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-theme-post/resources/hugo1.json")), `hugo1: {{ printf "no template handling in here" }}`)
 }
@@ -187,19 +187,19 @@ site RegularPages: {{ len site.RegularPages  }}
 	c.Assert(err, qt.IsNil)
 	c.Assert(len(h.Sites), qt.Equals, 2)
 
-	c.Assert(create.NewContent(h, "my-bundle", "post/my-post"), qt.IsNil)
+	c.Assert(create.NewContent(h, "my-bundle", "post/my-post", false), qt.IsNil)
 	cContains(c, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-post/index.md")), `site RegularPages: 10`)
 
 	// Default bundle archetype
-	c.Assert(create.NewContent(h, "", "post/my-post2"), qt.IsNil)
+	c.Assert(create.NewContent(h, "", "post/my-post2", false), qt.IsNil)
 	cContains(c, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-post2/index.md")), `default archetype index.md`)
 
 	// Regular file with bundle kind.
-	c.Assert(create.NewContent(h, "my-bundle", "post/foo.md"), qt.IsNil)
+	c.Assert(create.NewContent(h, "my-bundle", "post/foo.md", false), qt.IsNil)
 	cContains(c, readFileFromFs(t, fs.Source, filepath.Join("content", "post/foo.md")), `draft: true`)
 
 	// Regular files should fall back to the default archetype (we have no regular file archetype).
-	c.Assert(create.NewContent(h, "my-bundle", "mypage.md"), qt.IsNil)
+	c.Assert(create.NewContent(h, "my-bundle", "mypage.md", false), qt.IsNil)
 	cContains(c, readFileFromFs(t, fs.Source, filepath.Join("content", "mypage.md")), `draft: true`)
 
 }
@@ -237,7 +237,7 @@ i18n: {{ T "hugo" }}
 	c.Assert(err, qt.IsNil)
 	c.Assert(len(h.Sites), qt.Equals, 2)
 
-	c.Assert(create.NewContent(h, "my-bundle", "post/my-post"), qt.IsNil)
+	c.Assert(create.NewContent(h, "my-bundle", "post/my-post", false), qt.IsNil)
 
 	cContains(c, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-post/resources/hugo1.json")), `hugo1: {{ printf "no template handling in here" }}`)
 	cContains(c, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-post/resources/hugo2.xml")), `hugo2: {{ printf "no template handling in here" }}`)
@@ -247,11 +247,38 @@ i18n: {{ T "hugo" }}
 
 	cContains(c, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-post/pages/bio.md")), `File: bio.md`, `Name: Bio`)
 
-	c.Assert(create.NewContent(h, "my-theme-bundle", "post/my-theme-post"), qt.IsNil)
+	c.Assert(create.NewContent(h, "my-theme-bundle", "post/my-theme-post", false), qt.IsNil)
 	cContains(c, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-theme-post/index.md")), `File: index.md`, `Name: My Theme Post`, `i18n: Hugo Rocks!`)
 	cContains(c, readFileFromFs(t, fs.Source, filepath.Join("content", "post/my-theme-post/resources/hugo1.json")), `hugo1: {{ printf "no template handling in here" }}`)
 }
 
+func TestNewContentForce(t *testing.T) {
+	mm := afero.NewMemMapFs()
+	c := qt.New(t)
+
+	archetypeDir := filepath.Join("archetypes", "my-bundle")
+	c.Assert(mm.MkdirAll(archetypeDir, 0o755), qt.IsNil)
+	c.Assert(afero.WriteFile(mm, filepath.Join(archetypeDir, "index.md"), []byte(""), 0o755), qt.IsNil)
+	c.Assert(afero.WriteFile(mm, filepath.Join(archetypeDir, "index.nn.md"), []byte(""), 0o755), qt.IsNil)
+
+	c.Assert(initFs(mm), qt.IsNil)
+	cfg, fs := newTestCfg(c, mm)
+
+	h, err := hugolib.NewHugoSites(deps.DepsCfg{Cfg: cfg, Fs: fs})
+	c.Assert(err, qt.IsNil)
+	c.Assert(len(h.Sites), qt.Equals, 2)
+
+	// from file
+	c.Assert(create.NewContent(h, "post", "post/my-post.md", false), qt.IsNil)
+	c.Assert(create.NewContent(h, "post", "post/my-post.md", false), qt.IsNotNil)
+	c.Assert(create.NewContent(h, "post", "post/my-post.md", true), qt.IsNil)
+
+	// from dir
+	c.Assert(create.NewContent(h, "my-bundle", "post/my-post", false), qt.IsNil)
+	c.Assert(create.NewContent(h, "my-bundle", "post/my-post", false), qt.IsNotNil)
+	c.Assert(create.NewContent(h, "my-bundle", "post/my-post", true), qt.IsNil)
+}
+
 func initFs(fs afero.Fs) error {
 	perm := os.FileMode(0o755)
 	var err error
diff --git a/hugolib/content_factory_test.go b/hugolib/content_factory_test.go
index 23dcd660ac4..2c4b843a92f 100644
--- a/hugolib/content_factory_test.go
+++ b/hugolib/content_factory_test.go
@@ -43,7 +43,7 @@ Hello World.
 `)
 		b.CreateSites()
 		cf := NewContentFactory(b.H)
-		abs, err := cf.CreateContentPlaceHolder(filepath.FromSlash("mcontent/en/blog/mypage.md"))
+		abs, err := cf.CreateContentPlaceHolder(filepath.FromSlash("mcontent/en/blog/mypage.md"), false)
 		b.Assert(err, qt.IsNil)
 		b.Assert(abs, qt.Equals, filepath.FromSlash("/my/work/mcontent/en/blog/mypage.md"))
 		b.Build(BuildCfg{SkipRender: true})
@@ -69,7 +69,7 @@ theme = 'ipsum'
 		b.WithSourceFile(filepath.Join(themeDir, "content/posts/foo.txt"), `Hello.`)
 		b.CreateSites()
 		cf := NewContentFactory(b.H)
-		abs, err := cf.CreateContentPlaceHolder(filepath.FromSlash("posts/test.md"))
+		abs, err := cf.CreateContentPlaceHolder(filepath.FromSlash("posts/test.md"), false)
 		b.Assert(err, qt.IsNil)
 		b.Assert(abs, qt.Equals, filepath.FromSlash("content/posts/test.md"))