From 06672c8df699d765766732ee27a7f89ff4a35927 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Capelle?= Date: Thu, 18 Jul 2024 09:18:03 +0200 Subject: [PATCH] Refactor SemVer approach for more flexibility. --- .gitignore | 1 + include/uibase/imoinfo.h | 2 +- include/uibase/{version.h => versioning.h} | 77 +++++++++++++++++-- src/CMakeLists.txt | 6 +- src/uibase_en.ts | 3 +- src/{version.cpp => versioning.cpp} | 86 ++++++++++++++-------- tests/CMakeLists.txt | 1 + tests/test_versioning.cpp | 62 ++++++++-------- 8 files changed, 167 insertions(+), 71 deletions(-) rename include/uibase/{version.h => versioning.h} (52%) rename src/{version.cpp => versioning.cpp} (72%) diff --git a/.gitignore b/.gitignore index bcdbd6a5..a55b4baf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ edit +.vscode CMakeLists.txt.user /msbuild.log /*std*.log diff --git a/include/uibase/imoinfo.h b/include/uibase/imoinfo.h index 7999e843..bf6588d5 100644 --- a/include/uibase/imoinfo.h +++ b/include/uibase/imoinfo.h @@ -34,8 +34,8 @@ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA #include "guessedvalue.h" #include "imodlist.h" #include "iprofile.h" -#include "version.h" #include "versioninfo.h" +#include "versioning.h" namespace MOBase { diff --git a/include/uibase/version.h b/include/uibase/versioning.h similarity index 52% rename from include/uibase/version.h rename to include/uibase/versioning.h index 815e817a..b8fbfcc8 100644 --- a/include/uibase/version.h +++ b/include/uibase/versioning.h @@ -5,6 +5,9 @@ #include #include +#include +#include + #include "dllimport.h" #include "exceptions.h" @@ -17,7 +20,24 @@ class InvalidVersionException : public Exception using Exception::Exception; }; -// class representing a SemVer object, see https://semver.org/ +// class representing a Version object +// +// valid versions are an "extension" of SemVer (see https://semver.org/) with the +// following tweaks: +// - version can have a sub-patch, i.e., x.y.z.p, which are normally not allowed by +// SemVer +// - non-integer pre-release identifiers are limited to dev, alpha (a), beta (b) and rc, +// and dev is lower than alpha (according to SemVer, the pre-release should be +// ordered alphabetically) +// - the '-' between version and pre-release can be made optional, and also the '.' +// between pre-releases segment +// +// the extension from SemVer are only meant to be used by MO2 and USVFS versioning, +// plugins and extensions should follow SemVer standard (and not use dev), this is +// mainly +// - for back-compatibility purposes, because USVFS versioning contains sub-patches and +// there are old MO2 releases with sub-patch +// - because MO2 is not going to become MO3, so having an extra level make sense // // unlike VersionInfo, this class is immutable and only hold valid versions // @@ -26,7 +46,7 @@ class QDLLEXPORT Version public: enum class ParseMode { - // official semver parsing + // official semver parsing with pre-release limited to dev, alpha/a, beta/b and rc // SemVer, @@ -34,9 +54,39 @@ class QDLLEXPORT Version // information (e.g. 2.5.1) or with a single pre-release + a version (e.g., 2.5.1a1 // or 2.5.2rc1) // + // this mode can parse sub-patch (SemVer mode cannot) + // MO2 }; + enum class FormatMode + { + // show subpatch even if subpatch is 0 + // + ForceSubPatch = 0b0001, + + // add '-' between version and pre-release and '.' between pre-releases + // + Separator = 0b0010, + + // uses short form for alpha and beta (a/b instead of alpha/beta) + // + ShortAlphaBeta = 0b0100, + + // add metadata + // + Metadata = 0b1000 + }; + Q_DECLARE_FLAGS(FormatModes, FormatMode); + + // default format mode, equivalent to Separator | Metadata + // + static constexpr auto FormatDefault = + FormatModes{FormatMode::Separator, FormatMode::Metadata}; + + // + static constexpr auto FormatCondensed = FormatModes{FormatMode::ShortAlphaBeta}; + enum class ReleaseType { Development, // -dev @@ -54,10 +104,18 @@ class QDLLEXPORT Version public: // constructor Version(int major, int minor, int patch, QString metadata = {}); + Version(int major, int minor, int patch, int subpatch, QString metadata = {}); + Version(int major, int minor, int patch, ReleaseType type, QString metadata = {}); + Version(int major, int minor, int patch, int subpatch, ReleaseType type, + QString metadata = {}); + Version(int major, int minor, int patch, ReleaseType type, int prerelease, QString metadata = {}); - Version(int major, int minor, int patch, + Version(int major, int minor, int patch, int subpatch, ReleaseType type, + int prerelease, QString metadata = {}); + + Version(int major, int minor, int patch, int subpatch, std::vector> prereleases, QString metadata = {}); @@ -73,11 +131,12 @@ class QDLLEXPORT Version // bool isPreRelease() const { return !m_PreReleases.empty(); } - // retrieve major, minor and patch of this version + // retrieve major, minor, patch and sub-patch of this version // int major() const { return m_Major; } int minor() const { return m_Minor; } int patch() const { return m_Patch; } + int subpatch() const { return m_SubPatch; } // retrieve pre-releases information for this version // @@ -87,13 +146,15 @@ class QDLLEXPORT Version // const auto& buildMetadata() const { return m_BuildMetadata; } - // convert this version to a semver string + // convert this version to a semver string, the no-argument version of string() + // is equivalent to string(FormatMode::Separator | FormatMode::Metadata) // QString string() const; + QString string(const FormatModes& modes) const; private: // major.minor.patch - int m_Major, m_Minor, m_Patch; + int m_Major, m_Minor, m_Patch, m_SubPatch; // pre-release information std::vector> m_PreReleases; @@ -109,6 +170,8 @@ inline bool operator==(const Version& lhs, const Version& rhs) return (lhs <=> rhs) == 0; } +Q_DECLARE_OPERATORS_FOR_FLAGS(Version::FormatModes); + } // namespace MOBase template @@ -119,4 +182,4 @@ struct std::formatter : std::formatter { return std::formatter::format(v.string(), ctx); } -}; \ No newline at end of file +}; diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 3bd36c2c..c3010d6a 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -30,7 +30,7 @@ set(root_headers ../include/uibase/scopeguard.h ../include/uibase/steamutility.h ../include/uibase/utility.h - ../include/uibase/version.h + ../include/uibase/versioning.h ../include/uibase/versioninfo.h ) set(interface_headers @@ -116,14 +116,13 @@ mo2_target_sources(uibase nxmurl.cpp pch.cpp pluginrequirements.cpp - pluginsetting.cpp registry.cpp report.cpp safewritefile.cpp scopeguard.cpp steamutility.cpp utility.cpp - version.cpp + versioning.cpp versioninfo.cpp ) @@ -134,7 +133,6 @@ mo2_target_sources(uibase ifiletree.cpp imodrepositorybridge.cpp imoinfo.cpp - iplugininstaller.cpp ) mo2_target_sources(uibase diff --git a/src/uibase_en.ts b/src/uibase_en.ts index 3d397cee..7f02320f 100644 --- a/src/uibase_en.ts +++ b/src/uibase_en.ts @@ -180,7 +180,8 @@ - Failed to save '{}', could not create a temporary file: {} (error {}) + Failed to save '%1', could not create a temporary file: %2 (error %3) + Failed to save '{}', could not create a temporary file: {} (error {}) diff --git a/src/version.cpp b/src/versioning.cpp similarity index 72% rename from src/version.cpp rename to src/versioning.cpp index 904583d6..2f9f1132 100644 --- a/src/version.cpp +++ b/src/versioning.cpp @@ -1,4 +1,4 @@ -#include "version.h" +#include "versioning.h" #include #include @@ -16,11 +16,12 @@ static const QRegularExpression s_SemVerMO2RegEx{ // match from value to release type static const std::unordered_map - s_StringToRelease{ - {"dev", MOBase::Version::Development}, {"alpha", MOBase::Version::Alpha}, - {"alpha", MOBase::Version::Alpha}, {"a", MOBase::Version::Alpha}, - {"beta", MOBase::Version::Beta}, {"b", MOBase::Version::Beta}, - {"rc", MOBase::Version::ReleaseCandidate}}; + s_StringToRelease{{"dev", MOBase::Version::Development}, + {"alpha", MOBase::Version::Alpha}, + {"a", MOBase::Version::Alpha}, + {"beta", MOBase::Version::Beta}, + {"b", MOBase::Version::Beta}, + {"rc", MOBase::Version::ReleaseCandidate}}; namespace MOBase { @@ -64,7 +65,7 @@ namespace const auto buildMetadata = match.captured("buildmetadata").trimmed(); - return Version(major, minor, patch, prereleases, buildMetadata); + return Version(major, minor, patch, 0, prereleases, buildMetadata); } Version parseVersionMO2(QString const& value) @@ -80,12 +81,10 @@ namespace const auto minor = match.captured("minor").toInt(); const auto patch = match.captured("patch").toInt(); - std::vector> prereleases; - if (match.hasCaptured("subpatch")) { - prereleases.push_back(match.captured("subpatch").toInt()); - } + const auto subpatch = match.captured("subpatch").toInt(); // unlike semver, the regex will only match valid values + std::vector> prereleases; if (match.hasCaptured("type")) { prereleases.push_back(s_StringToRelease.at(match.captured("type"))); @@ -101,7 +100,7 @@ namespace const auto buildMetadata = match.captured("buildmetadata").trimmed(); - return Version(major, minor, patch, prereleases, buildMetadata); + return Version(major, minor, patch, subpatch, prereleases, buildMetadata); } } // namespace @@ -111,45 +110,73 @@ Version Version::parse(QString const& value, ParseMode mode) return mode == ParseMode::SemVer ? parseVersionSemVer(value) : parseVersionMO2(value); } +// constructors + Version::Version(int major, int minor, int patch, QString metadata) - : Version(major, minor, patch, std::vector>{}, - std::move(metadata)) + : Version(major, minor, patch, 0, std::move(metadata)) +{} +Version::Version(int major, int minor, int patch, int subpatch, QString metadata) + : m_Major{major}, m_Minor{minor}, m_Patch{patch}, m_SubPatch{subpatch}, + m_PreReleases{}, m_BuildMetadata{std::move(metadata)} {} Version::Version(int major, int minor, int patch, ReleaseType type, QString metadata) - : Version(major, minor, patch, std::vector>{type}, - std::move(metadata)) + : Version(major, minor, patch, 0, type, std::move(metadata)) +{} +Version::Version(int major, int minor, int patch, int subpatch, ReleaseType type, + QString metadata) + : m_Major{major}, m_Minor{minor}, m_Patch{patch}, m_SubPatch{subpatch}, + m_PreReleases{type}, m_BuildMetadata{std::move(metadata)} {} Version::Version(int major, int minor, int patch, ReleaseType type, int prerelease, QString metadata) - : Version(major, minor, patch, {type, prerelease}, std::move(metadata)) + : Version(major, minor, patch, 0, type, prerelease, std::move(metadata)) +{} +Version::Version(int major, int minor, int patch, int subpatch, ReleaseType type, + int prerelease, QString metadata) + : Version(major, minor, patch, subpatch, {type, prerelease}, std::move(metadata)) {} -Version::Version(int major, int minor, int patch, +Version::Version(int major, int minor, int patch, int subpatch, std::vector> prereleases, QString metadata) - : m_Major{major}, m_Minor{minor}, m_Patch{patch}, + : m_Major{major}, m_Minor{minor}, m_Patch{patch}, m_SubPatch{subpatch}, m_PreReleases{std::move(prereleases)}, m_BuildMetadata{std::move(metadata)} {} +// string + QString Version::string() const { - auto value = std::format("{}.{}.{}", m_Major, m_Minor, m_Patch); + return string(FormatMode::Separator | FormatMode::Metadata); +} + +QString Version::string(const FormatModes& modes) const +{ + const bool withSeparator = modes.testFlag(FormatMode::Separator); + const bool shortAlphaBeta = modes.testFlag(FormatMode::ShortAlphaBeta); + auto value = std::format("{}.{}.{}", m_Major, m_Minor, m_Patch); + + if (m_SubPatch || modes.testFlag(FormatMode::ForceSubPatch)) { + value += std::format(".{}", m_SubPatch); + } if (!m_PreReleases.empty()) { - value += "-"; + if (withSeparator) { + value += "-"; + } for (std::size_t i = 0; i < m_PreReleases.size(); ++i) { value += std::visit( - [](auto const& pre) -> std::string { + [shortAlphaBeta](auto const& pre) -> std::string { if constexpr (std::is_same_v) { switch (pre) { case Development: return "dev"; case Alpha: - return "alpha"; + return shortAlphaBeta ? "a" : "alpha"; case Beta: - return "beta"; + return shortAlphaBeta ? "b" : "beta"; case ReleaseCandidate: return "rc"; } @@ -159,13 +186,13 @@ QString Version::string() const } }, m_PreReleases[i]); - if (i < m_PreReleases.size() - 1) { + if (withSeparator && i < m_PreReleases.size() - 1) { value += "."; } } } - if (!m_BuildMetadata.isEmpty()) { + if (modes.testFlag(FormatMode::Metadata) && !m_BuildMetadata.isEmpty()) { value += "+" + m_BuildMetadata.toStdString(); } @@ -191,8 +218,9 @@ namespace std::strong_ordering operator<=>(const Version& lhs, const Version& rhs) { - auto mmp_cmp = std::forward_as_tuple(lhs.major(), lhs.minor(), lhs.patch()) <=> - std::forward_as_tuple(rhs.major(), rhs.minor(), rhs.patch()); + auto mmp_cmp = + std::forward_as_tuple(lhs.major(), lhs.minor(), lhs.patch(), lhs.subpatch()) <=> + std::forward_as_tuple(rhs.major(), rhs.minor(), rhs.patch(), rhs.subpatch()); // major.minor.patch have precedence over everything else if (mmp_cmp != std::strong_ordering::equal) { @@ -252,4 +280,4 @@ std::strong_ordering operator<=>(const Version& lhs, const Version& rhs) } } -} // namespace MOBase \ No newline at end of file +} // namespace MOBase diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index aed3adec..7baadd5c 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -5,6 +5,7 @@ target_sources(uibase-tests PRIVATE test_formatters.cpp test_ifiletree.cpp + test_versioning.cpp ) mo2_configure_tests(uibase-tests NO_SOURCES WARNINGS 4) target_link_libraries(uibase-tests PRIVATE uibase) diff --git a/tests/test_versioning.cpp b/tests/test_versioning.cpp index ddc18fde..9cf04290 100644 --- a/tests/test_versioning.cpp +++ b/tests/test_versioning.cpp @@ -6,7 +6,7 @@ #include #include -#include "version.h" +#include #include @@ -21,13 +21,13 @@ TEST(VersioningTest, VersionParse) // semver ASSERT_EQ(Version(1, 0, 0), Version::parse("1.0.0")); - ASSERT_EQ(Version(1, 0, 0, {Development, 1}), Version::parse("1.0.0-dev.1")); - ASSERT_EQ(Version(1, 0, 0, {Development, 2}), Version::parse("1.0.0-dev.2")); - ASSERT_EQ(Version(1, 0, 0, {Alpha}), Version::parse("1.0.0-a")); - ASSERT_EQ(Version(1, 0, 0, {Alpha}), Version::parse("1.0.0-alpha")); - ASSERT_EQ(Version(1, 0, 0, {Alpha, 1, Beta}), Version::parse("1.0.0-alpha.1.b")); - ASSERT_EQ(Version(1, 0, 0, {Beta, 2}), Version::parse("1.0.0-beta.2")); - ASSERT_EQ(Version(2, 5, 2, {ReleaseCandidate, 1}), Version::parse("2.5.2-rc.1")); + ASSERT_EQ(Version(1, 0, 0, Development, 1), Version::parse("1.0.0-dev.1")); + ASSERT_EQ(Version(1, 0, 0, Development, 2), Version::parse("1.0.0-dev.2")); + ASSERT_EQ(Version(1, 0, 0, Alpha), Version::parse("1.0.0-a")); + ASSERT_EQ(Version(1, 0, 0, Alpha), Version::parse("1.0.0-alpha")); + ASSERT_EQ(Version(1, 0, 0, 0, {Alpha, 1, Beta}), Version::parse("1.0.0-alpha.1.b")); + ASSERT_EQ(Version(1, 0, 0, Beta, 2), Version::parse("1.0.0-beta.2")); + ASSERT_EQ(Version(2, 5, 2, ReleaseCandidate, 1), Version::parse("2.5.2-rc.1")); // mo2 ASSERT_EQ(Version(1, 0, 0), Version::parse("1.0.0", ParseMode::MO2)); @@ -39,9 +39,9 @@ TEST(VersioningTest, VersionParse) ASSERT_EQ(Version(1, 0, 0, Alpha, 1), Version::parse("1.0.0alpha1", ParseMode::MO2)); ASSERT_EQ(Version(1, 0, 0, Beta, 2), Version::parse("1.0.0beta2", ParseMode::MO2)); ASSERT_EQ(Version(1, 0, 0, Beta, 2), Version::parse("1.0.0beta2", ParseMode::MO2)); - ASSERT_EQ(Version(2, 4, 1, {ReleaseCandidate, 1, 1}), + ASSERT_EQ(Version(2, 4, 1, 0, {ReleaseCandidate, 1, 1}), Version::parse("2.4.1rc1.1", ParseMode::MO2)); - ASSERT_EQ(Version(2, 2, 2, {1, Beta, 2}), + ASSERT_EQ(Version(2, 2, 2, 1, Beta, 2), Version::parse("2.2.2.1beta2", ParseMode::MO2)); ASSERT_EQ(Version(2, 5, 2, ReleaseCandidate, 1), Version::parse("v2.5.2rc1", ParseMode::MO2)); @@ -52,12 +52,15 @@ TEST(VersioningTest, VersionParse) TEST(VersioningTest, VersionString) { ASSERT_EQ("1.0.0", Version(1, 0, 0).string()); - ASSERT_EQ("1.0.0-dev.1", Version(1, 0, 0, {Development, 1}).string()); - ASSERT_EQ("1.0.0-dev.2", Version(1, 0, 0, {Development, 2}).string()); - ASSERT_EQ("1.0.0-alpha", Version(1, 0, 0, {Alpha}).string()); - ASSERT_EQ("1.0.0-alpha.1.beta", Version(1, 0, 0, {Alpha, 1, Beta}).string()); - ASSERT_EQ("1.0.0-beta.2", Version(1, 0, 0, {Beta, 2}).string()); - ASSERT_EQ("2.5.2-rc.1", Version(2, 5, 2, {ReleaseCandidate, 1}).string()); + ASSERT_EQ("1.0.0-dev.1", Version(1, 0, 0, Development, 1).string()); + ASSERT_EQ("1.0.0-dev.2", Version(1, 0, 0, Development, 2).string()); + ASSERT_EQ("1.0.0-alpha", Version(1, 0, 0, Alpha).string()); + ASSERT_EQ("1.0.0-alpha.1.beta", Version(1, 0, 0, 0, {Alpha, 1, Beta}).string()); + ASSERT_EQ("1.0.0-beta.2", Version(1, 0, 0, Beta, 2).string()); + ASSERT_EQ("2.5.2-rc.1", Version(2, 5, 2, ReleaseCandidate, 1).string()); + ASSERT_EQ( + "2.5.2.0rc1", + Version(2, 5, 2, ReleaseCandidate, 1).string(Version::FormatMode::ForceSubPatch)); } TEST(VersioningTest, VersionCompare) @@ -70,18 +73,19 @@ TEST(VersioningTest, VersionCompare) ASSERT_TRUE(v(2, 0, 0) < v(2, 1, 0)); ASSERT_TRUE(v(2, 1, 0) < v(2, 1, 1)); - ASSERT_TRUE(v(1, 0, 0, Alpha) < v(1, 0, 0, {Alpha, 1})); - ASSERT_TRUE(v(1, 0, 0, Alpha, 1) < v(1, 0, 0, {Alpha, Beta})); - ASSERT_TRUE(v(1, 0, 0, {Alpha, Beta}) < v(1, 0, 0, {Beta})); - ASSERT_TRUE(v(1, 0, 0, Beta) < v(1, 0, 0, {Beta, 2})); - ASSERT_TRUE(v(1, 0, 0, {Beta, 2}) < v(1, 0, 0, {Beta, 11})); - ASSERT_TRUE(v(1, 0, 0, {Beta, 11}) < v(1, 0, 0, {ReleaseCandidate, 1})); - ASSERT_TRUE(v(1, 0, 0, {ReleaseCandidate, 1}) < v(1, 0, 0)); + ASSERT_TRUE(v(1, 0, 0, Alpha) < v(1, 0, 0, Alpha, 1)); + ASSERT_TRUE(v(1, 0, 0, Alpha, 1) < v(1, 0, 0, 0, {Alpha, Beta})); + ASSERT_TRUE(v(1, 0, 0, 0, {Alpha, Beta}) < v(1, 0, 0, 1)); + ASSERT_TRUE(v(1, 0, 0, Beta) < v(1, 0, 0, Beta, 2)); + ASSERT_TRUE(v(1, 0, 0, Beta, 2) < v(1, 0, 0, Beta, 11)); + ASSERT_TRUE(v(1, 0, 0, Beta, 11) < v(1, 0, 0, ReleaseCandidate, 1)); + ASSERT_TRUE(v(1, 0, 0, ReleaseCandidate, 0) < v(1, 0, 0)); - ASSERT_TRUE(v(2, 4, 1, {ReleaseCandidate, 1, 0}) == - v(2, 4, 1, {ReleaseCandidate, 1})); - ASSERT_TRUE(v(2, 4, 1, {ReleaseCandidate, 1, 0}) < - v(2, 4, 1, {ReleaseCandidate, 1, 1})); - ASSERT_TRUE(v(2, 4, 1, {ReleaseCandidate, 1}) < v(2, 4, 1, {ReleaseCandidate, 1, 1})); - ASSERT_TRUE(v(1, 0, 0) < v(2, 0, 0, {Alpha})); + ASSERT_TRUE(v(2, 4, 1, 0, {ReleaseCandidate, 1, 0}) == + v(2, 4, 1, ReleaseCandidate, 1)); + ASSERT_TRUE(v(2, 4, 1, 0, {ReleaseCandidate, 1, 0}) < + v(2, 4, 1, 0, {ReleaseCandidate, 1, 1})); + ASSERT_TRUE(v(2, 4, 1, ReleaseCandidate, 1) < + v(2, 4, 1, 0, {ReleaseCandidate, 1, 1})); + ASSERT_TRUE(v(1, 0, 0) < v(2, 0, 0, Alpha)); }