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

GothenburgBitFactory / taskwarrior / 11420355627

19 Oct 2024 08:00PM UTC coverage: 84.853% (+0.6%) from 84.223%
11420355627

push

github

web-flow
Pass rc.weekstart to libshared for ISO8601 weeknum parsing if "monday" (#3654)

* libshared: bump for weekstart, epoch defines, eopww fix

mainly those visible changes, and miscellaneous others

see GothenburgBitFactory/taskwarrior#3623 (weekstart)
see GothenburgBitFactory/taskwarrior#3651 (epoch limit defines)
see GothenburgBitFactory/libshared#73 (eopww fix)

* Initialize libshared's weekstart from user's rc.weekstart config

This enables use of newer libshared code that can parse week numbers
according to ISO8601 instead of existing code which is always using
Sunday-based weeks.  To get ISO behavior, set rc.weekstart=monday.
Default is still Sunday / old algorithm, as before, since Sunday is in
the hardcoded default rcfile.

Weekstart does not yet fix week-relative shortcuts, which will still
always use Monday.

See #3623 for further details.

4 of 6 new or added lines in 2 files covered. (66.67%)

993 existing lines in 25 files now uncovered.

19019 of 22414 relevant lines covered (84.85%)

23067.98 hits per line

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

79.68
/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)
219✔
108
    if (i.first != "uuid" && i.second != other.get(i.first)) return false;
202✔
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,321✔
154
  if (input[0] == 'p')
28,321✔
155
    return Task::pending;
24,874✔
156
  else if (input[0] == 'c')
3,447✔
157
    return Task::completed;
1,708✔
158
  else if (input[0] == 'd')
1,739✔
159
    return Task::deleted;
779✔
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,560✔
172
  if (s == Task::pending)
7,560✔
173
    return "pending";
6,567✔
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,480✔
200
  char now[22];
201
  snprintf(now, 22, "%lli", (long long int)time(nullptr));
6,480✔
202
  set(att, now);
6,480✔
203

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

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

211
  return false;
133,648✔
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,627✔
218

219
  return all;
752,049✔
220
}
221

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

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

230
////////////////////////////////////////////////////////////////////////////////
231
const std::string& Task::get_ref(const std::string& name) const {
6,142✔
232
  auto i = data.find(name);
6,142✔
233
  if (i != data.end()) return i->second;
6,142✔
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,099✔
272
  data[name] = value;
20,099✔
273

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

276
  recalc_urgency = true;
20,099✔
277
}
20,099✔
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,541✔
295
  if (!has("status")) return Task::pending;
32,541✔
296

297
  auto status = textToStatus(get("status"));
28,321✔
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,315✔
303
    return Task::waiting;
120✔
304
  }
305

306
  return status;
28,195✔
307
}
308

309
////////////////////////////////////////////////////////////////////////////////
310
void Task::setStatus(Task::status status) {
5,623✔
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,623✔
314

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

317
  recalc_urgency = true;
5,623✔
318
}
5,623✔
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; }
×
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;
109✔
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,735✔
513
  if (has("wait") && get("status") == "pending") {
27,735✔
514
    Datetime now;
191✔
515
    Datetime wait(get_date("wait"));
191✔
516
    if (wait > now) return true;
191✔
517
  }
518

519
  return false;
27,603✔
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,790✔
724
    data[static_cast<std::string>(item.prop)] = static_cast<std::string>(item.value);
3,008,050✔
725
  }
726

727
  // count annotations
728
  annotation_count = 0;
746,740✔
729
  for (auto i : data) {
3,754,790✔
730
    if (isAnnotationAttr(i.first)) {
3,008,050✔
731
      ++annotation_count;
1,066✔
732
    }
733
  }
3,008,050✔
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:
×
UNCOV
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:
×
UNCOV
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:
×
UNCOV
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);
168✔
920
    ++now;
168✔
921
  } while (has(key));
168✔
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,132✔
1053
    if (!isDepAttr(attr)) continue;
3,765,734✔
1054
    auto dep = attr2Dep(attr);
605✔
1055
    uuids.push_back(dep);
605✔
1056
  }
749,003✔
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,766✔
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 {
644✔
1282
  assert(isDepAttr(attr));
644✔
1283
  return attr.substr(4);
644✔
1284
}
1285

