From ac2c3fb20fcd6ea517ccc74daacb87622e7b3a35 Mon Sep 17 00:00:00 2001 From: =?utf8?q?Bj=C3=B8rn=20Erik=20Pedersen?= Date: Fri, 23 Jan 2026 18:53:32 +0100 Subject: [PATCH] Allow v1,v2 etc. style version names while still supporting full semver in queries Fixes #14414 --- common/version/version.go | 22 +++++++-- common/version/version_test.go | 14 ++++++ .../sitematrix_integration_test.go | 47 +++++++++++++++++++ hugolib/versions/versions.go | 21 +++++++-- 4 files changed, 97 insertions(+), 7 deletions(-) diff --git a/common/version/version.go b/common/version/version.go index 83bd4435e..34ed9bfd1 100644 --- a/common/version/version.go +++ b/common/version/version.go @@ -37,6 +37,9 @@ type Version struct { // HugoVersionSuffix is the suffix used in the Hugo version string. // It will be blank for release versions. Suffix string + + // Set when parsed from a string, + source string } var ( @@ -51,6 +54,10 @@ func (v Version) IsAlphaBetaOrRC() bool { return strings.Contains(s, "alpha.") || strings.Contains(s, "beta.") || strings.Contains(s, "rc.") } +func (v Version) IsZero() bool { + return v.Major == 0 && v.Minor == 0 && v.PatchLevel == 0 && v.Suffix == "" +} + func (v Version) String() string { return version(v.Major, v.Minor, v.PatchLevel, v.Suffix) } @@ -109,6 +116,7 @@ func ParseVersion(s string) (Version, error) { vv.Suffix = suffix } vv.Major, vv.Minor, vv.PatchLevel = parseVersion(s) + vv.source = s return vv, nil } @@ -176,6 +184,10 @@ func CompareVersions(v1 Version, v2 any) int { case int64: c = compareFloatWithVersion(float64(d), v1) case Version: + if v1.IsZero() && d.IsZero() { + // Fall back to source comparison. + return strings.Compare(v1.source, d.source) + } if d.Major == v1.Major && d.Minor == v1.Minor && d.PatchLevel == v1.PatchLevel { return strings.Compare(v1.Suffix, d.Suffix) } @@ -200,11 +212,13 @@ func CompareVersions(v1 Version, v2 any) int { return -1 } - v, err := ParseVersion(s) - if err != nil { - return -1 + v2v, _ := ParseVersion(s) + if v1.IsZero() && v2v.IsZero() { + // Fall back to source comparison. + return strings.Compare(v1.source, v2v.source) } - return v1.Compare(v) + + return v1.Compare(v2v) } diff --git a/common/version/version_test.go b/common/version/version_test.go index f171e4d97..ed8d2a7cf 100644 --- a/common/version/version_test.go +++ b/common/version/version_test.go @@ -50,6 +50,11 @@ func TestHugoVersion(t *testing.T) { func TestCompareVersions(t *testing.T) { c := qt.New(t) + parseIgnoreErr := func(s string) Version { + v, _ := ParseVersion(s) + return v + } + c.Assert(CompareVersions(MustParseVersion("0.20.0"), 0.20), qt.Equals, 0) c.Assert(CompareVersions(MustParseVersion("0.20.0"), float32(0.20)), qt.Equals, 0) c.Assert(CompareVersions(MustParseVersion("0.20.0"), float64(0.20)), qt.Equals, 0) @@ -69,6 +74,15 @@ func TestCompareVersions(t *testing.T) { c.Assert(CompareVersions(MustParseVersion("0.22.0-DEV"), "0.22"), qt.Equals, 1) c.Assert(CompareVersions(MustParseVersion("0.22.1-DEV"), "0.22"), qt.Equals, -1) c.Assert(CompareVersions(MustParseVersion("0.22.1-DEV"), "0.22.1-DEV"), qt.Equals, 0) + + c.Assert(CompareVersions(parseIgnoreErr("foobar"), "v1.0.0"), qt.Equals, 1) + c.Assert(CompareVersions(parseIgnoreErr("v1.0.0"), "foobar"), qt.Equals, -1) + c.Assert(CompareVersions(parseIgnoreErr("foobar"), "foobar"), qt.Equals, 0) + c.Assert(CompareVersions(parseIgnoreErr("foobar"), parseIgnoreErr("foobar")), qt.Equals, 0) + c.Assert(CompareVersions(parseIgnoreErr("a"), "b"), qt.Equals, -1) + c.Assert(CompareVersions(parseIgnoreErr("a"), parseIgnoreErr("b")), qt.Equals, -1) + c.Assert(CompareVersions(parseIgnoreErr("b"), "a"), qt.Equals, 1) + c.Assert(CompareVersions(parseIgnoreErr("b"), parseIgnoreErr("a")), qt.Equals, 1) } func TestParseHugoVersion(t *testing.T) { diff --git a/hugolib/sitesmatrix/sitematrix_integration_test.go b/hugolib/sitesmatrix/sitematrix_integration_test.go index 8c2e77559..874936931 100644 --- a/hugolib/sitesmatrix/sitematrix_integration_test.go +++ b/hugolib/sitesmatrix/sitematrix_integration_test.go @@ -1590,3 +1590,50 @@ Title: {{ .Title }}| b.AssertFileContent("public/v1.0.0/p1/index.html", "Title: P1 v1|") b.AssertFileContent("public/v2.0.0/p1/index.html", "Title: P1 v1|") } + +func TestSitesMatrixFileMountThemeIssue14414(t *testing.T) { + t.Parallel() + + files := ` +-- hugo.toml -- + +disableKinds = ["taxonomy", "term", "rss", "sitemap"] +defaultContentVersion = "v1" +defaultContentVersionInSubDir = true + +[versions] +[versions."v1"] +[versions."v2"] + +[module] +[[module.mounts]] +source = 'content' +target = 'content' +[module.mounts.sites.matrix] +versions = '> v2.0.0' +[[module.imports]] +ignoreConfig = true +path = 'mytheme' +[[module.imports.mounts]] +source = 'content' +target = 'content' +[module.imports.mounts.sites.matrix] +versions = '<= v2.0.0' + +-- themes/mytheme/content/p1.md -- +--- +title: "P1 from theme" +--- +-- content/p1.md -- +--- +title: "P1 from site" +--- +-- layouts/all.html -- +Title: {{ .Title }}|Version: {{ .Site.Version.Name }}| + +` + + b := hugolib.Test(t, files) + + b.AssertFileContent("public/v1/p1/index.html", "Title: P1 from theme|Version: v1|") +} diff --git a/hugolib/versions/versions.go b/hugolib/versions/versions.go index ca372c23e..19ac181d2 100644 --- a/hugolib/versions/versions.go +++ b/hugolib/versions/versions.go @@ -64,6 +64,10 @@ type VersionInternal struct { // Name of the version. // This is the key from the config. Name string + + // Parsed version of Name. + parsedVersion version.Version + // Whether this version is the default version. // This will be by default rendered in the root. // There can only be one default version. @@ -104,6 +108,15 @@ func (r VersionsInternal) ResolveIndex(name string) int { return i } } + // Compare against parsed versions as well. + v, err := version.ParseVersion(name) + if err == nil { + for i, version := range r.Sorted { + if version.parsedVersion.Compare(v) == 0 { + return i + } + } + } return -1 } @@ -161,15 +174,17 @@ func (r *VersionsInternal) init(defaultContentVersion string) error { defaultSeen = true } - r.Sorted = append(r.Sorted, VersionInternal{Name: k, Default: isDefault, VersionConfig: v}) + // Ignore error, this may not be a semver version. + parsedVersion, _ := version.ParseVersion(k) + + r.Sorted = append(r.Sorted, VersionInternal{Name: k, parsedVersion: parsedVersion, Default: isDefault, VersionConfig: v}) } // Sort by weight if set, then semver descending. sort.SliceStable(r.Sorted, func(i, j int) bool { ri, rj := r.Sorted[i], r.Sorted[j] if ri.Weight == rj.Weight { - v1, v2 := version.MustParseVersion(ri.Name), version.MustParseVersion(rj.Name) - return v1.Compare(v2) < 0 + return ri.parsedVersion.Compare(rj.parsedVersion) < 0 } if rj.Weight == 0 { return true -- 2.39.5