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 36446323a283f5aaa313a85bf2073df0e923f67a

Abort operation if external editing has failed
Instead of silently continuing using original value.

The old behaviour is somewhat problematic when editor exits with error
code per user request, like on `:cquit` in Vim.
Author: xaizek
Author date (UTC): 2018-10-17 16:06
Committer name: xaizek
Committer date (UTC): 2018-10-17 16:06
Parent(s): 47a4683e480ab35d71d7c0c2d9bd69781da5786e
Signing key: 99DC5E4DB05F6BE2
Tree: 29e0867e6598b12b324a84536ae88513c7266639
File Lines added Lines deleted
src/cmds/AddCmd.cpp 7 1
src/cmds/ConfigCmd.cpp 8 2
src/cmds/SetCmd.cpp 7 1
src/integration.cpp 3 8
src/integration.hpp 0 2
tests/cmds/AddCmd.cpp 16 0
tests/cmds/ConfigCmd.cpp 32 0
tests/cmds/SetCmd.cpp 34 0
tests/integration.cpp 2 3
File src/cmds/AddCmd.cpp changed (mode: 100644) (index 1f31a63..0ee0412)
... ... AddCmd::run(Project &project, const std::vector<std::string> &args)
103 103 return EXIT_FAILURE; return EXIT_FAILURE;
104 104 } }
105 105
106 if (boost::optional<std::string> v = editValue(key, value, {})) {
106 if (value == "-") {
107 boost::optional<std::string> v = editValue(key, {});
108 if (!v) {
109 err() << "Failed to prompt for value of key \""
110 << key << "\"\n";
111 return EXIT_FAILURE;
112 }
107 113 value = std::move(*v); value = std::move(*v);
108 114 } }
109 115 fields[key] = value; fields[key] = value;
File src/cmds/ConfigCmd.cpp changed (mode: 100644) (index ff8eaef..32a4fba)
... ... ConfigCmd::run(Config &config, const std::vector<std::string> &args)
197 197 continue; continue;
198 198 } }
199 199
200 if (boost::optional<std::string> v = editValue(key, value,
201 config.get(key, {}))) {
200 if (value == "-") {
201 boost::optional<std::string> v = editValue(key,
202 config.get(key, {}));
203 if (!v) {
204 err() << "Failed to prompt for value of key \""
205 << key << "\"\n";
206 return EXIT_FAILURE;
207 }
202 208 value = std::move(*v); value = std::move(*v);
203 209 } }
204 210 config.set(key, value); config.set(key, value);
File src/cmds/SetCmd.cpp changed (mode: 100644) (index c62bf37..b197b5e)
... ... SetCmd::run(Project &project, const std::vector<std::string> &args)
121 121 } }
122 122
123 123 const std::string current = append ? std::string() : fields[key]; const std::string current = append ? std::string() : fields[key];
124 if (boost::optional<std::string> v = editValue(key, value, current)) {
124 if (value == "-") {
125 boost::optional<std::string> v = editValue(key, current);
126 if (!v) {
127 err() << "Failed to prompt for value of key \""
128 << key << "\"\n";
129 return EXIT_FAILURE;
130 }
125 131 value = std::move(*v); value = std::move(*v);
126 132 } }
127 133
File src/integration.cpp changed (mode: 100644) (index a20e35c..ae6c5eb)
... ... getTerminalSize()
296 296 } }
297 297
298 298 boost::optional<std::string> boost::optional<std::string>
299 editValue(const std::string &key, const std::string &value,
300 const std::string &current)
299 editValue(const std::string &key, const std::string &current)
301 300 { {
302 if (value != "-") {
303 return {};
304 }
305
306 // Create name for temporary file name and the file itself.
301 // Generate name of a temporary file and write the file.
307 302 TempFile tmpFile("buf"); TempFile tmpFile("buf");
308 303 writeBufferFile(tmpFile, key, current); writeBufferFile(tmpFile, key, current);
309 304
310 305 if (!editBufferFile(tmpFile)) { if (!editBufferFile(tmpFile)) {
311 return current;
306 return {};
312 307 } }
313 308
314 309 return readEditedValue(tmpFile); return readEditedValue(tmpFile);
File src/integration.hpp changed (mode: 100644) (index 0461bb0..8f12b4c)
... ... private:
60 60 * @brief Edits value if user requested for it. * @brief Edits value if user requested for it.
61 61 * *
62 62 * @param key Key (name of the value). * @param key Key (name of the value).
63 * @param value New value (can be special value, which means "edit").
64 63 * @param current Current value to be edited. * @param current Current value to be edited.
65 64 * *
66 65 * @returns Nothing if @p value can be used as is, otherwise new value. * @returns Nothing if @p value can be used as is, otherwise new value.
67 66 */ */
68 67 boost::optional<std::string> editValue(const std::string &key, boost::optional<std::string> editValue(const std::string &key,
69 const std::string &value,
70 68 const std::string &current); const std::string &current);
71 69
72 70 /** /**
File tests/cmds/AddCmd.cpp changed (mode: 100644) (index f170aa8..0976a94)
... ... TEST_CASE("Addition on new item", "[cmds][add]")
148 148 REQUIRE(out.str() == "Created item: fYP\n"); REQUIRE(out.str() == "Created item: fYP\n");
149 149 REQUIRE(err.str() == std::string()); REQUIRE(err.str() == std::string());
150 150 } }
151
152 SECTION("Item is not created on failed external editing")
153 {
154 static char editor_env[] = "EDITOR=wrong-command >>";
155 putenv(editor_env);
156
157 boost::optional<int> exitCode = cmd->run(*prj,
158 { "title:", "title",
159 "status:", "-",
160 "author:", "me" });
161 REQUIRE(exitCode);
162 REQUIRE(*exitCode == EXIT_FAILURE);
163
164 REQUIRE(out.str() == std::string());
165 REQUIRE(err.str() != std::string());
166 }
151 167 } }
152 168
153 169 TEST_CASE("Completion of first key name on addition", "[cmds][add][completion]") TEST_CASE("Completion of first key name on addition", "[cmds][add][completion]")
File tests/cmds/ConfigCmd.cpp changed (mode: 100644) (index 07bee4f..7fa4538)
... ... TEST_CASE("Config allows external editing", "[cmds][config][integration]")
120 120 REQUIRE(prj->getConfig(true).get("value") == "new-value"); REQUIRE(prj->getConfig(true).get("value") == "new-value");
121 121 } }
122 122
123 TEST_CASE("Config is not updated on failed external editing",
124 "[cmds][config][integration]")
125 {
126 std::unique_ptr<Project> prj = Tests::makeProject();
127
128 std::ostringstream out, err;
129 Tests::setStreams(out, err);
130
131 static char editor_env[] = "EDITOR=wrong-command >>";
132 putenv(editor_env);
133
134 Config &cfg = prj->getConfig(true);
135
136 cfg.set("a", "olda");
137 cfg.set("b", "oldb");
138 cfg.set("c", "oldc");
139
140 Command *const cmd = Commands::get("config");
141 boost::optional<int> exitCode = cmd->run(*prj, { "a:", "newa",
142 "b:", "-",
143 "c:", "newc" });
144 REQUIRE(exitCode);
145 REQUIRE(*exitCode == EXIT_FAILURE);
146
147 REQUIRE(out.str() == std::string());
148 REQUIRE(err.str() != std::string());
149
150 REQUIRE(cfg.get("a") == "olda");
151 REQUIRE(cfg.get("b") == "oldb");
152 REQUIRE(cfg.get("c") == "oldc");
153 }
154
123 155 TEST_CASE("Global configuration is processed", "[cmds][config]") TEST_CASE("Global configuration is processed", "[cmds][config]")
124 156 { {
125 157 Command *const cmd = Commands::get("config"); Command *const cmd = Commands::get("config");
File tests/cmds/SetCmd.cpp changed (mode: 100644) (index deb77c3..29936e3)
... ... TEST_CASE("Set allows external editing", "[cmds][set][integration]")
104 104 REQUIRE(storage.get("id").getValue("bug_number") == "22new-value"); REQUIRE(storage.get("id").getValue("bug_number") == "22new-value");
105 105 } }
106 106
107 TEST_CASE("Nothing is set on failed external editing",
108 "[cmds][set][integration]")
109 {
110 std::unique_ptr<Project> prj = Tests::makeProject();
111 Storage &storage = prj->getStorage();
112
113 Item item = Tests::makeItem("id");
114 item.setValue("title", "oldtitle");
115 item.setValue("author", "someone");
116
117 Tests::storeItem(storage, std::move(item));
118
119 std::ostringstream out, err;
120 Tests::setStreams(out, err);
121
122 static char editor_env[] = "EDITOR=wrong-command >>";
123 putenv(editor_env);
124
125 Command *const cmd = Commands::get("set");
126 boost::optional<int> exitCode = cmd->run(*prj, { "id",
127 "title:", "title",
128 "status:", "-",
129 "author:", "me" });
130 REQUIRE(exitCode);
131 REQUIRE(*exitCode == EXIT_FAILURE);
132
133 REQUIRE(out.str() == std::string());
134 REQUIRE(err.str() != std::string());
135
136 REQUIRE(storage.get("id").getValue("title") == "oldtitle");
137 REQUIRE(storage.get("id").getValue("status") == std::string());
138 REQUIRE(storage.get("id").getValue("author") == "someone");
139 }
140
107 141 TEST_CASE("Item is changed successfully", "[cmds][set]") TEST_CASE("Item is changed successfully", "[cmds][set]")
108 142 { {
109 143 std::unique_ptr<Project> prj = Tests::makeProject(); std::unique_ptr<Project> prj = Tests::makeProject();
File tests/integration.cpp changed (mode: 100644) (index adb6cb4..cbe8599)
... ... TEST_CASE("On failure curent value is returned.", "[integration]")
26 26 static char editor_env[] = "EDITOR=wrong-command >>"; static char editor_env[] = "EDITOR=wrong-command >>";
27 27 putenv(editor_env); putenv(editor_env);
28 28
29 boost::optional<std::string> val = editValue("key", "-", "current");
30 REQUIRE(val);
31 REQUIRE(*val == "current");
29 boost::optional<std::string> val = editValue("key", "current");
30 REQUIRE_FALSE(val);
32 31 } }
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