Compare commits

..

33 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
73 changed files with 2394 additions and 257 deletions

View File

@@ -32,7 +32,7 @@ jobs:
# 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.73.0" # MSRV
toolchain: "1.81.0" # MSRV
override: true
- uses: actions-rs/cargo@v1.0.3
@@ -64,3 +64,19 @@ jobs:
with:
command: fmt
args: --all -- --check
cargo-metadata:
runs-on: ubuntu-latest
name: "Cargo Metadata"
steps:
- uses: actions/checkout@v4
- uses: actions-rs/toolchain@v1
with:
profile: minimal
components: rustfmt
toolchain: stable
override: true
- name: "Check metadata"
run: ".github/workflows/metadata-check.sh"

View File

@@ -23,6 +23,10 @@ jobs:
id-token: write
steps:
- name: Create lowercase repository name
run: |
GHCR_REPOSITORY="${{ github.repository_owner }}"
echo "REPOSITORY=${GHCR_REPOSITORY,,}" >> ${GITHUB_ENV}
- name: Checkout repository
uses: actions/checkout@v4
with:
@@ -35,19 +39,19 @@ jobs:
uses: docker/login-action@v3.3.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Taskwarrior Docker image
id: build-and-push
uses: docker/build-push-action@v6.9.0
uses: docker/build-push-action@v6.10.0
with:
context: .
file: "./docker/task.dockerfile"
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
env:
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

@@ -118,7 +118,8 @@ jobs:
# 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.73.0" # MSRV
# 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
@@ -134,9 +135,9 @@ jobs:
- name: "Fedora 40"
runner: ubuntu-latest
dockerfile: fedora40
- name: "Fedora 39"
- name: "Fedora 41"
runner: ubuntu-latest
dockerfile: fedora39
dockerfile: fedora41
- name: "Debian Testing"
runner: ubuntu-latest
dockerfile: debiantesting

View File

@@ -9,7 +9,7 @@ repos:
- id: check-yaml
- id: check-added-large-files
- repo: https://github.com/pre-commit/mirrors-clang-format
rev: v19.1.3
rev: v19.1.6
hooks:
- id: clang-format
types_or: [c++, c]

View File

