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

GothenburgBitFactory / taskwarrior / 18185059396

02 Oct 2025 06:17AM UTC coverage: 85.235% (+0.04%) from 85.194%
18185059396

Pull #3963

github

web-flow
Merge 1791af106 into e6b9c71cb
Pull Request #3963: info: indent wrapped annotation lines (fixes #3914)

36 of 39 new or added lines in 1 file covered. (92.31%)

10 existing lines in 1 file now uncovered.

19633 of 23034 relevant lines covered (85.23%)

23461.25 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 <feedback.h>
38
#include <format.h>
39
#include <rules.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
#include <string>
48

49
////////////////////////////////////////////////////////////////////////////////
50

51
static std::string wrapWithPrefixes(const std::string& text,
17✔
52
                                    size_t width,
53
                                    const std::string& prefixFirst,
54
                                    const std::string& prefixCont) {
55
  if (width == 0) return prefixFirst + text;
17✔
56

57
  std::ostringstream out;
17✔
58
  std::istringstream is(text);
17✔
59
  std::string word, line;
17✔
60
  bool firstLine = true;
17✔
61

62
  auto safeSub = [](size_t a, size_t b){ return (a > b) ? (a - b) : 0u; };
34✔
63
  const size_t effFirst = safeSub(width, prefixFirst.size());
17✔
64
  const size_t effCont  = safeSub(width, prefixCont.size());
17✔
65

66
  auto flush = [&](bool isFirst){
17✔
67
    if (!line.empty()) {
17✔
68
      out << (isFirst ? prefixFirst : prefixCont) << line << '\n';
17✔
69
      line.clear();
17✔
70
    }
71
  };
34✔
72

73
  while (is >> word) {
38✔
74
    size_t eff = firstLine ? effFirst : effCont;
21✔
75
    if (line.empty()) {
21✔
76
      line = word;
17✔
77
    } else if (line.size() + 1 + word.size() <= eff) {
4✔
78
      line += ' ';
4✔
79
      line += word;
4✔
80
    } else {
NEW
81
      flush(firstLine);
×
NEW
82
      firstLine = false;
×
NEW
83
      line = word;
×
84
    }
85
  }
86
  flush(firstLine);
17✔
87

88
  std::string s = out.str();
17✔
89
  if (!s.empty() && s.back() == '\n') s.pop_back();
17✔
90
  return s;
17✔
91
}
17✔
92

93
CmdInfo::CmdInfo() {
4,600✔
94
  _keyword = "information";
4,600✔
95
  _usage = "task <filter> information";
4,600✔
96
  _description = "Shows all data and metadata";
4,600✔
97
  _read_only = true;
4,600✔
98

99
  // This is inaccurate, but it does prevent a GC.  While this doesn't make a
100
  // lot of sense, given that the info command shows the ID, it does mimic the
101
  // behavior of versions prior to 2.0, which the test suite relies upon.
102
  //
103
  // Once the test suite is completely modified, this can be corrected.
104
  _displays_id = false;
4,600✔
105
  _needs_gc = false;
4,600✔
106
  _needs_recur_update = false;
4,600✔
107
  _uses_context = false;
4,600✔
108
  _accepts_filter = true;
4,600✔
109
  _accepts_modifications = false;
4,600✔
110
  _accepts_miscellaneous = false;
4,600✔
111
  _category = Command::Category::metadata;
4,600✔
112
}
4,600✔
113

