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

GothenburgBitFactory / taskwarrior / 11420355627

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

push

github

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

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

mainly those visible changes, and miscellaneous others

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

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

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

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

See #3623 for further details.

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

993 existing lines in 25 files now uncovered.

19019 of 22414 relevant lines covered (84.85%)

23067.98 hits per line

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

94.66
/src/commands/CmdContext.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 <CmdConfig.h>
31
#include <CmdContext.h>
32
#include <Context.h>
33
#include <Filter.h>
34
#include <Table.h>
35
#include <format.h>
36
#include <main.h>
37
#include <shared.h>
38
#include <util.h>
39

40
#include <algorithm>
41
#include <set>
42
#include <sstream>
43

44
////////////////////////////////////////////////////////////////////////////////
45
CmdContext::CmdContext() {
4,497✔
46
  _keyword = "context";
4,497✔
47
  _usage = "task          context [<name> | <subcommand>]";
4,497✔
48
  _description = "Set and define contexts (default filters / modifications)";
4,497✔
49
  _read_only = true;
4,497✔
50
  _displays_id = false;
4,497✔
51
  _needs_gc = false;
4,497✔
52
  _uses_context = false;
4,497✔
53
  _accepts_filter = false;
4,497✔
54
  _accepts_modifications = false;
4,497✔
55
  _accepts_miscellaneous = true;
4,497✔
56
  _category = Command::Category::context;
4,497✔
57
}
4,497✔
58

59
////////////////////////////////////////////////////////////////////////////////
60
int CmdContext::execute(std::string& output) {
106✔
61
  std::stringstream out;
106✔
62

63
  // Get the non-attribute, non-fancy command line arguments.
64
  auto words = Context::getContext().cli2.getWords();
106✔
65
  if (words.size() > 0) {
106✔
66
    auto subcommand = words[0];
106✔
67

68
    if (subcommand == "define")
106✔
69
      defineContext(words, out);
56✔
70
    else if (subcommand == "delete")
50✔
71
      deleteContext(words, out);
4✔
72
    else if (subcommand == "list")
46✔
73
      listContexts(out);
3✔
74
    else if (subcommand == "none")
43✔
75
      unsetContext(out);
6✔
76
    else if (subcommand == "show")
37✔
77
      showContext(out);
8✔
78
    else if (words.size())
29✔
79
      setContext(words, out);
29✔
80
  } else {
106✔
81
    listContexts(out);
×
82
    out << "Use 'task context none' to unset the current context.\n";
×
83
  }
84

85
  output = out.str();
98✔
86
  return 0;
98✔
87
}
114✔
88

89
////////////////////////////////////////////////////////////////////////////////
90
// Joins all the words in the specified interval <from, to) to one string,
91
// which is then returned.
92
//
93
// If to is specified as 0 (default value), all the remaining words will be joined.
94
//
95
std::string CmdContext::joinWords(const std::vector<std::string>& words, unsigned int from,
55✔
96
                                  unsigned int to /* = 0 */) {
97
  std::string value = "";
55✔
98

99
  if (to == 0) to = words.size();
55✔
100

101
  for (unsigned int i = from; i < to; ++i) {
113✔
102
    if (i > from) value += ' ';
58✔
103

104
    value += words[i];
58✔
105
  }
106

107
  return value;
55✔
108
}
109

110
////////////////////////////////////////////////////////////////////////////////
111
// Validate the context as valid for writing and fail the write context definition
112
// A valid write context:
113
//   - does not contain any operators except AND
114
//   - does not contain tag exclusion
115
//   - does not use modifiers, except for 'equals' and 'is'
116
//
117
// Returns True if the context is a valid write context. If the context is
118
// invalid due to a wrong modifier use, the modifier string will contain the
119
// first invalid modifier.
120
//
121
bool CmdContext::validateWriteContext(const std::vector<A2>& lexedArgs, std::string& reason) {
55✔
122
  for (auto& arg : lexedArgs) {
219✔
123
    if (arg._lextype == Lexer::Type::op)
167✔
124
      if (arg.attribute("raw") == "or") {
1✔
125
        reason = "contains the 'OR' operator";
1✔
126
        return false;
3✔
127
      }
128

129
    if (arg._lextype == Lexer::Type::pair) {
166✔
130
      auto modifier = arg.attribute("modifier");
68✔
131
      if (modifier != "" && modifier != "is" && modifier != "equals") {
34✔
132
        reason = format("contains an attribute modifier '{1}'", arg.attribute("raw"));
1✔
133
        return false;
1✔
134
      }
135
    }
34✔
136

137
    if (arg._lextype == Lexer::Type::tag) {
165✔
138
      if (arg.attribute("sign") == "-") {
21✔
139
        reason = format("contains tag exclusion '{1}'", arg.attribute("raw"));
1✔
140
        return false;
1✔
141
      }
142
    }
143
  }
144

145
  return true;
52✔
146
}
147

