Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2e5177aa7c | ||
|
|
ae3651fd3f | ||
|
|
ddeec3512a | ||
|
|
630585d7b4 | ||
|
|
9105985c7c | ||
|
|
3bf0200602 | ||
|
|
1b9353dccc | ||
|
|
1ee69ea214 | ||
|
|
dcbe916286 | ||
|
|
cc505e4881 | ||
|
|
758ac8f850 | ||
|
|
ff325bc19e | ||
|
|
ddae5c4ba9 | ||
|
|
4add839548 | ||
|
|
3ea726f2bb | ||
|
|
ce70a182c1 | ||
|
|
8de7ff52e7 | ||
|
|
dfc36aefcf | ||
|
|
c2cb7f36a7 | ||
|
|
e5ab1bc7a5 | ||
|
|
4797c4e17e | ||
|
|
0b286460b6 | ||
|
|
a99b6084e8 | ||
|
|
5664182f5e | ||
|
|
68c63372c1 | ||
|
|
ed3667c19e | ||
|
|
caae3fa37b | ||
|
|
066bb3e331 | ||
|
|
98204b17a6 | ||
|
|
096f94d3d1 | ||
|
|
01ced3238e | ||
|
|
8cc4c461d6 | ||
|
|
3e8bda6a23 |
18
.github/workflows/checks.yml
vendored
18
.github/workflows/checks.yml
vendored
@@ -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"
|
||||
|
||||
12
.github/workflows/docker-image.yaml
vendored
12
.github/workflows/docker-image.yaml
vendored
@@ -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
32
.github/workflows/metadata-check.sh
vendored
Executable 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
|
||||
7
.github/workflows/tests.yaml
vendored
7
.github/workflows/tests.yaml
vendored
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
1609
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
24
ChangeLog
24
ChangeLog
@@ -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
44
INSTALL
@@ -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
|
||||
----------------------
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
[](https://github.com/GothenburgBitFactory/taskwarrior/releases/latest)
|
||||
[](https://github.com/GothenburgBitFactory/taskwarrior/releases/latest)
|
||||
[](https://github.com/sponsors/GothenburgBitFactory/)
|
||||
[](https://gurubase.io/g/taskwarrior)
|
||||
</br>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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" }
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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
184
src/TF2.cpp
Normal 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
66
src/TF2.h
Normal 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
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
135
src/commands/CmdImportV2.cpp
Normal file
135
src/commands/CmdImportV2.cpp
Normal 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;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
46
src/commands/CmdImportV2.h
Normal file
46
src/commands/CmdImportV2.h
Normal 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
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
Submodule src/libshared updated: 47a750c385...1a06cb4cae
@@ -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&);
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
81
test/import-v2.test.py
Executable 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
0
test/test_hooks/on-add-accept
Normal file → Executable file
0
test/test_hooks/on-add-misbehave1
Normal file → Executable file
0
test/test_hooks/on-add-misbehave1
Normal file → Executable file
0
test/test_hooks/on-add-misbehave2
Normal file → Executable file
0
test/test_hooks/on-add-misbehave2
Normal file → Executable file
0
test/test_hooks/on-add-misbehave3
Normal file → Executable file
0
test/test_hooks/on-add-misbehave3
Normal file → Executable file
0
test/test_hooks/on-add-misbehave4
Normal file → Executable file
0
test/test_hooks/on-add-misbehave4
Normal file → Executable file
0
test/test_hooks/on-add-misbehave5
Normal file → Executable file
0
test/test_hooks/on-add-misbehave5
Normal file → Executable file
0
test/test_hooks/on-add-misbehave6
Normal file → Executable file
0
test/test_hooks/on-add-misbehave6
Normal file → Executable file
0
test/test_hooks/on-add-modify
Normal file → Executable file
0
test/test_hooks/on-add-modify
Normal file → Executable file
0
test/test_hooks/on-add-reject
Normal file → Executable file
0
test/test_hooks/on-add-reject
Normal file → Executable file
0
test/test_hooks/on-add.dummy
Normal file → Executable file
0
test/test_hooks/on-add.dummy
Normal file → Executable file
0
test/test_hooks/on-exit-bad
Normal file → Executable file
0
test/test_hooks/on-exit-bad
Normal file → Executable file
0
test/test_hooks/on-exit-good
Normal file → Executable file
0
test/test_hooks/on-exit-good
Normal file → Executable file
0
test/test_hooks/on-exit-misbehave1
Normal file → Executable file
0
test/test_hooks/on-exit-misbehave1
Normal file → Executable file
0
test/test_hooks/on-exit-misbehave2
Normal file → Executable file
0
test/test_hooks/on-exit-misbehave2
Normal file → Executable file
0
test/test_hooks/on-exit.dummy
Normal file → Executable file
0
test/test_hooks/on-exit.dummy
Normal file → Executable file
0
test/test_hooks/on-launch-bad
Normal file → Executable file
0
test/test_hooks/on-launch-bad
Normal file → Executable file
0
test/test_hooks/on-launch-good
Normal file → Executable file
0
test/test_hooks/on-launch-good
Normal file → Executable file
0
test/test_hooks/on-launch-good-env
Normal file → Executable file
0
test/test_hooks/on-launch-good-env
Normal file → Executable file
0
test/test_hooks/on-launch-misbehave1
Normal file → Executable file
0
test/test_hooks/on-launch-misbehave1
Normal file → Executable file
0
test/test_hooks/on-launch-misbehave2
Normal file → Executable file
0
test/test_hooks/on-launch-misbehave2
Normal file → Executable file
0
test/test_hooks/on-launch.dummy
Normal file → Executable file
0
test/test_hooks/on-launch.dummy
Normal file → Executable file
0
test/test_hooks/on-modify-accept
Normal file → Executable file
0
test/test_hooks/on-modify-accept
Normal file → Executable file
0
test/test_hooks/on-modify-for-template-badexit.py
Normal file → Executable file
0
test/test_hooks/on-modify-for-template-badexit.py
Normal file → Executable file
0
test/test_hooks/on-modify-for-template.py
Normal file → Executable file
0
test/test_hooks/on-modify-for-template.py
Normal file → Executable file
0
test/test_hooks/on-modify-misbehave2
Normal file → Executable file
0
test/test_hooks/on-modify-misbehave2
Normal file → Executable file
0
test/test_hooks/on-modify-misbehave3
Normal file → Executable file
0
test/test_hooks/on-modify-misbehave3
Normal file → Executable file
0
test/test_hooks/on-modify-misbehave4
Normal file → Executable file
0
test/test_hooks/on-modify-misbehave4
Normal file → Executable file
0
test/test_hooks/on-modify-misbehave5
Normal file → Executable file
0
test/test_hooks/on-modify-misbehave5
Normal file → Executable file
0
test/test_hooks/on-modify-misbehave6
Normal file → Executable file
0
test/test_hooks/on-modify-misbehave6
Normal file → Executable file
0
test/test_hooks/on-modify-reject
Normal file → Executable file
0
test/test_hooks/on-modify-reject
Normal file → Executable file
0
test/test_hooks/on-modify-revert
Normal file → Executable file
0
test/test_hooks/on-modify-revert
Normal file → Executable file
0
test/test_hooks/on-modify.dummy
Normal file → Executable file
0
test/test_hooks/on-modify.dummy
Normal file → Executable file
0
test/test_hooks/wrapper.sh
Normal file → Executable file
0
test/test_hooks/wrapper.sh
Normal file → Executable 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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user