Compare commits

...

69 Commits

Author SHA1 Message Date
Tomas Babej
7400e6ed6b README: Update version reference 2022-03-16 01:52:27 -04:00
Tomas Babej
93394e7054 meta: Update CMake version reference for 2.6.2 2022-03-16 01:52:16 -04:00
Tomas Babej
f030154ef6 docs: Document #2689 2022-03-16 01:39:02 -04:00
Tomas Babej
10a2225aae docs: Document #2707 2022-03-16 01:34:42 -04:00
Tomas Babej
de793e5902 docs: Document #2748 2022-03-14 02:48:13 -04:00
Tomas Babej
1fc1884017 Context: Add parent column among default ones for the recurring report
This helps to identify parent-child relationships.
2022-03-14 02:48:13 -04:00
Tomas Babej
75ce386b44 Context: Adjust definition of the recurring report filter
Ensure the recurring report lists the recurring parent tasks as well as
the child tasks.
2022-03-14 02:48:13 -04:00
Tomas Babej
e60fdafdaa centos8: Fix missing repo metadata 2022-02-11 00:40:31 -05:00
Jan Palus
d541e0da65 TLSClient: fix multiple issues with error handling
- do not check errno on successful function calls (it might not be
  cleared after previous failed one)
- GNUTLS_E_* are not passed through errno but as function return value
- therefore there's more error spectrum than -1
- do not assume whole header is received, check number of bytes fetched

small additional improvements:
- read as many bytes into buffer as possible before appending to data
- skip writing nul byte at the end of buffer and use append() instead
- additional sanity checks
2022-02-11 00:19:04 -05:00
Patrick Flakus
59a1729a05 Cleanup: Fixed typo in docstring 2022-01-27 16:10:47 -05:00
Tomas Babej
112ac54a57 ColDepends: Support dependency removal for the same formats as dependency addition 2022-01-19 20:11:41 -05:00
Tomas Babej
0cc2de6179 tests: Add a test for handling short UUIDs in the depends column 2022-01-19 20:11:41 -05:00
Dustin J. Mitchell
28e268bd26 fix parsing invalid depends from server
The data from the server is read via Task::parseJSON, not Task::parse.

This also reverts the tests for Task::parse, and adds new tests for this
specific issue.
2022-01-19 19:34:45 -05:00
Dustin J. Mitchell
7321febe4f A more thorough fix to corruption of the depends property 2022-01-19 19:34:45 -05:00
Dustin J. Mitchell
96d6c1df9f Add special case for taskserver mis-formatting of depends 2022-01-16 15:36:21 -05:00
Tomas Babej
cb058f2e4b tests: Remove Ubuntu 20.10 (now beyond EOL) from the test matrix 2022-01-05 21:01:21 -05:00
Dustin J. Mitchell
c564bbc0b7 fix test to pass at the end of the year 2021-12-30 15:03:37 -05:00
Dustin J. Mitchell
b066a17ebe use a reference in CurrentTask 2021-12-30 15:03:37 -05:00
Dustin J. Mitchell
277ce0e226 Update doc comment in src/Context.h
Co-authored-by: Tomas Babej <tomas@tbabej.com>
2021-12-30 15:03:37 -05:00
Dustin J. Mitchell
65830dd705 replace the global contextTask with a Context field 2021-12-30 15:03:37 -05:00
Dustin J. Mitchell
53127bf844 add Context::withCurrentTask 2021-12-30 15:03:37 -05:00
Dustin J. Mitchell
2ea6dd627e Just set contextTask in Task::modify
Based on the observation that every command was setting contextTask
immediately before calling `task.modify(..)`.
2021-12-30 15:03:37 -05:00
Dustin J. Mitchell
db26a28bf9 move domSource to Eval, as it's an Eval source 2021-12-30 15:03:37 -05:00
Dustin J. Mitchell
6e9ad1048d move use of contextTask out of columns, into commands 2021-12-30 15:03:37 -05:00
Dustin J. Mitchell
e98b61f2b5 test for dashes in proj without incidentally testing DOM references in 'task add' 2021-12-30 15:03:37 -05:00
Dustin J. Mitchell
4fa1c0bcfb remove temporary function 2021-12-16 23:03:44 -05:00
Dustin J. Mitchell
e3dd91d45e use the existing (but misnamed) orphans method 2021-12-16 23:03:44 -05:00
Dustin J. Mitchell
e768e2c100 compare Tasks directly, not their properties 2021-12-16 23:03:44 -05:00
Dustin J. Mitchell
ac24ec1387 fixup and fix spelling for denotate 2021-12-16 23:03:44 -05:00
Dustin J. Mitchell
3a61289f6c replace a use of Task::data in tests 2021-12-16 23:03:44 -05:00
Dustin J. Mitchell
9f149a7f35 use Task::getAnnotations to get annotatoins 2021-12-16 23:03:44 -05:00
Dustin J. Mitchell
70f83b34ef Move diff formatting into Task (simple refactor)
The Task class already included two forms of diff. This adds another
two.
2021-12-16 23:03:44 -05:00
Dustin J. Mitchell
8d3953183a Only set contextTask if not adding a new task
When adding a new task, the current task is empty, so there is no
context in which to evaluate DOM references.  #2683 will address this in
a more robust fashion.
2021-12-16 23:03:44 -05:00
Dustin J. Mitchell
2812a8c77a Refactor getDOM to use a pointer for the optional context
It's possible to call getDOM without a contextual task.  Previously,
this was done by referencing a "dummy" task which necessitated a way to
distinguish such dummy tasks.  This switches to using a pointer and
treating the NULL value as meaning there is no context.

