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

GothenburgBitFactory / taskwarrior / 12357332129

16 Dec 2024 04:49PM UTC coverage: 84.906% (-0.6%) from 85.522%
12357332129

Pull #3725

github

web-flow
[pre-commit.ci] pre-commit autoupdate

updates:
- [github.com/pre-commit/mirrors-clang-format: v19.1.4 → v19.1.5](https://github.com/pre-commit/mirrors-clang-format/compare/v19.1.4...v19.1.5)
Pull Request #3725: [pre-commit.ci] pre-commit autoupdate

19278 of 22705 relevant lines covered (84.91%)

23266.13 hits per line

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

95.93
/src/commands/CmdInfo.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 <CmdInfo.h>
31
#include <Context.h>
32
#include <Datetime.h>
33
#include <Duration.h>
34
#include <Filter.h>
35
#include <Lexer.h>
36
#include <Operation.h>
37
#include <format.h>
38
#include <main.h>
39
#include <math.h>
40
#include <shared.h>
41
#include <stdlib.h>
42
#include <taskchampion-cpp/lib.h>
43
#include <util.h>
44

45
#include <algorithm>
46
#include <sstream>
47

48
////////////////////////////////////////////////////////////////////////////////
49
CmdInfo::CmdInfo() {
4,495✔
50
  _keyword = "information";
4,495✔
51
  _usage = "task <filter> information";
4,495✔
52
  _description = "Shows all data and metadata";
4,495✔
53
  _read_only = true;
4,495✔
54

55
  // This is inaccurate, but it does prevent a GC.  While this doesn't make a
56
  // lot of sense, given that the info command shows the ID, it does mimic the
57
  // behavior of versions prior to 2.0, which the test suite relies upon.
58
  //
59
  // Once the test suite is completely modified, this can be corrected.
60
  _displays_id = false;
4,495✔
61
  _needs_gc = false;
4,495✔
62
  _uses_context = false;
4,495✔
63
  _accepts_filter = true;
4,495✔
64
  _accepts_modifications = false;
4,495✔
65
  _accepts_miscellaneous = false;
4,495✔
66
  _category = Command::Category::metadata;
4,495✔
67
}
4,495✔
68

69
////////////////////////////////////////////////////////////////////////////////
70
int CmdInfo::execute(std::string& output) {
105✔
71
  auto rc = 0;
105✔
72

73
  // Apply filter.
74
  Filter filter;
105✔
75
  std::vector<Task> filtered;
105✔
76
  filter.subset(filtered);
105✔
77

78
  if (!filtered.size()) {
105✔
79
    Context::getContext().footnote("No matches.");
4✔
80
    rc = 1;
2✔
81
  }
82

83
  // Determine the output date format, which uses a hierarchy of definitions.
84
  //   rc.dateformat.info
85
  //   rc.dateformat
86
  auto dateformat = Context::getContext().config.get("dateformat.info");
210✔
87
  if (dateformat == "") dateformat = Context::getContext().config.get("dateformat");
107✔
88

89
  auto dateformatanno = Context::getContext().config.get("dateformat.annotation");
210✔
90
  if (dateformatanno == "") dateformatanno = dateformat;
105✔
91

92
  // Render each task.
93
  std::stringstream out;
105✔
94
  for (auto& task : filtered) {
210✔
95
    Table view;
105✔
96
    view.width(Context::getContext().getWidth());
105✔
97
    if (Context::getContext().config.getBoolean("obfuscate")) view.obfuscate();
315✔
98
    if (Context::getContext().color()) view.forceColor();
105✔
99
    view.add("Name");
210✔
100
    view.add("Value");
105✔
101
    setHeaderUnderline(view);
105✔
102

103
    Datetime now;
105✔
104

105
    // id
106
    auto row = view.addRow();
105✔
107
    view.set(row, 0, "ID");
210✔
108
    view.set(row, 1, (task.id ? format(task.id) : "-"));
105✔
109

110
    std::string status = Lexer::ucFirst(Task::statusToText(task.getStatus()));
105✔
111

112
    // description
113
    Color c;
105✔
114
    autoColorize(task, c);
105✔
115
    auto description = task.get("description");
105✔
116
    auto indent = Context::getContext().config.getInteger("indent.annotation");
210✔
117

118
    for (auto& anno : task.getAnnotations())
121✔
119
      description += '\n' + std::string(indent, ' ') +
32✔
120
                     Datetime(anno.first.substr(11)).toString(dateformatanno) + ' ' + anno.second;
185✔
121

122
    row = view.addRow();
105✔
123
    view.set(row, 0, "Description");
210✔
124
    view.set(row, 1, description, c);
105✔
125

126
    // status
127
    row = view.addRow();
105✔
128
    view.set(row, 0, "Status");
210✔
129
    view.set(row, 1, status);
105✔
130

131
    // project
132
    if (task.has("project")) {
210✔
133
      row = view.addRow();
14✔
134
      view.set(row, 0, "Project");
28✔
135
      view.set(row, 1, task.get("project"));
42✔
136
    }
137

138
    // dependencies: blocked
139
    {
140
      auto blocked = task.getDependencyTasks();
105✔
141
      if (blocked.size()) {
105✔
142
        std::stringstream message;
1✔
143
        for (auto& block : blocked) message << block.id << ' ' << block.get("description") << '\n';
3✔
144

145
        row = view.addRow();
1✔
146
        view.set(row, 0, "This task blocked by");
2✔
147
        view.set(row, 1, message.str());
1✔
148
      }
1✔
149
    }
105✔
150

151
    // dependencies: blocking
152
    {
153
      auto blocking = task.getBlockedTasks();
105✔
154
      if (blocking.size()) {
105✔
155
        std::stringstream message;
1✔
156
        for (auto& block : blocking) message << block.id << ' ' << block.get("description") << '\n';
3✔
157

158
        row = view.addRow();
1✔
159
        view.set(row, 0, "This task is blocking");
2✔
160
        view.set(row, 1, message.str());
1✔
161
      }
1✔
162
    }
105✔
163

164
    // recur
165
    if (task.has("recur")) {
210✔
166
      row = view.addRow();
8✔
167
      view.set(row, 0, "Recurrence");
16✔
168
      view.set(row, 1, task.get("recur"));
24✔
169
    }
170

171
    // parent
172
    // 2017-01-07: Deprecated in 2.6.0
173
    if (task.has("parent")) {
210✔
174
      row = view.addRow();
3✔
175
      view.set(row, 0, "Parent task");
6✔
176
      view.set(row, 1, task.get("parent"));
9✔
177
    }
178

179
    // mask
180
    // 2017-01-07: Deprecated in 2.6.0
181
    if (task.has("mask")) {
210✔
182
      row = view.addRow();
3✔
183
      view.set(row, 0, "Mask");
6✔
184
      view.set(row, 1, task.get("mask"));
9✔
185
    }
186

187
    // imask
188
    // 2017-01-07: Deprecated in 2.6.0
189
    if (task.has("imask")) {
210✔
190
      row = view.addRow();
3✔
191
      view.set(row, 0, "Mask Index");
6✔
192
      view.set(row, 1, task.get("imask"));
9✔
193
    }
194

195
    // template
196
    if (task.has("template")) {
210✔
197
      row = view.addRow();
×
198
      view.set(row, 0, "Template task");
×
199
      view.set(row, 1, task.get("template"));
×
200
    }
201

202
    // last
203
    if (task.has("last")) {
210✔
204
      row = view.addRow();
×
205
      view.set(row, 0, "Last instance");
×
206
      view.set(row, 1, task.get("last"));
×
207
    }
208

209
    // rtype
210
    if (task.has("rtype")) {
210✔
211
      row = view.addRow();
8✔
212
      view.set(row, 0, "Recurrence type");
16✔
213
      view.set(row, 1, task.get("rtype"));
24✔
214
    }
215

216
    // entry
217
    row = view.addRow();
105✔
218
    view.set(row, 0, "Entered");
315✔
219
    Datetime dt(task.get_date("entry"));
105✔
220
    std::string entry = dt.toString(dateformat);
105✔
221

222
    std::string age;
105✔
223
    auto created = task.get("entry");
105✔
224
    if (created.length()) {
105✔
225
      Datetime dt(strtoll(created.c_str(), nullptr, 10));
105✔
226
      age = Duration(now - dt).formatVague();
105✔
227
    }
228

229
    view.set(row, 1, entry + " (" + age + ')');
105✔
230

231
    // wait
232
    if (task.has("wait")) {
210✔
233
      row = view.addRow();
1✔
234
      view.set(row, 0, "Waiting until");
2✔
235
      view.set(row, 1, Datetime(task.get_date("wait")).toString(dateformat));
3✔
236
    }
237

238
    // scheduled
239
    if (task.has("scheduled")) {
210✔
240
      row = view.addRow();
1✔
241
      view.set(row, 0, "Scheduled");
2✔
242
      view.set(row, 1, Datetime(task.get_date("scheduled")).toString(dateformat));
3✔
243
    }
244

245
    // start
246
    if (task.has("start")) {
210✔
247
      row = view.addRow();
10✔
248
      view.set(row, 0, "Start");
20✔
249
      view.set(row, 1, Datetime(task.get_date("start")).toString(dateformat));
30✔
250
    }
251

252
    // due (colored)
253
    if (task.has("due")) {
210✔
254
      row = view.addRow();
15✔
255
      view.set(row, 0, "Due");
30✔
256
      view.set(row, 1, Datetime(task.get_date("due")).toString(dateformat));
45✔
257
    }
258

259
    // end
260
    if (task.has("end")) {
210✔
261
      row = view.addRow();
15✔
262
      view.set(row, 0, "End");
30✔
263
      view.set(row, 1, Datetime(task.get_date("end")).toString(dateformat));
45✔
264
    }
265

266
    // until
267
    if (task.has("until")) {
210✔
268
      row = view.addRow();
2✔
269
      view.set(row, 0, "Until");
4✔
270
      view.set(row, 1, Datetime(task.get_date("until")).toString(dateformat));
6✔
271
    }
272

273
    // modified
274
    if (task.has("modified")) {
210✔
275
      row = view.addRow();
105✔
276
      view.set(row, 0, "Last modified");
315✔
277

278
      Datetime mod(task.get_date("modified"));
105✔
279
      std::string age = Duration(now - mod).formatVague();
105✔
280
      view.set(row, 1, mod.toString(dateformat) + " (" + age + ')');
105✔
281
    }
105✔
282

283
    // tags ...
284
    auto tags = task.getTags();
105✔
285
    if (tags.size()) {
105✔
286
      auto allTags = join(" ", tags);
17✔
287

288
      row = view.addRow();
17✔
289
      view.set(row, 0, "Tags");
34✔
290
      view.set(row, 1, allTags);
17✔
291
    }
17✔
292

293
    // Virtual tags.
294
    {
295
      // Note: This list must match that in Task::hasTag.
296
      // Note: This list must match that in ::feedback_reserved_tags.
297
      std::string virtualTags = "";
210✔
298
      if (task.hasTag("ACTIVE")) virtualTags += "ACTIVE ";
210✔
299
      if (task.hasTag("ANNOTATED")) virtualTags += "ANNOTATED ";
210✔
300
      if (task.hasTag("BLOCKED")) virtualTags += "BLOCKED ";
210✔
301
      if (task.hasTag("BLOCKING")) virtualTags += "BLOCKING ";
210✔
302
      if (task.hasTag("CHILD")) virtualTags += "CHILD ";  // 2017-01-07: Deprecated in 2.6.0
210✔
303
      if (task.hasTag("COMPLETED")) virtualTags += "COMPLETED ";
210✔
304
      if (task.hasTag("DELETED")) virtualTags += "DELETED ";
210✔
305
      if (task.hasTag("DUE")) virtualTags += "DUE ";
210✔
306
      if (task.hasTag("DUETODAY")) virtualTags += "DUETODAY ";  // 2016-03-29: Deprecated in 2.6.0
210✔
307
      if (task.hasTag("INSTANCE")) virtualTags += "INSTANCE ";
210✔
308
      if (task.hasTag("LATEST")) virtualTags += "LATEST ";
210✔
309
      if (task.hasTag("MONTH")) virtualTags += "MONTH ";
210✔
310
      if (task.hasTag("ORPHAN")) virtualTags += "ORPHAN ";
210✔
311
      if (task.hasTag("OVERDUE")) virtualTags += "OVERDUE ";
210✔
312
      if (task.hasTag("PARENT")) virtualTags += "PARENT ";  // 2017-01-07: Deprecated in 2.6.0
210✔
313
      if (task.hasTag("PENDING")) virtualTags += "PENDING ";
210✔
314
      if (task.hasTag("PRIORITY")) virtualTags += "PRIORITY ";
210✔
315
      if (task.hasTag("PROJECT")) virtualTags += "PROJECT ";
210✔
316
      if (task.hasTag("QUARTER")) virtualTags += "QUARTER ";
210✔
317
      if (task.hasTag("READY")) virtualTags += "READY ";
210✔
318
      if (task.hasTag("SCHEDULED")) virtualTags += "SCHEDULED ";
210✔
319
      if (task.hasTag("TAGGED")) virtualTags += "TAGGED ";
210✔
320
      if (task.hasTag("TEMPLATE")) virtualTags += "TEMPLATE ";
210✔
321
      if (task.hasTag("TODAY")) virtualTags += "TODAY ";
210✔
322
      if (task.hasTag("TOMORROW")) virtualTags += "TOMORROW ";
210✔
323
      if (task.hasTag("UDA")) virtualTags += "UDA ";
210✔
324
      if (task.hasTag("UNBLOCKED")) virtualTags += "UNBLOCKED ";
210✔
325
      if (task.hasTag("UNTIL")) virtualTags += "UNTIL ";
210✔
326
      if (task.hasTag("WAITING")) virtualTags += "WAITING ";
210✔
327
      if (task.hasTag("WEEK")) virtualTags += "WEEK ";
210✔
328
      if (task.hasTag("YEAR")) virtualTags += "YEAR ";
210✔
329
      if (task.hasTag("YESTERDAY")) virtualTags += "YESTERDAY ";
210✔
330
      // If you update the above list, update src/Task.cpp and src/commands/CmdTags.cpp as well.
331

332
      row = view.addRow();
105✔
333
      view.set(row, 0, "Virtual tags");
210✔
334
      view.set(row, 1, virtualTags);
105✔
335
    }
105✔
336

337
    // uuid
338
    row = view.addRow();
105✔
339
    view.set(row, 0, "UUID");
315✔
340
    auto uuid = task.get("uuid");
105✔
341
    view.set(row, 1, uuid);
105✔
342

343
    // Task::urgency
344
    row = view.addRow();
105✔
345
    view.set(row, 0, "Urgency");
210✔
346
    view.set(row, 1, Lexer::trimLeft(format(task.urgency(), 4, 4)));
210✔
347

348
    // Show any UDAs
349
    auto all = task.all();
105✔
350
    for (auto& att : all) {
782✔
351
      if (Context::getContext().columns.find(att) != Context::getContext().columns.end()) {
677✔
352
        Column* col = Context::getContext().columns[att];
641✔
353
        if (col->is_uda()) {
641✔
354
          auto value = task.get(att);
15✔
355
          if (value != "") {
15✔
356
            row = view.addRow();
15✔
357
            view.set(row, 0, col->label());
15✔
358

359
            if (col->type() == "date")
15✔
360
              value = Datetime(value).toString(dateformat);
2✔
361
            else if (col->type() == "duration") {
14✔
362
              Duration iso;
1✔
363
              std::string::size_type cursor = 0;
1✔
364
              if (iso.parse(value, cursor))
1✔
365
                value = (std::string)Variant(iso.toTime_t(), Variant::type_duration);
1✔
366
              else
367
                value = "PT0S";
×
368
            }
369

370
            view.set(row, 1, value);
15✔
371
          }
372
        }
15✔
373
      }
374
    }
375

376
    // Show any orphaned UDAs, which are identified by not being represented in
377
    // the context.columns map.
378
    for (auto& att : all) {
782✔
379
      if (att.substr(0, 11) != "annotation_" && att.substr(0, 4) != "tag_" &&
1,338✔
380
          att.substr(0, 4) != "dep_" &&
2,658✔
381
          Context::getContext().columns.find(att) == Context::getContext().columns.end()) {
1,320✔
382
        row = view.addRow();
2✔
383
        view.set(row, 0, '[' + att);
2✔
384
        view.set(row, 1, task.get(att) + ']');
2✔
385
      }
386
    }
387

388
    // Create a second table, containing urgency details, if necessary.
389
    Table urgencyDetails;
105✔
390
    if (task.urgency() != 0.0) {
105✔
391
      setHeaderUnderline(urgencyDetails);
60✔
392
      if (Context::getContext().color()) {
60✔
393
        Color alternate(Context::getContext().config.get("color.alternate"));
36✔
394
        urgencyDetails.colorOdd(alternate);
18✔
395
        urgencyDetails.intraColorOdd(alternate);
18✔
396
      }
397

398
      if (Context::getContext().config.getBoolean("obfuscate")) urgencyDetails.obfuscate();
180✔
399
      if (Context::getContext().config.getBoolean("color")) view.forceColor();
180✔
400

401
      urgencyDetails.width(Context::getContext().getWidth());
60✔
402
      urgencyDetails.add("");  // Attribute
120✔
403
      urgencyDetails.add("");  // Value
120✔
404
      urgencyDetails.add("");  // *
120✔
405
      urgencyDetails.add("");  // Coefficient
120✔
406
      urgencyDetails.add("");  // =
120✔
407
      urgencyDetails.add("");  // Result
60✔
408

409
      urgencyTerm(urgencyDetails, "project", task.urgency_project(),
120✔
410
                  Task::urgencyProjectCoefficient);
411
      urgencyTerm(urgencyDetails, "active", task.urgency_active(), Task::urgencyActiveCoefficient);
120✔
412
      urgencyTerm(urgencyDetails, "scheduled", task.urgency_scheduled(),
120✔
413
                  Task::urgencyScheduledCoefficient);
414
      urgencyTerm(urgencyDetails, "waiting", task.urgency_waiting(),
120✔
415
                  Task::urgencyWaitingCoefficient);
416
      urgencyTerm(urgencyDetails, "blocked", task.urgency_blocked(),
120✔
417
                  Task::urgencyBlockedCoefficient);
418
      urgencyTerm(urgencyDetails, "blocking", task.urgency_blocking(),
120✔
419
                  Task::urgencyBlockingCoefficient);
420
      urgencyTerm(urgencyDetails, "annotations", task.urgency_annotations(),
120✔
421
                  Task::urgencyAnnotationsCoefficient);
422
      urgencyTerm(urgencyDetails, "tags", task.urgency_tags(), Task::urgencyTagsCoefficient);
120✔
423
      urgencyTerm(urgencyDetails, "due", task.urgency_due(), Task::urgencyDueCoefficient);
120✔
424
      urgencyTerm(urgencyDetails, "age", task.urgency_age(), Task::urgencyAgeCoefficient);
120✔
425

426
      // Tag, Project- and UDA-specific coefficients.
427
      for (auto& var : Task::coefficients) {
303✔
428
        if (var.first.substr(0, 13) == "urgency.user.") {
243✔
429
          // urgency.user.project.<project>.coefficient
430
          auto end = std::string::npos;
62✔
431
          if (var.first.substr(13, 8) == "project." &&
63✔
432
              (end = var.first.find(".coefficient")) != std::string::npos) {
1✔
433
            auto project = var.first.substr(21, end - 21);
1✔
434
            const std::string taskProjectName = task.get("project");
1✔
435
            if (taskProjectName == project || taskProjectName.find(project + '.') == 0) {
1✔
436
              urgencyTerm(urgencyDetails, "PROJECT " + project, 1.0, var.second);
1✔
437
            }
438
          }
1✔
439

440
          // urgency.user.tag.<tag>.coefficient
441
          if (var.first.substr(13, 4) == "tag." &&
122✔
442
              (end = var.first.find(".coefficient")) != std::string::npos) {
60✔
443
            auto name = var.first.substr(17, end - 17);
60✔
444
            if (task.hasTag(name)) urgencyTerm(urgencyDetails, "TAG " + name, 1.0, var.second);
60✔
445
          }
60✔
446

447
          // urgency.user.keyword.<keyword>.coefficient
448
          if (var.first.substr(13, 8) == "keyword." &&
63✔
449
              (end = var.first.find(".coefficient")) != std::string::npos) {
1✔
450
            auto keyword = var.first.substr(21, end - 21);
1✔
451
            if (task.get("description").find(keyword) != std::string::npos)
2✔
452
              urgencyTerm(urgencyDetails, "KEYWORD " + keyword, 1.0, var.second);
1✔
453
          }
1✔
454
        }
455

456
        // urgency.uda.<name>.coefficient
457
        else if (var.first.substr(0, 12) == "urgency.uda.") {
181✔
458
          // urgency.uda.<name>.coefficient
459
          // urgency.uda.<name>.<value>.coefficient
460
          auto end = var.first.find(".coefficient");
181✔
461
          if (end != std::string::npos) {
181✔
462
            auto uda = var.first.substr(12, end - 12);
181✔
463
            auto dot = uda.find('.');
181✔
464
            if (dot == std::string::npos) {
181✔
465
              // urgency.uda.<name>.coefficient
466
              if (task.has(uda))
1✔
467
                urgencyTerm(urgencyDetails, std::string("UDA ") + uda, 1.0, var.second);
3✔
468
            } else {
469
              // urgency.uda.<name>.<value>.coefficient
470
              if (task.get(uda.substr(0, dot)) == uda.substr(dot + 1))
180✔
471
                urgencyTerm(urgencyDetails, std::string("UDA ") + uda, 1.0, var.second);
33✔
472
            }
473
          }
181✔
474
        }
475
      }
476

477
      row = urgencyDetails.addRow();
60✔
478
      urgencyDetails.set(row, 5, rightJustify("------", 6));
120✔
479
      row = urgencyDetails.addRow();
60✔
480
      urgencyDetails.set(row, 5, rightJustify(format(task.urgency(), 4, 4), 6));
60✔
481
    }
482

483
    // Create a third table, containing undo log change details.
484
    Table journal;
105✔
485
    setHeaderUnderline(journal);
105✔
486

487
    if (Context::getContext().config.getBoolean("obfuscate")) journal.obfuscate();
315✔
488
    if (Context::getContext().config.getBoolean("color")) journal.forceColor();
315✔
489

490
    journal.width(Context::getContext().getWidth());
105✔
491
    journal.add("Date");
210✔
492
    journal.add("Modification");
105✔
493

494
    if (Context::getContext().config.getBoolean("journal.info")) {
315✔
495
      auto& replica = Context::getContext().tdb2.replica();
81✔
496
      tc::Uuid tcuuid = tc::uuid_from_string(uuid);
81✔
497
      auto tcoperations = replica->get_task_operations(tcuuid);
81✔
498
      auto operations = Operation::operations(tcoperations);
81✔
499

500
      // Sort by type (Create < Update < Delete < UndoPoint) and then by timestamp.
501
      std::sort(operations.begin(), operations.end());
81✔
502

503
      long last_timestamp = 0;
81✔
504
      for (size_t i = 0; i < operations.size(); i++) {
244✔
505
        auto& op = operations[i];
163✔
506

507
        // Only display updates -- creation and deletion aren't interesting.
508
        if (!op.is_update()) {
163✔
509
          continue;
81✔
510
        }
511

512
        // Group operations that occur within 1s of this one. This is a heuristic
513
        // for operations performed in the same `task` invocation, and allows e.g.,
514
        // `task done end:-2h` to take the updated `end` value into account. It also
515
        // groups these events into a single "row" of the table for better layout.
516
        size_t group_start = i;
82✔
517
        for (i++; i < operations.size(); i++) {
451✔
518
          auto& op2 = operations[i];
370✔
519
          if (!op2.is_update() || op2.get_timestamp() - op.get_timestamp() > 1) {
370✔
520
            break;
1✔
521
          }
522
        }
523
        size_t group_end = i;
82✔
524
        i--;
82✔
525

526
        std::optional<std::string> msg =
527
            formatForInfo(operations, group_start, group_end, dateformat, last_timestamp);
82✔
528

529
        if (!msg) {
82✔
530
          continue;
×
531
        }
532

533
        int row = journal.addRow();
82✔
534
        Datetime timestamp(op.get_timestamp());
82✔
535
        journal.set(row, 0, timestamp.toString(dateformat));
82✔
536
        journal.set(row, 1, *msg);
82✔
537
      }
82✔
538
    }
81✔
539

540
    out << optionalBlankLine() << view.render() << '\n';
105✔
541

542
    if (urgencyDetails.rows() > 0) out << urgencyDetails.render() << '\n';
105✔
543

544
    if (journal.rows() > 0) out << journal.render() << '\n';
105✔
545
  }
105✔
546

547
  output = out.str();
105✔
548
  return rc;
105✔
549
}
105✔
550

551
////////////////////////////////////////////////////////////////////////////////
552
void CmdInfo::urgencyTerm(Table& view, const std::string& label, float measure,
614✔
553
                          float coefficient) const {
554
  auto value = measure * coefficient;
614✔
555
  if (value != 0.0) {
614✔
556
    auto row = view.addRow();
86✔
557
    view.set(row, 0, "    " + label);
86✔
558
    view.set(row, 1, rightJustify(format(measure, 5, 3), 6));
86✔
559
    view.set(row, 2, "*");
172✔
560
    view.set(row, 3, rightJustify(format(coefficient, 4, 2), 4));
86✔
561
    view.set(row, 4, "=");
172✔
562
    view.set(row, 5, rightJustify(format(value, 5, 3), 6));
86✔
563
  }
564
}
614✔
565

566
////////////////////////////////////////////////////////////////////////////////
567
std::optional<std::string> CmdInfo::formatForInfo(const std::vector<Operation>& operations,
82✔
568
                                                  size_t group_start, size_t group_end,
569
                                                  const std::string& dateformat, long& last_start) {
570
  std::stringstream out;
82✔
571
  for (auto i = group_start; i < group_end; i++) {
533✔
572
    auto& operation = operations[i];
451✔
573
    assert(operation.is_update());
451✔
574

575
    // Extract the parts of the Update operation.
576
    std::string prop = operation.get_property();
451✔
577
    std::optional<std::string> value = operation.get_value();
451✔
578
    std::optional<std::string> old_value = operation.get_old_value();
451✔
579
    Datetime timestamp(operation.get_timestamp());
451✔
580

581
    // Never care about modifying the modification time, or the legacy properties `depends` and
582
    // `tags`.
583
    if (prop == "modified" || prop == "depends" || prop == "tags") {
451✔
584
      continue;
96✔
585
    }
586

587
    // Handle property deletions
588
    if (!value && old_value) {
355✔
589
      if (Task::isAnnotationAttr(prop)) {
6✔
590
        out << format("Annotation '{1}' deleted.\n", *old_value);
×
591
      } else if (Task::isTagAttr(prop)) {
6✔
592
        out << format("Tag '{1}' deleted.\n", Task::attr2Tag(prop));
×
593
      } else if (Task::isDepAttr(prop)) {
6✔
594
        out << format("Dependency on '{1}' deleted.\n", Task::attr2Dep(prop));
×
595
      } else if (prop == "start") {
6✔
596
        Datetime started(last_start);
4✔
597
        Datetime stopped = timestamp;
4✔
598

599
        // If any update in this group sets the `end` property, use that instead of the
600
        // timestamp deleting the `start` property as the stop time.
601
        // See https://github.com/GothenburgBitFactory/taskwarrior/issues/2514
602
        for (auto i = group_start; i < group_end; i++) {
33✔
603
          auto& op = operations[i];
29✔
604
          assert(op.is_update());
29✔
605
          if (op.get_property() == "end") {
29✔
606
            try {
607
              stopped = op.get_value().value();
4✔
608
            } catch (std::string) {
×
609
              // Fall back to the 'start' timestamp if its value is un-parseable.
610
              stopped = op.get_timestamp();
×
611
            }
×
612
          }
613
        }
614

615
        out << format("{1} deleted (duration: {2}).", Lexer::ucFirst(prop),
16✔
616
                      Duration(stopped - started).format())
8✔
617
            << "\n";
4✔
618
      } else {
619
        out << format("{1} deleted.\n", Lexer::ucFirst(prop));
6✔
620
      }
621
    }
622

623
    // Handle property additions.
624
    if (value && !old_value) {
355✔
625
      if (Task::isAnnotationAttr(prop)) {
341✔
626
        out << format("Annotation of '{1}' added.\n", *value);
33✔
627
      } else if (Task::isTagAttr(prop)) {
330✔
628
        out << format("Tag '{1}' added.\n", Task::attr2Tag(prop));
30✔
629
      } else if (Task::isDepAttr(prop)) {
320✔
630
        out << format("Dependency on '{1}' added.\n", Task::attr2Dep(prop));
3✔
631
      } else {
632
        // Record the last start time for later duration calculation.
633
        if (prop == "start") {
319✔
634
          last_start = Datetime(value.value()).toEpoch();
18✔
635
        }
636

637
        out << format("{1} set to '{2}'.", Lexer::ucFirst(prop),
1,276✔
638
                      renderAttribute(prop, *value, dateformat))
638✔
639
            << "\n";
319✔
640
      }
641
    }
642

643
    // Handle property changes.
644
    if (value && old_value) {
355✔
645
      if (Task::isTagAttr(prop) || Task::isDepAttr(prop)) {
8✔
646
        // Dependencies and tags do not have meaningful values.
647
      } else if (Task::isAnnotationAttr(prop)) {
8✔
648
        out << format("Annotation changed to '{1}'.\n", *value);
×
649
      } else {
650
        // Record the last start time for later duration calculation.
651
        if (prop == "start") {
8✔
652
          last_start = Datetime(value.value()).toEpoch();
×
653
        }
654

655
        out << format("{1} changed from '{2}' to '{3}'.", Lexer::ucFirst(prop),
32✔
656
                      renderAttribute(prop, *old_value, dateformat),
16✔
657
                      renderAttribute(prop, *value, dateformat))
16✔
658
            << "\n";
8✔
659
      }
660
    }
661
  }
643✔
662

663
  if (out.str().length() == 0) return std::nullopt;
82✔
664

665
  return out.str();
82✔
666
}
82✔
667

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

© 2026 Coveralls, Inc