mirror of
https://github.com/GothenburgBitFactory/taskwarrior.git
synced 2025-06-26 10:54:26 +02:00
Compare commits
39 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
c594ecb58d | ||
![]() |
baaf69202b | ||
![]() |
a949c698f9 | ||
![]() |
ffa0d3e944 | ||
![]() |
6d81c8cda0 | ||
![]() |
440d3f8c92 | ||
![]() |
e5b69afee2 | ||
![]() |
75d351afad | ||
![]() |
f6824e90a1 | ||
![]() |
89d84f0bdd | ||
![]() |
4620b5fd25 | ||
![]() |
6c60a8db84 | ||
![]() |
79eb38d582 | ||
![]() |
0e59a62ead | ||
![]() |
97bcc76ac1 | ||
![]() |
499f931f67 | ||
![]() |
416c6d3ca4 | ||
![]() |
36e5f8895d | ||
![]() |
b4e25fe42f | ||
![]() |
7be313e91f | ||
![]() |
36a449c935 | ||
![]() |
31829d61fc | ||
![]() |
bae37d9448 | ||
![]() |
bfea0f6836 | ||
![]() |
2a64b5c880 | ||
![]() |
15bb71764e | ||
![]() |
5b70ce6be2 | ||
![]() |
22608cb44e | ||
![]() |
f1cb656f75 | ||
![]() |
db23195f4d | ||
![]() |
4a464c13a8 | ||
![]() |
a3b44bdef5 | ||
![]() |
bc16297274 | ||
![]() |
7bf3be2f07 | ||
![]() |
768d45197b | ||
![]() |
f9c17d9b5b | ||
![]() |
1f6e7de569 | ||
![]() |
2ee5fb287c | ||
![]() |
b792018c00 |
35 changed files with 1019 additions and 423 deletions
5
.github/dependabot.yml
vendored
5
.github/dependabot.yml
vendored
|
@ -5,6 +5,11 @@ updates:
|
||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
interval: "weekly"
|
interval: "weekly"
|
||||||
|
# Enable version updates for git submodules
|
||||||
|
- package-ecosystem: "gitsubmodule"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
# Enable updates for Rust packages
|
# Enable updates for Rust packages
|
||||||
- package-ecosystem: "cargo"
|
- package-ecosystem: "cargo"
|
||||||
directory: "/" # Location of package manifests
|
directory: "/" # Location of package manifests
|
||||||
|
|
6
.github/workflows/docker-image.yaml
vendored
6
.github/workflows/docker-image.yaml
vendored
|
@ -33,10 +33,10 @@ jobs:
|
||||||
submodules: "recursive"
|
submodules: "recursive"
|
||||||
|
|
||||||
- name: Install cosign
|
- name: Install cosign
|
||||||
uses: sigstore/cosign-installer@v3.8.1
|
uses: sigstore/cosign-installer@v3.9.0
|
||||||
|
|
||||||
- name: Log into registry ${{ env.REGISTRY }}
|
- name: Log into registry ${{ env.REGISTRY }}
|
||||||
uses: docker/login-action@v3.3.0
|
uses: docker/login-action@v3.4.0
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
|
@ -44,7 +44,7 @@ jobs:
|
||||||
|
|
||||||
- name: Build and push Taskwarrior Docker image
|
- name: Build and push Taskwarrior Docker image
|
||||||
id: build-and-push
|
id: build-and-push
|
||||||
uses: docker/build-push-action@v6.15.0
|
uses: docker/build-push-action@v6.18.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: "./docker/task.dockerfile"
|
file: "./docker/task.dockerfile"
|
||||||
|
|
|
@ -9,7 +9,7 @@ repos:
|
||||||
- id: check-yaml
|
- id: check-yaml
|
||||||
- id: check-added-large-files
|
- id: check-added-large-files
|
||||||
- repo: https://github.com/pre-commit/mirrors-clang-format
|
- repo: https://github.com/pre-commit/mirrors-clang-format
|
||||||
rev: v19.1.7
|
rev: v20.1.6
|
||||||
hooks:
|
hooks:
|
||||||
- id: clang-format
|
- id: clang-format
|
||||||
types_or: [c++, c]
|
types_or: [c++, c]
|
||||||
|
|
|
@ -4,7 +4,7 @@ enable_testing()
|
||||||
set (CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
set (CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||||
|
|
||||||
project (task
|
project (task
|
||||||
VERSION 3.4.0
|
VERSION 3.4.1
|
||||||
DESCRIPTION "Taskwarrior - a command-line TODO list manager"
|
DESCRIPTION "Taskwarrior - a command-line TODO list manager"
|
||||||
HOMEPAGE_URL https://taskwarrior.org/)
|
HOMEPAGE_URL https://taskwarrior.org/)
|
||||||
|
|
||||||
|
@ -67,7 +67,6 @@ SET (TASK_BINDIR bin CACHE STRING "Installation directory for the bi
|
||||||
# rust libs require these
|
# rust libs require these
|
||||||
set (TASK_LIBRARIES dl pthread)
|
set (TASK_LIBRARIES dl pthread)
|
||||||
|
|
||||||
check_function_exists (timegm HAVE_TIMEGM)
|
|
||||||
check_function_exists (get_current_dir_name HAVE_GET_CURRENT_DIR_NAME)
|
check_function_exists (get_current_dir_name HAVE_GET_CURRENT_DIR_NAME)
|
||||||
check_function_exists (wordexp HAVE_WORDEXP)
|
check_function_exists (wordexp HAVE_WORDEXP)
|
||||||
|
|
||||||
|
|
1150
Cargo.lock
generated
1150
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,11 @@
|
||||||
------ current release ---------------------------
|
------ current release ---------------------------
|
||||||
|
|
||||||
|
3.4.1 -
|
||||||
|
|
||||||
|
- The nagging to read `task news` has been fixed.
|
||||||
|
|
||||||
|
------ old releases ------------------------------
|
||||||
|
|
||||||
3.4.0 -
|
3.4.0 -
|
||||||
|
|
||||||
- Where possible, the task DB is now opened in read-only mode, which improves
|
- Where possible, the task DB is now opened in read-only mode, which improves
|
||||||
|
@ -19,8 +25,6 @@ Thanks to the following people for contributions to this release:
|
||||||
- Yong Li
|
- Yong Li
|
||||||
- jrmarino
|
- jrmarino
|
||||||
|
|
||||||
------ old releases ------------------------------
|
|
||||||
|
|
||||||
3.3.0 -
|
3.3.0 -
|
||||||
|
|
||||||
- Sync now supports AWS S3 as a backend.
|
- Sync now supports AWS S3 as a backend.
|
||||||
|
|
9
INSTALL
9
INSTALL
|
@ -34,7 +34,7 @@ Briefly, these shell commands will unpack, build and install Taskwarrior:
|
||||||
$ cmake -S . -B build -DCMAKE_BUILD_TYPE=Release . [3]
|
$ cmake -S . -B build -DCMAKE_BUILD_TYPE=Release . [3]
|
||||||
$ cmake --build build [4]
|
$ cmake --build build [4]
|
||||||
$ sudo cmake --install build [5]
|
$ sudo cmake --install build [5]
|
||||||
$ cd .. ; rm -r task-X.Y.Z [6]
|
$ cd .. ; rm -r task-X.Y.Z [6] (see: Uninstallation)
|
||||||
|
|
||||||
These commands are explained below:
|
These commands are explained below:
|
||||||
|
|
||||||
|
@ -103,6 +103,13 @@ There is no uninstall option in CMake makefiles. This is a manual process.
|
||||||
To uninstall Taskwarrior, remove the files listed in the install_manifest.txt
|
To uninstall Taskwarrior, remove the files listed in the install_manifest.txt
|
||||||
file that was generated when you built Taskwarrior.
|
file that was generated when you built Taskwarrior.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd task-X.Y.Z
|
||||||
|
sudo xargs rm < build/install_manifest.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want to uninstall this way, you will need to omit step [6] above and
|
||||||
|
retain the source folder after installation.
|
||||||
|
|
||||||
Taskwarrior Build Notes
|
Taskwarrior Build Notes
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|
|
@ -41,9 +41,6 @@
|
||||||
/* Found tm_gmtoff */
|
/* Found tm_gmtoff */
|
||||||
#cmakedefine HAVE_TM_GMTOFF
|
#cmakedefine HAVE_TM_GMTOFF
|
||||||
|
|
||||||
/* Found timegm */
|
|
||||||
#cmakedefine HAVE_TIMEGM
|
|
||||||
|
|
||||||
/* Found st.st_birthtime struct member */
|
/* Found st.st_birthtime struct member */
|
||||||
#cmakedefine HAVE_ST_BIRTHTIME
|
#cmakedefine HAVE_ST_BIRTHTIME
|
||||||
|
|
||||||
|
|
|
@ -143,7 +143,9 @@ Then configure Taskwarrior with:
|
||||||
|
|
||||||
To synchronize your tasks to AWS, select a region near you and use the AWS
|
To synchronize your tasks to AWS, select a region near you and use the AWS
|
||||||
console to create a new S3 bucket. The default settings for the bucket are
|
console to create a new S3 bucket. The default settings for the bucket are
|
||||||
adequate.
|
adequate. In particular, ensure that no lifecycle policies are enabled, as they
|
||||||
|
may automatically delete or transition objects, potentially impacting data
|
||||||
|
availability.
|
||||||
|
|
||||||
You will also need an AWS IAM user with the following policy, where BUCKETNAME
|
You will also need an AWS IAM user with the following policy, where BUCKETNAME
|
||||||
is the name of the bucket. The same user can be configured for multiple
|
is the name of the bucket. The same user can be configured for multiple
|
||||||
|
|
|
@ -1384,7 +1384,7 @@ if you define a UDA named 'estimate', Taskwarrior will not know that this value
|
||||||
is weeks, hours, minutes, money, or some other resource count.
|
is weeks, hours, minutes, money, or some other resource count.
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B uda.<name>.type=string|numeric|date|duration
|
.B uda.<name>.type=string|numeric|uuid|date|duration
|
||||||
.RS
|
.RS
|
||||||
Defines a UDA called '<name>', of the specified type.
|
Defines a UDA called '<name>', of the specified type.
|
||||||
.RE
|
.RE
|
||||||
|
|
|
@ -1173,6 +1173,13 @@ void Context::staticInitialization() {
|
||||||
void Context::createDefaultConfig() {
|
void Context::createDefaultConfig() {
|
||||||
// Do we need to create a default rc?
|
// Do we need to create a default rc?
|
||||||
if (rc_file._data != "" && !rc_file.exists()) {
|
if (rc_file._data != "" && !rc_file.exists()) {
|
||||||
|
// If stdout is not a file, we are probably executing in a completion context and should not
|
||||||
|
// prompt (as the user won't see it) or modify the config (as completion functions are typically
|
||||||
|
// read-only).
|
||||||
|
if (!isatty(STDOUT_FILENO)) {
|
||||||
|
throw std::string("Cannot proceed without rc file.");
|
||||||
|
}
|
||||||
|
|
||||||
if (config.getBoolean("confirmation") &&
|
if (config.getBoolean("confirmation") &&
|
||||||
!confirm(format("A configuration file could not be found in {1}\n\nWould you like a sample "
|
!confirm(format("A configuration file could not be found in {1}\n\nWould you like a sample "
|
||||||
"{2} created, so Taskwarrior can proceed?",
|
"{2} created, so Taskwarrior can proceed?",
|
||||||
|
|
|
@ -286,6 +286,13 @@ bool getDOM(const std::string& name, const Task* task, Variant& value) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The "tags" property is deprecated, but it is documented as part of the DOM, so simulate it.
|
||||||
|
if (size == 1 && canonical == "tags") {
|
||||||
|
auto tags = ref->getTags();
|
||||||
|
value = Variant(join(",", tags));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
Column* column = Context::getContext().columns[canonical];
|
Column* column = Context::getContext().columns[canonical];
|
||||||
|
|
||||||
if (size == 1 && column) {
|
if (size == 1 && column) {
|
||||||
|
|
|
@ -275,7 +275,7 @@ void Hooks::onAdd(Task& task) const {
|
||||||
// - all emitted non-JSON lines are considered feedback or error messages
|
// - all emitted non-JSON lines are considered feedback or error messages
|
||||||
// depending on the status code.
|
// depending on the status code.
|
||||||
//
|
//
|
||||||
void Hooks::onModify(const Task& before, Task& after) const {
|
void Hooks::onModify(Task& before, Task& after) const {
|
||||||
if (!_enabled) return;
|
if (!_enabled) return;
|
||||||
|
|
||||||
Timer timer;
|
Timer timer;
|
||||||
|
|
|
@ -40,7 +40,7 @@ class Hooks {
|
||||||
void onLaunch() const;
|
void onLaunch() const;
|
||||||
void onExit() const;
|
void onExit() const;
|
||||||
void onAdd(Task&) const;
|
void onAdd(Task&) const;
|
||||||
void onModify(const Task&, Task&) const;
|
void onModify(Task&, Task&) const;
|
||||||
std::vector<std::string> list() const;
|
std::vector<std::string> list() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
|
|
@ -354,8 +354,7 @@ bool TDB2::get(const std::string& uuid, Task& task) {
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
// Locate task by UUID, wherever it is.
|
// Locate task by UUID, wherever it is.
|
||||||
bool TDB2::has(const std::string& uuid) {
|
bool TDB2::has(const std::string& uuid) {
|
||||||
Task task;
|
return replica()->get_task_data(tc::uuid_from_string(uuid)).is_some();
|
||||||
return get(uuid, task);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
|
@ -777,7 +777,7 @@ void Task::parseLegacy(const std::string& line) {
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
std::string Task::composeJSON(bool decorate /*= false*/) const {
|
std::string Task::composeJSON(bool decorate /*= false*/) {
|
||||||
std::stringstream out;
|
std::stringstream out;
|
||||||
out << '{';
|
out << '{';
|
||||||
|
|
||||||
|
@ -894,7 +894,7 @@ std::string Task::composeJSON(bool decorate /*= false*/) const {
|
||||||
|
|
||||||
#ifdef PRODUCT_TASKWARRIOR
|
#ifdef PRODUCT_TASKWARRIOR
|
||||||
// Include urgency.
|
// Include urgency.
|
||||||
if (decorate) out << ',' << "\"urgency\":" << urgency_c();
|
if (decorate) out << ',' << "\"urgency\":" << urgency();
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
out << '}';
|
out << '}';
|
||||||
|
@ -928,7 +928,7 @@ void Task::addAnnotation(const std::string& description) {
|
||||||
++now;
|
++now;
|
||||||
} while (has(key));
|
} while (has(key));
|
||||||
|
|
||||||
data[key] = json::decode(description);
|
data[key] = description;
|
||||||
++annotation_count;
|
++annotation_count;
|
||||||
recalc_urgency = true;
|
recalc_urgency = true;
|
||||||
}
|
}
|
||||||
|
@ -1999,7 +1999,7 @@ void Task::modify(modType type, bool text_required /* = false */) {
|
||||||
// Delegate modification to the column object or their base classes.
|
// Delegate modification to the column object or their base classes.
|
||||||
if (name == "depends" || name == "tags" || name == "recur" || column->type() == "date" ||
|
if (name == "depends" || name == "tags" || name == "recur" || column->type() == "date" ||
|
||||||
column->type() == "duration" || column->type() == "numeric" ||
|
column->type() == "duration" || column->type() == "numeric" ||
|
||||||
column->type() == "string") {
|
column->type() == "string" || column->type() == "uuid") {
|
||||||
column->modify(*this, value);
|
column->modify(*this, value);
|
||||||
mods = true;
|
mods = true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -68,7 +68,7 @@ class Task {
|
||||||
Task(rust::Box<tc::TaskData>);
|
Task(rust::Box<tc::TaskData>);
|
||||||
|
|
||||||
void parse(const std::string&);
|
void parse(const std::string&);
|
||||||
std::string composeJSON(bool decorate = false) const;
|
std::string composeJSON(bool decorate = false);
|
||||||
|
|
||||||
// Status values.
|
// Status values.
|
||||||
enum status { pending, completed, deleted, recurring, waiting };
|
enum status { pending, completed, deleted, recurring, waiting };
|
||||||
|
|
|
@ -56,11 +56,9 @@ void ColumnTypeDuration::modify(Task& task, const std::string& value) {
|
||||||
evaluatedValue = Variant(value);
|
evaluatedValue = Variant(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The duration is stored in raw form, but it must still be valid,
|
// The duration is first parsed, then stored inside the variant as a `time_t`
|
||||||
// and therefore is parsed first.
|
|
||||||
std::string label = " [1;37;43mMODIFICATION[0m ";
|
std::string label = " [1;37;43mMODIFICATION[0m ";
|
||||||
if (evaluatedValue.type() == Variant::type_duration) {
|
if (evaluatedValue.type() == Variant::type_duration) {
|
||||||
// Store the raw value, for 'recur'.
|
|
||||||
Context::getContext().debug(label + _name + " <-- " + (std::string)evaluatedValue + " <-- '" +
|
Context::getContext().debug(label + _name + " <-- " + (std::string)evaluatedValue + " <-- '" +
|
||||||
value + '\'');
|
value + '\'');
|
||||||
task.set(_name, evaluatedValue);
|
task.set(_name, evaluatedValue);
|
||||||
|
|
|
@ -297,3 +297,23 @@ void ColumnUDADuration::render(std::vector<std::string>& lines, Task& task, int
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
ColumnUDAUUID::ColumnUDAUUID() {
|
||||||
|
_name = "<uda>";
|
||||||
|
_type = "uuid";
|
||||||
|
_style = "long";
|
||||||
|
_label = "";
|
||||||
|
_modifiable = true;
|
||||||
|
_uda = true;
|
||||||
|
_styles = {"long", "short"};
|
||||||
|
_examples = {"f30cb9c3-3fc0-483f-bfb2-3bf134f00694", "f30cb9c3"};
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
bool ColumnUDAUUID::validate(const std::string& input) const {
|
||||||
|
Lexer lex(input);
|
||||||
|
std::string token;
|
||||||
|
Lexer::Type type;
|
||||||
|
return lex.isUUID(token, type, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
|
@ -31,6 +31,7 @@
|
||||||
#include <ColTypeDuration.h>
|
#include <ColTypeDuration.h>
|
||||||
#include <ColTypeNumeric.h>
|
#include <ColTypeNumeric.h>
|
||||||
#include <ColTypeString.h>
|
#include <ColTypeString.h>
|
||||||
|
#include <ColUUID.h>
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
class ColumnUDAString : public ColumnTypeString {
|
class ColumnUDAString : public ColumnTypeString {
|
||||||
|
@ -83,5 +84,12 @@ class ColumnUDADuration : public ColumnTypeDuration {
|
||||||
std::vector<std::string> _values;
|
std::vector<std::string> _values;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
class ColumnUDAUUID : public ColumnUUID {
|
||||||
|
public:
|
||||||
|
ColumnUDAUUID();
|
||||||
|
bool validate(const std::string&) const;
|
||||||
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
|
@ -246,9 +246,15 @@ Column* Column::uda(const std::string& name) {
|
||||||
c->_label = label;
|
c->_label = label;
|
||||||
if (values != "") c->_values = split(values, ',');
|
if (values != "") c->_values = split(values, ',');
|
||||||
return c;
|
return c;
|
||||||
|
} else if (type == "uuid") {
|
||||||
|
auto c = new ColumnUDAUUID();
|
||||||
|
c->_name = name;
|
||||||
|
c->_label = label;
|
||||||
|
return c;
|
||||||
} else if (type != "")
|
} else if (type != "")
|
||||||
throw std::string(
|
throw std::string(
|
||||||
"User defined attributes may only be of type 'string', 'date', 'duration' or 'numeric'.");
|
"User defined attributes may only be of type 'string', 'uuid', date', 'duration' or "
|
||||||
|
"'numeric'.");
|
||||||
|
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
|
@ -218,8 +218,8 @@ void CmdContext::defineContext(const std::vector<std::string>& words, std::strin
|
||||||
if (!valid_write_context) {
|
if (!valid_write_context) {
|
||||||
std::stringstream warning;
|
std::stringstream warning;
|
||||||
warning
|
warning
|
||||||
<< format("The filter '{1}' is not a valid modification string, because it contains {2}.",
|
<< format("The filter '{1}' is not a valid modification string, because it {2}.", value,
|
||||||
value, reason)
|
reason)
|
||||||
<< "\nAs such, value for the write context cannot be set (context will not apply on task "
|
<< "\nAs such, value for the write context cannot be set (context will not apply on task "
|
||||||
"add / task log).\n\n"
|
"add / task log).\n\n"
|
||||||
<< format(
|
<< format(
|
||||||
|
|
|
@ -317,7 +317,6 @@ void CmdEdit::parseTask(Task& task, const std::string& after, const std::string&
|
||||||
|
|
||||||
// tags
|
// tags
|
||||||
value = findValue(after, "\n Tags:");
|
value = findValue(after, "\n Tags:");
|
||||||
task.remove("tags");
|
|
||||||
task.setTags(split(value, ' '));
|
task.setTags(split(value, ' '));
|
||||||
|
|
||||||
// description.
|
// description.
|
||||||
|
@ -619,10 +618,9 @@ CmdEdit::editResult CmdEdit::editFile(Task& task) {
|
||||||
auto dateformat = Context::getContext().config.get("dateformat.edit");
|
auto dateformat = Context::getContext().config.get("dateformat.edit");
|
||||||
if (dateformat == "") dateformat = Context::getContext().config.get("dateformat");
|
if (dateformat == "") dateformat = Context::getContext().config.get("dateformat");
|
||||||
|
|
||||||
// Change directory for the editor
|
// Change directory for the editor, doing nothing on error.
|
||||||
auto current_dir = Directory::cwd();
|
auto current_dir = Directory::cwd();
|
||||||
int ignored = chdir(location._data.c_str());
|
chdir(location._data.c_str());
|
||||||
++ignored; // Keep compiler quiet.
|
|
||||||
|
|
||||||
// Check if the file already exists, if so, bail out
|
// Check if the file already exists, if so, bail out
|
||||||
Path filepath = Path(file.str());
|
Path filepath = Path(file.str());
|
||||||
|
@ -702,7 +700,7 @@ ARE_THESE_REALLY_HARMFUL:
|
||||||
|
|
||||||
// Cleanup.
|
// Cleanup.
|
||||||
File::remove(file.str());
|
File::remove(file.str());
|
||||||
ignored = chdir(current_dir.c_str());
|
chdir(current_dir.c_str());
|
||||||
return changes ? CmdEdit::editResult::changes : CmdEdit::editResult::nochanges;
|
return changes ? CmdEdit::editResult::changes : CmdEdit::editResult::nochanges;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -588,6 +588,11 @@ int CmdNews::execute(std::string& output) {
|
||||||
}
|
}
|
||||||
wait_for_enter();
|
wait_for_enter();
|
||||||
|
|
||||||
|
// Set a mark in the config to remember which version's release notes were displayed
|
||||||
|
if (news_version < current_version) {
|
||||||
|
CmdConfig::setConfigVariable("news.version", std::string(current_version), false);
|
||||||
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -36,6 +36,7 @@
|
||||||
#include <taskchampion-cpp/lib.h>
|
#include <taskchampion-cpp/lib.h>
|
||||||
#include <util.h>
|
#include <util.h>
|
||||||
|
|
||||||
|
#include <regex>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -78,6 +79,12 @@ int CmdSync::execute(std::string& output) {
|
||||||
out << "sync.server.origin is deprecated. Use sync.server.url instead.\n";
|
out << "sync.server.origin is deprecated. Use sync.server.url instead.\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// redact credentials from `server_url`, if present
|
||||||
|
std::regex remove_creds_regex("^(https?://.+):(.+)@(.+)");
|
||||||
|
std::string safe_server_url = std::regex_replace(server_url, remove_creds_regex, "$1:****@$3");
|
||||||
|
|
||||||
|
auto num_local_operations = replica->num_local_operations();
|
||||||
|
|
||||||
if (server_dir != "") {
|
if (server_dir != "") {
|
||||||
if (verbose) {
|
if (verbose) {
|
||||||
out << format("Syncing with {1}", server_dir) << '\n';
|
out << format("Syncing with {1}", server_dir) << '\n';
|
||||||
|
@ -130,6 +137,7 @@ int CmdSync::execute(std::string& output) {
|
||||||
replica->sync_to_aws_with_default_creds(aws_region, aws_bucket, encryption_secret,
|
replica->sync_to_aws_with_default_creds(aws_region, aws_bucket, encryption_secret,
|
||||||
avoid_snapshots);
|
avoid_snapshots);
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (gcp_bucket != "") {
|
} else if (gcp_bucket != "") {
|
||||||
std::string gcp_credential_path = Context::getContext().config.get("sync.gcp.credential_path");
|
std::string gcp_credential_path = Context::getContext().config.get("sync.gcp.credential_path");
|
||||||
if (encryption_secret == "") {
|
if (encryption_secret == "") {
|
||||||
|
@ -139,15 +147,17 @@ int CmdSync::execute(std::string& output) {
|
||||||
out << format("Syncing with GCP bucket {1}", gcp_bucket) << '\n';
|
out << format("Syncing with GCP bucket {1}", gcp_bucket) << '\n';
|
||||||
}
|
}
|
||||||
replica->sync_to_gcp(gcp_bucket, gcp_credential_path, encryption_secret, avoid_snapshots);
|
replica->sync_to_gcp(gcp_bucket, gcp_credential_path, encryption_secret, avoid_snapshots);
|
||||||
|
|
||||||
} else if (server_url != "") {
|
} else if (server_url != "") {
|
||||||
if (client_id == "" || encryption_secret == "") {
|
if (client_id == "" || encryption_secret == "") {
|
||||||
throw std::string("sync.server.client_id and sync.encryption_secret are required");
|
throw std::string("sync.server.client_id and sync.encryption_secret are required");
|
||||||
}
|
}
|
||||||
if (verbose) {
|
if (verbose) {
|
||||||
out << format("Syncing with sync server at {1}", server_url) << '\n';
|
out << format("Syncing with sync server at {1}", safe_server_url) << '\n';
|
||||||
}
|
}
|
||||||
replica->sync_to_remote(server_url, tc::uuid_from_string(client_id), encryption_secret,
|
replica->sync_to_remote(server_url, tc::uuid_from_string(client_id), encryption_secret,
|
||||||
avoid_snapshots);
|
avoid_snapshots);
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
throw std::string("No sync.* settings are configured. See task-sync(5).");
|
throw std::string("No sync.* settings are configured. See task-sync(5).");
|
||||||
}
|
}
|
||||||
|
@ -156,6 +166,15 @@ int CmdSync::execute(std::string& output) {
|
||||||
context.tdb2.expire_tasks();
|
context.tdb2.expire_tasks();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (verbose) {
|
||||||
|
out << "Success!\n";
|
||||||
|
// Taskchampion does not provide a measure of the number of operations received from
|
||||||
|
// the server, but we can give some indication of the number sent.
|
||||||
|
if (num_local_operations) {
|
||||||
|
out << format("Sent {1} local operations to the server", num_local_operations) << '\n';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
output = out.str();
|
output = out.str();
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
|
@ -102,7 +102,7 @@ bool CmdUndo::confirm_revert(const std::vector<Operation>& undo_ops) {
|
||||||
view.set(row, 1, mods.str());
|
view.set(row, 1, mods.str());
|
||||||
}
|
}
|
||||||
last_uuid = op.get_uuid();
|
last_uuid = op.get_uuid();
|
||||||
mods.clear();
|
mods = std::stringstream();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (op.is_create()) {
|
if (op.is_create()) {
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 1a06cb4caebdae3c5e58fe83e2fd2211d2959815
|
Subproject commit 121f757c3ec1b1f548f7835208b8c72d85d141a7
|
|
@ -1 +1 @@
|
||||||
Subproject commit fcd8b41981cb1e80f4dcc20fa8970dc6aa981c9f
|
Subproject commit 4eccadd67819b427978ca540e0c31e6cce08f226
|
18
src/util.cpp
18
src/util.cpp
|
@ -218,24 +218,6 @@ const std::vector<std::string> extractParents(const std::string& project,
|
||||||
return vec;
|
return vec;
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
#ifndef HAVE_TIMEGM
|
|
||||||
time_t timegm(struct tm* tm) {
|
|
||||||
time_t ret;
|
|
||||||
char* tz;
|
|
||||||
tz = getenv("TZ");
|
|
||||||
setenv("TZ", "UTC", 1);
|
|
||||||
tzset();
|
|
||||||
ret = mktime(tm);
|
|
||||||
if (tz)
|
|
||||||
setenv("TZ", tz, 1);
|
|
||||||
else
|
|
||||||
unsetenv("TZ");
|
|
||||||
tzset();
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
bool nontrivial(const std::string& input) {
|
bool nontrivial(const std::string& input) {
|
||||||
std::string::size_type i = 0;
|
std::string::size_type i = 0;
|
||||||
|
|
|
@ -54,10 +54,6 @@ const std::string indentProject(const std::string&, const std::string& whitespac
|
||||||
|
|
||||||
const std::vector<std::string> extractParents(const std::string&, const char& delimiter = '.');
|
const std::vector<std::string> extractParents(const std::string&, const char& delimiter = '.');
|
||||||
|
|
||||||
#ifndef HAVE_TIMEGM
|
|
||||||
time_t timegm(struct tm* tm);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
bool nontrivial(const std::string&);
|
bool nontrivial(const std::string&);
|
||||||
const char* optionalBlankLine();
|
const char* optionalBlankLine();
|
||||||
void setHeaderUnderline(Table&);
|
void setHeaderUnderline(Table&);
|
||||||
|
|
|
@ -109,29 +109,26 @@ class TestUUIDFormats(TestCase):
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
"""Executed once before any test in the class"""
|
"""Executed once before any test in the class"""
|
||||||
cls.t = Task()
|
cls.t = Task()
|
||||||
cls.t.config("report.xxx.columns", "id,uuid")
|
cls.t.config("report.xxx.columns", "uuid")
|
||||||
cls.t.config("verbose", "nothing")
|
cls.t.config("verbose", "nothing")
|
||||||
|
|
||||||
cls.t("add zero")
|
cls.t("add zero")
|
||||||
code, out, err = cls.t("_get 1.uuid")
|
code, out, err = cls.t("_get 1.uuid")
|
||||||
cls.uuid = out.strip()
|
cls.uuid = out.strip()
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
"""Executed before each test in the class"""
|
|
||||||
|
|
||||||
def test_uuid_long(self):
|
def test_uuid_long(self):
|
||||||
"""Verify formatting of 'uuid.long' column"""
|
"""Verify formatting of 'uuid.long' column"""
|
||||||
code, out, err = self.t("xxx rc.report.xxx.columns:id,uuid.long")
|
code, out, err = self.t("xxx rc.report.xxx.columns:uuid.long")
|
||||||
self.assertIn(self.uuid, out)
|
self.assertEqual(self.uuid, out.strip())
|
||||||
|
|
||||||
def test_uuid_short(self):
|
def test_uuid_short(self):
|
||||||
"""Verify formatting of 'uuid.short' column"""
|
"""Verify formatting of 'uuid.short' column"""
|
||||||
code, out, err = self.t("xxx rc.report.xxx.columns:id,uuid.short")
|
code, out, err = self.t("xxx rc.report.xxx.columns:uuid.short")
|
||||||
self.assertIn(self.uuid[:7], out)
|
self.assertEqual(self.uuid[:8], out.strip())
|
||||||
|
|
||||||
def test_uuid_format_unrecognized(self):
|
def test_uuid_format_unrecognized(self):
|
||||||
"""Verify uuid.donkey formatting fails"""
|
"""Verify uuid.donkey formatting fails"""
|
||||||
code, out, err = self.t.runError("xxx rc.report.xxx.columns:id,uuid.donkey")
|
code, out, err = self.t.runError("xxx rc.report.xxx.columns:uuid.donkey")
|
||||||
self.assertEqual(err, "Unrecognized column format 'uuid.donkey'\n")
|
self.assertEqual(err, "Unrecognized column format 'uuid.donkey'\n")
|
||||||
|
|
||||||
|
|
||||||
|
@ -482,6 +479,70 @@ start active* ✓
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class TestUDAUUIDFormats(TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
"""Executed once before any test in the class"""
|
||||||
|
cls.t = Task()
|
||||||
|
cls.t.config("verbose", "nothing")
|
||||||
|
cls.t.config("uda.uda_uuid.label", "uda_uuid")
|
||||||
|
cls.t.config("uda.uda_uuid.type", "uuid")
|
||||||
|
cls.t.config("report.xxx.columns", "uda_uuid")
|
||||||
|
|
||||||
|
cls.t("add zero")
|
||||||
|
code, out, err = cls.t("_get 1.uuid")
|
||||||
|
cls.t("add uda_uuid:{} one".format(out.strip()))
|
||||||
|
code, out, err = cls.t("_get 2.uda_uuid")
|
||||||
|
cls.uda_uuid = out.strip()
|
||||||
|
|
||||||
|
def test_uda_uuid_invalid_fails(self):
|
||||||
|
"""Verify adding invalid uuid fails"""
|
||||||
|
code, out, err = self.t.runError("add uda_uuid:shrek three")
|
||||||
|
self.assertNotEqual(code, 0)
|
||||||
|
self.assertIn("uda_uuid", err.strip())
|
||||||
|
self.assertIn("shrek", err.strip())
|
||||||
|
|
||||||
|
def test_uda_uuid_long(self):
|
||||||
|
"""Verify formatting of 'uda_uuid.long' column"""
|
||||||
|
code, out, err = self.t("2 xxx rc.report.xxx.columns:uda_uuid.long")
|
||||||
|
self.assertEqual(self.uda_uuid, out.strip())
|
||||||
|
|
||||||
|
def test_uda_uuid_short(self):
|
||||||
|
"""Verify formatting of 'uda_uuid.short' column"""
|
||||||
|
code, out, err = self.t("2 xxx rc.report.xxx.columns:uda_uuid.short")
|
||||||
|
self.assertEqual(self.uda_uuid[:8], out.strip())
|
||||||
|
|
||||||
|
def test_uda_uuid_format_unrecognized(self):
|
||||||
|
"""Verify uda_uuid.donkey formatting fails"""
|
||||||
|
code, out, err = self.t.runError("xxx rc.report.xxx.columns:id,uda_uuid.donkey")
|
||||||
|
self.assertEqual(err, "Unrecognized column format 'uda_uuid.donkey'\n")
|
||||||
|
|
||||||
|
|
||||||
|
class TestUDAUUIDReconfiguredFromString(TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
"""Executed once before any test in the class"""
|
||||||
|
cls.t = Task()
|
||||||
|
cls.t.config("verbose", "nothing")
|
||||||
|
cls.t.config("uda.uda_uuid.label", "uda_uuid")
|
||||||
|
cls.t.config("report.xxx.columns", "uda_uuid")
|
||||||
|
|
||||||
|
cls.t.config("uda.uda_uuid.type", "string")
|
||||||
|
cls.expected_str = 3 * "littlepigs"
|
||||||
|
cls.t("add uda_uuid:{} one".format(cls.expected_str))
|
||||||
|
cls.t.config("uda.uda_uuid.type", "uuid")
|
||||||
|
|
||||||
|
def test_uda_uuid_long(self):
|
||||||
|
"""Verify formatting of 'uda_uuid.long' column"""
|
||||||
|
code, out, err = self.t("1 xxx rc.report.xxx.columns:uda_uuid.long")
|
||||||
|
self.assertEqual(self.expected_str, out.strip())
|
||||||
|
|
||||||
|
def test_uda_uuid_short(self):
|
||||||
|
"""Verify formatting of 'uda_uuid.short' column"""
|
||||||
|
code, out, err = self.t("1 xxx rc.report.xxx.columns:uda_uuid.short")
|
||||||
|
self.assertEqual(self.expected_str[:8], out.strip())
|
||||||
|
|
||||||
|
|
||||||
class TestFeature1061(TestCase):
|
class TestFeature1061(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""Executed before each test in the class"""
|
"""Executed before each test in the class"""
|
||||||
|
|
|
@ -293,7 +293,10 @@ int TEST_NAME(int, char**) {
|
||||||
"'wonder'+0 : 'prowonderbread'+3 --> 6");
|
"'wonder'+0 : 'prowonderbread'+3 --> 6");
|
||||||
|
|
||||||
// Test all Lexer types.
|
// Test all Lexer types.
|
||||||
#define NO {"", Lexer::Type::word}
|
#define NO \
|
||||||
|
{ \
|
||||||
|
"", Lexer::Type::word \
|
||||||
|
}
|
||||||
struct {
|
struct {
|
||||||
const char* input;
|
const char* input;
|
||||||
struct {
|
struct {
|
||||||
|
|
|
@ -67,21 +67,23 @@ class TestBug268(TestCase):
|
||||||
self.assertIn("a/b or c", out)
|
self.assertIn("a/b or c", out)
|
||||||
|
|
||||||
|
|
||||||
class TestBug880(TestCase):
|
class TestBug3858(TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
"""Executed before each test in the class"""
|
"""Executed before each test in the class"""
|
||||||
self.t = Task()
|
self.t = Task()
|
||||||
|
|
||||||
def test_backslash_at_eol(self):
|
def test_backslash_at_eol(self):
|
||||||
"""880: Backslash at end of description/annotation causes problems"""
|
"""880: Backslashes at end of description/annotation are handled correctly"""
|
||||||
self.t(r"add one\\")
|
self.t(r"add one\\")
|
||||||
code, out, err = self.t("_get 1.description")
|
code, out, err = self.t("_get 1.description")
|
||||||
self.assertEqual("one\\\n", out)
|
self.assertEqual("one\\\n", out)
|
||||||
|
|
||||||
self.t(r"1 annotate 'two\\'")
|
self.t(r"1 annotate 'two\'")
|
||||||
|
self.t(r"1 annotate 'three\\'")
|
||||||
code, out, err = self.t("info rc.verbose:nothing")
|
code, out, err = self.t("info rc.verbose:nothing")
|
||||||
self.assertIn("one\\\n", out)
|
self.assertIn("one\\\n", out)
|
||||||
self.assertIn("two\\\n", out)
|
self.assertIn("two\\\n", out)
|
||||||
|
self.assertIn("three\\\\\n", out)
|
||||||
|
|
||||||
|
|
||||||
class TestBug1436(TestCase):
|
class TestBug1436(TestCase):
|
||||||
|
|
|
@ -40,6 +40,7 @@ class TestTaskrc(TestCase):
|
||||||
"""Executed before each test in the class"""
|
"""Executed before each test in the class"""
|
||||||
self.t = Task()
|
self.t = Task()
|
||||||
|
|
||||||
|
@unittest.skip("taskrc generation requires a tty - see #3751")
|
||||||
def test_default_taskrc(self):
|
def test_default_taskrc(self):
|
||||||
"""Verify that a default .taskrc is generated"""
|
"""Verify that a default .taskrc is generated"""
|
||||||
os.remove(self.t.taskrc)
|
os.remove(self.t.taskrc)
|
||||||
|
|
|
@ -27,6 +27,7 @@
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
# Ensure python finds the local simpletap module
|
# Ensure python finds the local simpletap module
|
||||||
|
@ -61,6 +62,25 @@ class TestUndo(TestCase):
|
||||||
code, out, err = self.t("_get 1.status")
|
code, out, err = self.t("_get 1.status")
|
||||||
self.assertEqual(out.strip(), "pending")
|
self.assertEqual(out.strip(), "pending")
|
||||||
|
|
||||||
|
def test_modify_multiple_tasks(self):
|
||||||
|
"""'add' then 'done' then 'undo'"""
|
||||||
|
self.t("add one")
|
||||||
|
self.t("add two")
|
||||||
|
self.t("add three")
|
||||||
|
self.t("rc.bulk=0 1,2,3 modify +sometag")
|
||||||
|
code, out, err = self.t("undo", input="y\n")
|
||||||
|
# This undo output should show one tag modification for each task, possibly with some
|
||||||
|
# modification-time updates if the modifications spanned a second boundary.
|
||||||
|
self.assertRegex(
|
||||||
|
out,
|
||||||
|
"\s+".join(
|
||||||
|
[
|
||||||
|
r"""[0-9a-f-]{36} (Update property 'modified' from\s+'[0-9]+' to '[0-9]+'\s+)?Add tag 'sometag'\s+Add property 'tags' with value 'sometag'"""
|
||||||
|
]
|
||||||
|
* 3
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
def test_undo_en_passant(self):
|
def test_undo_en_passant(self):
|
||||||
"""Verify that en-passant changes during undo are an error"""
|
"""Verify that en-passant changes during undo are an error"""
|
||||||
self.t("add one")
|
self.t("add one")
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue