Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0627447a6a | ||
|
|
f054a4061e | ||
|
|
304b84e4da | ||
|
|
f86a069faf | ||
|
|
0944c73716 | ||
|
|
10cec507cb | ||
|
|
4d9bb20bdd | ||
|
|
d243d000eb | ||
|
|
9040a7eb79 | ||
|
|
0a491f36ad | ||
|
|
7578768d9b | ||
|
|
cb0d21f96e | ||
|
|
3b414cd9bb | ||
|
|
c90eb8f71d | ||
|
|
7c465ceb8f | ||
|
|
a6b721853b | ||
|
|
fd306712b8 | ||
|
|
b5aa7c6ae2 | ||
|
|
933885f21c | ||
|
|
587f8910ef | ||
|
|
de42c8ba34 | ||
|
|
8a0a98d3ef | ||
|
|
5a56cff88b | ||
|
|
c91cef43b0 | ||
|
|
8c2c629a4d | ||
|
|
dfaf3dfcb2 | ||
|
|
06fdfc5af3 | ||
|
|
19f2c0d7b4 | ||
|
|
8bb08bf01d |
2
.github/workflows/checks.yml
vendored
2
.github/workflows/checks.yml
vendored
@@ -52,7 +52,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup mdBook
|
||||
uses: peaceiris/actions-mdbook@v1
|
||||
uses: peaceiris/actions-mdbook@v2
|
||||
with:
|
||||
# if this changes, change it in .github/workflows/publish-docs.yml as well
|
||||
mdbook-version: '0.4.10'
|
||||
|
||||
2
.github/workflows/docker-image.yaml
vendored
2
.github/workflows/docker-image.yaml
vendored
@@ -29,7 +29,7 @@ jobs:
|
||||
submodules: "recursive"
|
||||
|
||||
- name: Install cosign
|
||||
uses: sigstore/cosign-installer@v3.4.0
|
||||
uses: sigstore/cosign-installer@v3.5.0
|
||||
|
||||
- name: Log into registry ${{ env.REGISTRY }}
|
||||
uses: docker/login-action@v3.1.0
|
||||
|
||||
4
.github/workflows/publish-docs.yml
vendored
4
.github/workflows/publish-docs.yml
vendored
@@ -16,7 +16,7 @@ jobs:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup mdBook
|
||||
uses: peaceiris/actions-mdbook@v1
|
||||
uses: peaceiris/actions-mdbook@v2
|
||||
with:
|
||||
# if this changes, change it in .github/workflows/checks.yml as well
|
||||
mdbook-version: '0.4.10'
|
||||
@@ -24,7 +24,7 @@ jobs:
|
||||
- run: mdbook build taskchampion/docs
|
||||
|
||||
- name: Deploy
|
||||
uses: peaceiris/actions-gh-pages@v3
|
||||
uses: peaceiris/actions-gh-pages@v4
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
publish_dir: ./taskchampion/docs/book
|
||||
|
||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -1,3 +1,6 @@
|
||||
[submodule "src/libshared"]
|
||||
path = src/libshared
|
||||
url = https://github.com/GothenburgBitFactory/libshared.git
|
||||
[submodule "src/tc/corrosion"]
|
||||
path = src/tc/corrosion
|
||||
url = https://github.com/corrosion-rs/corrosion.git
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
cmake_minimum_required (VERSION 3.22)
|
||||
|
||||
set (CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||
|
||||
project (task
|
||||
VERSION 3.0.1
|
||||
DESCRIPTION "Taskwarrior - a command-line TODO list manager"
|
||||
HOMEPAGE_URL https://taskwarrior.org/)
|
||||
|
||||
set (CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake")
|
||||
|
||||
include (FetchContent)
|
||||
@@ -7,11 +15,8 @@ include (CheckStructHasMember)
|
||||
|
||||
set (HAVE_CMAKE true)
|
||||
|
||||
project (task)
|
||||
include (CXXSniffer)
|
||||
|
||||
set (PROJECT_VERSION "3.0.0")
|
||||
|
||||
OPTION (ENABLE_WASM "Enable 'wasm' support" OFF)
|
||||
|
||||
if (ENABLE_WASM)
|
||||
@@ -147,7 +152,7 @@ if (EXISTS performance)
|
||||
add_subdirectory (performance EXCLUDE_FROM_ALL)
|
||||
endif (EXISTS performance)
|
||||
|
||||
set (doc_FILES NEWS ChangeLog README.md INSTALL AUTHORS COPYING LICENSE)
|
||||
set (doc_FILES ChangeLog README.md INSTALL AUTHORS COPYING LICENSE)
|
||||
foreach (doc_FILE ${doc_FILES})
|
||||
install (FILES ${doc_FILE} DESTINATION ${TASK_DOCDIR})
|
||||
endforeach (doc_FILE)
|
||||
|
||||
563
Cargo.lock
generated
563
Cargo.lock
generated
@@ -2,188 +2,6 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "actix-codec"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57a7559404a7f3573127aab53c08ce37a6c6a315c374a31070f3c91cd1b4a7fe"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"bytes",
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"log",
|
||||
"memchr",
|
||||
"pin-project-lite",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-http"
|
||||
version = "3.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2079246596c18b4a33e274ae10c0e50613f4d32a4198e09c7b93771013fed74"
|
||||
dependencies = [
|
||||
"actix-codec",
|
||||
"actix-rt",
|
||||
"actix-service",
|
||||
"actix-utils",
|
||||
"ahash 0.8.3",
|
||||
"base64 0.21.0",
|
||||
"bitflags 1.3.2",
|
||||
"brotli",
|
||||
"bytes",
|
||||
"bytestring",
|
||||
"derive_more",
|
||||
"encoding_rs",
|
||||
"flate2",
|
||||
"futures-core",
|
||||
"h2",
|
||||
"http",
|
||||
"httparse",
|
||||
"httpdate",
|
||||
"itoa",
|
||||
"language-tags",
|
||||
"local-channel",
|
||||
"mime",
|
||||
"percent-encoding",
|
||||
"pin-project-lite",
|
||||
"rand",
|
||||
"sha1",
|
||||
"smallvec",
|
||||
"tokio",
|
||||
"tokio-util",
|
||||
"tracing",
|
||||
"zstd",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-macros"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "465a6172cf69b960917811022d8f29bc0b7fa1398bc4f78b3c466673db1213b6"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"syn 1.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-router"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d66ff4d247d2b160861fa2866457e85706833527840e4133f8f49aa423a38799"
|
||||
dependencies = [
|
||||
"bytestring",
|
||||
"http",
|
||||
"regex",
|
||||
"serde",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-rt"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "15265b6b8e2347670eb363c47fc8c75208b4a4994b27192f345fcbe707804f3e"
|
||||
dependencies = [
|
||||
"actix-macros",
|
||||
"futures-core",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-server"
|
||||
version = "2.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3e8613a75dd50cc45f473cee3c34d59ed677c0f7b44480ce3b8247d7dc519327"
|
||||
dependencies = [
|
||||
"actix-rt",
|
||||
"actix-service",
|
||||
"actix-utils",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"mio",
|
||||
"num_cpus",
|
||||
"socket2 0.4.9",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-service"
|
||||
version = "2.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"paste",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-utils"
|
||||
version = "3.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8"
|
||||
dependencies = [
|
||||
"local-waker",
|
||||
"pin-project-lite",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-web"
|
||||
version = "4.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd3cb42f9566ab176e1ef0b8b3a896529062b4efc6be0123046095914c4c1c96"
|
||||
dependencies = [
|
||||
"actix-codec",
|
||||
"actix-http",
|
||||
"actix-macros",
|
||||
"actix-router",
|
||||
"actix-rt",
|
||||
"actix-server",
|
||||
"actix-service",
|
||||
"actix-utils",
|
||||
"actix-web-codegen",
|
||||
"ahash 0.7.6",
|
||||
"bytes",
|
||||
"bytestring",
|
||||
"cfg-if",
|
||||
"cookie",
|
||||
"derive_more",
|
||||
"encoding_rs",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
"http",
|
||||
"itoa",
|
||||
"language-tags",
|
||||
"log",
|
||||
"mime",
|
||||
"once_cell",
|
||||
"pin-project-lite",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_urlencoded",
|
||||
"smallvec",
|
||||
"socket2 0.4.9",
|
||||
"time 0.3.20",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "actix-web-codegen"
|
||||
version = "4.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2262160a7ae29e3415554a3f1fc04c764b1540c116aa524683208078b7a75bc9"
|
||||
dependencies = [
|
||||
"actix-router",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "addr2line"
|
||||
version = "0.21.0"
|
||||
@@ -210,18 +28,6 @@ dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.8.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"getrandom",
|
||||
"once_cell",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "1.1.2"
|
||||
@@ -231,21 +37,6 @@ dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "alloc-no-stdlib"
|
||||
version = "2.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "35ef4730490ad1c4eae5c4325b2a95f521d023e5c885853ff7aca0a6a1631db3"
|
||||
|
||||
[[package]]
|
||||
name = "alloc-stdlib"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "697ed7edc0f1711de49ce108c541623a0af97c6c60b2f6e2b65229847ac843c2"
|
||||
dependencies = [
|
||||
"alloc-no-stdlib",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "android_system_properties"
|
||||
version = "0.1.5"
|
||||
@@ -255,55 +46,6 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is-terminal",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b"
|
||||
dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.66"
|
||||
@@ -418,27 +160,6 @@ dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "brotli"
|
||||
version = "3.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68"
|
||||
dependencies = [
|
||||
"alloc-no-stdlib",
|
||||
"alloc-stdlib",
|
||||
"brotli-decompressor",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "brotli-decompressor"
|
||||
version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59ad2d4653bf5ca36ae797b1f4bb4dbddb60ce49ca4aed8a2ce4829f60425b80"
|
||||
dependencies = [
|
||||
"alloc-no-stdlib",
|
||||
"alloc-stdlib",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.12.0"
|
||||
@@ -457,23 +178,11 @@ version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
|
||||
|
||||
[[package]]
|
||||
name = "bytestring"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90706ba19e97b90786e19dc0d5e2abd80008d99d4c0c5d1ad0b5e72cec7c494d"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.73"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
|
||||
dependencies = [
|
||||
"jobserver",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
@@ -497,63 +206,12 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93aae7a4192245f70fe75dd9157fc7b4a5bf53e88d30bd4396f7d8f9284d5acc"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4f423e341edefb78c9caba2d9c7f7687d0e72e89df3ce3394554754393ac3990"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"bitflags 1.3.2",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
|
||||
|
||||
[[package]]
|
||||
name = "const-oid"
|
||||
version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
|
||||
|
||||
[[package]]
|
||||
name = "cookie"
|
||||
version = "0.16.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
"time 0.3.20",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.3"
|
||||
@@ -599,19 +257,6 @@ dependencies = [
|
||||
"zeroize",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_more"
|
||||
version = "0.99.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321"
|
||||
dependencies = [
|
||||
"convert_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustc_version",
|
||||
"syn 1.0.104",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "diff"
|
||||
version = "0.1.12"
|
||||
@@ -643,19 +288,6 @@ dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_logger"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0"
|
||||
dependencies = [
|
||||
"humantime",
|
||||
"is-terminal",
|
||||
"log",
|
||||
"regex",
|
||||
"termcolor",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.1"
|
||||
@@ -972,7 +604,7 @@ version = "0.12.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "607c8a29735385251a339424dd462993c0fed8fa09d378f259377df08c126022"
|
||||
dependencies = [
|
||||
"ahash 0.7.6",
|
||||
"ahash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1054,12 +686,6 @@ version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
|
||||
|
||||
[[package]]
|
||||
name = "humantime"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
|
||||
|
||||
[[package]]
|
||||
name = "hyper"
|
||||
version = "0.14.20"
|
||||
@@ -1145,17 +771,12 @@ dependencies = [
|
||||
name = "integration-tests"
|
||||
version = "0.4.1"
|
||||
dependencies = [
|
||||
"actix-rt",
|
||||
"actix-web",
|
||||
"anyhow",
|
||||
"cc",
|
||||
"env_logger",
|
||||
"lazy_static",
|
||||
"log",
|
||||
"pretty_assertions",
|
||||
"taskchampion",
|
||||
"taskchampion-lib",
|
||||
"taskchampion-sync-server",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
@@ -1176,18 +797,6 @@ version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
|
||||
|
||||
[[package]]
|
||||
name = "is-terminal"
|
||||
version = "0.4.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f"
|
||||
dependencies = [
|
||||
"hermit-abi 0.3.1",
|
||||
"io-lifetimes",
|
||||
"rustix",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itertools"
|
||||
version = "0.10.5"
|
||||
@@ -1203,15 +812,6 @@ version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d"
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.59"
|
||||
@@ -1235,12 +835,6 @@ dependencies = [
|
||||
"simple_asn1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "language-tags"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388"
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.4.0"
|
||||
@@ -1296,24 +890,6 @@ version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519"
|
||||
|
||||
[[package]]
|
||||
name = "local-channel"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f303ec0e94c6c54447f84f3b0ef7af769858a9c4ef56ef2a986d3dcd4c3fc9c"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
"futures-util",
|
||||
"local-waker",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "local-waker"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e34f76eb3611940e0e7d53a9aaa4e6a3151f69541a282fd0dad5571420c53ff1"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.7"
|
||||
@@ -1380,7 +956,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
@@ -1464,12 +1039,6 @@ dependencies = [
|
||||
"windows-sys 0.45.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "paste"
|
||||
version = "1.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79"
|
||||
|
||||
[[package]]
|
||||
name = "pem"
|
||||
version = "1.1.1"
|
||||
@@ -1810,12 +1379,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls"
|
||||
version = "0.21.7"
|
||||
version = "0.21.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8"
|
||||
checksum = "7fecbfb7b1444f477b345853b1fce097a2c6fb637b2bfb87e6bc5db0f043fae4"
|
||||
dependencies = [
|
||||
"log",
|
||||
"ring 0.16.20",
|
||||
"ring 0.17.3",
|
||||
"rustls-webpki",
|
||||
"sct",
|
||||
]
|
||||
@@ -1831,12 +1400,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustls-webpki"
|
||||
version = "0.101.6"
|
||||
version = "0.101.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c7d5dece342910d9ba34d259310cae3e0154b873b35408b787b59bce53d34fe"
|
||||
checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
|
||||
dependencies = [
|
||||
"ring 0.16.20",
|
||||
"untrusted 0.7.1",
|
||||
"ring 0.17.3",
|
||||
"untrusted 0.9.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1928,17 +1497,6 @@ dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1"
|
||||
version = "0.10.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.6"
|
||||
@@ -1950,15 +1508,6 @@ dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "signal-hook-registry"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simple_asn1"
|
||||
version = "0.6.2"
|
||||
@@ -2025,12 +1574,6 @@ dependencies = [
|
||||
"der",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.25.0"
|
||||
@@ -2039,9 +1582,9 @@ checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125"
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.25.0"
|
||||
version = "0.25.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe9f3bd7d2e45dcc5e265fbb88d6513e4747d8ef9444cf01a533119bce28a157"
|
||||
checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
@@ -2095,6 +1638,7 @@ dependencies = [
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"ureq",
|
||||
"url",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
@@ -2109,27 +1653,6 @@ dependencies = [
|
||||
"taskchampion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "taskchampion-sync-server"
|
||||
version = "0.4.1"
|
||||
dependencies = [
|
||||
"actix-rt",
|
||||
"actix-web",
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"clap",
|
||||
"env_logger",
|
||||
"futures",
|
||||
"log",
|
||||
"pretty_assertions",
|
||||
"rusqlite",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.6.0"
|
||||
@@ -2144,15 +1667,6 @@ dependencies = [
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termcolor"
|
||||
version = "1.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.37"
|
||||
@@ -2227,9 +1741,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.36.0"
|
||||
version = "1.37.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931"
|
||||
checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787"
|
||||
dependencies = [
|
||||
"backtrace",
|
||||
"bytes",
|
||||
@@ -2238,7 +1752,6 @@ dependencies = [
|
||||
"num_cpus",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
"socket2 0.5.5",
|
||||
"tokio-macros",
|
||||
"windows-sys 0.48.0",
|
||||
@@ -2292,7 +1805,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d0ecdcb44a79f0fe9844f0c4f33a342cbcbb5117de8001e6ba0dc2351327d09"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"log",
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
"tracing-core",
|
||||
@@ -2411,17 +1923,11 @@ version = "2.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.7.0"
|
||||
version = "1.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a"
|
||||
checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"serde",
|
||||
@@ -2593,15 +2099,6 @@ version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
@@ -2835,33 +2332,3 @@ name = "zeroize"
|
||||
version = "1.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d"
|
||||
|
||||
[[package]]
|
||||
name = "zstd"
|
||||
version = "0.12.3+zstd.1.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76eea132fb024e0e13fd9c2f5d5d595d8a967aa72382ac2f9d39fcc95afd0806"
|
||||
dependencies = [
|
||||
"zstd-safe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd-safe"
|
||||
version = "6.0.5+zstd.1.5.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d56d9e60b4b1758206c238a10165fbcae3ca37b01744e394c463463f6529d23b"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"zstd-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zstd-sys"
|
||||
version = "2.0.8+zstd.1.5.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5556e6ee25d32df2586c098bbfa278803692a20d0ab9565e049480d52707ec8c"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
members = [
|
||||
"taskchampion/taskchampion",
|
||||
"taskchampion/sync-server",
|
||||
"taskchampion/lib",
|
||||
"taskchampion/integration-tests",
|
||||
"taskchampion/xtask",
|
||||
@@ -16,17 +15,12 @@ exclude = [ "src/tc/rust" ]
|
||||
# All Rust dependencies are defined here, and then referenced by the
|
||||
# Cargo.toml's in the members with `foo.workspace = true`.
|
||||
[workspace.dependencies]
|
||||
actix-rt = "2"
|
||||
actix-web = "^4.3.1"
|
||||
anyhow = "1.0"
|
||||
byteorder = "1.5"
|
||||
cc = "1.0.73"
|
||||
chrono = { version = "^0.4.22", features = ["serde"] }
|
||||
clap = { version = "^4.3.0", features = ["string"] }
|
||||
env_logger = "^0.10.0"
|
||||
ffizz-header = "0.5"
|
||||
flate2 = "1"
|
||||
futures = "^0.3.25"
|
||||
google-cloud-storage = { version = "0.15.0", default-features = false, features = ["rustls-tls", "auth"] }
|
||||
lazy_static = "1"
|
||||
libc = "0.2.136"
|
||||
@@ -44,4 +38,5 @@ tempfile = "3"
|
||||
tokio = { version = "1", features = ["rt-multi-thread"] }
|
||||
thiserror = "1.0"
|
||||
ureq = { version = "^2.9.0", features = ["tls"] }
|
||||
uuid = { version = "^1.7.0", features = ["serde", "v4"] }
|
||||
uuid = { version = "^1.8.0", features = ["serde", "v4"] }
|
||||
url = { version = "2" }
|
||||
|
||||
17
ChangeLog
17
ChangeLog
@@ -1,4 +1,15 @@
|
||||
------ current release ---------------------------
|
||||
|
||||
3.0.1 -
|
||||
|
||||
- Fix an error in creation of the 3.0.0 tarball which caused builds to fail (#3302)
|
||||
- Improvements to `task news`, including notes for the 3.0.0 release
|
||||
- Minor improvements to documentation and error handling
|
||||
- Fix incorrect task ID of 0 when using hooks (#3339)
|
||||
- Issue a warning if .data files remain (#3321)
|
||||
|
||||
------ old releases ------------------------------
|
||||
|
||||
3.0.0 -
|
||||
|
||||
- [BREAKING CHANGE] the sync functionality has been rewritten entirely, and
|
||||
@@ -19,7 +30,9 @@
|
||||
- `taskd.server`
|
||||
- `taskd.trust`
|
||||
|
||||
The Taskwarrior build no longer requires GnuTLS.
|
||||
The Taskwarrior build no longer requires GnuTLS. The build option
|
||||
`ENABLE_SYNC=OFF` is also no longer supported; sync support is always built
|
||||
in.
|
||||
|
||||
Deep thanks to the following for contributions to this work:
|
||||
|
||||
@@ -229,8 +242,6 @@
|
||||
Thanks to bharatvaj for contributing.
|
||||
- TW #2581 Config entry with a trailing comment cannot be modified
|
||||
|
||||
------ old releases ------------------------------
|
||||
|
||||
2.5.3 (2021-01-05) - 2f47226f91f0b02f7617912175274d9eed85924f
|
||||
|
||||
- #2375 task hangs then dies when certain tasks are present in a report
|
||||
|
||||
152
NEWS
152
NEWS
@@ -1,152 +0,0 @@
|
||||
New Features in Taskwarrior 2.6.0
|
||||
|
||||
- The logic behind new-uuid verbosity option changed. New-uuid now overrides
|
||||
new-id if set and will cause Taskwarrior to display UUIDs instead of IDs
|
||||
for new tasks (machine friendly).
|
||||
- If ~/.taskrc is not found, Taskwarrior will look for its configuration in
|
||||
$XDG_CONFIG_HOME/task/taskrc (defaulting to ~/.config/task/taskrc). This
|
||||
allows users to setup their Taskwarrior to follow XDG standard without
|
||||
using config overrides.
|
||||
- Newer Unicode characters, such as emojis are correctly handled and displayed.
|
||||
Taskwarrior now supports all Unicode characters up to Unicode 12.
|
||||
- Datetime values until year 9999 are now supported.
|
||||
Duration values of up to 1 000 000 years are now supported.
|
||||
- 64-bit numeric values (up to 9,223,372,036,854,775,807) are now supported.
|
||||
- Later/someday named datetime values now resolve to 9999-12-30 (instead of
|
||||
2038-01-18).
|
||||
- Calendar now supports displaying due dates until year 9999.
|
||||
- Calendar now displays waiting tasks with due dates on the calendar.
|
||||
- Calendar supports highlighting days with scheduled tasks.
|
||||
- Multi-day holidays are now supported.
|
||||
- Holiday data files for fr-CA, hu-HU, pt-BR, sk-SK and sv-FI locales are now
|
||||
generated and shipped with Taskwarrior.
|
||||
- The task edit command can now handle multi-line annotations and UDAs in a
|
||||
user friendly way, withouth having to handle with JSON escaping of special
|
||||
chars.
|
||||
- A large portion of currently known parser-related issues was fixed.
|
||||
- The taskrc file now supports relative paths, which are evaluated with
|
||||
respect to (a) current directory, (b) taskrc directory and (c) now also the
|
||||
installation directory of configuration files.
|
||||
- The currently selected context is now applied for "task add" and "task log"
|
||||
commands. Section on contexts in the manpage was updated to describe this
|
||||
functionality.
|
||||
- Users can specify per-context specific overrides of configuration variables.
|
||||
- The `task import` command can now accept annotations with missing entry
|
||||
values. Current time will be assumed.
|
||||
- The new 'by' filter attribute modifier compares using '<=' rather than '<'
|
||||
as 'before' uses. This allows the last second of the day to match with
|
||||
'due.by:eod', which it would not otherwise. It also works with
|
||||
whole units like days, e.g. 'add test due:2021-07-17' would not match
|
||||
'due.before:tomorrow' (on the 16th), but would match 'due.by:tomorrow'.
|
||||
- Waiting is now an entirely "virtual" concept, based on a task's
|
||||
'wait' property and the current time. Task is considered "waiting" if its
|
||||
wait attribute is in the future. TaskWarrior no longer explicitly
|
||||
"unwaits" a task (the wait attribute is not removed once its value is in
|
||||
the past), so the "unwait' verbosity token is no longer available.
|
||||
This allows for filtering for tasks that were waiting in the past
|
||||
intervals, but are not waiting anymore.
|
||||
- The configuration file now supports environment variables.
|
||||
- Taskwarrior can now handle displaying tasks in windows with limited width,
|
||||
even if columns contain long strings (like URLs).
|
||||
- The nag message is emitted at most once per task command, even with bulk
|
||||
operations. Additionally, the urgency of the task considered is taken
|
||||
before the completion, not after.
|
||||
- The export command now takes an optional argument that references an
|
||||
existing report. As such, "task export <report>" command will export
|
||||
the same tasks that "task <report>" prints, and in the same order.
|
||||
- The burndown command now supports non-cumulative display, where tasks only
|
||||
get plotted within the interval segment when they got completed.
|
||||
|
||||
|
||||
New Commands in Taskwarrior 2.6.0
|
||||
|
||||
- The 'news' command will guide the user through important release notes
|
||||
anytime a new version of Taskwarrior is installed. It provides personalized
|
||||
feedback, deprecation warnings and usage advice, where applicable.
|
||||
|
||||
New Configuration Options in Taskwarrior 2.6.0
|
||||
|
||||
- The context definitions for reporting commmands are now stored in
|
||||
"context.<name>.read". Context definitions for write commands are now
|
||||
supported using "context.<name>.write" configuration variable.
|
||||
- The context-specific configuration overrides are now supported. Use
|
||||
context.<name>.rc.<key>=<value> to override, such as
|
||||
context.work.rc.urgency.blocking=5.0 to override the value of urgency.blocking
|
||||
when the 'work' context is active.
|
||||
- Each report (and the timesheet command) can explicitly opt-out from the
|
||||
currently active context by setting the report.<name>.context variable to 0
|
||||
(defaults to 1). Useful for defining universal reports that ignore
|
||||
currently set context, such as 'inbox' report for GTD methodology.
|
||||
- Multi-day holidays are now supported. Use holiday.<name>.start=<date> and
|
||||
holiday.<name>.end=<date> to specify a range-based holiday, such as a
|
||||
vacation.
|
||||
- Verbosity token 'default' was introduced in order to display information
|
||||
about default actions.
|
||||
- The new burndown.cumulative option can be used to toggle between
|
||||
non-cumulative and cumulative version of the burndown command.
|
||||
- The new color.calendar.scheduled setting can be used to control the
|
||||
highlighting color of days in the calendar that have scheduled tasks.
|
||||
|
||||
|
||||
Newly Deprecated Features in Taskwarrior 2.6.0
|
||||
|
||||
- The 'PARENT' and 'CHILD' virtual tags are replaced by 'TEMPLATE' and 'INSTANCE'.
|
||||
- The 'waiting' status is now deprecated. We recommend using +WAITING virtual tag
|
||||
or wait-attribute based filters, such as 'wait.before:eow' instead.
|
||||
- The configuration variable 'monthsperline' is deprecated. Please use
|
||||
'calendar.monthsperline' instead.
|
||||
|
||||
Fixed regressions in 2.6.0
|
||||
|
||||
- The "end of <date>" named dates ('eod', 'eow', ...) were pointing to the
|
||||
first second of the next day, instead of last second of the referenced
|
||||
interval. This was a regression introduced in 2.5.2.
|
||||
- The "eow" and "eonw" were using a different weekday as a reference. This
|
||||
was a regeression introduced in 2.5.2.
|
||||
- The rc.verbose=<value> configuration override was applied only if it were
|
||||
the first configuration override. In #2247, this manifested itself as
|
||||
inability to supress footnotes about the overrides, and in #1953 as failure
|
||||
to force task to display UUIDs of on task add. This was a regression
|
||||
introduced in 2.5.2.
|
||||
- The attribute values of the form "<attribute name>-<arbitrary string>", for
|
||||
example "due-nextweek" or "scheduled-work" would fail to parse (see
|
||||
#1913). This was a regression introduced in 2.5.1.
|
||||
- The capitalized versions of named dates (such as Monday, February or
|
||||
Tomorrow) are again supported. This was a regression introduced in 2.5.2.
|
||||
- The duration periods are converted to datetime values using the
|
||||
current time as the anchor, as opposed to the beginning of unix time.
|
||||
This was a regression in 2.5.2.
|
||||
- Filtering for attribute values containing dashes and numbers (such as
|
||||
'vs.2021-01', see #2392) or spaces (such as "Home renovation", see #2388)
|
||||
is again supported. This was a regression introduced in 2.4.0.
|
||||
|
||||
Removed Features in 2.6.0
|
||||
|
||||
-
|
||||
|
||||
Other notable changes in 2.6.0
|
||||
|
||||
- C++17 compatible compiler is now required (GCC 7.1 or older / clang 5.0 or older).
|
||||
|
||||
Known Issues
|
||||
|
||||
- https://github.com/GothenburgBitFactory/taskwarrior
|
||||
|
||||
Taskwarrior 2.6.0 has been built and tested on the following configurations:
|
||||
|
||||
* Archlinux
|
||||
* OpenSUSE
|
||||
* macOS 10.15
|
||||
* Fedora (31, 32, 33, 34)
|
||||
* Ubuntu (18.04, 20.04, 21.04)
|
||||
* Debian (Stable, Testing)
|
||||
* CentOS (7, 8)
|
||||
|
||||
However, we expect Taskwarrior to work on other platforms as well.
|
||||
|
||||
---
|
||||
|
||||
While Taskwarrior has undergone testing, bugs are sure to remain. If you
|
||||
encounter a bug, please enter a new issue at:
|
||||
|
||||
https://github.com/GothenburgBitFactory/taskwarrior
|
||||
@@ -5,3 +5,4 @@
|
||||
* [Coding Style](coding_style.md)
|
||||
* [Branching Model](branching.md)
|
||||
* [Rust and C++](rust-and-c++.md)
|
||||
* [Releasing Taskwarrior](releasing.md)
|
||||
|
||||
@@ -12,9 +12,13 @@ See the [TaskChampion CONTRIBUTING guide](../../../taskchampion/CONTRIBUTING.md)
|
||||
* CMake 3.0 or later
|
||||
* gcc 7.0 or later, clang 6.0 or later, or a compiler with full C++17 support
|
||||
* libuuid (if not on macOS)
|
||||
* python 3 (optional, for running the test suite)
|
||||
* Rust 1.64.0 or higher (hint: use https://rustup.rs/ instead of using your system's package manager)
|
||||
|
||||
## Install Optional Dependencies:
|
||||
* python 3 (for running the test suite)
|
||||
* clangd or ccls (for C++ integration in many editors)
|
||||
* rust-analyzer (for Rust integration in many editors)
|
||||
|
||||
## Obtain and Build Code:
|
||||
The following documentation works with CMake 3.14 and later.
|
||||
Here are the minimal steps to get started, using an out of source build directory and calling the underlying build tool over the CMake interface.
|
||||
@@ -24,7 +28,7 @@ See the general CMake man pages or the [cmake-documentation](https://cmake.org/c
|
||||
```sh
|
||||
git clone https://github.com/GothenburgBitFactory/taskwarrior
|
||||
cd taskwarrior
|
||||
cmake -S . -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo
|
||||
cmake -S . -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo
|
||||
cmake --build build
|
||||
```
|
||||
Other possible build types can be `Release` and `Debug`.
|
||||
|
||||
24
doc/devel/contrib/releasing.md
Normal file
24
doc/devel/contrib/releasing.md
Normal file
@@ -0,0 +1,24 @@
|
||||
# Releasing Taskwarrior
|
||||
|
||||
To release Taskwarrior, follow this process:
|
||||
|
||||
- Examine the changes since the last version, and update `src/commands/CmdNews.cpp` accordingly.
|
||||
There are instructions at the top of the file.
|
||||
- Create a release PR
|
||||
- Update version in CMakeLists.txt
|
||||
- Update Changelog
|
||||
- get this merged
|
||||
- On `develop` after that PR merges, create a release tarball:
|
||||
- `git clone . release-tarball`
|
||||
- `cd release-tarball/`
|
||||
- edit `Cargo.toml` to contain only `taskchampion` and `taskchampion-lib` in `members` (see https://github.com/GothenburgBitFactory/taskwarrior/issues/3294).
|
||||
- `cmake -S. -Bbuild`
|
||||
- `make -Cbuild package_source`
|
||||
- copy build/task-*.tar.gz elsewhere and delete the `release-tarball` dir
|
||||
- NOTE: older releases had a `test-*.tar.gz` but it's unclear how to generate this
|
||||
- Update `stable` to the released commit and push upstream
|
||||
- Tag the commit as vX.Y.Z and push the tag upstream
|
||||
- Find the tag under https://github.com/GothenburgBitFactory/taskwarrior/tags and create a release from it
|
||||
- Give it a clever title if you can think of one; refer to previous releases
|
||||
- Include the tarball from earlier
|
||||
- Add a new item in `content/news` on https://github.com/GothenburgBitFactory/tw.org
|
||||
@@ -73,6 +73,8 @@ Configure Taskwarrior with these details:
|
||||
$ task config sync.server.origin <origin>
|
||||
$ task config sync.server.client_id <client_id>
|
||||
|
||||
Note that the origin must include the scheme, such as 'http://' or 'https://'.
|
||||
|
||||
.SS Google Cloud Platform
|
||||
|
||||
To synchronize your tasks to GCP, use the GCP Console to create a new project,
|
||||
@@ -142,16 +144,8 @@ Users are identified by a client ID, and users with different client IDs are
|
||||
entirely independent. Task data is encrypted by Taskwarrior, and the sync
|
||||
server never sees un-encrypted data.
|
||||
|
||||
To start the server, run it in your preferred HTTP hosting environment, using
|
||||
`--port` to set the TCP port on which it should listen. It is recommended to
|
||||
use TLS to protect communications with the server, but this is not required.
|
||||
|
||||
The server stores its data in a database, the path to which is given by the
|
||||
`--data-dir` argument, defaulting to "/var/lib/taskchampion-sync-server".
|
||||
|
||||
For example:
|
||||
|
||||
$ taskchampion-sync-server --port 8443 --data-dir /storage/taskdata
|
||||
The server is developed in
|
||||
https://github.com/GothenburgBitFactory/taskchampion-sync-server.
|
||||
|
||||
.SS Adding a New User
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ add_library (task STATIC CLI2.cpp CLI2.h
|
||||
TDB2.cpp TDB2.h
|
||||
Task.cpp Task.h
|
||||
Variant.cpp Variant.h
|
||||
Version.cpp Version.h
|
||||
ViewTask.cpp ViewTask.h
|
||||
dependency.cpp
|
||||
feedback.cpp
|
||||
|
||||
29
src/TDB2.cpp
29
src/TDB2.cpp
@@ -29,6 +29,7 @@
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <algorithm>
|
||||
#include <vector>
|
||||
#include <list>
|
||||
#include <unordered_set>
|
||||
#include <stdlib.h>
|
||||
@@ -57,6 +58,14 @@ TDB2::TDB2 ()
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
void TDB2::open_replica (const std::string& location, bool create_if_missing)
|
||||
{
|
||||
File pending_data = File (location + "/pending.data");
|
||||
if (pending_data.exists()) {
|
||||
Color warning = Color (Context::getContext ().config.get ("color.warning"));
|
||||
std::cerr << warning.colorize (
|
||||
format ("Found existing '.data' files in {1}", location)) << "\n";
|
||||
std::cerr << " Taskwarrior's storage format changed in 3.0, requiring a manual migration.\n";
|
||||
std::cerr << " See https://github.com/GothenburgBitFactory/taskwarrior/releases.\n";
|
||||
}
|
||||
replica = tc::Replica(location, create_if_missing);
|
||||
}
|
||||
|
||||
@@ -70,6 +79,8 @@ void TDB2::add (Task& task)
|
||||
task.validate (true);
|
||||
|
||||
std::string uuid = task.get ("uuid");
|
||||
changes[uuid] = task;
|
||||
|
||||
auto innertask = replica.import_task_with_uuid (uuid);
|
||||
|
||||
{
|
||||
@@ -79,7 +90,7 @@ void TDB2::add (Task& task)
|
||||
for (auto& attr : task.all ()) {
|
||||
// TaskChampion does not store uuid or id in the taskmap
|
||||
if (attr == "uuid" || attr == "id") {
|
||||
continue;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Use `set_status` for the task status, to get expected behavior
|
||||
@@ -110,12 +121,13 @@ void TDB2::add (Task& task)
|
||||
// update the cached working set with the new information
|
||||
_working_set = std::make_optional (std::move (ws));
|
||||
|
||||
if (id.has_value ()) {
|
||||
task.id = id.value();
|
||||
}
|
||||
|
||||
// run hooks for this new task
|
||||
Context::getContext ().hooks.onAdd (task);
|
||||
|
||||
if (id.has_value ()) {
|
||||
task.id = id.value();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
@@ -141,6 +153,8 @@ void TDB2::modify (Task& task)
|
||||
task.validate (false);
|
||||
auto uuid = task.get ("uuid");
|
||||
|
||||
changes[uuid] = task;
|
||||
|
||||
// invoke the hook and allow it to modify the task before updating
|
||||
Task original;
|
||||
get (uuid, original);
|
||||
@@ -200,9 +214,10 @@ const tc::WorkingSet &TDB2::working_set ()
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
void TDB2::get_changes (std::vector <Task>& changes)
|
||||
{
|
||||
// TODO: changes in an invocation of `task` are not currently tracked, so this
|
||||
// list is always empty.
|
||||
std::map<std::string, Task>& changes_map = this->changes;
|
||||
changes.clear();
|
||||
std::transform(changes_map.begin(), changes_map.end(), std::back_inserter(changes),
|
||||
[](const auto& kv) { return kv.second; });
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
@@ -84,6 +84,9 @@ private:
|
||||
tc::Replica replica;
|
||||
std::optional<tc::WorkingSet> _working_set;
|
||||
|
||||
// UUID -> Task containing all tasks modified in this invocation.
|
||||
std::map<std::string, Task> changes;
|
||||
|
||||
const tc::WorkingSet &working_set ();
|
||||
static std::string option_string (std::string input);
|
||||
static void show_diff (const std::string&, const std::string&, const std::string&);
|
||||
|
||||
118
src/Version.cpp
Normal file
118
src/Version.cpp
Normal file
@@ -0,0 +1,118 @@
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Copyright 2024, Dustin 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 <Version.h>
|
||||
#include <cmake.h>
|
||||
#include <iostream>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
#include <vector>
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
Version::Version(std::string version) {
|
||||
std::vector<int> parts;
|
||||
std::string part;
|
||||
std::istringstream input(version);
|
||||
|
||||
while (std::getline(input, part, '.')) {
|
||||
int value;
|
||||
// Try converting string to integer
|
||||
if (std::stringstream(part) >> value && value >= 0) {
|
||||
parts.push_back(value);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (parts.size() != 3) {
|
||||
return;
|
||||
}
|
||||
|
||||
major = parts[0];
|
||||
minor = parts[1];
|
||||
patch = parts[2];
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
Version Version::Current() { return Version(PACKAGE_VERSION); }
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
bool Version::is_valid() const { return major >= 0; }
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
bool Version::operator<(const Version &other) const {
|
||||
return std::tie(major, minor, patch) <
|
||||
std::tie(other.major, other.minor, other.patch);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
bool Version::operator<=(const Version &other) const {
|
||||
return std::tie(major, minor, patch) <=
|
||||
std::tie(other.major, other.minor, other.patch);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
bool Version::operator>(const Version &other) const {
|
||||
return std::tie(major, minor, patch) >
|
||||
std::tie(other.major, other.minor, other.patch);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
bool Version::operator>=(const Version &other) const {
|
||||
return std::tie(major, minor, patch) >=
|
||||
std::tie(other.major, other.minor, other.patch);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
bool Version::operator==(const Version &other) const {
|
||||
return std::tie(major, minor, patch) ==
|
||||
std::tie(other.major, other.minor, other.patch);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
bool Version::operator!=(const Version &other) const {
|
||||
std::cout << other;
|
||||
return std::tie(major, minor, patch) !=
|
||||
std::tie(other.major, other.minor, other.patch);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
Version::operator std::string() const {
|
||||
std::ostringstream output;
|
||||
if (is_valid()) {
|
||||
output << major << '.' << minor << '.' << patch;
|
||||
} else {
|
||||
output << "(invalid version)";
|
||||
}
|
||||
return output.str();
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
std::ostream &operator<<(std::ostream &os, const Version &version) {
|
||||
os << std::string(version);
|
||||
return os;
|
||||
}
|
||||
72
src/Version.h
Normal file
72
src/Version.h
Normal file
@@ -0,0 +1,72 @@
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Copyright 2024, Dustin 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_VERSION
|
||||
#define INCLUDED_VERSION
|
||||
|
||||
#include <string>
|
||||
|
||||
// A utility class for handling Taskwarrior versions.
|
||||
class Version {
|
||||
public:
|
||||
// Parse a version from a string. This must be of the format
|
||||
// digits.digits.digits.
|
||||
explicit Version(std::string version);
|
||||
|
||||
// Create an invalid version.
|
||||
Version() = default;
|
||||
|
||||
Version(const Version &other) = default;
|
||||
Version(Version &&other) = default;
|
||||
Version &operator=(const Version &) = default;
|
||||
Version &operator=(Version &&) = default;
|
||||
|
||||
// Return a version representing the release being built.
|
||||
static Version Current();
|
||||
|
||||
bool is_valid() const;
|
||||
|
||||
// Compare versions.
|
||||
bool operator<(const Version &) const;
|
||||
bool operator<=(const Version &) const;
|
||||
bool operator>(const Version &) const;
|
||||
bool operator>=(const Version &) const;
|
||||
bool operator==(const Version &) const;
|
||||
bool operator!=(const Version &) const;
|
||||
|
||||
// Convert back to a string.
|
||||
operator std::string() const;
|
||||
|
||||
friend std::ostream& operator<<(std::ostream& os, const Version& version);
|
||||
|
||||
private:
|
||||
int major = -1;
|
||||
int minor = -1;
|
||||
int patch = -1;
|
||||
};
|
||||
|
||||
#endif
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
@@ -35,6 +35,7 @@
|
||||
#include <Context.h>
|
||||
#include <Filter.h>
|
||||
#include <Lexer.h>
|
||||
#include <Version.h>
|
||||
#include <ViewTask.h>
|
||||
#include <format.h>
|
||||
#include <shared.h>
|
||||
@@ -250,24 +251,24 @@ int CmdCustom::execute (std::string& output)
|
||||
}
|
||||
|
||||
// Inform user about the new release highlights if not presented yet
|
||||
if (Context::getContext ().config.get ("news.version") != "2.6.0")
|
||||
Version news_version(Context::getContext ().config.get ("news.version"));
|
||||
Version current_version = Version::Current();
|
||||
if (news_version != current_version)
|
||||
{
|
||||
std::random_device device;
|
||||
std::mt19937 random_generator(device());
|
||||
std::uniform_int_distribution<std::mt19937::result_type> twentyfive_percent(1, 4);
|
||||
|
||||
std::string NEWS_NOTICE = (
|
||||
"Recently upgraded to 2.6.0. "
|
||||
"Please run 'task news' to read highlights about the new release."
|
||||
);
|
||||
|
||||
// 1 in 10 chance to display the message.
|
||||
if (twentyfive_percent(random_generator) == 4)
|
||||
{
|
||||
std::ostringstream notice;
|
||||
notice << "Recently upgraded to " << current_version << ". "
|
||||
"Please run 'task news' to read highlights about the new release.";
|
||||
if (Context::getContext ().verbose ("footnote"))
|
||||
Context::getContext ().footnote (NEWS_NOTICE);
|
||||
Context::getContext ().footnote (notice.str());
|
||||
else if (Context::getContext ().verbose ("header"))
|
||||
Context::getContext ().header (NEWS_NOTICE);
|
||||
Context::getContext ().header (notice.str());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -38,6 +38,14 @@
|
||||
#include <util.h>
|
||||
#include <main.h>
|
||||
|
||||
/* Adding a new version:
|
||||
*
|
||||
* - Add a new `versionX_Y_Z` method to `NewsItem`, and add news items for the new
|
||||
* release.
|
||||
* - Call the new method in `NewsItem.all()`. Calls should be in version order.
|
||||
* - Test with `task news`.
|
||||
*/
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
CmdNews::CmdNews ()
|
||||
{
|
||||
@@ -91,6 +99,7 @@ void wait_for_enter ()
|
||||
// Holds information about single improvement / bug.
|
||||
//
|
||||
NewsItem::NewsItem (
|
||||
Version version,
|
||||
bool major,
|
||||
const std::string& title,
|
||||
const std::string& bg_title,
|
||||
@@ -100,6 +109,7 @@ NewsItem::NewsItem (
|
||||
const std::string& reasoning,
|
||||
const std::string& actions
|
||||
) {
|
||||
_version = version;
|
||||
_major = major;
|
||||
_title = title;
|
||||
_bg_title = bg_title;
|
||||
@@ -127,7 +137,7 @@ void NewsItem::render () {
|
||||
|
||||
// TODO: For some reason, bold cannot be blended in 256-color terminals
|
||||
// Apply this workaround of colorizing twice.
|
||||
std::cout << bold.colorize (header.colorize (format ("{1}\n", _title)));
|
||||
std::cout << bold.colorize (header.colorize (format ("{1} ({2})\n", _title, _version)));
|
||||
if (_background.size ()) {
|
||||
if (_bg_title.empty ())
|
||||
_bg_title = "Background";
|
||||
@@ -138,7 +148,7 @@ void NewsItem::render () {
|
||||
|
||||
wait_for_enter ();
|
||||
|
||||
std::cout << " " << underline.colorize ("What changed in 2.6.0?\n");
|
||||
std::cout << " " << underline.colorize (format ("What changed in {1}?\n", _version));
|
||||
if (_punchline.size ())
|
||||
std::cout << footnote.colorize (format ("{1}\n", _punchline));
|
||||
|
||||
@@ -160,6 +170,13 @@ void NewsItem::render () {
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<NewsItem> NewsItem::all () {
|
||||
std::vector<NewsItem> items;
|
||||
version2_6_0(items);
|
||||
version3_0_0(items);
|
||||
return items;
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
// Generate the highlights for the 2.6.0 version.
|
||||
//
|
||||
@@ -174,7 +191,8 @@ void NewsItem::render () {
|
||||
// - The .by attribute modifier
|
||||
// - Exporting a report
|
||||
// - Multi-day holidays
|
||||
void CmdNews::version2_6_0 (std::vector<NewsItem>& items) {
|
||||
void NewsItem::version2_6_0 (std::vector<NewsItem>& items) {
|
||||
Version version("2.6.0");
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// - Writeable context (major)
|
||||
|
||||
@@ -234,6 +252,7 @@ void CmdNews::version2_6_0 (std::vector<NewsItem>& items) {
|
||||
" Read more about how to use contexts in CONTEXT section of 'man task'.";
|
||||
|
||||
NewsItem writeable_context (
|
||||
version,
|
||||
true,
|
||||
"'Writeable' context",
|
||||
"Background - what is context?",
|
||||
@@ -277,6 +296,7 @@ void CmdNews::version2_6_0 (std::vector<NewsItem>& items) {
|
||||
// - 64-bit datetime support (major)
|
||||
|
||||
NewsItem uint64_support (
|
||||
version,
|
||||
false,
|
||||
"Support for 64-bit timestamps and numeric values",
|
||||
"",
|
||||
@@ -294,6 +314,7 @@ void CmdNews::version2_6_0 (std::vector<NewsItem>& items) {
|
||||
// - Waiting is a virtual status
|
||||
|
||||
NewsItem waiting_status (
|
||||
version,
|
||||
true,
|
||||
"Deprecation of the status:waiting",
|
||||
"",
|
||||
@@ -315,6 +336,7 @@ void CmdNews::version2_6_0 (std::vector<NewsItem>& items) {
|
||||
// - Support for environment variables in the taskrc
|
||||
|
||||
NewsItem env_vars (
|
||||
version,
|
||||
true,
|
||||
"Environment variables in the taskrc",
|
||||
"",
|
||||
@@ -333,6 +355,7 @@ void CmdNews::version2_6_0 (std::vector<NewsItem>& items) {
|
||||
// - Reports outside of context
|
||||
|
||||
NewsItem contextless_reports (
|
||||
version,
|
||||
true,
|
||||
"Context-less reports",
|
||||
"",
|
||||
@@ -354,6 +377,7 @@ void CmdNews::version2_6_0 (std::vector<NewsItem>& items) {
|
||||
// - Exporting a particular report
|
||||
|
||||
NewsItem exportable_reports (
|
||||
version,
|
||||
false,
|
||||
"Exporting a particular report",
|
||||
"",
|
||||
@@ -377,6 +401,7 @@ void CmdNews::version2_6_0 (std::vector<NewsItem>& items) {
|
||||
// - Multi-day holidays
|
||||
|
||||
NewsItem multi_holidays (
|
||||
version,
|
||||
false,
|
||||
"Multi-day holidays",
|
||||
"",
|
||||
@@ -399,6 +424,7 @@ void CmdNews::version2_6_0 (std::vector<NewsItem>& items) {
|
||||
// - Unicode 12
|
||||
|
||||
NewsItem unicode_12 (
|
||||
version,
|
||||
false,
|
||||
"Extended Unicode support (Unicode 12)",
|
||||
"",
|
||||
@@ -417,6 +443,7 @@ void CmdNews::version2_6_0 (std::vector<NewsItem>& items) {
|
||||
// - The .by attribute modifier
|
||||
|
||||
NewsItem by_modifier (
|
||||
version,
|
||||
false,
|
||||
"The .by attribute modifier",
|
||||
"",
|
||||
@@ -435,6 +462,7 @@ void CmdNews::version2_6_0 (std::vector<NewsItem>& items) {
|
||||
// - Context-specific configuration overrides
|
||||
|
||||
NewsItem context_config (
|
||||
version,
|
||||
false,
|
||||
"Context-specific configuration overrides",
|
||||
"",
|
||||
@@ -459,6 +487,7 @@ void CmdNews::version2_6_0 (std::vector<NewsItem>& items) {
|
||||
// - XDG config home support
|
||||
|
||||
NewsItem xdg_support (
|
||||
version,
|
||||
true,
|
||||
"Support for XDG Base Directory Specification",
|
||||
"",
|
||||
@@ -487,6 +516,7 @@ void CmdNews::version2_6_0 (std::vector<NewsItem>& items) {
|
||||
// - Update holiday data
|
||||
|
||||
NewsItem holidata_2022 (
|
||||
version,
|
||||
false,
|
||||
"Updated holiday data for 2022",
|
||||
"",
|
||||
@@ -500,6 +530,28 @@ void CmdNews::version2_6_0 (std::vector<NewsItem>& items) {
|
||||
items.push_back(holidata_2022);
|
||||
}
|
||||
|
||||
void NewsItem::version3_0_0 (std::vector<NewsItem>& items) {
|
||||
Version version("3.0.0");
|
||||
NewsItem sync {
|
||||
version,
|
||||
/*major=*/true,
|
||||
/*title=*/"New data model and sync backend",
|
||||
/*bg_title=*/"",
|
||||
/*background=*/"",
|
||||
/*punchline=*/
|
||||
"The sync functionality for Taskwarrior has been rewritten entirely, and no longer\n"
|
||||
"supports taskserver/taskd. The most robust solution is a cloud-storage backend,\n"
|
||||
"although a less-mature taskchampion-sync-server is also available. See `task-sync(5)`\n"
|
||||
"For details. As part of this change, the on-disk storage format has also changed.\n",
|
||||
/*update=*/
|
||||
"This is a breaking upgrade: you must export your task database from 2.x and re-import\n"
|
||||
"it into 3.x. Hooks run during task import, so if you have any hooks defined,\n"
|
||||
"temporarily disable them for this operation.\n\n"
|
||||
"See https://taskwarrior.org/docs/upgrade-3/ for information on upgrading to Taskwarrior 3.0.",
|
||||
};
|
||||
items.push_back(sync);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////////////////////////////////////
|
||||
int CmdNews::execute (std::string& output)
|
||||
{
|
||||
@@ -509,11 +561,13 @@ int CmdNews::execute (std::string& output)
|
||||
// Supress compiler warning about unused argument
|
||||
output = "";
|
||||
|
||||
// TODO: 2.6.0 is the only version with explicit release notes, but in the
|
||||
// future we need to only execute yet unread release notes
|
||||
std::vector<NewsItem> items;
|
||||
std::string version = "2.6.0";
|
||||
version2_6_0 (items);
|
||||
std::vector<NewsItem> items = NewsItem::all();
|
||||
Version news_version(Context::getContext ().config.get ("news.version"));
|
||||
Version current_version = Version::Current();
|
||||
|
||||
// 2.6.0 is the earliest version with news support.
|
||||
if (!news_version.is_valid())
|
||||
news_version = Version("2.6.0");
|
||||
|
||||
bool full_summary = false;
|
||||
bool major_items = true;
|
||||
@@ -538,6 +592,12 @@ int CmdNews::execute (std::string& output)
|
||||
|
||||
signal (SIGINT, signal_handler);
|
||||
|
||||
// Remove items that have already been shown
|
||||
items.erase (
|
||||
std::remove_if (items.begin (), items.end (), [&](const NewsItem& n){return n._version <= news_version;}),
|
||||
items.end ()
|
||||
);
|
||||
|
||||
// Remove non-major items if displaying a non-full (abbreviated) summary
|
||||
int total_highlights = items.size ();
|
||||
if (! full_summary)
|
||||
@@ -546,23 +606,25 @@ int CmdNews::execute (std::string& output)
|
||||
items.end ()
|
||||
);
|
||||
|
||||
// Print release notes
|
||||
Color bold = Color ("bold");
|
||||
std::cout << bold.colorize (format (
|
||||
"\n"
|
||||
"==========================================\n"
|
||||
"Taskwarrior {1} {2} Release highlights\n"
|
||||
"==========================================\n",
|
||||
version,
|
||||
(full_summary ? "All" : (major_items ? "Major" : "Minor"))
|
||||
));
|
||||
if (items.empty ()) {
|
||||
std::cout << bold.colorize ("You are up to date!\n");
|
||||
} else {
|
||||
// Print release notes
|
||||
std::cout << bold.colorize (format (
|
||||
"\n"
|
||||
"================================================\n"
|
||||
"Taskwarrior {1} through {2} Release Highlights\n"
|
||||
"================================================\n",
|
||||
news_version,
|
||||
current_version));
|
||||
|
||||
for (unsigned short i=0; i < items.size (); i++) {
|
||||
std::cout << format ("\n({1}/{2}) ", i+1, items.size ());
|
||||
items[i].render ();
|
||||
for (unsigned short i=0; i < items.size (); i++) {
|
||||
std::cout << format ("\n({1}/{2}) ", i+1, items.size ());
|
||||
items[i].render ();
|
||||
}
|
||||
std::cout << "Thank you for catching up on the new features!\n";
|
||||
}
|
||||
|
||||
std::cout << "Thank you for catching up on the new features!\n";
|
||||
wait_for_enter ();
|
||||
|
||||
// Display outro
|
||||
@@ -588,9 +650,9 @@ int CmdNews::execute (std::string& output)
|
||||
std::cout << outro.str ();
|
||||
|
||||
// Set a mark in the config to remember which version's release notes were displayed
|
||||
if (config.get ("news.version") != "2.6.0")
|
||||
if (full_summary && news_version != current_version)
|
||||
{
|
||||
CmdConfig::setConfigVariable ("news.version", "2.6.0", false);
|
||||
CmdConfig::setConfigVariable ("news.version", std::string(current_version), false);
|
||||
|
||||
// Revert back to default signal handling after displaying the outro
|
||||
signal (SIGINT, SIG_DFL);
|
||||
@@ -627,14 +689,15 @@ int CmdNews::execute (std::string& output)
|
||||
else
|
||||
wait_for_enter (); // Do not display the outro and footnote at once
|
||||
|
||||
if (! full_summary && major_items)
|
||||
if (! items.empty() && ! full_summary && major_items) {
|
||||
Context::getContext ().footnote (format (
|
||||
"Only major highlights were displayed ({1} out of {2} total).\n"
|
||||
"If you're interested in more release highlights, run 'task news {3} minor'.",
|
||||
items.size (),
|
||||
total_highlights,
|
||||
version
|
||||
current_version
|
||||
));
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -31,9 +31,11 @@
|
||||
#include <Command.h>
|
||||
#include <CmdConfig.h>
|
||||
#include <CmdContext.h>
|
||||
#include <Version.h>
|
||||
|
||||
class NewsItem {
|
||||
public:
|
||||
Version _version;
|
||||
bool _major = false;
|
||||
std::string _title;
|
||||
std::string _bg_title;
|
||||
@@ -42,7 +44,16 @@ public:
|
||||
std::string _update;
|
||||
std::string _reasoning;
|
||||
std::string _actions;
|
||||
|
||||
void render ();
|
||||
|
||||
static std::vector<NewsItem> all();
|
||||
static void version2_6_0 (std::vector<NewsItem>&);
|
||||
static void version3_0_0 (std::vector<NewsItem>&);
|
||||
|
||||
private:
|
||||
NewsItem (
|
||||
Version,
|
||||
bool,
|
||||
const std::string&,
|
||||
const std::string& = "",
|
||||
@@ -52,7 +63,6 @@ public:
|
||||
const std::string& = "",
|
||||
const std::string& = ""
|
||||
);
|
||||
void render ();
|
||||
};
|
||||
|
||||
class CmdNews : public Command
|
||||
@@ -60,7 +70,6 @@ class CmdNews : public Command
|
||||
public:
|
||||
CmdNews ();
|
||||
int execute (std::string&);
|
||||
void version2_6_0 (std::vector<NewsItem>&);
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@@ -88,7 +88,7 @@ int CmdSync::execute (std::string& output)
|
||||
os << "Sync server at " << origin;
|
||||
server_ident = os.str();
|
||||
} else {
|
||||
throw std::string ("No sync.* settings are configured.");
|
||||
throw std::string ("No sync.* settings are configured. See task-sync(5).");
|
||||
}
|
||||
|
||||
std::stringstream out;
|
||||
|
||||
@@ -207,6 +207,11 @@ void feedback_unblocked (const Task& task)
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
void feedback_backlog ()
|
||||
{
|
||||
// If non-local sync is not set up, do not provide this feedback.
|
||||
if (Context::getContext ().config.get ("sync.encryption_secret") == "") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Context::getContext ().verbose ("sync"))
|
||||
{
|
||||
int count = Context::getContext ().tdb2.num_local_changes ();
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
cmake_minimum_required (VERSION 3.22)
|
||||
|
||||
FetchContent_Declare (
|
||||
Corrosion
|
||||
GIT_REPOSITORY https://github.com/corrosion-rs/corrosion.git
|
||||
GIT_TAG v0.4.7
|
||||
)
|
||||
FetchContent_MakeAvailable(Corrosion)
|
||||
add_subdirectory(${CMAKE_SOURCE_DIR}/src/tc/corrosion)
|
||||
|
||||
# Import taskchampion-lib as a CMake library.
|
||||
corrosion_import_crate(
|
||||
|
||||
@@ -39,7 +39,7 @@ tc::Server::new_local (const std::string &server_dir)
|
||||
TCString error;
|
||||
auto tcserver = tc_server_new_local (tc_server_dir, &error);
|
||||
if (!tcserver) {
|
||||
auto errmsg = format ("Could not configure local server at {1}: {2}",
|
||||
std::string errmsg = format ("Could not configure local server at {1}: {2}",
|
||||
server_dir, tc_string_content (&error));
|
||||
tc_string_free (&error);
|
||||
throw errmsg;
|
||||
@@ -61,13 +61,13 @@ tc::Server::new_sync (const std::string &origin, const std::string &client_id, c
|
||||
if (tc_uuid_from_str(tc_client_id, &tc_client_uuid) != TC_RESULT_OK) {
|
||||
tc_string_free(&tc_origin);
|
||||
tc_string_free(&tc_encryption_secret);
|
||||
throw "client_id must be a valid UUID";
|
||||
throw format ("client_id '{1}' is not a valid UUID", client_id);
|
||||
}
|
||||
|
||||
TCString error;
|
||||
auto tcserver = tc_server_new_sync (tc_origin, tc_client_uuid, tc_encryption_secret, &error);
|
||||
if (!tcserver) {
|
||||
auto errmsg = format ("Could not configure connection to server at {1}: {2}",
|
||||
std::string errmsg = format ("Could not configure connection to server at {1}: {2}",
|
||||
origin, tc_string_content (&error));
|
||||
tc_string_free (&error);
|
||||
throw errmsg;
|
||||
@@ -88,7 +88,7 @@ tc::Server::new_gcp (const std::string &bucket, const std::string &credential_pa
|
||||
TCString error;
|
||||
auto tcserver = tc_server_new_gcp (tc_bucket, tc_credential_path, tc_encryption_secret, &error);
|
||||
if (!tcserver) {
|
||||
auto errmsg = format ("Could not configure connection to GCP bucket {1}: {2}",
|
||||
std::string errmsg = format ("Could not configure connection to GCP bucket {1}: {2}",
|
||||
bucket, tc_string_content (&error));
|
||||
tc_string_free (&error);
|
||||
throw errmsg;
|
||||
|
||||
1
src/tc/corrosion
Submodule
1
src/tc/corrosion
Submodule
Submodule src/tc/corrosion added at 8ddd6d56ca
@@ -24,11 +24,8 @@ Other ideas;
|
||||
|
||||
TaskChampion is a typical Rust application.
|
||||
To work on TaskChampion, you'll need to [install the latest version of Rust](https://www.rust-lang.org/tools/install).
|
||||
Once you've done that, run `cargo build` at the top level of this repository to build the binaries.
|
||||
This will build `task` and `taskchampion-sync-server` executables in the `./target/debug` directory.
|
||||
You can build optimized versions of these binaries with `cargo build --release`, but the performance difference in the resulting binaries is not noticeable, and the build process will take a long time, so this is not recommended.
|
||||
|
||||
## Running Test
|
||||
## Running Tests
|
||||
|
||||
It's always a good idea to make sure tests run before you start hacking on a project.
|
||||
Run `cargo test` from the top-level of this repository to run the tests.
|
||||
@@ -39,13 +36,13 @@ Aside from that, start reading the docs and the source to learn more!
|
||||
The book documentation explains lots of the concepts in the design of TaskChampion.
|
||||
It is linked from the README.
|
||||
|
||||
There are three crates in this repository.
|
||||
There are three important crates in this repository.
|
||||
You may be able to limit the scope of what you need to understand to just one crate.
|
||||
* `taskchampion` is the core functionality of the application, implemented as a library
|
||||
* `taskchampion-cli` implements the command-line interface (in `cli/`)
|
||||
* `taskchampion-sync-server` implements the synchronization server (in `sync-server/`)
|
||||
* `taskchampion-lib` implements a C API for `taskchampion`, used by Taskwarrior
|
||||
* `integration-tests` contains some tests for integrations between multiple crates.
|
||||
|
||||
You can generate the documentation for the `taskchampion` crate with `cargo doc --release --open -p taskchampion`.
|
||||
You can generate the documentation for the `taskchampion` crate with `cargo doc --release --open -p taskchampion`.
|
||||
|
||||
## Making a Pull Request
|
||||
|
||||
|
||||
@@ -12,12 +12,11 @@ Until that is complete, the information here may be out-of-date.
|
||||
|
||||
## Structure
|
||||
|
||||
There are five crates here:
|
||||
There are four crates here:
|
||||
|
||||
* [taskchampion](./taskchampion) - the core of the tool
|
||||
* [taskchampion-sync-server](./sync-server) - the server against which `task sync` operates
|
||||
* [taskchampion-lib](./lib) - glue code to use _taskchampion_ from C
|
||||
* [integration-tests](./integration-tests) (private) - integration tests covering _taskchampion-cli_, _taskchampion-sync-server_, and _taskchampion-lib_.
|
||||
* [integration-tests](./integration-tests) (private) - integration tests covering _taskchampion_ and _taskchampion-lib_.
|
||||
* [xtask](./xtask) (private) - implementation of the `cargo xtask codegen` command
|
||||
|
||||
## Code Generation
|
||||
|
||||
@@ -7,18 +7,13 @@ publish = false
|
||||
build = "build.rs"
|
||||
|
||||
[dependencies]
|
||||
taskchampion = { path = "../taskchampion", features = ["server-sync"] }
|
||||
taskchampion = { path = "../taskchampion" }
|
||||
taskchampion-lib = { path = "../lib" }
|
||||
taskchampion-sync-server = { path = "../sync-server" }
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow.workspace = true
|
||||
actix-web.workspace = true
|
||||
actix-rt.workspace = true
|
||||
tempfile.workspace = true
|
||||
pretty_assertions.workspace = true
|
||||
log.workspace = true
|
||||
env_logger.workspace = true
|
||||
lazy_static.workspace = true
|
||||
|
||||
[build-dependencies]
|
||||
|
||||
@@ -186,7 +186,7 @@ static void test_replica_sync_local(void) {
|
||||
static void test_replica_remote_server(void) {
|
||||
TCString err;
|
||||
TCServer *server = tc_server_new_sync(
|
||||
tc_string_borrow("tc.freecinc.com"),
|
||||
tc_string_borrow("http://tc.freecinc.com"),
|
||||
tc_uuid_new_v4(),
|
||||
tc_string_borrow("\xf0\x28\x8c\x28"), // NOTE: not utf-8
|
||||
&err);
|
||||
|
||||
@@ -1,97 +1,66 @@
|
||||
use actix_web::{App, HttpServer};
|
||||
use pretty_assertions::assert_eq;
|
||||
use taskchampion::{Replica, ServerConfig, Status, StorageConfig, Uuid};
|
||||
use taskchampion_sync_server::{storage::InMemoryStorage, Server};
|
||||
use taskchampion::{Replica, ServerConfig, Status, StorageConfig};
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn cross_sync() -> anyhow::Result<()> {
|
||||
async fn server() -> anyhow::Result<u16> {
|
||||
let _ = env_logger::builder()
|
||||
.is_test(true)
|
||||
.filter_level(log::LevelFilter::Trace)
|
||||
.try_init();
|
||||
#[test]
|
||||
fn cross_sync() -> anyhow::Result<()> {
|
||||
// set up two replicas, and demonstrate replication between them
|
||||
let mut rep1 = Replica::new(StorageConfig::InMemory.into_storage()?);
|
||||
let mut rep2 = Replica::new(StorageConfig::InMemory.into_storage()?);
|
||||
|
||||
let server = Server::new(Default::default(), Box::new(InMemoryStorage::new()));
|
||||
let httpserver = HttpServer::new(move || App::new().configure(|sc| server.config(sc)))
|
||||
.bind("0.0.0.0:0")?;
|
||||
let tmp_dir = TempDir::new().expect("TempDir failed");
|
||||
let server_config = ServerConfig::Local {
|
||||
server_dir: tmp_dir.path().to_path_buf(),
|
||||
};
|
||||
let mut server = server_config.into_server()?;
|
||||
|
||||
// bind was to :0, so the kernel will have selected an unused port
|
||||
let port = httpserver.addrs()[0].port();
|
||||
actix_rt::spawn(httpserver.run());
|
||||
Ok(port)
|
||||
}
|
||||
// add some tasks on rep1
|
||||
let t1 = rep1.new_task(Status::Pending, "test 1".into())?;
|
||||
let t2 = rep1.new_task(Status::Pending, "test 2".into())?;
|
||||
|
||||
fn client(port: u16) -> anyhow::Result<()> {
|
||||
// set up two replicas, and demonstrate replication between them
|
||||
let mut rep1 = Replica::new(StorageConfig::InMemory.into_storage()?);
|
||||
let mut rep2 = Replica::new(StorageConfig::InMemory.into_storage()?);
|
||||
// modify t1
|
||||
let mut t1 = t1.into_mut(&mut rep1);
|
||||
t1.start()?;
|
||||
let t1 = t1.into_immut();
|
||||
|
||||
let client_id = Uuid::new_v4();
|
||||
let encryption_secret = b"abc123".to_vec();
|
||||
let make_server = || {
|
||||
ServerConfig::Remote {
|
||||
origin: format!("http://127.0.0.1:{}", port),
|
||||
client_id,
|
||||
encryption_secret: encryption_secret.clone(),
|
||||
}
|
||||
.into_server()
|
||||
};
|
||||
rep1.sync(&mut server, false)?;
|
||||
rep2.sync(&mut server, false)?;
|
||||
|
||||
let mut serv1 = make_server()?;
|
||||
let mut serv2 = make_server()?;
|
||||
// those tasks should exist on rep2 now
|
||||
let t12 = rep2
|
||||
.get_task(t1.get_uuid())?
|
||||
.expect("expected task 1 on rep2");
|
||||
let t22 = rep2
|
||||
.get_task(t2.get_uuid())?
|
||||
.expect("expected task 2 on rep2");
|
||||
|
||||
// add some tasks on rep1
|
||||
let t1 = rep1.new_task(Status::Pending, "test 1".into())?;
|
||||
let t2 = rep1.new_task(Status::Pending, "test 2".into())?;
|
||||
assert_eq!(t12.get_description(), "test 1");
|
||||
assert_eq!(t12.is_active(), true);
|
||||
assert_eq!(t22.get_description(), "test 2");
|
||||
assert_eq!(t22.is_active(), false);
|
||||
|
||||
// modify t1
|
||||
let mut t1 = t1.into_mut(&mut rep1);
|
||||
t1.start()?;
|
||||
let t1 = t1.into_immut();
|
||||
// make non-conflicting changes on the two replicas
|
||||
let mut t2 = t2.into_mut(&mut rep1);
|
||||
t2.set_status(Status::Completed)?;
|
||||
let t2 = t2.into_immut();
|
||||
|
||||
rep1.sync(&mut serv1, false)?;
|
||||
rep2.sync(&mut serv2, false)?;
|
||||
let mut t12 = t12.into_mut(&mut rep2);
|
||||
t12.set_status(Status::Completed)?;
|
||||
|
||||
// those tasks should exist on rep2 now
|
||||
let t12 = rep2
|
||||
.get_task(t1.get_uuid())?
|
||||
.expect("expected task 1 on rep2");
|
||||
let t22 = rep2
|
||||
.get_task(t2.get_uuid())?
|
||||
.expect("expected task 2 on rep2");
|
||||
// sync those changes back and forth
|
||||
rep1.sync(&mut server, false)?; // rep1 -> server
|
||||
rep2.sync(&mut server, false)?; // server -> rep2, rep2 -> server
|
||||
rep1.sync(&mut server, false)?; // server -> rep1
|
||||
|
||||
assert_eq!(t12.get_description(), "test 1");
|
||||
assert_eq!(t12.is_active(), true);
|
||||
assert_eq!(t22.get_description(), "test 2");
|
||||
assert_eq!(t22.is_active(), false);
|
||||
let t1 = rep1
|
||||
.get_task(t1.get_uuid())?
|
||||
.expect("expected task 1 on rep1");
|
||||
assert_eq!(t1.get_status(), Status::Completed);
|
||||
|
||||
// make non-conflicting changes on the two replicas
|
||||
let mut t2 = t2.into_mut(&mut rep1);
|
||||
t2.set_status(Status::Completed)?;
|
||||
let t2 = t2.into_immut();
|
||||
let t22 = rep2
|
||||
.get_task(t2.get_uuid())?
|
||||
.expect("expected task 2 on rep2");
|
||||
assert_eq!(t22.get_status(), Status::Completed);
|
||||
|
||||
let mut t12 = t12.into_mut(&mut rep2);
|
||||
t12.set_status(Status::Completed)?;
|
||||
|
||||
// sync those changes back and forth
|
||||
rep1.sync(&mut serv1, false)?; // rep1 -> server
|
||||
rep2.sync(&mut serv2, false)?; // server -> rep2, rep2 -> server
|
||||
rep1.sync(&mut serv1, false)?; // server -> rep1
|
||||
|
||||
let t1 = rep1
|
||||
.get_task(t1.get_uuid())?
|
||||
.expect("expected task 1 on rep1");
|
||||
assert_eq!(t1.get_status(), Status::Completed);
|
||||
|
||||
let t22 = rep2
|
||||
.get_task(t2.get_uuid())?
|
||||
.expect("expected task 2 on rep2");
|
||||
assert_eq!(t22.get_status(), Status::Completed);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
let port = server().await?;
|
||||
actix_rt::task::spawn_blocking(move || client(port)).await??;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
use actix_web::{App, HttpServer};
|
||||
use pretty_assertions::assert_eq;
|
||||
use taskchampion::{Replica, ServerConfig, Status, StorageConfig, Uuid};
|
||||
use taskchampion_sync_server::{
|
||||
storage::InMemoryStorage, Server, ServerConfig as SyncServerConfig,
|
||||
};
|
||||
|
||||
const NUM_VERSIONS: u32 = 50;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn sync_with_snapshots() -> anyhow::Result<()> {
|
||||
let _ = env_logger::builder()
|
||||
.is_test(true)
|
||||
.filter_level(log::LevelFilter::Trace)
|
||||
.try_init();
|
||||
|
||||
async fn server() -> anyhow::Result<u16> {
|
||||
let sync_server_config = SyncServerConfig {
|
||||
snapshot_days: 100,
|
||||
snapshot_versions: 3,
|
||||
};
|
||||
let server = Server::new(sync_server_config, Box::new(InMemoryStorage::new()));
|
||||
let httpserver = HttpServer::new(move || App::new().configure(|sc| server.config(sc)))
|
||||
.bind("0.0.0.0:0")?;
|
||||
|
||||
// bind was to :0, so the kernel will have selected an unused port
|
||||
let port = httpserver.addrs()[0].port();
|
||||
actix_rt::spawn(httpserver.run());
|
||||
Ok(port)
|
||||
}
|
||||
|
||||
fn client(port: u16) -> anyhow::Result<()> {
|
||||
let client_id = Uuid::new_v4();
|
||||
let encryption_secret = b"abc123".to_vec();
|
||||
let make_server = || {
|
||||
ServerConfig::Remote {
|
||||
origin: format!("http://127.0.0.1:{}", port),
|
||||
client_id,
|
||||
encryption_secret: encryption_secret.clone(),
|
||||
}
|
||||
.into_server()
|
||||
};
|
||||
|
||||
// first we set up a single replica and sync it a lot of times, to establish a sync history.
|
||||
let mut rep1 = Replica::new(StorageConfig::InMemory.into_storage()?);
|
||||
let mut serv1 = make_server()?;
|
||||
|
||||
let mut t1 = rep1.new_task(Status::Pending, "test 1".into())?;
|
||||
log::info!("Applying modifications on replica 1");
|
||||
for i in 0..=NUM_VERSIONS {
|
||||
let mut t1m = t1.into_mut(&mut rep1);
|
||||
t1m.start()?;
|
||||
t1m.stop()?;
|
||||
t1m.set_description(format!("revision {}", i))?;
|
||||
t1 = t1m.into_immut();
|
||||
|
||||
rep1.sync(&mut serv1, false)?;
|
||||
}
|
||||
|
||||
// now set up a second replica and sync it; it should catch up on that history, using a
|
||||
// snapshot. Note that we can't verify that it used a snapshot, because the server
|
||||
// currently keeps all versions (so rep2 could sync from the beginning of the version
|
||||
// history). You can manually verify that it is applying a snapshot by adding
|
||||
// `assert!(false)` below and skimming the logs.
|
||||
|
||||
let mut rep2 = Replica::new(StorageConfig::InMemory.into_storage()?);
|
||||
let mut serv2 = make_server()?;
|
||||
|
||||
log::info!("Syncing replica 2");
|
||||
rep2.sync(&mut serv2, false)?;
|
||||
|
||||
// those tasks should exist on rep2 now
|
||||
let t12 = rep2
|
||||
.get_task(t1.get_uuid())?
|
||||
.expect("expected task 1 on rep2");
|
||||
|
||||
assert_eq!(t12.get_description(), format!("revision {}", NUM_VERSIONS));
|
||||
assert_eq!(t12.is_active(), false);
|
||||
|
||||
// sync that back to replica 1
|
||||
t12.into_mut(&mut rep2)
|
||||
.set_description("sync-back".to_owned())?;
|
||||
rep2.sync(&mut serv2, false)?;
|
||||
rep1.sync(&mut serv1, false)?;
|
||||
|
||||
let t11 = rep1
|
||||
.get_task(t1.get_uuid())?
|
||||
.expect("expected task 1 on rep1");
|
||||
|
||||
assert_eq!(t11.get_description(), "sync-back");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
let port = server().await?;
|
||||
actix_rt::task::spawn_blocking(move || client(port)).await??;
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
[package]
|
||||
name = "taskchampion-sync-server"
|
||||
version = "0.4.1"
|
||||
authors = ["Dustin J. Mitchell <dustin@mozilla.com>"]
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
uuid.workspace = true
|
||||
actix-web.workspace = true
|
||||
anyhow.workspace = true
|
||||
thiserror.workspace = true
|
||||
futures.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
clap.workspace = true
|
||||
log.workspace = true
|
||||
env_logger.workspace = true
|
||||
rusqlite.workspace = true
|
||||
chrono.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
actix-rt.workspace = true
|
||||
tempfile.workspace = true
|
||||
pretty_assertions.workspace = true
|
||||
@@ -1,207 +0,0 @@
|
||||
use crate::api::{client_id_header, failure_to_ise, ServerState, SNAPSHOT_CONTENT_TYPE};
|
||||
use crate::server::{add_snapshot, VersionId, NIL_VERSION_ID};
|
||||
use actix_web::{error, post, web, HttpMessage, HttpRequest, HttpResponse, Result};
|
||||
use futures::StreamExt;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Max snapshot size: 100MB
|
||||
const MAX_SIZE: usize = 100 * 1024 * 1024;
|
||||
|
||||
/// Add a new snapshot, after checking prerequisites. The snapshot should be transmitted in the
|
||||
/// request entity body and must have content-type `application/vnd.taskchampion.snapshot`. The
|
||||
/// content can be encoded in any of the formats supported by actix-web.
|
||||
///
|
||||
/// On success, the response is a 200 OK. Even in a 200 OK, the snapshot may not appear in a
|
||||
/// subsequent `GetSnapshot` call.
|
||||
///
|
||||
/// Returns other 4xx or 5xx responses on other errors.
|
||||
#[post("/v1/client/add-snapshot/{version_id}")]
|
||||
pub(crate) async fn service(
|
||||
req: HttpRequest,
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
path: web::Path<VersionId>,
|
||||
mut payload: web::Payload,
|
||||
) -> Result<HttpResponse> {
|
||||
let version_id = path.into_inner();
|
||||
|
||||
// check content-type
|
||||
if req.content_type() != SNAPSHOT_CONTENT_TYPE {
|
||||
return Err(error::ErrorBadRequest("Bad content-type"));
|
||||
}
|
||||
|
||||
let client_id = client_id_header(&req)?;
|
||||
|
||||
// read the body in its entirety
|
||||
let mut body = web::BytesMut::new();
|
||||
while let Some(chunk) = payload.next().await {
|
||||
let chunk = chunk?;
|
||||
// limit max size of in-memory payload
|
||||
if (body.len() + chunk.len()) > MAX_SIZE {
|
||||
return Err(error::ErrorBadRequest("Snapshot over maximum allowed size"));
|
||||
}
|
||||
body.extend_from_slice(&chunk);
|
||||
}
|
||||
|
||||
if body.is_empty() {
|
||||
return Err(error::ErrorBadRequest("No snapshot supplied"));
|
||||
}
|
||||
|
||||
// note that we do not open the transaction until the body has been read
|
||||
// completely, to avoid blocking other storage access while that data is
|
||||
// in transit.
|
||||
let mut txn = server_state.storage.txn().map_err(failure_to_ise)?;
|
||||
|
||||
// get, or create, the client
|
||||
let client = match txn.get_client(client_id).map_err(failure_to_ise)? {
|
||||
Some(client) => client,
|
||||
None => {
|
||||
txn.new_client(client_id, NIL_VERSION_ID)
|
||||
.map_err(failure_to_ise)?;
|
||||
txn.get_client(client_id).map_err(failure_to_ise)?.unwrap()
|
||||
}
|
||||
};
|
||||
|
||||
add_snapshot(
|
||||
txn,
|
||||
&server_state.config,
|
||||
client_id,
|
||||
client,
|
||||
version_id,
|
||||
body.to_vec(),
|
||||
)
|
||||
.map_err(failure_to_ise)?;
|
||||
Ok(HttpResponse::Ok().body(""))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::api::CLIENT_ID_HEADER;
|
||||
use crate::storage::{InMemoryStorage, Storage};
|
||||
use crate::Server;
|
||||
use actix_web::{http::StatusCode, test, App};
|
||||
use pretty_assertions::assert_eq;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_success() -> anyhow::Result<()> {
|
||||
let client_id = Uuid::new_v4();
|
||||
let version_id = Uuid::new_v4();
|
||||
let storage: Box<dyn Storage> = Box::new(InMemoryStorage::new());
|
||||
|
||||
// set up the storage contents..
|
||||
{
|
||||
let mut txn = storage.txn().unwrap();
|
||||
txn.new_client(client_id, version_id).unwrap();
|
||||
txn.add_version(client_id, version_id, NIL_VERSION_ID, vec![])?;
|
||||
}
|
||||
|
||||
let server = Server::new(Default::default(), storage);
|
||||
let app = App::new().configure(|sc| server.config(sc));
|
||||
let mut app = test::init_service(app).await;
|
||||
|
||||
let uri = format!("/v1/client/add-snapshot/{}", version_id);
|
||||
let req = test::TestRequest::post()
|
||||
.uri(&uri)
|
||||
.insert_header(("Content-Type", "application/vnd.taskchampion.snapshot"))
|
||||
.insert_header((CLIENT_ID_HEADER, client_id.to_string()))
|
||||
.set_payload(b"abcd".to_vec())
|
||||
.to_request();
|
||||
let resp = test::call_service(&mut app, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
|
||||
// read back that snapshot
|
||||
let uri = "/v1/client/snapshot";
|
||||
let req = test::TestRequest::get()
|
||||
.uri(uri)
|
||||
.append_header((CLIENT_ID_HEADER, client_id.to_string()))
|
||||
.to_request();
|
||||
let resp = test::call_service(&mut app, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
|
||||
use actix_web::body::MessageBody;
|
||||
let bytes = resp.into_body().try_into_bytes().unwrap();
|
||||
assert_eq!(bytes.as_ref(), b"abcd");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_not_added_200() {
|
||||
let client_id = Uuid::new_v4();
|
||||
let version_id = Uuid::new_v4();
|
||||
let storage: Box<dyn Storage> = Box::new(InMemoryStorage::new());
|
||||
|
||||
// set up the storage contents..
|
||||
{
|
||||
let mut txn = storage.txn().unwrap();
|
||||
txn.new_client(client_id, NIL_VERSION_ID).unwrap();
|
||||
}
|
||||
|
||||
let server = Server::new(Default::default(), storage);
|
||||
let app = App::new().configure(|sc| server.config(sc));
|
||||
let mut app = test::init_service(app).await;
|
||||
|
||||
// add a snapshot for a nonexistent version
|
||||
let uri = format!("/v1/client/add-snapshot/{}", version_id);
|
||||
let req = test::TestRequest::post()
|
||||
.uri(&uri)
|
||||
.append_header(("Content-Type", "application/vnd.taskchampion.snapshot"))
|
||||
.append_header((CLIENT_ID_HEADER, client_id.to_string()))
|
||||
.set_payload(b"abcd".to_vec())
|
||||
.to_request();
|
||||
let resp = test::call_service(&mut app, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
|
||||
// read back, seeing no snapshot
|
||||
let uri = "/v1/client/snapshot";
|
||||
let req = test::TestRequest::get()
|
||||
.uri(uri)
|
||||
.append_header((CLIENT_ID_HEADER, client_id.to_string()))
|
||||
.to_request();
|
||||
let resp = test::call_service(&mut app, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_bad_content_type() {
|
||||
let client_id = Uuid::new_v4();
|
||||
let version_id = Uuid::new_v4();
|
||||
let storage: Box<dyn Storage> = Box::new(InMemoryStorage::new());
|
||||
let server = Server::new(Default::default(), storage);
|
||||
let app = App::new().configure(|sc| server.config(sc));
|
||||
let mut app = test::init_service(app).await;
|
||||
|
||||
let uri = format!("/v1/client/add-snapshot/{}", version_id);
|
||||
let req = test::TestRequest::post()
|
||||
.uri(&uri)
|
||||
.append_header(("Content-Type", "not/correct"))
|
||||
.append_header((CLIENT_ID_HEADER, client_id.to_string()))
|
||||
.set_payload(b"abcd".to_vec())
|
||||
.to_request();
|
||||
let resp = test::call_service(&mut app, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_empty_body() {
|
||||
let client_id = Uuid::new_v4();
|
||||
let version_id = Uuid::new_v4();
|
||||
let storage: Box<dyn Storage> = Box::new(InMemoryStorage::new());
|
||||
let server = Server::new(Default::default(), storage);
|
||||
let app = App::new().configure(|sc| server.config(sc));
|
||||
let mut app = test::init_service(app).await;
|
||||
|
||||
let uri = format!("/v1/client/add-snapshot/{}", version_id);
|
||||
let req = test::TestRequest::post()
|
||||
.uri(&uri)
|
||||
.append_header((
|
||||
"Content-Type",
|
||||
"application/vnd.taskchampion.history-segment",
|
||||
))
|
||||
.append_header((CLIENT_ID_HEADER, client_id.to_string()))
|
||||
.to_request();
|
||||
let resp = test::call_service(&mut app, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
@@ -1,233 +0,0 @@
|
||||
use crate::api::{
|
||||
client_id_header, failure_to_ise, ServerState, HISTORY_SEGMENT_CONTENT_TYPE,
|
||||
PARENT_VERSION_ID_HEADER, SNAPSHOT_REQUEST_HEADER, VERSION_ID_HEADER,
|
||||
};
|
||||
use crate::server::{add_version, AddVersionResult, SnapshotUrgency, VersionId, NIL_VERSION_ID};
|
||||
use actix_web::{error, post, web, HttpMessage, HttpRequest, HttpResponse, Result};
|
||||
use futures::StreamExt;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Max history segment size: 100MB
|
||||
const MAX_SIZE: usize = 100 * 1024 * 1024;
|
||||
|
||||
/// Add a new version, after checking prerequisites. The history segment should be transmitted in
|
||||
/// the request entity body and must have content-type
|
||||
/// `application/vnd.taskchampion.history-segment`. The content can be encoded in any of the
|
||||
/// formats supported by actix-web.
|
||||
///
|
||||
/// On success, the response is a 200 OK with the new version ID in the `X-Version-Id` header. If
|
||||
/// the version cannot be added due to a conflict, the response is a 409 CONFLICT with the expected
|
||||
/// parent version ID in the `X-Parent-Version-Id` header.
|
||||
///
|
||||
/// If included, a snapshot request appears in the `X-Snapshot-Request` header with value
|
||||
/// `urgency=low` or `urgency=high`.
|
||||
///
|
||||
/// Returns other 4xx or 5xx responses on other errors.
|
||||
#[post("/v1/client/add-version/{parent_version_id}")]
|
||||
pub(crate) async fn service(
|
||||
req: HttpRequest,
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
path: web::Path<VersionId>,
|
||||
mut payload: web::Payload,
|
||||
) -> Result<HttpResponse> {
|
||||
let parent_version_id = path.into_inner();
|
||||
|
||||
// check content-type
|
||||
if req.content_type() != HISTORY_SEGMENT_CONTENT_TYPE {
|
||||
return Err(error::ErrorBadRequest("Bad content-type"));
|
||||
}
|
||||
|
||||
let client_id = client_id_header(&req)?;
|
||||
|
||||
// read the body in its entirety
|
||||
let mut body = web::BytesMut::new();
|
||||
while let Some(chunk) = payload.next().await {
|
||||
let chunk = chunk?;
|
||||
// limit max size of in-memory payload
|
||||
if (body.len() + chunk.len()) > MAX_SIZE {
|
||||
return Err(error::ErrorBadRequest("overflow"));
|
||||
}
|
||||
body.extend_from_slice(&chunk);
|
||||
}
|
||||
|
||||
if body.is_empty() {
|
||||
return Err(error::ErrorBadRequest("Empty body"));
|
||||
}
|
||||
|
||||
// note that we do not open the transaction until the body has been read
|
||||
// completely, to avoid blocking other storage access while that data is
|
||||
// in transit.
|
||||
let mut txn = server_state.storage.txn().map_err(failure_to_ise)?;
|
||||
|
||||
// get, or create, the client
|
||||
let client = match txn.get_client(client_id).map_err(failure_to_ise)? {
|
||||
Some(client) => client,
|
||||
None => {
|
||||
txn.new_client(client_id, NIL_VERSION_ID)
|
||||
.map_err(failure_to_ise)?;
|
||||
txn.get_client(client_id).map_err(failure_to_ise)?.unwrap()
|
||||
}
|
||||
};
|
||||
|
||||
let (result, snap_urgency) = add_version(
|
||||
txn,
|
||||
&server_state.config,
|
||||
client_id,
|
||||
client,
|
||||
parent_version_id,
|
||||
body.to_vec(),
|
||||
)
|
||||
.map_err(failure_to_ise)?;
|
||||
|
||||
Ok(match result {
|
||||
AddVersionResult::Ok(version_id) => {
|
||||
let mut rb = HttpResponse::Ok();
|
||||
rb.append_header((VERSION_ID_HEADER, version_id.to_string()));
|
||||
match snap_urgency {
|
||||
SnapshotUrgency::None => {}
|
||||
SnapshotUrgency::Low => {
|
||||
rb.append_header((SNAPSHOT_REQUEST_HEADER, "urgency=low"));
|
||||
}
|
||||
SnapshotUrgency::High => {
|
||||
rb.append_header((SNAPSHOT_REQUEST_HEADER, "urgency=high"));
|
||||
}
|
||||
};
|
||||
rb.finish()
|
||||
}
|
||||
AddVersionResult::ExpectedParentVersion(parent_version_id) => {
|
||||
let mut rb = HttpResponse::Conflict();
|
||||
rb.append_header((PARENT_VERSION_ID_HEADER, parent_version_id.to_string()));
|
||||
rb.finish()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::api::CLIENT_ID_HEADER;
|
||||
use crate::storage::{InMemoryStorage, Storage};
|
||||
use crate::Server;
|
||||
use actix_web::{http::StatusCode, test, App};
|
||||
use pretty_assertions::assert_eq;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_success() {
|
||||
let client_id = Uuid::new_v4();
|
||||
let version_id = Uuid::new_v4();
|
||||
let parent_version_id = Uuid::new_v4();
|
||||
let storage: Box<dyn Storage> = Box::new(InMemoryStorage::new());
|
||||
|
||||
// set up the storage contents..
|
||||
{
|
||||
let mut txn = storage.txn().unwrap();
|
||||
txn.new_client(client_id, Uuid::nil()).unwrap();
|
||||
}
|
||||
|
||||
let server = Server::new(Default::default(), storage);
|
||||
let app = App::new().configure(|sc| server.config(sc));
|
||||
let mut app = test::init_service(app).await;
|
||||
|
||||
let uri = format!("/v1/client/add-version/{}", parent_version_id);
|
||||
let req = test::TestRequest::post()
|
||||
.uri(&uri)
|
||||
.append_header((
|
||||
"Content-Type",
|
||||
"application/vnd.taskchampion.history-segment",
|
||||
))
|
||||
.append_header((CLIENT_ID_HEADER, client_id.to_string()))
|
||||
.set_payload(b"abcd".to_vec())
|
||||
.to_request();
|
||||
let resp = test::call_service(&mut app, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
|
||||
// the returned version ID is random, but let's check that it's not
|
||||
// the passed parent version ID, at least
|
||||
let new_version_id = resp.headers().get("X-Version-Id").unwrap();
|
||||
assert!(new_version_id != &version_id.to_string());
|
||||
|
||||
// Shapshot should be requested, since there is no existing snapshot
|
||||
let snapshot_request = resp.headers().get("X-Snapshot-Request").unwrap();
|
||||
assert_eq!(snapshot_request, "urgency=high");
|
||||
|
||||
assert_eq!(resp.headers().get("X-Parent-Version-Id"), None);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_conflict() {
|
||||
let client_id = Uuid::new_v4();
|
||||
let version_id = Uuid::new_v4();
|
||||
let parent_version_id = Uuid::new_v4();
|
||||
let storage: Box<dyn Storage> = Box::new(InMemoryStorage::new());
|
||||
|
||||
// set up the storage contents..
|
||||
{
|
||||
let mut txn = storage.txn().unwrap();
|
||||
txn.new_client(client_id, version_id).unwrap();
|
||||
}
|
||||
|
||||
let server = Server::new(Default::default(), storage);
|
||||
let app = App::new().configure(|sc| server.config(sc));
|
||||
let mut app = test::init_service(app).await;
|
||||
|
||||
let uri = format!("/v1/client/add-version/{}", parent_version_id);
|
||||
let req = test::TestRequest::post()
|
||||
.uri(&uri)
|
||||
.append_header((
|
||||
"Content-Type",
|
||||
"application/vnd.taskchampion.history-segment",
|
||||
))
|
||||
.append_header((CLIENT_ID_HEADER, client_id.to_string()))
|
||||
.set_payload(b"abcd".to_vec())
|
||||
.to_request();
|
||||
let resp = test::call_service(&mut app, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::CONFLICT);
|
||||
assert_eq!(resp.headers().get("X-Version-Id"), None);
|
||||
assert_eq!(
|
||||
resp.headers().get("X-Parent-Version-Id").unwrap(),
|
||||
&version_id.to_string()
|
||||
);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_bad_content_type() {
|
||||
let client_id = Uuid::new_v4();
|
||||
let parent_version_id = Uuid::new_v4();
|
||||
let storage: Box<dyn Storage> = Box::new(InMemoryStorage::new());
|
||||
let server = Server::new(Default::default(), storage);
|
||||
let app = App::new().configure(|sc| server.config(sc));
|
||||
let mut app = test::init_service(app).await;
|
||||
|
||||
let uri = format!("/v1/client/add-version/{}", parent_version_id);
|
||||
let req = test::TestRequest::post()
|
||||
.uri(&uri)
|
||||
.append_header(("Content-Type", "not/correct"))
|
||||
.append_header((CLIENT_ID_HEADER, client_id.to_string()))
|
||||
.set_payload(b"abcd".to_vec())
|
||||
.to_request();
|
||||
let resp = test::call_service(&mut app, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_empty_body() {
|
||||
let client_id = Uuid::new_v4();
|
||||
let parent_version_id = Uuid::new_v4();
|
||||
let storage: Box<dyn Storage> = Box::new(InMemoryStorage::new());
|
||||
let server = Server::new(Default::default(), storage);
|
||||
let app = App::new().configure(|sc| server.config(sc));
|
||||
let mut app = test::init_service(app).await;
|
||||
|
||||
let uri = format!("/v1/client/add-version/{}", parent_version_id);
|
||||
let req = test::TestRequest::post()
|
||||
.uri(&uri)
|
||||
.append_header((
|
||||
"Content-Type",
|
||||
"application/vnd.taskchampion.history-segment",
|
||||
))
|
||||
.append_header((CLIENT_ID_HEADER, client_id.to_string()))
|
||||
.to_request();
|
||||
let resp = test::call_service(&mut app, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
@@ -1,170 +0,0 @@
|
||||
use crate::api::{
|
||||
client_id_header, failure_to_ise, ServerState, HISTORY_SEGMENT_CONTENT_TYPE,
|
||||
PARENT_VERSION_ID_HEADER, VERSION_ID_HEADER,
|
||||
};
|
||||
use crate::server::{get_child_version, GetVersionResult, VersionId};
|
||||
use actix_web::{error, get, web, HttpRequest, HttpResponse, Result};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Get a child version.
|
||||
///
|
||||
/// On succcess, the response is the same sequence of bytes originally sent to the server,
|
||||
/// with content-type `application/vnd.taskchampion.history-segment`. The `X-Version-Id` and
|
||||
/// `X-Parent-Version-Id` headers contain the corresponding values.
|
||||
///
|
||||
/// If no such child exists, returns a 404 with no content.
|
||||
/// Returns other 4xx or 5xx responses on other errors.
|
||||
#[get("/v1/client/get-child-version/{parent_version_id}")]
|
||||
pub(crate) async fn service(
|
||||
req: HttpRequest,
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
path: web::Path<VersionId>,
|
||||
) -> Result<HttpResponse> {
|
||||
let parent_version_id = path.into_inner();
|
||||
|
||||
let mut txn = server_state.storage.txn().map_err(failure_to_ise)?;
|
||||
|
||||
let client_id = client_id_header(&req)?;
|
||||
|
||||
let client = txn
|
||||
.get_client(client_id)
|
||||
.map_err(failure_to_ise)?
|
||||
.ok_or_else(|| error::ErrorNotFound("no such client"))?;
|
||||
|
||||
return match get_child_version(
|
||||
txn,
|
||||
&server_state.config,
|
||||
client_id,
|
||||
client,
|
||||
parent_version_id,
|
||||
)
|
||||
.map_err(failure_to_ise)?
|
||||
{
|
||||
GetVersionResult::Success {
|
||||
version_id,
|
||||
parent_version_id,
|
||||
history_segment,
|
||||
} => Ok(HttpResponse::Ok()
|
||||
.content_type(HISTORY_SEGMENT_CONTENT_TYPE)
|
||||
.append_header((VERSION_ID_HEADER, version_id.to_string()))
|
||||
.append_header((PARENT_VERSION_ID_HEADER, parent_version_id.to_string()))
|
||||
.body(history_segment)),
|
||||
GetVersionResult::NotFound => Err(error::ErrorNotFound("no such version")),
|
||||
GetVersionResult::Gone => Err(error::ErrorGone("version has been deleted")),
|
||||
};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::api::CLIENT_ID_HEADER;
|
||||
use crate::server::NIL_VERSION_ID;
|
||||
use crate::storage::{InMemoryStorage, Storage};
|
||||
use crate::Server;
|
||||
use actix_web::{http::StatusCode, test, App};
|
||||
use pretty_assertions::assert_eq;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_success() {
|
||||
let client_id = Uuid::new_v4();
|
||||
let version_id = Uuid::new_v4();
|
||||
let parent_version_id = Uuid::new_v4();
|
||||
let storage: Box<dyn Storage> = Box::new(InMemoryStorage::new());
|
||||
|
||||
// set up the storage contents..
|
||||
{
|
||||
let mut txn = storage.txn().unwrap();
|
||||
txn.new_client(client_id, Uuid::new_v4()).unwrap();
|
||||
txn.add_version(client_id, version_id, parent_version_id, b"abcd".to_vec())
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let server = Server::new(Default::default(), storage);
|
||||
let app = App::new().configure(|sc| server.config(sc));
|
||||
let mut app = test::init_service(app).await;
|
||||
|
||||
let uri = format!("/v1/client/get-child-version/{}", parent_version_id);
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&uri)
|
||||
.append_header((CLIENT_ID_HEADER, client_id.to_string()))
|
||||
.to_request();
|
||||
let resp = test::call_service(&mut app, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
resp.headers().get("X-Version-Id").unwrap(),
|
||||
&version_id.to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
resp.headers().get("X-Parent-Version-Id").unwrap(),
|
||||
&parent_version_id.to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
resp.headers().get("Content-Type").unwrap(),
|
||||
&"application/vnd.taskchampion.history-segment".to_string()
|
||||
);
|
||||
|
||||
use actix_web::body::MessageBody;
|
||||
let bytes = resp.into_body().try_into_bytes().unwrap();
|
||||
assert_eq!(bytes.as_ref(), b"abcd");
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_client_not_found() {
|
||||
let client_id = Uuid::new_v4();
|
||||
let parent_version_id = Uuid::new_v4();
|
||||
let storage: Box<dyn Storage> = Box::new(InMemoryStorage::new());
|
||||
let server = Server::new(Default::default(), storage);
|
||||
let app = App::new().configure(|sc| server.config(sc));
|
||||
let mut app = test::init_service(app).await;
|
||||
|
||||
let uri = format!("/v1/client/get-child-version/{}", parent_version_id);
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&uri)
|
||||
.append_header((CLIENT_ID_HEADER, client_id.to_string()))
|
||||
.to_request();
|
||||
let resp = test::call_service(&mut app, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
||||
assert_eq!(resp.headers().get("X-Version-Id"), None);
|
||||
assert_eq!(resp.headers().get("X-Parent-Version-Id"), None);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_version_not_found_and_gone() {
|
||||
let client_id = Uuid::new_v4();
|
||||
let parent_version_id = Uuid::new_v4();
|
||||
let storage: Box<dyn Storage> = Box::new(InMemoryStorage::new());
|
||||
|
||||
// create the client, but not the version
|
||||
{
|
||||
let mut txn = storage.txn().unwrap();
|
||||
txn.new_client(client_id, Uuid::new_v4()).unwrap();
|
||||
}
|
||||
let server = Server::new(Default::default(), storage);
|
||||
let app = App::new().configure(|sc| server.config(sc));
|
||||
let mut app = test::init_service(app).await;
|
||||
|
||||
// the child of an unknown parent_version_id is GONE
|
||||
let uri = format!("/v1/client/get-child-version/{}", parent_version_id);
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&uri)
|
||||
.append_header((CLIENT_ID_HEADER, client_id.to_string()))
|
||||
.to_request();
|
||||
let resp = test::call_service(&mut app, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::GONE);
|
||||
assert_eq!(resp.headers().get("X-Version-Id"), None);
|
||||
assert_eq!(resp.headers().get("X-Parent-Version-Id"), None);
|
||||
|
||||
// but the child of the nil parent_version_id is NOT FOUND, since
|
||||
// there is no snapshot. The tests in crate::server test more
|
||||
// corner cases.
|
||||
let uri = format!("/v1/client/get-child-version/{}", NIL_VERSION_ID);
|
||||
let req = test::TestRequest::get()
|
||||
.uri(&uri)
|
||||
.append_header((CLIENT_ID_HEADER, client_id.to_string()))
|
||||
.to_request();
|
||||
let resp = test::call_service(&mut app, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
||||
assert_eq!(resp.headers().get("X-Version-Id"), None);
|
||||
assert_eq!(resp.headers().get("X-Parent-Version-Id"), None);
|
||||
}
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
use crate::api::{
|
||||
client_id_header, failure_to_ise, ServerState, SNAPSHOT_CONTENT_TYPE, VERSION_ID_HEADER,
|
||||
};
|
||||
use crate::server::get_snapshot;
|
||||
use actix_web::{error, get, web, HttpRequest, HttpResponse, Result};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Get a snapshot.
|
||||
///
|
||||
/// If a snapshot for this client exists, it is returned with content-type
|
||||
/// `application/vnd.taskchampion.snapshot`. The `X-Version-Id` header contains the version of the
|
||||
/// snapshot.
|
||||
///
|
||||
/// If no snapshot exists, returns a 404 with no content. Returns other 4xx or 5xx responses on
|
||||
/// other errors.
|
||||
#[get("/v1/client/snapshot")]
|
||||
pub(crate) async fn service(
|
||||
req: HttpRequest,
|
||||
server_state: web::Data<Arc<ServerState>>,
|
||||
) -> Result<HttpResponse> {
|
||||
let mut txn = server_state.storage.txn().map_err(failure_to_ise)?;
|
||||
|
||||
let client_id = client_id_header(&req)?;
|
||||
|
||||
let client = txn
|
||||
.get_client(client_id)
|
||||
.map_err(failure_to_ise)?
|
||||
.ok_or_else(|| error::ErrorNotFound("no such client"))?;
|
||||
|
||||
if let Some((version_id, data)) =
|
||||
get_snapshot(txn, &server_state.config, client_id, client).map_err(failure_to_ise)?
|
||||
{
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type(SNAPSHOT_CONTENT_TYPE)
|
||||
.append_header((VERSION_ID_HEADER, version_id.to_string()))
|
||||
.body(data))
|
||||
} else {
|
||||
Err(error::ErrorNotFound("no snapshot"))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::api::CLIENT_ID_HEADER;
|
||||
use crate::storage::{InMemoryStorage, Snapshot, Storage};
|
||||
use crate::Server;
|
||||
use actix_web::{http::StatusCode, test, App};
|
||||
use chrono::{TimeZone, Utc};
|
||||
use pretty_assertions::assert_eq;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_not_found() {
|
||||
let client_id = Uuid::new_v4();
|
||||
let storage: Box<dyn Storage> = Box::new(InMemoryStorage::new());
|
||||
|
||||
// set up the storage contents..
|
||||
{
|
||||
let mut txn = storage.txn().unwrap();
|
||||
txn.new_client(client_id, Uuid::new_v4()).unwrap();
|
||||
}
|
||||
|
||||
let server = Server::new(Default::default(), storage);
|
||||
let app = App::new().configure(|sc| server.config(sc));
|
||||
let mut app = test::init_service(app).await;
|
||||
|
||||
let uri = "/v1/client/snapshot";
|
||||
let req = test::TestRequest::get()
|
||||
.uri(uri)
|
||||
.append_header((CLIENT_ID_HEADER, client_id.to_string()))
|
||||
.to_request();
|
||||
let resp = test::call_service(&mut app, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_success() {
|
||||
let client_id = Uuid::new_v4();
|
||||
let version_id = Uuid::new_v4();
|
||||
let snapshot_data = vec![1, 2, 3, 4];
|
||||
let storage: Box<dyn Storage> = Box::new(InMemoryStorage::new());
|
||||
|
||||
// set up the storage contents..
|
||||
{
|
||||
let mut txn = storage.txn().unwrap();
|
||||
txn.new_client(client_id, Uuid::new_v4()).unwrap();
|
||||
txn.set_snapshot(
|
||||
client_id,
|
||||
Snapshot {
|
||||
version_id,
|
||||
versions_since: 3,
|
||||
timestamp: Utc.ymd(2001, 9, 9).and_hms(1, 46, 40),
|
||||
},
|
||||
snapshot_data.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let server = Server::new(Default::default(), storage);
|
||||
let app = App::new().configure(|sc| server.config(sc));
|
||||
let mut app = test::init_service(app).await;
|
||||
|
||||
let uri = "/v1/client/snapshot";
|
||||
let req = test::TestRequest::get()
|
||||
.uri(uri)
|
||||
.append_header((CLIENT_ID_HEADER, client_id.to_string()))
|
||||
.to_request();
|
||||
let resp = test::call_service(&mut app, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
|
||||
use actix_web::body::MessageBody;
|
||||
let bytes = resp.into_body().try_into_bytes().unwrap();
|
||||
assert_eq!(bytes.as_ref(), snapshot_data);
|
||||
}
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
use crate::server::ClientId;
|
||||
use crate::storage::Storage;
|
||||
use crate::ServerConfig;
|
||||
use actix_web::{error, http::StatusCode, web, HttpRequest, Result, Scope};
|
||||
|
||||
mod add_snapshot;
|
||||
mod add_version;
|
||||
mod get_child_version;
|
||||
mod get_snapshot;
|
||||
|
||||
/// The content-type for history segments (opaque blobs of bytes)
|
||||
pub(crate) const HISTORY_SEGMENT_CONTENT_TYPE: &str =
|
||||
"application/vnd.taskchampion.history-segment";
|
||||
|
||||
/// The content-type for snapshots (opaque blobs of bytes)
|
||||
pub(crate) const SNAPSHOT_CONTENT_TYPE: &str = "application/vnd.taskchampion.snapshot";
|
||||
|
||||
/// The header name for version ID
|
||||
pub(crate) const VERSION_ID_HEADER: &str = "X-Version-Id";
|
||||
|
||||
/// The header name for client id
|
||||
pub(crate) const CLIENT_ID_HEADER: &str = "X-Client-Id";
|
||||
|
||||
/// The header name for parent version ID
|
||||
pub(crate) const PARENT_VERSION_ID_HEADER: &str = "X-Parent-Version-Id";
|
||||
|
||||
/// The header name for parent version ID
|
||||
pub(crate) const SNAPSHOT_REQUEST_HEADER: &str = "X-Snapshot-Request";
|
||||
|
||||
/// The type containing a reference to the persistent state for the server
|
||||
pub(crate) struct ServerState {
|
||||
pub(crate) storage: Box<dyn Storage>,
|
||||
pub(crate) config: ServerConfig,
|
||||
}
|
||||
|
||||
pub(crate) fn api_scope() -> Scope {
|
||||
web::scope("")
|
||||
.service(get_child_version::service)
|
||||
.service(add_version::service)
|
||||
.service(get_snapshot::service)
|
||||
.service(add_snapshot::service)
|
||||
}
|
||||
|
||||
/// Convert a failure::Error to an Actix ISE
|
||||
fn failure_to_ise(err: anyhow::Error) -> impl actix_web::ResponseError {
|
||||
error::InternalError::new(err, StatusCode::INTERNAL_SERVER_ERROR)
|
||||
}
|
||||
|
||||
/// Get the client id
|
||||
fn client_id_header(req: &HttpRequest) -> Result<ClientId> {
|
||||
fn badrequest() -> error::Error {
|
||||
error::ErrorBadRequest("bad x-client-id")
|
||||
}
|
||||
if let Some(client_id_hdr) = req.headers().get(CLIENT_ID_HEADER) {
|
||||
let client_id = client_id_hdr.to_str().map_err(|_| badrequest())?;
|
||||
let client_id = ClientId::parse_str(client_id).map_err(|_| badrequest())?;
|
||||
Ok(client_id)
|
||||
} else {
|
||||
Err(badrequest())
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
#![deny(clippy::all)]
|
||||
|
||||
use actix_web::{middleware::Logger, App, HttpServer};
|
||||
use clap::{arg, builder::ValueParser, value_parser, Command};
|
||||
use std::ffi::OsString;
|
||||
use taskchampion_sync_server::storage::SqliteStorage;
|
||||
use taskchampion_sync_server::{Server, ServerConfig};
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
env_logger::init();
|
||||
let defaults = ServerConfig::default();
|
||||
let default_snapshot_versions = defaults.snapshot_versions.to_string();
|
||||
let default_snapshot_days = defaults.snapshot_days.to_string();
|
||||
let matches = Command::new("taskchampion-sync-server")
|
||||
.version(env!("CARGO_PKG_VERSION"))
|
||||
.about("Server for TaskChampion")
|
||||
.arg(
|
||||
arg!(-p --port <PORT> "Port on which to serve")
|
||||
.help("Port on which to serve")
|
||||
.value_parser(value_parser!(usize))
|
||||
.default_value("8080"),
|
||||
)
|
||||
.arg(
|
||||
arg!(-d --"data-dir" <DIR> "Directory in which to store data")
|
||||
.value_parser(ValueParser::os_string())
|
||||
.default_value("/var/lib/taskchampion-sync-server"),
|
||||
)
|
||||
.arg(
|
||||
arg!(--"snapshot-versions" <NUM> "Target number of versions between snapshots")
|
||||
.value_parser(value_parser!(u32))
|
||||
.default_value(default_snapshot_versions),
|
||||
)
|
||||
.arg(
|
||||
arg!(--"snapshot-days" <NUM> "Target number of days between snapshots")
|
||||
.value_parser(value_parser!(i64))
|
||||
.default_value(default_snapshot_days),
|
||||
)
|
||||
.get_matches();
|
||||
|
||||
let data_dir: &OsString = matches.get_one("data-dir").unwrap();
|
||||
let port: usize = *matches.get_one("port").unwrap();
|
||||
let snapshot_versions: u32 = *matches.get_one("snapshot-versions").unwrap();
|
||||
let snapshot_days: i64 = *matches.get_one("snapshot-days").unwrap();
|
||||
|
||||
let config = ServerConfig::from_args(snapshot_days, snapshot_versions)?;
|
||||
let server = Server::new(config, Box::new(SqliteStorage::new(data_dir)?));
|
||||
|
||||
log::warn!("Serving on port {}", port);
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.wrap(Logger::default())
|
||||
.configure(|cfg| server.config(cfg))
|
||||
})
|
||||
.bind(format!("0.0.0.0:{}", port))?
|
||||
.run()
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use actix_web::{test, App};
|
||||
use taskchampion_sync_server::storage::InMemoryStorage;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_index_get() {
|
||||
let server = Server::new(Default::default(), Box::new(InMemoryStorage::new()));
|
||||
let app = App::new().configure(|sc| server.config(sc));
|
||||
let mut app = test::init_service(app).await;
|
||||
|
||||
let req = test::TestRequest::get().uri("/").to_request();
|
||||
let resp = test::call_service(&mut app, req).await;
|
||||
assert!(resp.status().is_success());
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
#![deny(clippy::all)]
|
||||
|
||||
mod api;
|
||||
mod server;
|
||||
pub mod storage;
|
||||
|
||||
use crate::storage::Storage;
|
||||
use actix_web::{get, middleware, web, Responder};
|
||||
use api::{api_scope, ServerState};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub use server::ServerConfig;
|
||||
|
||||
#[get("/")]
|
||||
async fn index() -> impl Responder {
|
||||
format!("TaskChampion sync server v{}", env!("CARGO_PKG_VERSION"))
|
||||
}
|
||||
|
||||
/// A Server represents a sync server.
|
||||
#[derive(Clone)]
|
||||
pub struct Server {
|
||||
server_state: Arc<ServerState>,
|
||||
}
|
||||
|
||||
impl Server {
|
||||
/// Create a new sync server with the given storage implementation.
|
||||
pub fn new(config: ServerConfig, storage: Box<dyn Storage>) -> Self {
|
||||
Self {
|
||||
server_state: Arc::new(ServerState { config, storage }),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get an Actix-web service for this server.
|
||||
pub fn config(&self, cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(
|
||||
web::scope("")
|
||||
.app_data(web::Data::new(self.server_state.clone()))
|
||||
.wrap(
|
||||
middleware::DefaultHeaders::new().add(("Cache-Control", "no-store, max-age=0")),
|
||||
)
|
||||
.service(index)
|
||||
.service(api_scope()),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::storage::InMemoryStorage;
|
||||
use actix_web::{test, App};
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
pub(crate) fn init_logging() {
|
||||
let _ = env_logger::builder().is_test(true).try_init();
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_cache_control() {
|
||||
let server = Server::new(Default::default(), Box::new(InMemoryStorage::new()));
|
||||
let app = App::new().configure(|sc| server.config(sc));
|
||||
let mut app = test::init_service(app).await;
|
||||
|
||||
let req = test::TestRequest::get().uri("/").to_request();
|
||||
let resp = test::call_service(&mut app, req).await;
|
||||
assert!(resp.status().is_success());
|
||||
assert_eq!(
|
||||
resp.headers().get("Cache-Control").unwrap(),
|
||||
&"no-store, max-age=0".to_string()
|
||||
)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,286 +0,0 @@
|
||||
use super::{Client, Snapshot, Storage, StorageTxn, Uuid, Version};
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Mutex, MutexGuard};
|
||||
|
||||
struct Inner {
|
||||
/// Clients, indexed by client_id
|
||||
clients: HashMap<Uuid, Client>,
|
||||
|
||||
/// Snapshot data, indexed by client id
|
||||
snapshots: HashMap<Uuid, Vec<u8>>,
|
||||
|
||||
/// Versions, indexed by (client_id, version_id)
|
||||
versions: HashMap<(Uuid, Uuid), Version>,
|
||||
|
||||
/// Child versions, indexed by (client_id, parent_version_id)
|
||||
children: HashMap<(Uuid, Uuid), Uuid>,
|
||||
}
|
||||
|
||||
pub struct InMemoryStorage(Mutex<Inner>);
|
||||
|
||||
impl InMemoryStorage {
|
||||
#[allow(clippy::new_without_default)]
|
||||
pub fn new() -> Self {
|
||||
Self(Mutex::new(Inner {
|
||||
clients: HashMap::new(),
|
||||
snapshots: HashMap::new(),
|
||||
versions: HashMap::new(),
|
||||
children: HashMap::new(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
struct InnerTxn<'a>(MutexGuard<'a, Inner>);
|
||||
|
||||
/// In-memory storage for testing and experimentation.
|
||||
///
|
||||
/// NOTE: this does not implement transaction rollback.
|
||||
impl Storage for InMemoryStorage {
|
||||
fn txn<'a>(&'a self) -> anyhow::Result<Box<dyn StorageTxn + 'a>> {
|
||||
Ok(Box::new(InnerTxn(self.0.lock().expect("poisoned lock"))))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> StorageTxn for InnerTxn<'a> {
|
||||
fn get_client(&mut self, client_id: Uuid) -> anyhow::Result<Option<Client>> {
|
||||
Ok(self.0.clients.get(&client_id).cloned())
|
||||
}
|
||||
|
||||
fn new_client(&mut self, client_id: Uuid, latest_version_id: Uuid) -> anyhow::Result<()> {
|
||||
if self.0.clients.get(&client_id).is_some() {
|
||||
return Err(anyhow::anyhow!("Client {} already exists", client_id));
|
||||
}
|
||||
self.0.clients.insert(
|
||||
client_id,
|
||||
Client {
|
||||
latest_version_id,
|
||||
snapshot: None,
|
||||
},
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_snapshot(
|
||||
&mut self,
|
||||
client_id: Uuid,
|
||||
snapshot: Snapshot,
|
||||
data: Vec<u8>,
|
||||
) -> anyhow::Result<()> {
|
||||
let client = self
|
||||
.0
|
||||
.clients
|
||||
.get_mut(&client_id)
|
||||
.ok_or_else(|| anyhow::anyhow!("no such client"))?;
|
||||
client.snapshot = Some(snapshot);
|
||||
self.0.snapshots.insert(client_id, data);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_snapshot_data(
|
||||
&mut self,
|
||||
client_id: Uuid,
|
||||
version_id: Uuid,
|
||||
) -> anyhow::Result<Option<Vec<u8>>> {
|
||||
// sanity check
|
||||
let client = self.0.clients.get(&client_id);
|
||||
let client = client.ok_or_else(|| anyhow::anyhow!("no such client"))?;
|
||||
if Some(&version_id) != client.snapshot.as_ref().map(|snap| &snap.version_id) {
|
||||
return Err(anyhow::anyhow!("unexpected snapshot_version_id"));
|
||||
}
|
||||
Ok(self.0.snapshots.get(&client_id).cloned())
|
||||
}
|
||||
|
||||
fn get_version_by_parent(
|
||||
&mut self,
|
||||
client_id: Uuid,
|
||||
parent_version_id: Uuid,
|
||||
) -> anyhow::Result<Option<Version>> {
|
||||
if let Some(parent_version_id) = self.0.children.get(&(client_id, parent_version_id)) {
|
||||
Ok(self
|
||||
.0
|
||||
.versions
|
||||
.get(&(client_id, *parent_version_id))
|
||||
.cloned())
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_version(
|
||||
&mut self,
|
||||
client_id: Uuid,
|
||||
version_id: Uuid,
|
||||
) -> anyhow::Result<Option<Version>> {
|
||||
Ok(self.0.versions.get(&(client_id, version_id)).cloned())
|
||||
}
|
||||
|
||||
fn add_version(
|
||||
&mut self,
|
||||
client_id: Uuid,
|
||||
version_id: Uuid,
|
||||
parent_version_id: Uuid,
|
||||
history_segment: Vec<u8>,
|
||||
) -> anyhow::Result<()> {
|
||||
// TODO: verify it doesn't exist (`.entry`?)
|
||||
let version = Version {
|
||||
version_id,
|
||||
parent_version_id,
|
||||
history_segment,
|
||||
};
|
||||
|
||||
if let Some(client) = self.0.clients.get_mut(&client_id) {
|
||||
client.latest_version_id = version_id;
|
||||
if let Some(ref mut snap) = client.snapshot {
|
||||
snap.versions_since += 1;
|
||||
}
|
||||
} else {
|
||||
return Err(anyhow::anyhow!("Client {} does not exist", client_id));
|
||||
}
|
||||
|
||||
self.0
|
||||
.children
|
||||
.insert((client_id, parent_version_id), version_id);
|
||||
self.0.versions.insert((client_id, version_id), version);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn commit(&mut self) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use chrono::Utc;
|
||||
|
||||
#[test]
|
||||
fn test_get_client_empty() -> anyhow::Result<()> {
|
||||
let storage = InMemoryStorage::new();
|
||||
let mut txn = storage.txn()?;
|
||||
let maybe_client = txn.get_client(Uuid::new_v4())?;
|
||||
assert!(maybe_client.is_none());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_client_storage() -> anyhow::Result<()> {
|
||||
let storage = InMemoryStorage::new();
|
||||
let mut txn = storage.txn()?;
|
||||
|
||||
let client_id = Uuid::new_v4();
|
||||
let latest_version_id = Uuid::new_v4();
|
||||
txn.new_client(client_id, latest_version_id)?;
|
||||
|
||||
let client = txn.get_client(client_id)?.unwrap();
|
||||
assert_eq!(client.latest_version_id, latest_version_id);
|
||||
assert!(client.snapshot.is_none());
|
||||
|
||||
let latest_version_id = Uuid::new_v4();
|
||||
txn.add_version(client_id, latest_version_id, Uuid::new_v4(), vec![1, 1])?;
|
||||
|
||||
let client = txn.get_client(client_id)?.unwrap();
|
||||
assert_eq!(client.latest_version_id, latest_version_id);
|
||||
assert!(client.snapshot.is_none());
|
||||
|
||||
let snap = Snapshot {
|
||||
version_id: Uuid::new_v4(),
|
||||
timestamp: Utc::now(),
|
||||
versions_since: 4,
|
||||
};
|
||||
txn.set_snapshot(client_id, snap.clone(), vec![1, 2, 3])?;
|
||||
|
||||
let client = txn.get_client(client_id)?.unwrap();
|
||||
assert_eq!(client.latest_version_id, latest_version_id);
|
||||
assert_eq!(client.snapshot.unwrap(), snap);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gvbp_empty() -> anyhow::Result<()> {
|
||||
let storage = InMemoryStorage::new();
|
||||
let mut txn = storage.txn()?;
|
||||
let maybe_version = txn.get_version_by_parent(Uuid::new_v4(), Uuid::new_v4())?;
|
||||
assert!(maybe_version.is_none());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_version_and_get_version() -> anyhow::Result<()> {
|
||||
let storage = InMemoryStorage::new();
|
||||
let mut txn = storage.txn()?;
|
||||
|
||||
let client_id = Uuid::new_v4();
|
||||
let version_id = Uuid::new_v4();
|
||||
let parent_version_id = Uuid::new_v4();
|
||||
let history_segment = b"abc".to_vec();
|
||||
|
||||
txn.new_client(client_id, parent_version_id)?;
|
||||
txn.add_version(
|
||||
client_id,
|
||||
version_id,
|
||||
parent_version_id,
|
||||
history_segment.clone(),
|
||||
)?;
|
||||
|
||||
let expected = Version {
|
||||
version_id,
|
||||
parent_version_id,
|
||||
history_segment,
|
||||
};
|
||||
|
||||
let version = txn
|
||||
.get_version_by_parent(client_id, parent_version_id)?
|
||||
.unwrap();
|
||||
assert_eq!(version, expected);
|
||||
|
||||
let version = txn.get_version(client_id, version_id)?.unwrap();
|
||||
assert_eq!(version, expected);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_snapshots() -> anyhow::Result<()> {
|
||||
let storage = InMemoryStorage::new();
|
||||
let mut txn = storage.txn()?;
|
||||
|
||||
let client_id = Uuid::new_v4();
|
||||
|
||||
txn.new_client(client_id, Uuid::new_v4())?;
|
||||
assert!(txn.get_client(client_id)?.unwrap().snapshot.is_none());
|
||||
|
||||
let snap = Snapshot {
|
||||
version_id: Uuid::new_v4(),
|
||||
timestamp: Utc::now(),
|
||||
versions_since: 3,
|
||||
};
|
||||
txn.set_snapshot(client_id, snap.clone(), vec![9, 8, 9])?;
|
||||
|
||||
assert_eq!(
|
||||
txn.get_snapshot_data(client_id, snap.version_id)?.unwrap(),
|
||||
vec![9, 8, 9]
|
||||
);
|
||||
assert_eq!(txn.get_client(client_id)?.unwrap().snapshot, Some(snap));
|
||||
|
||||
let snap2 = Snapshot {
|
||||
version_id: Uuid::new_v4(),
|
||||
timestamp: Utc::now(),
|
||||
versions_since: 10,
|
||||
};
|
||||
txn.set_snapshot(client_id, snap2.clone(), vec![0, 2, 4, 6])?;
|
||||
|
||||
assert_eq!(
|
||||
txn.get_snapshot_data(client_id, snap2.version_id)?.unwrap(),
|
||||
vec![0, 2, 4, 6]
|
||||
);
|
||||
assert_eq!(txn.get_client(client_id)?.unwrap().snapshot, Some(snap2));
|
||||
|
||||
// check that mismatched version is detected
|
||||
assert!(txn.get_snapshot_data(client_id, Uuid::new_v4()).is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
use chrono::{DateTime, Utc};
|
||||
use uuid::Uuid;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
mod inmemory;
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
pub use inmemory::InMemoryStorage;
|
||||
|
||||
mod sqlite;
|
||||
pub use self::sqlite::SqliteStorage;
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub struct Client {
|
||||
/// The latest version for this client (may be the nil version)
|
||||
pub latest_version_id: Uuid,
|
||||
/// Data about the latest snapshot for this client
|
||||
pub snapshot: Option<Snapshot>,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub struct Snapshot {
|
||||
/// ID of the version at which this snapshot was made
|
||||
pub version_id: Uuid,
|
||||
|
||||
/// Timestamp at which this snapshot was set
|
||||
pub timestamp: DateTime<Utc>,
|
||||
|
||||
/// Number of versions since this snapshot was made
|
||||
pub versions_since: u32,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||
pub struct Version {
|
||||
pub version_id: Uuid,
|
||||
pub parent_version_id: Uuid,
|
||||
pub history_segment: Vec<u8>,
|
||||
}
|
||||
|
||||
pub trait StorageTxn {
|
||||
/// Get information about the given client
|
||||
fn get_client(&mut self, client_id: Uuid) -> anyhow::Result<Option<Client>>;
|
||||
|
||||
/// Create a new client with the given latest_version_id
|
||||
fn new_client(&mut self, client_id: Uuid, latest_version_id: Uuid) -> anyhow::Result<()>;
|
||||
|
||||
/// Set the client's most recent snapshot.
|
||||
fn set_snapshot(
|
||||
&mut self,
|
||||
client_id: Uuid,
|
||||
snapshot: Snapshot,
|
||||
data: Vec<u8>,
|
||||
) -> anyhow::Result<()>;
|
||||
|
||||
/// Get the data for the most recent snapshot. The version_id
|
||||
/// is used to verify that the snapshot is for the correct version.
|
||||
fn get_snapshot_data(
|
||||
&mut self,
|
||||
client_id: Uuid,
|
||||
version_id: Uuid,
|
||||
) -> anyhow::Result<Option<Vec<u8>>>;
|
||||
|
||||
/// Get a version, indexed by parent version id
|
||||
fn get_version_by_parent(
|
||||
&mut self,
|
||||
client_id: Uuid,
|
||||
parent_version_id: Uuid,
|
||||
) -> anyhow::Result<Option<Version>>;
|
||||
|
||||
/// Get a version, indexed by its own version id
|
||||
fn get_version(&mut self, client_id: Uuid, version_id: Uuid)
|
||||
-> anyhow::Result<Option<Version>>;
|
||||
|
||||
/// Add a version (that must not already exist), and
|
||||
/// - update latest_version_id
|
||||
/// - increment snapshot.versions_since
|
||||
fn add_version(
|
||||
&mut self,
|
||||
client_id: Uuid,
|
||||
version_id: Uuid,
|
||||
parent_version_id: Uuid,
|
||||
history_segment: Vec<u8>,
|
||||
) -> anyhow::Result<()>;
|
||||
|
||||
/// Commit any changes made in the transaction. It is an error to call this more than
|
||||
/// once. It is safe to skip this call for read-only operations.
|
||||
fn commit(&mut self) -> anyhow::Result<()>;
|
||||
}
|
||||
|
||||
/// A trait for objects able to act as storage. Most of the interesting behavior is in the
|
||||
/// [`crate::storage::StorageTxn`] trait.
|
||||
pub trait Storage: Send + Sync {
|
||||
/// Begin a transaction
|
||||
fn txn<'a>(&'a self) -> anyhow::Result<Box<dyn StorageTxn + 'a>>;
|
||||
}
|
||||
@@ -1,451 +0,0 @@
|
||||
use super::{Client, Snapshot, Storage, StorageTxn, Uuid, Version};
|
||||
use anyhow::Context;
|
||||
use chrono::{TimeZone, Utc};
|
||||
use rusqlite::types::{FromSql, ToSql};
|
||||
use rusqlite::{params, Connection, OptionalExtension};
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
enum SqliteError {
|
||||
#[error("Failed to create SQLite transaction")]
|
||||
CreateTransactionFailed,
|
||||
}
|
||||
|
||||
/// Newtype to allow implementing `FromSql` for foreign `uuid::Uuid`
|
||||
struct StoredUuid(Uuid);
|
||||
|
||||
/// Conversion from Uuid stored as a string (rusqlite's uuid feature stores as binary blob)
|
||||
impl FromSql for StoredUuid {
|
||||
fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult<Self> {
|
||||
let u = Uuid::parse_str(value.as_str()?)
|
||||
.map_err(|_| rusqlite::types::FromSqlError::InvalidType)?;
|
||||
Ok(StoredUuid(u))
|
||||
}
|
||||
}
|
||||
|
||||
/// Store Uuid as string in database
|
||||
impl ToSql for StoredUuid {
|
||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
|
||||
let s = self.0.to_string();
|
||||
Ok(s.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// An on-disk storage backend which uses SQLite
|
||||
pub struct SqliteStorage {
|
||||
db_file: std::path::PathBuf,
|
||||
}
|
||||
|
||||
impl SqliteStorage {
|
||||
fn new_connection(&self) -> anyhow::Result<Connection> {
|
||||
Ok(Connection::open(&self.db_file)?)
|
||||
}
|
||||
|
||||
pub fn new<P: AsRef<Path>>(directory: P) -> anyhow::Result<SqliteStorage> {
|
||||
std::fs::create_dir_all(&directory)?;
|
||||
let db_file = directory.as_ref().join("taskchampion-sync-server.sqlite3");
|
||||
|
||||
let o = SqliteStorage { db_file };
|
||||
|
||||
{
|
||||
let mut con = o.new_connection()?;
|
||||
let txn = con.transaction()?;
|
||||
|
||||
let queries = vec![
|
||||
"CREATE TABLE IF NOT EXISTS clients (
|
||||
client_id STRING PRIMARY KEY,
|
||||
latest_version_id STRING,
|
||||
snapshot_version_id STRING,
|
||||
versions_since_snapshot INTEGER,
|
||||
snapshot_timestamp INTEGER,
|
||||
snapshot BLOB);",
|
||||
"CREATE TABLE IF NOT EXISTS versions (version_id STRING PRIMARY KEY, client_id STRING, parent_version_id STRING, history_segment BLOB);",
|
||||
"CREATE INDEX IF NOT EXISTS versions_by_parent ON versions (parent_version_id);",
|
||||
];
|
||||
for q in queries {
|
||||
txn.execute(q, [])
|
||||
.context("Error while creating SQLite tables")?;
|
||||
}
|
||||
txn.commit()?;
|
||||
}
|
||||
|
||||
Ok(o)
|
||||
}
|
||||
}
|
||||
|
||||
impl Storage for SqliteStorage {
|
||||
fn txn<'a>(&'a self) -> anyhow::Result<Box<dyn StorageTxn + 'a>> {
|
||||
let con = self.new_connection()?;
|
||||
let t = Txn { con };
|
||||
Ok(Box::new(t))
|
||||
}
|
||||
}
|
||||
|
||||
struct Txn {
|
||||
con: Connection,
|
||||
}
|
||||
|
||||
impl Txn {
|
||||
fn get_txn(&mut self) -> Result<rusqlite::Transaction, SqliteError> {
|
||||
self.con
|
||||
.transaction()
|
||||
.map_err(|_e| SqliteError::CreateTransactionFailed)
|
||||
}
|
||||
|
||||
/// Implementation for queries from the versions table
|
||||
fn get_version_impl(
|
||||
&mut self,
|
||||
query: &'static str,
|
||||
client_id: Uuid,
|
||||
version_id_arg: Uuid,
|
||||
) -> anyhow::Result<Option<Version>> {
|
||||
let t = self.get_txn()?;
|
||||
let r = t
|
||||
.query_row(
|
||||
query,
|
||||
params![&StoredUuid(version_id_arg), &StoredUuid(client_id)],
|
||||
|r| {
|
||||
let version_id: StoredUuid = r.get("version_id")?;
|
||||
let parent_version_id: StoredUuid = r.get("parent_version_id")?;
|
||||
|
||||
Ok(Version {
|
||||
version_id: version_id.0,
|
||||
parent_version_id: parent_version_id.0,
|
||||
history_segment: r.get("history_segment")?,
|
||||
})
|
||||
},
|
||||
)
|
||||
.optional()
|
||||
.context("Error getting version")?;
|
||||
Ok(r)
|
||||
}
|
||||
}
|
||||
|
||||
impl StorageTxn for Txn {
|
||||
fn get_client(&mut self, client_id: Uuid) -> anyhow::Result<Option<Client>> {
|
||||
let t = self.get_txn()?;
|
||||
let result: Option<Client> = t
|
||||
.query_row(
|
||||
"SELECT
|
||||
latest_version_id,
|
||||
snapshot_timestamp,
|
||||
versions_since_snapshot,
|
||||
snapshot_version_id
|
||||
FROM clients
|
||||
WHERE client_id = ?
|
||||
LIMIT 1",
|
||||
[&StoredUuid(client_id)],
|
||||
|r| {
|
||||
let latest_version_id: StoredUuid = r.get(0)?;
|
||||
let snapshot_timestamp: Option<i64> = r.get(1)?;
|
||||
let versions_since_snapshot: Option<u32> = r.get(2)?;
|
||||
let snapshot_version_id: Option<StoredUuid> = r.get(3)?;
|
||||
|
||||
// if all of the relevant fields are non-NULL, return a snapshot
|
||||
let snapshot = match (
|
||||
snapshot_timestamp,
|
||||
versions_since_snapshot,
|
||||
snapshot_version_id,
|
||||
) {
|
||||
(Some(ts), Some(vs), Some(v)) => Some(Snapshot {
|
||||
version_id: v.0,
|
||||
timestamp: Utc.timestamp(ts, 0),
|
||||
versions_since: vs,
|
||||
}),
|
||||
_ => None,
|
||||
};
|
||||
Ok(Client {
|
||||
latest_version_id: latest_version_id.0,
|
||||
snapshot,
|
||||
})
|
||||
},
|
||||
)
|
||||
.optional()
|
||||
.context("Error getting client")?;
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn new_client(&mut self, client_id: Uuid, latest_version_id: Uuid) -> anyhow::Result<()> {
|
||||
let t = self.get_txn()?;
|
||||
|
||||
t.execute(
|
||||
"INSERT OR REPLACE INTO clients (client_id, latest_version_id) VALUES (?, ?)",
|
||||
params![&StoredUuid(client_id), &StoredUuid(latest_version_id)],
|
||||
)
|
||||
.context("Error creating/updating client")?;
|
||||
t.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_snapshot(
|
||||
&mut self,
|
||||
client_id: Uuid,
|
||||
snapshot: Snapshot,
|
||||
data: Vec<u8>,
|
||||
) -> anyhow::Result<()> {
|
||||
let t = self.get_txn()?;
|
||||
|
||||
t.execute(
|
||||
"UPDATE clients
|
||||
SET
|
||||
snapshot_version_id = ?,
|
||||
snapshot_timestamp = ?,
|
||||
versions_since_snapshot = ?,
|
||||
snapshot = ?
|
||||
WHERE client_id = ?",
|
||||
params![
|
||||
&StoredUuid(snapshot.version_id),
|
||||
snapshot.timestamp.timestamp(),
|
||||
snapshot.versions_since,
|
||||
data,
|
||||
&StoredUuid(client_id),
|
||||
],
|
||||
)
|
||||
.context("Error creating/updating snapshot")?;
|
||||
t.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_snapshot_data(
|
||||
&mut self,
|
||||
client_id: Uuid,
|
||||
version_id: Uuid,
|
||||
) -> anyhow::Result<Option<Vec<u8>>> {
|
||||
let t = self.get_txn()?;
|
||||
let r = t
|
||||
.query_row(
|
||||
"SELECT snapshot, snapshot_version_id FROM clients WHERE client_id = ?",
|
||||
params![&StoredUuid(client_id)],
|
||||
|r| {
|
||||
let v: StoredUuid = r.get("snapshot_version_id")?;
|
||||
let d: Vec<u8> = r.get("snapshot")?;
|
||||
Ok((v.0, d))
|
||||
},
|
||||
)
|
||||
.optional()
|
||||
.context("Error getting snapshot")?;
|
||||
r.map(|(v, d)| {
|
||||
if v != version_id {
|
||||
return Err(anyhow::anyhow!("unexpected snapshot_version_id"));
|
||||
}
|
||||
|
||||
Ok(d)
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
|
||||
fn get_version_by_parent(
|
||||
&mut self,
|
||||
client_id: Uuid,
|
||||
parent_version_id: Uuid,
|
||||
) -> anyhow::Result<Option<Version>> {
|
||||
self.get_version_impl(
|
||||
"SELECT version_id, parent_version_id, history_segment FROM versions WHERE parent_version_id = ? AND client_id = ?",
|
||||
client_id,
|
||||
parent_version_id)
|
||||
}
|
||||
|
||||
fn get_version(
|
||||
&mut self,
|
||||
client_id: Uuid,
|
||||
version_id: Uuid,
|
||||
) -> anyhow::Result<Option<Version>> {
|
||||
self.get_version_impl(
|
||||
"SELECT version_id, parent_version_id, history_segment FROM versions WHERE version_id = ? AND client_id = ?",
|
||||
client_id,
|
||||
version_id)
|
||||
}
|
||||
|
||||
fn add_version(
|
||||
&mut self,
|
||||
client_id: Uuid,
|
||||
version_id: Uuid,
|
||||
parent_version_id: Uuid,
|
||||
history_segment: Vec<u8>,
|
||||
) -> anyhow::Result<()> {
|
||||
let t = self.get_txn()?;
|
||||
|
||||
t.execute(
|
||||
"INSERT INTO versions (version_id, client_id, parent_version_id, history_segment) VALUES(?, ?, ?, ?)",
|
||||
params![
|
||||
StoredUuid(version_id),
|
||||
StoredUuid(client_id),
|
||||
StoredUuid(parent_version_id),
|
||||
history_segment
|
||||
]
|
||||
)
|
||||
.context("Error adding version")?;
|
||||
t.execute(
|
||||
"UPDATE clients
|
||||
SET
|
||||
latest_version_id = ?,
|
||||
versions_since_snapshot = versions_since_snapshot + 1
|
||||
WHERE client_id = ?",
|
||||
params![StoredUuid(version_id), StoredUuid(client_id),],
|
||||
)
|
||||
.context("Error updating client for new version")?;
|
||||
|
||||
t.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn commit(&mut self) -> anyhow::Result<()> {
|
||||
// FIXME: Note the queries aren't currently run in a
|
||||
// transaction, as storing the transaction object and a pooled
|
||||
// connection in the `Txn` object is complex.
|
||||
// https://github.com/taskchampion/taskchampion/pull/206#issuecomment-860336073
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use chrono::DateTime;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn test_emtpy_dir() -> anyhow::Result<()> {
|
||||
let tmp_dir = TempDir::new()?;
|
||||
let non_existant = tmp_dir.path().join("subdir");
|
||||
let storage = SqliteStorage::new(non_existant)?;
|
||||
let mut txn = storage.txn()?;
|
||||
let maybe_client = txn.get_client(Uuid::new_v4())?;
|
||||
assert!(maybe_client.is_none());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_client_empty() -> anyhow::Result<()> {
|
||||
let tmp_dir = TempDir::new()?;
|
||||
let storage = SqliteStorage::new(tmp_dir.path())?;
|
||||
let mut txn = storage.txn()?;
|
||||
let maybe_client = txn.get_client(Uuid::new_v4())?;
|
||||
assert!(maybe_client.is_none());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_client_storage() -> anyhow::Result<()> {
|
||||
let tmp_dir = TempDir::new()?;
|
||||
let storage = SqliteStorage::new(tmp_dir.path())?;
|
||||
let mut txn = storage.txn()?;
|
||||
|
||||
let client_id = Uuid::new_v4();
|
||||
let latest_version_id = Uuid::new_v4();
|
||||
txn.new_client(client_id, latest_version_id)?;
|
||||
|
||||
let client = txn.get_client(client_id)?.unwrap();
|
||||
assert_eq!(client.latest_version_id, latest_version_id);
|
||||
assert!(client.snapshot.is_none());
|
||||
|
||||
let latest_version_id = Uuid::new_v4();
|
||||
txn.add_version(client_id, latest_version_id, Uuid::new_v4(), vec![1, 1])?;
|
||||
|
||||
let client = txn.get_client(client_id)?.unwrap();
|
||||
assert_eq!(client.latest_version_id, latest_version_id);
|
||||
assert!(client.snapshot.is_none());
|
||||
|
||||
let snap = Snapshot {
|
||||
version_id: Uuid::new_v4(),
|
||||
timestamp: "2014-11-28T12:00:09Z".parse::<DateTime<Utc>>().unwrap(),
|
||||
versions_since: 4,
|
||||
};
|
||||
txn.set_snapshot(client_id, snap.clone(), vec![1, 2, 3])?;
|
||||
|
||||
let client = txn.get_client(client_id)?.unwrap();
|
||||
assert_eq!(client.latest_version_id, latest_version_id);
|
||||
assert_eq!(client.snapshot.unwrap(), snap);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gvbp_empty() -> anyhow::Result<()> {
|
||||
let tmp_dir = TempDir::new()?;
|
||||
let storage = SqliteStorage::new(tmp_dir.path())?;
|
||||
let mut txn = storage.txn()?;
|
||||
let maybe_version = txn.get_version_by_parent(Uuid::new_v4(), Uuid::new_v4())?;
|
||||
assert!(maybe_version.is_none());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_version_and_get_version() -> anyhow::Result<()> {
|
||||
let tmp_dir = TempDir::new()?;
|
||||
let storage = SqliteStorage::new(tmp_dir.path())?;
|
||||
let mut txn = storage.txn()?;
|
||||
|
||||
let client_id = Uuid::new_v4();
|
||||
let version_id = Uuid::new_v4();
|
||||
let parent_version_id = Uuid::new_v4();
|
||||
let history_segment = b"abc".to_vec();
|
||||
txn.add_version(
|
||||
client_id,
|
||||
version_id,
|
||||
parent_version_id,
|
||||
history_segment.clone(),
|
||||
)?;
|
||||
|
||||
let expected = Version {
|
||||
version_id,
|
||||
parent_version_id,
|
||||
history_segment,
|
||||
};
|
||||
|
||||
let version = txn
|
||||
.get_version_by_parent(client_id, parent_version_id)?
|
||||
.unwrap();
|
||||
assert_eq!(version, expected);
|
||||
|
||||
let version = txn.get_version(client_id, version_id)?.unwrap();
|
||||
assert_eq!(version, expected);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_snapshots() -> anyhow::Result<()> {
|
||||
let tmp_dir = TempDir::new()?;
|
||||
let storage = SqliteStorage::new(tmp_dir.path())?;
|
||||
let mut txn = storage.txn()?;
|
||||
|
||||
let client_id = Uuid::new_v4();
|
||||
|
||||
txn.new_client(client_id, Uuid::new_v4())?;
|
||||
assert!(txn.get_client(client_id)?.unwrap().snapshot.is_none());
|
||||
|
||||
let snap = Snapshot {
|
||||
version_id: Uuid::new_v4(),
|
||||
timestamp: "2013-10-08T12:00:09Z".parse::<DateTime<Utc>>().unwrap(),
|
||||
versions_since: 3,
|
||||
};
|
||||
txn.set_snapshot(client_id, snap.clone(), vec![9, 8, 9])?;
|
||||
|
||||
assert_eq!(
|
||||
txn.get_snapshot_data(client_id, snap.version_id)?.unwrap(),
|
||||
vec![9, 8, 9]
|
||||
);
|
||||
assert_eq!(txn.get_client(client_id)?.unwrap().snapshot, Some(snap));
|
||||
|
||||
let snap2 = Snapshot {
|
||||
version_id: Uuid::new_v4(),
|
||||
timestamp: "2014-11-28T12:00:09Z".parse::<DateTime<Utc>>().unwrap(),
|
||||
versions_since: 10,
|
||||
};
|
||||
txn.set_snapshot(client_id, snap2.clone(), vec![0, 2, 4, 6])?;
|
||||
|
||||
assert_eq!(
|
||||
txn.get_snapshot_data(client_id, snap2.version_id)?.unwrap(),
|
||||
vec![0, 2, 4, 6]
|
||||
);
|
||||
assert_eq!(txn.get_client(client_id)?.unwrap().snapshot, Some(snap2));
|
||||
|
||||
// check that mismatched version is detected
|
||||
assert!(txn.get_snapshot_data(client_id, Uuid::new_v4()).is_err());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@ rust-version = "1.70.0"
|
||||
default = ["server-sync", "server-gcp"]
|
||||
|
||||
# Support for sync to a server
|
||||
server-sync = ["encryption", "dep:ureq"]
|
||||
server-sync = ["encryption", "dep:ureq", "dep:url"]
|
||||
# Support for sync to GCP
|
||||
server-gcp = ["cloud", "encryption", "dep:google-cloud-storage", "dep:tokio"]
|
||||
# (private) Support for sync protocol encryption
|
||||
@@ -43,10 +43,12 @@ byteorder.workspace = true
|
||||
ring.workspace = true
|
||||
google-cloud-storage.workspace = true
|
||||
tokio.workspace = true
|
||||
url.workspace = true
|
||||
|
||||
google-cloud-storage.optional = true
|
||||
tokio.optional = true
|
||||
ureq.optional = true
|
||||
url.optional = true
|
||||
ring.optional = true
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
@@ -332,7 +332,7 @@ mod test {
|
||||
let version_id = Uuid::parse_str("b0517957-f912-4d49-8330-f612e73030c4").unwrap();
|
||||
let encryption_secret = b"b4a4e6b7b811eda1dc1a2693ded".to_vec();
|
||||
let client_id = Uuid::parse_str("0666d464-418a-4a08-ad53-6f15c78270cd").unwrap();
|
||||
let salt = client_id.as_ref().to_vec();
|
||||
let salt = client_id.as_bytes().to_vec();
|
||||
(version_id, salt, encryption_secret)
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::server::{
|
||||
VersionId,
|
||||
};
|
||||
use std::time::Duration;
|
||||
use url::Url;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::encryption::{Cryptor, Sealed, Secret, Unsealed};
|
||||
@@ -28,8 +29,16 @@ impl SyncServer {
|
||||
/// identify this client to the server. Multiple replicas synchronizing the same task history
|
||||
/// should use the same client_id.
|
||||
pub fn new(origin: String, client_id: Uuid, encryption_secret: Vec<u8>) -> Result<SyncServer> {
|
||||
let origin = Url::parse(&origin)
|
||||
.map_err(|_| Error::Server(format!("Could not parse {} as a URL", origin)))?;
|
||||
if origin.path() != "/" {
|
||||
return Err(Error::Server(format!(
|
||||
"Server origin must have an empty path; got {}",
|
||||
origin
|
||||
)));
|
||||
}
|
||||
Ok(SyncServer {
|
||||
origin,
|
||||
origin: origin.to_string(),
|
||||
client_id,
|
||||
cryptor: Cryptor::new(client_id, &Secret(encryption_secret.to_vec()))?,
|
||||
agent: ureq::AgentBuilder::new()
|
||||
@@ -85,10 +94,7 @@ impl Server for SyncServer {
|
||||
parent_version_id: VersionId,
|
||||
history_segment: HistorySegment,
|
||||
) -> Result<(AddVersionResult, SnapshotUrgency)> {
|
||||
let url = format!(
|
||||
"{}/v1/client/add-version/{}",
|
||||
self.origin, parent_version_id
|
||||
);
|
||||
let url = format!("{}v1/client/add-version/{}", self.origin, parent_version_id);
|
||||
let unsealed = Unsealed {
|
||||
version_id: parent_version_id,
|
||||
payload: history_segment,
|
||||
@@ -121,7 +127,7 @@ impl Server for SyncServer {
|
||||
|
||||
fn get_child_version(&mut self, parent_version_id: VersionId) -> Result<GetVersionResult> {
|
||||
let url = format!(
|
||||
"{}/v1/client/get-child-version/{}",
|
||||
"{}v1/client/get-child-version/{}",
|
||||
self.origin, parent_version_id
|
||||
);
|
||||
match self
|
||||
@@ -150,7 +156,7 @@ impl Server for SyncServer {
|
||||
}
|
||||
|
||||
fn add_snapshot(&mut self, version_id: VersionId, snapshot: Snapshot) -> Result<()> {
|
||||
let url = format!("{}/v1/client/add-snapshot/{}", self.origin, version_id);
|
||||
let url = format!("{}v1/client/add-snapshot/{}", self.origin, version_id);
|
||||
let unsealed = Unsealed {
|
||||
version_id,
|
||||
payload: snapshot,
|
||||
@@ -166,7 +172,7 @@ impl Server for SyncServer {
|
||||
}
|
||||
|
||||
fn get_snapshot(&mut self) -> Result<Option<(VersionId, Snapshot)>> {
|
||||
let url = format!("{}/v1/client/snapshot", self.origin);
|
||||
let url = format!("{}v1/client/snapshot", self.origin);
|
||||
match self
|
||||
.agent
|
||||
.get(&url)
|
||||
|
||||
@@ -56,6 +56,22 @@ class TestHooksOnExit(TestCase):
|
||||
logs = hook.get_logs()
|
||||
self.assertEqual(logs["output"]["msgs"][0], "FEEDBACK")
|
||||
|
||||
def test_onexit_builtin_good_gets_changed_tasks(self):
|
||||
"""on-exit-good - a well-behaved, successful, on-exit hook."""
|
||||
hookname = 'on-exit-good'
|
||||
self.t.hooks.add_default(hookname, log=True)
|
||||
|
||||
code, out, err = self.t("add foo")
|
||||
self.assertIn("Created task", out)
|
||||
|
||||
hook = self.t.hooks[hookname]
|
||||
hook.assertTriggeredCount(1)
|
||||
hook.assertExitcode(0)
|
||||
|
||||
logs = hook.get_logs()
|
||||
self.assertEqual(logs["output"]["msgs"][0], "CHANGED TASK")
|
||||
self.assertEqual(logs["output"]["msgs"][1], "FEEDBACK")
|
||||
|
||||
def test_onexit_builtin_bad(self):
|
||||
"""on-exit-bad - a well-behaved, failing, on-exit hook."""
|
||||
hookname = 'on-exit-bad'
|
||||
|
||||
@@ -52,8 +52,8 @@ if __name__ == "__main__":
|
||||
unexpected = defaultdict(int)
|
||||
passed = defaultdict(int)
|
||||
|
||||
file = re.compile("^# (?:./)?(\S+\.t)(?:\.exe)?$")
|
||||
timestamp = re.compile("^# (\d+(?:\.\d+)?) ==>.*$")
|
||||
file = re.compile(r"^# (?:./)?(\S+\.t)(?:\.exe)?$")
|
||||
timestamp = re.compile(r"^# (\d+(?:\.\d+)?) ==>.*$")
|
||||
|
||||
expected_fail = re.compile(r"^not ok.*?#\s*TODO", re.I)
|
||||
unexpected_pass = re.compile(r"^not ok .*?#\s*FIXED", re.I)
|
||||
|
||||
Reference in New Issue
Block a user