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

GothenburgBitFactory / taskwarrior / 26460934399

26 May 2026 04:23PM UTC coverage: 85.168% (+0.001%) from 85.167%
26460934399

Pull #4113

github

web-flow
Merge 9a7062cd5 into 8902b1e7a
Pull Request #4113: Add iterative task handling to TaskWarrior

64 of 65 new or added lines in 8 files covered. (98.46%)

14 existing lines in 4 files now uncovered.

19667 of 23092 relevant lines covered (85.17%)

23613.32 hits per line

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

97.62
/src/TDB2.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 <Color.h>
31
#include <Context.h>
32
#include <Datetime.h>
33
#include <TDB2.h>
34
#include <Table.h>
35
#include <format.h>
36
#include <shared.h>
37
#include <stdlib.h>
38
#include <util.h>
39

40
#include <algorithm>
41
#include <unordered_set>
42
#include <vector>
43

44
bool TDB2::debug_mode = false;
45
static void dependency_scan(std::vector<Task>&);
46

47
////////////////////////////////////////////////////////////////////////////////
48
// Map the C++ on-disk status string to the typed `tc::Status` enum used by the
49
// taskchampion-cpp bridge.
50
static tc::Status statusFromString(const std::string& s) {
3,232✔
51
  if (s == "pending") return tc::Status::Pending;
3,232✔
52
  if (s == "completed") return tc::Status::Completed;
402✔
53
  if (s == "deleted") return tc::Status::Deleted;
193✔
54
  if (s == "recurring") return tc::Status::Recurring;
84✔
55
  if (s == "iterative") return tc::Status::Iterative;
10✔
NEW
56
  throw format("Unknown task status value '{1}'.", s);
×
57
}
58

59
////////////////////////////////////////////////////////////////////////////////
60
void TDB2::open_replica(const std::string& location, bool create_if_missing, bool read_write) {
4,644✔
61
  _replica = tc::new_replica_on_disk(location, create_if_missing, read_write);
4,645✔
62
}
4,643✔
63

64
////////////////////////////////////////////////////////////////////////////////
65
// Add the new task to the replica.
66
void TDB2::add(Task& task) {
3,025✔
67
  // Ensure the task is consistent, and provide defaults if necessary.
68
  // bool argument to validate() is "applyDefault", to apply default values for
69
  // properties not otherwise given.
70
  task.validate(true);
3,025✔
71

72
  rust::Vec<tc::Operation> ops;
3,025✔
73
  maybe_add_undo_point(ops);
3,025✔
74

75
  auto uuid = task.get("uuid");
3,025✔
76
  changes[uuid] = task;
3,025✔
77
  tc::Uuid tcuuid = tc::uuid_from_string(uuid);
3,025✔
78

79
  // run hooks for this new task
80
  Context::getContext().hooks.onAdd(task);
3,025✔
81

82
  auto tctask = replica()->create_task(tcuuid, ops);
3,018✔
83

84
  // Add the task attributes. Defer the `status` write until after every other
85
  // attribute is present so that `tc::Task::set_status`'s lifecycle hooks
86
  // see complete inputs.
87
  std::string deferred_status;
3,018✔
88
  for (auto& attr : task.all()) {
20,077✔
89
    // TaskChampion does not store uuid or id in the task data
90
    if (attr == "uuid" || attr == "id") {
17,059✔
91
      continue;
3,018✔
92
    }
93
    if (attr == "status") {
14,041✔
94
      deferred_status = task.get(attr);
3,018✔
95
      continue;
3,018✔
96
    }
97
    tctask->set_value(attr, task.get(attr), ops);
11,023✔
98
  }
3,018✔
99
  if (!deferred_status.empty()) {
3,018✔
100
    tctask->set_status(statusFromString(deferred_status), ops);
3,018✔
101
  }
102
  replica()->commit_operations(std::move(ops));
3,017✔
103

104
  invalidate_cached_info();
3,017✔
105

106
  // get the ID that was assigned to this task
107
  auto id = working_set()->by_uuid(tcuuid);
3,017✔
108
  if (id > 0) {
3,017✔
109
    task.id = id;
2,897✔
110
  }
111
}
3,035✔
112

