]> git.maquefel.me Git - brevno-suite/hugo/commitdiff
create: Fix non-deterministic conflict detection in hugo new content
authorJoe Mooring <joe.mooring@veriphor.com>
Fri, 17 Apr 2026 21:22:26 +0000 (14:22 -0700)
committerBjørn Erik Pedersen <bjorn.erik.pedersen@gmail.com>
Sat, 18 Apr 2026 13:06:28 +0000 (15:06 +0200)
The contentInclusionFilter used strings.Contains to match filenames
against the target path. Because strings.Contains is a substring check,
a directory entry like "content/about" matches "content/about.md",
causing unrelated files to be pulled into the mini-build. Whether the
conflict was then detected depended on whether the filesystem walker
delivered a directory entry or a full file path.

Also adds an upfront check for the directory-conflict case, since
a corrected filter alone would allow about.md to be created alongside
an existing about/ directory.

Closes #12602
Closes #12786
Closes #14112
Closes #14769

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
create/content.go
testscripts/commands/new_content.txt

index 519346960a5c68356cf8c890ab0b44029c05be82..91a7f94627faf394df74b4f56b2c08af459dc0d8 100644 (file)
@@ -162,9 +162,8 @@ func (b *contentBuilder) buildDir() error {
        if !b.dirMap.siteUsed {
                // We don't need to build everything.
                contentInclusionFilter = hglob.NewFilenameFilterForInclusionFunc(func(filename string) bool {
-                       filename = strings.TrimPrefix(filename, string(os.PathSeparator))
                        for _, cn := range contentTargetFilenames {
-                               if strings.Contains(cn, filename) {
+                               if strings.HasSuffix(cn, filename) {
                                        return true
                                }
                        }
@@ -219,6 +218,9 @@ func (b *contentBuilder) buildDir() error {
 func (b *contentBuilder) buildFile() (string, error) {
        contentPlaceholderAbsFilename, err := b.cf.CreateContentPlaceHolder(b.targetPath, b.force)
        if err != nil {
+               if fi, serr := b.sourceFs.Stat(contentPlaceholderAbsFilename); serr == nil && !fi.IsDir() {
+                       return "", errTargetConflict(contentPlaceholderAbsFilename)
+               }
                return "", err
        }
 
@@ -231,11 +233,17 @@ func (b *contentBuilder) buildFile() (string, error) {
        if !usesSite {
                // We don't need to build everything.
                contentInclusionFilter = hglob.NewFilenameFilterForInclusionFunc(func(filename string) bool {
-                       filename = strings.TrimPrefix(filename, string(os.PathSeparator))
-                       return strings.Contains(contentPlaceholderAbsFilename, filename)
+                       return strings.HasSuffix(contentPlaceholderAbsFilename, filename)
                })
        }
 
+       // If a directory with the target's name (sans extension) exists, this file
+       // would produce a URL conflict with the existing section or leaf bundle.
+       targetDir := strings.TrimSuffix(contentPlaceholderAbsFilename, filepath.Ext(contentPlaceholderAbsFilename))
+       if fi, err := b.sourceFs.Stat(targetDir); err == nil && fi.IsDir() {
+               return "", errTargetConflict(contentPlaceholderAbsFilename)
+       }
+
        if err := b.h.Build(hugolib.BuildCfg{NoBuildLock: true, SkipRender: true, ContentInclusionFilter: contentInclusionFilter}); err != nil {
                return "", err
        }
@@ -268,10 +276,14 @@ func (b *contentBuilder) setArcheTypeFilenameToUse(ext string) {
        }
 }
 
+func errTargetConflict(path string) error {
+       return fmt.Errorf("no page found for %q; the target path conflicts with existing content", path)
+}
+
 func (b *contentBuilder) applyArcheType(contentFilename string, archetypeFi hugofs.FileMetaInfo) error {
        p := b.h.GetContentPage(contentFilename)
        if p == nil {
-               return fmt.Errorf("no page found for %q; if a file with the same name but different case already exists, please use a different filename or remove the existing file", contentFilename)
+               return errTargetConflict(contentFilename)
        }
 
        f, err := b.sourceFs.Create(contentFilename)
index 3f8896b19fa4b154aac81f157a2aa109b764a731..f432c8acec55cfae36c9a289357a3c6b108b1353 100644 (file)
@@ -5,8 +5,8 @@ hugo new content --kind post post/first-post.md
 grep 'draft = true' content/post/first-post.md
 
 # Issue 12599
-cd $WORK
 
+cd $WORK
 hugo new project --format toml --force issue-12599
 cp hugo.toml issue-12599/hugo.toml
 cd issue-12599
@@ -16,6 +16,33 @@ grep 'DATE _2099-12-31_' public/s1/p1/index.html
 grep 'SLUG _p1_' public/s1/p1/index.html
 grep 'TITLE _2099 12 31 P1_' public/s1/p1/index.html
 
+# Issue 12602
+
+cd $WORK
+hugo new project issue-12602
+cd issue-12602
+hugo new content about/_index.md
+! hugo new content about.md
+stderr 'the target path conflicts with existing content'
+
+# Issue 12786
+
+cd $WORK
+hugo new project issue-12786
+cd issue-12786
+hugo new content content/p1/index.md
+! hugo new content content/p1/p2.md
+stderr 'the target path conflicts with existing content'
+
+# Issue 14112
+
+cd $WORK
+hugo new project issue-14112
+cd issue-14112
+hugo new content P1.md
+! hugo new content p1.md
+stderr 'the target path conflicts with existing content'
+
 -- hugo.toml --
 disableKinds = ['home','rss','section','sitemap','taxonomy','term']
 [frontmatter]