Compare commits
69 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7400e6ed6b | ||
|
|
93394e7054 | ||
|
|
f030154ef6 | ||
|
|
10a2225aae | ||
|
|
de793e5902 | ||
|
|
1fc1884017 | ||
|
|
75ce386b44 | ||
|
|
e60fdafdaa | ||
|
|
d541e0da65 | ||
|
|
59a1729a05 | ||
|
|
112ac54a57 | ||
|
|
0cc2de6179 | ||
|
|
28e268bd26 | ||
|
|
7321febe4f | ||
|
|
96d6c1df9f | ||
|
|
cb058f2e4b | ||
|
|
c564bbc0b7 | ||
|
|
b066a17ebe | ||
|
|
277ce0e226 | ||
|
|
65830dd705 | ||
|
|
53127bf844 | ||
|
|
2ea6dd627e | ||
|
|
db26a28bf9 | ||
|
|
6e9ad1048d | ||
|
|
e98b61f2b5 | ||
|
|
4fa1c0bcfb | ||
|
|
e3dd91d45e | ||
|
|
e768e2c100 | ||
|
|
ac24ec1387 | ||
|
|
3a61289f6c | ||
|
|
9f149a7f35 | ||
|
|
70f83b34ef | ||
|
|
8d3953183a | ||
|
|
2812a8c77a | ||
|
|
3af5ceadc1 | ||
|
|
0d9e402d3e | ||
|
|
dede40bc4e | ||
|
|
3a00956144 | ||
|
|
01add8a34a | ||
|
|
a09712d5d2 | ||
|
|
8074e509ba | ||
|
|
af10774aec | ||
|
|
d54c7e090e | ||
|
|
3937f1efb0 | ||
|
|
3e8190831c | ||
|
|
3c2b74a36f | ||
|
|
0558b6c7aa | ||
|
|
774f6df210 | ||
|
|
1e1c0e8f04 | ||
|
|
caf0f9db3e | ||
|
|
da43078eba | ||
|
|
6fae705b43 | ||
|
|
e4f1e05c1d | ||
|
|
4559287d07 | ||
|
|
2d4776586d | ||
|
|
ed4b932530 | ||
|
|
f0f704fc99 | ||
|
|
403c44b1fa | ||
|
|
493f36ecdd | ||
|
|
5d8f8dac35 | ||
|
|
f5dce013ce | ||
|
|
531881f651 | ||
|
|
7de681aa3b | ||
|
|
04ef785eea | ||
|
|
e1cfb91d42 | ||
|
|
72930708ec | ||
|
|
58763fd49f | ||
|
|
a89c875c49 | ||
|
|
e0f598f91c |
13
.github/workflows/tests.yaml
vendored
13
.github/workflows/tests.yaml
vendored
@@ -9,9 +9,6 @@ jobs:
|
||||
- name: "Centos 8"
|
||||
runner: ubuntu-latest
|
||||
dockerfile: centos8
|
||||
- name: "Fedora 31"
|
||||
runner: ubuntu-latest
|
||||
dockerfile: fedora31
|
||||
- name: "Fedora 32"
|
||||
runner: ubuntu-latest
|
||||
dockerfile: fedora32
|
||||
@@ -21,6 +18,9 @@ jobs:
|
||||
- name: "Fedora 34"
|
||||
runner: ubuntu-latest
|
||||
dockerfile: fedora34
|
||||
- name: "Fedora 35"
|
||||
runner: ubuntu-latest
|
||||
dockerfile: fedora35
|
||||
- name: "Debian Testing"
|
||||
runner: ubuntu-latest
|
||||
dockerfile: debiantesting
|
||||
@@ -33,9 +33,12 @@ jobs:
|
||||
- name: "Ubuntu 21.04"
|
||||
runner: ubuntu-latest
|
||||
dockerfile: ubuntu2104
|
||||
- name: "OpenSUSE 15.0"
|
||||
- name: "Ubuntu 21.10"
|
||||
runner: ubuntu-latest
|
||||
dockerfile: opensuse1500
|
||||
dockerfile: ubuntu2110
|
||||
- name: "OpenSUSE 15"
|
||||
runner: ubuntu-latest
|
||||
dockerfile: opensuse15
|
||||
- name: "Archlinux Base (Rolling)"
|
||||
runner: ubuntu-latest
|
||||
dockerfile: arch
|
||||
|
||||
@@ -9,7 +9,7 @@ set (HAVE_CMAKE true)
|
||||
project (task)
|
||||
include (CXXSniffer)
|
||||
|
||||
set (PROJECT_VERSION "2.6.1")
|
||||
set (PROJECT_VERSION "2.6.2")
|
||||
|
||||
OPTION (ENABLE_WASM "Enable 'wasm' support" OFF)
|
||||
|
||||
|
||||
27
ChangeLog
27
ChangeLog
@@ -1,4 +1,25 @@
|
||||
2.6.1 () -
|
||||
------ current release ---------------------------
|
||||
2.6.2 -
|
||||
|
||||
- TW #502 Sequence of IDs doesn't work with attribute "depends"
|
||||
Thanks to Andreas Kalex and Reg for reporting.
|
||||
- TW #2648 xdg-open is not available on Mac OS-X
|
||||
Thanks to chapterjson for reporting.
|
||||
- TW #2655 The bulk removal of depends: and tags: is ignored
|
||||
Thanks to angelus2014 for reporting.
|
||||
- TW #2664 Tag exclusion should be detected as invalid write context
|
||||
Thanks to bentwitthold for reporting.
|
||||
- TW #2671 The value of soww named date is incorrect
|
||||
Thanks to Lennart Kill for reporting.
|
||||
- TW #2689 Corruption of the depends attribute upon syncing with taskd 1.1.0
|
||||
Thanks to Klaus Ethgen for reporting, Dusting J. Mitchell for
|
||||
contributing.
|
||||
- TW #2707 Assigning dependencies via ID ranges
|
||||
Thanks to jaker-dotcom for reporting.
|
||||
- TW #2748 Recurring report does not include parent tasks
|
||||
Thanks to Klaus Ethgen for reporting.
|
||||
|
||||
2.6.1 (2021-10-19) - a696b6b155f9c8af87a6a496c0d298c58e6fe369
|
||||
|
||||
- TW #2619 Fish autocompletion fails when completing tag removal
|
||||
Thanks to Alexandre Provencio for reporting, Tin Lai and Alexandre
|
||||
@@ -10,7 +31,7 @@
|
||||
Thanks to Scott Mcdermott for reporting.
|
||||
- TW #2626 Waiting report lists deleted and/or completed tasks.
|
||||
Thanks to Don Harper for reporting.
|
||||
- TW #2629 New-style context definition should take precendence over old-style
|
||||
- TW #2629 New-style context definition should take precedence over old-style
|
||||
even if the new-style is an empty string.
|
||||
Thanks to Denis Kasak for reporting.
|
||||
- TW #2632 Cannot build on Cygwin
|
||||
@@ -18,8 +39,6 @@
|
||||
- TW #2639 Unable to use ranges for some tasks with IDs above 1000.
|
||||
Thanks to Manuel Haussmann for reporting.
|
||||
|
||||
------ current release ---------------------------
|
||||
|
||||
2.6.0 (2021-10-03) - 8174287f917a3b24c19a6601ba36fca9bdaa78c9
|
||||
|
||||
- TW #1654 "Due" parsing behaviour seems inconsistent
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
[](https://github.com/GothenburgBitFactory/taskwarrior/actions)
|
||||
[](https://github.com/GothenburgBitFactory/taskwarrior/releases/latest)
|
||||
[](https://github.com/GothenburgBitFactory/taskwarrior/releases/latest)
|
||||

|
||||
[](https://github.com/sponsors/GothenburgBitFactory/)
|
||||
</br>
|
||||
[](https://twitter.com/taskwarrior)
|
||||
@@ -87,9 +86,9 @@ There are many binary packages available, but to install from source requires:
|
||||
|
||||
Download the tarball, and expand it:
|
||||
|
||||
$ curl -O https://taskwarrior.org/download/task-2.6.1.tar.gz
|
||||
$ tar xzf task-2.6.1.tar.gz
|
||||
$ cd task-2.6.1
|
||||
$ curl -O https://taskwarrior.org/download/task-2.6.2.tar.gz
|
||||
$ tar xzf task-2.6.2.tar.gz
|
||||
$ cd task-2.6.2
|
||||
|
||||
Or clone this repository:
|
||||
|
||||
|
||||
@@ -16,14 +16,6 @@ services:
|
||||
security_opt:
|
||||
- label=type:container_runtime_t
|
||||
tty: true
|
||||
test-fedora31:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: test/docker/fedora31
|
||||
network_mode: "host"
|
||||
security_opt:
|
||||
- label=type:container_runtime_t
|
||||
tty: true
|
||||
test-fedora32:
|
||||
build:
|
||||
context: .
|
||||
@@ -48,6 +40,14 @@ services:
|
||||
security_opt:
|
||||
- label=type:container_runtime_t
|
||||
tty: true
|
||||
test-fedora35:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: test/docker/fedora35
|
||||
network_mode: "host"
|
||||
security_opt:
|
||||
- label=type:container_runtime_t
|
||||
tty: true
|
||||
test-ubuntu1804:
|
||||
build:
|
||||
context: .
|
||||
@@ -72,6 +72,14 @@ services:
|
||||
security_opt:
|
||||
- label=type:container_runtime_t
|
||||
tty: true
|
||||
test-ubuntu2110:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: test/docker/ubuntu2110
|
||||
network_mode: "host"
|
||||
security_opt:
|
||||
- label=type:container_runtime_t
|
||||
tty: true
|
||||
test-debianstable:
|
||||
build:
|
||||
context: .
|
||||
@@ -96,10 +104,10 @@ services:
|
||||
security_opt:
|
||||
- label=type:container_runtime_t
|
||||
tty: true
|
||||
test-opensuse1500:
|
||||
test-opensuse15:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: test/docker/opensuse1500
|
||||
dockerfile: test/docker/opensuse15
|
||||
network_mode: "host"
|
||||
security_opt:
|
||||
- label=type:container_runtime_t
|
||||
|
||||
@@ -5,7 +5,7 @@ FROM centos:8
|
||||
|
||||
RUN dnf update -y
|
||||
RUN yum install epel-release -y
|
||||
RUN dnf install python38 git gcc gcc-c++ cmake make gnutls-devel libuuid-devel libfaketime sudo man gdb -y
|
||||
RUN dnf install python38 vim git gcc gcc-c++ cmake make gnutls-devel libuuid-devel libfaketime sudo man gdb -y
|
||||
|
||||
RUN useradd warrior
|
||||
RUN echo warrior ALL=NOPASSWD:ALL > /etc/sudoers.d/warrior
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
#include <shared.h>
|
||||
#include <format.h>
|
||||
#include <main.h>
|
||||
#include <regex>
|
||||
|
||||
#ifdef HAVE_COMMIT
|
||||
#include <commit.h>
|
||||
@@ -115,7 +116,7 @@ std::string configurationDefaults =
|
||||
"expressions=infix # Prefer infix over postfix expressions\n"
|
||||
"json.array=1 # Enclose JSON output in [ ]\n"
|
||||
"abbreviation.minimum=2 # Shortest allowed abbreviation\n"
|
||||
"news.version= # Latest version higlights read by the user\n"
|
||||
"news.version= # Latest version highlights read by the user\n"
|
||||
"\n"
|
||||
"# Dates\n"
|
||||
"dateformat=Y-M-D # Preferred input and display date format\n"
|
||||
@@ -358,9 +359,9 @@ std::string configurationDefaults =
|
||||
"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 and (+PARENT or +CHILD)\n"
|
||||
"report.recurring.labels=ID,Active,Age,D,P,Parent,Project,Tags,Recur,Sch,Due,Until,Description,Urg\n"
|
||||
"report.recurring.columns=id,start.age,entry.age,depends.indicator,priority,parent.short,project,tags,recur,scheduled.countdown,due,until.remaining,description,urgency\n"
|
||||
"report.recurring.filter=(status:pending and +CHILD) or (status:recurring and +PARENT)\n"
|
||||
"report.recurring.sort=due+,urgency-,entry+\n"
|
||||
"report.recurring.context=1\n"
|
||||
"\n"
|
||||
@@ -474,7 +475,7 @@ int Context::initialize (int argc, const char** argv)
|
||||
// [1] Load the correct config file.
|
||||
// - Default to ~/.taskrc (ctor).
|
||||
// - If no ~/.taskrc, use $XDG_CONFIG_HOME/task/taskrc if exists, or
|
||||
// ~/.config/task/taskrc if $XDG_DATA_HOME is unset
|
||||
// ~/.config/task/taskrc if $XDG_CONFIG_HOME is unset
|
||||
// - Allow $TASKRC override.
|
||||
// - Allow command line override rc:<file>
|
||||
// - Load resultant file.
|
||||
@@ -667,9 +668,13 @@ int Context::initialize (int argc, const char** argv)
|
||||
rc = 4;
|
||||
}
|
||||
|
||||
catch (const std::regex_error& e)
|
||||
{
|
||||
std::cout << "regex_error caught: " << e.what() << '\n';
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
error ("Unknown error. Please report.");
|
||||
error ("knknown error. Please report.");
|
||||
rc = 3;
|
||||
}
|
||||
|
||||
@@ -1323,6 +1328,12 @@ void Context::debugTiming (const std::string& details, const Timer& timer)
|
||||
debug (out.str ());
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
CurrentTask Context::withCurrentTask (const Task *task)
|
||||
{
|
||||
return CurrentTask(*this, task);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// This capability is to answer the question of 'what did I just do to generate
|
||||
// this output?'.
|
||||
@@ -1424,6 +1435,19 @@ void Context::debug (const std::string& input)
|
||||
debugMessages.push_back (input);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
CurrentTask::CurrentTask (Context &context, const Task *task)
|
||||
: context {context}, previous {context.currentTask}
|
||||
{
|
||||
context.currentTask = task;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
CurrentTask::~CurrentTask ()
|
||||
{
|
||||
context.currentTask = previous;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// vim ts=2:sw=2
|
||||
|
||||
@@ -38,6 +38,8 @@
|
||||
#include <Timer.h>
|
||||
#include <set>
|
||||
|
||||
class CurrentTask;
|
||||
|
||||
class Context
|
||||
{
|
||||
public:
|
||||
@@ -73,6 +75,9 @@ public:
|
||||
void decomposeSortField (const std::string&, std::string&, bool&, bool&);
|
||||
void debugTiming (const std::string&, const Timer&);
|
||||
|
||||
CurrentTask withCurrentTask (const Task *);
|
||||
friend class CurrentTask;
|
||||
|
||||
private:
|
||||
void staticInitialization ();
|
||||
void createDefaultConfig ();
|
||||
@@ -115,6 +120,25 @@ public:
|
||||
long time_sort_us {0};
|
||||
long time_render_us {0};
|
||||
long time_hooks_us {0};
|
||||
|
||||
// the current task for DOM references, or NULL if there is no task
|
||||
const Task * currentTask {NULL};
|
||||
};
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// CurrentTask resets Context::currentTask to previous context task on destruction; this ensures
|
||||
// that this context value is restored when exiting the scope where the context was applied.
|
||||
class CurrentTask {
|
||||
public:
|
||||
~CurrentTask();
|
||||
|
||||
private:
|
||||
CurrentTask(Context &context, const Task *previous);
|
||||
|
||||
Context &context;
|
||||
const Task *previous;
|
||||
|
||||
friend class Context;
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
132
src/DOM.cpp
132
src/DOM.cpp
@@ -239,22 +239,25 @@ bool getDOM (const std::string& name, Variant& value)
|
||||
//
|
||||
// This code emphasizes speed, hence 'id' and 'urgency' being evaluated first
|
||||
// as special cases.
|
||||
bool getDOM (const std::string& name, const Task& task, Variant& value)
|
||||
//
|
||||
// If task is NULL, then the contextual task will be determined from the DOM
|
||||
// string, if any exists.
|
||||
bool getDOM (const std::string& name, const Task* task, Variant& value)
|
||||
{
|
||||
// Special case, blank refs cause problems.
|
||||
if (name == "")
|
||||
return false;
|
||||
|
||||
// Quickly deal with the most common cases.
|
||||
if (task.data.size () && name == "id")
|
||||
if (task && name == "id")
|
||||
{
|
||||
value = Variant (static_cast<int> (task.id));
|
||||
value = Variant (static_cast<int> (task->id));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (task.data.size () && name == "urgency")
|
||||
if (task && name == "urgency")
|
||||
{
|
||||
value = Variant (task.urgency_c ());
|
||||
value = Variant (task->urgency_c ());
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -262,54 +265,55 @@ bool getDOM (const std::string& name, const Task& task, Variant& value)
|
||||
auto elements = split (name, '.');
|
||||
Task loaded_task;
|
||||
|
||||
// Use a lambda to decide whether the reference is going to be the passed
|
||||
// decide whether the reference is going to be the passed
|
||||
// "task" or whether it's going to be a newly loaded task (if id/uuid was
|
||||
// given).
|
||||
const Task& ref = [&]() -> const Task&
|
||||
const Task* ref = task;
|
||||
Lexer lexer (elements[0]);
|
||||
std::string token;
|
||||
Lexer::Type type;
|
||||
|
||||
// If this can be ID/UUID reference (the name contains '.'),
|
||||
// lex it to figure out. Otherwise don't lex, as lexing can be slow.
|
||||
if ((elements.size() > 1) and lexer.token (token, type))
|
||||
{
|
||||
Lexer lexer (elements[0]);
|
||||
std::string token;
|
||||
Lexer::Type type;
|
||||
bool reloaded = false;
|
||||
|
||||
// If this can be ID/UUID reference (the name contains '.'),
|
||||
// lex it to figure out. Otherwise don't lex, as lexing can be slow.
|
||||
if ((elements.size() > 1) and lexer.token (token, type))
|
||||
if (type == Lexer::Type::uuid &&
|
||||
token.length () == elements[0].length ())
|
||||
{
|
||||
bool reloaded = false;
|
||||
|
||||
if (type == Lexer::Type::uuid &&
|
||||
token.length () == elements[0].length ())
|
||||
if (!task || token != task->get ("uuid"))
|
||||
{
|
||||
if (token != task.get ("uuid"))
|
||||
{
|
||||
Context::getContext ().tdb2.get (token, loaded_task);
|
||||
if (Context::getContext ().tdb2.get (token, loaded_task))
|
||||
reloaded = true;
|
||||
}
|
||||
|
||||
// Eat elements[0]/UUID.
|
||||
elements.erase (elements.begin ());
|
||||
}
|
||||
else if (type == Lexer::Type::number &&
|
||||
token.find ('.') == std::string::npos)
|
||||
{
|
||||
auto id = strtol (token.c_str (), nullptr, 10);
|
||||
if (id && id != task.id)
|
||||
{
|
||||
Context::getContext ().tdb2.get (id, loaded_task);
|
||||
reloaded = true;
|
||||
}
|
||||
|
||||
// Eat elements[0]/ID.
|
||||
elements.erase (elements.begin ());
|
||||
}
|
||||
|
||||
if (reloaded)
|
||||
return loaded_task;
|
||||
// Eat elements[0]/UUID.
|
||||
elements.erase (elements.begin ());
|
||||
}
|
||||
else if (type == Lexer::Type::number &&
|
||||
token.find ('.') == std::string::npos)
|
||||
{
|
||||
auto id = strtol (token.c_str (), nullptr, 10);
|
||||
if (id && (!task || id != task->id))
|
||||
{
|
||||
if (Context::getContext ().tdb2.get (id, loaded_task))
|
||||
reloaded = true;
|
||||
}
|
||||
|
||||
// Eat elements[0]/ID.
|
||||
elements.erase (elements.begin ());
|
||||
}
|
||||
|
||||
return task;
|
||||
if (reloaded)
|
||||
ref = &loaded_task;
|
||||
}
|
||||
|
||||
} ();
|
||||
|
||||
// The remainder of this method requires a contextual task, so if we do not
|
||||
// have one, delegate to the two-argument getDOM
|
||||
if (!ref)
|
||||
return getDOM (name, value);
|
||||
|
||||
auto size = elements.size ();
|
||||
|
||||
@@ -318,31 +322,31 @@ bool getDOM (const std::string& name, const Task& task, Variant& value)
|
||||
{
|
||||
// Now that 'ref' is the contextual task, and any ID/UUID is chopped off the
|
||||
// elements vector, DOM resolution is now simple.
|
||||
if (ref.data.size () && size == 1 && canonical == "id")
|
||||
if (size == 1 && canonical == "id")
|
||||
{
|
||||
value = Variant (static_cast<int> (ref.id));
|
||||
value = Variant (static_cast<int> (ref->id));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ref.data.size () && size == 1 && canonical == "urgency")
|
||||
if (size == 1 && canonical == "urgency")
|
||||
{
|
||||
value = Variant (ref.urgency_c ());
|
||||
value = Variant (ref->urgency_c ());
|
||||
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")
|
||||
if (size == 1 && canonical == "status")
|
||||
{
|
||||
value = Variant (ref.statusToText (ref.getStatus ()));
|
||||
value = Variant (ref->statusToText (ref->getStatus ()));
|
||||
return true;
|
||||
}
|
||||
|
||||
Column* column = Context::getContext ().columns[canonical];
|
||||
|
||||
if (ref.data.size () && size == 1 && column)
|
||||
if (size == 1 && column)
|
||||
{
|
||||
if (column->is_uda () && ! ref.has (canonical))
|
||||
if (column->is_uda () && ! ref->has (canonical))
|
||||
{
|
||||
value = Variant ("");
|
||||
return true;
|
||||
@@ -350,7 +354,7 @@ bool getDOM (const std::string& name, const Task& task, Variant& value)
|
||||
|
||||
if (column->type () == "date")
|
||||
{
|
||||
auto numeric = ref.get_date (canonical);
|
||||
auto numeric = ref->get_date (canonical);
|
||||
if (numeric == 0)
|
||||
value = Variant ("");
|
||||
else
|
||||
@@ -358,32 +362,32 @@ bool getDOM (const std::string& name, const Task& task, Variant& value)
|
||||
}
|
||||
else if (column->type () == "duration" || canonical == "recur")
|
||||
{
|
||||
auto period = ref.get (canonical);
|
||||
auto period = ref->get (canonical);
|
||||
|
||||
Duration iso;
|
||||
std::string::size_type cursor = 0;
|
||||
if (iso.parse (period, cursor))
|
||||
value = Variant (iso.toTime_t (), Variant::type_duration);
|
||||
else
|
||||
value = Variant (Duration (ref.get (canonical)).toTime_t (), Variant::type_duration);
|
||||
value = Variant (Duration (ref->get (canonical)).toTime_t (), Variant::type_duration);
|
||||
}
|
||||
else if (column->type () == "numeric")
|
||||
value = Variant (ref.get_float (canonical));
|
||||
value = Variant (ref->get_float (canonical));
|
||||
else
|
||||
value = Variant (ref.get (canonical));
|
||||
value = Variant (ref->get (canonical));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ref.data.size () && size == 2 && canonical == "tags")
|
||||
if (size == 2 && canonical == "tags")
|
||||
{
|
||||
value = Variant (ref.hasTag (elements[1]) ? elements[1] : "");
|
||||
value = Variant (ref->hasTag (elements[1]) ? elements[1] : "");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ref.data.size () && size == 2 && column && column->type () == "date")
|
||||
if (size == 2 && column && column->type () == "date")
|
||||
{
|
||||
Datetime date (ref.get_date (canonical));
|
||||
Datetime date (ref->get_date (canonical));
|
||||
if (elements[1] == "year") { value = Variant (static_cast<int> (date.year ())); return true; }
|
||||
else if (elements[1] == "month") { value = Variant (static_cast<int> (date.month ())); return true; }
|
||||
else if (elements[1] == "day") { value = Variant (static_cast<int> (date.day ())); return true; }
|
||||
@@ -396,15 +400,15 @@ bool getDOM (const std::string& name, const Task& task, Variant& value)
|
||||
}
|
||||
}
|
||||
|
||||
if (ref.data.size () && size == 2 && elements[0] == "annotations" && elements[1] == "count")
|
||||
if (size == 2 && elements[0] == "annotations" && elements[1] == "count")
|
||||
{
|
||||
value = Variant (static_cast<int> (ref.getAnnotationCount ()));
|
||||
value = Variant (static_cast<int> (ref->getAnnotationCount ()));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ref.data.size () && size == 3 && elements[0] == "annotations")
|
||||
if (size == 3 && elements[0] == "annotations")
|
||||
{
|
||||
auto annos = ref.getAnnotations ();
|
||||
auto annos = ref->getAnnotations ();
|
||||
|
||||
int a = strtol (elements[1].c_str (), nullptr, 10);
|
||||
int count = 0;
|
||||
@@ -430,9 +434,9 @@ bool getDOM (const std::string& name, const Task& task, Variant& value)
|
||||
}
|
||||
}
|
||||
|
||||
if (ref.data.size () && size == 4 && elements[0] == "annotations" && elements[2] == "entry")
|
||||
if (size == 4 && elements[0] == "annotations" && elements[2] == "entry")
|
||||
{
|
||||
auto annos = ref.getAnnotations ();
|
||||
auto annos = ref->getAnnotations ();
|
||||
|
||||
int a = strtol (elements[1].c_str (), nullptr, 10);
|
||||
int count = 0;
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
|
||||
// 2017-04-22 Deprecated, use DOM::get.
|
||||
bool getDOM (const std::string&, Variant&);
|
||||
bool getDOM (const std::string&, const Task&, Variant&);
|
||||
bool getDOM (const std::string&, const Task*, Variant&);
|
||||
|
||||
class DOM
|
||||
{
|
||||
|
||||
30
src/Eval.cpp
30
src/Eval.cpp
@@ -26,6 +26,7 @@
|
||||
|
||||
#include <cmake.h>
|
||||
#include <Eval.h>
|
||||
#include <DOM.h>
|
||||
#include <map>
|
||||
#include <time.h>
|
||||
#include <Context.h>
|
||||
@@ -34,8 +35,6 @@
|
||||
#include <shared.h>
|
||||
#include <format.h>
|
||||
|
||||
extern Task& contextTask;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Supported operators, borrowed from C++, particularly the precedence.
|
||||
// Note: table is sorted by length of operator string, so searches match
|
||||
@@ -101,6 +100,19 @@ static bool namedConstants (const std::string& name, Variant& value)
|
||||
return true;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Support for evaluating DOM references (add with `e.AddSource(domSource)`)
|
||||
bool domSource (const std::string& identifier, Variant& value)
|
||||
{
|
||||
if (getDOM (identifier, Context::getContext ().currentTask, value))
|
||||
{
|
||||
value.source (identifier);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
Eval::Eval ()
|
||||
{
|
||||
@@ -278,6 +290,8 @@ void Eval::evaluatePostfixStack (
|
||||
Variant left = values.back ();
|
||||
values.pop_back ();
|
||||
|
||||
auto contextTask = Context::getContext ().currentTask;
|
||||
|
||||
// Ordering these by anticipation frequency of use is a good idea.
|
||||
Variant result;
|
||||
if (token.first == "and") result = left && right;
|
||||
@@ -299,10 +313,14 @@ void Eval::evaluatePostfixStack (
|
||||
else if (token.first == "^") result = left ^ right;
|
||||
else if (token.first == "%") result = left % right;
|
||||
else if (token.first == "xor") result = left.operator_xor (right);
|
||||
else if (token.first == "~") result = left.operator_match (right, contextTask);
|
||||
else if (token.first == "!~") result = left.operator_nomatch (right, contextTask);
|
||||
else if (token.first == "_hastag_") result = left.operator_hastag (right, contextTask);
|
||||
else if (token.first == "_notag_") result = left.operator_notag (right, contextTask);
|
||||
else if (contextTask) {
|
||||
if (token.first == "~") result = left.operator_match (right, *contextTask);
|
||||
else if (token.first == "!~") result = left.operator_nomatch (right, *contextTask);
|
||||
else if (token.first == "_hastag_") result = left.operator_hastag (right, *contextTask);
|
||||
else if (token.first == "_notag_") result = left.operator_notag (right, *contextTask);
|
||||
else
|
||||
throw format ("Unsupported operator '{1}'.", token.first);
|
||||
}
|
||||
else
|
||||
throw format ("Unsupported operator '{1}'.", token.first);
|
||||
|
||||
|
||||
@@ -32,6 +32,8 @@
|
||||
#include <Lexer.h>
|
||||
#include <Variant.h>
|
||||
|
||||
bool domSource (const std::string&, Variant&);
|
||||
|
||||
class Eval
|
||||
{
|
||||
public:
|
||||
|
||||
@@ -35,23 +35,6 @@
|
||||
#include <format.h>
|
||||
#include <shared.h>
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Const iterator that can be derefenced into a Task by domSource.
|
||||
static Task dummy;
|
||||
Task& contextTask = dummy;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
bool domSource (const std::string& identifier, Variant& value)
|
||||
{
|
||||
if (getDOM (identifier, contextTask, value))
|
||||
{
|
||||
value.source (identifier);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Take an input set of tasks and filter into a subset.
|
||||
void Filter::subset (const std::vector <Task>& input, std::vector <Task>& output)
|
||||
@@ -79,7 +62,7 @@ void Filter::subset (const std::vector <Task>& input, std::vector <Task>& output
|
||||
for (auto& task : input)
|
||||
{
|
||||
// Set up context for any DOM references.
|
||||
contextTask = task;
|
||||
auto currentTask = Context::getContext ().withCurrentTask(&task);
|
||||
|
||||
Variant var;
|
||||
eval.evaluateCompiledExpression (var);
|
||||
@@ -131,7 +114,7 @@ void Filter::subset (std::vector <Task>& output)
|
||||
for (auto& task : pending)
|
||||
{
|
||||
// Set up context for any DOM references.
|
||||
contextTask = task;
|
||||
auto currentTask = Context::getContext ().withCurrentTask(&task);
|
||||
|
||||
Variant var;
|
||||
eval.evaluateCompiledExpression (var);
|
||||
@@ -150,7 +133,7 @@ void Filter::subset (std::vector <Task>& output)
|
||||
for (auto& task : completed)
|
||||
{
|
||||
// Set up context for any DOM references.
|
||||
contextTask = task;
|
||||
auto currentTask = Context::getContext ().withCurrentTask(&task);
|
||||
|
||||
Variant var;
|
||||
eval.evaluateCompiledExpression (var);
|
||||
|
||||
@@ -32,8 +32,6 @@
|
||||
#include <Task.h>
|
||||
#include <Variant.h>
|
||||
|
||||
bool domSource (const std::string&, Variant&);
|
||||
|
||||
class Filter
|
||||
{
|
||||
public:
|
||||
|
||||
195
src/TDB2.cpp
195
src/TDB2.cpp
@@ -1025,203 +1025,24 @@ void TDB2::show_diff (
|
||||
Color color_red (Context::getContext ().color () ? Context::getContext ().config.get ("color.undo.before") : "");
|
||||
Color color_green (Context::getContext ().color () ? Context::getContext ().config.get ("color.undo.after") : "");
|
||||
|
||||
auto before = prior == "" ? Task() : Task(prior);
|
||||
auto after = Task(current);
|
||||
|
||||
if (Context::getContext ().config.get ("undo.style") == "side")
|
||||
{
|
||||
Table view = before.diffForUndoSide(after);
|
||||
|
||||
std::cout << '\n'
|
||||
<< format ("The last modification was made {1}", lastChange.toString ())
|
||||
<< '\n';
|
||||
|
||||
// Attributes are all there is, so figure the different attribute names
|
||||
// between before and after.
|
||||
Table view;
|
||||
view.width (Context::getContext ().getWidth ());
|
||||
view.intraPadding (2);
|
||||
view.add ("");
|
||||
view.add ("Prior Values");
|
||||
view.add ("Current Values");
|
||||
setHeaderUnderline (view);
|
||||
|
||||
Task after (current);
|
||||
|
||||
if (prior != "")
|
||||
{
|
||||
Task before (prior);
|
||||
|
||||
std::vector <std::string> beforeAtts;
|
||||
for (auto& att : before.data)
|
||||
beforeAtts.push_back (att.first);
|
||||
|
||||
std::vector <std::string> afterAtts;
|
||||
for (auto& att : after.data)
|
||||
afterAtts.push_back (att.first);
|
||||
|
||||
std::vector <std::string> beforeOnly;
|
||||
std::vector <std::string> afterOnly;
|
||||
listDiff (beforeAtts, afterAtts, beforeOnly, afterOnly);
|
||||
|
||||
int row;
|
||||
for (auto& name : beforeOnly)
|
||||
{
|
||||
row = view.addRow ();
|
||||
view.set (row, 0, name);
|
||||
view.set (row, 1, renderAttribute (name, before.get (name)), color_red);
|
||||
}
|
||||
|
||||
for (auto& att : before.data)
|
||||
{
|
||||
std::string priorValue = before.get (att.first);
|
||||
std::string currentValue = after.get (att.first);
|
||||
|
||||
if (currentValue != "")
|
||||
{
|
||||
row = view.addRow ();
|
||||
view.set (row, 0, att.first);
|
||||
view.set (row, 1, renderAttribute (att.first, priorValue),
|
||||
(priorValue != currentValue ? color_red : Color ()));
|
||||
view.set (row, 2, renderAttribute (att.first, currentValue),
|
||||
(priorValue != currentValue ? color_green : Color ()));
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& name : afterOnly)
|
||||
{
|
||||
row = view.addRow ();
|
||||
view.set (row, 0, name);
|
||||
view.set (row, 2, renderAttribute (name, after.get (name)), color_green);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
int row;
|
||||
for (auto& att : after.data)
|
||||
{
|
||||
row = view.addRow ();
|
||||
view.set (row, 0, att.first);
|
||||
view.set (row, 2, renderAttribute (att.first, after.get (att.first)), color_green);
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << '\n'
|
||||
<< '\n'
|
||||
<< '\n'
|
||||
<< view.render ()
|
||||
<< '\n';
|
||||
}
|
||||
|
||||
// This style looks like this:
|
||||
// --- before 2009-07-04 00:00:25.000000000 +0200
|
||||
// +++ after 2009-07-04 00:00:45.000000000 +0200
|
||||
//
|
||||
// - name: old // att deleted
|
||||
// + name:
|
||||
//
|
||||
// - name: old // att changed
|
||||
// + name: new
|
||||
//
|
||||
// - name:
|
||||
// + name: new // att added
|
||||
//
|
||||
else if (Context::getContext ().config.get ("undo.style") == "diff")
|
||||
{
|
||||
// Create reference tasks.
|
||||
Task before;
|
||||
if (prior != "")
|
||||
before.parse (prior);
|
||||
|
||||
Task after (current);
|
||||
|
||||
// Generate table header.
|
||||
Table view;
|
||||
view.width (Context::getContext ().getWidth ());
|
||||
view.intraPadding (2);
|
||||
view.add ("");
|
||||
view.add ("");
|
||||
|
||||
int row = view.addRow ();
|
||||
view.set (row, 0, "--- previous state", color_red);
|
||||
view.set (row, 1, "Undo will restore this state", color_red);
|
||||
|
||||
row = view.addRow ();
|
||||
view.set (row, 0, "+++ current state ", color_green);
|
||||
view.set (row, 1, format ("Change made {1}",
|
||||
lastChange.toString (Context::getContext ().config.get ("dateformat"))),
|
||||
color_green);
|
||||
|
||||
view.addRow ();
|
||||
|
||||
// Add rows to table showing diffs.
|
||||
std::vector <std::string> all = Context::getContext ().getColumns ();
|
||||
|
||||
// Now factor in the annotation attributes.
|
||||
for (auto& it : before.data)
|
||||
if (it.first.substr (0, 11) == "annotation_")
|
||||
all.push_back (it.first);
|
||||
|
||||
for (auto& it : after.data)
|
||||
if (it.first.substr (0, 11) == "annotation_")
|
||||
all.push_back (it.first);
|
||||
|
||||
// Now render all the attributes.
|
||||
std::sort (all.begin (), all.end ());
|
||||
|
||||
std::string before_att;
|
||||
std::string after_att;
|
||||
std::string last_att;
|
||||
for (auto& a : all)
|
||||
{
|
||||
if (a != last_att) // Skip duplicates.
|
||||
{
|
||||
last_att = a;
|
||||
|
||||
before_att = before.get (a);
|
||||
after_att = after.get (a);
|
||||
|
||||
// Don't report different uuid.
|
||||
// Show nothing if values are the unchanged.
|
||||
if (a == "uuid" ||
|
||||
before_att == after_att)
|
||||
{
|
||||
// Show nothing - no point displaying that which did not change.
|
||||
|
||||
// row = view.addRow ();
|
||||
// view.set (row, 0, *a + ":");
|
||||
// view.set (row, 1, before_att);
|
||||
}
|
||||
|
||||
// Attribute deleted.
|
||||
else if (before_att != "" && after_att == "")
|
||||
{
|
||||
row = view.addRow ();
|
||||
view.set (row, 0, '-' + a + ':', color_red);
|
||||
view.set (row, 1, before_att, color_red);
|
||||
|
||||
row = view.addRow ();
|
||||
view.set (row, 0, '+' + a + ':', color_green);
|
||||
}
|
||||
|
||||
// Attribute added.
|
||||
else if (before_att == "" && after_att != "")
|
||||
{
|
||||
row = view.addRow ();
|
||||
view.set (row, 0, '-' + a + ':', color_red);
|
||||
|
||||
row = view.addRow ();
|
||||
view.set (row, 0, '+' + a + ':', color_green);
|
||||
view.set (row, 1, after_att, color_green);
|
||||
}
|
||||
|
||||
// Attribute changed.
|
||||
else
|
||||
{
|
||||
row = view.addRow ();
|
||||
view.set (row, 0, '-' + a + ':', color_red);
|
||||
view.set (row, 1, before_att, color_red);
|
||||
|
||||
row = view.addRow ();
|
||||
view.set (row, 0, '+' + a + ':', color_green);
|
||||
view.set (row, 1, after_att, color_green);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Table view = before.diffForUndoPatch(after, lastChange);
|
||||
std::cout << '\n'
|
||||
<< view.render ()
|
||||
<< '\n';
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
|
||||
#include <TLSClient.h>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <unistd.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
@@ -44,6 +45,7 @@
|
||||
#include <shared.h>
|
||||
#include <format.h>
|
||||
|
||||
#define HEADER_SIZE 4
|
||||
#define MAX_BUF 16384
|
||||
|
||||
#if GNUTLS_VERSION_NUMBER < 0x030406
|
||||
@@ -469,24 +471,15 @@ void TLSClient::send (const std::string& data)
|
||||
packet[3] = l;
|
||||
|
||||
unsigned int total = 0;
|
||||
unsigned int remaining = packet.length ();
|
||||
|
||||
while (total < packet.length ())
|
||||
int status;
|
||||
do
|
||||
{
|
||||
int status;
|
||||
do
|
||||
{
|
||||
status = gnutls_record_send (_session, packet.c_str () + total, remaining); // All
|
||||
}
|
||||
while (errno == GNUTLS_E_INTERRUPTED ||
|
||||
errno == GNUTLS_E_AGAIN);
|
||||
|
||||
if (status == -1)
|
||||
break;
|
||||
|
||||
total += (unsigned int) status;
|
||||
remaining -= (unsigned int) status;
|
||||
status = gnutls_record_send (_session, packet.c_str () + total, packet.length () - total); // All
|
||||
}
|
||||
while ((status > 0 && (total += status) < packet.length ()) ||
|
||||
status == GNUTLS_E_INTERRUPTED ||
|
||||
status == GNUTLS_E_AGAIN);
|
||||
|
||||
if (_debug)
|
||||
std::cout << "c: INFO Sending 'XXXX"
|
||||
@@ -500,18 +493,22 @@ void TLSClient::recv (std::string& data)
|
||||
{
|
||||
data = ""; // No appending of data.
|
||||
int received = 0;
|
||||
int total = 0;
|
||||
|
||||
// Get the encoded length.
|
||||
unsigned char header[4] {};
|
||||
unsigned char header[HEADER_SIZE] {};
|
||||
do
|
||||
{
|
||||
received = gnutls_record_recv (_session, header, 4); // All
|
||||
received = gnutls_record_recv (_session, header + total, HEADER_SIZE - total); // All
|
||||
}
|
||||
while (received > 0 &&
|
||||
(errno == GNUTLS_E_INTERRUPTED ||
|
||||
errno == GNUTLS_E_AGAIN));
|
||||
while ((received > 0 && (total += received) < HEADER_SIZE) ||
|
||||
received == GNUTLS_E_INTERRUPTED ||
|
||||
received == GNUTLS_E_AGAIN);
|
||||
|
||||
int total = received;
|
||||
if (total < HEADER_SIZE) {
|
||||
throw std::string ("Failed to receive header: ") +
|
||||
(received < 0 ? gnutls_strerror(received) : "connection lost?");
|
||||
}
|
||||
|
||||
// Decode the length.
|
||||
unsigned long expected = (header[0]<<24) |
|
||||
@@ -521,7 +518,11 @@ void TLSClient::recv (std::string& data)
|
||||
if (_debug)
|
||||
std::cout << "c: INFO expecting " << expected << " bytes.\n";
|
||||
|
||||
// TODO This would be a good place to assert 'expected < _limit'.
|
||||
if (_limit && expected >= (unsigned long) _limit) {
|
||||
std::ostringstream err_str;
|
||||
err_str << "Expected message size " << expected << " is larger than allowed limit " << _limit;
|
||||
throw err_str.str ();
|
||||
}
|
||||
|
||||
// Arbitrary buffer size.
|
||||
char buffer[MAX_BUF];
|
||||
@@ -531,13 +532,18 @@ void TLSClient::recv (std::string& data)
|
||||
// fits in the buffer.
|
||||
do
|
||||
{
|
||||
int chunk_size = 0;
|
||||
do
|
||||
{
|
||||
received = gnutls_record_recv (_session, buffer, MAX_BUF - 1); // All
|
||||
received = gnutls_record_recv (_session, buffer + chunk_size, MAX_BUF - chunk_size); // All
|
||||
if (received > 0) {
|
||||
total += received;
|
||||
chunk_size += received;
|
||||
}
|
||||
}
|
||||
while (received > 0 &&
|
||||
(errno == GNUTLS_E_INTERRUPTED ||
|
||||
errno == GNUTLS_E_AGAIN));
|
||||
while ((received > 0 && (unsigned long) total < expected && chunk_size < MAX_BUF) ||
|
||||
received == GNUTLS_E_INTERRUPTED ||
|
||||
received == GNUTLS_E_AGAIN);
|
||||
|
||||
// Other end closed the connection.
|
||||
if (received == 0)
|
||||
@@ -548,17 +554,10 @@ void TLSClient::recv (std::string& data)
|
||||
}
|
||||
|
||||
// Something happened.
|
||||
if (received < 0 && gnutls_error_is_fatal (received) == 0) // All
|
||||
{
|
||||
if (_debug)
|
||||
std::cout << "c: WARNING " << gnutls_strerror (received) << '\n'; // All
|
||||
}
|
||||
else if (received < 0)
|
||||
if (received < 0)
|
||||
throw std::string (gnutls_strerror (received)); // All
|
||||
|
||||
buffer [received] = '\0';
|
||||
data += buffer;
|
||||
total += received;
|
||||
data.append (buffer, chunk_size);
|
||||
|
||||
// Stop at defined limit.
|
||||
if (_limit && total > _limit)
|
||||
|
||||
261
src/Task.cpp
261
src/Task.cpp
@@ -60,8 +60,6 @@
|
||||
|
||||
#define APPROACHING_INFINITY 1000 // Close enough. This isn't rocket surgery.
|
||||
|
||||
extern Task& contextTask;
|
||||
|
||||
static const float epsilon = 0.000001;
|
||||
#endif
|
||||
|
||||
@@ -114,6 +112,12 @@ bool Task::operator== (const Task& other)
|
||||
return true;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
bool Task::operator!= (const Task& other)
|
||||
{
|
||||
return !(*this == other);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
Task::Task (const std::string& input)
|
||||
{
|
||||
@@ -363,6 +367,14 @@ Task::dateState Task::getDateState (const std::string& name) const
|
||||
return dateNotDue;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// An empty task is typically a "dummy", such as in DOM evaluation, which may or
|
||||
// may not occur in the context of a task.
|
||||
bool Task::is_empty () const
|
||||
{
|
||||
return data.size () == 0;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Ready means pending, not blocked and either not scheduled or scheduled before
|
||||
// now.
|
||||
@@ -765,7 +777,25 @@ void Task::parseJSON (const json::object* root_obj)
|
||||
else if (i.first == "depends" && i.second->type() == json::j_string)
|
||||
{
|
||||
auto deps = (json::string*)i.second;
|
||||
auto uuids = split (deps->_data, ',');
|
||||
|
||||
// Fix for issue#2689: taskserver sometimes encodes the depends
|
||||
// property as a string of the format `[\"uuid\",\"uuid\"]`
|
||||
// The string includes the backslash-escaped `"` characters, making
|
||||
// it invalid JSON. Since we know the characters we're looking for,
|
||||
// we'll just filter out everything else.
|
||||
std::string deps_str = deps->_data;
|
||||
if (deps_str.front () == '[' && deps_str.back () == ']') {
|
||||
std::string filtered;
|
||||
for (auto &c: deps_str) {
|
||||
if ((c >= '0' && c <= '9') ||
|
||||
(c >= 'a' && c <= 'f') ||
|
||||
c == ',' || c == '-') {
|
||||
filtered.push_back(c);
|
||||
}
|
||||
}
|
||||
deps_str = filtered;
|
||||
}
|
||||
auto uuids = split (deps_str, ',');
|
||||
|
||||
for (const auto& uuid : uuids)
|
||||
addDependency (uuid);
|
||||
@@ -1545,7 +1575,7 @@ bool Task::isAnnotationAttr(const std::string& attr)
|
||||
#ifdef PRODUCT_TASKWARRIOR
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// A UDA Orphan is an attribute that is not represented in context.columns.
|
||||
std::vector <std::string> Task::getUDAOrphanUUIDs () const
|
||||
std::vector <std::string> Task::getUDAOrphans () const
|
||||
{
|
||||
std::vector <std::string> orphans;
|
||||
for (auto& it : data)
|
||||
@@ -2257,6 +2287,10 @@ void Task::modify (modType type, bool text_required /* = false */)
|
||||
{
|
||||
std::string label = " [1;37;43mMODIFICATION[0m ";
|
||||
|
||||
// while reading the parse tree, consider DOM references in the context of
|
||||
// this task
|
||||
auto currentTask = Context::getContext ().withCurrentTask(this);
|
||||
|
||||
// Need this for later comparison.
|
||||
auto originalStatus = getStatus ();
|
||||
|
||||
@@ -2276,6 +2310,19 @@ void Task::modify (modType type, bool text_required /* = false */)
|
||||
value == "''" ||
|
||||
value == "\"\"")
|
||||
{
|
||||
// Special case: Handle bulk removal of 'tags' and 'depends" virtual
|
||||
// attributes
|
||||
if (name == "depends")
|
||||
{
|
||||
for (auto dep: getDependencyUUIDs ())
|
||||
removeDependency(dep);
|
||||
}
|
||||
else if (name == "tags")
|
||||
{
|
||||
for (auto tag: getTags ())
|
||||
removeTag(tag);
|
||||
}
|
||||
|
||||
// ::composeF4 will skip if the value is blank, but the presence of
|
||||
// the attribute will prevent ::validate from applying defaults.
|
||||
if ((has (name) && get (name) != "") ||
|
||||
@@ -2404,7 +2451,8 @@ void Task::modify (modType type, bool text_required /* = false */)
|
||||
#endif
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Compare this task to another and summarize the differences for display
|
||||
// Compare this task to another and summarize the differences for display, in
|
||||
// the future tense ("Foo will be set to ..").
|
||||
std::string Task::diff (const Task& after) const
|
||||
{
|
||||
// Attributes are all there is, so figure the different attribute names
|
||||
@@ -2649,3 +2697,206 @@ std::string Task::diffForInfo (
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Similar to diff, but formatted as a side-by-side table for an Undo preview
|
||||
Table Task::diffForUndoSide (
|
||||
const Task& after) const
|
||||
{
|
||||
// Set the colors.
|
||||
Color color_red (Context::getContext ().color () ? Context::getContext ().config.get ("color.undo.before") : "");
|
||||
Color color_green (Context::getContext ().color () ? Context::getContext ().config.get ("color.undo.after") : "");
|
||||
|
||||
// Attributes are all there is, so figure the different attribute names
|
||||
// between before and after.
|
||||
Table view;
|
||||
view.width (Context::getContext ().getWidth ());
|
||||
view.intraPadding (2);
|
||||
view.add ("");
|
||||
view.add ("Prior Values");
|
||||
view.add ("Current Values");
|
||||
setHeaderUnderline (view);
|
||||
|
||||
if (!is_empty ())
|
||||
{
|
||||
const Task &before = *this;
|
||||
|
||||
std::vector <std::string> beforeAtts;
|
||||
for (auto& att : before.data)
|
||||
beforeAtts.push_back (att.first);
|
||||
|
||||
std::vector <std::string> afterAtts;
|
||||
for (auto& att : after.data)
|
||||
afterAtts.push_back (att.first);
|
||||
|
||||
std::vector <std::string> beforeOnly;
|
||||
std::vector <std::string> afterOnly;
|
||||
listDiff (beforeAtts, afterAtts, beforeOnly, afterOnly);
|
||||
|
||||
int row;
|
||||
for (auto& name : beforeOnly)
|
||||
{
|
||||
row = view.addRow ();
|
||||
view.set (row, 0, name);
|
||||
view.set (row, 1, renderAttribute (name, before.get (name)), color_red);
|
||||
}
|
||||
|
||||
for (auto& att : before.data)
|
||||
{
|
||||
std::string priorValue = before.get (att.first);
|
||||
std::string currentValue = after.get (att.first);
|
||||
|
||||
if (currentValue != "")
|
||||
{
|
||||
row = view.addRow ();
|
||||
view.set (row, 0, att.first);
|
||||
view.set (row, 1, renderAttribute (att.first, priorValue),
|
||||
(priorValue != currentValue ? color_red : Color ()));
|
||||
view.set (row, 2, renderAttribute (att.first, currentValue),
|
||||
(priorValue != currentValue ? color_green : Color ()));
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& name : afterOnly)
|
||||
{
|
||||
row = view.addRow ();
|
||||
view.set (row, 0, name);
|
||||
view.set (row, 2, renderAttribute (name, after.get (name)), color_green);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
int row;
|
||||
for (auto& att : after.data)
|
||||
{
|
||||
row = view.addRow ();
|
||||
view.set (row, 0, att.first);
|
||||
view.set (row, 2, renderAttribute (att.first, after.get (att.first)), color_green);
|
||||
}
|
||||
}
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Similar to diff, but formatted as a diff for an Undo preview
|
||||
Table Task::diffForUndoPatch (
|
||||
const Task& after,
|
||||
const Datetime& lastChange) const
|
||||
{
|
||||
// This style looks like this:
|
||||
// --- before 2009-07-04 00:00:25.000000000 +0200
|
||||
// +++ after 2009-07-04 00:00:45.000000000 +0200
|
||||
//
|
||||
// - name: old // att deleted
|
||||
// + name:
|
||||
//
|
||||
// - name: old // att changed
|
||||
// + name: new
|
||||
//
|
||||
// - name:
|
||||
// + name: new // att added
|
||||
//
|
||||
|
||||
// Set the colors.
|
||||
Color color_red (Context::getContext ().color () ? Context::getContext ().config.get ("color.undo.before") : "");
|
||||
Color color_green (Context::getContext ().color () ? Context::getContext ().config.get ("color.undo.after") : "");
|
||||
|
||||
const Task &before = *this;
|
||||
|
||||
// Generate table header.
|
||||
Table view;
|
||||
view.width (Context::getContext ().getWidth ());
|
||||
view.intraPadding (2);
|
||||
view.add ("");
|
||||
view.add ("");
|
||||
|
||||
int row = view.addRow ();
|
||||
view.set (row, 0, "--- previous state", color_red);
|
||||
view.set (row, 1, "Undo will restore this state", color_red);
|
||||
|
||||
row = view.addRow ();
|
||||
view.set (row, 0, "+++ current state ", color_green);
|
||||
view.set (row, 1, format ("Change made {1}",
|
||||
lastChange.toString (Context::getContext ().config.get ("dateformat"))),
|
||||
color_green);
|
||||
|
||||
view.addRow ();
|
||||
|
||||
// Add rows to table showing diffs.
|
||||
std::vector <std::string> all = Context::getContext ().getColumns ();
|
||||
|
||||
// Now factor in the annotation attributes.
|
||||
for (auto& it : before.data)
|
||||
if (it.first.substr (0, 11) == "annotation_")
|
||||
all.push_back (it.first);
|
||||
|
||||
for (auto& it : after.data)
|
||||
if (it.first.substr (0, 11) == "annotation_")
|
||||
all.push_back (it.first);
|
||||
|
||||
// Now render all the attributes.
|
||||
std::sort (all.begin (), all.end ());
|
||||
|
||||
std::string before_att;
|
||||
std::string after_att;
|
||||
std::string last_att;
|
||||
for (auto& a : all)
|
||||
{
|
||||
if (a != last_att) // Skip duplicates.
|
||||
{
|
||||
last_att = a;
|
||||
|
||||
before_att = before.get (a);
|
||||
after_att = after.get (a);
|
||||
|
||||
// Don't report different uuid.
|
||||
// Show nothing if values are the unchanged.
|
||||
if (a == "uuid" ||
|
||||
before_att == after_att)
|
||||
{
|
||||
// Show nothing - no point displaying that which did not change.
|
||||
|
||||
// row = view.addRow ();
|
||||
// view.set (row, 0, *a + ":");
|
||||
// view.set (row, 1, before_att);
|
||||
}
|
||||
|
||||
// Attribute deleted.
|
||||
else if (before_att != "" && after_att == "")
|
||||
{
|
||||
row = view.addRow ();
|
||||
view.set (row, 0, '-' + a + ':', color_red);
|
||||
view.set (row, 1, before_att, color_red);
|
||||
|
||||
row = view.addRow ();
|
||||
view.set (row, 0, '+' + a + ':', color_green);
|
||||
}
|
||||
|
||||
// Attribute added.
|
||||
else if (before_att == "" && after_att != "")
|
||||
{
|
||||
row = view.addRow ();
|
||||
view.set (row, 0, '-' + a + ':', color_red);
|
||||
|
||||
row = view.addRow ();
|
||||
view.set (row, 0, '+' + a + ':', color_green);
|
||||
view.set (row, 1, after_att, color_green);
|
||||
}
|
||||
|
||||
// Attribute changed.
|
||||
else
|
||||
{
|
||||
row = view.addRow ();
|
||||
view.set (row, 0, '-' + a + ':', color_red);
|
||||
view.set (row, 1, before_att, color_red);
|
||||
|
||||
row = view.addRow ();
|
||||
view.set (row, 0, '+' + a + ':', color_green);
|
||||
view.set (row, 1, after_att, color_green);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
13
src/Task.h
13
src/Task.h
@@ -33,6 +33,8 @@
|
||||
#include <stdio.h>
|
||||
#include <time.h>
|
||||
#include <JSON.h>
|
||||
#include <Table.h>
|
||||
#include <Datetime.h>
|
||||
|
||||
class Task
|
||||
{
|
||||
@@ -60,6 +62,7 @@ public:
|
||||
public:
|
||||
Task () = default;
|
||||
bool operator== (const Task&);
|
||||
bool operator!= (const Task&);
|
||||
Task (const std::string&);
|
||||
Task (const json::object*);
|
||||
|
||||
@@ -74,7 +77,6 @@ public:
|
||||
enum dateState {dateNotDue, dateAfterToday, dateLaterToday, dateEarlierToday, dateBeforeToday};
|
||||
|
||||
// Public data.
|
||||
std::map <std::string, std::string> data {};
|
||||
int id {0};
|
||||
float urgency_value {0.0};
|
||||
bool recalc_urgency {true};
|
||||
@@ -100,6 +102,8 @@ public:
|
||||
void set (const std::string&, long long);
|
||||
void remove (const std::string&);
|
||||
|
||||
bool is_empty () const;
|
||||
|
||||
#ifdef PRODUCT_TASKWARRIOR
|
||||
bool is_ready () const;
|
||||
bool is_due () const;
|
||||
@@ -154,7 +158,7 @@ public:
|
||||
std::vector <Task> getBlockedTasks () const;
|
||||
std::vector <Task> getDependencyTasks () const;
|
||||
|
||||
std::vector <std::string> getUDAOrphanUUIDs () const;
|
||||
std::vector <std::string> getUDAOrphans () const;
|
||||
|
||||
void substitute (const std::string&, const std::string&, const std::string&);
|
||||
#endif
|
||||
@@ -171,6 +175,8 @@ public:
|
||||
|
||||
std::string diff (const Task& after) const;
|
||||
std::string diffForInfo (const Task& after, const std::string& dateformat, long& last_timestamp, const long current_timestamp) const;
|
||||
Table diffForUndoSide (const Task& after) const;
|
||||
Table diffForUndoPatch (const Task& after, const Datetime& lastChange) const;
|
||||
|
||||
private:
|
||||
int determineVersion (const std::string&);
|
||||
@@ -187,6 +193,9 @@ private:
|
||||
void fixDependsAttribute ();
|
||||
void fixTagsAttribute ();
|
||||
|
||||
protected:
|
||||
std::map <std::string, std::string> data {};
|
||||
|
||||
public:
|
||||
float urgency_project () const;
|
||||
float urgency_active () const;
|
||||
|
||||
@@ -32,7 +32,9 @@
|
||||
#include <format.h>
|
||||
#include <utf8.h>
|
||||
#include <main.h>
|
||||
#include <util.h>
|
||||
#include <stdlib.h>
|
||||
#include <regex>
|
||||
|
||||
#define STRING_COLUMN_LABEL_DEP "Depends"
|
||||
|
||||
@@ -152,20 +154,59 @@ void ColumnDepends::modify (Task& task, const std::string& value)
|
||||
// Apply or remove dendencies in turn.
|
||||
for (auto& dep : split (value, ','))
|
||||
{
|
||||
bool removal = false;
|
||||
if (dep[0] == '-')
|
||||
{
|
||||
if (dep.length () == 37)
|
||||
task.removeDependency (dep.substr (1));
|
||||
else
|
||||
task.removeDependency (strtol (dep.substr (1).c_str (), nullptr, 10));
|
||||
removal = true;
|
||||
dep = dep.substr(1);
|
||||
}
|
||||
else
|
||||
|
||||
auto hyphen = dep.find ('-');
|
||||
long lower, upper; // For ID ranges
|
||||
std::regex valid_uuid ("[a-f0-9]{8}([a-f0-9-]{4,28})?"); // TODO: Make more precise
|
||||
|
||||
// UUID
|
||||
if (dep.length () >= 8 && std::regex_match (dep, valid_uuid))
|
||||
{
|
||||
if (dep.length () == 36)
|
||||
task.addDependency (dep);
|
||||
else
|
||||
task.addDependency (strtol (dep.c_str (), nullptr, 10));
|
||||
// Full UUID, can be added directly
|
||||
if (dep.length () == 36)
|
||||
if (removal)
|
||||
task.removeDependency (dep);
|
||||
else
|
||||
task.addDependency (dep);
|
||||
|
||||
// Short UUID, need to look up full form
|
||||
else
|
||||
{
|
||||
Task loaded_task;
|
||||
if (Context::getContext ().tdb2.get (dep, loaded_task))
|
||||
if (removal)
|
||||
task.removeDependency (loaded_task.get ("uuid"));
|
||||
else
|
||||
task.addDependency (loaded_task.get ("uuid"));
|
||||
else
|
||||
throw format ("Dependency could not be set - task with UUID '{1}' does not exist.", dep);
|
||||
}
|
||||
}
|
||||
// ID range
|
||||
else if (dep.find ('-') != std::string::npos &&
|
||||
extractLongInteger (dep.substr (0, hyphen), lower) &&
|
||||
extractLongInteger (dep.substr (hyphen + 1), upper))
|
||||
{
|
||||
for (long i = lower; i <= upper; i++)
|
||||
if (removal)
|
||||
task.removeDependency (i);
|
||||
else
|
||||
task.addDependency (i);
|
||||
}
|
||||
// Simple ID
|
||||
else if (extractLongInteger (dep, lower))
|
||||
if (removal)
|
||||
task.removeDependency (lower);
|
||||
else
|
||||
task.addDependency (lower);
|
||||
else
|
||||
throw format ("Invalid dependency value: '{1}'", dep);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -36,8 +36,6 @@
|
||||
#include <utf8.h>
|
||||
#include <util.h>
|
||||
|
||||
extern Task& contextTask;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
ColumnProject::ColumnProject ()
|
||||
{
|
||||
@@ -121,7 +119,6 @@ void ColumnProject::modify (Task& task, const std::string& value)
|
||||
{
|
||||
Eval e;
|
||||
e.addSource (domSource);
|
||||
contextTask = task;
|
||||
|
||||
Variant v;
|
||||
e.evaluateInfixExpression (value, v);
|
||||
|
||||
@@ -36,8 +36,6 @@
|
||||
#include <format.h>
|
||||
#include <utf8.h>
|
||||
|
||||
extern Task& contextTask;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
ColumnRecur::ColumnRecur ()
|
||||
{
|
||||
@@ -108,7 +106,6 @@ void ColumnRecur::modify (Task& task, const std::string& value)
|
||||
{
|
||||
Eval e;
|
||||
e.addSource (domSource);
|
||||
contextTask = task;
|
||||
e.evaluateInfixExpression (value, evaluatedValue);
|
||||
}
|
||||
|
||||
|
||||
@@ -36,8 +36,6 @@
|
||||
#include <utf8.h>
|
||||
#include <main.h>
|
||||
|
||||
extern Task& contextTask;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
ColumnTags::ColumnTags ()
|
||||
{
|
||||
@@ -162,7 +160,6 @@ void ColumnTags::modify (Task& task, const std::string& value)
|
||||
{
|
||||
Eval e;
|
||||
e.addSource (domSource);
|
||||
contextTask = task;
|
||||
|
||||
Variant v;
|
||||
e.evaluateInfixExpression (value, v);
|
||||
|
||||
@@ -34,8 +34,6 @@
|
||||
#include <Filter.h>
|
||||
#include <format.h>
|
||||
|
||||
extern Task& contextTask;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
ColumnTypeDate::ColumnTypeDate ()
|
||||
{
|
||||
@@ -213,7 +211,6 @@ void ColumnTypeDate::modify (Task& task, const std::string& value)
|
||||
{
|
||||
Eval e;
|
||||
e.addSource (domSource);
|
||||
contextTask = task;
|
||||
e.evaluateInfixExpression (value, evaluatedValue);
|
||||
}
|
||||
|
||||
|
||||
@@ -32,8 +32,6 @@
|
||||
#include <Filter.h>
|
||||
#include <format.h>
|
||||
|
||||
extern Task& contextTask;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
ColumnTypeDuration::ColumnTypeDuration ()
|
||||
{
|
||||
@@ -55,7 +53,6 @@ void ColumnTypeDuration::modify (Task& task, const std::string& value)
|
||||
{
|
||||
Eval e;
|
||||
e.addSource (domSource);
|
||||
contextTask = task;
|
||||
e.evaluateInfixExpression (value, evaluatedValue);
|
||||
}
|
||||
|
||||
|
||||
@@ -32,8 +32,6 @@
|
||||
#include <Filter.h>
|
||||
#include <format.h>
|
||||
|
||||
extern Task& contextTask;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
ColumnTypeNumeric::ColumnTypeNumeric ()
|
||||
{
|
||||
@@ -55,7 +53,6 @@ void ColumnTypeNumeric::modify (Task& task, const std::string& value)
|
||||
{
|
||||
Eval e;
|
||||
e.addSource (domSource);
|
||||
contextTask = task;
|
||||
e.evaluateInfixExpression (value, evaluatedValue);
|
||||
}
|
||||
|
||||
|
||||
@@ -34,8 +34,6 @@
|
||||
|
||||
#define STRING_INVALID_MOD "The '{1}' attribute does not allow a value of '{2}'."
|
||||
|
||||
extern Task& contextTask;
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
ColumnTypeString::ColumnTypeString ()
|
||||
{
|
||||
@@ -67,7 +65,6 @@ void ColumnTypeString::modify (Task& task, const std::string& value)
|
||||
{
|
||||
Eval e;
|
||||
e.addSource (domSource);
|
||||
contextTask = task;
|
||||
|
||||
Variant v;
|
||||
e.evaluateInfixExpression (value, v);
|
||||
|
||||
@@ -51,6 +51,9 @@ int CmdAdd::execute (std::string& output)
|
||||
{
|
||||
// Apply the command line modifications to the new task.
|
||||
Task task;
|
||||
|
||||
// the task is empty, but DOM references can refer to earlier parts of the
|
||||
// command line, e.g., `task add due:20110101 wait:due`.
|
||||
task.modify (Task::modReplace, true);
|
||||
Context::getContext ().tdb2.add (task);
|
||||
|
||||
|
||||
@@ -110,33 +110,43 @@ std::string CmdContext::joinWords (const std::vector <std::string>& words, unsig
|
||||
// Validate the context as valid for writing and fail the write context definition
|
||||
// A valid write context:
|
||||
// - does not contain any operators except AND
|
||||
// - does not use modifiers
|
||||
// - does not contain tag exclusion
|
||||
// - does not use modifiers, except for 'equals' and 'is'
|
||||
//
|
||||
// Returns True if the context is a valid write context. If the context is
|
||||
// invalid due to a wrong modifier use, the modifier string will contain the
|
||||
// first invalid modifier.
|
||||
bool CmdContext::validateWriteContext (const std::vector <A2>& lexedArgs, std::string& modifier_token)
|
||||
//
|
||||
bool CmdContext::validateWriteContext (const std::vector <A2>& lexedArgs, std::string& reason)
|
||||
{
|
||||
bool contains_or = false;
|
||||
bool contains_modifier = false;
|
||||
|
||||
for (auto &arg: lexedArgs) {
|
||||
if (arg._lextype == Lexer::Type::op)
|
||||
if (arg.attribute ("raw") == "or")
|
||||
contains_or = true;
|
||||
{
|
||||
reason = "contains the 'OR' operator";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (arg._lextype == Lexer::Type::pair) {
|
||||
auto modifier = arg.attribute ("modifier");
|
||||
if (modifier != "" && modifier != "is" && modifier != "equals")
|
||||
{
|
||||
contains_modifier = true;
|
||||
modifier_token = arg.attribute ("raw");
|
||||
break;
|
||||
reason = format ("contains an attribute modifier '{1}'", arg.attribute ("raw"));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (arg._lextype == Lexer::Type::tag) {
|
||||
if (arg.attribute ("sign") == "-")
|
||||
{
|
||||
reason = format ("contains tag exclusion '{1}'", arg.attribute ("raw"));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return not contains_or and not contains_modifier;
|
||||
return true;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
@@ -211,14 +221,13 @@ void CmdContext::defineContext (const std::vector <std::string>& words, std::str
|
||||
! confirm (format ("The filter '{1}' matches 0 pending tasks. Do you wish to continue?", value)))
|
||||
throw std::string ("Context definition aborted.");
|
||||
|
||||
std::string modifier_token = "";
|
||||
bool valid_write_context = CmdContext::validateWriteContext (lexedArgs, modifier_token);
|
||||
std::string reason = "";
|
||||
bool valid_write_context = CmdContext::validateWriteContext (lexedArgs, reason);
|
||||
|
||||
if (! valid_write_context)
|
||||
{
|
||||
std::stringstream warning;
|
||||
warning << format ("The filter '{1}' is not a valid modification string, because it contains ", value)
|
||||
<< ( modifier_token.empty () ? "the OR operator." : format ("an attribute modifier ({1}).", modifier_token) )
|
||||
warning << format ("The filter '{1}' is not a valid modification string, because it contains {2}.", value, reason)
|
||||
<< "\nAs such, value for the write context cannot be set (context will not apply on task add / task log).\n\n"
|
||||
<< format ("Please use 'task config context.{1}.write <default mods>' to set default attribute values for new tasks in this context manually.\n\n", words[1]);
|
||||
out << colorizeFootnote (warning.str ());
|
||||
|
||||
@@ -249,7 +249,7 @@ int CmdCustom::execute (std::string& output)
|
||||
rc = 1;
|
||||
}
|
||||
|
||||
// Inform user about the new release higlights if not presented yet
|
||||
// Inform user about the new release highlights if not presented yet
|
||||
if (Context::getContext ().config.get ("news.version") != "2.6.0")
|
||||
{
|
||||
std::random_device device;
|
||||
@@ -258,7 +258,7 @@ int CmdCustom::execute (std::string& output)
|
||||
|
||||
std::string NEWS_NOTICE = (
|
||||
"Recently upgraded to 2.6.0. "
|
||||
"Please run 'task news' to read higlights about the new release."
|
||||
"Please run 'task news' to read highlights about the new release."
|
||||
);
|
||||
|
||||
// 1 in 10 chance to display the message.
|
||||
|
||||
@@ -128,7 +128,7 @@ int CmdDenotate::execute (std::string&)
|
||||
}
|
||||
}
|
||||
|
||||
if (before.data != task.data)
|
||||
if (before.getAnnotations () != task.getAnnotations ())
|
||||
{
|
||||
auto question = format ("Denotate task {1} '{2}'?",
|
||||
task.identifier (true),
|
||||
|
||||
@@ -324,7 +324,7 @@ std::string CmdEdit::formatTask (Task task, const std::string& dateformat)
|
||||
}
|
||||
|
||||
// UDA orphans
|
||||
auto orphans = task.getUDAOrphanUUIDs ();
|
||||
auto orphans = task.getUDAOrphans ();
|
||||
if (orphans.size ())
|
||||
{
|
||||
before << "# User Defined Attribute Orphans\n";
|
||||
|
||||
@@ -64,9 +64,8 @@ int CmdGet::execute (std::string& output)
|
||||
{
|
||||
case Lexer::Type::dom:
|
||||
{
|
||||
Task t;
|
||||
Variant result;
|
||||
if (getDOM (arg.attribute ("raw"), t, result))
|
||||
if (getDOM (arg.attribute ("raw"), NULL, result))
|
||||
results.emplace_back (result);
|
||||
else
|
||||
results.emplace_back ("");
|
||||
|
||||
@@ -198,7 +198,7 @@ void CmdImport::importSingleTask (json::object* obj)
|
||||
if (hasGeneratedEnd)
|
||||
task.set ("end", before.get ("end"));
|
||||
|
||||
if (before.data != task.data)
|
||||
if (before != task)
|
||||
{
|
||||
CmdModify modHelper;
|
||||
modHelper.checkConsistency (before, task);
|
||||
|
||||
@@ -79,7 +79,7 @@ int CmdModify::execute (std::string&)
|
||||
Task before (task);
|
||||
task.modify (Task::modReplace);
|
||||
|
||||
if (before.data != task.data)
|
||||
if (before != task)
|
||||
{
|
||||
// Abort if change introduces inconsistencies.
|
||||
checkConsistency(before, task);
|
||||
|
||||
@@ -477,7 +477,7 @@ void CmdNews::version2_6_0 (std::vector<NewsItem>& items) {
|
||||
" hooks.location=$XDG_CONFIG_HOME/task/hooks/\n\n"
|
||||
" Solutions in the past required symlinks or more cumbersome configuration overrides.",
|
||||
" If you configure your data.location and hooks.location as above, ensure\n"
|
||||
" that the XFG_DATA_HOME and XDG_CONFIG_HOME environment variables are set,\n"
|
||||
" that the XDG_DATA_HOME and XDG_CONFIG_HOME environment variables are set,\n"
|
||||
" otherwise they're going to expand to empty string. Alternatively you can\n"
|
||||
" hardcode the desired paths on your system."
|
||||
);
|
||||
@@ -616,7 +616,11 @@ int CmdNews::execute (std::string& output)
|
||||
autoComplete (answer, options, matches, 1); // Hard-coded 1.
|
||||
|
||||
if (matches.size () == 1 && matches[0] == "yes")
|
||||
#if defined (DARWIN)
|
||||
system ("open 'https://github.com/sponsors/GothenburgBitFactory/'");
|
||||
#else
|
||||
system ("xdg-open 'https://github.com/sponsors/GothenburgBitFactory/'");
|
||||
#endif
|
||||
|
||||
std::cout << std::endl;
|
||||
}
|
||||
@@ -625,7 +629,7 @@ int CmdNews::execute (std::string& output)
|
||||
|
||||
if (! full_summary && major_items)
|
||||
Context::getContext ().footnote (format (
|
||||
"Only major higlights were displayed ({1} out of {2} total).\n"
|
||||
"Only major highlights were displayed ({1} out of {2} total).\n"
|
||||
"If you're interested in more release highlights, run 'task news {3} minor'.",
|
||||
items.size (),
|
||||
total_highlights,
|
||||
|
||||
@@ -134,12 +134,8 @@ int CmdUDAs::execute (std::string& output)
|
||||
std::map <std::string, int> orphans;
|
||||
for (auto& i : filtered)
|
||||
{
|
||||
for (auto& att : i.data)
|
||||
if (! Task::isAnnotationAttr (att.first) &&
|
||||
! Task::isTagAttr (att.first) &&
|
||||
! Task::isDepAttr (att.first) &&
|
||||
Context::getContext ().columns.find (att.first) == Context::getContext ().columns.end ())
|
||||
orphans[att.first]++;
|
||||
for (auto& att : i.getUDAOrphans ())
|
||||
orphans[att]++;
|
||||
}
|
||||
|
||||
if (orphans.size ())
|
||||
|
||||
Submodule src/libshared updated: 8baf2dbcad...072e0e0cb9
@@ -29,6 +29,7 @@
|
||||
#include <new>
|
||||
#include <cstring>
|
||||
#include <Context.h>
|
||||
#include <regex>
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
int main (int argc, const char** argv)
|
||||
@@ -64,6 +65,11 @@ int main (int argc, const char** argv)
|
||||
status = -3;
|
||||
}
|
||||
|
||||
catch (const std::regex_error& e)
|
||||
{
|
||||
std::cout << "regex_error caught: " << e.what() << '\n';
|
||||
}
|
||||
|
||||
catch (...)
|
||||
{
|
||||
std::cerr << "Unknown error. Please report.\n";
|
||||
|
||||
@@ -189,10 +189,9 @@ static void colorizeKeyword (Task& task, const std::string& rule, const Color& b
|
||||
// first match.
|
||||
else
|
||||
{
|
||||
for (const auto& att : task.data)
|
||||
for (const auto& att : task.getAnnotations ())
|
||||
{
|
||||
if (! att.first.compare (0, 11, "annotation_", 11) &&
|
||||
find (att.second, rule.substr (14), sensitive) != std::string::npos)
|
||||
if (find (att.second, rule.substr (14), sensitive) != std::string::npos)
|
||||
{
|
||||
applyColor (base, c, merge);
|
||||
return;
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <cmake.h>
|
||||
#include <format.h>
|
||||
#include <shared.h>
|
||||
// If <iostream> is included, put it after <stdio.h>, because it includes
|
||||
// <stdio.h>, and therefore would ignore the _WITH_GETLINE.
|
||||
@@ -309,3 +310,10 @@ void setHeaderUnderline (Table& table)
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Perform strtol on a string and check if the extracted value matches.
|
||||
//
|
||||
bool extractLongInteger (const std::string& input, long& output)
|
||||
{
|
||||
output = strtol (input.c_str (), nullptr, 10);
|
||||
return (format ("{1}", output) == input);
|
||||
}
|
||||
|
||||
@@ -63,6 +63,7 @@ const std::vector <std::string> extractParents (
|
||||
bool nontrivial (const std::string&);
|
||||
const char* optionalBlankLine ();
|
||||
void setHeaderUnderline (Table&);
|
||||
bool extractLongInteger (const std::string&, long&);
|
||||
|
||||
#endif
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
1
test/.gitignore
vendored
1
test/.gitignore
vendored
@@ -34,5 +34,6 @@ variant_partial.t
|
||||
variant_subtract.t
|
||||
variant_xor.t
|
||||
view.t
|
||||
tw-2689.t
|
||||
|
||||
json_test
|
||||
|
||||
@@ -16,7 +16,7 @@ include_directories (${CMAKE_SOURCE_DIR}
|
||||
${CMAKE_SOURCE_DIR}/test
|
||||
${TASK_INCLUDE_DIRS})
|
||||
|
||||
set (test_SRCS col.t dom.t eval.t lexer.t t.t tdb2.t util.t variant_add.t variant_and.t variant_cast.t variant_divide.t variant_equal.t variant_exp.t variant_gt.t variant_gte.t variant_inequal.t variant_lt.t variant_lte.t variant_match.t variant_math.t variant_modulo.t variant_multiply.t variant_nomatch.t variant_not.t variant_or.t variant_partial.t variant_subtract.t variant_xor.t view.t)
|
||||
set (test_SRCS col.t dom.t eval.t lexer.t t.t tw-2689.t tdb2.t util.t variant_add.t variant_and.t variant_cast.t variant_divide.t variant_equal.t variant_exp.t variant_gt.t variant_gte.t variant_inequal.t variant_lt.t variant_lte.t variant_match.t variant_math.t variant_modulo.t variant_multiply.t variant_nomatch.t variant_not.t variant_or.t variant_partial.t variant_subtract.t variant_xor.t view.t)
|
||||
|
||||
add_custom_target (test ./run_all --verbose
|
||||
DEPENDS ${test_SRCS} task_executable
|
||||
|
||||
@@ -121,15 +121,15 @@ class ContextManagementTest(TestCase):
|
||||
self.assertEqual(self.t.taskrc_content.count(context_line), 1)
|
||||
|
||||
# Assert the config does not contain write context definition
|
||||
context_line = 'context.work.write=due.before:today\n'
|
||||
context_line = 'context.urgent.write=due.before:today\n'
|
||||
self.assertNotIn(context_line, self.t.taskrc_content)
|
||||
|
||||
# Assert that the write context was not set at all
|
||||
self.assertNotIn('context.work.write=', self.t.taskrc_content)
|
||||
self.assertNotIn('context.urgent.write=', self.t.taskrc_content)
|
||||
|
||||
# Assert that legacy style was not used
|
||||
# Assert the config contains read context definition
|
||||
context_line = 'context.work=due.before:today\n'
|
||||
context_line = 'context.urgent=due.before:today\n'
|
||||
self.assertNotIn(context_line, self.t.taskrc_content)
|
||||
|
||||
def test_context_define_invalid_for_write_due_to_operator(self):
|
||||
@@ -146,15 +146,40 @@ class ContextManagementTest(TestCase):
|
||||
self.assertEqual(self.t.taskrc_content.count(context_line), 1)
|
||||
|
||||
# Assert the config does not contain write context definition
|
||||
context_line = 'context.work.write=due:today or +next\n'
|
||||
context_line = 'context.urgent.write=due:today or +next\n'
|
||||
self.assertNotIn(context_line, self.t.taskrc_content)
|
||||
|
||||
# Assert that the write context was not set at all
|
||||
self.assertNotIn('context.work.write=', self.t.taskrc_content)
|
||||
self.assertNotIn('context.urgent.write=', self.t.taskrc_content)
|
||||
|
||||
# Assert that legacy style was not used
|
||||
# Assert the config contains read context definition
|
||||
context_line = 'context.work=due:today or +next\n'
|
||||
context_line = 'context.urgent=due:today or +next\n'
|
||||
self.assertNotIn(context_line, self.t.taskrc_content)
|
||||
|
||||
def test_context_define_invalid_for_write_due_to_tag_exclusion(self):
|
||||
"""Test definition of a context that is not a valid write context because it contains a tag exclusion."""
|
||||
self.t.config("confirmation", "off")
|
||||
code, out, err = self.t('context define nowork due:today -work')
|
||||
self.assertIn("Context 'nowork' defined", out)
|
||||
|
||||
# Assert the config contains read context definition
|
||||
context_line = 'context.nowork.read=due:today -work\n'
|
||||
self.assertIn(context_line, self.t.taskrc_content)
|
||||
|
||||
# Assert that it contains the definition only once
|
||||
self.assertEqual(self.t.taskrc_content.count(context_line), 1)
|
||||
|
||||
# Assert the config does not contain write context definition
|
||||
context_line = 'context.nowork.write=due:today -work\n'
|
||||
self.assertNotIn(context_line, self.t.taskrc_content)
|
||||
|
||||
# Assert that the write context was not set at all
|
||||
self.assertNotIn('context.nowork.write=', self.t.taskrc_content)
|
||||
|
||||
# Assert that legacy style was not used
|
||||
# Assert the config contains read context definition
|
||||
context_line = 'context.nowork=due:today -work\n'
|
||||
self.assertNotIn(context_line, self.t.taskrc_content)
|
||||
|
||||
def test_context_delete(self):
|
||||
|
||||
@@ -145,6 +145,22 @@ class TestDependencies(TestCase):
|
||||
code, out, err = self.t("_get 1.tags.BLOCKED")
|
||||
self.assertEqual("\n", out)
|
||||
|
||||
def test_dependency_bulk_removal(self):
|
||||
"""2655: Check that one can bulk undepend a task"""
|
||||
self.t("add three")
|
||||
self.t("1 modify dep:2,3")
|
||||
|
||||
code, out, err = self.t("_get 1.tags.BLOCKED")
|
||||
self.assertEqual("BLOCKED\n", out)
|
||||
|
||||
self.t("1 modify depends:")
|
||||
|
||||
code, out, err = self.t("_get 1.tags.BLOCKED")
|
||||
self.assertEqual("\n", out)
|
||||
|
||||
code, out, err = self.t("_get 1.depends")
|
||||
self.assertEqual("\n", out)
|
||||
|
||||
def test_chain_repair(self):
|
||||
"""Check that a broken chain is repaired"""
|
||||
self.t("add three")
|
||||
@@ -169,7 +185,6 @@ class TestDependencies(TestCase):
|
||||
self.assertNotIn("Would you like the dependency chain fixed?", out)
|
||||
self.assertIn("Deleted 1 task", out)
|
||||
|
||||
@unittest.expectedFailure
|
||||
def test_id_range_dep(self):
|
||||
"""Check that an ID range can be used for deps"""
|
||||
self.t("add three")
|
||||
@@ -178,7 +193,7 @@ class TestDependencies(TestCase):
|
||||
self.t("3 modify dep:1-2")
|
||||
code, out, err = self.t("_get 1.tags.BLOCKING")
|
||||
self.assertEqual("BLOCKING\n", out)
|
||||
code, out, err = self.t("_get 2.tag.BLOCKING")
|
||||
code, out, err = self.t("_get 2.tags.BLOCKING")
|
||||
self.assertEqual("BLOCKING\n", out)
|
||||
|
||||
def test_id_uuid_dep(self):
|
||||
@@ -196,6 +211,22 @@ class TestDependencies(TestCase):
|
||||
code, out, err = self.t("3 modify dep:-1,-%s" % uuid)
|
||||
self.assertIn("Modifying task 3 'three'.", out)
|
||||
|
||||
def test_id_uuid_short_dep(self):
|
||||
"""Check that short UUIDs are usable for deps"""
|
||||
|
||||
# Get 2.uuid
|
||||
code, out, err = self.t("_get 2.uuid")
|
||||
short_uuid = out.strip().split("-")[0]
|
||||
|
||||
# Add a mix of IDs and UUID
|
||||
code, out, err = self.t("add three dep:%s" % short_uuid)
|
||||
self.assertIn("Created task 3.", out)
|
||||
|
||||
# Remove a mix of IЅs and UUID
|
||||
code, out, err = self.t("3 modify dep:-%s" % short_uuid)
|
||||
self.assertIn("Modifying task 3 'three'.", out)
|
||||
|
||||
|
||||
class TestBug697(TestCase):
|
||||
def setUp(self):
|
||||
"""Executed before each test in the class"""
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
FROM centos:8
|
||||
|
||||
# Fix missing repo metadata
|
||||
RUN sed -i 's/mirrorlist/#mirrorlist/g' /etc/yum.repos.d/CentOS-Linux-*
|
||||
RUN sed -i 's|#baseurl=http://mirror.centos.org|baseurl=http://vault.centos.org|g' /etc/yum.repos.d/CentOS-Linux-*
|
||||
|
||||
RUN dnf update -y
|
||||
RUN dnf install python3 git gcc gcc-c++ make gnutls-devel libuuid-devel glibc-langpack-en -y
|
||||
RUN dnf install epel-release -y
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM fedora:31
|
||||
FROM fedora:35
|
||||
|
||||
RUN dnf update -y
|
||||
RUN dnf install python3 git gcc gcc-c++ cmake make gnutls-devel libuuid-devel libfaketime glibc-langpack-en -y
|
||||
27
test/docker/ubuntu2110
Normal file
27
test/docker/ubuntu2110
Normal file
@@ -0,0 +1,27 @@
|
||||
FROM ubuntu:21.10
|
||||
|
||||
RUN apt-get update
|
||||
RUN DEBIAN_FRONTEND="noninteractive" apt-get install -y build-essential cmake git uuid-dev libgnutls28-dev faketime locales python3
|
||||
|
||||
# Setup language environment
|
||||
RUN locale-gen en_US.UTF-8
|
||||
ENV LC_ALL en_US.UTF-8
|
||||
ENV LANG en_US.UTF-8
|
||||
ENV LANGUAGE en_US.UTF-8
|
||||
|
||||
# Setup taskwarrior
|
||||
ADD . /root/code/
|
||||
WORKDIR /root/code/
|
||||
RUN git clean -dfx
|
||||
RUN git submodule init
|
||||
RUN git submodule update
|
||||
RUN cmake -DCMAKE_BUILD_TYPE=debug .
|
||||
RUN make -j8
|
||||
RUN make install
|
||||
RUN task --version
|
||||
|
||||
# Setup tests
|
||||
WORKDIR /root/code/test/
|
||||
RUN make -j8
|
||||
|
||||
CMD ["bash", "-c", "./run_all -v ; cat all.log | grep 'not ok' ; ./problems"]
|
||||
@@ -500,17 +500,9 @@ class TestBug1900(TestCase):
|
||||
|
||||
def test_project_eval(self):
|
||||
"""1900: Project name can contain dashes"""
|
||||
self.t("add foo project:due-b")
|
||||
self.t("add foo project:doo-bee")
|
||||
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)
|
||||
self.assertEqual("doo-bee\n", out)
|
||||
|
||||
|
||||
class TestBug1904(TestCase):
|
||||
|
||||
@@ -126,7 +126,8 @@ class TestRecurrenceWeekdays(TestCase):
|
||||
# The due dates should be Friday and Monday, three days apart,
|
||||
# having skipped the weekend.
|
||||
# Note: On daylight savings in the fall, this '3' becomes '2.9583'.
|
||||
self.assertTrue(int(monday.strip()) - int(friday.strip()) >= 2)
|
||||
# Note: when monday is next year, friday+2 > 365
|
||||
self.assertTrue(int(monday.strip()) >= (int(friday.strip()) + 2) % 365)
|
||||
|
||||
|
||||
class TestRecurrenceUntil(TestCase):
|
||||
|
||||
@@ -187,7 +187,7 @@ TODO Task::decode
|
||||
test.is (task.get ("three"), "four", "three=four");
|
||||
|
||||
// Task::set
|
||||
task.data.clear ();
|
||||
task = Task();
|
||||
task.set ("name", "value");
|
||||
test.is (task.composeF4 (), "[name:\"value\"]", "Task::set");
|
||||
|
||||
@@ -211,7 +211,7 @@ TODO Task::decode
|
||||
test.is (task.composeF4 (), "[name:\"value\"]", "Task::remove");
|
||||
|
||||
// Task::all
|
||||
test.is (task.data.size (), (size_t)1, "Task::all size");
|
||||
test.is (task.all ().size (), (size_t)1, "Task::all size");
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
|
||||
18
test/tag.t
18
test/tag.t
@@ -36,13 +36,10 @@ from basetest import Task, TestCase
|
||||
|
||||
|
||||
class TestTags(TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""Executed once before any test in the class"""
|
||||
cls.t = Task()
|
||||
|
||||
def setUp(self):
|
||||
"""Executed before each test in the class"""
|
||||
self.t = Task()
|
||||
|
||||
def split_tags(self, tags):
|
||||
return sorted(tags.strip().split(','))
|
||||
@@ -81,6 +78,19 @@ class TestTags(TestCase):
|
||||
code, out, err = self.t("1 modify -missing")
|
||||
self.assertIn("Modified 0 tasks", out)
|
||||
|
||||
def test_tag_bulk_removal(self):
|
||||
"""2655: Test bulk removal of tags"""
|
||||
self.t("add +one This +two is a test +three")
|
||||
code, out, err = self.t("_get 1.tags")
|
||||
self.assertEqual(
|
||||
sorted(["one", "two", "three"]),
|
||||
self.split_tags(out))
|
||||
|
||||
# Remove all tags in bulk
|
||||
self.t("1 modify tags:")
|
||||
code, out, err = self.t("_get 1.tags")
|
||||
self.assertEqual("\n", out)
|
||||
|
||||
|
||||
class TestVirtualTags(TestCase):
|
||||
@classmethod
|
||||
|
||||
89
test/tw-2689.t.cpp
Normal file
89
test/tw-2689.t.cpp
Normal file
@@ -0,0 +1,89 @@
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Copyright 2006 - 2021, Tomas Babej, Paul Beckingham, Federico Hernandez.
|
||||
//
|
||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
// of this software and associated documentation files (the "Software"), to deal
|
||||
// in the Software without restriction, including without limitation the rights
|
||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
// copies of the Software, and to permit persons to whom the Software is
|
||||
// furnished to do so, subject to the following conditions:
|
||||
//
|
||||
// The above copyright notice and this permission notice shall be included
|
||||
// in all copies or substantial portions of the Software.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
// SOFTWARE.
|
||||
//
|
||||
// https://www.opensource.org/licenses/mit-license.php
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include <iostream>
|
||||
#include <cmake.h>
|
||||
#include <stdlib.h>
|
||||
#include <main.h>
|
||||
#include <test.h>
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
int main (int, char**)
|
||||
{
|
||||
UnitTest test (12);
|
||||
|
||||
// Ensure environment has no influence.
|
||||
unsetenv ("TASKDATA");
|
||||
unsetenv ("TASKRC");
|
||||
|
||||
// Inform Task about the attributes in the JSON below
|
||||
Task::attributes["depends"] = "string";
|
||||
Task::attributes["uuid"] = "string";
|
||||
|
||||
// depends in [..] string from a taskserver (issue#2689)
|
||||
auto sample = "{"
|
||||
"\"depends\":\"[\\\"92a40a34-37f3-4785-8ca1-ff89cfbfd105\\\",\\\"e08e35fa-e42b-4de0-acc4-518fca8f6365\\\"]\","
|
||||
"\"uuid\":\"00000000-0000-0000-0000-000000000000\""
|
||||
"}";
|
||||
auto json = Task (sample);
|
||||
auto value = json.get ("uuid");
|
||||
test.is (value, "00000000-0000-0000-0000-000000000000", "json [..] uuid");
|
||||
value = json.get ("depends");
|
||||
test.is (value, "92a40a34-37f3-4785-8ca1-ff89cfbfd105,e08e35fa-e42b-4de0-acc4-518fca8f6365", "json [..] depends");
|
||||
test.ok (json.has ("dep_92a40a34-37f3-4785-8ca1-ff89cfbfd105"), "json [..] dep attr");
|
||||
test.ok (json.has ("dep_e08e35fa-e42b-4de0-acc4-518fca8f6365"), "json [..] dep attr");
|
||||
|
||||
// depends in comma-delimited string from a taskserver (deprecated format)
|
||||
sample = "{"
|
||||
"\"depends\":\"92a40a34-37f3-4785-8ca1-ff89cfbfd105,e08e35fa-e42b-4de0-acc4-518fca8f6365\","
|
||||
"\"uuid\":\"00000000-0000-0000-0000-000000000000\""
|
||||
"}";
|
||||
json = Task (sample);
|
||||
value = json.get ("uuid");
|
||||
test.is (value, "00000000-0000-0000-0000-000000000000", "json comma-separated uuid");
|
||||
value = json.get ("depends");
|
||||
test.is (value, "92a40a34-37f3-4785-8ca1-ff89cfbfd105,e08e35fa-e42b-4de0-acc4-518fca8f6365", "json comma-separated depends");
|
||||
test.ok (json.has ("dep_92a40a34-37f3-4785-8ca1-ff89cfbfd105"), "json comma-separated dep attr");
|
||||
test.ok (json.has ("dep_e08e35fa-e42b-4de0-acc4-518fca8f6365"), "json comma-separated dep attr");
|
||||
|
||||
// depends in a JSON array from a taskserver
|
||||
sample = "{"
|
||||
"\"depends\":[\"92a40a34-37f3-4785-8ca1-ff89cfbfd105\",\"e08e35fa-e42b-4de0-acc4-518fca8f6365\"],"
|
||||
"\"uuid\":\"00000000-0000-0000-0000-000000000000\""
|
||||
"}";
|
||||
json = Task (sample);
|
||||
value = json.get ("uuid");
|
||||
test.is (value, "00000000-0000-0000-0000-000000000000", "json array uuid");
|
||||
value = json.get ("depends");
|
||||
test.is (value, "92a40a34-37f3-4785-8ca1-ff89cfbfd105,e08e35fa-e42b-4de0-acc4-518fca8f6365", "json array depends");
|
||||
test.ok (json.has ("dep_92a40a34-37f3-4785-8ca1-ff89cfbfd105"), "json array dep attr");
|
||||
test.ok (json.has ("dep_e08e35fa-e42b-4de0-acc4-518fca8f6365"), "json array dep attr");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -58,7 +58,7 @@ int main (int, char**)
|
||||
Task rightAgain (right);
|
||||
|
||||
std::string output = left.diff (right);
|
||||
t.ok (left.data != right.data, "Detected changes");
|
||||
t.ok (!(left == right), "Detected changes");
|
||||
t.ok (output.find ("Zero will be changed from '0' to '00'") != std::string::npos, "Detected change zero:0 -> zero:00");
|
||||
t.ok (output.find ("One will be deleted") != std::string::npos, "Detected deletion one:1 ->");
|
||||
t.ok (output.find ("Two") == std::string::npos, "Detected no change two:2 -> two:2");
|
||||
|
||||
@@ -77,11 +77,6 @@ class TestVersion(TestCase):
|
||||
self.assertIn("MIT license", out)
|
||||
self.assertIn("https://taskwarrior.org", out)
|
||||
|
||||
def slurp_git(self):
|
||||
git_cmd = ("git", "rev-parse", "--short", "--verify", "HEAD")
|
||||
_, hash, _ = run_cmd_wait(git_cmd)
|
||||
return hash.rstrip("\n")
|
||||
|
||||
def test_under_version(self):
|
||||
"""_version and diagnostics output expected version and syntax"""
|
||||
code, out, err = self.t("_version")
|
||||
@@ -94,8 +89,7 @@ class TestVersion(TestCase):
|
||||
if os.path.exists("../.git"):
|
||||
if 2 >= len(version) > 0:
|
||||
git = version[1]
|
||||
git_expected = "({0})".format(self.slurp_git())
|
||||
self.assertEqual(git_expected, git)
|
||||
self.assertRegex(git, r'\([a-f0-9]*\)'))
|
||||
else:
|
||||
raise ValueError("Unexpected output from _version '{0}'".format(
|
||||
out))
|
||||
|
||||
Reference in New Issue
Block a user