Compare commits

..

78 Commits

Author SHA1 Message Date
Dustin J. Mitchell
2e5177aa7c Update to TaskChampion 2.0.2 (#3746) 2025-01-06 13:29:19 -05:00
pre-commit-ci[bot]
ae3651fd3f [pre-commit.ci] pre-commit autoupdate (#3748)
updates:
- [github.com/pre-commit/mirrors-clang-format: v19.1.5 → v19.1.6](https://github.com/pre-commit/mirrors-clang-format/compare/v19.1.5...v19.1.6)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2025-01-06 12:48:12 -05:00
Dustin J. Mitchell
ddeec3512a Add more CMake options to INSTALL (#3743) 2025-01-01 13:29:17 -05:00
Karl
630585d7b4 Update git log in CMakeLists.txt to not include potential signatures (#3742) 2024-12-31 13:51:35 -05:00
jrmarino
9105985c7c Add offline build notes to INSTALL (#3705) (#3740)
Document additional steps that have been successful for NixOS and Ravenports.
2024-12-31 17:28:48 +00:00
Tejada-Omar
3bf0200602 Consider news read if news.version > current version (#3734)
Avoids two installations of taskwarrior with differing versions from
constantly nagging and rewriting `news.version`
2024-12-23 11:34:51 -05:00
Kalle Kietäväinen
1b9353dccc Fix suppressing news nag after reading the news (#3731) 2024-12-20 13:13:23 +01:00
Dustin J. Mitchell
1ee69ea214 Release 3.3.0 (#3729)
* Release 3.3.0

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-12-19 16:57:43 +01:00
Dustin J. Mitchell
dcbe916286 Make test hooks executable (#3728) 2024-12-17 19:08:48 -05:00
Dustin J. Mitchell
cc505e4881 Support importing Taskwarrior v2.x data files (#3724)
This should ease the pain of upgrading from v2.x to v3.x.
2024-12-17 01:24:45 +00:00
Dustin J. Mitchell
758ac8f850 Add support for sync to AWS (#3723)
This is closely modeled on support for sync to GCP (#3223), but with
different authentication options to mirror typical usage of AWS.
2024-12-17 01:08:50 +00:00
pre-commit-ci[bot]
ff325bc19e [pre-commit.ci] pre-commit autoupdate (#3725)
updates:
- [github.com/pre-commit/mirrors-clang-format: v19.1.4 → v19.1.5](https://github.com/pre-commit/mirrors-clang-format/compare/v19.1.4...v19.1.5)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-12-16 12:59:12 -05:00
dependabot[bot]
ddae5c4ba9 Bump taskchampion from 0.9.0 to 1.0.0 (#3722)
* Bump taskchampion from 0.9.0 to 1.0.0

Bumps [taskchampion](https://github.com/GothenburgBitFactory/taskchampion) from 0.9.0 to 1.0.0.
- [Release notes](https://github.com/GothenburgBitFactory/taskchampion/releases)
- [Commits](https://github.com/GothenburgBitFactory/taskchampion/compare/v0.9.0...v1.0.0)

---
updated-dependencies:
- dependency-name: taskchampion
  dependency-type: direct:production
  update-type: version-update:semver-major
...

* Bump MSRV

* update url to address RUSTSEC-2024-0421

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Dustin J. Mitchell <dustin@v.igoro.us>
2024-12-10 13:45:25 +00:00
Dustin J. Mitchell
4add839548 Add instructions for running against customized TaskChampion (#3719)
This is often useful when doing work that includes changes in both TC
and TW.
2024-12-09 12:05:09 +01:00
Dustin J. Mitchell
3ea726f2bb Cargo update hashbrown (#3716) 2024-12-07 21:56:34 -05:00
Kursat Aktas
ce70a182c1 Introducing Taskwarrior Guru on Gurubase.io (#3689)
* Introducing Taskwarrior Guru on Gurubase.io

Signed-off-by: Kursat Aktas <kursat.ce@gmail.com>
2024-12-07 11:18:52 -05:00
dependabot[bot]
8de7ff52e7 Bump docker/build-push-action from 6.9.0 to 6.10.0 (#3715)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.9.0 to 6.10.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6.9.0...v6.10.0)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-12-02 08:13:54 -05:00
David Tolnay
dfc36aefcf Rely on cxx to enforce matching versions (#3713) 2024-12-01 17:12:05 -05:00
Dustin J. Mitchell
c2cb7f36a7 Include cxxbridge-cmd in Cargo.lock, check version consistency (#3712)
This adds cxxbridge-cmd to Cargo.lock per
https://github.com/dtolnay/cxx/issues/1407#issuecomment-2509136343

It adds an MSRV to `src/taskchampion-cpp/Cargo.toml` so that the
version of `Cargo.lock` is stil compatible with the MSRV.

It additionally adds a check of the Cargo metadata for all of the cxx*
versions agreeing, and for the MSRV's agreeing.
2024-12-01 15:22:26 +00:00
Dustin J. Mitchell
e5ab1bc7a5 Update libshared (#3711)
This brings in https://github.com/GothenburgBitFactory/libshared/pull/89
2024-11-29 09:35:21 -05:00
Dustin J. Mitchell
4797c4e17e Check Datetime addition when performing recurrence (#3708) 2024-11-29 09:12:20 -05:00
Dustin J. Mitchell
0b286460b6 Update rustls to latest version (#3704) 2024-11-27 17:59:31 -05:00
Dustin J. Mitchell
a99b6084e8 Only nag to read news when there's news to read (#3699) 2024-11-25 17:48:06 -05:00
pre-commit-ci[bot]
5664182f5e [pre-commit.ci] pre-commit autoupdate (#3701)
updates:
- [github.com/pre-commit/mirrors-clang-format: v19.1.3 → v19.1.4](https://github.com/pre-commit/mirrors-clang-format/compare/v19.1.3...v19.1.4)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-11-25 20:33:39 +01:00
Felix Schurk
68c63372c1 change fedora39 to fedora41 runner (#3698)
* change fedora39 to fedora41 runner

* update github workflow runner file
2024-11-25 05:54:43 +01:00
Dustin J. Mitchell
ed3667c19e Revert "add cpp standard flag to cmake (#3684)"
This reverts commit 01ced3238e.
2024-11-22 22:47:29 -05:00
Thomas Lauf
caae3fa37b Use repository owner instead of actor to log into GitHub CR 2024-11-22 21:18:31 +01:00
geoffpaulsen
066bb3e331 Updating the URL for Migration Documentation (#3694) 2024-11-21 21:19:44 -05:00
Kalle Kietäväinen
98204b17a6 Set CMake C++ standard (#3688)
Instead of setting `-std` compiler flag directly, set the
`CMAKE_CXX_STANDARD` variable. This lets CMake know the required C++
standard and evaluate the final compiler flag correctly, taking into
account compile features set by `target_compile_features()`.

This change preserves the existing behavior, where compiler extensions
are disabled for other targets than Cygwin.
2024-11-17 15:18:28 -05:00
Dustin J. Mitchell
096f94d3d1 Use Signal instead of PGP to contact me securely (#3685) 2024-11-16 13:45:44 -05:00
Felix Schurk
01ced3238e add cpp standard flag to cmake (#3684)
add DCMAKE_CXX_STANDARD flag

See documentation in CMake.
https://cmake.org/cmake/help/latest/prop_tgt/CXX_STANDARD.html
2024-11-15 16:32:05 -05:00
Chongyun Lee
8cc4c461d6 Fix compile with libc++ 18 (#3680) 2024-11-13 22:09:08 -05:00
Scott Mcdermott
3e8bda6a23 release 3.2.0 links wrong PR for weekstart change (#3681)
ChangeLog: fix typo in linked PR number for weekstart change

off by 1000
2024-11-13 22:04:25 -05:00
Dustin J. Mitchell
7a092bea03 Release v3.2.0 (#3679) 2024-11-12 14:52:22 -05:00
Dustin J. Mitchell
54a94bd18c include bubblegum-256.theme in default .taskrc (#3673) 2024-11-08 13:10:04 +01:00
Dustin J. Mitchell
a2f9b92d6c Better undo output (and remove undo.style config) (#3672) 2024-11-07 14:56:34 -05:00
Dustin J. Mitchell
dcc8a8cdde bump libshared for bold 256color support (#3670)
In particular, commit 47a750c385.
2024-11-06 07:40:16 -05:00
Dustin J. Mitchell
c9967c20e2 Restore support for task info journal (#3671)
This support was removed before Taskwarrior-3.x, and is now restored,
including the original tests removed in
ddd367232e
2024-11-06 07:39:39 -05:00
Dustin J. Mitchell
7da23aee1c Run cargo test and fix it (#3663)
run cargo test and fix it
2024-11-05 08:55:10 -05:00
Denis Zh.
5b1be95f7d Add color.calendar.scheduled to no-color.theme (#3666)
* Add scheduled color setting for calendar report
* Add default color.calendar.scheduled to all themes
2024-11-05 08:54:49 -05:00
Denis Zh.
0ff7844732 Fix missing line in man task-color (#3665)
Escape leading single quote to prevent groff misinterpretation as a
control character.
2024-11-05 08:00:43 -05:00
pre-commit-ci[bot]
023e7958c9 [pre-commit.ci] pre-commit autoupdate (#3664)
updates:
- [github.com/pre-commit/mirrors-clang-format: v19.1.2 → v19.1.3](https://github.com/pre-commit/mirrors-clang-format/compare/v19.1.2...v19.1.3)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-11-04 16:42:35 -05:00
Dustin J. Mitchell
8184226319 Support ENABLE_TLS_NATIVE_ROOTS to use system TLS CAs (#3660) 2024-11-02 11:16:23 +01:00
Dustin J. Mitchell
94c95563ab Upgrade to TaskChampion 0.9.0 (#3662)
See https://github.com/GothenburgBitFactory/taskchampion/releases/tag/v0.9.0
2024-10-31 12:59:49 +00:00
Dustin J. Mitchell
6ff900f3fc Use Replica::pending_tasks (#3661)
This replaces a loop over _all_ tasks with one that fetches only pending
tasks, as determined by the working set.

This should be faster for task DB's with large numbers of completed
tasks, although on my medium-sized installation (~5000 total tasks) the
difference is negligible.
2024-10-30 21:49:04 -04:00
dependabot[bot]
8bad3cdcbc Bump docker/build-push-action from 6.8.0 to 6.9.0 (#3642)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.8.0 to 6.9.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6.8.0...v6.9.0)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-27 15:22:38 -04:00
pre-commit-ci[bot]
0bb32d188c [pre-commit.ci] pre-commit autoupdate (#3657)
updates:
- [github.com/pre-commit/mirrors-clang-format: v19.1.1 → v19.1.2](https://github.com/pre-commit/mirrors-clang-format/compare/v19.1.1...v19.1.2)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-10-27 15:22:29 -04:00
dependabot[bot]
c3b850898f Bump sigstore/cosign-installer from 3.6.0 to 3.7.0 (#3641)
Bumps [sigstore/cosign-installer](https://github.com/sigstore/cosign-installer) from 3.6.0 to 3.7.0.
- [Release notes](https://github.com/sigstore/cosign-installer/releases)
- [Commits](https://github.com/sigstore/cosign-installer/compare/v3.6.0...v3.7.0)

---
updated-dependencies:
- dependency-name: sigstore/cosign-installer
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-10-27 15:22:12 -04:00
Fredrik Lanker
af8c5d58c8 Limit the allowed epoch timestamps (#3651)
The code for parsing epoch timestamps when displaying tasks only
supports values between year 1980 and 9999. Previous to this change, it
was possible to set e.g., the due timestamp to a value outside of these
limits, which would make it impossible to later on show the task.

With this change, we only allow setting values within the same limits
used by the code for displaying tasks.
2024-10-23 19:18:21 -04:00
Dustin J. Mitchell
2db373d631 Update to TaskChampion 0.8.0 (#3648)
* Update to TaskChampion 0.8.0

* Cargo update
2024-10-22 19:37:47 -04:00
Dustin J. Mitchell
96c72f3e06 Issue warnings instead of errors for 'weird' tasks (#3646)
* Issue warnings instead of errors for 'weird' tasks

* Support more comprehensive checks when adding a task
2024-10-22 15:15:51 -04:00
Thomas Lauf
4bf6144daf Add SECURITY.md (#3655) 2024-10-21 15:16:25 -04:00
Scott Mcdermott
3e20ad6f6f 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.
2024-10-19 16:00:50 -04:00
Dustin J. Mitchell
7bd3d1b892 Install uuid-dev in GitHub action (#3647) 2024-10-14 17:48:41 -04:00
pre-commit-ci[bot]
0bd3989bab [pre-commit.ci] pre-commit autoupdate (#3650)
updates:
- [github.com/psf/black: 24.8.0 → 24.10.0](https://github.com/psf/black/compare/24.8.0...24.10.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-10-14 17:47:39 -04:00
Dustin J. Mitchell
26c383d615 Restore 'load' timer (#3635)
* Restore 'load' timer

* [pre-commit.ci] auto fixes from pre-commit.com hooks

for more information, see https://pre-commit.ci

---------

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-10-10 07:33:02 +02:00
pre-commit-ci[bot]
a8b4bcdda8 [pre-commit.ci] pre-commit autoupdate (#3638)
updates:
- [github.com/pre-commit/pre-commit-hooks: v4.6.0 → v5.0.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.6.0...v5.0.0)
- [github.com/pre-commit/mirrors-clang-format: v18.1.8 → v19.1.1](https://github.com/pre-commit/mirrors-clang-format/compare/v18.1.8...v19.1.1)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-10-07 14:36:44 -04:00
Dustin J. Mitchell
28628e5dca Add newline in sync error message (#3603) 2024-10-03 18:32:13 -04:00
dependabot[bot]
ff2b1cb888 Bump docker/build-push-action from 6.7.0 to 6.8.0 (#3637)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.7.0 to 6.8.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6.7.0...v6.8.0)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-30 08:59:26 -04:00
dependabot[bot]
cfe92ce845 Bump threeal/gcovr-action from 1.0.0 to 1.1.0 (#3636)
Bumps [threeal/gcovr-action](https://github.com/threeal/gcovr-action) from 1.0.0 to 1.1.0.
- [Release notes](https://github.com/threeal/gcovr-action/releases)
- [Commits](https://github.com/threeal/gcovr-action/compare/v1.0.0...v1.1.0)

---
updated-dependencies:
- dependency-name: threeal/gcovr-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-09-30 08:06:38 -04:00
Dustin J. Mitchell
c95dc9d149 Ignore SIGPIPE (#3627)
This replicates what the Rust runtime does, and matches what Rust code
expects, for example when writing to a socket which is no longer
connected to the remote end.
2024-09-22 18:57:46 -04:00
Gagan Nagaraj
d75ef7f197 Check if end date is being set to a pending task (#3622)
check if end date is being set to a pending task

-throw error if end date is being set to a pending task
- add test for the bug
2024-09-13 12:16:20 -04:00
Gagan Nagaraj
c00c0e941b Throw error when task config write is unsuccessfully (#3620) 2024-09-11 10:20:22 -04:00
Gagan Nagaraj
6a24510473 Exclude attributes starting with tag_ (#3619)
* Exclude attributes starting with tag_

* Check only for tag_*
2024-09-09 08:12:19 -04:00
Tobias Predel
72f9cd91a5 Refine INSTALL file (#3615)
Update INSTALL file

CMake can also abstract the install procedure from the underlying Makefile
2024-09-02 18:22:33 -04:00
Dustin J. Mitchell
44d443a8d6 Update INSTALL file (#3606) 2024-09-02 12:53:50 +00:00
Dustin J. Mitchell
2e3badbf99 Add some instructions to the MSRV (#3604)
There is no easy way to determine the MSRV for TaskChampion, other than
somehow pulling the right version of the source and grepping for it. In
practice, if we update the `taskchampion` dependency to one that has a
higher MSRV, we'll get a build error and find this comment. And if we
get an error building Taskwarrior due to an old MSRV (for example if
something changes on `crates.io`) then we will also find this comment.

This also removes some superfluous dependency versions from the root
workspace. `src/taskchampion-cpp/Cargo.toml` specifies versions
directly.
2024-08-26 21:45:19 -04:00
dependabot[bot]
6cfbb16966 Bump docker/build-push-action from 6.6.1 to 6.7.0 (#3602)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.6.1 to 6.7.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6.6.1...v6.7.0)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-19 09:22:41 -04:00
Dustin J. Mitchell
70632b088e Do not count undo operations in the 'would be reverted..' message (#3598) 2024-08-14 08:35:34 -04:00
dependabot[bot]
d46e5eca58 Bump docker/build-push-action from 6.5.0 to 6.6.1 (#3595)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.5.0 to 6.6.1.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6.5.0...v6.6.1)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-12 08:23:45 -04:00
dependabot[bot]
05da133eb6 Bump sigstore/cosign-installer from 3.5.0 to 3.6.0 (#3594)
Bumps [sigstore/cosign-installer](https://github.com/sigstore/cosign-installer) from 3.5.0 to 3.6.0.
- [Release notes](https://github.com/sigstore/cosign-installer/releases)
- [Commits](https://github.com/sigstore/cosign-installer/compare/v3.5.0...v3.6.0)

---
updated-dependencies:
- dependency-name: sigstore/cosign-installer
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-12 08:22:36 -04:00
Dustin J. Mitchell
c719cce4f1 Run all C++ tests from a single executable (#3582) 2024-08-12 00:20:17 +00:00
Dustin J. Mitchell
4ff63a7960 Use TaskChampion 0.7.0, now via cxx instead of hand-rolled FFI (#3588)
TC 0.7.0 introduces a new `TaskData` type that maps to Taskwarrior's
`Task` type more cleanly. It also introduces the idea of gathering lists
of operations and "committing" them to a replica.

A consequence of this change is that TaskChampion no longer
automatically maintains dependency information, so Taskwarrior must do
so, with its `TDB2::dependency_sync` method. This method does a very
similar thing to what TaskChampion had been doing, so this is a shift of
responsibility but not a major performance difference.

Cxx is .. not great. It is missing a lot of useful things that make a
general-purpose bridge impractical:

 - no support for trait objects
 - no support for `Option<T>` (https://github.com/dtolnay/cxx/issues/87)
 - no support for `Vec<Box<..>>`

As a result, some creativity is required in writing the bridge, for
example returning a `Vec<OptionTaskData>` from `all_task_data` to allow
individual `TaskData` values to be "taken" from the vector.

That said, Cxx is the current state-of-the-art, and does a good job of
ensuring memory safety, at the cost of some slightly awkward APIs.

Subsequent work can remove the "TDB2" layer and allow commands and other
parts of Taskwarrior to interface directly with the `Replica`.
2024-08-11 02:06:00 +00:00
Jan Christian Grünhage
0f96fd31bf Update google-cloud-auth to drop ring@0.16.20 (#3591)
* Update google-cloud-auth to drop ring@0.16.20

ring@0.16.20 doesn't build on ppc and risc-v, and updating
google-cloud-auth pulls in a newer version of jsonwebtoken,
which in turn depends on a newer version of ring that we depend on
already either way.

This necessitated an MSRV bump to 1.73.0
2024-08-09 21:24:12 -04:00
Jan Christian Grünhage
3d30f2ac46 Optionally use system provided corrosion (#3590) 2024-08-09 13:05:19 -04:00
Dustin J. Mitchell
49e09a9783 Remove accidentally-included sqlite3 file (#3589) 2024-08-08 03:11:56 +00:00
Dustin J. Mitchell
17889a3f25 Actually run shell tests (#3583)
Two of these used EXPFAIL which, because nothing is interpreting the TAP
output, does not work. So, that functionality is removed, and the
expected-to-fail bits are commented out or removed.

There was a conditional on the filename in `bash_tap.sh` which caused it
to not actually do anything and just run the test as a simple shell
script. That, too, has been removed.
2024-08-07 00:44:12 +00:00
pre-commit-ci[bot]
c0b708d1f3 [pre-commit.ci] pre-commit autoupdate (#3587)
updates:
- [github.com/psf/black: 24.4.2 → 24.8.0](https://github.com/psf/black/compare/24.4.2...24.8.0)

Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
2024-08-05 14:20:35 -04:00
195 changed files with 4974 additions and 8546 deletions

View File

@@ -1 +0,0 @@
../config.toml

View File

@@ -29,7 +29,10 @@ jobs:
- uses: actions-rs/toolchain@v1 - uses: actions-rs/toolchain@v1
with: with:
toolchain: "1.70.0" # MSRV # If this version is old enough to cause errors, or older than the
# TaskChampion MSRV, bump it to the MSRV of the currently-required
# TaskChampion package; if necessary, bump that version as well.
toolchain: "1.81.0" # MSRV
override: true override: true
- uses: actions-rs/cargo@v1.0.3 - uses: actions-rs/cargo@v1.0.3
@@ -62,38 +65,18 @@ jobs:
command: fmt command: fmt
args: --all -- --check args: --all -- --check
codegen: cargo-metadata:
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: "codegen" name: "Cargo Metadata"
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Cache cargo registry
uses: actions/cache@v4
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo build
uses: actions/cache@v4
with:
path: target
key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
- uses: actions-rs/toolchain@v1 - uses: actions-rs/toolchain@v1
with: with:
toolchain: "1.70.0" # MSRV profile: minimal
components: rustfmt
toolchain: stable
override: true override: true
- uses: actions-rs/cargo@v1.0.3 - name: "Check metadata"
with: run: ".github/workflows/metadata-check.sh"
command: xtask
args: codegen
- name: check for changes
run: |
if ! git diff; then
echo "Generated code not up-to-date;
run `cargo run --package xtask -- codegen` and commit the result";
exit 1;
fi

View File

@@ -23,31 +23,35 @@ jobs:
id-token: write id-token: write
steps: steps:
- name: Create lowercase repository name
run: |
GHCR_REPOSITORY="${{ github.repository_owner }}"
echo "REPOSITORY=${GHCR_REPOSITORY,,}" >> ${GITHUB_ENV}
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
submodules: "recursive" submodules: "recursive"
- name: Install cosign - name: Install cosign
uses: sigstore/cosign-installer@v3.5.0 uses: sigstore/cosign-installer@v3.7.0
- name: Log into registry ${{ env.REGISTRY }} - name: Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@v3.3.0 uses: docker/login-action@v3.3.0
with: with:
registry: ${{ env.REGISTRY }} registry: ${{ env.REGISTRY }}
username: ${{ github.actor }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Taskwarrior Docker image - name: Build and push Taskwarrior Docker image
id: build-and-push id: build-and-push
uses: docker/build-push-action@v6.5.0 uses: docker/build-push-action@v6.10.0
with: with:
context: . context: .
file: "./docker/task.dockerfile" file: "./docker/task.dockerfile"
push: true push: true
tags: ${{ env.REGISTRY }}/${{ github.actor }}/task:${{ github.ref_name }} tags: ${{ env.REGISTRY }}/${{ env.REPOSITORY }}/task:${{ github.ref_name }}
- name: Sign the published Docker image - name: Sign the published Docker image
env: env:
COSIGN_EXPERIMENTAL: "true" COSIGN_EXPERIMENTAL: "true"
run: cosign sign ${{ env.REGISTRY }}/${{ github.actor }}/task@${{ steps.build-and-push.outputs.digest }} run: cosign sign ${{ env.REGISTRY }}/${{ env.REPOSITORY }}/task@${{ steps.build-and-push.outputs.digest }}

32
.github/workflows/metadata-check.sh vendored Executable file
View File

@@ -0,0 +1,32 @@
#! /bin/bash
# Check the 'cargo metadata' for various requirements
set -e
META=$(mktemp)
trap 'rm -rf -- "${META}"' EXIT
cargo metadata --locked --format-version 1 > "${META}"
get_msrv() {
local package="${1}"
jq -r '.packages[] | select(.name == "'"${package}"'") | .rust_version' "${META}"
}
check_msrv() {
local taskchampion_msrv=$(get_msrv taskchampion)
local taskchampion_lib_msrv=$(get_msrv taskchampion-lib)
echo "Found taskchampion MSRV ${taskchampion_msrv}"
echo "Found taskchampion-lib MSRV ${taskchampion_lib_msrv}"
if [ "${taskchampion_msrv}" != "${taskchampion_lib_msrv}" ]; then
echo "Those MSRVs should be the same (or taskchampion-lib should be greater, in which case adjust this script)"
exit 1
else
echo "✓ MSRVs are at the same version."
fi
}
check_msrv

View File

@@ -17,6 +17,9 @@ jobs:
toolchain: "stable" toolchain: "stable"
override: true override: true
- name: Install uuid-dev
run: sudo apt install uuid-dev
- name: make a release tarball and build from it - name: make a release tarball and build from it
run: | run: |
cmake -S. -Bbuild && cmake -S. -Bbuild &&

View File

@@ -17,13 +17,13 @@ jobs:
run: cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_FLAGS=--coverage run: cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_FLAGS=--coverage
- name: Build project - name: Build project
run: cmake --build build --target build_tests run: cmake --build build --target test_runner --target task_executable
- name: Test project - name: Test project
run: ctest --test-dir build -j 8 --output-on-failure run: ctest --test-dir build -j 8 --output-on-failure
- name: Generate a code coverage report - name: Generate a code coverage report
uses: threeal/gcovr-action@v1.0.0 uses: threeal/gcovr-action@v1.1.0
with: with:
coveralls-out: coverage.coveralls.json coveralls-out: coverage.coveralls.json
excludes: | excludes: |
@@ -94,6 +94,38 @@ jobs:
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
cargo-test:
runs-on: ubuntu-latest
name: "Cargo Test"
steps:
- uses: actions/checkout@v4
- name: Cache cargo registry
uses: actions/cache@v4
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo build
uses: actions/cache@v4
with:
path: target
key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
- uses: actions-rs/toolchain@v1
with:
# If this version is old enough to cause errors, or older than the
# TaskChampion MSRV, bump it to the MSRV of the currently-required
# TaskChampion package; if necessary, bump that version as well.
# This should match the MSRV in `src/taskchampion-cpp/Cargo.toml`.
toolchain: "1.81.0" # MSRV
override: true
- uses: actions-rs/cargo@v1.0.3
with:
command: test
tests: tests:
needs: coverage needs: coverage
strategy: strategy:
@@ -103,9 +135,9 @@ jobs:
- name: "Fedora 40" - name: "Fedora 40"
runner: ubuntu-latest runner: ubuntu-latest
dockerfile: fedora40 dockerfile: fedora40
- name: "Fedora 39" - name: "Fedora 41"
runner: ubuntu-latest runner: ubuntu-latest
dockerfile: fedora39 dockerfile: fedora41
- name: "Debian Testing" - name: "Debian Testing"
runner: ubuntu-latest runner: ubuntu-latest
dockerfile: debiantesting dockerfile: debiantesting

4
.gitmodules vendored
View File

@@ -1,6 +1,6 @@
[submodule "src/libshared"] [submodule "src/libshared"]
path = src/libshared path = src/libshared
url = https://github.com/GothenburgBitFactory/libshared.git url = https://github.com/GothenburgBitFactory/libshared.git
[submodule "src/tc/corrosion"] [submodule "src/taskchampion-cpp/corrosion"]
path = src/tc/corrosion path = src/taskchampion-cpp/corrosion
url = https://github.com/corrosion-rs/corrosion.git url = https://github.com/corrosion-rs/corrosion.git

View File

@@ -2,18 +2,18 @@
# See https://pre-commit.com/hooks.html for more hooks # See https://pre-commit.com/hooks.html for more hooks
repos: repos:
- repo: https://github.com/pre-commit/pre-commit-hooks - repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0 rev: v5.0.0
hooks: hooks:
- id: trailing-whitespace - id: trailing-whitespace
- id: end-of-file-fixer - id: end-of-file-fixer
- id: check-yaml - id: check-yaml
- id: check-added-large-files - id: check-added-large-files
- repo: https://github.com/pre-commit/mirrors-clang-format - repo: https://github.com/pre-commit/mirrors-clang-format
rev: v18.1.8 rev: v19.1.6
hooks: hooks:
- id: clang-format - id: clang-format
types_or: [c++, c] types_or: [c++, c]
- repo: https://github.com/psf/black - repo: https://github.com/psf/black
rev: 24.4.2 rev: 24.10.0
hooks: hooks:
- id: black - id: black

View File

@@ -4,7 +4,7 @@ enable_testing()
set (CMAKE_EXPORT_COMPILE_COMMANDS ON) set (CMAKE_EXPORT_COMPILE_COMMANDS ON)
project (task project (task
VERSION 3.1.0 VERSION 3.3.0
DESCRIPTION "Taskwarrior - a command-line TODO list manager" DESCRIPTION "Taskwarrior - a command-line TODO list manager"
HOMEPAGE_URL https://taskwarrior.org/) HOMEPAGE_URL https://taskwarrior.org/)
@@ -26,18 +26,18 @@ if (ENABLE_WASM)
endif (ENABLE_WASM) endif (ENABLE_WASM)
message ("-- Looking for git submodules") message ("-- Looking for git submodules")
if (EXISTS ${CMAKE_SOURCE_DIR}/src/libshared/src AND EXISTS ${CMAKE_SOURCE_DIR}/src/tc/corrosion) if (EXISTS ${CMAKE_SOURCE_DIR}/src/libshared/src AND EXISTS ${CMAKE_SOURCE_DIR}/src/taskchampion-cpp/corrosion)
message ("-- Found git submodules") message ("-- Found git submodules")
else (EXISTS ${CMAKE_SOURCE_DIR}/src/libshared/src) else (EXISTS ${CMAKE_SOURCE_DIR}/src/libshared/src)
message ("-- Cloning git submodules") message ("-- Cloning git submodules")
execute_process (COMMAND git submodule update --init execute_process (COMMAND git submodule update --init
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
endif (EXISTS ${CMAKE_SOURCE_DIR}/src/libshared/src AND EXISTS ${CMAKE_SOURCE_DIR}/src/tc/corrosion) endif (EXISTS ${CMAKE_SOURCE_DIR}/src/libshared/src AND EXISTS ${CMAKE_SOURCE_DIR}/src/taskchampion-cpp/corrosion)
message ("-- Looking for SHA1 references") message ("-- Looking for SHA1 references")
if (EXISTS ${CMAKE_SOURCE_DIR}/.git/index) if (EXISTS ${CMAKE_SOURCE_DIR}/.git/index)
set (HAVE_COMMIT true) set (HAVE_COMMIT true)
execute_process (COMMAND git log -1 --pretty=format:%h execute_process (COMMAND git log -1 --pretty=format:%h --no-show-signature
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
OUTPUT_VARIABLE COMMIT) OUTPUT_VARIABLE COMMIT)
configure_file ( ${CMAKE_SOURCE_DIR}/commit.h.in configure_file ( ${CMAKE_SOURCE_DIR}/commit.h.in
@@ -142,7 +142,7 @@ configure_file (
add_subdirectory (src) add_subdirectory (src)
add_subdirectory (src/commands) add_subdirectory (src/commands)
add_subdirectory (src/tc) add_subdirectory (src/taskchampion-cpp)
add_subdirectory (src/columns) add_subdirectory (src/columns)
add_subdirectory (doc) add_subdirectory (doc)
add_subdirectory (scripts) add_subdirectory (scripts)

2236
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +1,7 @@
[workspace] [workspace]
members = [ members = [
"src/tc/lib", "src/taskchampion-cpp",
"xtask",
] ]
resolver = "2" resolver = "2"
# All Rust dependencies are defined here, and then referenced by the
# Cargo.toml's in the members with `foo.workspace = true`.
[workspace.dependencies]
anyhow = "1.0"
ffizz-header = "0.5"
libc = "0.2.136"
pretty_assertions = "1"
regex = "^1.10.2"
taskchampion = "0.6"

View File

@@ -1,5 +1,48 @@
------ current release --------------------------- ------ current release ---------------------------
- Sync now supports AWS S3 as a backend.
- A new `task import-v2` command allows importing Taskwarrior-2.x
data files directly.
3.3.0 -
Thanks to the following people for contributions to this release:
- Chongyun Lee
- David Tolnay
- Dustin J. Mitchell
- Felix Schurk
- geoffpaulsen
- Kalle Kietäväinen
- Kursat Aktas
- Scott Mcdermott
- Thomas Lauf
------ old releases ------------------------------
3.2.0 -
- Support for the journal in `task info` has been restored (#3671) and the
task info output no longer contains `tag_` values (#3619).
- The `rc.weekstart` value now affects calculation of week numbers in
expressions like `2013-W49` (#3654).
- Build-time flag `ENABLE_TLS_NATIVE_ROOTS` will cause `task sync` to use the
system TLS roots instead of its built-in roots to authenticate the server (#3660).
- The output from `task undo` is now more human-readable. The `undo.style`
configuraiton option, which has had no effect since 3.0.0, is now removed (3672).
- Fetching pending tasks is now more efficient (#3661).
Thanks to the following people for contributions to this release:
- Denis Zh.
- Dustin J. Mitchell
- Fredrik Lanker
- Gagan Nagaraj
- Jan Christian Grünhage
- Scott Mcdermott
- Thomas Lauf
- Tobias Predel
3.1.0 - 3.1.0 -
- Support for `task purge` has been restored, and new support added for automatically - Support for `task purge` has been restored, and new support added for automatically
@@ -35,8 +78,6 @@ Thanks to the following people for contributions to this release:
- Steve Dondley - Steve Dondley
- Will R S Hansen - Will R S Hansen
------ old releases ------------------------------
3.0.2 - 3.0.2 -
- Fix an accidentally-included debug print which polluted output of - Fix an accidentally-included debug print which polluted output of

50
INSTALL
View File

@@ -21,6 +21,8 @@ You will need a C++ compiler that supports full C++17, which includes:
You will need the following libraries: You will need the following libraries:
- libuuid (not needed for OSX) - libuuid (not needed for OSX)
You will need a Rust toolchain of the Minimum Supported Rust Version (MSRV):
- rust 1.81.0
Basic Installation Basic Installation
------------------ ------------------
@@ -29,9 +31,9 @@ Briefly, these shell commands will unpack, build and install Taskwarrior:
$ tar xzf task-X.Y.Z.tar.gz [1] $ tar xzf task-X.Y.Z.tar.gz [1]
$ cd task-X.Y.Z [2] $ cd task-X.Y.Z [2]
$ cmake -DCMAKE_BUILD_TYPE=release . [3] $ cmake -S . -B build -DCMAKE_BUILD_TYPE=Release . [3]
$ make [4] $ cmake --build build [4]
$ sudo make install [5] $ sudo cmake --install build [5]
$ cd .. ; rm -r task-X.Y.Z [6] $ cd .. ; rm -r task-X.Y.Z [6]
These commands are explained below: These commands are explained below:
@@ -87,6 +89,11 @@ get absolute installation directories:
CMAKE_INSTALL_PREFIX/TASK_MAN1DIR /usr/local/share/man/man1 CMAKE_INSTALL_PREFIX/TASK_MAN1DIR /usr/local/share/man/man1
CMAKE_INSTALL_PREFIX/TASK_MAN5DIR /usr/local/share/man/man5 CMAKE_INSTALL_PREFIX/TASK_MAN5DIR /usr/local/share/man/man5
The following variables control aspects of the build process:
SYSTEM_CORROSION - Use system provided corrosion instead of vendored version
ENABLE_TLS_NATIVE_ROOTS - Use the system's TLS root certificates
Uninstallation Uninstallation
-------------- --------------
@@ -108,6 +115,43 @@ If Taskwarrior will not build on your system, first take a look at the Operating
System notes below. If this doesn't help, then go to the Troubleshooting System notes below. If this doesn't help, then go to the Troubleshooting
section, which includes instructions on how to contact us for help. section, which includes instructions on how to contact us for help.
Offline Build Notes
-------------------
It is common for packaging systems (e.g. NixOS, FreeBSD Ports Collection, pkgsrc, etc)
to disable networking during builds. This restriction requires all distribution files
to be prepositioned after checksum verification as a prerequisite for the build. The
following steps have been successful in allowing Taskwarrior to be built in this
environment:
1. Extract all crates in a known location, e.g. ${WRKDIR}/cargo-crates
This includes crates needed for corrosion (search for Cargo.lock files)
2. Create .cargo-checksum.json for each crate
For example:
printf '{"package":"%s","files":{}}' $(sha256 -q ${DISTDIR}/rayon-core-1.12.1.tar.gz) \
> ${WRKDIR}/cargo-crates/rayon-core-1.12.1/.cargo-checksum.json
3. Create a custom config.toml file
For example, ${WRKDIR}/.cargo/config.toml
[source.cargo]
directory = '${WRKDIR}/cargo-crates'
[source.crates-io]
replace-with = 'cargo'
4. After running cmake, configure cargo
For example:
cd ${WRKSRC} && ${SETENV} ${MAKE_ENV} ${CARGO_ENV} \
/usr/local/bin/cargo update \
--manifest-path ${WRKDIR}/.cargo/config.toml \
--verbose
5. Set CARGO_HOME in environment
For example
CARGO_HOME=${WRKDIR}/.cargo
The build and installation steps should be the same as a standard build
at this point.
Operating System Notes Operating System Notes
---------------------- ----------------------

View File

@@ -6,6 +6,7 @@
[![Release](https://img.shields.io/github/v/release/GothenburgBitFactory/taskwarrior)](https://github.com/GothenburgBitFactory/taskwarrior/releases/latest) [![Release](https://img.shields.io/github/v/release/GothenburgBitFactory/taskwarrior)](https://github.com/GothenburgBitFactory/taskwarrior/releases/latest)
[![Release date](https://img.shields.io/github/release-date/GothenburgBitFactory/taskwarrior)](https://github.com/GothenburgBitFactory/taskwarrior/releases/latest) [![Release date](https://img.shields.io/github/release-date/GothenburgBitFactory/taskwarrior)](https://github.com/GothenburgBitFactory/taskwarrior/releases/latest)
[![GitHub Sponsors](https://img.shields.io/github/sponsors/GothenburgBitFactory?color=green)](https://github.com/sponsors/GothenburgBitFactory/) [![GitHub Sponsors](https://img.shields.io/github/sponsors/GothenburgBitFactory?color=green)](https://github.com/sponsors/GothenburgBitFactory/)
[![Gurubase](https://img.shields.io/badge/Gurubase-Ask%20Taskwarrior%20Guru-006BFF)](https://gurubase.io/g/taskwarrior)
</br> </br>
</div> </div>

13
SECURITY.md Normal file
View File

@@ -0,0 +1,13 @@
# Security
To report a vulnerability, please contact Dustin via signal, [`djmitche.78`](https://signal.me/#eu/2T98jpkMAzvFL2wg3OkZnNrfhk1DFfu6eqkMEPqcAuCsLZPVk39A67rp4khmrMNF).
Initial response is expected within ~48h.
We kindly ask to follow the responsible disclosure model and refrain from sharing information until:
1. Vulnerabilities are patched in Taskwarrior + 60 days to coordinate with distributions.
2. 90 days since the vulnerability is disclosed to us.
We recognise the legitimacy of public interest and accept that security researchers can publish information after 90-days deadline unilaterally.
We will assist with obtaining CVE and acknowledge the vulnerabilities reported.

View File

@@ -6,7 +6,8 @@ include (CheckCXXCompilerFlag)
CHECK_CXX_COMPILER_FLAG("-std=c++17" _HAS_CXX17) CHECK_CXX_COMPILER_FLAG("-std=c++17" _HAS_CXX17)
if (_HAS_CXX17) if (_HAS_CXX17)
set (_CXX14_FLAGS "-std=c++17") set (CMAKE_CXX_STANDARD 17)
set (CMAKE_CXX_EXTENSIONS OFF)
else (_HAS_CXX17) else (_HAS_CXX17)
message (FATAL_ERROR "C++17 support missing. Try upgrading your C++ compiler. If you have a good reason for using an outdated compiler, please let us know at support@gothenburgbitfactory.org.") message (FATAL_ERROR "C++17 support missing. Try upgrading your C++ compiler. If you have a good reason for using an outdated compiler, please let us know at support@gothenburgbitfactory.org.")
endif (_HAS_CXX17) endif (_HAS_CXX17)
@@ -32,7 +33,7 @@ elseif (${CMAKE_SYSTEM_NAME} STREQUAL "GNU")
set (GNUHURD true) set (GNUHURD true)
elseif (${CMAKE_SYSTEM_NAME} STREQUAL "CYGWIN") elseif (${CMAKE_SYSTEM_NAME} STREQUAL "CYGWIN")
set (CYGWIN true) set (CYGWIN true)
set (_CXX14_FLAGS "-std=gnu++17") set (CMAKE_CXX_EXTENSIONS ON)
else (${CMAKE_SYSTEM_NAME} MATCHES "Linux") else (${CMAKE_SYSTEM_NAME} MATCHES "Linux")
set (UNKNOWN true) set (UNKNOWN true)
endif (${CMAKE_SYSTEM_NAME} MATCHES "Linux") endif (${CMAKE_SYSTEM_NAME} MATCHES "Linux")

View File

@@ -1,2 +0,0 @@
[alias]
xtask = "run --package xtask --"

View File

@@ -52,9 +52,9 @@ cmake --build build-clang
## Run the Test Suite: ## Run the Test Suite:
For running the test suite [ctest](https://cmake.org/cmake/help/latest/manual/ctest.1.html) is used. For running the test suite [ctest](https://cmake.org/cmake/help/latest/manual/ctest.1.html) is used.
Before one can run the test suite the `task_executable` must be built. Before one can run the test suite the `task_executable` must be built.
After that also the `build_tests` target must be build, which can be done over: After that also the `test_runner` target must be build, which can be done over:
```sh ```sh
cmake --build build --target build_tests cmake --build build --target test_runner
``` ```
Again you may also use the `-j <number-of-jobs>` option for parallel builds. Again you may also use the `-j <number-of-jobs>` option for parallel builds.
@@ -94,3 +94,13 @@ They can be found in the [ctest](https://cmake.org/cmake/help/latest/manual/ctes
Note that any development should be performed using a git clone, and the current development branch. Note that any development should be performed using a git clone, and the current development branch.
The source tarballs do not reflect HEAD, and do not contain the test suite. The source tarballs do not reflect HEAD, and do not contain the test suite.
Follow the [GitHub flow](https://docs.github.com/en/get-started/quickstart/github-flow) for creating a pull request. Follow the [GitHub flow](https://docs.github.com/en/get-started/quickstart/github-flow) for creating a pull request.
## Using a Custom Version of TaskChampion
To build against a different version of Taskchampion, modify the requirement in `src/taskchampion-cpp/Cargo.toml`.
To build from a local checkout, replace the version with a [path dependency](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#specifying-path-dependencies), giving the path to the directory containing TaskChampion's `Cargo.toml`:
```toml
taskchampion = { path = "path/to/taskchampion" }
```

View File

@@ -7,15 +7,12 @@ Taskwarrior has historically been a C++ project, but as of taskwarrior-3.0.0, th
TaskChampion implements storage and access to "replicas" containing a user's tasks. TaskChampion implements storage and access to "replicas" containing a user's tasks.
It defines an abstract model for this data, and also provides a simple Rust API for manipulating replicas. It defines an abstract model for this data, and also provides a simple Rust API for manipulating replicas.
It also defines a method of synchronizing replicas and provides an implementation of that method in the form of a sync server. It also defines a method of synchronizing replicas and provides an implementation of that method in the form of a sync server.
TaskChampion provides a C interface via the `taskchampion-lib` crate, at `src/tc/lib`.
Other applications, besides Taskwarrior, can use TaskChampion to manage tasks. Other applications, besides Taskwarrior, can use TaskChampion to manage tasks.
Taskwarrior is just one application using the TaskChampion interface. Taskwarrior is just one application using the TaskChampion interface.
## Taskwarrior's use of TaskChampion ## Taskwarrior's use of TaskChampion
Taskwarrior's interface to TaskChampion has a few layers: Taskwarrior's interface to TaskChampion is in `src/taskchampion-cpp`.
This links to `taskchampion` as a Rust dependency, and uses [cxx](https://cxx.rs) to build a C++ API for it.
* A Rust library, `takschampion-lib`, that presents `extern "C"` functions for use from C++, essentially defining a C interface to TaskChampion. That API is defined, and documented, in `src/taskchampion-cpp/src/lib.rs`, and available in the `tc` namespace in C++ code.
* C++ wrappers for the types from `taskchampion-lib`, defined in [`src/tc`](../../src/tc), ensuring memory safety (with `unique_ptr`) and adding methods corresponding to the Rust API's methods.
The wrapper types are in the C++ namespace, `tc`.

View File

@@ -278,7 +278,7 @@ The keyword rule shown here as 'keyword.' corresponds to a wildcard pattern,
meaning 'color.keyword.*', or in other words all the keyword rules. meaning 'color.keyword.*', or in other words all the keyword rules.
There is also 'color.project.none', 'color.tag.none' and There is also 'color.project.none', 'color.tag.none' and
'color.uda.priority.none' to specifically represent missing data. \[aq]color.uda.priority.none' to specifically represent missing data.
.SH THEMES .SH THEMES
Taskwarrior supports themes. What this really means is that with the ability to Taskwarrior supports themes. What this really means is that with the ability to

View File

@@ -139,6 +139,83 @@ Then configure Taskwarrior with:
$ task config sync.gcp.credential_path <absolute-path-to-downloaded-credentials> $ task config sync.gcp.credential_path <absolute-path-to-downloaded-credentials>
.fi .fi
.SS Amazon Web Services
To synchronize your tasks to AWS, select a region near you and use the AWS
console to create a new S3 bucket. The default settings for the bucket are
adequate.
You will also need an AWS IAM user with the following policy, where BUCKETNAME
is the name of the bucket. The same user can be configured for multiple
Taskwarrior clients.
.nf
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "TaskChampion",
"Effect": "Allow",
"Action": [
"s3:PutObject",
"s3:GetObject",
"s3:ListBucket",
"s3:DeleteObject"
],
"Resource": [
"arn:aws:s3:::BUCKETNAME",
"arn:aws:s3:::BUCKETNAME/*"
]
}
]
}
.fi
To create such a user, create a new policy in the IAM console, select the JSON
option in the policy editor, and paste the policy. Click "Next" and give the
policy a name such as "TaskwarriorSync". Next, create a new user, with a name
of your choosing, select "Attach Policies Directly", and then choose the
newly-created policy.
You will need access keys configured for the new user. Find the user in the
user list, open the "Security Credentials" tab, then click "Create access key"
and follow the steps.
At this point, you can choose how to provide those credentials to Taskwarrior.
The simplest is to include them in the Taskwarrior configuration:
.nf
$ task config sync.aws.region <region>
$ task config sync.aws.bucket <bucket-name>
$ task config sync.aws.access_key_id <access-key-id>
$ task config sync.aws.secret_access_key <secret-access-key>
.fi
Alternatively, you can set up an AWS CLI profile, using a profile name of your
choosing such as "taskwarrior-creds":
.nf
$ aws configure --profile taskwarrior-creds
.fi
Enter the access key ID and secret access key. The default region and format
are not important. Then configure Taskwarrior with:
.nf
$ task config sync.aws.region <region>
$ task config sync.aws.bucket <bucket-name>
$ task config sync.aws.profile taskwarrior-creds
.fi
To use AWS's default credential sources, such as environment variables, the
default profile, or an instance profile, set
.nf
$ task config sync.aws.region <region>
$ task config sync.aws.bucket <bucket-name>
$ task config sync.aws.default_credentials true
.fi
.SS Local Synchronization .SS Local Synchronization
In order to take advantage of synchronization's side effect of saving disk In order to take advantage of synchronization's side effect of saving disk

View File

@@ -414,6 +414,11 @@ few example scripts, such as:
import-yaml.pl import-yaml.pl
.fi .fi
.TP
.B task import-v2
Imports tasks from the Taskwarrior v2.x format. This is used when upgrading from
version 2.x to version 3.x.
.TP .TP
.B task log <mods> .B task log <mods>
Adds a new task that is already completed, to the task list. It is affected by Adds a new task that is already completed, to the task list. It is affected by

View File

@@ -508,15 +508,6 @@ weekly recurring task is added with a due date of tomorrow, and recurrence.limit
is set to 2, then a report will list 2 pending recurring tasks, one for tomorrow, is set to 2, then a report will list 2 pending recurring tasks, one for tomorrow,
and one for a week from tomorrow. and one for a week from tomorrow.
.TP
.B undo.style=side
When the 'undo' command is run, Taskwarrior presents a before and after
comparison of the data. This can be in either the 'side' style, which compares
values side-by-side in a table, or 'diff' style, which uses a format similar to
the 'diff' command.
Currently not supported.
.TP .TP
.B abbreviation.minimum=2 .B abbreviation.minimum=2
Minimum length of any abbreviated command/value. This means that "ve", "ver", Minimum length of any abbreviated command/value. This means that "ve", "ver",

View File

@@ -84,6 +84,7 @@ color.calendar.due=color0 on rgb325
color.calendar.due.today=color0 on rgb404 color.calendar.due.today=color0 on rgb404
color.calendar.holiday=color15 on rgb102 color.calendar.holiday=color15 on rgb102
color.calendar.overdue=color0 on color5 color.calendar.overdue=color0 on color5
color.calendar.scheduled=
color.calendar.today=color15 on rgb103 color.calendar.today=color15 on rgb103
color.calendar.weekend=gray12 on gray3 color.calendar.weekend=gray12 on gray3
color.calendar.weeknumber=rgb104 color.calendar.weeknumber=rgb104

View File

@@ -86,6 +86,7 @@ color.calendar.due=white on red
color.calendar.due.today=bold white on red color.calendar.due.today=bold white on red
color.calendar.holiday=black on bright yellow color.calendar.holiday=black on bright yellow
color.calendar.overdue=black on bright red color.calendar.overdue=black on bright red
color.calendar.scheduled=
color.calendar.today=bold white on bright blue color.calendar.today=bold white on bright blue
color.calendar.weekend=white on bright black color.calendar.weekend=white on bright black
color.calendar.weeknumber=bold blue color.calendar.weeknumber=bold blue

View File

@@ -82,6 +82,7 @@ color.summary.bar=black on rgb141
color.calendar.due.today=color15 on color1 color.calendar.due.today=color15 on color1
color.calendar.due=color0 on color1 color.calendar.due=color0 on color1
color.calendar.holiday=color0 on color11 color.calendar.holiday=color0 on color11
color.calendar.scheduled=
color.calendar.overdue=color0 on color9 color.calendar.overdue=color0 on color9
color.calendar.today=color15 on rgb013 color.calendar.today=color15 on rgb013
color.calendar.weekend=on color235 color.calendar.weekend=on color235

View File

@@ -83,6 +83,7 @@ color.calendar.due.today=color0 on color252
color.calendar.due=color0 on color249 color.calendar.due=color0 on color249
color.calendar.holiday=color255 on rgb013 color.calendar.holiday=color255 on rgb013
color.calendar.overdue=color0 on color255 color.calendar.overdue=color0 on color255
color.calendar.scheduled=
color.calendar.today=color0 on rgb115 color.calendar.today=color0 on rgb115
color.calendar.weekend=on color235 color.calendar.weekend=on color235
color.calendar.weeknumber=rgb015 color.calendar.weeknumber=rgb015

View File

@@ -83,6 +83,7 @@ color.calendar.due=on gray8
color.calendar.due.today=black on gray15 color.calendar.due.today=black on gray15
color.calendar.holiday=black on gray20 color.calendar.holiday=black on gray20
color.calendar.overdue=gray2 on gray10 color.calendar.overdue=gray2 on gray10
color.calendar.scheduled=
color.calendar.today=bold white color.calendar.today=bold white
color.calendar.weekend=on gray2 color.calendar.weekend=on gray2
color.calendar.weeknumber=gray6 color.calendar.weeknumber=gray6

View File

@@ -83,6 +83,7 @@ color.calendar.due=color0 on gray10
color.calendar.due.today=color0 on gray15 color.calendar.due.today=color0 on gray15
color.calendar.holiday=color15 on rgb005 color.calendar.holiday=color15 on rgb005
color.calendar.overdue=color0 on gray20 color.calendar.overdue=color0 on gray20
color.calendar.scheduled=
color.calendar.today=underline black on color15 color.calendar.today=underline black on color15
color.calendar.weekend=on gray4 color.calendar.weekend=on gray4
color.calendar.weeknumber=gray10 color.calendar.weeknumber=gray10

View File

@@ -83,6 +83,7 @@ color.calendar.due.today=color0 on color225
color.calendar.due=color0 on color249 color.calendar.due=color0 on color249
color.calendar.holiday=rgb151 on rgb020 color.calendar.holiday=rgb151 on rgb020
color.calendar.overdue=color0 on color255 color.calendar.overdue=color0 on color255
color.calendar.scheduled=
color.calendar.today=color0 on rgb151 color.calendar.today=color0 on rgb151
color.calendar.weekend=on color235 color.calendar.weekend=on color235
color.calendar.weeknumber=rgb010 color.calendar.weeknumber=rgb010

View File

@@ -83,6 +83,7 @@ color.calendar.due.today=color0 on color252
color.calendar.due=color0 on color249 color.calendar.due=color0 on color249
color.calendar.holiday=rgb522 on rgb300 color.calendar.holiday=rgb522 on rgb300
color.calendar.overdue=color0 on color255 color.calendar.overdue=color0 on color255
color.calendar.scheduled=
color.calendar.today=color0 on rgb511 color.calendar.today=color0 on rgb511
color.calendar.weekend=on color235 color.calendar.weekend=on color235
color.calendar.weeknumber=rgb100 color.calendar.weeknumber=rgb100

View File

@@ -83,6 +83,7 @@ color.calendar.due=color0 on rgb325
color.calendar.due.today=color0 on rgb404 color.calendar.due.today=color0 on rgb404
color.calendar.holiday=color15 on rgb102 color.calendar.holiday=color15 on rgb102
color.calendar.overdue=color0 on color5 color.calendar.overdue=color0 on color5
color.calendar.scheduled=
color.calendar.today=color15 on rgb103 color.calendar.today=color15 on rgb103
color.calendar.weekend=gray12 on gray3 color.calendar.weekend=gray12 on gray3
color.calendar.weeknumber=rgb104 color.calendar.weeknumber=rgb104

View File

@@ -83,6 +83,7 @@ color.calendar.due=color0 on rgb440
color.calendar.due.today=color0 on rgb430 color.calendar.due.today=color0 on rgb430
color.calendar.holiday=rgb151 on rgb020 color.calendar.holiday=rgb151 on rgb020
color.calendar.overdue=color0 on rgb420 color.calendar.overdue=color0 on rgb420
color.calendar.scheduled=
color.calendar.today=color15 on rgb110 color.calendar.today=color15 on rgb110
color.calendar.weekend=on color235 color.calendar.weekend=on color235
color.calendar.weeknumber=rgb110 color.calendar.weeknumber=rgb110

View File

@@ -83,6 +83,7 @@ color.calendar.due=on bright green
color.calendar.due.today=blue on bright yellow color.calendar.due.today=blue on bright yellow
color.calendar.holiday=on yellow color.calendar.holiday=on yellow
color.calendar.overdue=on bright red color.calendar.overdue=on bright red
color.calendar.scheduled=
color.calendar.today=blue color.calendar.today=blue
color.calendar.weekend=on white color.calendar.weekend=on white
color.calendar.weeknumber=blue color.calendar.weeknumber=blue

View File

@@ -83,6 +83,7 @@ color.calendar.due=on rgb343
color.calendar.due.today=on rgb353 color.calendar.due.today=on rgb353
color.calendar.holiday=color0 on rgb530 color.calendar.holiday=color0 on rgb530
color.calendar.overdue=on rgb533 color.calendar.overdue=on rgb533
color.calendar.scheduled=
color.calendar.today=rgb005 color.calendar.today=rgb005
color.calendar.weekend=on gray21 color.calendar.weekend=on gray21
color.calendar.weeknumber=gray16 color.calendar.weeknumber=gray16

View File

@@ -86,6 +86,7 @@ color.calendar.due=
color.calendar.due.today= color.calendar.due.today=
color.calendar.holiday= color.calendar.holiday=
color.calendar.overdue= color.calendar.overdue=
color.calendar.scheduled=
color.calendar.today= color.calendar.today=
color.calendar.weekend= color.calendar.weekend=
color.calendar.weeknumber= color.calendar.weeknumber=

View File

@@ -100,6 +100,7 @@ color.calendar.due=color0 on color9
color.calendar.due.today=color0 on color1 color.calendar.due.today=color0 on color1
color.calendar.holiday=color0 on color3 color.calendar.holiday=color0 on color3
color.calendar.overdue=color0 on color5 color.calendar.overdue=color0 on color5
color.calendar.scheduled=
color.calendar.today=color0 on color4 color.calendar.today=color0 on color4
color.calendar.weekend=on color0 color.calendar.weekend=on color0
color.calendar.weeknumber=color4 color.calendar.weeknumber=color4

View File

@@ -100,6 +100,7 @@ color.calendar.due=color7 on color9
color.calendar.due.today=color7 on color1 color.calendar.due.today=color7 on color1
color.calendar.holiday=color7 on color3 color.calendar.holiday=color7 on color3
color.calendar.overdue=color7 on color5 color.calendar.overdue=color7 on color5
color.calendar.scheduled=
color.calendar.today=color7 on color4 color.calendar.today=color7 on color4
color.calendar.weekend=on color7 color.calendar.weekend=on color7
color.calendar.weeknumber=color14 color.calendar.weeknumber=color14

View File

@@ -8,10 +8,10 @@ services:
security_opt: security_opt:
- label=type:container_runtime_t - label=type:container_runtime_t
tty: true tty: true
test-fedora39: test-fedora41:
build: build:
context: . context: .
dockerfile: test/docker/fedora39 dockerfile: test/docker/fedora41
network_mode: "host" network_mode: "host"
security_opt: security_opt:
- label=type:container_runtime_t - label=type:container_runtime_t

View File

@@ -168,7 +168,6 @@ syn match taskrcGoodKey '^\s*\Vsugar='he=e-1
syn match taskrcGoodKey '^\s*\Vsync.\(server.\(url\|origin\|client_id\|encryption_secret\)\|local.server_dir\)='he=e-1 syn match taskrcGoodKey '^\s*\Vsync.\(server.\(url\|origin\|client_id\|encryption_secret\)\|local.server_dir\)='he=e-1
syn match taskrcGoodKey '^\s*\Vtag.indicator='he=e-1 syn match taskrcGoodKey '^\s*\Vtag.indicator='he=e-1
syn match taskrcGoodKey '^\s*\Vuda.\S\{-}.\(default\|type\|label\|values\|indicator\)='he=e-1 syn match taskrcGoodKey '^\s*\Vuda.\S\{-}.\(default\|type\|label\|values\|indicator\)='he=e-1
syn match taskrcGoodKey '^\s*\Vundo.style='he=e-1
syn match taskrcGoodKey '^\s*\Vurgency.active.coefficient='he=e-1 syn match taskrcGoodKey '^\s*\Vurgency.active.coefficient='he=e-1
syn match taskrcGoodKey '^\s*\Vurgency.age.coefficient='he=e-1 syn match taskrcGoodKey '^\s*\Vurgency.age.coefficient='he=e-1
syn match taskrcGoodKey '^\s*\Vurgency.age.max='he=e-1 syn match taskrcGoodKey '^\s*\Vurgency.age.max='he=e-1

View File

@@ -1,8 +1,6 @@
cmake_minimum_required (VERSION 3.22) cmake_minimum_required (VERSION 3.22)
include_directories (${CMAKE_SOURCE_DIR} include_directories (${CMAKE_SOURCE_DIR}
${CMAKE_SOURCE_DIR}/src ${CMAKE_SOURCE_DIR}/src
${CMAKE_SOURCE_DIR}/src/tc
${CMAKE_SOURCE_DIR}/src/tc/lib
${CMAKE_SOURCE_DIR}/src/commands ${CMAKE_SOURCE_DIR}/src/commands
${CMAKE_SOURCE_DIR}/src/columns ${CMAKE_SOURCE_DIR}/src/columns
${CMAKE_SOURCE_DIR}/src/libshared/src ${CMAKE_SOURCE_DIR}/src/libshared/src
@@ -15,6 +13,8 @@ add_library (task STATIC CLI2.cpp CLI2.h
Filter.cpp Filter.h Filter.cpp Filter.h
Hooks.cpp Hooks.h Hooks.cpp Hooks.h
Lexer.cpp Lexer.h Lexer.cpp Lexer.h
Operation.cpp Operation.h
TF2.cpp TF2.h
TDB2.cpp TDB2.h TDB2.cpp TDB2.h
Task.cpp Task.h Task.cpp Task.h
Variant.cpp Variant.h Variant.cpp Variant.h
@@ -28,6 +28,7 @@ add_library (task STATIC CLI2.cpp CLI2.h
rules.cpp rules.cpp
sort.cpp sort.cpp
util.cpp util.h) util.cpp util.h)
target_link_libraries(task taskchampion-cpp)
add_library (libshared STATIC libshared/src/Color.cpp libshared/src/Color.h add_library (libshared STATIC libshared/src/Color.cpp libshared/src/Color.h
libshared/src/Configuration.cpp libshared/src/Configuration.h libshared/src/Configuration.cpp libshared/src/Configuration.h
@@ -52,10 +53,9 @@ add_executable (calc_executable calc.cpp)
add_executable (lex_executable lex.cpp) add_executable (lex_executable lex.cpp)
# Yes, 'task' (and hence libshared) is included twice, otherwise linking fails on assorted OSes. # Yes, 'task' (and hence libshared) is included twice, otherwise linking fails on assorted OSes.
# Similarly for `tc`. target_link_libraries (task_executable task commands columns libshared task libshared ${TASK_LIBRARIES})
target_link_libraries (task_executable task tc commands tc columns libshared task libshared ${TASK_LIBRARIES}) target_link_libraries (calc_executable task commands columns libshared task libshared ${TASK_LIBRARIES})
target_link_libraries (calc_executable task tc commands tc columns libshared task libshared ${TASK_LIBRARIES}) target_link_libraries (lex_executable task commands columns libshared task libshared ${TASK_LIBRARIES})
target_link_libraries (lex_executable task tc commands tc columns libshared task libshared ${TASK_LIBRARIES})
if (DARWIN) if (DARWIN)
# SystemConfiguration is required by Rust libraries like reqwest, to get proxy configuration. # SystemConfiguration is required by Rust libraries like reqwest, to get proxy configuration.
target_link_libraries (task_executable "-framework CoreFoundation -framework Security -framework SystemConfiguration") target_link_libraries (task_executable "-framework CoreFoundation -framework Security -framework SystemConfiguration")

View File

@@ -37,9 +37,11 @@
#include <assert.h> #include <assert.h>
#include <format.h> #include <format.h>
#include <main.h> #include <main.h>
#include <rust/cxx.h>
#include <shared.h> #include <shared.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <taskchampion-cpp/lib.h>
#include <unistd.h> #include <unistd.h>
#include <algorithm> #include <algorithm>
@@ -125,7 +127,6 @@ std::string configurationDefaults =
"dependency.indicator=D # What to show as a dependency indicator\n" "dependency.indicator=D # What to show as a dependency indicator\n"
"recurrence.indicator=R # What to show as a task recurrence indicator\n" "recurrence.indicator=R # What to show as a task recurrence indicator\n"
"recurrence.limit=1 # Number of future recurring pending tasks\n" "recurrence.limit=1 # Number of future recurring pending tasks\n"
"undo.style=side # Undo style - can be 'side', or 'diff'\n"
"regex=1 # Assume all search/filter strings are " "regex=1 # Assume all search/filter strings are "
"regexes\n" "regexes\n"
"xterm.title=0 # Sets xterm title for some commands\n" "xterm.title=0 # Sets xterm title for some commands\n"
@@ -317,6 +318,12 @@ std::string configurationDefaults =
"#sync.server.client_id # Client ID for sync to a server\n" "#sync.server.client_id # Client ID for sync to a server\n"
"#sync.server.url # URL of the sync server\n" "#sync.server.url # URL of the sync server\n"
"#sync.local.server_dir # Directory for local sync\n" "#sync.local.server_dir # Directory for local sync\n"
"#sync.aws.region # region for AWS sync\n"
"#sync.aws.bucket # bucket for AWS sync\n"
"#sync.aws.access_key_id # access_key_id for AWS sync\n"
"#sync.aws.secret_access_key # secret_access_key for AWS sync\n"
"#sync.aws.profile # profile name for AWS sync\n"
"#sync.aws.default_credentials # use default credentials for AWS sync\n"
"#sync.gcp.credential_path # Path to JSON file containing credentials to " "#sync.gcp.credential_path # Path to JSON file containing credentials to "
"authenticate GCP Sync\n" "authenticate GCP Sync\n"
"#sync.gcp.bucket # Bucket for sync to GCP\n" "#sync.gcp.bucket # Bucket for sync to GCP\n"
@@ -681,6 +688,11 @@ int Context::initialize(int argc, const char** argv) {
rc = 2; rc = 2;
} }
catch (rust::Error& err) {
error(err.what());
rc = 2;
}
catch (int) { catch (int) {
// Hooks can terminate processing by throwing integers. // Hooks can terminate processing by throwing integers.
rc = 4; rc = 4;
@@ -689,7 +701,7 @@ int Context::initialize(int argc, const char** argv) {
catch (const std::regex_error& e) { catch (const std::regex_error& e) {
std::cout << "regex_error caught: " << e.what() << '\n'; std::cout << "regex_error caught: " << e.what() << '\n';
} catch (...) { } catch (...) {
error("knknown error. Please report."); error("Unknown error. Please report.");
rc = 3; rc = 3;
} }
@@ -772,6 +784,11 @@ int Context::run() {
rc = 2; rc = 2;
} }
catch (rust::Error& err) {
error(err.what());
rc = 2;
}
catch (int) { catch (int) {
// Hooks can terminate processing by throwing integers. // Hooks can terminate processing by throwing integers.
rc = 4; rc = 4;
@@ -1093,6 +1110,11 @@ void Context::staticInitialization() {
Task::regex = Variant::searchUsingRegex = config.getBoolean("regex"); Task::regex = Variant::searchUsingRegex = config.getBoolean("regex");
Lexer::dateFormat = Variant::dateFormat = config.get("dateformat"); Lexer::dateFormat = Variant::dateFormat = config.get("dateformat");
auto weekStart = Datetime::dayOfWeek(config.get("weekstart"));
if (weekStart != 0 && weekStart != 1)
throw std::string(
"The 'weekstart' configuration variable may only contain 'Sunday' or 'Monday'.");
Datetime::weekstart = weekStart;
Datetime::isoEnabled = config.getBoolean("date.iso"); Datetime::isoEnabled = config.getBoolean("date.iso");
Datetime::standaloneDateEnabled = false; Datetime::standaloneDateEnabled = false;
Datetime::standaloneTimeEnabled = false; Datetime::standaloneTimeEnabled = false;
@@ -1155,6 +1177,7 @@ void Context::createDefaultConfig() {
<< "\n# Color theme (uncomment one to use)\n" << "\n# Color theme (uncomment one to use)\n"
<< "#include light-16.theme\n" << "#include light-16.theme\n"
<< "#include light-256.theme\n" << "#include light-256.theme\n"
<< "#include bubblegum-256.theme\n"
<< "#include dark-16.theme\n" << "#include dark-16.theme\n"
<< "#include dark-256.theme\n" << "#include dark-256.theme\n"
<< "#include dark-red-256.theme\n" << "#include dark-red-256.theme\n"

View File

@@ -1,6 +1,6 @@
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
// //
// Copyright 2022, Dustin J. Mitchell // Copyright 2006 - 2024, Tomas Babej, Paul Beckingham, Federico Hernandez.
// //
// Permission is hereby granted, free of charge, to any person obtaining a copy // Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal // of this software and associated documentation files (the "Software"), to deal
@@ -27,53 +27,47 @@
#include <cmake.h> #include <cmake.h>
// cmake.h include header must come first // cmake.h include header must come first
#include <assert.h> #include <Operation.h>
#include <format.h> #include <taskchampion-cpp/lib.h>
#include "tc/Replica.h" #include <vector>
#include "tc/Task.h"
using namespace tc::ffi;
namespace tc {
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
TCString string2tc(const std::string& str) { Operation::Operation(const tc::Operation& op) : op(&op) {}
return tc_string_clone_with_len(str.data(), str.size());
////////////////////////////////////////////////////////////////////////////////
std::vector<Operation> Operation::operations(const rust::Vec<tc::Operation>& operations) {
return {operations.begin(), operations.end()};
} }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
std::string tc2string_clone(const TCString& str) { Operation& Operation::operator=(const Operation& other) {
size_t len; op = other.op;
auto ptr = tc_string_content_with_len(&str, &len); return *this;
auto rv = std::string(ptr, len);
return rv;
} }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
std::string tc2string(TCString& str) { bool Operation::operator<(const Operation& other) const {
auto rv = tc2string_clone(str); if (is_create()) {
tc_string_free(&str); return !other.is_create();
return rv; } else if (is_update()) {
if (other.is_create()) {
return false;
} else if (other.is_update()) {
return get_timestamp() < other.get_timestamp();
} else {
return true;
}
} else if (is_delete()) {
if (other.is_create() || other.is_update() || other.is_delete()) {
return false;
} else {
return true;
}
} else if (is_undo_point()) {
return !other.is_undo_point();
}
return false; // not reachable
} }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
TCUuid uuid2tc(const std::string& str) {
TCString tcstr = tc_string_borrow(str.c_str());
TCUuid rv;
if (TC_RESULT_OK != tc_uuid_from_str(tcstr, &rv)) {
throw std::string("invalid UUID");
}
return rv;
}
////////////////////////////////////////////////////////////////////////////////
std::string tc2uuid(TCUuid& uuid) {
char s[TC_UUID_STRING_BYTES];
tc_uuid_to_buf(uuid, s);
std::string str;
str.assign(s, TC_UUID_STRING_BYTES);
return str;
}
////////////////////////////////////////////////////////////////////////////////
} // namespace tc

88
src/Operation.h Normal file
View File

@@ -0,0 +1,88 @@
////////////////////////////////////////////////////////////////////////////////
//
// Copyright 2006 - 2024, Tomas Babej, Paul Beckingham, Federico Hernandez.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// https://www.opensource.org/licenses/mit-license.php
//
////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDED_OPERATIOn
#define INCLUDED_OPERATIOn
#include <taskchampion-cpp/lib.h>
#include <optional>
#include <vector>
// Representation of a TaskChampion operation.
//
// This class wraps `tc::Operation&` and thus cannot outlive that underlying
// type.
class Operation {
public:
explicit Operation(const tc::Operation &);
Operation(const Operation &other) = default;
Operation &operator=(const Operation &other);
// Create a vector of Operations given the result of `Replica::get_undo_operations` or
// `Replica::get_task_operations`. The resulting vector must not outlive the input `rust::Vec`.
static std::vector<Operation> operations(const rust::Vec<tc::Operation> &);
// Methods from the underlying `tc::Operation`.
bool is_create() const { return op->is_create(); }
bool is_update() const { return op->is_update(); }
bool is_delete() const { return op->is_delete(); }
bool is_undo_point() const { return op->is_undo_point(); }
std::string get_uuid() const { return std::string(op->get_uuid().to_string()); }
::rust::Vec<::tc::PropValuePair> get_old_task() const { return op->get_old_task(); };
std::string get_property() const {
std::string value;
op->get_property(value);
return value;
}
std::optional<std::string> get_value() const {
std::optional<std::string> value{std::string()};
if (!op->get_value(value.value())) {
value = std::nullopt;
}
return value;
}
std::optional<std::string> get_old_value() const {
std::optional<std::string> old_value{std::string()};
if (!op->get_old_value(old_value.value())) {
old_value = std::nullopt;
}
return old_value;
}
time_t get_timestamp() const { return static_cast<time_t>(op->get_timestamp()); }
// Define a partial order on Operations:
// - Create < Update < Delete < UndoPoint
// - Given two updates, sort by timestamp
bool operator<(const Operation &other) const;
private:
const tc::Operation *op;
};
#endif
////////////////////////////////////////////////////////////////////////////////

View File

@@ -46,79 +46,55 @@
#include <unordered_set> #include <unordered_set>
#include <vector> #include <vector>
#include "tc/Server.h"
#include "tc/util.h"
bool TDB2::debug_mode = false; bool TDB2::debug_mode = false;
static void dependency_scan(std::vector<Task>&); static void dependency_scan(std::vector<Task>&);
////////////////////////////////////////////////////////////////////////////////
TDB2::TDB2()
: replica{tc::Replica()} // in-memory Replica
,
_working_set{} {}
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
void TDB2::open_replica(const std::string& location, bool create_if_missing) { void TDB2::open_replica(const std::string& location, bool create_if_missing) {
replica = tc::Replica(location, create_if_missing); _replica = tc::new_replica_on_disk(location, create_if_missing);
} }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
// Add the new task to the replica. // Add the new task to the replica.
void TDB2::add(Task& task) { void TDB2::add(Task& task) {
// Validate a task for addition. This is stricter than `task.validate`, as any
// inconsistency is probably user error.
task.validate_add();
// Ensure the task is consistent, and provide defaults if necessary. // Ensure the task is consistent, and provide defaults if necessary.
// bool argument to validate() is "applyDefault", to apply default values for // bool argument to validate() is "applyDefault", to apply default values for
// properties not otherwise given. // properties not otherwise given.
task.validate(true); task.validate(true);
std::string uuid = task.get("uuid"); rust::Vec<tc::Operation> ops;
maybe_add_undo_point(ops);
auto uuid = task.get("uuid");
changes[uuid] = task; changes[uuid] = task;
tc::Uuid tcuuid = tc::uuid_from_string(uuid);
// run hooks for this new task // run hooks for this new task
Context::getContext().hooks.onAdd(task); Context::getContext().hooks.onAdd(task);
auto innertask = replica.import_task_with_uuid(uuid); auto taskdata = tc::create_task(tcuuid, ops);
{
auto guard = replica.mutate_task(innertask);
// add the task attributes // add the task attributes
for (auto& attr : task.all()) { for (auto& attr : task.all()) {
// TaskChampion does not store uuid or id in the taskmap // TaskChampion does not store uuid or id in the task data
if (attr == "uuid" || attr == "id") { if (attr == "uuid" || attr == "id") {
continue; continue;
} }
// Use `set_status` for the task status, to get expected behavior taskdata->update(attr, task.get(attr), ops);
// with respect to the working set.
else if (attr == "status") {
innertask.set_status(Task::status2tc(Task::textToStatus(task.get(attr))));
} }
replica()->commit_operations(std::move(ops));
// use `set_modified` to set the modified timestamp, avoiding automatic invalidate_cached_info();
// updates to this field by TaskChampion.
else if (attr == "modified") {
auto mod = (time_t)std::stoi(task.get(attr));
innertask.set_modified(mod);
}
// otherwise, just set the k/v map value
else {
innertask.set_value(attr, std::make_optional(task.get(attr)));
}
}
}
auto ws = replica.working_set();
// get the ID that was assigned to this task // get the ID that was assigned to this task
auto id = ws.by_uuid(uuid); auto id = working_set()->by_uuid(tcuuid);
if (id > 0) {
// update the cached working set with the new information task.id = id;
_working_set = std::make_optional(std::move(ws));
if (id.has_value()) {
task.id = id.value();
} }
} }
@@ -144,6 +120,9 @@ void TDB2::modify(Task& task) {
task.validate(false); task.validate(false);
auto uuid = task.get("uuid"); auto uuid = task.get("uuid");
rust::Vec<tc::Operation> ops;
maybe_add_undo_point(ops);
changes[uuid] = task; changes[uuid] = task;
// invoke the hook and allow it to modify the task before updating // invoke the hook and allow it to modify the task before updating
@@ -151,14 +130,15 @@ void TDB2::modify(Task& task) {
get(uuid, original); get(uuid, original);
Context::getContext().hooks.onModify(original, task); Context::getContext().hooks.onModify(original, task);
auto maybe_tctask = replica.get_task(uuid); tc::Uuid tcuuid = tc::uuid_from_string(uuid);
if (!maybe_tctask.has_value()) { auto maybe_tctask = replica()->get_task_data(tcuuid);
if (maybe_tctask.is_none()) {
throw std::string("task no longer exists"); throw std::string("task no longer exists");
} }
auto tctask = std::move(maybe_tctask.value()); auto tctask = maybe_tctask.take();
auto guard = replica.mutate_task(tctask);
auto tctask_map = tctask.get_taskmap();
// Perform the necessary `update` operations to set all keys in `tctask`
// equal to those in `task`.
std::unordered_set<std::string> seen; std::unordered_set<std::string> seen;
for (auto k : task.all()) { for (auto k : task.all()) {
// ignore task keys that aren't stored // ignore task keys that aren't stored
@@ -168,45 +148,76 @@ void TDB2::modify(Task& task) {
seen.insert(k); seen.insert(k);
bool update = false; bool update = false;
auto v_new = task.get(k); auto v_new = task.get(k);
try { std::string v_tctask;
auto v_tctask = tctask_map.at(k); if (tctask->get(k, v_tctask)) {
update = v_tctask != v_new; update = v_tctask != v_new;
} catch (const std::out_of_range& oor) { } else {
// tctask_map does not contain k, so update it // tctask does not contain k, so update it
update = true; update = true;
} }
if (update) { if (update) {
// An empty string indicates the value should be removed. // An empty string indicates the value should be removed.
if (v_new == "") { if (v_new == "") {
tctask.set_value(k, {}); tctask->update_remove(k, ops);
} else { } else {
tctask.set_value(k, make_optional(v_new)); tctask->update(k, v_new, ops);
} }
} }
} }
// we've now added and updated properties; but must find any deleted properties // we've now added and updated properties; but must find any deleted properties
for (auto kv : tctask_map) { for (auto k : tctask->properties()) {
if (seen.find(kv.first) == seen.end()) { auto kstr = static_cast<std::string>(k);
tctask.set_value(kv.first, {}); if (seen.find(kstr) == seen.end()) {
tctask->update_remove(kstr, ops);
} }
} }
replica()->commit_operations(std::move(ops));
invalidate_cached_info();
} }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
void TDB2::purge(Task& task) { void TDB2::purge(Task& task) {
auto uuid = task.get("uuid"); auto uuid = tc::uuid_from_string(task.get("uuid"));
replica.delete_task(uuid); rust::Vec<tc::Operation> ops;
auto maybe_tctask = replica()->get_task_data(uuid);
if (maybe_tctask.is_some()) {
auto tctask = maybe_tctask.take();
tctask->delete_task(ops);
replica()->commit_operations(std::move(ops));
}
invalidate_cached_info();
} }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
const tc::WorkingSet& TDB2::working_set() { rust::Box<tc::Replica>& TDB2::replica() {
// Create a replica in-memory if `open_replica` has not been called. This
// occurs in tests.
if (!_replica) {
_replica = tc::new_replica_in_memory();
}
return _replica.value();
}
////////////////////////////////////////////////////////////////////////////////
const rust::Box<tc::WorkingSet>& TDB2::working_set() {
if (!_working_set.has_value()) { if (!_working_set.has_value()) {
_working_set = std::make_optional(replica.working_set()); _working_set = replica()->working_set();
} }
return _working_set.value(); return _working_set.value();
} }
////////////////////////////////////////////////////////////////////////////////
void TDB2::maybe_add_undo_point(rust::Vec<tc::Operation>& ops) {
// Only add an UndoPoint if there are not yet any changes.
if (changes.size() == 0) {
tc::add_undo_point(ops);
}
}
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
void TDB2::get_changes(std::vector<Task>& changes) { void TDB2::get_changes(std::vector<Task>& changes) {
std::map<std::string, Task>& changes_map = this->changes; std::map<std::string, Task>& changes_map = this->changes;
@@ -215,170 +226,108 @@ void TDB2::get_changes(std::vector<Task>& changes) {
[](const auto& kv) { return kv.second; }); [](const auto& kv) { return kv.second; });
} }
////////////////////////////////////////////////////////////////////////////////
void TDB2::revert() {
auto undo_ops = replica.get_undo_ops();
if (undo_ops.len == 0) {
std::cout << "No operations to undo.";
return;
}
if (confirm_revert(undo_ops)) {
// Has the side-effect of freeing undo_ops.
replica.commit_undo_ops(undo_ops, NULL);
} else {
replica.free_replica_ops(undo_ops);
}
replica.rebuild_working_set(false);
}
////////////////////////////////////////////////////////////////////////////////
bool TDB2::confirm_revert(struct tc::ffi::TCReplicaOpList undo_ops) {
// TODO Use show_diff rather than this basic listing of operations, though
// this might be a worthy undo.style itself.
std::cout << "The following " << undo_ops.len << " operations would be reverted:\n";
for (size_t i = 0; i < undo_ops.len; i++) {
std::cout << "- ";
tc::ffi::TCReplicaOp op = undo_ops.items[i];
switch (op.operation_type) {
case tc::ffi::TCReplicaOpType::Create:
std::cout << "Create " << replica.get_op_uuid(op);
break;
case tc::ffi::TCReplicaOpType::Delete:
std::cout << "Delete " << replica.get_op_old_task_description(op);
break;
case tc::ffi::TCReplicaOpType::Update:
std::cout << "Update " << replica.get_op_uuid(op) << "\n";
std::cout << " " << replica.get_op_property(op) << ": "
<< option_string(replica.get_op_old_value(op)) << " -> "
<< option_string(replica.get_op_value(op));
break;
case tc::ffi::TCReplicaOpType::UndoPoint:
throw std::string("Can't undo UndoPoint.");
break;
default:
throw std::string("Can't undo non-operation.");
break;
}
std::cout << "\n";
}
return !Context::getContext().config.getBoolean("confirmation") ||
confirm(
"The undo command is not reversible. Are you sure you want to revert to the previous "
"state?");
}
////////////////////////////////////////////////////////////////////////////////
std::string TDB2::option_string(std::string input) { return input == "" ? "<empty>" : input; }
////////////////////////////////////////////////////////////////////////////////
void TDB2::show_diff(const std::string& current, const std::string& prior,
const std::string& when) {
Datetime lastChange(strtoll(when.c_str(), nullptr, 10));
// Set the colors.
Color color_red(
Context::getContext().color() ? Context::getContext().config.get("color.undo.before") : "");
Color color_green(
Context::getContext().color() ? Context::getContext().config.get("color.undo.after") : "");
auto before = prior == "" ? Task() : Task(prior);
auto after = Task(current);
if (Context::getContext().config.get("undo.style") == "side") {
Table view = before.diffForUndoSide(after);
std::cout << '\n'
<< format("The last modification was made {1}", lastChange.toString()) << '\n'
<< '\n'
<< view.render() << '\n';
}
else if (Context::getContext().config.get("undo.style") == "diff") {
Table view = before.diffForUndoPatch(after, lastChange);
std::cout << '\n' << view.render() << '\n';
}
}
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
void TDB2::gc() { void TDB2::gc() {
Timer timer; Timer timer;
// Allowed as an override, but not recommended. // Allowed as an override, but not recommended.
if (Context::getContext().config.getBoolean("gc")) { if (Context::getContext().config.getBoolean("gc")) {
replica.rebuild_working_set(true); replica()->rebuild_working_set(true);
} }
Context::getContext().time_gc_us += timer.total_us(); Context::getContext().time_gc_us += timer.total_us();
} }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
void TDB2::expire_tasks() { replica.expire_tasks(); } void TDB2::expire_tasks() { replica()->expire_tasks(); }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
// Latest ID is that of the last pending task. // Latest ID is that of the last pending task.
int TDB2::latest_id() { int TDB2::latest_id() {
const tc::WorkingSet& ws = working_set(); auto& ws = working_set();
return (int)ws.largest_index(); return (int)ws->largest_index();
} }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
const std::vector<Task> TDB2::all_tasks() { const std::vector<Task> TDB2::all_tasks() {
auto all_tctasks = replica.all_tasks(); Timer timer;
auto all_tctasks = replica()->all_task_data();
std::vector<Task> all; std::vector<Task> all;
for (auto& tctask : all_tctasks) all.push_back(Task(std::move(tctask))); for (auto& maybe_tctask : all_tctasks) {
auto tctask = maybe_tctask.take();
all.push_back(Task(std::move(tctask)));
}
dependency_scan(all);
Context::getContext().time_load_us += timer.total_us();
return all; return all;
} }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
const std::vector<Task> TDB2::pending_tasks() { const std::vector<Task> TDB2::pending_tasks() {
const tc::WorkingSet& ws = working_set(); if (!_pending_tasks) {
auto largest_index = ws.largest_index(); Timer timer;
auto pending_tctasks = replica()->pending_task_data();
std::vector<Task> result; std::vector<Task> result;
for (size_t i = 0; i <= largest_index; i++) { for (auto& maybe_tctask : pending_tctasks) {
auto maybe_uuid = ws.by_index(i); auto tctask = maybe_tctask.take();
if (maybe_uuid.has_value()) { result.push_back(Task(std::move(tctask)));
auto maybe_task = replica.get_task(maybe_uuid.value());
if (maybe_task.has_value()) {
result.push_back(Task(std::move(maybe_task.value())));
}
}
} }
dependency_scan(result); dependency_scan(result);
return result; Context::getContext().time_load_us += timer.total_us();
_pending_tasks = result;
}
return *_pending_tasks;
} }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
const std::vector<Task> TDB2::completed_tasks() { const std::vector<Task> TDB2::completed_tasks() {
auto all_tctasks = replica.all_tasks(); if (!_completed_tasks) {
const tc::WorkingSet& ws = working_set(); auto all_tctasks = replica()->all_task_data();
auto& ws = working_set();
std::vector<Task> result; std::vector<Task> result;
for (auto& tctask : all_tctasks) { for (auto& maybe_tctask : all_tctasks) {
auto tctask = maybe_tctask.take();
// if this task is _not_ in the working set, return it. // if this task is _not_ in the working set, return it.
if (!ws.by_uuid(tctask.get_uuid())) { if (ws->by_uuid(tctask->get_uuid()) == 0) {
result.push_back(Task(std::move(tctask))); result.push_back(Task(std::move(tctask)));
} }
} }
_completed_tasks = result;
}
return *_completed_tasks;
}
return result; ////////////////////////////////////////////////////////////////////////////////
void TDB2::invalidate_cached_info() {
_pending_tasks = std::nullopt;
_completed_tasks = std::nullopt;
_working_set = std::nullopt;
} }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
// Locate task by ID, wherever it is. // Locate task by ID, wherever it is.
bool TDB2::get(int id, Task& task) { bool TDB2::get(int id, Task& task) {
const tc::WorkingSet& ws = working_set(); auto& ws = working_set();
const auto maybe_uuid = ws.by_index(id); const auto tcuuid = ws->by_index(id);
if (maybe_uuid) { if (!tcuuid.is_nil()) {
auto maybe_task = replica.get_task(*maybe_uuid); std::string uuid = static_cast<std::string>(tcuuid.to_string());
if (maybe_task) { // Load all pending tasks in order to get dependency data, and in particular
task = Task{std::move(*maybe_task)}; // `task.is_blocking` and `task.is_blocked`, set correctly.
std::vector<Task> pending = pending_tasks();
for (auto& pending_task : pending) {
if (pending_task.get("uuid") == uuid) {
task = pending_task;
return true; return true;
} }
} }
}
return false; return false;
} }
@@ -386,22 +335,23 @@ bool TDB2::get(int id, Task& task) {
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
// Locate task by UUID, including by partial ID, wherever it is. // Locate task by UUID, including by partial ID, wherever it is.
bool TDB2::get(const std::string& uuid, Task& task) { bool TDB2::get(const std::string& uuid, Task& task) {
// Load all pending tasks in order to get dependency data, and in particular
// `task.is_blocking` and `task.is_blocked`, set correctly.
std::vector<Task> pending = pending_tasks();
// try by raw uuid, if the length is right // try by raw uuid, if the length is right
if (uuid.size() == 36) { for (auto& pending_task : pending) {
try { if (closeEnough(pending_task.get("uuid"), uuid, uuid.length())) {
auto maybe_task = replica.get_task(uuid); task = pending_task;
if (maybe_task) {
task = Task{std::move(*maybe_task)};
return true; return true;
} }
} catch (const std::string& err) {
return false;
}
} }
// Nothing to do but iterate over all tasks and check whether it's closeEnough // Nothing to do but iterate over all tasks and check whether it's closeEnough.
for (auto& tctask : replica.all_tasks()) { for (auto& maybe_tctask : replica()->all_task_data()) {
if (closeEnough(tctask.get_uuid(), uuid, uuid.length())) { auto tctask = maybe_tctask.take();
auto tctask_uuid = static_cast<std::string>(tctask->get_uuid().to_string());
if (closeEnough(tctask_uuid, uuid, uuid.length())) {
task = Task{std::move(tctask)}; task = Task{std::move(tctask)};
return true; return true;
} }
@@ -446,63 +396,59 @@ const std::vector<Task> TDB2::children(Task& parent) {
std::vector<Task> results; std::vector<Task> results;
std::string this_uuid = parent.get("uuid"); std::string this_uuid = parent.get("uuid");
const tc::WorkingSet& ws = working_set(); auto& ws = working_set();
size_t end_idx = ws.largest_index(); size_t end_idx = ws->largest_index();
for (size_t i = 0; i <= end_idx; i++) { for (size_t i = 0; i <= end_idx; i++) {
auto uuid_opt = ws.by_index(i); auto uuid = ws->by_index(i);
if (!uuid_opt) { if (uuid.is_nil()) {
continue; continue;
} }
auto uuid = uuid_opt.value();
// skip self-references // skip self-references
if (uuid == this_uuid) { if (uuid.to_string() == this_uuid) {
continue; continue;
} }
auto task_opt = replica.get_task(uuid_opt.value()); auto task_opt = replica()->get_task_data(uuid);
if (!task_opt) { if (task_opt.is_none()) {
continue; continue;
} }
auto task = std::move(task_opt.value()); auto task = task_opt.take();
auto parent_uuid_opt = task.get_value("parent"); std::string parent_uuid;
if (!parent_uuid_opt) { if (!task->get("parent", parent_uuid)) {
continue; continue;
} }
auto parent_uuid = parent_uuid_opt.value();
if (parent_uuid == this_uuid) { if (parent_uuid == this_uuid) {
results.push_back(Task(std::move(task))); results.push_back(Task(std::move(task)));
} }
} }
return results; return results;
} }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
std::string TDB2::uuid(int id) { std::string TDB2::uuid(int id) {
const tc::WorkingSet& ws = working_set(); auto& ws = working_set();
return ws.by_index((size_t)id).value_or(""); auto uuid = ws->by_index(id);
if (uuid.is_nil()) {
return "";
}
return static_cast<std::string>(uuid.to_string());
} }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
int TDB2::id(const std::string& uuid) { int TDB2::id(const std::string& uuid) {
const tc::WorkingSet& ws = working_set(); auto& ws = working_set();
return (int)ws.by_uuid(uuid).value_or(0); return ws->by_uuid(tc::uuid_from_string(uuid));
} }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
int TDB2::num_local_changes() { return (int)replica.num_local_operations(); } int TDB2::num_local_changes() { return (int)replica()->num_local_operations(); }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
int TDB2::num_reverts_possible() { return (int)replica.num_undo_points(); } int TDB2::num_reverts_possible() { return (int)replica()->num_undo_points(); }
////////////////////////////////////////////////////////////////////////////////
void TDB2::sync(tc::Server server, bool avoid_snapshots) {
replica.sync(std::move(server), avoid_snapshots);
}
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
void TDB2::dump() { void TDB2::dump() {

View File

@@ -30,32 +30,27 @@
#include <FS.h> #include <FS.h>
#include <Task.h> #include <Task.h>
#include <stdio.h> #include <stdio.h>
#include <tc/Replica.h> #include <taskchampion-cpp/lib.h>
#include <tc/WorkingSet.h>
#include <map> #include <map>
#include <optional>
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>
#include <unordered_set> #include <unordered_set>
#include <vector> #include <vector>
namespace tc {
class Server;
}
// TDB2 Class represents all the files in the task database. // TDB2 Class represents all the files in the task database.
class TDB2 { class TDB2 {
public: public:
static bool debug_mode; static bool debug_mode;
TDB2(); TDB2() = default;
void open_replica(const std::string &, bool create_if_missing); void open_replica(const std::string &, bool create_if_missing);
void add(Task &); void add(Task &);
void modify(Task &); void modify(Task &);
void purge(Task &); void purge(Task &);
void get_changes(std::vector<Task> &); void get_changes(std::vector<Task> &);
void revert();
void gc(); void gc();
void expire_tasks(); void expire_tasks();
int latest_id(); int latest_id();
@@ -79,19 +74,22 @@ class TDB2 {
void dump(); void dump();
void sync(tc::Server server, bool avoid_snapshots); rust::Box<tc::Replica> &replica();
bool confirm_revert(struct tc::ffi::TCReplicaOpList);
private: private:
tc::Replica replica; std::optional<rust::Box<tc::Replica>> _replica;
std::optional<tc::WorkingSet> _working_set;
// Cached information from the replica
std::optional<rust::Box<tc::WorkingSet>> _working_set;
std::optional<std::vector<Task>> _pending_tasks;
std::optional<std::vector<Task>> _completed_tasks;
void invalidate_cached_info();
// UUID -> Task containing all tasks modified in this invocation. // UUID -> Task containing all tasks modified in this invocation.
std::map<std::string, Task> changes; std::map<std::string, Task> changes;
const tc::WorkingSet &working_set(); const rust::Box<tc::WorkingSet> &working_set();
static std::string option_string(std::string input); void maybe_add_undo_point(rust::Vec<tc::Operation> &);
static void show_diff(const std::string &, const std::string &, const std::string &);
}; };
#endif #endif

184
src/TF2.cpp Normal file
View File

@@ -0,0 +1,184 @@
////////////////////////////////////////////////////////////////////////////////
//
// Copyright 2006 - 2021, Tomas Babej, Paul Beckingham, Federico Hernandez.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// https://www.opensource.org/licenses/mit-license.php
//
////////////////////////////////////////////////////////////////////////////////
#include <Color.h>
#include <Context.h>
#include <Datetime.h>
#include <TF2.h>
#include <Table.h>
#include <cmake.h>
#include <format.h>
#include <main.h>
#include <shared.h>
#include <signal.h>
#include <stdlib.h>
#include <util.h>
#include <algorithm>
#include <iostream>
#include <list>
#include <set>
#include <sstream>
#define STRING_TDB2_REVERTED "Modified task reverted."
////////////////////////////////////////////////////////////////////////////////
TF2::TF2() : _loaded_tasks(false), _loaded_lines(false) {}
////////////////////////////////////////////////////////////////////////////////
TF2::~TF2() {}
////////////////////////////////////////////////////////////////////////////////
void TF2::target(const std::string& f) { _file = File(f); }
////////////////////////////////////////////////////////////////////////////////
const std::vector<std::map<std::string, std::string>>& TF2::get_tasks() {
if (!_loaded_tasks) load_tasks();
return _tasks;
}
////////////////////////////////////////////////////////////////////////////////
// Attempt an FF4 parse.
//
// Note that FF1, FF2, FF3, and JSON are no longer supported.
//
// start --> [ --> Att --> ] --> end
// ^ |
// +-------+
//
std::map<std::string, std::string> TF2::load_task(const std::string& input) {
std::map<std::string, std::string> data;
// File format version 4, from 2009-5-16 - now, v1.7.1+
// This is the parse format tried first, because it is most used.
data.clear();
if (input[0] == '[') {
// Not using Pig to parse here (which would be idiomatic), because we
// don't need to differentiate betwen utf-8 and normal characters.
// Pig's scanning the string can be expensive.
auto ending_bracket = input.find_last_of(']');
if (ending_bracket != std::string::npos) {
std::string line = input.substr(1, ending_bracket);
if (line.length() == 0) throw std::string("Empty record in input.");
Pig attLine(line);
std::string name;
std::string value;
while (!attLine.eos()) {
if (attLine.getUntilAscii(':', name) && attLine.skip(':') &&
attLine.getQuoted('"', value)) {
#ifdef PRODUCT_TASKWARRIOR
legacyAttributeMap(name);
#endif
data[name] = decode(json::decode(value));
}
attLine.skip(' ');
}
std::string remainder;
attLine.getRemainder(remainder);
if (remainder.length()) throw std::string("Unrecognized characters at end of line.");
}
} else {
throw std::string("Record not recognized as format 4.");
}
// for compatibility, include all tags in `tags` as `tag_..` attributes
if (data.find("tags") != data.end()) {
for (auto& tag : split(data["tags"], ',')) {
data[Task::tag2Attr(tag)] = "x";
}
}
// same for `depends` / `dep_..`
if (data.find("depends") != data.end()) {
for (auto& dep : split(data["depends"], ',')) {
data[Task::dep2Attr(dep)] = "x";
}
}
return data;
}
////////////////////////////////////////////////////////////////////////////////
// Decode values after parse.
// [ <- &open;
// ] <- &close;
const std::string TF2::decode(const std::string& value) const {
if (value.find('&') == std::string::npos) return value;
auto modified = str_replace(value, "&open;", "[");
return str_replace(modified, "&close;", "]");
}
////////////////////////////////////////////////////////////////////////////////
void TF2::load_tasks() {
Timer timer;
if (!_loaded_lines) {
load_lines();
}
// Reduce unnecessary allocations/copies.
// Calling it on _tasks is the right thing to do even when from_gc is set.
_tasks.reserve(_lines.size());
int line_number = 0; // Used for error message in catch block.
try {
for (auto& line : _lines) {
++line_number;
auto task = load_task(line);
_tasks.push_back(task);
}
_loaded_tasks = true;
}
catch (const std::string& e) {
throw e + format(" in {1} at line {2}", _file._data, line_number);
}
Context::getContext().time_load_us += timer.total_us();
}
////////////////////////////////////////////////////////////////////////////////
void TF2::load_lines() {
if (_file.open()) {
if (Context::getContext().config.getBoolean("locking")) _file.lock();
_file.read(_lines);
_file.close();
_loaded_lines = true;
}
}
////////////////////////////////////////////////////////////////////////////////
// vim: ts=2 et sw=2

View File

@@ -1,6 +1,6 @@
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
// //
// Copyright 2022, Dustin J. Mitchell // Copyright 2006 - 2024, Tomas Babej, Paul Beckingham, Federico Hernandez.
// //
// Permission is hereby granted, free of charge, to any person obtaining a copy // Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal // of this software and associated documentation files (the "Software"), to deal
@@ -24,29 +24,43 @@
// //
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDED_TC_UTIL #ifndef INCLUDED_TF2
#define INCLUDED_TC_UTIL #define INCLUDED_TF2
#include <FS.h>
#include <Task.h>
#include <stdio.h>
#include <map>
#include <string> #include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
#include "tc/ffi.h" // TF2 Class represents a single 2.x-style file in the task database.
//
// This is only used for importing tasks from 2.x. It only reads format 4, based
// on a stripped-down version of the TF2 class from v2.6.2.
class TF2 {
public:
TF2();
~TF2();
namespace tc { void target(const std::string&);
// convert a std::string into a TCString, copying the contained data
tc::ffi::TCString string2tc(const std::string&);
// convert a TCString into a std::string, leaving the TCString as-is const std::vector<std::map<std::string, std::string>>& get_tasks();
std::string tc2string_clone(const tc::ffi::TCString&);
// convert a TCString into a std::string, freeing the TCString std::map<std::string, std::string> load_task(const std::string&);
std::string tc2string(tc::ffi::TCString&); void load_tasks();
void load_lines();
const std::string decode(const std::string& value) const;
// convert a TCUuid into a std::string bool _loaded_tasks;
std::string tc2uuid(tc::ffi::TCUuid&); bool _loaded_lines;
std::vector<std::map<std::string, std::string>> _tasks;
// parse a std::string into a TCUuid (throwing if parse fails) std::vector<std::string> _lines;
tc::ffi::TCUuid uuid2tc(const std::string&); File _file;
} // namespace tc };
#endif #endif
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////

View File

@@ -138,7 +138,7 @@ Task::Task(const json::object* obj) {
} }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
Task::Task(tc::Task obj) { Task::Task(rust::Box<tc::TaskData> obj) {
id = 0; id = 0;
urgency_value = 0.0; urgency_value = 0.0;
recalc_urgency = true; recalc_urgency = true;
@@ -146,7 +146,7 @@ Task::Task(tc::Task obj) {
is_blocking = false; is_blocking = false;
annotation_count = 0; annotation_count = 0;
parseTC(obj); parseTC(std::move(obj));
} }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
@@ -717,8 +717,12 @@ void Task::parseJSON(const json::object* root_obj) {
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
// Note that all fields undergo encode/decode. // Note that all fields undergo encode/decode.
void Task::parseTC(const tc::Task& task) { void Task::parseTC(rust::Box<tc::TaskData> task) {
data = task.get_taskmap(); auto items = task->items();
data.clear();
for (auto& item : items) {
data[static_cast<std::string>(item.prop)] = static_cast<std::string>(item.value);
}
// count annotations // count annotations
annotation_count = 0; annotation_count = 0;
@@ -728,11 +732,8 @@ void Task::parseTC(const tc::Task& task) {
} }
} }
data["uuid"] = task.get_uuid(); data["uuid"] = static_cast<std::string>(task->get_uuid().to_string());
id = Context::getContext().tdb2.id(data["uuid"]); id = Context::getContext().tdb2.id(data["uuid"]);
is_blocking = task.is_blocking();
is_blocked = task.is_blocked();
} }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
@@ -1242,14 +1243,14 @@ void Task::fixTagsAttribute() {
bool Task::isTagAttr(const std::string& attr) { return attr.compare(0, 4, "tag_") == 0; } bool Task::isTagAttr(const std::string& attr) { return attr.compare(0, 4, "tag_") == 0; }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
const std::string Task::tag2Attr(const std::string& tag) const { std::string Task::tag2Attr(const std::string& tag) {
std::stringstream tag_attr; std::stringstream tag_attr;
tag_attr << "tag_" << tag; tag_attr << "tag_" << tag;
return tag_attr.str(); return tag_attr.str();
} }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
const std::string Task::attr2Tag(const std::string& attr) const { std::string Task::attr2Tag(const std::string& attr) {
assert(isTagAttr(attr)); assert(isTagAttr(attr));
return attr.substr(4); return attr.substr(4);
} }
@@ -1270,14 +1271,14 @@ void Task::fixDependsAttribute() {
bool Task::isDepAttr(const std::string& attr) { return attr.compare(0, 4, "dep_") == 0; } bool Task::isDepAttr(const std::string& attr) { return attr.compare(0, 4, "dep_") == 0; }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
const std::string Task::dep2Attr(const std::string& tag) const { std::string Task::dep2Attr(const std::string& tag) {
std::stringstream tag_attr; std::stringstream tag_attr;
tag_attr << "dep_" << tag; tag_attr << "dep_" << tag;
return tag_attr.str(); return tag_attr.str();
} }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
const std::string Task::attr2Dep(const std::string& attr) const { std::string Task::attr2Dep(const std::string& attr) {
assert(isDepAttr(attr)); assert(isDepAttr(attr));
return attr.substr(4); return attr.substr(4);
} }
@@ -1407,13 +1408,29 @@ void Task::substitute(const std::string& from, const std::string& to, const std:
} }
#endif #endif
////////////////////////////////////////////////////////////////////////////////
// Validate a task for addition, raising user-visible errors for inconsistent or
// incorrect inputs. This is called before `Task::validate`.
void Task::validate_add() {
// There is no fixing a missing description.
if (!has("description"))
throw std::string("A task must have a description.");
else if (get("description") == "")
throw std::string("Cannot add a task that is blank.");
// Cannot have an old-style recur frequency with no due date - when would it recur?
if (has("recur") && (!has("due") || get("due") == ""))
throw std::string("A recurring task must also have a 'due' date.");
}
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
// The purpose of Task::validate is three-fold: // The purpose of Task::validate is three-fold:
// 1) To provide missing attributes where possible // 1) To provide missing attributes where possible
// 2) To provide suitable warnings about odd states // 2) To provide suitable warnings about odd states
// 3) To generate errors when the inconsistencies are not fixable // 3) To update status depending on other attributes
// 4) To update status depending on other attributes
// //
// As required by TaskChampion, no combination of properties and values is an
// error. This function will try to make sensible defaults and resolve inconsistencies.
// Critically, note that despite the name this is not a read-only function. // Critically, note that despite the name this is not a read-only function.
// //
void Task::validate(bool applyDefault /* = true */) { void Task::validate(bool applyDefault /* = true */) {
@@ -1427,6 +1444,8 @@ void Task::validate(bool applyDefault /* = true */) {
Lexer lex(uid); Lexer lex(uid);
std::string token; std::string token;
Lexer::Type type; Lexer::Type type;
// `uuid` is not a property in the TaskChampion model, so an invalid UUID is
// actually an error.
if (!lex.isUUID(token, type, true)) throw format("Not a valid UUID '{1}'.", uid); if (!lex.isUUID(token, type, true)) throw format("Not a valid UUID '{1}'.", uid);
} else } else
set("uuid", uuid()); set("uuid", uuid());
@@ -1542,27 +1561,27 @@ void Task::validate(bool applyDefault /* = true */) {
validate_before("scheduled", "due"); validate_before("scheduled", "due");
validate_before("scheduled", "end"); validate_before("scheduled", "end");
// 3) To generate errors when the inconsistencies are not fixable if (!has("description") || get("description") == "")
Context::getContext().footnote(format("Warning: task has no description."));
// There is no fixing a missing description. // Cannot have an old-style recur frequency with no due date - when would it recur?
if (!has("description")) if (has("recur") && (!has("due") || get("due") == "")) {
throw std::string("A task must have a description."); Context::getContext().footnote(format("Warning: recurring task has no due date."));
else if (get("description") == "") remove("recur");
throw std::string("Cannot add a task that is blank."); }
// Cannot have a recur frequency with no due date - when would it recur? // Old-style recur durations must be valid.
if (has("recur") && (!has("due") || get("due") == ""))
throw std::string("A recurring task must also have a 'due' date.");
// Recur durations must be valid.
if (has("recur")) { if (has("recur")) {
std::string value = get("recur"); std::string value = get("recur");
if (value != "") { if (value != "") {
Duration p; Duration p;
std::string::size_type i = 0; std::string::size_type i = 0;
if (!p.parse(value, i)) if (!p.parse(value, i)) {
// TODO Ideal location to map unsupported old recurrence periods to supported values. // TODO Ideal location to map unsupported old recurrence periods to supported values.
throw format("The recurrence value '{1}' is not valid.", value); Context::getContext().footnote(
format("Warning: The recurrence value '{1}' is not valid.", value));
remove("recur");
}
} }
} }
} }
@@ -1602,40 +1621,6 @@ const std::string Task::decode(const std::string& value) const {
return str_replace(modified, "&close;", "]"); return str_replace(modified, "&close;", "]");
} }
////////////////////////////////////////////////////////////////////////////////
tc::Status Task::status2tc(const Task::status status) {
switch (status) {
case Task::pending:
return tc::Status::Pending;
case Task::completed:
return tc::Status::Completed;
case Task::deleted:
return tc::Status::Deleted;
case Task::waiting:
return tc::Status::Pending; // waiting is no longer a status
case Task::recurring:
return tc::Status::Recurring;
default:
return tc::Status::Unknown;
}
}
////////////////////////////////////////////////////////////////////////////////
Task::status Task::tc2status(const tc::Status status) {
switch (status) {
case tc::Status::Pending:
return Task::pending;
case tc::Status::Completed:
return Task::completed;
case tc::Status::Deleted:
return Task::deleted;
case tc::Status::Recurring:
return Task::recurring;
default:
return Task::pending;
}
}
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
int Task::determineVersion(const std::string& line) { int Task::determineVersion(const std::string& line) {
// Version 2 looks like: // Version 2 looks like:
@@ -2167,274 +2152,3 @@ std::string Task::diff(const Task& after) const {
} }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
// Similar to diff, but formatted for inclusion in the output of the info command
std::string Task::diffForInfo(const Task& after, const std::string& dateformat,
long& last_timestamp, const long current_timestamp) const {
// Attributes are all there is, so figure the different attribute names
// between before and after.
std::vector<std::string> beforeAtts;
for (auto& att : data) beforeAtts.push_back(att.first);
std::vector<std::string> afterAtts;
for (auto& att : after.data) afterAtts.push_back(att.first);
std::vector<std::string> beforeOnly;
std::vector<std::string> afterOnly;
listDiff(beforeAtts, afterAtts, beforeOnly, afterOnly);
// Now start generating a description of the differences.
std::stringstream out;
for (auto& name : beforeOnly) {
if (isAnnotationAttr(name)) {
out << format("Annotation '{1}' deleted.\n", get(name));
} else if (isTagAttr(name)) {
out << format("Tag '{1}' deleted.\n", attr2Tag(name));
} else if (isDepAttr(name)) {
out << format("Dependency on '{1}' deleted.\n", attr2Dep(name));
} else if (name == "depends" || name == "tags") {
// do nothing for legacy attributes
} else if (name == "start") {
Datetime started(get("start"));
Datetime stopped;
if (after.has("end"))
// Task was marked as finished, use end time
stopped = Datetime(after.get("end"));
else
// Start attribute was removed, use modification time
stopped = Datetime(current_timestamp);
out << format("{1} deleted (duration: {2}).", Lexer::ucFirst(name),
Duration(stopped - started).format())
<< "\n";
} else {
out << format("{1} deleted.\n", Lexer::ucFirst(name));
}
}
for (auto& name : afterOnly) {
if (isAnnotationAttr(name)) {
out << format("Annotation of '{1}' added.\n", after.get(name));
} else if (isTagAttr(name)) {
out << format("Tag '{1}' added.\n", attr2Tag(name));
} else if (isDepAttr(name)) {
out << format("Dependency on '{1}' added.\n", attr2Dep(name));
} else if (name == "depends" || name == "tags") {
// do nothing for legacy attributes
} else {
if (name == "start") last_timestamp = current_timestamp;
out << format("{1} set to '{2}'.", Lexer::ucFirst(name),
renderAttribute(name, after.get(name), dateformat))
<< "\n";
}
}
for (auto& name : beforeAtts)
if (name != "uuid" && name != "modified" && get(name) != after.get(name) && get(name) != "" &&
after.get(name) != "") {
if (name == "depends" || name == "tags") {
// do nothing for legacy attributes
} else if (isTagAttr(name) || isDepAttr(name)) {
// ignore new attributes
} else if (isAnnotationAttr(name)) {
out << format("Annotation changed to '{1}'.\n", after.get(name));
} else
out << format("{1} changed from '{2}' to '{3}'.", Lexer::ucFirst(name),
renderAttribute(name, get(name), dateformat),
renderAttribute(name, after.get(name), dateformat))
<< "\n";
}
// Shouldn't just say nothing.
if (out.str().length() == 0) out << "No changes made.\n";
return out.str();
}
////////////////////////////////////////////////////////////////////////////////
// Similar to diff, but formatted as a side-by-side table for an Undo preview
Table Task::diffForUndoSide(const Task& after) const {
// Set the colors.
Color color_red(
Context::getContext().color() ? Context::getContext().config.get("color.undo.before") : "");
Color color_green(
Context::getContext().color() ? Context::getContext().config.get("color.undo.after") : "");
// Attributes are all there is, so figure the different attribute names
// between before and after.
Table view;
view.width(Context::getContext().getWidth());
view.intraPadding(2);
view.add("");
view.add("Prior Values");
view.add("Current Values");
setHeaderUnderline(view);
if (!is_empty()) {
const Task& before = *this;
std::vector<std::string> beforeAtts;
for (auto& att : before.data) beforeAtts.push_back(att.first);
std::vector<std::string> afterAtts;
for (auto& att : after.data) afterAtts.push_back(att.first);
std::vector<std::string> beforeOnly;
std::vector<std::string> afterOnly;
listDiff(beforeAtts, afterAtts, beforeOnly, afterOnly);
int row;
for (auto& name : beforeOnly) {
row = view.addRow();
view.set(row, 0, name);
view.set(row, 1, renderAttribute(name, before.get(name)), color_red);
}
for (auto& att : before.data) {
std::string priorValue = before.get(att.first);
std::string currentValue = after.get(att.first);
if (currentValue != "") {
row = view.addRow();
view.set(row, 0, att.first);
view.set(row, 1, renderAttribute(att.first, priorValue),
(priorValue != currentValue ? color_red : Color()));
view.set(row, 2, renderAttribute(att.first, currentValue),
(priorValue != currentValue ? color_green : Color()));
}
}
for (auto& name : afterOnly) {
row = view.addRow();
view.set(row, 0, name);
view.set(row, 2, renderAttribute(name, after.get(name)), color_green);
}
} else {
int row;
for (auto& att : after.data) {
row = view.addRow();
view.set(row, 0, att.first);
view.set(row, 2, renderAttribute(att.first, after.get(att.first)), color_green);
}
}
return view;
}
////////////////////////////////////////////////////////////////////////////////
// Similar to diff, but formatted as a diff for an Undo preview
Table Task::diffForUndoPatch(const Task& after, const Datetime& lastChange) const {
// This style looks like this:
// --- before 2009-07-04 00:00:25.000000000 +0200
// +++ after 2009-07-04 00:00:45.000000000 +0200
//
// - name: old // att deleted
// + name:
//
// - name: old // att changed
// + name: new
//
// - name:
// + name: new // att added
//
// Set the colors.
Color color_red(
Context::getContext().color() ? Context::getContext().config.get("color.undo.before") : "");
Color color_green(
Context::getContext().color() ? Context::getContext().config.get("color.undo.after") : "");
const Task& before = *this;
// Generate table header.
Table view;
view.width(Context::getContext().getWidth());
view.intraPadding(2);
view.add("");
view.add("");
int row = view.addRow();
view.set(row, 0, "--- previous state", color_red);
view.set(row, 1, "Undo will restore this state", color_red);
row = view.addRow();
view.set(row, 0, "+++ current state ", color_green);
view.set(row, 1,
format("Change made {1}",
lastChange.toString(Context::getContext().config.get("dateformat"))),
color_green);
view.addRow();
// Add rows to table showing diffs.
std::vector<std::string> all = Context::getContext().getColumns();
// Now factor in the annotation attributes.
for (auto& it : before.data)
if (it.first.substr(0, 11) == "annotation_") all.push_back(it.first);
for (auto& it : after.data)
if (it.first.substr(0, 11) == "annotation_") all.push_back(it.first);
// Now render all the attributes.
std::sort(all.begin(), all.end());
std::string before_att;
std::string after_att;
std::string last_att;
for (auto& a : all) {
if (a != last_att) // Skip duplicates.
{
last_att = a;
before_att = before.get(a);
after_att = after.get(a);
// Don't report different uuid.
// Show nothing if values are the unchanged.
if (a == "uuid" || before_att == after_att) {
// Show nothing - no point displaying that which did not change.
// row = view.addRow ();
// view.set (row, 0, *a + ":");
// view.set (row, 1, before_att);
}
// Attribute deleted.
else if (before_att != "" && after_att == "") {
row = view.addRow();
view.set(row, 0, '-' + a + ':', color_red);
view.set(row, 1, before_att, color_red);
row = view.addRow();
view.set(row, 0, '+' + a + ':', color_green);
}
// Attribute added.
else if (before_att == "" && after_att != "") {
row = view.addRow();
view.set(row, 0, '-' + a + ':', color_red);
row = view.addRow();
view.set(row, 0, '+' + a + ':', color_green);
view.set(row, 1, after_att, color_green);
}
// Attribute changed.
else {
row = view.addRow();
view.set(row, 0, '-' + a + ':', color_red);
view.set(row, 1, before_att, color_red);
row = view.addRow();
view.set(row, 0, '+' + a + ':', color_green);
view.set(row, 1, after_att, color_green);
}
}
}
return view;
}
////////////////////////////////////////////////////////////////////////////////

View File

@@ -31,7 +31,7 @@
#include <JSON.h> #include <JSON.h>
#include <Table.h> #include <Table.h>
#include <stdio.h> #include <stdio.h>
#include <tc/Task.h> #include <taskchampion-cpp/lib.h>
#include <time.h> #include <time.h>
#include <map> #include <map>
@@ -66,7 +66,7 @@ class Task {
bool operator!=(const Task&); bool operator!=(const Task&);
Task(const std::string&); Task(const std::string&);
Task(const json::object*); Task(const json::object*);
Task(tc::Task); Task(rust::Box<tc::TaskData>);
void parse(const std::string&); void parse(const std::string&);
std::string composeJSON(bool decorate = false) const; std::string composeJSON(bool decorate = false) const;
@@ -88,8 +88,6 @@ class Task {
// Series of helper functions. // Series of helper functions.
static status textToStatus(const std::string&); static status textToStatus(const std::string&);
static std::string statusToText(status); static std::string statusToText(status);
static tc::Status status2tc(const Task::status);
static Task::status tc2status(const tc::Status);
void setAsNow(const std::string&); void setAsNow(const std::string&);
bool has(const std::string&) const; bool has(const std::string&) const;
@@ -166,6 +164,12 @@ class Task {
void substitute(const std::string&, const std::string&, const std::string&); void substitute(const std::string&, const std::string&, const std::string&);
#endif #endif
static std::string tag2Attr(const std::string&);
static std::string attr2Tag(const std::string&);
static std::string dep2Attr(const std::string&);
static std::string attr2Dep(const std::string&);
void validate_add();
void validate(bool applyDefault = true); void validate(bool applyDefault = true);
float urgency_c() const; float urgency_c() const;
@@ -177,24 +181,16 @@ class Task {
#endif #endif
std::string diff(const Task& after) const; std::string diff(const Task& after) const;
std::string diffForInfo(const Task& after, const std::string& dateformat, long& last_timestamp,
const long current_timestamp) const;
Table diffForUndoSide(const Task& after) const;
Table diffForUndoPatch(const Task& after, const Datetime& lastChange) const;
private: private:
int determineVersion(const std::string&); int determineVersion(const std::string&);
void parseJSON(const std::string&); void parseJSON(const std::string&);
void parseJSON(const json::object*); void parseJSON(const json::object*);
void parseTC(const tc::Task&); void parseTC(rust::Box<tc::TaskData>);
void parseLegacy(const std::string&); void parseLegacy(const std::string&);
void validate_before(const std::string&, const std::string&); void validate_before(const std::string&, const std::string&);
const std::string encode(const std::string&) const; const std::string encode(const std::string&) const;
const std::string decode(const std::string&) const; const std::string decode(const std::string&) const;
const std::string tag2Attr(const std::string&) const;
const std::string attr2Tag(const std::string&) const;
const std::string dep2Attr(const std::string&) const;
const std::string attr2Dep(const std::string&) const;
void fixDependsAttribute(); void fixDependsAttribute();
void fixTagsAttribute(); void fixTagsAttribute();

View File

@@ -1,8 +1,6 @@
cmake_minimum_required (VERSION 3.22) cmake_minimum_required (VERSION 3.22)
include_directories (${CMAKE_SOURCE_DIR} include_directories (${CMAKE_SOURCE_DIR}
${CMAKE_SOURCE_DIR}/src ${CMAKE_SOURCE_DIR}/src
${CMAKE_SOURCE_DIR}/src/tc
${CMAKE_SOURCE_DIR}/src/tc/lib
${CMAKE_SOURCE_DIR}/src/commands ${CMAKE_SOURCE_DIR}/src/commands
${CMAKE_SOURCE_DIR}/src/columns ${CMAKE_SOURCE_DIR}/src/columns
${CMAKE_SOURCE_DIR}/src/libshared/src ${CMAKE_SOURCE_DIR}/src/libshared/src
@@ -39,6 +37,7 @@ set (columns_SRCS Column.cpp Column.h
ColWait.cpp ColWait.h) ColWait.cpp ColWait.h)
add_library (columns STATIC ${columns_SRCS}) add_library (columns STATIC ${columns_SRCS})
target_link_libraries(columns taskchampion-cpp)
#SET(CMAKE_BUILD_TYPE gcov) #SET(CMAKE_BUILD_TYPE gcov)
#SET(CMAKE_CXX_FLAGS_GCOV "--coverage") #SET(CMAKE_CXX_FLAGS_GCOV "--coverage")

View File

@@ -190,7 +190,11 @@ void ColumnTypeDate::modify(Task& task, const std::string& value) {
if (value != "" && evaluatedValue.get_date() == 0) if (value != "" && evaluatedValue.get_date() == 0)
throw format("'{1}' is not a valid date in the '{2}' format.", value, Variant::dateFormat); throw format("'{1}' is not a valid date in the '{2}' format.", value, Variant::dateFormat);
task.set(_name, evaluatedValue.get_date()); time_t epoch = evaluatedValue.get_date();
if (epoch < EPOCH_MIN_VALUE || epoch >= EPOCH_MAX_VALUE) {
throw format("'{1}' is not a valid date.", value);
}
task.set(_name, epoch);
} }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////

View File

@@ -1,8 +1,6 @@
cmake_minimum_required (VERSION 3.22) cmake_minimum_required (VERSION 3.22)
include_directories (${CMAKE_SOURCE_DIR} include_directories (${CMAKE_SOURCE_DIR}
${CMAKE_SOURCE_DIR}/src ${CMAKE_SOURCE_DIR}/src
${CMAKE_SOURCE_DIR}/src/tc
${CMAKE_SOURCE_DIR}/src/tc/lib
${CMAKE_SOURCE_DIR}/src/commands ${CMAKE_SOURCE_DIR}/src/commands
${CMAKE_SOURCE_DIR}/src/columns ${CMAKE_SOURCE_DIR}/src/columns
${CMAKE_SOURCE_DIR}/src/libshared/src ${CMAKE_SOURCE_DIR}/src/libshared/src
@@ -37,6 +35,7 @@ set (commands_SRCS Command.cpp Command.h
CmdHistory.cpp CmdHistory.h CmdHistory.cpp CmdHistory.h
CmdIDs.cpp CmdIDs.h CmdIDs.cpp CmdIDs.h
CmdImport.cpp CmdImport.h CmdImport.cpp CmdImport.h
CmdImportV2.cpp CmdImportV2.h
CmdInfo.cpp CmdInfo.h CmdInfo.cpp CmdInfo.h
CmdLog.cpp CmdLog.h CmdLog.cpp CmdLog.h
CmdLogo.cpp CmdLogo.h CmdLogo.cpp CmdLogo.h
@@ -61,6 +60,7 @@ set (commands_SRCS Command.cpp Command.h
CmdVersion.cpp CmdVersion.h) CmdVersion.cpp CmdVersion.h)
add_library (commands STATIC ${commands_SRCS}) add_library (commands STATIC ${commands_SRCS})
target_link_libraries(commands taskchampion-cpp)
#SET(CMAKE_BUILD_TYPE gcov) #SET(CMAKE_BUILD_TYPE gcov)
#SET(CMAKE_CXX_FLAGS_GCOV "--coverage") #SET(CMAKE_CXX_FLAGS_GCOV "--coverage")

View File

@@ -404,12 +404,7 @@ int CmdCalendar::execute(std::string& output) {
std::string CmdCalendar::renderMonths(int firstMonth, int firstYear, const Datetime& today, std::string CmdCalendar::renderMonths(int firstMonth, int firstYear, const Datetime& today,
std::vector<Task>& all, int monthsPerLine) { std::vector<Task>& all, int monthsPerLine) {
auto& config = Context::getContext().config; auto& config = Context::getContext().config;
auto weekStart = Datetime::weekstart;
// What day of the week does the user consider the first?
auto weekStart = Datetime::dayOfWeek(config.get("weekstart"));
if (weekStart != 0 && weekStart != 1)
throw std::string(
"The 'weekstart' configuration variable may only contain 'Sunday' or 'Monday'.");
// Build table for the number of months to be displayed. // Build table for the number of months to be displayed.
Table view; Table view;

View File

@@ -95,7 +95,9 @@ bool CmdConfig::setConfigVariable(const std::string& name, const std::string& va
change = true; change = true;
} }
if (change) File::write(Context::getContext().config.file(), contents); if (change)
if (!File::write(Context::getContext().config.file(), contents))
throw format("Could not write to '{1}'.", Context::getContext().config.file());
return change; return change;
} }
@@ -133,7 +135,9 @@ int CmdConfig::unsetConfigVariable(const std::string& name, bool confirmation /*
if (!lineDeleted) line++; if (!lineDeleted) line++;
} }
if (change) File::write(Context::getContext().config.file(), contents); if (change)
if (!File::write(Context::getContext().config.file(), contents))
throw format("Could not write to '{1}'.", Context::getContext().config.file());
if (change && found) if (change && found)
return 0; return 0;

View File

@@ -28,6 +28,7 @@
// cmake.h include header must come first // cmake.h include header must come first
#include <CmdCustom.h> #include <CmdCustom.h>
#include <CmdNews.h>
#include <Context.h> #include <Context.h>
#include <Filter.h> #include <Filter.h>
#include <Lexer.h> #include <Lexer.h>
@@ -222,11 +223,9 @@ int CmdCustom::execute(std::string& output) {
} }
// Inform user about the new release highlights if not presented yet // Inform user about the new release highlights if not presented yet
Version news_version(Context::getContext().config.get("news.version")); if (CmdNews::should_nag()) {
Version current_version = Version::Current();
auto should_nag = news_version != current_version && Context::getContext().verbose("news");
if (should_nag) {
std::ostringstream notice; std::ostringstream notice;
Version current_version = Version::Current();
notice << "Recently upgraded to " << current_version notice << "Recently upgraded to " << current_version
<< ". " << ". "
"Please run 'task news' to read highlights about the new release."; "Please run 'task news' to read highlights about the new release.";
@@ -242,7 +241,8 @@ int CmdCustom::execute(std::string& output) {
Color warning = Color(Context::getContext().config.get("color.warning")); Color warning = Color(Context::getContext().config.get("color.warning"));
std::cerr << warning.colorize(format("Found existing '*.data' files in {1}", location)) << "\n"; std::cerr << warning.colorize(format("Found existing '*.data' files in {1}", location)) << "\n";
std::cerr << " Taskwarrior's storage format changed in 3.0, requiring a manual migration.\n"; std::cerr << " Taskwarrior's storage format changed in 3.0, requiring a manual migration.\n";
std::cerr << " See https://github.com/GothenburgBitFactory/taskwarrior/releases.\n"; std::cerr << " See https://taskwarrior.org/docs/upgrade-3/. Run `task import-v2` to import\n";
std::cerr << " the tasks into the Taskwarrior-3.x format\n";
} }
feedback_backlog(); feedback_backlog();

View File

@@ -174,6 +174,9 @@ void CmdImport::importSingleTask(json::object* obj) {
// Parse the whole thing, validate the data. // Parse the whole thing, validate the data.
Task task(obj); Task task(obj);
// An empty task is probably not intentional - at least a UUID should be included.
if (task.is_empty()) throw format("Cannot import an empty task.");
auto hasGeneratedEntry = not task.has("entry"); auto hasGeneratedEntry = not task.has("entry");
auto hasExplicitEnd = task.has("end"); auto hasExplicitEnd = task.has("end");

View File

@@ -31,6 +31,7 @@
#include <JSON.h> #include <JSON.h>
#include <string> #include <string>
#include <unordered_map>
class CmdImport : public Command { class CmdImport : public Command {
public: public:

View File

@@ -0,0 +1,135 @@
////////////////////////////////////////////////////////////////////////////////
//
// Copyright 2006 - 2021, Tomas Babej, Paul Beckingham, Federico Hernandez.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// https://www.opensource.org/licenses/mit-license.php
//
////////////////////////////////////////////////////////////////////////////////
#include <cmake.h>
// cmake.h include header must come first
#include <CmdImportV2.h>
#include <CmdModify.h>
#include <Context.h>
#include <TF2.h>
#include <format.h>
#include <shared.h>
#include <util.h>
#include <iostream>
#include <unordered_map>
////////////////////////////////////////////////////////////////////////////////
CmdImportV2::CmdImportV2() {
_keyword = "import-v2";
_usage = "task import-v2";
_description = "Imports Taskwarrior v2.x files";
_read_only = false;
_displays_id = false;
_needs_gc = false;
_uses_context = false;
_accepts_filter = false;
_accepts_modifications = false;
_accepts_miscellaneous = true;
_category = Command::Category::migration;
}
////////////////////////////////////////////////////////////////////////////////
int CmdImportV2::execute(std::string&) {
std::vector<std::map<std::string, std::string>> task_data;
std::string location = (Context::getContext().data_dir);
File pending_file = File(location + "/pending.data");
if (pending_file.exists()) {
TF2 pending_tf;
pending_tf.target(pending_file);
auto& pending_tasks = pending_tf.get_tasks();
task_data.insert(task_data.end(), pending_tasks.begin(), pending_tasks.end());
}
File completed_file = File(location + "/completed.data");
if (completed_file.exists()) {
TF2 completed_tf;
completed_tf.target(completed_file);
auto& completed_tasks = completed_tf.get_tasks();
task_data.insert(task_data.end(), completed_tasks.begin(), completed_tasks.end());
}
auto count = import(task_data);
Context::getContext().footnote(
format("Imported {1} tasks from `*.data` files. You may now delete these files.", count));
return 0;
}
////////////////////////////////////////////////////////////////////////////////
int CmdImportV2::import(const std::vector<std::map<std::string, std::string>>& task_data) {
auto count = 0;
const std::string uuid_key = "uuid";
const std::string id_key = "id";
const std::string descr_key = "description";
auto& replica = Context::getContext().tdb2.replica();
rust::Vec<tc::Operation> ops;
tc::add_undo_point(ops);
for (auto& task : task_data) {
auto uuid_iter = task.find(uuid_key);
if (uuid_iter == task.end()) {
std::cout << " err - Task with no UUID\n";
continue;
}
auto uuid_str = uuid_iter->second;
auto uuid = tc::uuid_from_string(uuid_str);
bool added_task = false;
auto maybe_task_data = replica->get_task_data(uuid);
auto task_data = maybe_task_data.is_some() ? maybe_task_data.take() : [&]() {
added_task = true;
return tc::create_task(uuid, ops);
}();
for (auto& attr : task) {
if (attr.first == uuid_key || attr.first == id_key) {
continue;
}
task_data->update(attr.first, attr.second, ops);
}
count++;
if (added_task) {
std::cout << " add ";
} else {
std::cout << " mod ";
}
std::cout << uuid_str << ' ';
if (auto descr_iter = task.find(descr_key); descr_iter != task.end()) {
std::cout << descr_iter->second;
} else {
std::cout << "(no description)";
}
std::cout << "\n";
}
replica->commit_operations(std::move(ops));
return count;
}
////////////////////////////////////////////////////////////////////////////////

View File

@@ -1,6 +1,6 @@
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
// //
// Copyright 2022, Dustin J. Mitchell, Tomas Babej, Paul Beckingham, Federico Hernandez. // Copyright 2006 - 2021, Tomas Babej, Paul Beckingham, Federico Hernandez.
// //
// Permission is hereby granted, free of charge, to any person obtaining a copy // Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal // of this software and associated documentation files (the "Software"), to deal
@@ -24,13 +24,23 @@
// //
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDED_TC_FFI #ifndef INCLUDED_CMDIMPORTV2
#define INCLUDED_TC_FFI #define INCLUDED_CMDIMPORTV2
// The entire FFI API is embedded in the `tc::ffi` namespace #include <Command.h>
namespace tc::ffi { #include <JSON.h>
#include <taskchampion.h>
} #include <string>
#include <unordered_map>
class CmdImportV2 : public Command {
public:
CmdImportV2();
int execute(std::string &);
private:
int import(const std::vector<std::map<std::string, std::string>> &task_data);
};
#endif #endif
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////

View File

@@ -33,13 +33,16 @@
#include <Duration.h> #include <Duration.h>
#include <Filter.h> #include <Filter.h>
#include <Lexer.h> #include <Lexer.h>
#include <Operation.h>
#include <format.h> #include <format.h>
#include <main.h> #include <main.h>
#include <math.h> #include <math.h>
#include <shared.h> #include <shared.h>
#include <stdlib.h> #include <stdlib.h>
#include <taskchampion-cpp/lib.h>
#include <util.h> #include <util.h>
#include <algorithm>
#include <sstream> #include <sstream>
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
@@ -373,7 +376,7 @@ int CmdInfo::execute(std::string& output) {
// Show any orphaned UDAs, which are identified by not being represented in // Show any orphaned UDAs, which are identified by not being represented in
// the context.columns map. // the context.columns map.
for (auto& att : all) { for (auto& att : all) {
if (att.substr(0, 11) != "annotation_" && att.substr(0, 5) != "tags_" && if (att.substr(0, 11) != "annotation_" && att.substr(0, 4) != "tag_" &&
att.substr(0, 4) != "dep_" && att.substr(0, 4) != "dep_" &&
Context::getContext().columns.find(att) == Context::getContext().columns.end()) { Context::getContext().columns.find(att) == Context::getContext().columns.end()) {
row = view.addRow(); row = view.addRow();
@@ -477,9 +480,68 @@ int CmdInfo::execute(std::string& output) {
urgencyDetails.set(row, 5, rightJustify(format(task.urgency(), 4, 4), 6)); urgencyDetails.set(row, 5, rightJustify(format(task.urgency(), 4, 4), 6));
} }
// Create a third table, containing undo log change details.
Table journal;
setHeaderUnderline(journal);
if (Context::getContext().config.getBoolean("obfuscate")) journal.obfuscate();
if (Context::getContext().config.getBoolean("color")) journal.forceColor();
journal.width(Context::getContext().getWidth());
journal.add("Date");
journal.add("Modification");
if (Context::getContext().config.getBoolean("journal.info")) {
auto& replica = Context::getContext().tdb2.replica();
tc::Uuid tcuuid = tc::uuid_from_string(uuid);
auto tcoperations = replica->get_task_operations(tcuuid);
auto operations = Operation::operations(tcoperations);
// Sort by type (Create < Update < Delete < UndoPoint) and then by timestamp.
std::sort(operations.begin(), operations.end());
long last_timestamp = 0;
for (size_t i = 0; i < operations.size(); i++) {
auto& op = operations[i];
// Only display updates -- creation and deletion aren't interesting.
if (!op.is_update()) {
continue;
}
// Group operations that occur within 1s of this one. This is a heuristic
// for operations performed in the same `task` invocation, and allows e.g.,
// `task done end:-2h` to take the updated `end` value into account. It also
// groups these events into a single "row" of the table for better layout.
size_t group_start = i;
for (i++; i < operations.size(); i++) {
auto& op2 = operations[i];
if (!op2.is_update() || op2.get_timestamp() - op.get_timestamp() > 1) {
break;
}
}
size_t group_end = i;
i--;
std::optional<std::string> msg =
formatForInfo(operations, group_start, group_end, dateformat, last_timestamp);
if (!msg) {
continue;
}
int row = journal.addRow();
Datetime timestamp(op.get_timestamp());
journal.set(row, 0, timestamp.toString(dateformat));
journal.set(row, 1, *msg);
}
}
out << optionalBlankLine() << view.render() << '\n'; out << optionalBlankLine() << view.render() << '\n';
if (urgencyDetails.rows() > 0) out << urgencyDetails.render() << '\n'; if (urgencyDetails.rows() > 0) out << urgencyDetails.render() << '\n';
if (journal.rows() > 0) out << journal.render() << '\n';
} }
output = out.str(); output = out.str();
@@ -502,3 +564,105 @@ void CmdInfo::urgencyTerm(Table& view, const std::string& label, float measure,
} }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
std::optional<std::string> CmdInfo::formatForInfo(const std::vector<Operation>& operations,
size_t group_start, size_t group_end,
const std::string& dateformat, long& last_start) {
std::stringstream out;
for (auto i = group_start; i < group_end; i++) {
auto& operation = operations[i];
assert(operation.is_update());
// Extract the parts of the Update operation.
std::string prop = operation.get_property();
std::optional<std::string> value = operation.get_value();
std::optional<std::string> old_value = operation.get_old_value();
Datetime timestamp(operation.get_timestamp());
// Never care about modifying the modification time, or the legacy properties `depends` and
// `tags`.
if (prop == "modified" || prop == "depends" || prop == "tags") {
continue;
}
// Handle property deletions
if (!value && old_value) {
if (Task::isAnnotationAttr(prop)) {
out << format("Annotation '{1}' deleted.\n", *old_value);
} else if (Task::isTagAttr(prop)) {
out << format("Tag '{1}' deleted.\n", Task::attr2Tag(prop));
} else if (Task::isDepAttr(prop)) {
out << format("Dependency on '{1}' deleted.\n", Task::attr2Dep(prop));
} else if (prop == "start") {
Datetime started(last_start);
Datetime stopped = timestamp;
// If any update in this group sets the `end` property, use that instead of the
// timestamp deleting the `start` property as the stop time.
// See https://github.com/GothenburgBitFactory/taskwarrior/issues/2514
for (auto i = group_start; i < group_end; i++) {
auto& op = operations[i];
assert(op.is_update());
if (op.get_property() == "end") {
try {
stopped = op.get_value().value();
} catch (std::string) {
// Fall back to the 'start' timestamp if its value is un-parseable.
stopped = op.get_timestamp();
}
}
}
out << format("{1} deleted (duration: {2}).", Lexer::ucFirst(prop),
Duration(stopped - started).format())
<< "\n";
} else {
out << format("{1} deleted.\n", Lexer::ucFirst(prop));
}
}
// Handle property additions.
if (value && !old_value) {
if (Task::isAnnotationAttr(prop)) {
out << format("Annotation of '{1}' added.\n", *value);
} else if (Task::isTagAttr(prop)) {
out << format("Tag '{1}' added.\n", Task::attr2Tag(prop));
} else if (Task::isDepAttr(prop)) {
out << format("Dependency on '{1}' added.\n", Task::attr2Dep(prop));
} else {
// Record the last start time for later duration calculation.
if (prop == "start") {
last_start = Datetime(value.value()).toEpoch();
}
out << format("{1} set to '{2}'.", Lexer::ucFirst(prop),
renderAttribute(prop, *value, dateformat))
<< "\n";
}
}
// Handle property changes.
if (value && old_value) {
if (Task::isTagAttr(prop) || Task::isDepAttr(prop)) {
// Dependencies and tags do not have meaningful values.
} else if (Task::isAnnotationAttr(prop)) {
out << format("Annotation changed to '{1}'.\n", *value);
} else {
// Record the last start time for later duration calculation.
if (prop == "start") {
last_start = Datetime(value.value()).toEpoch();
}
out << format("{1} changed from '{2}' to '{3}'.", Lexer::ucFirst(prop),
renderAttribute(prop, *old_value, dateformat),
renderAttribute(prop, *value, dateformat))
<< "\n";
}
}
}
if (out.str().length() == 0) return std::nullopt;
return out.str();
}
////////////////////////////////////////////////////////////////////////////////

View File

@@ -28,9 +28,12 @@
#define INCLUDED_CMDINFO #define INCLUDED_CMDINFO
#include <Command.h> #include <Command.h>
#include <Operation.h>
#include <Table.h> #include <Table.h>
#include <taskchampion-cpp/lib.h>
#include <string> #include <string>
#include <vector>
class CmdInfo : public Command { class CmdInfo : public Command {
public: public:
@@ -39,6 +42,10 @@ class CmdInfo : public Command {
private: private:
void urgencyTerm(Table&, const std::string&, float, float) const; void urgencyTerm(Table&, const std::string&, float, float) const;
// Format a group of update operations for display in `task info`.
std::optional<std::string> formatForInfo(const std::vector<Operation>& operations,
size_t group_start, size_t group_end,
const std::string& dateformat, long& last_start);
}; };
#endif #endif

View File

@@ -116,6 +116,11 @@ void CmdModify::checkConsistency(Task &before, Task &after) {
if (before.has("recur") && (!after.has("recur") || after.get("recur") == "")) if (before.has("recur") && (!after.has("recur") || after.get("recur") == ""))
throw std::string("You cannot remove the recurrence from a recurring task."); throw std::string("You cannot remove the recurrence from a recurring task.");
if ((before.getStatus() == Task::pending) && (after.getStatus() == Task::pending) &&
(after.get("end") != ""))
throw format("Could not modify task {1}. You cannot set an end date on a pending task.",
before.identifier(true));
} }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////

View File

@@ -158,6 +158,8 @@ std::vector<NewsItem> NewsItem::all() {
version2_6_0(items); version2_6_0(items);
version3_0_0(items); version3_0_0(items);
version3_1_0(items); version3_1_0(items);
version3_2_0(items);
version3_3_0(items);
return items; return items;
} }
@@ -500,6 +502,34 @@ void NewsItem::version3_1_0(std::vector<NewsItem>& items) {
items.push_back(news); items.push_back(news);
} }
void NewsItem::version3_2_0(std::vector<NewsItem>& items) {
Version version("3.2.0");
NewsItem info{
version,
/*title=*/"`task info` Journal Restored",
/*bg_title=*/"",
/*background=*/"",
/*punchline=*/"",
/*update=*/
"Support for the \"journal\" output in `task info` has been restored. The command now\n"
"displays a list of changes made to the task, with timestamps.\n\n"};
items.push_back(info);
}
void NewsItem::version3_3_0(std::vector<NewsItem>& items) {
Version version("3.3.0");
NewsItem info{
version,
/*title=*/"AWS S3 Sync",
/*bg_title=*/"",
/*background=*/"",
/*punchline=*/"Use an AWS S3 bucket to sync Taskwarrior",
/*update=*/
"Taskwarrior now supports AWS as a backend for sync, in addition to existing support\n"
"for GCP and taskchampion-sync-server. See `man task-sync` for details.\n\n"};
items.push_back(info);
}
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
int CmdNews::execute(std::string& output) { int CmdNews::execute(std::string& output) {
auto words = Context::getContext().cli2.getWords(); auto words = Context::getContext().cli2.getWords();
@@ -563,7 +593,7 @@ int CmdNews::execute(std::string& output) {
std::cout << outro.str(); std::cout << outro.str();
// Set a mark in the config to remember which version's release notes were displayed // Set a mark in the config to remember which version's release notes were displayed
if (news_version != current_version) { if (news_version < current_version) {
CmdConfig::setConfigVariable("news.version", std::string(current_version), false); CmdConfig::setConfigVariable("news.version", std::string(current_version), false);
// Revert back to default signal handling after displaying the outro // Revert back to default signal handling after displaying the outro
@@ -600,3 +630,28 @@ int CmdNews::execute(std::string& output) {
return 0; return 0;
} }
bool CmdNews::should_nag() {
if (!Context::getContext().verbose("news")) {
return false;
}
Version news_version(Context::getContext().config.get("news.version"));
if (!news_version.is_valid()) news_version = Version("2.6.0");
Version current_version = Version::Current();
if (news_version >= current_version) {
return false;
}
// Check if there are actually any interesting news items to show.
std::vector<NewsItem> items = NewsItem::all();
for (auto& item : items) {
if (item._version > news_version) {
return true;
}
}
return false;
}

View File

@@ -51,6 +51,8 @@ class NewsItem {
static void version2_6_0(std::vector<NewsItem>&); static void version2_6_0(std::vector<NewsItem>&);
static void version3_0_0(std::vector<NewsItem>&); static void version3_0_0(std::vector<NewsItem>&);
static void version3_1_0(std::vector<NewsItem>&); static void version3_1_0(std::vector<NewsItem>&);
static void version3_2_0(std::vector<NewsItem>&);
static void version3_3_0(std::vector<NewsItem>&);
private: private:
NewsItem(Version, const std::string&, const std::string& = "", const std::string& = "", NewsItem(Version, const std::string&, const std::string& = "", const std::string& = "",
@@ -62,6 +64,8 @@ class CmdNews : public Command {
public: public:
CmdNews(); CmdNews();
int execute(std::string&); int execute(std::string&);
static bool should_nag();
}; };
#endif #endif

View File

@@ -194,15 +194,20 @@ int CmdShow::execute(std::string& output) {
" search.case.sensitive" " search.case.sensitive"
" sugar" " sugar"
" summary.all.projects" " summary.all.projects"
" sync.local.server_dir" " sync.aws.access_key_id"
" sync.aws.bucket"
" sync.aws.default_credentials"
" sync.aws.profile"
" sync.aws.region"
" sync.aws.secret_access_key"
" sync.gcp.credential_path" " sync.gcp.credential_path"
" sync.gcp.bucket" " sync.gcp.bucket"
" sync.local.server_dir"
" sync.server.client_id" " sync.server.client_id"
" sync.encryption_secret" " sync.encryption_secret"
" sync.server.url" " sync.server.url"
" sync.server.origin" " sync.server.origin"
" tag.indicator" " tag.indicator"
" undo.style"
" urgency.active.coefficient" " urgency.active.coefficient"
" urgency.scheduled.coefficient" " urgency.scheduled.coefficient"
" urgency.annotations.coefficient" " urgency.annotations.coefficient"

View File

@@ -35,12 +35,12 @@
#include <inttypes.h> #include <inttypes.h>
#include <shared.h> #include <shared.h>
#include <signal.h> #include <signal.h>
#include <taskchampion-cpp/lib.h>
#include <util.h> #include <util.h>
#include <iostream>
#include <sstream> #include <sstream>
#include "tc/Server.h"
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
CmdSync::CmdSync() { CmdSync::CmdSync() {
_keyword = "synchronize"; _keyword = "synchronize";
@@ -60,55 +60,99 @@ CmdSync::CmdSync() {
int CmdSync::execute(std::string& output) { int CmdSync::execute(std::string& output) {
int status = 0; int status = 0;
tc::Server server; Context& context = Context::getContext();
std::string server_ident; auto& replica = context.tdb2.replica();
std::stringstream out;
bool avoid_snapshots = false;
bool verbose = Context::getContext().verbose("sync");
// If no server is set up, quit.
std::string origin = Context::getContext().config.get("sync.server.origin"); std::string origin = Context::getContext().config.get("sync.server.origin");
std::string url = Context::getContext().config.get("sync.server.url"); std::string url = Context::getContext().config.get("sync.server.url");
std::string server_dir = Context::getContext().config.get("sync.local.server_dir"); std::string server_dir = Context::getContext().config.get("sync.local.server_dir");
std::string gcp_credential_path = Context::getContext().config.get("sync.gcp.credential_path"); std::string client_id = Context::getContext().config.get("sync.server.client_id");
std::string aws_bucket = Context::getContext().config.get("sync.aws.bucket");
std::string gcp_bucket = Context::getContext().config.get("sync.gcp.bucket"); std::string gcp_bucket = Context::getContext().config.get("sync.gcp.bucket");
std::string encryption_secret = Context::getContext().config.get("sync.encryption_secret"); std::string encryption_secret = Context::getContext().config.get("sync.encryption_secret");
// sync.server.origin is a deprecated synonym for sync.server.url // sync.server.origin is a deprecated synonym for sync.server.url
std::string server_url = url == "" ? origin : url; std::string server_url = url == "" ? origin : url;
if (server_dir != "") {
server = tc::Server::new_local(server_dir);
server_ident = server_dir;
} else if (gcp_bucket != "") {
if (encryption_secret == "") {
throw std::string("sync.encryption_secret is required");
}
server = tc::Server::new_gcp(gcp_bucket, gcp_credential_path, encryption_secret);
std::ostringstream os;
os << "GCP bucket " << gcp_bucket;
server_ident = os.str();
} else if (server_url != "") {
std::string client_id = Context::getContext().config.get("sync.server.client_id");
if (client_id == "" || encryption_secret == "") {
throw std::string("sync.server.client_id and sync.encryption_secret are required");
}
server = tc::Server::new_sync(server_url, client_id, encryption_secret);
std::ostringstream os;
os << "Sync server at " << server_url;
server_ident = os.str();
} else {
throw std::string("No sync.* settings are configured. See task-sync(5).");
}
std::stringstream out;
if (origin != "") { if (origin != "") {
out << "sync.server.origin is deprecated. Use sync.server.url instead.\n"; out << "sync.server.origin is deprecated. Use sync.server.url instead.\n";
} }
if (Context::getContext().verbose("sync")) { if (server_dir != "") {
out << format("Syncing with {1}", server_ident) << '\n'; if (verbose) {
out << format("Syncing with {1}", server_dir) << '\n';
}
replica->sync_to_local(server_dir, avoid_snapshots);
} else if (aws_bucket != "") {
std::string aws_region = Context::getContext().config.get("sync.aws.region");
std::string aws_profile = Context::getContext().config.get("sync.aws.profile");
std::string aws_access_key_id = Context::getContext().config.get("sync.aws.access_key_id");
std::string aws_secret_access_key =
Context::getContext().config.get("sync.aws.secret_access_key");
std::string aws_default_credentials =
Context::getContext().config.get("sync.aws.default_credentials");
if (aws_region == "") {
throw std::string("sync.aws.region is required");
}
if (encryption_secret == "") {
throw std::string("sync.encryption_secret is required");
} }
Context& context = Context::getContext(); bool using_profile = false;
context.tdb2.sync(std::move(server), false); bool using_creds = false;
bool using_default = false;
if (aws_profile != "") {
using_profile = true;
}
if (aws_access_key_id != "" || aws_secret_access_key != "") {
using_creds = true;
}
if (aws_default_credentials != "") {
using_default = true;
}
if (using_profile + using_creds + using_default != 1) {
throw std::string("exactly one method of specifying AWS credentials is required");
}
if (verbose) {
out << format("Syncing with AWS bucket {1}", aws_bucket) << '\n';
}
if (using_profile) {
replica->sync_to_aws_with_profile(aws_region, aws_bucket, aws_profile, encryption_secret,
avoid_snapshots);
} else if (using_creds) {
replica->sync_to_aws_with_access_key(aws_region, aws_bucket, aws_access_key_id,
aws_secret_access_key, encryption_secret,
avoid_snapshots);
} else {
replica->sync_to_aws_with_default_creds(aws_region, aws_bucket, encryption_secret,
avoid_snapshots);
}
} else if (gcp_bucket != "") {
std::string gcp_credential_path = Context::getContext().config.get("sync.gcp.credential_path");
if (encryption_secret == "") {
throw std::string("sync.encryption_secret is required");
}
if (verbose) {
out << format("Syncing with GCP bucket {1}", gcp_bucket) << '\n';
}
replica->sync_to_gcp(gcp_bucket, gcp_credential_path, encryption_secret, avoid_snapshots);
} else if (server_url != "") {
if (client_id == "" || encryption_secret == "") {
throw std::string("sync.server.client_id and sync.encryption_secret are required");
}
if (verbose) {
out << format("Syncing with sync server at {1}", server_url) << '\n';
}
replica->sync_to_remote(server_url, tc::uuid_from_string(client_id), encryption_secret,
avoid_snapshots);
} else {
throw std::string("No sync.* settings are configured. See task-sync(5).");
}
if (context.config.getBoolean("purge.on-sync")) { if (context.config.getBoolean("purge.on-sync")) {
context.tdb2.expire_tasks(); context.tdb2.expire_tasks();

View File

@@ -29,6 +29,13 @@
#include <CmdUndo.h> #include <CmdUndo.h>
#include <Context.h> #include <Context.h>
#include <Operation.h>
#include <Task.h>
#include <iostream>
#include <sstream>
#include "shared.h"
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
CmdUndo::CmdUndo() { CmdUndo::CmdUndo() {
@@ -47,8 +54,94 @@ CmdUndo::CmdUndo() {
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
int CmdUndo::execute(std::string&) { int CmdUndo::execute(std::string&) {
Context::getContext().tdb2.revert(); auto& replica = Context::getContext().tdb2.replica();
rust::Vec<tc::Operation> undo_ops = replica->get_undo_operations();
if (confirm_revert(Operation::operations(undo_ops))) {
// Note that commit_reversed_operations rebuilds the working set, so that
// need not be done here.
if (!replica->commit_reversed_operations(std::move(undo_ops))) {
std::cout << "Could not undo: other operations have occurred.";
}
}
return 0; return 0;
} }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
bool CmdUndo::confirm_revert(const std::vector<Operation>& undo_ops) {
// Count non-undo operations
int ops_count = 0;
for (auto& op : undo_ops) {
if (!op.is_undo_point()) {
ops_count++;
}
}
if (ops_count == 0) {
std::cout << "No operations to undo.\n";
return true;
}
std::cout << "The following " << ops_count << " operations would be reverted:\n";
Table view;
if (Context::getContext().config.getBoolean("obfuscate")) view.obfuscate();
view.width(Context::getContext().getWidth());
view.add("Uuid");
view.add("Modification");
std::string last_uuid;
std::stringstream mods;
for (auto& op : undo_ops) {
if (op.is_undo_point()) {
continue;
}
if (last_uuid != op.get_uuid()) {
if (last_uuid.size() != 0) {
int row = view.addRow();
view.set(row, 0, last_uuid);
view.set(row, 1, mods.str());
}
last_uuid = op.get_uuid();
mods.clear();
}
if (op.is_create()) {
mods << "Create task\n";
} else if (op.is_delete()) {
mods << "Delete (purge) task";
} else if (op.is_update()) {
auto property = op.get_property();
auto old_value = op.get_old_value();
auto value = op.get_value();
if (Task::isTagAttr(property)) {
if (value && *value == "x") {
mods << "Add tag '" << Task::attr2Tag(property) << "'\n";
continue;
} else if (!value && old_value && *old_value == "x") {
mods << "Remove tag '" << Task::attr2Tag(property) << "'\n";
continue;
}
}
if (old_value && value) {
mods << "Update property '" << property << "' from '" << *old_value << "' to '" << *value
<< "'\n";
} else if (old_value) {
mods << "Delete property '" << property << "' (was '" << *old_value << "')\n";
} else if (value) {
mods << "Add property '" << property << "' with value '" << *value << "'\n";
}
}
}
int row = view.addRow();
view.set(row, 0, last_uuid);
view.set(row, 1, mods.str());
std::cout << view.render() << "\n";
return !Context::getContext().config.getBoolean("confirmation") ||
confirm(
"The undo command is not reversible. Are you sure you want to revert to the previous "
"state?");
return true;
}
////////////////////////////////////////////////////////////////////////////////

View File

@@ -28,13 +28,18 @@
#define INCLUDED_CMDUNDO #define INCLUDED_CMDUNDO
#include <Command.h> #include <Command.h>
#include <Operation.h>
#include <string> #include <string>
#include <vector>
class CmdUndo : public Command { class CmdUndo : public Command {
public: public:
CmdUndo(); CmdUndo();
int execute(std::string &); int execute(std::string &);
private:
bool confirm_revert(const std::vector<Operation> &);
}; };
#endif #endif

View File

@@ -66,6 +66,7 @@
#include <CmdHistory.h> #include <CmdHistory.h>
#include <CmdIDs.h> #include <CmdIDs.h>
#include <CmdImport.h> #include <CmdImport.h>
#include <CmdImportV2.h>
#include <CmdInfo.h> #include <CmdInfo.h>
#include <CmdLog.h> #include <CmdLog.h>
#include <CmdLogo.h> #include <CmdLogo.h>
@@ -188,6 +189,8 @@ void Command::factory(std::map<std::string, Command*>& all) {
all[c->keyword()] = c; all[c->keyword()] = c;
c = new CmdImport(); c = new CmdImport();
all[c->keyword()] = c; all[c->keyword()] = c;
c = new CmdImportV2();
all[c->keyword()] = c;
c = new CmdInfo(); c = new CmdInfo();
all[c->keyword()] = c; all[c->keyword()] = c;
c = new CmdLog(); c = new CmdLog();

View File

@@ -28,6 +28,8 @@
// cmake.h include header must come first // cmake.h include header must come first
#include <Context.h> #include <Context.h>
#include <rust/cxx.h>
#include <signal.h>
#include <cstring> #include <cstring>
#include <iostream> #include <iostream>
@@ -38,6 +40,10 @@
int main(int argc, const char** argv) { int main(int argc, const char** argv) {
int status{0}; int status{0};
// Ignore SIGPIPE from writes to network sockets after the remote end has hung
// up. Rust code expects this, and the Rust runtime ignores this signal at startup.
signal(SIGPIPE, SIG_IGN);
Context globalContext; Context globalContext;
Context::setContext(&globalContext); Context::setContext(&globalContext);
@@ -55,6 +61,11 @@ int main(int argc, const char** argv) {
status = -1; status = -1;
} }
catch (rust::Error& err) {
std::cerr << err.what() << "\n";
status = -1;
}
catch (std::bad_alloc& error) { catch (std::bad_alloc& error) {
std::cerr << "Error: Memory allocation failed: " << error.what() << "\n"; std::cerr << "Error: Memory allocation failed: " << error.what() << "\n";
status = -3; status = -3;

View File

@@ -35,13 +35,15 @@
#include <algorithm> #include <algorithm>
#include <list> #include <list>
#include <map> #include <map>
#include <optional>
#include <string> #include <string>
#include <vector> #include <vector>
// recur.cpp // recur.cpp
void handleRecurrence(); void handleRecurrence();
void handleUntil(); void handleUntil();
Datetime getNextRecurrence(Datetime&, std::string&); std::optional<Datetime> checked_add_datetime(Datetime& base, time_t delta);
std::optional<Datetime> getNextRecurrence(Datetime&, std::string&);
bool generateDueDates(Task&, std::vector<Datetime>&); bool generateDueDates(Task&, std::vector<Datetime>&);
void updateRecurrenceMask(Task&); void updateRecurrenceMask(Task&);

View File

@@ -47,8 +47,24 @@
#include <fstream> #include <fstream>
#include <iomanip> #include <iomanip>
#include <iostream> #include <iostream>
#include <limits>
#include <optional>
#include <sstream> #include <sstream>
// Add a `time_t` delta to a Datetime, checking for and returning nullopt on integer overflow.
std::optional<Datetime> checked_add_datetime(Datetime& base, time_t delta) {
// Datetime::operator+ takes an integer delta, so check that range
if (static_cast<time_t>(std::numeric_limits<int>::max()) < delta) {
return std::nullopt;
}
// Check for time_t overflow in the Datetime.
if (std::numeric_limits<time_t>::max() - base.toEpoch() < delta) {
return std::nullopt;
}
return base + delta;
}
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
// Scans all tasks, and for any recurring tasks, determines whether any new // Scans all tasks, and for any recurring tasks, determines whether any new
// child tasks need to be generated to fill gaps. // child tasks need to be generated to fill gaps.
@@ -95,7 +111,12 @@ void handleRecurrence() {
Datetime old_wait(t.get_date("wait")); Datetime old_wait(t.get_date("wait"));
Datetime old_due(t.get_date("due")); Datetime old_due(t.get_date("due"));
Datetime due(d); Datetime due(d);
rec.set("wait", format((due + (old_wait - old_due)).toEpoch())); auto wait = checked_add_datetime(due, old_wait - old_due);
if (wait) {
rec.set("wait", format(wait->toEpoch()));
} else {
rec.remove("wait");
}
rec.setStatus(Task::waiting); rec.setStatus(Task::waiting);
mask += 'W'; mask += 'W';
} else { } else {
@@ -148,7 +169,8 @@ bool generateDueDates(Task& parent, std::vector<Datetime>& allDue) {
auto recurrence_limit = Context::getContext().config.getInteger("recurrence.limit"); auto recurrence_limit = Context::getContext().config.getInteger("recurrence.limit");
int recurrence_counter = 0; int recurrence_counter = 0;
Datetime now; Datetime now;
for (Datetime i = due;; i = getNextRecurrence(i, recur)) { Datetime i = due;
while (1) {
allDue.push_back(i); allDue.push_back(i);
if (specificEnd && i > until) { if (specificEnd && i > until) {
@@ -164,13 +186,23 @@ bool generateDueDates(Task& parent, std::vector<Datetime>& allDue) {
if (i > now) ++recurrence_counter; if (i > now) ++recurrence_counter;
if (recurrence_counter >= recurrence_limit) return true; if (recurrence_counter >= recurrence_limit) return true;
auto next = getNextRecurrence(i, recur);
if (next) {
i = *next;
} else {
return true;
}
} }
return true; return true;
} }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////
Datetime getNextRecurrence(Datetime& current, std::string& period) { /// Determine the next recurrence of the given period.
///
/// If no such date can be calculated, such as with a very large period, returns
/// nullopt.
std::optional<Datetime> getNextRecurrence(Datetime& current, std::string& period) {
auto m = current.month(); auto m = current.month();
auto d = current.day(); auto d = current.day();
auto y = current.year(); auto y = current.year();
@@ -201,7 +233,7 @@ Datetime getNextRecurrence(Datetime& current, std::string& period) {
else else
days = 1; days = 1;
return current + (days * 86400); return checked_add_datetime(current, days * 86400);
} }
else if (unicodeLatinDigit(period[0]) && period[period.length() - 1] == 'm') { else if (unicodeLatinDigit(period[0]) && period[period.length() - 1] == 'm') {
@@ -317,7 +349,7 @@ Datetime getNextRecurrence(Datetime& current, std::string& period) {
if (!p.parse(period, idx)) if (!p.parse(period, idx))
throw std::string(format("The recurrence value '{1}' is not valid.", period)); throw std::string(format("The recurrence value '{1}' is not valid.", period));
return current + p.toTime_t(); return checked_add_datetime(current, p.toTime_t());
} }
//////////////////////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////////////////////

View File

@@ -0,0 +1,29 @@
cmake_minimum_required (VERSION 3.22)
OPTION(SYSTEM_CORROSION "Use system provided corrosion instead of vendored version" OFF)
if(SYSTEM_CORROSION)
find_package(Corrosion REQUIRED)
else()
add_subdirectory(${CMAKE_SOURCE_DIR}/src/taskchampion-cpp/corrosion)
endif()
OPTION (ENABLE_TLS_NATIVE_ROOTS "Use the system's TLS root certificates" OFF)
if (ENABLE_TLS_NATIVE_ROOTS)
message ("Enabling native TLS roots")
set(TASKCHAMPION_FEATURES "tls-native-roots")
endif (ENABLE_TLS_NATIVE_ROOTS)
# Import taskchampion-lib as a CMake library. This implements the Rust side of
# the cxxbridge, and depends on the `taskchampion` crate.
corrosion_import_crate(
MANIFEST_PATH "${CMAKE_SOURCE_DIR}/Cargo.toml"
LOCKED
CRATES "taskchampion-lib"
FEATURES "${TASKCHAMPION_FEATURES}")
# Set up `taskchampion-cpp`, the C++ side of the bridge.
corrosion_add_cxxbridge(taskchampion-cpp
CRATE taskchampion_lib
FILES lib.rs
)

View File

@@ -0,0 +1,20 @@
[package]
name = "taskchampion-lib"
version = "0.1.0"
edition = "2021"
publish = false
rust-version = "1.81.0" # MSRV
[lib]
crate-type = ["staticlib"]
[dependencies]
taskchampion = "=2.0.2"
cxx = "1.0.133"
[features]
# use native CA roots, instead of bundled
tls-native-roots = ["taskchampion/tls-native-roots"]
[build-dependencies]
cxx-build = "1.0.133"

View File

@@ -0,0 +1,6 @@
#[allow(unused_must_use)]
fn main() {
cxx_build::bridge("src/lib.rs");
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed=src/lib.rs");
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,30 +0,0 @@
cmake_minimum_required (VERSION 3.22)
add_subdirectory(${CMAKE_SOURCE_DIR}/src/tc/corrosion)
# Import taskchampion-lib as a CMake library.
corrosion_import_crate(
MANIFEST_PATH "${CMAKE_SOURCE_DIR}/Cargo.toml"
LOCKED
CRATES "taskchampion-lib")
# TODO(#3425): figure out how to create taskchampion.h
include_directories (${CMAKE_SOURCE_DIR}
${CMAKE_SOURCE_DIR}/src
${CMAKE_SOURCE_DIR}/src/tc
${CMAKE_SOURCE_DIR}/src/tc/lib
${CMAKE_SOURCE_DIR}/src/libshared/src
${TASK_INCLUDE_DIRS})
set (tc_SRCS
ffi.h
lib/taskchampion.h
util.cpp util.h
Replica.cpp Replica.h
Server.cpp Server.h
WorkingSet.cpp WorkingSet.h
Task.cpp Task.h)
add_library (tc STATIC ${tc_SRCS})
target_link_libraries(tc taskchampion_lib)

View File

@@ -1,273 +0,0 @@
////////////////////////////////////////////////////////////////////////////////
//
// Copyright 2022, Dustin J. Mitchell
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// https://www.opensource.org/licenses/mit-license.php
//
////////////////////////////////////////////////////////////////////////////////
#include <cmake.h>
// cmake.h include header must come first
#include <format.h>
#include <iostream>
#include "tc/Replica.h"
#include "tc/Server.h"
#include "tc/Task.h"
#include "tc/WorkingSet.h"
#include "tc/util.h"
using namespace tc::ffi;
////////////////////////////////////////////////////////////////////////////////
tc::ReplicaGuard::ReplicaGuard(Replica &replica, Task &task) : replica(replica), task(task) {
// "steal" the reference from the Replica and store it locally, so that any
// attempt to use the Replica will fail
tcreplica = replica.inner.release();
task.to_mut(tcreplica);
}
////////////////////////////////////////////////////////////////////////////////
tc::ReplicaGuard::~ReplicaGuard() {
task.to_immut();
// return the reference to the Replica.
replica.inner.reset(tcreplica);
}
////////////////////////////////////////////////////////////////////////////////
tc::Replica::Replica() {
inner = unique_tcreplica_ptr(tc_replica_new_in_memory(),
[](TCReplica *rep) { tc_replica_free(rep); });
}
////////////////////////////////////////////////////////////////////////////////
tc::Replica::Replica(Replica &&other) noexcept {
// move inner from other
inner = unique_tcreplica_ptr(other.inner.release(), [](TCReplica *rep) { tc_replica_free(rep); });
}
////////////////////////////////////////////////////////////////////////////////
tc::Replica &tc::Replica::operator=(Replica &&other) noexcept {
if (this != &other) {
// move inner from other
inner =
unique_tcreplica_ptr(other.inner.release(), [](TCReplica *rep) { tc_replica_free(rep); });
}
return *this;
}
////////////////////////////////////////////////////////////////////////////////
tc::Replica::Replica(const std::string &dir, bool create_if_missing) {
TCString path = tc_string_borrow(dir.c_str());
TCString error;
auto tcreplica = tc_replica_new_on_disk(path, create_if_missing, &error);
if (!tcreplica) {
auto errmsg = format("Could not create replica at {1}: {2}", dir, tc_string_content(&error));
tc_string_free(&error);
throw errmsg;
}
inner = unique_tcreplica_ptr(tcreplica, [](TCReplica *rep) { tc_replica_free(rep); });
}
////////////////////////////////////////////////////////////////////////////////
tc::WorkingSet tc::Replica::working_set() {
TCWorkingSet *tcws = tc_replica_working_set(&*inner);
if (!tcws) {
throw replica_error();
}
return WorkingSet{tcws};
}
////////////////////////////////////////////////////////////////////////////////
std::optional<tc::Task> tc::Replica::get_task(const std::string &uuid) {
TCTask *tctask = tc_replica_get_task(&*inner, uuid2tc(uuid));
if (!tctask) {
auto error = tc_replica_error(&*inner);
if (error.ptr) {
throw replica_error(error);
} else {
return std::nullopt;
}
}
return std::make_optional(Task(tctask));
}
////////////////////////////////////////////////////////////////////////////////
tc::Task tc::Replica::new_task(tc::Status status, const std::string &description) {
TCTask *tctask = tc_replica_new_task(&*inner, (tc::ffi::TCStatus)status, string2tc(description));
if (!tctask) {
throw replica_error();
}
return Task(tctask);
}
////////////////////////////////////////////////////////////////////////////////
tc::Task tc::Replica::import_task_with_uuid(const std::string &uuid) {
TCTask *tctask = tc_replica_import_task_with_uuid(&*inner, uuid2tc(uuid));
if (!tctask) {
throw replica_error();
}
return Task(tctask);
}
////////////////////////////////////////////////////////////////////////////////
void tc::Replica::delete_task(const std::string &uuid) {
auto res = tc_replica_delete_task(&*inner, uuid2tc(uuid));
if (res != TC_RESULT_OK) {
throw replica_error();
}
}
////////////////////////////////////////////////////////////////////////////////
void tc::Replica::expire_tasks() {
auto res = tc_replica_expire_tasks(&*inner);
if (res != TC_RESULT_OK) {
throw replica_error();
}
}
////////////////////////////////////////////////////////////////////////////////
void tc::Replica::sync(Server server, bool avoid_snapshots) {
// The server remains owned by this function, per tc_replica_sync docs.
auto res = tc_replica_sync(&*inner, server.inner.get(), avoid_snapshots);
if (res != TC_RESULT_OK) {
throw replica_error();
}
}
////////////////////////////////////////////////////////////////////////////////
TCReplicaOpList tc::Replica::get_undo_ops() { return tc_replica_get_undo_ops(&*inner); }
////////////////////////////////////////////////////////////////////////////////
void tc::Replica::commit_undo_ops(TCReplicaOpList tc_undo_ops, int32_t *undone_out) {
auto res = tc_replica_commit_undo_ops(&*inner, tc_undo_ops, undone_out);
if (res != TC_RESULT_OK) {
throw replica_error();
}
}
////////////////////////////////////////////////////////////////////////////////
void tc::Replica::free_replica_ops(TCReplicaOpList tc_undo_ops) {
tc_replica_op_list_free(&tc_undo_ops);
}
////////////////////////////////////////////////////////////////////////////////
std::string tc::Replica::get_op_uuid(TCReplicaOp &tc_replica_op) const {
TCString uuid = tc_replica_op_get_uuid(&tc_replica_op);
return tc2string(uuid);
}
////////////////////////////////////////////////////////////////////////////////
std::string tc::Replica::get_op_property(TCReplicaOp &tc_replica_op) const {
TCString property = tc_replica_op_get_property(&tc_replica_op);
return tc2string(property);
}
////////////////////////////////////////////////////////////////////////////////
std::string tc::Replica::get_op_value(TCReplicaOp &tc_replica_op) const {
TCString value = tc_replica_op_get_value(&tc_replica_op);
return tc2string(value);
}
////////////////////////////////////////////////////////////////////////////////
std::string tc::Replica::get_op_old_value(TCReplicaOp &tc_replica_op) const {
TCString old_value = tc_replica_op_get_old_value(&tc_replica_op);
return tc2string(old_value);
}
////////////////////////////////////////////////////////////////////////////////
std::string tc::Replica::get_op_timestamp(TCReplicaOp &tc_replica_op) const {
TCString timestamp = tc_replica_op_get_timestamp(&tc_replica_op);
return tc2string(timestamp);
}
////////////////////////////////////////////////////////////////////////////////
std::string tc::Replica::get_op_old_task_description(TCReplicaOp &tc_replica_op) const {
TCString description = tc_replica_op_get_old_task_description(&tc_replica_op);
return tc2string(description);
}
////////////////////////////////////////////////////////////////////////////////
int64_t tc::Replica::num_local_operations() {
auto num = tc_replica_num_local_operations(&*inner);
if (num < 0) {
throw replica_error();
}
return num;
}
////////////////////////////////////////////////////////////////////////////////
int64_t tc::Replica::num_undo_points() {
auto num = tc_replica_num_undo_points(&*inner);
if (num < 0) {
throw replica_error();
}
return num;
}
////////////////////////////////////////////////////////////////////////////////
std::vector<tc::Task> tc::Replica::all_tasks() {
TCTaskList tasks = tc_replica_all_tasks(&*inner);
if (!tasks.items) {
throw replica_error();
}
std::vector<Task> all;
all.reserve(tasks.len);
for (size_t i = 0; i < tasks.len; i++) {
auto tctask = tc_task_list_take(&tasks, i);
if (tctask) {
all.push_back(Task(tctask));
}
}
return all;
}
////////////////////////////////////////////////////////////////////////////////
void tc::Replica::rebuild_working_set(bool force) {
auto res = tc_replica_rebuild_working_set(&*inner, force);
if (res != TC_RESULT_OK) {
throw replica_error();
}
}
////////////////////////////////////////////////////////////////////////////////
tc::ReplicaGuard tc::Replica::mutate_task(tc::Task &task) { return ReplicaGuard(*this, task); }
////////////////////////////////////////////////////////////////////////////////
std::string tc::Replica::replica_error() { return replica_error(tc_replica_error(&*inner)); }
////////////////////////////////////////////////////////////////////////////////
std::string tc::Replica::replica_error(TCString error) {
std::string errmsg;
if (!error.ptr) {
errmsg = std::string("Unknown TaskChampion error");
} else {
errmsg = std::string(tc_string_content(&error));
}
tc_string_free(&error);
return errmsg;
}
////////////////////////////////////////////////////////////////////////////////

View File

@@ -1,127 +0,0 @@
////////////////////////////////////////////////////////////////////////////////
//
// Copyright 2022, Dustin J. Mitchell
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// https://www.opensource.org/licenses/mit-license.php
//
////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDED_TC_REPLICA
#define INCLUDED_TC_REPLICA
#include <functional>
#include <memory>
#include <optional>
#include <string>
#include <vector>
#include "tc/Task.h"
#include "tc/ffi.h"
namespace tc {
class Task;
class WorkingSet;
class Server;
// a unique_ptr to a TCReplica which will automatically free the value when
// it goes out of scope.
using unique_tcreplica_ptr =
std::unique_ptr<tc::ffi::TCReplica, std::function<void(tc::ffi::TCReplica *)>>;
// ReplicaGuard uses RAII to ensure that a Replica is not accessed while it
// is mutably borrowed (specifically, to make a task mutable).
class ReplicaGuard {
protected:
friend class Replica;
explicit ReplicaGuard(Replica &, Task &);
public:
~ReplicaGuard();
// No moving or copying allowed
ReplicaGuard(const ReplicaGuard &) = delete;
ReplicaGuard &operator=(const ReplicaGuard &) = delete;
ReplicaGuard(ReplicaGuard &&) = delete;
ReplicaGuard &operator=(Replica &&) = delete;
private:
Replica &replica;
tc::ffi::TCReplica *tcreplica;
Task &task;
};
// Replica wraps the TCReplica type, managing its memory, errors, and so on.
//
// Except as noted, method names match the suffix to `tc_replica_..`.
class Replica {
public:
Replica(); // tc_replica_new_in_memory
Replica(const std::string &dir, bool create_if_missing); // tc_replica_new_on_disk
// This object "owns" inner, so copy is not allowed.
Replica(const Replica &) = delete;
Replica &operator=(const Replica &) = delete;
// Explicit move constructor and assignment
Replica(Replica &&) noexcept;
Replica &operator=(Replica &&) noexcept;
std::vector<tc::Task> all_tasks();
// TODO: struct TCUuidList tc_replica_all_task_uuids(struct TCReplica *rep);
tc::WorkingSet working_set();
std::optional<tc::Task> get_task(const std::string &uuid);
tc::Task new_task(Status status, const std::string &description);
tc::Task import_task_with_uuid(const std::string &uuid);
void delete_task(const std::string &uuid);
// TODO: struct TCTask *tc_replica_import_task_with_uuid(struct TCReplica *rep, struct TCUuid
// tcuuid);
void expire_tasks();
void sync(Server server, bool avoid_snapshots);
tc::ffi::TCReplicaOpList get_undo_ops();
void commit_undo_ops(tc::ffi::TCReplicaOpList tc_undo_ops, int32_t *undone_out);
void free_replica_ops(tc::ffi::TCReplicaOpList tc_undo_ops);
std::string get_op_uuid(tc::ffi::TCReplicaOp &tc_replica_op) const;
std::string get_op_property(tc::ffi::TCReplicaOp &tc_replica_op) const;
std::string get_op_value(tc::ffi::TCReplicaOp &tc_replica_op) const;
std::string get_op_old_value(tc::ffi::TCReplicaOp &tc_replica_op) const;
std::string get_op_timestamp(tc::ffi::TCReplicaOp &tc_replica_op) const;
std::string get_op_old_task_description(tc::ffi::TCReplicaOp &tc_replica_op) const;
int64_t num_local_operations();
int64_t num_undo_points();
// TODO: TCResult tc_replica_add_undo_point(struct TCReplica *rep, bool force);
void rebuild_working_set(bool force);
ReplicaGuard mutate_task(tc::Task &);
void immut_task(tc::Task &);
protected:
friend class ReplicaGuard;
unique_tcreplica_ptr inner;
// construct an error message from tc_replica_error, or from the given
// string retrieved from tc_replica_error.
std::string replica_error();
std::string replica_error(tc::ffi::TCString string);
};
} // namespace tc
#endif
////////////////////////////////////////////////////////////////////////////////

View File

@@ -1,109 +0,0 @@
////////////////////////////////////////////////////////////////////////////////
//
// Copyright 2022, Dustin J. Mitchell
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// https://www.opensource.org/licenses/mit-license.php
//
////////////////////////////////////////////////////////////////////////////////
#include <cmake.h>
// cmake.h include header must come first
#include <format.h>
#include "tc/Server.h"
#include "tc/util.h"
using namespace tc::ffi;
////////////////////////////////////////////////////////////////////////////////
tc::Server tc::Server::new_local(const std::string &server_dir) {
TCString tc_server_dir = tc_string_borrow(server_dir.c_str());
TCString error;
auto tcserver = tc_server_new_local(tc_server_dir, &error);
if (!tcserver) {
std::string errmsg = format("Could not configure local server at {1}: {2}", server_dir,
tc_string_content(&error));
tc_string_free(&error);
throw errmsg;
}
return Server(unique_tcserver_ptr(tcserver, [](TCServer *rep) { tc_server_free(rep); }));
}
////////////////////////////////////////////////////////////////////////////////
tc::Server tc::Server::new_sync(const std::string &url, const std::string &client_id,
const std::string &encryption_secret) {
TCString tc_url = tc_string_borrow(url.c_str());
TCString tc_client_id = tc_string_borrow(client_id.c_str());
TCString tc_encryption_secret = tc_string_borrow(encryption_secret.c_str());
TCUuid tc_client_uuid;
if (tc_uuid_from_str(tc_client_id, &tc_client_uuid) != TC_RESULT_OK) {
tc_string_free(&tc_url);
tc_string_free(&tc_encryption_secret);
throw format("client_id '{1}' is not a valid UUID", client_id);
}
TCString error;
auto tcserver = tc_server_new_sync(tc_url, tc_client_uuid, tc_encryption_secret, &error);
if (!tcserver) {
std::string errmsg = format("Could not configure connection to server at {1}: {2}", url,
tc_string_content(&error));
tc_string_free(&error);
throw errmsg;
}
return Server(unique_tcserver_ptr(tcserver, [](TCServer *rep) { tc_server_free(rep); }));
}
////////////////////////////////////////////////////////////////////////////////
tc::Server tc::Server::new_gcp(const std::string &bucket, const std::string &credential_path,
const std::string &encryption_secret) {
TCString tc_bucket = tc_string_borrow(bucket.c_str());
TCString tc_encryption_secret = tc_string_borrow(encryption_secret.c_str());
TCString tc_credential_path = tc_string_borrow(credential_path.c_str());
TCString error;
auto tcserver = tc_server_new_gcp(tc_bucket, tc_credential_path, tc_encryption_secret, &error);
if (!tcserver) {
std::string errmsg = format("Could not configure connection to GCP bucket {1}: {2}", bucket,
tc_string_content(&error));
tc_string_free(&error);
throw errmsg;
}
return Server(unique_tcserver_ptr(tcserver, [](TCServer *rep) { tc_server_free(rep); }));
}
////////////////////////////////////////////////////////////////////////////////
tc::Server::Server(tc::Server &&other) noexcept {
// move inner from other
inner = unique_tcserver_ptr(other.inner.release(), [](TCServer *rep) { tc_server_free(rep); });
}
////////////////////////////////////////////////////////////////////////////////
tc::Server &tc::Server::operator=(tc::Server &&other) noexcept {
if (this != &other) {
// move inner from other
inner = unique_tcserver_ptr(other.inner.release(), [](TCServer *rep) { tc_server_free(rep); });
}
return *this;
}
////////////////////////////////////////////////////////////////////////////////

View File

@@ -1,85 +0,0 @@
////////////////////////////////////////////////////////////////////////////////
//
// Copyright 2022, Dustin J. Mitchell
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// https://www.opensource.org/licenses/mit-license.php
//
////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDED_TC_SERVER
#define INCLUDED_TC_SERVER
#include <functional>
#include <memory>
#include <optional>
#include <string>
#include <vector>
#include "tc/ffi.h"
namespace tc {
// a unique_ptr to a TCServer which will automatically free the value when
// it goes out of scope.
using unique_tcserver_ptr =
std::unique_ptr<tc::ffi::TCServer, std::function<void(tc::ffi::TCServer *)>>;
// Server wraps the TCServer type, managing its memory, errors, and so on.
//
// Except as noted, method names match the suffix to `tc_server_..`.
class Server {
public:
// Construct a null server
Server() = default;
// Construct a local server (tc_server_new_local).
static Server new_local(const std::string &server_dir);
// Construct a remote server (tc_server_new_sync).
static Server new_sync(const std::string &url, const std::string &client_id,
const std::string &encryption_secret);
// Construct a GCP server (tc_server_new_gcp).
static Server new_gcp(const std::string &bucket, const std::string &credential_path,
const std::string &encryption_secret);
// This object "owns" inner, so copy is not allowed.
Server(const Server &) = delete;
Server &operator=(const Server &) = delete;
// Explicit move constructor and assignment
Server(Server &&) noexcept;
Server &operator=(Server &&) noexcept;
protected:
Server(unique_tcserver_ptr inner) : inner(std::move(inner)) {};
unique_tcserver_ptr inner;
// Replica accesses the inner pointer to call tc_replica_sync
friend class Replica;
// construct an error message from the given string.
std::string server_error(tc::ffi::TCString string);
};
} // namespace tc
#endif
////////////////////////////////////////////////////////////////////////////////

View File

@@ -1,162 +0,0 @@
////////////////////////////////////////////////////////////////////////////////
//
// Copyright 2022, Dustin J. Mitchell
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// https://www.opensource.org/licenses/mit-license.php
//
////////////////////////////////////////////////////////////////////////////////
#include <cmake.h>
// cmake.h include header must come first
#include <assert.h>
#include "tc/Task.h"
#include "tc/util.h"
using namespace tc::ffi;
////////////////////////////////////////////////////////////////////////////////
tc::Task::Task(TCTask* tctask) {
inner = unique_tctask_ptr(tctask, [](TCTask* task) { tc_task_free(task); });
}
////////////////////////////////////////////////////////////////////////////////
tc::Task::Task(Task&& other) noexcept {
// move inner from other
inner = unique_tctask_ptr(other.inner.release(), [](TCTask* task) { tc_task_free(task); });
}
////////////////////////////////////////////////////////////////////////////////
tc::Task& tc::Task::operator=(Task&& other) noexcept {
if (this != &other) {
// move inner from other
inner = unique_tctask_ptr(other.inner.release(), [](TCTask* task) { tc_task_free(task); });
}
return *this;
}
////////////////////////////////////////////////////////////////////////////////
void tc::Task::to_mut(TCReplica* replica) { tc_task_to_mut(&*inner, replica); }
////////////////////////////////////////////////////////////////////////////////
void tc::Task::to_immut() { tc_task_to_immut(&*inner); }
////////////////////////////////////////////////////////////////////////////////
std::string tc::Task::get_uuid() const {
auto uuid = tc_task_get_uuid(&*inner);
return tc2uuid(uuid);
}
////////////////////////////////////////////////////////////////////////////////
tc::Status tc::Task::get_status() const {
auto status = tc_task_get_status(&*inner);
return tc::Status(status);
}
////////////////////////////////////////////////////////////////////////////////
std::map<std::string, std::string> tc::Task::get_taskmap() const {
TCKVList kv = tc_task_get_taskmap(&*inner);
if (!kv.items) {
throw task_error();
}
std::map<std::string, std::string> taskmap;
for (size_t i = 0; i < kv.len; i++) {
auto k = tc2string_clone(kv.items[i].key);
auto v = tc2string_clone(kv.items[i].value);
taskmap[k] = v;
}
return taskmap;
}
////////////////////////////////////////////////////////////////////////////////
std::string tc::Task::get_description() const {
auto desc = tc_task_get_description(&*inner);
return tc2string(desc);
}
////////////////////////////////////////////////////////////////////////////////
std::optional<std::string> tc::Task::get_value(std::string property) const {
auto maybe_desc = tc_task_get_value(&*inner, string2tc(property));
if (maybe_desc.ptr == NULL) {
return std::nullopt;
}
return std::make_optional(tc2string(maybe_desc));
}
bool tc::Task::is_waiting() const { return tc_task_is_waiting(&*inner); }
////////////////////////////////////////////////////////////////////////////////
bool tc::Task::is_active() const { return tc_task_is_active(&*inner); }
////////////////////////////////////////////////////////////////////////////////
bool tc::Task::is_blocked() const { return tc_task_is_blocked(&*inner); }
////////////////////////////////////////////////////////////////////////////////
bool tc::Task::is_blocking() const { return tc_task_is_blocking(&*inner); }
////////////////////////////////////////////////////////////////////////////////
void tc::Task::set_status(tc::Status status) {
TCResult res = tc_task_set_status(&*inner, (TCStatus)status);
if (res != TC_RESULT_OK) {
throw task_error();
}
}
////////////////////////////////////////////////////////////////////////////////
void tc::Task::set_value(std::string property, std::optional<std::string> value) {
TCResult res;
if (value.has_value()) {
res = tc_task_set_value(&*inner, string2tc(property), string2tc(value.value()));
} else {
TCString nullstr;
nullstr.ptr = NULL;
res = tc_task_set_value(&*inner, string2tc(property), nullstr);
}
if (res != TC_RESULT_OK) {
throw task_error();
}
}
////////////////////////////////////////////////////////////////////////////////
void tc::Task::set_modified(time_t modified) {
TCResult res = tc_task_set_modified(&*inner, modified);
if (res != TC_RESULT_OK) {
throw task_error();
}
}
////////////////////////////////////////////////////////////////////////////////
std::string tc::Task::task_error() const {
TCString error = tc_task_error(&*inner);
std::string errmsg;
if (!error.ptr) {
errmsg = std::string("Unknown TaskChampion error");
} else {
errmsg = std::string(tc_string_content(&error));
}
tc_string_free(&error);
return errmsg;
}
////////////////////////////////////////////////////////////////////////////////

View File

@@ -1,130 +0,0 @@
////////////////////////////////////////////////////////////////////////////////
//
// Copyright 2022, Dustin J. Mitchell, Tomas Babej, Paul Beckingham, Federico Hernandez.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// https://www.opensource.org/licenses/mit-license.php
//
////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDED_TC_TASK
#define INCLUDED_TC_TASK
#include <functional>
#include <map>
#include <memory>
#include <optional>
#include <string>
#include "tc/ffi.h"
namespace tc {
class Replica;
class ReplicaGuard;
enum Status {
Pending = tc::ffi::TC_STATUS_PENDING,
Completed = tc::ffi::TC_STATUS_COMPLETED,
Deleted = tc::ffi::TC_STATUS_DELETED,
Recurring = tc::ffi::TC_STATUS_RECURRING,
Unknown = tc::ffi::TC_STATUS_UNKNOWN,
};
// a unique_ptr to a TCReplica which will automatically free the value when
// it goes out of scope.
using unique_tctask_ptr = std::unique_ptr<tc::ffi::TCTask, std::function<void(tc::ffi::TCTask *)>>;
// Task wraps the TCTask type, managing its memory, errors, and so on.
//
// Except as noted, method names match the suffix to `tc_task_..`.
class Task {
protected:
// Tasks may only be created and made mutable/immutable
// by tc::Replica
friend class tc::Replica;
explicit Task(tc::ffi::TCTask *);
// RplicaGuard handles mut/immut
friend class tc::ReplicaGuard;
void to_mut(tc::ffi::TCReplica *);
void to_immut();
public:
// This object "owns" inner, so copy is not allowed.
Task(const Task &) = delete;
Task &operator=(const Task &) = delete;
// Explicit move constructor and assignment
Task(Task &&) noexcept;
Task &operator=(Task &&) noexcept;
std::string get_uuid() const;
Status get_status() const;
std::map<std::string, std::string> get_taskmap() const;
std::string get_description() const;
std::optional<std::string> get_value(std::string property) const;
// TODO: time_t tc_task_get_entry(struct TCTask *task);
// TODO: time_t tc_task_get_wait(struct TCTask *task);
// TODO: time_t tc_task_get_modified(struct TCTask *task);
bool is_waiting() const;
bool is_active() const;
bool is_blocked() const;
bool is_blocking() const;
// TODO: bool tc_task_has_tag(struct TCTask *task, struct TCString tag);
// TODO: struct TCStringList tc_task_get_tags(struct TCTask *task);
// TODO: struct TCAnnotationList tc_task_get_annotations(struct TCTask *task);
// TODO: struct TCString tc_task_get_uda(struct TCTask *task, struct TCString ns, struct TCString
// key);
// TODO: struct TCString tc_task_get_legacy_uda(struct TCTask *task, struct TCString key);
// TODO: struct TCUdaList tc_task_get_udas(struct TCTask *task);
// TODO: struct TCUdaList tc_task_get_legacy_udas(struct TCTask *task);
void set_status(Status status);
// TODO: TCResult tc_task_set_description(struct TCTask *task, struct TCString description);
void set_value(std::string property, std::optional<std::string> value);
// TODO: TCResult tc_task_set_entry(struct TCTask *task, time_t entry);
// TODO: TCResult tc_task_set_wait(struct TCTask *task, time_t wait);
void set_modified(time_t modified);
// TODO: TCResult tc_task_start(struct TCTask *task);
// TODO: TCResult tc_task_stop(struct TCTask *task);
// TODO: TCResult tc_task_done(struct TCTask *task);
// TODO: TCResult tc_task_delete(struct TCTask *task);
// TODO: TCResult tc_task_add_tag(struct TCTask *task, struct TCString tag);
// TODO: TCResult tc_task_remove_tag(struct TCTask *task, struct TCString tag);
// TODO: TCResult tc_task_add_annotation(struct TCTask *task, struct TCAnnotation *annotation);
// TODO: TCResult tc_task_remove_annotation(struct TCTask *task, int64_t entry);
// TODO: TCResult tc_task_set_uda(struct TCTask *task,
// TODO: TCResult tc_task_remove_uda(struct TCTask *task, struct TCString ns, struct TCString
// key);
// TODO: TCResult tc_task_set_legacy_uda(struct TCTask *task, struct TCString key, struct TCString
// value);
// TODO: TCResult tc_task_remove_legacy_uda(struct TCTask *task, struct TCString key);
private:
unique_tctask_ptr inner;
std::string task_error() const; // tc_task_error
};
} // namespace tc
// TODO: struct TCTask *tc_task_list_take(struct TCTaskList *tasks, size_t index);
// TODO: void tc_task_list_free(struct TCTaskList *tasks);
#endif
////////////////////////////////////////////////////////////////////////////////

View File

@@ -1,87 +0,0 @@
////////////////////////////////////////////////////////////////////////////////
//
// Copyright 2022, Dustin J. Mitchell
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// https://www.opensource.org/licenses/mit-license.php
//
////////////////////////////////////////////////////////////////////////////////
#include <cmake.h>
// cmake.h include header must come first
#include <format.h>
#include "tc/Task.h"
#include "tc/WorkingSet.h"
#include "tc/util.h"
using namespace tc::ffi;
////////////////////////////////////////////////////////////////////////////////
tc::WorkingSet::WorkingSet(WorkingSet&& other) noexcept {
// move inner from other
inner = unique_tcws_ptr(other.inner.release(), [](TCWorkingSet* ws) { tc_working_set_free(ws); });
}
////////////////////////////////////////////////////////////////////////////////
tc::WorkingSet& tc::WorkingSet::operator=(WorkingSet&& other) noexcept {
if (this != &other) {
// move inner from other
inner =
unique_tcws_ptr(other.inner.release(), [](TCWorkingSet* ws) { tc_working_set_free(ws); });
}
return *this;
}
////////////////////////////////////////////////////////////////////////////////
tc::WorkingSet::WorkingSet(tc::ffi::TCWorkingSet* tcws) {
inner = unique_tcws_ptr(tcws, [](TCWorkingSet* ws) { tc_working_set_free(ws); });
}
////////////////////////////////////////////////////////////////////////////////
size_t tc::WorkingSet::len() const noexcept { return tc_working_set_len(&*inner); }
////////////////////////////////////////////////////////////////////////////////
size_t tc::WorkingSet::largest_index() const noexcept {
return tc_working_set_largest_index(&*inner);
}
////////////////////////////////////////////////////////////////////////////////
std::optional<std::string> tc::WorkingSet::by_index(size_t index) const noexcept {
TCUuid uuid;
if (tc_working_set_by_index(&*inner, index, &uuid)) {
return std::make_optional(tc2uuid(uuid));
} else {
return std::nullopt;
}
}
////////////////////////////////////////////////////////////////////////////////
std::optional<size_t> tc::WorkingSet::by_uuid(const std::string& uuid) const noexcept {
auto index = tc_working_set_by_uuid(&*inner, uuid2tc(uuid));
if (index > 0) {
return std::make_optional(index);
} else {
return std::nullopt;
}
}
////////////////////////////////////////////////////////////////////////////////

View File

@@ -1,74 +0,0 @@
////////////////////////////////////////////////////////////////////////////////
//
// Copyright 2022, Dustin J. Mitchell
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// https://www.opensource.org/licenses/mit-license.php
//
////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDED_TC_WORKINGSET
#define INCLUDED_TC_WORKINGSET
#include <functional>
#include <memory>
#include <optional>
#include <string>
#include "tc/Task.h"
#include "tc/ffi.h"
namespace tc {
class Task;
// a unique_ptr to a TCWorkingSet which will automatically free the value when
// it goes out of scope.
using unique_tcws_ptr =
std::unique_ptr<tc::ffi::TCWorkingSet, std::function<void(tc::ffi::TCWorkingSet *)>>;
// WorkingSet wraps the TCWorkingSet type, managing its memory, errors, and so on.
//
// Except as noted, method names match the suffix to `tc_working_set_..`.
class WorkingSet {
protected:
friend class tc::Replica;
WorkingSet(tc::ffi::TCWorkingSet *); // via tc_replica_working_set
public:
// This object "owns" inner, so copy is not allowed.
WorkingSet(const WorkingSet &) = delete;
WorkingSet &operator=(const WorkingSet &) = delete;
// Explicit move constructor and assignment
WorkingSet(WorkingSet &&) noexcept;
WorkingSet &operator=(WorkingSet &&) noexcept;
size_t len() const noexcept; // tc_working_set_len
size_t largest_index() const noexcept; // tc_working_set_largest_index
std::optional<std::string> by_index(size_t index) const noexcept; // tc_working_set_by_index
std::optional<size_t> by_uuid(const std::string &index) const noexcept; // tc_working_set_by_uuid
private:
unique_tcws_ptr inner;
};
} // namespace tc
#endif
////////////////////////////////////////////////////////////////////////////////

View File

@@ -1,18 +0,0 @@
[package]
name = "taskchampion-lib"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
name = "taskchampion_lib"
crate-type = ["staticlib", "rlib"]
[dependencies]
libc.workspace = true
anyhow.workspace = true
ffizz-header.workspace = true
taskchampion.workspace = true
[dev-dependencies]
pretty_assertions.workspace = true

View File

@@ -1,186 +0,0 @@
use crate::traits::*;
use crate::types::*;
use taskchampion::chrono::prelude::*;
#[ffizz_header::item]
#[ffizz(order = 400)]
/// ***** TCAnnotation *****
///
/// TCAnnotation contains the details of an annotation.
///
/// # Safety
///
/// An annotation must be initialized from a tc_.. function, and later freed
/// with `tc_annotation_free` or `tc_annotation_list_free`.
///
/// Any function taking a `*TCAnnotation` requires:
/// - the pointer must not be NUL;
/// - the pointer must be one previously returned from a tc_… function;
/// - the memory referenced by the pointer must never be modified by C code; and
/// - ownership transfers to the called function, and the value must not be used
/// after the call returns. In fact, the value will be zeroed out to ensure this.
///
/// TCAnnotations are not threadsafe.
///
/// ```c
/// typedef struct TCAnnotation {
/// // Time the annotation was made. Must be nonzero.
/// time_t entry;
/// // Content of the annotation. Must not be NULL.
/// TCString description;
/// } TCAnnotation;
/// ```
#[repr(C)]
pub struct TCAnnotation {
pub entry: libc::time_t,
pub description: TCString,
}
impl PassByValue for TCAnnotation {
// NOTE: we cannot use `RustType = Annotation` here because conversion of the
// Rust to a String can fail.
type RustType = (DateTime<Utc>, RustString<'static>);
unsafe fn from_ctype(mut self) -> Self::RustType {
// SAFETY:
// - any time_t value is valid
// - time_t is copy, so ownership is not important
let entry = unsafe { libc::time_t::val_from_arg(self.entry) }.unwrap();
// SAFETY:
// - self.description is valid (came from return_val in as_ctype)
// - self is owned, so we can take ownership of this TCString
let description =
unsafe { TCString::take_val_from_arg(&mut self.description, TCString::default()) };
(entry, description)
}
fn as_ctype((entry, description): Self::RustType) -> Self {
TCAnnotation {
entry: libc::time_t::as_ctype(Some(entry)),
// SAFETY:
// - ownership of the TCString tied to ownership of Self
description: unsafe { TCString::return_val(description) },
}
}
}
impl Default for TCAnnotation {
fn default() -> Self {
TCAnnotation {
entry: 0 as libc::time_t,
description: TCString::default(),
}
}
}
#[ffizz_header::item]
#[ffizz(order = 410)]
/// ***** TCAnnotationList *****
///
/// TCAnnotationList represents a list of annotations.
///
/// The content of this struct must be treated as read-only.
///
/// ```c
/// typedef struct TCAnnotationList {
/// // number of annotations in items
/// size_t len;
/// // reserved
/// size_t _u1;
/// // Array of annotations. These remain owned by the TCAnnotationList instance and will be freed by
/// // tc_annotation_list_free. This pointer is never NULL for a valid TCAnnotationList.
/// struct TCAnnotation *items;
/// } TCAnnotationList;
/// ```
#[repr(C)]
pub struct TCAnnotationList {
len: libc::size_t,
/// total size of items (internal use only)
capacity: libc::size_t,
items: *mut TCAnnotation,
}
impl CList for TCAnnotationList {
type Element = TCAnnotation;
unsafe fn from_raw_parts(items: *mut Self::Element, len: usize, cap: usize) -> Self {
TCAnnotationList {
len,
capacity: cap,
items,
}
}
fn slice(&mut self) -> &mut [Self::Element] {
// SAFETY:
// - because we have &mut self, we have read/write access to items[0..len]
// - all items are properly initialized Element's
// - return value lifetime is equal to &mmut self's, so access is exclusive
// - items and len came from Vec, so total size is < isize::MAX
unsafe { std::slice::from_raw_parts_mut(self.items, self.len) }
}
fn into_raw_parts(self) -> (*mut Self::Element, usize, usize) {
(self.items, self.len, self.capacity)
}
}
#[ffizz_header::item]
#[ffizz(order = 401)]
/// Free a TCAnnotation instance. The instance, and the TCString it contains, must not be used
/// after this call.
///
/// ```c
/// EXTERN_C void tc_annotation_free(struct TCAnnotation *tcann);
/// ```
#[no_mangle]
pub unsafe extern "C" fn tc_annotation_free(tcann: *mut TCAnnotation) {
debug_assert!(!tcann.is_null());
// SAFETY:
// - tcann is not NULL
// - *tcann is a valid TCAnnotation (caller promised to treat it as read-only)
let annotation = unsafe { TCAnnotation::take_val_from_arg(tcann, TCAnnotation::default()) };
drop(annotation);
}
#[ffizz_header::item]
#[ffizz(order = 411)]
/// Free a TCAnnotationList instance. The instance, and all TCAnnotations it contains, must not be used after
/// this call.
///
/// When this call returns, the `items` pointer will be NULL, signalling an invalid TCAnnotationList.
///
/// ```c
/// EXTERN_C void tc_annotation_list_free(struct TCAnnotationList *tcanns);
/// ```
#[no_mangle]
pub unsafe extern "C" fn tc_annotation_list_free(tcanns: *mut TCAnnotationList) {
// SAFETY:
// - tcanns is not NULL and points to a valid TCAnnotationList (caller is not allowed to
// modify the list)
// - caller promises not to use the value after return
unsafe { drop_value_list(tcanns) }
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn empty_list_has_non_null_pointer() {
let tcanns = unsafe { TCAnnotationList::return_val(Vec::new()) };
assert!(!tcanns.items.is_null());
assert_eq!(tcanns.len, 0);
assert_eq!(tcanns.capacity, 0);
}
#[test]
fn free_sets_null_pointer() {
let mut tcanns = unsafe { TCAnnotationList::return_val(Vec::new()) };
// SAFETY: testing expected behavior
unsafe { tc_annotation_list_free(&mut tcanns) };
assert!(tcanns.items.is_null());
assert_eq!(tcanns.len, 0);
assert_eq!(tcanns.capacity, 0);
}
}

View File

@@ -1,34 +0,0 @@
//! Trait implementations for a few atomic types
use crate::traits::*;
use taskchampion::chrono::{DateTime, Utc};
use taskchampion::utc_timestamp;
impl PassByValue for usize {
type RustType = usize;
unsafe fn from_ctype(self) -> usize {
self
}
fn as_ctype(arg: usize) -> usize {
arg
}
}
/// Convert an Option<DateTime<Utc>> to a libc::time_t, or zero if not set.
impl PassByValue for libc::time_t {
type RustType = Option<DateTime<Utc>>;
unsafe fn from_ctype(self) -> Option<DateTime<Utc>> {
if self != 0 {
return Some(utc_timestamp(self));
}
None
}
fn as_ctype(arg: Option<DateTime<Utc>>) -> libc::time_t {
arg.map(|ts| ts.timestamp() as libc::time_t)
.unwrap_or(0 as libc::time_t)
}
}

View File

@@ -1,155 +0,0 @@
use crate::traits::*;
use crate::types::*;
#[ffizz_header::item]
#[ffizz(order = 600)]
/// ***** TCKV *****
///
/// TCKV contains a key/value pair that is part of a task.
///
/// Neither key nor value are ever NULL. They remain owned by the TCKV and
/// will be freed when it is freed with tc_kv_list_free.
///
/// ```c
/// typedef struct TCKV {
/// struct TCString key;
/// struct TCString value;
/// } TCKV;
/// ```
#[repr(C)]
#[derive(Debug)]
pub struct TCKV {
pub key: TCString,
pub value: TCString,
}
impl PassByValue for TCKV {
type RustType = (RustString<'static>, RustString<'static>);
unsafe fn from_ctype(self) -> Self::RustType {
// SAFETY:
// - self.key is not NULL (field docstring)
// - self.key came from return_ptr in as_ctype
// - self is owned, so we can take ownership of this TCString
let key = unsafe { TCString::val_from_arg(self.key) };
// SAFETY: (same)
let value = unsafe { TCString::val_from_arg(self.value) };
(key, value)
}
fn as_ctype((key, value): Self::RustType) -> Self {
TCKV {
// SAFETY:
// - ownership of the TCString tied to ownership of Self
key: unsafe { TCString::return_val(key) },
// SAFETY:
// - ownership of the TCString tied to ownership of Self
value: unsafe { TCString::return_val(value) },
}
}
}
#[ffizz_header::item]
#[ffizz(order = 610)]
/// ***** TCKVList *****
///
/// TCKVList represents a list of key/value pairs.
///
/// The content of this struct must be treated as read-only.
///
/// ```c
/// typedef struct TCKVList {
/// // number of key/value pairs in items
/// size_t len;
/// // reserved
/// size_t _u1;
/// // Array of TCKV's. These remain owned by the TCKVList instance and will be freed by
/// // tc_kv_list_free. This pointer is never NULL for a valid TCKVList.
/// struct TCKV *items;
/// } TCKVList;
/// ```
#[repr(C)]
#[derive(Debug)]
pub struct TCKVList {
pub len: libc::size_t,
/// total size of items (internal use only)
pub _capacity: libc::size_t,
pub items: *mut TCKV,
}
impl CList for TCKVList {
type Element = TCKV;
unsafe fn from_raw_parts(items: *mut Self::Element, len: usize, cap: usize) -> Self {
TCKVList {
len,
_capacity: cap,
items,
}
}
fn slice(&mut self) -> &mut [Self::Element] {
// SAFETY:
// - because we have &mut self, we have read/write access to items[0..len]
// - all items are properly initialized Element's
// - return value lifetime is equal to &mmut self's, so access is exclusive
// - items and len came from Vec, so total size is < isize::MAX
unsafe { std::slice::from_raw_parts_mut(self.items, self.len) }
}
fn into_raw_parts(self) -> (*mut Self::Element, usize, usize) {
(self.items, self.len, self._capacity)
}
}
impl Default for TCKVList {
fn default() -> Self {
// SAFETY:
// - caller will free this list
unsafe { TCKVList::return_val(Vec::new()) }
}
}
// NOTE: callers never have a TCKV that is not in a list, so there is no tc_kv_free.
#[ffizz_header::item]
#[ffizz(order = 611)]
/// Free a TCKVList instance. The instance, and all TCKVs it contains, must not be used after
/// this call.
///
/// When this call returns, the `items` pointer will be NULL, signalling an invalid TCKVList.
///
/// ```c
/// EXTERN_C void tc_kv_list_free(struct TCKVList *tckvs);
/// ```
#[no_mangle]
pub unsafe extern "C" fn tc_kv_list_free(tckvs: *mut TCKVList) {
// SAFETY:
// - tckvs is not NULL and points to a valid TCKVList (caller is not allowed to
// modify the list)
// - caller promises not to use the value after return
unsafe { drop_value_list(tckvs) }
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn empty_list_has_non_null_pointer() {
let tckvs = unsafe { TCKVList::return_val(Vec::new()) };
assert!(!tckvs.items.is_null());
assert_eq!(tckvs.len, 0);
assert_eq!(tckvs._capacity, 0);
}
#[test]
fn free_sets_null_pointer() {
let mut tckvs = unsafe { TCKVList::return_val(Vec::new()) };
// SAFETY: testing expected behavior
unsafe { tc_kv_list_free(&mut tckvs) };
assert!(tckvs.items.is_null());
assert_eq!(tckvs.len, 0);
assert_eq!(tckvs._capacity, 0);
}
}

View File

@@ -1,172 +0,0 @@
#![warn(unsafe_op_in_unsafe_fn)]
#![allow(unused_unsafe)]
// Not working yet in stable - https://github.com/rust-lang/rust-clippy/issues/8020
// #![warn(clippy::undocumented_unsafe_blocks)]
// docstrings for extern "C" functions are reflected into C, and do not benefit
// from safety docs.
#![allow(clippy::missing_safety_doc)]
// deny some things that are typically warnings
#![deny(clippy::derivable_impls)]
#![deny(clippy::wrong_self_convention)]
#![deny(clippy::extra_unused_lifetimes)]
#![deny(clippy::unnecessary_to_owned)]
// ffizz_header orders:
//
// 000-099: header matter
// 100-199: TCResult
// 200-299: TCString / List
// 300-399: TCUuid / List
// 400-499: TCAnnotation / List
// 500-599: TCUda / List
// 600-699: TCKV / List
// 700-799: TCStatus
// 800-899: TCServer
// 900-999: TCReplica
// 1000-1099: TCTask / List
// 1100-1199: TCWorkingSet
// 10000-10099: footer
ffizz_header::snippet! {
#[ffizz(name="intro", order=0)]
/// TaskChampion
///
/// This file defines the C interface to libtaskchampion. This is a thin wrapper around the Rust
/// `taskchampion` crate. Refer to the documentation for that crate at
/// https://docs.rs/taskchampion/latest/taskchampion/ for API details. The comments in this file
/// focus mostly on the low-level details of passing values to and from TaskChampion.
///
/// # Overview
///
/// This library defines four major types used to interact with the API, that map directly to Rust
/// types.
///
/// * TCReplica - see https://docs.rs/taskchampion/latest/taskchampion/struct.Replica.html
/// * TCTask - see https://docs.rs/taskchampion/latest/taskchampion/struct.Task.html
/// * TCServer - see https://docs.rs/taskchampion/latest/taskchampion/trait.Server.html
/// * TCWorkingSet - see https://docs.rs/taskchampion/latest/taskchampion/struct.WorkingSet.html
///
/// It also defines a few utility types:
///
/// * TCString - a wrapper around both C (NUL-terminated) and Rust (always utf-8) strings.
/// * TC…List - a list of objects represented as a C array
/// * see below for the remainder
///
/// # Safety
///
/// Each type contains specific instructions to ensure memory safety. The general rules are as
/// follows.
///
/// No types in this library are threadsafe. All values should be used in only one thread for their
/// entire lifetime. It is safe to use unrelated values in different threads (for example,
/// different threads may use different TCReplica values concurrently).
///
/// ## Pass by Pointer
///
/// Several types such as TCReplica and TCString are "opaque" types and always handled as pointers
/// in C. The bytes these pointers address are private to the Rust implementation and must not be
/// accessed from C.
///
/// Pass-by-pointer values have exactly one owner, and that owner is responsible for freeing the
/// value (using a `tc_…_free` function), or transferring ownership elsewhere. Except where
/// documented otherwise, when a value is passed to C, ownership passes to C as well. When a value
/// is passed to Rust, ownership stays with the C code. The exception is TCString, ownership of
/// which passes to Rust when it is used as a function argument.
///
/// The limited circumstances where one value must not outlive another, due to pointer references
/// between them, are documented below.
///
/// ## Pass by Value
///
/// Types such as TCUuid and TC…List are passed by value, and contain fields that are accessible
/// from C. C code is free to access the content of these types in a _read_only_ fashion.
///
/// Pass-by-value values that contain pointers also have exactly one owner, responsible for freeing
/// the value or transferring ownership. The tc_…_free functions for these types will replace the
/// pointers with NULL to guard against use-after-free errors. The interior pointers in such values
/// should never be freed directly (for example, `tc_string_free(tcuda.value)` is an error).
///
/// TCUuid is a special case, because it does not contain pointers. It can be freely copied and
/// need not be freed.
///
/// ## Lists
///
/// Lists are a special kind of pass-by-value type. Each contains `len` and `items`, where `items`
/// is an array of length `len`. Lists, and the values in the `items` array, must be treated as
/// read-only. On return from an API function, a list's ownership is with the C caller, which must
/// eventually free the list. List data must be freed with the `tc_…_list_free` function. It is an
/// error to free any value in the `items` array of a list.
}
ffizz_header::snippet! {
#[ffizz(name="topmatter", order=1)]
/// ```c
/// #ifndef TASKCHAMPION_H
/// #define TASKCHAMPION_H
///
/// #include <stdbool.h>
/// #include <stdint.h>
/// #include <time.h>
///
/// #ifdef __cplusplus
/// #define EXTERN_C extern "C"
/// #else
/// #define EXTERN_C
/// #endif // __cplusplus
/// ```
}
ffizz_header::snippet! {
#[ffizz(name="bottomatter", order=10000)]
/// ```c
/// #endif /* TASKCHAMPION_H */
/// ```
}
mod traits;
mod util;
pub mod annotation;
pub use annotation::*;
pub mod atomic;
pub mod kv;
pub use kv::*;
pub mod replica;
pub use replica::*;
pub mod result;
pub use result::*;
pub mod server;
pub use server::*;
pub mod status;
pub use status::*;
pub mod string;
pub use string::*;
pub mod task;
pub use task::*;
pub mod uda;
pub use uda::*;
pub mod uuid;
pub use uuid::*;
pub mod workingset;
pub use workingset::*;
pub(crate) mod types {
pub(crate) use crate::annotation::{TCAnnotation, TCAnnotationList};
pub(crate) use crate::kv::TCKVList;
pub(crate) use crate::replica::TCReplica;
pub(crate) use crate::result::TCResult;
pub(crate) use crate::server::TCServer;
pub(crate) use crate::status::TCStatus;
pub(crate) use crate::string::{RustString, TCString, TCStringList};
pub(crate) use crate::task::{TCTask, TCTaskList};
pub(crate) use crate::uda::{TCUda, TCUdaList, Uda};
pub(crate) use crate::uuid::{TCUuid, TCUuidList};
pub(crate) use crate::workingset::TCWorkingSet;
}
#[cfg(debug_assertions)]
/// Generate the taskchapion.h header
pub fn generate_header() -> String {
ffizz_header::generate()
}

View File

@@ -1,958 +0,0 @@
use crate::traits::*;
use crate::types::*;
use crate::util::err_to_ruststring;
use std::ptr::NonNull;
use taskchampion::storage::ReplicaOp;
use taskchampion::{Replica, StorageConfig};
#[ffizz_header::item]
#[ffizz(order = 900)]
/// ***** TCReplica *****
///
/// A replica represents an instance of a user's task data, providing an easy interface
/// for querying and modifying that data.
///
/// # Error Handling
///
/// When a `tc_replica_..` function that returns a TCResult returns TC_RESULT_ERROR, then
/// `tc_replica_error` will return the error message.
///
/// # Safety
///
/// The `*TCReplica` returned from `tc_replica_new…` functions is owned by the caller and
/// must later be freed to avoid a memory leak.
///
/// Any function taking a `*TCReplica` requires:
/// - the pointer must not be NUL;
/// - the pointer must be one previously returned from a tc_… function;
/// - the memory referenced by the pointer must never be modified by C code; and
/// - except for `tc_replica_free`, ownership of a `*TCReplica` remains with the caller.
///
/// Once passed to `tc_replica_free`, a `*TCReplica` becomes invalid and must not be used again.
///
/// TCReplicas are not threadsafe.
///
/// ```c
/// typedef struct TCReplica TCReplica;
/// ```
pub struct TCReplica {
/// The wrapped Replica
inner: Replica,
/// If true, this replica has an outstanding &mut (for a TaskMut)
mut_borrowed: bool,
/// The error from the most recent operation, if any
error: Option<RustString<'static>>,
}
impl PassByPointer for TCReplica {}
impl TCReplica {
/// Mutably borrow the inner Replica
pub(crate) fn borrow_mut(&mut self) -> &mut Replica {
if self.mut_borrowed {
panic!("replica is already borrowed");
}
self.mut_borrowed = true;
&mut self.inner
}
/// Release the borrow made by [`borrow_mut`]
pub(crate) fn release_borrow(&mut self) {
if !self.mut_borrowed {
panic!("replica is not borrowed");
}
self.mut_borrowed = false;
}
}
impl From<Replica> for TCReplica {
fn from(rep: Replica) -> TCReplica {
TCReplica {
inner: rep,
mut_borrowed: false,
error: None,
}
}
}
/// Utility function to allow using `?` notation to return an error value. This makes
/// a mutable borrow, because most Replica methods require a `&mut`.
fn wrap<T, F>(rep: *mut TCReplica, f: F, err_value: T) -> T
where
F: FnOnce(&mut Replica) -> anyhow::Result<T>,
{
debug_assert!(!rep.is_null());
// SAFETY:
// - rep is not NULL (promised by caller)
// - *rep is a valid TCReplica (promised by caller)
// - rep is valid for the duration of this function
// - rep is not modified by anything else (not threadsafe)
let rep: &mut TCReplica = unsafe { TCReplica::from_ptr_arg_ref_mut(rep) };
if rep.mut_borrowed {
panic!("replica is borrowed and cannot be used");
}
rep.error = None;
match f(&mut rep.inner) {
Ok(v) => v,
Err(e) => {
rep.error = Some(err_to_ruststring(e));
err_value
}
}
}
/// Utility function to allow using `?` notation to return an error value in the constructor.
fn wrap_constructor<T, F>(f: F, error_out: *mut TCString, err_value: T) -> T
where
F: FnOnce() -> anyhow::Result<T>,
{
if !error_out.is_null() {
// SAFETY:
// - error_out is not NULL (just checked)
// - properly aligned and valid (promised by caller)
unsafe { *error_out = TCString::default() };
}
match f() {
Ok(v) => v,
Err(e) => {
if !error_out.is_null() {
// SAFETY:
// - error_out is not NULL (just checked)
// - properly aligned and valid (promised by caller)
unsafe {
TCString::val_to_arg_out(err_to_ruststring(e), error_out);
}
}
err_value
}
}
}
#[ffizz_header::item]
#[ffizz(order = 900)]
/// ***** TCReplicaOpType *****
///
/// ```c
/// enum TCReplicaOpType
/// #ifdef __cplusplus
/// : uint32_t
/// #endif // __cplusplus
/// {
/// Create = 0,
/// Delete = 1,
/// Update = 2,
/// UndoPoint = 3,
/// };
/// #ifndef __cplusplus
/// typedef uint32_t TCReplicaOpType;
/// #endif // __cplusplus
/// ```
#[derive(Debug, Default)]
#[repr(u32)]
pub enum TCReplicaOpType {
Create = 0,
Delete = 1,
Update = 2,
UndoPoint = 3,
#[default]
Error = 4,
}
#[ffizz_header::item]
#[ffizz(order = 901)]
/// Create a new TCReplica with an in-memory database. The contents of the database will be
/// lost when it is freed with tc_replica_free.
///
/// ```c
/// EXTERN_C struct TCReplica *tc_replica_new_in_memory(void);
/// ```
#[no_mangle]
pub unsafe extern "C" fn tc_replica_new_in_memory() -> *mut TCReplica {
let storage = StorageConfig::InMemory
.into_storage()
.expect("in-memory always succeeds");
// SAFETY:
// - caller promises to free this value
unsafe { TCReplica::from(Replica::new(storage)).return_ptr() }
}
#[ffizz_header::item]
#[ffizz(order = 901)]
/// Create a new TCReplica with an on-disk database having the given filename. On error, a string
/// is written to the error_out parameter (if it is not NULL) and NULL is returned. The caller
/// must free this string.
///
/// ```c
/// EXTERN_C struct TCReplica *tc_replica_new_on_disk(struct TCString path,
/// bool create_if_missing,
/// struct TCString *error_out);
/// ```
#[no_mangle]
pub unsafe extern "C" fn tc_replica_new_on_disk(
path: TCString,
create_if_missing: bool,
error_out: *mut TCString,
) -> *mut TCReplica {
wrap_constructor(
|| {
// SAFETY:
// - path is valid (promised by caller)
// - caller will not use path after this call (convention)
let mut path = unsafe { TCString::val_from_arg(path) };
let storage = StorageConfig::OnDisk {
taskdb_dir: path.to_path_buf_mut()?,
create_if_missing,
}
.into_storage()?;
// SAFETY:
// - caller promises to free this value
Ok(unsafe { TCReplica::from(Replica::new(storage)).return_ptr() })
},
error_out,
std::ptr::null_mut(),
)
}
#[ffizz_header::item]
#[ffizz(order = 901)]
/// ***** TCReplicaOp *****
///
/// ```c
/// struct TCReplicaOp {
/// TCReplicaOpType operation_type;
/// void* inner;
/// };
///
/// typedef struct TCReplicaOp TCReplicaOp;
/// ```
#[derive(Debug)]
#[repr(C)]
pub struct TCReplicaOp {
operation_type: TCReplicaOpType,
inner: Box<ReplicaOp>,
}
impl From<ReplicaOp> for TCReplicaOp {
fn from(replica_op: ReplicaOp) -> TCReplicaOp {
match replica_op {
ReplicaOp::Create { .. } => TCReplicaOp {
operation_type: TCReplicaOpType::Create,
inner: Box::new(replica_op),
},
ReplicaOp::Delete { .. } => TCReplicaOp {
operation_type: TCReplicaOpType::Delete,
inner: Box::new(replica_op),
},
ReplicaOp::Update { .. } => TCReplicaOp {
operation_type: TCReplicaOpType::Update,
inner: Box::new(replica_op),
},
ReplicaOp::UndoPoint => TCReplicaOp {
operation_type: TCReplicaOpType::UndoPoint,
inner: Box::new(replica_op),
},
}
}
}
#[ffizz_header::item]
#[ffizz(order = 901)]
/// ***** TCReplicaOpList *****
///
/// ```c
/// struct TCReplicaOpList {
/// struct TCReplicaOp *items;
/// size_t len;
/// size_t capacity;
/// };
///
/// typedef struct TCReplicaOpList TCReplicaOpList;
/// ```
#[repr(C)]
#[derive(Debug)]
pub struct TCReplicaOpList {
items: *mut TCReplicaOp,
len: usize,
capacity: usize,
}
impl Default for TCReplicaOpList {
fn default() -> Self {
// SAFETY:
// - caller will free this value
unsafe { TCReplicaOpList::return_val(Vec::new()) }
}
}
impl CList for TCReplicaOpList {
type Element = TCReplicaOp;
unsafe fn from_raw_parts(items: *mut Self::Element, len: usize, cap: usize) -> Self {
TCReplicaOpList {
len,
capacity: cap,
items,
}
}
fn slice(&mut self) -> &mut [Self::Element] {
// SAFETY:
// - because we have &mut self, we have read/write access to items[0..len]
// - all items are properly initialized Element's
// - return value lifetime is equal to &mmut self's, so access is exclusive
// - items and len came from Vec, so total size is < isize::MAX
unsafe { std::slice::from_raw_parts_mut(self.items, self.len) }
}
fn into_raw_parts(self) -> (*mut Self::Element, usize, usize) {
(self.items, self.len, self.capacity)
}
}
#[ffizz_header::item]
#[ffizz(order = 902)]
/// Get a list of all tasks in the replica.
///
/// Returns a TCTaskList with a NULL items field on error.
///
/// ```c
/// EXTERN_C struct TCTaskList tc_replica_all_tasks(struct TCReplica *rep);
/// ```
#[no_mangle]
pub unsafe extern "C" fn tc_replica_all_tasks(rep: *mut TCReplica) -> TCTaskList {
wrap(
rep,
|rep| {
// note that the Replica API returns a hashmap here, but we discard
// the keys and return a simple list. The task UUIDs are available
// from task.get_uuid(), so information is not lost.
let tasks: Vec<_> = rep
.all_tasks()?
.drain()
.map(|(_uuid, t)| {
Some(
NonNull::new(
// SAFETY:
// - caller promises to free this value (via freeing the list)
unsafe { TCTask::from(t).return_ptr() },
)
.expect("TCTask::return_ptr returned NULL"),
)
})
.collect();
// SAFETY:
// - value is not allocated and need not be freed
Ok(unsafe { TCTaskList::return_val(tasks) })
},
TCTaskList::null_value(),
)
}
#[ffizz_header::item]
#[ffizz(order = 902)]
/// Get a list of all uuids for tasks in the replica.
///
/// Returns a TCUuidList with a NULL items field on error.
///
/// The caller must free the UUID list with `tc_uuid_list_free`.
///
/// ```c
/// EXTERN_C struct TCUuidList tc_replica_all_task_uuids(struct TCReplica *rep);
/// ```
#[no_mangle]
pub unsafe extern "C" fn tc_replica_all_task_uuids(rep: *mut TCReplica) -> TCUuidList {
wrap(
rep,
|rep| {
let uuids: Vec<_> = rep
.all_task_uuids()?
.drain(..)
// SAFETY:
// - value is not allocated and need not be freed
.map(|uuid| unsafe { TCUuid::return_val(uuid) })
.collect();
// SAFETY:
// - value will be freed (promised by caller)
Ok(unsafe { TCUuidList::return_val(uuids) })
},
TCUuidList::null_value(),
)
}
#[ffizz_header::item]
#[ffizz(order = 902)]
/// Get the current working set for this replica. The resulting value must be freed
/// with tc_working_set_free.
///
/// Returns NULL on error.
///
/// ```c
/// EXTERN_C struct TCWorkingSet *tc_replica_working_set(struct TCReplica *rep);
/// ```
#[no_mangle]
pub unsafe extern "C" fn tc_replica_working_set(rep: *mut TCReplica) -> *mut TCWorkingSet {
wrap(
rep,
|rep| {
let ws = rep.working_set()?;
// SAFETY:
// - caller promises to free this value
Ok(unsafe { TCWorkingSet::return_ptr(ws.into()) })
},
std::ptr::null_mut(),
)
}
#[ffizz_header::item]
#[ffizz(order = 902)]
/// Get an existing task by its UUID.
///
/// Returns NULL when the task does not exist, and on error. Consult tc_replica_error
/// to distinguish the two conditions.
///
/// ```c
/// EXTERN_C struct TCTask *tc_replica_get_task(struct TCReplica *rep, struct TCUuid tcuuid);
/// ```
#[no_mangle]
pub unsafe extern "C" fn tc_replica_get_task(rep: *mut TCReplica, tcuuid: TCUuid) -> *mut TCTask {
wrap(
rep,
|rep| {
// SAFETY:
// - tcuuid is a valid TCUuid (all bytes are valid)
// - tcuuid is Copy so ownership doesn't matter
let uuid = unsafe { TCUuid::val_from_arg(tcuuid) };
if let Some(task) = rep.get_task(uuid)? {
// SAFETY:
// - caller promises to free this task
Ok(unsafe { TCTask::from(task).return_ptr() })
} else {
Ok(std::ptr::null_mut())
}
},
std::ptr::null_mut(),
)
}
#[ffizz_header::item]
#[ffizz(order = 902)]
/// Create a new task. The task must not already exist.
///
/// Returns the task, or NULL on error.
///
/// ```c
/// EXTERN_C struct TCTask *tc_replica_new_task(struct TCReplica *rep,
/// enum TCStatus status,
/// struct TCString description);
/// ```
#[no_mangle]
pub unsafe extern "C" fn tc_replica_new_task(
rep: *mut TCReplica,
status: TCStatus,
description: TCString,
) -> *mut TCTask {
// SAFETY:
// - description is valid (promised by caller)
// - caller will not use description after this call (convention)
let mut description = unsafe { TCString::val_from_arg(description) };
wrap(
rep,
|rep| {
let task = rep.new_task(status.into(), description.as_str()?.to_string())?;
// SAFETY:
// - caller promises to free this task
Ok(unsafe { TCTask::from(task).return_ptr() })
},
std::ptr::null_mut(),
)
}
#[ffizz_header::item]
#[ffizz(order = 902)]
/// Create a new task. The task must not already exist.
///
/// Returns the task, or NULL on error.
///
/// ```c
/// EXTERN_C struct TCTask *tc_replica_import_task_with_uuid(struct TCReplica *rep, struct TCUuid tcuuid);
/// ```
#[no_mangle]
pub unsafe extern "C" fn tc_replica_import_task_with_uuid(
rep: *mut TCReplica,
tcuuid: TCUuid,
) -> *mut TCTask {
wrap(
rep,
|rep| {
// SAFETY:
// - tcuuid is a valid TCUuid (all bytes are valid)
// - tcuuid is Copy so ownership doesn't matter
let uuid = unsafe { TCUuid::val_from_arg(tcuuid) };
let task = rep.import_task_with_uuid(uuid)?;
// SAFETY:
// - caller promises to free this task
Ok(unsafe { TCTask::from(task).return_ptr() })
},
std::ptr::null_mut(),
)
}
#[ffizz_header::item]
#[ffizz(order = 902)]
/// Delete a task. The task must exist. Note that this is different from setting status to
/// Deleted; this is the final purge of the task.
///
/// Deletion may interact poorly with modifications to the same task on other replicas. For
/// example, if a task is deleted on replica 1 and its description modified on replica 2, then
/// after both replicas have fully synced, the resulting task will only have a `description`
/// property.
///
/// ```c
/// EXTERN_C TCResult tc_replica_delete_task(struct TCReplica *rep, struct TCUuid tcuuid);
/// ```
#[no_mangle]
pub unsafe extern "C" fn tc_replica_delete_task(rep: *mut TCReplica, tcuuid: TCUuid) -> TCResult {
wrap(
rep,
|rep| {
// SAFETY:
// - tcuuid is a valid TCUuid (all bytes are valid)
// - tcuuid is Copy so ownership doesn't matter
let uuid = unsafe { TCUuid::val_from_arg(tcuuid) };
rep.delete_task(uuid)?;
Ok(TCResult::Ok)
},
TCResult::Error,
)
}
#[ffizz_header::item]
#[ffizz(order = 902)]
/// Synchronize this replica with a server.
///
/// The `server` argument remains owned by the caller, and must be freed explicitly.
///
/// ```c
/// EXTERN_C TCResult tc_replica_sync(struct TCReplica *rep, struct TCServer *server, bool avoid_snapshots);
/// ```
#[no_mangle]
pub unsafe extern "C" fn tc_replica_sync(
rep: *mut TCReplica,
server: *mut TCServer,
avoid_snapshots: bool,
) -> TCResult {
wrap(
rep,
|rep| {
debug_assert!(!server.is_null());
// SAFETY:
// - server is not NULL
// - *server is a valid TCServer (promised by caller)
// - server is valid for the lifetime of tc_replica_sync (not threadsafe)
// - server will not be accessed simultaneously (not threadsafe)
let server = unsafe { TCServer::from_ptr_arg_ref_mut(server) };
rep.sync(server.as_mut(), avoid_snapshots)?;
Ok(TCResult::Ok)
},
TCResult::Error,
)
}
#[ffizz_header::item]
#[ffizz(order = 902)]
/// Expire old, deleted tasks.
///
/// Expiration entails removal of tasks from the replica. Any modifications that occur after
/// the deletion (such as operations synchronized from other replicas) will do nothing.
///
/// Tasks are eligible for expiration when they have status Deleted and have not been modified
/// for 180 days (about six months). Note that completed tasks are not eligible.
///
/// ```c
/// EXTERN_C TCResult tc_replica_expire_tasks(struct TCReplica *rep);
/// ```
#[no_mangle]
pub unsafe extern "C" fn tc_replica_expire_tasks(rep: *mut TCReplica) -> TCResult {
wrap(
rep,
|rep| {
rep.expire_tasks()?;
Ok(TCResult::Ok)
},
TCResult::Error,
)
}
#[ffizz_header::item]
#[ffizz(order = 902)]
/// Return undo local operations until the most recent UndoPoint.
///
/// ```c
/// EXTERN_C TCReplicaOpList tc_replica_get_undo_ops(struct TCReplica *rep);
/// ```
#[no_mangle]
pub unsafe extern "C" fn tc_replica_get_undo_ops(rep: *mut TCReplica) -> TCReplicaOpList {
wrap(
rep,
|rep| {
// SAFETY:
// - caller will free this value, either with tc_replica_commit_undo_ops or
// tc_replica_op_list_free.
Ok(unsafe {
TCReplicaOpList::return_val(
rep.get_undo_ops()?
.into_iter()
.map(TCReplicaOp::from)
.collect(),
)
})
},
TCReplicaOpList::default(),
)
}
#[ffizz_header::item]
#[ffizz(order = 902)]
/// Undo local operations in storage.
///
/// If undone_out is not NULL, then on success it is set to 1 if operations were undone, or 0 if
/// there are no operations that can be done.
///
/// ```c
/// EXTERN_C TCResult tc_replica_commit_undo_ops(struct TCReplica *rep, TCReplicaOpList tc_undo_ops, int32_t *undone_out);
/// ```
#[no_mangle]
pub unsafe extern "C" fn tc_replica_commit_undo_ops(
rep: *mut TCReplica,
tc_undo_ops: TCReplicaOpList,
undone_out: *mut i32,
) -> TCResult {
wrap(
rep,
|rep| {
// SAFETY:
// - `tc_undo_ops` is a valid value, as it was acquired from `tc_replica_get_undo_ops`.
let undo_ops: Vec<ReplicaOp> = unsafe { TCReplicaOpList::val_from_arg(tc_undo_ops) }
.into_iter()
.map(|op| *op.inner)
.collect();
let undone = i32::from(rep.commit_undo_ops(undo_ops)?);
if !undone_out.is_null() {
// SAFETY:
// - undone_out is not NULL (just checked)
// - undone_out is properly aligned (implicitly promised by caller)
unsafe { *undone_out = undone };
}
Ok(TCResult::Ok)
},
TCResult::Error,
)
}
#[ffizz_header::item]
#[ffizz(order = 902)]
/// Get the number of local, un-synchronized operations (not including undo points), or -1 on
/// error.
///
/// ```c
/// EXTERN_C int64_t tc_replica_num_local_operations(struct TCReplica *rep);
/// ```
#[no_mangle]
pub unsafe extern "C" fn tc_replica_num_local_operations(rep: *mut TCReplica) -> i64 {
wrap(
rep,
|rep| {
let count = rep.num_local_operations()? as i64;
Ok(count)
},
-1,
)
}
#[ffizz_header::item]
#[ffizz(order = 902)]
/// Get the number of undo points (number of undo calls possible), or -1 on error.
///
/// ```c
/// EXTERN_C int64_t tc_replica_num_undo_points(struct TCReplica *rep);
/// ```
#[no_mangle]
pub unsafe extern "C" fn tc_replica_num_undo_points(rep: *mut TCReplica) -> i64 {
wrap(
rep,
|rep| {
let count = rep.num_undo_points()? as i64;
Ok(count)
},
-1,
)
}
#[ffizz_header::item]
#[ffizz(order = 902)]
/// Add an UndoPoint, if one has not already been added by this Replica. This occurs automatically
/// when a change is made. The `force` flag allows forcing a new UndoPoint even if one has already
/// been created by this Replica, and may be useful when a Replica instance is held for a long time
/// and used to apply more than one user-visible change.
///
/// ```c
/// EXTERN_C TCResult tc_replica_add_undo_point(struct TCReplica *rep, bool force);
/// ```
#[no_mangle]
pub unsafe extern "C" fn tc_replica_add_undo_point(rep: *mut TCReplica, force: bool) -> TCResult {
wrap(
rep,
|rep| {
rep.add_undo_point(force)?;
Ok(TCResult::Ok)
},
TCResult::Error,
)
}
#[ffizz_header::item]
#[ffizz(order = 902)]
/// Rebuild this replica's working set, based on whether tasks are pending or not. If `renumber`
/// is true, then existing tasks may be moved to new working-set indices; in any case, on
/// completion all pending tasks are in the working set and all non- pending tasks are not.
///
/// ```c
/// EXTERN_C TCResult tc_replica_rebuild_working_set(struct TCReplica *rep, bool renumber);
/// ```
#[no_mangle]
pub unsafe extern "C" fn tc_replica_rebuild_working_set(
rep: *mut TCReplica,
renumber: bool,
) -> TCResult {
wrap(
rep,
|rep| {
rep.rebuild_working_set(renumber)?;
Ok(TCResult::Ok)
},
TCResult::Error,
)
}
#[ffizz_header::item]
#[ffizz(order = 902)]
/// Get the latest error for a replica, or a string with NULL ptr if no error exists. Subsequent
/// calls to this function will return NULL. The rep pointer must not be NULL. The caller must
/// free the returned string.
///
/// ```c
/// EXTERN_C struct TCString tc_replica_error(struct TCReplica *rep);
/// ```
#[no_mangle]
pub unsafe extern "C" fn tc_replica_error(rep: *mut TCReplica) -> TCString {
// SAFETY:
// - rep is not NULL (promised by caller)
// - *rep is a valid TCReplica (promised by caller)
// - rep is valid for the duration of this function
// - rep is not modified by anything else (not threadsafe)
let rep: &mut TCReplica = unsafe { TCReplica::from_ptr_arg_ref_mut(rep) };
if let Some(rstring) = rep.error.take() {
// SAFETY:
// - caller promises to free this string
unsafe { TCString::return_val(rstring) }
} else {
TCString::default()
}
}
#[ffizz_header::item]
#[ffizz(order = 903)]
/// Free a replica. The replica may not be used after this function returns and must not be freed
/// more than once.
///
/// ```c
/// EXTERN_C void tc_replica_free(struct TCReplica *rep);
/// ```
#[no_mangle]
pub unsafe extern "C" fn tc_replica_free(rep: *mut TCReplica) {
// SAFETY:
// - replica is not NULL (promised by caller)
// - replica is valid (promised by caller)
// - caller will not use description after this call (promised by caller)
let replica = unsafe { TCReplica::take_from_ptr_arg(rep) };
if replica.mut_borrowed {
panic!("replica is borrowed and cannot be freed");
}
drop(replica);
}
#[ffizz_header::item]
#[ffizz(order = 903)]
/// Free a vector of ReplicaOp. The vector may not be used after this function returns and must not be freed
/// more than once.
///
/// ```c
/// EXTERN_C void tc_replica_op_list_free(struct TCReplicaOpList *oplist);
/// ```
#[no_mangle]
pub unsafe extern "C" fn tc_replica_op_list_free(oplist: *mut TCReplicaOpList) {
debug_assert!(!oplist.is_null());
// SAFETY:
// - arg is not NULL (just checked)
// - `*oplist` is valid (guaranteed by caller not double-freeing this value)
unsafe {
TCReplicaOpList::take_val_from_arg(
oplist,
// SAFETY:
// - value is empty, so the caller need not free it.
TCReplicaOpList::return_val(Vec::new()),
)
};
}
#[ffizz_header::item]
#[ffizz(order = 903)]
/// Return uuid field of ReplicaOp.
///
/// ```c
/// EXTERN_C struct TCString tc_replica_op_get_uuid(struct TCReplicaOp *op);
/// ```
#[no_mangle]
pub unsafe extern "C" fn tc_replica_op_get_uuid(op: *const TCReplicaOp) -> TCString {
// SAFETY:
// - inner is not null
// - inner is a living object
let rop: &ReplicaOp = unsafe { (*op).inner.as_ref() };
if let ReplicaOp::Create { uuid }
| ReplicaOp::Delete { uuid, .. }
| ReplicaOp::Update { uuid, .. } = rop
{
let uuid_rstr: RustString = uuid.to_string().into();
// SAFETY:
// - caller promises to free this string
unsafe { TCString::return_val(uuid_rstr) }
} else {
panic!("Operation has no uuid: {:#?}", rop);
}
}
#[ffizz_header::item]
#[ffizz(order = 903)]
/// Return property field of ReplicaOp.
///
/// ```c
/// EXTERN_C struct TCString tc_replica_op_get_property(struct TCReplicaOp *op);
/// ```
#[no_mangle]
pub unsafe extern "C" fn tc_replica_op_get_property(op: *const TCReplicaOp) -> TCString {
// SAFETY:
// - inner is not null
// - inner is a living object
let rop: &ReplicaOp = unsafe { (*op).inner.as_ref() };
if let ReplicaOp::Update { property, .. } = rop {
// SAFETY:
// - caller promises to free this string
unsafe { TCString::return_val(property.clone().into()) }
} else {
panic!("Operation has no property: {:#?}", rop);
}
}
#[ffizz_header::item]
#[ffizz(order = 903)]
/// Return value field of ReplicaOp.
///
/// ```c
/// EXTERN_C struct TCString tc_replica_op_get_value(struct TCReplicaOp *op);
/// ```
#[no_mangle]
pub unsafe extern "C" fn tc_replica_op_get_value(op: *const TCReplicaOp) -> TCString {
// SAFETY:
// - inner is not null
// - inner is a living object
let rop: &ReplicaOp = unsafe { (*op).inner.as_ref() };
if let ReplicaOp::Update { value, .. } = rop {
let value_rstr: RustString = value.clone().unwrap_or(String::new()).into();
// SAFETY:
// - caller promises to free this string
unsafe { TCString::return_val(value_rstr) }
} else {
panic!("Operation has no value: {:#?}", rop);
}
}
#[ffizz_header::item]
#[ffizz(order = 903)]
/// Return old value field of ReplicaOp.
///
/// ```c
/// EXTERN_C struct TCString tc_replica_op_get_old_value(struct TCReplicaOp *op);
/// ```
#[no_mangle]
pub unsafe extern "C" fn tc_replica_op_get_old_value(op: *const TCReplicaOp) -> TCString {
// SAFETY:
// - inner is not null
// - inner is a living object
let rop: &ReplicaOp = unsafe { (*op).inner.as_ref() };
if let ReplicaOp::Update { old_value, .. } = rop {
let old_value_rstr: RustString = old_value.clone().unwrap_or(String::new()).into();
// SAFETY:
// - caller promises to free this string
unsafe { TCString::return_val(old_value_rstr) }
} else {
panic!("Operation has no old value: {:#?}", rop);
}
}
#[ffizz_header::item]
#[ffizz(order = 903)]
/// Return timestamp field of ReplicaOp.
///
/// ```c
/// EXTERN_C struct TCString tc_replica_op_get_timestamp(struct TCReplicaOp *op);
/// ```
#[no_mangle]
pub unsafe extern "C" fn tc_replica_op_get_timestamp(op: *const TCReplicaOp) -> TCString {
// SAFETY:
// - inner is not null
// - inner is a living object
let rop: &ReplicaOp = unsafe { (*op).inner.as_ref() };
if let ReplicaOp::Update { timestamp, .. } = rop {
let timestamp_rstr: RustString = timestamp.to_string().into();
// SAFETY:
// - caller promises to free this string
unsafe { TCString::return_val(timestamp_rstr) }
} else {
panic!("Operation has no timestamp: {:#?}", rop);
}
}
#[ffizz_header::item]
#[ffizz(order = 903)]
/// Return description field of old task field of ReplicaOp.
///
/// ```c
/// EXTERN_C struct TCString tc_replica_op_get_old_task_description(struct TCReplicaOp *op);
/// ```
#[no_mangle]
pub unsafe extern "C" fn tc_replica_op_get_old_task_description(
op: *const TCReplicaOp,
) -> TCString {
// SAFETY:
// - inner is not null
// - inner is a living object
let rop: &ReplicaOp = unsafe { (*op).inner.as_ref() };
if let ReplicaOp::Delete { old_task, .. } = rop {
let description_rstr: RustString = old_task["description"].clone().into();
// SAFETY:
// - caller promises to free this string
unsafe { TCString::return_val(description_rstr) }
} else {
panic!("Operation has no timestamp: {:#?}", rop);
}
}

View File

@@ -1,25 +0,0 @@
#[ffizz_header::item]
#[ffizz(order = 100)]
/// ***** TCResult *****
///
/// A result from a TC operation. Typically if this value is TC_RESULT_ERROR,
/// the associated object's `tc_.._error` method will return an error message.
///
/// ```c
/// enum TCResult
/// #ifdef __cplusplus
/// : int32_t
/// #endif // __cplusplus
/// {
/// TC_RESULT_ERROR = -1,
/// TC_RESULT_OK = 0,
/// };
/// #ifndef __cplusplus
/// typedef int32_t TCResult;
/// #endif // __cplusplus
/// ```
#[repr(i32)]
pub enum TCResult {
Error = -1,
Ok = 0,
}

View File

@@ -1,234 +0,0 @@
use crate::traits::*;
use crate::types::*;
use crate::util::err_to_ruststring;
use taskchampion::{Server, ServerConfig};
#[ffizz_header::item]
#[ffizz(order = 800)]
/// ***** TCServer *****
///
/// TCServer represents an interface to a sync server. Aside from new and free, a server
/// has no C-accessible API, but is designed to be passed to `tc_replica_sync`.
///
/// ## Safety
///
/// TCServer are not threadsafe, and must not be used with multiple replicas simultaneously.
///
/// ```c
/// typedef struct TCServer TCServer;
/// ```
pub struct TCServer(Box<dyn Server>);
impl PassByPointer for TCServer {}
impl From<Box<dyn Server>> for TCServer {
fn from(server: Box<dyn Server>) -> TCServer {
TCServer(server)
}
}
impl AsMut<Box<dyn Server>> for TCServer {
fn as_mut(&mut self) -> &mut Box<dyn Server> {
&mut self.0
}
}
/// Utility function to allow using `?` notation to return an error value.
fn wrap<T, F>(f: F, error_out: *mut TCString, err_value: T) -> T
where
F: FnOnce() -> anyhow::Result<T>,
{
if !error_out.is_null() {
// SAFETY:
// - error_out is not NULL (just checked)
// - properly aligned and valid (promised by caller)
unsafe { *error_out = TCString::default() };
}
match f() {
Ok(v) => v,
Err(e) => {
if !error_out.is_null() {
// SAFETY:
// - error_out is not NULL (just checked)
// - properly aligned and valid (promised by caller)
unsafe {
TCString::val_to_arg_out(err_to_ruststring(e), error_out);
}
}
err_value
}
}
}
#[ffizz_header::item]
#[ffizz(order = 801)]
/// Create a new TCServer that operates locally (on-disk). See the TaskChampion docs for the
/// description of the arguments.
///
/// On error, a string is written to the error_out parameter (if it is not NULL) and NULL is
/// returned. The caller must free this string.
///
/// The server must be freed after it is used - tc_replica_sync does not automatically free it.
///
/// ```c
/// EXTERN_C struct TCServer *tc_server_new_local(struct TCString server_dir, struct TCString *error_out);
/// ```
#[no_mangle]
pub unsafe extern "C" fn tc_server_new_local(
server_dir: TCString,
error_out: *mut TCString,
) -> *mut TCServer {
wrap(
|| {
// SAFETY:
// - server_dir is valid (promised by caller)
// - caller will not use server_dir after this call (convention)
let mut server_dir = unsafe { TCString::val_from_arg(server_dir) };
let server_config = ServerConfig::Local {
server_dir: server_dir.to_path_buf_mut()?,
};
let server = server_config.into_server()?;
// SAFETY: caller promises to free this server.
Ok(unsafe { TCServer::return_ptr(server.into()) })
},
error_out,
std::ptr::null_mut(),
)
}
#[ffizz_header::item]
#[ffizz(order = 801)]
/// Create a new TCServer that connects to a remote server. See the TaskChampion docs for the
/// description of the arguments.
///
/// On error, a string is written to the error_out parameter (if it is not NULL) and NULL is
/// returned. The caller must free this string.
///
/// The server must be freed after it is used - tc_replica_sync does not automatically free it.
///
/// ```c
/// EXTERN_C struct TCServer *tc_server_new_sync(struct TCString url,
/// struct TCUuid client_id,
/// struct TCString encryption_secret,
/// struct TCString *error_out);
/// ```
#[no_mangle]
pub unsafe extern "C" fn tc_server_new_sync(
url: TCString,
client_id: TCUuid,
encryption_secret: TCString,
error_out: *mut TCString,
) -> *mut TCServer {
wrap(
|| {
// SAFETY:
// - url is valid (promised by caller)
// - url ownership is transferred to this function
let url = unsafe { TCString::val_from_arg(url) }.into_string()?;
// SAFETY:
// - client_id is a valid Uuid (any 8-byte sequence counts)
let client_id = unsafe { TCUuid::val_from_arg(client_id) };
// SAFETY:
// - encryption_secret is valid (promised by caller)
// - encryption_secret ownership is transferred to this function
let encryption_secret = unsafe { TCString::val_from_arg(encryption_secret) }
.as_bytes()
.to_vec();
let server_config = ServerConfig::Remote {
url,
client_id,
encryption_secret,
};
let server = server_config.into_server()?;
// SAFETY: caller promises to free this server.
Ok(unsafe { TCServer::return_ptr(server.into()) })
},
error_out,
std::ptr::null_mut(),
)
}
#[ffizz_header::item]
#[ffizz(order = 802)]
/// Create a new TCServer that connects to the Google Cloud Platform. See the TaskChampion docs
/// for the description of the arguments.
///
/// On error, a string is written to the error_out parameter (if it is not NULL) and NULL is
/// returned. The caller must free this string.
///
/// The server must be freed after it is used - tc_replica_sync does not automatically free it.
///
/// ```c
/// EXTERN_C struct TCServer *tc_server_new_gcp(struct TCString bucket,
/// struct TCString credential_path,
/// struct TCString encryption_secret,
/// struct TCString *error_out);
/// ```
#[no_mangle]
pub unsafe extern "C" fn tc_server_new_gcp(
bucket: TCString,
credential_path_argument: TCString,
encryption_secret: TCString,
error_out: *mut TCString,
) -> *mut TCServer {
wrap(
|| {
// SAFETY:
// - bucket is valid (promised by caller)
// - bucket ownership is transferred to this function
let bucket = unsafe { TCString::val_from_arg(bucket) }.into_string()?;
// SAFETY:
// - credential_path is valid (promised by caller)
// - credential_path ownership is transferred to this function
let credential_path =
unsafe { TCString::val_from_arg(credential_path_argument) }.into_string()?;
let credential_path = if credential_path.is_empty() {
None
} else {
Some(credential_path)
};
// SAFETY:
// - encryption_secret is valid (promised by caller)
// - encryption_secret ownership is transferred to this function
let encryption_secret = unsafe { TCString::val_from_arg(encryption_secret) }
.as_bytes()
.to_vec();
let server_config = ServerConfig::Gcp {
bucket,
credential_path,
encryption_secret,
};
let server = server_config.into_server()?;
// SAFETY: caller promises to free this server.
Ok(unsafe { TCServer::return_ptr(server.into()) })
},
error_out,
std::ptr::null_mut(),
)
}
#[ffizz_header::item]
#[ffizz(order = 899)]
/// Free a server. The server may not be used after this function returns and must not be freed
/// more than once.
///
/// ```c
/// EXTERN_C void tc_server_free(struct TCServer *server);
/// ```
#[no_mangle]
pub unsafe extern "C" fn tc_server_free(server: *mut TCServer) {
debug_assert!(!server.is_null());
// SAFETY:
// - server is not NULL
// - server came from tc_server_new_.., which used return_ptr
// - server will not be used after (promised by caller)
let server = unsafe { TCServer::take_from_ptr_arg(server) };
drop(server);
}

View File

@@ -1,73 +0,0 @@
pub use taskchampion::Status;
#[ffizz_header::item]
#[ffizz(order = 700)]
/// ***** TCStatus *****
///
/// The status of a task, as defined by the task data model.
///
/// ```c
/// #ifdef __cplusplus
/// typedef enum TCStatus : int32_t {
/// #else // __cplusplus
/// typedef int32_t TCStatus;
/// enum TCStatus {
/// #endif // __cplusplus
/// TC_STATUS_PENDING = 0,
/// TC_STATUS_COMPLETED = 1,
/// TC_STATUS_DELETED = 2,
/// TC_STATUS_RECURRING = 3,
/// // Unknown signifies a status in the task DB that was not
/// // recognized.
/// TC_STATUS_UNKNOWN = -1,
/// #ifdef __cplusplus
/// } TCStatus;
/// #else // __cplusplus
/// };
/// #endif // __cplusplus
/// ```
#[repr(i32)]
pub enum TCStatus {
Pending = 0,
Completed = 1,
Deleted = 2,
Recurring = 3,
Unknown = -1,
}
impl From<TCStatus> for Status {
fn from(status: TCStatus) -> Status {
match status {
TCStatus::Pending => Status::Pending,
TCStatus::Completed => Status::Completed,
TCStatus::Deleted => Status::Deleted,
TCStatus::Recurring => Status::Recurring,
_ => Status::Unknown(format!("unknown TCStatus {}", status as i32)),
}
}
}
impl From<Status> for TCStatus {
fn from(status: Status) -> TCStatus {
match status {
Status::Pending => TCStatus::Pending,
Status::Completed => TCStatus::Completed,
Status::Deleted => TCStatus::Deleted,
Status::Recurring => TCStatus::Recurring,
Status::Unknown(_) => TCStatus::Unknown,
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn conversion_from_unknown_tc_status_provides_discriminant_in_message() {
let tc_status = TCStatus::Unknown;
let status = Status::from(tc_status);
assert!(matches!(status, Status::Unknown(msg) if msg == "unknown TCStatus -1"));
}
}

View File

@@ -1,773 +0,0 @@
use crate::traits::*;
use crate::util::{string_into_raw_parts, vec_into_raw_parts};
use std::ffi::{CStr, CString, OsString};
use std::os::raw::c_char;
use std::path::PathBuf;
#[ffizz_header::item]
#[ffizz(order = 200)]
/// ***** TCString *****
///
/// TCString supports passing strings into and out of the TaskChampion API.
///
/// # Rust Strings and C Strings
///
/// A Rust string can contain embedded NUL characters, while C considers such a character to mark
/// the end of a string. Strings containing embedded NULs cannot be represented as a "C string"
/// and must be accessed using `tc_string_content_and_len` and `tc_string_clone_with_len`. In
/// general, these two functions should be used for handling arbitrary data, while more convenient
/// forms may be used where embedded NUL characters are impossible, such as in static strings.
///
/// # UTF-8
///
/// TaskChampion expects all strings to be valid UTF-8. `tc_string_…` functions will fail if given
/// a `*TCString` containing invalid UTF-8.
///
/// # Safety
///
/// The `ptr` field may be checked for NULL, where documentation indicates this is possible. All
/// other fields in a TCString are private and must not be used from C. They exist in the struct
/// to ensure proper allocation and alignment.
///
/// When a `TCString` appears as a return value or output argument, ownership is passed to the
/// caller. The caller must pass that ownership back to another function or free the string.
///
/// Any function taking a `TCString` requires:
/// - the pointer must not be NUL;
/// - the pointer must be one previously returned from a tc_… function; and
/// - the memory referenced by the pointer must never be modified by C code.
///
/// Unless specified otherwise, TaskChampion functions take ownership of a `TCString` when it is
/// given as a function argument, and the caller must not use or free TCStrings after passing them
/// to such API functions.
///
/// A TCString with a NULL `ptr` field need not be freed, although tc_free_string will not fail
/// for such a value.
///
/// TCString is not threadsafe.
///
/// ```c
/// typedef struct TCString {
/// void *ptr; // opaque, but may be checked for NULL
/// size_t _u1; // reserved
/// size_t _u2; // reserved
/// uint8_t _u3; // reserved
/// } TCString;
/// ```
#[repr(C)]
#[derive(Debug)]
pub struct TCString {
// defined based on the type
ptr: *mut libc::c_void,
len: usize,
cap: usize,
// type of TCString this represents
ty: u8,
}
// TODO: figure out how to ignore this but still use it in TCString
/// A discriminator for TCString
#[repr(u8)]
enum TCStringType {
/// Null. Nothing is contained in this string.
///
/// * `ptr` is NULL.
/// * `len` and `cap` are zero.
Null = 0,
/// A CString.
///
/// * `ptr` is the result of CString::into_raw, containing a terminating NUL. It may not be
/// valid UTF-8.
/// * `len` and `cap` are zero.
CString,
/// A CStr, referencing memory borrowed from C
///
/// * `ptr` points to the string, containing a terminating NUL. It may not be valid UTF-8.
/// * `len` and `cap` are zero.
CStr,
/// A String.
///
/// * `ptr`, `len`, and `cap` are as would be returned from String::into_raw_parts.
String,
/// A byte sequence.
///
/// * `ptr`, `len`, and `cap` are as would be returned from Vec::into_raw_parts.
Bytes,
}
impl Default for TCString {
fn default() -> Self {
TCString {
ptr: std::ptr::null_mut(),
len: 0,
cap: 0,
ty: TCStringType::Null as u8,
}
}
}
impl TCString {
pub(crate) fn is_null(&self) -> bool {
self.ptr.is_null()
}
}
#[derive(PartialEq, Eq, Debug, Default)]
pub enum RustString<'a> {
#[default]
Null,
CString(CString),
CStr(&'a CStr),
String(String),
Bytes(Vec<u8>),
}
impl PassByValue for TCString {
type RustType = RustString<'static>;
unsafe fn from_ctype(self) -> Self::RustType {
match self.ty {
ty if ty == TCStringType::CString as u8 => {
// SAFETY:
// - ptr was derived from CString::into_raw
// - data was not modified since that time (caller promises)
RustString::CString(unsafe { CString::from_raw(self.ptr as *mut c_char) })
}
ty if ty == TCStringType::CStr as u8 => {
// SAFETY:
// - ptr was created by CStr::as_ptr
// - data was not modified since that time (caller promises)
RustString::CStr(unsafe { CStr::from_ptr(self.ptr as *mut c_char) })
}
ty if ty == TCStringType::String as u8 => {
// SAFETY:
// - ptr was created by string_into_raw_parts
// - data was not modified since that time (caller promises)
RustString::String(unsafe {
String::from_raw_parts(self.ptr as *mut u8, self.len, self.cap)
})
}
ty if ty == TCStringType::Bytes as u8 => {
// SAFETY:
// - ptr was created by vec_into_raw_parts
// - data was not modified since that time (caller promises)
RustString::Bytes(unsafe {
Vec::from_raw_parts(self.ptr as *mut u8, self.len, self.cap)
})
}
_ => RustString::Null,
}
}
fn as_ctype(arg: Self::RustType) -> Self {
match arg {
RustString::Null => Self {
ty: TCStringType::Null as u8,
..Default::default()
},
RustString::CString(cstring) => Self {
ty: TCStringType::CString as u8,
ptr: cstring.into_raw() as *mut libc::c_void,
..Default::default()
},
RustString::CStr(cstr) => Self {
ty: TCStringType::CStr as u8,
ptr: cstr.as_ptr() as *mut libc::c_void,
..Default::default()
},
RustString::String(string) => {
let (ptr, len, cap) = string_into_raw_parts(string);
Self {
ty: TCStringType::String as u8,
ptr: ptr as *mut libc::c_void,
len,
cap,
}
}
RustString::Bytes(bytes) => {
let (ptr, len, cap) = vec_into_raw_parts(bytes);
Self {
ty: TCStringType::Bytes as u8,
ptr: ptr as *mut libc::c_void,
len,
cap,
}
}
}
}
}
impl<'a> RustString<'a> {
/// Get a regular Rust &str for this value.
pub(crate) fn as_str(&mut self) -> Result<&str, std::str::Utf8Error> {
match self {
RustString::CString(cstring) => cstring.as_c_str().to_str(),
RustString::CStr(cstr) => cstr.to_str(),
RustString::String(ref string) => Ok(string.as_ref()),
RustString::Bytes(_) => {
self.bytes_to_string()?;
self.as_str() // now the String variant, so won't recurse
}
RustString::Null => unreachable!(),
}
}
/// Consume this RustString and return an equivalent String, or an error if not
/// valid UTF-8. In the error condition, the original data is lost.
pub(crate) fn into_string(mut self) -> Result<String, std::str::Utf8Error> {
match self {
RustString::CString(cstring) => cstring.into_string().map_err(|e| e.utf8_error()),
RustString::CStr(cstr) => cstr.to_str().map(|s| s.to_string()),
RustString::String(string) => Ok(string),
RustString::Bytes(_) => {
self.bytes_to_string()?;
self.into_string() // now the String variant, so won't recurse
}
RustString::Null => unreachable!(),
}
}
pub(crate) fn as_bytes(&self) -> &[u8] {
match self {
RustString::CString(cstring) => cstring.as_bytes(),
RustString::CStr(cstr) => cstr.to_bytes(),
RustString::String(string) => string.as_bytes(),
RustString::Bytes(bytes) => bytes.as_ref(),
RustString::Null => unreachable!(),
}
}
/// Convert the RustString, in place, from the Bytes to String variant. On successful return,
/// the RustString has variant RustString::String.
fn bytes_to_string(&mut self) -> Result<(), std::str::Utf8Error> {
let mut owned = RustString::Null;
// temporarily swap a Null value into self; we'll swap that back
// shortly.
std::mem::swap(self, &mut owned);
match owned {
RustString::Bytes(bytes) => match String::from_utf8(bytes) {
Ok(string) => {
*self = RustString::String(string);
Ok(())
}
Err(e) => {
let (e, bytes) = (e.utf8_error(), e.into_bytes());
// put self back as we found it
*self = RustString::Bytes(bytes);
Err(e)
}
},
_ => {
// not bytes, so just swap back
std::mem::swap(self, &mut owned);
Ok(())
}
}
}
/// Convert the RustString, in place, into one of the C variants. If this is not
/// possible, such as if the string contains an embedded NUL, then the string
/// remains unchanged.
fn string_to_cstring(&mut self) {
let mut owned = RustString::Null;
// temporarily swap a Null value into self; we'll swap that back shortly
std::mem::swap(self, &mut owned);
match owned {
RustString::String(string) => {
match CString::new(string) {
Ok(cstring) => {
*self = RustString::CString(cstring);
}
Err(nul_err) => {
// recover the underlying String from the NulError and restore
// the RustString
let original_bytes = nul_err.into_vec();
// SAFETY: original_bytes came from a String moments ago, so still valid utf8
let string = unsafe { String::from_utf8_unchecked(original_bytes) };
*self = RustString::String(string);
}
}
}
_ => {
// not a CString, so just swap back
std::mem::swap(self, &mut owned);
}
}
}
pub(crate) fn to_path_buf_mut(&mut self) -> Result<PathBuf, std::str::Utf8Error> {
#[cfg(unix)]
let path: OsString = {
// on UNIX, we can use the bytes directly, without requiring that they
// be valid UTF-8.
use std::ffi::OsStr;
use std::os::unix::ffi::OsStrExt;
OsStr::from_bytes(self.as_bytes()).to_os_string()
};
#[cfg(windows)]
let path: OsString = {
// on Windows, we assume the filename is valid Unicode, so it can be
// represented as UTF-8.
OsString::from(self.as_str()?.to_string())
};
Ok(path.into())
}
}
impl<'a> From<String> for RustString<'a> {
fn from(string: String) -> RustString<'a> {
RustString::String(string)
}
}
impl From<&str> for RustString<'static> {
fn from(string: &str) -> RustString<'static> {
RustString::String(string.to_string())
}
}
/// Utility function to borrow a TCString from a pointer arg, modify it,
/// and restore it.
///
/// This implements a kind of "interior mutability", relying on the
/// single-threaded use of all TC* types.
///
/// # SAFETY
///
/// - tcstring must not be NULL
/// - *tcstring must be a valid TCString
/// - *tcstring must not be accessed by anything else, despite the *const
unsafe fn wrap<T, F>(tcstring: *const TCString, f: F) -> T
where
F: FnOnce(&mut RustString) -> T,
{
debug_assert!(!tcstring.is_null());
// SAFETY:
// - we have exclusive to *tcstring (promised by caller)
let tcstring = tcstring as *mut TCString;
// SAFETY:
// - tcstring is not NULL
// - *tcstring is a valid string (promised by caller)
let mut rstring = unsafe { TCString::take_val_from_arg(tcstring, TCString::default()) };
let rv = f(&mut rstring);
// update the caller's TCString with the updated RustString
// SAFETY:
// - tcstring is not NULL (we just took from it)
// - tcstring points to valid memory (we just took from it)
unsafe { TCString::val_to_arg_out(rstring, tcstring) };
rv
}
#[ffizz_header::item]
#[ffizz(order = 210)]
/// ***** TCStringList *****
///
/// TCStringList represents a list of strings.
///
/// The content of this struct must be treated as read-only.
///
/// ```c
/// typedef struct TCStringList {
/// // number of strings in items
/// size_t len;
/// // reserved
/// size_t _u1;
/// // TCStringList representing each string. These remain owned by the TCStringList instance and will
/// // be freed by tc_string_list_free. This pointer is never NULL for a valid TCStringList, and the
/// // *TCStringList at indexes 0..len-1 are not NULL.
/// struct TCString *items;
/// } TCStringList;
/// ```
#[repr(C)]
pub struct TCStringList {
len: libc::size_t,
/// total size of items (internal use only)
capacity: libc::size_t,
items: *mut TCString,
}
impl CList for TCStringList {
type Element = TCString;
unsafe fn from_raw_parts(items: *mut Self::Element, len: usize, cap: usize) -> Self {
TCStringList {
len,
capacity: cap,
items,
}
}
fn slice(&mut self) -> &mut [Self::Element] {
// SAFETY:
// - because we have &mut self, we have read/write access to items[0..len]
// - all items are properly initialized Element's
// - return value lifetime is equal to &mmut self's, so access is exclusive
// - items and len came from Vec, so total size is < isize::MAX
unsafe { std::slice::from_raw_parts_mut(self.items, self.len) }
}
fn into_raw_parts(self) -> (*mut Self::Element, usize, usize) {
(self.items, self.len, self.capacity)
}
}
#[ffizz_header::item]
#[ffizz(order = 201)]
/// Create a new TCString referencing the given C string. The C string must remain valid and
/// unchanged until after the TCString is freed. It's typically easiest to ensure this by using a
/// static string.
///
/// NOTE: this function does _not_ take responsibility for freeing the given C string. The
/// given string can be freed once the TCString referencing it has been freed.
///
/// For example:
///
/// ```text
/// char *url = get_item_url(..); // dynamically allocate C string
/// tc_task_annotate(task, tc_string_borrow(url)); // TCString created, passed, and freed
/// free(url); // string is no longer referenced and can be freed
/// ```
///
/// ```c
/// EXTERN_C struct TCString tc_string_borrow(const char *cstr);
/// ```
#[no_mangle]
pub unsafe extern "C" fn tc_string_borrow(cstr: *const libc::c_char) -> TCString {
debug_assert!(!cstr.is_null());
// SAFETY:
// - cstr is not NULL (promised by caller, verified by assertion)
// - cstr's lifetime exceeds that of the TCString (promised by caller)
// - cstr contains a valid NUL terminator (promised by caller)
// - cstr's content will not change before it is destroyed (promised by caller)
let cstr: &CStr = unsafe { CStr::from_ptr(cstr) };
// SAFETY:
// - caller promises to free this string
unsafe { TCString::return_val(RustString::CStr(cstr)) }
}
#[ffizz_header::item]
#[ffizz(order = 201)]
/// Create a new TCString by cloning the content of the given C string. The resulting TCString
/// is independent of the given string, which can be freed or overwritten immediately.
///
/// ```c
/// EXTERN_C struct TCString tc_string_clone(const char *cstr);
/// ```
#[no_mangle]
pub unsafe extern "C" fn tc_string_clone(cstr: *const libc::c_char) -> TCString {
debug_assert!(!cstr.is_null());
// SAFETY:
// - cstr is not NULL (promised by caller, verified by assertion)
// - cstr's lifetime exceeds that of this function (by C convention)
// - cstr contains a valid NUL terminator (promised by caller)
// - cstr's content will not change before it is destroyed (by C convention)
let cstr: &CStr = unsafe { CStr::from_ptr(cstr) };
let cstring: CString = cstr.into();
// SAFETY:
// - caller promises to free this string
unsafe { TCString::return_val(RustString::CString(cstring)) }
}
#[ffizz_header::item]
#[ffizz(order = 201)]
/// Create a new TCString containing the given string with the given length. This allows creation
/// of strings containing embedded NUL characters. As with `tc_string_clone`, the resulting
/// TCString is independent of the passed buffer, which may be reused or freed immediately.
///
/// The length should _not_ include any trailing NUL.
///
/// The given length must be less than half the maximum value of usize.
///
/// ```c
/// EXTERN_C struct TCString tc_string_clone_with_len(const char *buf, size_t len);
/// ```
#[no_mangle]
pub unsafe extern "C" fn tc_string_clone_with_len(
buf: *const libc::c_char,
len: usize,
) -> TCString {
debug_assert!(!buf.is_null());
debug_assert!(len < isize::MAX as usize);
// SAFETY:
// - buf is valid for len bytes (by C convention)
// - (no alignment requirements for a byte slice)
// - content of buf will not be mutated during the lifetime of this slice (lifetime
// does not outlive this function call)
// - the length of the buffer is less than isize::MAX (promised by caller)
let slice = unsafe { std::slice::from_raw_parts(buf as *const u8, len) };
// allocate and copy into Rust-controlled memory
let vec = slice.to_vec();
// SAFETY:
// - caller promises to free this string
unsafe { TCString::return_val(RustString::Bytes(vec)) }
}
#[ffizz_header::item]
#[ffizz(order = 201)]
/// Get the content of the string as a regular C string. The given string must be valid. The
/// returned value is NULL if the string contains NUL bytes or (in some cases) invalid UTF-8. The
/// returned C string is valid until the TCString is freed or passed to another TC API function.
///
/// In general, prefer [`tc_string_content_with_len`] except when it's certain that the string is
/// valid and NUL-free.
///
/// This function takes the TCString by pointer because it may be modified in-place to add a NUL
/// terminator. The pointer must not be NULL.
///
/// This function does _not_ take ownership of the TCString.
///
/// ```c
/// EXTERN_C const char *tc_string_content(const struct TCString *tcstring);
/// ```
#[no_mangle]
pub unsafe extern "C" fn tc_string_content(tcstring: *const TCString) -> *const libc::c_char {
// SAFETY;
// - tcstring is not NULL (promised by caller)
// - *tcstring is valid (promised by caller)
// - *tcstring is not accessed concurrently (single-threaded)
unsafe {
wrap(tcstring, |rstring| {
// try to eliminate the Bytes variant. If this fails, we'll return NULL
// below, so the error is ignorable.
let _ = rstring.bytes_to_string();
// and eliminate the String variant
rstring.string_to_cstring();
match &rstring {
RustString::CString(cstring) => cstring.as_ptr(),
RustString::String(_) => std::ptr::null(), // string_to_cstring failed
RustString::CStr(cstr) => cstr.as_ptr(),
RustString::Bytes(_) => std::ptr::null(), // already returned above
RustString::Null => unreachable!(),
}
})
}
}
#[ffizz_header::item]
#[ffizz(order = 201)]
/// Get the content of the string as a pointer and length. The given string must not be NULL.
/// This function can return any string, even one including NUL bytes or invalid UTF-8. The
/// returned buffer is valid until the TCString is freed or passed to another TaskChampio
/// function.
///
/// This function takes the TCString by pointer because it may be modified in-place to add a NUL
/// terminator. The pointer must not be NULL.
///
/// This function does _not_ take ownership of the TCString.
///
/// ```c
/// EXTERN_C const char *tc_string_content_with_len(const struct TCString *tcstring, size_t *len_out);
/// ```
#[no_mangle]
pub unsafe extern "C" fn tc_string_content_with_len(
tcstring: *const TCString,
len_out: *mut usize,
) -> *const libc::c_char {
// SAFETY;
// - tcstring is not NULL (promised by caller)
// - *tcstring is valid (promised by caller)
// - *tcstring is not accessed concurrently (single-threaded)
unsafe {
wrap(tcstring, |rstring| {
let bytes = rstring.as_bytes();
// SAFETY:
// - len_out is not NULL (promised by caller)
// - len_out points to valid memory (promised by caller)
// - len_out is properly aligned (C convention)
usize::val_to_arg_out(bytes.len(), len_out);
bytes.as_ptr() as *const libc::c_char
})
}
}
#[ffizz_header::item]
#[ffizz(order = 201)]
/// Free a TCString. The given string must not be NULL. The string must not be used
/// after this function returns, and must not be freed more than once.
///
/// ```c
/// EXTERN_C void tc_string_free(struct TCString *tcstring);
/// ```
#[no_mangle]
pub unsafe extern "C" fn tc_string_free(tcstring: *mut TCString) {
// SAFETY:
// - tcstring is not NULL (promised by caller)
// - caller is exclusive owner of tcstring (promised by caller)
drop(unsafe { TCString::take_val_from_arg(tcstring, TCString::default()) });
}
#[ffizz_header::item]
#[ffizz(order = 211)]
/// Free a TCStringList instance. The instance, and all TCStringList it contains, must not be used after
/// this call.
///
/// When this call returns, the `items` pointer will be NULL, signalling an invalid TCStringList.
///
/// ```c
/// EXTERN_C void tc_string_list_free(struct TCStringList *tcstrings);
/// ```
#[no_mangle]
pub unsafe extern "C" fn tc_string_list_free(tcstrings: *mut TCStringList) {
// SAFETY:
// - tcstrings is not NULL and points to a valid TCStringList (caller is not allowed to
// modify the list)
// - caller promises not to use the value after return
unsafe { drop_value_list(tcstrings) };
}
#[cfg(test)]
mod test {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn empty_list_has_non_null_pointer() {
let tcstrings = unsafe { TCStringList::return_val(Vec::new()) };
assert!(!tcstrings.items.is_null());
assert_eq!(tcstrings.len, 0);
assert_eq!(tcstrings.capacity, 0);
}
#[test]
fn free_sets_null_pointer() {
let mut tcstrings = unsafe { TCStringList::return_val(Vec::new()) };
// SAFETY: testing expected behavior
unsafe { tc_string_list_free(&mut tcstrings) };
assert!(tcstrings.items.is_null());
assert_eq!(tcstrings.len, 0);
assert_eq!(tcstrings.capacity, 0);
}
const INVALID_UTF8: &[u8] = b"abc\xf0\x28\x8c\x28";
fn make_cstring() -> RustString<'static> {
RustString::CString(CString::new("a string").unwrap())
}
fn make_cstr() -> RustString<'static> {
let cstr = CStr::from_bytes_with_nul(b"a string\0").unwrap();
RustString::CStr(cstr)
}
fn make_string() -> RustString<'static> {
RustString::String("a string".into())
}
fn make_string_with_nul() -> RustString<'static> {
RustString::String("a \0 nul!".into())
}
fn make_invalid_bytes() -> RustString<'static> {
RustString::Bytes(INVALID_UTF8.to_vec())
}
fn make_bytes() -> RustString<'static> {
RustString::Bytes(b"bytes".to_vec())
}
#[test]
fn cstring_as_str() {
assert_eq!(make_cstring().as_str().unwrap(), "a string");
}
#[test]
fn cstr_as_str() {
assert_eq!(make_cstr().as_str().unwrap(), "a string");
}
#[test]
fn string_as_str() {
assert_eq!(make_string().as_str().unwrap(), "a string");
}
#[test]
fn string_with_nul_as_str() {
assert_eq!(make_string_with_nul().as_str().unwrap(), "a \0 nul!");
}
#[test]
fn invalid_bytes_as_str() {
let as_str_err = make_invalid_bytes().as_str().unwrap_err();
assert_eq!(as_str_err.valid_up_to(), 3); // "abc" is valid
}
#[test]
fn valid_bytes_as_str() {
assert_eq!(make_bytes().as_str().unwrap(), "bytes");
}
#[test]
fn cstring_as_bytes() {
assert_eq!(make_cstring().as_bytes(), b"a string");
}
#[test]
fn cstr_as_bytes() {
assert_eq!(make_cstr().as_bytes(), b"a string");
}
#[test]
fn string_as_bytes() {
assert_eq!(make_string().as_bytes(), b"a string");
}
#[test]
fn string_with_nul_as_bytes() {
assert_eq!(make_string_with_nul().as_bytes(), b"a \0 nul!");
}
#[test]
fn invalid_bytes_as_bytes() {
assert_eq!(make_invalid_bytes().as_bytes(), INVALID_UTF8);
}
#[test]
fn cstring_string_to_cstring() {
let mut tcstring = make_cstring();
tcstring.string_to_cstring();
assert_eq!(tcstring, make_cstring()); // unchanged
}
#[test]
fn cstr_string_to_cstring() {
let mut tcstring = make_cstr();
tcstring.string_to_cstring();
assert_eq!(tcstring, make_cstr()); // unchanged
}
#[test]
fn string_string_to_cstring() {
let mut tcstring = make_string();
tcstring.string_to_cstring();
assert_eq!(tcstring, make_cstring()); // converted to CString, same content
}
#[test]
fn string_with_nul_string_to_cstring() {
let mut tcstring = make_string_with_nul();
tcstring.string_to_cstring();
assert_eq!(tcstring, make_string_with_nul()); // unchanged
}
#[test]
fn bytes_string_to_cstring() {
let mut tcstring = make_bytes();
tcstring.string_to_cstring();
assert_eq!(tcstring, make_bytes()); // unchanged
}
}

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More