148
////////////////////////////////////////////////////////////////////////////////
149
// Returns all user defined contexts.
150
//
151
std::vector<std::string> CmdContext::getContexts() {
34✔
152
  std::set<std::string> contexts;
34✔
153

154
  for (auto& name : Context::getContext().config)
8,476✔
155
    if (name.first.substr(0, 8) == "context.") {
8,442✔
156
      std::string suffix = name.first.substr(8);
99✔
157

158
      if (suffix.find(".") != std::string::npos)
99✔
159
        contexts.insert(suffix.substr(0, suffix.find(".")));
97✔
160
      else
161
        contexts.insert(suffix);
2✔
162
    }
99✔
163

164
  return std::vector<std::string>(contexts.begin(), contexts.end());
68✔
165
}
34✔
166

167
////////////////////////////////////////////////////////////////////////////////
168
// Defines a new user-provided context.
169
//  - The context definition is written into .taskrc as a context.<name> variable.
170
//  - Deletion of the context requires confirmation if rc.confirmation=yes.
171
//
172
// Returns: 0 if the addition of the config variable was successful, 1 otherwise
173
//
174
// Invoked with: task context define <name> <filter>
175
// Example:      task context define home project:Home
176
//
177
void CmdContext::defineContext(const std::vector<std::string>& words, std::stringstream& out) {
56✔
178
  auto config = Context::getContext().config;
56✔
179
  bool confirmation = config.getBoolean("confirmation");
56✔
180

181
  if (words.size() > 2) {
56✔
182
    auto name = "context." + words[1];
55✔
183
    auto value = joinWords(words, 2);
55✔
184

185
    // Make sure nobody creates a context with name 'list', 'none' or 'show'
186
    if (words[1] == "none" or words[1] == "list" or words[1] == "show") {
55✔
UNCOV
187
      throw format("The name '{1}' is reserved and not allowed to use as a context name.",
×
188
                   words[1]);
×
189
    }
190

191
    // Extract MISCELLANEOUS arguments (containing the filter definition) for later analysis
192
    std::vector<A2> lexedArgs = Context::getContext().cli2.getMiscellaneous();
55✔
193

194
    // Check if the value is a proper filter by filtering current pending.data
195
    Filter filter;
55✔
196
    std::vector<Task> filtered;
55✔
197
    auto pending = Context::getContext().tdb2.pending_tasks();
55✔
198

199
    try {
200
      // This result is not used, and is just to check validity.
201
      Context::getContext().cli2.addFilter(value);
55✔
202
      filter.subset(pending, filtered);
55✔
203
    } catch (std::string exception) {
×
204
      throw format("Filter validation failed: {1}", exception);
×
205
    }
206

207
    // Make user explicitly confirm filters that are matching no pending tasks
208
    if (filtered.size() == 0)
55✔
209
      if (confirmation &&
81✔
210
          !confirm(
28✔
211
              format("The filter '{1}' matches 0 pending tasks. Do you wish to continue?", value)))
81✔
212
        throw std::string("Context definition aborted.");
×
213

214
    std::string reason = "";
55✔
215
    bool valid_write_context = CmdContext::validateWriteContext(lexedArgs, reason);
55✔
216

217
    if (!valid_write_context) {
55✔
218
      std::stringstream warning;
3✔
219
      warning
220
          << format("The filter '{1}' is not a valid modification string, because it contains {2}.",
6✔
221
                    value, reason)
222
          << "\nAs such, value for the write context cannot be set (context will not apply on task "
223
             "add / task log).\n\n"
224
          << format(
6✔
225
                 "Please use 'task config context.{1}.write <default mods>' to set default "
226
                 "attribute values for new tasks in this context manually.\n\n",
227
                 words[1]);
6✔
228
      out << colorizeFootnote(warning.str());
3✔
229
    }
3✔
230

231
    // Set context definition config variable
232
    bool read_success = CmdConfig::setConfigVariable(name + ".read", value, confirmation);
55✔
233
    bool write_success = false;
55✔
234

235
    if (valid_write_context)
55✔
236
      write_success = CmdConfig::setConfigVariable(name + ".write", value, confirmation);
52✔
237

238
    // Remove old-school context name, if it exists, assuming the read context was defined
239
    if (read_success)
55✔
240
      if (config.has(name)) {
54✔
241
        CmdConfig::unsetConfigVariable(name, false);
×
242
      }
243

244
    if (!read_success and !write_success)
55✔
245
      throw format("Context '{1}' not defined.", words[1]);
1✔
246
    else if (!read_success)
54✔
247
      out << format("Context '{1}' defined (write only).", words[1]);
×
248
    else if (!write_success)
54✔
249
      out << format("Context '{1}' defined (read only).", words[1]);
27✔
250
    else
251
      out << format("Context '{1}' defined (read, write).", words[1]);
27✔
252

253
    out << format(" Use 'task context {1}' to activate.\n", words[1]);
54✔
254
  } else
60✔
255
    throw std::string("Both context name and its definition must be provided.");
1✔
256
}
56✔
257

258
////////////////////////////////////////////////////////////////////////////////
259
// Deletes the specified context.
260
//  - If the deleted context is currently active, unset it.
261
//  - Deletion of the context requires confirmation if rc.confirmation=yes.
262
//
263
// Returns: 0 if the removal of the config variable was successful, 1 otherwise
264
//
265
// Invoked with: task context delete <name>
266
// Example:      task context delete home
267
//
268
void CmdContext::deleteContext(const std::vector<std::string>& words, std::stringstream& out) {
4✔
269
  if (words.size() > 1) {
4✔
270
    // Delete the specified context
271
    auto name = "context." + words[1];
3✔
272

273
    auto confirmation = Context::getContext().config.getBoolean("confirmation");
3✔
274
    if (confirmation && !confirm(format("Do you want to delete context '{1}'?", words[1])))
3✔
275
      throw format("Context '{1}' not deleted.", words[1]);
×
276

277
    // Delete legacy format and .read / .write flavours
278
    auto rc = CmdConfig::unsetConfigVariable(name, false);
3✔
279
    rc += CmdConfig::unsetConfigVariable(name + ".read", false);
3✔
280
    rc += CmdConfig::unsetConfigVariable(name + ".write", false);
3✔
281

282
    // If the currently set context was deleted, unset it
283
    if (Context::getContext().config.get("context") == words[1])
3✔
284
      CmdConfig::unsetConfigVariable("context", false);
1✔
285

286
    // Output feedback, rc should be even because only 0 (found and removed)
287
    // and 2 (not found) are aceptable return values from unsetConfigVariable
288
    if (rc % 2 != 0)
3✔
289
      throw format("Context '{1}' not deleted.", words[1]);
×
290
    else if (rc == 6)
3✔
291
      throw format("Context '{1}' not found.", words[1]);
1✔
292

293
    out << format("Context '{1}' deleted.\n", words[1]);
2✔
294
  } else
3✔
295
    throw std::string("Context name needs to be specified.");
1✔
296
}
2✔
297

298
////////////////////////////////////////////////////////////////////////////////
299
// Render a list of context names and their definitions.
300
//
301
// Returns: 0 the resulting list is non-empty, 1 otherwise
302
//
303
// Invoked with: task context list
304
// Example:      task context list
305
//
306
void CmdContext::listContexts(std::stringstream& out) {
3✔
307
  auto contexts = getContexts();
3✔
308
  if (contexts.size()) {
3✔
309
    std::sort(contexts.begin(), contexts.end());
2✔
310

311
    Table table;
2✔
312
    table.width(Context::getContext().getWidth());
2✔
313
    table.add("Name");
2✔
314
    table.add("Type");
2✔
315
    table.add("Definition");
2✔
316
    table.add("Active");
2✔
317
    setHeaderUnderline(table);
2✔
318

319
    std::string activeContext = Context::getContext().config.get("context");
4✔
320

321
    for (auto& userContext : contexts) {
5✔
322
      std::string active = "no";
3✔
323
      if (userContext == activeContext) active = "yes";
3✔
324

325
      int row = table.addRow();
3✔
326
      table.set(row, 0, userContext);
3✔
327
      table.set(row, 1, "read");
3✔
328
      table.set(row, 2, Context::getContext().getTaskContext("read", userContext));
3✔
329
      table.set(row, 3, active);
3✔
330

331
      row = table.addRow();
3✔
332
      table.set(row, 0, "");
3✔
333
      table.set(row, 1, "write");
3✔
334
      table.set(row, 2, Context::getContext().getTaskContext("write", userContext));
3✔
335
      table.set(row, 3, active);
3✔
336
    }
3✔
337

338
    out << optionalBlankLine() << table.render() << optionalBlankLine();
2✔
339
  } else
2✔
340
    throw std::string("No contexts defined.");
1✔
341
}
3✔
342

343
////////////////////////////////////////////////////////////////////////////////
344
// Sets the specified context as currently active.
345
//   - If some other context was active, the value of currently active context
346
//     is replaced, not added.
347
//   - Setting of the context does not require confirmation.
348
//
349
// Returns: 0 if the setting of the context was successful, 1 otherwise
350
//
351
// Invoked with: task context <name>
352
// Example:      task context home
353
//
354
void CmdContext::setContext(const std::vector<std::string>& words, std::stringstream& out) {
29✔
355
  auto value = words[0];
29✔
356
  auto contexts = getContexts();
29✔
357

358
  // Check that the specified context is defined
359
  if (std::find(contexts.begin(), contexts.end(), value) == contexts.end())
29✔
360
    throw format("Context '{1}' not found.", value);
2✔
361

362
  // Set the active context.
363
  // Should always succeed, as we do not require confirmation.
364
  bool success = CmdConfig::setConfigVariable("context", value, false);
27✔
365

366
  if (!success) throw format("Context '{1}' not applied.", value);
27✔
367

368
  out << format("Context '{1}' set. Use 'task context none' to remove.\n", value);
27✔
369
}
31✔
370

371
////////////////////////////////////////////////////////////////////////////////
372
// Shows the currently active context.
373
//
374
// Returns: Always returns 0.
375
//
376
// Invoked with: task context show
377
// Example:      task context show
378
//
379
void CmdContext::showContext(std::stringstream& out) {
8✔
380
  auto currentContext = Context::getContext().config.get("context");
16✔
381

382
  if (currentContext == "")
8✔
383
    out << "No context is currently applied.\n";
7✔
384
  else {
385
    out << format(
2✔
386
        "Context '{1}' with \n\n* read filter: '{2}'\n* write filter: '{3}'\n\nis currently "
387
        "applied.\n",
388
        currentContext, Context::getContext().getTaskContext("read", ""),
2✔
389
        Context::getContext().getTaskContext("write", ""));
3✔
390
  }
391
}
8✔
392

393
////////////////////////////////////////////////////////////////////////////////
394
// Unsets the currently active context.
395
//   - Unsetting of the context does not require confirmation.
396
//
397
// Returns: 0 if the unsetting of the context was successful, 1 otherwise (also
398
//          returned if no context is currently active)
399
//
400
// Invoked with: task context none
401
// Example:      task context none
402
//
403
void CmdContext::unsetContext(std::stringstream& out) {
6✔
404
  if (CmdConfig::unsetConfigVariable("context", false)) throw std::string("Context not unset.");
6✔
405

406
  out << "Context unset.\n";
5✔
407
}
5✔
408

409
////////////////////////////////////////////////////////////////////////////////
410
CmdCompletionContext::CmdCompletionContext() {
4,497✔
411
  _keyword = "_context";
4,497✔
412
  _usage = "task          _context";
4,497✔
413
  _description = "Lists all supported contexts, for completion purposes";
4,497✔
414
  _read_only = true;
4,497✔
415
  _displays_id = false;
4,497✔
416
  _needs_gc = false;
4,497✔
417
  _uses_context = false;
4,497✔
418
  _accepts_filter = false;
4,497✔
419
  _accepts_modifications = false;
4,497✔
420
  _accepts_miscellaneous = false;
4,497✔
421
  _category = Command::Category::internal;
4,497✔
422
}
4,497✔
423

424
////////////////////////////////////////////////////////////////////////////////
425
int CmdCompletionContext::execute(std::string& output) {
2✔
426
  for (auto& context : CmdContext::getContexts()) output += context + '\n';
6✔
427

428
  return 0;
2✔
429
}
430

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