xaizek / dit (License: GPLv3) (since 2018-12-07)
Command-line task keeper that remembers all old values and is meant to combine several orthogonal features to be rather flexible in managing items.
Commit fb40bb00175618103c2f8cc47a6743466c292526

Add diffing of changes to "log" command
Author: xaizek
Author date (UTC): 2016-06-06 19:46
Committer name: xaizek
Committer date (UTC): 2016-06-06 19:46
Parent(s): cedec35134a1861f070fa2cfb05bd917df0dc77a
Signing key: 99DC5E4DB05F6BE2
Tree: e49d742aae6258930c339d411d337ae66460cefd
File Lines added Lines deleted
src/cmds/LogCmd.cpp 94 3
tests/cmds/LogCmd.cpp 51 0
File src/cmds/LogCmd.cpp changed (mode: 100644) (index 7dee951..85c8689)
17 17
18 18 #include <cstdlib> #include <cstdlib>
19 19
20 #include <deque>
21 #include <sstream>
20 22 #include <stdexcept> #include <stdexcept>
21 23 #include <unordered_map> #include <unordered_map>
22 24 #include <unordered_set> #include <unordered_set>
23 25 #include <vector> #include <vector>
24 26
27 #include "utils/strings.hpp"
25 28 #include "Change.hpp" #include "Change.hpp"
26 29 #include "Commands.hpp" #include "Commands.hpp"
27 30 #include "Item.hpp" #include "Item.hpp"
 
