Compare commits
78 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 | ||
|
|
7a092bea03 | ||
|
|
54a94bd18c | ||
|
|
a2f9b92d6c | ||
|
|
dcc8a8cdde | ||
|
|
c9967c20e2 | ||
|
|
7da23aee1c | ||
|
|
5b1be95f7d | ||
|
|
0ff7844732 | ||
|
|
023e7958c9 | ||
|
|
8184226319 | ||
|
|
94c95563ab | ||
|
|
6ff900f3fc | ||
|
|
8bad3cdcbc | ||
|
|
0bb32d188c | ||
|
|
c3b850898f | ||
|
|
af8c5d58c8 | ||
|
|
2db373d631 | ||
|
|
96c72f3e06 | ||
|
|
4bf6144daf | ||
|
|
3e20ad6f6f | ||
|
|
7bd3d1b892 | ||
|
|
0bd3989bab | ||
|
|
26c383d615 | ||
|
|
a8b4bcdda8 | ||
|
|
28628e5dca | ||
|
|
ff2b1cb888 | ||
|
|
cfe92ce845 | ||
|
|
c95dc9d149 | ||
|
|
d75ef7f197 | ||
|
|
c00c0e941b | ||
|
|
6a24510473 | ||
|
|
72f9cd91a5 | ||
|
|
44d443a8d6 | ||
|
|
2e3badbf99 | ||
|
|
6cfbb16966 | ||
|
|
70632b088e | ||
|
|
d46e5eca58 | ||
|
|
05da133eb6 | ||
|
|
c719cce4f1 | ||
|
|
4ff63a7960 | ||
|
|
0f96fd31bf | ||
|
|
3d30f2ac46 | ||
|
|
49e09a9783 | ||
|
|
17889a3f25 | ||
|
|
c0b708d1f3 |
@@ -1 +0,0 @@
|
|||||||
../config.toml
|
|
||||||
39
.github/workflows/checks.yml
vendored
39
.github/workflows/checks.yml
vendored
@@ -29,7 +29,10 @@ jobs:
|
|||||||
|
|
||||||
- uses: actions-rs/toolchain@v1
|
- uses: actions-rs/toolchain@v1
|
||||||
with:
|
with:
|
||||||
toolchain: "1.70.0" # MSRV
|
# If this version is old enough to cause errors, or older than the
|
||||||
|
# TaskChampion MSRV, bump it to the MSRV of the currently-required
|
||||||
|
# TaskChampion package; if necessary, bump that version as well.
|
||||||
|
toolchain: "1.81.0" # MSRV
|
||||||
override: true
|
override: true
|
||||||
|
|
||||||
- uses: actions-rs/cargo@v1.0.3
|
- uses: actions-rs/cargo@v1.0.3
|
||||||
@@ -62,38 +65,18 @@ jobs:
|
|||||||
command: fmt
|
command: fmt
|
||||||
args: --all -- --check
|
args: --all -- --check
|
||||||
|
|
||||||
codegen:
|
cargo-metadata:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
name: "codegen"
|
name: "Cargo Metadata"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Cache cargo registry
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: ~/.cargo/registry
|
|
||||||
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
|
||||||
|
|
||||||
- name: Cache cargo build
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: target
|
|
||||||
key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
|
|
||||||
|
|
||||||
- uses: actions-rs/toolchain@v1
|
- uses: actions-rs/toolchain@v1
|
||||||
with:
|
with:
|
||||||
toolchain: "1.70.0" # MSRV
|
profile: minimal
|
||||||
|
components: rustfmt
|
||||||
|
toolchain: stable
|
||||||
override: true
|
override: true
|
||||||
|
|
||||||
- uses: actions-rs/cargo@v1.0.3
|
- name: "Check metadata"
|
||||||
with:
|
run: ".github/workflows/metadata-check.sh"
|
||||||
command: xtask
|
|
||||||
args: codegen
|
|
||||||
|
|
||||||
- name: check for changes
|
|
||||||
run: |
|
|
||||||
if ! git diff; then
|
|
||||||
echo "Generated code not up-to-date;
|
|
||||||
run `cargo run --package xtask -- codegen` and commit the result";
|
|
||||||
exit 1;
|
|
||||||
fi
|
|
||||||
|
|||||||
14
.github/workflows/docker-image.yaml
vendored
14
.github/workflows/docker-image.yaml
vendored
@@ -23,31 +23,35 @@ jobs:
|
|||||||
id-token: write
|
id-token: write
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- name: Create lowercase repository name
|
||||||
|
run: |
|
||||||
|
GHCR_REPOSITORY="${{ github.repository_owner }}"
|
||||||
|
echo "REPOSITORY=${GHCR_REPOSITORY,,}" >> ${GITHUB_ENV}
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
submodules: "recursive"
|
submodules: "recursive"
|
||||||
|
|
||||||
- name: Install cosign
|
- name: Install cosign
|
||||||
uses: sigstore/cosign-installer@v3.5.0
|
uses: sigstore/cosign-installer@v3.7.0
|
||||||
|
|
||||||
- name: Log into registry ${{ env.REGISTRY }}
|
- name: Log into registry ${{ env.REGISTRY }}
|
||||||
uses: docker/login-action@v3.3.0
|
uses: docker/login-action@v3.3.0
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build and push Taskwarrior Docker image
|
- name: Build and push Taskwarrior Docker image
|
||||||
id: build-and-push
|
id: build-and-push
|
||||||
uses: docker/build-push-action@v6.5.0
|
uses: docker/build-push-action@v6.10.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: "./docker/task.dockerfile"
|
file: "./docker/task.dockerfile"
|
||||||
push: true
|
push: true
|
||||||
tags: ${{ env.REGISTRY }}/${{ github.actor }}/task:${{ github.ref_name }}
|
tags: ${{ env.REGISTRY }}/${{ env.REPOSITORY }}/task:${{ github.ref_name }}
|
||||||
|
|
||||||
- name: Sign the published Docker image
|
- name: Sign the published Docker image
|
||||||
env:
|
env:
|
||||||
COSIGN_EXPERIMENTAL: "true"
|
COSIGN_EXPERIMENTAL: "true"
|
||||||
run: cosign sign ${{ env.REGISTRY }}/${{ github.actor }}/task@${{ steps.build-and-push.outputs.digest }}
|
run: cosign sign ${{ env.REGISTRY }}/${{ env.REPOSITORY }}/task@${{ steps.build-and-push.outputs.digest }}
|
||||||
|
|||||||
32
.github/workflows/metadata-check.sh
vendored
Executable file
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
|
||||||
3
.github/workflows/release-check.yaml
vendored
3
.github/workflows/release-check.yaml
vendored
@@ -17,6 +17,9 @@ jobs:
|
|||||||
toolchain: "stable"
|
toolchain: "stable"
|
||||||
override: true
|
override: true
|
||||||
|
|
||||||
|
- name: Install uuid-dev
|
||||||
|
run: sudo apt install uuid-dev
|
||||||
|
|
||||||
- name: make a release tarball and build from it
|
- name: make a release tarball and build from it
|
||||||
run: |
|
run: |
|
||||||
cmake -S. -Bbuild &&
|
cmake -S. -Bbuild &&
|
||||||
|
|||||||
40
.github/workflows/tests.yaml
vendored
40
.github/workflows/tests.yaml
vendored
@@ -17,13 +17,13 @@ jobs:
|
|||||||
run: cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_FLAGS=--coverage
|
run: cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_FLAGS=--coverage
|
||||||
|
|
||||||
- name: Build project
|
- name: Build project
|
||||||
run: cmake --build build --target build_tests
|
run: cmake --build build --target test_runner --target task_executable
|
||||||
|
|
||||||
- name: Test project
|
- name: Test project
|
||||||
run: ctest --test-dir build -j 8 --output-on-failure
|
run: ctest --test-dir build -j 8 --output-on-failure
|
||||||
|
|
||||||
- name: Generate a code coverage report
|
- name: Generate a code coverage report
|
||||||
uses: threeal/gcovr-action@v1.0.0
|
uses: threeal/gcovr-action@v1.1.0
|
||||||
with:
|
with:
|
||||||
coveralls-out: coverage.coveralls.json
|
coveralls-out: coverage.coveralls.json
|
||||||
excludes: |
|
excludes: |
|
||||||
@@ -94,6 +94,38 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
cargo-test:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: "Cargo Test"
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Cache cargo registry
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ~/.cargo/registry
|
||||||
|
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
|
||||||
|
- name: Cache cargo build
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: target
|
||||||
|
key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
|
||||||
|
- uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
# If this version is old enough to cause errors, or older than the
|
||||||
|
# TaskChampion MSRV, bump it to the MSRV of the currently-required
|
||||||
|
# TaskChampion package; if necessary, bump that version as well.
|
||||||
|
# This should match the MSRV in `src/taskchampion-cpp/Cargo.toml`.
|
||||||
|
toolchain: "1.81.0" # MSRV
|
||||||
|
override: true
|
||||||
|
|
||||||
|
- uses: actions-rs/cargo@v1.0.3
|
||||||
|
with:
|
||||||
|
command: test
|
||||||
|
|
||||||
tests:
|
tests:
|
||||||
needs: coverage
|
needs: coverage
|
||||||
strategy:
|
strategy:
|
||||||
@@ -103,9 +135,9 @@ jobs:
|
|||||||
- name: "Fedora 40"
|
- name: "Fedora 40"
|
||||||
runner: ubuntu-latest
|
runner: ubuntu-latest
|
||||||
dockerfile: fedora40
|
dockerfile: fedora40
|
||||||
- name: "Fedora 39"
|
- name: "Fedora 41"
|
||||||
runner: ubuntu-latest
|
runner: ubuntu-latest
|
||||||
dockerfile: fedora39
|
dockerfile: fedora41
|
||||||
- name: "Debian Testing"
|
- name: "Debian Testing"
|
||||||
runner: ubuntu-latest
|
runner: ubuntu-latest
|
||||||
dockerfile: debiantesting
|
dockerfile: debiantesting
|
||||||
|
|||||||
4
.gitmodules
vendored
4
.gitmodules
vendored
@@ -1,6 +1,6 @@
|
|||||||
[submodule "src/libshared"]
|
[submodule "src/libshared"]
|
||||||
path = src/libshared
|
path = src/libshared
|
||||||
url = https://github.com/GothenburgBitFactory/libshared.git
|
url = https://github.com/GothenburgBitFactory/libshared.git
|
||||||
[submodule "src/tc/corrosion"]
|
[submodule "src/taskchampion-cpp/corrosion"]
|
||||||
path = src/tc/corrosion
|
path = src/taskchampion-cpp/corrosion
|
||||||
url = https://github.com/corrosion-rs/corrosion.git
|
url = https://github.com/corrosion-rs/corrosion.git
|
||||||
|
|||||||
@@ -2,18 +2,18 @@
|
|||||||
# See https://pre-commit.com/hooks.html for more hooks
|
# See https://pre-commit.com/hooks.html for more hooks
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v4.6.0
|
rev: v5.0.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
- id: end-of-file-fixer
|
- id: end-of-file-fixer
|
||||||
- id: check-yaml
|
- id: check-yaml
|
||||||
- id: check-added-large-files
|
- id: check-added-large-files
|
||||||
- repo: https://github.com/pre-commit/mirrors-clang-format
|
- repo: https://github.com/pre-commit/mirrors-clang-format
|
||||||
rev: v18.1.8
|
rev: v19.1.6
|
||||||
hooks:
|
hooks:
|
||||||
- id: clang-format
|
- id: clang-format
|
||||||
types_or: [c++, c]
|
types_or: [c++, c]
|
||||||
- repo: https://github.com/psf/black
|
- repo: https://github.com/psf/black
|
||||||
rev: 24.4.2
|
rev: 24.10.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: black
|
- id: black
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ enable_testing()
|
|||||||
set (CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
set (CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||||
|
|
||||||
project (task
|
project (task
|
||||||
VERSION 3.1.0
|
VERSION 3.3.0
|
||||||
DESCRIPTION "Taskwarrior - a command-line TODO list manager"
|
DESCRIPTION "Taskwarrior - a command-line TODO list manager"
|
||||||
HOMEPAGE_URL https://taskwarrior.org/)
|
HOMEPAGE_URL https://taskwarrior.org/)
|
||||||
|
|
||||||
@@ -26,18 +26,18 @@ if (ENABLE_WASM)
|
|||||||
endif (ENABLE_WASM)
|
endif (ENABLE_WASM)
|
||||||
|
|
||||||
message ("-- Looking for git submodules")
|
message ("-- Looking for git submodules")
|
||||||
if (EXISTS ${CMAKE_SOURCE_DIR}/src/libshared/src AND EXISTS ${CMAKE_SOURCE_DIR}/src/tc/corrosion)
|
if (EXISTS ${CMAKE_SOURCE_DIR}/src/libshared/src AND EXISTS ${CMAKE_SOURCE_DIR}/src/taskchampion-cpp/corrosion)
|
||||||
message ("-- Found git submodules")
|
message ("-- Found git submodules")
|
||||||
else (EXISTS ${CMAKE_SOURCE_DIR}/src/libshared/src)
|
else (EXISTS ${CMAKE_SOURCE_DIR}/src/libshared/src)
|
||||||
message ("-- Cloning git submodules")
|
message ("-- Cloning git submodules")
|
||||||
execute_process (COMMAND git submodule update --init
|
execute_process (COMMAND git submodule update --init
|
||||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
|
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
endif (EXISTS ${CMAKE_SOURCE_DIR}/src/libshared/src AND EXISTS ${CMAKE_SOURCE_DIR}/src/tc/corrosion)
|
endif (EXISTS ${CMAKE_SOURCE_DIR}/src/libshared/src AND EXISTS ${CMAKE_SOURCE_DIR}/src/taskchampion-cpp/corrosion)
|
||||||
|
|
||||||
message ("-- Looking for SHA1 references")
|
message ("-- Looking for SHA1 references")
|
||||||
if (EXISTS ${CMAKE_SOURCE_DIR}/.git/index)
|
if (EXISTS ${CMAKE_SOURCE_DIR}/.git/index)
|
||||||
set (HAVE_COMMIT true)
|
set (HAVE_COMMIT true)
|
||||||
execute_process (COMMAND git log -1 --pretty=format:%h
|
execute_process (COMMAND git log -1 --pretty=format:%h --no-show-signature
|
||||||
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
|
||||||
OUTPUT_VARIABLE COMMIT)
|
OUTPUT_VARIABLE COMMIT)
|
||||||
configure_file ( ${CMAKE_SOURCE_DIR}/commit.h.in
|
configure_file ( ${CMAKE_SOURCE_DIR}/commit.h.in
|
||||||
@@ -142,7 +142,7 @@ configure_file (
|
|||||||
|
|
||||||
add_subdirectory (src)
|
add_subdirectory (src)
|
||||||
add_subdirectory (src/commands)
|
add_subdirectory (src/commands)
|
||||||
add_subdirectory (src/tc)
|
add_subdirectory (src/taskchampion-cpp)
|
||||||
add_subdirectory (src/columns)
|
add_subdirectory (src/columns)
|
||||||
add_subdirectory (doc)
|
add_subdirectory (doc)
|
||||||
add_subdirectory (scripts)
|
add_subdirectory (scripts)
|
||||||
|
|||||||
2236
Cargo.lock
generated
2236
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
13
Cargo.toml
13
Cargo.toml
@@ -1,18 +1,7 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
|
|
||||||
members = [
|
members = [
|
||||||
"src/tc/lib",
|
"src/taskchampion-cpp",
|
||||||
"xtask",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
# All Rust dependencies are defined here, and then referenced by the
|
|
||||||
# Cargo.toml's in the members with `foo.workspace = true`.
|
|
||||||
[workspace.dependencies]
|
|
||||||
anyhow = "1.0"
|
|
||||||
ffizz-header = "0.5"
|
|
||||||
libc = "0.2.136"
|
|
||||||
pretty_assertions = "1"
|
|
||||||
regex = "^1.10.2"
|
|
||||||
taskchampion = "0.6"
|
|
||||||
|
|||||||
45
ChangeLog
45
ChangeLog
@@ -1,5 +1,48 @@
|
|||||||
------ current release ---------------------------
|
------ current release ---------------------------
|
||||||
|
|
||||||
|
- Sync now supports AWS S3 as a backend.
|
||||||
|
- A new `task import-v2` command allows importing Taskwarrior-2.x
|
||||||
|
data files directly.
|
||||||
|
|
||||||
|
3.3.0 -
|
||||||
|
|
||||||
|
Thanks to the following people for contributions to this release:
|
||||||
|
|
||||||
|
- Chongyun Lee
|
||||||
|
- David Tolnay
|
||||||
|
- Dustin J. Mitchell
|
||||||
|
- Felix Schurk
|
||||||
|
- geoffpaulsen
|
||||||
|
- Kalle Kietäväinen
|
||||||
|
- Kursat Aktas
|
||||||
|
- Scott Mcdermott
|
||||||
|
- Thomas Lauf
|
||||||
|
|
||||||
|
------ old releases ------------------------------
|
||||||
|
|
||||||
|
3.2.0 -
|
||||||
|
|
||||||
|
- Support for the journal in `task info` has been restored (#3671) and the
|
||||||
|
task info output no longer contains `tag_` values (#3619).
|
||||||
|
- The `rc.weekstart` value now affects calculation of week numbers in
|
||||||
|
expressions like `2013-W49` (#3654).
|
||||||
|
- Build-time flag `ENABLE_TLS_NATIVE_ROOTS` will cause `task sync` to use the
|
||||||
|
system TLS roots instead of its built-in roots to authenticate the server (#3660).
|
||||||
|
- The output from `task undo` is now more human-readable. The `undo.style`
|
||||||
|
configuraiton option, which has had no effect since 3.0.0, is now removed (3672).
|
||||||
|
- Fetching pending tasks is now more efficient (#3661).
|
||||||
|
|
||||||
|
Thanks to the following people for contributions to this release:
|
||||||
|
|
||||||
|
- Denis Zh.
|
||||||
|
- Dustin J. Mitchell
|
||||||
|
- Fredrik Lanker
|
||||||
|
- Gagan Nagaraj
|
||||||
|
- Jan Christian Grünhage
|
||||||
|
- Scott Mcdermott
|
||||||
|
- Thomas Lauf
|
||||||
|
- Tobias Predel
|
||||||
|
|
||||||
3.1.0 -
|
3.1.0 -
|
||||||
|
|
||||||
- Support for `task purge` has been restored, and new support added for automatically
|
- Support for `task purge` has been restored, and new support added for automatically
|
||||||
@@ -35,8 +78,6 @@ Thanks to the following people for contributions to this release:
|
|||||||
- Steve Dondley
|
- Steve Dondley
|
||||||
- Will R S Hansen
|
- Will R S Hansen
|
||||||
|
|
||||||
------ old releases ------------------------------
|
|
||||||
|
|
||||||
3.0.2 -
|
3.0.2 -
|
||||||
|
|
||||||
- Fix an accidentally-included debug print which polluted output of
|
- Fix an accidentally-included debug print which polluted output of
|
||||||
|
|||||||
50
INSTALL
50
INSTALL
@@ -21,6 +21,8 @@ You will need a C++ compiler that supports full C++17, which includes:
|
|||||||
You will need the following libraries:
|
You will need the following libraries:
|
||||||
- libuuid (not needed for OSX)
|
- libuuid (not needed for OSX)
|
||||||
|
|
||||||
|
You will need a Rust toolchain of the Minimum Supported Rust Version (MSRV):
|
||||||
|
- rust 1.81.0
|
||||||
|
|
||||||
Basic Installation
|
Basic Installation
|
||||||
------------------
|
------------------
|
||||||
@@ -29,9 +31,9 @@ Briefly, these shell commands will unpack, build and install Taskwarrior:
|
|||||||
|
|
||||||
$ tar xzf task-X.Y.Z.tar.gz [1]
|
$ tar xzf task-X.Y.Z.tar.gz [1]
|
||||||
$ cd task-X.Y.Z [2]
|
$ cd task-X.Y.Z [2]
|
||||||
$ cmake -DCMAKE_BUILD_TYPE=release . [3]
|
$ cmake -S . -B build -DCMAKE_BUILD_TYPE=Release . [3]
|
||||||
$ make [4]
|
$ cmake --build build [4]
|
||||||
$ sudo make install [5]
|
$ sudo cmake --install build [5]
|
||||||
$ cd .. ; rm -r task-X.Y.Z [6]
|
$ cd .. ; rm -r task-X.Y.Z [6]
|
||||||
|
|
||||||
These commands are explained below:
|
These commands are explained below:
|
||||||
@@ -87,6 +89,11 @@ get absolute installation directories:
|
|||||||
CMAKE_INSTALL_PREFIX/TASK_MAN1DIR /usr/local/share/man/man1
|
CMAKE_INSTALL_PREFIX/TASK_MAN1DIR /usr/local/share/man/man1
|
||||||
CMAKE_INSTALL_PREFIX/TASK_MAN5DIR /usr/local/share/man/man5
|
CMAKE_INSTALL_PREFIX/TASK_MAN5DIR /usr/local/share/man/man5
|
||||||
|
|
||||||
|
The following variables control aspects of the build process:
|
||||||
|
|
||||||
|
SYSTEM_CORROSION - Use system provided corrosion instead of vendored version
|
||||||
|
ENABLE_TLS_NATIVE_ROOTS - Use the system's TLS root certificates
|
||||||
|
|
||||||
|
|
||||||
Uninstallation
|
Uninstallation
|
||||||
--------------
|
--------------
|
||||||
@@ -108,6 +115,43 @@ If Taskwarrior will not build on your system, first take a look at the Operating
|
|||||||
System notes below. If this doesn't help, then go to the Troubleshooting
|
System notes below. If this doesn't help, then go to the Troubleshooting
|
||||||
section, which includes instructions on how to contact us for help.
|
section, which includes instructions on how to contact us for help.
|
||||||
|
|
||||||
|
Offline Build Notes
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
It is common for packaging systems (e.g. NixOS, FreeBSD Ports Collection, pkgsrc, etc)
|
||||||
|
to disable networking during builds. This restriction requires all distribution files
|
||||||
|
to be prepositioned after checksum verification as a prerequisite for the build. The
|
||||||
|
following steps have been successful in allowing Taskwarrior to be built in this
|
||||||
|
environment:
|
||||||
|
|
||||||
|
1. Extract all crates in a known location, e.g. ${WRKDIR}/cargo-crates
|
||||||
|
This includes crates needed for corrosion (search for Cargo.lock files)
|
||||||
|
|
||||||
|
2. Create .cargo-checksum.json for each crate
|
||||||
|
For example:
|
||||||
|
printf '{"package":"%s","files":{}}' $(sha256 -q ${DISTDIR}/rayon-core-1.12.1.tar.gz) \
|
||||||
|
> ${WRKDIR}/cargo-crates/rayon-core-1.12.1/.cargo-checksum.json
|
||||||
|
|
||||||
|
3. Create a custom config.toml file
|
||||||
|
For example, ${WRKDIR}/.cargo/config.toml
|
||||||
|
[source.cargo]
|
||||||
|
directory = '${WRKDIR}/cargo-crates'
|
||||||
|
[source.crates-io]
|
||||||
|
replace-with = 'cargo'
|
||||||
|
|
||||||
|
4. After running cmake, configure cargo
|
||||||
|
For example:
|
||||||
|
cd ${WRKSRC} && ${SETENV} ${MAKE_ENV} ${CARGO_ENV} \
|
||||||
|
/usr/local/bin/cargo update \
|
||||||
|
--manifest-path ${WRKDIR}/.cargo/config.toml \
|
||||||
|
--verbose
|
||||||
|
|
||||||
|
5. Set CARGO_HOME in environment
|
||||||
|
For example
|
||||||
|
CARGO_HOME=${WRKDIR}/.cargo
|
||||||
|
|
||||||
|
The build and installation steps should be the same as a standard build
|
||||||
|
at this point.
|
||||||
|
|
||||||
Operating System Notes
|
Operating System Notes
|
||||||
----------------------
|
----------------------
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
[](https://github.com/GothenburgBitFactory/taskwarrior/releases/latest)
|
[](https://github.com/GothenburgBitFactory/taskwarrior/releases/latest)
|
||||||
[](https://github.com/GothenburgBitFactory/taskwarrior/releases/latest)
|
[](https://github.com/GothenburgBitFactory/taskwarrior/releases/latest)
|
||||||
[](https://github.com/sponsors/GothenburgBitFactory/)
|
[](https://github.com/sponsors/GothenburgBitFactory/)
|
||||||
|
[](https://gurubase.io/g/taskwarrior)
|
||||||
</br>
|
</br>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
13
SECURITY.md
Normal file
13
SECURITY.md
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Security
|
||||||
|
|
||||||
|
To report a vulnerability, please contact Dustin via signal, [`djmitche.78`](https://signal.me/#eu/2T98jpkMAzvFL2wg3OkZnNrfhk1DFfu6eqkMEPqcAuCsLZPVk39A67rp4khmrMNF).
|
||||||
|
Initial response is expected within ~48h.
|
||||||
|
|
||||||
|
We kindly ask to follow the responsible disclosure model and refrain from sharing information until:
|
||||||
|
|
||||||
|
1. Vulnerabilities are patched in Taskwarrior + 60 days to coordinate with distributions.
|
||||||
|
2. 90 days since the vulnerability is disclosed to us.
|
||||||
|
|
||||||
|
We recognise the legitimacy of public interest and accept that security researchers can publish information after 90-days deadline unilaterally.
|
||||||
|
|
||||||
|
We will assist with obtaining CVE and acknowledge the vulnerabilities reported.
|
||||||
@@ -6,7 +6,8 @@ include (CheckCXXCompilerFlag)
|
|||||||
CHECK_CXX_COMPILER_FLAG("-std=c++17" _HAS_CXX17)
|
CHECK_CXX_COMPILER_FLAG("-std=c++17" _HAS_CXX17)
|
||||||
|
|
||||||
if (_HAS_CXX17)
|
if (_HAS_CXX17)
|
||||||
set (_CXX14_FLAGS "-std=c++17")
|
set (CMAKE_CXX_STANDARD 17)
|
||||||
|
set (CMAKE_CXX_EXTENSIONS OFF)
|
||||||
else (_HAS_CXX17)
|
else (_HAS_CXX17)
|
||||||
message (FATAL_ERROR "C++17 support missing. Try upgrading your C++ compiler. If you have a good reason for using an outdated compiler, please let us know at support@gothenburgbitfactory.org.")
|
message (FATAL_ERROR "C++17 support missing. Try upgrading your C++ compiler. If you have a good reason for using an outdated compiler, please let us know at support@gothenburgbitfactory.org.")
|
||||||
endif (_HAS_CXX17)
|
endif (_HAS_CXX17)
|
||||||
@@ -32,7 +33,7 @@ elseif (${CMAKE_SYSTEM_NAME} STREQUAL "GNU")
|
|||||||
set (GNUHURD true)
|
set (GNUHURD true)
|
||||||
elseif (${CMAKE_SYSTEM_NAME} STREQUAL "CYGWIN")
|
elseif (${CMAKE_SYSTEM_NAME} STREQUAL "CYGWIN")
|
||||||
set (CYGWIN true)
|
set (CYGWIN true)
|
||||||
set (_CXX14_FLAGS "-std=gnu++17")
|
set (CMAKE_CXX_EXTENSIONS ON)
|
||||||
else (${CMAKE_SYSTEM_NAME} MATCHES "Linux")
|
else (${CMAKE_SYSTEM_NAME} MATCHES "Linux")
|
||||||
set (UNKNOWN true)
|
set (UNKNOWN true)
|
||||||
endif (${CMAKE_SYSTEM_NAME} MATCHES "Linux")
|
endif (${CMAKE_SYSTEM_NAME} MATCHES "Linux")
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
[alias]
|
|
||||||
xtask = "run --package xtask --"
|
|
||||||
@@ -52,9 +52,9 @@ cmake --build build-clang
|
|||||||
## Run the Test Suite:
|
## Run the Test Suite:
|
||||||
For running the test suite [ctest](https://cmake.org/cmake/help/latest/manual/ctest.1.html) is used.
|
For running the test suite [ctest](https://cmake.org/cmake/help/latest/manual/ctest.1.html) is used.
|
||||||
Before one can run the test suite the `task_executable` must be built.
|
Before one can run the test suite the `task_executable` must be built.
|
||||||
After that also the `build_tests` target must be build, which can be done over:
|
After that also the `test_runner` target must be build, which can be done over:
|
||||||
```sh
|
```sh
|
||||||
cmake --build build --target build_tests
|
cmake --build build --target test_runner
|
||||||
```
|
```
|
||||||
Again you may also use the `-j <number-of-jobs>` option for parallel builds.
|
Again you may also use the `-j <number-of-jobs>` option for parallel builds.
|
||||||
|
|
||||||
@@ -94,3 +94,13 @@ They can be found in the [ctest](https://cmake.org/cmake/help/latest/manual/ctes
|
|||||||
Note that any development should be performed using a git clone, and the current development branch.
|
Note that any development should be performed using a git clone, and the current development branch.
|
||||||
The source tarballs do not reflect HEAD, and do not contain the test suite.
|
The source tarballs do not reflect HEAD, and do not contain the test suite.
|
||||||
Follow the [GitHub flow](https://docs.github.com/en/get-started/quickstart/github-flow) for creating a pull request.
|
Follow the [GitHub flow](https://docs.github.com/en/get-started/quickstart/github-flow) for creating a pull request.
|
||||||
|
|
||||||
|
## Using a Custom Version of TaskChampion
|
||||||
|
|
||||||
|
To build against a different version of Taskchampion, modify the requirement in `src/taskchampion-cpp/Cargo.toml`.
|
||||||
|
|
||||||
|
To build from a local checkout, replace the version with a [path dependency](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#specifying-path-dependencies), giving the path to the directory containing TaskChampion's `Cargo.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
taskchampion = { path = "path/to/taskchampion" }
|
||||||
|
```
|
||||||
|
|||||||
@@ -7,15 +7,12 @@ Taskwarrior has historically been a C++ project, but as of taskwarrior-3.0.0, th
|
|||||||
TaskChampion implements storage and access to "replicas" containing a user's tasks.
|
TaskChampion implements storage and access to "replicas" containing a user's tasks.
|
||||||
It defines an abstract model for this data, and also provides a simple Rust API for manipulating replicas.
|
It defines an abstract model for this data, and also provides a simple Rust API for manipulating replicas.
|
||||||
It also defines a method of synchronizing replicas and provides an implementation of that method in the form of a sync server.
|
It also defines a method of synchronizing replicas and provides an implementation of that method in the form of a sync server.
|
||||||
TaskChampion provides a C interface via the `taskchampion-lib` crate, at `src/tc/lib`.
|
|
||||||
|
|
||||||
Other applications, besides Taskwarrior, can use TaskChampion to manage tasks.
|
Other applications, besides Taskwarrior, can use TaskChampion to manage tasks.
|
||||||
Taskwarrior is just one application using the TaskChampion interface.
|
Taskwarrior is just one application using the TaskChampion interface.
|
||||||
|
|
||||||
## Taskwarrior's use of TaskChampion
|
## Taskwarrior's use of TaskChampion
|
||||||
|
|
||||||
Taskwarrior's interface to TaskChampion has a few layers:
|
Taskwarrior's interface to TaskChampion is in `src/taskchampion-cpp`.
|
||||||
|
This links to `taskchampion` as a Rust dependency, and uses [cxx](https://cxx.rs) to build a C++ API for it.
|
||||||
* A Rust library, `takschampion-lib`, that presents `extern "C"` functions for use from C++, essentially defining a C interface to TaskChampion.
|
That API is defined, and documented, in `src/taskchampion-cpp/src/lib.rs`, and available in the `tc` namespace in C++ code.
|
||||||
* C++ wrappers for the types from `taskchampion-lib`, defined in [`src/tc`](../../src/tc), ensuring memory safety (with `unique_ptr`) and adding methods corresponding to the Rust API's methods.
|
|
||||||
The wrapper types are in the C++ namespace, `tc`.
|
|
||||||
|
|||||||
@@ -278,7 +278,7 @@ The keyword rule shown here as 'keyword.' corresponds to a wildcard pattern,
|
|||||||
meaning 'color.keyword.*', or in other words all the keyword rules.
|
meaning 'color.keyword.*', or in other words all the keyword rules.
|
||||||
|
|
||||||
There is also 'color.project.none', 'color.tag.none' and
|
There is also 'color.project.none', 'color.tag.none' and
|
||||||
'color.uda.priority.none' to specifically represent missing data.
|
\[aq]color.uda.priority.none' to specifically represent missing data.
|
||||||
|
|
||||||
.SH THEMES
|
.SH THEMES
|
||||||
Taskwarrior supports themes. What this really means is that with the ability to
|
Taskwarrior supports themes. What this really means is that with the ability to
|
||||||
|
|||||||
@@ -139,6 +139,83 @@ Then configure Taskwarrior with:
|
|||||||
$ task config sync.gcp.credential_path <absolute-path-to-downloaded-credentials>
|
$ task config sync.gcp.credential_path <absolute-path-to-downloaded-credentials>
|
||||||
.fi
|
.fi
|
||||||
|
|
||||||
|
.SS Amazon Web Services
|
||||||
|
|
||||||
|
To synchronize your tasks to AWS, select a region near you and use the AWS
|
||||||
|
console to create a new S3 bucket. The default settings for the bucket are
|
||||||
|
adequate.
|
||||||
|
|
||||||
|
You will also need an AWS IAM user with the following policy, where BUCKETNAME
|
||||||
|
is the name of the bucket. The same user can be configured for multiple
|
||||||
|
Taskwarrior clients.
|
||||||
|
|
||||||
|
.nf
|
||||||
|
{
|
||||||
|
"Version": "2012-10-17",
|
||||||
|
"Statement": [
|
||||||
|
{
|
||||||
|
"Sid": "TaskChampion",
|
||||||
|
"Effect": "Allow",
|
||||||
|
"Action": [
|
||||||
|
"s3:PutObject",
|
||||||
|
"s3:GetObject",
|
||||||
|
"s3:ListBucket",
|
||||||
|
"s3:DeleteObject"
|
||||||
|
],
|
||||||
|
"Resource": [
|
||||||
|
"arn:aws:s3:::BUCKETNAME",
|
||||||
|
"arn:aws:s3:::BUCKETNAME/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
.fi
|
||||||
|
|
||||||
|
To create such a user, create a new policy in the IAM console, select the JSON
|
||||||
|
option in the policy editor, and paste the policy. Click "Next" and give the
|
||||||
|
policy a name such as "TaskwarriorSync". Next, create a new user, with a name
|
||||||
|
of your choosing, select "Attach Policies Directly", and then choose the
|
||||||
|
newly-created policy.
|
||||||
|
|
||||||
|
You will need access keys configured for the new user. Find the user in the
|
||||||
|
user list, open the "Security Credentials" tab, then click "Create access key"
|
||||||
|
and follow the steps.
|
||||||
|
|
||||||
|
At this point, you can choose how to provide those credentials to Taskwarrior.
|
||||||
|
The simplest is to include them in the Taskwarrior configuration:
|
||||||
|
|
||||||
|
.nf
|
||||||
|
$ task config sync.aws.region <region>
|
||||||
|
$ task config sync.aws.bucket <bucket-name>
|
||||||
|
$ task config sync.aws.access_key_id <access-key-id>
|
||||||
|
$ task config sync.aws.secret_access_key <secret-access-key>
|
||||||
|
.fi
|
||||||
|
|
||||||
|
Alternatively, you can set up an AWS CLI profile, using a profile name of your
|
||||||
|
choosing such as "taskwarrior-creds":
|
||||||
|
|
||||||
|
.nf
|
||||||
|
$ aws configure --profile taskwarrior-creds
|
||||||
|
.fi
|
||||||
|
|
||||||
|
Enter the access key ID and secret access key. The default region and format
|
||||||
|
are not important. Then configure Taskwarrior with:
|
||||||
|
|
||||||
|
.nf
|
||||||
|
$ task config sync.aws.region <region>
|
||||||
|
$ task config sync.aws.bucket <bucket-name>
|
||||||
|
$ task config sync.aws.profile taskwarrior-creds
|
||||||
|
.fi
|
||||||
|
|
||||||
|
To use AWS's default credential sources, such as environment variables, the
|
||||||
|
default profile, or an instance profile, set
|
||||||
|
|
||||||
|
.nf
|
||||||
|
$ task config sync.aws.region <region>
|
||||||
|
$ task config sync.aws.bucket <bucket-name>
|
||||||
|
$ task config sync.aws.default_credentials true
|
||||||
|
.fi
|
||||||
|
|
||||||
.SS Local Synchronization
|
.SS Local Synchronization
|
||||||
|
|
||||||
In order to take advantage of synchronization's side effect of saving disk
|
In order to take advantage of synchronization's side effect of saving disk
|
||||||
|
|||||||
@@ -414,6 +414,11 @@ few example scripts, such as:
|
|||||||
import-yaml.pl
|
import-yaml.pl
|
||||||
.fi
|
.fi
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.B task import-v2
|
||||||
|
Imports tasks from the Taskwarrior v2.x format. This is used when upgrading from
|
||||||
|
version 2.x to version 3.x.
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B task log <mods>
|
.B task log <mods>
|
||||||
Adds a new task that is already completed, to the task list. It is affected by
|
Adds a new task that is already completed, to the task list. It is affected by
|
||||||
|
|||||||
@@ -508,15 +508,6 @@ weekly recurring task is added with a due date of tomorrow, and recurrence.limit
|
|||||||
is set to 2, then a report will list 2 pending recurring tasks, one for tomorrow,
|
is set to 2, then a report will list 2 pending recurring tasks, one for tomorrow,
|
||||||
and one for a week from tomorrow.
|
and one for a week from tomorrow.
|
||||||
|
|
||||||
.TP
|
|
||||||
.B undo.style=side
|
|
||||||
When the 'undo' command is run, Taskwarrior presents a before and after
|
|
||||||
comparison of the data. This can be in either the 'side' style, which compares
|
|
||||||
values side-by-side in a table, or 'diff' style, which uses a format similar to
|
|
||||||
the 'diff' command.
|
|
||||||
|
|
||||||
Currently not supported.
|
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B abbreviation.minimum=2
|
.B abbreviation.minimum=2
|
||||||
Minimum length of any abbreviated command/value. This means that "ve", "ver",
|
Minimum length of any abbreviated command/value. This means that "ve", "ver",
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ color.calendar.due=color0 on rgb325
|
|||||||
color.calendar.due.today=color0 on rgb404
|
color.calendar.due.today=color0 on rgb404
|
||||||
color.calendar.holiday=color15 on rgb102
|
color.calendar.holiday=color15 on rgb102
|
||||||
color.calendar.overdue=color0 on color5
|
color.calendar.overdue=color0 on color5
|
||||||
|
color.calendar.scheduled=
|
||||||
color.calendar.today=color15 on rgb103
|
color.calendar.today=color15 on rgb103
|
||||||
color.calendar.weekend=gray12 on gray3
|
color.calendar.weekend=gray12 on gray3
|
||||||
color.calendar.weeknumber=rgb104
|
color.calendar.weeknumber=rgb104
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ color.calendar.due=white on red
|
|||||||
color.calendar.due.today=bold white on red
|
color.calendar.due.today=bold white on red
|
||||||
color.calendar.holiday=black on bright yellow
|
color.calendar.holiday=black on bright yellow
|
||||||
color.calendar.overdue=black on bright red
|
color.calendar.overdue=black on bright red
|
||||||
|
color.calendar.scheduled=
|
||||||
color.calendar.today=bold white on bright blue
|
color.calendar.today=bold white on bright blue
|
||||||
color.calendar.weekend=white on bright black
|
color.calendar.weekend=white on bright black
|
||||||
color.calendar.weeknumber=bold blue
|
color.calendar.weeknumber=bold blue
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ color.summary.bar=black on rgb141
|
|||||||
color.calendar.due.today=color15 on color1
|
color.calendar.due.today=color15 on color1
|
||||||
color.calendar.due=color0 on color1
|
color.calendar.due=color0 on color1
|
||||||
color.calendar.holiday=color0 on color11
|
color.calendar.holiday=color0 on color11
|
||||||
|
color.calendar.scheduled=
|
||||||
color.calendar.overdue=color0 on color9
|
color.calendar.overdue=color0 on color9
|
||||||
color.calendar.today=color15 on rgb013
|
color.calendar.today=color15 on rgb013
|
||||||
color.calendar.weekend=on color235
|
color.calendar.weekend=on color235
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ color.calendar.due.today=color0 on color252
|
|||||||
color.calendar.due=color0 on color249
|
color.calendar.due=color0 on color249
|
||||||
color.calendar.holiday=color255 on rgb013
|
color.calendar.holiday=color255 on rgb013
|
||||||
color.calendar.overdue=color0 on color255
|
color.calendar.overdue=color0 on color255
|
||||||
|
color.calendar.scheduled=
|
||||||
color.calendar.today=color0 on rgb115
|
color.calendar.today=color0 on rgb115
|
||||||
color.calendar.weekend=on color235
|
color.calendar.weekend=on color235
|
||||||
color.calendar.weeknumber=rgb015
|
color.calendar.weeknumber=rgb015
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ color.calendar.due=on gray8
|
|||||||
color.calendar.due.today=black on gray15
|
color.calendar.due.today=black on gray15
|
||||||
color.calendar.holiday=black on gray20
|
color.calendar.holiday=black on gray20
|
||||||
color.calendar.overdue=gray2 on gray10
|
color.calendar.overdue=gray2 on gray10
|
||||||
|
color.calendar.scheduled=
|
||||||
color.calendar.today=bold white
|
color.calendar.today=bold white
|
||||||
color.calendar.weekend=on gray2
|
color.calendar.weekend=on gray2
|
||||||
color.calendar.weeknumber=gray6
|
color.calendar.weeknumber=gray6
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ color.calendar.due=color0 on gray10
|
|||||||
color.calendar.due.today=color0 on gray15
|
color.calendar.due.today=color0 on gray15
|
||||||
color.calendar.holiday=color15 on rgb005
|
color.calendar.holiday=color15 on rgb005
|
||||||
color.calendar.overdue=color0 on gray20
|
color.calendar.overdue=color0 on gray20
|
||||||
|
color.calendar.scheduled=
|
||||||
color.calendar.today=underline black on color15
|
color.calendar.today=underline black on color15
|
||||||
color.calendar.weekend=on gray4
|
color.calendar.weekend=on gray4
|
||||||
color.calendar.weeknumber=gray10
|
color.calendar.weeknumber=gray10
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ color.calendar.due.today=color0 on color225
|
|||||||
color.calendar.due=color0 on color249
|
color.calendar.due=color0 on color249
|
||||||
color.calendar.holiday=rgb151 on rgb020
|
color.calendar.holiday=rgb151 on rgb020
|
||||||
color.calendar.overdue=color0 on color255
|
color.calendar.overdue=color0 on color255
|
||||||
|
color.calendar.scheduled=
|
||||||
color.calendar.today=color0 on rgb151
|
color.calendar.today=color0 on rgb151
|
||||||
color.calendar.weekend=on color235
|
color.calendar.weekend=on color235
|
||||||
color.calendar.weeknumber=rgb010
|
color.calendar.weeknumber=rgb010
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ color.calendar.due.today=color0 on color252
|
|||||||
color.calendar.due=color0 on color249
|
color.calendar.due=color0 on color249
|
||||||
color.calendar.holiday=rgb522 on rgb300
|
color.calendar.holiday=rgb522 on rgb300
|
||||||
color.calendar.overdue=color0 on color255
|
color.calendar.overdue=color0 on color255
|
||||||
|
color.calendar.scheduled=
|
||||||
color.calendar.today=color0 on rgb511
|
color.calendar.today=color0 on rgb511
|
||||||
color.calendar.weekend=on color235
|
color.calendar.weekend=on color235
|
||||||
color.calendar.weeknumber=rgb100
|
color.calendar.weeknumber=rgb100
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ color.calendar.due=color0 on rgb325
|
|||||||
color.calendar.due.today=color0 on rgb404
|
color.calendar.due.today=color0 on rgb404
|
||||||
color.calendar.holiday=color15 on rgb102
|
color.calendar.holiday=color15 on rgb102
|
||||||
color.calendar.overdue=color0 on color5
|
color.calendar.overdue=color0 on color5
|
||||||
|
color.calendar.scheduled=
|
||||||
color.calendar.today=color15 on rgb103
|
color.calendar.today=color15 on rgb103
|
||||||
color.calendar.weekend=gray12 on gray3
|
color.calendar.weekend=gray12 on gray3
|
||||||
color.calendar.weeknumber=rgb104
|
color.calendar.weeknumber=rgb104
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ color.calendar.due=color0 on rgb440
|
|||||||
color.calendar.due.today=color0 on rgb430
|
color.calendar.due.today=color0 on rgb430
|
||||||
color.calendar.holiday=rgb151 on rgb020
|
color.calendar.holiday=rgb151 on rgb020
|
||||||
color.calendar.overdue=color0 on rgb420
|
color.calendar.overdue=color0 on rgb420
|
||||||
|
color.calendar.scheduled=
|
||||||
color.calendar.today=color15 on rgb110
|
color.calendar.today=color15 on rgb110
|
||||||
color.calendar.weekend=on color235
|
color.calendar.weekend=on color235
|
||||||
color.calendar.weeknumber=rgb110
|
color.calendar.weeknumber=rgb110
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ color.calendar.due=on bright green
|
|||||||
color.calendar.due.today=blue on bright yellow
|
color.calendar.due.today=blue on bright yellow
|
||||||
color.calendar.holiday=on yellow
|
color.calendar.holiday=on yellow
|
||||||
color.calendar.overdue=on bright red
|
color.calendar.overdue=on bright red
|
||||||
|
color.calendar.scheduled=
|
||||||
color.calendar.today=blue
|
color.calendar.today=blue
|
||||||
color.calendar.weekend=on white
|
color.calendar.weekend=on white
|
||||||
color.calendar.weeknumber=blue
|
color.calendar.weeknumber=blue
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ color.calendar.due=on rgb343
|
|||||||
color.calendar.due.today=on rgb353
|
color.calendar.due.today=on rgb353
|
||||||
color.calendar.holiday=color0 on rgb530
|
color.calendar.holiday=color0 on rgb530
|
||||||
color.calendar.overdue=on rgb533
|
color.calendar.overdue=on rgb533
|
||||||
|
color.calendar.scheduled=
|
||||||
color.calendar.today=rgb005
|
color.calendar.today=rgb005
|
||||||
color.calendar.weekend=on gray21
|
color.calendar.weekend=on gray21
|
||||||
color.calendar.weeknumber=gray16
|
color.calendar.weeknumber=gray16
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ color.calendar.due=
|
|||||||
color.calendar.due.today=
|
color.calendar.due.today=
|
||||||
color.calendar.holiday=
|
color.calendar.holiday=
|
||||||
color.calendar.overdue=
|
color.calendar.overdue=
|
||||||
|
color.calendar.scheduled=
|
||||||
color.calendar.today=
|
color.calendar.today=
|
||||||
color.calendar.weekend=
|
color.calendar.weekend=
|
||||||
color.calendar.weeknumber=
|
color.calendar.weeknumber=
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ color.calendar.due=color0 on color9
|
|||||||
color.calendar.due.today=color0 on color1
|
color.calendar.due.today=color0 on color1
|
||||||
color.calendar.holiday=color0 on color3
|
color.calendar.holiday=color0 on color3
|
||||||
color.calendar.overdue=color0 on color5
|
color.calendar.overdue=color0 on color5
|
||||||
|
color.calendar.scheduled=
|
||||||
color.calendar.today=color0 on color4
|
color.calendar.today=color0 on color4
|
||||||
color.calendar.weekend=on color0
|
color.calendar.weekend=on color0
|
||||||
color.calendar.weeknumber=color4
|
color.calendar.weeknumber=color4
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ color.calendar.due=color7 on color9
|
|||||||
color.calendar.due.today=color7 on color1
|
color.calendar.due.today=color7 on color1
|
||||||
color.calendar.holiday=color7 on color3
|
color.calendar.holiday=color7 on color3
|
||||||
color.calendar.overdue=color7 on color5
|
color.calendar.overdue=color7 on color5
|
||||||
|
color.calendar.scheduled=
|
||||||
color.calendar.today=color7 on color4
|
color.calendar.today=color7 on color4
|
||||||
color.calendar.weekend=on color7
|
color.calendar.weekend=on color7
|
||||||
color.calendar.weeknumber=color14
|
color.calendar.weeknumber=color14
|
||||||
|
|||||||
@@ -8,10 +8,10 @@ services:
|
|||||||
security_opt:
|
security_opt:
|
||||||
- label=type:container_runtime_t
|
- label=type:container_runtime_t
|
||||||
tty: true
|
tty: true
|
||||||
test-fedora39:
|
test-fedora41:
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: test/docker/fedora39
|
dockerfile: test/docker/fedora41
|
||||||
network_mode: "host"
|
network_mode: "host"
|
||||||
security_opt:
|
security_opt:
|
||||||
- label=type:container_runtime_t
|
- label=type:container_runtime_t
|
||||||
|
|||||||
@@ -168,7 +168,6 @@ syn match taskrcGoodKey '^\s*\Vsugar='he=e-1
|
|||||||
syn match taskrcGoodKey '^\s*\Vsync.\(server.\(url\|origin\|client_id\|encryption_secret\)\|local.server_dir\)='he=e-1
|
syn match taskrcGoodKey '^\s*\Vsync.\(server.\(url\|origin\|client_id\|encryption_secret\)\|local.server_dir\)='he=e-1
|
||||||
syn match taskrcGoodKey '^\s*\Vtag.indicator='he=e-1
|
syn match taskrcGoodKey '^\s*\Vtag.indicator='he=e-1
|
||||||
syn match taskrcGoodKey '^\s*\Vuda.\S\{-}.\(default\|type\|label\|values\|indicator\)='he=e-1
|
syn match taskrcGoodKey '^\s*\Vuda.\S\{-}.\(default\|type\|label\|values\|indicator\)='he=e-1
|
||||||
syn match taskrcGoodKey '^\s*\Vundo.style='he=e-1
|
|
||||||
syn match taskrcGoodKey '^\s*\Vurgency.active.coefficient='he=e-1
|
syn match taskrcGoodKey '^\s*\Vurgency.active.coefficient='he=e-1
|
||||||
syn match taskrcGoodKey '^\s*\Vurgency.age.coefficient='he=e-1
|
syn match taskrcGoodKey '^\s*\Vurgency.age.coefficient='he=e-1
|
||||||
syn match taskrcGoodKey '^\s*\Vurgency.age.max='he=e-1
|
syn match taskrcGoodKey '^\s*\Vurgency.age.max='he=e-1
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
cmake_minimum_required (VERSION 3.22)
|
cmake_minimum_required (VERSION 3.22)
|
||||||
include_directories (${CMAKE_SOURCE_DIR}
|
include_directories (${CMAKE_SOURCE_DIR}
|
||||||
${CMAKE_SOURCE_DIR}/src
|
${CMAKE_SOURCE_DIR}/src
|
||||||
${CMAKE_SOURCE_DIR}/src/tc
|
|
||||||
${CMAKE_SOURCE_DIR}/src/tc/lib
|
|
||||||
${CMAKE_SOURCE_DIR}/src/commands
|
${CMAKE_SOURCE_DIR}/src/commands
|
||||||
${CMAKE_SOURCE_DIR}/src/columns
|
${CMAKE_SOURCE_DIR}/src/columns
|
||||||
${CMAKE_SOURCE_DIR}/src/libshared/src
|
${CMAKE_SOURCE_DIR}/src/libshared/src
|
||||||
@@ -15,6 +13,8 @@ add_library (task STATIC CLI2.cpp CLI2.h
|
|||||||
Filter.cpp Filter.h
|
Filter.cpp Filter.h
|
||||||
Hooks.cpp Hooks.h
|
Hooks.cpp Hooks.h
|
||||||
Lexer.cpp Lexer.h
|
Lexer.cpp Lexer.h
|
||||||
|
Operation.cpp Operation.h
|
||||||
|
TF2.cpp TF2.h
|
||||||
TDB2.cpp TDB2.h
|
TDB2.cpp TDB2.h
|
||||||
Task.cpp Task.h
|
Task.cpp Task.h
|
||||||
Variant.cpp Variant.h
|
Variant.cpp Variant.h
|
||||||
@@ -28,6 +28,7 @@ add_library (task STATIC CLI2.cpp CLI2.h
|
|||||||
rules.cpp
|
rules.cpp
|
||||||
sort.cpp
|
sort.cpp
|
||||||
util.cpp util.h)
|
util.cpp util.h)
|
||||||
|
target_link_libraries(task taskchampion-cpp)
|
||||||
|
|
||||||
add_library (libshared STATIC libshared/src/Color.cpp libshared/src/Color.h
|
add_library (libshared STATIC libshared/src/Color.cpp libshared/src/Color.h
|
||||||
libshared/src/Configuration.cpp libshared/src/Configuration.h
|
libshared/src/Configuration.cpp libshared/src/Configuration.h
|
||||||
@@ -52,10 +53,9 @@ add_executable (calc_executable calc.cpp)
|
|||||||
add_executable (lex_executable lex.cpp)
|
add_executable (lex_executable lex.cpp)
|
||||||
|
|
||||||
# Yes, 'task' (and hence libshared) is included twice, otherwise linking fails on assorted OSes.
|
# Yes, 'task' (and hence libshared) is included twice, otherwise linking fails on assorted OSes.
|
||||||
# Similarly for `tc`.
|
target_link_libraries (task_executable task commands columns libshared task libshared ${TASK_LIBRARIES})
|
||||||
target_link_libraries (task_executable task tc commands tc columns libshared task libshared ${TASK_LIBRARIES})
|
target_link_libraries (calc_executable task commands columns libshared task libshared ${TASK_LIBRARIES})
|
||||||
target_link_libraries (calc_executable task tc commands tc columns libshared task libshared ${TASK_LIBRARIES})
|
target_link_libraries (lex_executable task commands columns libshared task libshared ${TASK_LIBRARIES})
|
||||||
target_link_libraries (lex_executable task tc commands tc columns libshared task libshared ${TASK_LIBRARIES})
|
|
||||||
if (DARWIN)
|
if (DARWIN)
|
||||||
# SystemConfiguration is required by Rust libraries like reqwest, to get proxy configuration.
|
# SystemConfiguration is required by Rust libraries like reqwest, to get proxy configuration.
|
||||||
target_link_libraries (task_executable "-framework CoreFoundation -framework Security -framework SystemConfiguration")
|
target_link_libraries (task_executable "-framework CoreFoundation -framework Security -framework SystemConfiguration")
|
||||||
|
|||||||
@@ -37,9 +37,11 @@
|
|||||||
#include <assert.h>
|
#include <assert.h>
|
||||||
#include <format.h>
|
#include <format.h>
|
||||||
#include <main.h>
|
#include <main.h>
|
||||||
|
#include <rust/cxx.h>
|
||||||
#include <shared.h>
|
#include <shared.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
|
#include <taskchampion-cpp/lib.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
@@ -125,7 +127,6 @@ std::string configurationDefaults =
|
|||||||
"dependency.indicator=D # What to show as a dependency indicator\n"
|
"dependency.indicator=D # What to show as a dependency indicator\n"
|
||||||
"recurrence.indicator=R # What to show as a task recurrence indicator\n"
|
"recurrence.indicator=R # What to show as a task recurrence indicator\n"
|
||||||
"recurrence.limit=1 # Number of future recurring pending tasks\n"
|
"recurrence.limit=1 # Number of future recurring pending tasks\n"
|
||||||
"undo.style=side # Undo style - can be 'side', or 'diff'\n"
|
|
||||||
"regex=1 # Assume all search/filter strings are "
|
"regex=1 # Assume all search/filter strings are "
|
||||||
"regexes\n"
|
"regexes\n"
|
||||||
"xterm.title=0 # Sets xterm title for some commands\n"
|
"xterm.title=0 # Sets xterm title for some commands\n"
|
||||||
@@ -317,6 +318,12 @@ std::string configurationDefaults =
|
|||||||
"#sync.server.client_id # Client ID for sync to a server\n"
|
"#sync.server.client_id # Client ID for sync to a server\n"
|
||||||
"#sync.server.url # URL of the sync server\n"
|
"#sync.server.url # URL of the sync server\n"
|
||||||
"#sync.local.server_dir # Directory for local sync\n"
|
"#sync.local.server_dir # Directory for local sync\n"
|
||||||
|
"#sync.aws.region # region for AWS sync\n"
|
||||||
|
"#sync.aws.bucket # bucket for AWS sync\n"
|
||||||
|
"#sync.aws.access_key_id # access_key_id for AWS sync\n"
|
||||||
|
"#sync.aws.secret_access_key # secret_access_key for AWS sync\n"
|
||||||
|
"#sync.aws.profile # profile name for AWS sync\n"
|
||||||
|
"#sync.aws.default_credentials # use default credentials for AWS sync\n"
|
||||||
"#sync.gcp.credential_path # Path to JSON file containing credentials to "
|
"#sync.gcp.credential_path # Path to JSON file containing credentials to "
|
||||||
"authenticate GCP Sync\n"
|
"authenticate GCP Sync\n"
|
||||||
"#sync.gcp.bucket # Bucket for sync to GCP\n"
|
"#sync.gcp.bucket # Bucket for sync to GCP\n"
|
||||||
@@ -681,6 +688,11 @@ int Context::initialize(int argc, const char** argv) {
|
|||||||
rc = 2;
|
rc = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
catch (rust::Error& err) {
|
||||||
|
error(err.what());
|
||||||
|
rc = 2;
|
||||||
|
}
|
||||||
|
|
||||||
catch (int) {
|
catch (int) {
|
||||||
// Hooks can terminate processing by throwing integers.
|
// Hooks can terminate processing by throwing integers.
|
||||||
rc = 4;
|
rc = 4;
|
||||||
@@ -689,7 +701,7 @@ int Context::initialize(int argc, const char** argv) {
|
|||||||
catch (const std::regex_error& e) {
|
catch (const std::regex_error& e) {
|
||||||
std::cout << "regex_error caught: " << e.what() << '\n';
|
std::cout << "regex_error caught: " << e.what() << '\n';
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
error("knknown error. Please report.");
|
error("Unknown error. Please report.");
|
||||||
rc = 3;
|
rc = 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -772,6 +784,11 @@ int Context::run() {
|
|||||||
rc = 2;
|
rc = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
catch (rust::Error& err) {
|
||||||
|
error(err.what());
|
||||||
|
rc = 2;
|
||||||
|
}
|
||||||
|
|
||||||
catch (int) {
|
catch (int) {
|
||||||
// Hooks can terminate processing by throwing integers.
|
// Hooks can terminate processing by throwing integers.
|
||||||
rc = 4;
|
rc = 4;
|
||||||
@@ -1093,6 +1110,11 @@ void Context::staticInitialization() {
|
|||||||
Task::regex = Variant::searchUsingRegex = config.getBoolean("regex");
|
Task::regex = Variant::searchUsingRegex = config.getBoolean("regex");
|
||||||
Lexer::dateFormat = Variant::dateFormat = config.get("dateformat");
|
Lexer::dateFormat = Variant::dateFormat = config.get("dateformat");
|
||||||
|
|
||||||
|
auto weekStart = Datetime::dayOfWeek(config.get("weekstart"));
|
||||||
|
if (weekStart != 0 && weekStart != 1)
|
||||||
|
throw std::string(
|
||||||
|
"The 'weekstart' configuration variable may only contain 'Sunday' or 'Monday'.");
|
||||||
|
Datetime::weekstart = weekStart;
|
||||||
Datetime::isoEnabled = config.getBoolean("date.iso");
|
Datetime::isoEnabled = config.getBoolean("date.iso");
|
||||||
Datetime::standaloneDateEnabled = false;
|
Datetime::standaloneDateEnabled = false;
|
||||||
Datetime::standaloneTimeEnabled = false;
|
Datetime::standaloneTimeEnabled = false;
|
||||||
@@ -1155,6 +1177,7 @@ void Context::createDefaultConfig() {
|
|||||||
<< "\n# Color theme (uncomment one to use)\n"
|
<< "\n# Color theme (uncomment one to use)\n"
|
||||||
<< "#include light-16.theme\n"
|
<< "#include light-16.theme\n"
|
||||||
<< "#include light-256.theme\n"
|
<< "#include light-256.theme\n"
|
||||||
|
<< "#include bubblegum-256.theme\n"
|
||||||
<< "#include dark-16.theme\n"
|
<< "#include dark-16.theme\n"
|
||||||
<< "#include dark-256.theme\n"
|
<< "#include dark-256.theme\n"
|
||||||
<< "#include dark-red-256.theme\n"
|
<< "#include dark-red-256.theme\n"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Copyright 2022, Dustin J. Mitchell
|
// Copyright 2006 - 2024, Tomas Babej, Paul Beckingham, Federico Hernandez.
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
// of this software and associated documentation files (the "Software"), to deal
|
||||||
@@ -27,53 +27,47 @@
|
|||||||
#include <cmake.h>
|
#include <cmake.h>
|
||||||
// cmake.h include header must come first
|
// cmake.h include header must come first
|
||||||
|
|
||||||
#include <assert.h>
|
#include <Operation.h>
|
||||||
#include <format.h>
|
#include <taskchampion-cpp/lib.h>
|
||||||
|
|
||||||
#include "tc/Replica.h"
|
#include <vector>
|
||||||
#include "tc/Task.h"
|
|
||||||
|
|
||||||
using namespace tc::ffi;
|
|
||||||
|
|
||||||
namespace tc {
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
TCString string2tc(const std::string& str) {
|
Operation::Operation(const tc::Operation& op) : op(&op) {}
|
||||||
return tc_string_clone_with_len(str.data(), str.size());
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
std::vector<Operation> Operation::operations(const rust::Vec<tc::Operation>& operations) {
|
||||||
|
return {operations.begin(), operations.end()};
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
std::string tc2string_clone(const TCString& str) {
|
Operation& Operation::operator=(const Operation& other) {
|
||||||
size_t len;
|
op = other.op;
|
||||||
auto ptr = tc_string_content_with_len(&str, &len);
|
return *this;
|
||||||
auto rv = std::string(ptr, len);
|
|
||||||
return rv;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
std::string tc2string(TCString& str) {
|
bool Operation::operator<(const Operation& other) const {
|
||||||
auto rv = tc2string_clone(str);
|
if (is_create()) {
|
||||||
tc_string_free(&str);
|
return !other.is_create();
|
||||||
return rv;
|
} else if (is_update()) {
|
||||||
|
if (other.is_create()) {
|
||||||
|
return false;
|
||||||
|
} else if (other.is_update()) {
|
||||||
|
return get_timestamp() < other.get_timestamp();
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else if (is_delete()) {
|
||||||
|
if (other.is_create() || other.is_update() || other.is_delete()) {
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else if (is_undo_point()) {
|
||||||
|
return !other.is_undo_point();
|
||||||
|
}
|
||||||
|
return false; // not reachable
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
TCUuid uuid2tc(const std::string& str) {
|
|
||||||
TCString tcstr = tc_string_borrow(str.c_str());
|
|
||||||
TCUuid rv;
|
|
||||||
if (TC_RESULT_OK != tc_uuid_from_str(tcstr, &rv)) {
|
|
||||||
throw std::string("invalid UUID");
|
|
||||||
}
|
|
||||||
return rv;
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
std::string tc2uuid(TCUuid& uuid) {
|
|
||||||
char s[TC_UUID_STRING_BYTES];
|
|
||||||
tc_uuid_to_buf(uuid, s);
|
|
||||||
std::string str;
|
|
||||||
str.assign(s, TC_UUID_STRING_BYTES);
|
|
||||||
return str;
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
} // namespace tc
|
|
||||||
88
src/Operation.h
Normal file
88
src/Operation.h
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// Copyright 2006 - 2024, Tomas Babej, Paul Beckingham, Federico Hernandez.
|
||||||
|
//
|
||||||
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
// of this software and associated documentation files (the "Software"), to deal
|
||||||
|
// in the Software without restriction, including without limitation the rights
|
||||||
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
// copies of the Software, and to permit persons to whom the Software is
|
||||||
|
// furnished to do so, subject to the following conditions:
|
||||||
|
//
|
||||||
|
// The above copyright notice and this permission notice shall be included
|
||||||
|
// in all copies or substantial portions of the Software.
|
||||||
|
//
|
||||||
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
||||||
|
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||||
|
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
// SOFTWARE.
|
||||||
|
//
|
||||||
|
// https://www.opensource.org/licenses/mit-license.php
|
||||||
|
//
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
#ifndef INCLUDED_OPERATIOn
|
||||||
|
#define INCLUDED_OPERATIOn
|
||||||
|
|
||||||
|
#include <taskchampion-cpp/lib.h>
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
// Representation of a TaskChampion operation.
|
||||||
|
//
|
||||||
|
// This class wraps `tc::Operation&` and thus cannot outlive that underlying
|
||||||
|
// type.
|
||||||
|
class Operation {
|
||||||
|
public:
|
||||||
|
explicit Operation(const tc::Operation &);
|
||||||
|
|
||||||
|
Operation(const Operation &other) = default;
|
||||||
|
Operation &operator=(const Operation &other);
|
||||||
|
|
||||||
|
// Create a vector of Operations given the result of `Replica::get_undo_operations` or
|
||||||
|
// `Replica::get_task_operations`. The resulting vector must not outlive the input `rust::Vec`.
|
||||||
|
static std::vector<Operation> operations(const rust::Vec<tc::Operation> &);
|
||||||
|
|
||||||
|
// Methods from the underlying `tc::Operation`.
|
||||||
|
bool is_create() const { return op->is_create(); }
|
||||||
|
bool is_update() const { return op->is_update(); }
|
||||||
|
bool is_delete() const { return op->is_delete(); }
|
||||||
|
bool is_undo_point() const { return op->is_undo_point(); }
|
||||||
|
std::string get_uuid() const { return std::string(op->get_uuid().to_string()); }
|
||||||
|
::rust::Vec<::tc::PropValuePair> get_old_task() const { return op->get_old_task(); };
|
||||||
|
std::string get_property() const {
|
||||||
|
std::string value;
|
||||||
|
op->get_property(value);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
std::optional<std::string> get_value() const {
|
||||||
|
std::optional<std::string> value{std::string()};
|
||||||
|
if (!op->get_value(value.value())) {
|
||||||
|
value = std::nullopt;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
std::optional<std::string> get_old_value() const {
|
||||||
|
std::optional<std::string> old_value{std::string()};
|
||||||
|
if (!op->get_old_value(old_value.value())) {
|
||||||
|
old_value = std::nullopt;
|
||||||
|
}
|
||||||
|
return old_value;
|
||||||
|
}
|
||||||
|
time_t get_timestamp() const { return static_cast<time_t>(op->get_timestamp()); }
|
||||||
|
|
||||||
|
// Define a partial order on Operations:
|
||||||
|
// - Create < Update < Delete < UndoPoint
|
||||||
|
// - Given two updates, sort by timestamp
|
||||||
|
bool operator<(const Operation &other) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
const tc::Operation *op;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
360
src/TDB2.cpp
360
src/TDB2.cpp
@@ -46,79 +46,55 @@
|
|||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "tc/Server.h"
|
|
||||||
#include "tc/util.h"
|
|
||||||
|
|
||||||
bool TDB2::debug_mode = false;
|
bool TDB2::debug_mode = false;
|
||||||
static void dependency_scan(std::vector<Task>&);
|
static void dependency_scan(std::vector<Task>&);
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
TDB2::TDB2()
|
|
||||||
: replica{tc::Replica()} // in-memory Replica
|
|
||||||
,
|
|
||||||
_working_set{} {}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
void TDB2::open_replica(const std::string& location, bool create_if_missing) {
|
void TDB2::open_replica(const std::string& location, bool create_if_missing) {
|
||||||
replica = tc::Replica(location, create_if_missing);
|
_replica = tc::new_replica_on_disk(location, create_if_missing);
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
// Add the new task to the replica.
|
// Add the new task to the replica.
|
||||||
void TDB2::add(Task& task) {
|
void TDB2::add(Task& task) {
|
||||||
|
// Validate a task for addition. This is stricter than `task.validate`, as any
|
||||||
|
// inconsistency is probably user error.
|
||||||
|
task.validate_add();
|
||||||
|
|
||||||
// Ensure the task is consistent, and provide defaults if necessary.
|
// Ensure the task is consistent, and provide defaults if necessary.
|
||||||
// bool argument to validate() is "applyDefault", to apply default values for
|
// bool argument to validate() is "applyDefault", to apply default values for
|
||||||
// properties not otherwise given.
|
// properties not otherwise given.
|
||||||
task.validate(true);
|
task.validate(true);
|
||||||
|
|
||||||
std::string uuid = task.get("uuid");
|
rust::Vec<tc::Operation> ops;
|
||||||
|
maybe_add_undo_point(ops);
|
||||||
|
|
||||||
|
auto uuid = task.get("uuid");
|
||||||
changes[uuid] = task;
|
changes[uuid] = task;
|
||||||
|
tc::Uuid tcuuid = tc::uuid_from_string(uuid);
|
||||||
|
|
||||||
// run hooks for this new task
|
// run hooks for this new task
|
||||||
Context::getContext().hooks.onAdd(task);
|
Context::getContext().hooks.onAdd(task);
|
||||||
|
|
||||||
auto innertask = replica.import_task_with_uuid(uuid);
|
auto taskdata = tc::create_task(tcuuid, ops);
|
||||||
|
|
||||||
{
|
|
||||||
auto guard = replica.mutate_task(innertask);
|
|
||||||
|
|
||||||
// add the task attributes
|
// add the task attributes
|
||||||
for (auto& attr : task.all()) {
|
for (auto& attr : task.all()) {
|
||||||
// TaskChampion does not store uuid or id in the taskmap
|
// TaskChampion does not store uuid or id in the task data
|
||||||
if (attr == "uuid" || attr == "id") {
|
if (attr == "uuid" || attr == "id") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use `set_status` for the task status, to get expected behavior
|
taskdata->update(attr, task.get(attr), ops);
|
||||||
// with respect to the working set.
|
|
||||||
else if (attr == "status") {
|
|
||||||
innertask.set_status(Task::status2tc(Task::textToStatus(task.get(attr))));
|
|
||||||
}
|
}
|
||||||
|
replica()->commit_operations(std::move(ops));
|
||||||
|
|
||||||
// use `set_modified` to set the modified timestamp, avoiding automatic
|
invalidate_cached_info();
|
||||||
// updates to this field by TaskChampion.
|
|
||||||
else if (attr == "modified") {
|
|
||||||
auto mod = (time_t)std::stoi(task.get(attr));
|
|
||||||
innertask.set_modified(mod);
|
|
||||||
}
|
|
||||||
|
|
||||||
// otherwise, just set the k/v map value
|
|
||||||
else {
|
|
||||||
innertask.set_value(attr, std::make_optional(task.get(attr)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
auto ws = replica.working_set();
|
|
||||||
|
|
||||||
// get the ID that was assigned to this task
|
// get the ID that was assigned to this task
|
||||||
auto id = ws.by_uuid(uuid);
|
auto id = working_set()->by_uuid(tcuuid);
|
||||||
|
if (id > 0) {
|
||||||
// update the cached working set with the new information
|
task.id = id;
|
||||||
_working_set = std::make_optional(std::move(ws));
|
|
||||||
|
|
||||||
if (id.has_value()) {
|
|
||||||
task.id = id.value();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,6 +120,9 @@ void TDB2::modify(Task& task) {
|
|||||||
task.validate(false);
|
task.validate(false);
|
||||||
auto uuid = task.get("uuid");
|
auto uuid = task.get("uuid");
|
||||||
|
|
||||||
|
rust::Vec<tc::Operation> ops;
|
||||||
|
maybe_add_undo_point(ops);
|
||||||
|
|
||||||
changes[uuid] = task;
|
changes[uuid] = task;
|
||||||
|
|
||||||
// invoke the hook and allow it to modify the task before updating
|
// invoke the hook and allow it to modify the task before updating
|
||||||
@@ -151,14 +130,15 @@ void TDB2::modify(Task& task) {
|
|||||||
get(uuid, original);
|
get(uuid, original);
|
||||||
Context::getContext().hooks.onModify(original, task);
|
Context::getContext().hooks.onModify(original, task);
|
||||||
|
|
||||||
auto maybe_tctask = replica.get_task(uuid);
|
tc::Uuid tcuuid = tc::uuid_from_string(uuid);
|
||||||
if (!maybe_tctask.has_value()) {
|
auto maybe_tctask = replica()->get_task_data(tcuuid);
|
||||||
|
if (maybe_tctask.is_none()) {
|
||||||
throw std::string("task no longer exists");
|
throw std::string("task no longer exists");
|
||||||
}
|
}
|
||||||
auto tctask = std::move(maybe_tctask.value());
|
auto tctask = maybe_tctask.take();
|
||||||
auto guard = replica.mutate_task(tctask);
|
|
||||||
auto tctask_map = tctask.get_taskmap();
|
|
||||||
|
|
||||||
|
// Perform the necessary `update` operations to set all keys in `tctask`
|
||||||
|
// equal to those in `task`.
|
||||||
std::unordered_set<std::string> seen;
|
std::unordered_set<std::string> seen;
|
||||||
for (auto k : task.all()) {
|
for (auto k : task.all()) {
|
||||||
// ignore task keys that aren't stored
|
// ignore task keys that aren't stored
|
||||||
@@ -168,45 +148,76 @@ void TDB2::modify(Task& task) {
|
|||||||
seen.insert(k);
|
seen.insert(k);
|
||||||
bool update = false;
|
bool update = false;
|
||||||
auto v_new = task.get(k);
|
auto v_new = task.get(k);
|
||||||
try {
|
std::string v_tctask;
|
||||||
auto v_tctask = tctask_map.at(k);
|
if (tctask->get(k, v_tctask)) {
|
||||||
update = v_tctask != v_new;
|
update = v_tctask != v_new;
|
||||||
} catch (const std::out_of_range& oor) {
|
} else {
|
||||||
// tctask_map does not contain k, so update it
|
// tctask does not contain k, so update it
|
||||||
update = true;
|
update = true;
|
||||||
}
|
}
|
||||||
if (update) {
|
if (update) {
|
||||||
// An empty string indicates the value should be removed.
|
// An empty string indicates the value should be removed.
|
||||||
if (v_new == "") {
|
if (v_new == "") {
|
||||||
tctask.set_value(k, {});
|
tctask->update_remove(k, ops);
|
||||||
} else {
|
} else {
|
||||||
tctask.set_value(k, make_optional(v_new));
|
tctask->update(k, v_new, ops);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// we've now added and updated properties; but must find any deleted properties
|
// we've now added and updated properties; but must find any deleted properties
|
||||||
for (auto kv : tctask_map) {
|
for (auto k : tctask->properties()) {
|
||||||
if (seen.find(kv.first) == seen.end()) {
|
auto kstr = static_cast<std::string>(k);
|
||||||
tctask.set_value(kv.first, {});
|
if (seen.find(kstr) == seen.end()) {
|
||||||
|
tctask->update_remove(kstr, ops);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
replica()->commit_operations(std::move(ops));
|
||||||
|
|
||||||
|
invalidate_cached_info();
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
void TDB2::purge(Task& task) {
|
void TDB2::purge(Task& task) {
|
||||||
auto uuid = task.get("uuid");
|
auto uuid = tc::uuid_from_string(task.get("uuid"));
|
||||||
replica.delete_task(uuid);
|
rust::Vec<tc::Operation> ops;
|
||||||
|
auto maybe_tctask = replica()->get_task_data(uuid);
|
||||||
|
if (maybe_tctask.is_some()) {
|
||||||
|
auto tctask = maybe_tctask.take();
|
||||||
|
tctask->delete_task(ops);
|
||||||
|
replica()->commit_operations(std::move(ops));
|
||||||
|
}
|
||||||
|
|
||||||
|
invalidate_cached_info();
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
const tc::WorkingSet& TDB2::working_set() {
|
rust::Box<tc::Replica>& TDB2::replica() {
|
||||||
|
// Create a replica in-memory if `open_replica` has not been called. This
|
||||||
|
// occurs in tests.
|
||||||
|
if (!_replica) {
|
||||||
|
_replica = tc::new_replica_in_memory();
|
||||||
|
}
|
||||||
|
return _replica.value();
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
const rust::Box<tc::WorkingSet>& TDB2::working_set() {
|
||||||
if (!_working_set.has_value()) {
|
if (!_working_set.has_value()) {
|
||||||
_working_set = std::make_optional(replica.working_set());
|
_working_set = replica()->working_set();
|
||||||
}
|
}
|
||||||
return _working_set.value();
|
return _working_set.value();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
void TDB2::maybe_add_undo_point(rust::Vec<tc::Operation>& ops) {
|
||||||
|
// Only add an UndoPoint if there are not yet any changes.
|
||||||
|
if (changes.size() == 0) {
|
||||||
|
tc::add_undo_point(ops);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
void TDB2::get_changes(std::vector<Task>& changes) {
|
void TDB2::get_changes(std::vector<Task>& changes) {
|
||||||
std::map<std::string, Task>& changes_map = this->changes;
|
std::map<std::string, Task>& changes_map = this->changes;
|
||||||
@@ -215,170 +226,108 @@ void TDB2::get_changes(std::vector<Task>& changes) {
|
|||||||
[](const auto& kv) { return kv.second; });
|
[](const auto& kv) { return kv.second; });
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
void TDB2::revert() {
|
|
||||||
auto undo_ops = replica.get_undo_ops();
|
|
||||||
if (undo_ops.len == 0) {
|
|
||||||
std::cout << "No operations to undo.";
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (confirm_revert(undo_ops)) {
|
|
||||||
// Has the side-effect of freeing undo_ops.
|
|
||||||
replica.commit_undo_ops(undo_ops, NULL);
|
|
||||||
} else {
|
|
||||||
replica.free_replica_ops(undo_ops);
|
|
||||||
}
|
|
||||||
replica.rebuild_working_set(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
bool TDB2::confirm_revert(struct tc::ffi::TCReplicaOpList undo_ops) {
|
|
||||||
// TODO Use show_diff rather than this basic listing of operations, though
|
|
||||||
// this might be a worthy undo.style itself.
|
|
||||||
std::cout << "The following " << undo_ops.len << " operations would be reverted:\n";
|
|
||||||
for (size_t i = 0; i < undo_ops.len; i++) {
|
|
||||||
std::cout << "- ";
|
|
||||||
tc::ffi::TCReplicaOp op = undo_ops.items[i];
|
|
||||||
switch (op.operation_type) {
|
|
||||||
case tc::ffi::TCReplicaOpType::Create:
|
|
||||||
std::cout << "Create " << replica.get_op_uuid(op);
|
|
||||||
break;
|
|
||||||
case tc::ffi::TCReplicaOpType::Delete:
|
|
||||||
std::cout << "Delete " << replica.get_op_old_task_description(op);
|
|
||||||
break;
|
|
||||||
case tc::ffi::TCReplicaOpType::Update:
|
|
||||||
std::cout << "Update " << replica.get_op_uuid(op) << "\n";
|
|
||||||
std::cout << " " << replica.get_op_property(op) << ": "
|
|
||||||
<< option_string(replica.get_op_old_value(op)) << " -> "
|
|
||||||
<< option_string(replica.get_op_value(op));
|
|
||||||
break;
|
|
||||||
case tc::ffi::TCReplicaOpType::UndoPoint:
|
|
||||||
throw std::string("Can't undo UndoPoint.");
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw std::string("Can't undo non-operation.");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
std::cout << "\n";
|
|
||||||
}
|
|
||||||
return !Context::getContext().config.getBoolean("confirmation") ||
|
|
||||||
confirm(
|
|
||||||
"The undo command is not reversible. Are you sure you want to revert to the previous "
|
|
||||||
"state?");
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
std::string TDB2::option_string(std::string input) { return input == "" ? "<empty>" : input; }
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
void TDB2::show_diff(const std::string& current, const std::string& prior,
|
|
||||||
const std::string& when) {
|
|
||||||
Datetime lastChange(strtoll(when.c_str(), nullptr, 10));
|
|
||||||
|
|
||||||
// Set the colors.
|
|
||||||
Color color_red(
|
|
||||||
Context::getContext().color() ? Context::getContext().config.get("color.undo.before") : "");
|
|
||||||
Color color_green(
|
|
||||||
Context::getContext().color() ? Context::getContext().config.get("color.undo.after") : "");
|
|
||||||
|
|
||||||
auto before = prior == "" ? Task() : Task(prior);
|
|
||||||
auto after = Task(current);
|
|
||||||
|
|
||||||
if (Context::getContext().config.get("undo.style") == "side") {
|
|
||||||
Table view = before.diffForUndoSide(after);
|
|
||||||
|
|
||||||
std::cout << '\n'
|
|
||||||
<< format("The last modification was made {1}", lastChange.toString()) << '\n'
|
|
||||||
<< '\n'
|
|
||||||
<< view.render() << '\n';
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (Context::getContext().config.get("undo.style") == "diff") {
|
|
||||||
Table view = before.diffForUndoPatch(after, lastChange);
|
|
||||||
std::cout << '\n' << view.render() << '\n';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
void TDB2::gc() {
|
void TDB2::gc() {
|
||||||
Timer timer;
|
Timer timer;
|
||||||
|
|
||||||
// Allowed as an override, but not recommended.
|
// Allowed as an override, but not recommended.
|
||||||
if (Context::getContext().config.getBoolean("gc")) {
|
if (Context::getContext().config.getBoolean("gc")) {
|
||||||
replica.rebuild_working_set(true);
|
replica()->rebuild_working_set(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
Context::getContext().time_gc_us += timer.total_us();
|
Context::getContext().time_gc_us += timer.total_us();
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
void TDB2::expire_tasks() { replica.expire_tasks(); }
|
void TDB2::expire_tasks() { replica()->expire_tasks(); }
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
// Latest ID is that of the last pending task.
|
// Latest ID is that of the last pending task.
|
||||||
int TDB2::latest_id() {
|
int TDB2::latest_id() {
|
||||||
const tc::WorkingSet& ws = working_set();
|
auto& ws = working_set();
|
||||||
return (int)ws.largest_index();
|
return (int)ws->largest_index();
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
const std::vector<Task> TDB2::all_tasks() {
|
const std::vector<Task> TDB2::all_tasks() {
|
||||||
auto all_tctasks = replica.all_tasks();
|
Timer timer;
|
||||||
|
auto all_tctasks = replica()->all_task_data();
|
||||||
std::vector<Task> all;
|
std::vector<Task> all;
|
||||||
for (auto& tctask : all_tctasks) all.push_back(Task(std::move(tctask)));
|
for (auto& maybe_tctask : all_tctasks) {
|
||||||
|
auto tctask = maybe_tctask.take();
|
||||||
|
all.push_back(Task(std::move(tctask)));
|
||||||
|
}
|
||||||
|
|
||||||
|
dependency_scan(all);
|
||||||
|
|
||||||
|
Context::getContext().time_load_us += timer.total_us();
|
||||||
return all;
|
return all;
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
const std::vector<Task> TDB2::pending_tasks() {
|
const std::vector<Task> TDB2::pending_tasks() {
|
||||||
const tc::WorkingSet& ws = working_set();
|
if (!_pending_tasks) {
|
||||||
auto largest_index = ws.largest_index();
|
Timer timer;
|
||||||
|
|
||||||
|
auto pending_tctasks = replica()->pending_task_data();
|
||||||
std::vector<Task> result;
|
std::vector<Task> result;
|
||||||
for (size_t i = 0; i <= largest_index; i++) {
|
for (auto& maybe_tctask : pending_tctasks) {
|
||||||
auto maybe_uuid = ws.by_index(i);
|
auto tctask = maybe_tctask.take();
|
||||||
if (maybe_uuid.has_value()) {
|
result.push_back(Task(std::move(tctask)));
|
||||||
auto maybe_task = replica.get_task(maybe_uuid.value());
|
|
||||||
if (maybe_task.has_value()) {
|
|
||||||
result.push_back(Task(std::move(maybe_task.value())));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dependency_scan(result);
|
dependency_scan(result);
|
||||||
|
|
||||||
return result;
|
Context::getContext().time_load_us += timer.total_us();
|
||||||
|
_pending_tasks = result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return *_pending_tasks;
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
const std::vector<Task> TDB2::completed_tasks() {
|
const std::vector<Task> TDB2::completed_tasks() {
|
||||||
auto all_tctasks = replica.all_tasks();
|
if (!_completed_tasks) {
|
||||||
const tc::WorkingSet& ws = working_set();
|
auto all_tctasks = replica()->all_task_data();
|
||||||
|
auto& ws = working_set();
|
||||||
|
|
||||||
std::vector<Task> result;
|
std::vector<Task> result;
|
||||||
for (auto& tctask : all_tctasks) {
|
for (auto& maybe_tctask : all_tctasks) {
|
||||||
|
auto tctask = maybe_tctask.take();
|
||||||
// if this task is _not_ in the working set, return it.
|
// if this task is _not_ in the working set, return it.
|
||||||
if (!ws.by_uuid(tctask.get_uuid())) {
|
if (ws->by_uuid(tctask->get_uuid()) == 0) {
|
||||||
result.push_back(Task(std::move(tctask)));
|
result.push_back(Task(std::move(tctask)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
_completed_tasks = result;
|
||||||
|
}
|
||||||
|
return *_completed_tasks;
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
void TDB2::invalidate_cached_info() {
|
||||||
|
_pending_tasks = std::nullopt;
|
||||||
|
_completed_tasks = std::nullopt;
|
||||||
|
_working_set = std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
// Locate task by ID, wherever it is.
|
// Locate task by ID, wherever it is.
|
||||||
bool TDB2::get(int id, Task& task) {
|
bool TDB2::get(int id, Task& task) {
|
||||||
const tc::WorkingSet& ws = working_set();
|
auto& ws = working_set();
|
||||||
const auto maybe_uuid = ws.by_index(id);
|
const auto tcuuid = ws->by_index(id);
|
||||||
if (maybe_uuid) {
|
if (!tcuuid.is_nil()) {
|
||||||
auto maybe_task = replica.get_task(*maybe_uuid);
|
std::string uuid = static_cast<std::string>(tcuuid.to_string());
|
||||||
if (maybe_task) {
|
// Load all pending tasks in order to get dependency data, and in particular
|
||||||
task = Task{std::move(*maybe_task)};
|
// `task.is_blocking` and `task.is_blocked`, set correctly.
|
||||||
|
std::vector<Task> pending = pending_tasks();
|
||||||
|
for (auto& pending_task : pending) {
|
||||||
|
if (pending_task.get("uuid") == uuid) {
|
||||||
|
task = pending_task;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -386,22 +335,23 @@ bool TDB2::get(int id, Task& task) {
|
|||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
// Locate task by UUID, including by partial ID, wherever it is.
|
// Locate task by UUID, including by partial ID, wherever it is.
|
||||||
bool TDB2::get(const std::string& uuid, Task& task) {
|
bool TDB2::get(const std::string& uuid, Task& task) {
|
||||||
|
// Load all pending tasks in order to get dependency data, and in particular
|
||||||
|
// `task.is_blocking` and `task.is_blocked`, set correctly.
|
||||||
|
std::vector<Task> pending = pending_tasks();
|
||||||
|
|
||||||
// try by raw uuid, if the length is right
|
// try by raw uuid, if the length is right
|
||||||
if (uuid.size() == 36) {
|
for (auto& pending_task : pending) {
|
||||||
try {
|
if (closeEnough(pending_task.get("uuid"), uuid, uuid.length())) {
|
||||||
auto maybe_task = replica.get_task(uuid);
|
task = pending_task;
|
||||||
if (maybe_task) {
|
|
||||||
task = Task{std::move(*maybe_task)};
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} catch (const std::string& err) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Nothing to do but iterate over all tasks and check whether it's closeEnough
|
// Nothing to do but iterate over all tasks and check whether it's closeEnough.
|
||||||
for (auto& tctask : replica.all_tasks()) {
|
for (auto& maybe_tctask : replica()->all_task_data()) {
|
||||||
if (closeEnough(tctask.get_uuid(), uuid, uuid.length())) {
|
auto tctask = maybe_tctask.take();
|
||||||
|
auto tctask_uuid = static_cast<std::string>(tctask->get_uuid().to_string());
|
||||||
|
if (closeEnough(tctask_uuid, uuid, uuid.length())) {
|
||||||
task = Task{std::move(tctask)};
|
task = Task{std::move(tctask)};
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -446,63 +396,59 @@ const std::vector<Task> TDB2::children(Task& parent) {
|
|||||||
std::vector<Task> results;
|
std::vector<Task> results;
|
||||||
std::string this_uuid = parent.get("uuid");
|
std::string this_uuid = parent.get("uuid");
|
||||||
|
|
||||||
const tc::WorkingSet& ws = working_set();
|
auto& ws = working_set();
|
||||||
size_t end_idx = ws.largest_index();
|
size_t end_idx = ws->largest_index();
|
||||||
|
|
||||||
for (size_t i = 0; i <= end_idx; i++) {
|
for (size_t i = 0; i <= end_idx; i++) {
|
||||||
auto uuid_opt = ws.by_index(i);
|
auto uuid = ws->by_index(i);
|
||||||
if (!uuid_opt) {
|
if (uuid.is_nil()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
auto uuid = uuid_opt.value();
|
|
||||||
|
|
||||||
// skip self-references
|
// skip self-references
|
||||||
if (uuid == this_uuid) {
|
if (uuid.to_string() == this_uuid) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto task_opt = replica.get_task(uuid_opt.value());
|
auto task_opt = replica()->get_task_data(uuid);
|
||||||
if (!task_opt) {
|
if (task_opt.is_none()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
auto task = std::move(task_opt.value());
|
auto task = task_opt.take();
|
||||||
|
|
||||||
auto parent_uuid_opt = task.get_value("parent");
|
std::string parent_uuid;
|
||||||
if (!parent_uuid_opt) {
|
if (!task->get("parent", parent_uuid)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
auto parent_uuid = parent_uuid_opt.value();
|
|
||||||
|
|
||||||
if (parent_uuid == this_uuid) {
|
if (parent_uuid == this_uuid) {
|
||||||
results.push_back(Task(std::move(task)));
|
results.push_back(Task(std::move(task)));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
std::string TDB2::uuid(int id) {
|
std::string TDB2::uuid(int id) {
|
||||||
const tc::WorkingSet& ws = working_set();
|
auto& ws = working_set();
|
||||||
return ws.by_index((size_t)id).value_or("");
|
auto uuid = ws->by_index(id);
|
||||||
|
if (uuid.is_nil()) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return static_cast<std::string>(uuid.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
int TDB2::id(const std::string& uuid) {
|
int TDB2::id(const std::string& uuid) {
|
||||||
const tc::WorkingSet& ws = working_set();
|
auto& ws = working_set();
|
||||||
return (int)ws.by_uuid(uuid).value_or(0);
|
return ws->by_uuid(tc::uuid_from_string(uuid));
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
int TDB2::num_local_changes() { return (int)replica.num_local_operations(); }
|
int TDB2::num_local_changes() { return (int)replica()->num_local_operations(); }
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
int TDB2::num_reverts_possible() { return (int)replica.num_undo_points(); }
|
int TDB2::num_reverts_possible() { return (int)replica()->num_undo_points(); }
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
void TDB2::sync(tc::Server server, bool avoid_snapshots) {
|
|
||||||
replica.sync(std::move(server), avoid_snapshots);
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
void TDB2::dump() {
|
void TDB2::dump() {
|
||||||
|
|||||||
28
src/TDB2.h
28
src/TDB2.h
@@ -30,32 +30,27 @@
|
|||||||
#include <FS.h>
|
#include <FS.h>
|
||||||
#include <Task.h>
|
#include <Task.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <tc/Replica.h>
|
#include <taskchampion-cpp/lib.h>
|
||||||
#include <tc/WorkingSet.h>
|
|
||||||
|
|
||||||
#include <map>
|
#include <map>
|
||||||
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
namespace tc {
|
|
||||||
class Server;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TDB2 Class represents all the files in the task database.
|
// TDB2 Class represents all the files in the task database.
|
||||||
class TDB2 {
|
class TDB2 {
|
||||||
public:
|
public:
|
||||||
static bool debug_mode;
|
static bool debug_mode;
|
||||||
|
|
||||||
TDB2();
|
TDB2() = default;
|
||||||
|
|
||||||
void open_replica(const std::string &, bool create_if_missing);
|
void open_replica(const std::string &, bool create_if_missing);
|
||||||
void add(Task &);
|
void add(Task &);
|
||||||
void modify(Task &);
|
void modify(Task &);
|
||||||
void purge(Task &);
|
void purge(Task &);
|
||||||
void get_changes(std::vector<Task> &);
|
void get_changes(std::vector<Task> &);
|
||||||
void revert();
|
|
||||||
void gc();
|
void gc();
|
||||||
void expire_tasks();
|
void expire_tasks();
|
||||||
int latest_id();
|
int latest_id();
|
||||||
@@ -79,19 +74,22 @@ class TDB2 {
|
|||||||
|
|
||||||
void dump();
|
void dump();
|
||||||
|
|
||||||
void sync(tc::Server server, bool avoid_snapshots);
|
rust::Box<tc::Replica> &replica();
|
||||||
bool confirm_revert(struct tc::ffi::TCReplicaOpList);
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
tc::Replica replica;
|
std::optional<rust::Box<tc::Replica>> _replica;
|
||||||
std::optional<tc::WorkingSet> _working_set;
|
|
||||||
|
// Cached information from the replica
|
||||||
|
std::optional<rust::Box<tc::WorkingSet>> _working_set;
|
||||||
|
std::optional<std::vector<Task>> _pending_tasks;
|
||||||
|
std::optional<std::vector<Task>> _completed_tasks;
|
||||||
|
void invalidate_cached_info();
|
||||||
|
|
||||||
// UUID -> Task containing all tasks modified in this invocation.
|
// UUID -> Task containing all tasks modified in this invocation.
|
||||||
std::map<std::string, Task> changes;
|
std::map<std::string, Task> changes;
|
||||||
|
|
||||||
const tc::WorkingSet &working_set();
|
const rust::Box<tc::WorkingSet> &working_set();
|
||||||
static std::string option_string(std::string input);
|
void maybe_add_undo_point(rust::Vec<tc::Operation> &);
|
||||||
static void show_diff(const std::string &, const std::string &, const std::string &);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
184
src/TF2.cpp
Normal file
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
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Copyright 2022, Dustin J. Mitchell
|
// Copyright 2006 - 2024, Tomas Babej, Paul Beckingham, Federico Hernandez.
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
// of this software and associated documentation files (the "Software"), to deal
|
||||||
@@ -24,29 +24,43 @@
|
|||||||
//
|
//
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
#ifndef INCLUDED_TC_UTIL
|
#ifndef INCLUDED_TF2
|
||||||
#define INCLUDED_TC_UTIL
|
#define INCLUDED_TF2
|
||||||
|
|
||||||
|
#include <FS.h>
|
||||||
|
#include <Task.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
#include <map>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <unordered_set>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include "tc/ffi.h"
|
// TF2 Class represents a single 2.x-style file in the task database.
|
||||||
|
//
|
||||||
|
// This is only used for importing tasks from 2.x. It only reads format 4, based
|
||||||
|
// on a stripped-down version of the TF2 class from v2.6.2.
|
||||||
|
class TF2 {
|
||||||
|
public:
|
||||||
|
TF2();
|
||||||
|
~TF2();
|
||||||
|
|
||||||
namespace tc {
|
void target(const std::string&);
|
||||||
// convert a std::string into a TCString, copying the contained data
|
|
||||||
tc::ffi::TCString string2tc(const std::string&);
|
|
||||||
|
|
||||||
// convert a TCString into a std::string, leaving the TCString as-is
|
const std::vector<std::map<std::string, std::string>>& get_tasks();
|
||||||
std::string tc2string_clone(const tc::ffi::TCString&);
|
|
||||||
|
|
||||||
// convert a TCString into a std::string, freeing the TCString
|
std::map<std::string, std::string> load_task(const std::string&);
|
||||||
std::string tc2string(tc::ffi::TCString&);
|
void load_tasks();
|
||||||
|
void load_lines();
|
||||||
|
const std::string decode(const std::string& value) const;
|
||||||
|
|
||||||
// convert a TCUuid into a std::string
|
bool _loaded_tasks;
|
||||||
std::string tc2uuid(tc::ffi::TCUuid&);
|
bool _loaded_lines;
|
||||||
|
std::vector<std::map<std::string, std::string>> _tasks;
|
||||||
// parse a std::string into a TCUuid (throwing if parse fails)
|
std::vector<std::string> _lines;
|
||||||
tc::ffi::TCUuid uuid2tc(const std::string&);
|
File _file;
|
||||||
} // namespace tc
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
378
src/Task.cpp
378
src/Task.cpp
@@ -138,7 +138,7 @@ Task::Task(const json::object* obj) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
Task::Task(tc::Task obj) {
|
Task::Task(rust::Box<tc::TaskData> obj) {
|
||||||
id = 0;
|
id = 0;
|
||||||
urgency_value = 0.0;
|
urgency_value = 0.0;
|
||||||
recalc_urgency = true;
|
recalc_urgency = true;
|
||||||
@@ -146,7 +146,7 @@ Task::Task(tc::Task obj) {
|
|||||||
is_blocking = false;
|
is_blocking = false;
|
||||||
annotation_count = 0;
|
annotation_count = 0;
|
||||||
|
|
||||||
parseTC(obj);
|
parseTC(std::move(obj));
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
@@ -717,8 +717,12 @@ void Task::parseJSON(const json::object* root_obj) {
|
|||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
// Note that all fields undergo encode/decode.
|
// Note that all fields undergo encode/decode.
|
||||||
void Task::parseTC(const tc::Task& task) {
|
void Task::parseTC(rust::Box<tc::TaskData> task) {
|
||||||
data = task.get_taskmap();
|
auto items = task->items();
|
||||||
|
data.clear();
|
||||||
|
for (auto& item : items) {
|
||||||
|
data[static_cast<std::string>(item.prop)] = static_cast<std::string>(item.value);
|
||||||
|
}
|
||||||
|
|
||||||
// count annotations
|
// count annotations
|
||||||
annotation_count = 0;
|
annotation_count = 0;
|
||||||
@@ -728,11 +732,8 @@ void Task::parseTC(const tc::Task& task) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data["uuid"] = task.get_uuid();
|
data["uuid"] = static_cast<std::string>(task->get_uuid().to_string());
|
||||||
id = Context::getContext().tdb2.id(data["uuid"]);
|
id = Context::getContext().tdb2.id(data["uuid"]);
|
||||||
|
|
||||||
is_blocking = task.is_blocking();
|
|
||||||
is_blocked = task.is_blocked();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
@@ -1242,14 +1243,14 @@ void Task::fixTagsAttribute() {
|
|||||||
bool Task::isTagAttr(const std::string& attr) { return attr.compare(0, 4, "tag_") == 0; }
|
bool Task::isTagAttr(const std::string& attr) { return attr.compare(0, 4, "tag_") == 0; }
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
const std::string Task::tag2Attr(const std::string& tag) const {
|
std::string Task::tag2Attr(const std::string& tag) {
|
||||||
std::stringstream tag_attr;
|
std::stringstream tag_attr;
|
||||||
tag_attr << "tag_" << tag;
|
tag_attr << "tag_" << tag;
|
||||||
return tag_attr.str();
|
return tag_attr.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
const std::string Task::attr2Tag(const std::string& attr) const {
|
std::string Task::attr2Tag(const std::string& attr) {
|
||||||
assert(isTagAttr(attr));
|
assert(isTagAttr(attr));
|
||||||
return attr.substr(4);
|
return attr.substr(4);
|
||||||
}
|
}
|
||||||
@@ -1270,14 +1271,14 @@ void Task::fixDependsAttribute() {
|
|||||||
bool Task::isDepAttr(const std::string& attr) { return attr.compare(0, 4, "dep_") == 0; }
|
bool Task::isDepAttr(const std::string& attr) { return attr.compare(0, 4, "dep_") == 0; }
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
const std::string Task::dep2Attr(const std::string& tag) const {
|
std::string Task::dep2Attr(const std::string& tag) {
|
||||||
std::stringstream tag_attr;
|
std::stringstream tag_attr;
|
||||||
tag_attr << "dep_" << tag;
|
tag_attr << "dep_" << tag;
|
||||||
return tag_attr.str();
|
return tag_attr.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
const std::string Task::attr2Dep(const std::string& attr) const {
|
std::string Task::attr2Dep(const std::string& attr) {
|
||||||
assert(isDepAttr(attr));
|
assert(isDepAttr(attr));
|
||||||
return attr.substr(4);
|
return attr.substr(4);
|
||||||
}
|
}
|
||||||
@@ -1407,13 +1408,29 @@ void Task::substitute(const std::string& from, const std::string& to, const std:
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Validate a task for addition, raising user-visible errors for inconsistent or
|
||||||
|
// incorrect inputs. This is called before `Task::validate`.
|
||||||
|
void Task::validate_add() {
|
||||||
|
// There is no fixing a missing description.
|
||||||
|
if (!has("description"))
|
||||||
|
throw std::string("A task must have a description.");
|
||||||
|
else if (get("description") == "")
|
||||||
|
throw std::string("Cannot add a task that is blank.");
|
||||||
|
|
||||||
|
// Cannot have an old-style recur frequency with no due date - when would it recur?
|
||||||
|
if (has("recur") && (!has("due") || get("due") == ""))
|
||||||
|
throw std::string("A recurring task must also have a 'due' date.");
|
||||||
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
// The purpose of Task::validate is three-fold:
|
// The purpose of Task::validate is three-fold:
|
||||||
// 1) To provide missing attributes where possible
|
// 1) To provide missing attributes where possible
|
||||||
// 2) To provide suitable warnings about odd states
|
// 2) To provide suitable warnings about odd states
|
||||||
// 3) To generate errors when the inconsistencies are not fixable
|
// 3) To update status depending on other attributes
|
||||||
// 4) To update status depending on other attributes
|
|
||||||
//
|
//
|
||||||
|
// As required by TaskChampion, no combination of properties and values is an
|
||||||
|
// error. This function will try to make sensible defaults and resolve inconsistencies.
|
||||||
// Critically, note that despite the name this is not a read-only function.
|
// Critically, note that despite the name this is not a read-only function.
|
||||||
//
|
//
|
||||||
void Task::validate(bool applyDefault /* = true */) {
|
void Task::validate(bool applyDefault /* = true */) {
|
||||||
@@ -1427,6 +1444,8 @@ void Task::validate(bool applyDefault /* = true */) {
|
|||||||
Lexer lex(uid);
|
Lexer lex(uid);
|
||||||
std::string token;
|
std::string token;
|
||||||
Lexer::Type type;
|
Lexer::Type type;
|
||||||
|
// `uuid` is not a property in the TaskChampion model, so an invalid UUID is
|
||||||
|
// actually an error.
|
||||||
if (!lex.isUUID(token, type, true)) throw format("Not a valid UUID '{1}'.", uid);
|
if (!lex.isUUID(token, type, true)) throw format("Not a valid UUID '{1}'.", uid);
|
||||||
} else
|
} else
|
||||||
set("uuid", uuid());
|
set("uuid", uuid());
|
||||||
@@ -1542,27 +1561,27 @@ void Task::validate(bool applyDefault /* = true */) {
|
|||||||
validate_before("scheduled", "due");
|
validate_before("scheduled", "due");
|
||||||
validate_before("scheduled", "end");
|
validate_before("scheduled", "end");
|
||||||
|
|
||||||
// 3) To generate errors when the inconsistencies are not fixable
|
if (!has("description") || get("description") == "")
|
||||||
|
Context::getContext().footnote(format("Warning: task has no description."));
|
||||||
|
|
||||||
// There is no fixing a missing description.
|
// Cannot have an old-style recur frequency with no due date - when would it recur?
|
||||||
if (!has("description"))
|
if (has("recur") && (!has("due") || get("due") == "")) {
|
||||||
throw std::string("A task must have a description.");
|
Context::getContext().footnote(format("Warning: recurring task has no due date."));
|
||||||
else if (get("description") == "")
|
remove("recur");
|
||||||
throw std::string("Cannot add a task that is blank.");
|
}
|
||||||
|
|
||||||
// Cannot have a recur frequency with no due date - when would it recur?
|
// Old-style recur durations must be valid.
|
||||||
if (has("recur") && (!has("due") || get("due") == ""))
|
|
||||||
throw std::string("A recurring task must also have a 'due' date.");
|
|
||||||
|
|
||||||
// Recur durations must be valid.
|
|
||||||
if (has("recur")) {
|
if (has("recur")) {
|
||||||
std::string value = get("recur");
|
std::string value = get("recur");
|
||||||
if (value != "") {
|
if (value != "") {
|
||||||
Duration p;
|
Duration p;
|
||||||
std::string::size_type i = 0;
|
std::string::size_type i = 0;
|
||||||
if (!p.parse(value, i))
|
if (!p.parse(value, i)) {
|
||||||
// TODO Ideal location to map unsupported old recurrence periods to supported values.
|
// TODO Ideal location to map unsupported old recurrence periods to supported values.
|
||||||
throw format("The recurrence value '{1}' is not valid.", value);
|
Context::getContext().footnote(
|
||||||
|
format("Warning: The recurrence value '{1}' is not valid.", value));
|
||||||
|
remove("recur");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1602,40 +1621,6 @@ const std::string Task::decode(const std::string& value) const {
|
|||||||
return str_replace(modified, "&close;", "]");
|
return str_replace(modified, "&close;", "]");
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
tc::Status Task::status2tc(const Task::status status) {
|
|
||||||
switch (status) {
|
|
||||||
case Task::pending:
|
|
||||||
return tc::Status::Pending;
|
|
||||||
case Task::completed:
|
|
||||||
return tc::Status::Completed;
|
|
||||||
case Task::deleted:
|
|
||||||
return tc::Status::Deleted;
|
|
||||||
case Task::waiting:
|
|
||||||
return tc::Status::Pending; // waiting is no longer a status
|
|
||||||
case Task::recurring:
|
|
||||||
return tc::Status::Recurring;
|
|
||||||
default:
|
|
||||||
return tc::Status::Unknown;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
Task::status Task::tc2status(const tc::Status status) {
|
|
||||||
switch (status) {
|
|
||||||
case tc::Status::Pending:
|
|
||||||
return Task::pending;
|
|
||||||
case tc::Status::Completed:
|
|
||||||
return Task::completed;
|
|
||||||
case tc::Status::Deleted:
|
|
||||||
return Task::deleted;
|
|
||||||
case tc::Status::Recurring:
|
|
||||||
return Task::recurring;
|
|
||||||
default:
|
|
||||||
return Task::pending;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
int Task::determineVersion(const std::string& line) {
|
int Task::determineVersion(const std::string& line) {
|
||||||
// Version 2 looks like:
|
// Version 2 looks like:
|
||||||
@@ -2167,274 +2152,3 @@ std::string Task::diff(const Task& after) const {
|
|||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
// Similar to diff, but formatted for inclusion in the output of the info command
|
|
||||||
std::string Task::diffForInfo(const Task& after, const std::string& dateformat,
|
|
||||||
long& last_timestamp, const long current_timestamp) const {
|
|
||||||
// Attributes are all there is, so figure the different attribute names
|
|
||||||
// between before and after.
|
|
||||||
std::vector<std::string> beforeAtts;
|
|
||||||
for (auto& att : data) beforeAtts.push_back(att.first);
|
|
||||||
|
|
||||||
std::vector<std::string> afterAtts;
|
|
||||||
for (auto& att : after.data) afterAtts.push_back(att.first);
|
|
||||||
|
|
||||||
std::vector<std::string> beforeOnly;
|
|
||||||
std::vector<std::string> afterOnly;
|
|
||||||
listDiff(beforeAtts, afterAtts, beforeOnly, afterOnly);
|
|
||||||
|
|
||||||
// Now start generating a description of the differences.
|
|
||||||
std::stringstream out;
|
|
||||||
for (auto& name : beforeOnly) {
|
|
||||||
if (isAnnotationAttr(name)) {
|
|
||||||
out << format("Annotation '{1}' deleted.\n", get(name));
|
|
||||||
} else if (isTagAttr(name)) {
|
|
||||||
out << format("Tag '{1}' deleted.\n", attr2Tag(name));
|
|
||||||
} else if (isDepAttr(name)) {
|
|
||||||
out << format("Dependency on '{1}' deleted.\n", attr2Dep(name));
|
|
||||||
} else if (name == "depends" || name == "tags") {
|
|
||||||
// do nothing for legacy attributes
|
|
||||||
} else if (name == "start") {
|
|
||||||
Datetime started(get("start"));
|
|
||||||
Datetime stopped;
|
|
||||||
|
|
||||||
if (after.has("end"))
|
|
||||||
// Task was marked as finished, use end time
|
|
||||||
stopped = Datetime(after.get("end"));
|
|
||||||
else
|
|
||||||
// Start attribute was removed, use modification time
|
|
||||||
stopped = Datetime(current_timestamp);
|
|
||||||
|
|
||||||
out << format("{1} deleted (duration: {2}).", Lexer::ucFirst(name),
|
|
||||||
Duration(stopped - started).format())
|
|
||||||
<< "\n";
|
|
||||||
} else {
|
|
||||||
out << format("{1} deleted.\n", Lexer::ucFirst(name));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto& name : afterOnly) {
|
|
||||||
if (isAnnotationAttr(name)) {
|
|
||||||
out << format("Annotation of '{1}' added.\n", after.get(name));
|
|
||||||
} else if (isTagAttr(name)) {
|
|
||||||
out << format("Tag '{1}' added.\n", attr2Tag(name));
|
|
||||||
} else if (isDepAttr(name)) {
|
|
||||||
out << format("Dependency on '{1}' added.\n", attr2Dep(name));
|
|
||||||
} else if (name == "depends" || name == "tags") {
|
|
||||||
// do nothing for legacy attributes
|
|
||||||
} else {
|
|
||||||
if (name == "start") last_timestamp = current_timestamp;
|
|
||||||
|
|
||||||
out << format("{1} set to '{2}'.", Lexer::ucFirst(name),
|
|
||||||
renderAttribute(name, after.get(name), dateformat))
|
|
||||||
<< "\n";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto& name : beforeAtts)
|
|
||||||
if (name != "uuid" && name != "modified" && get(name) != after.get(name) && get(name) != "" &&
|
|
||||||
after.get(name) != "") {
|
|
||||||
if (name == "depends" || name == "tags") {
|
|
||||||
// do nothing for legacy attributes
|
|
||||||
} else if (isTagAttr(name) || isDepAttr(name)) {
|
|
||||||
// ignore new attributes
|
|
||||||
} else if (isAnnotationAttr(name)) {
|
|
||||||
out << format("Annotation changed to '{1}'.\n", after.get(name));
|
|
||||||
} else
|
|
||||||
out << format("{1} changed from '{2}' to '{3}'.", Lexer::ucFirst(name),
|
|
||||||
renderAttribute(name, get(name), dateformat),
|
|
||||||
renderAttribute(name, after.get(name), dateformat))
|
|
||||||
<< "\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shouldn't just say nothing.
|
|
||||||
if (out.str().length() == 0) out << "No changes made.\n";
|
|
||||||
|
|
||||||
return out.str();
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// Similar to diff, but formatted as a side-by-side table for an Undo preview
|
|
||||||
Table Task::diffForUndoSide(const Task& after) const {
|
|
||||||
// Set the colors.
|
|
||||||
Color color_red(
|
|
||||||
Context::getContext().color() ? Context::getContext().config.get("color.undo.before") : "");
|
|
||||||
Color color_green(
|
|
||||||
Context::getContext().color() ? Context::getContext().config.get("color.undo.after") : "");
|
|
||||||
|
|
||||||
// Attributes are all there is, so figure the different attribute names
|
|
||||||
// between before and after.
|
|
||||||
Table view;
|
|
||||||
view.width(Context::getContext().getWidth());
|
|
||||||
view.intraPadding(2);
|
|
||||||
view.add("");
|
|
||||||
view.add("Prior Values");
|
|
||||||
view.add("Current Values");
|
|
||||||
setHeaderUnderline(view);
|
|
||||||
|
|
||||||
if (!is_empty()) {
|
|
||||||
const Task& before = *this;
|
|
||||||
|
|
||||||
std::vector<std::string> beforeAtts;
|
|
||||||
for (auto& att : before.data) beforeAtts.push_back(att.first);
|
|
||||||
|
|
||||||
std::vector<std::string> afterAtts;
|
|
||||||
for (auto& att : after.data) afterAtts.push_back(att.first);
|
|
||||||
|
|
||||||
std::vector<std::string> beforeOnly;
|
|
||||||
std::vector<std::string> afterOnly;
|
|
||||||
listDiff(beforeAtts, afterAtts, beforeOnly, afterOnly);
|
|
||||||
|
|
||||||
int row;
|
|
||||||
for (auto& name : beforeOnly) {
|
|
||||||
row = view.addRow();
|
|
||||||
view.set(row, 0, name);
|
|
||||||
view.set(row, 1, renderAttribute(name, before.get(name)), color_red);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto& att : before.data) {
|
|
||||||
std::string priorValue = before.get(att.first);
|
|
||||||
std::string currentValue = after.get(att.first);
|
|
||||||
|
|
||||||
if (currentValue != "") {
|
|
||||||
row = view.addRow();
|
|
||||||
view.set(row, 0, att.first);
|
|
||||||
view.set(row, 1, renderAttribute(att.first, priorValue),
|
|
||||||
(priorValue != currentValue ? color_red : Color()));
|
|
||||||
view.set(row, 2, renderAttribute(att.first, currentValue),
|
|
||||||
(priorValue != currentValue ? color_green : Color()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (auto& name : afterOnly) {
|
|
||||||
row = view.addRow();
|
|
||||||
view.set(row, 0, name);
|
|
||||||
view.set(row, 2, renderAttribute(name, after.get(name)), color_green);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
int row;
|
|
||||||
for (auto& att : after.data) {
|
|
||||||
row = view.addRow();
|
|
||||||
view.set(row, 0, att.first);
|
|
||||||
view.set(row, 2, renderAttribute(att.first, after.get(att.first)), color_green);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return view;
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
// Similar to diff, but formatted as a diff for an Undo preview
|
|
||||||
Table Task::diffForUndoPatch(const Task& after, const Datetime& lastChange) const {
|
|
||||||
// This style looks like this:
|
|
||||||
// --- before 2009-07-04 00:00:25.000000000 +0200
|
|
||||||
// +++ after 2009-07-04 00:00:45.000000000 +0200
|
|
||||||
//
|
|
||||||
// - name: old // att deleted
|
|
||||||
// + name:
|
|
||||||
//
|
|
||||||
// - name: old // att changed
|
|
||||||
// + name: new
|
|
||||||
//
|
|
||||||
// - name:
|
|
||||||
// + name: new // att added
|
|
||||||
//
|
|
||||||
|
|
||||||
// Set the colors.
|
|
||||||
Color color_red(
|
|
||||||
Context::getContext().color() ? Context::getContext().config.get("color.undo.before") : "");
|
|
||||||
Color color_green(
|
|
||||||
Context::getContext().color() ? Context::getContext().config.get("color.undo.after") : "");
|
|
||||||
|
|
||||||
const Task& before = *this;
|
|
||||||
|
|
||||||
// Generate table header.
|
|
||||||
Table view;
|
|
||||||
view.width(Context::getContext().getWidth());
|
|
||||||
view.intraPadding(2);
|
|
||||||
view.add("");
|
|
||||||
view.add("");
|
|
||||||
|
|
||||||
int row = view.addRow();
|
|
||||||
view.set(row, 0, "--- previous state", color_red);
|
|
||||||
view.set(row, 1, "Undo will restore this state", color_red);
|
|
||||||
|
|
||||||
row = view.addRow();
|
|
||||||
view.set(row, 0, "+++ current state ", color_green);
|
|
||||||
view.set(row, 1,
|
|
||||||
format("Change made {1}",
|
|
||||||
lastChange.toString(Context::getContext().config.get("dateformat"))),
|
|
||||||
color_green);
|
|
||||||
|
|
||||||
view.addRow();
|
|
||||||
|
|
||||||
// Add rows to table showing diffs.
|
|
||||||
std::vector<std::string> all = Context::getContext().getColumns();
|
|
||||||
|
|
||||||
// Now factor in the annotation attributes.
|
|
||||||
for (auto& it : before.data)
|
|
||||||
if (it.first.substr(0, 11) == "annotation_") all.push_back(it.first);
|
|
||||||
|
|
||||||
for (auto& it : after.data)
|
|
||||||
if (it.first.substr(0, 11) == "annotation_") all.push_back(it.first);
|
|
||||||
|
|
||||||
// Now render all the attributes.
|
|
||||||
std::sort(all.begin(), all.end());
|
|
||||||
|
|
||||||
std::string before_att;
|
|
||||||
std::string after_att;
|
|
||||||
std::string last_att;
|
|
||||||
for (auto& a : all) {
|
|
||||||
if (a != last_att) // Skip duplicates.
|
|
||||||
{
|
|
||||||
last_att = a;
|
|
||||||
|
|
||||||
before_att = before.get(a);
|
|
||||||
after_att = after.get(a);
|
|
||||||
|
|
||||||
// Don't report different uuid.
|
|
||||||
// Show nothing if values are the unchanged.
|
|
||||||
if (a == "uuid" || before_att == after_att) {
|
|
||||||
// Show nothing - no point displaying that which did not change.
|
|
||||||
|
|
||||||
// row = view.addRow ();
|
|
||||||
// view.set (row, 0, *a + ":");
|
|
||||||
// view.set (row, 1, before_att);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attribute deleted.
|
|
||||||
else if (before_att != "" && after_att == "") {
|
|
||||||
row = view.addRow();
|
|
||||||
view.set(row, 0, '-' + a + ':', color_red);
|
|
||||||
view.set(row, 1, before_att, color_red);
|
|
||||||
|
|
||||||
row = view.addRow();
|
|
||||||
view.set(row, 0, '+' + a + ':', color_green);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attribute added.
|
|
||||||
else if (before_att == "" && after_att != "") {
|
|
||||||
row = view.addRow();
|
|
||||||
view.set(row, 0, '-' + a + ':', color_red);
|
|
||||||
|
|
||||||
row = view.addRow();
|
|
||||||
view.set(row, 0, '+' + a + ':', color_green);
|
|
||||||
view.set(row, 1, after_att, color_green);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attribute changed.
|
|
||||||
else {
|
|
||||||
row = view.addRow();
|
|
||||||
view.set(row, 0, '-' + a + ':', color_red);
|
|
||||||
view.set(row, 1, before_att, color_red);
|
|
||||||
|
|
||||||
row = view.addRow();
|
|
||||||
view.set(row, 0, '+' + a + ':', color_green);
|
|
||||||
view.set(row, 1, after_att, color_green);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return view;
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|||||||
22
src/Task.h
22
src/Task.h
@@ -31,7 +31,7 @@
|
|||||||
#include <JSON.h>
|
#include <JSON.h>
|
||||||
#include <Table.h>
|
#include <Table.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <tc/Task.h>
|
#include <taskchampion-cpp/lib.h>
|
||||||
#include <time.h>
|
#include <time.h>
|
||||||
|
|
||||||
#include <map>
|
#include <map>
|
||||||
@@ -66,7 +66,7 @@ class Task {
|
|||||||
bool operator!=(const Task&);
|
bool operator!=(const Task&);
|
||||||
Task(const std::string&);
|
Task(const std::string&);
|
||||||
Task(const json::object*);
|
Task(const json::object*);
|
||||||
Task(tc::Task);
|
Task(rust::Box<tc::TaskData>);
|
||||||
|
|
||||||
void parse(const std::string&);
|
void parse(const std::string&);
|
||||||
std::string composeJSON(bool decorate = false) const;
|
std::string composeJSON(bool decorate = false) const;
|
||||||
@@ -88,8 +88,6 @@ class Task {
|
|||||||
// Series of helper functions.
|
// Series of helper functions.
|
||||||
static status textToStatus(const std::string&);
|
static status textToStatus(const std::string&);
|
||||||
static std::string statusToText(status);
|
static std::string statusToText(status);
|
||||||
static tc::Status status2tc(const Task::status);
|
|
||||||
static Task::status tc2status(const tc::Status);
|
|
||||||
|
|
||||||
void setAsNow(const std::string&);
|
void setAsNow(const std::string&);
|
||||||
bool has(const std::string&) const;
|
bool has(const std::string&) const;
|
||||||
@@ -166,6 +164,12 @@ class Task {
|
|||||||
void substitute(const std::string&, const std::string&, const std::string&);
|
void substitute(const std::string&, const std::string&, const std::string&);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
static std::string tag2Attr(const std::string&);
|
||||||
|
static std::string attr2Tag(const std::string&);
|
||||||
|
static std::string dep2Attr(const std::string&);
|
||||||
|
static std::string attr2Dep(const std::string&);
|
||||||
|
|
||||||
|
void validate_add();
|
||||||
void validate(bool applyDefault = true);
|
void validate(bool applyDefault = true);
|
||||||
|
|
||||||
float urgency_c() const;
|
float urgency_c() const;
|
||||||
@@ -177,24 +181,16 @@ class Task {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
std::string diff(const Task& after) const;
|
std::string diff(const Task& after) const;
|
||||||
std::string diffForInfo(const Task& after, const std::string& dateformat, long& last_timestamp,
|
|
||||||
const long current_timestamp) const;
|
|
||||||
Table diffForUndoSide(const Task& after) const;
|
|
||||||
Table diffForUndoPatch(const Task& after, const Datetime& lastChange) const;
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
int determineVersion(const std::string&);
|
int determineVersion(const std::string&);
|
||||||
void parseJSON(const std::string&);
|
void parseJSON(const std::string&);
|
||||||
void parseJSON(const json::object*);
|
void parseJSON(const json::object*);
|
||||||
void parseTC(const tc::Task&);
|
void parseTC(rust::Box<tc::TaskData>);
|
||||||
void parseLegacy(const std::string&);
|
void parseLegacy(const std::string&);
|
||||||
void validate_before(const std::string&, const std::string&);
|
void validate_before(const std::string&, const std::string&);
|
||||||
const std::string encode(const std::string&) const;
|
const std::string encode(const std::string&) const;
|
||||||
const std::string decode(const std::string&) const;
|
const std::string decode(const std::string&) const;
|
||||||
const std::string tag2Attr(const std::string&) const;
|
|
||||||
const std::string attr2Tag(const std::string&) const;
|
|
||||||
const std::string dep2Attr(const std::string&) const;
|
|
||||||
const std::string attr2Dep(const std::string&) const;
|
|
||||||
void fixDependsAttribute();
|
void fixDependsAttribute();
|
||||||
void fixTagsAttribute();
|
void fixTagsAttribute();
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
cmake_minimum_required (VERSION 3.22)
|
cmake_minimum_required (VERSION 3.22)
|
||||||
include_directories (${CMAKE_SOURCE_DIR}
|
include_directories (${CMAKE_SOURCE_DIR}
|
||||||
${CMAKE_SOURCE_DIR}/src
|
${CMAKE_SOURCE_DIR}/src
|
||||||
${CMAKE_SOURCE_DIR}/src/tc
|
|
||||||
${CMAKE_SOURCE_DIR}/src/tc/lib
|
|
||||||
${CMAKE_SOURCE_DIR}/src/commands
|
${CMAKE_SOURCE_DIR}/src/commands
|
||||||
${CMAKE_SOURCE_DIR}/src/columns
|
${CMAKE_SOURCE_DIR}/src/columns
|
||||||
${CMAKE_SOURCE_DIR}/src/libshared/src
|
${CMAKE_SOURCE_DIR}/src/libshared/src
|
||||||
@@ -39,6 +37,7 @@ set (columns_SRCS Column.cpp Column.h
|
|||||||
ColWait.cpp ColWait.h)
|
ColWait.cpp ColWait.h)
|
||||||
|
|
||||||
add_library (columns STATIC ${columns_SRCS})
|
add_library (columns STATIC ${columns_SRCS})
|
||||||
|
target_link_libraries(columns taskchampion-cpp)
|
||||||
|
|
||||||
#SET(CMAKE_BUILD_TYPE gcov)
|
#SET(CMAKE_BUILD_TYPE gcov)
|
||||||
#SET(CMAKE_CXX_FLAGS_GCOV "--coverage")
|
#SET(CMAKE_CXX_FLAGS_GCOV "--coverage")
|
||||||
|
|||||||
@@ -190,7 +190,11 @@ void ColumnTypeDate::modify(Task& task, const std::string& value) {
|
|||||||
if (value != "" && evaluatedValue.get_date() == 0)
|
if (value != "" && evaluatedValue.get_date() == 0)
|
||||||
throw format("'{1}' is not a valid date in the '{2}' format.", value, Variant::dateFormat);
|
throw format("'{1}' is not a valid date in the '{2}' format.", value, Variant::dateFormat);
|
||||||
|
|
||||||
task.set(_name, evaluatedValue.get_date());
|
time_t epoch = evaluatedValue.get_date();
|
||||||
|
if (epoch < EPOCH_MIN_VALUE || epoch >= EPOCH_MAX_VALUE) {
|
||||||
|
throw format("'{1}' is not a valid date.", value);
|
||||||
|
}
|
||||||
|
task.set(_name, epoch);
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
cmake_minimum_required (VERSION 3.22)
|
cmake_minimum_required (VERSION 3.22)
|
||||||
include_directories (${CMAKE_SOURCE_DIR}
|
include_directories (${CMAKE_SOURCE_DIR}
|
||||||
${CMAKE_SOURCE_DIR}/src
|
${CMAKE_SOURCE_DIR}/src
|
||||||
${CMAKE_SOURCE_DIR}/src/tc
|
|
||||||
${CMAKE_SOURCE_DIR}/src/tc/lib
|
|
||||||
${CMAKE_SOURCE_DIR}/src/commands
|
${CMAKE_SOURCE_DIR}/src/commands
|
||||||
${CMAKE_SOURCE_DIR}/src/columns
|
${CMAKE_SOURCE_DIR}/src/columns
|
||||||
${CMAKE_SOURCE_DIR}/src/libshared/src
|
${CMAKE_SOURCE_DIR}/src/libshared/src
|
||||||
@@ -37,6 +35,7 @@ set (commands_SRCS Command.cpp Command.h
|
|||||||
CmdHistory.cpp CmdHistory.h
|
CmdHistory.cpp CmdHistory.h
|
||||||
CmdIDs.cpp CmdIDs.h
|
CmdIDs.cpp CmdIDs.h
|
||||||
CmdImport.cpp CmdImport.h
|
CmdImport.cpp CmdImport.h
|
||||||
|
CmdImportV2.cpp CmdImportV2.h
|
||||||
CmdInfo.cpp CmdInfo.h
|
CmdInfo.cpp CmdInfo.h
|
||||||
CmdLog.cpp CmdLog.h
|
CmdLog.cpp CmdLog.h
|
||||||
CmdLogo.cpp CmdLogo.h
|
CmdLogo.cpp CmdLogo.h
|
||||||
@@ -61,6 +60,7 @@ set (commands_SRCS Command.cpp Command.h
|
|||||||
CmdVersion.cpp CmdVersion.h)
|
CmdVersion.cpp CmdVersion.h)
|
||||||
|
|
||||||
add_library (commands STATIC ${commands_SRCS})
|
add_library (commands STATIC ${commands_SRCS})
|
||||||
|
target_link_libraries(commands taskchampion-cpp)
|
||||||
|
|
||||||
#SET(CMAKE_BUILD_TYPE gcov)
|
#SET(CMAKE_BUILD_TYPE gcov)
|
||||||
#SET(CMAKE_CXX_FLAGS_GCOV "--coverage")
|
#SET(CMAKE_CXX_FLAGS_GCOV "--coverage")
|
||||||
|
|||||||
@@ -404,12 +404,7 @@ int CmdCalendar::execute(std::string& output) {
|
|||||||
std::string CmdCalendar::renderMonths(int firstMonth, int firstYear, const Datetime& today,
|
std::string CmdCalendar::renderMonths(int firstMonth, int firstYear, const Datetime& today,
|
||||||
std::vector<Task>& all, int monthsPerLine) {
|
std::vector<Task>& all, int monthsPerLine) {
|
||||||
auto& config = Context::getContext().config;
|
auto& config = Context::getContext().config;
|
||||||
|
auto weekStart = Datetime::weekstart;
|
||||||
// What day of the week does the user consider the first?
|
|
||||||
auto weekStart = Datetime::dayOfWeek(config.get("weekstart"));
|
|
||||||
if (weekStart != 0 && weekStart != 1)
|
|
||||||
throw std::string(
|
|
||||||
"The 'weekstart' configuration variable may only contain 'Sunday' or 'Monday'.");
|
|
||||||
|
|
||||||
// Build table for the number of months to be displayed.
|
// Build table for the number of months to be displayed.
|
||||||
Table view;
|
Table view;
|
||||||
|
|||||||
@@ -95,7 +95,9 @@ bool CmdConfig::setConfigVariable(const std::string& name, const std::string& va
|
|||||||
change = true;
|
change = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (change) File::write(Context::getContext().config.file(), contents);
|
if (change)
|
||||||
|
if (!File::write(Context::getContext().config.file(), contents))
|
||||||
|
throw format("Could not write to '{1}'.", Context::getContext().config.file());
|
||||||
|
|
||||||
return change;
|
return change;
|
||||||
}
|
}
|
||||||
@@ -133,7 +135,9 @@ int CmdConfig::unsetConfigVariable(const std::string& name, bool confirmation /*
|
|||||||
if (!lineDeleted) line++;
|
if (!lineDeleted) line++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (change) File::write(Context::getContext().config.file(), contents);
|
if (change)
|
||||||
|
if (!File::write(Context::getContext().config.file(), contents))
|
||||||
|
throw format("Could not write to '{1}'.", Context::getContext().config.file());
|
||||||
|
|
||||||
if (change && found)
|
if (change && found)
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
// cmake.h include header must come first
|
// cmake.h include header must come first
|
||||||
|
|
||||||
#include <CmdCustom.h>
|
#include <CmdCustom.h>
|
||||||
|
#include <CmdNews.h>
|
||||||
#include <Context.h>
|
#include <Context.h>
|
||||||
#include <Filter.h>
|
#include <Filter.h>
|
||||||
#include <Lexer.h>
|
#include <Lexer.h>
|
||||||
@@ -222,11 +223,9 @@ int CmdCustom::execute(std::string& output) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Inform user about the new release highlights if not presented yet
|
// Inform user about the new release highlights if not presented yet
|
||||||
Version news_version(Context::getContext().config.get("news.version"));
|
if (CmdNews::should_nag()) {
|
||||||
Version current_version = Version::Current();
|
|
||||||
auto should_nag = news_version != current_version && Context::getContext().verbose("news");
|
|
||||||
if (should_nag) {
|
|
||||||
std::ostringstream notice;
|
std::ostringstream notice;
|
||||||
|
Version current_version = Version::Current();
|
||||||
notice << "Recently upgraded to " << current_version
|
notice << "Recently upgraded to " << current_version
|
||||||
<< ". "
|
<< ". "
|
||||||
"Please run 'task news' to read highlights about the new release.";
|
"Please run 'task news' to read highlights about the new release.";
|
||||||
@@ -242,7 +241,8 @@ int CmdCustom::execute(std::string& output) {
|
|||||||
Color warning = Color(Context::getContext().config.get("color.warning"));
|
Color warning = Color(Context::getContext().config.get("color.warning"));
|
||||||
std::cerr << warning.colorize(format("Found existing '*.data' files in {1}", location)) << "\n";
|
std::cerr << warning.colorize(format("Found existing '*.data' files in {1}", location)) << "\n";
|
||||||
std::cerr << " Taskwarrior's storage format changed in 3.0, requiring a manual migration.\n";
|
std::cerr << " Taskwarrior's storage format changed in 3.0, requiring a manual migration.\n";
|
||||||
std::cerr << " See https://github.com/GothenburgBitFactory/taskwarrior/releases.\n";
|
std::cerr << " See https://taskwarrior.org/docs/upgrade-3/. Run `task import-v2` to import\n";
|
||||||
|
std::cerr << " the tasks into the Taskwarrior-3.x format\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
feedback_backlog();
|
feedback_backlog();
|
||||||
|
|||||||
@@ -174,6 +174,9 @@ void CmdImport::importSingleTask(json::object* obj) {
|
|||||||
// Parse the whole thing, validate the data.
|
// Parse the whole thing, validate the data.
|
||||||
Task task(obj);
|
Task task(obj);
|
||||||
|
|
||||||
|
// An empty task is probably not intentional - at least a UUID should be included.
|
||||||
|
if (task.is_empty()) throw format("Cannot import an empty task.");
|
||||||
|
|
||||||
auto hasGeneratedEntry = not task.has("entry");
|
auto hasGeneratedEntry = not task.has("entry");
|
||||||
auto hasExplicitEnd = task.has("end");
|
auto hasExplicitEnd = task.has("end");
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
#include <JSON.h>
|
#include <JSON.h>
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
class CmdImport : public Command {
|
class CmdImport : public Command {
|
||||||
public:
|
public:
|
||||||
|
|||||||
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
//
|
//
|
||||||
// Copyright 2022, Dustin J. Mitchell, Tomas Babej, Paul Beckingham, Federico Hernandez.
|
// Copyright 2006 - 2021, Tomas Babej, Paul Beckingham, Federico Hernandez.
|
||||||
//
|
//
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
// of this software and associated documentation files (the "Software"), to deal
|
||||||
@@ -24,13 +24,23 @@
|
|||||||
//
|
//
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
#ifndef INCLUDED_TC_FFI
|
#ifndef INCLUDED_CMDIMPORTV2
|
||||||
#define INCLUDED_TC_FFI
|
#define INCLUDED_CMDIMPORTV2
|
||||||
|
|
||||||
// The entire FFI API is embedded in the `tc::ffi` namespace
|
#include <Command.h>
|
||||||
namespace tc::ffi {
|
#include <JSON.h>
|
||||||
#include <taskchampion.h>
|
|
||||||
}
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
class CmdImportV2 : public Command {
|
||||||
|
public:
|
||||||
|
CmdImportV2();
|
||||||
|
int execute(std::string &);
|
||||||
|
|
||||||
|
private:
|
||||||
|
int import(const std::vector<std::map<std::string, std::string>> &task_data);
|
||||||
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
@@ -33,13 +33,16 @@
|
|||||||
#include <Duration.h>
|
#include <Duration.h>
|
||||||
#include <Filter.h>
|
#include <Filter.h>
|
||||||
#include <Lexer.h>
|
#include <Lexer.h>
|
||||||
|
#include <Operation.h>
|
||||||
#include <format.h>
|
#include <format.h>
|
||||||
#include <main.h>
|
#include <main.h>
|
||||||
#include <math.h>
|
#include <math.h>
|
||||||
#include <shared.h>
|
#include <shared.h>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
|
#include <taskchampion-cpp/lib.h>
|
||||||
#include <util.h>
|
#include <util.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
@@ -373,7 +376,7 @@ int CmdInfo::execute(std::string& output) {
|
|||||||
// Show any orphaned UDAs, which are identified by not being represented in
|
// Show any orphaned UDAs, which are identified by not being represented in
|
||||||
// the context.columns map.
|
// the context.columns map.
|
||||||
for (auto& att : all) {
|
for (auto& att : all) {
|
||||||
if (att.substr(0, 11) != "annotation_" && att.substr(0, 5) != "tags_" &&
|
if (att.substr(0, 11) != "annotation_" && att.substr(0, 4) != "tag_" &&
|
||||||
att.substr(0, 4) != "dep_" &&
|
att.substr(0, 4) != "dep_" &&
|
||||||
Context::getContext().columns.find(att) == Context::getContext().columns.end()) {
|
Context::getContext().columns.find(att) == Context::getContext().columns.end()) {
|
||||||
row = view.addRow();
|
row = view.addRow();
|
||||||
@@ -477,9 +480,68 @@ int CmdInfo::execute(std::string& output) {
|
|||||||
urgencyDetails.set(row, 5, rightJustify(format(task.urgency(), 4, 4), 6));
|
urgencyDetails.set(row, 5, rightJustify(format(task.urgency(), 4, 4), 6));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create a third table, containing undo log change details.
|
||||||
|
Table journal;
|
||||||
|
setHeaderUnderline(journal);
|
||||||
|
|
||||||
|
if (Context::getContext().config.getBoolean("obfuscate")) journal.obfuscate();
|
||||||
|
if (Context::getContext().config.getBoolean("color")) journal.forceColor();
|
||||||
|
|
||||||
|
journal.width(Context::getContext().getWidth());
|
||||||
|
journal.add("Date");
|
||||||
|
journal.add("Modification");
|
||||||
|
|
||||||
|
if (Context::getContext().config.getBoolean("journal.info")) {
|
||||||
|
auto& replica = Context::getContext().tdb2.replica();
|
||||||
|
tc::Uuid tcuuid = tc::uuid_from_string(uuid);
|
||||||
|
auto tcoperations = replica->get_task_operations(tcuuid);
|
||||||
|
auto operations = Operation::operations(tcoperations);
|
||||||
|
|
||||||
|
// Sort by type (Create < Update < Delete < UndoPoint) and then by timestamp.
|
||||||
|
std::sort(operations.begin(), operations.end());
|
||||||
|
|
||||||
|
long last_timestamp = 0;
|
||||||
|
for (size_t i = 0; i < operations.size(); i++) {
|
||||||
|
auto& op = operations[i];
|
||||||
|
|
||||||
|
// Only display updates -- creation and deletion aren't interesting.
|
||||||
|
if (!op.is_update()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group operations that occur within 1s of this one. This is a heuristic
|
||||||
|
// for operations performed in the same `task` invocation, and allows e.g.,
|
||||||
|
// `task done end:-2h` to take the updated `end` value into account. It also
|
||||||
|
// groups these events into a single "row" of the table for better layout.
|
||||||
|
size_t group_start = i;
|
||||||
|
for (i++; i < operations.size(); i++) {
|
||||||
|
auto& op2 = operations[i];
|
||||||
|
if (!op2.is_update() || op2.get_timestamp() - op.get_timestamp() > 1) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
size_t group_end = i;
|
||||||
|
i--;
|
||||||
|
|
||||||
|
std::optional<std::string> msg =
|
||||||
|
formatForInfo(operations, group_start, group_end, dateformat, last_timestamp);
|
||||||
|
|
||||||
|
if (!msg) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int row = journal.addRow();
|
||||||
|
Datetime timestamp(op.get_timestamp());
|
||||||
|
journal.set(row, 0, timestamp.toString(dateformat));
|
||||||
|
journal.set(row, 1, *msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
out << optionalBlankLine() << view.render() << '\n';
|
out << optionalBlankLine() << view.render() << '\n';
|
||||||
|
|
||||||
if (urgencyDetails.rows() > 0) out << urgencyDetails.render() << '\n';
|
if (urgencyDetails.rows() > 0) out << urgencyDetails.render() << '\n';
|
||||||
|
|
||||||
|
if (journal.rows() > 0) out << journal.render() << '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
output = out.str();
|
output = out.str();
|
||||||
@@ -502,3 +564,105 @@ void CmdInfo::urgencyTerm(Table& view, const std::string& label, float measure,
|
|||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
std::optional<std::string> CmdInfo::formatForInfo(const std::vector<Operation>& operations,
|
||||||
|
size_t group_start, size_t group_end,
|
||||||
|
const std::string& dateformat, long& last_start) {
|
||||||
|
std::stringstream out;
|
||||||
|
for (auto i = group_start; i < group_end; i++) {
|
||||||
|
auto& operation = operations[i];
|
||||||
|
assert(operation.is_update());
|
||||||
|
|
||||||
|
// Extract the parts of the Update operation.
|
||||||
|
std::string prop = operation.get_property();
|
||||||
|
std::optional<std::string> value = operation.get_value();
|
||||||
|
std::optional<std::string> old_value = operation.get_old_value();
|
||||||
|
Datetime timestamp(operation.get_timestamp());
|
||||||
|
|
||||||
|
// Never care about modifying the modification time, or the legacy properties `depends` and
|
||||||
|
// `tags`.
|
||||||
|
if (prop == "modified" || prop == "depends" || prop == "tags") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle property deletions
|
||||||
|
if (!value && old_value) {
|
||||||
|
if (Task::isAnnotationAttr(prop)) {
|
||||||
|
out << format("Annotation '{1}' deleted.\n", *old_value);
|
||||||
|
} else if (Task::isTagAttr(prop)) {
|
||||||
|
out << format("Tag '{1}' deleted.\n", Task::attr2Tag(prop));
|
||||||
|
} else if (Task::isDepAttr(prop)) {
|
||||||
|
out << format("Dependency on '{1}' deleted.\n", Task::attr2Dep(prop));
|
||||||
|
} else if (prop == "start") {
|
||||||
|
Datetime started(last_start);
|
||||||
|
Datetime stopped = timestamp;
|
||||||
|
|
||||||
|
// If any update in this group sets the `end` property, use that instead of the
|
||||||
|
// timestamp deleting the `start` property as the stop time.
|
||||||
|
// See https://github.com/GothenburgBitFactory/taskwarrior/issues/2514
|
||||||
|
for (auto i = group_start; i < group_end; i++) {
|
||||||
|
auto& op = operations[i];
|
||||||
|
assert(op.is_update());
|
||||||
|
if (op.get_property() == "end") {
|
||||||
|
try {
|
||||||
|
stopped = op.get_value().value();
|
||||||
|
} catch (std::string) {
|
||||||
|
// Fall back to the 'start' timestamp if its value is un-parseable.
|
||||||
|
stopped = op.get_timestamp();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out << format("{1} deleted (duration: {2}).", Lexer::ucFirst(prop),
|
||||||
|
Duration(stopped - started).format())
|
||||||
|
<< "\n";
|
||||||
|
} else {
|
||||||
|
out << format("{1} deleted.\n", Lexer::ucFirst(prop));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle property additions.
|
||||||
|
if (value && !old_value) {
|
||||||
|
if (Task::isAnnotationAttr(prop)) {
|
||||||
|
out << format("Annotation of '{1}' added.\n", *value);
|
||||||
|
} else if (Task::isTagAttr(prop)) {
|
||||||
|
out << format("Tag '{1}' added.\n", Task::attr2Tag(prop));
|
||||||
|
} else if (Task::isDepAttr(prop)) {
|
||||||
|
out << format("Dependency on '{1}' added.\n", Task::attr2Dep(prop));
|
||||||
|
} else {
|
||||||
|
// Record the last start time for later duration calculation.
|
||||||
|
if (prop == "start") {
|
||||||
|
last_start = Datetime(value.value()).toEpoch();
|
||||||
|
}
|
||||||
|
|
||||||
|
out << format("{1} set to '{2}'.", Lexer::ucFirst(prop),
|
||||||
|
renderAttribute(prop, *value, dateformat))
|
||||||
|
<< "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle property changes.
|
||||||
|
if (value && old_value) {
|
||||||
|
if (Task::isTagAttr(prop) || Task::isDepAttr(prop)) {
|
||||||
|
// Dependencies and tags do not have meaningful values.
|
||||||
|
} else if (Task::isAnnotationAttr(prop)) {
|
||||||
|
out << format("Annotation changed to '{1}'.\n", *value);
|
||||||
|
} else {
|
||||||
|
// Record the last start time for later duration calculation.
|
||||||
|
if (prop == "start") {
|
||||||
|
last_start = Datetime(value.value()).toEpoch();
|
||||||
|
}
|
||||||
|
|
||||||
|
out << format("{1} changed from '{2}' to '{3}'.", Lexer::ucFirst(prop),
|
||||||
|
renderAttribute(prop, *old_value, dateformat),
|
||||||
|
renderAttribute(prop, *value, dateformat))
|
||||||
|
<< "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (out.str().length() == 0) return std::nullopt;
|
||||||
|
|
||||||
|
return out.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|||||||
@@ -28,9 +28,12 @@
|
|||||||
#define INCLUDED_CMDINFO
|
#define INCLUDED_CMDINFO
|
||||||
|
|
||||||
#include <Command.h>
|
#include <Command.h>
|
||||||
|
#include <Operation.h>
|
||||||
#include <Table.h>
|
#include <Table.h>
|
||||||
|
#include <taskchampion-cpp/lib.h>
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
class CmdInfo : public Command {
|
class CmdInfo : public Command {
|
||||||
public:
|
public:
|
||||||
@@ -39,6 +42,10 @@ class CmdInfo : public Command {
|
|||||||
|
|
||||||
private:
|
private:
|
||||||
void urgencyTerm(Table&, const std::string&, float, float) const;
|
void urgencyTerm(Table&, const std::string&, float, float) const;
|
||||||
|
// Format a group of update operations for display in `task info`.
|
||||||
|
std::optional<std::string> formatForInfo(const std::vector<Operation>& operations,
|
||||||
|
size_t group_start, size_t group_end,
|
||||||
|
const std::string& dateformat, long& last_start);
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -116,6 +116,11 @@ void CmdModify::checkConsistency(Task &before, Task &after) {
|
|||||||
|
|
||||||
if (before.has("recur") && (!after.has("recur") || after.get("recur") == ""))
|
if (before.has("recur") && (!after.has("recur") || after.get("recur") == ""))
|
||||||
throw std::string("You cannot remove the recurrence from a recurring task.");
|
throw std::string("You cannot remove the recurrence from a recurring task.");
|
||||||
|
|
||||||
|
if ((before.getStatus() == Task::pending) && (after.getStatus() == Task::pending) &&
|
||||||
|
(after.get("end") != ""))
|
||||||
|
throw format("Could not modify task {1}. You cannot set an end date on a pending task.",
|
||||||
|
before.identifier(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|||||||
@@ -158,6 +158,8 @@ std::vector<NewsItem> NewsItem::all() {
|
|||||||
version2_6_0(items);
|
version2_6_0(items);
|
||||||
version3_0_0(items);
|
version3_0_0(items);
|
||||||
version3_1_0(items);
|
version3_1_0(items);
|
||||||
|
version3_2_0(items);
|
||||||
|
version3_3_0(items);
|
||||||
return items;
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -500,6 +502,34 @@ void NewsItem::version3_1_0(std::vector<NewsItem>& items) {
|
|||||||
items.push_back(news);
|
items.push_back(news);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void NewsItem::version3_2_0(std::vector<NewsItem>& items) {
|
||||||
|
Version version("3.2.0");
|
||||||
|
NewsItem info{
|
||||||
|
version,
|
||||||
|
/*title=*/"`task info` Journal Restored",
|
||||||
|
/*bg_title=*/"",
|
||||||
|
/*background=*/"",
|
||||||
|
/*punchline=*/"",
|
||||||
|
/*update=*/
|
||||||
|
"Support for the \"journal\" output in `task info` has been restored. The command now\n"
|
||||||
|
"displays a list of changes made to the task, with timestamps.\n\n"};
|
||||||
|
items.push_back(info);
|
||||||
|
}
|
||||||
|
|
||||||
|
void NewsItem::version3_3_0(std::vector<NewsItem>& items) {
|
||||||
|
Version version("3.3.0");
|
||||||
|
NewsItem info{
|
||||||
|
version,
|
||||||
|
/*title=*/"AWS S3 Sync",
|
||||||
|
/*bg_title=*/"",
|
||||||
|
/*background=*/"",
|
||||||
|
/*punchline=*/"Use an AWS S3 bucket to sync Taskwarrior",
|
||||||
|
/*update=*/
|
||||||
|
"Taskwarrior now supports AWS as a backend for sync, in addition to existing support\n"
|
||||||
|
"for GCP and taskchampion-sync-server. See `man task-sync` for details.\n\n"};
|
||||||
|
items.push_back(info);
|
||||||
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
int CmdNews::execute(std::string& output) {
|
int CmdNews::execute(std::string& output) {
|
||||||
auto words = Context::getContext().cli2.getWords();
|
auto words = Context::getContext().cli2.getWords();
|
||||||
@@ -563,7 +593,7 @@ int CmdNews::execute(std::string& output) {
|
|||||||
std::cout << outro.str();
|
std::cout << outro.str();
|
||||||
|
|
||||||
// Set a mark in the config to remember which version's release notes were displayed
|
// Set a mark in the config to remember which version's release notes were displayed
|
||||||
if (news_version != current_version) {
|
if (news_version < current_version) {
|
||||||
CmdConfig::setConfigVariable("news.version", std::string(current_version), false);
|
CmdConfig::setConfigVariable("news.version", std::string(current_version), false);
|
||||||
|
|
||||||
// Revert back to default signal handling after displaying the outro
|
// Revert back to default signal handling after displaying the outro
|
||||||
@@ -600,3 +630,28 @@ int CmdNews::execute(std::string& output) {
|
|||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool CmdNews::should_nag() {
|
||||||
|
if (!Context::getContext().verbose("news")) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Version news_version(Context::getContext().config.get("news.version"));
|
||||||
|
if (!news_version.is_valid()) news_version = Version("2.6.0");
|
||||||
|
|
||||||
|
Version current_version = Version::Current();
|
||||||
|
|
||||||
|
if (news_version >= current_version) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there are actually any interesting news items to show.
|
||||||
|
std::vector<NewsItem> items = NewsItem::all();
|
||||||
|
for (auto& item : items) {
|
||||||
|
if (item._version > news_version) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ class NewsItem {
|
|||||||
static void version2_6_0(std::vector<NewsItem>&);
|
static void version2_6_0(std::vector<NewsItem>&);
|
||||||
static void version3_0_0(std::vector<NewsItem>&);
|
static void version3_0_0(std::vector<NewsItem>&);
|
||||||
static void version3_1_0(std::vector<NewsItem>&);
|
static void version3_1_0(std::vector<NewsItem>&);
|
||||||
|
static void version3_2_0(std::vector<NewsItem>&);
|
||||||
|
static void version3_3_0(std::vector<NewsItem>&);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
NewsItem(Version, const std::string&, const std::string& = "", const std::string& = "",
|
NewsItem(Version, const std::string&, const std::string& = "", const std::string& = "",
|
||||||
@@ -62,6 +64,8 @@ class CmdNews : public Command {
|
|||||||
public:
|
public:
|
||||||
CmdNews();
|
CmdNews();
|
||||||
int execute(std::string&);
|
int execute(std::string&);
|
||||||
|
|
||||||
|
static bool should_nag();
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -194,15 +194,20 @@ int CmdShow::execute(std::string& output) {
|
|||||||
" search.case.sensitive"
|
" search.case.sensitive"
|
||||||
" sugar"
|
" sugar"
|
||||||
" summary.all.projects"
|
" summary.all.projects"
|
||||||
" sync.local.server_dir"
|
" sync.aws.access_key_id"
|
||||||
|
" sync.aws.bucket"
|
||||||
|
" sync.aws.default_credentials"
|
||||||
|
" sync.aws.profile"
|
||||||
|
" sync.aws.region"
|
||||||
|
" sync.aws.secret_access_key"
|
||||||
" sync.gcp.credential_path"
|
" sync.gcp.credential_path"
|
||||||
" sync.gcp.bucket"
|
" sync.gcp.bucket"
|
||||||
|
" sync.local.server_dir"
|
||||||
" sync.server.client_id"
|
" sync.server.client_id"
|
||||||
" sync.encryption_secret"
|
" sync.encryption_secret"
|
||||||
" sync.server.url"
|
" sync.server.url"
|
||||||
" sync.server.origin"
|
" sync.server.origin"
|
||||||
" tag.indicator"
|
" tag.indicator"
|
||||||
" undo.style"
|
|
||||||
" urgency.active.coefficient"
|
" urgency.active.coefficient"
|
||||||
" urgency.scheduled.coefficient"
|
" urgency.scheduled.coefficient"
|
||||||
" urgency.annotations.coefficient"
|
" urgency.annotations.coefficient"
|
||||||
|
|||||||
@@ -35,12 +35,12 @@
|
|||||||
#include <inttypes.h>
|
#include <inttypes.h>
|
||||||
#include <shared.h>
|
#include <shared.h>
|
||||||
#include <signal.h>
|
#include <signal.h>
|
||||||
|
#include <taskchampion-cpp/lib.h>
|
||||||
#include <util.h>
|
#include <util.h>
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
|
||||||
#include "tc/Server.h"
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
CmdSync::CmdSync() {
|
CmdSync::CmdSync() {
|
||||||
_keyword = "synchronize";
|
_keyword = "synchronize";
|
||||||
@@ -60,55 +60,99 @@ CmdSync::CmdSync() {
|
|||||||
int CmdSync::execute(std::string& output) {
|
int CmdSync::execute(std::string& output) {
|
||||||
int status = 0;
|
int status = 0;
|
||||||
|
|
||||||
tc::Server server;
|
Context& context = Context::getContext();
|
||||||
std::string server_ident;
|
auto& replica = context.tdb2.replica();
|
||||||
|
std::stringstream out;
|
||||||
|
bool avoid_snapshots = false;
|
||||||
|
bool verbose = Context::getContext().verbose("sync");
|
||||||
|
|
||||||
// If no server is set up, quit.
|
|
||||||
std::string origin = Context::getContext().config.get("sync.server.origin");
|
std::string origin = Context::getContext().config.get("sync.server.origin");
|
||||||
std::string url = Context::getContext().config.get("sync.server.url");
|
std::string url = Context::getContext().config.get("sync.server.url");
|
||||||
std::string server_dir = Context::getContext().config.get("sync.local.server_dir");
|
std::string server_dir = Context::getContext().config.get("sync.local.server_dir");
|
||||||
std::string gcp_credential_path = Context::getContext().config.get("sync.gcp.credential_path");
|
std::string client_id = Context::getContext().config.get("sync.server.client_id");
|
||||||
|
std::string aws_bucket = Context::getContext().config.get("sync.aws.bucket");
|
||||||
std::string gcp_bucket = Context::getContext().config.get("sync.gcp.bucket");
|
std::string gcp_bucket = Context::getContext().config.get("sync.gcp.bucket");
|
||||||
std::string encryption_secret = Context::getContext().config.get("sync.encryption_secret");
|
std::string encryption_secret = Context::getContext().config.get("sync.encryption_secret");
|
||||||
|
|
||||||
// sync.server.origin is a deprecated synonym for sync.server.url
|
// sync.server.origin is a deprecated synonym for sync.server.url
|
||||||
std::string server_url = url == "" ? origin : url;
|
std::string server_url = url == "" ? origin : url;
|
||||||
|
|
||||||
if (server_dir != "") {
|
|
||||||
server = tc::Server::new_local(server_dir);
|
|
||||||
server_ident = server_dir;
|
|
||||||
} else if (gcp_bucket != "") {
|
|
||||||
if (encryption_secret == "") {
|
|
||||||
throw std::string("sync.encryption_secret is required");
|
|
||||||
}
|
|
||||||
server = tc::Server::new_gcp(gcp_bucket, gcp_credential_path, encryption_secret);
|
|
||||||
std::ostringstream os;
|
|
||||||
os << "GCP bucket " << gcp_bucket;
|
|
||||||
server_ident = os.str();
|
|
||||||
} else if (server_url != "") {
|
|
||||||
std::string client_id = Context::getContext().config.get("sync.server.client_id");
|
|
||||||
if (client_id == "" || encryption_secret == "") {
|
|
||||||
throw std::string("sync.server.client_id and sync.encryption_secret are required");
|
|
||||||
}
|
|
||||||
server = tc::Server::new_sync(server_url, client_id, encryption_secret);
|
|
||||||
std::ostringstream os;
|
|
||||||
os << "Sync server at " << server_url;
|
|
||||||
server_ident = os.str();
|
|
||||||
} else {
|
|
||||||
throw std::string("No sync.* settings are configured. See task-sync(5).");
|
|
||||||
}
|
|
||||||
|
|
||||||
std::stringstream out;
|
|
||||||
if (origin != "") {
|
if (origin != "") {
|
||||||
out << "sync.server.origin is deprecated. Use sync.server.url instead.\n";
|
out << "sync.server.origin is deprecated. Use sync.server.url instead.\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Context::getContext().verbose("sync")) {
|
if (server_dir != "") {
|
||||||
out << format("Syncing with {1}", server_ident) << '\n';
|
if (verbose) {
|
||||||
|
out << format("Syncing with {1}", server_dir) << '\n';
|
||||||
|
}
|
||||||
|
replica->sync_to_local(server_dir, avoid_snapshots);
|
||||||
|
} else if (aws_bucket != "") {
|
||||||
|
std::string aws_region = Context::getContext().config.get("sync.aws.region");
|
||||||
|
std::string aws_profile = Context::getContext().config.get("sync.aws.profile");
|
||||||
|
std::string aws_access_key_id = Context::getContext().config.get("sync.aws.access_key_id");
|
||||||
|
std::string aws_secret_access_key =
|
||||||
|
Context::getContext().config.get("sync.aws.secret_access_key");
|
||||||
|
std::string aws_default_credentials =
|
||||||
|
Context::getContext().config.get("sync.aws.default_credentials");
|
||||||
|
if (aws_region == "") {
|
||||||
|
throw std::string("sync.aws.region is required");
|
||||||
|
}
|
||||||
|
if (encryption_secret == "") {
|
||||||
|
throw std::string("sync.encryption_secret is required");
|
||||||
}
|
}
|
||||||
|
|
||||||
Context& context = Context::getContext();
|
bool using_profile = false;
|
||||||
context.tdb2.sync(std::move(server), false);
|
bool using_creds = false;
|
||||||
|
bool using_default = false;
|
||||||
|
if (aws_profile != "") {
|
||||||
|
using_profile = true;
|
||||||
|
}
|
||||||
|
if (aws_access_key_id != "" || aws_secret_access_key != "") {
|
||||||
|
using_creds = true;
|
||||||
|
}
|
||||||
|
if (aws_default_credentials != "") {
|
||||||
|
using_default = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (using_profile + using_creds + using_default != 1) {
|
||||||
|
throw std::string("exactly one method of specifying AWS credentials is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (verbose) {
|
||||||
|
out << format("Syncing with AWS bucket {1}", aws_bucket) << '\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (using_profile) {
|
||||||
|
replica->sync_to_aws_with_profile(aws_region, aws_bucket, aws_profile, encryption_secret,
|
||||||
|
avoid_snapshots);
|
||||||
|
} else if (using_creds) {
|
||||||
|
replica->sync_to_aws_with_access_key(aws_region, aws_bucket, aws_access_key_id,
|
||||||
|
aws_secret_access_key, encryption_secret,
|
||||||
|
avoid_snapshots);
|
||||||
|
} else {
|
||||||
|
replica->sync_to_aws_with_default_creds(aws_region, aws_bucket, encryption_secret,
|
||||||
|
avoid_snapshots);
|
||||||
|
}
|
||||||
|
} else if (gcp_bucket != "") {
|
||||||
|
std::string gcp_credential_path = Context::getContext().config.get("sync.gcp.credential_path");
|
||||||
|
if (encryption_secret == "") {
|
||||||
|
throw std::string("sync.encryption_secret is required");
|
||||||
|
}
|
||||||
|
if (verbose) {
|
||||||
|
out << format("Syncing with GCP bucket {1}", gcp_bucket) << '\n';
|
||||||
|
}
|
||||||
|
replica->sync_to_gcp(gcp_bucket, gcp_credential_path, encryption_secret, avoid_snapshots);
|
||||||
|
} else if (server_url != "") {
|
||||||
|
if (client_id == "" || encryption_secret == "") {
|
||||||
|
throw std::string("sync.server.client_id and sync.encryption_secret are required");
|
||||||
|
}
|
||||||
|
if (verbose) {
|
||||||
|
out << format("Syncing with sync server at {1}", server_url) << '\n';
|
||||||
|
}
|
||||||
|
replica->sync_to_remote(server_url, tc::uuid_from_string(client_id), encryption_secret,
|
||||||
|
avoid_snapshots);
|
||||||
|
} else {
|
||||||
|
throw std::string("No sync.* settings are configured. See task-sync(5).");
|
||||||
|
}
|
||||||
|
|
||||||
if (context.config.getBoolean("purge.on-sync")) {
|
if (context.config.getBoolean("purge.on-sync")) {
|
||||||
context.tdb2.expire_tasks();
|
context.tdb2.expire_tasks();
|
||||||
|
|||||||
@@ -29,6 +29,13 @@
|
|||||||
|
|
||||||
#include <CmdUndo.h>
|
#include <CmdUndo.h>
|
||||||
#include <Context.h>
|
#include <Context.h>
|
||||||
|
#include <Operation.h>
|
||||||
|
#include <Task.h>
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
#include "shared.h"
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
CmdUndo::CmdUndo() {
|
CmdUndo::CmdUndo() {
|
||||||
@@ -47,8 +54,94 @@ CmdUndo::CmdUndo() {
|
|||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
int CmdUndo::execute(std::string&) {
|
int CmdUndo::execute(std::string&) {
|
||||||
Context::getContext().tdb2.revert();
|
auto& replica = Context::getContext().tdb2.replica();
|
||||||
|
rust::Vec<tc::Operation> undo_ops = replica->get_undo_operations();
|
||||||
|
if (confirm_revert(Operation::operations(undo_ops))) {
|
||||||
|
// Note that commit_reversed_operations rebuilds the working set, so that
|
||||||
|
// need not be done here.
|
||||||
|
if (!replica->commit_reversed_operations(std::move(undo_ops))) {
|
||||||
|
std::cout << "Could not undo: other operations have occurred.";
|
||||||
|
}
|
||||||
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
bool CmdUndo::confirm_revert(const std::vector<Operation>& undo_ops) {
|
||||||
|
// Count non-undo operations
|
||||||
|
int ops_count = 0;
|
||||||
|
for (auto& op : undo_ops) {
|
||||||
|
if (!op.is_undo_point()) {
|
||||||
|
ops_count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ops_count == 0) {
|
||||||
|
std::cout << "No operations to undo.\n";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << "The following " << ops_count << " operations would be reverted:\n";
|
||||||
|
|
||||||
|
Table view;
|
||||||
|
if (Context::getContext().config.getBoolean("obfuscate")) view.obfuscate();
|
||||||
|
view.width(Context::getContext().getWidth());
|
||||||
|
view.add("Uuid");
|
||||||
|
view.add("Modification");
|
||||||
|
|
||||||
|
std::string last_uuid;
|
||||||
|
std::stringstream mods;
|
||||||
|
for (auto& op : undo_ops) {
|
||||||
|
if (op.is_undo_point()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (last_uuid != op.get_uuid()) {
|
||||||
|
if (last_uuid.size() != 0) {
|
||||||
|
int row = view.addRow();
|
||||||
|
view.set(row, 0, last_uuid);
|
||||||
|
view.set(row, 1, mods.str());
|
||||||
|
}
|
||||||
|
last_uuid = op.get_uuid();
|
||||||
|
mods.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (op.is_create()) {
|
||||||
|
mods << "Create task\n";
|
||||||
|
} else if (op.is_delete()) {
|
||||||
|
mods << "Delete (purge) task";
|
||||||
|
} else if (op.is_update()) {
|
||||||
|
auto property = op.get_property();
|
||||||
|
auto old_value = op.get_old_value();
|
||||||
|
auto value = op.get_value();
|
||||||
|
if (Task::isTagAttr(property)) {
|
||||||
|
if (value && *value == "x") {
|
||||||
|
mods << "Add tag '" << Task::attr2Tag(property) << "'\n";
|
||||||
|
continue;
|
||||||
|
} else if (!value && old_value && *old_value == "x") {
|
||||||
|
mods << "Remove tag '" << Task::attr2Tag(property) << "'\n";
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (old_value && value) {
|
||||||
|
mods << "Update property '" << property << "' from '" << *old_value << "' to '" << *value
|
||||||
|
<< "'\n";
|
||||||
|
} else if (old_value) {
|
||||||
|
mods << "Delete property '" << property << "' (was '" << *old_value << "')\n";
|
||||||
|
} else if (value) {
|
||||||
|
mods << "Add property '" << property << "' with value '" << *value << "'\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
int row = view.addRow();
|
||||||
|
view.set(row, 0, last_uuid);
|
||||||
|
view.set(row, 1, mods.str());
|
||||||
|
std::cout << view.render() << "\n";
|
||||||
|
|
||||||
|
return !Context::getContext().config.getBoolean("confirmation") ||
|
||||||
|
confirm(
|
||||||
|
"The undo command is not reversible. Are you sure you want to revert to the previous "
|
||||||
|
"state?");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|||||||
@@ -28,13 +28,18 @@
|
|||||||
#define INCLUDED_CMDUNDO
|
#define INCLUDED_CMDUNDO
|
||||||
|
|
||||||
#include <Command.h>
|
#include <Command.h>
|
||||||
|
#include <Operation.h>
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
class CmdUndo : public Command {
|
class CmdUndo : public Command {
|
||||||
public:
|
public:
|
||||||
CmdUndo();
|
CmdUndo();
|
||||||
int execute(std::string &);
|
int execute(std::string &);
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool confirm_revert(const std::vector<Operation> &);
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -66,6 +66,7 @@
|
|||||||
#include <CmdHistory.h>
|
#include <CmdHistory.h>
|
||||||
#include <CmdIDs.h>
|
#include <CmdIDs.h>
|
||||||
#include <CmdImport.h>
|
#include <CmdImport.h>
|
||||||
|
#include <CmdImportV2.h>
|
||||||
#include <CmdInfo.h>
|
#include <CmdInfo.h>
|
||||||
#include <CmdLog.h>
|
#include <CmdLog.h>
|
||||||
#include <CmdLogo.h>
|
#include <CmdLogo.h>
|
||||||
@@ -188,6 +189,8 @@ void Command::factory(std::map<std::string, Command*>& all) {
|
|||||||
all[c->keyword()] = c;
|
all[c->keyword()] = c;
|
||||||
c = new CmdImport();
|
c = new CmdImport();
|
||||||
all[c->keyword()] = c;
|
all[c->keyword()] = c;
|
||||||
|
c = new CmdImportV2();
|
||||||
|
all[c->keyword()] = c;
|
||||||
c = new CmdInfo();
|
c = new CmdInfo();
|
||||||
all[c->keyword()] = c;
|
all[c->keyword()] = c;
|
||||||
c = new CmdLog();
|
c = new CmdLog();
|
||||||
|
|||||||
Submodule src/libshared updated: 47c3262fa9...1a06cb4cae
11
src/main.cpp
11
src/main.cpp
@@ -28,6 +28,8 @@
|
|||||||
// cmake.h include header must come first
|
// cmake.h include header must come first
|
||||||
|
|
||||||
#include <Context.h>
|
#include <Context.h>
|
||||||
|
#include <rust/cxx.h>
|
||||||
|
#include <signal.h>
|
||||||
|
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
@@ -38,6 +40,10 @@
|
|||||||
int main(int argc, const char** argv) {
|
int main(int argc, const char** argv) {
|
||||||
int status{0};
|
int status{0};
|
||||||
|
|
||||||
|
// Ignore SIGPIPE from writes to network sockets after the remote end has hung
|
||||||
|
// up. Rust code expects this, and the Rust runtime ignores this signal at startup.
|
||||||
|
signal(SIGPIPE, SIG_IGN);
|
||||||
|
|
||||||
Context globalContext;
|
Context globalContext;
|
||||||
Context::setContext(&globalContext);
|
Context::setContext(&globalContext);
|
||||||
|
|
||||||
@@ -55,6 +61,11 @@ int main(int argc, const char** argv) {
|
|||||||
status = -1;
|
status = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
catch (rust::Error& err) {
|
||||||
|
std::cerr << err.what() << "\n";
|
||||||
|
status = -1;
|
||||||
|
}
|
||||||
|
|
||||||
catch (std::bad_alloc& error) {
|
catch (std::bad_alloc& error) {
|
||||||
std::cerr << "Error: Memory allocation failed: " << error.what() << "\n";
|
std::cerr << "Error: Memory allocation failed: " << error.what() << "\n";
|
||||||
status = -3;
|
status = -3;
|
||||||
|
|||||||
@@ -35,13 +35,15 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <list>
|
#include <list>
|
||||||
#include <map>
|
#include <map>
|
||||||
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
// recur.cpp
|
// recur.cpp
|
||||||
void handleRecurrence();
|
void handleRecurrence();
|
||||||
void handleUntil();
|
void handleUntil();
|
||||||
Datetime getNextRecurrence(Datetime&, std::string&);
|
std::optional<Datetime> checked_add_datetime(Datetime& base, time_t delta);
|
||||||
|
std::optional<Datetime> getNextRecurrence(Datetime&, std::string&);
|
||||||
bool generateDueDates(Task&, std::vector<Datetime>&);
|
bool generateDueDates(Task&, std::vector<Datetime>&);
|
||||||
void updateRecurrenceMask(Task&);
|
void updateRecurrenceMask(Task&);
|
||||||
|
|
||||||
|
|||||||
@@ -47,8 +47,24 @@
|
|||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <iomanip>
|
#include <iomanip>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
|
#include <limits>
|
||||||
|
#include <optional>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
|
|
||||||
|
// Add a `time_t` delta to a Datetime, checking for and returning nullopt on integer overflow.
|
||||||
|
std::optional<Datetime> checked_add_datetime(Datetime& base, time_t delta) {
|
||||||
|
// Datetime::operator+ takes an integer delta, so check that range
|
||||||
|
if (static_cast<time_t>(std::numeric_limits<int>::max()) < delta) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for time_t overflow in the Datetime.
|
||||||
|
if (std::numeric_limits<time_t>::max() - base.toEpoch() < delta) {
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
return base + delta;
|
||||||
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
// Scans all tasks, and for any recurring tasks, determines whether any new
|
// Scans all tasks, and for any recurring tasks, determines whether any new
|
||||||
// child tasks need to be generated to fill gaps.
|
// child tasks need to be generated to fill gaps.
|
||||||
@@ -95,7 +111,12 @@ void handleRecurrence() {
|
|||||||
Datetime old_wait(t.get_date("wait"));
|
Datetime old_wait(t.get_date("wait"));
|
||||||
Datetime old_due(t.get_date("due"));
|
Datetime old_due(t.get_date("due"));
|
||||||
Datetime due(d);
|
Datetime due(d);
|
||||||
rec.set("wait", format((due + (old_wait - old_due)).toEpoch()));
|
auto wait = checked_add_datetime(due, old_wait - old_due);
|
||||||
|
if (wait) {
|
||||||
|
rec.set("wait", format(wait->toEpoch()));
|
||||||
|
} else {
|
||||||
|
rec.remove("wait");
|
||||||
|
}
|
||||||
rec.setStatus(Task::waiting);
|
rec.setStatus(Task::waiting);
|
||||||
mask += 'W';
|
mask += 'W';
|
||||||
} else {
|
} else {
|
||||||
@@ -148,7 +169,8 @@ bool generateDueDates(Task& parent, std::vector<Datetime>& allDue) {
|
|||||||
auto recurrence_limit = Context::getContext().config.getInteger("recurrence.limit");
|
auto recurrence_limit = Context::getContext().config.getInteger("recurrence.limit");
|
||||||
int recurrence_counter = 0;
|
int recurrence_counter = 0;
|
||||||
Datetime now;
|
Datetime now;
|
||||||
for (Datetime i = due;; i = getNextRecurrence(i, recur)) {
|
Datetime i = due;
|
||||||
|
while (1) {
|
||||||
allDue.push_back(i);
|
allDue.push_back(i);
|
||||||
|
|
||||||
if (specificEnd && i > until) {
|
if (specificEnd && i > until) {
|
||||||
@@ -164,13 +186,23 @@ bool generateDueDates(Task& parent, std::vector<Datetime>& allDue) {
|
|||||||
if (i > now) ++recurrence_counter;
|
if (i > now) ++recurrence_counter;
|
||||||
|
|
||||||
if (recurrence_counter >= recurrence_limit) return true;
|
if (recurrence_counter >= recurrence_limit) return true;
|
||||||
|
auto next = getNextRecurrence(i, recur);
|
||||||
|
if (next) {
|
||||||
|
i = *next;
|
||||||
|
} else {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
Datetime getNextRecurrence(Datetime& current, std::string& period) {
|
/// Determine the next recurrence of the given period.
|
||||||
|
///
|
||||||
|
/// If no such date can be calculated, such as with a very large period, returns
|
||||||
|
/// nullopt.
|
||||||
|
std::optional<Datetime> getNextRecurrence(Datetime& current, std::string& period) {
|
||||||
auto m = current.month();
|
auto m = current.month();
|
||||||
auto d = current.day();
|
auto d = current.day();
|
||||||
auto y = current.year();
|
auto y = current.year();
|
||||||
@@ -201,7 +233,7 @@ Datetime getNextRecurrence(Datetime& current, std::string& period) {
|
|||||||
else
|
else
|
||||||
days = 1;
|
days = 1;
|
||||||
|
|
||||||
return current + (days * 86400);
|
return checked_add_datetime(current, days * 86400);
|
||||||
}
|
}
|
||||||
|
|
||||||
else if (unicodeLatinDigit(period[0]) && period[period.length() - 1] == 'm') {
|
else if (unicodeLatinDigit(period[0]) && period[period.length() - 1] == 'm') {
|
||||||
@@ -317,7 +349,7 @@ Datetime getNextRecurrence(Datetime& current, std::string& period) {
|
|||||||
if (!p.parse(period, idx))
|
if (!p.parse(period, idx))
|
||||||
throw std::string(format("The recurrence value '{1}' is not valid.", period));
|
throw std::string(format("The recurrence value '{1}' is not valid.", period));
|
||||||
|
|
||||||
return current + p.toTime_t();
|
return checked_add_datetime(current, p.toTime_t());
|
||||||
}
|
}
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
|||||||
29
src/taskchampion-cpp/CMakeLists.txt
Normal file
29
src/taskchampion-cpp/CMakeLists.txt
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
cmake_minimum_required (VERSION 3.22)
|
||||||
|
|
||||||
|
OPTION(SYSTEM_CORROSION "Use system provided corrosion instead of vendored version" OFF)
|
||||||
|
if(SYSTEM_CORROSION)
|
||||||
|
find_package(Corrosion REQUIRED)
|
||||||
|
else()
|
||||||
|
add_subdirectory(${CMAKE_SOURCE_DIR}/src/taskchampion-cpp/corrosion)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
OPTION (ENABLE_TLS_NATIVE_ROOTS "Use the system's TLS root certificates" OFF)
|
||||||
|
|
||||||
|
if (ENABLE_TLS_NATIVE_ROOTS)
|
||||||
|
message ("Enabling native TLS roots")
|
||||||
|
set(TASKCHAMPION_FEATURES "tls-native-roots")
|
||||||
|
endif (ENABLE_TLS_NATIVE_ROOTS)
|
||||||
|
|
||||||
|
# Import taskchampion-lib as a CMake library. This implements the Rust side of
|
||||||
|
# the cxxbridge, and depends on the `taskchampion` crate.
|
||||||
|
corrosion_import_crate(
|
||||||
|
MANIFEST_PATH "${CMAKE_SOURCE_DIR}/Cargo.toml"
|
||||||
|
LOCKED
|
||||||
|
CRATES "taskchampion-lib"
|
||||||
|
FEATURES "${TASKCHAMPION_FEATURES}")
|
||||||
|
|
||||||
|
# Set up `taskchampion-cpp`, the C++ side of the bridge.
|
||||||
|
corrosion_add_cxxbridge(taskchampion-cpp
|
||||||
|
CRATE taskchampion_lib
|
||||||
|
FILES lib.rs
|
||||||
|
)
|
||||||
20
src/taskchampion-cpp/Cargo.toml
Normal file
20
src/taskchampion-cpp/Cargo.toml
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
[package]
|
||||||
|
name = "taskchampion-lib"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
rust-version = "1.81.0" # MSRV
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["staticlib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
taskchampion = "=2.0.2"
|
||||||
|
cxx = "1.0.133"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
# use native CA roots, instead of bundled
|
||||||
|
tls-native-roots = ["taskchampion/tls-native-roots"]
|
||||||
|
|
||||||
|
[build-dependencies]
|
||||||
|
cxx-build = "1.0.133"
|
||||||
6
src/taskchampion-cpp/build.rs
Normal file
6
src/taskchampion-cpp/build.rs
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
#[allow(unused_must_use)]
|
||||||
|
fn main() {
|
||||||
|
cxx_build::bridge("src/lib.rs");
|
||||||
|
println!("cargo:rerun-if-changed=build.rs");
|
||||||
|
println!("cargo:rerun-if-changed=src/lib.rs");
|
||||||
|
}
|
||||||
1105
src/taskchampion-cpp/src/lib.rs
Normal file
1105
src/taskchampion-cpp/src/lib.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,30 +0,0 @@
|
|||||||
cmake_minimum_required (VERSION 3.22)
|
|
||||||
|
|
||||||
add_subdirectory(${CMAKE_SOURCE_DIR}/src/tc/corrosion)
|
|
||||||
|
|
||||||
# Import taskchampion-lib as a CMake library.
|
|
||||||
corrosion_import_crate(
|
|
||||||
MANIFEST_PATH "${CMAKE_SOURCE_DIR}/Cargo.toml"
|
|
||||||
LOCKED
|
|
||||||
CRATES "taskchampion-lib")
|
|
||||||
|
|
||||||
# TODO(#3425): figure out how to create taskchampion.h
|
|
||||||
|
|
||||||
include_directories (${CMAKE_SOURCE_DIR}
|
|
||||||
${CMAKE_SOURCE_DIR}/src
|
|
||||||
${CMAKE_SOURCE_DIR}/src/tc
|
|
||||||
${CMAKE_SOURCE_DIR}/src/tc/lib
|
|
||||||
${CMAKE_SOURCE_DIR}/src/libshared/src
|
|
||||||
${TASK_INCLUDE_DIRS})
|
|
||||||
|
|
||||||
set (tc_SRCS
|
|
||||||
ffi.h
|
|
||||||
lib/taskchampion.h
|
|
||||||
util.cpp util.h
|
|
||||||
Replica.cpp Replica.h
|
|
||||||
Server.cpp Server.h
|
|
||||||
WorkingSet.cpp WorkingSet.h
|
|
||||||
Task.cpp Task.h)
|
|
||||||
|
|
||||||
add_library (tc STATIC ${tc_SRCS})
|
|
||||||
target_link_libraries(tc taskchampion_lib)
|
|
||||||
@@ -1,273 +0,0 @@
|
|||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
//
|
|
||||||
// Copyright 2022, Dustin J. Mitchell
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included
|
|
||||||
// in all copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
||||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
||||||
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
//
|
|
||||||
// https://www.opensource.org/licenses/mit-license.php
|
|
||||||
//
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
#include <cmake.h>
|
|
||||||
// cmake.h include header must come first
|
|
||||||
|
|
||||||
#include <format.h>
|
|
||||||
|
|
||||||
#include <iostream>
|
|
||||||
|
|
||||||
#include "tc/Replica.h"
|
|
||||||
#include "tc/Server.h"
|
|
||||||
#include "tc/Task.h"
|
|
||||||
#include "tc/WorkingSet.h"
|
|
||||||
#include "tc/util.h"
|
|
||||||
|
|
||||||
using namespace tc::ffi;
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
tc::ReplicaGuard::ReplicaGuard(Replica &replica, Task &task) : replica(replica), task(task) {
|
|
||||||
// "steal" the reference from the Replica and store it locally, so that any
|
|
||||||
// attempt to use the Replica will fail
|
|
||||||
tcreplica = replica.inner.release();
|
|
||||||
task.to_mut(tcreplica);
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
tc::ReplicaGuard::~ReplicaGuard() {
|
|
||||||
task.to_immut();
|
|
||||||
// return the reference to the Replica.
|
|
||||||
replica.inner.reset(tcreplica);
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
tc::Replica::Replica() {
|
|
||||||
inner = unique_tcreplica_ptr(tc_replica_new_in_memory(),
|
|
||||||
[](TCReplica *rep) { tc_replica_free(rep); });
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
tc::Replica::Replica(Replica &&other) noexcept {
|
|
||||||
// move inner from other
|
|
||||||
inner = unique_tcreplica_ptr(other.inner.release(), [](TCReplica *rep) { tc_replica_free(rep); });
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
tc::Replica &tc::Replica::operator=(Replica &&other) noexcept {
|
|
||||||
if (this != &other) {
|
|
||||||
// move inner from other
|
|
||||||
inner =
|
|
||||||
unique_tcreplica_ptr(other.inner.release(), [](TCReplica *rep) { tc_replica_free(rep); });
|
|
||||||
}
|
|
||||||
return *this;
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
tc::Replica::Replica(const std::string &dir, bool create_if_missing) {
|
|
||||||
TCString path = tc_string_borrow(dir.c_str());
|
|
||||||
TCString error;
|
|
||||||
auto tcreplica = tc_replica_new_on_disk(path, create_if_missing, &error);
|
|
||||||
if (!tcreplica) {
|
|
||||||
auto errmsg = format("Could not create replica at {1}: {2}", dir, tc_string_content(&error));
|
|
||||||
tc_string_free(&error);
|
|
||||||
throw errmsg;
|
|
||||||
}
|
|
||||||
inner = unique_tcreplica_ptr(tcreplica, [](TCReplica *rep) { tc_replica_free(rep); });
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
tc::WorkingSet tc::Replica::working_set() {
|
|
||||||
TCWorkingSet *tcws = tc_replica_working_set(&*inner);
|
|
||||||
if (!tcws) {
|
|
||||||
throw replica_error();
|
|
||||||
}
|
|
||||||
return WorkingSet{tcws};
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
std::optional<tc::Task> tc::Replica::get_task(const std::string &uuid) {
|
|
||||||
TCTask *tctask = tc_replica_get_task(&*inner, uuid2tc(uuid));
|
|
||||||
if (!tctask) {
|
|
||||||
auto error = tc_replica_error(&*inner);
|
|
||||||
if (error.ptr) {
|
|
||||||
throw replica_error(error);
|
|
||||||
} else {
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return std::make_optional(Task(tctask));
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
tc::Task tc::Replica::new_task(tc::Status status, const std::string &description) {
|
|
||||||
TCTask *tctask = tc_replica_new_task(&*inner, (tc::ffi::TCStatus)status, string2tc(description));
|
|
||||||
if (!tctask) {
|
|
||||||
throw replica_error();
|
|
||||||
}
|
|
||||||
return Task(tctask);
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
tc::Task tc::Replica::import_task_with_uuid(const std::string &uuid) {
|
|
||||||
TCTask *tctask = tc_replica_import_task_with_uuid(&*inner, uuid2tc(uuid));
|
|
||||||
if (!tctask) {
|
|
||||||
throw replica_error();
|
|
||||||
}
|
|
||||||
return Task(tctask);
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
void tc::Replica::delete_task(const std::string &uuid) {
|
|
||||||
auto res = tc_replica_delete_task(&*inner, uuid2tc(uuid));
|
|
||||||
if (res != TC_RESULT_OK) {
|
|
||||||
throw replica_error();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
void tc::Replica::expire_tasks() {
|
|
||||||
auto res = tc_replica_expire_tasks(&*inner);
|
|
||||||
if (res != TC_RESULT_OK) {
|
|
||||||
throw replica_error();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
void tc::Replica::sync(Server server, bool avoid_snapshots) {
|
|
||||||
// The server remains owned by this function, per tc_replica_sync docs.
|
|
||||||
auto res = tc_replica_sync(&*inner, server.inner.get(), avoid_snapshots);
|
|
||||||
if (res != TC_RESULT_OK) {
|
|
||||||
throw replica_error();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
TCReplicaOpList tc::Replica::get_undo_ops() { return tc_replica_get_undo_ops(&*inner); }
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
void tc::Replica::commit_undo_ops(TCReplicaOpList tc_undo_ops, int32_t *undone_out) {
|
|
||||||
auto res = tc_replica_commit_undo_ops(&*inner, tc_undo_ops, undone_out);
|
|
||||||
if (res != TC_RESULT_OK) {
|
|
||||||
throw replica_error();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
void tc::Replica::free_replica_ops(TCReplicaOpList tc_undo_ops) {
|
|
||||||
tc_replica_op_list_free(&tc_undo_ops);
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
std::string tc::Replica::get_op_uuid(TCReplicaOp &tc_replica_op) const {
|
|
||||||
TCString uuid = tc_replica_op_get_uuid(&tc_replica_op);
|
|
||||||
return tc2string(uuid);
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
std::string tc::Replica::get_op_property(TCReplicaOp &tc_replica_op) const {
|
|
||||||
TCString property = tc_replica_op_get_property(&tc_replica_op);
|
|
||||||
return tc2string(property);
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
std::string tc::Replica::get_op_value(TCReplicaOp &tc_replica_op) const {
|
|
||||||
TCString value = tc_replica_op_get_value(&tc_replica_op);
|
|
||||||
return tc2string(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
std::string tc::Replica::get_op_old_value(TCReplicaOp &tc_replica_op) const {
|
|
||||||
TCString old_value = tc_replica_op_get_old_value(&tc_replica_op);
|
|
||||||
return tc2string(old_value);
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
std::string tc::Replica::get_op_timestamp(TCReplicaOp &tc_replica_op) const {
|
|
||||||
TCString timestamp = tc_replica_op_get_timestamp(&tc_replica_op);
|
|
||||||
return tc2string(timestamp);
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
std::string tc::Replica::get_op_old_task_description(TCReplicaOp &tc_replica_op) const {
|
|
||||||
TCString description = tc_replica_op_get_old_task_description(&tc_replica_op);
|
|
||||||
return tc2string(description);
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
int64_t tc::Replica::num_local_operations() {
|
|
||||||
auto num = tc_replica_num_local_operations(&*inner);
|
|
||||||
if (num < 0) {
|
|
||||||
throw replica_error();
|
|
||||||
}
|
|
||||||
return num;
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
int64_t tc::Replica::num_undo_points() {
|
|
||||||
auto num = tc_replica_num_undo_points(&*inner);
|
|
||||||
if (num < 0) {
|
|
||||||
throw replica_error();
|
|
||||||
}
|
|
||||||
return num;
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
std::vector<tc::Task> tc::Replica::all_tasks() {
|
|
||||||
TCTaskList tasks = tc_replica_all_tasks(&*inner);
|
|
||||||
if (!tasks.items) {
|
|
||||||
throw replica_error();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<Task> all;
|
|
||||||
all.reserve(tasks.len);
|
|
||||||
for (size_t i = 0; i < tasks.len; i++) {
|
|
||||||
auto tctask = tc_task_list_take(&tasks, i);
|
|
||||||
if (tctask) {
|
|
||||||
all.push_back(Task(tctask));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return all;
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
void tc::Replica::rebuild_working_set(bool force) {
|
|
||||||
auto res = tc_replica_rebuild_working_set(&*inner, force);
|
|
||||||
if (res != TC_RESULT_OK) {
|
|
||||||
throw replica_error();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
tc::ReplicaGuard tc::Replica::mutate_task(tc::Task &task) { return ReplicaGuard(*this, task); }
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
std::string tc::Replica::replica_error() { return replica_error(tc_replica_error(&*inner)); }
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
std::string tc::Replica::replica_error(TCString error) {
|
|
||||||
std::string errmsg;
|
|
||||||
if (!error.ptr) {
|
|
||||||
errmsg = std::string("Unknown TaskChampion error");
|
|
||||||
} else {
|
|
||||||
errmsg = std::string(tc_string_content(&error));
|
|
||||||
}
|
|
||||||
tc_string_free(&error);
|
|
||||||
return errmsg;
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
127
src/tc/Replica.h
127
src/tc/Replica.h
@@ -1,127 +0,0 @@
|
|||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
//
|
|
||||||
// Copyright 2022, Dustin J. Mitchell
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included
|
|
||||||
// in all copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
||||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
||||||
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
//
|
|
||||||
// https://www.opensource.org/licenses/mit-license.php
|
|
||||||
//
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
#ifndef INCLUDED_TC_REPLICA
|
|
||||||
#define INCLUDED_TC_REPLICA
|
|
||||||
|
|
||||||
#include <functional>
|
|
||||||
#include <memory>
|
|
||||||
#include <optional>
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include "tc/Task.h"
|
|
||||||
#include "tc/ffi.h"
|
|
||||||
|
|
||||||
namespace tc {
|
|
||||||
class Task;
|
|
||||||
class WorkingSet;
|
|
||||||
class Server;
|
|
||||||
|
|
||||||
// a unique_ptr to a TCReplica which will automatically free the value when
|
|
||||||
// it goes out of scope.
|
|
||||||
using unique_tcreplica_ptr =
|
|
||||||
std::unique_ptr<tc::ffi::TCReplica, std::function<void(tc::ffi::TCReplica *)>>;
|
|
||||||
|
|
||||||
// ReplicaGuard uses RAII to ensure that a Replica is not accessed while it
|
|
||||||
// is mutably borrowed (specifically, to make a task mutable).
|
|
||||||
class ReplicaGuard {
|
|
||||||
protected:
|
|
||||||
friend class Replica;
|
|
||||||
explicit ReplicaGuard(Replica &, Task &);
|
|
||||||
|
|
||||||
public:
|
|
||||||
~ReplicaGuard();
|
|
||||||
|
|
||||||
// No moving or copying allowed
|
|
||||||
ReplicaGuard(const ReplicaGuard &) = delete;
|
|
||||||
ReplicaGuard &operator=(const ReplicaGuard &) = delete;
|
|
||||||
ReplicaGuard(ReplicaGuard &&) = delete;
|
|
||||||
ReplicaGuard &operator=(Replica &&) = delete;
|
|
||||||
|
|
||||||
private:
|
|
||||||
Replica &replica;
|
|
||||||
tc::ffi::TCReplica *tcreplica;
|
|
||||||
Task &task;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Replica wraps the TCReplica type, managing its memory, errors, and so on.
|
|
||||||
//
|
|
||||||
// Except as noted, method names match the suffix to `tc_replica_..`.
|
|
||||||
class Replica {
|
|
||||||
public:
|
|
||||||
Replica(); // tc_replica_new_in_memory
|
|
||||||
Replica(const std::string &dir, bool create_if_missing); // tc_replica_new_on_disk
|
|
||||||
|
|
||||||
// This object "owns" inner, so copy is not allowed.
|
|
||||||
Replica(const Replica &) = delete;
|
|
||||||
Replica &operator=(const Replica &) = delete;
|
|
||||||
|
|
||||||
// Explicit move constructor and assignment
|
|
||||||
Replica(Replica &&) noexcept;
|
|
||||||
Replica &operator=(Replica &&) noexcept;
|
|
||||||
|
|
||||||
std::vector<tc::Task> all_tasks();
|
|
||||||
// TODO: struct TCUuidList tc_replica_all_task_uuids(struct TCReplica *rep);
|
|
||||||
tc::WorkingSet working_set();
|
|
||||||
std::optional<tc::Task> get_task(const std::string &uuid);
|
|
||||||
tc::Task new_task(Status status, const std::string &description);
|
|
||||||
tc::Task import_task_with_uuid(const std::string &uuid);
|
|
||||||
void delete_task(const std::string &uuid);
|
|
||||||
// TODO: struct TCTask *tc_replica_import_task_with_uuid(struct TCReplica *rep, struct TCUuid
|
|
||||||
// tcuuid);
|
|
||||||
void expire_tasks();
|
|
||||||
void sync(Server server, bool avoid_snapshots);
|
|
||||||
tc::ffi::TCReplicaOpList get_undo_ops();
|
|
||||||
void commit_undo_ops(tc::ffi::TCReplicaOpList tc_undo_ops, int32_t *undone_out);
|
|
||||||
void free_replica_ops(tc::ffi::TCReplicaOpList tc_undo_ops);
|
|
||||||
std::string get_op_uuid(tc::ffi::TCReplicaOp &tc_replica_op) const;
|
|
||||||
std::string get_op_property(tc::ffi::TCReplicaOp &tc_replica_op) const;
|
|
||||||
std::string get_op_value(tc::ffi::TCReplicaOp &tc_replica_op) const;
|
|
||||||
std::string get_op_old_value(tc::ffi::TCReplicaOp &tc_replica_op) const;
|
|
||||||
std::string get_op_timestamp(tc::ffi::TCReplicaOp &tc_replica_op) const;
|
|
||||||
std::string get_op_old_task_description(tc::ffi::TCReplicaOp &tc_replica_op) const;
|
|
||||||
int64_t num_local_operations();
|
|
||||||
int64_t num_undo_points();
|
|
||||||
// TODO: TCResult tc_replica_add_undo_point(struct TCReplica *rep, bool force);
|
|
||||||
void rebuild_working_set(bool force);
|
|
||||||
|
|
||||||
ReplicaGuard mutate_task(tc::Task &);
|
|
||||||
void immut_task(tc::Task &);
|
|
||||||
|
|
||||||
protected:
|
|
||||||
friend class ReplicaGuard;
|
|
||||||
unique_tcreplica_ptr inner;
|
|
||||||
|
|
||||||
// construct an error message from tc_replica_error, or from the given
|
|
||||||
// string retrieved from tc_replica_error.
|
|
||||||
std::string replica_error();
|
|
||||||
std::string replica_error(tc::ffi::TCString string);
|
|
||||||
};
|
|
||||||
} // namespace tc
|
|
||||||
|
|
||||||
#endif
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
//
|
|
||||||
// Copyright 2022, Dustin J. Mitchell
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included
|
|
||||||
// in all copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
||||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
||||||
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
//
|
|
||||||
// https://www.opensource.org/licenses/mit-license.php
|
|
||||||
//
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
#include <cmake.h>
|
|
||||||
// cmake.h include header must come first
|
|
||||||
|
|
||||||
#include <format.h>
|
|
||||||
|
|
||||||
#include "tc/Server.h"
|
|
||||||
#include "tc/util.h"
|
|
||||||
|
|
||||||
using namespace tc::ffi;
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
tc::Server tc::Server::new_local(const std::string &server_dir) {
|
|
||||||
TCString tc_server_dir = tc_string_borrow(server_dir.c_str());
|
|
||||||
TCString error;
|
|
||||||
auto tcserver = tc_server_new_local(tc_server_dir, &error);
|
|
||||||
if (!tcserver) {
|
|
||||||
std::string errmsg = format("Could not configure local server at {1}: {2}", server_dir,
|
|
||||||
tc_string_content(&error));
|
|
||||||
tc_string_free(&error);
|
|
||||||
throw errmsg;
|
|
||||||
}
|
|
||||||
return Server(unique_tcserver_ptr(tcserver, [](TCServer *rep) { tc_server_free(rep); }));
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
tc::Server tc::Server::new_sync(const std::string &url, const std::string &client_id,
|
|
||||||
const std::string &encryption_secret) {
|
|
||||||
TCString tc_url = tc_string_borrow(url.c_str());
|
|
||||||
TCString tc_client_id = tc_string_borrow(client_id.c_str());
|
|
||||||
TCString tc_encryption_secret = tc_string_borrow(encryption_secret.c_str());
|
|
||||||
|
|
||||||
TCUuid tc_client_uuid;
|
|
||||||
if (tc_uuid_from_str(tc_client_id, &tc_client_uuid) != TC_RESULT_OK) {
|
|
||||||
tc_string_free(&tc_url);
|
|
||||||
tc_string_free(&tc_encryption_secret);
|
|
||||||
throw format("client_id '{1}' is not a valid UUID", client_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
TCString error;
|
|
||||||
auto tcserver = tc_server_new_sync(tc_url, tc_client_uuid, tc_encryption_secret, &error);
|
|
||||||
if (!tcserver) {
|
|
||||||
std::string errmsg = format("Could not configure connection to server at {1}: {2}", url,
|
|
||||||
tc_string_content(&error));
|
|
||||||
tc_string_free(&error);
|
|
||||||
throw errmsg;
|
|
||||||
}
|
|
||||||
return Server(unique_tcserver_ptr(tcserver, [](TCServer *rep) { tc_server_free(rep); }));
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
tc::Server tc::Server::new_gcp(const std::string &bucket, const std::string &credential_path,
|
|
||||||
const std::string &encryption_secret) {
|
|
||||||
TCString tc_bucket = tc_string_borrow(bucket.c_str());
|
|
||||||
TCString tc_encryption_secret = tc_string_borrow(encryption_secret.c_str());
|
|
||||||
TCString tc_credential_path = tc_string_borrow(credential_path.c_str());
|
|
||||||
|
|
||||||
TCString error;
|
|
||||||
auto tcserver = tc_server_new_gcp(tc_bucket, tc_credential_path, tc_encryption_secret, &error);
|
|
||||||
if (!tcserver) {
|
|
||||||
std::string errmsg = format("Could not configure connection to GCP bucket {1}: {2}", bucket,
|
|
||||||
tc_string_content(&error));
|
|
||||||
tc_string_free(&error);
|
|
||||||
throw errmsg;
|
|
||||||
}
|
|
||||||
return Server(unique_tcserver_ptr(tcserver, [](TCServer *rep) { tc_server_free(rep); }));
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
tc::Server::Server(tc::Server &&other) noexcept {
|
|
||||||
// move inner from other
|
|
||||||
inner = unique_tcserver_ptr(other.inner.release(), [](TCServer *rep) { tc_server_free(rep); });
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
tc::Server &tc::Server::operator=(tc::Server &&other) noexcept {
|
|
||||||
if (this != &other) {
|
|
||||||
// move inner from other
|
|
||||||
inner = unique_tcserver_ptr(other.inner.release(), [](TCServer *rep) { tc_server_free(rep); });
|
|
||||||
}
|
|
||||||
return *this;
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
//
|
|
||||||
// Copyright 2022, Dustin J. Mitchell
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included
|
|
||||||
// in all copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
||||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
||||||
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
//
|
|
||||||
// https://www.opensource.org/licenses/mit-license.php
|
|
||||||
//
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
#ifndef INCLUDED_TC_SERVER
|
|
||||||
#define INCLUDED_TC_SERVER
|
|
||||||
|
|
||||||
#include <functional>
|
|
||||||
#include <memory>
|
|
||||||
#include <optional>
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include "tc/ffi.h"
|
|
||||||
|
|
||||||
namespace tc {
|
|
||||||
// a unique_ptr to a TCServer which will automatically free the value when
|
|
||||||
// it goes out of scope.
|
|
||||||
using unique_tcserver_ptr =
|
|
||||||
std::unique_ptr<tc::ffi::TCServer, std::function<void(tc::ffi::TCServer *)>>;
|
|
||||||
|
|
||||||
// Server wraps the TCServer type, managing its memory, errors, and so on.
|
|
||||||
//
|
|
||||||
// Except as noted, method names match the suffix to `tc_server_..`.
|
|
||||||
class Server {
|
|
||||||
public:
|
|
||||||
// Construct a null server
|
|
||||||
Server() = default;
|
|
||||||
|
|
||||||
// Construct a local server (tc_server_new_local).
|
|
||||||
static Server new_local(const std::string &server_dir);
|
|
||||||
|
|
||||||
// Construct a remote server (tc_server_new_sync).
|
|
||||||
static Server new_sync(const std::string &url, const std::string &client_id,
|
|
||||||
const std::string &encryption_secret);
|
|
||||||
|
|
||||||
// Construct a GCP server (tc_server_new_gcp).
|
|
||||||
static Server new_gcp(const std::string &bucket, const std::string &credential_path,
|
|
||||||
const std::string &encryption_secret);
|
|
||||||
|
|
||||||
// This object "owns" inner, so copy is not allowed.
|
|
||||||
Server(const Server &) = delete;
|
|
||||||
Server &operator=(const Server &) = delete;
|
|
||||||
|
|
||||||
// Explicit move constructor and assignment
|
|
||||||
Server(Server &&) noexcept;
|
|
||||||
Server &operator=(Server &&) noexcept;
|
|
||||||
|
|
||||||
protected:
|
|
||||||
Server(unique_tcserver_ptr inner) : inner(std::move(inner)) {};
|
|
||||||
|
|
||||||
unique_tcserver_ptr inner;
|
|
||||||
|
|
||||||
// Replica accesses the inner pointer to call tc_replica_sync
|
|
||||||
friend class Replica;
|
|
||||||
|
|
||||||
// construct an error message from the given string.
|
|
||||||
std::string server_error(tc::ffi::TCString string);
|
|
||||||
};
|
|
||||||
} // namespace tc
|
|
||||||
|
|
||||||
#endif
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
162
src/tc/Task.cpp
162
src/tc/Task.cpp
@@ -1,162 +0,0 @@
|
|||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
//
|
|
||||||
// Copyright 2022, Dustin J. Mitchell
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included
|
|
||||||
// in all copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
||||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
||||||
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
//
|
|
||||||
// https://www.opensource.org/licenses/mit-license.php
|
|
||||||
//
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
#include <cmake.h>
|
|
||||||
// cmake.h include header must come first
|
|
||||||
|
|
||||||
#include <assert.h>
|
|
||||||
|
|
||||||
#include "tc/Task.h"
|
|
||||||
#include "tc/util.h"
|
|
||||||
|
|
||||||
using namespace tc::ffi;
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
tc::Task::Task(TCTask* tctask) {
|
|
||||||
inner = unique_tctask_ptr(tctask, [](TCTask* task) { tc_task_free(task); });
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
tc::Task::Task(Task&& other) noexcept {
|
|
||||||
// move inner from other
|
|
||||||
inner = unique_tctask_ptr(other.inner.release(), [](TCTask* task) { tc_task_free(task); });
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
tc::Task& tc::Task::operator=(Task&& other) noexcept {
|
|
||||||
if (this != &other) {
|
|
||||||
// move inner from other
|
|
||||||
inner = unique_tctask_ptr(other.inner.release(), [](TCTask* task) { tc_task_free(task); });
|
|
||||||
}
|
|
||||||
return *this;
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
void tc::Task::to_mut(TCReplica* replica) { tc_task_to_mut(&*inner, replica); }
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
void tc::Task::to_immut() { tc_task_to_immut(&*inner); }
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
std::string tc::Task::get_uuid() const {
|
|
||||||
auto uuid = tc_task_get_uuid(&*inner);
|
|
||||||
return tc2uuid(uuid);
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
tc::Status tc::Task::get_status() const {
|
|
||||||
auto status = tc_task_get_status(&*inner);
|
|
||||||
return tc::Status(status);
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
std::map<std::string, std::string> tc::Task::get_taskmap() const {
|
|
||||||
TCKVList kv = tc_task_get_taskmap(&*inner);
|
|
||||||
if (!kv.items) {
|
|
||||||
throw task_error();
|
|
||||||
}
|
|
||||||
|
|
||||||
std::map<std::string, std::string> taskmap;
|
|
||||||
for (size_t i = 0; i < kv.len; i++) {
|
|
||||||
auto k = tc2string_clone(kv.items[i].key);
|
|
||||||
auto v = tc2string_clone(kv.items[i].value);
|
|
||||||
taskmap[k] = v;
|
|
||||||
}
|
|
||||||
|
|
||||||
return taskmap;
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
std::string tc::Task::get_description() const {
|
|
||||||
auto desc = tc_task_get_description(&*inner);
|
|
||||||
return tc2string(desc);
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
std::optional<std::string> tc::Task::get_value(std::string property) const {
|
|
||||||
auto maybe_desc = tc_task_get_value(&*inner, string2tc(property));
|
|
||||||
if (maybe_desc.ptr == NULL) {
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
return std::make_optional(tc2string(maybe_desc));
|
|
||||||
}
|
|
||||||
|
|
||||||
bool tc::Task::is_waiting() const { return tc_task_is_waiting(&*inner); }
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
bool tc::Task::is_active() const { return tc_task_is_active(&*inner); }
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
bool tc::Task::is_blocked() const { return tc_task_is_blocked(&*inner); }
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
bool tc::Task::is_blocking() const { return tc_task_is_blocking(&*inner); }
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
void tc::Task::set_status(tc::Status status) {
|
|
||||||
TCResult res = tc_task_set_status(&*inner, (TCStatus)status);
|
|
||||||
if (res != TC_RESULT_OK) {
|
|
||||||
throw task_error();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
void tc::Task::set_value(std::string property, std::optional<std::string> value) {
|
|
||||||
TCResult res;
|
|
||||||
if (value.has_value()) {
|
|
||||||
res = tc_task_set_value(&*inner, string2tc(property), string2tc(value.value()));
|
|
||||||
} else {
|
|
||||||
TCString nullstr;
|
|
||||||
nullstr.ptr = NULL;
|
|
||||||
res = tc_task_set_value(&*inner, string2tc(property), nullstr);
|
|
||||||
}
|
|
||||||
if (res != TC_RESULT_OK) {
|
|
||||||
throw task_error();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
void tc::Task::set_modified(time_t modified) {
|
|
||||||
TCResult res = tc_task_set_modified(&*inner, modified);
|
|
||||||
if (res != TC_RESULT_OK) {
|
|
||||||
throw task_error();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
std::string tc::Task::task_error() const {
|
|
||||||
TCString error = tc_task_error(&*inner);
|
|
||||||
std::string errmsg;
|
|
||||||
if (!error.ptr) {
|
|
||||||
errmsg = std::string("Unknown TaskChampion error");
|
|
||||||
} else {
|
|
||||||
errmsg = std::string(tc_string_content(&error));
|
|
||||||
}
|
|
||||||
tc_string_free(&error);
|
|
||||||
return errmsg;
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
130
src/tc/Task.h
130
src/tc/Task.h
@@ -1,130 +0,0 @@
|
|||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
//
|
|
||||||
// Copyright 2022, Dustin J. Mitchell, Tomas Babej, Paul Beckingham, Federico Hernandez.
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included
|
|
||||||
// in all copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
||||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
||||||
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
//
|
|
||||||
// https://www.opensource.org/licenses/mit-license.php
|
|
||||||
//
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
#ifndef INCLUDED_TC_TASK
|
|
||||||
#define INCLUDED_TC_TASK
|
|
||||||
|
|
||||||
#include <functional>
|
|
||||||
#include <map>
|
|
||||||
#include <memory>
|
|
||||||
#include <optional>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
#include "tc/ffi.h"
|
|
||||||
|
|
||||||
namespace tc {
|
|
||||||
class Replica;
|
|
||||||
class ReplicaGuard;
|
|
||||||
|
|
||||||
enum Status {
|
|
||||||
Pending = tc::ffi::TC_STATUS_PENDING,
|
|
||||||
Completed = tc::ffi::TC_STATUS_COMPLETED,
|
|
||||||
Deleted = tc::ffi::TC_STATUS_DELETED,
|
|
||||||
Recurring = tc::ffi::TC_STATUS_RECURRING,
|
|
||||||
Unknown = tc::ffi::TC_STATUS_UNKNOWN,
|
|
||||||
};
|
|
||||||
|
|
||||||
// a unique_ptr to a TCReplica which will automatically free the value when
|
|
||||||
// it goes out of scope.
|
|
||||||
using unique_tctask_ptr = std::unique_ptr<tc::ffi::TCTask, std::function<void(tc::ffi::TCTask *)>>;
|
|
||||||
|
|
||||||
// Task wraps the TCTask type, managing its memory, errors, and so on.
|
|
||||||
//
|
|
||||||
// Except as noted, method names match the suffix to `tc_task_..`.
|
|
||||||
class Task {
|
|
||||||
protected:
|
|
||||||
// Tasks may only be created and made mutable/immutable
|
|
||||||
// by tc::Replica
|
|
||||||
friend class tc::Replica;
|
|
||||||
explicit Task(tc::ffi::TCTask *);
|
|
||||||
|
|
||||||
// RplicaGuard handles mut/immut
|
|
||||||
friend class tc::ReplicaGuard;
|
|
||||||
void to_mut(tc::ffi::TCReplica *);
|
|
||||||
void to_immut();
|
|
||||||
|
|
||||||
public:
|
|
||||||
// This object "owns" inner, so copy is not allowed.
|
|
||||||
Task(const Task &) = delete;
|
|
||||||
Task &operator=(const Task &) = delete;
|
|
||||||
|
|
||||||
// Explicit move constructor and assignment
|
|
||||||
Task(Task &&) noexcept;
|
|
||||||
Task &operator=(Task &&) noexcept;
|
|
||||||
|
|
||||||
std::string get_uuid() const;
|
|
||||||
Status get_status() const;
|
|
||||||
std::map<std::string, std::string> get_taskmap() const;
|
|
||||||
std::string get_description() const;
|
|
||||||
std::optional<std::string> get_value(std::string property) const;
|
|
||||||
// TODO: time_t tc_task_get_entry(struct TCTask *task);
|
|
||||||
// TODO: time_t tc_task_get_wait(struct TCTask *task);
|
|
||||||
// TODO: time_t tc_task_get_modified(struct TCTask *task);
|
|
||||||
bool is_waiting() const;
|
|
||||||
bool is_active() const;
|
|
||||||
bool is_blocked() const;
|
|
||||||
bool is_blocking() const;
|
|
||||||
// TODO: bool tc_task_has_tag(struct TCTask *task, struct TCString tag);
|
|
||||||
// TODO: struct TCStringList tc_task_get_tags(struct TCTask *task);
|
|
||||||
// TODO: struct TCAnnotationList tc_task_get_annotations(struct TCTask *task);
|
|
||||||
// TODO: struct TCString tc_task_get_uda(struct TCTask *task, struct TCString ns, struct TCString
|
|
||||||
// key);
|
|
||||||
// TODO: struct TCString tc_task_get_legacy_uda(struct TCTask *task, struct TCString key);
|
|
||||||
// TODO: struct TCUdaList tc_task_get_udas(struct TCTask *task);
|
|
||||||
// TODO: struct TCUdaList tc_task_get_legacy_udas(struct TCTask *task);
|
|
||||||
void set_status(Status status);
|
|
||||||
// TODO: TCResult tc_task_set_description(struct TCTask *task, struct TCString description);
|
|
||||||
void set_value(std::string property, std::optional<std::string> value);
|
|
||||||
// TODO: TCResult tc_task_set_entry(struct TCTask *task, time_t entry);
|
|
||||||
// TODO: TCResult tc_task_set_wait(struct TCTask *task, time_t wait);
|
|
||||||
void set_modified(time_t modified);
|
|
||||||
// TODO: TCResult tc_task_start(struct TCTask *task);
|
|
||||||
// TODO: TCResult tc_task_stop(struct TCTask *task);
|
|
||||||
// TODO: TCResult tc_task_done(struct TCTask *task);
|
|
||||||
// TODO: TCResult tc_task_delete(struct TCTask *task);
|
|
||||||
// TODO: TCResult tc_task_add_tag(struct TCTask *task, struct TCString tag);
|
|
||||||
// TODO: TCResult tc_task_remove_tag(struct TCTask *task, struct TCString tag);
|
|
||||||
// TODO: TCResult tc_task_add_annotation(struct TCTask *task, struct TCAnnotation *annotation);
|
|
||||||
// TODO: TCResult tc_task_remove_annotation(struct TCTask *task, int64_t entry);
|
|
||||||
// TODO: TCResult tc_task_set_uda(struct TCTask *task,
|
|
||||||
// TODO: TCResult tc_task_remove_uda(struct TCTask *task, struct TCString ns, struct TCString
|
|
||||||
// key);
|
|
||||||
// TODO: TCResult tc_task_set_legacy_uda(struct TCTask *task, struct TCString key, struct TCString
|
|
||||||
// value);
|
|
||||||
// TODO: TCResult tc_task_remove_legacy_uda(struct TCTask *task, struct TCString key);
|
|
||||||
|
|
||||||
private:
|
|
||||||
unique_tctask_ptr inner;
|
|
||||||
|
|
||||||
std::string task_error() const; // tc_task_error
|
|
||||||
};
|
|
||||||
} // namespace tc
|
|
||||||
|
|
||||||
// TODO: struct TCTask *tc_task_list_take(struct TCTaskList *tasks, size_t index);
|
|
||||||
// TODO: void tc_task_list_free(struct TCTaskList *tasks);
|
|
||||||
|
|
||||||
#endif
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
//
|
|
||||||
// Copyright 2022, Dustin J. Mitchell
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included
|
|
||||||
// in all copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
||||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
||||||
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
//
|
|
||||||
// https://www.opensource.org/licenses/mit-license.php
|
|
||||||
//
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
#include <cmake.h>
|
|
||||||
// cmake.h include header must come first
|
|
||||||
|
|
||||||
#include <format.h>
|
|
||||||
|
|
||||||
#include "tc/Task.h"
|
|
||||||
#include "tc/WorkingSet.h"
|
|
||||||
#include "tc/util.h"
|
|
||||||
|
|
||||||
using namespace tc::ffi;
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
tc::WorkingSet::WorkingSet(WorkingSet&& other) noexcept {
|
|
||||||
// move inner from other
|
|
||||||
inner = unique_tcws_ptr(other.inner.release(), [](TCWorkingSet* ws) { tc_working_set_free(ws); });
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
tc::WorkingSet& tc::WorkingSet::operator=(WorkingSet&& other) noexcept {
|
|
||||||
if (this != &other) {
|
|
||||||
// move inner from other
|
|
||||||
inner =
|
|
||||||
unique_tcws_ptr(other.inner.release(), [](TCWorkingSet* ws) { tc_working_set_free(ws); });
|
|
||||||
}
|
|
||||||
return *this;
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
tc::WorkingSet::WorkingSet(tc::ffi::TCWorkingSet* tcws) {
|
|
||||||
inner = unique_tcws_ptr(tcws, [](TCWorkingSet* ws) { tc_working_set_free(ws); });
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
size_t tc::WorkingSet::len() const noexcept { return tc_working_set_len(&*inner); }
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
size_t tc::WorkingSet::largest_index() const noexcept {
|
|
||||||
return tc_working_set_largest_index(&*inner);
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
std::optional<std::string> tc::WorkingSet::by_index(size_t index) const noexcept {
|
|
||||||
TCUuid uuid;
|
|
||||||
if (tc_working_set_by_index(&*inner, index, &uuid)) {
|
|
||||||
return std::make_optional(tc2uuid(uuid));
|
|
||||||
} else {
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
std::optional<size_t> tc::WorkingSet::by_uuid(const std::string& uuid) const noexcept {
|
|
||||||
auto index = tc_working_set_by_uuid(&*inner, uuid2tc(uuid));
|
|
||||||
if (index > 0) {
|
|
||||||
return std::make_optional(index);
|
|
||||||
} else {
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
//
|
|
||||||
// Copyright 2022, Dustin J. Mitchell
|
|
||||||
//
|
|
||||||
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
// of this software and associated documentation files (the "Software"), to deal
|
|
||||||
// in the Software without restriction, including without limitation the rights
|
|
||||||
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
// copies of the Software, and to permit persons to whom the Software is
|
|
||||||
// furnished to do so, subject to the following conditions:
|
|
||||||
//
|
|
||||||
// The above copyright notice and this permission notice shall be included
|
|
||||||
// in all copies or substantial portions of the Software.
|
|
||||||
//
|
|
||||||
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
|
|
||||||
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
||||||
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
// SOFTWARE.
|
|
||||||
//
|
|
||||||
// https://www.opensource.org/licenses/mit-license.php
|
|
||||||
//
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
|
|
||||||
#ifndef INCLUDED_TC_WORKINGSET
|
|
||||||
#define INCLUDED_TC_WORKINGSET
|
|
||||||
|
|
||||||
#include <functional>
|
|
||||||
#include <memory>
|
|
||||||
#include <optional>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
#include "tc/Task.h"
|
|
||||||
#include "tc/ffi.h"
|
|
||||||
|
|
||||||
namespace tc {
|
|
||||||
class Task;
|
|
||||||
|
|
||||||
// a unique_ptr to a TCWorkingSet which will automatically free the value when
|
|
||||||
// it goes out of scope.
|
|
||||||
using unique_tcws_ptr =
|
|
||||||
std::unique_ptr<tc::ffi::TCWorkingSet, std::function<void(tc::ffi::TCWorkingSet *)>>;
|
|
||||||
|
|
||||||
// WorkingSet wraps the TCWorkingSet type, managing its memory, errors, and so on.
|
|
||||||
//
|
|
||||||
// Except as noted, method names match the suffix to `tc_working_set_..`.
|
|
||||||
class WorkingSet {
|
|
||||||
protected:
|
|
||||||
friend class tc::Replica;
|
|
||||||
WorkingSet(tc::ffi::TCWorkingSet *); // via tc_replica_working_set
|
|
||||||
|
|
||||||
public:
|
|
||||||
// This object "owns" inner, so copy is not allowed.
|
|
||||||
WorkingSet(const WorkingSet &) = delete;
|
|
||||||
WorkingSet &operator=(const WorkingSet &) = delete;
|
|
||||||
|
|
||||||
// Explicit move constructor and assignment
|
|
||||||
WorkingSet(WorkingSet &&) noexcept;
|
|
||||||
WorkingSet &operator=(WorkingSet &&) noexcept;
|
|
||||||
|
|
||||||
size_t len() const noexcept; // tc_working_set_len
|
|
||||||
size_t largest_index() const noexcept; // tc_working_set_largest_index
|
|
||||||
std::optional<std::string> by_index(size_t index) const noexcept; // tc_working_set_by_index
|
|
||||||
std::optional<size_t> by_uuid(const std::string &index) const noexcept; // tc_working_set_by_uuid
|
|
||||||
|
|
||||||
private:
|
|
||||||
unique_tcws_ptr inner;
|
|
||||||
};
|
|
||||||
} // namespace tc
|
|
||||||
|
|
||||||
#endif
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "taskchampion-lib"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2021"
|
|
||||||
publish = false
|
|
||||||
|
|
||||||
[lib]
|
|
||||||
name = "taskchampion_lib"
|
|
||||||
crate-type = ["staticlib", "rlib"]
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
libc.workspace = true
|
|
||||||
anyhow.workspace = true
|
|
||||||
ffizz-header.workspace = true
|
|
||||||
taskchampion.workspace = true
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
pretty_assertions.workspace = true
|
|
||||||
@@ -1,186 +0,0 @@
|
|||||||
use crate::traits::*;
|
|
||||||
use crate::types::*;
|
|
||||||
use taskchampion::chrono::prelude::*;
|
|
||||||
|
|
||||||
#[ffizz_header::item]
|
|
||||||
#[ffizz(order = 400)]
|
|
||||||
/// ***** TCAnnotation *****
|
|
||||||
///
|
|
||||||
/// TCAnnotation contains the details of an annotation.
|
|
||||||
///
|
|
||||||
/// # Safety
|
|
||||||
///
|
|
||||||
/// An annotation must be initialized from a tc_.. function, and later freed
|
|
||||||
/// with `tc_annotation_free` or `tc_annotation_list_free`.
|
|
||||||
///
|
|
||||||
/// Any function taking a `*TCAnnotation` requires:
|
|
||||||
/// - the pointer must not be NUL;
|
|
||||||
/// - the pointer must be one previously returned from a tc_… function;
|
|
||||||
/// - the memory referenced by the pointer must never be modified by C code; and
|
|
||||||
/// - ownership transfers to the called function, and the value must not be used
|
|
||||||
/// after the call returns. In fact, the value will be zeroed out to ensure this.
|
|
||||||
///
|
|
||||||
/// TCAnnotations are not threadsafe.
|
|
||||||
///
|
|
||||||
/// ```c
|
|
||||||
/// typedef struct TCAnnotation {
|
|
||||||
/// // Time the annotation was made. Must be nonzero.
|
|
||||||
/// time_t entry;
|
|
||||||
/// // Content of the annotation. Must not be NULL.
|
|
||||||
/// TCString description;
|
|
||||||
/// } TCAnnotation;
|
|
||||||
/// ```
|
|
||||||
#[repr(C)]
|
|
||||||
pub struct TCAnnotation {
|
|
||||||
pub entry: libc::time_t,
|
|
||||||
pub description: TCString,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PassByValue for TCAnnotation {
|
|
||||||
// NOTE: we cannot use `RustType = Annotation` here because conversion of the
|
|
||||||
// Rust to a String can fail.
|
|
||||||
type RustType = (DateTime<Utc>, RustString<'static>);
|
|
||||||
|
|
||||||
unsafe fn from_ctype(mut self) -> Self::RustType {
|
|
||||||
// SAFETY:
|
|
||||||
// - any time_t value is valid
|
|
||||||
// - time_t is copy, so ownership is not important
|
|
||||||
let entry = unsafe { libc::time_t::val_from_arg(self.entry) }.unwrap();
|
|
||||||
// SAFETY:
|
|
||||||
// - self.description is valid (came from return_val in as_ctype)
|
|
||||||
// - self is owned, so we can take ownership of this TCString
|
|
||||||
let description =
|
|
||||||
unsafe { TCString::take_val_from_arg(&mut self.description, TCString::default()) };
|
|
||||||
(entry, description)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn as_ctype((entry, description): Self::RustType) -> Self {
|
|
||||||
TCAnnotation {
|
|
||||||
entry: libc::time_t::as_ctype(Some(entry)),
|
|
||||||
// SAFETY:
|
|
||||||
// - ownership of the TCString tied to ownership of Self
|
|
||||||
description: unsafe { TCString::return_val(description) },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for TCAnnotation {
|
|
||||||
fn default() -> Self {
|
|
||||||
TCAnnotation {
|
|
||||||
entry: 0 as libc::time_t,
|
|
||||||
description: TCString::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[ffizz_header::item]
|
|
||||||
#[ffizz(order = 410)]
|
|
||||||
/// ***** TCAnnotationList *****
|
|
||||||
///
|
|
||||||
/// TCAnnotationList represents a list of annotations.
|
|
||||||
///
|
|
||||||
/// The content of this struct must be treated as read-only.
|
|
||||||
///
|
|
||||||
/// ```c
|
|
||||||
/// typedef struct TCAnnotationList {
|
|
||||||
/// // number of annotations in items
|
|
||||||
/// size_t len;
|
|
||||||
/// // reserved
|
|
||||||
/// size_t _u1;
|
|
||||||
/// // Array of annotations. These remain owned by the TCAnnotationList instance and will be freed by
|
|
||||||
/// // tc_annotation_list_free. This pointer is never NULL for a valid TCAnnotationList.
|
|
||||||
/// struct TCAnnotation *items;
|
|
||||||
/// } TCAnnotationList;
|
|
||||||
/// ```
|
|
||||||
#[repr(C)]
|
|
||||||
pub struct TCAnnotationList {
|
|
||||||
len: libc::size_t,
|
|
||||||
/// total size of items (internal use only)
|
|
||||||
capacity: libc::size_t,
|
|
||||||
items: *mut TCAnnotation,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CList for TCAnnotationList {
|
|
||||||
type Element = TCAnnotation;
|
|
||||||
|
|
||||||
unsafe fn from_raw_parts(items: *mut Self::Element, len: usize, cap: usize) -> Self {
|
|
||||||
TCAnnotationList {
|
|
||||||
len,
|
|
||||||
capacity: cap,
|
|
||||||
items,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn slice(&mut self) -> &mut [Self::Element] {
|
|
||||||
// SAFETY:
|
|
||||||
// - because we have &mut self, we have read/write access to items[0..len]
|
|
||||||
// - all items are properly initialized Element's
|
|
||||||
// - return value lifetime is equal to &mmut self's, so access is exclusive
|
|
||||||
// - items and len came from Vec, so total size is < isize::MAX
|
|
||||||
unsafe { std::slice::from_raw_parts_mut(self.items, self.len) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn into_raw_parts(self) -> (*mut Self::Element, usize, usize) {
|
|
||||||
(self.items, self.len, self.capacity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[ffizz_header::item]
|
|
||||||
#[ffizz(order = 401)]
|
|
||||||
/// Free a TCAnnotation instance. The instance, and the TCString it contains, must not be used
|
|
||||||
/// after this call.
|
|
||||||
///
|
|
||||||
/// ```c
|
|
||||||
/// EXTERN_C void tc_annotation_free(struct TCAnnotation *tcann);
|
|
||||||
/// ```
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn tc_annotation_free(tcann: *mut TCAnnotation) {
|
|
||||||
debug_assert!(!tcann.is_null());
|
|
||||||
// SAFETY:
|
|
||||||
// - tcann is not NULL
|
|
||||||
// - *tcann is a valid TCAnnotation (caller promised to treat it as read-only)
|
|
||||||
let annotation = unsafe { TCAnnotation::take_val_from_arg(tcann, TCAnnotation::default()) };
|
|
||||||
drop(annotation);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[ffizz_header::item]
|
|
||||||
#[ffizz(order = 411)]
|
|
||||||
/// Free a TCAnnotationList instance. The instance, and all TCAnnotations it contains, must not be used after
|
|
||||||
/// this call.
|
|
||||||
///
|
|
||||||
/// When this call returns, the `items` pointer will be NULL, signalling an invalid TCAnnotationList.
|
|
||||||
///
|
|
||||||
/// ```c
|
|
||||||
/// EXTERN_C void tc_annotation_list_free(struct TCAnnotationList *tcanns);
|
|
||||||
/// ```
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn tc_annotation_list_free(tcanns: *mut TCAnnotationList) {
|
|
||||||
// SAFETY:
|
|
||||||
// - tcanns is not NULL and points to a valid TCAnnotationList (caller is not allowed to
|
|
||||||
// modify the list)
|
|
||||||
// - caller promises not to use the value after return
|
|
||||||
unsafe { drop_value_list(tcanns) }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn empty_list_has_non_null_pointer() {
|
|
||||||
let tcanns = unsafe { TCAnnotationList::return_val(Vec::new()) };
|
|
||||||
assert!(!tcanns.items.is_null());
|
|
||||||
assert_eq!(tcanns.len, 0);
|
|
||||||
assert_eq!(tcanns.capacity, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn free_sets_null_pointer() {
|
|
||||||
let mut tcanns = unsafe { TCAnnotationList::return_val(Vec::new()) };
|
|
||||||
// SAFETY: testing expected behavior
|
|
||||||
unsafe { tc_annotation_list_free(&mut tcanns) };
|
|
||||||
assert!(tcanns.items.is_null());
|
|
||||||
assert_eq!(tcanns.len, 0);
|
|
||||||
assert_eq!(tcanns.capacity, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
//! Trait implementations for a few atomic types
|
|
||||||
|
|
||||||
use crate::traits::*;
|
|
||||||
use taskchampion::chrono::{DateTime, Utc};
|
|
||||||
use taskchampion::utc_timestamp;
|
|
||||||
|
|
||||||
impl PassByValue for usize {
|
|
||||||
type RustType = usize;
|
|
||||||
|
|
||||||
unsafe fn from_ctype(self) -> usize {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn as_ctype(arg: usize) -> usize {
|
|
||||||
arg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert an Option<DateTime<Utc>> to a libc::time_t, or zero if not set.
|
|
||||||
impl PassByValue for libc::time_t {
|
|
||||||
type RustType = Option<DateTime<Utc>>;
|
|
||||||
|
|
||||||
unsafe fn from_ctype(self) -> Option<DateTime<Utc>> {
|
|
||||||
if self != 0 {
|
|
||||||
return Some(utc_timestamp(self));
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
fn as_ctype(arg: Option<DateTime<Utc>>) -> libc::time_t {
|
|
||||||
arg.map(|ts| ts.timestamp() as libc::time_t)
|
|
||||||
.unwrap_or(0 as libc::time_t)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,155 +0,0 @@
|
|||||||
use crate::traits::*;
|
|
||||||
use crate::types::*;
|
|
||||||
|
|
||||||
#[ffizz_header::item]
|
|
||||||
#[ffizz(order = 600)]
|
|
||||||
/// ***** TCKV *****
|
|
||||||
///
|
|
||||||
/// TCKV contains a key/value pair that is part of a task.
|
|
||||||
///
|
|
||||||
/// Neither key nor value are ever NULL. They remain owned by the TCKV and
|
|
||||||
/// will be freed when it is freed with tc_kv_list_free.
|
|
||||||
///
|
|
||||||
/// ```c
|
|
||||||
/// typedef struct TCKV {
|
|
||||||
/// struct TCString key;
|
|
||||||
/// struct TCString value;
|
|
||||||
/// } TCKV;
|
|
||||||
/// ```
|
|
||||||
#[repr(C)]
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct TCKV {
|
|
||||||
pub key: TCString,
|
|
||||||
pub value: TCString,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PassByValue for TCKV {
|
|
||||||
type RustType = (RustString<'static>, RustString<'static>);
|
|
||||||
|
|
||||||
unsafe fn from_ctype(self) -> Self::RustType {
|
|
||||||
// SAFETY:
|
|
||||||
// - self.key is not NULL (field docstring)
|
|
||||||
// - self.key came from return_ptr in as_ctype
|
|
||||||
// - self is owned, so we can take ownership of this TCString
|
|
||||||
let key = unsafe { TCString::val_from_arg(self.key) };
|
|
||||||
// SAFETY: (same)
|
|
||||||
let value = unsafe { TCString::val_from_arg(self.value) };
|
|
||||||
(key, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn as_ctype((key, value): Self::RustType) -> Self {
|
|
||||||
TCKV {
|
|
||||||
// SAFETY:
|
|
||||||
// - ownership of the TCString tied to ownership of Self
|
|
||||||
key: unsafe { TCString::return_val(key) },
|
|
||||||
// SAFETY:
|
|
||||||
// - ownership of the TCString tied to ownership of Self
|
|
||||||
value: unsafe { TCString::return_val(value) },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[ffizz_header::item]
|
|
||||||
#[ffizz(order = 610)]
|
|
||||||
/// ***** TCKVList *****
|
|
||||||
///
|
|
||||||
/// TCKVList represents a list of key/value pairs.
|
|
||||||
///
|
|
||||||
/// The content of this struct must be treated as read-only.
|
|
||||||
///
|
|
||||||
/// ```c
|
|
||||||
/// typedef struct TCKVList {
|
|
||||||
/// // number of key/value pairs in items
|
|
||||||
/// size_t len;
|
|
||||||
/// // reserved
|
|
||||||
/// size_t _u1;
|
|
||||||
/// // Array of TCKV's. These remain owned by the TCKVList instance and will be freed by
|
|
||||||
/// // tc_kv_list_free. This pointer is never NULL for a valid TCKVList.
|
|
||||||
/// struct TCKV *items;
|
|
||||||
/// } TCKVList;
|
|
||||||
/// ```
|
|
||||||
#[repr(C)]
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct TCKVList {
|
|
||||||
pub len: libc::size_t,
|
|
||||||
/// total size of items (internal use only)
|
|
||||||
pub _capacity: libc::size_t,
|
|
||||||
pub items: *mut TCKV,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CList for TCKVList {
|
|
||||||
type Element = TCKV;
|
|
||||||
|
|
||||||
unsafe fn from_raw_parts(items: *mut Self::Element, len: usize, cap: usize) -> Self {
|
|
||||||
TCKVList {
|
|
||||||
len,
|
|
||||||
_capacity: cap,
|
|
||||||
items,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn slice(&mut self) -> &mut [Self::Element] {
|
|
||||||
// SAFETY:
|
|
||||||
// - because we have &mut self, we have read/write access to items[0..len]
|
|
||||||
// - all items are properly initialized Element's
|
|
||||||
// - return value lifetime is equal to &mmut self's, so access is exclusive
|
|
||||||
// - items and len came from Vec, so total size is < isize::MAX
|
|
||||||
unsafe { std::slice::from_raw_parts_mut(self.items, self.len) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn into_raw_parts(self) -> (*mut Self::Element, usize, usize) {
|
|
||||||
(self.items, self.len, self._capacity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for TCKVList {
|
|
||||||
fn default() -> Self {
|
|
||||||
// SAFETY:
|
|
||||||
// - caller will free this list
|
|
||||||
unsafe { TCKVList::return_val(Vec::new()) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NOTE: callers never have a TCKV that is not in a list, so there is no tc_kv_free.
|
|
||||||
|
|
||||||
#[ffizz_header::item]
|
|
||||||
#[ffizz(order = 611)]
|
|
||||||
/// Free a TCKVList instance. The instance, and all TCKVs it contains, must not be used after
|
|
||||||
/// this call.
|
|
||||||
///
|
|
||||||
/// When this call returns, the `items` pointer will be NULL, signalling an invalid TCKVList.
|
|
||||||
///
|
|
||||||
/// ```c
|
|
||||||
/// EXTERN_C void tc_kv_list_free(struct TCKVList *tckvs);
|
|
||||||
/// ```
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn tc_kv_list_free(tckvs: *mut TCKVList) {
|
|
||||||
// SAFETY:
|
|
||||||
// - tckvs is not NULL and points to a valid TCKVList (caller is not allowed to
|
|
||||||
// modify the list)
|
|
||||||
// - caller promises not to use the value after return
|
|
||||||
unsafe { drop_value_list(tckvs) }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn empty_list_has_non_null_pointer() {
|
|
||||||
let tckvs = unsafe { TCKVList::return_val(Vec::new()) };
|
|
||||||
assert!(!tckvs.items.is_null());
|
|
||||||
assert_eq!(tckvs.len, 0);
|
|
||||||
assert_eq!(tckvs._capacity, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn free_sets_null_pointer() {
|
|
||||||
let mut tckvs = unsafe { TCKVList::return_val(Vec::new()) };
|
|
||||||
// SAFETY: testing expected behavior
|
|
||||||
unsafe { tc_kv_list_free(&mut tckvs) };
|
|
||||||
assert!(tckvs.items.is_null());
|
|
||||||
assert_eq!(tckvs.len, 0);
|
|
||||||
assert_eq!(tckvs._capacity, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,172 +0,0 @@
|
|||||||
#![warn(unsafe_op_in_unsafe_fn)]
|
|
||||||
#![allow(unused_unsafe)]
|
|
||||||
// Not working yet in stable - https://github.com/rust-lang/rust-clippy/issues/8020
|
|
||||||
// #![warn(clippy::undocumented_unsafe_blocks)]
|
|
||||||
|
|
||||||
// docstrings for extern "C" functions are reflected into C, and do not benefit
|
|
||||||
// from safety docs.
|
|
||||||
#![allow(clippy::missing_safety_doc)]
|
|
||||||
// deny some things that are typically warnings
|
|
||||||
#![deny(clippy::derivable_impls)]
|
|
||||||
#![deny(clippy::wrong_self_convention)]
|
|
||||||
#![deny(clippy::extra_unused_lifetimes)]
|
|
||||||
#![deny(clippy::unnecessary_to_owned)]
|
|
||||||
|
|
||||||
// ffizz_header orders:
|
|
||||||
//
|
|
||||||
// 000-099: header matter
|
|
||||||
// 100-199: TCResult
|
|
||||||
// 200-299: TCString / List
|
|
||||||
// 300-399: TCUuid / List
|
|
||||||
// 400-499: TCAnnotation / List
|
|
||||||
// 500-599: TCUda / List
|
|
||||||
// 600-699: TCKV / List
|
|
||||||
// 700-799: TCStatus
|
|
||||||
// 800-899: TCServer
|
|
||||||
// 900-999: TCReplica
|
|
||||||
// 1000-1099: TCTask / List
|
|
||||||
// 1100-1199: TCWorkingSet
|
|
||||||
// 10000-10099: footer
|
|
||||||
|
|
||||||
ffizz_header::snippet! {
|
|
||||||
#[ffizz(name="intro", order=0)]
|
|
||||||
/// TaskChampion
|
|
||||||
///
|
|
||||||
/// This file defines the C interface to libtaskchampion. This is a thin wrapper around the Rust
|
|
||||||
/// `taskchampion` crate. Refer to the documentation for that crate at
|
|
||||||
/// https://docs.rs/taskchampion/latest/taskchampion/ for API details. The comments in this file
|
|
||||||
/// focus mostly on the low-level details of passing values to and from TaskChampion.
|
|
||||||
///
|
|
||||||
/// # Overview
|
|
||||||
///
|
|
||||||
/// This library defines four major types used to interact with the API, that map directly to Rust
|
|
||||||
/// types.
|
|
||||||
///
|
|
||||||
/// * TCReplica - see https://docs.rs/taskchampion/latest/taskchampion/struct.Replica.html
|
|
||||||
/// * TCTask - see https://docs.rs/taskchampion/latest/taskchampion/struct.Task.html
|
|
||||||
/// * TCServer - see https://docs.rs/taskchampion/latest/taskchampion/trait.Server.html
|
|
||||||
/// * TCWorkingSet - see https://docs.rs/taskchampion/latest/taskchampion/struct.WorkingSet.html
|
|
||||||
///
|
|
||||||
/// It also defines a few utility types:
|
|
||||||
///
|
|
||||||
/// * TCString - a wrapper around both C (NUL-terminated) and Rust (always utf-8) strings.
|
|
||||||
/// * TC…List - a list of objects represented as a C array
|
|
||||||
/// * see below for the remainder
|
|
||||||
///
|
|
||||||
/// # Safety
|
|
||||||
///
|
|
||||||
/// Each type contains specific instructions to ensure memory safety. The general rules are as
|
|
||||||
/// follows.
|
|
||||||
///
|
|
||||||
/// No types in this library are threadsafe. All values should be used in only one thread for their
|
|
||||||
/// entire lifetime. It is safe to use unrelated values in different threads (for example,
|
|
||||||
/// different threads may use different TCReplica values concurrently).
|
|
||||||
///
|
|
||||||
/// ## Pass by Pointer
|
|
||||||
///
|
|
||||||
/// Several types such as TCReplica and TCString are "opaque" types and always handled as pointers
|
|
||||||
/// in C. The bytes these pointers address are private to the Rust implementation and must not be
|
|
||||||
/// accessed from C.
|
|
||||||
///
|
|
||||||
/// Pass-by-pointer values have exactly one owner, and that owner is responsible for freeing the
|
|
||||||
/// value (using a `tc_…_free` function), or transferring ownership elsewhere. Except where
|
|
||||||
/// documented otherwise, when a value is passed to C, ownership passes to C as well. When a value
|
|
||||||
/// is passed to Rust, ownership stays with the C code. The exception is TCString, ownership of
|
|
||||||
/// which passes to Rust when it is used as a function argument.
|
|
||||||
///
|
|
||||||
/// The limited circumstances where one value must not outlive another, due to pointer references
|
|
||||||
/// between them, are documented below.
|
|
||||||
///
|
|
||||||
/// ## Pass by Value
|
|
||||||
///
|
|
||||||
/// Types such as TCUuid and TC…List are passed by value, and contain fields that are accessible
|
|
||||||
/// from C. C code is free to access the content of these types in a _read_only_ fashion.
|
|
||||||
///
|
|
||||||
/// Pass-by-value values that contain pointers also have exactly one owner, responsible for freeing
|
|
||||||
/// the value or transferring ownership. The tc_…_free functions for these types will replace the
|
|
||||||
/// pointers with NULL to guard against use-after-free errors. The interior pointers in such values
|
|
||||||
/// should never be freed directly (for example, `tc_string_free(tcuda.value)` is an error).
|
|
||||||
///
|
|
||||||
/// TCUuid is a special case, because it does not contain pointers. It can be freely copied and
|
|
||||||
/// need not be freed.
|
|
||||||
///
|
|
||||||
/// ## Lists
|
|
||||||
///
|
|
||||||
/// Lists are a special kind of pass-by-value type. Each contains `len` and `items`, where `items`
|
|
||||||
/// is an array of length `len`. Lists, and the values in the `items` array, must be treated as
|
|
||||||
/// read-only. On return from an API function, a list's ownership is with the C caller, which must
|
|
||||||
/// eventually free the list. List data must be freed with the `tc_…_list_free` function. It is an
|
|
||||||
/// error to free any value in the `items` array of a list.
|
|
||||||
}
|
|
||||||
|
|
||||||
ffizz_header::snippet! {
|
|
||||||
#[ffizz(name="topmatter", order=1)]
|
|
||||||
/// ```c
|
|
||||||
/// #ifndef TASKCHAMPION_H
|
|
||||||
/// #define TASKCHAMPION_H
|
|
||||||
///
|
|
||||||
/// #include <stdbool.h>
|
|
||||||
/// #include <stdint.h>
|
|
||||||
/// #include <time.h>
|
|
||||||
///
|
|
||||||
/// #ifdef __cplusplus
|
|
||||||
/// #define EXTERN_C extern "C"
|
|
||||||
/// #else
|
|
||||||
/// #define EXTERN_C
|
|
||||||
/// #endif // __cplusplus
|
|
||||||
/// ```
|
|
||||||
}
|
|
||||||
|
|
||||||
ffizz_header::snippet! {
|
|
||||||
#[ffizz(name="bottomatter", order=10000)]
|
|
||||||
/// ```c
|
|
||||||
/// #endif /* TASKCHAMPION_H */
|
|
||||||
/// ```
|
|
||||||
}
|
|
||||||
|
|
||||||
mod traits;
|
|
||||||
mod util;
|
|
||||||
|
|
||||||
pub mod annotation;
|
|
||||||
pub use annotation::*;
|
|
||||||
pub mod atomic;
|
|
||||||
pub mod kv;
|
|
||||||
pub use kv::*;
|
|
||||||
pub mod replica;
|
|
||||||
pub use replica::*;
|
|
||||||
pub mod result;
|
|
||||||
pub use result::*;
|
|
||||||
pub mod server;
|
|
||||||
pub use server::*;
|
|
||||||
pub mod status;
|
|
||||||
pub use status::*;
|
|
||||||
pub mod string;
|
|
||||||
pub use string::*;
|
|
||||||
pub mod task;
|
|
||||||
pub use task::*;
|
|
||||||
pub mod uda;
|
|
||||||
pub use uda::*;
|
|
||||||
pub mod uuid;
|
|
||||||
pub use uuid::*;
|
|
||||||
pub mod workingset;
|
|
||||||
pub use workingset::*;
|
|
||||||
|
|
||||||
pub(crate) mod types {
|
|
||||||
pub(crate) use crate::annotation::{TCAnnotation, TCAnnotationList};
|
|
||||||
pub(crate) use crate::kv::TCKVList;
|
|
||||||
pub(crate) use crate::replica::TCReplica;
|
|
||||||
pub(crate) use crate::result::TCResult;
|
|
||||||
pub(crate) use crate::server::TCServer;
|
|
||||||
pub(crate) use crate::status::TCStatus;
|
|
||||||
pub(crate) use crate::string::{RustString, TCString, TCStringList};
|
|
||||||
pub(crate) use crate::task::{TCTask, TCTaskList};
|
|
||||||
pub(crate) use crate::uda::{TCUda, TCUdaList, Uda};
|
|
||||||
pub(crate) use crate::uuid::{TCUuid, TCUuidList};
|
|
||||||
pub(crate) use crate::workingset::TCWorkingSet;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(debug_assertions)]
|
|
||||||
/// Generate the taskchapion.h header
|
|
||||||
pub fn generate_header() -> String {
|
|
||||||
ffizz_header::generate()
|
|
||||||
}
|
|
||||||
@@ -1,958 +0,0 @@
|
|||||||
use crate::traits::*;
|
|
||||||
use crate::types::*;
|
|
||||||
use crate::util::err_to_ruststring;
|
|
||||||
use std::ptr::NonNull;
|
|
||||||
use taskchampion::storage::ReplicaOp;
|
|
||||||
use taskchampion::{Replica, StorageConfig};
|
|
||||||
|
|
||||||
#[ffizz_header::item]
|
|
||||||
#[ffizz(order = 900)]
|
|
||||||
/// ***** TCReplica *****
|
|
||||||
///
|
|
||||||
/// A replica represents an instance of a user's task data, providing an easy interface
|
|
||||||
/// for querying and modifying that data.
|
|
||||||
///
|
|
||||||
/// # Error Handling
|
|
||||||
///
|
|
||||||
/// When a `tc_replica_..` function that returns a TCResult returns TC_RESULT_ERROR, then
|
|
||||||
/// `tc_replica_error` will return the error message.
|
|
||||||
///
|
|
||||||
/// # Safety
|
|
||||||
///
|
|
||||||
/// The `*TCReplica` returned from `tc_replica_new…` functions is owned by the caller and
|
|
||||||
/// must later be freed to avoid a memory leak.
|
|
||||||
///
|
|
||||||
/// Any function taking a `*TCReplica` requires:
|
|
||||||
/// - the pointer must not be NUL;
|
|
||||||
/// - the pointer must be one previously returned from a tc_… function;
|
|
||||||
/// - the memory referenced by the pointer must never be modified by C code; and
|
|
||||||
/// - except for `tc_replica_free`, ownership of a `*TCReplica` remains with the caller.
|
|
||||||
///
|
|
||||||
/// Once passed to `tc_replica_free`, a `*TCReplica` becomes invalid and must not be used again.
|
|
||||||
///
|
|
||||||
/// TCReplicas are not threadsafe.
|
|
||||||
///
|
|
||||||
/// ```c
|
|
||||||
/// typedef struct TCReplica TCReplica;
|
|
||||||
/// ```
|
|
||||||
pub struct TCReplica {
|
|
||||||
/// The wrapped Replica
|
|
||||||
inner: Replica,
|
|
||||||
|
|
||||||
/// If true, this replica has an outstanding &mut (for a TaskMut)
|
|
||||||
mut_borrowed: bool,
|
|
||||||
|
|
||||||
/// The error from the most recent operation, if any
|
|
||||||
error: Option<RustString<'static>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PassByPointer for TCReplica {}
|
|
||||||
|
|
||||||
impl TCReplica {
|
|
||||||
/// Mutably borrow the inner Replica
|
|
||||||
pub(crate) fn borrow_mut(&mut self) -> &mut Replica {
|
|
||||||
if self.mut_borrowed {
|
|
||||||
panic!("replica is already borrowed");
|
|
||||||
}
|
|
||||||
self.mut_borrowed = true;
|
|
||||||
&mut self.inner
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Release the borrow made by [`borrow_mut`]
|
|
||||||
pub(crate) fn release_borrow(&mut self) {
|
|
||||||
if !self.mut_borrowed {
|
|
||||||
panic!("replica is not borrowed");
|
|
||||||
}
|
|
||||||
self.mut_borrowed = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Replica> for TCReplica {
|
|
||||||
fn from(rep: Replica) -> TCReplica {
|
|
||||||
TCReplica {
|
|
||||||
inner: rep,
|
|
||||||
mut_borrowed: false,
|
|
||||||
error: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Utility function to allow using `?` notation to return an error value. This makes
|
|
||||||
/// a mutable borrow, because most Replica methods require a `&mut`.
|
|
||||||
fn wrap<T, F>(rep: *mut TCReplica, f: F, err_value: T) -> T
|
|
||||||
where
|
|
||||||
F: FnOnce(&mut Replica) -> anyhow::Result<T>,
|
|
||||||
{
|
|
||||||
debug_assert!(!rep.is_null());
|
|
||||||
// SAFETY:
|
|
||||||
// - rep is not NULL (promised by caller)
|
|
||||||
// - *rep is a valid TCReplica (promised by caller)
|
|
||||||
// - rep is valid for the duration of this function
|
|
||||||
// - rep is not modified by anything else (not threadsafe)
|
|
||||||
let rep: &mut TCReplica = unsafe { TCReplica::from_ptr_arg_ref_mut(rep) };
|
|
||||||
if rep.mut_borrowed {
|
|
||||||
panic!("replica is borrowed and cannot be used");
|
|
||||||
}
|
|
||||||
rep.error = None;
|
|
||||||
match f(&mut rep.inner) {
|
|
||||||
Ok(v) => v,
|
|
||||||
Err(e) => {
|
|
||||||
rep.error = Some(err_to_ruststring(e));
|
|
||||||
err_value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Utility function to allow using `?` notation to return an error value in the constructor.
|
|
||||||
fn wrap_constructor<T, F>(f: F, error_out: *mut TCString, err_value: T) -> T
|
|
||||||
where
|
|
||||||
F: FnOnce() -> anyhow::Result<T>,
|
|
||||||
{
|
|
||||||
if !error_out.is_null() {
|
|
||||||
// SAFETY:
|
|
||||||
// - error_out is not NULL (just checked)
|
|
||||||
// - properly aligned and valid (promised by caller)
|
|
||||||
unsafe { *error_out = TCString::default() };
|
|
||||||
}
|
|
||||||
|
|
||||||
match f() {
|
|
||||||
Ok(v) => v,
|
|
||||||
Err(e) => {
|
|
||||||
if !error_out.is_null() {
|
|
||||||
// SAFETY:
|
|
||||||
// - error_out is not NULL (just checked)
|
|
||||||
// - properly aligned and valid (promised by caller)
|
|
||||||
unsafe {
|
|
||||||
TCString::val_to_arg_out(err_to_ruststring(e), error_out);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
err_value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[ffizz_header::item]
|
|
||||||
#[ffizz(order = 900)]
|
|
||||||
/// ***** TCReplicaOpType *****
|
|
||||||
///
|
|
||||||
/// ```c
|
|
||||||
/// enum TCReplicaOpType
|
|
||||||
/// #ifdef __cplusplus
|
|
||||||
/// : uint32_t
|
|
||||||
/// #endif // __cplusplus
|
|
||||||
/// {
|
|
||||||
/// Create = 0,
|
|
||||||
/// Delete = 1,
|
|
||||||
/// Update = 2,
|
|
||||||
/// UndoPoint = 3,
|
|
||||||
/// };
|
|
||||||
/// #ifndef __cplusplus
|
|
||||||
/// typedef uint32_t TCReplicaOpType;
|
|
||||||
/// #endif // __cplusplus
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
#[repr(u32)]
|
|
||||||
pub enum TCReplicaOpType {
|
|
||||||
Create = 0,
|
|
||||||
Delete = 1,
|
|
||||||
Update = 2,
|
|
||||||
UndoPoint = 3,
|
|
||||||
#[default]
|
|
||||||
Error = 4,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[ffizz_header::item]
|
|
||||||
#[ffizz(order = 901)]
|
|
||||||
/// Create a new TCReplica with an in-memory database. The contents of the database will be
|
|
||||||
/// lost when it is freed with tc_replica_free.
|
|
||||||
///
|
|
||||||
/// ```c
|
|
||||||
/// EXTERN_C struct TCReplica *tc_replica_new_in_memory(void);
|
|
||||||
/// ```
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn tc_replica_new_in_memory() -> *mut TCReplica {
|
|
||||||
let storage = StorageConfig::InMemory
|
|
||||||
.into_storage()
|
|
||||||
.expect("in-memory always succeeds");
|
|
||||||
// SAFETY:
|
|
||||||
// - caller promises to free this value
|
|
||||||
unsafe { TCReplica::from(Replica::new(storage)).return_ptr() }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[ffizz_header::item]
|
|
||||||
#[ffizz(order = 901)]
|
|
||||||
/// Create a new TCReplica with an on-disk database having the given filename. On error, a string
|
|
||||||
/// is written to the error_out parameter (if it is not NULL) and NULL is returned. The caller
|
|
||||||
/// must free this string.
|
|
||||||
///
|
|
||||||
/// ```c
|
|
||||||
/// EXTERN_C struct TCReplica *tc_replica_new_on_disk(struct TCString path,
|
|
||||||
/// bool create_if_missing,
|
|
||||||
/// struct TCString *error_out);
|
|
||||||
/// ```
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn tc_replica_new_on_disk(
|
|
||||||
path: TCString,
|
|
||||||
create_if_missing: bool,
|
|
||||||
error_out: *mut TCString,
|
|
||||||
) -> *mut TCReplica {
|
|
||||||
wrap_constructor(
|
|
||||||
|| {
|
|
||||||
// SAFETY:
|
|
||||||
// - path is valid (promised by caller)
|
|
||||||
// - caller will not use path after this call (convention)
|
|
||||||
let mut path = unsafe { TCString::val_from_arg(path) };
|
|
||||||
let storage = StorageConfig::OnDisk {
|
|
||||||
taskdb_dir: path.to_path_buf_mut()?,
|
|
||||||
create_if_missing,
|
|
||||||
}
|
|
||||||
.into_storage()?;
|
|
||||||
|
|
||||||
// SAFETY:
|
|
||||||
// - caller promises to free this value
|
|
||||||
Ok(unsafe { TCReplica::from(Replica::new(storage)).return_ptr() })
|
|
||||||
},
|
|
||||||
error_out,
|
|
||||||
std::ptr::null_mut(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[ffizz_header::item]
|
|
||||||
#[ffizz(order = 901)]
|
|
||||||
/// ***** TCReplicaOp *****
|
|
||||||
///
|
|
||||||
/// ```c
|
|
||||||
/// struct TCReplicaOp {
|
|
||||||
/// TCReplicaOpType operation_type;
|
|
||||||
/// void* inner;
|
|
||||||
/// };
|
|
||||||
///
|
|
||||||
/// typedef struct TCReplicaOp TCReplicaOp;
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug)]
|
|
||||||
#[repr(C)]
|
|
||||||
pub struct TCReplicaOp {
|
|
||||||
operation_type: TCReplicaOpType,
|
|
||||||
inner: Box<ReplicaOp>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<ReplicaOp> for TCReplicaOp {
|
|
||||||
fn from(replica_op: ReplicaOp) -> TCReplicaOp {
|
|
||||||
match replica_op {
|
|
||||||
ReplicaOp::Create { .. } => TCReplicaOp {
|
|
||||||
operation_type: TCReplicaOpType::Create,
|
|
||||||
inner: Box::new(replica_op),
|
|
||||||
},
|
|
||||||
ReplicaOp::Delete { .. } => TCReplicaOp {
|
|
||||||
operation_type: TCReplicaOpType::Delete,
|
|
||||||
inner: Box::new(replica_op),
|
|
||||||
},
|
|
||||||
ReplicaOp::Update { .. } => TCReplicaOp {
|
|
||||||
operation_type: TCReplicaOpType::Update,
|
|
||||||
inner: Box::new(replica_op),
|
|
||||||
},
|
|
||||||
ReplicaOp::UndoPoint => TCReplicaOp {
|
|
||||||
operation_type: TCReplicaOpType::UndoPoint,
|
|
||||||
inner: Box::new(replica_op),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[ffizz_header::item]
|
|
||||||
#[ffizz(order = 901)]
|
|
||||||
/// ***** TCReplicaOpList *****
|
|
||||||
///
|
|
||||||
/// ```c
|
|
||||||
/// struct TCReplicaOpList {
|
|
||||||
/// struct TCReplicaOp *items;
|
|
||||||
/// size_t len;
|
|
||||||
/// size_t capacity;
|
|
||||||
/// };
|
|
||||||
///
|
|
||||||
/// typedef struct TCReplicaOpList TCReplicaOpList;
|
|
||||||
/// ```
|
|
||||||
#[repr(C)]
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct TCReplicaOpList {
|
|
||||||
items: *mut TCReplicaOp,
|
|
||||||
len: usize,
|
|
||||||
capacity: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for TCReplicaOpList {
|
|
||||||
fn default() -> Self {
|
|
||||||
// SAFETY:
|
|
||||||
// - caller will free this value
|
|
||||||
unsafe { TCReplicaOpList::return_val(Vec::new()) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CList for TCReplicaOpList {
|
|
||||||
type Element = TCReplicaOp;
|
|
||||||
|
|
||||||
unsafe fn from_raw_parts(items: *mut Self::Element, len: usize, cap: usize) -> Self {
|
|
||||||
TCReplicaOpList {
|
|
||||||
len,
|
|
||||||
capacity: cap,
|
|
||||||
items,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn slice(&mut self) -> &mut [Self::Element] {
|
|
||||||
// SAFETY:
|
|
||||||
// - because we have &mut self, we have read/write access to items[0..len]
|
|
||||||
// - all items are properly initialized Element's
|
|
||||||
// - return value lifetime is equal to &mmut self's, so access is exclusive
|
|
||||||
// - items and len came from Vec, so total size is < isize::MAX
|
|
||||||
unsafe { std::slice::from_raw_parts_mut(self.items, self.len) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn into_raw_parts(self) -> (*mut Self::Element, usize, usize) {
|
|
||||||
(self.items, self.len, self.capacity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[ffizz_header::item]
|
|
||||||
#[ffizz(order = 902)]
|
|
||||||
/// Get a list of all tasks in the replica.
|
|
||||||
///
|
|
||||||
/// Returns a TCTaskList with a NULL items field on error.
|
|
||||||
///
|
|
||||||
/// ```c
|
|
||||||
/// EXTERN_C struct TCTaskList tc_replica_all_tasks(struct TCReplica *rep);
|
|
||||||
/// ```
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn tc_replica_all_tasks(rep: *mut TCReplica) -> TCTaskList {
|
|
||||||
wrap(
|
|
||||||
rep,
|
|
||||||
|rep| {
|
|
||||||
// note that the Replica API returns a hashmap here, but we discard
|
|
||||||
// the keys and return a simple list. The task UUIDs are available
|
|
||||||
// from task.get_uuid(), so information is not lost.
|
|
||||||
let tasks: Vec<_> = rep
|
|
||||||
.all_tasks()?
|
|
||||||
.drain()
|
|
||||||
.map(|(_uuid, t)| {
|
|
||||||
Some(
|
|
||||||
NonNull::new(
|
|
||||||
// SAFETY:
|
|
||||||
// - caller promises to free this value (via freeing the list)
|
|
||||||
unsafe { TCTask::from(t).return_ptr() },
|
|
||||||
)
|
|
||||||
.expect("TCTask::return_ptr returned NULL"),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
// SAFETY:
|
|
||||||
// - value is not allocated and need not be freed
|
|
||||||
Ok(unsafe { TCTaskList::return_val(tasks) })
|
|
||||||
},
|
|
||||||
TCTaskList::null_value(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[ffizz_header::item]
|
|
||||||
#[ffizz(order = 902)]
|
|
||||||
/// Get a list of all uuids for tasks in the replica.
|
|
||||||
///
|
|
||||||
/// Returns a TCUuidList with a NULL items field on error.
|
|
||||||
///
|
|
||||||
/// The caller must free the UUID list with `tc_uuid_list_free`.
|
|
||||||
///
|
|
||||||
/// ```c
|
|
||||||
/// EXTERN_C struct TCUuidList tc_replica_all_task_uuids(struct TCReplica *rep);
|
|
||||||
/// ```
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn tc_replica_all_task_uuids(rep: *mut TCReplica) -> TCUuidList {
|
|
||||||
wrap(
|
|
||||||
rep,
|
|
||||||
|rep| {
|
|
||||||
let uuids: Vec<_> = rep
|
|
||||||
.all_task_uuids()?
|
|
||||||
.drain(..)
|
|
||||||
// SAFETY:
|
|
||||||
// - value is not allocated and need not be freed
|
|
||||||
.map(|uuid| unsafe { TCUuid::return_val(uuid) })
|
|
||||||
.collect();
|
|
||||||
// SAFETY:
|
|
||||||
// - value will be freed (promised by caller)
|
|
||||||
Ok(unsafe { TCUuidList::return_val(uuids) })
|
|
||||||
},
|
|
||||||
TCUuidList::null_value(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[ffizz_header::item]
|
|
||||||
#[ffizz(order = 902)]
|
|
||||||
/// Get the current working set for this replica. The resulting value must be freed
|
|
||||||
/// with tc_working_set_free.
|
|
||||||
///
|
|
||||||
/// Returns NULL on error.
|
|
||||||
///
|
|
||||||
/// ```c
|
|
||||||
/// EXTERN_C struct TCWorkingSet *tc_replica_working_set(struct TCReplica *rep);
|
|
||||||
/// ```
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn tc_replica_working_set(rep: *mut TCReplica) -> *mut TCWorkingSet {
|
|
||||||
wrap(
|
|
||||||
rep,
|
|
||||||
|rep| {
|
|
||||||
let ws = rep.working_set()?;
|
|
||||||
// SAFETY:
|
|
||||||
// - caller promises to free this value
|
|
||||||
Ok(unsafe { TCWorkingSet::return_ptr(ws.into()) })
|
|
||||||
},
|
|
||||||
std::ptr::null_mut(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[ffizz_header::item]
|
|
||||||
#[ffizz(order = 902)]
|
|
||||||
/// Get an existing task by its UUID.
|
|
||||||
///
|
|
||||||
/// Returns NULL when the task does not exist, and on error. Consult tc_replica_error
|
|
||||||
/// to distinguish the two conditions.
|
|
||||||
///
|
|
||||||
/// ```c
|
|
||||||
/// EXTERN_C struct TCTask *tc_replica_get_task(struct TCReplica *rep, struct TCUuid tcuuid);
|
|
||||||
/// ```
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn tc_replica_get_task(rep: *mut TCReplica, tcuuid: TCUuid) -> *mut TCTask {
|
|
||||||
wrap(
|
|
||||||
rep,
|
|
||||||
|rep| {
|
|
||||||
// SAFETY:
|
|
||||||
// - tcuuid is a valid TCUuid (all bytes are valid)
|
|
||||||
// - tcuuid is Copy so ownership doesn't matter
|
|
||||||
let uuid = unsafe { TCUuid::val_from_arg(tcuuid) };
|
|
||||||
if let Some(task) = rep.get_task(uuid)? {
|
|
||||||
// SAFETY:
|
|
||||||
// - caller promises to free this task
|
|
||||||
Ok(unsafe { TCTask::from(task).return_ptr() })
|
|
||||||
} else {
|
|
||||||
Ok(std::ptr::null_mut())
|
|
||||||
}
|
|
||||||
},
|
|
||||||
std::ptr::null_mut(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[ffizz_header::item]
|
|
||||||
#[ffizz(order = 902)]
|
|
||||||
/// Create a new task. The task must not already exist.
|
|
||||||
///
|
|
||||||
/// Returns the task, or NULL on error.
|
|
||||||
///
|
|
||||||
/// ```c
|
|
||||||
/// EXTERN_C struct TCTask *tc_replica_new_task(struct TCReplica *rep,
|
|
||||||
/// enum TCStatus status,
|
|
||||||
/// struct TCString description);
|
|
||||||
/// ```
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn tc_replica_new_task(
|
|
||||||
rep: *mut TCReplica,
|
|
||||||
status: TCStatus,
|
|
||||||
description: TCString,
|
|
||||||
) -> *mut TCTask {
|
|
||||||
// SAFETY:
|
|
||||||
// - description is valid (promised by caller)
|
|
||||||
// - caller will not use description after this call (convention)
|
|
||||||
let mut description = unsafe { TCString::val_from_arg(description) };
|
|
||||||
wrap(
|
|
||||||
rep,
|
|
||||||
|rep| {
|
|
||||||
let task = rep.new_task(status.into(), description.as_str()?.to_string())?;
|
|
||||||
// SAFETY:
|
|
||||||
// - caller promises to free this task
|
|
||||||
Ok(unsafe { TCTask::from(task).return_ptr() })
|
|
||||||
},
|
|
||||||
std::ptr::null_mut(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[ffizz_header::item]
|
|
||||||
#[ffizz(order = 902)]
|
|
||||||
/// Create a new task. The task must not already exist.
|
|
||||||
///
|
|
||||||
/// Returns the task, or NULL on error.
|
|
||||||
///
|
|
||||||
/// ```c
|
|
||||||
/// EXTERN_C struct TCTask *tc_replica_import_task_with_uuid(struct TCReplica *rep, struct TCUuid tcuuid);
|
|
||||||
/// ```
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn tc_replica_import_task_with_uuid(
|
|
||||||
rep: *mut TCReplica,
|
|
||||||
tcuuid: TCUuid,
|
|
||||||
) -> *mut TCTask {
|
|
||||||
wrap(
|
|
||||||
rep,
|
|
||||||
|rep| {
|
|
||||||
// SAFETY:
|
|
||||||
// - tcuuid is a valid TCUuid (all bytes are valid)
|
|
||||||
// - tcuuid is Copy so ownership doesn't matter
|
|
||||||
let uuid = unsafe { TCUuid::val_from_arg(tcuuid) };
|
|
||||||
let task = rep.import_task_with_uuid(uuid)?;
|
|
||||||
// SAFETY:
|
|
||||||
// - caller promises to free this task
|
|
||||||
Ok(unsafe { TCTask::from(task).return_ptr() })
|
|
||||||
},
|
|
||||||
std::ptr::null_mut(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[ffizz_header::item]
|
|
||||||
#[ffizz(order = 902)]
|
|
||||||
/// Delete a task. The task must exist. Note that this is different from setting status to
|
|
||||||
/// Deleted; this is the final purge of the task.
|
|
||||||
///
|
|
||||||
/// Deletion may interact poorly with modifications to the same task on other replicas. For
|
|
||||||
/// example, if a task is deleted on replica 1 and its description modified on replica 2, then
|
|
||||||
/// after both replicas have fully synced, the resulting task will only have a `description`
|
|
||||||
/// property.
|
|
||||||
///
|
|
||||||
/// ```c
|
|
||||||
/// EXTERN_C TCResult tc_replica_delete_task(struct TCReplica *rep, struct TCUuid tcuuid);
|
|
||||||
/// ```
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn tc_replica_delete_task(rep: *mut TCReplica, tcuuid: TCUuid) -> TCResult {
|
|
||||||
wrap(
|
|
||||||
rep,
|
|
||||||
|rep| {
|
|
||||||
// SAFETY:
|
|
||||||
// - tcuuid is a valid TCUuid (all bytes are valid)
|
|
||||||
// - tcuuid is Copy so ownership doesn't matter
|
|
||||||
let uuid = unsafe { TCUuid::val_from_arg(tcuuid) };
|
|
||||||
rep.delete_task(uuid)?;
|
|
||||||
Ok(TCResult::Ok)
|
|
||||||
},
|
|
||||||
TCResult::Error,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[ffizz_header::item]
|
|
||||||
#[ffizz(order = 902)]
|
|
||||||
/// Synchronize this replica with a server.
|
|
||||||
///
|
|
||||||
/// The `server` argument remains owned by the caller, and must be freed explicitly.
|
|
||||||
///
|
|
||||||
/// ```c
|
|
||||||
/// EXTERN_C TCResult tc_replica_sync(struct TCReplica *rep, struct TCServer *server, bool avoid_snapshots);
|
|
||||||
/// ```
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn tc_replica_sync(
|
|
||||||
rep: *mut TCReplica,
|
|
||||||
server: *mut TCServer,
|
|
||||||
avoid_snapshots: bool,
|
|
||||||
) -> TCResult {
|
|
||||||
wrap(
|
|
||||||
rep,
|
|
||||||
|rep| {
|
|
||||||
debug_assert!(!server.is_null());
|
|
||||||
// SAFETY:
|
|
||||||
// - server is not NULL
|
|
||||||
// - *server is a valid TCServer (promised by caller)
|
|
||||||
// - server is valid for the lifetime of tc_replica_sync (not threadsafe)
|
|
||||||
// - server will not be accessed simultaneously (not threadsafe)
|
|
||||||
let server = unsafe { TCServer::from_ptr_arg_ref_mut(server) };
|
|
||||||
rep.sync(server.as_mut(), avoid_snapshots)?;
|
|
||||||
Ok(TCResult::Ok)
|
|
||||||
},
|
|
||||||
TCResult::Error,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[ffizz_header::item]
|
|
||||||
#[ffizz(order = 902)]
|
|
||||||
/// Expire old, deleted tasks.
|
|
||||||
///
|
|
||||||
/// Expiration entails removal of tasks from the replica. Any modifications that occur after
|
|
||||||
/// the deletion (such as operations synchronized from other replicas) will do nothing.
|
|
||||||
///
|
|
||||||
/// Tasks are eligible for expiration when they have status Deleted and have not been modified
|
|
||||||
/// for 180 days (about six months). Note that completed tasks are not eligible.
|
|
||||||
///
|
|
||||||
/// ```c
|
|
||||||
/// EXTERN_C TCResult tc_replica_expire_tasks(struct TCReplica *rep);
|
|
||||||
/// ```
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn tc_replica_expire_tasks(rep: *mut TCReplica) -> TCResult {
|
|
||||||
wrap(
|
|
||||||
rep,
|
|
||||||
|rep| {
|
|
||||||
rep.expire_tasks()?;
|
|
||||||
Ok(TCResult::Ok)
|
|
||||||
},
|
|
||||||
TCResult::Error,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[ffizz_header::item]
|
|
||||||
#[ffizz(order = 902)]
|
|
||||||
/// Return undo local operations until the most recent UndoPoint.
|
|
||||||
///
|
|
||||||
/// ```c
|
|
||||||
/// EXTERN_C TCReplicaOpList tc_replica_get_undo_ops(struct TCReplica *rep);
|
|
||||||
/// ```
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn tc_replica_get_undo_ops(rep: *mut TCReplica) -> TCReplicaOpList {
|
|
||||||
wrap(
|
|
||||||
rep,
|
|
||||||
|rep| {
|
|
||||||
// SAFETY:
|
|
||||||
// - caller will free this value, either with tc_replica_commit_undo_ops or
|
|
||||||
// tc_replica_op_list_free.
|
|
||||||
Ok(unsafe {
|
|
||||||
TCReplicaOpList::return_val(
|
|
||||||
rep.get_undo_ops()?
|
|
||||||
.into_iter()
|
|
||||||
.map(TCReplicaOp::from)
|
|
||||||
.collect(),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
TCReplicaOpList::default(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[ffizz_header::item]
|
|
||||||
#[ffizz(order = 902)]
|
|
||||||
/// Undo local operations in storage.
|
|
||||||
///
|
|
||||||
/// If undone_out is not NULL, then on success it is set to 1 if operations were undone, or 0 if
|
|
||||||
/// there are no operations that can be done.
|
|
||||||
///
|
|
||||||
/// ```c
|
|
||||||
/// EXTERN_C TCResult tc_replica_commit_undo_ops(struct TCReplica *rep, TCReplicaOpList tc_undo_ops, int32_t *undone_out);
|
|
||||||
/// ```
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn tc_replica_commit_undo_ops(
|
|
||||||
rep: *mut TCReplica,
|
|
||||||
tc_undo_ops: TCReplicaOpList,
|
|
||||||
undone_out: *mut i32,
|
|
||||||
) -> TCResult {
|
|
||||||
wrap(
|
|
||||||
rep,
|
|
||||||
|rep| {
|
|
||||||
// SAFETY:
|
|
||||||
// - `tc_undo_ops` is a valid value, as it was acquired from `tc_replica_get_undo_ops`.
|
|
||||||
let undo_ops: Vec<ReplicaOp> = unsafe { TCReplicaOpList::val_from_arg(tc_undo_ops) }
|
|
||||||
.into_iter()
|
|
||||||
.map(|op| *op.inner)
|
|
||||||
.collect();
|
|
||||||
let undone = i32::from(rep.commit_undo_ops(undo_ops)?);
|
|
||||||
if !undone_out.is_null() {
|
|
||||||
// SAFETY:
|
|
||||||
// - undone_out is not NULL (just checked)
|
|
||||||
// - undone_out is properly aligned (implicitly promised by caller)
|
|
||||||
unsafe { *undone_out = undone };
|
|
||||||
}
|
|
||||||
Ok(TCResult::Ok)
|
|
||||||
},
|
|
||||||
TCResult::Error,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[ffizz_header::item]
|
|
||||||
#[ffizz(order = 902)]
|
|
||||||
/// Get the number of local, un-synchronized operations (not including undo points), or -1 on
|
|
||||||
/// error.
|
|
||||||
///
|
|
||||||
/// ```c
|
|
||||||
/// EXTERN_C int64_t tc_replica_num_local_operations(struct TCReplica *rep);
|
|
||||||
/// ```
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn tc_replica_num_local_operations(rep: *mut TCReplica) -> i64 {
|
|
||||||
wrap(
|
|
||||||
rep,
|
|
||||||
|rep| {
|
|
||||||
let count = rep.num_local_operations()? as i64;
|
|
||||||
Ok(count)
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[ffizz_header::item]
|
|
||||||
#[ffizz(order = 902)]
|
|
||||||
/// Get the number of undo points (number of undo calls possible), or -1 on error.
|
|
||||||
///
|
|
||||||
/// ```c
|
|
||||||
/// EXTERN_C int64_t tc_replica_num_undo_points(struct TCReplica *rep);
|
|
||||||
/// ```
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn tc_replica_num_undo_points(rep: *mut TCReplica) -> i64 {
|
|
||||||
wrap(
|
|
||||||
rep,
|
|
||||||
|rep| {
|
|
||||||
let count = rep.num_undo_points()? as i64;
|
|
||||||
Ok(count)
|
|
||||||
},
|
|
||||||
-1,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[ffizz_header::item]
|
|
||||||
#[ffizz(order = 902)]
|
|
||||||
/// Add an UndoPoint, if one has not already been added by this Replica. This occurs automatically
|
|
||||||
/// when a change is made. The `force` flag allows forcing a new UndoPoint even if one has already
|
|
||||||
/// been created by this Replica, and may be useful when a Replica instance is held for a long time
|
|
||||||
/// and used to apply more than one user-visible change.
|
|
||||||
///
|
|
||||||
/// ```c
|
|
||||||
/// EXTERN_C TCResult tc_replica_add_undo_point(struct TCReplica *rep, bool force);
|
|
||||||
/// ```
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn tc_replica_add_undo_point(rep: *mut TCReplica, force: bool) -> TCResult {
|
|
||||||
wrap(
|
|
||||||
rep,
|
|
||||||
|rep| {
|
|
||||||
rep.add_undo_point(force)?;
|
|
||||||
Ok(TCResult::Ok)
|
|
||||||
},
|
|
||||||
TCResult::Error,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[ffizz_header::item]
|
|
||||||
#[ffizz(order = 902)]
|
|
||||||
/// Rebuild this replica's working set, based on whether tasks are pending or not. If `renumber`
|
|
||||||
/// is true, then existing tasks may be moved to new working-set indices; in any case, on
|
|
||||||
/// completion all pending tasks are in the working set and all non- pending tasks are not.
|
|
||||||
///
|
|
||||||
/// ```c
|
|
||||||
/// EXTERN_C TCResult tc_replica_rebuild_working_set(struct TCReplica *rep, bool renumber);
|
|
||||||
/// ```
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn tc_replica_rebuild_working_set(
|
|
||||||
rep: *mut TCReplica,
|
|
||||||
renumber: bool,
|
|
||||||
) -> TCResult {
|
|
||||||
wrap(
|
|
||||||
rep,
|
|
||||||
|rep| {
|
|
||||||
rep.rebuild_working_set(renumber)?;
|
|
||||||
Ok(TCResult::Ok)
|
|
||||||
},
|
|
||||||
TCResult::Error,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[ffizz_header::item]
|
|
||||||
#[ffizz(order = 902)]
|
|
||||||
/// Get the latest error for a replica, or a string with NULL ptr if no error exists. Subsequent
|
|
||||||
/// calls to this function will return NULL. The rep pointer must not be NULL. The caller must
|
|
||||||
/// free the returned string.
|
|
||||||
///
|
|
||||||
/// ```c
|
|
||||||
/// EXTERN_C struct TCString tc_replica_error(struct TCReplica *rep);
|
|
||||||
/// ```
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn tc_replica_error(rep: *mut TCReplica) -> TCString {
|
|
||||||
// SAFETY:
|
|
||||||
// - rep is not NULL (promised by caller)
|
|
||||||
// - *rep is a valid TCReplica (promised by caller)
|
|
||||||
// - rep is valid for the duration of this function
|
|
||||||
// - rep is not modified by anything else (not threadsafe)
|
|
||||||
let rep: &mut TCReplica = unsafe { TCReplica::from_ptr_arg_ref_mut(rep) };
|
|
||||||
if let Some(rstring) = rep.error.take() {
|
|
||||||
// SAFETY:
|
|
||||||
// - caller promises to free this string
|
|
||||||
unsafe { TCString::return_val(rstring) }
|
|
||||||
} else {
|
|
||||||
TCString::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[ffizz_header::item]
|
|
||||||
#[ffizz(order = 903)]
|
|
||||||
/// Free a replica. The replica may not be used after this function returns and must not be freed
|
|
||||||
/// more than once.
|
|
||||||
///
|
|
||||||
/// ```c
|
|
||||||
/// EXTERN_C void tc_replica_free(struct TCReplica *rep);
|
|
||||||
/// ```
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn tc_replica_free(rep: *mut TCReplica) {
|
|
||||||
// SAFETY:
|
|
||||||
// - replica is not NULL (promised by caller)
|
|
||||||
// - replica is valid (promised by caller)
|
|
||||||
// - caller will not use description after this call (promised by caller)
|
|
||||||
let replica = unsafe { TCReplica::take_from_ptr_arg(rep) };
|
|
||||||
if replica.mut_borrowed {
|
|
||||||
panic!("replica is borrowed and cannot be freed");
|
|
||||||
}
|
|
||||||
drop(replica);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[ffizz_header::item]
|
|
||||||
#[ffizz(order = 903)]
|
|
||||||
/// Free a vector of ReplicaOp. The vector may not be used after this function returns and must not be freed
|
|
||||||
/// more than once.
|
|
||||||
///
|
|
||||||
/// ```c
|
|
||||||
/// EXTERN_C void tc_replica_op_list_free(struct TCReplicaOpList *oplist);
|
|
||||||
/// ```
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn tc_replica_op_list_free(oplist: *mut TCReplicaOpList) {
|
|
||||||
debug_assert!(!oplist.is_null());
|
|
||||||
// SAFETY:
|
|
||||||
// - arg is not NULL (just checked)
|
|
||||||
// - `*oplist` is valid (guaranteed by caller not double-freeing this value)
|
|
||||||
unsafe {
|
|
||||||
TCReplicaOpList::take_val_from_arg(
|
|
||||||
oplist,
|
|
||||||
// SAFETY:
|
|
||||||
// - value is empty, so the caller need not free it.
|
|
||||||
TCReplicaOpList::return_val(Vec::new()),
|
|
||||||
)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
#[ffizz_header::item]
|
|
||||||
#[ffizz(order = 903)]
|
|
||||||
/// Return uuid field of ReplicaOp.
|
|
||||||
///
|
|
||||||
/// ```c
|
|
||||||
/// EXTERN_C struct TCString tc_replica_op_get_uuid(struct TCReplicaOp *op);
|
|
||||||
/// ```
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn tc_replica_op_get_uuid(op: *const TCReplicaOp) -> TCString {
|
|
||||||
// SAFETY:
|
|
||||||
// - inner is not null
|
|
||||||
// - inner is a living object
|
|
||||||
let rop: &ReplicaOp = unsafe { (*op).inner.as_ref() };
|
|
||||||
|
|
||||||
if let ReplicaOp::Create { uuid }
|
|
||||||
| ReplicaOp::Delete { uuid, .. }
|
|
||||||
| ReplicaOp::Update { uuid, .. } = rop
|
|
||||||
{
|
|
||||||
let uuid_rstr: RustString = uuid.to_string().into();
|
|
||||||
// SAFETY:
|
|
||||||
// - caller promises to free this string
|
|
||||||
unsafe { TCString::return_val(uuid_rstr) }
|
|
||||||
} else {
|
|
||||||
panic!("Operation has no uuid: {:#?}", rop);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[ffizz_header::item]
|
|
||||||
#[ffizz(order = 903)]
|
|
||||||
/// Return property field of ReplicaOp.
|
|
||||||
///
|
|
||||||
/// ```c
|
|
||||||
/// EXTERN_C struct TCString tc_replica_op_get_property(struct TCReplicaOp *op);
|
|
||||||
/// ```
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn tc_replica_op_get_property(op: *const TCReplicaOp) -> TCString {
|
|
||||||
// SAFETY:
|
|
||||||
// - inner is not null
|
|
||||||
// - inner is a living object
|
|
||||||
let rop: &ReplicaOp = unsafe { (*op).inner.as_ref() };
|
|
||||||
|
|
||||||
if let ReplicaOp::Update { property, .. } = rop {
|
|
||||||
// SAFETY:
|
|
||||||
// - caller promises to free this string
|
|
||||||
unsafe { TCString::return_val(property.clone().into()) }
|
|
||||||
} else {
|
|
||||||
panic!("Operation has no property: {:#?}", rop);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[ffizz_header::item]
|
|
||||||
#[ffizz(order = 903)]
|
|
||||||
/// Return value field of ReplicaOp.
|
|
||||||
///
|
|
||||||
/// ```c
|
|
||||||
/// EXTERN_C struct TCString tc_replica_op_get_value(struct TCReplicaOp *op);
|
|
||||||
/// ```
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn tc_replica_op_get_value(op: *const TCReplicaOp) -> TCString {
|
|
||||||
// SAFETY:
|
|
||||||
// - inner is not null
|
|
||||||
// - inner is a living object
|
|
||||||
let rop: &ReplicaOp = unsafe { (*op).inner.as_ref() };
|
|
||||||
|
|
||||||
if let ReplicaOp::Update { value, .. } = rop {
|
|
||||||
let value_rstr: RustString = value.clone().unwrap_or(String::new()).into();
|
|
||||||
// SAFETY:
|
|
||||||
// - caller promises to free this string
|
|
||||||
unsafe { TCString::return_val(value_rstr) }
|
|
||||||
} else {
|
|
||||||
panic!("Operation has no value: {:#?}", rop);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[ffizz_header::item]
|
|
||||||
#[ffizz(order = 903)]
|
|
||||||
/// Return old value field of ReplicaOp.
|
|
||||||
///
|
|
||||||
/// ```c
|
|
||||||
/// EXTERN_C struct TCString tc_replica_op_get_old_value(struct TCReplicaOp *op);
|
|
||||||
/// ```
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn tc_replica_op_get_old_value(op: *const TCReplicaOp) -> TCString {
|
|
||||||
// SAFETY:
|
|
||||||
// - inner is not null
|
|
||||||
// - inner is a living object
|
|
||||||
let rop: &ReplicaOp = unsafe { (*op).inner.as_ref() };
|
|
||||||
|
|
||||||
if let ReplicaOp::Update { old_value, .. } = rop {
|
|
||||||
let old_value_rstr: RustString = old_value.clone().unwrap_or(String::new()).into();
|
|
||||||
// SAFETY:
|
|
||||||
// - caller promises to free this string
|
|
||||||
unsafe { TCString::return_val(old_value_rstr) }
|
|
||||||
} else {
|
|
||||||
panic!("Operation has no old value: {:#?}", rop);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[ffizz_header::item]
|
|
||||||
#[ffizz(order = 903)]
|
|
||||||
/// Return timestamp field of ReplicaOp.
|
|
||||||
///
|
|
||||||
/// ```c
|
|
||||||
/// EXTERN_C struct TCString tc_replica_op_get_timestamp(struct TCReplicaOp *op);
|
|
||||||
/// ```
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn tc_replica_op_get_timestamp(op: *const TCReplicaOp) -> TCString {
|
|
||||||
// SAFETY:
|
|
||||||
// - inner is not null
|
|
||||||
// - inner is a living object
|
|
||||||
let rop: &ReplicaOp = unsafe { (*op).inner.as_ref() };
|
|
||||||
|
|
||||||
if let ReplicaOp::Update { timestamp, .. } = rop {
|
|
||||||
let timestamp_rstr: RustString = timestamp.to_string().into();
|
|
||||||
// SAFETY:
|
|
||||||
// - caller promises to free this string
|
|
||||||
unsafe { TCString::return_val(timestamp_rstr) }
|
|
||||||
} else {
|
|
||||||
panic!("Operation has no timestamp: {:#?}", rop);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[ffizz_header::item]
|
|
||||||
#[ffizz(order = 903)]
|
|
||||||
/// Return description field of old task field of ReplicaOp.
|
|
||||||
///
|
|
||||||
/// ```c
|
|
||||||
/// EXTERN_C struct TCString tc_replica_op_get_old_task_description(struct TCReplicaOp *op);
|
|
||||||
/// ```
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn tc_replica_op_get_old_task_description(
|
|
||||||
op: *const TCReplicaOp,
|
|
||||||
) -> TCString {
|
|
||||||
// SAFETY:
|
|
||||||
// - inner is not null
|
|
||||||
// - inner is a living object
|
|
||||||
let rop: &ReplicaOp = unsafe { (*op).inner.as_ref() };
|
|
||||||
|
|
||||||
if let ReplicaOp::Delete { old_task, .. } = rop {
|
|
||||||
let description_rstr: RustString = old_task["description"].clone().into();
|
|
||||||
// SAFETY:
|
|
||||||
// - caller promises to free this string
|
|
||||||
unsafe { TCString::return_val(description_rstr) }
|
|
||||||
} else {
|
|
||||||
panic!("Operation has no timestamp: {:#?}", rop);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
#[ffizz_header::item]
|
|
||||||
#[ffizz(order = 100)]
|
|
||||||
/// ***** TCResult *****
|
|
||||||
///
|
|
||||||
/// A result from a TC operation. Typically if this value is TC_RESULT_ERROR,
|
|
||||||
/// the associated object's `tc_.._error` method will return an error message.
|
|
||||||
///
|
|
||||||
/// ```c
|
|
||||||
/// enum TCResult
|
|
||||||
/// #ifdef __cplusplus
|
|
||||||
/// : int32_t
|
|
||||||
/// #endif // __cplusplus
|
|
||||||
/// {
|
|
||||||
/// TC_RESULT_ERROR = -1,
|
|
||||||
/// TC_RESULT_OK = 0,
|
|
||||||
/// };
|
|
||||||
/// #ifndef __cplusplus
|
|
||||||
/// typedef int32_t TCResult;
|
|
||||||
/// #endif // __cplusplus
|
|
||||||
/// ```
|
|
||||||
#[repr(i32)]
|
|
||||||
pub enum TCResult {
|
|
||||||
Error = -1,
|
|
||||||
Ok = 0,
|
|
||||||
}
|
|
||||||
@@ -1,234 +0,0 @@
|
|||||||
use crate::traits::*;
|
|
||||||
use crate::types::*;
|
|
||||||
use crate::util::err_to_ruststring;
|
|
||||||
use taskchampion::{Server, ServerConfig};
|
|
||||||
|
|
||||||
#[ffizz_header::item]
|
|
||||||
#[ffizz(order = 800)]
|
|
||||||
/// ***** TCServer *****
|
|
||||||
///
|
|
||||||
/// TCServer represents an interface to a sync server. Aside from new and free, a server
|
|
||||||
/// has no C-accessible API, but is designed to be passed to `tc_replica_sync`.
|
|
||||||
///
|
|
||||||
/// ## Safety
|
|
||||||
///
|
|
||||||
/// TCServer are not threadsafe, and must not be used with multiple replicas simultaneously.
|
|
||||||
///
|
|
||||||
/// ```c
|
|
||||||
/// typedef struct TCServer TCServer;
|
|
||||||
/// ```
|
|
||||||
pub struct TCServer(Box<dyn Server>);
|
|
||||||
|
|
||||||
impl PassByPointer for TCServer {}
|
|
||||||
|
|
||||||
impl From<Box<dyn Server>> for TCServer {
|
|
||||||
fn from(server: Box<dyn Server>) -> TCServer {
|
|
||||||
TCServer(server)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AsMut<Box<dyn Server>> for TCServer {
|
|
||||||
fn as_mut(&mut self) -> &mut Box<dyn Server> {
|
|
||||||
&mut self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Utility function to allow using `?` notation to return an error value.
|
|
||||||
fn wrap<T, F>(f: F, error_out: *mut TCString, err_value: T) -> T
|
|
||||||
where
|
|
||||||
F: FnOnce() -> anyhow::Result<T>,
|
|
||||||
{
|
|
||||||
if !error_out.is_null() {
|
|
||||||
// SAFETY:
|
|
||||||
// - error_out is not NULL (just checked)
|
|
||||||
// - properly aligned and valid (promised by caller)
|
|
||||||
unsafe { *error_out = TCString::default() };
|
|
||||||
}
|
|
||||||
|
|
||||||
match f() {
|
|
||||||
Ok(v) => v,
|
|
||||||
Err(e) => {
|
|
||||||
if !error_out.is_null() {
|
|
||||||
// SAFETY:
|
|
||||||
// - error_out is not NULL (just checked)
|
|
||||||
// - properly aligned and valid (promised by caller)
|
|
||||||
unsafe {
|
|
||||||
TCString::val_to_arg_out(err_to_ruststring(e), error_out);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
err_value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[ffizz_header::item]
|
|
||||||
#[ffizz(order = 801)]
|
|
||||||
/// Create a new TCServer that operates locally (on-disk). See the TaskChampion docs for the
|
|
||||||
/// description of the arguments.
|
|
||||||
///
|
|
||||||
/// On error, a string is written to the error_out parameter (if it is not NULL) and NULL is
|
|
||||||
/// returned. The caller must free this string.
|
|
||||||
///
|
|
||||||
/// The server must be freed after it is used - tc_replica_sync does not automatically free it.
|
|
||||||
///
|
|
||||||
/// ```c
|
|
||||||
/// EXTERN_C struct TCServer *tc_server_new_local(struct TCString server_dir, struct TCString *error_out);
|
|
||||||
/// ```
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn tc_server_new_local(
|
|
||||||
server_dir: TCString,
|
|
||||||
error_out: *mut TCString,
|
|
||||||
) -> *mut TCServer {
|
|
||||||
wrap(
|
|
||||||
|| {
|
|
||||||
// SAFETY:
|
|
||||||
// - server_dir is valid (promised by caller)
|
|
||||||
// - caller will not use server_dir after this call (convention)
|
|
||||||
let mut server_dir = unsafe { TCString::val_from_arg(server_dir) };
|
|
||||||
let server_config = ServerConfig::Local {
|
|
||||||
server_dir: server_dir.to_path_buf_mut()?,
|
|
||||||
};
|
|
||||||
let server = server_config.into_server()?;
|
|
||||||
// SAFETY: caller promises to free this server.
|
|
||||||
Ok(unsafe { TCServer::return_ptr(server.into()) })
|
|
||||||
},
|
|
||||||
error_out,
|
|
||||||
std::ptr::null_mut(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[ffizz_header::item]
|
|
||||||
#[ffizz(order = 801)]
|
|
||||||
/// Create a new TCServer that connects to a remote server. See the TaskChampion docs for the
|
|
||||||
/// description of the arguments.
|
|
||||||
///
|
|
||||||
/// On error, a string is written to the error_out parameter (if it is not NULL) and NULL is
|
|
||||||
/// returned. The caller must free this string.
|
|
||||||
///
|
|
||||||
/// The server must be freed after it is used - tc_replica_sync does not automatically free it.
|
|
||||||
///
|
|
||||||
/// ```c
|
|
||||||
/// EXTERN_C struct TCServer *tc_server_new_sync(struct TCString url,
|
|
||||||
/// struct TCUuid client_id,
|
|
||||||
/// struct TCString encryption_secret,
|
|
||||||
/// struct TCString *error_out);
|
|
||||||
/// ```
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn tc_server_new_sync(
|
|
||||||
url: TCString,
|
|
||||||
client_id: TCUuid,
|
|
||||||
encryption_secret: TCString,
|
|
||||||
error_out: *mut TCString,
|
|
||||||
) -> *mut TCServer {
|
|
||||||
wrap(
|
|
||||||
|| {
|
|
||||||
// SAFETY:
|
|
||||||
// - url is valid (promised by caller)
|
|
||||||
// - url ownership is transferred to this function
|
|
||||||
let url = unsafe { TCString::val_from_arg(url) }.into_string()?;
|
|
||||||
|
|
||||||
// SAFETY:
|
|
||||||
// - client_id is a valid Uuid (any 8-byte sequence counts)
|
|
||||||
let client_id = unsafe { TCUuid::val_from_arg(client_id) };
|
|
||||||
|
|
||||||
// SAFETY:
|
|
||||||
// - encryption_secret is valid (promised by caller)
|
|
||||||
// - encryption_secret ownership is transferred to this function
|
|
||||||
let encryption_secret = unsafe { TCString::val_from_arg(encryption_secret) }
|
|
||||||
.as_bytes()
|
|
||||||
.to_vec();
|
|
||||||
|
|
||||||
let server_config = ServerConfig::Remote {
|
|
||||||
url,
|
|
||||||
client_id,
|
|
||||||
encryption_secret,
|
|
||||||
};
|
|
||||||
let server = server_config.into_server()?;
|
|
||||||
// SAFETY: caller promises to free this server.
|
|
||||||
Ok(unsafe { TCServer::return_ptr(server.into()) })
|
|
||||||
},
|
|
||||||
error_out,
|
|
||||||
std::ptr::null_mut(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[ffizz_header::item]
|
|
||||||
#[ffizz(order = 802)]
|
|
||||||
/// Create a new TCServer that connects to the Google Cloud Platform. See the TaskChampion docs
|
|
||||||
/// for the description of the arguments.
|
|
||||||
///
|
|
||||||
/// On error, a string is written to the error_out parameter (if it is not NULL) and NULL is
|
|
||||||
/// returned. The caller must free this string.
|
|
||||||
///
|
|
||||||
/// The server must be freed after it is used - tc_replica_sync does not automatically free it.
|
|
||||||
///
|
|
||||||
/// ```c
|
|
||||||
/// EXTERN_C struct TCServer *tc_server_new_gcp(struct TCString bucket,
|
|
||||||
/// struct TCString credential_path,
|
|
||||||
/// struct TCString encryption_secret,
|
|
||||||
/// struct TCString *error_out);
|
|
||||||
/// ```
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn tc_server_new_gcp(
|
|
||||||
bucket: TCString,
|
|
||||||
credential_path_argument: TCString,
|
|
||||||
encryption_secret: TCString,
|
|
||||||
error_out: *mut TCString,
|
|
||||||
) -> *mut TCServer {
|
|
||||||
wrap(
|
|
||||||
|| {
|
|
||||||
// SAFETY:
|
|
||||||
// - bucket is valid (promised by caller)
|
|
||||||
// - bucket ownership is transferred to this function
|
|
||||||
let bucket = unsafe { TCString::val_from_arg(bucket) }.into_string()?;
|
|
||||||
|
|
||||||
// SAFETY:
|
|
||||||
// - credential_path is valid (promised by caller)
|
|
||||||
// - credential_path ownership is transferred to this function
|
|
||||||
|
|
||||||
let credential_path =
|
|
||||||
unsafe { TCString::val_from_arg(credential_path_argument) }.into_string()?;
|
|
||||||
let credential_path = if credential_path.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(credential_path)
|
|
||||||
};
|
|
||||||
|
|
||||||
// SAFETY:
|
|
||||||
// - encryption_secret is valid (promised by caller)
|
|
||||||
// - encryption_secret ownership is transferred to this function
|
|
||||||
let encryption_secret = unsafe { TCString::val_from_arg(encryption_secret) }
|
|
||||||
.as_bytes()
|
|
||||||
.to_vec();
|
|
||||||
let server_config = ServerConfig::Gcp {
|
|
||||||
bucket,
|
|
||||||
credential_path,
|
|
||||||
encryption_secret,
|
|
||||||
};
|
|
||||||
let server = server_config.into_server()?;
|
|
||||||
// SAFETY: caller promises to free this server.
|
|
||||||
Ok(unsafe { TCServer::return_ptr(server.into()) })
|
|
||||||
},
|
|
||||||
error_out,
|
|
||||||
std::ptr::null_mut(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[ffizz_header::item]
|
|
||||||
#[ffizz(order = 899)]
|
|
||||||
/// Free a server. The server may not be used after this function returns and must not be freed
|
|
||||||
/// more than once.
|
|
||||||
///
|
|
||||||
/// ```c
|
|
||||||
/// EXTERN_C void tc_server_free(struct TCServer *server);
|
|
||||||
/// ```
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn tc_server_free(server: *mut TCServer) {
|
|
||||||
debug_assert!(!server.is_null());
|
|
||||||
// SAFETY:
|
|
||||||
// - server is not NULL
|
|
||||||
// - server came from tc_server_new_.., which used return_ptr
|
|
||||||
// - server will not be used after (promised by caller)
|
|
||||||
let server = unsafe { TCServer::take_from_ptr_arg(server) };
|
|
||||||
drop(server);
|
|
||||||
}
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
pub use taskchampion::Status;
|
|
||||||
|
|
||||||
#[ffizz_header::item]
|
|
||||||
#[ffizz(order = 700)]
|
|
||||||
/// ***** TCStatus *****
|
|
||||||
///
|
|
||||||
/// The status of a task, as defined by the task data model.
|
|
||||||
///
|
|
||||||
/// ```c
|
|
||||||
/// #ifdef __cplusplus
|
|
||||||
/// typedef enum TCStatus : int32_t {
|
|
||||||
/// #else // __cplusplus
|
|
||||||
/// typedef int32_t TCStatus;
|
|
||||||
/// enum TCStatus {
|
|
||||||
/// #endif // __cplusplus
|
|
||||||
/// TC_STATUS_PENDING = 0,
|
|
||||||
/// TC_STATUS_COMPLETED = 1,
|
|
||||||
/// TC_STATUS_DELETED = 2,
|
|
||||||
/// TC_STATUS_RECURRING = 3,
|
|
||||||
/// // Unknown signifies a status in the task DB that was not
|
|
||||||
/// // recognized.
|
|
||||||
/// TC_STATUS_UNKNOWN = -1,
|
|
||||||
/// #ifdef __cplusplus
|
|
||||||
/// } TCStatus;
|
|
||||||
/// #else // __cplusplus
|
|
||||||
/// };
|
|
||||||
/// #endif // __cplusplus
|
|
||||||
/// ```
|
|
||||||
#[repr(i32)]
|
|
||||||
pub enum TCStatus {
|
|
||||||
Pending = 0,
|
|
||||||
Completed = 1,
|
|
||||||
Deleted = 2,
|
|
||||||
Recurring = 3,
|
|
||||||
Unknown = -1,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<TCStatus> for Status {
|
|
||||||
fn from(status: TCStatus) -> Status {
|
|
||||||
match status {
|
|
||||||
TCStatus::Pending => Status::Pending,
|
|
||||||
TCStatus::Completed => Status::Completed,
|
|
||||||
TCStatus::Deleted => Status::Deleted,
|
|
||||||
TCStatus::Recurring => Status::Recurring,
|
|
||||||
_ => Status::Unknown(format!("unknown TCStatus {}", status as i32)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Status> for TCStatus {
|
|
||||||
fn from(status: Status) -> TCStatus {
|
|
||||||
match status {
|
|
||||||
Status::Pending => TCStatus::Pending,
|
|
||||||
Status::Completed => TCStatus::Completed,
|
|
||||||
Status::Deleted => TCStatus::Deleted,
|
|
||||||
Status::Recurring => TCStatus::Recurring,
|
|
||||||
Status::Unknown(_) => TCStatus::Unknown,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn conversion_from_unknown_tc_status_provides_discriminant_in_message() {
|
|
||||||
let tc_status = TCStatus::Unknown;
|
|
||||||
let status = Status::from(tc_status);
|
|
||||||
|
|
||||||
assert!(matches!(status, Status::Unknown(msg) if msg == "unknown TCStatus -1"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,773 +0,0 @@
|
|||||||
use crate::traits::*;
|
|
||||||
use crate::util::{string_into_raw_parts, vec_into_raw_parts};
|
|
||||||
use std::ffi::{CStr, CString, OsString};
|
|
||||||
use std::os::raw::c_char;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
#[ffizz_header::item]
|
|
||||||
#[ffizz(order = 200)]
|
|
||||||
/// ***** TCString *****
|
|
||||||
///
|
|
||||||
/// TCString supports passing strings into and out of the TaskChampion API.
|
|
||||||
///
|
|
||||||
/// # Rust Strings and C Strings
|
|
||||||
///
|
|
||||||
/// A Rust string can contain embedded NUL characters, while C considers such a character to mark
|
|
||||||
/// the end of a string. Strings containing embedded NULs cannot be represented as a "C string"
|
|
||||||
/// and must be accessed using `tc_string_content_and_len` and `tc_string_clone_with_len`. In
|
|
||||||
/// general, these two functions should be used for handling arbitrary data, while more convenient
|
|
||||||
/// forms may be used where embedded NUL characters are impossible, such as in static strings.
|
|
||||||
///
|
|
||||||
/// # UTF-8
|
|
||||||
///
|
|
||||||
/// TaskChampion expects all strings to be valid UTF-8. `tc_string_…` functions will fail if given
|
|
||||||
/// a `*TCString` containing invalid UTF-8.
|
|
||||||
///
|
|
||||||
/// # Safety
|
|
||||||
///
|
|
||||||
/// The `ptr` field may be checked for NULL, where documentation indicates this is possible. All
|
|
||||||
/// other fields in a TCString are private and must not be used from C. They exist in the struct
|
|
||||||
/// to ensure proper allocation and alignment.
|
|
||||||
///
|
|
||||||
/// When a `TCString` appears as a return value or output argument, ownership is passed to the
|
|
||||||
/// caller. The caller must pass that ownership back to another function or free the string.
|
|
||||||
///
|
|
||||||
/// Any function taking a `TCString` requires:
|
|
||||||
/// - the pointer must not be NUL;
|
|
||||||
/// - the pointer must be one previously returned from a tc_… function; and
|
|
||||||
/// - the memory referenced by the pointer must never be modified by C code.
|
|
||||||
///
|
|
||||||
/// Unless specified otherwise, TaskChampion functions take ownership of a `TCString` when it is
|
|
||||||
/// given as a function argument, and the caller must not use or free TCStrings after passing them
|
|
||||||
/// to such API functions.
|
|
||||||
///
|
|
||||||
/// A TCString with a NULL `ptr` field need not be freed, although tc_free_string will not fail
|
|
||||||
/// for such a value.
|
|
||||||
///
|
|
||||||
/// TCString is not threadsafe.
|
|
||||||
///
|
|
||||||
/// ```c
|
|
||||||
/// typedef struct TCString {
|
|
||||||
/// void *ptr; // opaque, but may be checked for NULL
|
|
||||||
/// size_t _u1; // reserved
|
|
||||||
/// size_t _u2; // reserved
|
|
||||||
/// uint8_t _u3; // reserved
|
|
||||||
/// } TCString;
|
|
||||||
/// ```
|
|
||||||
#[repr(C)]
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct TCString {
|
|
||||||
// defined based on the type
|
|
||||||
ptr: *mut libc::c_void,
|
|
||||||
len: usize,
|
|
||||||
cap: usize,
|
|
||||||
|
|
||||||
// type of TCString this represents
|
|
||||||
ty: u8,
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: figure out how to ignore this but still use it in TCString
|
|
||||||
/// A discriminator for TCString
|
|
||||||
#[repr(u8)]
|
|
||||||
enum TCStringType {
|
|
||||||
/// Null. Nothing is contained in this string.
|
|
||||||
///
|
|
||||||
/// * `ptr` is NULL.
|
|
||||||
/// * `len` and `cap` are zero.
|
|
||||||
Null = 0,
|
|
||||||
|
|
||||||
/// A CString.
|
|
||||||
///
|
|
||||||
/// * `ptr` is the result of CString::into_raw, containing a terminating NUL. It may not be
|
|
||||||
/// valid UTF-8.
|
|
||||||
/// * `len` and `cap` are zero.
|
|
||||||
CString,
|
|
||||||
|
|
||||||
/// A CStr, referencing memory borrowed from C
|
|
||||||
///
|
|
||||||
/// * `ptr` points to the string, containing a terminating NUL. It may not be valid UTF-8.
|
|
||||||
/// * `len` and `cap` are zero.
|
|
||||||
CStr,
|
|
||||||
|
|
||||||
/// A String.
|
|
||||||
///
|
|
||||||
/// * `ptr`, `len`, and `cap` are as would be returned from String::into_raw_parts.
|
|
||||||
String,
|
|
||||||
|
|
||||||
/// A byte sequence.
|
|
||||||
///
|
|
||||||
/// * `ptr`, `len`, and `cap` are as would be returned from Vec::into_raw_parts.
|
|
||||||
Bytes,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for TCString {
|
|
||||||
fn default() -> Self {
|
|
||||||
TCString {
|
|
||||||
ptr: std::ptr::null_mut(),
|
|
||||||
len: 0,
|
|
||||||
cap: 0,
|
|
||||||
ty: TCStringType::Null as u8,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TCString {
|
|
||||||
pub(crate) fn is_null(&self) -> bool {
|
|
||||||
self.ptr.is_null()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(PartialEq, Eq, Debug, Default)]
|
|
||||||
pub enum RustString<'a> {
|
|
||||||
#[default]
|
|
||||||
Null,
|
|
||||||
CString(CString),
|
|
||||||
CStr(&'a CStr),
|
|
||||||
String(String),
|
|
||||||
Bytes(Vec<u8>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PassByValue for TCString {
|
|
||||||
type RustType = RustString<'static>;
|
|
||||||
|
|
||||||
unsafe fn from_ctype(self) -> Self::RustType {
|
|
||||||
match self.ty {
|
|
||||||
ty if ty == TCStringType::CString as u8 => {
|
|
||||||
// SAFETY:
|
|
||||||
// - ptr was derived from CString::into_raw
|
|
||||||
// - data was not modified since that time (caller promises)
|
|
||||||
RustString::CString(unsafe { CString::from_raw(self.ptr as *mut c_char) })
|
|
||||||
}
|
|
||||||
ty if ty == TCStringType::CStr as u8 => {
|
|
||||||
// SAFETY:
|
|
||||||
// - ptr was created by CStr::as_ptr
|
|
||||||
// - data was not modified since that time (caller promises)
|
|
||||||
RustString::CStr(unsafe { CStr::from_ptr(self.ptr as *mut c_char) })
|
|
||||||
}
|
|
||||||
ty if ty == TCStringType::String as u8 => {
|
|
||||||
// SAFETY:
|
|
||||||
// - ptr was created by string_into_raw_parts
|
|
||||||
// - data was not modified since that time (caller promises)
|
|
||||||
RustString::String(unsafe {
|
|
||||||
String::from_raw_parts(self.ptr as *mut u8, self.len, self.cap)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
ty if ty == TCStringType::Bytes as u8 => {
|
|
||||||
// SAFETY:
|
|
||||||
// - ptr was created by vec_into_raw_parts
|
|
||||||
// - data was not modified since that time (caller promises)
|
|
||||||
RustString::Bytes(unsafe {
|
|
||||||
Vec::from_raw_parts(self.ptr as *mut u8, self.len, self.cap)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
_ => RustString::Null,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn as_ctype(arg: Self::RustType) -> Self {
|
|
||||||
match arg {
|
|
||||||
RustString::Null => Self {
|
|
||||||
ty: TCStringType::Null as u8,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
RustString::CString(cstring) => Self {
|
|
||||||
ty: TCStringType::CString as u8,
|
|
||||||
ptr: cstring.into_raw() as *mut libc::c_void,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
RustString::CStr(cstr) => Self {
|
|
||||||
ty: TCStringType::CStr as u8,
|
|
||||||
ptr: cstr.as_ptr() as *mut libc::c_void,
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
RustString::String(string) => {
|
|
||||||
let (ptr, len, cap) = string_into_raw_parts(string);
|
|
||||||
Self {
|
|
||||||
ty: TCStringType::String as u8,
|
|
||||||
ptr: ptr as *mut libc::c_void,
|
|
||||||
len,
|
|
||||||
cap,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
RustString::Bytes(bytes) => {
|
|
||||||
let (ptr, len, cap) = vec_into_raw_parts(bytes);
|
|
||||||
Self {
|
|
||||||
ty: TCStringType::Bytes as u8,
|
|
||||||
ptr: ptr as *mut libc::c_void,
|
|
||||||
len,
|
|
||||||
cap,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> RustString<'a> {
|
|
||||||
/// Get a regular Rust &str for this value.
|
|
||||||
pub(crate) fn as_str(&mut self) -> Result<&str, std::str::Utf8Error> {
|
|
||||||
match self {
|
|
||||||
RustString::CString(cstring) => cstring.as_c_str().to_str(),
|
|
||||||
RustString::CStr(cstr) => cstr.to_str(),
|
|
||||||
RustString::String(ref string) => Ok(string.as_ref()),
|
|
||||||
RustString::Bytes(_) => {
|
|
||||||
self.bytes_to_string()?;
|
|
||||||
self.as_str() // now the String variant, so won't recurse
|
|
||||||
}
|
|
||||||
RustString::Null => unreachable!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Consume this RustString and return an equivalent String, or an error if not
|
|
||||||
/// valid UTF-8. In the error condition, the original data is lost.
|
|
||||||
pub(crate) fn into_string(mut self) -> Result<String, std::str::Utf8Error> {
|
|
||||||
match self {
|
|
||||||
RustString::CString(cstring) => cstring.into_string().map_err(|e| e.utf8_error()),
|
|
||||||
RustString::CStr(cstr) => cstr.to_str().map(|s| s.to_string()),
|
|
||||||
RustString::String(string) => Ok(string),
|
|
||||||
RustString::Bytes(_) => {
|
|
||||||
self.bytes_to_string()?;
|
|
||||||
self.into_string() // now the String variant, so won't recurse
|
|
||||||
}
|
|
||||||
RustString::Null => unreachable!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn as_bytes(&self) -> &[u8] {
|
|
||||||
match self {
|
|
||||||
RustString::CString(cstring) => cstring.as_bytes(),
|
|
||||||
RustString::CStr(cstr) => cstr.to_bytes(),
|
|
||||||
RustString::String(string) => string.as_bytes(),
|
|
||||||
RustString::Bytes(bytes) => bytes.as_ref(),
|
|
||||||
RustString::Null => unreachable!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert the RustString, in place, from the Bytes to String variant. On successful return,
|
|
||||||
/// the RustString has variant RustString::String.
|
|
||||||
fn bytes_to_string(&mut self) -> Result<(), std::str::Utf8Error> {
|
|
||||||
let mut owned = RustString::Null;
|
|
||||||
// temporarily swap a Null value into self; we'll swap that back
|
|
||||||
// shortly.
|
|
||||||
std::mem::swap(self, &mut owned);
|
|
||||||
match owned {
|
|
||||||
RustString::Bytes(bytes) => match String::from_utf8(bytes) {
|
|
||||||
Ok(string) => {
|
|
||||||
*self = RustString::String(string);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
let (e, bytes) = (e.utf8_error(), e.into_bytes());
|
|
||||||
// put self back as we found it
|
|
||||||
*self = RustString::Bytes(bytes);
|
|
||||||
Err(e)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
_ => {
|
|
||||||
// not bytes, so just swap back
|
|
||||||
std::mem::swap(self, &mut owned);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert the RustString, in place, into one of the C variants. If this is not
|
|
||||||
/// possible, such as if the string contains an embedded NUL, then the string
|
|
||||||
/// remains unchanged.
|
|
||||||
fn string_to_cstring(&mut self) {
|
|
||||||
let mut owned = RustString::Null;
|
|
||||||
// temporarily swap a Null value into self; we'll swap that back shortly
|
|
||||||
std::mem::swap(self, &mut owned);
|
|
||||||
match owned {
|
|
||||||
RustString::String(string) => {
|
|
||||||
match CString::new(string) {
|
|
||||||
Ok(cstring) => {
|
|
||||||
*self = RustString::CString(cstring);
|
|
||||||
}
|
|
||||||
Err(nul_err) => {
|
|
||||||
// recover the underlying String from the NulError and restore
|
|
||||||
// the RustString
|
|
||||||
let original_bytes = nul_err.into_vec();
|
|
||||||
// SAFETY: original_bytes came from a String moments ago, so still valid utf8
|
|
||||||
let string = unsafe { String::from_utf8_unchecked(original_bytes) };
|
|
||||||
*self = RustString::String(string);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
// not a CString, so just swap back
|
|
||||||
std::mem::swap(self, &mut owned);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn to_path_buf_mut(&mut self) -> Result<PathBuf, std::str::Utf8Error> {
|
|
||||||
#[cfg(unix)]
|
|
||||||
let path: OsString = {
|
|
||||||
// on UNIX, we can use the bytes directly, without requiring that they
|
|
||||||
// be valid UTF-8.
|
|
||||||
use std::ffi::OsStr;
|
|
||||||
use std::os::unix::ffi::OsStrExt;
|
|
||||||
OsStr::from_bytes(self.as_bytes()).to_os_string()
|
|
||||||
};
|
|
||||||
#[cfg(windows)]
|
|
||||||
let path: OsString = {
|
|
||||||
// on Windows, we assume the filename is valid Unicode, so it can be
|
|
||||||
// represented as UTF-8.
|
|
||||||
OsString::from(self.as_str()?.to_string())
|
|
||||||
};
|
|
||||||
Ok(path.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> From<String> for RustString<'a> {
|
|
||||||
fn from(string: String) -> RustString<'a> {
|
|
||||||
RustString::String(string)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&str> for RustString<'static> {
|
|
||||||
fn from(string: &str) -> RustString<'static> {
|
|
||||||
RustString::String(string.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Utility function to borrow a TCString from a pointer arg, modify it,
|
|
||||||
/// and restore it.
|
|
||||||
///
|
|
||||||
/// This implements a kind of "interior mutability", relying on the
|
|
||||||
/// single-threaded use of all TC* types.
|
|
||||||
///
|
|
||||||
/// # SAFETY
|
|
||||||
///
|
|
||||||
/// - tcstring must not be NULL
|
|
||||||
/// - *tcstring must be a valid TCString
|
|
||||||
/// - *tcstring must not be accessed by anything else, despite the *const
|
|
||||||
unsafe fn wrap<T, F>(tcstring: *const TCString, f: F) -> T
|
|
||||||
where
|
|
||||||
F: FnOnce(&mut RustString) -> T,
|
|
||||||
{
|
|
||||||
debug_assert!(!tcstring.is_null());
|
|
||||||
|
|
||||||
// SAFETY:
|
|
||||||
// - we have exclusive to *tcstring (promised by caller)
|
|
||||||
let tcstring = tcstring as *mut TCString;
|
|
||||||
|
|
||||||
// SAFETY:
|
|
||||||
// - tcstring is not NULL
|
|
||||||
// - *tcstring is a valid string (promised by caller)
|
|
||||||
let mut rstring = unsafe { TCString::take_val_from_arg(tcstring, TCString::default()) };
|
|
||||||
|
|
||||||
let rv = f(&mut rstring);
|
|
||||||
|
|
||||||
// update the caller's TCString with the updated RustString
|
|
||||||
// SAFETY:
|
|
||||||
// - tcstring is not NULL (we just took from it)
|
|
||||||
// - tcstring points to valid memory (we just took from it)
|
|
||||||
unsafe { TCString::val_to_arg_out(rstring, tcstring) };
|
|
||||||
|
|
||||||
rv
|
|
||||||
}
|
|
||||||
|
|
||||||
#[ffizz_header::item]
|
|
||||||
#[ffizz(order = 210)]
|
|
||||||
/// ***** TCStringList *****
|
|
||||||
///
|
|
||||||
/// TCStringList represents a list of strings.
|
|
||||||
///
|
|
||||||
/// The content of this struct must be treated as read-only.
|
|
||||||
///
|
|
||||||
/// ```c
|
|
||||||
/// typedef struct TCStringList {
|
|
||||||
/// // number of strings in items
|
|
||||||
/// size_t len;
|
|
||||||
/// // reserved
|
|
||||||
/// size_t _u1;
|
|
||||||
/// // TCStringList representing each string. These remain owned by the TCStringList instance and will
|
|
||||||
/// // be freed by tc_string_list_free. This pointer is never NULL for a valid TCStringList, and the
|
|
||||||
/// // *TCStringList at indexes 0..len-1 are not NULL.
|
|
||||||
/// struct TCString *items;
|
|
||||||
/// } TCStringList;
|
|
||||||
/// ```
|
|
||||||
#[repr(C)]
|
|
||||||
pub struct TCStringList {
|
|
||||||
len: libc::size_t,
|
|
||||||
/// total size of items (internal use only)
|
|
||||||
capacity: libc::size_t,
|
|
||||||
items: *mut TCString,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CList for TCStringList {
|
|
||||||
type Element = TCString;
|
|
||||||
|
|
||||||
unsafe fn from_raw_parts(items: *mut Self::Element, len: usize, cap: usize) -> Self {
|
|
||||||
TCStringList {
|
|
||||||
len,
|
|
||||||
capacity: cap,
|
|
||||||
items,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn slice(&mut self) -> &mut [Self::Element] {
|
|
||||||
// SAFETY:
|
|
||||||
// - because we have &mut self, we have read/write access to items[0..len]
|
|
||||||
// - all items are properly initialized Element's
|
|
||||||
// - return value lifetime is equal to &mmut self's, so access is exclusive
|
|
||||||
// - items and len came from Vec, so total size is < isize::MAX
|
|
||||||
unsafe { std::slice::from_raw_parts_mut(self.items, self.len) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fn into_raw_parts(self) -> (*mut Self::Element, usize, usize) {
|
|
||||||
(self.items, self.len, self.capacity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[ffizz_header::item]
|
|
||||||
#[ffizz(order = 201)]
|
|
||||||
/// Create a new TCString referencing the given C string. The C string must remain valid and
|
|
||||||
/// unchanged until after the TCString is freed. It's typically easiest to ensure this by using a
|
|
||||||
/// static string.
|
|
||||||
///
|
|
||||||
/// NOTE: this function does _not_ take responsibility for freeing the given C string. The
|
|
||||||
/// given string can be freed once the TCString referencing it has been freed.
|
|
||||||
///
|
|
||||||
/// For example:
|
|
||||||
///
|
|
||||||
/// ```text
|
|
||||||
/// char *url = get_item_url(..); // dynamically allocate C string
|
|
||||||
/// tc_task_annotate(task, tc_string_borrow(url)); // TCString created, passed, and freed
|
|
||||||
/// free(url); // string is no longer referenced and can be freed
|
|
||||||
/// ```
|
|
||||||
///
|
|
||||||
/// ```c
|
|
||||||
/// EXTERN_C struct TCString tc_string_borrow(const char *cstr);
|
|
||||||
/// ```
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn tc_string_borrow(cstr: *const libc::c_char) -> TCString {
|
|
||||||
debug_assert!(!cstr.is_null());
|
|
||||||
// SAFETY:
|
|
||||||
// - cstr is not NULL (promised by caller, verified by assertion)
|
|
||||||
// - cstr's lifetime exceeds that of the TCString (promised by caller)
|
|
||||||
// - cstr contains a valid NUL terminator (promised by caller)
|
|
||||||
// - cstr's content will not change before it is destroyed (promised by caller)
|
|
||||||
let cstr: &CStr = unsafe { CStr::from_ptr(cstr) };
|
|
||||||
// SAFETY:
|
|
||||||
// - caller promises to free this string
|
|
||||||
unsafe { TCString::return_val(RustString::CStr(cstr)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[ffizz_header::item]
|
|
||||||
#[ffizz(order = 201)]
|
|
||||||
/// Create a new TCString by cloning the content of the given C string. The resulting TCString
|
|
||||||
/// is independent of the given string, which can be freed or overwritten immediately.
|
|
||||||
///
|
|
||||||
/// ```c
|
|
||||||
/// EXTERN_C struct TCString tc_string_clone(const char *cstr);
|
|
||||||
/// ```
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn tc_string_clone(cstr: *const libc::c_char) -> TCString {
|
|
||||||
debug_assert!(!cstr.is_null());
|
|
||||||
// SAFETY:
|
|
||||||
// - cstr is not NULL (promised by caller, verified by assertion)
|
|
||||||
// - cstr's lifetime exceeds that of this function (by C convention)
|
|
||||||
// - cstr contains a valid NUL terminator (promised by caller)
|
|
||||||
// - cstr's content will not change before it is destroyed (by C convention)
|
|
||||||
let cstr: &CStr = unsafe { CStr::from_ptr(cstr) };
|
|
||||||
let cstring: CString = cstr.into();
|
|
||||||
// SAFETY:
|
|
||||||
// - caller promises to free this string
|
|
||||||
unsafe { TCString::return_val(RustString::CString(cstring)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[ffizz_header::item]
|
|
||||||
#[ffizz(order = 201)]
|
|
||||||
/// Create a new TCString containing the given string with the given length. This allows creation
|
|
||||||
/// of strings containing embedded NUL characters. As with `tc_string_clone`, the resulting
|
|
||||||
/// TCString is independent of the passed buffer, which may be reused or freed immediately.
|
|
||||||
///
|
|
||||||
/// The length should _not_ include any trailing NUL.
|
|
||||||
///
|
|
||||||
/// The given length must be less than half the maximum value of usize.
|
|
||||||
///
|
|
||||||
/// ```c
|
|
||||||
/// EXTERN_C struct TCString tc_string_clone_with_len(const char *buf, size_t len);
|
|
||||||
/// ```
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn tc_string_clone_with_len(
|
|
||||||
buf: *const libc::c_char,
|
|
||||||
len: usize,
|
|
||||||
) -> TCString {
|
|
||||||
debug_assert!(!buf.is_null());
|
|
||||||
debug_assert!(len < isize::MAX as usize);
|
|
||||||
// SAFETY:
|
|
||||||
// - buf is valid for len bytes (by C convention)
|
|
||||||
// - (no alignment requirements for a byte slice)
|
|
||||||
// - content of buf will not be mutated during the lifetime of this slice (lifetime
|
|
||||||
// does not outlive this function call)
|
|
||||||
// - the length of the buffer is less than isize::MAX (promised by caller)
|
|
||||||
let slice = unsafe { std::slice::from_raw_parts(buf as *const u8, len) };
|
|
||||||
|
|
||||||
// allocate and copy into Rust-controlled memory
|
|
||||||
let vec = slice.to_vec();
|
|
||||||
|
|
||||||
// SAFETY:
|
|
||||||
// - caller promises to free this string
|
|
||||||
unsafe { TCString::return_val(RustString::Bytes(vec)) }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[ffizz_header::item]
|
|
||||||
#[ffizz(order = 201)]
|
|
||||||
/// Get the content of the string as a regular C string. The given string must be valid. The
|
|
||||||
/// returned value is NULL if the string contains NUL bytes or (in some cases) invalid UTF-8. The
|
|
||||||
/// returned C string is valid until the TCString is freed or passed to another TC API function.
|
|
||||||
///
|
|
||||||
/// In general, prefer [`tc_string_content_with_len`] except when it's certain that the string is
|
|
||||||
/// valid and NUL-free.
|
|
||||||
///
|
|
||||||
/// This function takes the TCString by pointer because it may be modified in-place to add a NUL
|
|
||||||
/// terminator. The pointer must not be NULL.
|
|
||||||
///
|
|
||||||
/// This function does _not_ take ownership of the TCString.
|
|
||||||
///
|
|
||||||
/// ```c
|
|
||||||
/// EXTERN_C const char *tc_string_content(const struct TCString *tcstring);
|
|
||||||
/// ```
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn tc_string_content(tcstring: *const TCString) -> *const libc::c_char {
|
|
||||||
// SAFETY;
|
|
||||||
// - tcstring is not NULL (promised by caller)
|
|
||||||
// - *tcstring is valid (promised by caller)
|
|
||||||
// - *tcstring is not accessed concurrently (single-threaded)
|
|
||||||
unsafe {
|
|
||||||
wrap(tcstring, |rstring| {
|
|
||||||
// try to eliminate the Bytes variant. If this fails, we'll return NULL
|
|
||||||
// below, so the error is ignorable.
|
|
||||||
let _ = rstring.bytes_to_string();
|
|
||||||
|
|
||||||
// and eliminate the String variant
|
|
||||||
rstring.string_to_cstring();
|
|
||||||
|
|
||||||
match &rstring {
|
|
||||||
RustString::CString(cstring) => cstring.as_ptr(),
|
|
||||||
RustString::String(_) => std::ptr::null(), // string_to_cstring failed
|
|
||||||
RustString::CStr(cstr) => cstr.as_ptr(),
|
|
||||||
RustString::Bytes(_) => std::ptr::null(), // already returned above
|
|
||||||
RustString::Null => unreachable!(),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[ffizz_header::item]
|
|
||||||
#[ffizz(order = 201)]
|
|
||||||
/// Get the content of the string as a pointer and length. The given string must not be NULL.
|
|
||||||
/// This function can return any string, even one including NUL bytes or invalid UTF-8. The
|
|
||||||
/// returned buffer is valid until the TCString is freed or passed to another TaskChampio
|
|
||||||
/// function.
|
|
||||||
///
|
|
||||||
/// This function takes the TCString by pointer because it may be modified in-place to add a NUL
|
|
||||||
/// terminator. The pointer must not be NULL.
|
|
||||||
///
|
|
||||||
/// This function does _not_ take ownership of the TCString.
|
|
||||||
///
|
|
||||||
/// ```c
|
|
||||||
/// EXTERN_C const char *tc_string_content_with_len(const struct TCString *tcstring, size_t *len_out);
|
|
||||||
/// ```
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn tc_string_content_with_len(
|
|
||||||
tcstring: *const TCString,
|
|
||||||
len_out: *mut usize,
|
|
||||||
) -> *const libc::c_char {
|
|
||||||
// SAFETY;
|
|
||||||
// - tcstring is not NULL (promised by caller)
|
|
||||||
// - *tcstring is valid (promised by caller)
|
|
||||||
// - *tcstring is not accessed concurrently (single-threaded)
|
|
||||||
unsafe {
|
|
||||||
wrap(tcstring, |rstring| {
|
|
||||||
let bytes = rstring.as_bytes();
|
|
||||||
|
|
||||||
// SAFETY:
|
|
||||||
// - len_out is not NULL (promised by caller)
|
|
||||||
// - len_out points to valid memory (promised by caller)
|
|
||||||
// - len_out is properly aligned (C convention)
|
|
||||||
usize::val_to_arg_out(bytes.len(), len_out);
|
|
||||||
bytes.as_ptr() as *const libc::c_char
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[ffizz_header::item]
|
|
||||||
#[ffizz(order = 201)]
|
|
||||||
/// Free a TCString. The given string must not be NULL. The string must not be used
|
|
||||||
/// after this function returns, and must not be freed more than once.
|
|
||||||
///
|
|
||||||
/// ```c
|
|
||||||
/// EXTERN_C void tc_string_free(struct TCString *tcstring);
|
|
||||||
/// ```
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn tc_string_free(tcstring: *mut TCString) {
|
|
||||||
// SAFETY:
|
|
||||||
// - tcstring is not NULL (promised by caller)
|
|
||||||
// - caller is exclusive owner of tcstring (promised by caller)
|
|
||||||
drop(unsafe { TCString::take_val_from_arg(tcstring, TCString::default()) });
|
|
||||||
}
|
|
||||||
|
|
||||||
#[ffizz_header::item]
|
|
||||||
#[ffizz(order = 211)]
|
|
||||||
/// Free a TCStringList instance. The instance, and all TCStringList it contains, must not be used after
|
|
||||||
/// this call.
|
|
||||||
///
|
|
||||||
/// When this call returns, the `items` pointer will be NULL, signalling an invalid TCStringList.
|
|
||||||
///
|
|
||||||
/// ```c
|
|
||||||
/// EXTERN_C void tc_string_list_free(struct TCStringList *tcstrings);
|
|
||||||
/// ```
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn tc_string_list_free(tcstrings: *mut TCStringList) {
|
|
||||||
// SAFETY:
|
|
||||||
// - tcstrings is not NULL and points to a valid TCStringList (caller is not allowed to
|
|
||||||
// modify the list)
|
|
||||||
// - caller promises not to use the value after return
|
|
||||||
unsafe { drop_value_list(tcstrings) };
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use super::*;
|
|
||||||
use pretty_assertions::assert_eq;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn empty_list_has_non_null_pointer() {
|
|
||||||
let tcstrings = unsafe { TCStringList::return_val(Vec::new()) };
|
|
||||||
assert!(!tcstrings.items.is_null());
|
|
||||||
assert_eq!(tcstrings.len, 0);
|
|
||||||
assert_eq!(tcstrings.capacity, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn free_sets_null_pointer() {
|
|
||||||
let mut tcstrings = unsafe { TCStringList::return_val(Vec::new()) };
|
|
||||||
// SAFETY: testing expected behavior
|
|
||||||
unsafe { tc_string_list_free(&mut tcstrings) };
|
|
||||||
assert!(tcstrings.items.is_null());
|
|
||||||
assert_eq!(tcstrings.len, 0);
|
|
||||||
assert_eq!(tcstrings.capacity, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
const INVALID_UTF8: &[u8] = b"abc\xf0\x28\x8c\x28";
|
|
||||||
|
|
||||||
fn make_cstring() -> RustString<'static> {
|
|
||||||
RustString::CString(CString::new("a string").unwrap())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn make_cstr() -> RustString<'static> {
|
|
||||||
let cstr = CStr::from_bytes_with_nul(b"a string\0").unwrap();
|
|
||||||
RustString::CStr(cstr)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn make_string() -> RustString<'static> {
|
|
||||||
RustString::String("a string".into())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn make_string_with_nul() -> RustString<'static> {
|
|
||||||
RustString::String("a \0 nul!".into())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn make_invalid_bytes() -> RustString<'static> {
|
|
||||||
RustString::Bytes(INVALID_UTF8.to_vec())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn make_bytes() -> RustString<'static> {
|
|
||||||
RustString::Bytes(b"bytes".to_vec())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn cstring_as_str() {
|
|
||||||
assert_eq!(make_cstring().as_str().unwrap(), "a string");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn cstr_as_str() {
|
|
||||||
assert_eq!(make_cstr().as_str().unwrap(), "a string");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn string_as_str() {
|
|
||||||
assert_eq!(make_string().as_str().unwrap(), "a string");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn string_with_nul_as_str() {
|
|
||||||
assert_eq!(make_string_with_nul().as_str().unwrap(), "a \0 nul!");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn invalid_bytes_as_str() {
|
|
||||||
let as_str_err = make_invalid_bytes().as_str().unwrap_err();
|
|
||||||
assert_eq!(as_str_err.valid_up_to(), 3); // "abc" is valid
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn valid_bytes_as_str() {
|
|
||||||
assert_eq!(make_bytes().as_str().unwrap(), "bytes");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn cstring_as_bytes() {
|
|
||||||
assert_eq!(make_cstring().as_bytes(), b"a string");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn cstr_as_bytes() {
|
|
||||||
assert_eq!(make_cstr().as_bytes(), b"a string");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn string_as_bytes() {
|
|
||||||
assert_eq!(make_string().as_bytes(), b"a string");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn string_with_nul_as_bytes() {
|
|
||||||
assert_eq!(make_string_with_nul().as_bytes(), b"a \0 nul!");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn invalid_bytes_as_bytes() {
|
|
||||||
assert_eq!(make_invalid_bytes().as_bytes(), INVALID_UTF8);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn cstring_string_to_cstring() {
|
|
||||||
let mut tcstring = make_cstring();
|
|
||||||
tcstring.string_to_cstring();
|
|
||||||
assert_eq!(tcstring, make_cstring()); // unchanged
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn cstr_string_to_cstring() {
|
|
||||||
let mut tcstring = make_cstr();
|
|
||||||
tcstring.string_to_cstring();
|
|
||||||
assert_eq!(tcstring, make_cstr()); // unchanged
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn string_string_to_cstring() {
|
|
||||||
let mut tcstring = make_string();
|
|
||||||
tcstring.string_to_cstring();
|
|
||||||
assert_eq!(tcstring, make_cstring()); // converted to CString, same content
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn string_with_nul_string_to_cstring() {
|
|
||||||
let mut tcstring = make_string_with_nul();
|
|
||||||
tcstring.string_to_cstring();
|
|
||||||
assert_eq!(tcstring, make_string_with_nul()); // unchanged
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn bytes_string_to_cstring() {
|
|
||||||
let mut tcstring = make_bytes();
|
|
||||||
tcstring.string_to_cstring();
|
|
||||||
assert_eq!(tcstring, make_bytes()); // unchanged
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user