From 4d9bb20bdde8517ff1e03327aff3de47258d9628 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Mon, 15 Apr 2024 22:04:16 -0400 Subject: [PATCH] Update `task news` to support 3.0.0 (#3342) * Introduce Version, use it to check current version in custom reports * Support multiple versions in 'task news' --- doc/devel/contrib/releasing.md | 2 + src/CMakeLists.txt | 1 + src/Version.cpp | 118 +++++++++++++++++++++++++++++++++ src/Version.h | 72 ++++++++++++++++++++ src/commands/CmdCustom.cpp | 17 ++--- src/commands/CmdNews.cpp | 115 ++++++++++++++++++++++++-------- src/commands/CmdNews.h | 13 +++- 7 files changed, 302 insertions(+), 36 deletions(-) create mode 100644 src/Version.cpp create mode 100644 src/Version.h diff --git a/doc/devel/contrib/releasing.md b/doc/devel/contrib/releasing.md index ff18c3f2d..15fd1292a 100644 --- a/doc/devel/contrib/releasing.md +++ b/doc/devel/contrib/releasing.md @@ -2,6 +2,8 @@ To release Taskwarrior, follow this process: +- Examine the changes since the last version, and update `src/commands/CmdNews.cpp` accordingly. + There are instructions at the top of the file. - Create a release PR - Update version in CMakeLists.txt - Update Changelog diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 204f9d8cf..9b095c45e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -18,6 +18,7 @@ add_library (task STATIC CLI2.cpp CLI2.h TDB2.cpp TDB2.h Task.cpp Task.h Variant.cpp Variant.h + Version.cpp Version.h ViewTask.cpp ViewTask.h dependency.cpp feedback.cpp diff --git a/src/Version.cpp b/src/Version.cpp new file mode 100644 index 000000000..56d01afb9 --- /dev/null +++ b/src/Version.cpp @@ -0,0 +1,118 @@ +//////////////////////////////////////////////////////////////////////////////// +// +// Copyright 2024, Dustin Mitchell. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +// https://www.opensource.org/licenses/mit-license.php +// +//////////////////////////////////////////////////////////////////////////////// + +#include +#include +#include +#include +#include +#include +#include + +//////////////////////////////////////////////////////////////////////////////// +Version::Version(std::string version) { + std::vector parts; + std::string part; + std::istringstream input(version); + + while (std::getline(input, part, '.')) { + int value; + // Try converting string to integer + if (std::stringstream(part) >> value && value >= 0) { + parts.push_back(value); + } else { + return; + } + } + + if (parts.size() != 3) { + return; + } + + major = parts[0]; + minor = parts[1]; + patch = parts[2]; +} + +//////////////////////////////////////////////////////////////////////////////// +Version Version::Current() { return Version(PACKAGE_VERSION); } + +//////////////////////////////////////////////////////////////////////////////// +bool Version::is_valid() const { return major >= 0; } + +//////////////////////////////////////////////////////////////////////////////// +bool Version::operator<(const Version &other) const { + return std::tie(major, minor, patch) < + std::tie(other.major, other.minor, other.patch); +} + +//////////////////////////////////////////////////////////////////////////////// +bool Version::operator<=(const Version &other) const { + return std::tie(major, minor, patch) <= + std::tie(other.major, other.minor, other.patch); +} + +//////////////////////////////////////////////////////////////////////////////// +bool Version::operator>(const Version &other) const { + return std::tie(major, minor, patch) > + std::tie(other.major, other.minor, other.patch); +} + +//////////////////////////////////////////////////////////////////////////////// +bool Version::operator>=(const Version &other) const { + return std::tie(major, minor, patch) >= + std::tie(other.major, other.minor, other.patch); +} + +//////////////////////////////////////////////////////////////////////////////// +bool Version::operator==(const Version &other) const { + return std::tie(major, minor, patch) == + std::tie(other.major, other.minor, other.patch); +} + +//////////////////////////////////////////////////////////////////////////////// +bool Version::operator!=(const Version &other) const { + std::cout << other; + return std::tie(major, minor, patch) != + std::tie(other.major, other.minor, other.patch); +} + +//////////////////////////////////////////////////////////////////////////////// +Version::operator std::string() const { + std::ostringstream output; + if (is_valid()) { + output << major << '.' << minor << '.' << patch; + } else { + output << "(invalid version)"; + } + return output.str(); +} + +//////////////////////////////////////////////////////////////////////////////// +std::ostream &operator<<(std::ostream &os, const Version &version) { + os << std::string(version); + return os; +} diff --git a/src/Version.h b/src/Version.h new file mode 100644 index 000000000..2c96d61e7 --- /dev/null +++ b/src/Version.h @@ -0,0 +1,72 @@ +//////////////////////////////////////////////////////////////////////////////// +// +// Copyright 2024, Dustin Mitchell. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included +// in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +// https://www.opensource.org/licenses/mit-license.php +// +//////////////////////////////////////////////////////////////////////////////// + +#ifndef INCLUDED_VERSION +#define INCLUDED_VERSION + +#include + +// A utility class for handling Taskwarrior versions. +class Version { +public: + // Parse a version from a string. This must be of the format + // digits.digits.digits. + explicit Version(std::string version); + + // Create an invalid version. + Version() = default; + + Version(const Version &other) = default; + Version(Version &&other) = default; + Version &operator=(const Version &) = default; + Version &operator=(Version &&) = default; + + // Return a version representing the release being built. + static Version Current(); + + bool is_valid() const; + + // Compare versions. + bool operator<(const Version &) const; + bool operator<=(const Version &) const; + bool operator>(const Version &) const; + bool operator>=(const Version &) const; + bool operator==(const Version &) const; + bool operator!=(const Version &) const; + + // Convert back to a string. + operator std::string() const; + + friend std::ostream& operator<<(std::ostream& os, const Version& version); + +private: + int major = -1; + int minor = -1; + int patch = -1; +}; + +#endif +//////////////////////////////////////////////////////////////////////////////// diff --git a/src/commands/CmdCustom.cpp b/src/commands/CmdCustom.cpp index c3c59aaf6..d6e1b7503 100644 --- a/src/commands/CmdCustom.cpp +++ b/src/commands/CmdCustom.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include #include #include @@ -250,24 +251,24 @@ int CmdCustom::execute (std::string& output) } // Inform user about the new release highlights if not presented yet - if (Context::getContext ().config.get ("news.version") != "2.6.0") + Version news_version(Context::getContext ().config.get ("news.version")); + Version current_version = Version::Current(); + if (news_version != current_version) { std::random_device device; std::mt19937 random_generator(device()); std::uniform_int_distribution twentyfive_percent(1, 4); - std::string NEWS_NOTICE = ( - "Recently upgraded to 2.6.0. " - "Please run 'task news' to read highlights about the new release." - ); - // 1 in 10 chance to display the message. if (twentyfive_percent(random_generator) == 4) { + std::ostringstream notice; + notice << "Recently upgraded to " << current_version << ". " + "Please run 'task news' to read highlights about the new release."; if (Context::getContext ().verbose ("footnote")) - Context::getContext ().footnote (NEWS_NOTICE); + Context::getContext ().footnote (notice.str()); else if (Context::getContext ().verbose ("header")) - Context::getContext ().header (NEWS_NOTICE); + Context::getContext ().header (notice.str()); } } diff --git a/src/commands/CmdNews.cpp b/src/commands/CmdNews.cpp index fb6cb2789..5e22fbb5b 100644 --- a/src/commands/CmdNews.cpp +++ b/src/commands/CmdNews.cpp @@ -38,6 +38,14 @@ #include #include +/* Adding a new version: + * + * - Add a new `versionX_Y_Z` method to `NewsItem`, and add news items for the new + * release. + * - Call the new method in `NewsItem.all()`. Calls should be in version order. + * - Test with `task news`. + */ + //////////////////////////////////////////////////////////////////////////////// CmdNews::CmdNews () { @@ -91,6 +99,7 @@ void wait_for_enter () // Holds information about single improvement / bug. // NewsItem::NewsItem ( + Version version, bool major, const std::string& title, const std::string& bg_title, @@ -100,6 +109,7 @@ NewsItem::NewsItem ( const std::string& reasoning, const std::string& actions ) { + _version = version; _major = major; _title = title; _bg_title = bg_title; @@ -127,7 +137,7 @@ void NewsItem::render () { // TODO: For some reason, bold cannot be blended in 256-color terminals // Apply this workaround of colorizing twice. - std::cout << bold.colorize (header.colorize (format ("{1}\n", _title))); + std::cout << bold.colorize (header.colorize (format ("{1} ({2})\n", _title, _version))); if (_background.size ()) { if (_bg_title.empty ()) _bg_title = "Background"; @@ -138,7 +148,7 @@ void NewsItem::render () { wait_for_enter (); - std::cout << " " << underline.colorize ("What changed in 2.6.0?\n"); + std::cout << " " << underline.colorize (format ("What changed in {1}?\n", _version)); if (_punchline.size ()) std::cout << footnote.colorize (format ("{1}\n", _punchline)); @@ -160,6 +170,13 @@ void NewsItem::render () { } } +std::vector NewsItem::all () { + std::vector items; + version2_6_0(items); + version3_0_0(items); + return items; +} + //////////////////////////////////////////////////////////////////////////////// // Generate the highlights for the 2.6.0 version. // @@ -174,7 +191,8 @@ void NewsItem::render () { // - The .by attribute modifier // - Exporting a report // - Multi-day holidays -void CmdNews::version2_6_0 (std::vector& items) { +void NewsItem::version2_6_0 (std::vector& items) { + Version version("2.6.0"); ///////////////////////////////////////////////////////////////////////////// // - Writeable context (major) @@ -234,6 +252,7 @@ void CmdNews::version2_6_0 (std::vector& items) { " Read more about how to use contexts in CONTEXT section of 'man task'."; NewsItem writeable_context ( + version, true, "'Writeable' context", "Background - what is context?", @@ -277,6 +296,7 @@ void CmdNews::version2_6_0 (std::vector& items) { // - 64-bit datetime support (major) NewsItem uint64_support ( + version, false, "Support for 64-bit timestamps and numeric values", "", @@ -294,6 +314,7 @@ void CmdNews::version2_6_0 (std::vector& items) { // - Waiting is a virtual status NewsItem waiting_status ( + version, true, "Deprecation of the status:waiting", "", @@ -315,6 +336,7 @@ void CmdNews::version2_6_0 (std::vector& items) { // - Support for environment variables in the taskrc NewsItem env_vars ( + version, true, "Environment variables in the taskrc", "", @@ -333,6 +355,7 @@ void CmdNews::version2_6_0 (std::vector& items) { // - Reports outside of context NewsItem contextless_reports ( + version, true, "Context-less reports", "", @@ -354,6 +377,7 @@ void CmdNews::version2_6_0 (std::vector& items) { // - Exporting a particular report NewsItem exportable_reports ( + version, false, "Exporting a particular report", "", @@ -377,6 +401,7 @@ void CmdNews::version2_6_0 (std::vector& items) { // - Multi-day holidays NewsItem multi_holidays ( + version, false, "Multi-day holidays", "", @@ -399,6 +424,7 @@ void CmdNews::version2_6_0 (std::vector& items) { // - Unicode 12 NewsItem unicode_12 ( + version, false, "Extended Unicode support (Unicode 12)", "", @@ -417,6 +443,7 @@ void CmdNews::version2_6_0 (std::vector& items) { // - The .by attribute modifier NewsItem by_modifier ( + version, false, "The .by attribute modifier", "", @@ -435,6 +462,7 @@ void CmdNews::version2_6_0 (std::vector& items) { // - Context-specific configuration overrides NewsItem context_config ( + version, false, "Context-specific configuration overrides", "", @@ -459,6 +487,7 @@ void CmdNews::version2_6_0 (std::vector& items) { // - XDG config home support NewsItem xdg_support ( + version, true, "Support for XDG Base Directory Specification", "", @@ -487,6 +516,7 @@ void CmdNews::version2_6_0 (std::vector& items) { // - Update holiday data NewsItem holidata_2022 ( + version, false, "Updated holiday data for 2022", "", @@ -500,6 +530,28 @@ void CmdNews::version2_6_0 (std::vector& items) { items.push_back(holidata_2022); } +void NewsItem::version3_0_0 (std::vector& items) { + Version version("3.0.0"); + NewsItem sync { + version, + /*major=*/true, + /*title=*/"New data model and sync backend", + /*bg_title=*/"", + /*background=*/"", + /*punchline=*/ + "The sync functionality for Taskwarrior has been rewritten entirely, and no longer\n" + "supports taskserver/taskd. The most robust solution is a cloud-storage backend,\n" + "although a less-mature taskchampion-sync-server is also available. See `task-sync(5)`\n" + "For details. As part of this change, the on-disk storage format has also changed.\n", + /*update=*/ + "This is a breaking upgrade: you must export your task database from 2.x and re-import\n" + "it into 3.x. Hooks run during task import, so if you have any hooks defined,\n" + "temporarily disable them for this operation.\n\n" + "See https://taskwarrior.org/docs/upgrade-3/ for information on upgrading to Taskwarrior 3.0.", + }; + items.push_back(sync); +} + //////////////////////////////////////////////////////////////////////////////// int CmdNews::execute (std::string& output) { @@ -509,11 +561,13 @@ int CmdNews::execute (std::string& output) // Supress compiler warning about unused argument output = ""; - // TODO: 2.6.0 is the only version with explicit release notes, but in the - // future we need to only execute yet unread release notes - std::vector items; - std::string version = "2.6.0"; - version2_6_0 (items); + std::vector items = NewsItem::all(); + Version news_version(Context::getContext ().config.get ("news.version")); + Version current_version = Version::Current(); + + // 2.6.0 is the earliest version with news support. + if (!news_version.is_valid()) + news_version = Version("2.6.0"); bool full_summary = false; bool major_items = true; @@ -538,6 +592,12 @@ int CmdNews::execute (std::string& output) signal (SIGINT, signal_handler); + // Remove items that have already been shown + items.erase ( + std::remove_if (items.begin (), items.end (), [&](const NewsItem& n){return n._version <= news_version;}), + items.end () + ); + // Remove non-major items if displaying a non-full (abbreviated) summary int total_highlights = items.size (); if (! full_summary) @@ -546,23 +606,25 @@ int CmdNews::execute (std::string& output) items.end () ); - // Print release notes Color bold = Color ("bold"); - std::cout << bold.colorize (format ( - "\n" - "==========================================\n" - "Taskwarrior {1} {2} Release highlights\n" - "==========================================\n", - version, - (full_summary ? "All" : (major_items ? "Major" : "Minor")) - )); + if (items.empty ()) { + std::cout << bold.colorize ("You are up to date!\n"); + } else { + // Print release notes + std::cout << bold.colorize (format ( + "\n" + "================================================\n" + "Taskwarrior {1} through {2} Release Highlights\n" + "================================================\n", + news_version, + current_version)); - for (unsigned short i=0; i < items.size (); i++) { - std::cout << format ("\n({1}/{2}) ", i+1, items.size ()); - items[i].render (); + for (unsigned short i=0; i < items.size (); i++) { + std::cout << format ("\n({1}/{2}) ", i+1, items.size ()); + items[i].render (); + } + std::cout << "Thank you for catching up on the new features!\n"; } - - std::cout << "Thank you for catching up on the new features!\n"; wait_for_enter (); // Display outro @@ -588,9 +650,9 @@ int CmdNews::execute (std::string& output) std::cout << outro.str (); // Set a mark in the config to remember which version's release notes were displayed - if (config.get ("news.version") != "2.6.0") + if (full_summary && news_version != current_version) { - CmdConfig::setConfigVariable ("news.version", "2.6.0", false); + CmdConfig::setConfigVariable ("news.version", std::string(current_version), false); // Revert back to default signal handling after displaying the outro signal (SIGINT, SIG_DFL); @@ -627,14 +689,15 @@ int CmdNews::execute (std::string& output) else wait_for_enter (); // Do not display the outro and footnote at once - if (! full_summary && major_items) + if (! items.empty() && ! full_summary && major_items) { Context::getContext ().footnote (format ( "Only major highlights were displayed ({1} out of {2} total).\n" "If you're interested in more release highlights, run 'task news {3} minor'.", items.size (), total_highlights, - version + current_version )); + } return 0; } diff --git a/src/commands/CmdNews.h b/src/commands/CmdNews.h index b85b44c27..a74b2dda3 100644 --- a/src/commands/CmdNews.h +++ b/src/commands/CmdNews.h @@ -31,9 +31,11 @@ #include #include #include +#include class NewsItem { public: + Version _version; bool _major = false; std::string _title; std::string _bg_title; @@ -42,7 +44,16 @@ public: std::string _update; std::string _reasoning; std::string _actions; + + void render (); + + static std::vector all(); + static void version2_6_0 (std::vector&); + static void version3_0_0 (std::vector&); + +private: NewsItem ( + Version, bool, const std::string&, const std::string& = "", @@ -52,7 +63,6 @@ public: const std::string& = "", const std::string& = "" ); - void render (); }; class CmdNews : public Command @@ -60,7 +70,6 @@ class CmdNews : public Command public: CmdNews (); int execute (std::string&); - void version2_6_0 (std::vector&); }; #endif