114
////////////////////////////////////////////////////////////////////////////////
115
int CmdInfo::execute(std::string& output) {
115✔
116
  auto rc = 0;
115✔
117

118
  // Apply filter.
119
  Filter filter;
115✔
120
  std::vector<Task> filtered;
115✔
121
  filter.subset(filtered);
115✔
122

123
  if (!filtered.size()) {
115✔
124
    Context::getContext().footnote("No matches.");
4✔
125
    rc = 1;
2✔
126
  }
127

128
  // Determine the output date format, which uses a hierarchy of definitions.
129
  //   rc.dateformat.info
130
  //   rc.dateformat
131
  auto dateformat = Context::getContext().config.get("dateformat.info");
230✔
132
  if (dateformat == "") dateformat = Context::getContext().config.get("dateformat");
117✔
133

134
  auto dateformatanno = Context::getContext().config.get("dateformat.annotation");
230✔
135
  if (dateformatanno == "") dateformatanno = dateformat;
115✔
136

137
  // Render each task.
138
  std::stringstream out;
115✔
139
  for (auto& task : filtered) {
230✔
140
    Table view;
115✔
141
    view.width(Context::getContext().getWidth());
115✔
142
    if (Context::getContext().config.getBoolean("obfuscate")) view.obfuscate();
345✔
143
    if (Context::getContext().color()) view.forceColor();
115✔
144
    view.add("Name");
230✔
145
    view.add("Value");
115✔
146
    setHeaderUnderline(view);
115✔
147

148
    Datetime now;
115✔
149

150
    // id
151
    auto row = view.addRow();
115✔
152
    view.set(row, 0, "ID");
230✔
153
    view.set(row, 1, (task.id ? format(task.id) : "-"));
120✔
154

155
    std::string status = Lexer::ucFirst(Task::statusToText(task.getStatus()));
115✔
156

157
    // description
158
    Color c;
115✔
159
    autoColorize(task, c);
115✔
160
    auto description = task.get("description");
115✔
161
    auto indent = Context::getContext().config.getInteger("indent.annotation");
230✔
162

163
    {
164
  const size_t termWidth = Context::getContext().getWidth();
115✔
165
  const size_t approxNameCol = 20; // conservative; covers labels like "Last modified"
115✔
166
  const size_t valueWidth = termWidth > approxNameCol ? termWidth - approxNameCol : termWidth;
115✔
167

168
  for (const auto& anno : task.getAnnotations()) {
132✔
169
    // first line looks exactly like before: <indent><timestamp><space>
170
    const std::string firstPrefix =
171
      std::string(indent, ' ') +
34✔
172
      Datetime(anno.first.substr(11)).toString(dateformatanno) + ' ';
68✔
173

174
    // continuation lines: same width, all spaces
175
    const std::string contPrefix(firstPrefix.size(), ' ');
17✔
176

177
    // wrap just the annotation body with our prefixes
178
    const std::string wrapped = wrapWithPrefixes(anno.second, valueWidth, firstPrefix, contPrefix);
17✔
179

180
    description += '\n' + wrapped;
17✔
181
  }
132✔
182
}
183

184
    if (task.has("description")) {
230✔
185
      row = view.addRow();
107✔
186
      view.set(row, 0, "Description");
214✔
187
      view.set(row, 1, description, c);
107✔
188
    }
189

190
    // status
191
    row = view.addRow();
115✔
192
    view.set(row, 0, "Status");
230✔
193
    view.set(row, 1, status);
115✔
194

195
    // project
196
    if (task.has("project")) {
230✔
197
      row = view.addRow();
14✔
198
      view.set(row, 0, "Project");
28✔
199
      view.set(row, 1, task.get("project"));
42✔
200
    }
201

202
    // dependencies: blocked
203
    {
204
      auto blocked = task.getDependencyTasks();
115✔
205
      if (blocked.size()) {
115✔
206
        std::stringstream message;
1✔
207
        for (auto& block : blocked) message << block.id << ' ' << block.get("description") << '\n';
3✔
208

209
        row = view.addRow();
1✔
210
        view.set(row, 0, "This task blocked by");
2✔
211
        view.set(row, 1, message.str());
1✔
212
      }
1✔
213
    }
115✔
214

215
    // dependencies: blocking
216
    {
217
      auto blocking = task.getBlockedTasks();
115✔
218
      if (blocking.size()) {
115✔
219
        std::stringstream message;
1✔
220
        for (auto& block : blocking) message << block.id << ' ' << block.get("description") << '\n';
3✔
221

222
        row = view.addRow();
1✔
223
        view.set(row, 0, "This task is blocking");
2✔
224
        view.set(row, 1, message.str());
1✔
225
      }
1✔
226
    }
115✔
227

228
    // recur
229
    if (task.has("recur")) {
230✔
230
      row = view.addRow();
9✔
231
      view.set(row, 0, "Recurrence");
18✔
232
      view.set(row, 1, task.get("recur"));
27✔
233
    }
234

235
    // parent
236
    // 2017-01-07: Deprecated in 2.6.0
237
    if (task.has("parent")) {
230✔
238
      row = view.addRow();
3✔
239
      view.set(row, 0, "Parent task");
6✔
240
      view.set(row, 1, task.get("parent"));
9✔
241
    }
242

243
    // mask
244
    // 2017-01-07: Deprecated in 2.6.0
245
    if (task.has("mask")) {
230✔
246
      row = view.addRow();
3✔
247
      view.set(row, 0, "Mask");
6✔
248
      view.set(row, 1, task.get("mask"));
9✔
249
    }
250

251
    // imask
252
    // 2017-01-07: Deprecated in 2.6.0
253
    if (task.has("imask")) {
230✔
254
      row = view.addRow();
3✔
255
      view.set(row, 0, "Mask Index");
6✔
256
      view.set(row, 1, task.get("imask"));
9✔
257
    }
258

259
    // template
260
    if (task.has("template")) {
230✔
261
      row = view.addRow();
×
262
      view.set(row, 0, "Template task");
×
UNCOV
263
      view.set(row, 1, task.get("template"));
×
264
    }
265

266
    // last
267
    if (task.has("last")) {
230✔
268
      row = view.addRow();
×
269
      view.set(row, 0, "Last instance");
×
UNCOV
270
      view.set(row, 1, task.get("last"));
×
271
    }
272

273
    // rtype
274
    if (task.has("rtype")) {
230✔
275
      row = view.addRow();
10✔
276
      view.set(row, 0, "Recurrence type");
20✔
277
      view.set(row, 1, task.get("rtype"));
30✔
278
    }
279

280
    // entry
281
    if (task.has("entry") && task.get_date("entry")) {
559✔
282
      row = view.addRow();
106✔
283
      view.set(row, 0, "Entered");
318✔
284
      Datetime dt(task.get_date("entry"));
106✔
285
      std::string entry = dt.toString(dateformat);
106✔
286

287
      std::string age;
106✔
288
      auto created = task.get("entry");
106✔
289
      if (created.length()) {
106✔
290
        Datetime dt(strtoll(created.c_str(), nullptr, 10));
106✔
291
        age = Duration(now - dt).formatVague();
106✔
292
      }
293

294
      view.set(row, 1, entry + " (" + age + ')');
106✔
295
    }
106✔
296

297
    auto validDate = [&](const char* prop) {
805✔
298
      if (!task.has(prop)) {
2,415✔
299
        return false;
642✔
300
      }
301
      if (task.get_date(prop) == 0) {
489✔
302
        return false;
9✔
303
      }
304
      return true;
154✔
305
    };
115✔
306

307
    // wait
308
    if (validDate("wait")) {
115✔
309
      row = view.addRow();
1✔
310
      view.set(row, 0, "Waiting until");
2✔
311
      view.set(row, 1, Datetime(task.get_date("wait")).toString(dateformat));
3✔
312
    }
313

314
    // scheduled
315
    if (validDate("scheduled")) {
115✔
316
      row = view.addRow();
1✔
317
      view.set(row, 0, "Scheduled");
2✔
318
      view.set(row, 1, Datetime(task.get_date("scheduled")).toString(dateformat));
3✔
319
    }
320

321
    // start
322
    if (validDate("start")) {
115✔
323
      row = view.addRow();
10✔
324
      view.set(row, 0, "Start");
20✔
325
      view.set(row, 1, Datetime(task.get_date("start")).toString(dateformat));
30✔
326
    }
327

328
    // due (colored)
329
    if (validDate("due")) {
115✔
330
      row = view.addRow();
19✔
331
      view.set(row, 0, "Due");
38✔
332
      view.set(row, 1, Datetime(task.get_date("due")).toString(dateformat));
57✔
333
    }
334

335
    // end
336
    if (validDate("end")) {
115✔
337
      row = view.addRow();
15✔
338
      view.set(row, 0, "End");
30✔
339
      view.set(row, 1, Datetime(task.get_date("end")).toString(dateformat));
45✔
340
    }
341

342
    // until
343
    if (validDate("until")) {
115✔
344
      row = view.addRow();
2✔
345
      view.set(row, 0, "Until");
4✔
346
      view.set(row, 1, Datetime(task.get_date("until")).toString(dateformat));
6✔
347
    }
348

349
    // modified
350
    if (validDate("modified")) {
115✔
351
      row = view.addRow();
106✔
352
      view.set(row, 0, "Last modified");
318✔
353

354
      Datetime mod(task.get_date("modified"));
106✔
355
      std::string age = Duration(now - mod).formatVague();
106✔
356
      view.set(row, 1, mod.toString(dateformat) + " (" + age + ')');
106✔
357
    }
106✔
358

359
    // tags ...
360
    auto tags = task.getTags();
115✔
361
    if (tags.size()) {
115✔
362
      auto allTags = join(" ", tags);
19✔
363

364
      row = view.addRow();
19✔
365
      view.set(row, 0, "Tags");
38✔
366
      view.set(row, 1, allTags);
19✔
367
    }
19✔
368

369
    // Virtual tags.
370
    {
371
      // Note: This list must match that in Task::hasTag.
372
      // Note: This list must match that in ::feedback_reserved_tags.
373
      std::string virtualTags = "";
230✔
374
      if (task.hasTag("ACTIVE")) virtualTags += "ACTIVE ";
230✔
375
      if (task.hasTag("ANNOTATED")) virtualTags += "ANNOTATED ";
230✔
376
      if (task.hasTag("BLOCKED")) virtualTags += "BLOCKED ";
230✔
377
      if (task.hasTag("BLOCKING")) virtualTags += "BLOCKING ";
230✔
378
      if (task.hasTag("CHILD")) virtualTags += "CHILD ";  // 2017-01-07: Deprecated in 2.6.0
230✔
379
      if (task.hasTag("COMPLETED")) virtualTags += "COMPLETED ";
230✔
380
      if (task.hasTag("DELETED")) virtualTags += "DELETED ";
230✔
381
      if (task.hasTag("DUE")) virtualTags += "DUE ";
230✔
382
      if (task.hasTag("DUETODAY")) virtualTags += "DUETODAY ";  // 2016-03-29: Deprecated in 2.6.0
230✔
383
      if (task.hasTag("INSTANCE")) virtualTags += "INSTANCE ";
230✔
384
      if (task.hasTag("LATEST")) virtualTags += "LATEST ";
230✔
385
      if (task.hasTag("MONTH")) virtualTags += "MONTH ";
230✔
386
      if (task.hasTag("ORPHAN")) virtualTags += "ORPHAN ";
230✔
387
      if (task.hasTag("OVERDUE")) virtualTags += "OVERDUE ";
230✔
388
      if (task.hasTag("PARENT")) virtualTags += "PARENT ";  // 2017-01-07: Deprecated in 2.6.0
230✔
389
      if (task.hasTag("PENDING")) virtualTags += "PENDING ";
230✔
390
      if (task.hasTag("PRIORITY")) virtualTags += "PRIORITY ";
230✔
391
      if (task.hasTag("PROJECT")) virtualTags += "PROJECT ";
230✔
392
      if (task.hasTag("QUARTER")) virtualTags += "QUARTER ";
230✔
393
      if (task.hasTag("READY")) virtualTags += "READY ";
230✔
394
      if (task.hasTag("SCHEDULED")) virtualTags += "SCHEDULED ";
230✔
395
      if (task.hasTag("TAGGED")) virtualTags += "TAGGED ";
230✔
396
      if (task.hasTag("TEMPLATE")) virtualTags += "TEMPLATE ";
230✔
397
      if (task.hasTag("TODAY")) virtualTags += "TODAY ";
230✔
398
      if (task.hasTag("TOMORROW")) virtualTags += "TOMORROW ";
230✔
399
      if (task.hasTag("UDA")) virtualTags += "UDA ";
230✔
400
      if (task.hasTag("UNBLOCKED")) virtualTags += "UNBLOCKED ";
230✔
401
      if (task.hasTag("UNTIL")) virtualTags += "UNTIL ";
230✔
402
      if (task.hasTag("WAITING")) virtualTags += "WAITING ";
230✔
403
      if (task.hasTag("WEEK")) virtualTags += "WEEK ";
230✔
404
      if (task.hasTag("YEAR")) virtualTags += "YEAR ";
230✔
405
      if (task.hasTag("YESTERDAY")) virtualTags += "YESTERDAY ";
230✔
406
      // If you update the above list, update src/Task.cpp and src/commands/CmdTags.cpp as well.
407

408
      row = view.addRow();
115✔
409
      view.set(row, 0, "Virtual tags");
230✔
410
      view.set(row, 1, virtualTags);
115✔
411
    }
115✔
412

413
    // uuid
414
    row = view.addRow();
115✔
415
    view.set(row, 0, "UUID");
345✔
416
    auto uuid = task.get("uuid");
115✔
417
    view.set(row, 1, uuid);
115✔
418

419
    // Task::urgency
420
    row = view.addRow();
115✔
421
    view.set(row, 0, "Urgency");
230✔
422
    view.set(row, 1, Lexer::trimLeft(format(task.urgency(), 4, 4)));
230✔
423

424
    // Show any UDAs
425
    auto all = task.all();
115✔
426
    for (auto& att : all) {
833✔
427
      if (Context::getContext().columns.find(att) != Context::getContext().columns.end()) {
718✔
428
        Column* col = Context::getContext().columns[att];
678✔
429
        if (col->is_uda()) {
678✔
430
          auto value = task.get(att);
15✔
431
          if (value != "") {
15✔
432
            row = view.addRow();
15✔
433
            view.set(row, 0, col->label());
15✔
434

435
            if (col->type() == "date")
15✔
436
              value = Datetime(value).toString(dateformat);
2✔
437
            else if (col->type() == "duration") {
14✔
438
              Duration iso;
1✔
439
              std::string::size_type cursor = 0;
1✔
440
              if (iso.parse(value, cursor))
1✔
441
                value = (std::string)Variant(iso.toTime_t(), Variant::type_duration);
1✔
442
              else
UNCOV
443
                value = "PT0S";
×
444
            }
445

446
            view.set(row, 1, value);
15✔
447
          }
448
        }
15✔
449
      }
450
    }
451

452
    // Show any orphaned UDAs, which are identified by not being represented in
453
    // the context.columns map.
454
    for (auto& att : all) {
833✔
455
      if (att.substr(0, 11) != "annotation_" && att.substr(0, 4) != "tag_" &&
1,419✔
456
          att.substr(0, 4) != "dep_" &&
2,817✔
457
          Context::getContext().columns.find(att) == Context::getContext().columns.end()) {
1,398✔
458
        row = view.addRow();
2✔
459
        view.set(row, 0, '[' + att);
2✔
460
        view.set(row, 1, task.get(att) + ']');
2✔
461
      }
462
    }
463

464
    // Create a second table, containing urgency details, if necessary.
465
    Table urgencyDetails;
115✔
466
    if (task.urgency() != 0.0) {
115✔
467
      setHeaderUnderline(urgencyDetails);
70✔
468
      if (Context::getContext().color()) {
70✔
469
        Color alternate(Context::getContext().config.get("color.alternate"));
36✔
470
        urgencyDetails.colorOdd(alternate);
18✔
471
        urgencyDetails.intraColorOdd(alternate);
18✔
472
      }
473

474
      if (Context::getContext().config.getBoolean("obfuscate")) urgencyDetails.obfuscate();
210✔
475
      if (Context::getContext().config.getBoolean("color")) view.forceColor();
210✔
476

477
      urgencyDetails.width(Context::getContext().getWidth());
70✔
478
      urgencyDetails.add("");  // Attribute
140✔
479
      urgencyDetails.add("");  // Value
140✔
480
      urgencyDetails.add("");  // *
140✔
481
      urgencyDetails.add("");  // Coefficient
140✔
482
      urgencyDetails.add("");  // =
140✔
483
      urgencyDetails.add("");  // Result
70✔
484

485
      urgencyTerm(urgencyDetails, "project", task.urgency_project(),
140✔
486
                  Task::urgencyProjectCoefficient);
487
      urgencyTerm(urgencyDetails, "active", task.urgency_active(), Task::urgencyActiveCoefficient);
140✔
488
      urgencyTerm(urgencyDetails, "scheduled", task.urgency_scheduled(),
140✔
489
                  Task::urgencyScheduledCoefficient);
490
      urgencyTerm(urgencyDetails, "waiting", task.urgency_waiting(),
140✔
491
                  Task::urgencyWaitingCoefficient);
492
      urgencyTerm(urgencyDetails, "blocked", task.urgency_blocked(),
140✔
493
                  Task::urgencyBlockedCoefficient);
494
      urgencyTerm(urgencyDetails, "blocking", task.urgency_blocking(),
140✔
495
                  Task::urgencyBlockingCoefficient);
496
      urgencyTerm(urgencyDetails, "annotations", task.urgency_annotations(),
140✔
497
                  Task::urgencyAnnotationsCoefficient);
498
      urgencyTerm(urgencyDetails, "tags", task.urgency_tags(), Task::urgencyTagsCoefficient);
140✔
499
      urgencyTerm(urgencyDetails, "due", task.urgency_due(), Task::urgencyDueCoefficient);
140✔
500
      urgencyTerm(urgencyDetails, "age", task.urgency_age(), Task::urgencyAgeCoefficient);
140✔
501

502
      // Tag, Project- and UDA-specific coefficients.
503
      for (auto& var : Task::coefficients) {
353✔
504
        if (var.first.substr(0, 13) == "urgency.user.") {
283✔
505
          // urgency.user.project.<project>.coefficient
506
          auto end = std::string::npos;
72✔
507
          if (var.first.substr(13, 8) == "project." &&
73✔
508
              (end = var.first.find(".coefficient")) != std::string::npos) {
1✔
509
            auto project = var.first.substr(21, end - 21);
1✔
510
            const std::string taskProjectName = task.get("project");
1✔
511
            if (taskProjectName == project || taskProjectName.find(project + '.') == 0) {
1✔
512
              urgencyTerm(urgencyDetails, "PROJECT " + project, 1.0, var.second);
1✔
513
            }
514
          }
1✔
515

516
          // urgency.user.tag.<tag>.coefficient
517
          if (var.first.substr(13, 4) == "tag." &&
142✔
518
              (end = var.first.find(".coefficient")) != std::string::npos) {
70✔
519
            auto name = var.first.substr(17, end - 17);
70✔
520
            if (task.hasTag(name)) urgencyTerm(urgencyDetails, "TAG " + name, 1.0, var.second);
70✔
521
          }
70✔
522

523
          // urgency.user.keyword.<keyword>.coefficient
524
          if (var.first.substr(13, 8) == "keyword." &&
73✔
525
              (end = var.first.find(".coefficient")) != std::string::npos) {
1✔
526
            auto keyword = var.first.substr(21, end - 21);
1✔
527
            if (task.get("description").find(keyword) != std::string::npos)
2✔
528
              urgencyTerm(urgencyDetails, "KEYWORD " + keyword, 1.0, var.second);
1✔
529
          }
1✔
530
        }
531

532
        // urgency.uda.<name>.coefficient
533
        else if (var.first.substr(0, 12) == "urgency.uda.") {
211✔
534
          // urgency.uda.<name>.coefficient
535
          // urgency.uda.<name>.<value>.coefficient
536
          auto end = var.first.find(".coefficient");
211✔
537
          if (end != std::string::npos) {
211✔
538
            auto uda = var.first.substr(12, end - 12);
211✔
539
            auto dot = uda.find('.');
211✔
540
            if (dot == std::string::npos) {
211✔
541
              // urgency.uda.<name>.coefficient
542
              if (task.has(uda))
1✔
543
                urgencyTerm(urgencyDetails, std::string("UDA ") + uda, 1.0, var.second);
3✔
544
            } else {
545
              // urgency.uda.<name>.<value>.coefficient
546
              if (task.get(uda.substr(0, dot)) == uda.substr(dot + 1))
210✔
547
                urgencyTerm(urgencyDetails, std::string("UDA ") + uda, 1.0, var.second);
33✔
548
            }
549
          }
211✔
550
        }
551
      }
552

553
      row = urgencyDetails.addRow();
70✔
554
      urgencyDetails.set(row, 5, rightJustify("------", 6));
140✔
555
      row = urgencyDetails.addRow();
70✔
556
      urgencyDetails.set(row, 5, rightJustify(format(task.urgency(), 4, 4), 6));
70✔
557
    }
558

559
    // Create a third table, containing undo log change details.
560
    Table journal;
115✔
561
    setHeaderUnderline(journal);
115✔
562

563
    if (Context::getContext().config.getBoolean("obfuscate")) journal.obfuscate();
345✔
564
    if (Context::getContext().config.getBoolean("color")) journal.forceColor();
345✔
565

566
    journal.width(Context::getContext().getWidth());
115✔
567
    journal.add("Date");
230✔
568
    journal.add("Modification");
115✔
569

570
    if (Context::getContext().config.getBoolean("journal.info")) {
345✔
571
      auto& replica = Context::getContext().tdb2.replica();
91✔
572
      tc::Uuid tcuuid = tc::uuid_from_string(uuid);
91✔
573
      auto tcoperations = replica->get_task_operations(tcuuid);
91✔
574
      auto operations = Operation::operations(tcoperations);
91✔
575

576
      // Sort by type (Create < Update < Delete < UndoPoint) and then by timestamp.
577
      std::sort(operations.begin(), operations.end());
91✔
578

579
      long last_timestamp = 0;
91✔
580
      for (size_t i = 0; i < operations.size(); i++) {
273✔
581
        auto& op = operations[i];
182✔
582

583
        // Only display updates -- creation and deletion aren't interesting.
584
        if (!op.is_update()) {
182✔
585
          continue;
92✔
586
        }
587

588
        // Group operations that occur within 1s of this one. This is a heuristic
589
        // for operations performed in the same `task` invocation, and allows e.g.,
590
        // `task done end:-2h` to take the updated `end` value into account. It also
591
        // groups these events into a single "row" of the table for better layout.
592
        size_t group_start = i;
91✔
593
        for (i++; i < operations.size(); i++) {
480✔
594
          auto& op2 = operations[i];
390✔
595
          if (!op2.is_update() || op2.get_timestamp() - op.get_timestamp() > 1) {
390✔
596
            break;
1✔
597
          }
598
        }
599
        size_t group_end = i;
91✔
600
        i--;
91✔
601

602
        std::optional<std::string> msg =
603
            formatForInfo(operations, group_start, group_end, dateformat, last_timestamp);
91✔
604

605
        if (!msg) {
91✔
606
          continue;
1✔
607
        }
608

609
        int row = journal.addRow();
90✔
610
        Datetime timestamp(op.get_timestamp());
90✔
611
        journal.set(row, 0, timestamp.toString(dateformat));
90✔
612
        journal.set(row, 1, *msg);
90✔
613
      }
91✔
614
    }
91✔
615

616
    out << optionalBlankLine() << view.render() << '\n';
115✔
617

618
    if (urgencyDetails.rows() > 0) out << urgencyDetails.render() << '\n';
115✔
619

620
    if (journal.rows() > 0) out << journal.render() << '\n';
115✔
621
  }
115✔
622

623
  output = out.str();
115✔
624
  return rc;
115✔
625
}
115✔
626

