mirror of
https://github.com/GothenburgBitFactory/taskwarrior.git
synced 2025-06-26 10:54:26 +02:00
Restore support for task info
journal (#3671)
This support was removed before Taskwarrior-3.x, and is now restored,
including the original tests removed in
ddd367232e
This commit is contained in:
parent
7da23aee1c
commit
c9967c20e2
13 changed files with 438 additions and 101 deletions
|
@ -13,6 +13,7 @@ add_library (task STATIC CLI2.cpp CLI2.h
|
|||
Filter.cpp Filter.h
|
||||
Hooks.cpp Hooks.h
|
||||
Lexer.cpp Lexer.h
|
||||
Operation.cpp Operation.h
|
||||
TDB2.cpp TDB2.h
|
||||
Task.cpp Task.h
|
||||
Variant.cpp Variant.h
|
||||
|
|
73
src/Operation.cpp
Normal file
73
src/Operation.cpp
Normal file
|
@ -0,0 +1,73 @@
|
|||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Copyright 2006 - 2024, 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 <cmake.h>
|
||||
// cmake.h include header must come first
|
||||
|
||||
#include <Operation.h>
|
||||
#include <taskchampion-cpp/lib.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
Operation::Operation(const tc::Operation& op) : op(&op) {}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
std::vector<Operation> Operation::operations(const rust::Vec<tc::Operation>& operations) {
|
||||
return {operations.begin(), operations.end()};
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
Operation& Operation::operator=(const Operation& other) {
|
||||
op = other.op;
|
||||
return *this;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
bool Operation::operator<(Operation& other) const {
|
||||
if (is_create()) {
|
||||
return !other.is_create();
|
||||
} else if (is_update()) {
|
||||
if (other.is_create()) {
|
||||
return false;
|
||||
} else if (other.is_update()) {
|
||||
return get_timestamp() < other.get_timestamp();
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
} else if (is_delete()) {
|
||||
if (other.is_create() || other.is_update() || other.is_delete()) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
} else if (is_undo_point()) {
|
||||
return !other.is_undo_point();
|
||||
}
|
||||
return false; // not reachable
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
88
src/Operation.h
Normal file
88
src/Operation.h
Normal file
|
@ -0,0 +1,88 @@
|
|||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Copyright 2006 - 2024, 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
|
||||
//
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#ifndef INCLUDED_OPERATIOn
|
||||
#define INCLUDED_OPERATIOn
|
||||
|
||||
#include <taskchampion-cpp/lib.h>
|
||||
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
// Representation of a TaskChampion operation.
|
||||
//
|
||||
// This class wraps `tc::Operation&` and thus cannot outlive that underlying
|
||||
// type.
|
||||
class Operation {
|
||||
public:
|
||||
explicit Operation(const tc::Operation &);
|
||||
|
||||
Operation(const Operation &other) = default;
|
||||
Operation &operator=(const Operation &other);
|
||||
|
||||
// Create a vector of Operations given the result of `Replica::get_undo_operations` or
|
||||
// `Replica::get_task_operations`. The resulting vector must not outlive the input `rust::Vec`.
|
||||
static std::vector<Operation> operations(const rust::Vec<tc::Operation> &);
|
||||
|
||||
// Methods from the underlying `tc::Operation`.
|
||||
bool is_create() const { return op->is_create(); }
|
||||
bool is_update() const { return op->is_update(); }
|
||||
bool is_delete() const { return op->is_delete(); }
|
||||
bool is_undo_point() const { return op->is_undo_point(); }
|
||||
std::string get_uuid() const { return std::string(op->get_uuid().to_string()); }
|
||||
::rust::Vec<::tc::PropValuePair> get_old_task() const { return op->get_old_task(); };
|
||||
std::string get_property() const {
|
||||
std::string value;
|
||||
op->get_property(value);
|
||||
return value;
|
||||
}
|
||||
std::optional<std::string> get_value() const {
|
||||
std::optional<std::string> value{std::string()};
|
||||
if (!op->get_value(value.value())) {
|
||||
value = std::nullopt;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
std::optional<std::string> get_old_value() const {
|
||||
std::optional<std::string> old_value{std::string()};
|
||||
if (!op->get_old_value(old_value.value())) {
|
||||
old_value = std::nullopt;
|
||||
}
|
||||
return old_value;
|
||||
}
|
||||
time_t get_timestamp() const { return static_cast<time_t>(op->get_timestamp()); }
|
||||
|
||||
// Define a partial order on Operations:
|
||||
// - Create < Update < Delete < UndoPoint
|
||||
// - Given two updates, sort by timestamp
|
||||
bool operator<(Operation &other) const;
|
||||
|
||||
private:
|
||||
const tc::Operation *op;
|
||||
};
|
||||
|
||||
#endif
|
||||
////////////////////////////////////////////////////////////////////////////////
|
|
@ -244,8 +244,7 @@ void TDB2::revert() {
|
|||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
bool TDB2::confirm_revert(rust::Vec<tc::Operation>& undo_ops) {
|
||||
// TODO Use show_diff rather than this basic listing of operations, though
|
||||
// this might be a worthy undo.style itself.
|
||||
// TODO: convert to Operation and use that type for display, similar to CmdInfo.
|
||||
|
||||
// Count non-undo operations
|
||||
int ops_count = 0;
|
||||
|
|
94
src/Task.cpp
94
src/Task.cpp
|
@ -1243,14 +1243,14 @@ void Task::fixTagsAttribute() {
|
|||
bool Task::isTagAttr(const std::string& attr) { return attr.compare(0, 4, "tag_") == 0; }
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
const std::string Task::tag2Attr(const std::string& tag) const {
|
||||
std::string Task::tag2Attr(const std::string& tag) {
|
||||
std::stringstream tag_attr;
|
||||
tag_attr << "tag_" << tag;
|
||||
return tag_attr.str();
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
const std::string Task::attr2Tag(const std::string& attr) const {
|
||||
std::string Task::attr2Tag(const std::string& attr) {
|
||||
assert(isTagAttr(attr));
|
||||
return attr.substr(4);
|
||||
}
|
||||
|
@ -1271,14 +1271,14 @@ void Task::fixDependsAttribute() {
|
|||
bool Task::isDepAttr(const std::string& attr) { return attr.compare(0, 4, "dep_") == 0; }
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
const std::string Task::dep2Attr(const std::string& tag) const {
|
||||
std::string Task::dep2Attr(const std::string& tag) {
|
||||
std::stringstream tag_attr;
|
||||
tag_attr << "dep_" << tag;
|
||||
return tag_attr.str();
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
const std::string Task::attr2Dep(const std::string& attr) const {
|
||||
std::string Task::attr2Dep(const std::string& attr) {
|
||||
assert(isDepAttr(attr));
|
||||
return attr.substr(4);
|
||||
}
|
||||
|
@ -2151,92 +2151,6 @@ std::string Task::diff(const Task& after) const {
|
|||
return out.str();
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Similar to diff, but formatted for inclusion in the output of the info command
|
||||
std::string Task::diffForInfo(const Task& after, const std::string& dateformat,
|
||||
long& last_timestamp, const long current_timestamp) const {
|
||||
// Attributes are all there is, so figure the different attribute names
|
||||
// between before and after.
|
||||
std::vector<std::string> beforeAtts;
|
||||
for (auto& att : 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);
|
||||
|
||||
// Now start generating a description of the differences.
|
||||
std::stringstream out;
|
||||
for (auto& name : beforeOnly) {
|
||||
if (isAnnotationAttr(name)) {
|
||||
out << format("Annotation '{1}' deleted.\n", get(name));
|
||||
} else if (isTagAttr(name)) {
|
||||
out << format("Tag '{1}' deleted.\n", attr2Tag(name));
|
||||
} else if (isDepAttr(name)) {
|
||||
out << format("Dependency on '{1}' deleted.\n", attr2Dep(name));
|
||||
} else if (name == "depends" || name == "tags") {
|
||||
// do nothing for legacy attributes
|
||||
} else if (name == "start") {
|
||||
Datetime started(get("start"));
|
||||
Datetime stopped;
|
||||
|
||||
if (after.has("end"))
|
||||
// Task was marked as finished, use end time
|
||||
stopped = Datetime(after.get("end"));
|
||||
else
|
||||
// Start attribute was removed, use modification time
|
||||
stopped = Datetime(current_timestamp);
|
||||
|
||||
out << format("{1} deleted (duration: {2}).", Lexer::ucFirst(name),
|
||||
Duration(stopped - started).format())
|
||||
<< "\n";
|
||||
} else {
|
||||
out << format("{1} deleted.\n", Lexer::ucFirst(name));
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& name : afterOnly) {
|
||||
if (isAnnotationAttr(name)) {
|
||||
out << format("Annotation of '{1}' added.\n", after.get(name));
|
||||
} else if (isTagAttr(name)) {
|
||||
out << format("Tag '{1}' added.\n", attr2Tag(name));
|
||||
} else if (isDepAttr(name)) {
|
||||
out << format("Dependency on '{1}' added.\n", attr2Dep(name));
|
||||
} else if (name == "depends" || name == "tags") {
|
||||
// do nothing for legacy attributes
|
||||
} else {
|
||||
if (name == "start") last_timestamp = current_timestamp;
|
||||
|
||||
out << format("{1} set to '{2}'.", Lexer::ucFirst(name),
|
||||
renderAttribute(name, after.get(name), dateformat))
|
||||
<< "\n";
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& name : beforeAtts)
|
||||
if (name != "uuid" && name != "modified" && get(name) != after.get(name) && get(name) != "" &&
|
||||
after.get(name) != "") {
|
||||
if (name == "depends" || name == "tags") {
|
||||
// do nothing for legacy attributes
|
||||
} else if (isTagAttr(name) || isDepAttr(name)) {
|
||||
// ignore new attributes
|
||||
} else if (isAnnotationAttr(name)) {
|
||||
out << format("Annotation changed to '{1}'.\n", after.get(name));
|
||||
} else
|
||||
out << format("{1} changed from '{2}' to '{3}'.", Lexer::ucFirst(name),
|
||||
renderAttribute(name, get(name), dateformat),
|
||||
renderAttribute(name, after.get(name), dateformat))
|
||||
<< "\n";
|
||||
}
|
||||
|
||||
// Shouldn't just say nothing.
|
||||
if (out.str().length() == 0) out << "No changes made.\n";
|
||||
|
||||
return out.str();
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Similar to diff, but formatted as a side-by-side table for an Undo preview
|
||||
Table Task::diffForUndoSide(const Task& after) const {
|
||||
|
|
11
src/Task.h
11
src/Task.h
|
@ -164,6 +164,11 @@ class Task {
|
|||
void substitute(const std::string&, const std::string&, const std::string&);
|
||||
#endif
|
||||
|
||||
static std::string tag2Attr(const std::string&);
|
||||
static std::string attr2Tag(const std::string&);
|
||||
static std::string dep2Attr(const std::string&);
|
||||
static std::string attr2Dep(const std::string&);
|
||||
|
||||
void validate_add();
|
||||
void validate(bool applyDefault = true);
|
||||
|
||||
|
@ -176,8 +181,6 @@ class Task {
|
|||
#endif
|
||||
|
||||
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;
|
||||
|
||||
|
@ -190,10 +193,6 @@ class Task {
|
|||
void validate_before(const std::string&, const std::string&);
|
||||
const std::string encode(const std::string&) const;
|
||||
const std::string decode(const std::string&) const;
|
||||
const std::string tag2Attr(const std::string&) const;
|
||||
const std::string attr2Tag(const std::string&) const;
|
||||
const std::string dep2Attr(const std::string&) const;
|
||||
const std::string attr2Dep(const std::string&) const;
|
||||
void fixDependsAttribute();
|
||||
void fixTagsAttribute();
|
||||
|
||||
|
|
|
@ -33,13 +33,16 @@
|
|||
#include <Duration.h>
|
||||
#include <Filter.h>
|
||||
#include <Lexer.h>
|
||||
#include <Operation.h>
|
||||
#include <format.h>
|
||||
#include <main.h>
|
||||
#include <math.h>
|
||||
#include <shared.h>
|
||||
#include <stdlib.h>
|
||||
#include <taskchampion-cpp/lib.h>
|
||||
#include <util.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <sstream>
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
@ -477,9 +480,68 @@ int CmdInfo::execute(std::string& output) {
|
|||
urgencyDetails.set(row, 5, rightJustify(format(task.urgency(), 4, 4), 6));
|
||||
}
|
||||
|
||||
// Create a third table, containing undo log change details.
|
||||
Table journal;
|
||||
setHeaderUnderline(journal);
|
||||
|
||||
if (Context::getContext().config.getBoolean("obfuscate")) journal.obfuscate();
|
||||
if (Context::getContext().config.getBoolean("color")) journal.forceColor();
|
||||
|
||||
journal.width(Context::getContext().getWidth());
|
||||
journal.add("Date");
|
||||
journal.add("Modification");
|
||||
|
||||
if (Context::getContext().config.getBoolean("journal.info")) {
|
||||
auto& replica = Context::getContext().tdb2.replica();
|
||||
tc::Uuid tcuuid = tc::uuid_from_string(uuid);
|
||||
auto tcoperations = replica->get_task_operations(tcuuid);
|
||||
auto operations = Operation::operations(tcoperations);
|
||||
|
||||
// Sort by type (Create < Update < Delete < UndoPoint) and then by timestamp.
|
||||
std::sort(operations.begin(), operations.end());
|
||||
|
||||
long last_timestamp = 0;
|
||||
for (size_t i = 0; i < operations.size(); i++) {
|
||||
auto& op = operations[i];
|
||||
|
||||
// Only display updates -- creation and deletion aren't interesting.
|
||||
if (!op.is_update()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Group operations that occur within 1s of this one. This is a heuristic
|
||||
// for operations performed in the same `task` invocation, and allows e.g.,
|
||||
// `task done end:-2h` to take the updated `end` value into account. It also
|
||||
// groups these events into a single "row" of the table for better layout.
|
||||
size_t group_start = i;
|
||||
for (i++; i < operations.size(); i++) {
|
||||
auto& op2 = operations[i];
|
||||
if (!op2.is_update() || op2.get_timestamp() - op.get_timestamp() > 1) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
size_t group_end = i;
|
||||
i--;
|
||||
|
||||
std::optional<std::string> msg =
|
||||
formatForInfo(operations, group_start, group_end, dateformat, last_timestamp);
|
||||
|
||||
if (!msg) {
|
||||
continue;
|
||||
}
|
||||
|
||||
int row = journal.addRow();
|
||||
Datetime timestamp(op.get_timestamp());
|
||||
journal.set(row, 0, timestamp.toString(dateformat));
|
||||
journal.set(row, 1, *msg);
|
||||
}
|
||||
}
|
||||
|
||||
out << optionalBlankLine() << view.render() << '\n';
|
||||
|
||||
if (urgencyDetails.rows() > 0) out << urgencyDetails.render() << '\n';
|
||||
|
||||
if (journal.rows() > 0) out << journal.render() << '\n';
|
||||
}
|
||||
|
||||
output = out.str();
|
||||
|
@ -502,3 +564,105 @@ void CmdInfo::urgencyTerm(Table& view, const std::string& label, float measure,
|
|||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
std::optional<std::string> CmdInfo::formatForInfo(const std::vector<Operation>& operations,
|
||||
size_t group_start, size_t group_end,
|
||||
const std::string& dateformat, long& last_start) {
|
||||
std::stringstream out;
|
||||
for (auto i = group_start; i < group_end; i++) {
|
||||
auto& operation = operations[i];
|
||||
assert(operation.is_update());
|
||||
|
||||
// Extract the parts of the Update operation.
|
||||
std::string prop = operation.get_property();
|
||||
std::optional<std::string> value = operation.get_value();
|
||||
std::optional<std::string> old_value = operation.get_old_value();
|
||||
Datetime timestamp(operation.get_timestamp());
|
||||
|
||||
// Never care about modifying the modification time, or the legacy properties `depends` and
|
||||
// `tags`.
|
||||
if (prop == "modified" || prop == "depends" || prop == "tags") {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle property deletions
|
||||
if (!value && old_value) {
|
||||
if (Task::isAnnotationAttr(prop)) {
|
||||
out << format("Annotation '{1}' deleted.\n", *old_value);
|
||||
} else if (Task::isTagAttr(prop)) {
|
||||
out << format("Tag '{1}' deleted.\n", Task::attr2Tag(prop));
|
||||
} else if (Task::isDepAttr(prop)) {
|
||||
out << format("Dependency on '{1}' deleted.\n", Task::attr2Dep(prop));
|
||||
} else if (prop == "start") {
|
||||
Datetime started(last_start);
|
||||
Datetime stopped = timestamp;
|
||||
|
||||
// If any update in this group sets the `end` property, use that instead of the
|
||||
// timestamp deleting the `start` property as the stop time.
|
||||
// See https://github.com/GothenburgBitFactory/taskwarrior/issues/2514
|
||||
for (auto i = group_start; i < group_end; i++) {
|
||||
auto& op = operations[i];
|
||||
assert(op.is_update());
|
||||
if (op.get_property() == "end") {
|
||||
try {
|
||||
stopped = op.get_value().value();
|
||||
} catch (std::string) {
|
||||
// Fall back to the 'start' timestamp if its value is un-parseable.
|
||||
stopped = op.get_timestamp();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
out << format("{1} deleted (duration: {2}).", Lexer::ucFirst(prop),
|
||||
Duration(stopped - started).format())
|
||||
<< "\n";
|
||||
} else {
|
||||
out << format("{1} deleted.\n", Lexer::ucFirst(prop));
|
||||
}
|
||||
}
|
||||
|
||||
// Handle property additions.
|
||||
if (value && !old_value) {
|
||||
if (Task::isAnnotationAttr(prop)) {
|
||||
out << format("Annotation of '{1}' added.\n", *value);
|
||||
} else if (Task::isTagAttr(prop)) {
|
||||
out << format("Tag '{1}' added.\n", Task::attr2Tag(prop));
|
||||
} else if (Task::isDepAttr(prop)) {
|
||||
out << format("Dependency on '{1}' added.\n", Task::attr2Dep(prop));
|
||||
} else {
|
||||
// Record the last start time for later duration calculation.
|
||||
if (prop == "start") {
|
||||
last_start = Datetime(value.value()).toEpoch();
|
||||
}
|
||||
|
||||
out << format("{1} set to '{2}'.", Lexer::ucFirst(prop),
|
||||
renderAttribute(prop, *value, dateformat))
|
||||
<< "\n";
|
||||
}
|
||||
}
|
||||
|
||||
// Handle property changes.
|
||||
if (value && old_value) {
|
||||
if (Task::isTagAttr(prop) || Task::isDepAttr(prop)) {
|
||||
// Dependencies and tags do not have meaningful values.
|
||||
} else if (Task::isAnnotationAttr(prop)) {
|
||||
out << format("Annotation changed to '{1}'.\n", *value);
|
||||
} else {
|
||||
// Record the last start time for later duration calculation.
|
||||
if (prop == "start") {
|
||||
last_start = Datetime(value.value()).toEpoch();
|
||||
}
|
||||
|
||||
out << format("{1} changed from '{2}' to '{3}'.", Lexer::ucFirst(prop),
|
||||
renderAttribute(prop, *old_value, dateformat),
|
||||
renderAttribute(prop, *value, dateformat))
|
||||
<< "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (out.str().length() == 0) return std::nullopt;
|
||||
|
||||
return out.str();
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
|
|
@ -28,9 +28,12 @@
|
|||
#define INCLUDED_CMDINFO
|
||||
|
||||
#include <Command.h>
|
||||
#include <Operation.h>
|
||||
#include <Table.h>
|
||||
#include <taskchampion-cpp/lib.h>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
class CmdInfo : public Command {
|
||||
public:
|
||||
|
@ -39,6 +42,10 @@ class CmdInfo : public Command {
|
|||
|
||||
private:
|
||||
void urgencyTerm(Table&, const std::string&, float, float) const;
|
||||
// Format a group of update operations for display in `task info`.
|
||||
std::optional<std::string> formatForInfo(const std::vector<Operation>& operations,
|
||||
size_t group_start, size_t group_end,
|
||||
const std::string& dateformat, long& last_start);
|
||||
};
|
||||
|
||||
#endif
|
||||
|
|
|
@ -133,6 +133,9 @@ mod ffi {
|
|||
/// Get an existing task by its UUID.
|
||||
fn get_task_data(&mut self, uuid: Uuid) -> Result<OptionTaskData>;
|
||||
|
||||
/// Get the operations for a task task by its UUID.
|
||||
fn get_task_operations(&mut self, uuid: Uuid) -> Result<Vec<Operation>>;
|
||||
|
||||
/// Return the operations back to and including the last undo point, or since the last sync if
|
||||
/// no undo point is found.
|
||||
fn get_undo_operations(&mut self) -> Result<Vec<Operation>>;
|
||||
|
@ -529,6 +532,10 @@ impl Replica {
|
|||
Ok(self.0.get_task_data(uuid.into())?.into())
|
||||
}
|
||||
|
||||
fn get_task_operations(&mut self, uuid: ffi::Uuid) -> Result<Vec<Operation>, CppError> {
|
||||
Ok(from_tc_operations(self.0.get_task_operations(uuid.into())?))
|
||||
}
|
||||
|
||||
fn get_undo_operations(&mut self) -> Result<Vec<Operation>, CppError> {
|
||||
Ok(from_tc_operations(self.0.get_undo_operations()?))
|
||||
}
|
||||
|
@ -921,6 +928,26 @@ mod test {
|
|||
assert_eq!(t.take().get_uuid(), uuid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_task_operations() {
|
||||
cxx::let_cxx_string!(prop = "prop");
|
||||
cxx::let_cxx_string!(value = "value");
|
||||
let mut rep = new_replica_in_memory().unwrap();
|
||||
|
||||
let uuid = uuid_v4();
|
||||
assert!(rep.get_task_operations(uuid).unwrap().is_empty());
|
||||
|
||||
let mut operations = new_operations();
|
||||
let mut t = create_task(uuid, &mut operations);
|
||||
t.update(&prop, &value, &mut operations);
|
||||
rep.commit_operations(operations).unwrap();
|
||||
|
||||
let ops = rep.get_task_operations(uuid).unwrap();
|
||||
assert_eq!(ops.len(), 2);
|
||||
assert!(ops[0].is_create());
|
||||
assert!(ops[1].is_update());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn task_properties() {
|
||||
cxx::let_cxx_string!(prop = "prop");
|
||||
|
|
|
@ -174,6 +174,7 @@ set (pythonTests
|
|||
timesheet.test.py
|
||||
tw-1379.test.py
|
||||
tw-1837.test.py
|
||||
tw-1999.test.py
|
||||
tw-20.test.py
|
||||
tw-2575.test.py
|
||||
tw-262.test.py
|
||||
|
|
|
@ -98,6 +98,9 @@ class TestInfoCommand(TestCase):
|
|||
self.assertRegex(out, r"Urgency\s+\d+(\.\d+)?")
|
||||
self.assertRegex(out, r"Priority\s+H")
|
||||
|
||||
self.assertRegex(out, r"Annotation of 'bar' added\.")
|
||||
self.assertRegex(out, r"Tag 'tag' added\.")
|
||||
self.assertRegex(out, r"tatus set to 'recurring'\.")
|
||||
self.assertIn("project", out)
|
||||
self.assertIn("active", out)
|
||||
self.assertIn("annotations", out)
|
||||
|
|
62
test/tw-1999.test.py
Executable file
62
test/tw-1999.test.py
Executable file
|
@ -0,0 +1,62 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
###############################################################################
|
||||
#
|
||||
# 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
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
import sys
|
||||
import os
|
||||
import unittest
|
||||
|
||||
# Ensure python finds the local simpletap module
|
||||
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from basetest import Task, TestCase
|
||||
|
||||
|
||||
class TestBug1999(TestCase):
|
||||
"""Bug 1999: Taskwarrior reports wrong active time"""
|
||||
|
||||
def setUp(self):
|
||||
self.t = Task()
|
||||
|
||||
def test_correct_active_time(self):
|
||||
"""Ensure correct active time locally"""
|
||||
desc = "Testing task"
|
||||
self.t(("add", desc))
|
||||
self.t(("start", "1"))
|
||||
self.t.faketime("+10m")
|
||||
self.t(("stop", "1"))
|
||||
|
||||
code, out, err = self.t(("info", "1"))
|
||||
self.assertRegex(out, "duration: 0:10:0[0-5]")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from simpletap import TAPTestRunner
|
||||
|
||||
unittest.main(testRunner=TAPTestRunner())
|
||||
|
||||
# vim: ai sts=4 et sw=4 ft=python
|
|
@ -6,7 +6,6 @@ task add Something I did yesterday
|
|||
task 1 mod start:yesterday+18h
|
||||
task 1 done end:yesterday+20h
|
||||
|
||||
# this does not work without journal.info
|
||||
# Check that 2 hour interval is reported by task info
|
||||
#task info | grep -F "Start deleted"
|
||||
#[[ ! -z `task info | grep -F "Start deleted (duration: 2:00:00)."` ]]
|
||||
task info | grep -F "Start deleted"
|
||||
[[ ! -z `task info | grep -F "Start deleted (duration: 2:00:00)."` ]]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue