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

GothenburgBitFactory / taskwarrior / 12343201393

15 Dec 2024 11:30PM UTC coverage: 84.419% (-1.1%) from 85.522%
12343201393

Pull #3724

github

web-flow
Merge 532931b9f into ddae5c4ba
Pull Request #3724: Support importing Taskwarrior v2.x data files

15 of 145 new or added lines in 4 files covered. (10.34%)

183 existing lines in 48 files now uncovered.

19289 of 22849 relevant lines covered (84.42%)

23168.82 hits per line

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

96.25
/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 <main.h>
37
#include <shared.h>
38
#include <signal.h>
39
#include <stdlib.h>
40
#include <util.h>
41

42
#include <algorithm>
43
#include <iostream>
44
#include <list>
45
#include <sstream>
46
#include <unordered_set>
47
#include <vector>
48

49
bool TDB2::debug_mode = false;
50
static void dependency_scan(std::vector<Task>&);
51

52
////////////////////////////////////////////////////////////////////////////////
53
void TDB2::open_replica(const std::string& location, bool create_if_missing) {
4,497✔
54
  _replica = tc::new_replica_on_disk(location, create_if_missing);
4,498✔
55
}
4,496✔
56

57
////////////////////////////////////////////////////////////////////////////////
58
// Add the new task to the replica.
59
void TDB2::add(Task& task) {
2,985✔
60
  // Validate a task for addition. This is stricter than `task.validate`, as any
61
  // inconsistency is probably user error.
62
  task.validate_add();
2,985✔
63

64
  // Ensure the task is consistent, and provide defaults if necessary.
65
  // bool argument to validate() is "applyDefault", to apply default values for
66
  // properties not otherwise given.
67
  task.validate(true);
2,984✔
68

69
  rust::Vec<tc::Operation> ops;
2,984✔
70
  maybe_add_undo_point(ops);
2,984✔
71

72
  auto uuid = task.get("uuid");
2,984✔
73
  changes[uuid] = task;
2,984✔
74
  tc::Uuid tcuuid = tc::uuid_from_string(uuid);
2,984✔
75

76
  // run hooks for this new task
77
  Context::getContext().hooks.onAdd(task);
2,984✔
78

79
  auto taskdata = tc::create_task(tcuuid, ops);
2,977✔
80

81
  // add the task attributes
82
  for (auto& attr : task.all()) {
19,773✔
83
    // TaskChampion does not store uuid or id in the task data
84
    if (attr == "uuid" || attr == "id") {
16,796✔
85
      continue;
2,977✔
86
    }
87

88
    taskdata->update(attr, task.get(attr), ops);
13,819✔
89
  }
2,977✔
90
  replica()->commit_operations(std::move(ops));
2,977✔
91

92
  invalidate_cached_info();
2,977✔
93

94
  // get the ID that was assigned to this task
95
  auto id = working_set()->by_uuid(tcuuid);
2,977✔
96
  if (id > 0) {
2,977✔
97
    task.id = id;
2,857✔
98
  }
99
}
2,991✔
100

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

123
  rust::Vec<tc::Operation> ops;
576✔
124
  maybe_add_undo_point(ops);
576✔
125

126
  changes[uuid] = task;
576✔
127

128
  // invoke the hook and allow it to modify the task before updating
129
  Task original;
576✔
130
  get(uuid, original);
576✔
131
  Context::getContext().hooks.onModify(original, task);
576✔
132

133
  tc::Uuid tcuuid = tc::uuid_from_string(uuid);
570✔
134
  auto maybe_tctask = replica()->get_task_data(tcuuid);
570✔
135
  if (maybe_tctask.is_none()) {
570✔
136
    throw std::string("task no longer exists");
×
137
  }
138
  auto tctask = maybe_tctask.take();
570✔
139

140
  // Perform the necessary `update` operations to set all keys in `tctask`
141
  // equal to those in `task`.
142
  std::unordered_set<std::string> seen;
570✔
143
  for (auto k : task.all()) {
4,941✔
144
    // ignore task keys that aren't stored
145
    if (k == "uuid") {
4,371✔
146
      continue;
570✔
147
    }
148
    seen.insert(k);
3,801✔
149
    bool update = false;
3,801✔
150
    auto v_new = task.get(k);
3,801✔
151
    std::string v_tctask;
3,801✔
152
    if (tctask->get(k, v_tctask)) {
3,801✔
153
      update = v_tctask != v_new;
3,262✔
154
    } else {
155
      // tctask does not contain k, so update it
156
      update = true;
539✔
157
    }
158
    if (update) {
3,801✔
159
      // An empty string indicates the value should be removed.
160
      if (v_new == "") {
969✔
161
        tctask->update_remove(k, ops);
4✔
162
      } else {
163
        tctask->update(k, v_new, ops);
965✔
164
      }
165
    }
166
  }
4,941✔
167

168
  // we've now added and updated properties; but must find any deleted properties
169
  for (auto k : tctask->properties()) {
4,428✔
170
    auto kstr = static_cast<std::string>(k);
3,858✔
171
    if (seen.find(kstr) == seen.end()) {
3,858✔
172
      tctask->update_remove(kstr, ops);
61✔
173
    }
174
  }
4,428✔
175

176
  replica()->commit_operations(std::move(ops));
570✔
177

178
  invalidate_cached_info();
570✔
179
}
588✔
180

181
////////////////////////////////////////////////////////////////////////////////
182
void TDB2::purge(Task& task) {
6✔
183
  auto uuid = tc::uuid_from_string(task.get("uuid"));
6✔
184
  rust::Vec<tc::Operation> ops;
6✔
185
  auto maybe_tctask = replica()->get_task_data(uuid);
6✔
186
  if (maybe_tctask.is_some()) {
6✔
187
    auto tctask = maybe_tctask.take();
6✔
188
    tctask->delete_task(ops);
6✔
189
    replica()->commit_operations(std::move(ops));
6✔
190
  }
6✔
191

192
  invalidate_cached_info();
6✔
193
}
6✔
194

195
////////////////////////////////////////////////////////////////////////////////
196
rust::Box<tc::Replica>& TDB2::replica() {
16,513✔
197
  // Create a replica in-memory if `open_replica` has not been called. This
198
  // occurs in tests.
199
  if (!_replica) {
16,513✔
200
    _replica = tc::new_replica_in_memory();
1✔
201
  }
202
  return _replica.value();
16,513✔
203
}
204

205
////////////////////////////////////////////////////////////////////////////////
206
const rust::Box<tc::WorkingSet>& TDB2::working_set() {
750,668✔
207
  if (!_working_set.has_value()) {
750,668✔
208
    _working_set = replica()->working_set();
5,061✔
209
  }
210
  return _working_set.value();
750,668✔
211
}
212

213
////////////////////////////////////////////////////////////////////////////////
214
void TDB2::maybe_add_undo_point(rust::Vec<tc::Operation>& ops) {
3,560✔
215
  // Only add an UndoPoint if there are not yet any changes.
216
  if (changes.size() == 0) {
3,560✔
217
    tc::add_undo_point(ops);
1,774✔
218
  }
219
}
3,560✔
220

221
////////////////////////////////////////////////////////////////////////////////
222
void TDB2::get_changes(std::vector<Task>& changes) {
5✔
223
  std::map<std::string, Task>& changes_map = this->changes;
5✔
224
  changes.clear();
5✔
225
  std::transform(changes_map.begin(), changes_map.end(), std::back_inserter(changes),
5✔
226
                 [](const auto& kv) { return kv.second; });
1✔
227
}
5✔
228

229
////////////////////////////////////////////////////////////////////////////////
230
void TDB2::gc() {
913✔
231
  Timer timer;
913✔
232

233
  // Allowed as an override, but not recommended.
234
  if (Context::getContext().config.getBoolean("gc")) {
2,739✔
235
    replica()->rebuild_working_set(true);
910✔
236
  }
237

238
  Context::getContext().time_gc_us += timer.total_us();
913✔
239
}
913✔
240

241
////////////////////////////////////////////////////////////////////////////////
242
void TDB2::expire_tasks() { replica()->expire_tasks(); }
1✔
243

244
////////////////////////////////////////////////////////////////////////////////
245
// Latest ID is that of the last pending task.
246
int TDB2::latest_id() {
170✔
247
  auto& ws = working_set();
170✔
248
  return (int)ws->largest_index();
170✔
249
}
250

251
////////////////////////////////////////////////////////////////////////////////
252
const std::vector<Task> TDB2::all_tasks() {
439✔
253
  Timer timer;
439✔
254
  auto all_tctasks = replica()->all_task_data();
439✔
255
  std::vector<Task> all;
439✔
256
  for (auto& maybe_tctask : all_tctasks) {
1,629✔
257
    auto tctask = maybe_tctask.take();
1,190✔
258
    all.push_back(Task(std::move(tctask)));
1,190✔
259
  }
1,190✔
260

261
  dependency_scan(all);
439✔
262

263
  Context::getContext().time_load_us += timer.total_us();
439✔
264
  return all;
878✔
265
}
439✔
266

267
////////////////////////////////////////////////////////////////////////////////
268
const std::vector<Task> TDB2::pending_tasks() {
6,379✔
269
  if (!_pending_tasks) {
6,379✔
270
    Timer timer;
3,788✔
271

272
    auto pending_tctasks = replica()->pending_task_data();
3,788✔
273
    std::vector<Task> result;
3,788✔
274
    for (auto& maybe_tctask : pending_tctasks) {
749,117✔
275
      auto tctask = maybe_tctask.take();
745,329✔
276
      result.push_back(Task(std::move(tctask)));
745,329✔
277
    }
745,329✔
278

279
    dependency_scan(result);
3,788✔
280

281
    Context::getContext().time_load_us += timer.total_us();
3,788✔
282
    _pending_tasks = result;
3,788✔
283
  }
3,788✔
284

285
  return *_pending_tasks;
6,379✔
286
}
287

288
////////////////////////////////////////////////////////////////////////////////
289
const std::vector<Task> TDB2::completed_tasks() {
299✔
290
  if (!_completed_tasks) {
299✔
291
    auto all_tctasks = replica()->all_task_data();
299✔
292
    auto& ws = working_set();
299✔
293

294
    std::vector<Task> result;
299✔
295
    for (auto& maybe_tctask : all_tctasks) {
2,158✔
296
      auto tctask = maybe_tctask.take();
1,859✔
297
      // if this task is _not_ in the working set, return it.
298
      if (ws->by_uuid(tctask->get_uuid()) == 0) {
1,859✔
299
        result.push_back(Task(std::move(tctask)));
226✔
300
      }
301
    }
1,859✔
302
    _completed_tasks = result;
299✔
303
  }
299✔
304
  return *_completed_tasks;
299✔
305
}
306

307
////////////////////////////////////////////////////////////////////////////////
308
void TDB2::invalidate_cached_info() {
3,553✔
309
  _pending_tasks = std::nullopt;
3,553✔
310
  _completed_tasks = std::nullopt;
3,553✔
311
  _working_set = std::nullopt;
3,553✔
312
}
3,553✔
313

314
////////////////////////////////////////////////////////////////////////////////
315
// Locate task by ID, wherever it is.
316
bool TDB2::get(int id, Task& task) {
308✔
317
  auto& ws = working_set();
308✔
318
  const auto tcuuid = ws->by_index(id);
308✔
319
  if (!tcuuid.is_nil()) {
308✔
320
    std::string uuid = static_cast<std::string>(tcuuid.to_string());
296✔
321
    // Load all pending tasks in order to get dependency data, and in particular
322
    // `task.is_blocking` and `task.is_blocked`, set correctly.
323
    std::vector<Task> pending = pending_tasks();
296✔
324
    for (auto& pending_task : pending) {
1,537✔
325
      if (pending_task.get("uuid") == uuid) {
3,074✔
326
        task = pending_task;
296✔
327
        return true;
296✔
328
      }
329
    }
330
  }
592✔
331

332
  return false;
12✔
333
}
334

335
////////////////////////////////////////////////////////////////////////////////
336
// Locate task by UUID, including by partial ID, wherever it is.
337
bool TDB2::get(const std::string& uuid, Task& task) {
2,286✔
338
  // Load all pending tasks in order to get dependency data, and in particular
339
  // `task.is_blocking` and `task.is_blocked`, set correctly.
340
  std::vector<Task> pending = pending_tasks();
2,286✔
341

342
  // try by raw uuid, if the length is right
343
  for (auto& pending_task : pending) {
735,101✔
344
    if (closeEnough(pending_task.get("uuid"), uuid, uuid.length())) {
2,200,458✔
345
      task = pending_task;
671✔
346
      return true;
671✔
347
    }
348
  }
349

350
  // Nothing to do but iterate over all tasks and check whether it's closeEnough.
351
  for (auto& maybe_tctask : replica()->all_task_data()) {
733,519✔
352
    auto tctask = maybe_tctask.take();
731,929✔
353
    auto tctask_uuid = static_cast<std::string>(tctask->get_uuid().to_string());
731,929✔
354
    if (closeEnough(tctask_uuid, uuid, uuid.length())) {
731,929✔
355
      task = Task{std::move(tctask)};
25✔
356
      return true;
25✔
357
    }
358
  }
733,569✔
359

360
  return false;
1,590✔
361
}
2,286✔
362

363
////////////////////////////////////////////////////////////////////////////////
364
// Locate task by UUID, wherever it is.
365
bool TDB2::has(const std::string& uuid) {
×
366
  Task task;
×
367
  return get(uuid, task);
×
UNCOV
368
}
×
369

370
////////////////////////////////////////////////////////////////////////////////
371
const std::vector<Task> TDB2::siblings(Task& task) {
8✔
372
  std::vector<Task> results;
8✔
373
  if (task.has("parent")) {
16✔
374
    std::string parent = task.get("parent");
8✔
375

376
    for (auto& i : this->pending_tasks()) {
35✔
377
      // Do not include self in results.
378
      if (i.id != task.id) {
27✔
379
        // Do not include completed or deleted tasks.
380
        if (i.getStatus() != Task::completed && i.getStatus() != Task::deleted) {
19✔
381
          // If task has the same parent, it is a sibling.
382
          if (i.has("parent") && i.get("parent") == parent) {
77✔
383
            results.push_back(i);
10✔
384
          }
385
        }
386
      }
387
    }
8✔
388
  }
8✔
389

390
  return results;
8✔
UNCOV
391
}
×
392

393
////////////////////////////////////////////////////////////////////////////////
394
const std::vector<Task> TDB2::children(Task& parent) {
71✔
395
  // scan _pending_ tasks for those with `parent` equal to this task
396
  std::vector<Task> results;
71✔
397
  std::string this_uuid = parent.get("uuid");
71✔
398

399
  auto& ws = working_set();
71✔
400
  size_t end_idx = ws->largest_index();
71✔
401

402
  for (size_t i = 0; i <= end_idx; i++) {
387✔
403
    auto uuid = ws->by_index(i);
316✔
404
    if (uuid.is_nil()) {
316✔
405
      continue;
302✔
406
    }
407

408
    // skip self-references
409
    if (uuid.to_string() == this_uuid) {
245✔
410
      continue;
70✔
411
    }
412

413
    auto task_opt = replica()->get_task_data(uuid);
175✔
414
    if (task_opt.is_none()) {
175✔
415
      continue;
×
416
    }
417
    auto task = task_opt.take();
175✔
418

419
    std::string parent_uuid;
175✔
420
    if (!task->get("parent", parent_uuid)) {
525✔
421
      continue;
161✔
422
    }
423

424
    if (parent_uuid == this_uuid) {
14✔
425
      results.push_back(Task(std::move(task)));
14✔
426
    }
427
  }
336✔
428
  return results;
142✔
429
}
71✔
430

431
////////////////////////////////////////////////////////////////////////////////
432
std::string TDB2::uuid(int id) {
59✔
433
  auto& ws = working_set();
59✔
434
  auto uuid = ws->by_index(id);
59✔
435
  if (uuid.is_nil()) {
59✔
436
    return "";
2✔
437
  }
438
  return static_cast<std::string>(uuid.to_string());
58✔
439
}
440

441
////////////////////////////////////////////////////////////////////////////////
442
int TDB2::id(const std::string& uuid) {
746,784✔
443
  auto& ws = working_set();
746,784✔
444
  return ws->by_uuid(tc::uuid_from_string(uuid));
746,784✔
445
}
446

447
////////////////////////////////////////////////////////////////////////////////
448
int TDB2::num_local_changes() { return (int)replica()->num_local_operations(); }
5✔
449

450
////////////////////////////////////////////////////////////////////////////////
451
int TDB2::num_reverts_possible() { return (int)replica()->num_undo_points(); }
3✔
452

453
////////////////////////////////////////////////////////////////////////////////
454
void TDB2::dump() {
×
455
  // TODO
UNCOV
456
}
×
457

458
////////////////////////////////////////////////////////////////////////////////
459
// For any task that has depenencies, follow the chain of dependencies until the
460
// end.  Along the way, update the Task::is_blocked and Task::is_blocking data
461
// cache.
462
static void dependency_scan(std::vector<Task>& tasks) {
4,227✔
463
  for (auto& left : tasks) {
750,746✔
464
    for (auto& dep : left.getDependencyUUIDs()) {
746,878✔
465
      for (auto& right : tasks) {
1,479✔
466
        if (right.get("uuid") == dep) {
2,946✔
467
          // GC hasn't run yet, check both tasks for their current status
468
          Task::status lstatus = left.getStatus();
353✔
469
          Task::status rstatus = right.getStatus();
353✔
470
          if (lstatus != Task::completed && lstatus != Task::deleted &&
353✔
471
              rstatus != Task::completed && rstatus != Task::deleted) {
330✔
472
            left.is_blocked = true;
321✔
473
            right.is_blocking = true;
321✔
474
          }
475

476
          // Only want to break out of the "right" loop.
477
          break;
353✔
478
        }
479
      }
480
    }
746,519✔
481
  }
482
}
4,227✔
483

484
////////////////////////////////////////////////////////////////////////////////
485
// 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

© 2025 Coveralls, Inc