Merge branch '2.6.0' into getFromContext

This commit is contained in:
Tomas Babej
2021-08-28 23:53:40 -04:00
committed by GitHub
39 changed files with 640 additions and 302 deletions

View File

@@ -34,6 +34,10 @@
#include <Color.h>
#include <shared.h>
#include <format.h>
#include <CmdCustom.h>
#include <CmdTimesheet.h>
#include <utf8.h>
// Overridden by rc.abbreviation.minimum.
int CLI2::minimumMatchLength = 3;
@@ -411,14 +415,33 @@ void CLI2::lexArguments ()
_args.push_back (a);
}
// Process muktiple-token arguments.
// Process multiple-token arguments.
else
{
std::string quote = "'";
std::string escaped = _original_args[i].attribute ("raw");
escaped = str_replace (escaped, quote, "\\'");
const std::string quote = "'";
// Escape unescaped single quotes
std::string escaped = "";
// For performance reasons. The escaped string is as long as the original.
escaped.reserve (_original_args[i].attribute ("raw").size ());
std::string::size_type cursor = 0;
bool nextEscaped = false;
while (int num = utf8_next_char (_original_args[i].attribute ("raw"), cursor))
{
std::string character = utf8_character (num);
if (!nextEscaped && (character == "\\"))
nextEscaped = true;
else {
if (character == quote && !nextEscaped)
escaped += "\\";
nextEscaped = false;
}
escaped += character;
}
cursor = 0;
std::string word;
if (Lexer::readWord (quote + escaped + quote, quote, cursor, word))
{
@@ -607,17 +630,18 @@ void CLI2::addContext (bool readable, bool writeable)
if (contextString.empty ())
return;
// Detect if UUID or ID is set, and bail out
for (auto& a : _args)
{
if (a._lextype == Lexer::Type::uuid ||
a._lextype == Lexer::Type::number ||
a._lextype == Lexer::Type::set)
// For readable contexts: Detect if UUID or ID is set, and bail out
if (readable)
for (auto& a : _args)
{
Context::getContext ().debug (format ("UUID/ID argument found '{1}', not applying context.", a.attribute ("raw")));
return;
if (a._lextype == Lexer::Type::uuid ||
a._lextype == Lexer::Type::number ||
a._lextype == Lexer::Type::set)
{
Context::getContext ().debug (format ("UUID/ID argument found '{1}', not applying context.", a.attribute ("raw")));
return;
}
}
}
// Apply the context. Readable (filtering) takes precedence. Also set the
// block now, since addFilter calls analyze(), which calls addContext().
@@ -944,7 +968,23 @@ void CLI2::categorizeArgs ()
// Context is only applied for commands that request it.
std::string command = getCommand ();
Command* cmd = Context::getContext ().commands[command];
if (cmd && cmd->uses_context ())
// Determine if the command uses Context. CmdCustom and CmdTimesheet need to
// be handled separately, as they override the parent Command::use_context
// method, and this is a pointer to Command class.
//
// All Command classes overriding uses_context () getter need to be specified
// here.
bool uses_context;
if (dynamic_cast<CmdCustom*> (cmd))
uses_context = (dynamic_cast<CmdCustom*> (cmd))->uses_context ();
else if (dynamic_cast<CmdTimesheet*> (cmd))
uses_context = (dynamic_cast<CmdTimesheet*> (cmd))->uses_context ();
else if (cmd)
uses_context = cmd->uses_context ();
// Apply the context, if applicable
if (cmd && uses_context)
addContext (cmd->accepts_filter (), cmd->accepts_modifications ());
bool changes = false;

View File

@@ -87,7 +87,7 @@ std::string configurationDefaults =
"\n"
"# Miscellaneous\n"
"# # Comma-separated list. May contain any subset of:\n"
"verbose=blank,header,footnote,label,new-id,affected,edit,special,project,sync,filter,unwait,override,recur\n"
"verbose=blank,header,footnote,label,new-id,affected,edit,special,project,sync,filter,override,recur\n"
"confirmation=1 # Confirmation on delete, big changes\n"
"recurrence=1 # Enable recurrence\n"
"recurrence.confirmation=prompt # Confirmation for propagating changes among recurring tasks (yes/no/prompt)\n"
@@ -109,7 +109,6 @@ std::string configurationDefaults =
"xterm.title=0 # Sets xterm title for some commands\n"
"expressions=infix # Prefer infix over postfix expressions\n"
"json.array=1 # Enclose JSON output in [ ]\n"
"json.depends.array=0 # Encode dependencies as a JSON array\n"
"abbreviation.minimum=2 # Shortest allowed abbreviation\n"
"\n"
"# Dates\n"
@@ -292,105 +291,123 @@ std::string configurationDefaults =
"report.long.description=All details of tasks\n"
"report.long.labels=ID,A,Created,Mod,Deps,P,Project,Tags,Recur,Wait,Sched,Due,Until,Description\n"
"report.long.columns=id,start.active,entry,modified.age,depends,priority,project,tags,recur,wait.remaining,scheduled,due,until,description\n"
"report.long.filter=status:pending\n"
"report.long.filter=status:pending -WAITING\n"
"report.long.sort=modified-\n"
"report.long.context=1\n"
"\n"
"report.list.description=Most details of tasks\n"
"report.list.labels=ID,Active,Age,D,P,Project,Tags,R,Sch,Due,Until,Description,Urg\n"
"report.list.columns=id,start.age,entry.age,depends.indicator,priority,project,tags,recur.indicator,scheduled.countdown,due,until.remaining,description.count,urgency\n"
"report.list.filter=status:pending\n"
"report.list.filter=status:pending -WAITING\n"
"report.list.sort=start-,due+,project+,urgency-\n"
"report.list.context=1\n"
"\n"
"report.ls.description=Few details of tasks\n"
"report.ls.labels=ID,A,D,Project,Tags,R,Wait,S,Due,Until,Description\n"
"report.ls.columns=id,start.active,depends.indicator,project,tags,recur.indicator,wait.remaining,scheduled.countdown,due.countdown,until.countdown,description.count\n"
"report.ls.filter=status:pending\n"
"report.ls.filter=status:pending -WAITING\n"
"report.ls.sort=start-,description+\n"
"report.ls.context=1\n"
"\n"
"report.minimal.description=Minimal details of tasks\n"
"report.minimal.labels=ID,Project,Tags,Description\n"
"report.minimal.columns=id,project,tags.count,description.count\n"
"report.minimal.filter=status:pending or status:waiting\n"
"report.minimal.filter=status:pending\n"
"report.minimal.sort=project+/,description+\n"
"report.minimal.context=1\n"
"\n"
"report.newest.description=Newest tasks\n"
"report.newest.labels=ID,Active,Created,Age,Mod,D,P,Project,Tags,R,Wait,Sch,Due,Until,Description\n"
"report.newest.columns=id,start.age,entry,entry.age,modified.age,depends.indicator,priority,project,tags,recur.indicator,wait.remaining,scheduled.countdown,due,until.age,description\n"
"report.newest.filter=status:pending or status:waiting\n"
"report.newest.filter=status:pending\n"
"report.newest.sort=entry-\n"
"report.newest.context=1\n"
"\n"
"report.oldest.description=Oldest tasks\n"
"report.oldest.labels=ID,Active,Created,Age,Mod,D,P,Project,Tags,R,Wait,Sch,Due,Until,Description\n"
"report.oldest.columns=id,start.age,entry,entry.age,modified.age,depends.indicator,priority,project,tags,recur.indicator,wait.remaining,scheduled.countdown,due,until.age,description\n"
"report.oldest.filter=status:pending or status:waiting\n"
"report.oldest.filter=status:pending\n"
"report.oldest.sort=entry+\n"
"report.oldest.context=1\n"
"\n"
"report.overdue.description=Overdue tasks\n"
"report.overdue.labels=ID,Active,Age,Deps,P,Project,Tag,R,S,Due,Until,Description,Urg\n"
"report.overdue.columns=id,start.age,entry.age,depends,priority,project,tags,recur.indicator,scheduled.countdown,due,until,description,urgency\n"
"report.overdue.filter=(status:pending or status:waiting) and +OVERDUE\n"
"report.overdue.filter=status:pending and +OVERDUE\n"
"report.overdue.sort=urgency-,due+\n"
"report.overdue.context=1\n"
"\n"
"report.active.description=Active tasks\n"
"report.active.labels=ID,Started,Active,Age,D,P,Project,Tags,Recur,W,Sch,Due,Until,Description\n"
"report.active.columns=id,start,start.age,entry.age,depends.indicator,priority,project,tags,recur,wait,scheduled.remaining,due,until,description\n"
"report.active.filter=status:pending and +ACTIVE\n"
"report.active.sort=project+,start+\n"
"report.active.context=1\n"
"\n"
"report.completed.description=Completed tasks\n"
"report.completed.labels=ID,UUID,Created,Completed,Age,Deps,P,Project,Tags,R,Due,Description\n"
"report.completed.columns=id,uuid.short,entry,end,entry.age,depends,priority,project,tags,recur.indicator,due,description\n"
"report.completed.filter=status:completed\n"
"report.completed.sort=end+\n"
"report.completed.context=1\n"
"\n"
"report.recurring.description=Recurring Tasks\n"
"report.recurring.labels=ID,Active,Age,D,P,Project,Tags,Recur,Sch,Due,Until,Description,Urg\n"
"report.recurring.columns=id,start.age,entry.age,depends.indicator,priority,project,tags,recur,scheduled.countdown,due,until.remaining,description,urgency\n"
"report.recurring.filter=(status:pending or status:waiting) and (+PARENT or +CHILD)\n"
"report.recurring.filter=status:pending and (+PARENT or +CHILD)\n"
"report.recurring.sort=due+,urgency-,entry+\n"
"report.recurring.context=1\n"
"\n"
"report.waiting.description=Waiting (hidden) tasks\n"
"report.waiting.labels=ID,A,Age,D,P,Project,Tags,R,Wait,Remaining,Sched,Due,Until,Description\n"
"report.waiting.columns=id,start.active,entry.age,depends.indicator,priority,project,tags,recur.indicator,wait,wait.remaining,scheduled,due,until,description\n"
"report.waiting.filter=+WAITING\n"
"report.waiting.sort=due+,wait+,entry+\n"
"report.waiting.context=1\n"
"\n"
"report.all.description=All tasks\n"
"report.all.labels=ID,St,UUID,A,Age,Done,D,P,Project,Tags,R,Wait,Sch,Due,Until,Description\n"
"report.all.columns=id,status.short,uuid.short,start.active,entry.age,end.age,depends.indicator,priority,project.parent,tags.count,recur.indicator,wait.remaining,scheduled.remaining,due,until.remaining,description\n"
"report.all.sort=entry-\n"
"report.all.context=1\n"
"\n"
"report.next.description=Most urgent tasks\n"
"report.next.labels=ID,Active,Age,Deps,P,Project,Tag,Recur,S,Due,Until,Description,Urg\n"
"report.next.columns=id,start.age,entry.age,depends,priority,project,tags,recur,scheduled.countdown,due.relative,until.remaining,description,urgency\n"
"report.next.filter=status:pending limit:page\n"
"report.next.filter=status:pending -WAITING limit:page\n"
"report.next.sort=urgency-\n"
"report.next.context=1\n"
"\n"
"report.ready.description=Most urgent actionable tasks\n"
"report.ready.labels=ID,Active,Age,D,P,Project,Tags,R,S,Due,Until,Description,Urg\n"
"report.ready.columns=id,start.age,entry.age,depends.indicator,priority,project,tags,recur.indicator,scheduled.countdown,due.countdown,until.remaining,description,urgency\n"
"report.ready.filter=+READY\n"
"report.ready.sort=start-,urgency-\n"
"report.ready.context=1\n"
"\n"
"report.blocked.description=Blocked tasks\n"
"report.blocked.columns=id,depends,project,priority,due,start.active,entry.age,description\n"
"report.blocked.labels=ID,Deps,Proj,Pri,Due,Active,Age,Description\n"
"report.blocked.sort=due+,priority-,start-,project+\n"
"report.blocked.filter=status:pending +BLOCKED\n"
"report.blocked.filter=status:pending -WAITING +BLOCKED\n"
"report.blocked.context=1\n"
"\n"
"report.unblocked.description=Unblocked tasks\n"
"report.unblocked.columns=id,depends,project,priority,due,start.active,entry.age,description\n"
"report.unblocked.labels=ID,Deps,Proj,Pri,Due,Active,Age,Description\n"
"report.unblocked.sort=due+,priority-,start-,project+\n"
"report.unblocked.filter=status:pending -BLOCKED\n"
"report.unblocked.filter=status:pending -WAITING -BLOCKED\n"
"report.unblocked.context=1\n"
"\n"
"report.blocking.description=Blocking tasks\n"
"report.blocking.labels=ID,UUID,A,Deps,Project,Tags,R,W,Sch,Due,Until,Description,Urg\n"
"report.blocking.columns=id,uuid.short,start.active,depends,project,tags,recur,wait,scheduled.remaining,due.relative,until.remaining,description.count,urgency\n"
"report.blocking.sort=urgency-,due+,entry+\n"
"report.blocking.filter=status:pending +BLOCKING\n"
"report.blocking.filter=status:pending -WAITING +BLOCKING\n"
"report.blocking.context=1\n"
"\n"
"report.timesheet.filter=(+PENDING and start.after:now-4wks) or (+COMPLETED and end.after:now-4wks)\n"
"report.timesheet.context=0\n"
"\n";
// Supported modifiers, synonyms on the same line.
@@ -1029,7 +1046,6 @@ bool Context::verbose (const std::string& token)
v != "project" && //
v != "sync" && //
v != "filter" && //
v != "unwait" && //
v != "override" && //
v != "context" && //
v != "recur") //
@@ -1043,7 +1059,7 @@ bool Context::verbose (const std::string& token)
if (! verbosity.count ("footnote"))
{
// TODO: Some of these may not use footnotes yet. They should.
for (auto flag : {"affected", "new-id", "new-uuid", "project", "unwait", "override", "recur"})
for (auto flag : {"affected", "new-id", "new-uuid", "project", "override", "recur"})
{
if (verbosity.count (flag))
{

View File

@@ -330,6 +330,14 @@ bool getDOM (const std::string& name, const Task& task, Variant& value)
return true;
}
// Special handling of status required for virtual waiting status
// implementation. Remove in 3.0.0.
if (ref.data.size () && size == 1 && canonical == "status")
{
value = Variant (ref.statusToText (ref.getStatus ()));
return true;
}
Column* column = Context::getContext ().columns[canonical];
if (ref.data.size () && size == 1 && column)

View File

@@ -355,22 +355,13 @@ void TF2::load_gc (Task& task)
{
Context::getContext ().tdb2.pending._tasks.push_back (task);
}
// 2.6.0: Waiting status is deprecated. Convert to pending to upgrade status
// field value in the data files.
else if (status == "waiting")
{
Datetime wait (task.get_date ("wait"));
if (wait < now)
{
task.set ("status", "pending");
task.remove ("wait");
// Unwaiting pending tasks is the only case not caught by the size()
// checks in TDB2::gc(), so we need to signal it here.
Context::getContext ().tdb2.pending._dirty = true;
if (Context::getContext ().verbose ("unwait"))
Context::getContext ().footnote (format ("Un-waiting task {1} '{2}'", task.id, task.get ("description")));
}
task.set ("status", "pending");
Context::getContext ().tdb2.pending._tasks.push_back (task);
Context::getContext ().tdb2.pending._dirty = true;
}
else
{
@@ -525,29 +516,26 @@ void TF2::dependency_scan ()
// Iterate and modify TDB2 in-place. Don't do this at home.
for (auto& left : _tasks)
{
if (left.has ("depends"))
for (auto& dep : left.getDependencyUUIDs ())
{
for (auto& dep : left.getDependencyUUIDs ())
for (auto& right : _tasks)
{
for (auto& right : _tasks)
if (right.get ("uuid") == dep)
{
if (right.get ("uuid") == dep)
// GC hasn't run yet, check both tasks for their current status
Task::status lstatus = left.getStatus ();
Task::status rstatus = right.getStatus ();
if (lstatus != Task::completed &&
lstatus != Task::deleted &&
rstatus != Task::completed &&
rstatus != Task::deleted)
{
// GC hasn't run yet, check both tasks for their current status
Task::status lstatus = left.getStatus ();
Task::status rstatus = right.getStatus ();
if (lstatus != Task::completed &&
lstatus != Task::deleted &&
rstatus != Task::completed &&
rstatus != Task::deleted)
{
left.is_blocked = true;
right.is_blocking = true;
}
// Only want to break out of the "right" loop.
break;
left.is_blocked = true;
right.is_blocking = true;
}
// Only want to break out of the "right" loop.
break;
}
}
}
@@ -1249,7 +1237,6 @@ void TDB2::show_diff (
// Possible scenarios:
// - task in pending that needs to be in completed
// - task in completed that needs to be in pending
// - waiting task in pending that needs to be un-waited
void TDB2::gc ()
{
Timer timer;

View File

@@ -147,7 +147,9 @@ Task::status Task::textToStatus (const std::string& input)
else if (input[0] == 'c') return Task::completed;
else if (input[0] == 'd') return Task::deleted;
else if (input[0] == 'r') return Task::recurring;
else if (input[0] == 'w') return Task::waiting;
// for compatibility, parse `w` as pending; Task::getStatus will
// apply the virtual waiting status if appropriate
else if (input[0] == 'w') return Task::pending;
throw format ("The status '{1}' is not valid.", input);
}
@@ -197,7 +199,7 @@ bool Task::has (const std::string& name) const
}
////////////////////////////////////////////////////////////////////////////////
std::vector <std::string> Task::all ()
std::vector <std::string> Task::all () const
{
std::vector <std::string> all;
for (const auto& i : data)
@@ -301,12 +303,26 @@ Task::status Task::getStatus () const
if (! has ("status"))
return Task::pending;
return textToStatus (get ("status"));
auto status = textToStatus (get ("status"));
// Implement the "virtual" Task::waiting status, which is not stored on-disk
// but is defined as a pending task with a `wait` attribute in the future.
// This is workaround for 2.6.0, remove in 3.0.0.
if (status == Task::pending && is_waiting ()) {
return Task::waiting;
}
return status;
}
////////////////////////////////////////////////////////////////////////////////
void Task::setStatus (Task::status status)
{
// the 'waiting' status is a virtual version of 'pending', so translate
// that back to 'pending' here
if (status == Task::waiting)
status = Task::pending;
set ("status", statusToText (status));
recalc_urgency = true;
@@ -559,6 +575,23 @@ bool Task::is_overdue () const
}
#endif
////////////////////////////////////////////////////////////////////////////////
bool Task::is_waiting () const
{
// note that is_waiting can return true for tasks in an actual status other
// than pending; in this case +WAITING will be set but the status will not be
// "waiting"
if (has ("wait"))
{
Datetime now;
Datetime wait (get_date ("wait"));
if (wait > now)
return true;
}
return false;
}
////////////////////////////////////////////////////////////////////////////////
// Attempt an FF4 parse first, using Task::parse, and in the event of an error
// try a JSON parse, otherwise a legacy parse (currently no legacy formats are
@@ -639,6 +672,14 @@ void Task::parse (const std::string& input)
// ..and similarly, update `tags` to match the `tag_..` attributes
fixTagsAttribute();
// same for `depends` / `dep_..`
if (data.find ("depends") != data.end ()) {
for (auto& dep : split(data["depends"], ',')) {
data[dep2Attr(dep)] = "x";
}
}
fixDependsAttribute();
recalc_urgency = true;
}
@@ -752,6 +793,11 @@ void Task::parseJSON (const json::object* root_obj)
{
std::map <std::string, std::string> annos;
// Fail if 'annotations' is not an array
if (i.second->type() != json::j_array) {
throw format ("Annotations is malformed: {1}", i.second->dump ());
}
auto atts = (json::array*)i.second;
for (auto& annotations : atts->_data)
{
@@ -905,8 +951,10 @@ std::string Task::composeJSON (bool decorate /*= false*/) const
if (! i.first.compare (0, 11, "annotation_", 11))
continue;
// Tags and dependencies are handled below
if (i.first == "tags" || isTagAttr (i.first))
// Tags are handled below
continue;
if (i.first == "depends" || isDepAttr (i.first))
continue;
// If value is an empty string, do not ever output it
@@ -950,45 +998,6 @@ std::string Task::composeJSON (bool decorate /*= false*/) const
++attributes_written;
}
// Dependencies are an array by default.
else if (i.first == "depends"
#ifdef PRODUCT_TASKWARRIOR
// 2016-02-20: Taskwarrior 2.5.0 introduced the 'json.depends.array' setting
// which defaulted to 'on', and emitted this JSON for
// dependencies:
//
// With json.depends.array=on "depends":["<uuid>","<uuid>"]
// With json.depends.array=off "depends":"<uuid>,<uuid>"
//
// Taskwarrior 2.5.1 defaults this to 'off', because Taskserver
// 1.0.0 and 1.1.0 both expect that. Taskserver 1.2.0 will
// accept both forms, but emit the 'off' variant.
//
// When Taskwarrior 2.5.0 is no longer the dominant version,
// and Taskserver 1.2.0 is released, the default for
// 'json.depends.array' can revert to 'on'.
&& Context::getContext ().config.getBoolean ("json.depends.array")
#endif
)
{
auto deps = split (i.second, ',');
out << "\"depends\":[";
int count = 0;
for (const auto& i : deps)
{
if (count++)
out << ',';
out << '"' << i << '"';
}
out << ']';
++attributes_written;
}
// Everything else is a quoted value.
else
{
@@ -1049,6 +1058,25 @@ std::string Task::composeJSON (bool decorate /*= false*/) const
++attributes_written;
}
auto depends = getDependencyUUIDs ();
if (depends.size() > 0)
{
out << ','
<< "\"depends\":[";
int count = 0;
for (const auto& dep : depends)
{
if (count++)
out << ',';
out << '"' << dep << '"';
}
out << ']';
++attributes_written;
}
#ifdef PRODUCT_TASKWARRIOR
// Include urgency.
if (decorate)
@@ -1153,8 +1181,10 @@ void Task::addDependency (int depid)
if (uuid == "")
throw format ("Could not create a dependency on task {1} - not found.", depid);
std::string depends = get ("depends");
if (depends.find (uuid) != std::string::npos)
// the addDependency(&std::string) overload will check this, too, but here we
// can give an more natural error message containing the id the user
// provided.
if (hasDependency (uuid))
{
Context::getContext ().footnote (format ("Task {1} already depends on task {2}.", id, depid));
return;
@@ -1170,23 +1200,16 @@ void Task::addDependency (const std::string& uuid)
if (uuid == get ("uuid"))
throw std::string ("A task cannot be dependent on itself.");
// Store the dependency.
std::string depends = get ("depends");
if (depends != "")
if (hasDependency (uuid))
{
// Check for extant dependency.
if (depends.find (uuid) == std::string::npos)
set ("depends", depends + ',' + uuid);
else
{
#ifdef PRODUCT_TASKWARRIOR
Context::getContext ().footnote (format ("Task {1} already depends on task {2}.", get ("uuid"), uuid));
Context::getContext ().footnote (format ("Task {1} already depends on task {2}.", get ("uuid"), uuid));
#endif
return;
}
return;
}
else
set ("depends", uuid);
// Store the dependency.
set (dep2Attr (uuid), "x");
// Prevent circular dependencies.
#ifdef PRODUCT_TASKWARRIOR
@@ -1195,64 +1218,102 @@ void Task::addDependency (const std::string& uuid)
#endif
recalc_urgency = true;
fixDependsAttribute();
}
#ifdef PRODUCT_TASKWARRIOR
////////////////////////////////////////////////////////////////////////////////
void Task::removeDependency (const std::string& uuid)
void Task::removeDependency (int id)
{
auto deps = split (get ("depends"), ',');
std::string uuid = Context::getContext ().tdb2.pending.uuid (id);
auto i = std::find (deps.begin (), deps.end (), uuid);
if (i != deps.end ())
{
deps.erase (i);
set ("depends", join (",", deps));
recalc_urgency = true;
}
else
throw format ("Could not delete a dependency on task {1} - not found.", uuid);
// The removeDependency(std::string&) method will check this too, but here we
// can give a more natural error message containing the id provided by the user
if (uuid == "" || !has (dep2Attr (uuid)))
throw format ("Could not delete a dependency on task {1} - not found.", id);
removeDependency (uuid);
}
////////////////////////////////////////////////////////////////////////////////
void Task::removeDependency (int id)
void Task::removeDependency (const std::string& uuid)
{
std::string depends = get ("depends");
std::string uuid = Context::getContext ().tdb2.pending.uuid (id);
if (uuid != "" && depends.find (uuid) != std::string::npos)
removeDependency (uuid);
auto depattr = dep2Attr (uuid);
if (has (depattr))
remove (depattr);
else
throw format ("Could not delete a dependency on task {1} - not found.", id);
throw format ("Could not delete a dependency on task {1} - not found.", uuid);
recalc_urgency = true;
fixDependsAttribute();
}
////////////////////////////////////////////////////////////////////////////////
bool Task::hasDependency (const std::string& uuid) const
{
auto depattr = dep2Attr (uuid);
return has (depattr);
}
////////////////////////////////////////////////////////////////////////////////
std::vector <int> Task::getDependencyIDs () const
{
std::vector <int> all;
for (auto& dep : split (get ("depends"), ','))
all.push_back (Context::getContext ().tdb2.pending.id (dep));
std::vector <int> ids;
for (auto& attr : all ()) {
if (!isDepAttr (attr))
continue;
auto dep = attr2Dep (attr);
ids.push_back (Context::getContext ().tdb2.pending.id (dep));
}
return all;
return ids;
}
////////////////////////////////////////////////////////////////////////////////
std::vector <std::string> Task::getDependencyUUIDs () const
{
return split (get ("depends"), ',');
std::vector <std::string> uuids;
for (auto& attr : all ()) {
if (!isDepAttr (attr))
continue;
auto dep = attr2Dep (attr);
uuids.push_back (dep);
}
return uuids;
}
////////////////////////////////////////////////////////////////////////////////
std::vector <Task> Task::getDependencyTasks () const
{
std::vector <Task> all;
for (auto& dep : split (get ("depends"), ','))
{
Task task;
Context::getContext ().tdb2.get (dep, task);
all.push_back (task);
}
auto uuids = getDependencyUUIDs ();
return all;
// 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 <Task> blocking;
if (uuids.size() > 0)
for (auto& it : Context::getContext ().tdb2.pending.get_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);
return blocking;
}
////////////////////////////////////////////////////////////////////////////////
std::vector <Task> Task::getBlockedTasks () const
{
auto uuid = get ("uuid");
std::vector <Task> blocked;
for (auto& it : Context::getContext ().tdb2.pending.get_tasks ())
if (it.getStatus () != Task::completed &&
it.getStatus () != Task::deleted &&
it.hasDependency (uuid))
blocked.push_back (it);
return blocked;
}
#endif
@@ -1311,10 +1372,10 @@ bool Task::hasTag (const std::string& tag) const
if (tag == "TAGGED") return getTagCount() > 0;
if (tag == "PARENT") return has ("mask") || has ("last"); // 2017-01-07: Deprecated in 2.6.0
if (tag == "TEMPLATE") return has ("last") || has ("mask");
if (tag == "WAITING") return get ("status") == "waiting";
if (tag == "PENDING") return get ("status") == "pending";
if (tag == "COMPLETED") return get ("status") == "completed";
if (tag == "DELETED") return get ("status") == "deleted";
if (tag == "WAITING") return is_waiting ();
if (tag == "PENDING") return getStatus () == Task::pending;
if (tag == "COMPLETED") return getStatus () == Task::completed;
if (tag == "DELETED") return getStatus () == Task::deleted;
#ifdef PRODUCT_TASKWARRIOR
if (tag == "UDA") return is_udaPresent ();
if (tag == "ORPHAN") return is_orphanPresent ();
@@ -1434,6 +1495,40 @@ const std::string Task::attr2Tag (const std::string& attr) const
return attr.substr(5);
}
////////////////////////////////////////////////////////////////////////////////
void Task::fixDependsAttribute ()
{
// Fix up the old `depends` attribute to match the `dep_..` attributes (or
// remove it if there are no deps)
auto deps = getDependencyUUIDs ();
if (deps.size () > 0) {
set ("depends", join (",", deps));
} else {
remove ("depends");
}
}
////////////////////////////////////////////////////////////////////////////////
bool Task::isDepAttr(const std::string& attr) const
{
return attr.compare(0, 4, "dep_") == 0;
}
////////////////////////////////////////////////////////////////////////////////
const std::string Task::dep2Attr (const std::string& tag) const
{
std::stringstream tag_attr;
tag_attr << "dep_" << tag;
return tag_attr.str();
}
////////////////////////////////////////////////////////////////////////////////
const std::string Task::attr2Dep (const std::string& attr) const
{
assert (isDepAttr (attr));
return attr.substr(4);
}
#ifdef PRODUCT_TASKWARRIOR
////////////////////////////////////////////////////////////////////////////////
// A UDA Orphan is an attribute that is not represented in context.columns.
@@ -2005,9 +2100,9 @@ float Task::urgency_inherit () const
{
float v = FLT_MIN;
#ifdef PRODUCT_TASKWARRIOR
// Calling dependencyGetBlocked is rather expensive.
// Calling getBlockedTasks is rather expensive.
// It is called recursively for each dependency in the chain here.
for (auto& task : dependencyGetBlocked (*this))
for (auto& task : getBlockedTasks ())
{
// Find highest urgency in all blocked tasks.
v = std::max (v, task.urgency ());
@@ -2048,7 +2143,7 @@ float Task::urgency_scheduled () const
////////////////////////////////////////////////////////////////////////////////
float Task::urgency_waiting () const
{
if (get_ref ("status") == "waiting")
if (is_waiting ())
return 1.0;
return 0.0;

View File

@@ -88,7 +88,7 @@ public:
void setAsNow (const std::string&);
bool has (const std::string&) const;
std::vector <std::string> all ();
std::vector <std::string> all () const;
const std::string identifier (bool shortened = false) const;
const std::string get (const std::string&) const;
const std::string& get_ref (const std::string&) const;
@@ -114,6 +114,7 @@ public:
bool is_udaPresent () const;
bool is_orphanPresent () const;
#endif
bool is_waiting () const;
status getStatus () const;
void setStatus (status);
@@ -143,8 +144,10 @@ public:
#ifdef PRODUCT_TASKWARRIOR
void removeDependency (int);
void removeDependency (const std::string&);
bool hasDependency (const std::string&) const;
std::vector <int> getDependencyIDs () const;
std::vector <std::string> getDependencyUUIDs () const;
std::vector <Task> getBlockedTasks () const;
std::vector <Task> getDependencyTasks () const;
std::vector <std::string> getUDAOrphanUUIDs () const;
@@ -173,6 +176,10 @@ private:
bool isTagAttr (const std::string&) const;
const std::string tag2Attr (const std::string&) const;
const std::string attr2Tag (const std::string&) const;
bool isDepAttr (const std::string&) const;
const std::string dep2Attr (const std::string&) const;
const std::string attr2Dep (const std::string&) const;
void fixDependsAttribute ();
void fixTagsAttribute ();
public:

View File

@@ -68,7 +68,9 @@ void ColumnDepends::setStyle (const std::string& value)
void ColumnDepends::measure (Task& task, unsigned int& minimum, unsigned int& maximum)
{
minimum = maximum = 0;
if (task.has (_name))
auto deptasks = task.getDependencyTasks ();
if (deptasks.size () > 0)
{
if (_style == "indicator")
{
@@ -77,25 +79,24 @@ void ColumnDepends::measure (Task& task, unsigned int& minimum, unsigned int& ma
else if (_style == "count")
{
minimum = maximum = 2 + format ((int) dependencyGetBlocking (task).size ()).length ();
minimum = maximum = 2 + format ((int) deptasks.size ()).length ();
}
else if (_style == "default" ||
_style == "list")
{
minimum = maximum = 0;
auto blocking = dependencyGetBlocking (task);
std::vector <int> blocking_ids;
blocking_ids.reserve(blocking.size());
for (auto& i : blocking)
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 : blocking)
for (auto& i : deptasks)
{
length = format (i.id).length ();
if (length > minimum)
@@ -112,7 +113,9 @@ void ColumnDepends::render (
int width,
Color& color)
{
if (task.has (_name))
auto deptasks = task.getDependencyTasks ();
if (deptasks.size () > 0)
{
if (_style == "indicator")
{
@@ -121,17 +124,15 @@ void ColumnDepends::render (
else if (_style == "count")
{
renderStringRight (lines, width, color, '[' + format (static_cast <int>(dependencyGetBlocking (task).size ())) + ']');
renderStringRight (lines, width, color, '[' + format (static_cast <int>(deptasks.size ())) + ']');
}
else if (_style == "default" ||
_style == "list")
{
auto blocking = dependencyGetBlocking (task);
std::vector <int> blocking_ids;
blocking_ids.reserve(blocking.size());
for (const auto& t : blocking)
blocking_ids.reserve(deptasks.size());
for (const auto& t : deptasks)
blocking_ids.push_back (t.id);
auto combined = join (" ", blocking_ids);

View File

@@ -64,23 +64,28 @@ bool CmdConfig::setConfigVariable (
for (auto& line : contents)
{
// Get l-trimmed version of the line
auto trimmed_line = trim (line, " ");
// If there is a comment on the line, it must follow the pattern.
auto comment = line.find ('#');
auto pos = line.find (name + '=');
auto pos = trimmed_line.find (name + '=');
if (pos != std::string::npos &&
(comment == std::string::npos ||
comment > pos))
// TODO: Use std::regex here
if (pos == 0)
{
found = true;
if (!confirmation ||
confirm (format ("Are you sure you want to change the value of '{1}' from '{2}' to '{3}'?", name, Context::getContext ().config.get (name), value)))
{
line = name + '=' + json::encode (value);
auto new_line = line.substr (0, pos + name.length () + 1) + json::encode (value);
// Preserve the comment
if (comment != std::string::npos)
line += ' ' + line.substr (comment);
new_line += " " + line.substr (comment);
// Rewrite the line
line = new_line;
change = true;
}
}
@@ -115,13 +120,13 @@ int CmdConfig::unsetConfigVariable (const std::string& name, bool confirmation /
{
auto lineDeleted = false;
// If there is a comment on the line, it must follow the pattern.
auto comment = line->find ('#');
auto pos = line->find (name + '=');
// Get l-trimmed version of the line
if (pos != std::string::npos &&
(comment == std::string::npos ||
comment > pos))
// If there is a comment on the line, it must follow the pattern.
auto pos = trim (*line, " ").find (name + '=');
// TODO: Use std::regex here
if (pos == 0)
{
found = true;

View File

@@ -59,6 +59,21 @@ CmdCustom::CmdCustom (
_category = Category::report;
}
////////////////////////////////////////////////////////////////////////////////
// Whether a report uses context is defined by the report.<name>.context
// configuration variable.
//
bool CmdCustom::uses_context () const
{
auto config = Context::getContext ().config;
auto key = "report." + _keyword + ".context";
if (config.has (key))
return config.getBoolean (key);
else
return _uses_context;
}
////////////////////////////////////////////////////////////////////////////////
int CmdCustom::execute (std::string& output)
{

View File

@@ -34,6 +34,7 @@ class CmdCustom : public Command
{
public:
CmdCustom (const std::string&, const std::string&, const std::string&);
bool uses_context () const override;
int execute (std::string&);
private:

View File

@@ -92,10 +92,6 @@ int CmdDelete::execute (std::string&)
if (! task.has ("end"))
task.setAsNow ("end");
// Un-wait the task, if waiting.
if (task.has ("wait"))
task.remove ("wait");
if (permission (question, filtered.size ()))
{
updateRecurrenceMask (task);

View File

@@ -98,10 +98,6 @@ int CmdDone::execute (std::string&)
task.addAnnotation (Context::getContext ().config.get ("journal.time.stop.annotation"));
}
// Un-wait the task, if waiting.
if (task.has ("wait"))
task.remove ("wait");
if (permission (taskDifferences (before, task) + question, filtered.size ()))
{
updateRecurrenceMask (task);

View File

@@ -667,7 +667,8 @@ void CmdEdit::parseTask (Task& task, const std::string& after, const std::string
value = findValue (after, "\n Dependencies:");
auto dependencies = split (value, ',');
task.remove ("depends");
for (auto& dep : task.getDependencyUUIDs ())
task.removeDependency (dep);
for (auto& dep : dependencies)
{
if (dep.length () >= 7)

View File

@@ -147,7 +147,7 @@ int CmdInfo::execute (std::string& output)
// dependencies: blocked
{
auto blocked = dependencyGetBlocking (task);
auto blocked = task.getDependencyTasks ();
if (blocked.size ())
{
std::stringstream message;
@@ -162,7 +162,7 @@ int CmdInfo::execute (std::string& output)
// dependencies: blocking
{
auto blocking = dependencyGetBlocked (task);
auto blocking = task.getBlockedTasks ();
if (blocking.size ())
{
std::stringstream message;
@@ -414,6 +414,7 @@ int CmdInfo::execute (std::string& output)
{
if (att.substr (0, 11) != "annotation_" &&
att.substr (0, 5) != "tags_" &&
att.substr (0, 4) != "dep_" &&
Context::getContext ().columns.find (att) == Context::getContext ().columns.end ())
{
row = view.addRow ();

View File

@@ -71,8 +71,7 @@ void CmdPurge::handleDeps (Task& task)
for (auto& blockedConst: Context::getContext ().tdb2.all_tasks ())
{
Task& blocked = const_cast<Task&>(blockedConst);
if (blocked.has ("depends") &&
blocked.get ("depends").find (uuid) != std::string::npos)
if (blocked.hasDependency (uuid))
{
blocked.removeDependency (uuid);
Context::getContext ().tdb2.modify (blocked);

View File

@@ -172,7 +172,6 @@ int CmdShow::execute (std::string& output)
" journal.time.start.annotation"
" journal.time.stop.annotation"
" json.array"
" json.depends.array"
" list.all.projects"
" list.all.tags"
" locking"

View File

@@ -53,6 +53,22 @@ CmdTimesheet::CmdTimesheet ()
_category = Command::Category::report;
}
////////////////////////////////////////////////////////////////////////////////
// Whether a the timesheet uses context is defined by the
// report.timesheet.context configuration variable.
//
bool CmdTimesheet::uses_context () const
{
auto config = Context::getContext ().config;
auto key = "report.timesheet.context";
if (config.has (key))
return config.getBoolean (key);
else
return _uses_context;
}
////////////////////////////////////////////////////////////////////////////////
int CmdTimesheet::execute (std::string& output)
{

View File

@@ -35,6 +35,7 @@ class CmdTimesheet : public Command
public:
CmdTimesheet ();
int execute (std::string&);
bool uses_context () const override;
};
#endif

View File

@@ -63,7 +63,7 @@ public:
bool read_only () const;
bool displays_id () const;
bool needs_gc () const;
bool uses_context () const;
virtual bool uses_context () const;
bool accepts_filter () const;
bool accepts_modifications () const;
bool accepts_miscellaneous () const;

View File

@@ -36,38 +36,6 @@
#define STRING_DEPEND_BLOCKED "Task {1} is blocked by:"
////////////////////////////////////////////////////////////////////////////////
std::vector <Task> dependencyGetBlocked (const Task& task)
{
auto uuid = task.get ("uuid");
std::vector <Task> blocked;
for (auto& it : Context::getContext ().tdb2.pending.get_tasks ())
if (it.getStatus () != Task::completed &&
it.getStatus () != Task::deleted &&
it.has ("depends") &&
it.get ("depends").find (uuid) != std::string::npos)
blocked.push_back (it);
return blocked;
}
////////////////////////////////////////////////////////////////////////////////
std::vector <Task> dependencyGetBlocking (const Task& task)
{
auto depends = task.get ("depends");
std::vector <Task> blocking;
if (depends != "")
for (auto& it : Context::getContext ().tdb2.pending.get_tasks ())
if (it.getStatus () != Task::completed &&
it.getStatus () != Task::deleted &&
depends.find (it.get ("uuid")) != std::string::npos)
blocking.push_back (it);
return blocking;
}
////////////////////////////////////////////////////////////////////////////////
// Returns true if the supplied task adds a cycle to the dependency chain.
bool dependencyIsCircular (const Task& task)
@@ -150,12 +118,12 @@ bool dependencyIsCircular (const Task& task)
//
void dependencyChainOnComplete (Task& task)
{
auto blocking = dependencyGetBlocking (task);
auto blocking = task.getDependencyTasks ();
// If the task is anything but the tail end of a dependency chain.
if (blocking.size ())
{
auto blocked = dependencyGetBlocked (task);
auto blocked = task.getBlockedTasks ();
// Nag about broken chain.
if (Context::getContext ().config.getBoolean ("dependency.reminder"))
@@ -207,7 +175,7 @@ void dependencyChainOnStart (Task& task)
{
if (Context::getContext ().config.getBoolean ("dependency.reminder"))
{
auto blocking = dependencyGetBlocking (task);
auto blocking = task.getDependencyTasks ();
// If the task is anything but the tail end of a dependency chain, nag about
// broken chain.

View File

@@ -78,6 +78,7 @@ std::string taskDifferences (const Task& before, const Task& after)
<< format ("{1} will be deleted.", Lexer::ucFirst (name))
<< "\n";
// TODO: #2572 - rewrite to look at dep_ and tag_
for (auto& name : afterOnly)
{
if (name == "depends")
@@ -384,12 +385,12 @@ void feedback_unblocked (const Task& task)
if (Context::getContext ().verbose ("affected"))
{
// Get a list of tasks that depended on this task.
auto blocked = dependencyGetBlocked (task);
auto blocked = task.getBlockedTasks ();
// Scan all the tasks that were blocked by this task
for (auto& i : blocked)
{
auto blocking = dependencyGetBlocking (i);
auto blocking = i.getDependencyTasks ();
if (blocking.size () == 0)
{
if (i.id)

View File

@@ -59,8 +59,6 @@ std::string colorizeError (const std::string&);
std::string colorizeDebug (const std::string&);
// dependency.cpp
std::vector <Task> dependencyGetBlocked (const Task&);
std::vector <Task> dependencyGetBlocking (const Task&);
bool dependencyIsCircular (const Task&);
void dependencyChainOnComplete (Task&);
void dependencyChainOnStart (Task&);

View File

@@ -200,22 +200,25 @@ static bool sort_compare (int left, int right)
// Depends string.
else if (field == "depends")
{
// Raw data is a comma-separated list of uuids
auto left_string = (*global_data)[left].get_ref (field);
auto right_string = (*global_data)[right].get_ref (field);
// 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());
if (left_string == right_string)
if (left_deps == right_deps)
continue;
if (left_string == "" && right_string != "")
if (left_deps.size () == 0 && right_deps.size () > 0)
return ascending;
if (left_string != "" && right_string == "")
if (left_deps.size () > 0 && right_deps.size () == 0)
return !ascending;
// Sort on the first dependency.
left_number = Context::getContext ().tdb2.id (left_string.substr (0, 36));
right_number = Context::getContext ().tdb2.id (right_string.substr (0, 36));
left_number = Context::getContext ().tdb2.id (left_deps[0]);
right_number = Context::getContext ().tdb2.id (right_deps[0]);
if (left_number == right_number)
continue;