From: Joe Mooring Date: Fri, 17 Apr 2026 21:22:26 +0000 (-0700) Subject: create: Fix non-deterministic conflict detection in hugo new content X-Git-Url: http://git.maquefel.me/?a=commitdiff_plain;h=6436deb3e19c279c23aa9caf67b65e44d863b872;p=brevno-suite%2Fhugo create: Fix non-deterministic conflict detection in hugo new content 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 --- diff --git a/create/content.go b/create/content.go index 519346960..91a7f9462 100644 --- a/create/content.go +++ b/create/content.go @@ -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) diff --git a/testscripts/commands/new_content.txt b/testscripts/commands/new_content.txt index 3f8896b19..f432c8ace 100644 --- a/testscripts/commands/new_content.txt +++ b/testscripts/commands/new_content.txt @@ -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]