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
commit d91e30ee13
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 640 additions and 302 deletions

View file

@ -24,9 +24,6 @@ jobs:
- name: "Fedora 34"
runner: ubuntu-latest
dockerfile: fedora34
- name: "Debian Stable"
runner: ubuntu-latest
dockerfile: debianstable
- name: "Debian Testing"
runner: ubuntu-latest
dockerfile: debiantesting

View file

@ -157,6 +157,7 @@ The following submitted code, packages or analysis, and deserve special thanks:
Julien Rabinow
Daniel Mowitz
Scott Mcdermott
Bharatvaj
Thanks to the following, who submitted detailed bug reports and excellent
suggestions:
@ -344,3 +345,4 @@ suggestions:
Arvedui
reportaman
Pablo Vizcay
Jake C.

View file

@ -8,32 +8,42 @@
- TW #1804 Importing malformed annotation (without entry timestamp) causes
segmentation fault.
Thanks to David Badura.
- TW #1824 Fixed countdown formatting
Thanks to Sebastian Uharek
- TW #1896 Parser cannot handle empty parentheses
Thanks to Tomas Babej.
- TW #1908 Cannot create task with explicit description 'start ....'
Thanks to Matt Chun-Lum.
- TW #1911 Support holidays longer then 1 day
Thanks to Daniel Mowitz.
- TW #1913 Project names with dashes and attribute names fail to parse
Thanks to Yanick Champoux.
- TW #1938 Missing annotation on import if entry is duplicated
Thanks to Florian.
- TW #1955 Adding tasks in context.
Thanks to Jean-Francois Joly, Matt Smith.
- TW #1960 Fixed bug with double escaped single quotes.
Thanks to Sebastian Uharek
- TW #2004 "shell" should not be expand to "exec tasksh"
Thanks to Arvedui
- TW #2007 Compute number of current tasks correctly
Thanks to Janik Rabe
- TW #2017 Support 64-bit datetime values
Thanks to Evgeniy Vasilev
- TW #2257 UDA string fields can't start with certain keywords
Thanks to Michael Russell.
- TW #2060 Review timestamp is displayed as unix time, not formatted
Thanks to JavaZauber
- TW #2093 wrong order under projects command
Thanks to Beka, Max Rossmannek.
- TW #2101 Numeric UDA values above 2,147,483,647 overflow without warning
Thanks to Adam Monsen.
- TW #2136 Configuration options can be overwritten for current context
Thanks to Sebastian Uharek
- TW #2208 Feature: added coloring of dates with scheduled tasks to calendar
Thanks to Sebastian Uharek
- TW #2247 Configuration override rc.verbose:off not respected
Thanks to Vignesh Prabhu.
- TW #2257 UDA string fields can't start with certain keywords
Thanks to Michael Russell.
- TW #2290 Support moving the config file to XDG_CONFIG_HOME
Thanks to Julien Rabinow.
- TW #2292 CmdEdit: Interruption should remove lock file
@ -77,13 +87,20 @@
- TW #2536 Feature: inclusive range-end attribute modifier 'by' so 'end of'
named dates can be filtered inclusively
Thanks to Scott Mcdermott
- TW #1824 Fixed countdown formatting
Thanks to Sebastian Uharek
- #2208 Feature: added coloring of dates with scheduled tasks to calendar
Thanks to Sebastian Uharek
- Feature: Configuration options can be overwritten for current
context
Thanks to Sebastian Uharek
- TW #2550 Write context skipped if description contains an identifier
Thanks to Sebastian Fricke.
- TW #2554 Remove the waiting state, and consider any task with wait>now to be
waiting
Thanks to Dustin J. Mitchell
- TW #2560 Add report.<name>.context configuration variable
Thanks to Jake C.
- TW #2569 The `json.depends.array` configuration option is now ignored.
Dependencies are always represented as an array in JSON output.
Thanks to Dustin J. Mitchell
- TW #2580 Importing malformed JSON task crashes TW
Thanks to bharatvaj.
- TW #2581 Config entry with a trailing comment cannot be modified
------ current release ---------------------------

17
NEWS
View file