113
////////////////////////////////////////////////////////////////////////////////
114
// Modify the task in storage to match the given task.
115
//
116
// Note that there are a few race conditions to consider here.  Taskwarrior
117
// loads the enitre task into memory and this method then essentially writes
118
// the entire task back to the database. So, if the task in the database
119
// changes between loading the task and this method being called, this method
120
// will "revert" those changes. In practice this would only occur when multiple
121
// `task` invocatoins run at the same time and try to modify the same task.
122
//
123
// There is also the possibility that another task process has deleted the task
124
// from the database between the time this process loaded the tsak and called
125
// this method. In this case, this method throws an error that will make sense
126
// to the user. This is especially unlikely since tasks are only deleted when
127
// they have been unmodified for a long time.
128
void TDB2::modify(Task& task) {
601✔
129
  // All locally modified tasks are timestamped, implicitly overwriting any
130
  // changes the user or hooks tried to apply to the "modified" attribute.
131
  task.setAsNow("modified");
601✔
132
  task.validate(false);
601✔
133
  auto uuid = task.get("uuid");
601✔
134

135
  rust::Vec<tc::Operation> ops;
601✔
136
  maybe_add_undo_point(ops);
601✔
137

138
  changes[uuid] = task;
601✔
139

140
  // invoke the hook and allow it to modify the task before updating
141
  Task original;
601✔
142
  get(uuid, original);
601✔
143
  Context::getContext().hooks.onModify(original, task);
601✔
144

145
  tc::Uuid tcuuid = tc::uuid_from_string(uuid);
595✔
146
  auto maybe_tctask = replica()->get_task(tcuuid);
595✔
147
  if (maybe_tctask.is_none()) {
595✔
148
    throw std::string("task no longer exists");
×
149
  }
150
  auto tctask = maybe_tctask.take();
595✔
151

152
  // Diff against the previously-loaded `original` (which holds the persisted
153
  // state) and update through `tc::Task` so its hooks fire.
154
  // Status is handled last so iterative-task can see other attributes.
155
  std::string deferred_status;
595✔
156
  bool deferred_status_changed = false;
595✔
157
  std::unordered_set<std::string> seen;
595✔
158
  for (auto k : task.all()) {
5,172✔
159
    // ignore task keys that aren't stored
160
    if (k == "uuid") {
4,577✔
161
      continue;
595✔
162
    }
163
    seen.insert(k);
3,982✔
164
    auto v_new = task.get(k);
3,982✔
165
    auto v_old = original.get(k);  // "" if missing
3,982✔
166
    if (k == "status") {
3,982✔
167
      if (v_new != v_old) {
595✔
168
        deferred_status = v_new;
214✔
169
        deferred_status_changed = true;
214✔
170
      }
171
      continue;
595✔
172
    }
173
    if (v_new == v_old) continue;
3,387✔
174
    // An empty string indicates the value should be removed.
175
    if (v_new == "") {
822✔
176
      tctask->set_value_remove(k, ops);
4✔
177
    } else {
178
      tctask->set_value(k, v_new, ops);
818✔
179
    }
180
  }
11,492✔
181

182
  // we've now added any updated properties; but must find any deleted properties
183
  for (auto k : original.all()) {
4,652✔
184
    if (k == "uuid" || k == "status") continue;
4,057✔
185
    if (seen.find(k) == seen.end()) {
2,870✔
186
      tctask->set_value_remove(k, ops);
63✔
187
    }
188
  }
4,652✔
189

190
  if (deferred_status_changed) {
595✔
191
    tctask->set_status(statusFromString(deferred_status), ops);
214✔
192
  }
193

194
  replica()->commit_operations(std::move(ops));
595✔
195

196
  invalidate_cached_info();
595✔
197
}
613✔
198

199
////////////////////////////////////////////////////////////////////////////////
200
void TDB2::purge(Task& task) {
10✔
201
  auto uuid = tc::uuid_from_string(task.get("uuid"));
10✔
202
  rust::Vec<tc::Operation> ops;
10✔
203
  auto maybe_tctask = replica()->get_task_data(uuid);
10✔
204
  if (maybe_tctask.is_some()) {
10✔
205
    auto tctask = maybe_tctask.take();
10✔
206
    tctask->delete_task(ops);
10✔
207
    replica()->commit_operations(std::move(ops));
10✔
208
  }
10✔
209

210
  invalidate_cached_info();
10✔
211
}
10✔
212

