• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

GothenburgBitFactory / taskwarrior / 11335495770

14 Oct 2024 09:47PM UTC coverage: 84.223% (-0.6%) from 84.776%
11335495770

push

github

web-flow
[pre-commit.ci] pre-commit autoupdate (#3650)

updates:
- [github.com/psf/black: 24.8.0 → 24.10.0](https://github.com/psf/black/compare/24.8.0...24.10.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>

19005 of 22565 relevant lines covered (84.22%)

23473.55 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

68.32
/src/commands/CmdEdit.cpp
1
////////////////////////////////////////////////////////////////////////////////
2
//
3
// Copyright 2006 - 2021, Tomas Babej, Paul Beckingham, Federico Hernandez.
4
//
5
// Permission is hereby granted, free of charge, to any person obtaining a copy
6
// of this software and associated documentation files (the "Software"), to deal
7
// in the Software without restriction, including without limitation the rights
8
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
// copies of the Software, and to permit persons to whom the Software is
10
// furnished to do so, subject to the following conditions:
11
//
12
// The above copyright notice and this permission notice shall be included
13
// in all copies or substantial portions of the Software.
14
//
15
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
16
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
18
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
// SOFTWARE.
22
//
23
// https://www.opensource.org/licenses/mit-license.php
24
//
25
////////////////////////////////////////////////////////////////////////////////
26

27
#include <cmake.h>
28
// cmake.h include header must come first
29

30
#include <CmdEdit.h>
31
#include <Context.h>
32
#include <Datetime.h>
33
#include <Duration.h>
34
#include <Filter.h>
35
#include <JSON.h>
36
#include <Lexer.h>
37
#include <Pig.h>
38
#include <format.h>
39
#include <main.h>
40
#include <shared.h>
41
#include <unistd.h>
42
#include <util.h>
43

44
#include <algorithm>
45
#include <cerrno>
46
#include <cstdlib>
47
#include <cstring>
48
#include <iostream>
49
#include <sstream>
50

51
#define STRING_EDIT_START_MOD "Start date modified."
52
#define STRING_EDIT_SCHED_MOD "Scheduled date modified."
53
#define STRING_EDIT_DUE_MOD "Due date modified."
54
#define STRING_EDIT_UNTIL_MOD "Until date modified."
55
#define STRING_EDIT_WAIT_MOD "Wait date modified."
56

57
const std::string CmdEdit::ANNOTATION_EDIT_MARKER = "\n                     ";
58

59
////////////////////////////////////////////////////////////////////////////////
60
CmdEdit::CmdEdit() {
4,497✔
61
  _keyword = "edit";
4,497✔
62
  _usage = "task <filter> edit";
4,497✔
63
  _description = "Launches an editor to modify a task directly";
4,497✔
64
  _read_only = false;
4,497✔
65
  _displays_id = false;
4,497✔
66
  _needs_gc = false;
4,497✔
67
  _uses_context = true;
4,497✔
68
  _accepts_filter = true;
4,497✔
69
  _accepts_modifications = false;
4,497✔
70
  _accepts_miscellaneous = false;
4,497✔
71
  _category = Command::Category::operation;
4,497✔
72
}
4,497✔
73

74
////////////////////////////////////////////////////////////////////////////////
75
// Introducing the Silver Bullet.  This feature is the catch-all fixative for
76
// various other ills.  This is like opening up the hood and going in with a
77
// wrench.  To be used sparingly.
78
int CmdEdit::execute(std::string&) {
4✔
79
  // Filter the tasks.
80
  handleUntil();
4✔
81
  handleRecurrence();
4✔
82
  Filter filter;
4✔
83
  std::vector<Task> filtered;
4✔
84
  filter.subset(filtered);
4✔
85

86
  if (!filtered.size()) {
4✔
87
    Context::getContext().footnote("No matches.");
×
88
    return 1;
×
89
  }
90

91
  unsigned int bulk = Context::getContext().config.getInteger("bulk");
8✔
92

93
  // If we are editing more than "bulk" tasks, ask for confirmation.
94
  // Bulk = 0 denotes infinite bulk.
95
  if ((filtered.size() > bulk) && (bulk != 0))
4✔
96
    if (!confirm(format("Do you wish to manually edit {1} tasks?", filtered.size()))) return 2;
×
97

98
  // Find number of matching tasks.
99
  for (auto& task : filtered) {
8✔
100
    auto result = editFile(task);
4✔
101
    if (result == CmdEdit::editResult::error)
4✔
102
      break;
×
103
    else if (result == CmdEdit::editResult::changes)
4✔
104
      Context::getContext().tdb2.modify(task);
4✔
105
  }
106

107
  return 0;
4✔
108
}
4✔
109

110
////////////////////////////////////////////////////////////////////////////////
111
std::string CmdEdit::findValue(const std::string& text, const std::string& name) {
56✔
112
  auto found = text.find(name);
56✔
113
  if (found != std::string::npos) {
56✔
114
    auto eol = text.find('\n', found + 1);
56✔
115
    if (eol != std::string::npos) {
56✔
116
      std::string value = text.substr(found + name.length(), eol - (found + name.length()));
56✔
117

118
      return Lexer::trim(value, "\t ");
56✔
119
    }
56✔
120
  }
121

122
  return "";
×
123
}
124

125
////////////////////////////////////////////////////////////////////////////////
126
std::string CmdEdit::findMultilineValue(const std::string& text, const std::string& startMarker,
4✔
127
                                        const std::string& endMarker) {
128
  auto start = text.find(startMarker);
4✔
129
  if (start != std::string::npos) {
4✔
130
    auto end = text.find(endMarker, start);
4✔
131
    if (end != std::string::npos) {
4✔
132
      std::string value =
133
          text.substr(start + startMarker.length(), end - (start + startMarker.length()));
4✔
134
      return Lexer::trim(value, "\\\t ");
4✔
135
    }
4✔
136
  }
137
  return "";
×
138
}
139

140
////////////////////////////////////////////////////////////////////////////////
141
std::vector<std::string> CmdEdit::findValues(const std::string& text, const std::string& name) {
4✔
142
  std::vector<std::string> results;
4✔
143
  std::string::size_type found = 0;
4✔
144

145
  while (found != std::string::npos) {
9✔
146
    found = text.find(name, found + 1);
5✔
147
    if (found != std::string::npos) {
5✔
148
      auto eol = text.find('\n', found + 1);
1✔
149
      if (eol != std::string::npos) {
1✔
150
        auto value = text.substr(found + name.length(), eol - (found + name.length()));
1✔
151

152
        found = eol - 1;
1✔
153
        results.push_back(Lexer::trim(value, "\t "));
1✔
154
      }
1✔
155
    }
156
  }
157

158
  return results;
4✔
159
}
×
160

161
////////////////////////////////////////////////////////////////////////////////
162
std::string CmdEdit::formatDate(Task& task, const std::string& attribute,
42✔
163
                                const std::string& dateformat) {
164
  auto value = task.get(attribute);
42✔
165
  if (value.length()) value = Datetime(value).toString(dateformat);
88✔
166

167
  return value;
42✔
168
}
×
169

170
////////////////////////////////////////////////////////////////////////////////
171
std::string CmdEdit::formatDuration(Task& task, const std::string& attribute) {
1✔
172
  auto value = task.get(attribute);
1✔
173
  if (value.length()) value = Duration(value).formatISO();
1✔
174

175
  return value;
1✔
176
}
×
177

178
////////////////////////////////////////////////////////////////////////////////
179
std::string CmdEdit::formatTask(Task task, const std::string& dateformat) {
4✔
180
  std::stringstream before;
4✔
181
  auto verbose = Context::getContext().verbose("edit");
8✔
182

183
  if (verbose)
4✔
184
    before << "# The 'task <id> edit' command allows you to modify all aspects of a task\n"
185
              "# using a text editor.  Below is a representation of all the task details.\n"
186
              "# Modify what you wish, and when you save and quit your editor,\n"
187
              "# Taskwarrior will read this file, determine what changed, and apply\n"
188
              "# those changes.  If you exit your editor without saving or making\n"
189
              "# modifications, Taskwarrior will do nothing.\n"
190
              "#\n"
191
              "# Lines that begin with # represent data you cannot change, like ID.\n"
192
              "# If you get too creative with your editing, Taskwarrior will send you\n"
193
              "# back to the editor to try again.\n"
194
              "#\n"
195
              "# Should you find yourself in an endless loop, re-editing the same file,\n"
196
              "# just quit the editor without making any changes.  Taskwarrior will\n"
197
              "# notice this and stop the editing.\n"
198
              "#\n";
4✔
199

200
  before << "# Name               Editable details\n"
201
         << "# -----------------  ----------------------------------------------------\n"
202
         << "# ID:                " << task.id << '\n'
4✔
203
         << "# UUID:              " << task.get("uuid") << '\n'
8✔
204
         << "# Status:            " << Lexer::ucFirst(Task::statusToText(task.getStatus())) << '\n'
8✔
205
         << "# Mask:              " << task.get("mask") << '\n'
12✔
206
         << "# iMask:             " << task.get("imask") << '\n'
12✔
207
         << "  Project:           " << task.get("project") << '\n';
28✔
208

209
  if (verbose) before << "# Separate the tags with spaces, like this: tag1 tag2\n";
4✔
210

211
  before << "  Tags:              " << join(" ", task.getTags()) << '\n'
12✔
212
         << "  Description:       " << task.get("description") << '\n'
12✔
213
         << "  Created:           " << formatDate(task, "entry", dateformat) << '\n'
12✔
214
         << "  Started:           " << formatDate(task, "start", dateformat) << '\n'
12✔
215
         << "  Ended:             " << formatDate(task, "end", dateformat) << '\n'
12✔
216
         << "  Scheduled:         " << formatDate(task, "scheduled", dateformat) << '\n'
12✔
217
         << "  Due:               " << formatDate(task, "due", dateformat) << '\n'
12✔
218
         << "  Until:             " << formatDate(task, "until", dateformat) << '\n'
12✔
219
         << "  Recur:             " << task.get("recur") << '\n'
12✔
220
         << "  Wait until:        " << formatDate(task, "wait", dateformat) << '\n'
12✔
221
         << "# Modified:          " << formatDate(task, "modified", dateformat) << '\n'
12✔
222
         << "  Parent:            " << task.get("parent") << '\n';
56✔
223

224
  if (verbose)
4✔
225
    before
226
        << "# Annotations look like this: <date> -- <text> and there can be any number of them.\n"
227
           "# The ' -- ' separator between the date and text field should not be removed.\n"
228
           "# Multiline annotations need to be indented up to <date> ("
4✔
229
        << ANNOTATION_EDIT_MARKER.length() - 1
4✔
230
        << " spaces).\n"
231
           "# A \"blank slot\" for adding an annotation follows for your convenience.\n";
4✔
232

233
  for (auto& anno : task.getAnnotations()) {
8✔
234
    Datetime dt(strtoll(anno.first.substr(11).c_str(), nullptr, 10));
4✔
235
    before << "  Annotation:        " << dt.toString(dateformat) << " -- "
4✔
236
           << str_replace(anno.second, "\n", ANNOTATION_EDIT_MARKER) << '\n';
12✔
237
  }
4✔
238

239
  Datetime now;
4✔
240
  before << "  Annotation:        " << now.toString(dateformat) << " -- \n";
4✔
241

242
  // Add dependencies here.
243
  auto dependencies = task.getDependencyUUIDs();
4✔
244
  std::stringstream allDeps;
4✔
245
  for (unsigned int i = 0; i < dependencies.size(); ++i) {
4✔
246
    if (i) allDeps << ",";
×
247

248
    Task t;
×
249
    Context::getContext().tdb2.get(dependencies[i], t);
×
250
    if (t.getStatus() == Task::pending || t.getStatus() == Task::waiting)
×
251
      allDeps << t.id;
×
252
    else
253
      allDeps << dependencies[i];
×
254
  }
×
255

256
  if (verbose)
4✔
257
    before << "# Dependencies should be a comma-separated list of task IDs/UUIDs or ID ranges, "
258
              "with no spaces.\n";
4✔
259

260
  before << "  Dependencies:      " << allDeps.str() << '\n';
4✔
261

262
  // UDAs
263
  std::vector<std::string> udas;
4✔
264
  for (auto& col : Context::getContext().columns)
104✔
265
    if (Context::getContext().config.get("uda." + col.first + ".type") != "")
100✔
266
      udas.push_back(col.first);
8✔
267

268
  if (udas.size()) {
4✔
269
    before << "# User Defined Attributes\n";
4✔
270
    std::sort(udas.begin(), udas.end());
4✔
271
    for (auto& uda : udas) {
12✔
272
      int pad = 13 - uda.length();
8✔
273
      std::string padding = "";
8✔
274
      if (pad > 0) padding = std::string(pad, ' ');
24✔
275

276
      std::string type = Context::getContext().config.get("uda." + uda + ".type");
8✔
277
      if (type == "string" || type == "numeric") {
8✔
278
        auto value = task.get(uda);
6✔
279
        if (type == "string") value = json::encode(value);
6✔
280
        before << "  UDA " << uda << ": " << padding << value << '\n';
6✔
281
      } else if (type == "date")
8✔
282
        before << "  UDA " << uda << ": " << padding << formatDate(task, uda, dateformat) << '\n';
1✔
283
      else if (type == "duration")
1✔
284
        before << "  UDA " << uda << ": " << padding << formatDuration(task, uda) << '\n';
1✔
285
    }
8✔
286
  }
287

288
  // UDA orphans
289
  auto orphans = task.getUDAOrphans();
4✔
290
  if (orphans.size()) {
4✔
291
    before << "# User Defined Attribute Orphans\n";
1✔
292
    std::sort(orphans.begin(), orphans.end());
1✔
293
    for (auto& orphan : orphans) {
2✔
294
      int pad = 6 - orphan.length();
1✔
295
      std::string padding = "";
1✔
296
      if (pad > 0) padding = std::string(pad, ' ');
1✔
297

298
      before << "  UDA Orphan " << orphan << ": " << padding << task.get(orphan) << '\n';
1✔
299
    }
1✔
300
  }
301

302
  before << "# End\n";
4✔
303
  return before.str();
8✔
304
}
4✔
305

306
////////////////////////////////////////////////////////////////////////////////
307
void CmdEdit::parseTask(Task& task, const std::string& after, const std::string& dateformat) {
4✔
308
  // project
309
  auto value = findValue(after, "\n  Project:");
8✔
310
  if (task.get("project") != value) {
8✔
311
    if (value != "") {
×
312
      Context::getContext().footnote("Project modified.");
×
313
      task.set("project", value);
×
314
    } else {
315
      Context::getContext().footnote("Project deleted.");
×
316
      task.remove("project");
×
317
    }
318
  }
319

320
  // tags
321
  value = findValue(after, "\n  Tags:");
8✔
322
  task.remove("tags");
4✔
323
  task.setTags(split(value, ' '));
4✔
324

325
  // description.
326
  value = findMultilineValue(after, "\n  Description:", "\n  Created:");
16✔
327
  if (task.get("description") != value) {
8✔
328
    if (value != "") {
×
329
      Context::getContext().footnote("Description modified.");
×
330
      task.set("description", value);
×
331
    } else
332
      throw std::string("Cannot remove description.");
×
333
  }
334

335
  // entry
336
  value = findValue(after, "\n  Created:");
4✔
337
  if (value != "") {
4✔
338
    if (value != formatDate(task, "entry", dateformat)) {
8✔
339
      Context::getContext().footnote("Creation date modified.");
×
340
      task.set("entry", Datetime(value, dateformat).toEpochString());
×
341
    }
342
  } else
343
    throw std::string("Cannot remove creation date.");
×
344

345
  // start
346
  value = findValue(after, "\n  Started:");
4✔
347
  if (value != "") {
4✔
348
    if (task.get("start") != "") {
2✔
349
      if (value != formatDate(task, "start", dateformat)) {
2✔
350
        Context::getContext().footnote(STRING_EDIT_START_MOD);
×
351
        task.set("start", Datetime(value, dateformat).toEpochString());
×
352
      }
353
    } else {
354
      Context::getContext().footnote(STRING_EDIT_START_MOD);
×
355
      task.set("start", Datetime(value, dateformat).toEpochString());
×
356
    }
357
  } else {
358
    if (task.get("start") != "") {
6✔
359
      Context::getContext().footnote("Start date removed.");
×
360
      task.remove("start");
×
361
    }
362
  }
363

364
  // end
365
  value = findValue(after, "\n  Ended:");
4✔
366
  if (value != "") {
4✔
367
    if (task.get("end") != "") {
×
368
      if (value != formatDate(task, "end", dateformat)) {
×
369
        Context::getContext().footnote("End date modified.");
×
370
        task.set("end", Datetime(value, dateformat).toEpochString());
×
371
      }
372
    } else if (task.getStatus() != Task::deleted)
×
373
      throw std::string("Cannot set a done date on a pending task.");
×
374
  } else {
375
    if (task.get("end") != "") {
8✔
376
      Context::getContext().footnote("End date removed.");
×
377
      task.setStatus(Task::pending);
×
378
      task.remove("end");
×
379
    }
380
  }
381

382
  // scheduled
383
  value = findValue(after, "\n  Scheduled:");
4✔
384
  if (value != "") {
4✔
385
    if (task.get("scheduled") != "") {
2✔
386
      if (value != formatDate(task, "scheduled", dateformat)) {
2✔
387
        Context::getContext().footnote(STRING_EDIT_SCHED_MOD);
×
388
        task.set("scheduled", Datetime(value, dateformat).toEpochString());
×
389
      }
390
    } else {
391
      Context::getContext().footnote(STRING_EDIT_SCHED_MOD);
×
392
      task.set("scheduled", Datetime(value, dateformat).toEpochString());
×
393
    }
394
  } else {
395
    if (task.get("scheduled") != "") {
6✔
396
      Context::getContext().footnote("Scheduled date removed.");
×
397
      task.remove("scheduled");
×
398
    }
399
  }
400

401
  // due
402
  value = findValue(after, "\n  Due:");
4✔
403
  if (value != "") {
4✔
404
    if (task.get("due") != "") {
2✔
405
      if (value != formatDate(task, "due", dateformat)) {
2✔
406
        Context::getContext().footnote(STRING_EDIT_DUE_MOD);
×
407
        task.set("due", Datetime(value, dateformat).toEpochString());
×
408
      }
409
    } else {
410
      Context::getContext().footnote(STRING_EDIT_DUE_MOD);
×
411
      task.set("due", Datetime(value, dateformat).toEpochString());
×
412
    }
413
  } else {
414
    if (task.get("due") != "") {
6✔
415
      if (task.getStatus() == Task::recurring || task.get("parent") != "") {
×
416
        Context::getContext().footnote("Cannot remove a due date from a recurring task.");
×
417
      } else {
418
        Context::getContext().footnote("Due date removed.");
×
419
        task.remove("due");
×
420
      }
421
    }
422
  }
423

424
  // until
425
  value = findValue(after, "\n  Until:");
4✔
426
  if (value != "") {
4✔
427
    if (task.get("until") != "") {
2✔
428
      if (value != formatDate(task, "until", dateformat)) {
2✔
429
        Context::getContext().footnote(STRING_EDIT_UNTIL_MOD);
×
430
        task.set("until", Datetime(value, dateformat).toEpochString());
×
431
      }
432
    } else {
433
      Context::getContext().footnote(STRING_EDIT_UNTIL_MOD);
×
434
      task.set("until", Datetime(value, dateformat).toEpochString());
×
435
    }
436
  } else {
437
    if (task.get("until") != "") {
6✔
438
      Context::getContext().footnote("Until date removed.");
×
439
      task.remove("until");
×
440
    }
441
  }
442

443
  // recur
444
  value = findValue(after, "\n  Recur:");
8✔
445
  if (value != task.get("recur")) {
8✔
446
    if (value != "") {
×
447
      Duration p;
×
448
      std::string::size_type idx = 0;
×
449
      if (p.parse(value, idx)) {
×
450
        Context::getContext().footnote("Recurrence modified.");
×
451
        if (task.get("due") != "") {
×
452
          task.set("recur", value);
×
453
          task.setStatus(Task::recurring);
×
454
        } else
455
          throw std::string("A recurring task must have a due date.");
×
456
      } else
457
        throw std::string("Not a valid recurrence duration.");
×
458
    } else {
459
      Context::getContext().footnote("Recurrence removed.");
×
460
      task.setStatus(Task::pending);
×
461
      task.remove("recur");
×
462
      task.remove("until");
×
463
      task.remove("mask");
×
464
      task.remove("imask");
×
465
    }
466
  }
467

468
  // wait
469
  value = findValue(after, "\n  Wait until:");
4✔
470
  if (value != "") {
4✔
471
    if (task.get("wait") != "") {
2✔
472
      if (value != formatDate(task, "wait", dateformat)) {
2✔
473
        Context::getContext().footnote(STRING_EDIT_WAIT_MOD);
×
474
        task.set("wait", Datetime(value, dateformat).toEpochString());
×
475
        task.setStatus(Task::waiting);
×
476
      }
477
    } else {
478
      Context::getContext().footnote(STRING_EDIT_WAIT_MOD);
×
479
      task.set("wait", Datetime(value, dateformat).toEpochString());
×
480
      task.setStatus(Task::waiting);
×
481
    }
482
  } else {
483
    if (task.get("wait") != "") {
6✔
484
      Context::getContext().footnote("Wait date removed.");
×
485
      task.remove("wait");
×
486
      task.setStatus(Task::pending);
×
487
    }
488
  }
489

490
  // parent
491
  value = findValue(after, "\n  Parent:");
8✔
492
  if (value != task.get("parent")) {
8✔
493
    if (value != "") {
×
494
      Context::getContext().footnote("Parent UUID modified.");
×
495
      task.set("parent", value);
×
496
    } else {
497
      Context::getContext().footnote("Parent UUID removed.");
×
498
      task.remove("parent");
×
499
    }
500
  }
501

502
  // Annotations
503
  std::map<std::string, std::string> annotations;
4✔
504
  std::string::size_type found = 0;
4✔
505
  while ((found = after.find("\n  Annotation:", found)) != std::string::npos) {
12✔
506
    found += 14;  // Length of "\n  Annotation:".
8✔
507

508
    auto eol = found;
8✔
509
    while ((eol = after.find('\n', ++eol)) != std::string::npos)
9✔
510
      if (after.substr(eol, ANNOTATION_EDIT_MARKER.length()) != ANNOTATION_EDIT_MARKER) break;
9✔
511

512
    if (eol != std::string::npos) {
8✔
513
      auto value = Lexer::trim(
514
          str_replace(after.substr(found, eol - found), ANNOTATION_EDIT_MARKER, "\n"), "\t ");
24✔
515
      auto gap = value.find(" -- ");
8✔
516
      if (gap != std::string::npos) {
8✔
517
        // TODO keeping the initial dates even if dateformat approximates them
518
        // is complex as finding the correspondence between each original line
519
        // and edited line may be impossible (bug #705). It would be simpler if
520
        // each annotation was put on a line with a distinguishable id (then
521
        // for each line: if the annotation is the same, then it is copied; if
522
        // the annotation is modified, then its original date may be kept; and
523
        // if there is no corresponding id, then a new unique date is created).
524
        Datetime when(value.substr(0, gap), dateformat);
4✔
525

526
        // If the map already contains an annotation for a given timestamp
527
        // we need to increment until we find an unused key
528
        int timestamp = (int)when.toEpoch();
4✔
529

530
        std::stringstream name;
4✔
531

532
        do {
533
          name.str("");  // Clear
4✔
534
          name << "annotation_" << timestamp;
4✔
535
          timestamp++;
4✔
536
        } while (annotations.find(name.str()) != annotations.end());
4✔
537

538
        auto text = Lexer::trim(value.substr(gap + 4), "\t ");
4✔
539
        annotations.emplace(name.str(), text);
4✔
540
      }
4✔
541
    }
8✔
542
  }
543

544
  task.setAnnotations(annotations);
4✔
545

546
  // Dependencies
547
  value = findValue(after, "\n  Dependencies:");
4✔
548
  auto dependencies = split(value, ',');
4✔
549

550
  for (auto& dep : task.getDependencyUUIDs()) task.removeDependency(dep);
4✔
551
  for (auto& dep : dependencies) {
4✔
552
    if (dep.length() >= 7)
×
553
      task.addDependency(dep);
×
554
    else
555
      task.addDependency((int)strtol(dep.c_str(), nullptr, 10));
×
556
  }
557

558
  // UDAs
559
  for (auto& col : Context::getContext().columns) {
104✔
560
    auto type = Context::getContext().config.get("uda." + col.first + ".type");
100✔
561
    if (type != "") {
100✔
562
      auto value = findValue(after, "\n  UDA " + col.first + ":");
8✔
563
      if (type == "string") value = json::decode(value);
8✔
564
      if ((task.get(col.first) != value) &&
8✔
565
          (type != "date" ||
2✔
566
           (task.get(col.first) != Datetime(value, dateformat).toEpochString())) &&
18✔
567
          (type != "duration" || (task.get(col.first) != Duration(value).toString()))) {
8✔
568
        if (value != "") {
×
569
          Context::getContext().footnote(format("UDA {1} modified.", col.first));
×
570

571
          if (type == "string") {
×
572
            task.set(col.first, value);
×
573
          } else if (type == "numeric") {
×
574
            Pig pig(value);
×
575
            double d;
576
            if (pig.getNumber(d) && pig.eos())
×
577
              task.set(col.first, value);
×
578
            else
579
              throw format("The value '{1}' is not a valid numeric value.", value);
×
580
          } else if (type == "date") {
×
581
            task.set(col.first, Datetime(value, dateformat).toEpochString());
×
582
          } else if (type == "duration") {
×
583
            task.set(col.first, Duration(value).toTime_t());
×
584
          }
585
        } else {
586
          Context::getContext().footnote(format("UDA {1} deleted.", col.first));
×
587
          task.remove(col.first);
×
588
        }
589
      }
590
    }
8✔
591
  }
100✔
592

593
  // UDA orphans
594
  for (auto& orphan : findValues(after, "\n  UDA Orphan ")) {
9✔
595
    auto colon = orphan.find(':');
1✔
596
    if (colon != std::string::npos) {
1✔
597
      std::string name = Lexer::trim(orphan.substr(0, colon), "\t ");
2✔
598
      std::string value = Lexer::trim(orphan.substr(colon + 1), "\t ");
1✔
599

600
      if (value != "")
1✔
601
        task.set(name, value);
1✔
602
      else
603
        task.remove(name);
×
604
    }
1✔
605
  }
4✔
606
}
4✔
607

608
////////////////////////////////////////////////////////////////////////////////
609
CmdEdit::editResult CmdEdit::editFile(Task& task) {
4✔
610
  // Check for file permissions.
611
  Directory location(Context::getContext().config.get("data.location"));
8✔
612
  if (!location.writable()) throw std::string("Your data.location directory is not writable.");
4✔
613

614
  // Create a temp file name in data.location.
615
  std::stringstream file;
4✔
616
  file << "task." << task.get("uuid").substr(0, 8) << ".task";
8✔
617

618
  // Determine the output date format, which uses a hierarchy of definitions.
619
  //   rc.dateformat.edit
620
  //   rc.dateformat
621
  auto dateformat = Context::getContext().config.get("dateformat.edit");
8✔
622
  if (dateformat == "") dateformat = Context::getContext().config.get("dateformat");
4✔
623

624
  // Change directory for the editor
625
  auto current_dir = Directory::cwd();
4✔
626
  int ignored = chdir(location._data.c_str());
4✔
627
  ++ignored;  // Keep compiler quiet.
4✔
628

629
  // Check if the file already exists, if so, bail out
630
  Path filepath = Path(file.str());
4✔
631
  if (filepath.exists()) throw std::string("Task is already being edited.");
4✔
632

633
  // Format the contents, T -> text, write to a file.
634
  auto before = formatTask(task, dateformat);
4✔
635
  auto before_orig = before;
4✔
636
  File::write(file.str(), before);
4✔
637

638
  // Determine correct editor: .taskrc:editor > $VISUAL > $EDITOR > vi
639
  auto editor = Context::getContext().config.get("editor");
8✔
640
  char* peditor = getenv("VISUAL");
4✔
641
  if (editor == "" && peditor) editor = std::string(peditor);
12✔
642
  peditor = getenv("EDITOR");
4✔
643
  if (editor == "" && peditor) editor = std::string(peditor);
4✔
644
  if (editor == "") editor = "vi";
4✔
645

646
  // Complete the command line.
647
  editor += ' ';
4✔
648
  editor += '"' + file.str() + '"';
4✔
649

650
ARE_THESE_REALLY_HARMFUL:
4✔
651
  bool changes = false;  // No changes made.
4✔
652

653
  // Launch the editor.
654
  std::cout << format("Launching '{1}' now...\n", editor);
12✔
655
  int exitcode = system(editor.c_str());
4✔
656
  auto captured_errno = errno;
4✔
657
  if (0 == exitcode)
4✔
658
    std::cout << "Editing complete.\n";
4✔
659
  else {
660
    std::cout << format("Editing failed with exit code {1}.\n", exitcode);
×
661
    if (-1 == exitcode) std::cout << std::strerror(captured_errno) << '\n';
×
662
    File::remove(file.str());
×
663
    return CmdEdit::editResult::error;
×
664
  }
665

666
  // Slurp file.
667
  std::string after;
4✔
668
  File::read(file.str(), after);
4✔
669

670
  // Update task based on what can be parsed back out of the file, but only
671
  // if changes were made.
672
  if (before_orig != after) {
4✔
673
    std::cout << "Edits were detected.\n";
4✔
674
    std::string problem = "";
4✔
675
    auto oops = false;
4✔
676

677
    try {
678
      parseTask(task, after, dateformat);
4✔
679
    }
680

681
    catch (const std::string& e) {
×
682
      problem = e;
×
683
      oops = true;
×
684
    }
×
685

686
    if (oops) {
4✔
687
      std::cerr << "Error: " << problem << '\n';
×
688

689
      File::remove(file.str());
×
690

691
      if (confirm("Taskwarrior couldn't handle your edits.  Would you like to try again?")) {
×
692
        // Preserve the edits.
693
        before = after;
×
694
        File::write(file.str(), before);
×
695

696
        goto ARE_THESE_REALLY_HARMFUL;
×
697
      }
698
    } else
699
      changes = true;
4✔
700
  } else {
4✔
701
    std::cout << "No edits were detected.\n";
×
702
    changes = false;
×
703
  }
704

705
  // Cleanup.
706
  File::remove(file.str());
4✔
707
  ignored = chdir(current_dir.c_str());
4✔
708
  return changes ? CmdEdit::editResult::changes : CmdEdit::editResult::nochanges;
4✔
709
}
4✔
710

711
////////////////////////////////////////////////////////////////////////////////
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc