Skip to content

Commit

Permalink
Refactor SemVer approach for more flexibility.
Browse files Browse the repository at this point in the history
  • Loading branch information
Holt59 committed Jul 18, 2024
1 parent f75dfa8 commit 996e6b6
Show file tree
Hide file tree
Showing 9 changed files with 168 additions and 71 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
edit
.vscode
CMakeLists.txt.user
/msbuild.log
/*std*.log
Expand Down
2 changes: 1 addition & 1 deletion include/uibase/imoinfo.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
77 changes: 70 additions & 7 deletions include/uibase/version.h → include/uibase/versioning.h
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
#include <variant>
#include <vector>

#include <QFlags>
#include <QString>

#include "dllimport.h"
#include "exceptions.h"

Expand All @@ -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
//
Expand All @@ -26,17 +46,47 @@ 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,

// MO2 parsing, e.g., 2.5.1rc1 - this either parse a string with no pre-release
// 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
Expand All @@ -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<std::variant<int, ReleaseType>> prereleases,
QString metadata = {});

Expand All @@ -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
//
Expand All @@ -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<std::variant<int, ReleaseType>> m_PreReleases;
Expand All @@ -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 <class CharT>
Expand All @@ -119,4 +182,4 @@ struct std::formatter<MOBase::Version, CharT> : std::formatter<QString, CharT>
{
return std::formatter<QString, CharT>::format(v.string(), ctx);
}
};
};
6 changes: 2 additions & 4 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
)

Expand All @@ -134,7 +133,6 @@ mo2_target_sources(uibase
ifiletree.cpp
imodrepositorybridge.cpp
imoinfo.cpp
iplugininstaller.cpp
)

mo2_target_sources(uibase
Expand Down
3 changes: 2 additions & 1 deletion src/uibase_en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,8 @@
</message>
<message>
<location filename="safewritefile.cpp" line="42"/>
<source>Failed to save &apos;{}&apos;, could not create a temporary file: {} (error {})</source>
<source>Failed to save &apos;%1&apos;, could not create a temporary file: %2 (error %3)</source>
<oldsource>Failed to save &apos;{}&apos;, could not create a temporary file: {} (error {})</oldsource>
<translation type="unfinished"></translation>
</message>
<message>
Expand Down
86 changes: 57 additions & 29 deletions src/version.cpp → src/versioning.cpp
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#include "version.h"
#include "versioning.h"

#include <QRegularExpression>
#include <format>
Expand All @@ -16,11 +16,12 @@ static const QRegularExpression s_SemVerMO2RegEx{

// match from value to release type
static const std::unordered_map<QString, MOBase::Version::ReleaseType>
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
{
Expand Down Expand Up @@ -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)
Expand All @@ -80,12 +81,10 @@ namespace
const auto minor = match.captured("minor").toInt();
const auto patch = match.captured("patch").toInt();

std::vector<std::variant<int, Version::ReleaseType>> 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<std::variant<int, Version::ReleaseType>> prereleases;
if (match.hasCaptured("type")) {
prereleases.push_back(s_StringToRelease.at(match.captured("type")));

Expand All @@ -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
Expand All @@ -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::variant<int, ReleaseType>>{},
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<std::variant<int, ReleaseType>>{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<std::variant<int, ReleaseType>> 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<decltype(pre), ReleaseType const&>) {
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";
}
Expand All @@ -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();
}

Expand All @@ -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) {
Expand Down Expand Up @@ -252,4 +280,4 @@ std::strong_ordering operator<=>(const Version& lhs, const Version& rhs)
}
}

} // namespace MOBase
} // namespace MOBase
1 change: 1 addition & 0 deletions tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Loading

0 comments on commit 996e6b6

Please sign in to comment.