213
////////////////////////////////////////////////////////////////////////////////
214
rust::Box<tc::Replica>& TDB2::replica() {
19,931✔
215
  // One of the open_replica_ methods must be called before this one.
216
  assert(_replica);
19,931✔
217
  return _replica.value();
19,931✔
218
}
219

220
////////////////////////////////////////////////////////////////////////////////
221
const rust::Box<tc::WorkingSet>& TDB2::working_set() {
750,929✔
222
  if (!_working_set.has_value()) {
750,929✔
223
    _working_set = replica()->working_set();
5,184✔
224
  }
225
  return _working_set.value();
750,929✔
226
}
227

228
////////////////////////////////////////////////////////////////////////////////
229
void TDB2::maybe_add_undo_point(rust::Vec<tc::Operation>& ops) {
3,626✔
230
  // Only add an UndoPoint if there are not yet any changes.
231
  if (changes.size() == 0) {
3,626✔
232
    tc::add_undo_point(ops);
1,825✔
233
  }
234
}
3,626✔
235

236
////////////////////////////////////////////////////////////////////////////////
237
void TDB2::get_changes(std::vector<Task>& changes) {
5✔
238
  std::map<std::string, Task>& changes_map = this->changes;
5✔
239
  changes.clear();
5✔
240
  std::transform(changes_map.begin(), changes_map.end(), std::back_inserter(changes),
5✔
241
                 [](const auto& kv) { return kv.second; });
1✔
242
}
5✔
243

244
////////////////////////////////////////////////////////////////////////////////
245
void TDB2::gc() {
949✔
246
  Timer timer;
949✔
247

248
  // Allowed as an override, but not recommended.
249
  if (Context::getContext().config.getBoolean("gc")) {
2,847✔
250
    replica()->rebuild_working_set(true);
944✔
251
  }
252

253
  Context::getContext().time_gc_us += timer.total_us();
949✔
254
}
949✔
255

256
////////////////////////////////////////////////////////////////////////////////
257
void TDB2::expire_tasks() { replica()->expire_tasks(); }
1✔
258

259
////////////////////////////////////////////////////////////////////////////////
260
// Latest ID is that of the last pending task.
261
int TDB2::latest_id() {
180✔
262
  auto& ws = working_set();
180✔
263
  return (int)ws->largest_index();
180✔
264
}
265

266
////////////////////////////////////////////////////////////////////////////////
267
const std::vector<Task> TDB2::all_tasks() {
459✔
268
  Timer timer;
459✔
269
  auto all_tctasks = replica()->all_task_data();
459✔
270
  std::vector<Task> all;
459✔
271
  for (auto& maybe_tctask : all_tctasks) {
1,687✔
272
    auto tctask = maybe_tctask.take();
1,228✔
273
    all.push_back(Task(std::move(tctask)));
1,228✔
274
  }
1,228✔
275

276
  dependency_scan(all);
459✔
277

278
  Context::getContext().time_load_us += timer.total_us();
459✔
279
  return all;
918✔
280
}
459✔
281

282
////////////////////////////////////////////////////////////////////////////////
283
const std::vector<Task> TDB2::pending_tasks() {
6,461✔
284
  if (!_pending_tasks) {
6,461✔
285
    Timer timer;
3,866✔
286

287
    auto pending_tctasks = replica()->pending_task_data();
3,866✔
288
    std::vector<Task> result;
3,866✔
289
    for (auto& maybe_tctask : pending_tctasks) {
749,309✔
290
      auto tctask = maybe_tctask.take();
745,443✔
291
      result.push_back(Task(std::move(tctask)));
745,443✔
292
    }
745,443✔
293

294
    dependency_scan(result);
3,866✔
295

296
    Context::getContext().time_load_us += timer.total_us();
3,866✔
297
    _pending_tasks = result;
3,866✔
298
  }
3,866✔
299

300
  return *_pending_tasks;
6,461✔
301
}
302