1286
////////////////////////////////////////////////////////////////////////////////
1287
bool Task::isAnnotationAttr(const std::string& attr) {
3,029,646✔
1288
  return attr.compare(0, 11, "annotation_") == 0;
3,029,646✔
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✔
UNCOV
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)
×
UNCOV
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
// The purpose of Task::validate is three-fold:
1413
//   1) To provide missing attributes where possible
1414
//   2) To provide suitable warnings about odd states
1415
//   3) To generate errors when the inconsistencies are not fixable
1416
//   4) To update status depending on other attributes
1417
//
1418
// Critically, note that despite the name this is not a read-only function.
1419
//
1420
void Task::validate(bool applyDefault /* = true */) {
5,168✔
1421
  Task::status status = Task::pending;
5,168✔
1422
  if (get("status") != "") status = getStatus();
5,168✔
1423

1424
  // 1) Provide missing attributes where possible
1425
  // Provide a UUID if necessary. Validate if present.
1426
  std::string uid = get("uuid");
10,332✔
1427
  if (has("uuid") && uid != "") {
5,166✔
1428
    Lexer lex(uid);
2,485✔
1429
    std::string token;
2,485✔
1430
    Lexer::Type type;
1431
    if (!lex.isUUID(token, type, true)) throw format("Not a valid UUID '{1}'.", uid);
2,485✔
1432
  } else
2,490✔
1433
    set("uuid", uuid());
2,681✔
1434

1435
  // TODO Obsolete remove for 3.0.0
1436
  // Recurring tasks get a special status.
1437
  if (status == Task::pending && has("due") && has("recur") &&
9,792✔
1438
      (!has("parent") || get("parent") == "") && (!has("template") || get("template") == "")) {
9,792✔
1439
    status = Task::recurring;
72✔
1440
  }
1441
  /*
1442
    // TODO Add for 3.0.0
1443
    if (status == Task::pending &&
1444
        has ("due")             &&
1445
        has ("recur")          &&
1446
        (! has ("template") || get ("template") == ""))
1447
    {
1448
      status = Task::recurring;
1449
    }
1450
  */
1451

1452
  // Tasks with a wait: date get a special status.
1453
  else if (status == Task::pending && has("wait") && get("wait") != "")
5,089✔
1454
    status = Task::waiting;
23✔
1455

1456
  // By default, tasks are pending.
1457
  else if (!has("status") || get("status") == "")
5,066✔
1458
    status = Task::pending;
2,589✔
1459

1460
  // Default to 'periodic' type recurrence.
1461
  if (status == Task::recurring && (!has("rtype") || get("rtype") == "")) {
5,161✔
1462
    set("rtype", "periodic");
72✔
1463
  }
1464

1465
  // Store the derived status.
1466
  setStatus(status);
5,161✔
1467

1468
#ifdef PRODUCT_TASKWARRIOR
1469
  // Provide an entry date unless user already specified one.
1470
  if (!has("entry") || get("entry") == "") setAsNow("entry");
5,161✔
1471

1472
  // Completed tasks need an end date, so inherit the entry date.
1473
  if ((status == Task::completed || status == Task::deleted) && (!has("end") || get("end") == ""))
5,161✔
1474
    setAsNow("end");
20✔
1475

1476
  // Pending tasks cannot have an end date, remove if present
1477
  if ((status == Task::pending) && (get("end") != "")) remove("end");
5,161✔
1478

1479
  // Provide a modified date unless user already specified one.
1480
  if (!has("modified") || get("modified") == "") setAsNow("modified");
5,161✔
1481

1482
  if (applyDefault && (!has("parent") || get("parent") == "")) {
5,161✔
1483
    // Override with default.project, if not specified.
1484
    if (Task::defaultProject != "" && !has("project")) {
4,471✔
1485
      if (Context::getContext().columns["project"]->validate(Task::defaultProject))
5✔
1486
        set("project", Task::defaultProject);
5✔
1487
    }
1488

1489
    // Override with default.due, if not specified.
1490
    if (Task::defaultDue != "" && !has("due")) {
4,471✔
1491
      if (Context::getContext().columns["due"]->validate(Task::defaultDue)) {
4✔
1492
        Duration dur(Task::defaultDue);
4✔
1493
        if (dur.toTime_t() != 0)
4✔
1494
          set("due", (Datetime() + dur.toTime_t()).toEpoch());
×
1495
        else
1496
          set("due", Datetime(Task::defaultDue).toEpoch());
4✔
1497
      }
1498
    }
1499

1500
    // Override with default.scheduled, if not specified.
1501
    if (Task::defaultScheduled != "" && !has("scheduled")) {
4,471✔
1502
      if (Context::getContext().columns["scheduled"]->validate(Task::defaultScheduled)) {
5✔
1503
        Duration dur(Task::defaultScheduled);
5✔
1504
        if (dur.toTime_t() != 0)
5✔
1505
          set("scheduled", (Datetime() + dur.toTime_t()).toEpoch());
×
1506
        else
1507
          set("scheduled", Datetime(Task::defaultScheduled).toEpoch());
5✔
1508
      }
1509
    }
1510

1511
    // If a UDA has a default value in the configuration,
1512
    // override with uda.(uda).default, if not specified.
1513
    // Gather a list of all UDAs with a .default value
1514
    std::vector<std::string> udas;
4,471✔
1515
    for (auto& var : Context::getContext().config) {
1,100,905✔
1516
      if (!var.first.compare(0, 4, "uda.", 4) && var.first.find(".default") != std::string::npos) {
1,096,434✔
1517
        auto period = var.first.find('.', 4);
8✔
1518
        if (period != std::string::npos) udas.push_back(var.first.substr(4, period - 4));
8✔
1519
      }
1520
    }
1521

1522
    if (udas.size()) {
4,471✔
1523
      // For each of those, setup the default value on the task now,
1524
      // of course only if we don't have one on the command line already
1525
      for (auto& uda : udas) {
16✔
1526
        std::string defVal = Context::getContext().config.get("uda." + uda + ".default");
16✔
1527

1528
        // If the default is empty, or we already have a value, skip it
1529
        if (defVal != "" && get(uda) == "") set(uda, defVal);
8✔
1530
      }
8✔
1531
    }
1532
  }
4,471✔
1533
#endif
1534

1535
  // 2) To provide suitable warnings about odd states
1536

1537
  // Date relationships.
1538
  validate_before("wait", "due");
5,161✔
1539
  validate_before("entry", "start");
5,161✔
1540
  validate_before("entry", "end");
5,161✔
1541
  validate_before("wait", "scheduled");
5,161✔
1542
  validate_before("scheduled", "start");
5,161✔
1543
  validate_before("scheduled", "due");
5,161✔
1544
  validate_before("scheduled", "end");
5,161✔
1545

1546
  // 3) To generate errors when the inconsistencies are not fixable
1547

1548
  // There is no fixing a missing description.
1549
  if (!has("description"))
5,161✔
1550
    throw std::string("A task must have a description.");
2✔
1551
  else if (get("description") == "")
5,159✔
1552
    throw std::string("Cannot add a task that is blank.");
×
1553

1554
  // Cannot have a recur frequency with no due date - when would it recur?
1555
  if (has("recur") && (!has("due") || get("due") == ""))
5,159✔
1556
    throw std::string("A recurring task must also have a 'due' date.");
1✔
1557

1558
  // Recur durations must be valid.
1559
  if (has("recur")) {
5,158✔
1560
    std::string value = get("recur");
688✔
1561
    if (value != "") {
344✔
1562
      Duration p;
344✔
1563
      std::string::size_type i = 0;
344✔
1564
      if (!p.parse(value, i))
344✔
1565
        // TODO Ideal location to map unsupported old recurrence periods to supported values.
1566
        throw format("The recurrence value '{1}' is not valid.", value);
×
1567
    }
1568
  }
344✔
1569
}
5,166✔
1570