@@ -4,7 +4,7 @@ enable_testing()
set (CMAKE_EXPORT_COMPILE_COMMANDS ON)
project (task
VERSION 3.2.0
VERSION 3.3.0
DESCRIPTION "Taskwarrior - a command-line TODO list manager"
HOMEPAGE_URL https://taskwarrior.org/)
@@ -37,7 +37,7 @@ endif (EXISTS ${CMAKE_SOURCE_DIR}/src/libshared/src AND EXISTS ${CMAKE_SOURCE_DI
message ("-- Looking for SHA1 references")
if (EXISTS ${CMAKE_SOURCE_DIR}/.git/index)
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}
OUTPUT_VARIABLE COMMIT)
configure_file ( ${CMAKE_SOURCE_DIR}/commit.h.in

1609
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,31 @@
------ 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` (#2654).
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`
@@ -23,8 +43,6 @@ Thanks to the following people for contributions to this release:
- Thomas Lauf
- Tobias Predel
------ old releases ------------------------------
3.1.0 -
- Support for `task purge` has been restored, and new support added for automatically

44
INSTALL
View File

@@ -22,7 +22,7 @@ You will need the following libraries:
- libuuid (not needed for OSX)
You will need a Rust toolchain of the Minimum Supported Rust Version (MSRV):
- rust 1.73.0
- rust 1.81.0
Basic Installation
------------------
@@ -89,6 +89,11 @@ get absolute installation directories:
CMAKE_INSTALL_PREFIX/TASK_MAN1DIR /usr/local/share/man/man1
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
--------------
@@ -110,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
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
----------------------

View File

@@ -6,6 +6,7 @@
[![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)
[![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>
</div>

View File

@@ -1,6 +1,6 @@
# Security
To report a vulnerability, please contact [dustin@cs.uchicago.edu](mailto:dustin@cs.uchicago.edu), you may use GPG public-key D8097934A92E4B4210368102FF8B7AC6154E3226 which is available [here](https://keybase.io/djmitche/pgp_keys.asc?fingerprint=d8097934a92e4b4210368102ff8b7ac6154e3226).
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:

View File

@@ -6,7 +6,8 @@ include (CheckCXXCompilerFlag)
CHECK_CXX_COMPILER_FLAG("-std=c++17" _HAS_CXX17)
if (_HAS_CXX17)
set (_CXX14_FLAGS "-std=c++17")
set (CMAKE_CXX_STANDARD 17)
set (CMAKE_CXX_EXTENSIONS OFF)
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.")
endif (_HAS_CXX17)
@@ -32,7 +33,7 @@ elseif (${CMAKE_SYSTEM_NAME} STREQUAL "GNU")
set (GNUHURD true)
elseif (${CMAKE_SYSTEM_NAME} STREQUAL "CYGWIN")
set (CYGWIN true)
set (_CXX14_FLAGS "-std=gnu++17")
set (CMAKE_CXX_EXTENSIONS ON)
else (${CMAKE_SYSTEM_NAME} MATCHES "Linux")
set (UNKNOWN true)
endif (${CMAKE_SYSTEM_NAME} MATCHES "Linux")

View File

@@ -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.
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.
## 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

@@ -139,6 +139,83 @@ Then configure Taskwarrior with:
$ task config sync.gcp.credential_path <absolute-path-to-downloaded-credentials>
.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
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
.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
.B task log <mods>
Adds a new task that is already completed, to the task list. It is affected by

View File

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

View File

@@ -14,6 +14,7 @@ add_library (task STATIC CLI2.cpp CLI2.h
Hooks.cpp Hooks.h
Lexer.cpp Lexer.h
Operation.cpp Operation.h
TF2.cpp TF2.h
TDB2.cpp TDB2.h
Task.cpp Task.h
Variant.cpp Variant.h

View File

@@ -318,6 +318,12 @@ std::string configurationDefaults =
"#sync.server.client_id # Client ID for sync to a server\n"
"#sync.server.url # URL of the sync server\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 "
"authenticate GCP Sync\n"
"#sync.gcp.bucket # Bucket for sync to GCP\n"

View File

@@ -47,7 +47,7 @@ Operation& Operation::operator=(const Operation& other) {
}
////////////////////////////////////////////////////////////////////////////////
bool Operation::operator<(Operation& other) const {
bool Operation::operator<(const Operation& other) const {
if (is_create()) {
return !other.is_create();
} else if (is_update()) {

View File

@@ -78,7 +78,7 @@ class Operation {
// Define a partial order on Operations:
// - Create < Update < Delete < UndoPoint
// - Given two updates, sort by timestamp
bool operator<(Operation &other) const;
bool operator<(const Operation &other) const;
private:
const tc::Operation *op;

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

66
src/TF2.h Normal file
View File

@@ -0,0 +1,66 @@
////////////////////////////////////////////////////////////////////////////////
//
// 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_TF2
#define INCLUDED_TF2
#include <FS.h>
#include <Task.h>
#include <stdio.h>
#include <map>
#include <string>
#include <unordered_map>
#include <unordered_set>
#include <vector>
// 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();
void target(const std::string&);
const std::vector<std::map<std::string, std::string>>& get_tasks();
std::map<std::string, std::string> load_task(const std::string&);
void load_tasks();
void load_lines();
const std::string decode(const std::string& value) const;
bool _loaded_tasks;
bool _loaded_lines;
std::vector<std::map<std::string, std::string>> _tasks;
std::vector<std::string> _lines;
File _file;
};
#endif
////////////////////////////////////////////////////////////////////////////////

View File

@@ -35,6 +35,7 @@ set (commands_SRCS Command.cpp Command.h
CmdHistory.cpp CmdHistory.h
CmdIDs.cpp CmdIDs.h
CmdImport.cpp CmdImport.h
CmdImportV2.cpp CmdImportV2.h
CmdInfo.cpp CmdInfo.h
CmdLog.cpp CmdLog.h
CmdLogo.cpp CmdLogo.h

View File

@@ -28,6 +28,7 @@
// cmake.h include header must come first
#include <CmdCustom.h>
#include <CmdNews.h>
#include <Context.h>
#include <Filter.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
Version news_version(Context::getContext().config.get("news.version"));
Version current_version = Version::Current();
auto should_nag = news_version != current_version && Context::getContext().verbose("news");
if (should_nag) {
if (CmdNews::should_nag()) {
std::ostringstream notice;
Version current_version = Version::Current();
notice << "Recently upgraded to " << current_version
<< ". "
"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"));
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 << " 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();

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

@@ -0,0 +1,46 @@
////////////////////////////////////////////////////////////////////////////////
//
// 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
//
////////////////////////////////////////////////////////////////////////////////
#ifndef INCLUDED_CMDIMPORTV2
#define INCLUDED_CMDIMPORTV2
#include <Command.h>
#include <JSON.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
////////////////////////////////////////////////////////////////////////////////

View File

@@ -159,6 +159,7 @@ std::vector<NewsItem> NewsItem::all() {
version3_0_0(items);
version3_1_0(items);
version3_2_0(items);
version3_3_0(items);
return items;
}
@@ -515,6 +516,20 @@ void NewsItem::version3_2_0(std::vector<NewsItem>& items) {
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) {
auto words = Context::getContext().cli2.getWords();
@@ -578,7 +593,7 @@ int CmdNews::execute(std::string& output) {
std::cout << outro.str();
// 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);
// Revert back to default signal handling after displaying the outro
@@ -615,3 +630,28 @@ int CmdNews::execute(std::string& output) {
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

@@ -52,6 +52,7 @@ class NewsItem {
static void version3_0_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:
NewsItem(Version, const std::string&, const std::string& = "", const std::string& = "",
@@ -63,6 +64,8 @@ class CmdNews : public Command {
public:
CmdNews();
int execute(std::string&);
static bool should_nag();
};
#endif

View File

@@ -194,9 +194,15 @@ int CmdShow::execute(std::string& output) {
" search.case.sensitive"
" sugar"
" 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.bucket"
" sync.local.server_dir"
" sync.server.client_id"
" sync.encryption_secret"
" sync.server.url"

View File

@@ -38,6 +38,7 @@
#include <taskchampion-cpp/lib.h>
#include <util.h>
#include <iostream>
#include <sstream>
////////////////////////////////////////////////////////////////////////////////
@@ -65,12 +66,11 @@ int CmdSync::execute(std::string& output) {
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 url = Context::getContext().config.get("sync.server.url");
std::string server_dir = Context::getContext().config.get("sync.local.server_dir");
std::string client_id = Context::getContext().config.get("sync.server.client_id");
std::string gcp_credential_path = Context::getContext().config.get("sync.gcp.credential_path");
std::string aws_bucket = Context::getContext().config.get("sync.aws.bucket");
std::string gcp_bucket = Context::getContext().config.get("sync.gcp.bucket");
std::string encryption_secret = Context::getContext().config.get("sync.encryption_secret");
@@ -85,7 +85,55 @@ int CmdSync::execute(std::string& output) {
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");
}
bool using_profile = 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");
}

View File

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

View File

@@ -35,13 +35,15 @@
#include <algorithm>
#include <list>
#include <map>
#include <optional>
#include <string>
#include <vector>
// recur.cpp
void handleRecurrence();
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>&);
void updateRecurrenceMask(Task&);

View File

@@ -47,8 +47,24 @@
#include <fstream>
#include <iomanip>
#include <iostream>
#include <limits>
#include <optional>
#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
// child tasks need to be generated to fill gaps.
@@ -95,7 +111,12 @@ void handleRecurrence() {
Datetime old_wait(t.get_date("wait"));
Datetime old_due(t.get_date("due"));
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);
mask += 'W';
} else {
@@ -148,7 +169,8 @@ bool generateDueDates(Task& parent, std::vector<Datetime>& allDue) {
auto recurrence_limit = Context::getContext().config.getInteger("recurrence.limit");
int recurrence_counter = 0;
Datetime now;
for (Datetime i = due;; i = getNextRecurrence(i, recur)) {
Datetime i = due;
while (1) {
allDue.push_back(i);
if (specificEnd && i > until) {
@@ -164,13 +186,23 @@ bool generateDueDates(Task& parent, std::vector<Datetime>& allDue) {
if (i > now) ++recurrence_counter;
if (recurrence_counter >= recurrence_limit) return true;
auto next = getNextRecurrence(i, recur);
if (next) {
i = *next;
} else {
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 d = current.day();
auto y = current.year();
@@ -201,7 +233,7 @@ Datetime getNextRecurrence(Datetime& current, std::string& period) {
else
days = 1;
return current + (days * 86400);
return checked_add_datetime(current, days * 86400);
}
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))
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

@@ -3,17 +3,18 @@ name = "taskchampion-lib"
version = "0.1.0"
edition = "2021"
publish = false
rust-version = "1.81.0" # MSRV
[lib]
crate-type = ["staticlib"]
[dependencies]
taskchampion = "0.9.0"
cxx = "1.0.124"
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"
cxx-build = "1.0.133"

View File

@@ -164,6 +164,36 @@ mod ffi {
avoid_snapshots: bool,
) -> Result<()>;
/// Sync with a server created from `ServerConfig::Aws` using `AwsCredentials::Profile`.
fn sync_to_aws_with_profile(
&mut self,
region: String,
bucket: String,
profile_name: String,
encryption_secret: &CxxString,
avoid_snapshots: bool,
) -> Result<()>;
/// Sync with a server created from `ServerConfig::Aws` using `AwsCredentials::AccessKey`.
fn sync_to_aws_with_access_key(
&mut self,
region: String,
bucket: String,
access_key_id: String,
secret_access_key: String,
encryption_secret: &CxxString,
avoid_snapshots: bool,
) -> Result<()>;
/// Sync with a server created from `ServerConfig::Aws` using `AwsCredentials::Default`.
fn sync_to_aws_with_default_creds(
&mut self,
region: String,
bucket: String,
encryption_secret: &CxxString,
avoid_snapshots: bool,
) -> Result<()>;
/// Sync with a server created from `ServerConfig::Gcp`.
///
/// An empty value for `credential_path` is converted to `Option::None`.
@@ -464,6 +494,7 @@ fn new_replica_on_disk(
let storage = tc::StorageConfig::OnDisk {
taskdb_dir: PathBuf::from(taskdb_dir),
create_if_missing,
access_mode: tc::storage::AccessMode::ReadWrite,
}
.into_storage()?;
Ok(Box::new(tc::Replica::new(storage).into()))
@@ -580,6 +611,63 @@ impl Replica {
Ok(self.0.sync(&mut server, avoid_snapshots)?)
}
fn sync_to_aws_with_profile(
&mut self,
region: String,
bucket: String,
profile_name: String,
encryption_secret: &CxxString,
avoid_snapshots: bool,
) -> Result<(), CppError> {
let mut server = tc::server::ServerConfig::Aws {
region,
bucket,
credentials: tc::server::AwsCredentials::Profile { profile_name },
encryption_secret: encryption_secret.as_bytes().to_vec(),
}
.into_server()?;
Ok(self.0.sync(&mut server, avoid_snapshots)?)
}
fn sync_to_aws_with_access_key(
&mut self,
region: String,
bucket: String,
access_key_id: String,
secret_access_key: String,
encryption_secret: &CxxString,
avoid_snapshots: bool,
) -> Result<(), CppError> {
let mut server = tc::server::ServerConfig::Aws {
region,
bucket,
credentials: tc::server::AwsCredentials::AccessKey {
access_key_id,
secret_access_key,
},
encryption_secret: encryption_secret.as_bytes().to_vec(),
}
.into_server()?;
Ok(self.0.sync(&mut server, avoid_snapshots)?)
}
fn sync_to_aws_with_default_creds(
&mut self,
region: String,
bucket: String,
encryption_secret: &CxxString,
avoid_snapshots: bool,
) -> Result<(), CppError> {
let mut server = tc::server::ServerConfig::Aws {
region,
bucket,
credentials: tc::server::AwsCredentials::Default,
encryption_secret: encryption_secret.as_bytes().to_vec(),
}
.into_server()?;
Ok(self.0.sync(&mut server, avoid_snapshots)?)
}
fn sync_to_gcp(
&mut self,
bucket: String,

View File

@@ -136,6 +136,7 @@ set (pythonTests
hyphenate.test.py
ids.test.py
import.test.py
import-v2.test.py
info.test.py
limit.test.py
list.all.projects.test.py

View File

@@ -1,4 +1,4 @@
FROM fedora:39
FROM fedora:41
RUN dnf update -y
RUN dnf install python3 git gcc gcc-c++ cmake make libuuid-devel libfaketime glibc-langpack-en curl -y

81
test/import-v2.test.py Executable file
View File

@@ -0,0 +1,81 @@
#!/usr/bin/env python3
###############################################################################
#
# 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
#
###############################################################################
import sys
import os
import unittest
import json
# Ensure python finds the local simpletap module
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
from basetest import Task, TestCase
from basetest.utils import mkstemp
class TestImport(TestCase):
def setUp(self):
self.t = Task()
self.t.config("dateformat", "m/d/Y")
# Multiple tasks.
self.pending = """\
[description:"bing" due:"1734480000" entry:"1734397061" modified:"1734397061" status:"pending" uuid:"ad7f7585-bff3-4b57-a116-abfc9f71ee4a"]
[description:"baz" entry:"1734397063" modified:"1734397063" status:"pending" uuid:"591ccfee-dd8d-44e9-908a-40618257cf54"]\
"""
self.completed = """\
[description:"foo" end:"1734397073" entry:"1734397054" modified:"1734397074" status:"deleted" uuid:"6849568f-55d7-4152-8db0-00356e39f0bb"]
[description:"bar" end:"1734397065" entry:"1734397056" modified:"1734397065" status:"completed" uuid:"51921813-7abb-412d-8ada-7c1417d01209"]\
"""
def test_import_v2(self):
with open(os.path.join(self.t.datadir, "pending.data"), "w") as f:
f.write(self.pending)
with open(os.path.join(self.t.datadir, "completed.data"), "w") as f:
f.write(self.completed)
code, out, err = self.t("import-v2")
self.assertIn("Imported 4 tasks", err)
code, out, err = self.t("list")
self.assertIn("bing", out)
self.assertIn("baz", out)
self.assertNotIn("foo", out)
self.assertNotIn("bar", out)
code, out, err = self.t("completed")
self.assertNotIn("bing", out)
self.assertNotIn("baz", out)
self.assertNotIn("foo", out) # deleted, not in the completed report
self.assertIn("bar", out)
if __name__ == "__main__":
from simpletap import TAPTestRunner
unittest.main(testRunner=TAPTestRunner())
# vim: ai sts=4 et sw=4 ft=python

0
test/test_hooks/on-add-accept Normal file → Executable file
View File

0
test/test_hooks/on-add-misbehave1 Normal file → Executable file
View File

0
test/test_hooks/on-add-misbehave2 Normal file → Executable file
View File

0
test/test_hooks/on-add-misbehave3 Normal file → Executable file
View File

0
test/test_hooks/on-add-misbehave4 Normal file → Executable file
View File

0
test/test_hooks/on-add-misbehave5 Normal file → Executable file
View File

0
test/test_hooks/on-add-misbehave6 Normal file → Executable file
View File

0
test/test_hooks/on-add-modify Normal file → Executable file
View File

0
test/test_hooks/on-add-reject Normal file → Executable file
View File

0
test/test_hooks/on-add.dummy Normal file → Executable file
View File

0
test/test_hooks/on-exit-bad Normal file → Executable file
View File

0
test/test_hooks/on-exit-good Normal file → Executable file
View File

0
test/test_hooks/on-exit-misbehave1 Normal file → Executable file
View File

0
test/test_hooks/on-exit-misbehave2 Normal file → Executable file
View File

0
test/test_hooks/on-exit.dummy Normal file → Executable file
View File

0
test/test_hooks/on-launch-bad Normal file → Executable file
View File

0
test/test_hooks/on-launch-good Normal file → Executable file
View File

0
test/test_hooks/on-launch-good-env Normal file → Executable file
View File

0
test/test_hooks/on-launch-misbehave1 Normal file → Executable file
View File

0
test/test_hooks/on-launch-misbehave2 Normal file → Executable file
View File

0
test/test_hooks/on-launch.dummy Normal file → Executable file
View File

0
test/test_hooks/on-modify-accept Normal file → Executable file
View File

0
test/test_hooks/on-modify-for-template-badexit.py Normal file → Executable file
View File

0
test/test_hooks/on-modify-for-template.py Normal file → Executable file
View File

0
test/test_hooks/on-modify-misbehave2 Normal file → Executable file
View File

0
test/test_hooks/on-modify-misbehave3 Normal file → Executable file
View File

0
test/test_hooks/on-modify-misbehave4 Normal file → Executable file
View File

0
test/test_hooks/on-modify-misbehave5 Normal file → Executable file
View File

0
test/test_hooks/on-modify-misbehave6 Normal file → Executable file
View File

0
test/test_hooks/on-modify-reject Normal file → Executable file
View File

0
test/test_hooks/on-modify-revert Normal file → Executable file
View File

0
test/test_hooks/on-modify.dummy Normal file → Executable file
View File

0
test/test_hooks/wrapper.sh Normal file → Executable file
View File

View File

@@ -33,6 +33,7 @@
#include <util.h>
#include <iostream>
#include <limits>
////////////////////////////////////////////////////////////////////////////////
int TEST_NAME(int, char**) {
@@ -91,6 +92,12 @@ int TEST_NAME(int, char**) {
t.ok(nontrivial(" \t\ta"), "nontrivial ' \\t\\ta' -> true");
t.ok(nontrivial("a\t\t "), "nontrivial 'a\\t\\t ' -> true");
Datetime dt(1234526400);
Datetime max(std::numeric_limits<time_t>::max());
t.ok(checked_add_datetime(dt, 10).has_value(), "small delta");
t.ok(!checked_add_datetime(dt, 0x100000000).has_value(), "delta > 32bit");
t.ok(!checked_add_datetime(max, 1).has_value(), "huge base time");
return 0;
}