Better undo output (and remove undo.style config) (#3672)

This commit is contained in:
Dustin J. Mitchell 2024-11-07 14:56:34 -05:00 committed by GitHub
parent dcc8a8cdde
commit a2f9b92d6c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 105 additions and 331 deletions

View file

@ -508,15 +508,6 @@ weekly recurring task is added with a due date of tomorrow, and recurrence.limit
is set to 2, then a report will list 2 pending recurring tasks, one for tomorrow, is set to 2, then a report will list 2 pending recurring tasks, one for tomorrow,
and one for a week from tomorrow. and one for a week from tomorrow.
.TP
.B undo.style=side
When the 'undo' command is run, Taskwarrior presents a before and after
comparison of the data. This can be in either the 'side' style, which compares
values side-by-side in a table, or 'diff' style, which uses a format similar to
the 'diff' command.
Currently not supported.
.TP .TP
.B abbreviation.minimum=2 .B abbreviation.minimum=2
Minimum length of any abbreviated command/value. This means that "ve", "ver", Minimum length of any abbreviated command/value. This means that "ve", "ver",

View file

@ -168,7 +168,6 @@ syn match taskrcGoodKey '^\s*\Vsugar='he=e-1
syn match taskrcGoodKey '^\s*\Vsync.\(server.\(url\|origin\|client_id\|encryption_secret\)\|local.server_dir\)='he=e-1 syn match taskrcGoodKey '^\s*\Vsync.\(server.\(url\|origin\|client_id\|encryption_secret\)\|local.server_dir\)='he=e-1
syn match taskrcGoodKey '^\s*\Vtag.indicator='he=e-1 syn match taskrcGoodKey '^\s*\Vtag.indicator='he=e-1
syn match taskrcGoodKey '^\s*\Vuda.\S\{-}.\(default\|type\|label\|values\|indicator\)='he=e-1 syn match taskrcGoodKey '^\s*\Vuda.\S\{-}.\(default\|type\|label\|values\|indicator\)='he=e-1
syn match taskrcGoodKey '^\s*\Vundo.style='he=e-1
syn match taskrcGoodKey '^\s*\Vurgency.active.coefficient='he=e-1 syn match taskrcGoodKey '^\s*\Vurgency.active.coefficient='he=e-1
syn match taskrcGoodKey '^\s*\Vurgency.age.coefficient='he=e-1 syn match taskrcGoodKey '^\s*\Vurgency.age.coefficient='he=e-1
syn match taskrcGoodKey '^\s*\Vurgency.age.max='he=e-1 syn match taskrcGoodKey '^\s*\Vurgency.age.max='he=e-1

View file

@ -127,7 +127,6 @@ std::string configurationDefaults =
"dependency.indicator=D # What to show as a dependency indicator\n" "dependency.indicator=D # What to show as a dependency indicator\n"
"recurrence.indicator=R # What to show as a task recurrence indicator\n" "recurrence.indicator=R # What to show as a task recurrence indicator\n"
"recurrence.limit=1 # Number of future recurring pending tasks\n" "recurrence.limit=1 # Number of future recurring pending tasks\n"
"undo.style=side # Undo style - can be 'side', or 'diff'\n"
"regex=1 # Assume all search/filter strings are " "regex=1 # Assume all search/filter strings are "
"regexes\n" "regexes\n"
"xterm.title=0 # Sets xterm title for some commands\n" "xterm.title=0 # Sets xterm title for some commands\n"

View file

@ -226,107 +226,6 @@ void TDB2::get_changes(std::vector<Task>& changes) {
[](const auto& kv) { return kv.second; }); [](const auto& kv) { return kv.second; });
} }
////////////////////////////////////////////////////////////////////////////////
void TDB2::revert() {
rust::Vec<tc::Operation> undo_ops = replica()->get_undo_operations();
if (undo_ops.size() == 0) {
std::cout << "No operations to undo.\n";
return;
}
if (confirm_revert(undo_ops)) {
// Note that commit_reversed_operations rebuilds the working set, so that
// need not be done here.
if (!replica()->commit_reversed_operations(std::move(undo_ops))) {
std::cout << "Could not undo: other operations have occurred.";
}
}
}
////////////////////////////////////////////////////////////////////////////////
bool TDB2::confirm_revert(rust::Vec<tc::Operation>& undo_ops) {
// TODO: convert to Operation and use that type for display, similar to CmdInfo.
// Count non-undo operations
int ops_count = 0;
for (auto& op : undo_ops) {
if (!op.is_undo_point()) {
ops_count++;
}
}
std::cout << "The following " << ops_count << " operations would be reverted:\n";
for (auto& op : undo_ops) {
if (op.is_undo_point()) {
continue;
}
std::cout << "- ";
std::string uuid = static_cast<std::string>(op.get_uuid().to_string());
if (op.is_create()) {
std::cout << "Create " << uuid;
} else if (op.is_delete()) {
std::cout << "Delete ";
auto old_task = op.get_old_task();
bool found_description = false;
for (auto& pv : old_task) {
if (static_cast<std::string>(pv.prop) == "description") {
std::cout << static_cast<std::string>(pv.value) << " (" << uuid << ")";
found_description = true;
}
}
if (!found_description) {
std::cout << uuid;
}
} else if (op.is_update()) {
std::cout << "Update " << uuid << "\n";
std::string property;
op.get_property(property);
std::string value;
bool have_value = op.get_value(value);
std::string old_value;
bool have_old_value = op.get_old_value(old_value);
std::cout << " " << property << ": ";
std::cout << (have_old_value ? old_value : "<empty>") << " -> ";
std::cout << (have_value ? value : "<empty>");
}
std::cout << "\n";
}
return !Context::getContext().config.getBoolean("confirmation") ||
confirm(
"The undo command is not reversible. Are you sure you want to revert to the previous "
"state?");
return true;
}
////////////////////////////////////////////////////////////////////////////////
void TDB2::show_diff(const std::string& current, const std::string& prior,
const std::string& when) {
Datetime lastChange(strtoll(when.c_str(), nullptr, 10));
// 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") : "");
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'
<< '\n'
<< view.render() << '\n';
}
else if (Context::getContext().config.get("undo.style") == "diff") {
Table view = before.diffForUndoPatch(after, lastChange);
std::cout << '\n' << view.render() << '\n';
}
}
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
void TDB2::gc() { void TDB2::gc() {
Timer timer; Timer timer;

View file

@ -51,7 +51,6 @@ class TDB2 {
void modify(Task &); void modify(Task &);
void purge(Task &); void purge(Task &);
void get_changes(std::vector<Task> &); void get_changes(std::vector<Task> &);
void revert();
void gc(); void gc();
void expire_tasks(); void expire_tasks();
int latest_id(); int latest_id();
@ -75,8 +74,6 @@ class TDB2 {
void dump(); void dump();
bool confirm_revert(rust::Vec<tc::Operation> &);
rust::Box<tc::Replica> &replica(); rust::Box<tc::Replica> &replica();
private: private:
@ -93,7 +90,6 @@ class TDB2 {
const rust::Box<tc::WorkingSet> &working_set(); const rust::Box<tc::WorkingSet> &working_set();
void maybe_add_undo_point(rust::Vec<tc::Operation> &); void maybe_add_undo_point(rust::Vec<tc::Operation> &);
static void show_diff(const std::string &, const std::string &, const std::string &);
}; };
#endif #endif

View file

@ -2152,188 +2152,3 @@ std::string Task::diff(const Task& after) const {
} }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
// 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

@ -181,8 +181,6 @@ class Task {
#endif #endif
std::string diff(const Task& after) const; std::string diff(const Task& after) const;
Table diffForUndoSide(const Task& after) const;
Table diffForUndoPatch(const Task& after, const Datetime& lastChange) const;
private: private:
int determineVersion(const std::string&); int determineVersion(const std::string&);

View file

@ -202,7 +202,6 @@ int CmdShow::execute(std::string& output) {
" sync.server.url" " sync.server.url"
" sync.server.origin" " sync.server.origin"
" tag.indicator" " tag.indicator"
" undo.style"
" urgency.active.coefficient" " urgency.active.coefficient"
" urgency.scheduled.coefficient" " urgency.scheduled.coefficient"
" urgency.annotations.coefficient" " urgency.annotations.coefficient"

View file

@ -29,6 +29,13 @@
#include <CmdUndo.h> #include <CmdUndo.h>
#include <Context.h> #include <Context.h>
#include <Operation.h>
#include <Task.h>
#include <iostream>
#include <sstream>
#include "shared.h"
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
CmdUndo::CmdUndo() { CmdUndo::CmdUndo() {
@ -47,8 +54,94 @@ CmdUndo::CmdUndo() {
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
int CmdUndo::execute(std::string&) { int CmdUndo::execute(std::string&) {
Context::getContext().tdb2.revert(); auto& replica = Context::getContext().tdb2.replica();
rust::Vec<tc::Operation> undo_ops = replica->get_undo_operations();
if (confirm_revert(Operation::operations(undo_ops))) {
// Note that commit_reversed_operations rebuilds the working set, so that
// need not be done here.
if (!replica->commit_reversed_operations(std::move(undo_ops))) {
std::cout << "Could not undo: other operations have occurred.";
}
}
return 0; return 0;
} }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
bool CmdUndo::confirm_revert(const std::vector<Operation>& undo_ops) {
// Count non-undo operations
int ops_count = 0;
for (auto& op : undo_ops) {
if (!op.is_undo_point()) {
ops_count++;
}
}
if (ops_count == 0) {
std::cout << "No operations to undo.\n";
return true;
}
std::cout << "The following " << ops_count << " operations would be reverted:\n";
Table view;
if (Context::getContext().config.getBoolean("obfuscate")) view.obfuscate();
view.width(Context::getContext().getWidth());
view.add("Uuid");
view.add("Modification");
std::string last_uuid;
std::stringstream mods;
for (auto& op : undo_ops) {
if (op.is_undo_point()) {
continue;
}
if (last_uuid != op.get_uuid()) {
if (last_uuid.size() != 0) {
int row = view.addRow();
view.set(row, 0, last_uuid);
view.set(row, 1, mods.str());
}
last_uuid = op.get_uuid();
mods.clear();
}
if (op.is_create()) {
mods << "Create task\n";
} else if (op.is_delete()) {
mods << "Delete (purge) task";
} else if (op.is_update()) {
auto property = op.get_property();
auto old_value = op.get_old_value();
auto value = op.get_value();
if (Task::isTagAttr(property)) {
if (value && *value == "x") {
mods << "Add tag '" << Task::attr2Tag(property) << "'\n";
continue;
} else if (!value && old_value && *old_value == "x") {
mods << "Remove tag '" << Task::attr2Tag(property) << "'\n";
continue;
}
}
if (old_value && value) {
mods << "Update property '" << property << "' from '" << *old_value << "' to '" << *value
<< "'\n";
} else if (old_value) {
mods << "Delete property '" << property << "' (was '" << *old_value << "')\n";
} else if (value) {
mods << "Add property '" << property << "' with value '" << *value << "'\n";
}
}
}
int row = view.addRow();
view.set(row, 0, last_uuid);
view.set(row, 1, mods.str());
std::cout << view.render() << "\n";
return !Context::getContext().config.getBoolean("confirmation") ||
confirm(
"The undo command is not reversible. Are you sure you want to revert to the previous "
"state?");
return true;
}
////////////////////////////////////////////////////////////////////////////////

View file

@ -28,13 +28,18 @@
#define INCLUDED_CMDUNDO #define INCLUDED_CMDUNDO
#include <Command.h> #include <Command.h>
#include <Operation.h>
#include <string> #include <string>
#include <vector>
class CmdUndo : public Command { class CmdUndo : public Command {
public: public:
CmdUndo(); CmdUndo();
int execute(std::string &); int execute(std::string &);
private:
bool confirm_revert(const std::vector<Operation> &);
}; };
#endif #endif

View file

@ -74,33 +74,13 @@ class TestUndoStyle(TestCase):
self.t("add one project:foo priority:H") self.t("add one project:foo priority:H")
self.t("1 modify +tag project:bar priority:") self.t("1 modify +tag project:bar priority:")
@unittest.expectedFailure # undo diffs are not supported def test_undo_output(self):
def test_undo_side_style(self): """Test that 'task undo' generates the right output"""
"""Test that 'rc.undo.style:side' generates the right output"""
self.t.config("undo.style", "side")
code, out, err = self.t("undo", input="n\n") code, out, err = self.t("undo", input="n\n")
self.assertNotRegex(out, "-tags:\\s*\n\\+tags:\\s+tag") self.assertNotRegex(out, "-tags:\\s*\n\\+tags:\\s+tag")
self.assertRegex(out, r"tags\s+tag\s*") self.assertRegex(out, r"Delete property 'priority'")
self.assertRegex(out, r"Update property 'project'")
@unittest.expectedFailure # undo diffs are not supported self.assertRegex(out, r"Add tag 'tag'")
def test_undo_diff_style(self):
"""Test that 'rc.undo.style:diff' generates the right output"""
self.t.config("undo.style", "diff")
code, out, err = self.t("undo", input="n\n")
self.assertRegex(out, "-tags:\\s*\n\\+tags:\\s+tag")
self.assertNotRegex(out, r"tags\s+tag\s*")
def test_undo_diff_operations(self):
code, out, err = self.t("undo", input="n\n")
# If the clock ticks a second between `add` and `modify` there is a
# fifth operation setting the `modified` property.
self.assertRegex(out, "The following [4|5] operations would be reverted:")
self.assertIn("tag_tag: <empty> -> x", out)
self.assertIn("tags: <empty> -> tag", out)
self.assertIn("project: foo -> bar", out)
self.assertIn("priority: H -> <empty>", out)
class TestBug634(TestCase): class TestBug634(TestCase):