Note that this cannot use `std::optional<&Task>`, as optional does not
support reference types.
2021-12-16 23:03:44 -05:00
Dustin J. Mitchell
3af5ceadc1 Return early from getDOM if there is no contextual task (refactor)
Before this change, all conditionals checked `have_ref`; returning early
is clearer.
2021-12-16 23:03:44 -05:00
Dustin J. Mitchell
0d9e402d3e Add Task.is_empty, needed to identify "dummy" tasks (simple refactor)
getDOM takes an &Task that may be a reference to a dummy, or may be a
real task.  The is_empty method replaces `task.data.size() == 0` as a
way to distinguish the two.
2021-12-16 23:03:44 -05:00
Dustin J. Mitchell
dede40bc4e Replace direct Task.data access with a temp getter (syntactic refactor)
Now this getter, `data_removeme`, can easily be grepped out and
replaced, one usage at a time in small PRs.
2021-12-16 23:03:44 -05:00
Tomas Babej
3a00956144 ColDepends: Allow specifying short(er) UUIDs 2021-12-03 03:57:37 -05:00
Siôn le Roux
01add8a34a docs: Fix typo in XDG news
This had XFG written in it, it's surely a typo of XDG since on the next
line it's written correctly.
2021-11-23 21:05:01 +01:00
Tomas Babej
a09712d5d2 docs: Document #2655 2021-11-22 15:51:27 +01:00
Tomas Babej
8074e509ba tests: Add bulk removal test for tags attribute 2021-11-22 15:51:27 +01:00
Tomas Babej
af10774aec tests: Add bulk removal test for depends attribute 2021-11-22 15:51:27 +01:00
Tomas Babej
d54c7e090e Task: Correctly handle bulk removal of virtual tags/depends attributes
This edge case would happen if a user issued a following command

    $ task 1 mod depends:
or

    $ task 1 mod tags:

which would not perform as expected for tasks with non-empty depends /
tags attributes.

The problem under the hood is the fact that current synchronization
between 'tags" attribute and its constituent decomposed `tag_X`
attributes is performed in a way where the set of tags obtained from
`tag_X` attributes is taken as the source of truth.

If the legacy 'tags:' attribute was set to empty, the fixTags() method
would then restore its previous value from the `tag_X` attributes,
effectively leading to a no-op.

The same happens with dependencies. The fix here is to detect removal of
depends and tags attributes, and instead of setting the legacy
attributes to empty values, we iteratively remove all tags/dependencies.

Closes #2655.
2021-11-22 15:51:27 +01:00
Dustin J. Mitchell
3937f1efb0 Don't look for a specific sha when testing task version
This allows tests to pass even with a dirty working copy or when running
tests via `git rebase`.
2021-11-21 23:34:46 +01:00
Tomas Babej
3e8190831c docs: Document #502 2021-11-20 15:19:34 -05:00
Tomas Babej
3c2b74a36f tests: Remove expected failure for ID-range based dependencies 2021-11-20 15:19:34 -05:00
Tomas Babej
0558b6c7aa ColDepends: Recognize and properly handle ID ranges 2021-11-20 15:19:34 -05:00
Tomas Babej
774f6df210 docs: Document #2671 2021-11-09 08:00:05 -05:00
Tomas Babej
1e1c0e8f04 libshared: Bump to include fix for soww
See https://github.com/GothenburgBitFactory/libshared/pull/66 for
reference. Closes #2671.
2021-11-09 07:57:37 -05:00
Tomas Babej
caf0f9db3e tests: Clarify we're testing OpenSUSE 15.x rolling 2021-11-06 13:08:51 -04:00
Tomas Babej
da43078eba tests: Add Ubuntu 21.10 into the test matrix 2021-11-06 12:59:16 -04:00
Tomas Babej
6fae705b43 tests: Add Ubuntu 20.10 into the test matrix 2021-11-06 12:57:26 -04:00
Tomas Babej
e4f1e05c1d tests: Add Fedora 35 into the test matrix 2021-11-06 12:54:46 -04:00
Tomas Babej
4559287d07 tests: Remove tests on F31
This distribution release is more than a year out of its support window.
2021-11-06 12:52:24 -04:00
Tomas Babej
2d4776586d README: Remove inacurrate badge
The commit count has been inacurrate for some time, undercounting
(perhaps due to aggreessive caching), but also overcounting at times
(due to merges).
2021-11-06 12:39:22 -04:00
Tomas Babej
ed4b932530 tests: Add test to cover invalid contexts due to tag exclusion 2021-11-06 11:58:47 -04:00
Tomas Babej
f0f704fc99 tests: Fix copy-paste error
The context name in these two tests was invalid in some assertions.
2021-11-06 11:58:46 -04:00
Tomas Babej
403c44b1fa docs: Document #2664 2021-11-06 11:58:45 -04:00
Tomas Babej
493f36ecdd CmdContext: Utilize new reason-providing validateWriteContext 2021-11-06 11:53:45 -04:00
Tomas Babej
5d8f8dac35 validateWriteContext: Refactor to return the underlying reason for invalidity directly 2021-11-06 11:53:44 -04:00
Tomas Babej
f5dce013ce CmdContext: Expand the definition of an invalid write context to incllude tag exclusion 2021-11-06 11:53:44 -04:00
Tomas Babej
531881f651 validateWriteContext: Tag exclusion should also be detected as invalid write context 2021-11-06 11:53:43 -04:00
Tomas Babej
7de681aa3b validateWriteContext: Apply early break when detected the OR operator 2021-11-06 11:53:42 -04:00
Jakub Wilk
04ef785eea Fix typo 2021-11-06 11:50:02 -04:00
Tomas Babej
e1cfb91d42 docs: Document #2648 2021-11-06 11:28:05 -04:00
Tomas Babej
72930708ec CmdNews: Support Mac OS-X url opener 2021-11-04 22:07:39 -04:00
Tomas Babej
58763fd49f reproduce: Make vim available in the reproduction image 2021-10-30 06:12:48 -04:00
Steven Kreuzer
a89c875c49 Fix typo in ChangeLog
Change 'precendence' to 'precedence'
2021-10-22 18:54:32 -04:00
Tomas Babej
e0f598f91c Changelog: Document 2.6.1 release commit hash 2021-10-19 00:30:53 -04:00
56 changed files with 833 additions and 452 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -4,7 +4,6 @@
[![GitHub Actions build status](https://github.com/GothenburgBitFactory/taskwarrior/workflows/tests/badge.svg?branch=develop)](https://github.com/GothenburgBitFactory/taskwarrior/actions)
[![Release](https://img.shields.io/github/v/release/GothenburgBitFactory/taskwarrior)](https://github.com/GothenburgBitFactory/taskwarrior/releases/latest)
[![Release date](https://img.shields.io/github/release-date/GothenburgBitFactory/taskwarrior)](https://github.com/GothenburgBitFactory/taskwarrior/releases/latest)
![Commits since release](https://img.shields.io/github/commits-since/GothenburgBitFactory/taskwarrior/latest)
[![GitHub Sponsors](https://img.shields.io/github/sponsors/GothenburgBitFactory?color=green)](https://github.com/sponsors/GothenburgBitFactory/)
</br>
[![Twitter](https://img.shields.io/twitter/follow/taskwarrior?style=social)](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:

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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;

View File

@@ -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
{

View File

@@ -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);

View File

@@ -32,6 +32,8 @@
#include <Lexer.h>
#include <Variant.h>
bool domSource (const std::string&, Variant&);
class Eval
{
public:

View File

@@ -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);

View File

@@ -32,8 +32,6 @@
#include <Task.h>
#include <Variant.h>
bool domSource (const std::string&, Variant&);
class Filter
{
public:

View File

@@ -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';

View File

@@ -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)

View File

@@ -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 = " MODIFICATION ";
// 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;
}
////////////////////////////////////////////////////////////////////////////////

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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);

View File

@@ -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 ());

View File

@@ -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.

View File

@@ -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),

View File

@@ -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";

View File

@@ -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 ("");

View File

@@ -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);

View File

@@ -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);

View File

@@ -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,

View File

@@ -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 ())

View File

@@ -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";

View File

@@ -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;

View File

@@ -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);
}

View File

@@ -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
View File

@@ -34,5 +34,6 @@ variant_partial.t
variant_subtract.t
variant_xor.t
view.t
tw-2689.t
json_test

View File

@@ -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

View File

@@ -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):

View File

@@ -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"""

View File

@@ -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

View File

@@ -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
View 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"]

View File

@@ -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):

View File

@@ -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):

View File

@@ -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");
////////////////////////////////////////////////////////////////////////////////

View File

@@ -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
View 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;
}
////////////////////////////////////////////////////////////////////////////////

View File

@@ -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");

View File

@@ -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))