1571
////////////////////////////////////////////////////////////////////////////////
1572
void Task::validate_before(const std::string& left, const std::string& right) {
36,127✔
1573
#ifdef PRODUCT_TASKWARRIOR
1574
  if (has(left) && has(right)) {
36,127✔
1575
    Datetime date_left(get_date(left));
552✔
1576
    Datetime date_right(get_date(right));
552✔
1577

1578
    // if date is zero, then it is being removed (e.g. "due: wait:1day")
1579
    if (date_left > date_right && date_right.toEpoch() != 0)
552✔
1580
      Context::getContext().footnote(format(
11✔
1581
          "Warning: You have specified that the '{1}' date is after the '{2}' date.", left, right));
1582
  }
1583
#endif
1584
}
36,127✔
1585

1586
////////////////////////////////////////////////////////////////////////////////
1587
// Encode values prior to serialization.
1588
//   [  -> &open;
1589
//   ]  -> &close;
1590
const std::string Task::encode(const std::string& value) const {
×
1591
  auto modified = str_replace(value, "[", "&open;");
×
1592
  return str_replace(modified, "]", "&close;");
×
1593
}
1594

1595
////////////////////////////////////////////////////////////////////////////////
1596
// Decode values after parse.
1597
//   [  <- &open;
1598
//   ]  <- &close;
1599
const std::string Task::decode(const std::string& value) const {
×
1600
  if (value.find('&') == std::string::npos) return value;
×
1601

1602
  auto modified = str_replace(value, "&open;", "[");
×
1603
  return str_replace(modified, "&close;", "]");
×
1604
}
1605

1606
////////////////////////////////////////////////////////////////////////////////
1607
int Task::determineVersion(const std::string& line) {
×
1608
  // Version 2 looks like:
1609
  //
1610
  //   uuid status [tags] [attributes] description\n
1611
  //
1612
  // Where uuid looks like:
1613
  //
1614
  //   27755d92-c5e9-4c21-bd8e-c3dd9e6d3cf7
1615
  //
1616
  // Scan for the hyphens in the uuid, the following space, and a valid status
1617
  // character.
1618
  if (line[8] == '-' && line[13] == '-' && line[18] == '-' && line[23] == '-' && line[36] == ' ' &&
×
1619
      (line[37] == '-' || line[37] == '+' || line[37] == 'X' || line[37] == 'r')) {
×
1620
    // Version 3 looks like:
1621
    //
1622
    //   uuid status [tags] [attributes] [annotations] description\n
1623
    //
1624
    // Scan for the number of [] pairs.
1625
    auto tagAtts = line.find("] [", 0);
×
1626
    auto attsAnno = line.find("] [", tagAtts + 1);
×
1627
    auto annoDesc = line.find("] ", attsAnno + 1);
×
1628
    if (tagAtts != std::string::npos && attsAnno != std::string::npos &&
×
1629
        annoDesc != std::string::npos)
1630
      return 3;
×
1631
    else
1632
      return 2;
×
1633
  }
1634

1635
  // Version 4 looks like:
1636
  //
1637
  //   [name:"value" ...]
1638
  //
1639
  // Scan for [, ] and :".
1640
  else if (line[0] == '[' && line[line.length() - 1] == ']' &&
×
1641
           line.find("uuid:\"") != std::string::npos)
×
1642
    return 4;
×
1643

1644
  // Version 1 looks like:
1645
  //
1646
  //   [tags] [attributes] description\n
1647
  //   X [tags] [attributes] description\n
1648
  //
1649
  // Scan for the first character being either the bracket or X.
1650
  else if (line.find("X [") == 0 ||
×
1651
           (line[0] == '[' && line.substr(line.length() - 1, 1) != "]" && line.length() > 3))
×
1652
    return 1;
×
1653

1654
  // Version 5?
1655
  //
1656
  // Fortunately, with the hindsight that will come with version 5, the
1657
  // identifying characteristics of 1, 2, 3 and 4 may be modified such that if 5
1658
  // has a UUID followed by a status, then there is still a way to differentiate
1659
  // between 2, 3, 4 and 5.
1660
  //
1661
  // The danger is that a version 3 binary reads and misinterprets a version 4
1662
  // file.  This is why it is a good idea to rely on an explicit version
1663
  // declaration rather than chance positioning.
1664

1665
  // Zero means 'no idea'.
1666
  return 0;
×
1667
}
1668

1669
////////////////////////////////////////////////////////////////////////////////
1670
// Urgency is defined as a polynomial, the value of which is calculated in this
1671
// function, according to:
1672
//
1673
//   U = A.t  + B.t  + C.t  ...
1674
//          a      b      c
1675
//
1676
//   U       = urgency
1677
//   A       = coefficient for term a
1678
//   t sub a = numeric scale from 0 -> 1, with 1 being the highest
1679
//             urgency, derived from one task attribute and mapped
1680
//             to the numeric scale
1681
//
1682
// See rfc31-urgency.txt for full details.
1683
//
1684
float Task::urgency_c() const {
1,019✔
1685
  float value = 0.0;
1,019✔
1686
#ifdef PRODUCT_TASKWARRIOR
1687
  value += fabsf(Task::urgencyProjectCoefficient) > epsilon
2,038✔
1688
               ? (urgency_project() * Task::urgencyProjectCoefficient)
1,019✔
1689
               : 0.0;
1690
  value += fabsf(Task::urgencyActiveCoefficient) > epsilon
2,038✔
1691
               ? (urgency_active() * Task::urgencyActiveCoefficient)
1,019✔
1692
               : 0.0;
1693
  value += fabsf(Task::urgencyScheduledCoefficient) > epsilon
2,038✔
1694
               ? (urgency_scheduled() * Task::urgencyScheduledCoefficient)
1,019✔
1695
               : 0.0;
1696
  value += fabsf(Task::urgencyWaitingCoefficient) > epsilon
2,038✔
1697
               ? (urgency_waiting() * Task::urgencyWaitingCoefficient)
1,019✔
1698
               : 0.0;
1699
  value += fabsf(Task::urgencyBlockedCoefficient) > epsilon
2,038✔
1700
               ? (urgency_blocked() * Task::urgencyBlockedCoefficient)
1,019✔
1701
               : 0.0;
1702
  value += fabsf(Task::urgencyAnnotationsCoefficient) > epsilon
2,038✔
1703
               ? (urgency_annotations() * Task::urgencyAnnotationsCoefficient)
1,019✔
1704
               : 0.0;
1705
  value += fabsf(Task::urgencyTagsCoefficient) > epsilon
2,038✔
1706
               ? (urgency_tags() * Task::urgencyTagsCoefficient)
1,019✔
1707
               : 0.0;
1708
  value += fabsf(Task::urgencyDueCoefficient) > epsilon
2,038✔
1709
               ? (urgency_due() * Task::urgencyDueCoefficient)
1,019✔
1710
               : 0.0;
1711
  value += fabsf(Task::urgencyBlockingCoefficient) > epsilon
2,038✔
1712
               ? (urgency_blocking() * Task::urgencyBlockingCoefficient)
1,019✔
1713
               : 0.0;
1714
  value += fabsf(Task::urgencyAgeCoefficient) > epsilon
2,038✔
1715
               ? (urgency_age() * Task::urgencyAgeCoefficient)
1,019✔
1716
               : 0.0;
1717

1718
  const std::string taskProjectName = get("project");
2,038✔
1719
  // Tag- and project-specific coefficients.
1720
  for (auto& var : Task::coefficients) {
5,294✔
1721
    if (fabs(var.second) > epsilon) {
4,275✔
1722
      if (!var.first.compare(0, 13, "urgency.user.", 13)) {
4,275✔
1723
        // urgency.user.project.<project>.coefficient
1724
        auto end = std::string::npos;
1,226✔
1725
        if (var.first.substr(13, 8) == "project." &&
1,325✔
1726
            (end = var.first.find(".coefficient")) != std::string::npos) {
99✔
1727
          std::string project = var.first.substr(21, end - 21);
99✔
1728

1729
          if (taskProjectName == project || taskProjectName.find(project + '.') == 0) {
99✔
1730
            value += var.second;
3✔
1731
          }
1732
        }
99✔
1733

1734
        // urgency.user.tag.<tag>.coefficient
1735
        if (var.first.substr(13, 4) == "tag." &&
2,352✔
1736
            (end = var.first.find(".coefficient")) != std::string::npos) {
1,126✔
1737
          std::string tag = var.first.substr(17, end - 17);
1,126✔
1738

1739
          if (hasTag(tag)) value += var.second;
1,126✔
1740
        }
1,126✔
1741

1742
        // urgency.user.keyword.<keyword>.coefficient
1743
        if (var.first.substr(13, 8) == "keyword." &&
1,227✔
1744
            (end = var.first.find(".coefficient")) != std::string::npos) {
1✔
1745
          std::string keyword = var.first.substr(21, end - 21);
1✔
1746

1747
          if (get("description").find(keyword) != std::string::npos) value += var.second;
1✔
1748
        }
1✔
1749
      } else if (var.first.substr(0, 12) == "urgency.uda.") {
3,049✔
1750
        // urgency.uda.<name>.coefficient
1751
        // urgency.uda.<name>.<value>.coefficient
1752
        auto end = var.first.find(".coefficient");
3,049✔
1753
        if (end != std::string::npos) {
3,049✔
1754
          const std::string uda = var.first.substr(12, end - 12);
3,049✔
1755
          auto dot = uda.find('.');
3,049✔
1756
          if (dot == std::string::npos) {
3,049✔
1757
            // urgency.uda.<name>.coefficient
1758
            if (has(uda)) value += var.second;
1✔
1759
          } else {
1760
            // urgency.uda.<name>.<value>.coefficient
1761
            if (get(uda.substr(0, dot)) == uda.substr(dot + 1)) value += var.second;
3,048✔
1762
          }
1763
        }
3,049✔
1764
      }
1765
    }
1766
  }
1767

1768
  if (is_blocking && Context::getContext().config.getBoolean("urgency.inherit")) {
1,019✔
1769
    float prev = value;
3✔
1770
    value = std::max(value, urgency_inherit());
3✔
1771

1772
    // This is a hackish way of making sure parent tasks are sorted above
1773
    // child tasks.  For reports that hide blocked tasks, this is not needed.
1774
    if (prev <= value) value += 0.01;
3✔
1775
  }
1776
#endif
1777

1778
  return value;
1,019✔
1779
}
1,019✔
1780

1781
////////////////////////////////////////////////////////////////////////////////
1782
float Task::urgency() {
1,701✔
1783
  if (recalc_urgency) {
1,701✔
1784
    urgency_value = urgency_c();
760✔
1785

1786
    // Return the sum of all terms.
1787
    recalc_urgency = false;
760✔
1788
  }
1789

1790
  return urgency_value;
1,701✔
1791
}
1792

1793
////////////////////////////////////////////////////////////////////////////////
1794
float Task::urgency_inherit() const {
3✔
1795
  float v = -FLT_MAX;
3✔
1796
#ifdef PRODUCT_TASKWARRIOR
1797
  // Calling getBlockedTasks is rather expensive.
1798
  // It is called recursively for each dependency in the chain here.
1799
  for (auto& task : getBlockedTasks()) {
6✔
1800
    // Find highest urgency in all blocked tasks.
1801
    v = std::max(v, task.urgency());
3✔
1802
  }
3✔
1803
#endif
1804

1805
  return v;
3✔
1806
}
1807

1808
////////////////////////////////////////////////////////////////////////////////
1809
float Task::urgency_project() const {
1,076✔
1810
  if (has("project")) return 1.0;
1,076✔
1811

1812
  return 0.0;
823✔
1813
}
1814

1815
////////////////////////////////////////////////////////////////////////////////
1816
float Task::urgency_active() const {
1,076✔
1817
  if (has("start")) return 1.0;
1,076✔
1818

1819
  return 0.0;
1,035✔
1820
}
1821

1822
////////////////////////////////////////////////////////////////////////////////
1823
float Task::urgency_scheduled() const {
1,076✔
1824
  if (has("scheduled") && get_date("scheduled") < time(nullptr)) return 1.0;
1,076✔
1825

1826
  return 0.0;
1,073✔
1827
}
1828

1829
////////////////////////////////////////////////////////////////////////////////
1830
float Task::urgency_waiting() const {
1,076✔
1831
  if (is_waiting()) return 1.0;
1,076✔
1832

1833
  return 0.0;
1,070✔
1834
}
1835