303
////////////////////////////////////////////////////////////////////////////////
304
const std::vector<Task> TDB2::completed_tasks() {
326✔
305
  if (!_completed_tasks) {
326✔
306
    auto all_tctasks = replica()->all_task_data();
326✔
307
    auto& ws = working_set();
326✔
308

309
    std::vector<Task> result;
326✔
310
    for (auto& maybe_tctask : all_tctasks) {
2,225✔
311
      auto tctask = maybe_tctask.take();
1,899✔
312
      // if this task is _not_ in the working set, return it.
313
      if (ws->by_uuid(tctask->get_uuid()) == 0) {
1,899✔
314
        result.push_back(Task(std::move(tctask)));
243✔
315
      }
316
    }
1,899✔
317
    _completed_tasks = result;
326✔
318
  }
326✔
319
  return *_completed_tasks;
326✔
320
}
321

322
////////////////////////////////////////////////////////////////////////////////
323
void TDB2::invalidate_cached_info() {
3,622✔
324
  _pending_tasks = std::nullopt;
3,622✔
325
  _completed_tasks = std::nullopt;
3,622✔
326
  _working_set = std::nullopt;
3,622✔
327
}
3,622✔
328

329
////////////////////////////////////////////////////////////////////////////////
330
// Locate task by ID, wherever it is.
331
bool TDB2::get(int id, Task& task) {
316✔
332
  auto& ws = working_set();
316✔
333
  const auto tcuuid = ws->by_index(id);
316✔
334
  if (!tcuuid.is_nil()) {
316✔
335
    std::string uuid = static_cast<std::string>(tcuuid.to_string());
304✔
336
    // Load all pending tasks in order to get dependency data, and in particular
337
    // `task.is_blocking` and `task.is_blocked`, set correctly.
338
    std::vector<Task> pending = pending_tasks();
304✔
339
    for (auto& pending_task : pending) {
1,549✔
340
      if (pending_task.get("uuid") == uuid) {
3,098✔
341
        task = pending_task;
304✔
342
        return true;
304✔
343
      }
344
    }
345
  }
608✔
346

347
  return false;
12✔
348
}
349

350
////////////////////////////////////////////////////////////////////////////////
351
// Locate task by UUID, including by partial ID, wherever it is.
352
bool TDB2::get(const std::string& uuid, Task& task) {
2,314✔
353
  // Load all pending tasks in order to get dependency data, and in particular
354
  // `task.is_blocking` and `task.is_blocked`, set correctly.
355
  std::vector<Task> pending = pending_tasks();
2,314✔
356

357
  // try by raw uuid, if the length is right
358
  for (auto& pending_task : pending) {
735,138✔
359
    if (closeEnough(pending_task.get("uuid"), uuid, uuid.length())) {
2,200,560✔
360
      task = pending_task;
696✔
361
      return true;
696✔
362
    }
363
  }
364

365
  // Nothing to do but iterate over all tasks and check whether it's closeEnough.
366
  for (auto& maybe_tctask : replica()->all_task_data()) {
733,497✔
367
    auto tctask = maybe_tctask.take();
731,907✔
368
    auto tctask_uuid = static_cast<std::string>(tctask->get_uuid().to_string());
731,907✔
369
    if (closeEnough(tctask_uuid, uuid, uuid.length())) {
731,907✔
370
      task = Task{std::move(tctask)};
28✔
371
      return true;
28✔
372
    }
373
  }
733,553✔
374

375
  return false;
1,590✔
376
}
2,314✔
377

378
////////////////////////////////////////////////////////////////////////////////
379
// Locate task by UUID, wherever it is.
380
bool TDB2::has(const std::string& uuid) {
×
381
  return replica()->get_task_data(tc::uuid_from_string(uuid)).is_some();
×
382
}
383