627
////////////////////////////////////////////////////////////////////////////////
628
void CmdInfo::urgencyTerm(Table& view, const std::string& label, float measure,
714✔
629
                          float coefficient) const {
630
  auto value = measure * coefficient;
714✔
631
  if (value != 0.0) {
714✔
632
    auto row = view.addRow();
105✔
633
    view.set(row, 0, "    " + label);
105✔
634
    view.set(row, 1, rightJustify(format(measure, 5, 3), 6));
105✔
635
    view.set(row, 2, "*");
210✔
636
    view.set(row, 3, rightJustify(format(coefficient, 4, 2), 4));
105✔
637
    view.set(row, 4, "=");
210✔
638
    view.set(row, 5, rightJustify(format(value, 5, 3), 6));
105✔
639
  }
640
}
714✔
641

642
////////////////////////////////////////////////////////////////////////////////
643
std::optional<std::string> CmdInfo::formatForInfo(const std::vector<Operation>& operations,
91✔
644
                                                  size_t group_start, size_t group_end,
645
                                                  const std::string& dateformat, long& last_start) {
646
  std::stringstream out;
91✔
647
  for (auto i = group_start; i < group_end; i++) {
571✔
648
    auto& operation = operations[i];
480✔
649
    assert(operation.is_update());
480✔
650

651
    // Extract the parts of the Update operation.
652
    std::string prop = operation.get_property();
480✔
653
    std::optional<std::string> value = operation.get_value();
480✔
654
    std::optional<std::string> old_value = operation.get_old_value();
480✔
655
    Datetime timestamp(operation.get_timestamp());
480✔
656

657
    // Never care about modifying the modification time, or the legacy properties `depends` and
658
    // `tags`.
659
    if (prop == "modified" || prop == "depends" || prop == "tags") {
480✔
660
      continue;
98✔
661
    }
662

663
    // Handle property deletions
664
    if (!value && old_value) {
382✔
665
      if (Task::isAnnotationAttr(prop)) {
6✔
UNCOV
666
        out << format("Annotation '{1}' deleted.\n", *old_value);
×
667
      } else if (Task::isTagAttr(prop)) {
6✔
UNCOV
668
        out << format("Tag '{1}' deleted.\n", Task::attr2Tag(prop));
×
669
      } else if (Task::isDepAttr(prop)) {
6✔
UNCOV
670
        out << format("Dependency on '{1}' deleted.\n", Task::attr2Dep(prop));
×
671
      } else if (prop == "start") {
6✔
672
        Datetime started(last_start);
4✔
673
        Datetime stopped = timestamp;
4✔
674

675
        // If any update in this group sets the `end` property, use that instead of the
676
        // timestamp deleting the `start` property as the stop time.
677
        // See https://github.com/GothenburgBitFactory/taskwarrior/issues/2514
678
        for (auto i = group_start; i < group_end; i++) {
30✔
679
          auto& op = operations[i];
26✔
680
          assert(op.is_update());
26✔
681
          if (op.get_property() == "end") {
26✔
682
            try {
683
              stopped = op.get_value().value();
4✔
UNCOV
684
            } catch (std::string) {
×
685
              // Fall back to the 'start' timestamp if its value is un-parseable.
686
              stopped = op.get_timestamp();
×
UNCOV
687
            }
×
688
          }
689
        }
690

691
        out << format("{1} deleted (duration: {2}).", Lexer::ucFirst(prop),
16✔
692
                      Duration(stopped - started).format())
8✔
693
            << "\n";
4✔
694
      } else {
695
        out << format("{1} deleted.\n", Lexer::ucFirst(prop));
6✔
696
      }
697
    }
698

699
    // Handle property additions.
700
    if (value && !old_value) {
382✔
701
      if (Task::isAnnotationAttr(prop)) {
368✔
702
        out << format("Annotation of '{1}' added.\n", *value);
36✔
703
      } else if (Task::isTagAttr(prop)) {
356✔
704
        out << format("Tag '{1}' added.\n", Task::attr2Tag(prop));
39✔
705
      } else if (Task::isDepAttr(prop)) {
343✔
706
        out << format("Dependency on '{1}' added.\n", Task::attr2Dep(prop));
3✔
707
      } else {
708
        // Record the last start time for later duration calculation.
709
        if (prop == "start") {
342✔
710
          try {
711
            last_start = Datetime(value.value()).toEpoch();
22✔
712
          } catch (std::string) {
2✔
713
            // ignore invalid dates
714
          }
2✔
715
        }
716

717
        out << format("{1} set to '{2}'.", Lexer::ucFirst(prop),
1,368✔
718
                      renderAttribute(prop, *value, dateformat))
684✔
719
            << "\n";
342✔
720
      }
721
    }
722

723
    // Handle property changes.
724
    if (value && old_value) {
382✔
725
      if (Task::isTagAttr(prop) || Task::isDepAttr(prop)) {
8✔
726
        // Dependencies and tags do not have meaningful values.
727
      } else if (Task::isAnnotationAttr(prop)) {
8✔
UNCOV
728
        out << format("Annotation changed to '{1}'.\n", *value);
×
729
      } else {
730
        // Record the last start time for later duration calculation.
731
        if (prop == "start") {
8✔
UNCOV
732
          last_start = Datetime(value.value()).toEpoch();
×
733
        }
734

735
        out << format("{1} changed from '{2}' to '{3}'.", Lexer::ucFirst(prop),
32✔
736
                      renderAttribute(prop, *old_value, dateformat),
16✔
737
                      renderAttribute(prop, *value, dateformat))
16✔
738
            << "\n";
8✔
739
      }
740
    }
741
  }
676✔
742

743
  if (out.str().length() == 0) return std::nullopt;
91✔
744

745
  return out.str();
90✔
746
}
91✔
747

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