1836
////////////////////////////////////////////////////////////////////////////////
1837
// A task is blocked only if the task it depends upon is pending/waiting.
1838
float Task::urgency_blocked() const {
1,064✔
1839
  if (is_blocked) return 1.0;
1,064✔
1840

1841
  return 0.0;
1,051✔
1842
}
1843

1844
////////////////////////////////////////////////////////////////////////////////
1845
float Task::urgency_annotations() const {
1,076✔
1846
  if (annotation_count >= 3)
1,076✔
1847
    return 1.0;
4✔
1848
  else if (annotation_count == 2)
1,072✔
1849
    return 0.9;
6✔
1850
  else if (annotation_count == 1)
1,066✔
1851
    return 0.8;
39✔
1852

1853
  return 0.0;
1,027✔
1854
}
1855

1856
////////////////////////////////////////////////////////////////////////////////
1857
float Task::urgency_tags() const {
1,076✔
1858
  switch (getTagCount()) {
1,076✔
1859
    case 0:
859✔
1860
      return 0.0;
859✔
1861
    case 1:
186✔
1862
      return 0.8;
186✔
1863
    case 2:
27✔
1864
      return 0.9;
27✔
1865
    default:
4✔
1866
      return 1.0;
4✔
1867
  }
1868
}
1869

1870
////////////////////////////////////////////////////////////////////////////////
1871
//
1872
//     Past                  Present                              Future
1873
//     Overdue               Due                                     Due
1874
//
1875
//     -7 -6 -5 -4 -3 -2 -1  0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 days
1876
//
1877
// <-- 1.0                         linear                            0.2 -->
1878
//     capped                                                        capped
1879
//
1880
//
1881
float Task::urgency_due() const {
1,076✔
1882
  if (has("due")) {
1,076✔
1883
    Datetime now;
268✔
1884
    Datetime due(get_date("due"));
268✔
1885

1886
    // Map a range of 21 days to the value 0.2 - 1.0
1887
    float days_overdue = (now - due) / 86400.0;
268✔
1888
    if (days_overdue >= 7.0)
268✔
1889
      return 1.0;  // < 1 wk ago
20✔
1890
    else if (days_overdue >= -14.0)
248✔
1891
      return ((days_overdue + 14.0) * 0.8 / 21.0) + 0.2;
227✔
1892
    else
1893
      return 0.2;  // > 2 wks
21✔
1894
  }
1895

1896
  return 0.0;
808✔
1897
}
1898

1899
////////////////////////////////////////////////////////////////////////////////
1900
float Task::urgency_age() const {
1,064✔
1901
  if (!has("entry")) return 1.0;
1,064✔
1902

1903
  Datetime now;
1,064✔
1904
  Datetime entry(get_date("entry"));
1,064✔
1905
  int age = (now - entry) / 86400;  // in days
1,064✔
1906

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

1909
  return (1.0 * age / Task::urgencyAgeMax);
1,015✔
1910
}
1911

1912
////////////////////////////////////////////////////////////////////////////////
1913
float Task::urgency_blocking() const {
1,064✔
1914
  if (is_blocking) return 1.0;
1,064✔
1915

1916
  return 0.0;
1,046✔
1917
}
1918