@ -27,7 +27,13 @@ New Features in Taskwarrior 2.6.0
'due.by:eod', which it would not otherwise. It also works with
whole units like days, e.g. 'add test due:2021-07-17' would not match
'due.before:tomorrow' (on the 16th), but would match 'due.by:tomorrow'.
- Waiting is now an entirely "virtual" concept, based on a task's
'wait' property and the current time. Task is consiered "waiting" if its
wait attribute is in the future. TaskWarrior no longer explicitly
"unwaits" a task (the wait attribute is not removed once its value is in
the past), so the "unwait' verbosity token is no longer available.
This allows for filtering for tasks that were waiting in the past
intervals, but are not waiting anymore.
New Commands in Taskwarrior 2.6.0
@ -38,6 +44,10 @@ New Configuration Options in Taskwarrior 2.6.0
- The context definitions for reporting commmands are now stored in
"context.<name>.read". Context definitions for write commands are now
supported using "context.<name>.write" configuration variable.
- Each report (and the timesheet command) can explicitly opt-out from the
currently active context by setting the report.<name>.context variable to 0
(defaults to 1). Useful for defining universal reports that ignore
currently set context, such as 'inbox' report for GTD methodology.
- Multi-day holidays are now supported. Use holiday.<name>.start=<date> and
holiday.<name>.end=<date> to specify a range-based holiday, such as a
vacation.
@ -46,6 +56,8 @@ New Configuration Options in Taskwarrior 2.6.0
Newly Deprecated Features in Taskwarrior 2.6.0
- The 'PARENT' and 'CHILD' virtual tags are replaced by 'TEMPLATE' and 'INSTANCE'.
- The 'waiting' status is now deprecated. We recommend using +WAITING virtual tag
or wait-attribute based filters, such as 'wait.before:eow' instead.
Fixed regressions in 2.6.0
@ -56,6 +68,9 @@ Fixed regressions in 2.6.0
the first configuration override. Otherwise task would by default inform
about the other overrides (see #2247). This was a regression introduced in
2.5.2.
- The attribute values of the form "<attribute name>-<arbitrary string>", for
example "due-nextweek" or "scheduled-work" would fail to parse (see
#1913). This was a regression introduced in 2.5.1.
Removed Features in 2.6.0

View file

@ -304,7 +304,7 @@ value.
.B task <filter> ready
Shows a page of the most urgent ready tasks, sorted by urgency with started
tasks first. A ready task is one that is either unscheduled, or has a scheduled
date that is past and has no wait date.
date that is past and is not waiting.
.TP
.B task <filter> oldest
@ -796,9 +796,10 @@ to 25 lines.
.TP
.B wait:<wait-date>
When a task is given a wait date, it is hidden from most reports by changing
its status to 'waiting'. When that date is passed, the status is changed back
to 'pending', and the task becomes visible.
When a task is given a wait date, it is hidden from most built-in reports, which
exclude +WAITING. When the date is in the past, the task is not considered +WAITING,
and again becomes visible. Note that, for compatibilty, such tasks are shown as
having status "waiting", but this will change in a future release.
.TP
.B depends:<id1,id2 ...>

View file

@ -299,11 +299,10 @@ control specific occasions when output is generated. This list may contain:
sync Feedback about sync
filter Shows the filter used in the command
context Show the current context. Displayed in footnote.
unwait Notification when a task leaves the 'waiting' state
override Notification when configuration options are overridden
recur Notification when a new recurring task instance is created
"affected", "new-id", "new-uuid", "project", "unwait", "override" and "recur"
"affected", "new-id", "new-uuid", "project", "override" and "recur"
imply "footnote".
Note that the "1" setting is equivalent to all the tokens being specified,
@ -312,7 +311,7 @@ and the "nothing" setting is equivalent to none of the tokens being specified.
Here are the shortcut equivalents:
verbose=on
verbose=blank,header,footnote,label,new-id,affected,edit,special,project,sync,filter,unwait,override,recur
verbose=blank,header,footnote,label,new-id,affected,edit,special,project,sync,filter,override,recur
verbose=0
verbose=blank,label,new-id,edit
@ -425,12 +424,6 @@ array.
With json.array=0, export writes raw JSON objects to STDOUT, one per line.
Defaults to "1".
.TP
.B json.depends.array=1
Determines whether the export command encodes dependencies as an array of string
UUIDs, or one comma-separated string.
Defaults to "1".
.TP
.B _forcecolor=1
Taskwarrior shuts off color automatically when the output is not sent directly
@ -1286,6 +1279,12 @@ The description for report X when running the "task help" command.
This is a comma-separated list of columns and formatting specifiers. See the
command 'task columns' for a full list of options and examples.
.TP
.B report.X.context
A boolean value representing whether the given report should respect (apply)
the currently active context. See CONTEXT section for details about context.
Defaults to 1.
.TP
.B report.X.labels
The labels for each column that will be used when generating report X. The

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,7 +630,8 @@ void CLI2::addContext (bool readable, bool writeable)
if (contextString.empty ())
return;
// Detect if UUID or ID is set, and bail out
// For readable contexts: Detect if UUID or ID is set, and bail out
if (readable)
for (auto& a : _args)
{
if (a._lextype == Lexer::Type::uuid ||
@ -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")));
}
Context::getContext ().tdb2.pending._tasks.push_back (task);
Context::getContext ().tdb2.pending._dirty = true;
}
else
{
@ -524,8 +515,6 @@ 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 ())
{
@ -552,7 +541,6 @@ void TF2::dependency_scan ()
}
}
}
}
////////////////////////////////////////////////////////////////////////////////
const std::string TF2::dump ()
@ -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 != "")
{
// Check for extant dependency.
if (depends.find (uuid) == std::string::npos)
set ("depends", depends + ',' + uuid);
else
if (hasDependency (uuid))
{
#ifdef PRODUCT_TASKWARRIOR
Context::getContext ().footnote (format ("Task {1} already depends on task {2}.", get ("uuid"), uuid));
#endif
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 ();
// 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;
}
return all;
////////////////////////////////////////////////////////////////////////////////
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;

View file

@ -490,6 +490,33 @@ class ContextEvaluationTest(TestCase):
self.assertNotIn("work today task", output)
self.assertNotIn("home today task", output)
def test_context_ignored(self):
"""Test the context is not applied with report list command if
report.list.context is set to 0."""
# Turn off context for this report
self.t.config("report.list.context", "0")
# Get the tasks
code, out, err = self.t('list')
# Assert all the tasks are present in the output
self.assertIn("work task", out)
self.assertIn("home task", out)
self.assertIn("work today task", out)
self.assertIn("home today task", out)
# Set the home context and rerun the report
self.t('context home')
code, out, err = self.t('list')
# Assert nothing changed - all the tasks are present in the output
self.assertIn("work task", out)
self.assertIn("home task", out)
self.assertIn("work today task", out)
self.assertIn("home today task", out)
class ContextErrorHandling(TestCase):
def setUp(self):

View file

@ -146,7 +146,6 @@ class TestExportCommand(TestCase):
self.t(('add', 'everything depends on me task'))
self.t(('add', 'wrong, everything depends on me task'))
self.t('1 modify depends:2,3')
self.t.config('json.depends.array', 'on')
deps = self.export(1)['depends']
self.assertType(deps, list)
@ -155,19 +154,6 @@ class TestExportCommand(TestCase):
for uuid in deps:
self.assertString(uuid, UUID_REGEXP, regexp=True)
def test_export_depends_oldformat(self):
self.t(('add', 'everything depends on me task'))
self.t(('add', 'wrong, everything depends on me task'))
self.t('1 modify depends:2,3')
code, out, err = self.t("rc.json.array=off rc.json.depends.array=off 1 export")
deps = json.loads(out)["depends"]
self.assertString(deps)
self.assertEqual(len(deps.split(",")), 2)
for uuid in deps.split(','):
self.assertString(uuid, UUID_REGEXP, regexp=True)
def test_export_urgency(self):
self.t('add urgent task +urgent')

View file

@ -1130,6 +1130,24 @@ class TestBug1915(TestCase):
self.assertIn("thingB", out)
self.assertNotIn("thingC", out)
class Test2577(TestCase):
def setUp(self):
self.t = Task()
def test_filtering_for_datetime_like(self):
"""2577: Check that filtering for datetime-like project names works"""
self.t('add one pro:sat') # looks like "saturday"
self.t('add two pro:whatever')
# This should not fail (fails on 2.5.3)
code, out, err = self.t('pro:sat')
# Assert expected output, but the crucial part of this test is success
# of the call above
self.assertIn("one", out)
if __name__ == "__main__":
from simpletap import TAPTestRunner
unittest.main(testRunner=TAPTestRunner())

View file

@ -187,15 +187,6 @@ class TestImport(TestCase):
self.assertIn("Imported 3 tasks", err)
self.assertData1()
def test_import_old_depend(self):
"""One dependency used to be a plain string"""
_data = """{"uuid":"a0000000-a000-a000-a000-a00000000000","depends":"a1111111-a111-a111-a111-a11111111111","description":"zero","project":"A","status":"pending","entry":"1234567889"}"""
self.t("import", input=self.data1)
self.t("import", input=_data)
self.t.config("json.depends.array", "0")
_t = self.t.export("a0000000-a000-a000-a000-a00000000000")[0]
self.assertEqual(_t["depends"], "a1111111-a111-a111-a111-a11111111111")
def test_import_old_depends(self):
"""Several dependencies used to be a comma seperated string"""
_data = """{"uuid":"a0000000-a000-a000-a000-a00000000000","depends":"a1111111-a111-a111-a111-a11111111111,a2222222-a222-a222-a222-a22222222222","description":"zero","project":"A","status":"pending","entry":"1234567889"}"""
@ -207,7 +198,6 @@ class TestImport(TestCase):
def test_import_new_depend(self):
"""One dependency is a single array element"""
self.t.config('json.depends.array', 'on')
_data = """{"uuid":"a0000000-a000-a000-a000-a00000000000","depends":["a1111111-a111-a111-a111-a11111111111"],"description":"zero","project":"A","status":"pending","entry":"1234567889"}"""
self.t("import", input=self.data1)
self.t("import", input=_data)
@ -216,7 +206,6 @@ class TestImport(TestCase):
def test_import_new_depends(self):
"""Several dependencies are an array"""
self.t.config('json.depends.array', 'on')
_data = """{"uuid":"a0000000-a000-a000-a000-a00000000000","depends":["a1111111-a111-a111-a111-a11111111111","a2222222-a222-a222-a222-a22222222222"],"description":"zero","project":"A","status":"pending","entry":"1234567889"}"""
self.t("import", input=self.data1)
self.t("import", input=_data)
@ -303,6 +292,12 @@ class TestImportValidate(TestCase):
code, out, err = self.t.runError("import", input=j)
self.assertIn("The status 'foo' is not valid.", err)
def test_import_malformed_annotation(self):
"""Verify invalid 'annnotations' is caught"""
j = '{"description": "bad", "annotations": "bad"}'
code, out, err = self.t.runError("import", input=j)
self.assertIn('Annotations is malformed: "bad"', err)
class TestImportWithoutISO(TestCase):
def setUp(self):

View file

@ -480,6 +480,26 @@ class TestBug1627(TestCase):
self.assertEqual("mon\n", out)
class TestBug1900(TestCase):
def setUp(self):
"""Executed before each test in the class"""
self.t = Task()
def test_project_eval(self):
"""1900: Project name can contain dashes"""
self.t("add foo project:due-b")
code, out, err = self.t("_get 1.project")
self.assertEqual("due-b\n", out)
self.t("add foo project:scheduled-home")
code, out, err = self.t("_get 2.project")
self.assertEqual("scheduled-home\n", out)
self.t("add foo project:entry-work")
code, out, err = self.t("_get 3.project")
self.assertEqual("entry-work\n", out)
class TestBug1904(TestCase):
def setUp(self):
"""Executed before each test in the class"""

17
test/tw-2189.t Executable file
View file

@ -0,0 +1,17 @@
#!/bin/bash
. bash_tap_tw.sh
task add "foo \' bar"
task list
# Assert the task was correctly added
[[ ! -z `task list | grep "foo ' bar"` ]]
[[ `task _get 1.description` == "foo ' bar" ]]
# Bonus: Assert escaped double quotes are also handled correctly
task add 'foo \" bar'
task list
# Assert the task was correctly added
[[ ! -z `task list | grep 'foo " bar'` ]]
[[ `task _get 2.description` == 'foo " bar' ]]

33
test/tw-2550.t Executable file
View file

@ -0,0 +1,33 @@
#!/usr/bin/env bash
. bash_tap_tw.sh
# Setup the context
task config context.work.read '+work'
task config context.work.write '+work'
# Create a task outside of the context
task add outside
# Activate the context
task context work
# Add multiple tasks within the context, some of which contain numbers or uuids
task add inside
task add inside 2
task add inside 3
task add inside aabbccdd
task add inside 4-5
# Assertion: Task defined outside of the context should not show up
[[ -z `task all | grep outside` ]]
# Five tasks were defined within the context
task count
[[ `task count` == "5" ]]
# Unset the context
task context none
# Exactly five tasks have the tag work
task +work count
[[ `task +work count` == "5" ]]

45
test/tw-2563.t Executable file
View file

@ -0,0 +1,45 @@
#!/usr/bin/env bash
# This tests the migration path from 2.5.3 or earlier to 2.6.0 with respect to
# the upgrade of the status field from waiting to pending
. bash_tap_tw.sh
# Setup
task add Actionable task wait:yesterday
task add Non-actionable task wait:tomorrow+1h
# Simulate this was created in 2.5.3 or earlier (status is equal to waiting,
# not pending). Using more cumbersome sed syntax for Mac OS-X compatibility.
sed -i".bak" 's/pending/waiting/g' $TASKDATA/pending.data
rm -f $TASKDATA/pending.data.bak
# Trigger upgrade
task all
# Report file content
echo pending.data
cat $TASKDATA/pending.data
echo completed.data
cat $TASKDATA/completed.data
# Assertion: Exactly one task is considered waiting
[[ `task +WAITING count` == "1" ]]
[[ `task status:waiting count` == "1" ]]
# Assertion: Exactly one task is considered pending
[[ `task +PENDING count` == "1" ]]
[[ `task status:pending count` == "1" ]]
# Assertion: Task 1 is pending
[[ `task _get 1.status` == "pending" ]]
# Assertion: Task 2 is waiting
[[ `task _get 2.status` == "waiting" ]]
# Assertion: No lines in data files with "waiting" status
[[ -z `cat $TASKDATA/pending.data | grep waiting` ]]
[[ -z `cat $TASKDATA/completed.data | grep waiting` ]]
# Assertion: No tasks were moved into completed.data
cat $TASKDATA/pending.data | wc -l | tr -d ' '
[[ `cat $TASKDATA/pending.data | wc -l | tr -d ' '` == "2" ]]
[[ `cat $TASKDATA/completed.data | wc -l | tr -d ' '` == "0" ]]

14
test/tw-2581.t Executable file
View file

@ -0,0 +1,14 @@
#!/usr/bin/env bash
# Test setting configuration variable with a trailing comment works
. bash_tap_tw.sh
# Add configuration variable with a trailing comment into taskrc
echo 'weekstart=Monday # Europe standard' >> taskrc
cat taskrc
# Use config the change the value to "Sunday"
task config weekstart Sunday
# Ensure the comment was preserved and value changed
cat taskrc | grep weekstart=Sunday
[[ `cat taskrc | grep weekstart=Sunday` == 'weekstart=Sunday # Europe standard' ]]

View file

@ -62,8 +62,6 @@ class TestWait(TestCase):
self.assertIn("visible", out)
self.assertIn("hidden", out)
self.assertIn("Un-waiting task 2 'hidden'", err)
class TestBug434(TestCase):
# Bug #434: Task should not prevent users from marking as done tasks with
@ -100,30 +98,30 @@ class Test1486(TestCase):
self.assertNotIn('regular', out)
class TestFeature2322(TestCase):
class TestFeature2563(TestCase):
def setUp(self):
"""Executed before each test in the class"""
self.t = Task()
def test_done_unwait(self):
"""2322: Done should un-wait a waiting task"""
"""2563: Done should NOT remove the wait attribute"""
self.t("add foo wait:tomorrow")
code, out, err = self.t("export")
self.assertIn('"wait":', out)
self.t("1 done")
code, out, err = self.t("export")
self.assertNotIn('"wait":', out)
self.assertIn('"wait":', out)
def test_delete_unwait(self):
"""2322: Deleteion should un-wait a waiting task"""
"""2563: Delete should NOT remove the wait attribute"""
self.t("add bar wait:tomorrow")
code, out, err = self.t("export")
self.assertIn('"wait":', out)
self.t("1 delete", input="y\n")
code, out, err = self.t("export")
self.assertNotIn('"wait":', out)
self.assertIn('"wait":', out)
if __name__ == "__main__":