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

GothenburgBitFactory / taskwarrior / 11466980179

22 Oct 2024 07:15PM UTC coverage: 84.834% (-0.03%) from 84.867%
11466980179

push

github

web-flow
Issue warnings instead of errors for 'weird' tasks (#3646)

* Issue warnings instead of errors for 'weird' tasks

* Support more comprehensive checks when adding a task

11 of 19 new or added lines in 3 files covered. (57.89%)

4 existing lines in 2 files now uncovered.

19024 of 22425 relevant lines covered (84.83%)

23058.76 hits per line

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

79.42
/src/Task.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 <Task.h>
31
#include <assert.h>
32
#include <stdlib.h>
33

34
#include <sstream>
35
#include <string>
36
#ifdef PRODUCT_TASKWARRIOR
37
#include <ctype.h>
38
#include <math.h>
39
#endif
40
#include <Lexer.h>
41

42
#include <algorithm>
43
#include <cfloat>
44
#ifdef PRODUCT_TASKWARRIOR
45
#include <Context.h>
46
#include <Pig.h>
47
#endif
48
#include <Datetime.h>
49
#include <Duration.h>
50
#ifdef PRODUCT_TASKWARRIOR
51
#include <RX.h>
52
#endif
53
#include <format.h>
54
#include <shared.h>
55
#include <util.h>
56

57
#ifdef PRODUCT_TASKWARRIOR
58
#include <Eval.h>
59
#include <Filter.h>
60
#include <Variant.h>
61
#include <main.h>
62

63
#define APPROACHING_INFINITY 1000  // Close enough.  This isn't rocket surgery.
64

65
static const float epsilon = 0.000001;
66
#endif
67

68
std::string Task::defaultProject = "";
69
std::string Task::defaultDue = "";
70
std::string Task::defaultScheduled = "";
71
bool Task::searchCaseSensitive = true;
72
bool Task::regex = false;
73
std::map<std::string, std::string> Task::attributes;
74

75
std::map<std::string, float> Task::coefficients;
76
float Task::urgencyProjectCoefficient = 0.0;
77
float Task::urgencyActiveCoefficient = 0.0;
78
float Task::urgencyScheduledCoefficient = 0.0;
79
float Task::urgencyWaitingCoefficient = 0.0;
80
float Task::urgencyBlockedCoefficient = 0.0;
81
float Task::urgencyAnnotationsCoefficient = 0.0;
82
float Task::urgencyTagsCoefficient = 0.0;
83
float Task::urgencyDueCoefficient = 0.0;
84
float Task::urgencyBlockingCoefficient = 0.0;
85
float Task::urgencyAgeCoefficient = 0.0;
86
float Task::urgencyAgeMax = 0.0;
87

88
std::map<std::string, std::vector<std::string>> Task::customOrder;
89

90
static const std::string dummy("");
91

92
////////////////////////////////////////////////////////////////////////////////
93
// The uuid and id attributes must be exempt from comparison.
94
//
95
// This performs two tests which are sufficient and necessary for Task
96
// object equality (neglecting uuid and id):
97
//     - The attribute set sizes are the same
98
//     - For each attribute in the first set, there exists a same
99
//       attribute with a same value in the second set
100
//
101
// These two conditions are necessary. They are also sufficient, since there
102
// can be no extra data attribute in the second set, due to the same attribute
103
// set sizes.
104
bool Task::operator==(const Task& other) {
150✔
105
  if (data.size() != other.data.size()) return false;
150✔
106

107
  for (const auto& i : data)
220✔
108
    if (i.first != "uuid" && i.second != other.get(i.first)) return false;
203✔
109

110
  return true;
17✔
111
}
112

113
////////////////////////////////////////////////////////////////////////////////
114
bool Task::operator!=(const Task& other) { return !(*this == other); }
147✔
115

116
////////////////////////////////////////////////////////////////////////////////
117
Task::Task(const std::string& input) {
15✔
118
  id = 0;
15✔
119
  urgency_value = 0.0;
15✔
120
  recalc_urgency = true;
15✔
121
  is_blocked = false;
15✔
122
  is_blocking = false;
15✔
123
  annotation_count = 0;
15✔
124

125
  parse(input);
15✔
126
}
15✔
127

128
////////////////////////////////////////////////////////////////////////////////
129
Task::Task(const json::object* obj) {
1,610✔
130
  id = 0;
1,610✔
131
  urgency_value = 0.0;
1,610✔
132
  recalc_urgency = true;
1,610✔
133
  is_blocked = false;
1,610✔
134
  is_blocking = false;
1,610✔
135
  annotation_count = 0;
1,610✔
136

137
  parseJSON(obj);
1,610✔
138
}
1,610✔
139

140
////////////////////////////////////////////////////////////////////////////////
141
Task::Task(rust::Box<tc::TaskData> obj) {
746,740✔
142
  id = 0;
746,740✔
143
  urgency_value = 0.0;
746,740✔
144
  recalc_urgency = true;
746,740✔
145
  is_blocked = false;
746,740✔
146
  is_blocking = false;
746,740✔
147
  annotation_count = 0;
746,740✔
148

149
  parseTC(std::move(obj));
746,740✔
150
}
746,740✔
151

152
////////////////////////////////////////////////////////////////////////////////
153
Task::status Task::textToStatus(const std::string& input) {
28,319✔
154
  if (input[0] == 'p')
28,319✔
155
    return Task::pending;
24,873✔
156
  else if (input[0] == 'c')
3,446✔
157
    return Task::completed;
1,708✔
158
  else if (input[0] == 'd')
1,738✔
159
    return Task::deleted;
778✔
160
  else if (input[0] == 'r')
960✔
161
    return Task::recurring;
958✔
162
  // for compatibility, parse `w` as pending; Task::getStatus will
163
  // apply the virtual waiting status if appropriate
164
  else if (input[0] == 'w')
2✔
165
    return Task::pending;
×
166

167
  throw format("The status '{1}' is not valid.", input);
2✔
168
}
169

170
////////////////////////////////////////////////////////////////////////////////
171
std::string Task::statusToText(Task::status s) {
7,557✔
172
  if (s == Task::pending)
7,557✔
173
    return "pending";
6,564✔
174
  else if (s == Task::recurring)
993✔
175
    return "recurring";
273✔
176
  else if (s == Task::waiting)
720✔
177
    return "waiting";
2✔
178
  else if (s == Task::completed)
718✔
179
    return "completed";
462✔
180
  else if (s == Task::deleted)
256✔
181
    return "deleted";
256✔
182

183
  return "pending";
×
184
}
185

186
////////////////////////////////////////////////////////////////////////////////
187
// Returns a proper handle to the task. Tasks should not be referenced by UUIDs
188
// as long as they have non-zero ID.
189
const std::string Task::identifier(bool shortened /* = false */) const {
979✔
190
  if (id != 0)
979✔
191
    return format(id);
942✔
192
  else if (shortened)
37✔
193
    return get("uuid").substr(0, 8);
60✔
194
  else
195
    return get("uuid");
7✔
196
}
197

198
////////////////////////////////////////////////////////////////////////////////
199
void Task::setAsNow(const std::string& att) {
6,474✔
200
  char now[22];
201
  snprintf(now, 22, "%lli", (long long int)time(nullptr));
6,474✔
202
  set(att, now);
6,474✔
203

204
  recalc_urgency = true;
6,474✔
205
}
6,474✔
206

207
////////////////////////////////////////////////////////////////////////////////
208
bool Task::has(const std::string& name) const {
203,896✔
209
  if (data.find(name) != data.end()) return true;
203,896✔
210

211
  return false;
136,401✔
212
}
213

214
////////////////////////////////////////////////////////////////////////////////
215
std::vector<std::string> Task::all() const {
752,049✔
216
  std::vector<std::string> all;
752,049✔
217
  for (const auto& i : data) all.push_back(i.first);
4,539,625✔
218

219
  return all;
752,049✔
220
}
221

222
////////////////////////////////////////////////////////////////////////////////
223
const std::string Task::get(const std::string& name) const {
848,381✔
224
  auto i = data.find(name);
848,381✔
225
  if (i != data.end()) return i->second;
848,381✔
226

227
  return "";
18,777✔
228
}
229

230
////////////////////////////////////////////////////////////////////////////////
231
const std::string& Task::get_ref(const std::string& name) const {
6,032✔
232
  auto i = data.find(name);
6,032✔
233
  if (i != data.end()) return i->second;
6,032✔
234

235
  return dummy;
2,524✔
236
}
237

238
////////////////////////////////////////////////////////////////////////////////
239
int Task::get_int(const std::string& name) const {
1✔
240
  auto i = data.find(name);
1✔
241
  if (i != data.end()) return strtol(i->second.c_str(), nullptr, 10);
1✔
242

243
  return 0;
×
244
}
245

246
////////////////////////////////////////////////////////////////////////////////
247
unsigned long Task::get_ulong(const std::string& name) const {
1✔
248
  auto i = data.find(name);
1✔
249
  if (i != data.end()) return strtoul(i->second.c_str(), nullptr, 10);
1✔
250

251
  return 0;
×
252
}
253

254
////////////////////////////////////////////////////////////////////////////////
255
float Task::get_float(const std::string& name) const {
7✔
256
  auto i = data.find(name);
7✔
257
  if (i != data.end()) return strtof(i->second.c_str(), nullptr);
7✔
258

259
  return 0.0;
×
260
}
261

262
////////////////////////////////////////////////////////////////////////////////
263
time_t Task::get_date(const std::string& name) const {
6,403✔
264
  auto i = data.find(name);
6,403✔
265
  if (i != data.end()) return (time_t)strtoul(i->second.c_str(), nullptr, 10);
6,403✔
266

267
  return 0;
51✔
268
}
269

270
////////////////////////////////////////////////////////////////////////////////
271
void Task::set(const std::string& name, const std::string& value) {
20,087✔
272
  data[name] = value;
20,087✔
273

274
  if (isAnnotationAttr(name)) ++annotation_count;
20,087✔
275

276
  recalc_urgency = true;
20,087✔
277
}
20,087✔
278

279
////////////////////////////////////////////////////////////////////////////////
280
void Task::set(const std::string& name, long long value) {
479✔
281
  data[name] = format(value);
479✔
282

283
  recalc_urgency = true;
479✔
284
}
479✔
285

286
////////////////////////////////////////////////////////////////////////////////
287
void Task::remove(const std::string& name) {
215✔
288
  if (data.erase(name)) recalc_urgency = true;
215✔
289

290
  if (isAnnotationAttr(name)) --annotation_count;
215✔
291
}
215✔
292

293
////////////////////////////////////////////////////////////////////////////////
294
Task::status Task::getStatus() const {
32,539✔
295
  if (!has("status")) return Task::pending;
32,539✔
296

297
  auto status = textToStatus(get("status"));
28,319✔
298

299
  // Implement the "virtual" Task::waiting status, which is not stored on-disk
300
  // but is defined as a pending task with a `wait` attribute in the future.
301
  // This is workaround for 2.6.0, remove in 3.0.0.
302
  if (status == Task::pending && is_waiting()) {
28,313✔
303
    return Task::waiting;
120✔
304
  }
305

306
  return status;
28,193✔
307
}
308

309
////////////////////////////////////////////////////////////////////////////////
310
void Task::setStatus(Task::status status) {
5,620✔
311
  // the 'waiting' status is a virtual version of 'pending', so translate
312
  // that back to 'pending' here
313
  if (status == Task::waiting) status = Task::pending;
5,620✔
314

315
  set("status", statusToText(status));
5,620✔
316

317
  recalc_urgency = true;
5,620✔
318
}
5,620✔
319

320
#ifdef PRODUCT_TASKWARRIOR
321
////////////////////////////////////////////////////////////////////////////////
322
// Determines status of a date attribute.
323
Task::dateState Task::getDateState(const std::string& name) const {
129✔
324
  std::string value = get(name);
129✔
325
  if (value.length()) {
129✔
326
    Datetime reference(value);
129✔
327
    Datetime now;
129✔
328
    Datetime today("today");
129✔
329

330
    if (reference < today) return dateBeforeToday;
215✔
331

332
    if (reference.sameDay(now)) {
105✔
333
      if (reference < now)
24✔
334
        return dateEarlierToday;
23✔
335
      else
336
        return dateLaterToday;
1✔
337
    }
338

339
    int imminentperiod = Context::getContext().config.getInteger("due");
81✔
340
    if (imminentperiod == 0) return dateAfterToday;
81✔
341

342
    Datetime imminentDay = today + imminentperiod * 86400;
77✔
343
    if (reference < imminentDay) return dateAfterToday;
77✔
344
  }
345

346
  return dateNotDue;
19✔
347
}
129✔
348

349
////////////////////////////////////////////////////////////////////////////////
350
// An empty task is typically a "dummy", such as in DOM evaluation, which may or
351
// may not occur in the context of a task.
352
bool Task::is_empty() const { return data.size() == 0; }
1,606✔
353

354
////////////////////////////////////////////////////////////////////////////////
355
// Ready means pending, not blocked and either not scheduled or scheduled before
356
// now.
357
bool Task::is_ready() const {
649✔
358
  return getStatus() == Task::pending && !is_blocked &&
822✔
359
         (!has("scheduled") || Datetime("now").operator>(get_date("scheduled")));
822✔
360
}
361

362
////////////////////////////////////////////////////////////////////////////////
363
bool Task::is_due() const {
147✔
364
  if (has("due")) {
147✔
365
    Task::status status = getStatus();
33✔
366

367
    if (status != Task::completed && status != Task::deleted) {
33✔
368
      Task::dateState state = getDateState("due");
33✔
369
      if (state == dateAfterToday || state == dateEarlierToday || state == dateLaterToday)
33✔
370
        return true;
23✔
371
    }
372
  }
373

374
  return false;
124✔
375
}
376

377
////////////////////////////////////////////////////////////////////////////////
378
bool Task::is_dueyesterday() const {
109✔
379
  if (has("due")) {
109✔
380
    Task::status status = getStatus();
22✔
381

382
    if (status != Task::completed && status != Task::deleted) {
22✔
383
      if (Datetime("yesterday").sameDay(get_date("due"))) return true;
22✔
384
    }
385
  }
386

387
  return false;
106✔
388
}
389

390
////////////////////////////////////////////////////////////////////////////////
391
bool Task::is_duetoday() const {
257✔
392
  if (has("due")) {
257✔
393
    Task::status status = getStatus();
56✔
394

395
    if (status != Task::completed && status != Task::deleted) {
56✔
396
      Task::dateState state = getDateState("due");
56✔
397
      if (state == dateEarlierToday || state == dateLaterToday) return true;
56✔
398
    }
399
  }
400

401
  return false;
245✔
402
}
403

404
////////////////////////////////////////////////////////////////////////////////
405
bool Task::is_duetomorrow() const {
109✔
406
  if (has("due")) {
109✔
407
    Task::status status = getStatus();
22✔
408

409
    if (status != Task::completed && status != Task::deleted) {
22✔
410
      if (Datetime("tomorrow").sameDay(get_date("due"))) return true;
22✔
411
    }
412
  }
413

414
  return false;
100✔
415
}
416

417
////////////////////////////////////////////////////////////////////////////////
418
bool Task::is_dueweek() const {
126✔
419
  if (has("due")) {
126✔
420
    Task::status status = getStatus();
25✔
421

422
    if (status != Task::completed && status != Task::deleted) {
25✔
423
      Datetime due(get_date("due"));
25✔
424
      if (due >= Datetime("sow") && due <= Datetime("eow")) return true;
25✔
425
    }
426
  }
427

428
  return false;
108✔
429
}
430

431
////////////////////////////////////////////////////////////////////////////////
432
bool Task::is_duemonth() const {
122✔
433
  if (has("due")) {
122✔
434
    Task::status status = getStatus();
25✔
435

436
    if (status != Task::completed && status != Task::deleted) {
25✔
437
      Datetime due(get_date("due"));
25✔
438
      if (due >= Datetime("som") && due <= Datetime("eom")) return true;
25✔
439
    }
440
  }
441

442
  return false;
101✔
443
}
444

445
////////////////////////////////////////////////////////////////////////////////
446
bool Task::is_duequarter() const {
102✔
447
  if (has("due")) {
102✔
448
    Task::status status = getStatus();
15✔
449

450
    if (status != Task::completed && status != Task::deleted) {
15✔
451
      Datetime due(get_date("due"));
15✔
452
      if (due >= Datetime("soq") && due <= Datetime("eoq")) return true;
15✔
453
    }
454
  }
455

456
  return false;
87✔
457
}
458

459
////////////////////////////////////////////////////////////////////////////////
460
bool Task::is_dueyear() const {
126✔
461
  if (has("due")) {
126✔
462
    Task::status status = getStatus();
25✔
463

464
    if (status != Task::completed && status != Task::deleted) {
25✔
465
      Datetime due(get_date("due"));
25✔
466
      if (due >= Datetime("soy") && due <= Datetime("eoy")) return true;
25✔
467
    }
468
  }
469

470
  return false;
101✔
471
}
472

473
////////////////////////////////////////////////////////////////////////////////
474
bool Task::is_udaPresent() const {
104✔
475
  for (auto& col : Context::getContext().columns)
2,452✔
476
    if (col.second->is_uda() && has(col.first)) return true;
2,362✔
477

478
  return false;
90✔
479
}
480

481
////////////////////////////////////////////////////////////////////////////////
482
bool Task::is_orphanPresent() const {
108✔
483
  for (auto& att : data)
789✔
484
    if (!isAnnotationAttr(att.first) && !isTagAttr(att.first) && !isDepAttr(att.first) &&
1,332✔
485
        Context::getContext().columns.find(att.first) == Context::getContext().columns.end())
1,332✔
486
      return true;
4✔
487

488
  return false;
104✔
489
}
490

491
////////////////////////////////////////////////////////////////////////////////
492
bool Task::is_overdue() const {
171✔
493
  if (has("due")) {
171✔
494
    Task::status status = getStatus();
47✔
495

496
    if (status != Task::completed && status != Task::deleted && status != Task::recurring) {
47✔
497
      Task::dateState state = getDateState("due");
39✔
498
      if (state == dateEarlierToday || state == dateBeforeToday) return true;
39✔
499
    }
500
  }
501

502
  return false;
157✔
503
}
504
#endif
505

506
////////////////////////////////////////////////////////////////////////////////
507
// Task is considered waiting if it's pending and the wait attribute is set as
508
// future datetime value.
509
// While this is not consistent with other attribute-based virtual tags, such
510
// as +BLOCKED, it is more backwards compatible with how +WAITING virtual tag
511
// behaved in the past, when waiting had a dedicated status value.
512
bool Task::is_waiting() const {
27,734✔
513
  if (has("wait") && get("status") == "pending") {
27,734✔
514
    Datetime now;
191✔
515
    Datetime wait(get_date("wait"));
191✔
516
    if (wait > now) return true;
191✔
517
  }
518

519
  return false;
27,602✔
520
}
521

522
////////////////////////////////////////////////////////////////////////////////
523
// Try a JSON parse.
524
void Task::parse(const std::string& input) {
15✔
525
  parseJSON(input);
15✔
526

527
  // for compatibility, include all tags in `tags` as `tag_..` attributes
528
  if (data.find("tags") != data.end()) {
14✔
529
    for (auto& tag : split(data["tags"], ',')) {
8✔
530
      data[tag2Attr(tag)] = "x";
5✔
531
    }
3✔
532
  }
533
  // ..and similarly, update `tags` to match the `tag_..` attributes
534
  fixTagsAttribute();
14✔
535

536
  // same for `depends` / `dep_..`
537
  if (data.find("depends") != data.end()) {
14✔
538
    for (auto& dep : split(data["depends"], ',')) {
11✔
539
      data[dep2Attr(dep)] = "x";
7✔
540
    }
4✔
541
  }
542
  fixDependsAttribute();
14✔
543

544
  recalc_urgency = true;
14✔
545
}
14✔
546

547
////////////////////////////////////////////////////////////////////////////////
548
// Note that all fields undergo encode/decode.
549
void Task::parseJSON(const std::string& line) {
15✔
550
  // Parse the whole thing.
551
  json::value* root = json::parse(line);
15✔
552
  if (root && root->type() == json::j_object) parseJSON((json::object*)root);
14✔
553

554
  delete root;
14✔
555
}
14✔
556

557
////////////////////////////////////////////////////////////////////////////////
558
void Task::parseJSON(const json::object* root_obj) {
1,624✔
559
  // For each object element...
560
  for (auto& i : root_obj->_data) {
4,052✔
561
    // If the attribute is a recognized column.
562
    std::string type = Task::attributes[i.first];
2,432✔
563
    if (type != "") {
2,432✔
564
      // Any specified id is ignored.
565
      if (i.first == "id")
2,403✔
566
        ;
567

568
      // Urgency, if present, is ignored.
569
      else if (i.first == "urgency")
2,397✔
570
        ;
571

572
      // TW-1274 Standardization.
573
      else if (i.first == "modification") {
2,391✔
574
        auto text = i.second->dump();
×
575
        Lexer::dequote(text);
×
576
        Datetime d(text);
×
577
        set("modified", d.toEpochString());
×
578
      }
579

580
      // Dates are converted from ISO to epoch.
581
      else if (type == "date") {
2,391✔
582
        auto text = i.second->dump();
337✔
583
        Lexer::dequote(text);
337✔
584
        Datetime d(text);
337✔
585
        set(i.first, text == "" ? "" : d.toEpochString());
337✔
586
      }
337✔
587

588
      // Tags are an array of JSON strings.
589
      else if (i.first == "tags" && i.second->type() == json::j_array) {
2,054✔
590
        auto tags = (json::array*)i.second;
3✔
591
        for (auto& t : tags->_data) {
8✔
592
          auto tag = (json::string*)t;
5✔
593
          addTag(tag->_data);
5✔
594
        }
595
      }
596

597
      // Dependencies can be exported as an array of strings.
598
      // 2016-02-21: This will be the only option in future releases.
599
      //             See other 2016-02-21 comments for details.
600
      else if (i.first == "depends" && i.second->type() == json::j_array) {
2,051✔
601
        auto deps = (json::array*)i.second;
4✔
602
        for (auto& t : deps->_data) {
10✔
603
          auto dep = (json::string*)t;
6✔
604
          addDependency(dep->_data);
6✔
605
        }
606
      }
607

608
      // Dependencies can be exported as a single comma-separated string.
609
      // 2016-02-21: Deprecated - see other 2016-02-21 comments for details.
610
      else if (i.first == "depends" && i.second->type() == json::j_string) {
2,047✔
611
        auto deps = (json::string*)i.second;
3✔
612

613
        // Fix for issue#2689: taskserver sometimes encodes the depends
614
        // property as a string of the format `[\"uuid\",\"uuid\"]`
615
        // The string includes the backslash-escaped `"` characters, making
616
        // it invalid JSON.  Since we know the characters we're looking for,
617
        // we'll just filter out everything else.
618
        std::string deps_str = deps->_data;
3✔
619
        if (deps_str.front() == '[' && deps_str.back() == ']') {
3✔
620
          std::string filtered;
1✔
621
          for (auto& c : deps_str) {
84✔
622
            if ((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || c == ',' || c == '-') {
83✔
623
              filtered.push_back(c);
73✔
624
            }
625
          }
626
          deps_str = filtered;
1✔
627
        }
1✔
628
        auto uuids = split(deps_str, ',');
3✔
629

630
        for (const auto& uuid : uuids) addDependency(uuid);
9✔
631
      }
3✔
632

633
      // Strings are decoded.
634
      else if (type == "string") {
2,044✔
635
        auto text = i.second->dump();
2,044✔
636
        Lexer::dequote(text);
2,044✔
637
        set(i.first, json::decode(text));
2,044✔
638
      }
2,044✔
639

640
      // Other types are simply added.
641
      else {
642
        auto text = i.second->dump();
×
643
        Lexer::dequote(text);
×
644
        set(i.first, text);
×
645
      }
646
    }
647

648
    // UDA orphans and annotations do not have columns.
649
    else {
650
      // Annotations are an array of JSON objects with 'entry' and
651
      // 'description' values and must be converted.
652
      if (i.first == "annotations") {
29✔
653
        std::map<std::string, std::string> annos;
6✔
654

655
        // Fail if 'annotations' is not an array
656
        if (i.second->type() != json::j_array) {
6✔
657
          throw format("Annotations is malformed: {1}", i.second->dump());
2✔
658
        }
659

660
        auto atts = (json::array*)i.second;
4✔
661
        for (auto& annotations : atts->_data) {
7✔
662
          auto annotation = (json::object*)annotations;
5✔
663

664
          // Extract description. Fail if not present.
665
          auto what = (json::string*)annotation->_data["description"];
5✔
666
          if (!what) {
5✔
667
            annotation->_data.erase(
2✔
668
                "description");  // Erase NULL description inserted by failed lookup above
669
            throw format("Annotation is missing a description: {1}", annotation->dump());
2✔
670
          }
671

672
          // Extract 64-bit annotation entry value
673
          // Time travelers from 2038, we have your back.
674
          long long ann_timestamp;
675

676
          // Extract entry. Use current time if not present.
677
          auto when = (json::string*)annotation->_data["entry"];
3✔
678
          if (when)
3✔
679
            ann_timestamp = (long long)(Datetime(when->_data).toEpoch());
2✔
680
          else {
681
            annotation->_data.erase("entry");  // Erase NULL entry inserted by failed lookup above
1✔
682
            ann_timestamp = (long long)(Datetime().toEpoch());
1✔
683
          }
684

685
          std::stringstream name;
3✔
686
          name << "annotation_" << ann_timestamp;
3✔
687

688
          // Increment the entry timestamp in case of a conflict. Same
689
          // behaviour as CmdAnnotate.
690
          while (annos.find(name.str()) != annos.end()) {
4✔
691
            name.str("");  // Clear
1✔
692
            ann_timestamp++;
1✔
693
            name << "annotation_" << ann_timestamp;
1✔
694
          }
695

696
          annos.emplace(name.str(), json::decode(what->_data));
3✔
697
        }
3✔
698

699
        setAnnotations(annos);
2✔
700
      }
6✔
701

702
      // UDA Orphan - must be preserved.
703
      else {
704
#ifdef PRODUCT_TASKWARRIOR
705
        std::stringstream message;
23✔
706
        message << "Task::parseJSON found orphan '" << i.first << "' with value '" << i.second
23✔
707
                << "' --> preserved\n";
23✔
708
        Context::getContext().debug(message.str());
23✔
709
#endif
710
        auto text = i.second->dump();
23✔
711
        Lexer::dequote(text);
23✔
712
        set(i.first, json::decode(text));
23✔
713
      }
23✔
714
    }
715
  }
2,432✔
716
}
1,620✔
717

718
////////////////////////////////////////////////////////////////////////////////
719
// Note that all fields undergo encode/decode.
720
void Task::parseTC(rust::Box<tc::TaskData> task) {
746,740✔
721
  auto items = task->items();
746,740✔
722
  data.clear();
746,740✔
723
  for (auto& item : items) {
3,754,788✔
724
    data[static_cast<std::string>(item.prop)] = static_cast<std::string>(item.value);
3,008,048✔
725
  }
726

727
  // count annotations
728
  annotation_count = 0;
746,740✔
729
  for (auto i : data) {
3,754,788✔
730
    if (isAnnotationAttr(i.first)) {
3,008,048✔
731
      ++annotation_count;
1,066✔
732
    }
733
  }
3,008,048✔
734

735
  data["uuid"] = static_cast<std::string>(task->get_uuid().to_string());
746,740✔
736
  id = Context::getContext().tdb2.id(data["uuid"]);
746,740✔
737
}
746,740✔
738

739
////////////////////////////////////////////////////////////////////////////////
740
// No legacy formats are currently supported as of 2.4.0.
741
void Task::parseLegacy(const std::string& line) {
×
742
  switch (determineVersion(line)) {
×
743
    // File format version 1, from 2006-11-27 - 2007-12-31, v0.x+ - v0.9.3
744
    case 1:
×
745
      throw std::string(
×
746
          "Taskwarrior no longer supports file format 1, originally used between 27 November 2006 "
747
          "and 31 December 2007.");
×
748

749
    // File format version 2, from 2008-1-1 - 2009-3-23, v0.9.3 - v1.5.0
750
    case 2:
×
751
      throw std::string(
×
752
          "Taskwarrior no longer supports file format 2, originally used between 1 January 2008 "
753
          "and 12 April 2009.");
×
754

755
    // File format version 3, from 2009-3-23 - 2009-05-16, v1.6.0 - v1.7.1
756
    case 3:
×
757
      throw std::string(
×
758
          "Taskwarrior no longer supports file format 3, originally used between 23 March 2009 and "
759
          "16 May 2009.");
×
760

761
    // File format version 4, from 2009-05-16 - today, v1.7.1+
762
    case 4:
×
763
      break;
×
764

765
    default:
×
766
#ifdef PRODUCT_TASKWARRIOR
767
      std::stringstream message;
×
768
      message << "Invalid fileformat at line '" << line << '\'';
×
769
      Context::getContext().debug(message.str());
×
770
#endif
771
      throw std::string("Unrecognized Taskwarrior file format or blank line in data.");
×
772
      break;
773
  }
774

775
  recalc_urgency = true;
×
776
}
777

778
////////////////////////////////////////////////////////////////////////////////
779
std::string Task::composeJSON(bool decorate /*= false*/) const {
210✔
780
  std::stringstream out;
210✔
781
  out << '{';
210✔
782

783
  // ID inclusion is optional, but not a good idea, because it remains correct
784
  // only until the next gc.
785
  if (decorate) out << "\"id\":" << id << ',';
210✔
786

787
  // First the non-annotations.
788
  int attributes_written = 0;
210✔
789
  for (auto& i : data) {
1,697✔
790
    // Annotations are not written out here.
791
    if (!i.first.compare(0, 11, "annotation_", 11)) continue;
1,701✔
792

793
    // Tags and dependencies are handled below
794
    if (i.first == "tags" || isTagAttr(i.first)) continue;
1,487✔
795
    if (i.first == "depends" || isDepAttr(i.first)) continue;
1,298✔
796

797
    // If value is an empty string, do not ever output it
798
    if (i.second == "") continue;
1,273✔
799

800
    if (attributes_written) out << ',';
1,273✔
801

802
    std::string type = Task::attributes[i.first];
1,273✔
803
    if (type == "") type = "string";
1,273✔
804

805
    // Date fields are written as ISO 8601.
806
    if (type == "date") {
1,273✔
807
      Datetime d(i.second);
537✔
808
      out << '"' << (i.first == "modification" ? "modified" : i.first)
1,074✔
809
          << "\":\""
810
          // Date was deleted, do not export parsed empty string
811
          << (i.second == "" ? "" : d.toISO()) << '"';
1,074✔
812

813
      ++attributes_written;
537✔
814
    }
815

816
    /*
817
        else if (type == "duration")
818
        {
819
          // TODO Emit Datetime
820
        }
821
    */
822
    else if (type == "numeric") {
736✔
823
      out << '"' << i.first << "\":" << i.second;
14✔
824

825
      ++attributes_written;
14✔
826
    }
827

828
    // Everything else is a quoted value.
829
    else {
830
      out << '"' << i.first << "\":\"" << (type == "string" ? json::encode(i.second) : i.second)
1,444✔
831
          << '"';
1,444✔
832

833
      ++attributes_written;
722✔
834
    }
835
  }
1,273✔
836

837
  // Now the annotations, if any.
838
  if (annotation_count) {
210✔
839
    out << ',' << "\"annotations\":[";
×
840

841
    int annotations_written = 0;
×
842
    for (auto& i : data) {
×
843
      if (!i.first.compare(0, 11, "annotation_", 11)) {
×
844
        if (annotations_written) out << ',';
×
845

846
        Datetime d(i.first.substr(11));
×
847
        out << R"({"entry":")" << d.toISO() << R"(","description":")" << json::encode(i.second)
×
848
            << "\"}";
×
849

850
        ++annotations_written;
×
851
      }
852
    }
853

854
    out << ']';
×
855
  }
856

857
  auto tags = getTags();
210✔
858
  if (tags.size() > 0) {
210✔
859
    out << ',' << "\"tags\":[";
88✔
860

861
    int count = 0;
88✔
862
    for (const auto& tag : tags) {
189✔
863
      if (count++) out << ',';
101✔
864

865
      out << '"' << tag << '"';
101✔
866
    }
867

868
    out << ']';
88✔
869
    ++attributes_written;
88✔
870
  }
871

872
  auto depends = getDependencyUUIDs();
210✔
873
  if (depends.size() > 0) {
210✔
874
    out << ',' << "\"depends\":[";
11✔
875

876
    int count = 0;
11✔
877
    for (const auto& dep : depends) {
25✔
878
      if (count++) out << ',';
14✔
879

880
      out << '"' << dep << '"';
14✔
881
    }
882

883
    out << ']';
11✔
884
    ++attributes_written;
11✔
885
  }
886

887
#ifdef PRODUCT_TASKWARRIOR
888
  // Include urgency.
889
  if (decorate) out << ',' << "\"urgency\":" << urgency_c();
210✔
890
#endif
891

892
  out << '}';
210✔
893
  return out.str();
420✔
894
}
210✔
895

896
////////////////////////////////////////////////////////////////////////////////
897
int Task::getAnnotationCount() const {
4✔
898
  int count = 0;
4✔
899
  for (auto& ann : data)
34✔
900
    if (!ann.first.compare(0, 11, "annotation_", 11)) ++count;
30✔
901

902
  return count;
4✔
903
}
904

905
////////////////////////////////////////////////////////////////////////////////
906
bool Task::hasAnnotations() const { return annotation_count ? true : false; }
122✔
907

908
////////////////////////////////////////////////////////////////////////////////
909
// The timestamp is part of the name:
910
//    annotation_1234567890:"..."
911
//
912
// Note that the time is incremented (one second) in order to find a unique
913
// timestamp.
914
void Task::addAnnotation(const std::string& description) {
90✔
915
  time_t now = time(nullptr);
90✔
916
  std::string key;
90✔
917

918
  do {
919
    key = "annotation_" + format((long long int)now);
165✔
920
    ++now;
165✔
921
  } while (has(key));
165✔
922

923
  data[key] = json::decode(description);
90✔
924
  ++annotation_count;
90✔
925
  recalc_urgency = true;
90✔
926
}
90✔
927

928
////////////////////////////////////////////////////////////////////////////////
929
void Task::removeAnnotations() {
43✔
930
  // Erase old annotations.
931
  auto i = data.begin();
43✔
932
  while (i != data.end()) {
295✔
933
    if (!i->first.compare(0, 11, "annotation_", 11)) {
252✔
934
      --annotation_count;
23✔
935
      data.erase(i++);
23✔
936
    } else
937
      i++;
229✔
938
  }
939

940
  recalc_urgency = true;
43✔
941
}
43✔
942

943
////////////////////////////////////////////////////////////////////////////////
944
std::map<std::string, std::string> Task::getAnnotations() const {
801✔
945
  std::map<std::string, std::string> a;
801✔
946
  for (auto& ann : data)
6,123✔
947
    if (!ann.first.compare(0, 11, "annotation_", 11)) a.insert(ann);
5,322✔
948

949
  return a;
801✔
950
}
951

952
////////////////////////////////////////////////////////////////////////////////
953
void Task::setAnnotations(const std::map<std::string, std::string>& annotations) {
43✔
954
  // Erase old annotations.
955
  removeAnnotations();
43✔
956

957
  for (auto& anno : annotations) data.insert(anno);
63✔
958

959
  annotation_count = annotations.size();
43✔
960
  recalc_urgency = true;
43✔
961
}
43✔
962

963
#ifdef PRODUCT_TASKWARRIOR
964
////////////////////////////////////////////////////////////////////////////////
965
void Task::addDependency(int depid) {
55✔
966
  // Check that id is resolvable.
967
  std::string uuid = Context::getContext().tdb2.uuid(depid);
55✔
968
  if (uuid == "") throw format("Could not create a dependency on task {1} - not found.", depid);
55✔
969

970
  // the addDependency(&std::string) overload will check this, too, but here we
971
  // can give an more natural error message containing the id the user
972
  // provided.
973
  if (hasDependency(uuid)) {
54✔
974
    Context::getContext().footnote(format("Task {1} already depends on task {2}.", id, depid));
2✔
975
    return;
2✔
976
  }
977

978
  addDependency(uuid);
52✔
979
}
55✔
980
#endif
981

982
////////////////////////////////////////////////////////////////////////////////
983
void Task::addDependency(const std::string& uuid) {
70✔
984
  if (uuid == get("uuid")) throw std::string("A task cannot be dependent on itself.");
70✔
985

986
  if (hasDependency(uuid)) {
69✔
987
#ifdef PRODUCT_TASKWARRIOR
988
    Context::getContext().footnote(
×
989
        format("Task {1} already depends on task {2}.", get("uuid"), uuid));
×
990
#endif
991
    return;
×
992
  }
993

994
  // Store the dependency.
995
  set(dep2Attr(uuid), "x");
69✔
996

997
  // Prevent circular dependencies.
998
#ifdef PRODUCT_TASKWARRIOR
999
  if (dependencyIsCircular(*this))
69✔
1000
    throw std::string("Circular dependency detected and disallowed.");
2✔
1001
#endif
1002

1003
  recalc_urgency = true;
67✔
1004
  fixDependsAttribute();
67✔
1005
}
1006

1007
#ifdef PRODUCT_TASKWARRIOR
1008
////////////////////////////////////////////////////////////////////////////////
1009
void Task::removeDependency(int id) {
4✔
1010
  std::string uuid = Context::getContext().tdb2.uuid(id);
4✔
1011

1012
  // The removeDependency(std::string&) method will check this too, but here we
1013
  // can give a more natural error message containing the id provided by the user
1014
  if (uuid == "" || !has(dep2Attr(uuid)))
4✔
1015
    throw format("Could not delete a dependency on task {1} - not found.", id);
1✔
1016
  removeDependency(uuid);
3✔
1017
}
4✔
1018

1019
////////////////////////////////////////////////////////////////////////////////
1020
void Task::removeDependency(const std::string& uuid) {
9✔
1021
  auto depattr = dep2Attr(uuid);
9✔
1022
  if (has(depattr))
9✔
1023
    remove(depattr);
9✔
1024
  else
1025
    throw format("Could not delete a dependency on task {1} - not found.", uuid);
×
1026

1027
  recalc_urgency = true;
9✔
1028
  fixDependsAttribute();
9✔
1029
}
9✔
1030

1031
////////////////////////////////////////////////////////////////////////////////
1032
bool Task::hasDependency(const std::string& uuid) const {
1,275✔
1033
  auto depattr = dep2Attr(uuid);
1,275✔
1034
  return has(depattr);
2,550✔
1035
}
1,275✔
1036

1037
////////////////////////////////////////////////////////////////////////////////
1038
std::vector<int> Task::getDependencyIDs() const {
×
1039
  std::vector<int> ids;
×
1040
  for (auto& attr : all()) {
×
1041
    if (!isDepAttr(attr)) continue;
×
1042
    auto dep = attr2Dep(attr);
×
1043
    ids.push_back(Context::getContext().tdb2.id(dep));
×
1044
  }
1045

1046
  return ids;
×
1047
}
1048

1049
////////////////////////////////////////////////////////////////////////////////
1050
std::vector<std::string> Task::getDependencyUUIDs() const {
748,398✔
1051
  std::vector<std::string> uuids;
748,398✔
1052
  for (auto& attr : all()) {
4,514,130✔
1053
    if (!isDepAttr(attr)) continue;
3,765,732✔
1054
    auto dep = attr2Dep(attr);
604✔
1055
    uuids.push_back(dep);
604✔
1056
  }
749,002✔
1057

1058
  return uuids;
748,398✔
1059
}
1060

1061
////////////////////////////////////////////////////////////////////////////////
1062
std::vector<Task> Task::getDependencyTasks() const {
1,522✔
1063
  auto uuids = getDependencyUUIDs();
1,522✔
1064

1065
  // NOTE: this may seem inefficient, but note that `TDB2::get` performs a
1066
  // linear search on each invocation, so scanning *once* is quite a bit more
1067
  // efficient.
1068
  std::vector<Task> blocking;
1,522✔
1069
  if (uuids.size() > 0)
1,522✔
1070
    for (auto& it : Context::getContext().tdb2.pending_tasks())
479✔
1071
      if (it.getStatus() != Task::completed && it.getStatus() != Task::deleted &&
799✔
1072
          std::find(uuids.begin(), uuids.end(), it.get("uuid")) != uuids.end())
799✔
1073
        blocking.push_back(it);
128✔
1074

1075
  return blocking;
3,044✔
1076
}
1,522✔
1077

1078
////////////////////////////////////////////////////////////////////////////////
1079
std::vector<Task> Task::getBlockedTasks() const {
415✔
1080
  auto uuid = get("uuid");
830✔
1081

1082
  std::vector<Task> blocked;
415✔
1083
  for (auto& it : Context::getContext().tdb2.pending_tasks())
2,251✔
1084
    if (it.getStatus() != Task::completed && it.getStatus() != Task::deleted &&
2,957✔
1085
        it.hasDependency(uuid))
1,121✔
1086
      blocked.push_back(it);
434✔
1087

1088
  return blocked;
830✔
1089
}
415✔
1090
#endif
1091

1092
////////////////////////////////////////////////////////////////////////////////
1093
int Task::getTagCount() const {
1,213✔
1094
  auto count = 0;
1,213✔
1095
  for (auto& attr : data) {
9,414✔
1096
    if (isTagAttr(attr.first)) {
8,201✔
1097
      count++;
278✔
1098
    }
1099
  }
1100
  return count;
1,213✔
1101
}
1102

1103
////////////////////////////////////////////////////////////////////////////////
1104
//
1105
//              OVERDUE YESTERDAY DUE TODAY TOMORROW WEEK MONTH YEAR
1106
// due:-1week      Y       -       -    -       -      ?    ?     ?
1107
// due:-1day       Y       Y       -    -       -      ?    ?     ?
1108
// due:today       Y       -       Y    Y       -      ?    ?     ?
1109
// due:tomorrow    -       -       Y    -       Y      ?    ?     ?
1110
// due:3days       -       -       Y    -       -      ?    ?     ?
1111
// due:1month      -       -       -    -       -      -    -     ?
1112
// due:1year       -       -       -    -       -      -    -     -
1113
//
1114
bool Task::hasTag(const std::string& tag) const {
8,160✔
1115
  // Synthetic tags - dynamically generated, but do not occupy storage space.
1116
  // Note: This list must match that in CmdInfo::execute.
1117
  // Note: This list must match that in ::feedback_reserved_tags.
1118
  if (isupper(tag[0])) {
8,160✔
1119
    // NOTE: This list should be kept synchronized with:
1120
    // * the list in CmdTags.cpp for the _tags command.
1121
    // * the list in CmdInfo.cpp for the info command.
1122
    if (tag == "BLOCKED") return is_blocked;
6,187✔
1123
    if (tag == "UNBLOCKED") return !is_blocked;
6,054✔
1124
    if (tag == "BLOCKING") return is_blocking;
5,929✔
1125
#ifdef PRODUCT_TASKWARRIOR
1126
    if (tag == "READY") return is_ready();
5,796✔
1127
    if (tag == "DUE") return is_due();
5,147✔
1128
    if (tag == "DUETODAY") return is_duetoday();  // 2016-03-29: Deprecated in 2.6.0
5,038✔
1129
    if (tag == "TODAY") return is_duetoday();
4,929✔
1130
    if (tag == "YESTERDAY") return is_dueyesterday();
4,819✔
1131
    if (tag == "TOMORROW") return is_duetomorrow();
4,710✔
1132
    if (tag == "OVERDUE") return is_overdue();
4,601✔
1133
    if (tag == "WEEK") return is_dueweek();
4,468✔
1134
    if (tag == "MONTH") return is_duemonth();
4,342✔
1135
    if (tag == "QUARTER") return is_duequarter();
4,220✔
1136
    if (tag == "YEAR") return is_dueyear();
4,118✔
1137
#endif
1138
    if (tag == "ACTIVE") return has("start");
3,992✔
1139
    if (tag == "SCHEDULED") return has("scheduled");
3,864✔
1140
    if (tag == "CHILD") return has("parent") || has("template");  // 2017-01-07: Deprecated in 2.6.0
3,740✔
1141
    if (tag == "INSTANCE") return has("template") || has("parent");
3,635✔
1142
    if (tag == "UNTIL") return has("until");
3,517✔
1143
    if (tag == "ANNOTATED") return hasAnnotations();
3,391✔
1144
    if (tag == "TAGGED") return getTagCount() > 0;
3,269✔
1145
    if (tag == "PARENT") return has("mask") || has("last");  // 2017-01-07: Deprecated in 2.6.0
3,147✔
1146
    if (tag == "TEMPLATE") return has("last") || has("mask");
3,042✔
1147
    if (tag == "WAITING") return is_waiting();
2,924✔
1148
    if (tag == "PENDING") return getStatus() == Task::pending;
1,138✔
1149
    if (tag == "COMPLETED") return getStatus() == Task::completed;
994✔
1150
    if (tag == "DELETED") return getStatus() == Task::deleted;
850✔
1151
#ifdef PRODUCT_TASKWARRIOR
1152
    if (tag == "UDA") return is_udaPresent();
726✔
1153
    if (tag == "ORPHAN") return is_orphanPresent();
622✔
1154
    if (tag == "LATEST") return id == Context::getContext().tdb2.latest_id();
514✔
1155
#endif
1156
    if (tag == "PROJECT") return has("project");
347✔
1157
    if (tag == "PRIORITY") return has("priority");
245✔
1158
  }
1159

1160
  // Concrete tags.
1161
  if (has(tag2Attr(tag))) return true;
2,116✔
1162

1163
  return false;
1,942✔
1164
}
1165

1166
////////////////////////////////////////////////////////////////////////////////
1167
void Task::addTag(const std::string& tag) {
203✔
1168
  auto attr = tag2Attr(tag);
203✔
1169
  if (!has(attr)) {
203✔
1170
    set(attr, "x");
199✔
1171
    recalc_urgency = true;
199✔
1172
    fixTagsAttribute();
199✔
1173
  }
1174
}
203✔
1175

1176
////////////////////////////////////////////////////////////////////////////////
1177
void Task::setTags(const std::vector<std::string>& tags) {
8✔
1178
  auto existing = getTags();
8✔
1179

1180
  // edit in-place, determining which should be
1181
  // added and which should be removed
1182
  std::vector<std::string> toAdd;
8✔
1183
  std::vector<std::string> toRemove;
8✔
1184

1185
  for (auto& tag : tags) {
20✔
1186
    if (std::find(existing.begin(), existing.end(), tag) == existing.end()) toAdd.push_back(tag);
12✔
1187
  }
1188

1189
  for (auto& tag : getTags()) {
11✔
1190
    if (std::find(tags.begin(), tags.end(), tag) == tags.end()) {
3✔
1191
      toRemove.push_back(tag);
1✔
1192
    }
1193
  }
8✔
1194

1195
  for (auto& tag : toRemove) {
9✔
1196
    removeTag(tag);
1✔
1197
  }
1198
  for (auto& tag : toAdd) {
18✔
1199
    addTag(tag);
10✔
1200
  }
1201

1202
  // (note: addTag / removeTag took care of recalculating urgency)
1203
}
8✔
1204

1205
////////////////////////////////////////////////////////////////////////////////
1206
std::vector<std::string> Task::getTags() const {
862✔
1207
  std::vector<std::string> tags;
862✔
1208

1209
  for (auto& attr : data) {
6,028✔
1210
    if (!isTagAttr(attr.first)) {
5,166✔
1211
      continue;
4,640✔
1212
    }
1213
    auto tag = attr2Tag(attr.first);
526✔
1214
    tags.push_back(tag);
526✔
1215
  }
526✔
1216

1217
  return tags;
862✔
1218
}
1219

1220
////////////////////////////////////////////////////////////////////////////////
1221
void Task::removeTag(const std::string& tag) {
12✔
1222
  auto attr = tag2Attr(tag);
12✔
1223
  if (has(attr)) {
12✔
1224
    data.erase(attr);
11✔
1225
    recalc_urgency = true;
11✔
1226
    fixTagsAttribute();
11✔
1227
  }
1228
}
12✔
1229

1230
////////////////////////////////////////////////////////////////////////////////
1231
void Task::fixTagsAttribute() {
224✔
1232
  // Fix up the old `tags` attribute to match the `tag_..` attributes (or
1233
  // remove it if there are no tags)
1234
  auto tags = getTags();
224✔
1235
  if (tags.size() > 0) {
224✔
1236
    set("tags", join(",", tags));
209✔
1237
  } else {
1238
    remove("tags");
15✔
1239
  }
1240
}
224✔
1241

1242
////////////////////////////////////////////////////////////////////////////////
1243
bool Task::isTagAttr(const std::string& attr) { return attr.compare(0, 4, "tag_") == 0; }
16,501✔
1244

1245
////////////////////////////////////////////////////////////////////////////////
1246
const std::string Task::tag2Attr(const std::string& tag) const {
2,336✔
1247
  std::stringstream tag_attr;
2,336✔
1248
  tag_attr << "tag_" << tag;
2,336✔
1249
  return tag_attr.str();
4,672✔
1250
}
2,336✔
1251

1252
////////////////////////////////////////////////////////////////////////////////
1253
const std::string Task::attr2Tag(const std::string& attr) const {
569✔
1254
  assert(isTagAttr(attr));
569✔
1255
  return attr.substr(4);
569✔
1256
}
1257

1258
////////////////////////////////////////////////////////////////////////////////
1259
void Task::fixDependsAttribute() {
90✔
1260
  // Fix up the old `depends` attribute to match the `dep_..` attributes (or
1261
  // remove it if there are no deps)
1262
  auto deps = getDependencyUUIDs();
90✔
1263
  if (deps.size() > 0) {
90✔
1264
    set("depends", join(",", deps));
74✔
1265
  } else {
1266
    remove("depends");
16✔
1267
  }
1268
}
90✔
1269

1270
////////////////////////////////////////////////////////////////////////////////
1271
bool Task::isDepAttr(const std::string& attr) { return attr.compare(0, 4, "dep_") == 0; }
3,768,763✔
1272

1273
////////////////////////////////////////////////////////////////////////////////
1274
const std::string Task::dep2Attr(const std::string& tag) const {
1,364✔
1275
  std::stringstream tag_attr;
1,364✔
1276
  tag_attr << "dep_" << tag;
1,364✔
1277
  return tag_attr.str();
2,728✔
1278
}
1,364✔
1279

1280
////////////////////////////////////////////////////////////////////////////////
1281
const std::string Task::attr2Dep(const std::string& attr) const {
643✔
1282
  assert(isDepAttr(attr));
643✔
1283
  return attr.substr(4);
643✔
1284
}
1285

1286
////////////////////////////////////////////////////////////////////////////////
1287
bool Task::isAnnotationAttr(const std::string& attr) {
3,029,632✔
1288
  return attr.compare(0, 11, "annotation_") == 0;
3,029,632✔
1289
}
1290

1291
#ifdef PRODUCT_TASKWARRIOR
1292
////////////////////////////////////////////////////////////////////////////////
1293
// A UDA Orphan is an attribute that is not represented in context.columns.
1294
std::vector<std::string> Task::getUDAOrphans() const {
9✔
1295
  std::vector<std::string> orphans;
9✔
1296
  for (auto& it : data)
84✔
1297
    if (Context::getContext().columns.find(it.first) == Context::getContext().columns.end())
75✔
1298
      if (not(isAnnotationAttr(it.first) || isTagAttr(it.first) || isDepAttr(it.first)))
12✔
1299
        orphans.push_back(it.first);
3✔
1300

1301
  return orphans;
9✔
1302
}
1303

1304
////////////////////////////////////////////////////////////////////////////////
1305
void Task::substitute(const std::string& from, const std::string& to, const std::string& flags) {
35✔
1306
  bool global = (flags.find('g') != std::string::npos ? true : false);
35✔
1307

1308
  // Get the data to modify.
1309
  std::string description = get("description");
70✔
1310
  auto annotations = getAnnotations();
35✔
1311

1312
  // Count the changes, so we know whether to proceed to annotations, after
1313
  // modifying description.
1314
  int changes = 0;
35✔
1315
  bool done = false;
35✔
1316

1317
  // Regex support is optional.
1318
  if (Task::regex) {
35✔
1319
    // Create the regex.
1320
    RX rx(from, Task::searchCaseSensitive);
29✔
1321
    std::vector<int> start;
29✔
1322
    std::vector<int> end;
29✔
1323

1324
    // Perform all subs on description.
1325
    if (rx.match(start, end, description)) {
29✔
1326
      int skew = 0;
21✔
1327
      for (unsigned int i = 0; i < start.size() && !done; ++i) {
43✔
1328
        description.replace(start[i] + skew, end[i] - start[i], to);
22✔
1329
        skew += to.length() - (end[i] - start[i]);
22✔
1330
        ++changes;
22✔
1331

1332
        if (!global) done = true;
22✔
1333
      }
1334
    }
1335

1336
    if (!done) {
29✔
1337
      // Perform all subs on annotations.
1338
      for (auto& it : annotations) {
17✔
1339
        start.clear();
8✔
1340
        end.clear();
8✔
1341
        if (rx.match(start, end, it.second)) {
8✔
1342
          int skew = 0;
4✔
1343
          for (unsigned int i = 0; i < start.size() && !done; ++i) {
9✔
1344
            it.second.replace(start[i + skew], end[i] - start[i], to);
5✔
1345
            skew += to.length() - (end[i] - start[i]);
5✔
1346
            ++changes;
5✔
1347

1348
            if (!global) done = true;
5✔
1349
          }
1350
        }
1351
      }
1352
    }
1353
  } else {
29✔
1354
    // Perform all subs on description.
1355
    int counter = 0;
6✔
1356
    std::string::size_type pos = 0;
6✔
1357
    int skew = 0;
6✔
1358

1359
    while ((pos = ::find(description, from, pos, Task::searchCaseSensitive)) != std::string::npos &&
23✔
1360
           !done) {
10✔
1361
      description.replace(pos + skew, from.length(), to);
7✔
1362
      skew += to.length() - from.length();
7✔
1363

1364
      pos += to.length();
7✔
1365
      ++changes;
7✔
1366

1367
      if (!global) done = true;
7✔
1368

1369
      if (++counter > APPROACHING_INFINITY)
7✔
1370
        throw format(
×
1371
            "Terminated substitution because more than {1} changes were made - infinite loop "
1372
            "protection.",
1373
            APPROACHING_INFINITY);
×
1374
    }
1375

1376
    if (!done) {
6✔
1377
      // Perform all subs on annotations.
1378
      counter = 0;
1✔
1379
      for (auto& anno : annotations) {
1✔
1380
        pos = 0;
×
1381
        skew = 0;
×
1382
        while ((pos = ::find(anno.second, from, pos, Task::searchCaseSensitive)) !=
×
1383
                   std::string::npos &&
×
1384
               !done) {
×
1385
          anno.second.replace(pos + skew, from.length(), to);
×
1386
          skew += to.length() - from.length();
×
1387

1388
          pos += to.length();
×
1389
          ++changes;
×
1390

1391
          if (!global) done = true;
×
1392

1393
          if (++counter > APPROACHING_INFINITY)
×
1394
            throw format(
×
1395
                "Terminated substitution because more than {1} changes were made - infinite loop "
1396
                "protection.",
1397
                APPROACHING_INFINITY);
×
1398
        }
1399
      }
1400
    }
1401
  }
1402

1403
  if (changes) {
35✔
1404
    set("description", description);
31✔
1405
    setAnnotations(annotations);
31✔
1406
    recalc_urgency = true;
31✔
1407
  }
1408
}
35✔
1409
#endif
1410

1411
////////////////////////////////////////////////////////////////////////////////
1412
// Validate a task for addition, raising user-visible errors for inconsistent or
1413
// incorrect inputs. This is called before `Task::validate`.
1414
void Task::validate_add() {
2,986✔
1415
  // There is no fixing a missing description.
1416
  if (!has("description"))
2,986✔
NEW
1417
    throw std::string("A task must have a description.");
×
1418
  else if (get("description") == "")
2,986✔
NEW
1419
    throw std::string("Cannot add a task that is blank.");
×
1420

1421
  // Cannot have an old-style recur frequency with no due date - when would it recur?
1422
  if (has("recur") && (!has("due") || get("due") == ""))
2,986✔
1423
    throw std::string("A recurring task must also have a 'due' date.");
1✔
1424
}
2,985✔
1425

1426
////////////////////////////////////////////////////////////////////////////////
1427
// The purpose of Task::validate is three-fold:
1428
//   1) To provide missing attributes where possible
1429
//   2) To provide suitable warnings about odd states
1430
//   3) To update status depending on other attributes
1431
//
1432
// As required by TaskChampion, no combination of properties and values is an
1433
// error. This function will try to make sensible defaults and resolve inconsistencies.
1434
// Critically, note that despite the name this is not a read-only function.
1435
//
1436
void Task::validate(bool applyDefault /* = true */) {
5,165✔
1437
  Task::status status = Task::pending;
5,165✔
1438
  if (get("status") != "") status = getStatus();
5,165✔
1439

1440
  // 1) Provide missing attributes where possible
1441
  // Provide a UUID if necessary. Validate if present.
1442
  std::string uid = get("uuid");
10,326✔
1443
  if (has("uuid") && uid != "") {
5,163✔
1444
    Lexer lex(uid);
2,485✔
1445
    std::string token;
2,485✔
1446
    Lexer::Type type;
1447
    // `uuid` is not a property in the TaskChampion model, so an invalid UUID is
1448
    // actually an error.
1449
    if (!lex.isUUID(token, type, true)) throw format("Not a valid UUID '{1}'.", uid);
2,485✔
1450
  } else
2,490✔
1451
    set("uuid", uuid());
2,678✔
1452

1453
  // TODO Obsolete remove for 3.0.0
1454
  // Recurring tasks get a special status.
1455
  if (status == Task::pending && has("due") && has("recur") &&
9,786✔
1456
      (!has("parent") || get("parent") == "") && (!has("template") || get("template") == "")) {
9,786✔
1457
    status = Task::recurring;
72✔
1458
  }
1459
  /*
1460
    // TODO Add for 3.0.0
1461
    if (status == Task::pending &&
1462
        has ("due")             &&
1463
        has ("recur")          &&
1464
        (! has ("template") || get ("template") == ""))
1465
    {
1466
      status = Task::recurring;
1467
    }
1468
  */
1469

1470
  // Tasks with a wait: date get a special status.
1471
  else if (status == Task::pending && has("wait") && get("wait") != "")
5,086✔
1472
    status = Task::waiting;
23✔
1473

1474
  // By default, tasks are pending.
1475
  else if (!has("status") || get("status") == "")
5,063✔
1476
    status = Task::pending;
2,586✔
1477

1478
  // Default to 'periodic' type recurrence.
1479
  if (status == Task::recurring && (!has("rtype") || get("rtype") == "")) {
5,158✔
1480
    set("rtype", "periodic");
72✔
1481
  }
1482

1483
  // Store the derived status.
1484
  setStatus(status);
5,158✔
1485

1486
#ifdef PRODUCT_TASKWARRIOR
1487
  // Provide an entry date unless user already specified one.
1488
  if (!has("entry") || get("entry") == "") setAsNow("entry");
5,158✔
1489

1490
  // Completed tasks need an end date, so inherit the entry date.
1491
  if ((status == Task::completed || status == Task::deleted) && (!has("end") || get("end") == ""))
5,158✔
1492
    setAsNow("end");
20✔
1493

1494
  // Pending tasks cannot have an end date, remove if present
1495
  if ((status == Task::pending) && (get("end") != "")) remove("end");
5,158✔
1496

1497
  // Provide a modified date unless user already specified one.
1498
  if (!has("modified") || get("modified") == "") setAsNow("modified");
5,158✔
1499

1500
  if (applyDefault && (!has("parent") || get("parent") == "")) {
5,158✔
1501
    // Override with default.project, if not specified.
1502
    if (Task::defaultProject != "" && !has("project")) {
4,468✔
1503
      if (Context::getContext().columns["project"]->validate(Task::defaultProject))
5✔
1504
        set("project", Task::defaultProject);
5✔
1505
    }
1506

1507
    // Override with default.due, if not specified.
1508
    if (Task::defaultDue != "" && !has("due")) {
4,468✔
1509
      if (Context::getContext().columns["due"]->validate(Task::defaultDue)) {
4✔
1510
        Duration dur(Task::defaultDue);
4✔
1511
        if (dur.toTime_t() != 0)
4✔
1512
          set("due", (Datetime() + dur.toTime_t()).toEpoch());
×
1513
        else
1514
          set("due", Datetime(Task::defaultDue).toEpoch());
4✔
1515
      }
1516
    }
1517

1518
    // Override with default.scheduled, if not specified.
1519
    if (Task::defaultScheduled != "" && !has("scheduled")) {
4,468✔
1520
      if (Context::getContext().columns["scheduled"]->validate(Task::defaultScheduled)) {
5✔
1521
        Duration dur(Task::defaultScheduled);
5✔
1522
        if (dur.toTime_t() != 0)
5✔
1523
          set("scheduled", (Datetime() + dur.toTime_t()).toEpoch());
×
1524
        else
1525
          set("scheduled", Datetime(Task::defaultScheduled).toEpoch());
5✔
1526
      }
1527
    }
1528

1529
    // If a UDA has a default value in the configuration,
1530
    // override with uda.(uda).default, if not specified.
1531
    // Gather a list of all UDAs with a .default value
1532
    std::vector<std::string> udas;
4,468✔
1533
    for (auto& var : Context::getContext().config) {
1,100,167✔
1534
      if (!var.first.compare(0, 4, "uda.", 4) && var.first.find(".default") != std::string::npos) {
1,095,699✔
1535
        auto period = var.first.find('.', 4);
8✔
1536
        if (period != std::string::npos) udas.push_back(var.first.substr(4, period - 4));
8✔
1537
      }
1538
    }
1539

1540
    if (udas.size()) {
4,468✔
1541
      // For each of those, setup the default value on the task now,
1542
      // of course only if we don't have one on the command line already
1543
      for (auto& uda : udas) {
16✔
1544
        std::string defVal = Context::getContext().config.get("uda." + uda + ".default");
16✔
1545

1546
        // If the default is empty, or we already have a value, skip it
1547
        if (defVal != "" && get(uda) == "") set(uda, defVal);
8✔
1548
      }
8✔
1549
    }
1550
  }
4,468✔
1551
#endif
1552

1553
  // 2) To provide suitable warnings about odd states
1554

1555
  // Date relationships.
1556
  validate_before("wait", "due");
5,158✔
1557
  validate_before("entry", "start");
5,158✔
1558
  validate_before("entry", "end");
5,158✔
1559
  validate_before("wait", "scheduled");
5,158✔
1560
  validate_before("scheduled", "start");
5,158✔
1561
  validate_before("scheduled", "due");
5,158✔
1562
  validate_before("scheduled", "end");
5,158✔
1563

1564
  if (!has("description") || get("description") == "")
5,158✔
NEW
1565
    Context::getContext().footnote(format("Warning: task has no description."));
×
1566

1567
  // Cannot have an old-style recur frequency with no due date - when would it recur?
1568
  if (has("recur") && (!has("due") || get("due") == "")) {
5,158✔
NEW
1569
    Context::getContext().footnote(format("Warning: recurring task has no due date."));
×
NEW
1570
    remove("recur");
×
1571
  }
1572

1573
  // Old-style recur durations must be valid.
1574
  if (has("recur")) {
5,158✔
1575
    std::string value = get("recur");
688✔
1576
    if (value != "") {
344✔
1577
      Duration p;
344✔
1578
      std::string::size_type i = 0;
344✔
1579
      if (!p.parse(value, i)) {
344✔
1580
        // TODO Ideal location to map unsupported old recurrence periods to supported values.
NEW
1581
        Context::getContext().footnote(
×
NEW
1582
            format("Warning: The recurrence value '{1}' is not valid.", value));
×
NEW
1583
        remove("recur");
×
1584
      }
1585
    }
1586
  }
344✔
1587
}
5,163✔
1588

1589
////////////////////////////////////////////////////////////////////////////////
1590
void Task::validate_before(const std::string& left, const std::string& right) {
36,106✔
1591
#ifdef PRODUCT_TASKWARRIOR
1592
  if (has(left) && has(right)) {
36,106✔
1593
    Datetime date_left(get_date(left));
552✔
1594
    Datetime date_right(get_date(right));
552✔
1595

1596
    // if date is zero, then it is being removed (e.g. "due: wait:1day")
1597
    if (date_left > date_right && date_right.toEpoch() != 0)
552✔
1598
      Context::getContext().footnote(format(
11✔
1599
          "Warning: You have specified that the '{1}' date is after the '{2}' date.", left, right));
1600
  }
1601
#endif
1602
}
36,106✔
1603

1604
////////////////////////////////////////////////////////////////////////////////
1605
// Encode values prior to serialization.
1606
//   [  -> &open;
1607
//   ]  -> &close;
1608
const std::string Task::encode(const std::string& value) const {
×
1609
  auto modified = str_replace(value, "[", "&open;");
×
1610
  return str_replace(modified, "]", "&close;");
×
1611
}
1612

1613
////////////////////////////////////////////////////////////////////////////////
1614
// Decode values after parse.
1615
//   [  <- &open;
1616
//   ]  <- &close;
1617
const std::string Task::decode(const std::string& value) const {
×
1618
  if (value.find('&') == std::string::npos) return value;
×
1619

1620
  auto modified = str_replace(value, "&open;", "[");
×
1621
  return str_replace(modified, "&close;", "]");
×
1622
}
1623

1624
////////////////////////////////////////////////////////////////////////////////
1625
int Task::determineVersion(const std::string& line) {
×
1626
  // Version 2 looks like:
1627
  //
1628
  //   uuid status [tags] [attributes] description\n
1629
  //
1630
  // Where uuid looks like:
1631
  //
1632
  //   27755d92-c5e9-4c21-bd8e-c3dd9e6d3cf7
1633
  //
1634
  // Scan for the hyphens in the uuid, the following space, and a valid status
1635
  // character.
1636
  if (line[8] == '-' && line[13] == '-' && line[18] == '-' && line[23] == '-' && line[36] == ' ' &&
×
1637
      (line[37] == '-' || line[37] == '+' || line[37] == 'X' || line[37] == 'r')) {
×
1638
    // Version 3 looks like:
1639
    //
1640
    //   uuid status [tags] [attributes] [annotations] description\n
1641
    //
1642
    // Scan for the number of [] pairs.
1643
    auto tagAtts = line.find("] [", 0);
×
1644
    auto attsAnno = line.find("] [", tagAtts + 1);
×
1645
    auto annoDesc = line.find("] ", attsAnno + 1);
×
1646
    if (tagAtts != std::string::npos && attsAnno != std::string::npos &&
×
1647
        annoDesc != std::string::npos)
1648
      return 3;
×
1649
    else
1650
      return 2;
×
1651
  }
1652

1653
  // Version 4 looks like:
1654
  //
1655
  //   [name:"value" ...]
1656
  //
1657
  // Scan for [, ] and :".
1658
  else if (line[0] == '[' && line[line.length() - 1] == ']' &&
×
1659
           line.find("uuid:\"") != std::string::npos)
×
1660
    return 4;
×
1661

1662
  // Version 1 looks like:
1663
  //
1664
  //   [tags] [attributes] description\n
1665
  //   X [tags] [attributes] description\n
1666
  //
1667
  // Scan for the first character being either the bracket or X.
1668
  else if (line.find("X [") == 0 ||
×
1669
           (line[0] == '[' && line.substr(line.length() - 1, 1) != "]" && line.length() > 3))
×
1670
    return 1;
×
1671

1672
  // Version 5?
1673
  //
1674
  // Fortunately, with the hindsight that will come with version 5, the
1675
  // identifying characteristics of 1, 2, 3 and 4 may be modified such that if 5
1676
  // has a UUID followed by a status, then there is still a way to differentiate
1677
  // between 2, 3, 4 and 5.
1678
  //
1679
  // The danger is that a version 3 binary reads and misinterprets a version 4
1680
  // file.  This is why it is a good idea to rely on an explicit version
1681
  // declaration rather than chance positioning.
1682

1683
  // Zero means 'no idea'.
1684
  return 0;
×
1685
}
1686

1687
////////////////////////////////////////////////////////////////////////////////
1688
// Urgency is defined as a polynomial, the value of which is calculated in this
1689
// function, according to:
1690
//
1691
//   U = A.t  + B.t  + C.t  ...
1692
//          a      b      c
1693
//
1694
//   U       = urgency
1695
//   A       = coefficient for term a
1696
//   t sub a = numeric scale from 0 -> 1, with 1 being the highest
1697
//             urgency, derived from one task attribute and mapped
1698
//             to the numeric scale
1699
//
1700
// See rfc31-urgency.txt for full details.
1701
//
1702
float Task::urgency_c() const {
1,019✔
1703
  float value = 0.0;
1,019✔
1704
#ifdef PRODUCT_TASKWARRIOR
1705
  value += fabsf(Task::urgencyProjectCoefficient) > epsilon
2,038✔
1706
               ? (urgency_project() * Task::urgencyProjectCoefficient)
1,019✔
1707
               : 0.0;
1708
  value += fabsf(Task::urgencyActiveCoefficient) > epsilon
2,038✔
1709
               ? (urgency_active() * Task::urgencyActiveCoefficient)
1,019✔
1710
               : 0.0;
1711
  value += fabsf(Task::urgencyScheduledCoefficient) > epsilon
2,038✔
1712
               ? (urgency_scheduled() * Task::urgencyScheduledCoefficient)
1,019✔
1713
               : 0.0;
1714
  value += fabsf(Task::urgencyWaitingCoefficient) > epsilon
2,038✔
1715
               ? (urgency_waiting() * Task::urgencyWaitingCoefficient)
1,019✔
1716
               : 0.0;
1717
  value += fabsf(Task::urgencyBlockedCoefficient) > epsilon
2,038✔
1718
               ? (urgency_blocked() * Task::urgencyBlockedCoefficient)
1,019✔
1719
               : 0.0;
1720
  value += fabsf(Task::urgencyAnnotationsCoefficient) > epsilon
2,038✔
1721
               ? (urgency_annotations() * Task::urgencyAnnotationsCoefficient)
1,019✔
1722
               : 0.0;
1723
  value += fabsf(Task::urgencyTagsCoefficient) > epsilon
2,038✔
1724
               ? (urgency_tags() * Task::urgencyTagsCoefficient)
1,019✔
1725
               : 0.0;
1726
  value += fabsf(Task::urgencyDueCoefficient) > epsilon
2,038✔
1727
               ? (urgency_due() * Task::urgencyDueCoefficient)
1,019✔
1728
               : 0.0;
1729
  value += fabsf(Task::urgencyBlockingCoefficient) > epsilon
2,038✔
1730
               ? (urgency_blocking() * Task::urgencyBlockingCoefficient)
1,019✔
1731
               : 0.0;
1732
  value += fabsf(Task::urgencyAgeCoefficient) > epsilon
2,038✔
1733
               ? (urgency_age() * Task::urgencyAgeCoefficient)
1,019✔
1734
               : 0.0;
1735

1736
  const std::string taskProjectName = get("project");
2,038✔
1737
  // Tag- and project-specific coefficients.
1738
  for (auto& var : Task::coefficients) {
5,294✔
1739
    if (fabs(var.second) > epsilon) {
4,275✔
1740
      if (!var.first.compare(0, 13, "urgency.user.", 13)) {
4,275✔
1741
        // urgency.user.project.<project>.coefficient
1742
        auto end = std::string::npos;
1,226✔
1743
        if (var.first.substr(13, 8) == "project." &&
1,325✔
1744
            (end = var.first.find(".coefficient")) != std::string::npos) {
99✔
1745
          std::string project = var.first.substr(21, end - 21);
99✔
1746

1747
          if (taskProjectName == project || taskProjectName.find(project + '.') == 0) {
99✔
1748
            value += var.second;
3✔
1749
          }
1750
        }
99✔
1751

1752
        // urgency.user.tag.<tag>.coefficient
1753
        if (var.first.substr(13, 4) == "tag." &&
2,352✔
1754
            (end = var.first.find(".coefficient")) != std::string::npos) {
1,126✔
1755
          std::string tag = var.first.substr(17, end - 17);
1,126✔
1756

1757
          if (hasTag(tag)) value += var.second;
1,126✔
1758
        }
1,126✔
1759

1760
        // urgency.user.keyword.<keyword>.coefficient
1761
        if (var.first.substr(13, 8) == "keyword." &&
1,227✔
1762
            (end = var.first.find(".coefficient")) != std::string::npos) {
1✔
1763
          std::string keyword = var.first.substr(21, end - 21);
1✔
1764

1765
          if (get("description").find(keyword) != std::string::npos) value += var.second;
1✔
1766
        }
1✔
1767
      } else if (var.first.substr(0, 12) == "urgency.uda.") {
3,049✔
1768
        // urgency.uda.<name>.coefficient
1769
        // urgency.uda.<name>.<value>.coefficient
1770
        auto end = var.first.find(".coefficient");
3,049✔
1771
        if (end != std::string::npos) {
3,049✔
1772
          const std::string uda = var.first.substr(12, end - 12);
3,049✔
1773
          auto dot = uda.find('.');
3,049✔
1774
          if (dot == std::string::npos) {
3,049✔
1775
            // urgency.uda.<name>.coefficient
1776
            if (has(uda)) value += var.second;
1✔
1777
          } else {
1778
            // urgency.uda.<name>.<value>.coefficient
1779
            if (get(uda.substr(0, dot)) == uda.substr(dot + 1)) value += var.second;
3,048✔
1780
          }
1781
        }
3,049✔
1782
      }
1783
    }
1784
  }
1785

1786
  if (is_blocking && Context::getContext().config.getBoolean("urgency.inherit")) {
1,019✔
1787
    float prev = value;
3✔
1788
    value = std::max(value, urgency_inherit());
3✔
1789

1790
    // This is a hackish way of making sure parent tasks are sorted above
1791
    // child tasks.  For reports that hide blocked tasks, this is not needed.
1792
    if (prev <= value) value += 0.01;
3✔
1793
  }
1794
#endif
1795

1796
  return value;
1,019✔
1797
}
1,019✔
1798

1799
////////////////////////////////////////////////////////////////////////////////
1800
float Task::urgency() {
1,701✔
1801
  if (recalc_urgency) {
1,701✔
1802
    urgency_value = urgency_c();
760✔
1803

1804
    // Return the sum of all terms.
1805
    recalc_urgency = false;
760✔
1806
  }
1807

1808
  return urgency_value;
1,701✔
1809
}
1810

1811
////////////////////////////////////////////////////////////////////////////////
1812
float Task::urgency_inherit() const {
3✔
1813
  float v = -FLT_MAX;
3✔
1814
#ifdef PRODUCT_TASKWARRIOR
1815
  // Calling getBlockedTasks is rather expensive.
1816
  // It is called recursively for each dependency in the chain here.
1817
  for (auto& task : getBlockedTasks()) {
6✔
1818
    // Find highest urgency in all blocked tasks.
1819
    v = std::max(v, task.urgency());
3✔
1820
  }
3✔
1821
#endif
1822

1823
  return v;
3✔
1824
}
1825

1826
////////////////////////////////////////////////////////////////////////////////
1827
float Task::urgency_project() const {
1,076✔
1828
  if (has("project")) return 1.0;
1,076✔
1829

1830
  return 0.0;
823✔
1831
}
1832

1833
////////////////////////////////////////////////////////////////////////////////
1834
float Task::urgency_active() const {
1,076✔
1835
  if (has("start")) return 1.0;
1,076✔
1836

1837
  return 0.0;
1,035✔
1838
}
1839

1840
////////////////////////////////////////////////////////////////////////////////
1841
float Task::urgency_scheduled() const {
1,076✔
1842
  if (has("scheduled") && get_date("scheduled") < time(nullptr)) return 1.0;
1,076✔
1843

1844
  return 0.0;
1,073✔
1845
}
1846

1847
////////////////////////////////////////////////////////////////////////////////
1848
float Task::urgency_waiting() const {
1,076✔
1849
  if (is_waiting()) return 1.0;
1,076✔
1850

1851
  return 0.0;
1,070✔
1852
}
1853

1854
////////////////////////////////////////////////////////////////////////////////
1855
// A task is blocked only if the task it depends upon is pending/waiting.
1856
float Task::urgency_blocked() const {
1,064✔
1857
  if (is_blocked) return 1.0;
1,064✔
1858

1859
  return 0.0;
1,051✔
1860
}
1861

1862
////////////////////////////////////////////////////////////////////////////////
1863
float Task::urgency_annotations() const {
1,076✔
1864
  if (annotation_count >= 3)
1,076✔
1865
    return 1.0;
4✔
1866
  else if (annotation_count == 2)
1,072✔
1867
    return 0.9;
6✔
1868
  else if (annotation_count == 1)
1,066✔
1869
    return 0.8;
39✔
1870

1871
  return 0.0;
1,027✔
1872
}
1873

1874
////////////////////////////////////////////////////////////////////////////////
1875
float Task::urgency_tags() const {
1,076✔
1876
  switch (getTagCount()) {
1,076✔
1877
    case 0:
859✔
1878
      return 0.0;
859✔
1879
    case 1:
186✔
1880
      return 0.8;
186✔
1881
    case 2:
27✔
1882
      return 0.9;
27✔
1883
    default:
4✔
1884
      return 1.0;
4✔
1885
  }
1886
}
1887

1888
////////////////////////////////////////////////////////////////////////////////
1889
//
1890
//     Past                  Present                              Future
1891
//     Overdue               Due                                     Due
1892
//
1893
//     -7 -6 -5 -4 -3 -2 -1  0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 days
1894
//
1895
// <-- 1.0                         linear                            0.2 -->
1896
//     capped                                                        capped
1897
//
1898
//
1899
float Task::urgency_due() const {
1,076✔
1900
  if (has("due")) {
1,076✔
1901
    Datetime now;
268✔
1902
    Datetime due(get_date("due"));
268✔
1903

1904
    // Map a range of 21 days to the value 0.2 - 1.0
1905
    float days_overdue = (now - due) / 86400.0;
268✔
1906
    if (days_overdue >= 7.0)
268✔
1907
      return 1.0;  // < 1 wk ago
20✔
1908
    else if (days_overdue >= -14.0)
248✔
1909
      return ((days_overdue + 14.0) * 0.8 / 21.0) + 0.2;
233✔
1910
    else
1911
      return 0.2;  // > 2 wks
15✔
1912
  }
1913

1914
  return 0.0;
808✔
1915
}
1916

1917
////////////////////////////////////////////////////////////////////////////////
1918
float Task::urgency_age() const {
1,064✔
1919
  if (!has("entry")) return 1.0;
1,064✔
1920

1921
  Datetime now;
1,064✔
1922
  Datetime entry(get_date("entry"));
1,064✔
1923
  int age = (now - entry) / 86400;  // in days
1,064✔
1924

1925
  if (Task::urgencyAgeMax == 0 || age > Task::urgencyAgeMax) return 1.0;
1,064✔
1926

1927
  return (1.0 * age / Task::urgencyAgeMax);
1,015✔
1928
}
1929

1930
////////////////////////////////////////////////////////////////////////////////
1931
float Task::urgency_blocking() const {
1,064✔
1932
  if (is_blocking) return 1.0;
1,064✔
1933

1934
  return 0.0;
1,046✔
1935
}
1936

1937
#ifdef PRODUCT_TASKWARRIOR
1938
////////////////////////////////////////////////////////////////////////////////
1939
// Arguably does not belong here. This method reads the parse tree and calls
1940
// Task methods. It could be a standalone function with no loss in access, as
1941
// well as reducing the object depdendencies of Task.
1942
//
1943
// It came from the Command base object, but doesn't really belong there either.
1944
void Task::modify(modType type, bool text_required /* = false */) {
2,167✔
1945
  std::string label = "   [1;37;43mMODIFICATION [0m ";
2,167✔
1946

1947
  // while reading the parse tree, consider DOM references in the context of
1948
  // this task
1949
  auto currentTask = Context::getContext().withCurrentTask(this);
2,167✔
1950

1951
  // Need this for later comparison.
1952
  auto originalStatus = getStatus();
2,167✔
1953

1954
  std::string text = "";
2,167✔
1955
  bool mods = false;
2,167✔
1956
  for (auto& a : Context::getContext().cli2._args) {
14,177✔
1957
    if (a.hasTag("MODIFICATION")) {
12,404✔
1958
      if (a._lextype == Lexer::Type::pair) {
3,252✔
1959
        // 'canonical' is the canonical name. Needs to be said.
1960
        // 'value' requires eval.
1961
        std::string name = a.attribute("canonical");
2,546✔
1962
        std::string value = a.attribute("value");
2,546✔
1963
        if (value == "" || value == "''" || value == "\"\"") {
1,273✔
1964
          // Special case: Handle bulk removal of 'tags' and 'depends" virtual
1965
          // attributes
1966
          if (name == "depends") {
14✔
1967
            for (auto dep : getDependencyUUIDs()) removeDependency(dep);
3✔
1968
          } else if (name == "tags") {
13✔
1969
            for (auto tag : getTags()) removeTag(tag);
4✔
1970
          }
1971

1972
          // ::composeF4 will skip if the value is blank, but the presence of
1973
          // the attribute will prevent ::validate from applying defaults.
1974
          if ((has(name) && get(name) != "") ||
28✔
1975
              (name == "due" && Context::getContext().config.has("default.due")) ||
25✔
1976
              (name == "scheduled" && Context::getContext().config.has("default.scheduled")) ||
49✔
1977
              (name == "project" && Context::getContext().config.has("default.project"))) {
21✔
1978
            mods = true;
11✔
1979
            set(name, "");
11✔
1980
          }
1981

1982
          Context::getContext().debug(label + name + " <-- ''");
14✔
1983
        } else {
1984
          Lexer::dequote(value);
1,259✔
1985

1986
          // Get the column info. Some columns are not modifiable.
1987
          Column* column = Context::getContext().columns[name];
1,259✔
1988
          if (!column || !column->modifiable())
1,259✔
1989
            throw format("The '{1}' attribute does not allow a value of '{2}'.", name, value);
×
1990

1991
          // Delegate modification to the column object or their base classes.
1992
          if (name == "depends" || name == "tags" || name == "recur" || column->type() == "date" ||
2,855✔
1993
              column->type() == "duration" || column->type() == "numeric" ||
3,219✔
1994
              column->type() == "string") {
364✔
1995
            column->modify(*this, value);
1,259✔
1996
            mods = true;
865✔
1997
          }
1998

1999
          else
2000
            throw format("Unrecognized column type '{1}' for column '{2}'", column->type(), name);
×
2001
        }
2002
      }
1,667✔
2003

2004
      // Perform description/annotation substitution.
2005
      else if (a._lextype == Lexer::Type::substitution) {
1,979✔
2006
        Context::getContext().debug(label + "substitute " + a.attribute("raw"));
35✔
2007
        substitute(a.attribute("from"), a.attribute("to"), a.attribute("flags"));
35✔
2008
        mods = true;
35✔
2009
      }
2010

2011
      // Tags need special handling because they are essentially a vector stored
2012
      // in a single string, therefore Task::{add,remove}Tag must be called as
2013
      // appropriate.
2014
      else if (a._lextype == Lexer::Type::tag) {
1,944✔
2015
        std::string tag = a.attribute("name");
388✔
2016
        feedback_reserved_tags(tag);
194✔
2017

2018
        if (a.attribute("sign") == "+") {
194✔
2019
          Context::getContext().debug(label + "tags <-- add '" + tag + '\'');
186✔
2020
          addTag(tag);
186✔
2021
          feedback_special_tags(*this, tag);
186✔
2022
        } else {
2023
          Context::getContext().debug(label + "tags <-- remove '" + tag + '\'');
8✔
2024
          removeTag(tag);
8✔
2025
        }
2026

2027
        mods = true;
194✔
2028
      }
194✔
2029

2030
      // Unknown args are accumulated as though they were WORDs.
2031
      else {
2032
        if (text != "") text += ' ';
1,750✔
2033
        text += a.attribute("raw");
1,750✔
2034
      }
2035
    }
2036
  }
2037

2038
  // Task::modType determines what happens to the WORD arguments, if there are
2039
  //  any.
2040
  if (text != "") {
1,773✔
2041
    Lexer::dequote(text);
1,382✔
2042

2043
    switch (type) {
1,382✔
2044
      case modReplace:
1,288✔
2045
        Context::getContext().debug(label + "description <-- '" + text + '\'');
1,288✔
2046
        set("description", text);
1,288✔
2047
        break;
1,288✔
2048

2049
      case modPrepend:
4✔
2050
        Context::getContext().debug(label + "description <-- '" + text + "' + description");
4✔
2051
        set("description", text + ' ' + get("description"));
4✔
2052
        break;
4✔
2053

2054
      case modAppend:
6✔
2055
        Context::getContext().debug(label + "description <-- description + '" + text + '\'');
6✔
2056
        set("description", get("description") + ' ' + text);
6✔
2057
        break;
6✔
2058

2059
      case modAnnotate:
84✔
2060
        Context::getContext().debug(label + "new annotation <-- '" + text + '\'');
84✔
2061
        addAnnotation(text);
84✔
2062
        break;
84✔
2063
    }
2064
  } else if (!mods && text_required)
391✔
2065
    throw std::string("Additional text must be provided.");
4✔
2066

2067
  // Modifying completed/deleted tasks generates a message, if the modification
2068
  // does not change status.
2069
  if ((getStatus() == Task::completed || getStatus() == Task::deleted) &&
1,774✔
2070
      getStatus() == originalStatus) {
5✔
2071
    auto uuid = get("uuid").substr(0, 8);
10✔
2072
    Context::getContext().footnote(
10✔
2073
        format("Note: Modified task {1} is {2}. You may wish to make this task pending with: task "
15✔
2074
               "{3} modify status:pending",
2075
               uuid, get("status"), uuid));
10✔
2076
  }
5✔
2077
}
2,963✔
2078
#endif
2079

2080
////////////////////////////////////////////////////////////////////////////////
2081
// Compare this task to another and summarize the differences for display, in
2082
// the future tense ("Foo will be set to ..").
2083
std::string Task::diff(const Task& after) const {
358✔
2084
  // Attributes are all there is, so figure the different attribute names
2085
  // between this (before) and after.
2086
  std::vector<std::string> beforeAtts;
358✔
2087
  for (auto& att : data) beforeAtts.push_back(att.first);
2,554✔
2088

2089
  std::vector<std::string> afterAtts;
358✔
2090
  for (auto& att : after.data) afterAtts.push_back(att.first);
2,880✔
2091

2092
  std::vector<std::string> beforeOnly;
358✔
2093
  std::vector<std::string> afterOnly;
358✔
2094
  listDiff(beforeAtts, afterAtts, beforeOnly, afterOnly);
358✔
2095

2096
  // Now start generating a description of the differences.
2097
  std::stringstream out;
358✔
2098
  for (auto& name : beforeOnly) {
405✔
2099
    if (isAnnotationAttr(name)) {
47✔
2100
      out << "  - " << format("Annotation {1} will be removed.", name) << "\n";
6✔
2101
    } else if (isTagAttr(name)) {
41✔
2102
      out << "  - " << format("Tag {1} will be removed.", attr2Tag(name)) << "\n";
10✔
2103
    } else if (isDepAttr(name)) {
31✔
2104
      out << "  - " << format("Depenency on {1} will be removed.", attr2Dep(name)) << "\n";
6✔
2105
    } else if (name == "depends" || name == "tags") {
25✔
2106
      // do nothing for legacy attributes
2107
    } else {
2108
      out << "  - " << format("{1} will be deleted.", Lexer::ucFirst(name)) << "\n";
19✔
2109
    }
2110
  }
2111

2112
  for (auto& name : afterOnly) {
731✔
2113
    if (isAnnotationAttr(name)) {
373✔
2114
      out << format("Annotation of {1} will be added.\n", after.get(name));
88✔
2115
    } else if (isTagAttr(name)) {
285✔
2116
      out << format("Tag {1} will be added.\n", attr2Tag(name));
33✔
2117
    } else if (isDepAttr(name)) {
252✔
2118
      out << format("Dependency on {1} will be added.\n", attr2Dep(name));
33✔
2119
    } else if (name == "depends" || name == "tags") {
219✔
2120
      // do nothing for legacy attributes
2121
    } else
2122
      out << "  - "
2123
          << format("{1} will be set to '{2}'.", Lexer::ucFirst(name),
342✔
2124
                    renderAttribute(name, after.get(name)))
342✔
2125
          << "\n";
342✔
2126
  }
2127

2128
  for (auto& name : beforeAtts) {
2,554✔
2129
    // Ignore UUID differences, and find values that changed, but are not also
2130
    // in the beforeOnly and afterOnly lists, which have been handled above..
2131
    if (name != "uuid" && get(name) != after.get(name) &&
4,257✔
2132
        std::find(beforeOnly.begin(), beforeOnly.end(), name) == beforeOnly.end() &&
4,257✔
2133
        std::find(afterOnly.begin(), afterOnly.end(), name) == afterOnly.end()) {
2,370✔
2134
      if (name == "depends" || name == "tags") {
174✔
2135
        // do nothing for legacy attributes
2136
      } else if (isTagAttr(name) || isDepAttr(name)) {
165✔
2137
        // ignore new attributes
2138
      } else if (isAnnotationAttr(name)) {
165✔
2139
        out << format("Annotation will be changed to {1}.\n", after.get(name));
4✔
2140
      } else
2141
        out << "  - "
2142
            << format("{1} will be changed from '{2}' to '{3}'.", Lexer::ucFirst(name),
322✔
2143
                      renderAttribute(name, get(name)), renderAttribute(name, after.get(name)))
322✔
2144
            << "\n";
322✔
2145
    }
2146
  }
2147

2148
  // Shouldn't just say nothing.
2149
  if (out.str().length() == 0) out << "  - No changes will be made.\n";
358✔
2150

2151
  return out.str();
716✔
2152
}
358✔
2153

2154
////////////////////////////////////////////////////////////////////////////////
2155
// Similar to diff, but formatted for inclusion in the output of the info command
2156
std::string Task::diffForInfo(const Task& after, const std::string& dateformat,
×
2157
                              long& last_timestamp, const long current_timestamp) const {
2158
  // Attributes are all there is, so figure the different attribute names
2159
  // between before and after.
2160
  std::vector<std::string> beforeAtts;
×
2161
  for (auto& att : data) beforeAtts.push_back(att.first);
×
2162

2163
  std::vector<std::string> afterAtts;
×
2164
  for (auto& att : after.data) afterAtts.push_back(att.first);
×
2165

2166
  std::vector<std::string> beforeOnly;
×
2167
  std::vector<std::string> afterOnly;
×
2168
  listDiff(beforeAtts, afterAtts, beforeOnly, afterOnly);
×
2169

2170
  // Now start generating a description of the differences.
2171
  std::stringstream out;
×
2172
  for (auto& name : beforeOnly) {
×
2173
    if (isAnnotationAttr(name)) {
×
2174
      out << format("Annotation '{1}' deleted.\n", get(name));
×
2175
    } else if (isTagAttr(name)) {
×
2176
      out << format("Tag '{1}' deleted.\n", attr2Tag(name));
×
2177
    } else if (isDepAttr(name)) {
×
2178
      out << format("Dependency on '{1}' deleted.\n", attr2Dep(name));
×
2179
    } else if (name == "depends" || name == "tags") {
×
2180
      // do nothing for legacy attributes
2181
    } else if (name == "start") {
×
2182
      Datetime started(get("start"));
×
2183
      Datetime stopped;
×
2184

2185
      if (after.has("end"))
×
2186
        // Task was marked as finished, use end time
2187
        stopped = Datetime(after.get("end"));
×
2188
      else
2189
        // Start attribute was removed, use modification time
2190
        stopped = Datetime(current_timestamp);
×
2191

2192
      out << format("{1} deleted (duration: {2}).", Lexer::ucFirst(name),
×
2193
                    Duration(stopped - started).format())
×
2194
          << "\n";
×
2195
    } else {
2196
      out << format("{1} deleted.\n", Lexer::ucFirst(name));
×
2197
    }
2198
  }
2199

2200
  for (auto& name : afterOnly) {
×
2201
    if (isAnnotationAttr(name)) {
×
2202
      out << format("Annotation of '{1}' added.\n", after.get(name));
×
2203
    } else if (isTagAttr(name)) {
×
2204
      out << format("Tag '{1}' added.\n", attr2Tag(name));
×
2205
    } else if (isDepAttr(name)) {
×
2206
      out << format("Dependency on '{1}' added.\n", attr2Dep(name));
×
2207
    } else if (name == "depends" || name == "tags") {
×
2208
      // do nothing for legacy attributes
2209
    } else {
2210
      if (name == "start") last_timestamp = current_timestamp;
×
2211

2212
      out << format("{1} set to '{2}'.", Lexer::ucFirst(name),
×
2213
                    renderAttribute(name, after.get(name), dateformat))
×
2214
          << "\n";
×
2215
    }
2216
  }
2217

2218
  for (auto& name : beforeAtts)
×
2219
    if (name != "uuid" && name != "modified" && get(name) != after.get(name) && get(name) != "" &&
×
2220
        after.get(name) != "") {
×
2221
      if (name == "depends" || name == "tags") {
×
2222
        // do nothing for legacy attributes
2223
      } else if (isTagAttr(name) || isDepAttr(name)) {
×
2224
        // ignore new attributes
2225
      } else if (isAnnotationAttr(name)) {
×
2226
        out << format("Annotation changed to '{1}'.\n", after.get(name));
×
2227
      } else
2228
        out << format("{1} changed from '{2}' to '{3}'.", Lexer::ucFirst(name),
×
2229
                      renderAttribute(name, get(name), dateformat),
×
2230
                      renderAttribute(name, after.get(name), dateformat))
×
2231
            << "\n";
×
2232
    }
2233

2234
  // Shouldn't just say nothing.
2235
  if (out.str().length() == 0) out << "No changes made.\n";
×
2236

2237
  return out.str();
×
2238
}
2239

2240
////////////////////////////////////////////////////////////////////////////////
2241
// Similar to diff, but formatted as a side-by-side table for an Undo preview
2242
Table Task::diffForUndoSide(const Task& after) const {
×
2243
  // Set the colors.
2244
  Color color_red(
2245
      Context::getContext().color() ? Context::getContext().config.get("color.undo.before") : "");
×
2246
  Color color_green(
2247
      Context::getContext().color() ? Context::getContext().config.get("color.undo.after") : "");
×
2248

2249
  // Attributes are all there is, so figure the different attribute names
2250
  // between before and after.
2251
  Table view;
×
2252
  view.width(Context::getContext().getWidth());
×
2253
  view.intraPadding(2);
×
2254
  view.add("");
×
2255
  view.add("Prior Values");
×
2256
  view.add("Current Values");
×
2257
  setHeaderUnderline(view);
×
2258

2259
  if (!is_empty()) {
×
2260
    const Task& before = *this;
×
2261

2262
    std::vector<std::string> beforeAtts;
×
2263
    for (auto& att : before.data) beforeAtts.push_back(att.first);
×
2264

2265
    std::vector<std::string> afterAtts;
×
2266
    for (auto& att : after.data) afterAtts.push_back(att.first);
×
2267

2268
    std::vector<std::string> beforeOnly;
×
2269
    std::vector<std::string> afterOnly;
×
2270
    listDiff(beforeAtts, afterAtts, beforeOnly, afterOnly);
×
2271

2272
    int row;
2273
    for (auto& name : beforeOnly) {
×
2274
      row = view.addRow();
×
2275
      view.set(row, 0, name);
×
2276
      view.set(row, 1, renderAttribute(name, before.get(name)), color_red);
×
2277
    }
2278

2279
    for (auto& att : before.data) {
×
2280
      std::string priorValue = before.get(att.first);
×
2281
      std::string currentValue = after.get(att.first);
×
2282

2283
      if (currentValue != "") {
×
2284
        row = view.addRow();
×
2285
        view.set(row, 0, att.first);
×
2286
        view.set(row, 1, renderAttribute(att.first, priorValue),
×
2287
                 (priorValue != currentValue ? color_red : Color()));
×
2288
        view.set(row, 2, renderAttribute(att.first, currentValue),
×
2289
                 (priorValue != currentValue ? color_green : Color()));
×
2290
      }
2291
    }
2292

2293
    for (auto& name : afterOnly) {
×
2294
      row = view.addRow();
×
2295
      view.set(row, 0, name);
×
2296
      view.set(row, 2, renderAttribute(name, after.get(name)), color_green);
×
2297
    }
2298
  } else {
×
2299
    int row;
2300
    for (auto& att : after.data) {
×
2301
      row = view.addRow();
×
2302
      view.set(row, 0, att.first);
×
2303
      view.set(row, 2, renderAttribute(att.first, after.get(att.first)), color_green);
×
2304
    }
2305
  }
2306

2307
  return view;
×
2308
}
2309

2310
////////////////////////////////////////////////////////////////////////////////
2311
// Similar to diff, but formatted as a diff for an Undo preview
2312
Table Task::diffForUndoPatch(const Task& after, const Datetime& lastChange) const {
×
2313
  // This style looks like this:
2314
  //  --- before    2009-07-04 00:00:25.000000000 +0200
2315
  //  +++ after    2009-07-04 00:00:45.000000000 +0200
2316
  //
2317
  // - name: old           // att deleted
2318
  // + name:
2319
  //
2320
  // - name: old           // att changed
2321
  // + name: new
2322
  //
2323
  // - name:
2324
  // + name: new           // att added
2325
  //
2326

2327
  // Set the colors.
2328
  Color color_red(
2329
      Context::getContext().color() ? Context::getContext().config.get("color.undo.before") : "");
×
2330
  Color color_green(
2331
      Context::getContext().color() ? Context::getContext().config.get("color.undo.after") : "");
×
2332

2333
  const Task& before = *this;
×
2334

2335
  // Generate table header.
2336
  Table view;
×
2337
  view.width(Context::getContext().getWidth());
×
2338
  view.intraPadding(2);
×
2339
  view.add("");
×
2340
  view.add("");
×
2341

2342
  int row = view.addRow();
×
2343
  view.set(row, 0, "--- previous state", color_red);
×
2344
  view.set(row, 1, "Undo will restore this state", color_red);
×
2345

2346
  row = view.addRow();
×
2347
  view.set(row, 0, "+++ current state ", color_green);
×
2348
  view.set(row, 1,
×
2349
           format("Change made {1}",
×
2350
                  lastChange.toString(Context::getContext().config.get("dateformat"))),
×
2351
           color_green);
2352

2353
  view.addRow();
×
2354

2355
  // Add rows to table showing diffs.
2356
  std::vector<std::string> all = Context::getContext().getColumns();
×
2357

2358
  // Now factor in the annotation attributes.
2359
  for (auto& it : before.data)
×
2360
    if (it.first.substr(0, 11) == "annotation_") all.push_back(it.first);
×
2361

2362
  for (auto& it : after.data)
×
2363
    if (it.first.substr(0, 11) == "annotation_") all.push_back(it.first);
×
2364

2365
  // Now render all the attributes.
2366
  std::sort(all.begin(), all.end());
×
2367

2368
  std::string before_att;
×
2369
  std::string after_att;
×
2370
  std::string last_att;
×
2371
  for (auto& a : all) {
×
2372
    if (a != last_att)  // Skip duplicates.
×
2373
    {
2374
      last_att = a;
×
2375

2376
      before_att = before.get(a);
×
2377
      after_att = after.get(a);
×
2378

2379
      // Don't report different uuid.
2380
      // Show nothing if values are the unchanged.
2381
      if (a == "uuid" || before_att == after_att) {
×
2382
        // Show nothing - no point displaying that which did not change.
2383

2384
        // row = view.addRow ();
2385
        // view.set (row, 0, *a + ":");
2386
        // view.set (row, 1, before_att);
2387
      }
2388

2389
      // Attribute deleted.
2390
      else if (before_att != "" && after_att == "") {
×
2391
        row = view.addRow();
×
2392
        view.set(row, 0, '-' + a + ':', color_red);
×
2393
        view.set(row, 1, before_att, color_red);
×
2394

2395
        row = view.addRow();
×
2396
        view.set(row, 0, '+' + a + ':', color_green);
×
2397
      }
2398

2399
      // Attribute added.
2400
      else if (before_att == "" && after_att != "") {
×
2401
        row = view.addRow();
×
2402
        view.set(row, 0, '-' + a + ':', color_red);
×
2403

2404
        row = view.addRow();
×
2405
        view.set(row, 0, '+' + a + ':', color_green);
×
2406
        view.set(row, 1, after_att, color_green);
×
2407
      }
2408

2409
      // Attribute changed.
2410
      else {
2411
        row = view.addRow();
×
2412
        view.set(row, 0, '-' + a + ':', color_red);
×
2413
        view.set(row, 1, before_att, color_red);
×
2414

2415
        row = view.addRow();
×
2416
        view.set(row, 0, '+' + a + ':', color_green);
×
2417
        view.set(row, 1, after_att, color_green);
×
2418
      }
2419
    }
2420
  }
2421

2422
  return view;
×
2423
}
2424

2425
////////////////////////////////////////////////////////////////////////////////
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