1919
#ifdef PRODUCT_TASKWARRIOR
1920
////////////////////////////////////////////////////////////////////////////////
1921
// Arguably does not belong here. This method reads the parse tree and calls
1922
// Task methods. It could be a standalone function with no loss in access, as
1923
// well as reducing the object depdendencies of Task.
1924
//
1925
// It came from the Command base object, but doesn't really belong there either.
1926
void Task::modify(modType type, bool text_required /* = false */) {
2,167✔
1927
  std::string label = "   [1;37;43mMODIFICATION [0m ";
2,167✔
1928

1929
  // while reading the parse tree, consider DOM references in the context of
1930
  // this task
1931
  auto currentTask = Context::getContext().withCurrentTask(this);
2,167✔
1932

1933
  // Need this for later comparison.
1934
  auto originalStatus = getStatus();
2,167✔
1935

1936
  std::string text = "";
2,167✔
1937
  bool mods = false;
2,167✔
1938
  for (auto& a : Context::getContext().cli2._args) {
14,177✔
1939
    if (a.hasTag("MODIFICATION")) {
12,404✔
1940
      if (a._lextype == Lexer::Type::pair) {
3,252✔
1941
        // 'canonical' is the canonical name. Needs to be said.
1942
        // 'value' requires eval.
1943
        std::string name = a.attribute("canonical");
2,546✔
1944
        std::string value = a.attribute("value");
2,546✔
1945
        if (value == "" || value == "''" || value == "\"\"") {
1,273✔
1946
          // Special case: Handle bulk removal of 'tags' and 'depends" virtual
1947
          // attributes
1948
          if (name == "depends") {
14✔
1949
            for (auto dep : getDependencyUUIDs()) removeDependency(dep);
3✔
1950
          } else if (name == "tags") {
13✔
1951
            for (auto tag : getTags()) removeTag(tag);
4✔
1952
          }
1953

1954
          // ::composeF4 will skip if the value is blank, but the presence of
1955
          // the attribute will prevent ::validate from applying defaults.
1956
          if ((has(name) && get(name) != "") ||
28✔
1957
              (name == "due" && Context::getContext().config.has("default.due")) ||
25✔
1958
              (name == "scheduled" && Context::getContext().config.has("default.scheduled")) ||
49✔
1959
              (name == "project" && Context::getContext().config.has("default.project"))) {
21✔
1960
            mods = true;
11✔
1961
            set(name, "");
11✔
1962
          }
1963

1964
          Context::getContext().debug(label + name + " <-- ''");
14✔
1965
        } else {
1966
          Lexer::dequote(value);
1,259✔
1967

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

1973
          // Delegate modification to the column object or their base classes.
1974
          if (name == "depends" || name == "tags" || name == "recur" || column->type() == "date" ||
2,855✔
1975
              column->type() == "duration" || column->type() == "numeric" ||
3,219✔
1976
              column->type() == "string") {
364✔
1977
            column->modify(*this, value);
1,259✔
1978
            mods = true;
865✔
1979
          }
1980

1981
          else
1982
            throw format("Unrecognized column type '{1}' for column '{2}'", column->type(), name);
×
1983
        }
1984
      }
1,667✔
1985

1986
      // Perform description/annotation substitution.
1987
      else if (a._lextype == Lexer::Type::substitution) {
1,979✔
1988
        Context::getContext().debug(label + "substitute " + a.attribute("raw"));
35✔
1989
        substitute(a.attribute("from"), a.attribute("to"), a.attribute("flags"));
35✔
1990
        mods = true;
35✔
1991
      }
1992

1993
      // Tags need special handling because they are essentially a vector stored
1994
      // in a single string, therefore Task::{add,remove}Tag must be called as
1995
      // appropriate.
1996
      else if (a._lextype == Lexer::Type::tag) {
1,944✔
1997
        std::string tag = a.attribute("name");
388✔
1998
        feedback_reserved_tags(tag);
194✔
1999

2000
        if (a.attribute("sign") == "+") {
194✔
2001
          Context::getContext().debug(label + "tags <-- add '" + tag + '\'');
186✔
2002
          addTag(tag);
186✔
2003
          feedback_special_tags(*this, tag);
186✔
2004
        } else {
2005
          Context::getContext().debug(label + "tags <-- remove '" + tag + '\'');
8✔
2006
          removeTag(tag);
8✔
2007
        }
2008

2009
        mods = true;
194✔
2010
      }
194✔
2011

2012
      // Unknown args are accumulated as though they were WORDs.
2013
      else {
2014
        if (text != "") text += ' ';
1,750✔
2015
        text += a.attribute("raw");
1,750✔
2016
      }
2017
    }
2018
  }
2019

2020
  // Task::modType determines what happens to the WORD arguments, if there are
2021
  //  any.
2022
  if (text != "") {
1,773✔
2023
    Lexer::dequote(text);
1,382✔
2024

2025
    switch (type) {
1,382✔
2026
      case modReplace:
1,288✔
2027
        Context::getContext().debug(label + "description <-- '" + text + '\'');
1,288✔
2028
        set("description", text);
1,288✔
2029
        break;
1,288✔
2030

2031
      case modPrepend:
4✔
2032
        Context::getContext().debug(label + "description <-- '" + text + "' + description");
4✔
2033
        set("description", text + ' ' + get("description"));
4✔
2034
        break;
4✔
2035

2036
      case modAppend:
6✔
2037
        Context::getContext().debug(label + "description <-- description + '" + text + '\'');
6✔
2038
        set("description", get("description") + ' ' + text);
6✔
2039
        break;
6✔
2040

2041
      case modAnnotate:
84✔
2042
        Context::getContext().debug(label + "new annotation <-- '" + text + '\'');
84✔
2043
        addAnnotation(text);
84✔
2044
        break;
84✔
2045
    }
2046
  } else if (!mods && text_required)
391✔
2047
    throw std::string("Additional text must be provided.");
4✔
2048

2049
  // Modifying completed/deleted tasks generates a message, if the modification
2050
  // does not change status.
2051
  if ((getStatus() == Task::completed || getStatus() == Task::deleted) &&
1,774✔
2052
      getStatus() == originalStatus) {
5✔
2053
    auto uuid = get("uuid").substr(0, 8);
10✔
2054
    Context::getContext().footnote(
10✔
2055
        format("Note: Modified task {1} is {2}. You may wish to make this task pending with: task "
15✔
2056
               "{3} modify status:pending",
2057
               uuid, get("status"), uuid));
10✔
2058
  }
5✔
2059
}
2,963✔
2060
#endif
2061

2062
////////////////////////////////////////////////////////////////////////////////
2063
// Compare this task to another and summarize the differences for display, in
2064
// the future tense ("Foo will be set to ..").
2065
std::string Task::diff(const Task& after) const {
358✔
2066
  // Attributes are all there is, so figure the different attribute names
2067
  // between this (before) and after.
2068
  std::vector<std::string> beforeAtts;
358✔
2069
  for (auto& att : data) beforeAtts.push_back(att.first);
2,554✔
2070

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

2074
  std::vector<std::string> beforeOnly;
358✔
2075
  std::vector<std::string> afterOnly;
358✔
2076
  listDiff(beforeAtts, afterAtts, beforeOnly, afterOnly);
358✔
2077

2078
  // Now start generating a description of the differences.
2079
  std::stringstream out;
358✔
2080
  for (auto& name : beforeOnly) {
405✔
2081
    if (isAnnotationAttr(name)) {
47✔
2082
      out << "  - " << format("Annotation {1} will be removed.", name) << "\n";
6✔
2083
    } else if (isTagAttr(name)) {
41✔
2084
      out << "  - " << format("Tag {1} will be removed.", attr2Tag(name)) << "\n";
10✔
2085
    } else if (isDepAttr(name)) {
31✔
2086
      out << "  - " << format("Depenency on {1} will be removed.", attr2Dep(name)) << "\n";
6✔
2087
    } else if (name == "depends" || name == "tags") {
25✔
2088
      // do nothing for legacy attributes
2089
    } else {
2090
      out << "  - " << format("{1} will be deleted.", Lexer::ucFirst(name)) << "\n";
19✔
2091
    }
2092
  }
2093

2094
  for (auto& name : afterOnly) {
731✔
2095
    if (isAnnotationAttr(name)) {
373✔
2096
      out << format("Annotation of {1} will be added.\n", after.get(name));
88✔
2097
    } else if (isTagAttr(name)) {
285✔
2098
      out << format("Tag {1} will be added.\n", attr2Tag(name));
33✔
2099
    } else if (isDepAttr(name)) {
252✔
2100
      out << format("Dependency on {1} will be added.\n", attr2Dep(name));
33✔
2101
    } else if (name == "depends" || name == "tags") {
219✔
2102
      // do nothing for legacy attributes
2103
    } else
2104
      out << "  - "
2105
          << format("{1} will be set to '{2}'.", Lexer::ucFirst(name),
342✔
2106
                    renderAttribute(name, after.get(name)))
342✔
2107
          << "\n";
342✔
2108
  }
2109

2110
  for (auto& name : beforeAtts) {
2,554✔
2111
    // Ignore UUID differences, and find values that changed, but are not also
2112
    // in the beforeOnly and afterOnly lists, which have been handled above..
2113
    if (name != "uuid" && get(name) != after.get(name) &&
4,257✔
2114
        std::find(beforeOnly.begin(), beforeOnly.end(), name) == beforeOnly.end() &&
4,257✔
2115
        std::find(afterOnly.begin(), afterOnly.end(), name) == afterOnly.end()) {
2,370✔
2116
      if (name == "depends" || name == "tags") {
174✔
2117
        // do nothing for legacy attributes
2118
      } else if (isTagAttr(name) || isDepAttr(name)) {
165✔
2119
        // ignore new attributes
2120
      } else if (isAnnotationAttr(name)) {
165✔
2121
        out << format("Annotation will be changed to {1}.\n", after.get(name));
4✔
2122
      } else
2123
        out << "  - "
2124
            << format("{1} will be changed from '{2}' to '{3}'.", Lexer::ucFirst(name),
322✔
2125
                      renderAttribute(name, get(name)), renderAttribute(name, after.get(name)))
322✔
2126
            << "\n";
322✔
2127
    }
2128
  }
2129

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

2133
  return out.str();
716✔
2134
}
358✔
2135

2136
////////////////////////////////////////////////////////////////////////////////
2137
// Similar to diff, but formatted for inclusion in the output of the info command
2138
std::string Task::diffForInfo(const Task& after, const std::string& dateformat,
×
2139
                              long& last_timestamp, const long current_timestamp) const {
2140
  // Attributes are all there is, so figure the different attribute names
2141
  // between before and after.
2142
  std::vector<std::string> beforeAtts;
×
2143
  for (auto& att : data) beforeAtts.push_back(att.first);
×
2144

2145
  std::vector<std::string> afterAtts;
×
2146
  for (auto& att : after.data) afterAtts.push_back(att.first);
×
2147

2148
  std::vector<std::string> beforeOnly;
×
2149
  std::vector<std::string> afterOnly;
×
2150
  listDiff(beforeAtts, afterAtts, beforeOnly, afterOnly);
×
2151

2152
  // Now start generating a description of the differences.
2153
  std::stringstream out;
×
2154
  for (auto& name : beforeOnly) {
×
2155
    if (isAnnotationAttr(name)) {
×
2156
      out << format("Annotation '{1}' deleted.\n", get(name));
×
2157
    } else if (isTagAttr(name)) {
×
2158
      out << format("Tag '{1}' deleted.\n", attr2Tag(name));
×
2159
    } else if (isDepAttr(name)) {
×
2160
      out << format("Dependency on '{1}' deleted.\n", attr2Dep(name));
×
2161
    } else if (name == "depends" || name == "tags") {
×
2162
      // do nothing for legacy attributes
2163
    } else if (name == "start") {
×
2164
      Datetime started(get("start"));
×
2165
      Datetime stopped;
×
2166

2167
      if (after.has("end"))
×
2168
        // Task was marked as finished, use end time
2169
        stopped = Datetime(after.get("end"));
×
2170
      else
2171
        // Start attribute was removed, use modification time
2172
        stopped = Datetime(current_timestamp);
×
2173

2174
      out << format("{1} deleted (duration: {2}).", Lexer::ucFirst(name),
×
2175
                    Duration(stopped - started).format())
×
2176
          << "\n";
×
2177
    } else {
2178
      out << format("{1} deleted.\n", Lexer::ucFirst(name));
×
2179
    }
2180
  }
2181

2182
  for (auto& name : afterOnly) {
×
2183
    if (isAnnotationAttr(name)) {
×
2184
      out << format("Annotation of '{1}' added.\n", after.get(name));
×
2185
    } else if (isTagAttr(name)) {
×
2186
      out << format("Tag '{1}' added.\n", attr2Tag(name));
×
2187
    } else if (isDepAttr(name)) {
×
2188
      out << format("Dependency on '{1}' added.\n", attr2Dep(name));
×
2189
    } else if (name == "depends" || name == "tags") {
×
2190
      // do nothing for legacy attributes
2191
    } else {
2192
      if (name == "start") last_timestamp = current_timestamp;
×
2193

2194
      out << format("{1} set to '{2}'.", Lexer::ucFirst(name),
×
2195
                    renderAttribute(name, after.get(name), dateformat))
×
2196
          << "\n";
×
2197
    }
2198
  }
2199

2200
  for (auto& name : beforeAtts)
×
2201
    if (name != "uuid" && name != "modified" && get(name) != after.get(name) && get(name) != "" &&
×
2202
        after.get(name) != "") {
×
2203
      if (name == "depends" || name == "tags") {
×
2204
        // do nothing for legacy attributes
2205
      } else if (isTagAttr(name) || isDepAttr(name)) {
×
2206
        // ignore new attributes
2207
      } else if (isAnnotationAttr(name)) {
×
2208
        out << format("Annotation changed to '{1}'.\n", after.get(name));
×
2209
      } else
2210
        out << format("{1} changed from '{2}' to '{3}'.", Lexer::ucFirst(name),
×
2211
                      renderAttribute(name, get(name), dateformat),
×
2212
                      renderAttribute(name, after.get(name), dateformat))
×
2213
            << "\n";
×
2214
    }
2215

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

2219
  return out.str();
×
2220
}
2221