31 34 #include "decoration.hpp" #include "decoration.hpp"
32 35 #include "printing.hpp" #include "printing.hpp"
33 36
37 static std::string diff(const std::vector<std::string> &f,
38 const std::vector<std::string> &s);
39
34 40 namespace { namespace {
35 41
36 42 /** /**
 
... ... LogCmd::run(Project &project, const std::vector<std::string> &args)
99 105 << Value{value} << '\n'; << Value{value} << '\n';
100 106 } else { } else {
101 107 out() << Key{key} out() << Key{key}
102 << decor::blue_fg << decor::bold << " changed to"
103 << decor::def
104 << Value{value} << '\n';
108 << decor::blue_fg << decor::bold << " changed" << decor::def
109 << Value{diff(split(values[key], '\n'), split(value, '\n'))};
105 110 } }
106 111
107 112 values[key] = value; values[key] = value;
 
... ... LogCmd::run(Project &project, const std::vector<std::string> &args)
110 115 return EXIT_SUCCESS; return EXIT_SUCCESS;
111 116 } }
112 117
118 /**
119 * @brief Finds difference between two lists of lines.
120 *
121 * Implements solution for longest common subsequence problem that matches
122 * modified finding of edit distance (substitution operation excluded) with
123 * backtracking afterward to compose result.
124 *
125 * @param f New state.
126 * @param s Previous state.
127 *
128 * @returns Colored difference showing how to get new state from the old one.
129 */
130 static std::string
131 diff(const std::vector<std::string> &f, const std::vector<std::string> &s)
132 {
133 int d[f.size() + 1][s.size() + 1];
134
135 // Modified edit distance finding.
136 using size_type = std::vector<std::string>::size_type;
137 for (size_type i = 0U, nf = f.size(); i <= nf; ++i) {
138 for (size_type j = 0U, ns = s.size(); j <= ns; ++j) {
139 if (i == 0U) {
140 d[i][j] = j;
141 } else if (j == 0U) {
142 d[i][j] = i;
143 } else {
144 d[i][j] = std::min(d[i - 1U][j] + 1, d[i][j - 1U] + 1);
145 if (f[i - 1U] == s[j - 1U]) {
146 d[i][j] = std::min(d[i - 1U][j - 1U], d[i][j]);
147 }
148 }
149 }
150 }
151
152 std::deque<std::string> result;
153 int identicalLines = 0;
154
155 auto foldIdentical = [&identicalLines, &result]() {
156 if (identicalLines > 3) {
157 result.erase(result.cbegin() + 1,
158 result.cbegin() + (identicalLines - 1));
159 result.insert(result.cbegin() + 1,
160 "<" + std::to_string(identicalLines - 2) +
161 " unchanged lines folded>");
162 }
163 identicalLines = 0;
164 };
165
166 // Compose results with folding of long runs of identical lines (longer than
167 // three lines).
168 int i = f.size(), j = s.size();
169 while (i != 0 || j != 0) {
170 if (i == 0) {
171 foldIdentical();
172 result.push_front("+ " + s[--j]);
173 } else if (j == 0) {
174 foldIdentical();
175 result.push_front("- " + f[--i]);
176 } else if (d[i][j] == d[i][j - 1] + 1) {
177 foldIdentical();
178 result.push_front("+ " + s[--j]);
179 } else if (d[i][j] == d[i - 1][j] + 1) {
180 foldIdentical();
181 result.push_front("- " + f[--i]);
182 } else {
183 result.push_front(" " + f[--i]);
184 --j;
185 ++identicalLines;
186 }
187 }
188 foldIdentical();
189
190 // Color results and turn them into a string.
191 std::ostringstream oss;
192 for (const std::string &line : result) {
193 switch (line[0]) {
194 case '+': oss << decor::green_fg; break;
195 case '-': oss << decor::red_fg; break;
196 case '<': oss << decor::black_fg << decor::bold; break;
197 }
198 oss << line << decor::def << '\n';
199 }
200
201 return oss.str();
202 }
203
113 204 boost::optional<int> boost::optional<int>
114 205 LogCmd::complete(Project &project, const std::vector<std::string> &args) LogCmd::complete(Project &project, const std::vector<std::string> &args)
115 206 { {
File tests/cmds/LogCmd.cpp changed (mode: 100644) (index 68953e3..b68ce4d)
... ... TEST_CASE("Log displays operations correctly", "[cmds][log]")
142 142 } }
143 143 } }
144 144
145 TEST_CASE("Log produces diffs", "[cmds][log]")
146 {
147 std::unique_ptr<Project> prj = Tests::makeProject();
148 Command *const cmd = Commands::get("log");
149 Storage &storage = prj->getStorage();
150
151 std::ostringstream out, err;
152 Tests::setStreams(out, err);
153
154 std::time_t t = std::time(nullptr);
155 MockTimeSource timeMock([&t](){ return t++; });
156
157 SECTION("Runs of identical lines are folded with single-line context")
158 {
159 Item item = Tests::makeItem("id");
160 item.setValue("title", "a\nb\nc\nd\ne");
161 item.setValue("title", "a\nb\nc\nd\nf");
162 Tests::storeItem(storage, std::move(item));
163
164 boost::optional<int> exitCode = cmd->run(*prj, { "id" });
165 REQUIRE(exitCode);
166 REQUIRE(*exitCode == EXIT_SUCCESS);
167
168 const std::vector<std::string> lines = split(out.str(), '\n');
169 REQUIRE(boost::starts_with(lines[6], "title changed:"));
170 REQUIRE(lines[7] == " a");
171 REQUIRE(boost::starts_with(lines[8], "<2"));
172 REQUIRE(lines[9] == " d");
173 REQUIRE(lines[10] == "- e");
174 REQUIRE(lines[11] == "+ f");
175 REQUIRE(err.str() == std::string());
176 }
177
178 SECTION("Runs of identical lines are folded with single-line context")
179 {
180 Item item = Tests::makeItem("id");
181 item.setValue("title", "a");
182 item.setValue("title", "0\na");
183 Tests::storeItem(storage, std::move(item));
184
185 boost::optional<int> exitCode = cmd->run(*prj, { "id" });
186 REQUIRE(exitCode);
187 REQUIRE(*exitCode == EXIT_SUCCESS);
188
189 const std::vector<std::string> lines = split(out.str(), '\n');
190 REQUIRE(boost::starts_with(lines[1], "title changed:"));
191 REQUIRE(lines[2] == "+ 0");
192 REQUIRE(err.str() == std::string());
193 }
194 }
195
145 196 TEST_CASE("Completion of id for log", "[cmds][log][completion]") TEST_CASE("Completion of id for log", "[cmds][log][completion]")
146 197 { {
147 198 std::unique_ptr<Project> prj = Tests::makeProject(); std::unique_ptr<Project> prj = Tests::makeProject();
Hints

Before first commit, do not forget to setup your git environment:
git config --global user.name "your_name_here"
git config --global user.email "your@email_here"

Clone this repository using HTTP(S):
git clone https://code.reversed.top/user/xaizek/dit

Clone this repository using ssh (do not forget to upload a key first):
git clone ssh://rocketgit@code.reversed.top/user/xaizek/dit

You are allowed to anonymously push to this repository.
This means that your pushed commits will automatically be transformed into a pull request:
... clone the repository ...
... make some changes and some commits ...
git push origin master