diff --git a/src/Context.cpp b/src/Context.cpp index 10409288e..efd422569 100644 --- a/src/Context.cpp +++ b/src/Context.cpp @@ -738,12 +738,11 @@ int Context::run() { #endif << ' ' << Datetime().toISO() - << " init:" << time_init_us << " load:" << time_load_us - << " gc:" << (time_gc_us > 0 ? time_gc_us - time_load_us : time_gc_us) + << " init:" << time_init_us << " load:" << time_load_us << " gc:" << time_gc_us << " filter:" << time_filter_us << " commit:" << time_commit_us << " sort:" << time_sort_us << " render:" << time_render_us << " hooks:" << time_hooks_us << " other:" - << time_total_us - time_init_us - time_gc_us - time_filter_us - time_commit_us - - time_sort_us - time_render_us - time_hooks_us + << time_total_us - time_init_us - time_load_us - time_gc_us - time_filter_us - + time_commit_us - time_sort_us - time_render_us - time_hooks_us << " total:" << time_total_us << '\n'; debug(s.str()); } @@ -1126,6 +1125,10 @@ void Context::staticInitialization() { for (auto& var : config.all()) if (var.substr(0, 13) == "urgency.user." || var.substr(0, 12) == "urgency.uda.") Task::coefficients[var] = config.getReal(var); + + // Pre-parse the coefficient keys, so urgency_c() doesn't have to re-parse for + // each task. + Task::setUrgencyCoefficients(); } //////////////////////////////////////////////////////////////////////////////// diff --git a/src/Filter.cpp b/src/Filter.cpp index 2e6d20a00..ae333e48a 100644 --- a/src/Filter.cpp +++ b/src/Filter.cpp @@ -40,112 +40,64 @@ // Take an input set of tasks and filter into a subset. void Filter::subset(const std::vector& input, std::vector& output) { Timer timer; + const auto load_before = Context::getContext().time_load_us; _startCount = (int)input.size(); Context::getContext().cli2.prepareFilter(); - std::vector> precompiled; - for (auto& a : Context::getContext().cli2._args) - if (a.hasTag("FILTER")) precompiled.emplace_back(a.getToken(), a._lextype); - - if (precompiled.size()) { - Eval eval; - eval.addSource(domSource); - - // Debug output from Eval during compilation is useful. During evaluation - // it is mostly noise. - eval.debug(Context::getContext().config.getInteger("debug.parser") >= 3 ? true : false); - eval.compileExpression(precompiled); - - for (auto& task : input) { - // Set up context for any DOM references. - auto currentTask = Context::getContext().withCurrentTask(&task); - - Variant var; - eval.evaluateCompiledExpression(var); - if (var.get_bool()) output.push_back(task); - } - - eval.debug(false); - } else - output = input; + filter_to_tasks(input, output); _endCount = (int)output.size(); Context::getContext().debug( format("Filtered {1} tasks --> {2} tasks [list subset]", _startCount, _endCount)); - Context::getContext().time_filter_us += timer.total_us(); + Context::getContext().time_filter_us += + timer.total_us() - (Context::getContext().time_load_us - load_before); } //////////////////////////////////////////////////////////////////////////////// // Take the set of all tasks and filter into a subset. void Filter::subset(std::vector& output) { Timer timer; + const auto load_before = Context::getContext().time_load_us; Context::getContext().cli2.prepareFilter(); std::vector> precompiled; for (auto& a : Context::getContext().cli2._args) if (a.hasTag("FILTER")) precompiled.emplace_back(a.getToken(), a._lextype); - // Shortcut indicates that only pending.data needs to be loaded. + // Shortcut indicates that only tasks in the working set are loaded. bool shortcut = false; if (precompiled.size()) { - Timer timer_pending; - auto pending = Context::getContext().tdb2.pending_tasks(); - Context::getContext().time_filter_us -= timer_pending.total_us(); + const auto& pending = Context::getContext().tdb2.pending_tasks(); _startCount = (int)pending.size(); - Eval eval; - eval.addSource(domSource); - - // Debug output from Eval during compilation is useful. During evaluation - // it is mostly noise. - eval.debug(Context::getContext().config.getInteger("debug.parser") >= 3 ? true : false); - eval.compileExpression(precompiled); - output.clear(); - for (auto& task : pending) { - // Set up context for any DOM references. - auto currentTask = Context::getContext().withCurrentTask(&task); - Variant var; - eval.evaluateCompiledExpression(var); - if (var.get_bool()) output.push_back(task); - } + filter_to_tasks(pending, output); shortcut = pendingOnly(); if (!shortcut) { - Timer timer_completed; - auto completed = Context::getContext().tdb2.completed_tasks(); - Context::getContext().time_filter_us -= timer_completed.total_us(); + const auto& completed = Context::getContext().tdb2.completed_tasks(); _startCount += (int)completed.size(); - for (auto& task : completed) { - // Set up context for any DOM references. - auto currentTask = Context::getContext().withCurrentTask(&task); - - Variant var; - eval.evaluateCompiledExpression(var); - if (var.get_bool()) output.push_back(task); - } + filter_to_tasks(completed, output); } - - eval.debug(false); } else { safety(); - Timer pending_completed; output = Context::getContext().tdb2.all_tasks(); - Context::getContext().time_filter_us -= pending_completed.total_us(); + _startCount = (int)output.size(); } _endCount = (int)output.size(); Context::getContext().debug(format("Filtered {1} tasks --> {2} tasks [{3}]", _startCount, _endCount, (shortcut ? "pending only" : "all tasks"))); - Context::getContext().time_filter_us += timer.total_us(); + Context::getContext().time_filter_us += + timer.total_us() - (Context::getContext().time_load_us - load_before); } -//////////////////////////////////////////////////////////////////////////////// +///////////////////////////////////////////////////////////////////////////////// bool Filter::hasFilter() const { for (const auto& a : Context::getContext().cli2._args) if (a.hasTag("FILTER")) return true; @@ -153,67 +105,128 @@ bool Filter::hasFilter() const { return false; } -//////////////////////////////////////////////////////////////////////////////// -// If the filter contains no 'or', 'xor' or 'not' operators, and only includes -// status values 'pending', 'waiting' or 'recurring', then the filter is -// guaranteed to only need data from pending.data. -bool Filter::pendingOnly() const { - // When GC is off, there are no shortcuts. - if (!Context::getContext().config.getBoolean("gc")) return false; +///////////////////////////////////////////////////////////////////////////////// +// Evaluates a pre-parsed filter against a set of tasks and stores their indices +// from the vector. The filter is parsed with prepareFilter(), but this +// function does not call that or safety() itself - callers are expected to do so. +void Filter::filter_to_indices(const std::vector& pending, std::vector& indices) const { + std::vector> precompiled; + for (auto& a : Context::getContext().cli2._args) + if (a.hasTag("FILTER")) precompiled.emplace_back(a.getToken(), a._lextype); - // To skip loading completed.data, there should be: - // - 'status' in filter - // - no 'completed' - // - no 'deleted' - // - no 'xor' - // - no 'or' - int countStatus = 0; - int countPending = 0; - int countWaiting = 0; - int countRecurring = 0; - int countId = (int)Context::getContext().cli2._id_ranges.size(); - int countUUID = (int)Context::getContext().cli2._uuid_list.size(); - int countOr = 0; - int countXor = 0; - int countNot = 0; - bool pendingTag = false; - bool activeTag = false; - - for (const auto& a : Context::getContext().cli2._args) { - if (a.hasTag("FILTER")) { - std::string raw = a.attribute("raw"); - std::string canonical = a.attribute("canonical"); - - if (a._lextype == Lexer::Type::op && raw == "or") ++countOr; - if (a._lextype == Lexer::Type::op && raw == "xor") ++countXor; - if (a._lextype == Lexer::Type::op && raw == "not") ++countNot; - if (a._lextype == Lexer::Type::dom && canonical == "status") ++countStatus; - if (raw == "pending") ++countPending; - if (raw == "waiting") ++countWaiting; - if (raw == "recurring") ++countRecurring; + if (precompiled.empty()) { + indices.reserve(pending.size()); + for (int i = 0; i < (int)pending.size(); ++i) indices.push_back(i); + } else { + Eval eval; + eval.addSource(domSource); + eval.debug(Context::getContext().config.getInteger("debug.parser") >= 3); + eval.compileExpression(precompiled); + for (int i = 0; i < (int)pending.size(); ++i) { + auto currentTask = Context::getContext().withCurrentTask(&pending[i]); + Variant var; + eval.evaluateCompiledExpression(var); + if (var.get_bool()) indices.push_back(i); } + eval.debug(false); } +} - for (const auto& word : Context::getContext().cli2._original_args) { - if (word.attribute("raw") == "+PENDING") pendingTag = true; - if (word.attribute("raw") == "+ACTIVE") activeTag = true; +//////////////////////////////////////////////////////////////////////////////// +// Like filter_to_indices, but copies matched tasks into the output. +void Filter::filter_to_tasks(const std::vector& input, std::vector& output) const { + std::vector> precompiled; + for (auto& a : Context::getContext().cli2._args) + if (a.hasTag("FILTER")) precompiled.emplace_back(a.getToken(), a._lextype); + + if (precompiled.empty()) { + output = input; + } else { + Eval eval; + eval.addSource(domSource); + eval.debug(Context::getContext().config.getInteger("debug.parser") >= 3); + eval.compileExpression(precompiled); + for (auto& task : input) { + auto currentTask = Context::getContext().withCurrentTask(&task); + Variant var; + eval.evaluateCompiledExpression(var); + if (var.get_bool()) output.push_back(task); + } + eval.debug(false); } +} - if (countUUID) return false; +//////////////////////////////////////////////////////////////////////////////// +// Recognizes pending only constraints that allow us to use the shortcut. - if (countOr || countXor || countNot) return false; +bool Filter::pendingOnly() const { + if (!Context::getContext().config.getBoolean("gc")) return false; - if (pendingTag || activeTag) return true; + const auto& cli = Context::getContext().cli2; + if (!cli._uuid_list.empty()) return false; - if (countStatus) { - if (!countPending && !countWaiting && !countRecurring) return false; + std::vector filter_args; + for (const auto& arg : cli._args) { + if (!arg.hasTag("FILTER")) continue; - return true; + filter_args.push_back(&arg); } - if (countId) return true; + const auto requires_pending = [&](const auto& self, size_t begin, size_t end) -> bool { + if (begin == end) return false; + int depth = 0; + size_t first_close = end; + std::vector conjunctions; + for (size_t i = begin; i < end; ++i) { + const auto& arg = *filter_args[i]; + if (arg._lextype != Lexer::Type::op) continue; + const auto& op = arg.attribute("raw"); + if (op == "(") { + ++depth; + } else if (op == ")") { + if (--depth < 0) return false; + if (depth == 0 && first_close == end) first_close = i; + } else if (depth == 0) { + if (op == "or" || op == "xor") return false; + if (op == "and") conjunctions.push_back(i); + } + } - return false; + if (depth != 0) return false; + if (filter_args[begin]->_lextype == Lexer::Type::op && + filter_args[begin]->attribute("raw") == "(" && first_close == end - 1) + return self(self, begin + 1, end - 1); + + if (!conjunctions.empty()) { + bool required = false; + for (auto boundary : conjunctions) { + required |= self(self, begin, boundary); + begin = boundary + 1; + } + return self(self, begin, end) || required; + } + + if (end - begin != 3) return false; + const auto& left = *filter_args[begin]; + const auto& op = *filter_args[begin + 1]; + const auto& right = *filter_args[begin + 2]; + if (left._lextype != Lexer::Type::dom || op._lextype != Lexer::Type::op) return false; + const auto& operation = op.attribute("raw"); + const auto& value = right.attribute("raw"); + + if (left.attribute("raw") == "id" && right._lextype == Lexer::Type::number && + (operation == "=" || operation == "==" || operation == ">=") && + value.find_first_not_of("0123456789") == std::string::npos && + value.find_first_not_of('0') != std::string::npos) + return true; + if (right._lextype != Lexer::Type::string) return false; + return (left.attribute("canonical") == "status" && (operation == "=" || operation == "==") && + (value == "pending" || value == "waiting" || value == "recurring")) || + (left.attribute("raw") == "tags" && operation == "_hastag_" && + (value == "PENDING" || value == "ACTIVE" || value == "READY" || value == "WAITING")); + }; + + return requires_pending(requires_pending, 0, filter_args.size()); } //////////////////////////////////////////////////////////////////////////////// diff --git a/src/Filter.h b/src/Filter.h index bb7449318..dfd80a3e9 100644 --- a/src/Filter.h +++ b/src/Filter.h @@ -40,6 +40,13 @@ class Filter { void subset(std::vector&); bool hasFilter() const; bool pendingOnly() const; + + // These evaluate already-prepared filters (ie. caller calls + // cli2.prepareFilter() and safety()). filter_to_indices() stores matching + // indices, filter_to_tasks() copies task objects. + void filter_to_indices(const std::vector&, std::vector&) const; + void filter_to_tasks(const std::vector&, std::vector&) const; + void safety() const; void disableSafety(); diff --git a/src/Hooks.cpp b/src/Hooks.cpp index 1d1e440a4..ad73ab18b 100644 --- a/src/Hooks.cpp +++ b/src/Hooks.cpp @@ -30,6 +30,7 @@ #include #include +#include // If is included, put it after , because it includes // , and therefore would ignore the _WITH_GETLINE. #ifdef FREEBSD @@ -313,7 +314,9 @@ void Hooks::onModify(Task& before, Task& after) const { } } - after = Task(input[1]); + Task updated(input[1]); + updated.copyTransientState(after); + after = std::move(updated); } Context::getContext().time_hooks_us += timer.total_us(); @@ -322,6 +325,9 @@ void Hooks::onModify(Task& before, Task& after) const { //////////////////////////////////////////////////////////////////////////////// std::vector Hooks::list() const { return _scripts; } +//////////////////////////////////////////////////////////////////////////////// +bool Hooks::hasOnModify() const { return _enabled && !scripts("on-modify").empty(); } + //////////////////////////////////////////////////////////////////////////////// std::vector Hooks::scripts(const std::string& event) const { std::vector matching; diff --git a/src/Hooks.h b/src/Hooks.h index 448c59744..894599900 100644 --- a/src/Hooks.h +++ b/src/Hooks.h @@ -41,6 +41,7 @@ class Hooks { void onExit() const; void onAdd(Task&) const; void onModify(Task&, Task&) const; + bool hasOnModify() const; std::vector list() const; private: diff --git a/src/TDB2.cpp b/src/TDB2.cpp index d559e0c1d..b32a1581a 100644 --- a/src/TDB2.cpp +++ b/src/TDB2.cpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include #include @@ -42,9 +43,16 @@ #include #include +#include "taskchampion-cpp/lib.h" + bool TDB2::debug_mode = false; // This functions main job is to set Task::is_blocked / Task::is_blocking flags. -static void dependency_scan(std::vector&, const std::unordered_map&); +static void dependency_scan(std::vector&, const std::unordered_map&, + std::unordered_map* = nullptr); +static void dependency_update(std::vector&, const std::unordered_map&, + std::unordered_map&, size_t, + const std::vector&, const std::vector&); +static bool participates_in_dependency_graph(const Task&); // Build maps for dependency queries. static DependencyGraph build_dependency_graph(const std::vector&, @@ -64,6 +72,8 @@ static tc::Status statusFromString(const std::string& s) { //////////////////////////////////////////////////////////////////////////////// void TDB2::open_replica(const std::string& location, bool create_if_missing, bool read_write) { _replica = tc::new_replica_on_disk(location, create_if_missing, read_write); + invalidate_cached_info(); + changes.clear(); } //////////////////////////////////////////////////////////////////////////////// @@ -78,7 +88,6 @@ void TDB2::add(Task& task) { maybe_add_undo_point(ops); auto uuid = task.get_ref("uuid"); - changes[uuid] = task; tc::Uuid tcuuid = tc::uuid_from_string(uuid); // run hooks for this new task @@ -103,7 +112,7 @@ void TDB2::add(Task& task) { if (!deferred_status.empty()) { tctask->set_status(statusFromString(deferred_status), ops); } - replica()->commit_operations(std::move(ops)); + commit_operations(std::move(ops)); invalidate_cached_info(); @@ -112,6 +121,7 @@ void TDB2::add(Task& task) { if (id > 0) { task.id = id; } + changes[uuid] = task; } //////////////////////////////////////////////////////////////////////////////// @@ -129,24 +139,30 @@ void TDB2::add(Task& task) { // this method. In this case, this method throws an error that will make sense // to the user. This is especially unlikely since tasks are only deleted when // they have been unmodified for a long time. -void TDB2::modify(Task& task) { +Task TDB2::prepare_modify(Task& task) { // All locally modified tasks are timestamped, implicitly overwriting any // changes the user or hooks tried to apply to the "modified" attribute. task.setAsNow("modified"); task.validate(false); auto uuid = task.get_ref("uuid"); - rust::Vec ops; - maybe_add_undo_point(ops); - - changes[uuid] = task; - // invoke the hook and allow it to modify the task before updating Task original; - bool found_original = get(uuid, original); + tc::Uuid tcuuid = tc::uuid_from_string(uuid); + if (!_pending_tasks && task.id > 0) { + auto maybe_original = replica()->get_task_data(tcuuid); + if (maybe_original.is_some()) { + original = Task{maybe_original.take(), task.id}; + } + } else { + get(uuid, original); + } Context::getContext().hooks.onModify(original, task); + return original; +} - tc::Uuid tcuuid = tc::uuid_from_string(uuid); +void TDB2::append_modify(const Task& task, const Task& original, rust::Vec& ops) { + auto tcuuid = tc::uuid_from_string(task.get_ref("uuid")); auto maybe_tctask = replica()->get_task(tcuuid); if (maybe_tctask.is_none()) { throw std::string("task no longer exists"); @@ -189,17 +205,47 @@ void TDB2::modify(Task& task) { if (deferred_status) { tctask->set_status(statusFromString(deferred_status.value()), ops); } +} - replica()->commit_operations(std::move(ops)); +void TDB2::modify(Task& task, const std::function(const Task&)>& related) { + auto original = prepare_modify(task); + std::optional related_task; + std::optional related_task_original; + + if (related) { + related_task = related(task); + if (related_task) { + if (related_task->get_ref("uuid") == task.get_ref("uuid")) + throw std::string("A related-task modification must refer to a different task."); + related_task_original = prepare_modify(*related_task); + } + } + + rust::Vec ops; + maybe_add_undo_point(ops); + append_modify(task, original, ops); + if (related_task) append_modify(*related_task, *related_task_original, ops); + commit_operations(std::move(ops)); + const auto& uuid = task.get_ref("uuid"); + changes[uuid] = task; + if (related_task) { + changes[related_task->get_ref("uuid")] = *related_task; + invalidate_cached_info(); + return; + } - // If the task entered or left the pending set, we must invalidate the cache. - bool was_pending = found_original && (original.getStatus() == Task::pending); - bool now_pending = task.getStatus() == Task::pending; - if (was_pending != now_pending || !found_original) { + // If the task entered or left the working set/dependency graph, we must + // invalidate the cache. + bool found_original = original.has("uuid"); + bool was_active = found_original && participates_in_dependency_graph(original); + bool now_active = participates_in_dependency_graph(task); + if (was_active != now_active || !found_original) { invalidate_cached_info(); return; } + Task::invalidateUrgencyCaches(); + // If the task stayed in the set, we can edit the vector in-place. // This speeds up modifications a lot relative to reloading and parsing from rust. bool deps_changed = false; @@ -207,11 +253,16 @@ void TDB2::modify(Task& task) { auto* pt = find_pending(uuid); if (pt) { auto old_deps = pt->getDependencyUUIDs(); + task.copyTransientState(*pt); *pt = task; auto new_deps = task.getDependencyUUIDs(); if (old_deps != new_deps) { deps_changed = true; - dependency_scan(*_pending_tasks, pending_index()); + auto& index = pending_index(); + dependency_update(*_pending_tasks, index, *_pending_dependency_counts, index.at(uuid), + old_deps, new_deps); + task.is_blocked = pt->is_blocked; + task.is_blocking = pt->is_blocking; } } } @@ -231,7 +282,7 @@ void TDB2::purge(Task& task) { if (maybe_tctask.is_some()) { auto tctask = maybe_tctask.take(); tctask->delete_task(ops); - replica()->commit_operations(std::move(ops)); + commit_operations(std::move(ops)); } invalidate_cached_info(); @@ -243,6 +294,12 @@ rust::Box& TDB2::replica() { assert(_replica); return _replica.value(); } +//////////////////////////////////////////////////////////////////////////////// +void TDB2::commit_operations(rust::Vec&& ops) { + Timer timer; + replica()->commit_operations(std::move(ops)); + Context::getContext().time_commit_us += timer.total_us(); +} //////////////////////////////////////////////////////////////////////////////// const rust::Box& TDB2::working_set() { @@ -271,17 +328,43 @@ void TDB2::get_changes(std::vector& changes) { //////////////////////////////////////////////////////////////////////////////// void TDB2::gc() { Timer timer; + auto load_before = Context::getContext().time_load_us; // Allowed as an override, but not recommended. - if (Context::getContext().config.getBoolean("gc")) { + if (Context::getContext().config.getBoolean("gc") && !working_set_is_clean()) { replica()->rebuild_working_set(true); + invalidate_cached_info(); } - Context::getContext().time_gc_us += timer.total_us(); + Context::getContext().time_gc_us += + timer.total_us() - (Context::getContext().time_load_us - load_before); +} + +bool TDB2::working_set_is_clean() { + const auto& ws = working_set(); + const auto& tasks = pending_tasks(); + const auto largest = ws->largest_index(); + + if (tasks.size() != largest) return false; + + for (size_t i = 1; i <= largest; ++i) { + if (ws->by_index(i).is_nil()) return false; + } + + return std::all_of(tasks.begin(), tasks.end(), [](const Task& task) { + const auto& status = task.get_ref("status"); + return status == "pending" || status == "recurring" || status == "iterative"; + }); +} + +//////////////////////////////////////////////////////////////////////////////// +void TDB2::expire_tasks() { + replica()->expire_tasks(); + invalidate_cached_info(); } //////////////////////////////////////////////////////////////////////////////// -void TDB2::expire_tasks() { replica()->expire_tasks(); } +void TDB2::invalidate_cache() { invalidate_cached_info(); } //////////////////////////////////////////////////////////////////////////////// // Latest ID is that of the last pending task. @@ -337,7 +420,8 @@ const std::vector& TDB2::pending_tasks() { for (size_t i = 0, n = result.size(); i < n; ++i) _pending_index->emplace(result[i].get_ref("uuid"), i); - dependency_scan(result, *_pending_index); + _pending_dependency_counts.emplace(); + dependency_scan(result, *_pending_index, &*_pending_dependency_counts); Context::getContext().time_load_us += timer.total_us(); _pending_tasks = std::move(result); @@ -352,6 +436,7 @@ const std::vector& TDB2::pending_tasks() { // which involve completed tasks. const std::vector& TDB2::completed_tasks() { if (!_completed_tasks) { + Timer timer; auto all_tctasks = replica()->all_task_data(); auto& ws = working_set(); @@ -367,6 +452,7 @@ const std::vector& TDB2::completed_tasks() { } } _completed_tasks = std::move(result); + Context::getContext().time_load_us += timer.total_us(); } return *_completed_tasks; } @@ -387,6 +473,7 @@ const DependencyGraph& TDB2::dependency_graph() { ///////////////////////////////////////////////////////////////////////////////// // This builds and returns the UUID map if it is missing. const std::unordered_map& TDB2::pending_index() { + pending_tasks(); if (!_pending_index) { _pending_index.emplace(); _pending_index->reserve(_pending_tasks->size()); @@ -409,11 +496,13 @@ Task* TDB2::find_pending(const std::string& uuid) { //////////////////////////////////////////////////////////////////////////////// void TDB2::invalidate_cached_info() { + Task::invalidateUrgencyCaches(); _pending_tasks = std::nullopt; _completed_tasks = std::nullopt; _working_set = std::nullopt; _dependency_graph = std::nullopt; _pending_index = std::nullopt; + _pending_dependency_counts = std::nullopt; } //////////////////////////////////////////////////////////////////////////////// @@ -423,8 +512,6 @@ bool TDB2::get(int id, Task& task) { const auto tcuuid = ws->by_index(id); if (!tcuuid.is_nil()) { std::string uuid = static_cast(tcuuid.to_string()); - // Load index of pending tasks. - pending_tasks(); // Lookup the UUID in the index instead of scanning the vector. auto* pt = find_pending(uuid); if (pt) { @@ -439,8 +526,6 @@ bool TDB2::get(int id, Task& task) { //////////////////////////////////////////////////////////////////////////////// // Locate task by UUID, including by partial ID, wherever it is. bool TDB2::get(const std::string& uuid, Task& task) { - pending_tasks(); - // Try to match exact UUID within the index. auto* pt = find_pending(uuid); if (pt) { @@ -448,6 +533,18 @@ bool TDB2::get(const std::string& uuid, Task& task) { return true; } + if (uuid.length() == 36) { + Lexer lexer(uuid); + Lexer::Type type; + std::string token; + if (lexer.isUUID(token, type, true) && token.length() == uuid.length()) { + auto maybe_tctask = replica()->get_task_data(tc::uuid_from_string(uuid)); + if (maybe_tctask.is_none()) return false; + task = Task{maybe_tctask.take()}; + return true; + } + } + // try a partial match if (uuid.length() < 36) { for (const auto& pending_task : *_pending_tasks) { @@ -539,7 +636,9 @@ int TDB2::num_reverts_possible() { return (int)replica()->num_undo_points(); } //////////////////////////////////////////////////////////////////////////////// // Set Task::is_blocked / Task::is_blocking flags using the pre-built UUID map static void dependency_scan(std::vector& tasks, - const std::unordered_map& uuid_index) { + const std::unordered_map& uuid_index, + std::unordered_map* dependency_counts) { + if (dependency_counts) dependency_counts->clear(); // Reset all flags first. This is for safety reasons - if we don't do this // dependency_scan() only sets them to true, so it can stay true (within the cache) // even after we have changed a task's dependencies after a modify when @@ -551,22 +650,70 @@ static void dependency_scan(std::vector& tasks, task.is_blocked = false; } for (size_t i = 0; i < tasks.size(); ++i) { - auto lstatus = tasks[i].getStatus(); + if (!participates_in_dependency_graph(tasks[i])) continue; for (const auto& dep : tasks[i].getDependencyUUIDs()) { auto it = uuid_index.find(dep); if (it == uuid_index.end()) continue; size_t j = it->second; - auto rstatus = tasks[j].getStatus(); - if (lstatus != Task::completed && lstatus != Task::deleted && rstatus != Task::completed && - rstatus != Task::deleted) { + if (participates_in_dependency_graph(tasks[j])) { tasks[i].is_blocked = true; tasks[j].is_blocking = true; + if (dependency_counts) ++(*dependency_counts)[dep]; } } } } +///////////////////////////////////////////////////////////////////////////////// +static void dependency_update(std::vector& tasks, + const std::unordered_map& uuid_index, + std::unordered_map& dependency_counts, + size_t task_index, const std::vector& old_deps, + const std::vector& new_deps) { + std::unordered_set old_set(old_deps.begin(), old_deps.end()); + std::unordered_set new_set(new_deps.begin(), new_deps.end()); + auto& task = tasks[task_index]; + + task.is_blocked = false; + bool task_is_active = participates_in_dependency_graph(task); + + for (const auto& dep : old_set) { + if (new_set.find(dep) != new_set.end()) continue; + auto target = uuid_index.find(dep); + if (target == uuid_index.end()) continue; + if (!task_is_active || !participates_in_dependency_graph(tasks[target->second])) continue; + + auto count = dependency_counts.find(dep); + if (count != dependency_counts.end()) { + if (--count->second == 0) dependency_counts.erase(count); + } + tasks[target->second].is_blocking = dependency_counts.find(dep) != dependency_counts.end(); + } + + for (const auto& dep : new_set) { + auto target = uuid_index.find(dep); + if (target == uuid_index.end()) continue; + + bool target_is_active = participates_in_dependency_graph(tasks[target->second]); + if (!task_is_active || !target_is_active) continue; + + task.is_blocked = true; + if (old_set.find(dep) == old_set.end()) ++dependency_counts[dep]; + tasks[target->second].is_blocking = true; + } + + const auto& uuid = task.get_ref("uuid"); + task.is_blocking = dependency_counts.find(uuid) != dependency_counts.end(); +} + +////////////////////////////////////////////////////////////////////////////////// +// Defined here for reuse. +static bool participates_in_dependency_graph(const Task& task) { + auto status = task.getStatus(); + return status != Task::completed && status != Task::deleted; +} + ///////////////////////////////////////////////////////////////////////////////// // Build the full dependency map from the task vector. static DependencyGraph build_dependency_graph( diff --git a/src/TDB2.h b/src/TDB2.h index a3d671c59..ddc88d514 100644 --- a/src/TDB2.h +++ b/src/TDB2.h @@ -30,6 +30,7 @@ #include #include +#include #include #include #include @@ -57,11 +58,14 @@ class TDB2 { void open_replica(const std::string&, bool create_if_missing, bool read_write); void add(Task&); - void modify(Task&); + // The optional related task change is taken after the first task's hooks have ran. + // Both tasks' hooks then finish before either modification is committed. + void modify(Task&, const std::function(const Task&)>& related = {}); void purge(Task&); void get_changes(std::vector&); void gc(); void expire_tasks(); + void invalidate_cache(); int latest_id(); // Generalized task accessors. @@ -73,8 +77,7 @@ class TDB2 { const std::vector& pending_tasks(); const std::vector& completed_tasks(); // dependency_graph is built on first use from pending_tasks() and reused. - // functions that use it are Task::getDependencyTasks(), getBlockedTasks() - // and urgency_inherit(). + // It is used by Task::getBlockedTasks() and urgency_inherit(). const DependencyGraph& dependency_graph(); bool get(int, Task&); @@ -111,6 +114,7 @@ class TDB2 { // Lazily cache UUIDs within the pending set. // Avoids scans of the vectors with get/modify.. std::optional> _pending_index; + std::optional> _pending_dependency_counts; void invalidate_cached_info(); // Return the full pending UUID map. @@ -120,6 +124,10 @@ class TDB2 { std::map changes; const rust::Box& working_set(); + Task prepare_modify(Task&); + void append_modify(const Task&, const Task&, rust::Vec&); + void commit_operations(rust::Vec&&); + bool working_set_is_clean(); void maybe_add_undo_point(rust::Vec&); }; diff --git a/src/Task.cpp b/src/Task.cpp index 6608db850..682b0472a 100644 --- a/src/Task.cpp +++ b/src/Task.cpp @@ -74,6 +74,9 @@ bool Task::regex = false; std::map Task::attributes; std::map Task::coefficients; +std::vector Task::userCoefficients; +bool Task::urgencyInherit = false; +uint64_t Task::urgencyGeneration = 1; float Task::urgencyProjectCoefficient = 0.0; float Task::urgencyActiveCoefficient = 0.0; float Task::urgencyScheduledCoefficient = 0.0; @@ -114,6 +117,13 @@ bool Task::operator==(const Task& other) { //////////////////////////////////////////////////////////////////////////////// bool Task::operator!=(const Task& other) { return !(*this == other); } +//////////////////////////////////////////////////////////////////////////////// +void Task::copyTransientState(const Task& other) { + id = other.id; + is_blocked = other.is_blocked; + is_blocking = other.is_blocking; +} + //////////////////////////////////////////////////////////////////////////////// Task::Task(const std::string& input) { id = 0; @@ -139,7 +149,9 @@ Task::Task(const json::object* obj) { } //////////////////////////////////////////////////////////////////////////////// -Task::Task(rust::Box obj) { +Task::Task(rust::Box obj) : Task(std::move(obj), -1) {} + +Task::Task(rust::Box obj, int known_id) { id = 0; urgency_value = 0.0; recalc_urgency = true; @@ -147,7 +159,7 @@ Task::Task(rust::Box obj) { is_blocking = false; annotation_count = 0; - parseTC(std::move(obj)); + parseTC(std::move(obj), known_id); } //////////////////////////////////////////////////////////////////////////////// @@ -191,9 +203,9 @@ const std::string Task::identifier(bool shortened /* = false */) const { if (id != 0) return format(id); else if (shortened) - return get("uuid").substr(0, 8); + return get_ref("uuid").substr(0, 8); else - return get("uuid"); + return get_ref("uuid"); } //////////////////////////////////////////////////////////////////////////////// @@ -718,23 +730,22 @@ void Task::parseJSON(const json::object* root_obj) { //////////////////////////////////////////////////////////////////////////////// // Note that all fields undergo encode/decode. -void Task::parseTC(rust::Box task) { +void Task::parseTC(rust::Box task, int known_id) { auto items = task->items(); data.clear(); - for (auto& item : items) { - data[static_cast(item.prop)] = static_cast(item.value); - } // count annotations annotation_count = 0; - for (auto i : data) { - if (isAnnotationAttr(i.first)) { + for (auto& item : items) { + auto key = static_cast(item.prop); + data[key] = static_cast(item.value); + if (isAnnotationAttr(key)) { ++annotation_count; } } data["uuid"] = static_cast(task->get_uuid().to_string()); - id = Context::getContext().tdb2.id(data["uuid"]); + id = known_id >= 0 ? known_id : Context::getContext().tdb2.id(data["uuid"]); } //////////////////////////////////////////////////////////////////////////////// @@ -1045,53 +1056,72 @@ bool Task::hasDependency(const std::string& uuid) const { //////////////////////////////////////////////////////////////////////////////// std::vector Task::getDependencyIDs() const { std::vector ids; - for (auto& attr : all()) { - if (!isDepAttr(attr)) continue; - auto dep = attr2Dep(attr); - ids.push_back(Context::getContext().tdb2.id(dep)); + + auto dependencies = getDependencyUUIDs(); + ids.reserve(dependencies.size()); + for (const auto& dependency : dependencies) { + auto* task = Context::getContext().tdb2.find_pending(dependency); + if (!task) continue; + auto status = task->getStatus(); + if (status != Task::completed && status != Task::deleted) ids.push_back(task->id); } + std::sort(ids.begin(), ids.end()); + return ids; } //////////////////////////////////////////////////////////////////////////////// std::vector Task::getDependencyUUIDs() const { std::vector uuids; - for (auto& attr : all()) { - if (!isDepAttr(attr)) continue; - auto dep = attr2Dep(attr); - uuids.push_back(dep); + for (auto& pair : data) { + if (!isDepAttr(pair.first)) continue; + uuids.push_back(attr2Dep(pair.first)); } return uuids; } //////////////////////////////////////////////////////////////////////////////// +// Return the pending task this task depends on using cached UUID lookup. std::vector Task::getDependencyTasks() const { - auto uuids = getDependencyUUIDs(); - - // NOTE: this may seem inefficient, but note that `TDB2::get` performs a - // linear search on each invocation, so scanning *once* is quite a bit more - // efficient. std::vector blocking; - if (uuids.size() > 0) - for (auto& it : Context::getContext().tdb2.pending_tasks()) - if (it.getStatus() != Task::completed && it.getStatus() != Task::deleted && - std::find(uuids.begin(), uuids.end(), it.get("uuid")) != uuids.end()) - blocking.push_back(it); + + auto dependencies = getDependencyUUIDs(); + blocking.reserve(dependencies.size()); + for (const auto& dependency : dependencies) { + auto* task = Context::getContext().tdb2.find_pending(dependency); + if (task && task->getStatus() != Task::completed && task->getStatus() != Task::deleted) + blocking.push_back(*task); + } + + std::sort(blocking.begin(), blocking.end(), + [](const Task& left, const Task& right) { return left.id < right.id; }); return blocking; } //////////////////////////////////////////////////////////////////////////////// +// Returns the pending tasks that depend on a given task. std::vector Task::getBlockedTasks() const { - auto uuid = get("uuid"); + const auto& uuid = get_ref("uuid"); std::vector blocked; - for (auto& it : Context::getContext().tdb2.pending_tasks()) - if (it.getStatus() != Task::completed && it.getStatus() != Task::deleted && - it.hasDependency(uuid)) - blocked.push_back(it); + + auto& graph = Context::getContext().tdb2.dependency_graph(); + auto found = graph.dependents.find(uuid); + + if (found == graph.dependents.end()) return blocked; + + blocked.reserve(found->second.size()); + + const auto& tasks = Context::getContext().tdb2.pending_tasks(); + for (auto idx : found->second) + if (tasks[idx].getStatus() != Task::completed && tasks[idx].getStatus() != Task::deleted) + blocked.push_back(tasks[idx]); + + std::sort(blocked.begin(), blocked.end(), + [](const Task& left, const Task& right) { return left.id < right.id; }); return blocked; } @@ -1692,6 +1722,51 @@ int Task::determineVersion(const std::string& line) { return 0; } +///////////////////////////////////////////////////////////////////////////////// +// Pre-parse the user's UDA/urgency coefficient keys once, instead of reparsing +// for every task in the report. The keys are static. +void Task::setUrgencyCoefficients() { + urgencyInherit = Context::getContext().config.getBoolean("urgency.inherit"); + + userCoefficients.clear(); + userCoefficients.reserve(coefficients.size()); + + for (const auto& var : coefficients) { + const std::string& key = var.first; + float coeff = var.second; + if (fabs(coeff) <= epsilon) continue; + + auto end = key.find(".coefficient"); + if (end == std::string::npos) continue; + + if (!key.compare(0, 13, "urgency.user.", 13)) { + if (!key.compare(13, 8, "project.", 8)) + userCoefficients.push_back( + {UrgencyCoefficient::project, key.substr(21, end - 21), "", coeff}); + else if (!key.compare(13, 4, "tag.", 4)) + userCoefficients.push_back({UrgencyCoefficient::tag, key.substr(17, end - 17), "", coeff}); + else if (!key.compare(13, 8, "keyword.", 8)) + userCoefficients.push_back( + {UrgencyCoefficient::keyword, key.substr(21, end - 21), "", coeff}); + } else if (!key.compare(0, 12, "urgency.uda.", 12)) { + // covers both urgency.uda..coefficient and + // urgency.uda...coefficient + std::string uda = key.substr(12, end - 12); + auto dot = uda.find('.'); + if (dot == std::string::npos) + userCoefficients.push_back({UrgencyCoefficient::uda, std::move(uda), "", coeff}); + else + userCoefficients.push_back( + {UrgencyCoefficient::udaValue, uda.substr(0, dot), uda.substr(dot + 1), coeff}); + } + } + + invalidateUrgencyCaches(); +} + +//////////////////////////////////////////////////////////////////////////////// +void Task::invalidateUrgencyCaches() { ++urgencyGeneration; } + //////////////////////////////////////////////////////////////////////////////// // Urgency is defined as a polynomial, the value of which is calculated in this // function, according to: @@ -1740,58 +1815,45 @@ float Task::urgency_c() const { value += fabsf(Task::urgencyAgeCoefficient) > epsilon ? (urgency_age() * Task::urgencyAgeCoefficient) : 0.0; - - const std::string taskProjectName = get("project"); - // Tag- and project-specific coefficients. - for (auto& var : Task::coefficients) { - if (fabs(var.second) > epsilon) { - if (!var.first.compare(0, 13, "urgency.user.", 13)) { - // urgency.user.project..coefficient - auto end = std::string::npos; - if (var.first.substr(13, 8) == "project." && - (end = var.first.find(".coefficient")) != std::string::npos) { - std::string project = var.first.substr(21, end - 21); - - if (taskProjectName == project || taskProjectName.find(project + '.') == 0) { - value += var.second; - } - } - - // urgency.user.tag..coefficient - if (var.first.substr(13, 4) == "tag." && - (end = var.first.find(".coefficient")) != std::string::npos) { - std::string tag = var.first.substr(17, end - 17); - - if (hasTag(tag)) value += var.second; - } - - // urgency.user.keyword..coefficient - if (var.first.substr(13, 8) == "keyword." && - (end = var.first.find(".coefficient")) != std::string::npos) { - std::string keyword = var.first.substr(21, end - 21); - - if (get("description").find(keyword) != std::string::npos) value += var.second; - } - } else if (var.first.substr(0, 12) == "urgency.uda.") { - // urgency.uda..coefficient - // urgency.uda...coefficient - auto end = var.first.find(".coefficient"); - if (end != std::string::npos) { - const std::string uda = var.first.substr(12, end - 12); - auto dot = uda.find('.'); - if (dot == std::string::npos) { - // urgency.uda..coefficient - if (has(uda)) value += var.second; - } else { - // urgency.uda...coefficient - if (get(uda.substr(0, dot)) == uda.substr(dot + 1)) value += var.second; - } - } + // Tag-, project-, keyword-, and UDA specific coefficients. + // We pre-parse with Task::userCoefficients so we can make + // per-task attribute lookups. We cache project and description + // across multiple coefficients of the same kind to avoid + // repeated lookups. + // The reason we do it with description, despite descriptions + // varying a large amount, is for the keyword coefficient. + // We don't do it with tag/UDAs because each refer to a unique one. + const std::string* projectName = nullptr; + const std::string* description = nullptr; + + for (const auto& uc : Task::userCoefficients) { + switch (uc.kind) { + case UrgencyCoefficient::project: { + if (projectName == nullptr) projectName = &get_ref("project"); + // Match exact project or subproject prefix. + if (*projectName == uc.name || (projectName->size() > uc.name.size() && + projectName->compare(0, uc.name.size(), uc.name) == 0 && + (*projectName)[uc.name.size()] == '.')) + value += uc.coefficient; + break; } + case UrgencyCoefficient::tag: + if (hasTag(uc.name)) value += uc.coefficient; + break; + case UrgencyCoefficient::keyword: + if (description == nullptr) description = &get_ref("description"); + if (description->find(uc.name) != std::string::npos) value += uc.coefficient; + break; + case UrgencyCoefficient::uda: + if (has(uc.name)) value += uc.coefficient; + break; + case UrgencyCoefficient::udaValue: + if (get_ref(uc.name) == uc.value) value += uc.coefficient; + break; } } - if (is_blocking && Context::getContext().config.getBoolean("urgency.inherit")) { + if (is_blocking && Task::urgencyInherit) { float prev = value; value = std::max(value, urgency_inherit()); @@ -1805,26 +1867,36 @@ float Task::urgency_c() const { } //////////////////////////////////////////////////////////////////////////////// -float Task::urgency() { - if (recalc_urgency) { - urgency_value = urgency_c(); - - // Return the sum of all terms. +float Task::urgency() const { + if (recalc_urgency || urgency_generation != urgencyGeneration) { + // We set the guard first to avoid infinite recursion. It will be 0.0 on + // first call and then the computed value will be reused from the cache. + urgency_value = 0.0; recalc_urgency = false; + urgency_generation = urgencyGeneration; + urgency_value = urgency_c(); } return urgency_value; } //////////////////////////////////////////////////////////////////////////////// +// Inherit urgency from the highest-urgency task dependent on this one. Uses +// the dependency graph via TDB2::dependency_graph() instead of scanning. float Task::urgency_inherit() const { float v = -FLT_MAX; #ifdef PRODUCT_TASKWARRIOR - // Calling getBlockedTasks is rather expensive. - // It is called recursively for each dependency in the chain here. - for (auto& task : getBlockedTasks()) { - // Find highest urgency in all blocked tasks. - v = std::max(v, task.urgency()); + auto& graph = Context::getContext().tdb2.dependency_graph(); + const auto& uuid = get_ref("uuid"); + auto found = graph.dependents.find(uuid); + + if (found != graph.dependents.end()) { + const auto& tasks = Context::getContext().tdb2.pending_tasks(); + for (auto idx : found->second) { + if (tasks[idx].getStatus() != Task::completed && tasks[idx].getStatus() != Task::deleted) { + v = std::max(v, tasks[idx].urgency()); + } + } } #endif diff --git a/src/Task.h b/src/Task.h index 0b577baff..545c21094 100644 --- a/src/Task.h +++ b/src/Task.h @@ -33,12 +33,22 @@ #include #include +#include #include #include #include class Task { public: + // A user/UDA coefficient with a pre-parsed key, so urgency_c() + // doesn't re-parse each time. + struct UrgencyCoefficient { + enum Kind { project, tag, keyword, uda, udaValue } kind; + std::string name; // The name of the project/tag/keyword/UDA. + std::string value; // only for udaValue + float coefficient; + }; + static std::string defaultProject; static std::string defaultDue; static std::string defaultScheduled; @@ -46,6 +56,11 @@ class Task { static bool regex; static std::map attributes; // name -> type static std::map coefficients; + // Parsed version of coefficients, built by setUrgencyCoefficients(). + static std::vector userCoefficients; + // Cached value of rc.urgency.inherit. + static bool urgencyInherit; + static uint64_t urgencyGeneration; static std::map> customOrder; static float urgencyProjectCoefficient; static float urgencyActiveCoefficient; @@ -63,9 +78,11 @@ class Task { Task() = default; bool operator==(const Task&); bool operator!=(const Task&); + void copyTransientState(const Task&); Task(const std::string&); Task(const json::object*); Task(rust::Box); + Task(rust::Box, int); void parse(const std::string&); std::string composeJSON(bool decorate = false); @@ -78,8 +95,9 @@ class Task { // Public data. int id{0}; - float urgency_value{0.0}; - bool recalc_urgency{true}; + mutable float urgency_value{0.0}; + mutable bool recalc_urgency{true}; + mutable uint64_t urgency_generation{0}; bool is_blocked{false}; bool is_blocking{false}; int annotation_count{0}; @@ -88,6 +106,11 @@ class Task { static status textToStatus(const std::string&); static std::string statusToText(status); + // Parse coefficients into userCoefficients. This is called + // after that map has been initialized. + static void setUrgencyCoefficients(); + static void invalidateUrgencyCaches(); + void setAsNow(const std::string&); bool has(const std::string&) const; std::vector all() const; @@ -172,7 +195,7 @@ class Task { void validate(bool applyDefault = true); float urgency_c() const; - float urgency(); + float urgency() const; #ifdef PRODUCT_TASKWARRIOR enum modType { modReplace, modPrepend, modAppend, modAnnotate }; @@ -185,7 +208,7 @@ class Task { int determineVersion(const std::string&); void parseJSON(const std::string&); void parseJSON(const json::object*); - void parseTC(rust::Box); + void parseTC(rust::Box, int); void parseLegacy(const std::string&); void validate_before(const std::string&, const std::string&); const std::string encode(const std::string&) const; diff --git a/src/ViewTask.cpp b/src/ViewTask.cpp index 5c04ec8f9..db0846f50 100644 --- a/src/ViewTask.cpp +++ b/src/ViewTask.cpp @@ -105,8 +105,9 @@ ViewTask::~ViewTask() { // the larger fields. If the widest field is W0, and the second widest // field is W1, then a solution may be achievable by reducing W0 --> W1. // -std::string ViewTask::render(std::vector& data, std::vector& sequence) { +std::string ViewTask::render(const std::vector& data, std::vector& sequence) { Timer timer; + const auto load_before = Context::getContext().time_load_us; bool const obfuscate = Context::getContext().config.getBoolean("obfuscate"); bool const print_empty_columns = Context::getContext().config.getBoolean("print.empty.columns"); @@ -293,7 +294,8 @@ std::string ViewTask::render(std::vector& data, std::vector& sequence // Stop if the line limit is exceeded. if (++_lines >= _truncate_lines && _truncate_lines != 0) { - Context::getContext().time_render_us += timer.total_us(); + Context::getContext().time_render_us += + timer.total_us() - (Context::getContext().time_load_us - load_before); return out; } } @@ -331,7 +333,7 @@ std::string ViewTask::render(std::vector& data, std::vector& sequence // changes. if (s > 0 && _breaks.size() > 0) { for (const auto& b : _breaks) { - if (data[sequence[s - 1]].get(b) != data[sequence[s]].get(b)) { + if (data[sequence[s - 1]].get_ref(b) != data[sequence[s]].get_ref(b)) { out += "\n"; ++_lines; @@ -366,7 +368,8 @@ std::string ViewTask::render(std::vector& data, std::vector& sequence // Stop if the line limit is exceeded. if (++_lines >= _truncate_lines && _truncate_lines != 0) { - Context::getContext().time_render_us += timer.total_us(); + Context::getContext().time_render_us += + timer.total_us() - (Context::getContext().time_load_us - load_before); return out; } } @@ -375,12 +378,14 @@ std::string ViewTask::render(std::vector& data, std::vector& sequence // Stop if the row limit is exceeded. if (++_rows >= _truncate_rows && _truncate_rows != 0) { - Context::getContext().time_render_us += timer.total_us(); + Context::getContext().time_render_us += + timer.total_us() - (Context::getContext().time_load_us - load_before); return out; } } - Context::getContext().time_render_us += timer.total_us(); + Context::getContext().time_render_us += + timer.total_us() - (Context::getContext().time_load_us - load_before); return out; } diff --git a/src/ViewTask.h b/src/ViewTask.h index b606ec992..479edd6a4 100644 --- a/src/ViewTask.h +++ b/src/ViewTask.h @@ -66,7 +66,7 @@ class ViewTask { int rows() { return _rows; } // View rendering. - std::string render(std::vector&, std::vector&); + std::string render(const std::vector&, std::vector&); private: std::vector _columns; diff --git a/src/columns/ColDepends.cpp b/src/columns/ColDepends.cpp index 446737d6d..f75a32add 100644 --- a/src/columns/ColDepends.cpp +++ b/src/columns/ColDepends.cpp @@ -34,10 +34,17 @@ #include #include +#include #include #define STRING_COLUMN_LABEL_DEP "Depends" +static bool hasActiveDependency(const Task& task) { + if (task.is_blocked) return true; + const auto& status = task.get_ref("status"); + return (status == "completed" || status == "deleted") && !task.getDependencyIDs().empty(); +} + //////////////////////////////////////////////////////////////////////////////// ColumnDepends::ColumnDepends() { _name = "depends"; @@ -63,32 +70,29 @@ void ColumnDepends::setStyle(const std::string& value) { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. -void ColumnDepends::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnDepends::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { minimum = maximum = 0; - auto deptasks = task.getDependencyTasks(); - if (deptasks.size() > 0) { - if (_style == "indicator") { + if (_style == "indicator") { + if (hasActiveDependency(task)) minimum = maximum = utf8_width(Context::getContext().config.get("dependency.indicator")); - } + return; + } - else if (_style == "count") { - minimum = maximum = 2 + format((int)deptasks.size()).length(); - } + auto blocking_ids = task.getDependencyIDs(); - else if (_style == "default" || _style == "list") { + if (blocking_ids.size() > 0) { + if (_style == "count") { + minimum = maximum = 2 + format((int)blocking_ids.size()).length(); + } else if (_style == "default" || _style == "list") { minimum = maximum = 0; - std::vector blocking_ids; - blocking_ids.reserve(deptasks.size()); - for (auto& i : deptasks) blocking_ids.push_back(i.id); - auto all = join(" ", blocking_ids); maximum = all.length(); unsigned int length; - for (auto& i : deptasks) { - length = format(i.id).length(); + for (auto id : blocking_ids) { + length = format(id).length(); if (length > minimum) minimum = length; } } @@ -96,24 +100,24 @@ void ColumnDepends::measure(Task& task, unsigned int& minimum, unsigned int& max } //////////////////////////////////////////////////////////////////////////////// -void ColumnDepends::render(std::vector& lines, Task& task, int width, Color& color) { - auto deptasks = task.getDependencyTasks(); - - if (deptasks.size() > 0) { - if (_style == "indicator") { +void ColumnDepends::render(std::vector& lines, const Task& task, int width, + Color& color) { + // We only need to know if the task has a dependency. We don't have to + // look at the whole list. The flags are set during cache construction. + if (_style == "indicator") { + if (hasActiveDependency(task)) renderStringRight(lines, width, color, Context::getContext().config.get("dependency.indicator")); - } - - else if (_style == "count") { - renderStringRight(lines, width, color, '[' + format(static_cast(deptasks.size())) + ']'); - } + return; + } - else if (_style == "default" || _style == "list") { - std::vector blocking_ids; - blocking_ids.reserve(deptasks.size()); - for (const auto& t : deptasks) blocking_ids.push_back(t.id); + auto blocking_ids = task.getDependencyIDs(); + if (blocking_ids.size() > 0) { + if (_style == "count") { + renderStringRight(lines, width, color, + '[' + format(static_cast(blocking_ids.size())) + ']'); + } else if (_style == "default" || _style == "list") { auto combined = join(" ", blocking_ids); std::vector all; @@ -152,9 +156,9 @@ void ColumnDepends::modify(Task& task, const std::string& value) { Task loaded_task; if (Context::getContext().tdb2.get(dep, loaded_task)) if (removal) - task.removeDependency(loaded_task.get("uuid")); + task.removeDependency(loaded_task.get_ref("uuid")); else - task.addDependency(loaded_task.get("uuid")); + task.addDependency(loaded_task.get_ref("uuid")); else throw format("Dependency could not be set - task with UUID '{1}' does not exist.", dep); } diff --git a/src/columns/ColDepends.h b/src/columns/ColDepends.h index ef79b8f06..80c3540e6 100644 --- a/src/columns/ColDepends.h +++ b/src/columns/ColDepends.h @@ -34,8 +34,8 @@ class ColumnDepends : public ColumnTypeString { ColumnDepends(); void setStyle(const std::string&); - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); void modify(Task&, const std::string&); private: diff --git a/src/columns/ColDescription.cpp b/src/columns/ColDescription.cpp index e14b940f2..b11aa35d9 100644 --- a/src/columns/ColDescription.cpp +++ b/src/columns/ColDescription.cpp @@ -73,8 +73,8 @@ ColumnDescription::ColumnDescription() { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. -void ColumnDescription::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { - std::string description = task.get(_name); +void ColumnDescription::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { + const auto& description = task.get_ref(_name); // The text // @@ -132,8 +132,10 @@ void ColumnDescription::measure(Task& task, unsigned int& minimum, unsigned int& } //////////////////////////////////////////////////////////////////////////////// -void ColumnDescription::render(std::vector& lines, Task& task, int width, +void ColumnDescription::render(std::vector& lines, const Task& task, int width, Color& color) { + // Unlike measure, we use get() here because this render function appends + // annotation counts or strings. std::string description = task.get(_name); // This is a description diff --git a/src/columns/ColDescription.h b/src/columns/ColDescription.h index 0178316a5..fdac89005 100644 --- a/src/columns/ColDescription.h +++ b/src/columns/ColDescription.h @@ -32,8 +32,8 @@ class ColumnDescription : public ColumnTypeString { public: ColumnDescription(); - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); private: bool _hyphenate; diff --git a/src/columns/ColID.cpp b/src/columns/ColID.cpp index 69f572434..105958281 100644 --- a/src/columns/ColID.cpp +++ b/src/columns/ColID.cpp @@ -43,7 +43,7 @@ ColumnID::ColumnID() { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. -void ColumnID::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnID::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { int length; if (task.id < 10) @@ -63,7 +63,7 @@ void ColumnID::measure(Task& task, unsigned int& minimum, unsigned int& maximum) } //////////////////////////////////////////////////////////////////////////////// -void ColumnID::render(std::vector& lines, Task& task, int width, Color& color) { +void ColumnID::render(std::vector& lines, const Task& task, int width, Color& color) { // Completed and deleted tasks have no ID. if (task.id) renderInteger(lines, width, color, task.id); diff --git a/src/columns/ColID.h b/src/columns/ColID.h index 8279995a3..4021a0b49 100644 --- a/src/columns/ColID.h +++ b/src/columns/ColID.h @@ -32,8 +32,8 @@ class ColumnID : public ColumnTypeNumeric { public: ColumnID(); - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); private: }; diff --git a/src/columns/ColIMask.cpp b/src/columns/ColIMask.cpp index 067e8b85c..e59031d7c 100644 --- a/src/columns/ColIMask.cpp +++ b/src/columns/ColIMask.cpp @@ -42,14 +42,15 @@ ColumnIMask::ColumnIMask() { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. -void ColumnIMask::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnIMask::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { minimum = maximum = 0; - if (task.has(_name)) minimum = maximum = task.get(_name).length(); + if (task.has(_name)) minimum = maximum = task.get_ref(_name).length(); } //////////////////////////////////////////////////////////////////////////////// -void ColumnIMask::render(std::vector& lines, Task& task, int width, Color& color) { - if (task.has(_name)) renderStringRight(lines, width, color, task.get(_name)); +void ColumnIMask::render(std::vector& lines, const Task& task, int width, + Color& color) { + if (task.has(_name)) renderStringRight(lines, width, color, task.get_ref(_name)); } //////////////////////////////////////////////////////////////////////////////// diff --git a/src/columns/ColIMask.h b/src/columns/ColIMask.h index 7fdc4b07f..c9abbedb6 100644 --- a/src/columns/ColIMask.h +++ b/src/columns/ColIMask.h @@ -32,8 +32,8 @@ class ColumnIMask : public ColumnTypeNumeric { public: ColumnIMask(); - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); private: }; diff --git a/src/columns/ColLast.cpp b/src/columns/ColLast.cpp index 341f4dcee..ec8313808 100644 --- a/src/columns/ColLast.cpp +++ b/src/columns/ColLast.cpp @@ -42,14 +42,15 @@ ColumnLast::ColumnLast() { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. -void ColumnLast::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnLast::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { minimum = maximum = 0; - if (task.has(_name)) minimum = maximum = task.get(_name).length(); + if (task.has(_name)) minimum = maximum = task.get_ref(_name).length(); } //////////////////////////////////////////////////////////////////////////////// -void ColumnLast::render(std::vector& lines, Task& task, int width, Color& color) { - if (task.has(_name)) renderStringRight(lines, width, color, task.get(_name)); +void ColumnLast::render(std::vector& lines, const Task& task, int width, + Color& color) { + if (task.has(_name)) renderStringRight(lines, width, color, task.get_ref(_name)); } //////////////////////////////////////////////////////////////////////////////// diff --git a/src/columns/ColLast.h b/src/columns/ColLast.h index 8f24c291f..a56afcc14 100644 --- a/src/columns/ColLast.h +++ b/src/columns/ColLast.h @@ -32,8 +32,8 @@ class ColumnLast : public ColumnTypeNumeric { public: ColumnLast(); - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); private: }; diff --git a/src/columns/ColMask.cpp b/src/columns/ColMask.cpp index ca5a52f92..cd34df30b 100644 --- a/src/columns/ColMask.cpp +++ b/src/columns/ColMask.cpp @@ -42,14 +42,15 @@ ColumnMask::ColumnMask() { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. -void ColumnMask::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnMask::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { minimum = maximum = 0; - if (task.has(_name)) minimum = maximum = task.get(_name).length(); + if (task.has(_name)) minimum = maximum = task.get_ref(_name).length(); } //////////////////////////////////////////////////////////////////////////////// -void ColumnMask::render(std::vector& lines, Task& task, int width, Color& color) { - if (task.has(_name)) renderStringLeft(lines, width, color, task.get(_name)); +void ColumnMask::render(std::vector& lines, const Task& task, int width, + Color& color) { + if (task.has(_name)) renderStringLeft(lines, width, color, task.get_ref(_name)); } //////////////////////////////////////////////////////////////////////////////// diff --git a/src/columns/ColMask.h b/src/columns/ColMask.h index b7f28a9f4..1a4e9c4d2 100644 --- a/src/columns/ColMask.h +++ b/src/columns/ColMask.h @@ -32,8 +32,8 @@ class ColumnMask : public ColumnTypeString { public: ColumnMask(); - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); private: }; diff --git a/src/columns/ColParent.cpp b/src/columns/ColParent.cpp index e49315904..398048fd8 100644 --- a/src/columns/ColParent.cpp +++ b/src/columns/ColParent.cpp @@ -42,7 +42,7 @@ ColumnParent::ColumnParent() { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. -void ColumnParent::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnParent::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { minimum = maximum = 0; if (task.has(_name)) { if (_style == "default" || _style == "long") @@ -53,15 +53,16 @@ void ColumnParent::measure(Task& task, unsigned int& minimum, unsigned int& maxi } //////////////////////////////////////////////////////////////////////////////// -void ColumnParent::render(std::vector& lines, Task& task, int width, Color& color) { +void ColumnParent::render(std::vector& lines, const Task& task, int width, + Color& color) { if (task.has(_name)) { // f30cb9c3-3fc0-483f-bfb2-3bf134f00694 default // f30cb9c3 short if (_style == "default" || _style == "long") - renderStringLeft(lines, width, color, task.get(_name)); + renderStringLeft(lines, width, color, task.get_ref(_name)); else if (_style == "short") - renderStringLeft(lines, width, color, task.get(_name).substr(0, 8)); + renderStringLeft(lines, width, color, task.get_ref(_name).substr(0, 8)); } } diff --git a/src/columns/ColParent.h b/src/columns/ColParent.h index 9332a729d..7fe814c6d 100644 --- a/src/columns/ColParent.h +++ b/src/columns/ColParent.h @@ -32,8 +32,8 @@ class ColumnParent : public ColumnTypeString { public: ColumnParent(); - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); private: }; diff --git a/src/columns/ColProject.cpp b/src/columns/ColProject.cpp index 0454bb5bb..fcd9833f8 100644 --- a/src/columns/ColProject.cpp +++ b/src/columns/ColProject.cpp @@ -50,13 +50,13 @@ ColumnProject::ColumnProject() { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. -void ColumnProject::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnProject::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { minimum = maximum = 0; if (task.has(_name)) { std::string project = task.get(_name); if (_style == "parent") { - auto period = project.find('.'); + auto period = project.find("."); if (period != std::string::npos) project = project.substr(0, period); } else if (_style == "indented") { project = indentProject(project, " ", '.'); @@ -68,7 +68,8 @@ void ColumnProject::measure(Task& task, unsigned int& minimum, unsigned int& max } //////////////////////////////////////////////////////////////////////////////// -void ColumnProject::render(std::vector& lines, Task& task, int width, Color& color) { +void ColumnProject::render(std::vector& lines, const Task& task, int width, + Color& color) { if (task.has(_name)) { std::string project = task.get(_name); if (_style == "parent") { diff --git a/src/columns/ColProject.h b/src/columns/ColProject.h index ae470f3b9..3097d4245 100644 --- a/src/columns/ColProject.h +++ b/src/columns/ColProject.h @@ -32,8 +32,8 @@ class ColumnProject : public ColumnTypeString { public: ColumnProject(); - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); void modify(Task&, const std::string&); private: diff --git a/src/columns/ColRType.cpp b/src/columns/ColRType.cpp index 600e2201c..3c2651cae 100644 --- a/src/columns/ColRType.cpp +++ b/src/columns/ColRType.cpp @@ -56,25 +56,26 @@ void ColumnRType::setStyle(const std::string& value) { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. -void ColumnRType::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnRType::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { minimum = maximum = 0; if (task.has(_name)) { if (_style == "default") - minimum = maximum = task.get(_name).length(); + minimum = maximum = task.get_ref(_name).length(); else if (_style == "indicator") minimum = maximum = 1; } } //////////////////////////////////////////////////////////////////////////////// -void ColumnRType::render(std::vector& lines, Task& task, int width, Color& color) { +void ColumnRType::render(std::vector& lines, const Task& task, int width, + Color& color) { if (task.has(_name)) { if (_style == "default") - renderStringRight(lines, width, color, task.get(_name)); + renderStringRight(lines, width, color, task.get_ref(_name)); else if (_style == "indicator") { std::string value{" "}; - value[0] = toupper(task.get(_name)[0]); + value[0] = toupper(task.get_ref(_name)[0]); renderStringRight(lines, width, color, value); } } diff --git a/src/columns/ColRType.h b/src/columns/ColRType.h index dc7b86ffe..1b2482716 100644 --- a/src/columns/ColRType.h +++ b/src/columns/ColRType.h @@ -33,8 +33,8 @@ class ColumnRType : public ColumnTypeString { public: ColumnRType(); void setStyle(const std::string&); - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); bool validate(const std::string&) const; private: diff --git a/src/columns/ColRecur.cpp b/src/columns/ColRecur.cpp index dc6f0fefc..fd7687ff8 100644 --- a/src/columns/ColRecur.cpp +++ b/src/columns/ColRecur.cpp @@ -60,11 +60,11 @@ void ColumnRecur::setStyle(const std::string& value) { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. -void ColumnRecur::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnRecur::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { minimum = maximum = 0; if (task.has(_name)) { if (_style == "default" || _style == "duration") { - minimum = maximum = Duration(task.get(_name)).formatISO().length(); + minimum = maximum = Duration(task.get_ref(_name)).formatISO().length(); } else if (_style == "indicator") { minimum = maximum = utf8_width(Context::getContext().config.get("recurrence.indicator")); } @@ -72,10 +72,11 @@ void ColumnRecur::measure(Task& task, unsigned int& minimum, unsigned int& maxim } //////////////////////////////////////////////////////////////////////////////// -void ColumnRecur::render(std::vector& lines, Task& task, int width, Color& color) { +void ColumnRecur::render(std::vector& lines, const Task& task, int width, + Color& color) { if (task.has(_name)) { if (_style == "default" || _style == "duration") - renderStringRight(lines, width, color, Duration(task.get(_name)).formatISO()); + renderStringRight(lines, width, color, Duration(task.get_ref(_name)).formatISO()); else if (_style == "indicator") renderStringRight(lines, width, color, diff --git a/src/columns/ColRecur.h b/src/columns/ColRecur.h index 3de674aa9..c2fb104c3 100644 --- a/src/columns/ColRecur.h +++ b/src/columns/ColRecur.h @@ -35,8 +35,8 @@ class ColumnRecur : public ColumnTypeString { public: ColumnRecur(); void setStyle(const std::string&); - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); void modify(Task&, const std::string&); private: diff --git a/src/columns/ColStart.cpp b/src/columns/ColStart.cpp index 4a4dcc1a4..703b085ae 100644 --- a/src/columns/ColStart.cpp +++ b/src/columns/ColStart.cpp @@ -51,7 +51,7 @@ void ColumnStart::setStyle(const std::string& value) { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. -void ColumnStart::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnStart::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { minimum = maximum = 0; if (task.has(_name)) { if (_style == "active") @@ -64,7 +64,8 @@ void ColumnStart::measure(Task& task, unsigned int& minimum, unsigned int& maxim } //////////////////////////////////////////////////////////////////////////////// -void ColumnStart::render(std::vector& lines, Task& task, int width, Color& color) { +void ColumnStart::render(std::vector& lines, const Task& task, int width, + Color& color) { if (task.has(_name)) { if (_style == "active") { if (!task.has("end")) diff --git a/src/columns/ColStart.h b/src/columns/ColStart.h index 2196ccabd..040c86aa7 100644 --- a/src/columns/ColStart.h +++ b/src/columns/ColStart.h @@ -33,8 +33,8 @@ class ColumnStart : public ColumnTypeDate { public: ColumnStart(); void setStyle(const std::string&); - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); }; #endif diff --git a/src/columns/ColStatus.cpp b/src/columns/ColStatus.cpp index 1fed6d73f..43fb1e6f3 100644 --- a/src/columns/ColStatus.cpp +++ b/src/columns/ColStatus.cpp @@ -51,7 +51,7 @@ void ColumnStatus::setStyle(const std::string& value) { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. -void ColumnStatus::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnStatus::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { Task::status status = task.getStatus(); if (_style == "default" || _style == "long") { @@ -70,7 +70,8 @@ void ColumnStatus::measure(Task& task, unsigned int& minimum, unsigned int& maxi } //////////////////////////////////////////////////////////////////////////////// -void ColumnStatus::render(std::vector& lines, Task& task, int width, Color& color) { +void ColumnStatus::render(std::vector& lines, const Task& task, int width, + Color& color) { Task::status status = task.getStatus(); std::string value; diff --git a/src/columns/ColStatus.h b/src/columns/ColStatus.h index 9c6f2d9a4..1c2026ef4 100644 --- a/src/columns/ColStatus.h +++ b/src/columns/ColStatus.h @@ -33,8 +33,8 @@ class ColumnStatus : public ColumnTypeString { public: ColumnStatus(); void setStyle(const std::string&); - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); private: }; diff --git a/src/columns/ColTags.cpp b/src/columns/ColTags.cpp index ceaf86187..d5a801688 100644 --- a/src/columns/ColTags.cpp +++ b/src/columns/ColTags.cpp @@ -64,7 +64,7 @@ void ColumnTags::setStyle(const std::string& value) { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. -void ColumnTags::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnTags::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { minimum = maximum = 0; if (task.getTagCount() > 0) { if (_style == "indicator") { @@ -86,7 +86,8 @@ void ColumnTags::measure(Task& task, unsigned int& minimum, unsigned int& maximu } //////////////////////////////////////////////////////////////////////////////// -void ColumnTags::render(std::vector& lines, Task& task, int width, Color& color) { +void ColumnTags::render(std::vector& lines, const Task& task, int width, + Color& color) { auto all = task.getTags(); if (all.size() > 0) { if (_style == "default" || _style == "list") { diff --git a/src/columns/ColTags.h b/src/columns/ColTags.h index 86d58e335..709b7a554 100644 --- a/src/columns/ColTags.h +++ b/src/columns/ColTags.h @@ -33,8 +33,8 @@ class ColumnTags : public ColumnTypeString { public: ColumnTags(); void setStyle(const std::string&); - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); void modify(Task&, const std::string&); private: diff --git a/src/columns/ColTemplate.cpp b/src/columns/ColTemplate.cpp index 02f536b1c..379110120 100644 --- a/src/columns/ColTemplate.cpp +++ b/src/columns/ColTemplate.cpp @@ -42,7 +42,7 @@ ColumnTemplate::ColumnTemplate() { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. -void ColumnTemplate::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnTemplate::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { minimum = maximum = 0; if (task.has(_name)) { if (_style == "default" || _style == "long") @@ -53,15 +53,16 @@ void ColumnTemplate::measure(Task& task, unsigned int& minimum, unsigned int& ma } //////////////////////////////////////////////////////////////////////////////// -void ColumnTemplate::render(std::vector& lines, Task& task, int width, Color& color) { +void ColumnTemplate::render(std::vector& lines, const Task& task, int width, + Color& color) { if (task.has(_name)) { // f30cb9c3-3fc0-483f-bfb2-3bf134f00694 default // f30cb9c3 short if (_style == "default" || _style == "long") - renderStringLeft(lines, width, color, task.get(_name)); + renderStringLeft(lines, width, color, task.get_ref(_name)); else if (_style == "short") - renderStringLeft(lines, width, color, task.get(_name).substr(0, 8)); + renderStringLeft(lines, width, color, task.get_ref(_name).substr(0, 8)); } } diff --git a/src/columns/ColTemplate.h b/src/columns/ColTemplate.h index 991f42a37..88d7880e1 100644 --- a/src/columns/ColTemplate.h +++ b/src/columns/ColTemplate.h @@ -32,8 +32,8 @@ class ColumnTemplate : public ColumnTypeString { public: ColumnTemplate(); - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); private: }; diff --git a/src/columns/ColTypeDate.cpp b/src/columns/ColTypeDate.cpp index cb459cd3b..bf74358bb 100644 --- a/src/columns/ColTypeDate.cpp +++ b/src/columns/ColTypeDate.cpp @@ -58,7 +58,7 @@ ColumnTypeDate::ColumnTypeDate() { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. -void ColumnTypeDate::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnTypeDate::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { minimum = maximum = 0; if (task.has(_name)) { Datetime date(task.get_date(_name)); @@ -102,7 +102,8 @@ void ColumnTypeDate::measure(Task& task, unsigned int& minimum, unsigned int& ma } //////////////////////////////////////////////////////////////////////////////// -void ColumnTypeDate::render(std::vector& lines, Task& task, int width, Color& color) { +void ColumnTypeDate::render(std::vector& lines, const Task& task, int width, + Color& color) { if (task.has(_name)) { Datetime date(task.get_date(_name)); diff --git a/src/columns/ColTypeDate.h b/src/columns/ColTypeDate.h index dc4bc9aea..29fd9617a 100644 --- a/src/columns/ColTypeDate.h +++ b/src/columns/ColTypeDate.h @@ -37,8 +37,8 @@ class ColumnTypeDate : public Column { public: ColumnTypeDate(); - virtual void measure(Task&, unsigned int&, unsigned int&); - virtual void render(std::vector&, Task&, int, Color&); + virtual void measure(const Task&, unsigned int&, unsigned int&); + virtual void render(std::vector&, const Task&, int, Color&); virtual bool validate(const std::string&) const; virtual void modify(Task&, const std::string&); }; diff --git a/src/columns/ColUDA.cpp b/src/columns/ColUDA.cpp index a6452c631..c9c3f50b6 100644 --- a/src/columns/ColUDA.cpp +++ b/src/columns/ColUDA.cpp @@ -63,11 +63,11 @@ bool ColumnUDAString::validate(const std::string& value) const { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. // -void ColumnUDAString::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnUDAString::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { minimum = maximum = 0; if (task.has(_name)) { if (_style == "default") { - std::string value = task.get(_name); + const auto& value = task.get_ref(_name); if (value != "") { auto stripped = Color::strip(value); maximum = longestLine(stripped); @@ -83,10 +83,11 @@ void ColumnUDAString::measure(Task& task, unsigned int& minimum, unsigned int& m } //////////////////////////////////////////////////////////////////////////////// -void ColumnUDAString::render(std::vector& lines, Task& task, int width, Color& color) { +void ColumnUDAString::render(std::vector& lines, const Task& task, int width, + Color& color) { if (task.has(_name)) { if (_style == "default") { - std::string value = task.get(_name); + const auto& value = task.get_ref(_name); std::vector raw; wrapText(raw, value, width, _hyphenate); @@ -126,11 +127,11 @@ bool ColumnUDANumeric::validate(const std::string& value) const { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. // -void ColumnUDANumeric::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnUDANumeric::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { minimum = maximum = 0; if (task.has(_name)) { if (_style == "default") { - auto value = task.get(_name); + const auto& value = task.get_ref(_name); if (value != "") minimum = maximum = value.length(); } else if (_style == "indicator") { auto indicator = Context::getContext().config.get("uda." + _name + ".indicator"); @@ -142,11 +143,11 @@ void ColumnUDANumeric::measure(Task& task, unsigned int& minimum, unsigned int& } //////////////////////////////////////////////////////////////////////////////// -void ColumnUDANumeric::render(std::vector& lines, Task& task, int width, +void ColumnUDANumeric::render(std::vector& lines, const Task& task, int width, Color& color) { if (task.has(_name)) { if (_style == "default") { - auto value = task.get(_name); + const auto& value = task.get_ref(_name); renderStringRight(lines, width, color, value); } else if (_style == "indicator") { auto indicator = Context::getContext().config.get("uda." + _name + ".indicator"); @@ -183,11 +184,11 @@ bool ColumnUDADate::validate(const std::string& value) const { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. // -void ColumnUDADate::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnUDADate::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { minimum = maximum = 0; if (task.has(_name)) { if (_style == "default") { - auto value = task.get(_name); + const auto& value = task.get_ref(_name); if (value != "") { // Determine the output date format, which uses a hierarchy of definitions. // rc.report..dateformat @@ -210,10 +211,11 @@ void ColumnUDADate::measure(Task& task, unsigned int& minimum, unsigned int& max } //////////////////////////////////////////////////////////////////////////////// -void ColumnUDADate::render(std::vector& lines, Task& task, int width, Color& color) { +void ColumnUDADate::render(std::vector& lines, const Task& task, int width, + Color& color) { if (task.has(_name)) { if (_style == "default") { - auto value = task.get(_name); + const auto& value = task.get_ref(_name); // Determine the output date format, which uses a hierarchy of definitions. // rc.report..dateformat @@ -262,11 +264,11 @@ bool ColumnUDADuration::validate(const std::string& value) const { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. // -void ColumnUDADuration::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnUDADuration::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { minimum = maximum = 0; if (task.has(_name)) { if (_style == "default") { - auto value = task.get(_name); + const auto& value = task.get_ref(_name); if (value != "") minimum = maximum = Duration(value).formatISO().length(); } else if (_style == "indicator") { if (task.has(_name)) { @@ -281,11 +283,11 @@ void ColumnUDADuration::measure(Task& task, unsigned int& minimum, unsigned int& } //////////////////////////////////////////////////////////////////////////////// -void ColumnUDADuration::render(std::vector& lines, Task& task, int width, +void ColumnUDADuration::render(std::vector& lines, const Task& task, int width, Color& color) { if (task.has(_name)) { if (_style == "default") { - auto value = task.get(_name); + const auto& value = task.get_ref(_name); renderStringRight(lines, width, color, Duration(value).formatISO()); } else if (_style == "indicator") { auto indicator = Context::getContext().config.get("uda." + _name + ".indicator"); diff --git a/src/columns/ColUDA.h b/src/columns/ColUDA.h index 04d57a8d7..662ed7e66 100644 --- a/src/columns/ColUDA.h +++ b/src/columns/ColUDA.h @@ -38,8 +38,8 @@ class ColumnUDAString : public ColumnTypeString { public: ColumnUDAString(); bool validate(const std::string&) const; - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); public: std::vector _values; @@ -53,8 +53,8 @@ class ColumnUDANumeric : public ColumnTypeNumeric { public: ColumnUDANumeric(); bool validate(const std::string&) const; - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); public: std::vector _values; @@ -65,8 +65,8 @@ class ColumnUDADate : public ColumnTypeDate { public: ColumnUDADate(); bool validate(const std::string&) const; - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); public: std::vector _values; @@ -77,8 +77,8 @@ class ColumnUDADuration : public ColumnTypeDuration { public: ColumnUDADuration(); bool validate(const std::string&) const; - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); public: std::vector _values; diff --git a/src/columns/ColUUID.cpp b/src/columns/ColUUID.cpp index 3b81a4419..df1353dad 100644 --- a/src/columns/ColUUID.cpp +++ b/src/columns/ColUUID.cpp @@ -42,7 +42,7 @@ ColumnUUID::ColumnUUID() { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. -void ColumnUUID::measure(Task&, unsigned int& minimum, unsigned int& maximum) { +void ColumnUUID::measure(const Task&, unsigned int& minimum, unsigned int& maximum) { // Mandatory attribute, no need to check the value. if (_style == "default" || _style == "long") @@ -52,16 +52,17 @@ void ColumnUUID::measure(Task&, unsigned int& minimum, unsigned int& maximum) { } //////////////////////////////////////////////////////////////////////////////// -void ColumnUUID::render(std::vector& lines, Task& task, int width, Color& color) { +void ColumnUUID::render(std::vector& lines, const Task& task, int width, + Color& color) { // No need to check the presence of UUID - all tasks have one. // f30cb9c3-3fc0-483f-bfb2-3bf134f00694 default // f30cb9c3 short if (_style == "default" || _style == "long") - renderStringLeft(lines, width, color, task.get(_name)); + renderStringLeft(lines, width, color, task.get_ref(_name)); else if (_style == "short") - renderStringLeft(lines, width, color, task.get(_name).substr(0, 8)); + renderStringLeft(lines, width, color, task.get_ref(_name).substr(0, 8)); } //////////////////////////////////////////////////////////////////////////////// diff --git a/src/columns/ColUUID.h b/src/columns/ColUUID.h index 7a8232a41..b45808c95 100644 --- a/src/columns/ColUUID.h +++ b/src/columns/ColUUID.h @@ -32,8 +32,8 @@ class ColumnUUID : public ColumnTypeString { public: ColumnUUID(); - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); private: }; diff --git a/src/columns/ColUrgency.cpp b/src/columns/ColUrgency.cpp index d202a7d91..87fcb058f 100644 --- a/src/columns/ColUrgency.cpp +++ b/src/columns/ColUrgency.cpp @@ -42,7 +42,7 @@ ColumnUrgency::ColumnUrgency() { //////////////////////////////////////////////////////////////////////////////// // Set the minimum and maximum widths for the value. -void ColumnUrgency::measure(Task& task, unsigned int& minimum, unsigned int& maximum) { +void ColumnUrgency::measure(const Task& task, unsigned int& minimum, unsigned int& maximum) { if (_style == "default" || _style == "real") minimum = maximum = format(task.urgency(), 4, 3).length(); @@ -51,7 +51,8 @@ void ColumnUrgency::measure(Task& task, unsigned int& minimum, unsigned int& max } //////////////////////////////////////////////////////////////////////////////// -void ColumnUrgency::render(std::vector& lines, Task& task, int width, Color& color) { +void ColumnUrgency::render(std::vector& lines, const Task& task, int width, + Color& color) { if (_style == "default" || _style == "real") renderDouble(lines, width, color, task.urgency()); diff --git a/src/columns/ColUrgency.h b/src/columns/ColUrgency.h index c4c31ab0d..e826f4daf 100644 --- a/src/columns/ColUrgency.h +++ b/src/columns/ColUrgency.h @@ -32,8 +32,8 @@ class ColumnUrgency : public ColumnTypeNumeric { public: ColumnUrgency(); - void measure(Task&, unsigned int&, unsigned int&); - void render(std::vector&, Task&, int, Color&); + void measure(const Task&, unsigned int&, unsigned int&); + void render(std::vector&, const Task&, int, Color&); private: }; diff --git a/src/columns/Column.h b/src/columns/Column.h index 20dad26fb..a6cb728f9 100644 --- a/src/columns/Column.h +++ b/src/columns/Column.h @@ -58,10 +58,10 @@ class Column { virtual void setReport(const std::string& value) { _report = value; } virtual void measure(const std::string&, unsigned int&, unsigned int&) {}; - virtual void measure(Task&, unsigned int&, unsigned int&) {}; + virtual void measure(const Task&, unsigned int&, unsigned int&) {}; virtual void renderHeader(std::vector&, int, Color&); virtual void render(std::vector&, const std::string&, int, Color&) {}; - virtual void render(std::vector&, Task&, int, Color&) {}; + virtual void render(std::vector&, const Task&, int, Color&) {}; virtual bool validate(const std::string&) const { return false; }; virtual void modify(Task&, const std::string&) {}; diff --git a/src/commands/CmdBurndown.cpp b/src/commands/CmdBurndown.cpp index 7ec55f867..9ea83e09a 100644 --- a/src/commands/CmdBurndown.cpp +++ b/src/commands/CmdBurndown.cpp @@ -244,6 +244,7 @@ void Chart::accumulateTasks(const std::vector& tasks, auto last_it = _peak_day_index.find(last_day); if (first_it == _peak_day_index.end() || last_it == _peak_day_index.end()) return; size_t fi = first_it->second, li = last_it->second; + if (fi > li) return; _peak_diff[fi]++; if (li + 1 < _peak_diff.size()) _peak_diff[li + 1]--; }; @@ -266,6 +267,7 @@ void Chart::accumulateTasks(const std::vector& tasks, // ranges is the taskEpochRange returned by findTaskEpochRange(). const auto& r = ranges[i]; const auto& task = tasks[i]; + if (r.first_epoch > r.last_epoch) continue; // Peak and _current_count are made regardless of the task status. peak_add(r.peak_entry, r.peak_end); @@ -872,6 +874,7 @@ static int runBurndown(char period, std::string& output) { for (const auto& task : tasks) { auto r = findTaskEpochRange(task, now_epoch, now_day_epoch, period, cumulative); result.ranges.push_back(r); + if (r.first_epoch > r.last_epoch) continue; if (r.peak_entry < result.earliest_day) result.earliest_day = r.peak_entry; if (r.peak_end > result.latest_day) result.latest_day = r.peak_end; } diff --git a/src/commands/CmdCustom.cpp b/src/commands/CmdCustom.cpp index 8bba6c902..056f33f2c 100644 --- a/src/commands/CmdCustom.cpp +++ b/src/commands/CmdCustom.cpp @@ -32,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -101,27 +102,55 @@ int CmdCustom::execute(std::string& output) { // Add the report filter to any existing filter. if (reportFilter != "") Context::getContext().cli2.addFilter(reportFilter); - // Apply filter. + // Apply filter. When the filter is pending only, we use indices with the pending + // cache. Otherwise we fall back to copying all tasks. Filter filter; + const std::vector* data = nullptr; + std::vector sequence; std::vector filtered; - filter.subset(filtered); + Timer filterTimer; + const auto load_before = Context::getContext().time_load_us; + size_t sourceCount = 0; + + // call prepareFilter() here so that filter_to_indices and filter_to_tasks + // don't have to call it. + Context::getContext().cli2.prepareFilter(); + + bool use_pending_indices = filter.pendingOnly(); + + if (use_pending_indices) { + const auto& pending = Context::getContext().tdb2.pending_tasks(); + sourceCount = pending.size(); + filter.filter_to_indices(pending, sequence); + data = &pending; + } else { + auto all = Context::getContext().tdb2.all_tasks(); + sourceCount = all.size(); + filter.filter_to_tasks(all, filtered); + sequence.clear(); + for (unsigned int i = 0; i < filtered.size(); ++i) sequence.push_back(i); + data = &filtered; + } + + Context::getContext().debug(format("Filtered {1} tasks --> {2} tasks [{3}]", sourceCount, + sequence.size(), + use_pending_indices ? "pending only" : "all tasks")); + Context::getContext().time_filter_us += + filterTimer.total_us() - (Context::getContext().time_load_us - load_before); - std::vector sequence; if (sortOrder.size() && sortOrder[0] == "none") { - // Assemble a sequence vector that represents the tasks listed in - // Context::getContext ().cli2._uuid_ranges, in the order in which they appear. This - // equates to no sorting, just a specified order. + // If there is no sort order, we preserve the order they were specified + // and rebuild the sequence using indices. sortOrder.clear(); + sequence.clear(); for (auto& i : Context::getContext().cli2._uuid_list) - for (unsigned int t = 0; t < filtered.size(); ++t) - if (filtered[t].get("uuid") == i) sequence.push_back(t); + for (unsigned int t = 0; t < data->size(); ++t) + if ((*data)[t].get_ref("uuid") == i) sequence.push_back(t); } else { - // There is a sortOrder, so sorting will take place, which means the initial - // order of sequence is ascending. - for (unsigned int i = 0; i < filtered.size(); ++i) sequence.push_back(i); - - // Sort the tasks. - if (sortOrder.size()) sort_tasks(filtered, sequence, reportSort); + if (sortOrder.size()) { + // If there is a sortOrder, sorting will reorder the indices. + sort_tasks(*data, sequence, reportSort); + } } // Configure the view. @@ -197,19 +226,19 @@ int CmdCustom::execute(std::string& output) { // Render. std::stringstream out; - if (filtered.size()) { + if (sequence.size()) { view.truncateRows(maxrows); view.truncateLines(maxlines); - out << optionalBlankLine() << view.render(filtered, sequence) << optionalBlankLine(); + out << optionalBlankLine() << view.render(*data, sequence) << optionalBlankLine(); // Print the number of rendered tasks if (Context::getContext().verbose("affected")) { - out << (filtered.size() == 1 ? "1 task" : format("{1} tasks", filtered.size())); + out << (sequence.size() == 1 ? "1 task" : format("{1} tasks", sequence.size())); - if (maxrows && maxrows < (int)filtered.size()) out << ", " << format("{1} shown", maxrows); + if (maxrows && maxrows < (int)sequence.size()) out << ", " << format("{1} shown", maxrows); - if (maxlines && maxlines < (int)filtered.size()) + if (maxlines && maxlines < (int)sequence.size()) out << ", " << format("truncated to {1} lines", maxlines - table_header); out << '\n'; diff --git a/src/commands/CmdDelete.cpp b/src/commands/CmdDelete.cpp index 35b5c7ecf..d78be4708 100644 --- a/src/commands/CmdDelete.cpp +++ b/src/commands/CmdDelete.cpp @@ -36,6 +36,7 @@ #include #include +#include #include #define STRING_CMD_DELETE_TASK_R "Deleting recurring task {1} '{2}'." @@ -74,97 +75,116 @@ int CmdDelete::execute(std::string&) { } // Accumulated project change notifications. - std::map projectChanges; + std::map projectChanges; + + RecurrenceMaskUpdates recurrenceMaskUpdates; + const auto recurrenceConfirmation = Context::getContext().config.get("recurrence.confirmation"); + const auto hasRecurringParent = + std::any_of(filtered.begin(), filtered.end(), + [](const Task& task) { return task.getStatus() == Task::recurring; }); + RecurrenceMaskUpdates* recurrenceMaskUpdatesPtr = nullptr; + if (!Context::getContext().hooks.hasOnModify() && recurrenceConfirmation != "prompt" && + !Context::getContext().config.getBoolean("recurrence.confirmation") && !hasRecurringParent) + recurrenceMaskUpdatesPtr = &recurrenceMaskUpdates; if (filtered.size() > 1) { feedback_affected("This command will alter {1} tasks.", filtered.size()); } - for (auto& task : filtered) { - Task before(task); - - if (task.getStatus() != Task::deleted) { - // Delete the specified task. - std::string question; - question = format("Delete task {1} '{2}'?", task.identifier(true), task.get("description")); - - task.modify(Task::modAnnotate); - task.setStatus(Task::deleted); - if (!task.has("end")) task.setAsNow("end"); - - if (permission(question, filtered.size())) { - updateRecurrenceMask(task); - ++count; - Context::getContext().tdb2.modify(task); - feedback_affected("Deleting task {1} '{2}'.", task); - feedback_unblocked(task); - dependencyChainOnComplete(task); - if (Context::getContext().verbose("project")) - projectChanges[task.get("project")] = onProjectChange(task); - - // Delete siblings. - if (task.has("parent")) { - if ((Context::getContext().config.get("recurrence.confirmation") == "prompt" && - confirm(STRING_CMD_DELETE_CONFIRM_R)) || - Context::getContext().config.getBoolean("recurrence.confirmation")) { - std::vector siblings = Context::getContext().tdb2.siblings(task); - for (auto& sibling : siblings) { - sibling.modify(Task::modAnnotate); - sibling.setStatus(Task::deleted); - if (!sibling.has("end")) sibling.setAsNow("end"); - - updateRecurrenceMask(sibling); - Context::getContext().tdb2.modify(sibling); - feedback_affected(STRING_CMD_DELETE_TASK_R, sibling); - feedback_unblocked(sibling); - ++count; - } - - // Delete the parent - Task parent; - Context::getContext().tdb2.get(task.get("parent"), parent); - parent.setStatus(Task::deleted); - if (!parent.has("end")) parent.setAsNow("end"); - Context::getContext().tdb2.modify(parent); - } - } - - // Task potentially has child tasks - optionally delete them. - else { - std::vector children = Context::getContext().tdb2.children(task); - if (children.size() && - ((Context::getContext().config.get("recurrence.confirmation") == "prompt" && - confirm(STRING_CMD_DELETE_CONFIRM_R)) || - Context::getContext().config.getBoolean("recurrence.confirmation"))) { - for (auto& child : children) { - child.modify(Task::modAnnotate); - child.setStatus(Task::deleted); - if (!child.has("end")) child.setAsNow("end"); - - updateRecurrenceMask(child); - Context::getContext().tdb2.modify(child); - feedback_affected(STRING_CMD_DELETE_TASK_R, child); - feedback_unblocked(child); - ++count; + try { + for (auto& task : filtered) { + Task before(task); + + if (task.getStatus() != Task::deleted) { + std::string question; + question = format("Delete task {1} '{2}'?", task.identifier(true), task.get("description")); + + task.modify(Task::modAnnotate); + task.setStatus(Task::deleted); + if (!task.has("end")) task.setAsNow("end"); + + if (permission(question, filtered.size())) { + modifyRecurringTask(task, recurrenceMaskUpdatesPtr); + if (task.getStatus() != Task::deleted) continue; + ++count; + feedback_affected("Deleting task {1} '{2}'.", task); + if (task.is_blocking) feedback_unblocked(task); + dependencyChainOnComplete(task); + if (Context::getContext().verbose("project")) + projectChanges.insert_or_assign(task.get("project"), task); + + // Delete the siblings + if (task.has("parent")) { + if ((Context::getContext().config.get("recurrence.confirmation") == "prompt" && + confirm(STRING_CMD_DELETE_CONFIRM_R)) || + Context::getContext().config.getBoolean("recurrence.confirmation")) { + std::vector siblings = Context::getContext().tdb2.siblings(task); + bool all_deleted = true; + for (auto& sibling : siblings) { + sibling.modify(Task::modAnnotate); + sibling.setStatus(Task::deleted); + if (!sibling.has("end")) sibling.setAsNow("end"); + + modifyRecurringTask(sibling, recurrenceMaskUpdatesPtr); + if (sibling.getStatus() != Task::deleted) { + all_deleted = false; + continue; + } + feedback_affected(STRING_CMD_DELETE_TASK_R, sibling); + feedback_unblocked(sibling); + ++count; + } + + // Delete the parent + if (!all_deleted) continue; + Task parent; + Context::getContext().tdb2.get(task.get("parent"), parent); + parent.setStatus(Task::deleted); + if (!parent.has("end")) parent.setAsNow("end"); + + Context::getContext().tdb2.modify(parent); + } + } else { + std::vector children = Context::getContext().tdb2.children(task); + if (children.size() && + ((Context::getContext().config.get("recurrence.confirmation") == "prompt" && + confirm(STRING_CMD_DELETE_CONFIRM_R)) || + Context::getContext().config.getBoolean("recurrence.confirmation"))) { + for (auto& child : children) { + child.modify(Task::modAnnotate); + child.setStatus(Task::deleted); + if (!child.has("end")) child.setAsNow("end"); + + modifyRecurringTask(child, recurrenceMaskUpdatesPtr); + if (child.getStatus() != Task::deleted) continue; + feedback_affected(STRING_CMD_DELETE_TASK_R, child); + feedback_unblocked(child); + ++count; + } } } + } else { + std::cout << "Task not deleted.\n"; + rc = 1; + if (_permission_quit) break; } } else { - std::cout << "Task not deleted.\n"; + std::cout << format("Task {1} '{2}' is not deleteable.", task.identifier(true), + task.get("description")) + << '\n'; rc = 1; - if (_permission_quit) break; } - } else { - std::cout << format("Task {1} '{2}' is not deletable.", task.identifier(true), - task.get("description")) - << '\n'; - rc = 1; } + } catch (...) { + commitRecurrenceMaskUpdates(recurrenceMaskUpdates); + throw; } + commitRecurrenceMaskUpdates(recurrenceMaskUpdates); + // Now list the project changes. - for (const auto& change : projectChanges) - if (change.first != "") Context::getContext().footnote(change.second); + for (auto& change : projectChanges) + if (change.first != "") Context::getContext().footnote(onProjectChange(change.second)); feedback_affected(count == 1 ? "Deleted {1} task." : "Deleted {1} tasks.", count); diff --git a/src/commands/CmdDone.cpp b/src/commands/CmdDone.cpp index deb10a0dc..52edfaaf1 100644 --- a/src/commands/CmdDone.cpp +++ b/src/commands/CmdDone.cpp @@ -70,62 +70,72 @@ int CmdDone::execute(std::string&) { } // Accumulated project change notifications. - std::map projectChanges; + std::map projectChanges; + + RecurrenceMaskUpdates recurrenceMaskUpdates; + auto* recurrenceMaskUpdatesPtr = + Context::getContext().hooks.hasOnModify() ? nullptr : &recurrenceMaskUpdates; if (filtered.size() > 1) { feedback_affected("This command will alter {1} tasks.", filtered.size()); } std::vector modified; - for (auto& task : filtered) { - Task before(task); - - if (task.getStatus() == Task::pending || task.getStatus() == Task::waiting) { - // Complete the specified task. - std::string question = - format("Complete task {1} '{2}'?", task.identifier(true), task.get("description")); - - task.modify(Task::modAnnotate); - task.setStatus(Task::completed); - if (!task.has("end")) task.setAsNow("end"); - - // Stop the task, if started. - if (task.has("start")) { - task.remove("start"); - if (Context::getContext().config.getBoolean("journal.time")) - task.addAnnotation(Context::getContext().config.get("journal.time.stop.annotation")); - } - - if (permission(before.diff(task) + question, filtered.size())) { - updateRecurrenceMask(task); - Context::getContext().tdb2.modify(task); - ++count; - feedback_affected("Completed task {1} '{2}'.", task); - feedback_unblocked(task); - dependencyChainOnComplete(task); - if (Context::getContext().verbose("project")) - projectChanges[task.get("project")] = onProjectChange(task); - - // Save unmodified task for potential nagging later - modified.push_back(before); + try { + for (auto& task : filtered) { + Task before(task); + + if (task.getStatus() == Task::pending || task.getStatus() == Task::waiting) { + // Complete the task + std::string question = + format("Complete task {1} '{2}'?", task.identifier(true), task.get("description")); + + task.modify(Task::modAnnotate); + task.setStatus(Task::completed); + if (!task.has("end")) task.setAsNow("end"); + + // Stop the task if it has a start time. + if (task.has("start")) { + task.remove("start"); + if (Context::getContext().config.getBoolean("journal.time")) + task.addAnnotation(Context::getContext().config.get("journal.time.stop.annotation")); + } + + if (permission(before.diff(task) + question, filtered.size())) { + modifyRecurringTask(task, recurrenceMaskUpdatesPtr); + if (task.getStatus() != Task::completed) continue; + ++count; + feedback_affected("Completed task {1} '{2}'.", task); + if (task.is_blocking) feedback_unblocked(task); + dependencyChainOnComplete(task); + if (Context::getContext().verbose("project")) + projectChanges.insert_or_assign(task.get("project"), task); + + // Save unmodified task for nag. + modified.push_back(before); + } else { + std::cout << "Task not completed.\n"; + rc = 1; + if (_permission_quit) break; + } } else { - std::cout << "Task not completed.\n"; + std::cout << format("Task {1} '{2}' is neither pending nor waiting.", task.identifier(true), + task.get("description")) + << '\n'; rc = 1; - if (_permission_quit) break; } - } else { - std::cout << format("Task {1} '{2}' is neither pending nor waiting.", task.identifier(true), - task.get("description")) - << '\n'; - rc = 1; } + } catch (...) { + commitRecurrenceMaskUpdates(recurrenceMaskUpdates); + throw; } + commitRecurrenceMaskUpdates(recurrenceMaskUpdates); nag(modified); // Now list the project changes. - for (const auto& change : projectChanges) - if (change.first != "") Context::getContext().footnote(change.second); + for (auto& change : projectChanges) + if (change.first != "") Context::getContext().footnote(onProjectChange(change.second)); feedback_affected(count == 1 ? "Completed {1} task." : "Completed {1} tasks.", count); return rc; diff --git a/src/commands/CmdExport.cpp b/src/commands/CmdExport.cpp index 2924645dd..7e382a6b3 100644 --- a/src/commands/CmdExport.cpp +++ b/src/commands/CmdExport.cpp @@ -97,7 +97,7 @@ int CmdExport::execute(std::string& output) { sortOrder.clear(); for (auto& i : Context::getContext().cli2._uuid_list) for (unsigned int t = 0; t < filtered.size(); ++t) - if (filtered[t].get("uuid") == i) sequence.push_back(t); + if (filtered[t].get_ref("uuid") == i) sequence.push_back(t); } else { // sort_tasks requires the order array initially be identity for (unsigned int i = 0; i < filtered.size(); ++i) sequence.push_back(i); diff --git a/src/commands/CmdImport.cpp b/src/commands/CmdImport.cpp index b3ccc0c2a..7260c4218 100644 --- a/src/commands/CmdImport.cpp +++ b/src/commands/CmdImport.cpp @@ -189,7 +189,8 @@ void CmdImport::importSingleTask(json::object* obj) { Task before; auto uuid = task.get("uuid"); uuid_occurrences[uuid]++; - if (Context::getContext().tdb2.get(uuid, before)) { + auto& tdb2 = Context::getContext().tdb2; + if (tdb2.has(uuid) && tdb2.get(uuid, before)) { // We need to neglect updates from attributes with dynamic defaults // unless they have been explicitly specified on import. // @@ -218,7 +219,7 @@ void CmdImport::importSingleTask(json::object* obj) { std::cout << " skip "; } } else { - Context::getContext().tdb2.add(task); + tdb2.add(task); std::cout << " add "; } diff --git a/src/commands/CmdStart.cpp b/src/commands/CmdStart.cpp index 057b61fc5..7cf0fbbb6 100644 --- a/src/commands/CmdStart.cpp +++ b/src/commands/CmdStart.cpp @@ -70,7 +70,7 @@ int CmdStart::execute(std::string&) { } // Accumulated project change notifications. - std::map projectChanges; + std::map projectChanges; if (filtered.size() > 1) { feedback_affected("This command will alter {1} tasks.", filtered.size()); @@ -103,7 +103,7 @@ int CmdStart::execute(std::string&) { feedback_affected("Starting task {1} '{2}'.", task); dependencyChainOnStart(task); if (Context::getContext().verbose("project")) - projectChanges[task.get("project")] = onProjectChange(task, false); + projectChanges.insert_or_assign(task.get("project"), task); // Save unmodified task for potential nagging later modified.push_back(before); @@ -123,7 +123,7 @@ int CmdStart::execute(std::string&) { // Now list the project changes. for (auto& change : projectChanges) - if (change.first != "") Context::getContext().footnote(change.second); + if (change.first != "") Context::getContext().footnote(onProjectChange(change.second, false)); feedback_affected(count == 1 ? "Started {1} task." : "Started {1} tasks.", count); return rc; diff --git a/src/commands/CmdStop.cpp b/src/commands/CmdStop.cpp index 9924c5a58..5df670371 100644 --- a/src/commands/CmdStop.cpp +++ b/src/commands/CmdStop.cpp @@ -68,7 +68,7 @@ int CmdStop::execute(std::string&) { } // Accumulated project change notifications. - std::map projectChanges; + std::map projectChanges; if (filtered.size() > 1) { feedback_affected("This command will alter {1} tasks.", filtered.size()); @@ -94,7 +94,7 @@ int CmdStop::execute(std::string&) { feedback_affected("Stopping task {1} '{2}'.", task); dependencyChainOnStart(task); if (Context::getContext().verbose("project")) - projectChanges[task.get("project")] = onProjectChange(task, false); + projectChanges.insert_or_assign(task.get("project"), task); } else { std::cout << "Task not stopped.\n"; rc = 1; @@ -110,7 +110,7 @@ int CmdStop::execute(std::string&) { // Now list the project changes. for (auto& change : projectChanges) - if (change.first != "") Context::getContext().footnote(change.second); + if (change.first != "") Context::getContext().footnote(onProjectChange(change.second, false)); feedback_affected(count == 1 ? "Stopped {1} task." : "Stopped {1} tasks.", count); return rc; diff --git a/src/commands/CmdSync.cpp b/src/commands/CmdSync.cpp index 6e966286a..7dafecd05 100644 --- a/src/commands/CmdSync.cpp +++ b/src/commands/CmdSync.cpp @@ -190,6 +190,8 @@ int CmdSync::execute(std::string& output) { if (context.config.getBoolean("purge.on-sync")) { context.tdb2.expire_tasks(); + } else { + context.tdb2.invalidate_cache(); } if (verbose) { diff --git a/src/commands/CmdUndo.cpp b/src/commands/CmdUndo.cpp index 83156410e..8cac605b9 100644 --- a/src/commands/CmdUndo.cpp +++ b/src/commands/CmdUndo.cpp @@ -61,6 +61,8 @@ int CmdUndo::execute(std::string&) { // need not be done here. if (!replica->commit_reversed_operations(std::move(undo_ops))) { std::cout << "Could not undo: other operations have occurred."; + } else { + Context::getContext().tdb2.invalidate_cache(); } } return 0; diff --git a/src/feedback.cpp b/src/feedback.cpp index efd2302b4..b33176131 100644 --- a/src/feedback.cpp +++ b/src/feedback.cpp @@ -90,7 +90,7 @@ void feedback_affected(const std::string& effect, int quantity) { // {2} Description void feedback_affected(const std::string& effect, const Task& task) { if (Context::getContext().verbose("affected")) { - std::cout << format(effect, task.identifier(true), task.get("description")) << "\n"; + std::cout << format(effect, task.identifier(true), task.get_ref("description")) << "\n"; } } @@ -152,10 +152,10 @@ void feedback_unblocked(const Task& task) { auto blocking = i.getDependencyTasks(); if (blocking.size() == 0) { if (i.id) - std::cout << format("Unblocked {1} '{2}'.", i.id, i.get("description")) << "\n"; + std::cout << format("Unblocked {1} '{2}'.", i.id, i.get_ref("description")) << "\n"; else { - std::string uuid = i.get("uuid"); - std::cout << format("Unblocked {1} '{2}'.", i.get("uuid"), i.get("description")) << "\n"; + const auto& uuid = i.get_ref("uuid"); + std::cout << format("Unblocked {1} '{2}'.", uuid, i.get_ref("description")) << "\n"; } } } @@ -182,7 +182,7 @@ void feedback_backlog() { /////////////////////////////////////////////////////////////////////////////// std::string onProjectChange(Task& task, bool scope /* = true */) { std::stringstream msg; - std::string project = task.get("project"); + const auto& project = task.get_ref("project"); if (project != "") { if (scope) msg << format("The project '{1}' has changed.", project) << " "; @@ -220,7 +220,7 @@ std::string onProjectChange(Task& task, bool scope /* = true */) { /////////////////////////////////////////////////////////////////////////////// std::string onProjectChange(Task& task1, Task& task2) { - if (task1.get("project") == task2.get("project")) return onProjectChange(task1, false); + if (task1.get_ref("project") == task2.get_ref("project")) return onProjectChange(task1, false); std::string messages1 = onProjectChange(task1); std::string messages2 = onProjectChange(task2); @@ -236,7 +236,7 @@ std::string onExpiration(Task& task) { if (Context::getContext().verbose("affected")) msg << format("Task {1} '{2}' expired and was deleted.", task.identifier(true), - task.get("description")); + task.get_ref("description")); return msg.str(); } @@ -245,7 +245,7 @@ std::string onExpiration(Task& task) { static void countTasks(const std::vector& all, const std::string& project, int& count_pending, int& count_done) { for (auto& it : all) { - if (it.get("project") == project) { + if (it.get_ref("project") == project) { switch (it.getStatus()) { case Task::pending: case Task::waiting: diff --git a/src/nag.cpp b/src/nag.cpp index 356287f45..5a65a3145 100644 --- a/src/nag.cpp +++ b/src/nag.cpp @@ -36,12 +36,13 @@ void nag(std::vector& tasks) { auto msg = Context::getContext().config.get("nag"); if (msg == "") return; - auto pending = Context::getContext().tdb2.pending_tasks(); - for (auto& t1 : tasks) { + const auto& pending = Context::getContext().tdb2.pending_tasks(); + for (const auto& t1 : tasks) { if (t1.hasTag("nonag")) continue; - for (auto& t2 : pending) { - if (t1.get("uuid") != t2.get("uuid") && t2.hasTag("READY") && t1.urgency() < t2.urgency()) { + for (const auto& t2 : pending) { + if (t1.get_ref("uuid") != t2.get_ref("uuid") && t2.hasTag("READY") && + t1.urgency() < t2.urgency()) { Context::getContext().footnote(msg); return; } diff --git a/src/recur.cpp b/src/recur.cpp index dc3d957d3..7593027bf 100644 --- a/src/recur.cpp +++ b/src/recur.cpp @@ -42,6 +42,8 @@ #include #include +#include +#include #include #include @@ -365,35 +367,67 @@ std::optional getNextRecurrence(Datetime& current, std::string& period //////////////////////////////////////////////////////////////////////////////// // When the status of a recurring child task changes, the parent task must // update it's mask. -void updateRecurrenceMask(Task& task) { +static void updateRecurrenceMaskValue(const Task& task, Task& parent) { + const auto& imask = task.get_ref("imask"); + unsigned int index = 0; + auto parsed = std::from_chars(imask.data(), imask.data() + imask.size(), index); + if (parsed.ec != std::errc() || index == std::numeric_limits::max()) return; + const auto end = imask.data() + imask.size(); + if (parsed.ptr != end && (*parsed.ptr != '.' || parsed.ptr + 1 == end || + !std::all_of(parsed.ptr + 1, end, [](char c) { return c == '0'; }))) + return; + auto mask = parent.get("mask"); + auto value = (task.getStatus() == Task::pending) ? '-' + : (task.getStatus() == Task::completed) ? '+' + : (task.getStatus() == Task::deleted) ? 'X' + : (task.getStatus() == Task::waiting) ? 'W' + : '?'; + if (mask.length() <= index) mask.resize(index + 1, '?'); + mask[index] = value; + parent.set("mask", mask); +} + +void updateRecurrenceMask(Task& task, RecurrenceMaskUpdates* updates) { auto uuid = task.get("parent"); - Task parent; - - if (uuid != "" && Context::getContext().tdb2.get(uuid, parent)) { - unsigned int index = strtol(task.get("imask").c_str(), nullptr, 10); - auto mask = parent.get("mask"); - if (mask.length() > index) { - mask[index] = (task.getStatus() == Task::pending) ? '-' - : (task.getStatus() == Task::completed) ? '+' - : (task.getStatus() == Task::deleted) ? 'X' - : (task.getStatus() == Task::waiting) ? 'W' - : '?'; - } else { - std::string mask; - for (unsigned int i = 0; i < index; ++i) mask += "?"; - - mask += (task.getStatus() == Task::pending) ? '-' - : (task.getStatus() == Task::completed) ? '+' - : (task.getStatus() == Task::deleted) ? 'X' - : (task.getStatus() == Task::waiting) ? 'W' - : '?'; + if (uuid == "") return; + + if (updates) { + auto existing = updates->find(uuid); + if (existing == updates->end()) { + Task parent; + if (!Context::getContext().tdb2.get(uuid, parent)) return; + existing = updates->emplace(uuid, std::move(parent)).first; } - parent.set("mask", mask); + updateRecurrenceMaskValue(task, existing->second); + } else { + Task parent; + if (!Context::getContext().tdb2.get(uuid, parent)) return; + updateRecurrenceMaskValue(task, parent); Context::getContext().tdb2.modify(parent); } } +void modifyRecurringTask(Task& task, RecurrenceMaskUpdates* updates) { + if (updates) { + Context::getContext().tdb2.modify(task); + updateRecurrenceMask(task, updates); + } else { + Context::getContext().tdb2.modify(task, [](const Task& accepted) -> std::optional { + const auto& uuid = accepted.get_ref("parent"); + Task parent; + if (uuid.empty() || !Context::getContext().tdb2.get(uuid, parent)) return std::nullopt; + updateRecurrenceMaskValue(accepted, parent); + return parent; + }); + } +} + +void commitRecurrenceMaskUpdates(RecurrenceMaskUpdates& updates) { + for (auto& update : updates) Context::getContext().tdb2.modify(update.second); + updates.clear(); +} + //////////////////////////////////////////////////////////////////////////////// // Delete expired tasks. void handleUntil() { diff --git a/src/recur.h b/src/recur.h index db0b6306d..c0880e1f9 100644 --- a/src/recur.h +++ b/src/recur.h @@ -43,13 +43,18 @@ #include #include +#include #include +using RecurrenceMaskUpdates = std::map; + std::optional checked_add_datetime(Datetime& base, time_t delta); void handleRecurrence(); bool generateDueDates(Task& parent, std::vector& allDue); std::optional getNextRecurrence(Datetime& current, std::string& period); -void updateRecurrenceMask(Task& task); +void updateRecurrenceMask(Task& task, RecurrenceMaskUpdates* updates = nullptr); +void commitRecurrenceMaskUpdates(RecurrenceMaskUpdates& updates); +void modifyRecurringTask(Task& task, RecurrenceMaskUpdates* updates); void handleUntil(); #endif diff --git a/src/rules.cpp b/src/rules.cpp index 21c46307d..9fabdc19e 100644 --- a/src/rules.cpp +++ b/src/rules.cpp @@ -85,51 +85,51 @@ static void applyColor(const Color& base, Color& c, bool merge) { } //////////////////////////////////////////////////////////////////////////////// -static void colorizeBlocked(Task& task, const Color& base, Color& c, bool merge) { +static void colorizeBlocked(const Task& task, const Color& base, Color& c, bool merge) { if (task.is_blocked) applyColor(base, c, merge); } //////////////////////////////////////////////////////////////////////////////// -static void colorizeBlocking(Task& task, const Color& base, Color& c, bool merge) { +static void colorizeBlocking(const Task& task, const Color& base, Color& c, bool merge) { if (task.is_blocking) applyColor(base, c, merge); } //////////////////////////////////////////////////////////////////////////////// -static void colorizeTagged(Task& task, const Color& base, Color& c, bool merge) { +static void colorizeTagged(const Task& task, const Color& base, Color& c, bool merge) { if (task.getTagCount()) applyColor(base, c, merge); } //////////////////////////////////////////////////////////////////////////////// -static void colorizeActive(Task& task, const Color& base, Color& c, bool merge) { +static void colorizeActive(const Task& task, const Color& base, Color& c, bool merge) { // TODO: Not consistent with the implementation of the +ACTIVE virtual tag if (task.has("start") && !task.has("end")) applyColor(base, c, merge); } //////////////////////////////////////////////////////////////////////////////// -static void colorizeScheduled(Task& task, const Color& base, Color& c, bool merge) { +static void colorizeScheduled(const Task& task, const Color& base, Color& c, bool merge) { // TODO: Not consistent with the implementation of the +SCHEDULED virtual tag if (task.has("scheduled") && Datetime(task.get_date("scheduled")) <= now) applyColor(base, c, merge); } //////////////////////////////////////////////////////////////////////////////// -static void colorizeUntil(Task& task, const Color& base, Color& c, bool merge) { +static void colorizeUntil(const Task& task, const Color& base, Color& c, bool merge) { if (task.has("until")) applyColor(base, c, merge); } //////////////////////////////////////////////////////////////////////////////// -static void colorizeTag(Task& task, const std::string& rule, const Color& base, Color& c, +static void colorizeTag(const Task& task, const std::string& rule, const Color& base, Color& c, bool merge) { if (task.hasTag(rule.substr(10))) applyColor(base, c, merge); } //////////////////////////////////////////////////////////////////////////////// -static void colorizeProject(Task& task, const std::string& rule, const Color& base, Color& c, +static void colorizeProject(const Task& task, const std::string& rule, const Color& base, Color& c, bool merge) { // Observe the case sensitivity setting. bool sensitive = Context::getContext().config.getBoolean("search.case.sensitive"); - auto project = task.get("project"); + const auto& project = task.get_ref("project"); auto rule_trunc = rule.substr(14); // Match project names leftmost. @@ -139,24 +139,24 @@ static void colorizeProject(Task& task, const std::string& rule, const Color& ba } //////////////////////////////////////////////////////////////////////////////// -static void colorizeProjectNone(Task& task, const Color& base, Color& c, bool merge) { +static void colorizeProjectNone(const Task& task, const Color& base, Color& c, bool merge) { if (!task.has("project")) applyColor(base, c, merge); } //////////////////////////////////////////////////////////////////////////////// -static void colorizeTagNone(Task& task, const Color& base, Color& c, bool merge) { +static void colorizeTagNone(const Task& task, const Color& base, Color& c, bool merge) { if (task.getTagCount() == 0) applyColor(base, c, merge); } //////////////////////////////////////////////////////////////////////////////// -static void colorizeKeyword(Task& task, const std::string& rule, const Color& base, Color& c, +static void colorizeKeyword(const Task& task, const std::string& rule, const Color& base, Color& c, bool merge) { // Observe the case sensitivity setting. auto sensitive = Context::getContext().config.getBoolean("search.case.sensitive"); // The easiest thing to check is the description, because it is just one // attribute. - if (find(task.get("description"), rule.substr(14), sensitive) != std::string::npos) + if (find(task.get_ref("description"), rule.substr(14), sensitive) != std::string::npos) applyColor(base, c, merge); // Failing the description check, look at all annotations, returning on the @@ -172,7 +172,7 @@ static void colorizeKeyword(Task& task, const std::string& rule, const Color& ba } //////////////////////////////////////////////////////////////////////////////// -static void colorizeUDA(Task& task, const std::string& rule, const Color& base, Color& c, +static void colorizeUDA(const Task& task, const std::string& rule, const Color& base, Color& c, bool merge) { // Is the rule color.uda.name.value or color.uda.name? auto pos = rule.find('.', 10); @@ -181,42 +181,42 @@ static void colorizeUDA(Task& task, const std::string& rule, const Color& base, } else { auto uda = rule.substr(10, pos - 10); auto val = rule.substr(pos + 1); - if ((val == "none" && !task.has(uda)) || task.get(uda) == val) applyColor(base, c, merge); + if ((val == "none" && !task.has(uda)) || task.get_ref(uda) == val) applyColor(base, c, merge); } } //////////////////////////////////////////////////////////////////////////////// -static void colorizeDue(Task& task, const Color& base, Color& c, bool merge) { +static void colorizeDue(const Task& task, const Color& base, Color& c, bool merge) { if (task.is_due()) applyColor(base, c, merge); } //////////////////////////////////////////////////////////////////////////////// -static void colorizeDueToday(Task& task, const Color& base, Color& c, bool merge) { +static void colorizeDueToday(const Task& task, const Color& base, Color& c, bool merge) { if (task.is_duetoday()) applyColor(base, c, merge); } //////////////////////////////////////////////////////////////////////////////// -static void colorizeOverdue(Task& task, const Color& base, Color& c, bool merge) { +static void colorizeOverdue(const Task& task, const Color& base, Color& c, bool merge) { if (task.is_overdue()) applyColor(base, c, merge); } //////////////////////////////////////////////////////////////////////////////// -static void colorizeRecurring(Task& task, const Color& base, Color& c, bool merge) { +static void colorizeRecurring(const Task& task, const Color& base, Color& c, bool merge) { if (task.has("recur")) applyColor(base, c, merge); } //////////////////////////////////////////////////////////////////////////////// -static void colorizeCompleted(Task& task, const Color& base, Color& c, bool merge) { +static void colorizeCompleted(const Task& task, const Color& base, Color& c, bool merge) { if (task.getStatus() == Task::completed) applyColor(base, c, merge); } //////////////////////////////////////////////////////////////////////////////// -static void colorizeDeleted(Task& task, const Color& base, Color& c, bool merge) { +static void colorizeDeleted(const Task& task, const Color& base, Color& c, bool merge) { if (task.getStatus() == Task::deleted) applyColor(base, c, merge); } //////////////////////////////////////////////////////////////////////////////// -void autoColorize(Task& task, Color& c) { +void autoColorize(const Task& task, Color& c) { // The special tag 'nocolor' overrides all auto and specific colorization. if (!Context::getContext().color() || task.hasTag("nocolor")) { c = Color(); diff --git a/src/rules.h b/src/rules.h index d0d8c7f85..cbe73b56f 100644 --- a/src/rules.h +++ b/src/rules.h @@ -36,7 +36,7 @@ #include void initializeColorRules(); -void autoColorize(Task& task, Color& c); +void autoColorize(const Task& task, Color& c); std::string colorizeHeader(const std::string& input); std::string colorizeFootnote(const std::string& input); std::string colorizeError(const std::string& input); diff --git a/src/sort.cpp b/src/sort.cpp index f87be111a..e7175309b 100644 --- a/src/sort.cpp +++ b/src/sort.cpp @@ -41,38 +41,116 @@ #include #include #include +#include #include -static std::vector* global_data = nullptr; -static std::vector global_keys; +static const std::vector* global_data = nullptr; static unsigned int sort_random_seed = 0; static bool sort_compare(int, int); +struct SortKey { + std::string field; + bool ascending; +}; + +static std::vector global_sort_keys; + +// Pre-computed values to avoid repeated parsing. Duration fields are parsed +// once into time_t and stored by field name (recur and duration UDAs). +static std::unordered_map> global_durations; + +// Pre-computed dependency UUIDs to avoid repeated sort calls. +static std::vector> global_sorted_dep_uuids; + +// UDA types to avoid repeated lookups. +static std::unordered_map global_uda_types; +static std::vector global_random_keys; + //////////////////////////////////////////////////////////////////////////////// -void sort_tasks(std::vector& data, std::vector& order, const std::string& keys) { +void sort_tasks(const std::vector& data, std::vector& order, const std::string& keys) { Timer timer; + const auto load_before = Context::getContext().time_load_us; global_data = &data; // Split the key defs. - global_keys = split(keys, ','); - - // Generate a random seend for sorting by "random". - if (sort_random_seed == 0) { - // For testing purposes, allow the seed to be specified in an undocumented configuration - // setting. - std::string seed_str = Context::getContext().config.get("debug.random.seed"); - if (seed_str.empty()) { - std::random_device rd; - sort_random_seed = rd(); - } else { - sort_random_seed = std::stoul(seed_str); + auto key_defs = split(keys, ','); + + // Pre-computing of sorting values. + global_durations.clear(); + global_uda_types.clear(); + global_sorted_dep_uuids.clear(); + global_random_keys.clear(); + global_sort_keys.clear(); + global_sort_keys.reserve(key_defs.size()); + for (auto& k : key_defs) { + std::string field; + bool ascending, breakIndicator; + Context::getContext().decomposeSortField(k, field, ascending, breakIndicator); + global_sort_keys.push_back({field, ascending}); + + // Generate a random seend for sorting by "random". + if (field == "random") { + if (sort_random_seed == 0) { + // For testing purposes, allow the seed to be specified in an undocumented configuration + // setting. + std::string seed_str = Context::getContext().config.get("debug.random.seed"); + if (seed_str.empty()) { + std::random_device rd; + sort_random_seed = rd(); + } else { + sort_random_seed = std::stoul(seed_str); + } + } + + auto seed = std::to_string(sort_random_seed); + global_random_keys.resize(data.size()); + for (size_t i = 0; i < data.size(); ++i) { + global_random_keys[i] = + std::to_string(std::hash{}(data[i].get_ref("uuid") + seed)); + } + continue; + } + + if (field == "depends") { + global_sorted_dep_uuids.resize(data.size()); + for (size_t i = 0; i < data.size(); ++i) { + auto deps = data[i].getDependencyUUIDs(); + std::sort(deps.begin(), deps.end()); + global_sorted_dep_uuids[i] = std::move(deps); + } + continue; + } + + if (field == "recur") { + auto& cache = global_durations[field]; + cache.resize(data.size(), 0); + for (size_t i = 0; i < data.size(); ++i) { + auto s = data[i].get_ref("recur"); + if (!s.empty()) cache[i] = Duration(s).toTime_t(); + } + continue; + } + + auto col_it = Context::getContext().columns.find(field); + if (col_it != Context::getContext().columns.end()) { + auto type = col_it->second->type(); + global_uda_types[field] = type; + if (type == "duration") { + auto& cache = global_durations[field]; + cache.resize(data.size(), 0); + for (size_t i = 0; i < data.size(); ++i) { + auto s = data[i].get_ref(field); + if (!s.empty()) cache[i] = Duration(s).toTime_t(); + } + } } } // Only sort if necessary. if (order.size()) std::stable_sort(order.begin(), order.end(), sort_compare); - Context::getContext().time_sort_us += timer.total_us(); + Context::getContext().time_sort_us += + timer.total_us() - (Context::getContext().time_load_us - load_before); } void sort_projects(std::list>& sorted, @@ -114,29 +192,14 @@ void sort_projects(std::list>& sorted, // // Essentially a static implementation of a dynamic operator<. static bool sort_compare(int left, int right) { - std::string field; - bool ascending; - bool breakIndicator; - Column* column; - int left_number; - int right_number; - float left_real; - float right_real; - - for (auto& k : global_keys) { - Context::getContext().decomposeSortField(k, field, ascending, breakIndicator); + for (const auto& key : global_sort_keys) { + const auto& field = key.field; + bool ascending = key.ascending; // Random. if (field == "random") { - // For "random" sort, we produce a stable number for each task based on a hash of its - // UUID plus the random seed. - std::string left_uuid = (*global_data)[left].get("uuid"); - std::string right_uuid = (*global_data)[right].get("uuid"); - - std::string left_scrambled = - std::to_string(std::hash{}(left_uuid + std::to_string(sort_random_seed))); - std::string right_scrambled = - std::to_string(std::hash{}(right_uuid + std::to_string(sort_random_seed))); + const auto& left_scrambled = global_random_keys[left]; + const auto& right_scrambled = global_random_keys[right]; if (left_scrambled == right_scrambled) continue; @@ -145,8 +208,8 @@ static bool sort_compare(int left, int right) { // Urgency. else if (field == "urgency") { - left_real = (*global_data)[left].urgency(); - right_real = (*global_data)[right].urgency(); + auto left_real = (*global_data)[left].urgency(); + auto right_real = (*global_data)[right].urgency(); if (left_real == right_real) continue; @@ -155,8 +218,8 @@ static bool sort_compare(int left, int right) { // Number. else if (field == "id") { - left_number = (*global_data)[left].id; - right_number = (*global_data)[right].id; + auto left_number = (*global_data)[left].id; + auto right_number = (*global_data)[right].id; if (left_number == right_number) continue; @@ -166,8 +229,8 @@ static bool sort_compare(int left, int right) { // String. else if (field == "description" || field == "project" || field == "status" || field == "tags" || field == "uuid" || field == "parent" || field == "imask" || field == "mask") { - auto left_string = (*global_data)[left].get_ref(field); - auto right_string = (*global_data)[right].get_ref(field); + const auto& left_string = (*global_data)[left].get_ref(field); + const auto& right_string = (*global_data)[right].get_ref(field); if (left_string == right_string) continue; @@ -177,8 +240,8 @@ static bool sort_compare(int left, int right) { // Due Date. else if (field == "due" || field == "end" || field == "entry" || field == "start" || field == "until" || field == "wait" || field == "modified" || field == "scheduled") { - auto left_string = (*global_data)[left].get_ref(field); - auto right_string = (*global_data)[right].get_ref(field); + const auto& left_string = (*global_data)[left].get_ref(field); + const auto& right_string = (*global_data)[right].get_ref(field); if (left_string != "" && right_string == "") return true; @@ -191,22 +254,18 @@ static bool sort_compare(int left, int right) { // Depends string. else if (field == "depends") { - // Raw data is an un-sorted list of UUIDs. We just need a stable - // sort, so we sort them lexically. - auto left_deps = (*global_data)[left].getDependencyUUIDs(); - std::sort(left_deps.begin(), left_deps.end()); - auto right_deps = (*global_data)[right].getDependencyUUIDs(); - std::sort(right_deps.begin(), right_deps.end()); + const auto& left_deps = global_sorted_dep_uuids[left]; + const auto& right_deps = global_sorted_dep_uuids[right]; if (left_deps == right_deps) continue; - if (left_deps.size() == 0 && right_deps.size() > 0) return ascending; + if (left_deps.empty() && !right_deps.empty()) return ascending; - if (left_deps.size() > 0 && right_deps.size() == 0) return !ascending; + if (!left_deps.empty() && right_deps.empty()) return !ascending; // Sort on the first dependency. - left_number = Context::getContext().tdb2.id(left_deps[0]); - right_number = Context::getContext().tdb2.id(right_deps[0]); + auto left_number = Context::getContext().tdb2.id(left_deps[0]); + auto right_number = Context::getContext().tdb2.id(right_deps[0]); if (left_number == right_number) continue; @@ -215,19 +274,23 @@ static bool sort_compare(int left, int right) { // Duration. else if (field == "recur") { - auto left_string = (*global_data)[left].get_ref(field); - auto right_string = (*global_data)[right].get_ref(field); - - if (left_string == right_string) continue; - - Duration left_duration(left_string); - Duration right_duration(right_string); - return ascending ? (left_duration < right_duration) : (left_duration > right_duration); + auto it = global_durations.find(field); + if (it != global_durations.end()) { + auto left_dur = it->second[left]; + auto right_dur = it->second[right]; + if (left_dur == right_dur) continue; + return ascending ? (left_dur < right_dur) : (left_dur > right_dur); + } } // UDAs. - else if ((column = Context::getContext().columns[field]) != nullptr) { - std::string type = column->type(); + else { + auto type_it = global_uda_types.find(field); + if (type_it == global_uda_types.end()) + throw format("The '{1}' column is not a valid sort field.", field); + + const auto& type = type_it->second; + if (type == "numeric") { auto left_real = strtof(((*global_data)[left].get_ref(field)).c_str(), nullptr); auto right_real = strtof(((*global_data)[right].get_ref(field)).c_str(), nullptr); @@ -236,8 +299,8 @@ static bool sort_compare(int left, int right) { return ascending ? (left_real < right_real) : (left_real > right_real); } else if (type == "string") { - auto left_string = (*global_data)[left].get_ref(field); - auto right_string = (*global_data)[right].get_ref(field); + const auto& left_string = (*global_data)[left].get_ref(field); + const auto& right_string = (*global_data)[right].get_ref(field); if (left_string == right_string) continue; @@ -260,8 +323,8 @@ static bool sort_compare(int left, int right) { } else if (type == "date") { - auto left_string = (*global_data)[left].get_ref(field); - auto right_string = (*global_data)[right].get_ref(field); + const auto& left_string = (*global_data)[left].get_ref(field); + const auto& right_string = (*global_data)[right].get_ref(field); if (left_string != "" && right_string == "") return true; @@ -271,17 +334,15 @@ static bool sort_compare(int left, int right) { return ascending ? (left_string < right_string) : (left_string > right_string); } else if (type == "duration") { - auto left_string = (*global_data)[left].get_ref(field); - auto right_string = (*global_data)[right].get_ref(field); - - if (left_string == right_string) continue; - - Duration left_duration(left_string); - Duration right_duration(right_string); - return ascending ? (left_duration < right_duration) : (left_duration > right_duration); + auto it = global_durations.find(field); + if (it != global_durations.end()) { + auto left_dur = it->second[left]; + auto right_dur = it->second[right]; + if (left_dur == right_dur) continue; + return ascending ? (left_dur < right_dur) : (left_dur > right_dur); + } } - } else - throw format("The '{1}' column is not a valid sort field.", field); + } } return false; diff --git a/src/sort.h b/src/sort.h index 6972f9c2e..4d842d3e0 100644 --- a/src/sort.h +++ b/src/sort.h @@ -38,7 +38,7 @@ #include #include -void sort_tasks(std::vector& data, std::vector& order, const std::string& keys); +void sort_tasks(const std::vector& data, std::vector& order, const std::string& keys); void sort_projects(std::list>& sorted, std::map& allProjects); diff --git a/test/completed.test.py b/test/completed.test.py index 91f612808..abfbe109f 100755 --- a/test/completed.test.py +++ b/test/completed.test.py @@ -54,6 +54,16 @@ def test_completed_report(self): self.assertNotIn("two", out) self.assertNotIn("three", out) + def test_complete_with_empty_filter(self): + """Verify that you can complete all tasks with an empty filter, + this should be successful with cold pending set cache""" + self.t("add one") + self.t("add two") + + code, out, err = self.t("rc.allow.empty.filter=1 rc.bulk=0 done", input="yes\n") + self.assertIn("Completed 2 tasks", out) + self.assertEqual([], self.t.export("status:pending")) + if __name__ == "__main__": from simpletap import TAPTestRunner diff --git a/test/debug.test.py b/test/debug.test.py index 1838a51d9..d4bf5a534 100755 --- a/test/debug.test.py +++ b/test/debug.test.py @@ -85,6 +85,28 @@ def test_debug_hooks_output(self): self.assertIn("Filtered 2 tasks --> 2 tasks [pending only]", err) self.assertIn("Perf task", err) + def test_all_report_debug_filter_output(self): + """Verify reports that call all tasks instead of pending + retain the filter debug summary""" + self.t("1 done") + + code, out, err = self.t("status:completed all rc.debug=1") + + self.assertIn("Filtered 2 tasks --> 1 tasks [all tasks]", err) + + def test_status_filter_shortcut_ignores_other_filters(self): + """Checks that we aren't accidentally matching other filters + with the same keywords""" + self.t("1 modify description:pending") + self.t("1 done") + + for filter_ in ( + "status:completed description:pending", + "status.not:pending description:pending", + ): + code, out, err = self.t(f"{filter_} all") + self.assertIn("pending", out) + if __name__ == "__main__": from simpletap import TAPTestRunner diff --git a/test/dependencies.test.py b/test/dependencies.test.py index 951129ec3..b5767d4aa 100755 --- a/test/dependencies.test.py +++ b/test/dependencies.test.py @@ -87,6 +87,33 @@ def test_circular_5(self): code, out, err = self.t.runError("1 modify dep:5") self.assertIn("Circular dependency detected and disallowed.", err) + def test_completed_dependency_is_not_rendered(self): + """Completed dependencies should not be listed even if working set is stale""" + self.t("2 modify dep:1") + self.t("1 done rc.gc=off") + + self.assertIn("depends", self.t.export_one("rc.gc=off 2")) + code, out, err = self.t( + "2 list rc.gc=off rc.report.list.columns=id,depends " + "rc.report.list.labels=ID,Depends" + ) + + self.assertIn(["2"], [line.split() for line in out.splitlines()]) + + def test_completed_dependent_renders_active_dep(self): + """Completed tasks retain references to IDs for active dependencies""" + self.t("2 modify dep:1") + self.t("2 done rc.confirmation=off") + + code, out, err = self.t( + "status:completed all rc.verbose=nothing " + "rc.report.all.columns=description,depends " + "rc.report.all.labels=Description,Depends " + "rc.report.all.sort=description+" + ) + + self.assertIn(["two", "1"], [line.split() for line in out.splitlines()]) + def test_dag(self): """Check acyclic graph support""" self.t("add three") diff --git a/test/hooks.on-modify.test.py b/test/hooks.on-modify.test.py index b5fbf5386..1a6b35d9c 100755 --- a/test/hooks.on-modify.test.py +++ b/test/hooks.on-modify.test.py @@ -204,6 +204,18 @@ def test_onmodify_escaped_backslash(self): code, out, err = self.t(f"_get 1.description") self.assertEqual(out.strip(), r"tab\ttab foo") + def test_onmodify_recurring_bulk_completion(self): + hookname = "on-modify-accept" + self.t("add one due:tomorrow recur:daily") + self.t("list rc.recurrence.limit:3 rc.verbose:nothing") + self.t.hooks.add_default(hookname, log=True) + + self.t("2-4 done rc.bulk=0 rc.confirmation=off") + + hook = self.t.hooks[hookname] + hook.assertTriggeredCount(6) + self.assertEqual("+++", self.t.export_one("status:recurring")["mask"]) + if __name__ == "__main__": from simpletap import TAPTestRunner diff --git a/test/recurrence.test.py b/test/recurrence.test.py index 155eb9d1f..deaa80eba 100755 --- a/test/recurrence.test.py +++ b/test/recurrence.test.py @@ -649,6 +649,23 @@ def test_annual_creep(self): self.assertIn("17 20150101", out) +class TestBulkRecurrenceMask(TestCase): + def setUp(self): + self.t = Task() + self.t("add one due:tomorrow recur:daily") + self.t("list rc.recurrence.limit:3 rc.verbose:nothing") + + def test_done(self): + self.t("2-4 done rc.bulk=0 rc.confirmation=off") + self.assertEqual("+++", self.t.export_one("status:recurring")["mask"]) + + def test_delete(self): + self.t( + "2-4 delete rc.bulk=0 rc.confirmation=off rc.recurrence.confirmation=off" + ) + self.assertEqual("XXX", self.t.export_one("status:recurring")["mask"]) + + # TODO Wait a recurring task # TODO Downgrade a recurring task to a regular task # TODO Duplicate a recurring child task diff --git a/test/tdb2_test.cpp b/test/tdb2_test.cpp index f1baf2444..ff640c0de 100644 --- a/test/tdb2_test.cpp +++ b/test/tdb2_test.cpp @@ -44,7 +44,7 @@ void cleardb() { //////////////////////////////////////////////////////////////////////////////// int TEST_NAME(int, char**) { - UnitTest t(12); + UnitTest t(36); Context context; Context::setContext(&context); @@ -84,7 +84,7 @@ int TEST_NAME(int, char**) { t.is((int)pending.size(), 1, "TDB2 after add, 1 pending task"); t.is((int)completed.size(), 0, "TDB2 after add, 0 completed tasks"); t.is((int)num_reverts_possible, 1, "TDB2 after add, 1 revert possible"); - t.is((int)num_local_changes, 6, "TDB2 after add, 6 local changes"); + t.is((int)num_local_changes, 7, "TDB2 after add, 7 local changes"); task.set("description", "This is a test"); context.tdb2.modify(task); @@ -98,11 +98,106 @@ int TEST_NAME(int, char**) { t.is((int)completed.size(), 0, "TDB2 after set, 0 completed tasks"); t.is((int)num_reverts_possible, 1, "TDB2 after set, 1 revert possible"); - // At this point, there may be 7 or 8 local changes, depending on whether + // At this point, there may be 8 or 9 local changes, depending on whether // the `modified` property changed between the `add` and `modify` // invocation. That only happens if the clock ticks over to the next second // between those invocations. - t.ok(num_local_changes == 7 || num_local_changes == 8, "TDB2 after set, 7 or 8 local changes"); + t.ok(num_local_changes == 8 || num_local_changes == 9, "TDB2 after set, 7 or 8 local changes"); + + // Reset for reuse. + cleardb(); + context.tdb2.open_replica(".", /*create_if_missing=*/true, /*read_write=*/true); + t.is((int)context.tdb2.pending_tasks().size(), 0, "Fresh invocation uses fresh cache"); + + Task blocker(R"([description:"blocking"])"); + Task first(R"([description:"first"])"); + Task second(R"([description:"second"])"); + + first.addDependency(blocker.get_ref("uuid")); + context.tdb2.modify(first); + second.addDependency(blocker.get_ref("uuid")); + context.tdb2.modify(second); + + t.ok(context.tdb2.find_pending(blocker.get_ref("uuid"))->is_blocking, + "TDB2 dependency is blocking"); + t.ok(context.tdb2.find_pending(first.get_ref("uuid"))->is_blocked, + "TDB2 first dependent is blocked"); + t.ok(context.tdb2.find_pending(second.get_ref("uuid"))->is_blocked, + "TDB2 second dependent is blocked"); + t.is((int)context.tdb2.dependency_graph().dependencies.size(), 2, + "TDB2 dependency graph contains two dependents"); + t.is((int)context.tdb2.dependency_graph().dependents.at(blocker.get_ref("uuid")).size(), 2, + "TDB2 dependency map contains both edges"); + + first.removeDependency(blocker.get_ref("uuid")); + context.tdb2.modify(first); + + t.ok(context.tdb2.find_pending(blocker.get_ref("uuid"))->is_blocking, + "TDB2 dependency remains blocking"); + t.notok(context.tdb2.find_pending(first.get_ref("uuid"))->is_blocked, + "TDB2 first dependent is unblocked"); + t.ok(context.tdb2.find_pending(second.get_ref("uuid"))->is_blocked, + "TDB2 second dependent remains blocked"); + t.is((int)context.tdb2.dependency_graph().dependencies.size(), 1, + "TDB2 dependency map correctly removed the first dependent"); + t.is((int)context.tdb2.dependency_graph().dependents.at(blocker.get_ref("uuid")).size(), 1, + "TDB2 dependency map contains the second edge"); + + second.removeDependency(blocker.get_ref("uuid")); + context.tdb2.modify(second); + + t.notok(context.tdb2.find_pending(blocker.get_ref("uuid"))->is_blocking, + "TDB2 dependency no longer blocks tasks"); + t.notok(context.tdb2.find_pending(second.get_ref("uuid"))->is_blocked, + "TDB2 second dependent is unblocked"); + t.is((int)context.tdb2.pending_tasks().size(), 3, + "TDB2 dependency changes keep correct pending number"); + t.is((int)context.tdb2.dependency_graph().dependencies.size(), 0, + "TDB2 dependency changes invalidated the old graph - dependencies"); + t.is((int)context.tdb2.dependency_graph().dependents.size(), 0, + "TDB2 dependency changes remove dependents"); + + // Reset for reuse. + cleardb(); + + context.tdb2.open_replica(".", /*create_if_missing=*/true, /*read_write=*/true); + + first.addDependency(blocker.get_ref("uuid")); + context.tdb2.modify(first); + second.addDependency(blocker.get_ref("uuid")); + + Task waiting = *context.tdb2.find_pending(first.get_ref("uuid")); + waiting.set("wait", "20380118T000000Z"); + context.tdb2.modify(waiting); + + Task recurring = *context.tdb2.find_pending(second.get_ref("uuid")); + recurring.setStatus(Task::recurring); + context.tdb2.modify(recurring); + + t.ok(context.tdb2.find_pending(first.get_ref("uuid"))->is_blocked, + "TDB2 waiting dependent is blocked"); + t.ok(context.tdb2.find_pending(second.get_ref("uuid"))->is_blocked, + "TDB2 recurring dependent is blocked"); + t.ok(context.tdb2.find_pending(blocker.get_ref("uuid"))->is_blocking, + "TDB2 active dependents keep the dependency as blocking"); + + waiting.setStatus(Task::completed); + context.tdb2.modify(waiting); + + t.notok(context.tdb2.find_pending(first.get_ref("uuid"))->is_blocked, + "TDB2 completed waiting dependent is unblocked"); + t.ok(context.tdb2.find_pending(second.get_ref("uuid"))->is_blocked, + "TDB2 recurring dependent stays blocked"); + t.ok(context.tdb2.find_pending(blocker.get_ref("uuid"))->is_blocking, + "TDB2 active dependents keep the dependency as blocking - recurring tasks"); + + recurring.setStatus(Task::deleted); + context.tdb2.modify(recurring); + + t.notok(context.tdb2.find_pending(second.get_ref("uuid"))->is_blocked, + "TDB2 deleted recurring dependent gets unblocked"); + t.notok(context.tdb2.find_pending(blocker.get_ref("uuid"))->is_blocking, + "TDB2 inactive dependents cause dependency to clear blocking"); // Reset for reuse. cleardb();