2222
////////////////////////////////////////////////////////////////////////////////
2223
// Similar to diff, but formatted as a side-by-side table for an Undo preview
2224
Table Task::diffForUndoSide(const Task& after) const {
×
2225
  // Set the colors.
2226
  Color color_red(
2227
      Context::getContext().color() ? Context::getContext().config.get("color.undo.before") : "");
×
2228
  Color color_green(
2229
      Context::getContext().color() ? Context::getContext().config.get("color.undo.after") : "");
×
2230

2231
  // Attributes are all there is, so figure the different attribute names
2232
  // between before and after.
2233
  Table view;
×
2234
  view.width(Context::getContext().getWidth());
×
2235
  view.intraPadding(2);
×
2236
  view.add("");
×
2237
  view.add("Prior Values");
×
2238
  view.add("Current Values");
×
2239
  setHeaderUnderline(view);
×
2240

2241
  if (!is_empty()) {
×
2242
    const Task& before = *this;
×
2243

2244
    std::vector<std::string> beforeAtts;
×
2245
    for (auto& att : before.data) beforeAtts.push_back(att.first);
×
2246

2247
    std::vector<std::string> afterAtts;
×
2248
    for (auto& att : after.data) afterAtts.push_back(att.first);
×
2249

2250
    std::vector<std::string> beforeOnly;
×
2251
    std::vector<std::string> afterOnly;
×
2252
    listDiff(beforeAtts, afterAtts, beforeOnly, afterOnly);
×
2253

2254
    int row;
2255
    for (auto& name : beforeOnly) {
×
2256
      row = view.addRow();
×
2257
      view.set(row, 0, name);
×
2258
      view.set(row, 1, renderAttribute(name, before.get(name)), color_red);
×
2259
    }
2260

2261
    for (auto& att : before.data) {
×
2262
      std::string priorValue = before.get(att.first);
×
2263
      std::string currentValue = after.get(att.first);
×
2264

2265
      if (currentValue != "") {
×
2266
        row = view.addRow();
×
2267
        view.set(row, 0, att.first);
×
2268
        view.set(row, 1, renderAttribute(att.first, priorValue),
×
2269
                 (priorValue != currentValue ? color_red : Color()));
×
2270
        view.set(row, 2, renderAttribute(att.first, currentValue),
×
2271
                 (priorValue != currentValue ? color_green : Color()));
×
2272
      }
2273
    }
2274

2275
    for (auto& name : afterOnly) {
×
2276
      row = view.addRow();
×
2277
      view.set(row, 0, name);
×
2278
      view.set(row, 2, renderAttribute(name, after.get(name)), color_green);
×
2279
    }
2280
  } else {
×
2281
    int row;
2282
    for (auto& att : after.data) {
×
2283
      row = view.addRow();
×
2284
      view.set(row, 0, att.first);
×
2285
      view.set(row, 2, renderAttribute(att.first, after.get(att.first)), color_green);
×
2286
    }
2287
  }
2288

2289
  return view;
×
2290
}
2291

2292
////////////////////////////////////////////////////////////////////////////////
2293
// Similar to diff, but formatted as a diff for an Undo preview
2294
Table Task::diffForUndoPatch(const Task& after, const Datetime& lastChange) const {
×
2295
  // This style looks like this:
2296
  //  --- before    2009-07-04 00:00:25.000000000 +0200
2297
  //  +++ after    2009-07-04 00:00:45.000000000 +0200
2298
  //
2299
  // - name: old           // att deleted
2300
  // + name:
2301
  //
2302
  // - name: old           // att changed
2303
  // + name: new
2304
  //
2305
  // - name:
2306
  // + name: new           // att added
2307
  //
2308

2309
  // Set the colors.
2310
  Color color_red(
2311
      Context::getContext().color() ? Context::getContext().config.get("color.undo.before") : "");
×
2312
  Color color_green(
2313
      Context::getContext().color() ? Context::getContext().config.get("color.undo.after") : "");
×
2314

2315
  const Task& before = *this;
×
2316

2317
  // Generate table header.
2318
  Table view;
×
2319
  view.width(Context::getContext().getWidth());
×
2320
  view.intraPadding(2);
×
2321
  view.add("");
×
2322
  view.add("");
×
2323

2324
  int row = view.addRow();
×
2325
  view.set(row, 0, "--- previous state", color_red);
×
2326
  view.set(row, 1, "Undo will restore this state", color_red);
×
2327

2328
  row = view.addRow();
×
2329
  view.set(row, 0, "+++ current state ", color_green);
×
2330
  view.set(row, 1,
×
2331
           format("Change made {1}",
×
2332
                  lastChange.toString(Context::getContext().config.get("dateformat"))),
×
2333
           color_green);
2334

2335
  view.addRow();
×
2336

2337
  // Add rows to table showing diffs.
2338
  std::vector<std::string> all = Context::getContext().getColumns();
×
2339

2340
  // Now factor in the annotation attributes.
2341
  for (auto& it : before.data)
×
2342
    if (it.first.substr(0, 11) == "annotation_") all.push_back(it.first);
×
2343

2344
  for (auto& it : after.data)
×
2345
    if (it.first.substr(0, 11) == "annotation_") all.push_back(it.first);
×
2346

2347
  // Now render all the attributes.
2348
  std::sort(all.begin(), all.end());
×
2349

2350
  std::string before_att;
×
2351
  std::string after_att;
×
2352
  std::string last_att;
×
2353
  for (auto& a : all) {
×
2354
    if (a != last_att)  // Skip duplicates.
×
2355
    {
2356
      last_att = a;
×
2357

2358
      before_att = before.get(a);
×
2359
      after_att = after.get(a);
×
2360

2361
      // Don't report different uuid.
2362
      // Show nothing if values are the unchanged.
2363
      if (a == "uuid" || before_att == after_att) {
×
2364
        // Show nothing - no point displaying that which did not change.
2365

2366
        // row = view.addRow ();
2367
        // view.set (row, 0, *a + ":");
2368
        // view.set (row, 1, before_att);
2369
      }
2370

2371
      // Attribute deleted.
2372
      else if (before_att != "" && after_att == "") {
×
2373
        row = view.addRow();
×
2374
        view.set(row, 0, '-' + a + ':', color_red);
×
2375
        view.set(row, 1, before_att, color_red);
×
2376

2377
        row = view.addRow();
×
2378
        view.set(row, 0, '+' + a + ':', color_green);
×
2379
      }
2380

2381
      // Attribute added.
2382
      else if (before_att == "" && after_att != "") {
×
2383
        row = view.addRow();
×
2384
        view.set(row, 0, '-' + a + ':', color_red);
×
2385

2386
        row = view.addRow();
×
2387
        view.set(row, 0, '+' + a + ':', color_green);
×
2388
        view.set(row, 1, after_att, color_green);
×
2389
      }
2390

2391
      // Attribute changed.
2392
      else {
2393
        row = view.addRow();
×
2394
        view.set(row, 0, '-' + a + ':', color_red);
×
2395
        view.set(row, 1, before_att, color_red);
×
2396

2397
        row = view.addRow();
×
2398
        view.set(row, 0, '+' + a + ':', color_green);
×
2399
        view.set(row, 1, after_att, color_green);
×
2400
      }
2401
    }
2402
  }
2403

2404
  return view;
×
2405
}
2406

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