384
////////////////////////////////////////////////////////////////////////////////
385
const std::vector<Task> TDB2::siblings(Task& task) {
8✔
386
  std::vector<Task> results;
8✔
387
  if (task.has("parent")) {
16✔
388
    std::string parent = task.get("parent");
8✔
389

390
    for (auto& i : this->pending_tasks()) {
35✔
391
      // Do not include self in results.
392
      if (i.id != task.id) {
27✔
393
        // Do not include completed or deleted tasks.
394
        if (i.getStatus() != Task::completed && i.getStatus() != Task::deleted) {
19✔
395
          // If task has the same parent, it is a sibling.
396
          if (i.has("parent") && i.get("parent") == parent) {
77✔
397
            results.push_back(i);
10✔
398
          }
399
        }
400
      }
401
    }
8✔
402
  }
8✔
403

404
  return results;
8✔
405
}
×
406

407
////////////////////////////////////////////////////////////////////////////////
408
const std::vector<Task> TDB2::children(Task& parent) {
72✔
409
  // scan _pending_ tasks for those with `parent` equal to this task
410
  std::vector<Task> results;
72✔
411
  std::string this_uuid = parent.get("uuid");
72✔
412

413
  auto& ws = working_set();
72✔
414
  size_t end_idx = ws->largest_index();
72✔
415

416
  for (size_t i = 0; i <= end_idx; i++) {
393✔
417
    auto uuid = ws->by_index(i);
321✔
418
    if (uuid.is_nil()) {
321✔
419
      continue;
304✔
420
    }
421

422
    // skip self-references
423
    if (uuid.to_string() == this_uuid) {
249✔
424
      continue;
71✔
425
    }
426

427
    auto task_opt = replica()->get_task_data(uuid);
178✔
428
    if (task_opt.is_none()) {
178✔
429
      continue;
×
430
    }
431
    auto task = task_opt.take();
178✔
432

433
    std::string parent_uuid;
178✔
434
    if (!task->get("parent", parent_uuid)) {
534✔
435
      continue;
161✔
436
    }
437

438
    if (parent_uuid == this_uuid) {
17✔
439
      results.push_back(Task(std::move(task)));
17✔
440
    }
441
  }
339✔
442
  return results;
144✔
443
}
72✔
444

445
////////////////////////////////////////////////////////////////////////////////
446
std::string TDB2::uuid(int id) {
59✔
447
  auto& ws = working_set();
59✔
448
  auto uuid = ws->by_index(id);
59✔
449
  if (uuid.is_nil()) {
59✔
450
    return "";
2✔
451
  }
452
  return static_cast<std::string>(uuid.to_string());
58✔
453
}
454

455
////////////////////////////////////////////////////////////////////////////////
456
int TDB2::id(const std::string& uuid) {
746,959✔
457
  auto& ws = working_set();
746,959✔
458
  return ws->by_uuid(tc::uuid_from_string(uuid));
746,959✔
459
}
460

461
////////////////////////////////////////////////////////////////////////////////
462
int TDB2::num_local_changes() { return (int)replica()->num_local_operations(); }
6✔
463

464
////////////////////////////////////////////////////////////////////////////////
465
int TDB2::num_reverts_possible() { return (int)replica()->num_undo_points(); }
4✔
466

467
////////////////////////////////////////////////////////////////////////////////
468
// For any task that has depenencies, follow the chain of dependencies until the
469
// end.  Along the way, update the Task::is_blocked and Task::is_blocking data
470
// cache.
471
static void dependency_scan(std::vector<Task>& tasks) {
4,325✔
472
  for (auto& left : tasks) {
750,996✔
473
    for (auto& dep : left.getDependencyUUIDs()) {
747,028✔
474
      for (auto& right : tasks) {
1,491✔
475
        if (right.get("uuid") == dep) {
2,970✔
476
          // GC hasn't run yet, check both tasks for their current status
477
          Task::status lstatus = left.getStatus();
351✔
478
          Task::status rstatus = right.getStatus();
351✔
479
          if (lstatus != Task::completed && lstatus != Task::deleted &&
351✔
480
              rstatus != Task::completed && rstatus != Task::deleted) {
328✔
481
            left.is_blocked = true;
321✔
482
            right.is_blocking = true;
321✔
483
          }
484

485
          // Only want to break out of the "right" loop.
486
          break;
351✔
487
        }
488
      }
489
    }
746,671✔
490
  }
491
}
4,325✔
492

493
////////////////////////////////////////////////////////////////////////////////
494
// vim: ts=2 et sw=2
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