// Copyright (c) 2026 Progmasoft. // SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-2.0 #include #include #include #include #include #include #include "tools/Versions.hpp" namespace Progmasoft::Catch3::Tools { namespace { constexpr std::string_view kVersionHeader = "src/Progmasoft/Catch3/Version.hpp"; } // namespace namespace { /// The dot-separated numbers of `text`, at most `limit` of them, none /// empty or with a leading zero; nothing for anything else. std::optional> Numbers(std::string_view text, std::size_t limit) { std::vector numbers; while (true) { const auto dot = text.find('.'); const auto number = text.substr(0, dot); if (numbers.size() == limit || number.empty() || (number.size() > 1 && number.front() == '0')) { return std::nullopt; } unsigned value = 0; const auto *end = number.data() + number.size(); const auto parsed = std::from_chars(number.data(), end, value); if (parsed.ec != std::errc{} || parsed.ptr != end) { return std::nullopt; } numbers.push_back(value); if (dot == std::string_view::npos) { return numbers; } text.remove_prefix(dot + 1); } } } // namespace std::optional ParseVersion(std::string_view text) { const auto numbers = Numbers(text, 3); if (!numbers || numbers->size() != 3) { return std::nullopt; } return Version{ (*numbers)[0], (*numbers)[1], (*numbers)[2] }; } std::string FormatVersion(const Version &version) { return std::to_string(version[0]) + "." + std::to_string(version[1]) + "." + std::to_string(version[2]); } std::optional NextPatch(std::string_view current) { const auto numbers = Numbers(current, 4); // A patch number at its largest has no next one; adding one would // wrap around to 0 and move the version back. if (!numbers || numbers->size() < 3 || (*numbers)[2] == std::numeric_limits::max()) { return std::nullopt; } return Version{ (*numbers)[0], (*numbers)[1], (*numbers)[2] + 1 }; } const std::array & VersionSites() { static constexpr std::array kSites{ { { "CMakeLists.txt", "\n VERSION ", "\n" }, { "MODULE.bazel", "module(name = \"catch3\", version = \"", "\"" }, { "meson.build", "\n version: '", "'" }, { "xmake.lua", "\nset_version(\"", "\"" }, { kVersionHeader, "kVersion = \"", "\"" }, } }; return kSites; } std::optional ReadVersionSite(std::string_view text, const VersionSite &site) { const auto before = text.find(site.Before); if (before == std::string_view::npos) { return std::nullopt; } const auto first = before + site.Before.size(); const auto end = text.find(site.After, first); if (end == std::string_view::npos) { return std::nullopt; } return std::string{ text.substr(first, end - first) }; } std::optional WriteVersionSite(std::string_view text, const VersionSite &site, std::string_view version) { const auto current = ReadVersionSite(text, site); if (!current) { return std::nullopt; } const auto first = text.find(site.Before) + site.Before.size(); std::string result{ text.substr(0, first) }; result += version; result += text.substr(first + current->size()); return result; } std::optional CurrentVersion(const std::filesystem::path &root, Problems &problems) { const auto &sites = VersionSites(); const auto &site = *std::find_if(sites.begin(), sites.end(), [](const VersionSite &candidate) { return candidate.File == kVersionHeader; }); auto version = ReadVersionSite(ReadOrReport(root / site.File, problems), site); if (!version) { problems.emplace_back("no version in Version.hpp"); } return version; } void CheckVersions(const std::filesystem::path &root, Problems &problems) { const auto current = CurrentVersion(root, problems); if (!current) { return; } for (const auto &site : VersionSites()) { const auto version = ReadVersionSite(ReadOrReport(root / site.File, problems), site); if (version != current) { problems.push_back(std::string{ site.File } + " states " + version.value_or("no version") + ", Version.hpp states " + *current); } } } void SetVersion(const std::filesystem::path &root, const std::string &version, Problems &problems) { std::vector> writes; for (const auto &site : VersionSites()) { const auto path = root / site.File; auto text = WriteVersionSite(ReadOrReport(path, problems), site, version); if (text) { writes.emplace_back(path, *std::move(text)); } else { problems.push_back("cannot set the version in " + std::string{ site.File }); } } // All of them or none, so that the places never disagree. if (!problems.empty()) { return; } for (const auto &[path, text] : writes) { if (!WriteText(path, text)) { problems.push_back("cannot write " + path.generic_string()); } } } std::optional ReleaseNotesOf(std::string_view notes, std::string_view version) { // Every release is headed by its version as it was published: three // numbers, or four for the releases before Semantic Versioning. const auto heading = "\n## " + std::string{ version } + "\n"; const auto found = notes.find(heading); if (found == std::string_view::npos) { return std::nullopt; } auto section = notes.substr(found + heading.size()); section = section.substr(0, section.find("\n## ")); return std::string{ Trimmed(section) } + "\n"; } } // namespace Progmasoft::Catch3::Tools