From 5bb98579841d41e680280ada7bf3a3e9a9940553 Mon Sep 17 00:00:00 2001 From: "Dustin J. Mitchell" Date: Thu, 15 Dec 2022 02:52:47 +0000 Subject: [PATCH] Use Taskchampion to store Taskwarrior data This replaces the TF2 task files with a TaskChampion replica. --- src/Context.cpp | 21 +- src/TDB2.cpp | 1292 +++++++----------------------------- src/TDB2.h | 95 +-- src/Task.cpp | 64 ++ src/Task.h | 6 + src/commands/CmdExport.cpp | 12 +- src/recur.cpp | 1 - src/tc/Replica.cpp | 67 +- src/tc/Replica.h | 37 +- src/tc/Task.cpp | 55 ++ src/tc/Task.h | 18 +- test/.gitignore | 1 + test/CMakeLists.txt | 1 + test/backlog.t | 88 --- test/feature.559.t | 2 +- test/ids.t | 3 +- test/recurrence.t | 1 + test/tc.t.cpp | 2 +- test/tdb2.t.cpp | 4 +- test/tw-1688.t | 2 +- test/tw-2563.t | 45 -- test/tw-46.t | 58 -- test/undo.t | 3 +- test/uuid.t | 21 +- 24 files changed, 537 insertions(+), 1362 deletions(-) delete mode 100755 test/backlog.t delete mode 100755 test/tw-2563.t delete mode 100755 test/tw-46.t diff --git a/src/Context.cpp b/src/Context.cpp index 313bdc20d..7879d336b 100644 --- a/src/Context.cpp +++ b/src/Context.cpp @@ -560,8 +560,8 @@ int Context::initialize (int argc, const char** argv) if (taskdata_overridden && verbose ("override")) header (format ("TASKDATA override: {1}", data_dir._data)); - tdb2.set_location (data_dir); - createDefaultConfig (); + bool create_if_missing = !config.getBoolean ("exit.on.missing.db"); + tdb2.open_replica (data_dir, create_if_missing); //////////////////////////////////////////////////////////////////////////// // @@ -1254,23 +1254,6 @@ void Context::createDefaultConfig () if (! File::write (rc_file._data, contents.str ())) throw format ("Could not write to '{1}'.", rc_file._data); } - - // Create data location, if necessary. - Directory d (data_dir); - if (! d.exists ()) - { - if (config.getBoolean ("exit.on.missing.db")) - throw std::string ("Error: rc.data.location does not exist - exiting according to rc.exit.on.missing.db setting."); - - d.create (); - - if (config.has ("hooks.location")) - d = Directory (config.get ("hooks.location")); - else - d += "hooks"; - - d.create (); - } } //////////////////////////////////////////////////////////////////////////////// diff --git a/src/TDB2.cpp b/src/TDB2.cpp index cbb3faf1d..4348da01c 100644 --- a/src/TDB2.cpp +++ b/src/TDB2.cpp @@ -30,7 +30,7 @@ #include #include #include -#include +#include #include #include #include @@ -42,967 +42,161 @@ #include #include -#define STRING_TDB2_REVERTED "Modified task reverted." - bool TDB2::debug_mode = false; - -//////////////////////////////////////////////////////////////////////////////// -TF2::TF2 () -: _read_only (false) -, _dirty (false) -, _loaded_tasks (false) -, _loaded_lines (false) -, _has_ids (false) -, _auto_dep_scan (false) -{ -} - -//////////////////////////////////////////////////////////////////////////////// -TF2::~TF2 () -{ - if (_dirty && TDB2::debug_mode) - std::cout << format ("Exiting with unwritten changes to {1}\n", std::string (_file)); -} - -//////////////////////////////////////////////////////////////////////////////// -void TF2::target (const std::string& f) -{ - _file = File (f); - - // A missing file is not considered unwritable. - _read_only = false; - if (_file.exists () && ! _file.writable ()) - _read_only = true; -} - -//////////////////////////////////////////////////////////////////////////////// -const std::vector & TF2::get_tasks () -{ - if (! _loaded_tasks) - load_tasks (); - - return _tasks; -} - -//////////////////////////////////////////////////////////////////////////////// -const std::vector & TF2::get_lines () -{ - if (! _loaded_lines) - load_lines (); - - return _lines; -} - -//////////////////////////////////////////////////////////////////////////////// -// Locate task by id. -bool TF2::get (int id, Task& task) -{ - if (! _loaded_tasks) - load_tasks (); - - // This is an optimization. Since the 'id' is based on the line number of - // pending.data file, the task in question cannot appear earlier than line - // (id - 1) in the file. It can, however, appear significantly later because - // it is not known how recent a GC operation was run. - for (unsigned int i = id - 1; i < _tasks.size (); ++i) - { - if (_tasks[i].id == id) - { - task = _tasks[i]; - return true; - } - } - - return false; -} - -//////////////////////////////////////////////////////////////////////////////// -// Locate task by uuid, which may be a partial UUID. -bool TF2::get (const std::string& uuid, Task& task) -{ - if (! _loaded_tasks) - load_tasks (); - - if (_tasks_map.size () > 0 && uuid.size () == 36) - { - // Fast lookup, same result as below. Only used during "task import". - auto i = _tasks_map.find (uuid); - if (i != _tasks_map.end ()) - { - task = i->second; - return true; - } - } - else - { - // Slow lookup, same result as above. - for (auto& i : _tasks) - { - if (closeEnough (i.get ("uuid"), uuid, uuid.length ())) - { - task = i; - return true; - } - } - } - - return false; -} - -//////////////////////////////////////////////////////////////////////////////// -bool TF2::has (const std::string& uuid) -{ - if (! _loaded_tasks) - load_tasks (); - - for (auto& i : _tasks) - if (i.get ("uuid") == uuid) - return true; - - return false; -} - -//////////////////////////////////////////////////////////////////////////////// -void TF2::add_task (Task& task) -{ - _tasks.push_back (task); // For subsequent queries - _added_tasks.push_back (task); // For commit/synch - - // For faster lookup - if (Context::getContext ().cli2.getCommand () == "import") - _tasks_map.emplace (task.get("uuid"), task); - - Task::status status = task.getStatus (); - if (task.id == 0 && - (status == Task::pending || - status == Task::recurring || - status == Task::waiting)) - { - task.id = Context::getContext ().tdb2.next_id (); - } - - _I2U[task.id] = task.get ("uuid"); - _U2I[task.get ("uuid")] = task.id; - - _dirty = true; -} - -//////////////////////////////////////////////////////////////////////////////// -bool TF2::modify_task (const Task& task) -{ - std::string uuid = task.get ("uuid"); - - if (Context::getContext ().cli2.getCommand () == "import") - { - // Update map used for faster lookup - auto i = _tasks_map.find (uuid); - if (i != _tasks_map.end ()) - { - i->second = task; - } - } - - for (auto& i : _tasks) - { - if (i.get ("uuid") == uuid) - { - // Modify in-place. - i = task; - _modified_tasks.push_back (task); - _dirty = true; - - return true; - } - } - - return false; -} - -//////////////////////////////////////////////////////////////////////////////// -bool TF2::purge_task (const Task& task) -{ - // Bail out if task is not found in this file - std::string uuid = task.get ("uuid"); - if (!has (uuid)) - return false; - - // Mark the task to be purged - _purged_tasks.insert (uuid); - _dirty = true; - - return true; -} - -//////////////////////////////////////////////////////////////////////////////// -void TF2::add_line (const std::string& line) -{ - _lines.push_back (line); - _added_lines.push_back (line); - _dirty = true; -} - -//////////////////////////////////////////////////////////////////////////////// -void TF2::clear_tasks () -{ - _tasks.clear (); - _dirty = true; -} - -//////////////////////////////////////////////////////////////////////////////// -void TF2::clear_lines () -{ - _lines.clear (); - _dirty = true; -} - -//////////////////////////////////////////////////////////////////////////////// -// Top-down recomposition. -void TF2::commit () -{ - // The _dirty flag indicates that the file needs to be written. - if (_dirty) - { - // Special case: added but no modified means just append to the file. - if (!_modified_tasks.size () && !_purged_tasks.size () && - (_added_tasks.size () || _added_lines.size ())) - { - if (_file.open ()) - { - if (Context::getContext ().config.getBoolean ("locking")) - _file.lock (); - - // Write out all the added tasks. - _file.append (std::string("")); // Seek to end of file - for (auto& task : _added_tasks) - _file.write_raw (task.composeF4 () + "\n"); - - _added_tasks.clear (); - - // Write out all the added lines. - _file.append (_added_lines); - - _added_lines.clear (); - _file.close (); - _dirty = false; - } - } - else - { - if (_file.open ()) - { - if (Context::getContext ().config.getBoolean ("locking")) - _file.lock (); - - // Truncate the file and rewrite. - _file.truncate (); - - // Only write out _tasks, because any deltas have already been applied. - _file.append (std::string("")); // Seek to end of file - for (auto& task : _tasks) - // Skip over the tasks that are marked to be purged - if (_purged_tasks.find (task.get ("uuid")) == _purged_tasks.end ()) - _file.write_raw (task.composeF4 () + '\n'); - - // Write out all the added lines. - _file.append (_added_lines); - - _added_lines.clear (); - _file.close (); - _dirty = false; - } - } - } -} - -//////////////////////////////////////////////////////////////////////////////// -// Load a single Task object, handle necessary plumbing work -Task TF2::load_task (const std::string& line) -{ - Task task (line); - - // Some tasks get an ID. - if (_has_ids) - { - Task::status status = task.getStatus (); - // Completed / deleted tasks in pending.data get an ID if GC is off. - if (! Context::getContext ().run_gc || - (status != Task::completed && status != Task::deleted)) - task.id = Context::getContext ().tdb2.next_id (); - } - - // Maintain mapping for ease of link/dependency resolution. - // Note that this mapping is not restricted by the filter, and is - // therefore a complete set. - if (task.id) - { - _I2U[task.id] = task.get ("uuid"); - _U2I[task.get ("uuid")] = task.id; - } - - return task; -} - -//////////////////////////////////////////////////////////////////////////////// -// Check whether task needs to be relocated to pending/completed, -// or needs to be 'woken'. -void TF2::load_gc (Task& task) -{ - Datetime now; - - std::string status = task.get ("status"); - if (status == "pending" || - status == "recurring") - { - Context::getContext ().tdb2.pending._tasks.push_back (task); - } - // 2.6.0: Waiting status is deprecated. Convert to pending to upgrade status - // field value in the data files. - else if (status == "waiting") - { - task.set ("status", "pending"); - Context::getContext ().tdb2.pending._tasks.push_back (task); - Context::getContext ().tdb2.pending._dirty = true; - } - else - { - Context::getContext ().tdb2.completed._tasks.push_back (task); - } -} - -//////////////////////////////////////////////////////////////////////////////// -void TF2::load_tasks (bool from_gc /* = false */) -{ - Timer timer; - - if (! _loaded_lines) - { - load_lines (); - - // Apply previously added lines. - for (auto& line : _added_lines) - _lines.push_back (line); - } - - // Reduce unnecessary allocations/copies. - // Calling it on _tasks is the right thing to do even when from_gc is set. - _tasks.reserve (_lines.size ()); - - int line_number = 0; // Used for error message in catch block. - try - { - for (auto& line : _lines) - { - ++line_number; - auto task = load_task (line); - - if (from_gc) - load_gc (task); - else - _tasks.push_back (task); - - if (Context::getContext ().cli2.getCommand () == "import") // For faster lookup only - _tasks_map.emplace (task.get("uuid"), task); - } - - // TDB2::gc() calls this after loading both pending and completed - if (_auto_dep_scan && !from_gc) - dependency_scan (); - - _loaded_tasks = true; - } - - catch (const std::string& e) - { - throw e + format (" in {1} at line {2}", _file._data, line_number); - } - - Context::getContext ().time_load_us += timer.total_us (); -} - -//////////////////////////////////////////////////////////////////////////////// -void TF2::load_lines () -{ - if (_file.open ()) - { - if (Context::getContext ().config.getBoolean ("locking")) - _file.lock (); - - _file.read (_lines); - _file.close (); - _loaded_lines = true; - } -} - -//////////////////////////////////////////////////////////////////////////////// -std::string TF2::uuid (int id) -{ - if (! _loaded_tasks) - { - load_tasks (); - - // Apply previously added tasks. - for (auto& task : _added_tasks) - _tasks.push_back (task); - } - - auto i = _I2U.find (id); - if (i != _I2U.end ()) - return i->second; - - return ""; -} - -//////////////////////////////////////////////////////////////////////////////// -int TF2::id (const std::string& uuid) -{ - if (! _loaded_tasks) - { - load_tasks (); - - // Apply previously added tasks. - for (auto& task : _added_tasks) - _tasks.push_back (task); - } - - auto i = _U2I.find (uuid); - if (i != _U2I.end ()) - return i->second; - - return 0; -} - -//////////////////////////////////////////////////////////////////////////////// -void TF2::has_ids () -{ - _has_ids = true; -} - -//////////////////////////////////////////////////////////////////////////////// -void TF2::auto_dep_scan () -{ - _auto_dep_scan = true; -} - -//////////////////////////////////////////////////////////////////////////////// -// Completely wipe it all clean. -void TF2::clear () -{ - _read_only = false; - _dirty = false; - _loaded_tasks = false; - _loaded_lines = false; - - // Note that these are deliberately not cleared. - //_file._data = ""; - //_has_ids = false; - //_auto_dep_scan = false; - - _tasks.clear (); - _added_tasks.clear (); - _modified_tasks.clear (); - _purged_tasks.clear (); - _lines.clear (); - _added_lines.clear (); - _I2U.clear (); - _U2I.clear (); -} - -//////////////////////////////////////////////////////////////////////////////// -// For any task that has depenencies, follow the chain of dependencies until the -// end. Along the way, update the Task::is_blocked and Task::is_blocking data -// cache. -void TF2::dependency_scan () -{ - // Iterate and modify TDB2 in-place. Don't do this at home. - for (auto& left : _tasks) - { - for (auto& dep : left.getDependencyUUIDs ()) - { - for (auto& right : _tasks) - { - if (right.get ("uuid") == dep) - { - // GC hasn't run yet, check both tasks for their current status - Task::status lstatus = left.getStatus (); - Task::status rstatus = right.getStatus (); - if (lstatus != Task::completed && - lstatus != Task::deleted && - rstatus != Task::completed && - rstatus != Task::deleted) - { - left.is_blocked = true; - right.is_blocking = true; - } - - // Only want to break out of the "right" loop. - break; - } - } - } - } -} - -//////////////////////////////////////////////////////////////////////////////// -const std::string TF2::dump () -{ - Color red ("rgb500 on rgb100"); - Color yellow ("rgb550 on rgb220"); - Color green ("rgb050 on rgb010"); - - // File label. - std::string label; - auto slash = _file._data.rfind ('/'); - if (slash != std::string::npos) - label = rightJustify (_file._data.substr (slash + 1), 14); - - // File mode. - std::string mode = std::string (_file.exists () && _file.readable () ? "r" : "-") + - std::string (_file.exists () && _file.writable () ? "w" : "-"); - if (mode == "r-") mode = red.colorize (mode); - else if (mode == "rw") mode = green.colorize (mode); - else mode = yellow.colorize (mode); - - // Hygiene. - std::string hygiene = _dirty ? red.colorize ("O") : green.colorize ("-"); - - std::string tasks = green.colorize (rightJustifyZero ((int) _tasks.size (), 4)); - std::string tasks_added = red.colorize (rightJustifyZero ((int) _added_tasks.size (), 3)); - std::string tasks_modified = yellow.colorize (rightJustifyZero ((int) _modified_tasks.size (), 3)); - std::string tasks_purged = red.colorize (rightJustifyZero ((int) _purged_tasks.size (), 3)); - std::string lines = green.colorize (rightJustifyZero ((int) _lines.size (), 4)); - std::string lines_added = red.colorize (rightJustifyZero ((int) _added_lines.size (), 3)); - - char buffer[256]; // Composed string is actually 246 bytes. Yikes. - snprintf (buffer, 256, "%14s %s %s T%s+%s~%s-%s L%s+%s", - label.c_str (), - mode.c_str (), - hygiene.c_str (), - tasks.c_str (), - tasks_added.c_str (), - tasks_modified.c_str (), - tasks_purged.c_str (), - lines.c_str (), - lines_added.c_str ()); - - return std::string (buffer); -} +static void dependency_scan (std::vector &); //////////////////////////////////////////////////////////////////////////////// TDB2::TDB2 () -: _location ("") -, _id (1) +: replica {tc::Replica()} // in-memory Replica +, _working_set {} { - // Mark the pending file as the only one that has ID numbers. - pending.has_ids (); - - // Indicate that dependencies should be automatically scanned on startup, - // setting Task::is_blocked and Task::is_blocking accordingly. - pending.auto_dep_scan (); } //////////////////////////////////////////////////////////////////////////////// -// Once a location is known, the files can be set up. Note that they are not -// read. -void TDB2::set_location (const std::string& location) +void TDB2::open_replica (const std::string& location, bool create_if_missing) { - _location = location; - - pending.target (location + "/pending.data"); - completed.target (location + "/completed.data"); - undo.target (location + "/undo.data"); - backlog.target (location + "/backlog.data"); + replica = tc::Replica(location, create_if_missing); } //////////////////////////////////////////////////////////////////////////////// -// Add the new task to the appropriate file. -void TDB2::add (Task& task, bool add_to_backlog /* = true */) +// Add the new task to the replica. +void TDB2::add (Task& task, bool/* = true */) { // Ensure the task is consistent, and provide defaults if necessary. // bool argument to validate() is "applyDefault". Pass add_to_backlog through // in order to not apply defaults to synchronized tasks. - task.validate (add_to_backlog); + // This also ensures that the `uuid` attribute is set. + task.validate (true); + std::string uuid = task.get ("uuid"); + auto innertask = replica.import_task_with_uuid (uuid); - // If the tasks are loaded, then verify that this uuid is not already in - // the file. - if (!verifyUniqueUUID (uuid)) - throw format ("Cannot add task because the uuid '{1}' is not unique.", uuid); - - // Only locally-added tasks trigger hooks. This means that tasks introduced - // via 'sync' do not trigger hooks. - if (add_to_backlog) - Context::getContext ().hooks.onAdd (task); - - update (task, add_to_backlog, true); -} - -//////////////////////////////////////////////////////////////////////////////// -void TDB2::modify (Task& task, bool add_to_backlog /* = true */) -{ - // Ensure the task is consistent. - task.validate (false); - std::string uuid = task.get ("uuid"); - - // Get the unmodified task as reference, so the hook can compare. - if (add_to_backlog) { - Task original; - get (uuid, original); - Context::getContext ().hooks.onModify (original, task); - } + auto guard = replica.mutate_task(innertask); - update (task, add_to_backlog); -} + // add the task attributes + for (auto& attr : task.all ()) { + // TaskChampion does not store uuid or id in the taskmap + if (attr == "uuid" || attr == "id") { + continue; + } -//////////////////////////////////////////////////////////////////////////////// -void TDB2::update ( - Task& task, - const bool add_to_backlog, - const bool addition /* = false */) -{ - // Validate to add metadata. - task.validate (false); + // Use `set_status` for the task status, to get expected behavior + // with respect to the working set. + else if (attr == "status") { + innertask.set_status (Task::status2tc (Task::textToStatus (task.get (attr)))); + } - // If the task already exists, it is a modification, else addition. - Task original; - if (not addition && get (task.get ("uuid"), original)) - { - // Update only if the tasks differ - if (task == original) - return; + // use `set_modified` to set the modified timestamp, avoiding automatic + // updates to this field by TaskChampion. + else if (attr == "modified") { + auto mod = (time_t) std::stoi (task.get (attr)); + innertask.set_modified (mod); + } - if (add_to_backlog) - { - // All locally modified tasks are timestamped, implicitly overwriting any - // changes the user or hooks tried to apply to the "modified" attribute. - task.setAsNow ("modified"); + // otherwise, just set the k/v map value + else { + innertask.set_value (attr, std::make_optional (task.get (attr))); + } } - - // Update the task, wherever it is. - if (!pending.modify_task (task)) - completed.modify_task (task); - - // time