Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc86a1e53f | ||
|
|
9b35ab37aa | ||
|
|
a9995808ec | ||
|
|
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
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup mdBook
|
- name: Setup mdBook
|
||||||
uses: peaceiris/actions-mdbook@v1
|
uses: peaceiris/actions-mdbook@v2
|
||||||
with:
|
with:
|
||||||
# if this changes, change it in .github/workflows/publish-docs.yml as well
|
# if this changes, change it in .github/workflows/publish-docs.yml as well
|
||||||
mdbook-version: '0.4.10'
|
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"
|
submodules: "recursive"
|
||||||
|
|
||||||
- name: Install cosign
|
- name: Install cosign
|
||||||
uses: sigstore/cosign-installer@v3.4.0
|
uses: sigstore/cosign-installer@v3.5.0
|
||||||
|
|
||||||
- name: Log into registry ${{ env.REGISTRY }}
|
- name: Log into registry ${{ env.REGISTRY }}
|
||||||
uses: docker/login-action@v3.1.0
|
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
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup mdBook
|
- name: Setup mdBook
|
||||||
uses: peaceiris/actions-mdbook@v1
|
uses: peaceiris/actions-mdbook@v2
|
||||||
with:
|
with:
|
||||||
# if this changes, change it in .github/workflows/checks.yml as well
|
# if this changes, change it in .github/workflows/checks.yml as well
|
||||||
mdbook-version: '0.4.10'
|
mdbook-version: '0.4.10'
|
||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
- run: mdbook build taskchampion/docs
|
- run: mdbook build taskchampion/docs
|
||||||
|
|
||||||
- name: Deploy
|
- name: Deploy
|
||||||
uses: peaceiris/actions-gh-pages@v3
|
uses: peaceiris/actions-gh-pages@v4
|
||||||
with:
|
with:
|
||||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
publish_dir: ./taskchampion/docs/book
|
publish_dir: ./taskchampion/docs/book
|
||||||
|
|||||||
3
.gitmodules
vendored
3
.gitmodules
vendored
@@ -1,3 +1,6 @@
|
|||||||
[submodule "src/libshared"]
|
[submodule "src/libshared"]
|
||||||
path = src/libshared
|
path = src/libshared
|
||||||
url = https://github.com/GothenburgBitFactory/libshared.git
|
url = https://github.com/GothenburgBitFactory/libshared.git
|
||||||
|
[submodule "src/tc/corrosion"]
|
||||||
|
path = src/tc/corrosion
|
||||||
|
url = https://github.com/corrosion-rs/corrosion.git
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
cmake_minimum_required (VERSION 3.22)
|
cmake_minimum_required (VERSION 3.22)
|
||||||
|
|
||||||
|
set (CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||||
|
|
||||||
|
project (task
|
||||||
|
VERSION 3.0.2
|
||||||
|
DESCRIPTION "Taskwarrior - a command-line TODO list manager"
|
||||||
|
HOMEPAGE_URL https://taskwarrior.org/)
|
||||||
|
|
||||||
set (CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake")
|
set (CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake")
|
||||||
|
|
||||||
include (FetchContent)
|
include (FetchContent)
|
||||||
@@ -7,11 +15,8 @@ include (CheckStructHasMember)
|
|||||||
|
|
||||||
set (HAVE_CMAKE true)
|
set (HAVE_CMAKE true)
|
||||||
|
|
||||||
project (task)
|
|
||||||
include (CXXSniffer)
|
include (CXXSniffer)
|
||||||
|
|
||||||
set (PROJECT_VERSION "3.0.0")
|
|
||||||
|
|
||||||
OPTION (ENABLE_WASM "Enable 'wasm' support" OFF)
|
OPTION (ENABLE_WASM "Enable 'wasm' support" OFF)
|
||||||
|
|
||||||
if (ENABLE_WASM)
|
if (ENABLE_WASM)
|
||||||
@@ -19,14 +24,14 @@ if (ENABLE_WASM)
|
|||||||
set(CMAKE_EXECUTABLE_SUFFIX ".js")
|
set(CMAKE_EXECUTABLE_SUFFIX ".js")
|
||||||
endif (ENABLE_WASM)
|
endif (ENABLE_WASM)
|
||||||
|
|
||||||
message ("-- Looking for libshared")
|
message ("-- Looking for git submodules")
|
||||||
if (EXISTS ${CMAKE_SOURCE_DIR}/src/libshared/src)
|
if (EXISTS ${CMAKE_SOURCE_DIR}/src/libshared/src AND EXISTS ${CMAKE_SOURCE_DIR}/src/tc/corrosion)
|
||||||
message ("-- Found libshared")
|
message ("-- Found git submodules")
|
||||||
else (EXISTS ${CMAKE_SOURCE_DIR}/src/libshared/src)
|
else (EXISTS ${CMAKE_SOURCE_DIR}/src/libshared/src)
|
||||||
message ("-- Cloning libshared")
|
message ("-- Cloning git submodules")
|
||||||
execute_process (COMMAND git submodule update --init
|
execute_process (COMMAND git submodule update --init
|
||||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
|
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR})
|
||||||
endif (EXISTS ${CMAKE_SOURCE_DIR}/src/libshared/src)
|
endif (EXISTS ${CMAKE_SOURCE_DIR}/src/libshared/src AND EXISTS ${CMAKE_SOURCE_DIR}/src/tc/corrosion)
|
||||||
|
|
||||||
message ("-- Looking for SHA1 references")
|
message ("-- Looking for SHA1 references")
|
||||||
if (EXISTS ${CMAKE_SOURCE_DIR}/.git/index)
|
if (EXISTS ${CMAKE_SOURCE_DIR}/.git/index)
|
||||||
@@ -147,7 +152,7 @@ if (EXISTS performance)
|
|||||||
add_subdirectory (performance EXCLUDE_FROM_ALL)
|
add_subdirectory (performance EXCLUDE_FROM_ALL)
|
||||||
endif (EXISTS performance)
|
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})
|
foreach (doc_FILE ${doc_FILES})
|
||||||
install (FILES ${doc_FILE} DESTINATION ${TASK_DOCDIR})
|
install (FILES ${doc_FILE} DESTINATION ${TASK_DOCDIR})
|
||||||
endforeach (doc_FILE)
|
endforeach (doc_FILE)
|
||||||
|
|||||||
563
Cargo.lock
generated
563
Cargo.lock
generated
@@ -2,188 +2,6 @@
|
|||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 3
|
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]]
|
[[package]]
|
||||||
name = "addr2line"
|
name = "addr2line"
|
||||||
version = "0.21.0"
|
version = "0.21.0"
|
||||||
@@ -210,18 +28,6 @@ dependencies = [
|
|||||||
"version_check",
|
"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]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "1.1.2"
|
version = "1.1.2"
|
||||||
@@ -231,21 +37,6 @@ dependencies = [
|
|||||||
"memchr",
|
"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]]
|
[[package]]
|
||||||
name = "android_system_properties"
|
name = "android_system_properties"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
@@ -255,55 +46,6 @@ dependencies = [
|
|||||||
"libc",
|
"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]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.66"
|
version = "1.0.66"
|
||||||
@@ -418,27 +160,6 @@ dependencies = [
|
|||||||
"generic-array",
|
"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]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
version = "3.12.0"
|
version = "3.12.0"
|
||||||
@@ -457,23 +178,11 @@ version = "1.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
|
checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "bytestring"
|
|
||||||
version = "1.0.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "90706ba19e97b90786e19dc0d5e2abd80008d99d4c0c5d1ad0b5e72cec7c494d"
|
|
||||||
dependencies = [
|
|
||||||
"bytes",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cc"
|
name = "cc"
|
||||||
version = "1.0.73"
|
version = "1.0.73"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
|
checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
|
||||||
dependencies = [
|
|
||||||
"jobserver",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg-if"
|
name = "cfg-if"
|
||||||
@@ -497,63 +206,12 @@ dependencies = [
|
|||||||
"winapi",
|
"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]]
|
[[package]]
|
||||||
name = "const-oid"
|
name = "const-oid"
|
||||||
version = "0.9.6"
|
version = "0.9.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
|
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]]
|
[[package]]
|
||||||
name = "core-foundation-sys"
|
name = "core-foundation-sys"
|
||||||
version = "0.8.3"
|
version = "0.8.3"
|
||||||
@@ -599,19 +257,6 @@ dependencies = [
|
|||||||
"zeroize",
|
"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]]
|
[[package]]
|
||||||
name = "diff"
|
name = "diff"
|
||||||
version = "0.1.12"
|
version = "0.1.12"
|
||||||
@@ -643,19 +288,6 @@ dependencies = [
|
|||||||
"cfg-if",
|
"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]]
|
[[package]]
|
||||||
name = "errno"
|
name = "errno"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
@@ -972,7 +604,7 @@ version = "0.12.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "607c8a29735385251a339424dd462993c0fed8fa09d378f259377df08c126022"
|
checksum = "607c8a29735385251a339424dd462993c0fed8fa09d378f259377df08c126022"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ahash 0.7.6",
|
"ahash",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1054,12 +686,6 @@ version = "1.0.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
|
checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "humantime"
|
|
||||||
version = "2.1.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper"
|
name = "hyper"
|
||||||
version = "0.14.20"
|
version = "0.14.20"
|
||||||
@@ -1145,17 +771,12 @@ dependencies = [
|
|||||||
name = "integration-tests"
|
name = "integration-tests"
|
||||||
version = "0.4.1"
|
version = "0.4.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"actix-rt",
|
|
||||||
"actix-web",
|
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"cc",
|
"cc",
|
||||||
"env_logger",
|
|
||||||
"lazy_static",
|
"lazy_static",
|
||||||
"log",
|
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
"taskchampion",
|
"taskchampion",
|
||||||
"taskchampion-lib",
|
"taskchampion-lib",
|
||||||
"taskchampion-sync-server",
|
|
||||||
"tempfile",
|
"tempfile",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -1176,18 +797,6 @@ version = "2.9.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
|
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]]
|
[[package]]
|
||||||
name = "itertools"
|
name = "itertools"
|
||||||
version = "0.10.5"
|
version = "0.10.5"
|
||||||
@@ -1203,15 +812,6 @@ version = "1.0.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d"
|
checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "jobserver"
|
|
||||||
version = "0.1.26"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.59"
|
version = "0.3.59"
|
||||||
@@ -1235,12 +835,6 @@ dependencies = [
|
|||||||
"simple_asn1",
|
"simple_asn1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "language-tags"
|
|
||||||
version = "0.3.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "lazy_static"
|
name = "lazy_static"
|
||||||
version = "1.4.0"
|
version = "1.4.0"
|
||||||
@@ -1296,24 +890,6 @@ version = "0.3.8"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519"
|
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]]
|
[[package]]
|
||||||
name = "lock_api"
|
name = "lock_api"
|
||||||
version = "0.4.7"
|
version = "0.4.7"
|
||||||
@@ -1380,7 +956,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
|
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
|
||||||
"wasi",
|
"wasi",
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
@@ -1464,12 +1039,6 @@ dependencies = [
|
|||||||
"windows-sys 0.45.0",
|
"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]]
|
[[package]]
|
||||||
name = "pem"
|
name = "pem"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
@@ -1810,12 +1379,12 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls"
|
name = "rustls"
|
||||||
version = "0.21.7"
|
version = "0.21.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8"
|
checksum = "7fecbfb7b1444f477b345853b1fce097a2c6fb637b2bfb87e6bc5db0f043fae4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"ring 0.16.20",
|
"ring 0.17.3",
|
||||||
"rustls-webpki",
|
"rustls-webpki",
|
||||||
"sct",
|
"sct",
|
||||||
]
|
]
|
||||||
@@ -1831,12 +1400,12 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustls-webpki"
|
name = "rustls-webpki"
|
||||||
version = "0.101.6"
|
version = "0.101.7"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3c7d5dece342910d9ba34d259310cae3e0154b873b35408b787b59bce53d34fe"
|
checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ring 0.16.20",
|
"ring 0.17.3",
|
||||||
"untrusted 0.7.1",
|
"untrusted 0.9.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1928,17 +1497,6 @@ dependencies = [
|
|||||||
"serde",
|
"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]]
|
[[package]]
|
||||||
name = "sha2"
|
name = "sha2"
|
||||||
version = "0.10.6"
|
version = "0.10.6"
|
||||||
@@ -1950,15 +1508,6 @@ dependencies = [
|
|||||||
"digest",
|
"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]]
|
[[package]]
|
||||||
name = "simple_asn1"
|
name = "simple_asn1"
|
||||||
version = "0.6.2"
|
version = "0.6.2"
|
||||||
@@ -2025,12 +1574,6 @@ dependencies = [
|
|||||||
"der",
|
"der",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "strsim"
|
|
||||||
version = "0.10.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strum"
|
name = "strum"
|
||||||
version = "0.25.0"
|
version = "0.25.0"
|
||||||
@@ -2039,9 +1582,9 @@ checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "strum_macros"
|
name = "strum_macros"
|
||||||
version = "0.25.0"
|
version = "0.25.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "fe9f3bd7d2e45dcc5e265fbb88d6513e4747d8ef9444cf01a533119bce28a157"
|
checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck",
|
"heck",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -2095,6 +1638,7 @@ dependencies = [
|
|||||||
"thiserror",
|
"thiserror",
|
||||||
"tokio",
|
"tokio",
|
||||||
"ureq",
|
"ureq",
|
||||||
|
"url",
|
||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -2109,27 +1653,6 @@ dependencies = [
|
|||||||
"taskchampion",
|
"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]]
|
[[package]]
|
||||||
name = "tempfile"
|
name = "tempfile"
|
||||||
version = "3.6.0"
|
version = "3.6.0"
|
||||||
@@ -2144,15 +1667,6 @@ dependencies = [
|
|||||||
"windows-sys 0.48.0",
|
"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]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.37"
|
version = "1.0.37"
|
||||||
@@ -2227,9 +1741,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.36.0"
|
version = "1.37.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931"
|
checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"backtrace",
|
"backtrace",
|
||||||
"bytes",
|
"bytes",
|
||||||
@@ -2238,7 +1752,6 @@ dependencies = [
|
|||||||
"num_cpus",
|
"num_cpus",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"signal-hook-registry",
|
|
||||||
"socket2 0.5.5",
|
"socket2 0.5.5",
|
||||||
"tokio-macros",
|
"tokio-macros",
|
||||||
"windows-sys 0.48.0",
|
"windows-sys 0.48.0",
|
||||||
@@ -2292,7 +1805,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "5d0ecdcb44a79f0fe9844f0c4f33a342cbcbb5117de8001e6ba0dc2351327d09"
|
checksum = "5d0ecdcb44a79f0fe9844f0c4f33a342cbcbb5117de8001e6ba0dc2351327d09"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
"log",
|
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"tracing-attributes",
|
"tracing-attributes",
|
||||||
"tracing-core",
|
"tracing-core",
|
||||||
@@ -2411,17 +1923,11 @@ version = "2.1.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "utf8parse"
|
|
||||||
version = "0.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uuid"
|
name = "uuid"
|
||||||
version = "1.7.0"
|
version = "1.8.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a"
|
checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"getrandom",
|
"getrandom",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -2593,15 +2099,6 @@ version = "0.4.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
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]]
|
[[package]]
|
||||||
name = "winapi-x86_64-pc-windows-gnu"
|
name = "winapi-x86_64-pc-windows-gnu"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
@@ -2835,33 +2332,3 @@ name = "zeroize"
|
|||||||
version = "1.7.0"
|
version = "1.7.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d"
|
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 = [
|
members = [
|
||||||
"taskchampion/taskchampion",
|
"taskchampion/taskchampion",
|
||||||
"taskchampion/sync-server",
|
|
||||||
"taskchampion/lib",
|
"taskchampion/lib",
|
||||||
"taskchampion/integration-tests",
|
"taskchampion/integration-tests",
|
||||||
"taskchampion/xtask",
|
"taskchampion/xtask",
|
||||||
@@ -16,17 +15,12 @@ exclude = [ "src/tc/rust" ]
|
|||||||
# All Rust dependencies are defined here, and then referenced by the
|
# All Rust dependencies are defined here, and then referenced by the
|
||||||
# Cargo.toml's in the members with `foo.workspace = true`.
|
# Cargo.toml's in the members with `foo.workspace = true`.
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
actix-rt = "2"
|
|
||||||
actix-web = "^4.3.1"
|
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
byteorder = "1.5"
|
byteorder = "1.5"
|
||||||
cc = "1.0.73"
|
cc = "1.0.73"
|
||||||
chrono = { version = "^0.4.22", features = ["serde"] }
|
chrono = { version = "^0.4.22", features = ["serde"] }
|
||||||
clap = { version = "^4.3.0", features = ["string"] }
|
|
||||||
env_logger = "^0.10.0"
|
|
||||||
ffizz-header = "0.5"
|
ffizz-header = "0.5"
|
||||||
flate2 = "1"
|
flate2 = "1"
|
||||||
futures = "^0.3.25"
|
|
||||||
google-cloud-storage = { version = "0.15.0", default-features = false, features = ["rustls-tls", "auth"] }
|
google-cloud-storage = { version = "0.15.0", default-features = false, features = ["rustls-tls", "auth"] }
|
||||||
lazy_static = "1"
|
lazy_static = "1"
|
||||||
libc = "0.2.136"
|
libc = "0.2.136"
|
||||||
@@ -44,4 +38,5 @@ tempfile = "3"
|
|||||||
tokio = { version = "1", features = ["rt-multi-thread"] }
|
tokio = { version = "1", features = ["rt-multi-thread"] }
|
||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
ureq = { version = "^2.9.0", features = ["tls"] }
|
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" }
|
||||||
|
|||||||
23
ChangeLog
23
ChangeLog
@@ -1,4 +1,21 @@
|
|||||||
------ current release ---------------------------
|
------ current release ---------------------------
|
||||||
|
|
||||||
|
3.0.2 -
|
||||||
|
|
||||||
|
- Fix an accidentally-included debug print which polluted output of
|
||||||
|
reports with the Taskwarrior version (#3389)
|
||||||
|
|
||||||
|
------ old releases ------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
3.0.0 -
|
3.0.0 -
|
||||||
|
|
||||||
- [BREAKING CHANGE] the sync functionality has been rewritten entirely, and
|
- [BREAKING CHANGE] the sync functionality has been rewritten entirely, and
|
||||||
@@ -19,7 +36,9 @@
|
|||||||
- `taskd.server`
|
- `taskd.server`
|
||||||
- `taskd.trust`
|
- `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:
|
Deep thanks to the following for contributions to this work:
|
||||||
|
|
||||||
@@ -229,8 +248,6 @@
|
|||||||
Thanks to bharatvaj for contributing.
|
Thanks to bharatvaj for contributing.
|
||||||
- TW #2581 Config entry with a trailing comment cannot be modified
|
- TW #2581 Config entry with a trailing comment cannot be modified
|
||||||
|
|
||||||
------ old releases ------------------------------
|
|
||||||
|
|
||||||
2.5.3 (2021-01-05) - 2f47226f91f0b02f7617912175274d9eed85924f
|
2.5.3 (2021-01-05) - 2f47226f91f0b02f7617912175274d9eed85924f
|
||||||
|
|
||||||
- #2375 task hangs then dies when certain tasks are present in a report
|
- #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)
|
* [Coding Style](coding_style.md)
|
||||||
* [Branching Model](branching.md)
|
* [Branching Model](branching.md)
|
||||||
* [Rust and C++](rust-and-c++.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
|
* CMake 3.0 or later
|
||||||
* gcc 7.0 or later, clang 6.0 or later, or a compiler with full C++17 support
|
* gcc 7.0 or later, clang 6.0 or later, or a compiler with full C++17 support
|
||||||
* libuuid (if not on macOS)
|
* 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)
|
* 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:
|
## Obtain and Build Code:
|
||||||
The following documentation works with CMake 3.14 and later.
|
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.
|
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
|
```sh
|
||||||
git clone https://github.com/GothenburgBitFactory/taskwarrior
|
git clone https://github.com/GothenburgBitFactory/taskwarrior
|
||||||
cd taskwarrior
|
cd taskwarrior
|
||||||
cmake -S . -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo
|
cmake -S . -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo
|
||||||
cmake --build build
|
cmake --build build
|
||||||
```
|
```
|
||||||
Other possible build types can be `Release` and `Debug`.
|
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.origin <origin>
|
||||||
$ task config sync.server.client_id <client_id>
|
$ 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
|
.SS Google Cloud Platform
|
||||||
|
|
||||||
To synchronize your tasks to GCP, use the GCP Console to create a new project,
|
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
|
entirely independent. Task data is encrypted by Taskwarrior, and the sync
|
||||||
server never sees un-encrypted data.
|
server never sees un-encrypted data.
|
||||||
|
|
||||||
To start the server, run it in your preferred HTTP hosting environment, using
|
The server is developed in
|
||||||
`--port` to set the TCP port on which it should listen. It is recommended to
|
https://github.com/GothenburgBitFactory/taskchampion-sync-server.
|
||||||
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
|
|
||||||
|
|
||||||
.SS Adding a New User
|
.SS Adding a New User
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ add_library (task STATIC CLI2.cpp CLI2.h
|
|||||||
TDB2.cpp TDB2.h
|
TDB2.cpp TDB2.h
|
||||||
Task.cpp Task.h
|
Task.cpp Task.h
|
||||||
Variant.cpp Variant.h
|
Variant.cpp Variant.h
|
||||||
|
Version.cpp Version.h
|
||||||
ViewTask.cpp ViewTask.h
|
ViewTask.cpp ViewTask.h
|
||||||
dependency.cpp
|
dependency.cpp
|
||||||
feedback.cpp
|
feedback.cpp
|
||||||
|
|||||||
27
src/TDB2.cpp
27
src/TDB2.cpp
@@ -29,6 +29,7 @@
|
|||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <vector>
|
||||||
#include <list>
|
#include <list>
|
||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
@@ -57,6 +58,14 @@ TDB2::TDB2 ()
|
|||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
void TDB2::open_replica (const std::string& location, bool create_if_missing)
|
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);
|
replica = tc::Replica(location, create_if_missing);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,6 +79,8 @@ void TDB2::add (Task& task)
|
|||||||
task.validate (true);
|
task.validate (true);
|
||||||
|
|
||||||
std::string uuid = task.get ("uuid");
|
std::string uuid = task.get ("uuid");
|
||||||
|
changes[uuid] = task;
|
||||||
|
|
||||||
auto innertask = replica.import_task_with_uuid (uuid);
|
auto innertask = replica.import_task_with_uuid (uuid);
|
||||||
|
|
||||||
{
|
{
|
||||||
@@ -110,12 +121,13 @@ void TDB2::add (Task& task)
|
|||||||
// update the cached working set with the new information
|
// update the cached working set with the new information
|
||||||
_working_set = std::make_optional (std::move (ws));
|
_working_set = std::make_optional (std::move (ws));
|
||||||
|
|
||||||
if (id.has_value ()) {
|
|
||||||
task.id = id.value();
|
|
||||||
}
|
|
||||||
|
|
||||||
// run hooks for this new task
|
// run hooks for this new task
|
||||||
Context::getContext ().hooks.onAdd (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);
|
task.validate (false);
|
||||||
auto uuid = task.get ("uuid");
|
auto uuid = task.get ("uuid");
|
||||||
|
|
||||||
|
changes[uuid] = task;
|
||||||
|
|
||||||
// invoke the hook and allow it to modify the task before updating
|
// invoke the hook and allow it to modify the task before updating
|
||||||
Task original;
|
Task original;
|
||||||
get (uuid, original);
|
get (uuid, original);
|
||||||
@@ -200,9 +214,10 @@ const tc::WorkingSet &TDB2::working_set ()
|
|||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
void TDB2::get_changes (std::vector <Task>& changes)
|
void TDB2::get_changes (std::vector <Task>& changes)
|
||||||
{
|
{
|
||||||
// TODO: changes in an invocation of `task` are not currently tracked, so this
|
std::map<std::string, Task>& changes_map = this->changes;
|
||||||
// list is always empty.
|
|
||||||
changes.clear();
|
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;
|
tc::Replica replica;
|
||||||
std::optional<tc::WorkingSet> _working_set;
|
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 ();
|
const tc::WorkingSet &working_set ();
|
||||||
static std::string option_string (std::string input);
|
static std::string option_string (std::string input);
|
||||||
static void show_diff (const std::string&, const std::string&, const std::string&);
|
static void show_diff (const std::string&, const std::string&, const std::string&);
|
||||||
|
|||||||
117
src/Version.cpp
Normal file
117
src/Version.cpp
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
|
//
|
||||||
|
// 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 {
|
||||||
|
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 <Context.h>
|
||||||
#include <Filter.h>
|
#include <Filter.h>
|
||||||
#include <Lexer.h>
|
#include <Lexer.h>
|
||||||
|
#include <Version.h>
|
||||||
#include <ViewTask.h>
|
#include <ViewTask.h>
|
||||||
#include <format.h>
|
#include <format.h>
|
||||||
#include <shared.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
|
// 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::random_device device;
|
||||||
std::mt19937 random_generator(device());
|
std::mt19937 random_generator(device());
|
||||||
std::uniform_int_distribution<std::mt19937::result_type> twentyfive_percent(1, 4);
|
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.
|
// 1 in 10 chance to display the message.
|
||||||
if (twentyfive_percent(random_generator) == 4)
|
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"))
|
if (Context::getContext ().verbose ("footnote"))
|
||||||
Context::getContext ().footnote (NEWS_NOTICE);
|
Context::getContext ().footnote (notice.str());
|
||||||
else if (Context::getContext ().verbose ("header"))
|
else if (Context::getContext ().verbose ("header"))
|
||||||
Context::getContext ().header (NEWS_NOTICE);
|
Context::getContext ().header (notice.str());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,14 @@
|
|||||||
#include <util.h>
|
#include <util.h>
|
||||||
#include <main.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 ()
|
CmdNews::CmdNews ()
|
||||||
{
|
{
|
||||||
@@ -91,6 +99,7 @@ void wait_for_enter ()
|
|||||||
// Holds information about single improvement / bug.
|
// Holds information about single improvement / bug.
|
||||||
//
|
//
|
||||||
NewsItem::NewsItem (
|
NewsItem::NewsItem (
|
||||||
|
Version version,
|
||||||
bool major,
|
bool major,
|
||||||
const std::string& title,
|
const std::string& title,
|
||||||
const std::string& bg_title,
|
const std::string& bg_title,
|
||||||
@@ -100,6 +109,7 @@ NewsItem::NewsItem (
|
|||||||
const std::string& reasoning,
|
const std::string& reasoning,
|
||||||
const std::string& actions
|
const std::string& actions
|
||||||
) {
|
) {
|
||||||
|
_version = version;
|
||||||
_major = major;
|
_major = major;
|
||||||
_title = title;
|
_title = title;
|
||||||
_bg_title = bg_title;
|
_bg_title = bg_title;
|
||||||
@@ -127,7 +137,7 @@ void NewsItem::render () {
|
|||||||
|
|
||||||
// TODO: For some reason, bold cannot be blended in 256-color terminals
|
// TODO: For some reason, bold cannot be blended in 256-color terminals
|
||||||
// Apply this workaround of colorizing twice.
|
// 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 (_background.size ()) {
|
||||||
if (_bg_title.empty ())
|
if (_bg_title.empty ())
|
||||||
_bg_title = "Background";
|
_bg_title = "Background";
|
||||||
@@ -138,7 +148,7 @@ void NewsItem::render () {
|
|||||||
|
|
||||||
wait_for_enter ();
|
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 ())
|
if (_punchline.size ())
|
||||||
std::cout << footnote.colorize (format ("{1}\n", _punchline));
|
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.
|
// Generate the highlights for the 2.6.0 version.
|
||||||
//
|
//
|
||||||
@@ -174,7 +191,8 @@ void NewsItem::render () {
|
|||||||
// - The .by attribute modifier
|
// - The .by attribute modifier
|
||||||
// - Exporting a report
|
// - Exporting a report
|
||||||
// - Multi-day holidays
|
// - 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)
|
// - 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'.";
|
" Read more about how to use contexts in CONTEXT section of 'man task'.";
|
||||||
|
|
||||||
NewsItem writeable_context (
|
NewsItem writeable_context (
|
||||||
|
version,
|
||||||
true,
|
true,
|
||||||
"'Writeable' context",
|
"'Writeable' context",
|
||||||
"Background - what is context?",
|
"Background - what is context?",
|
||||||
@@ -277,6 +296,7 @@ void CmdNews::version2_6_0 (std::vector<NewsItem>& items) {
|
|||||||
// - 64-bit datetime support (major)
|
// - 64-bit datetime support (major)
|
||||||
|
|
||||||
NewsItem uint64_support (
|
NewsItem uint64_support (
|
||||||
|
version,
|
||||||
false,
|
false,
|
||||||
"Support for 64-bit timestamps and numeric values",
|
"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
|
// - Waiting is a virtual status
|
||||||
|
|
||||||
NewsItem waiting_status (
|
NewsItem waiting_status (
|
||||||
|
version,
|
||||||
true,
|
true,
|
||||||
"Deprecation of the status:waiting",
|
"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
|
// - Support for environment variables in the taskrc
|
||||||
|
|
||||||
NewsItem env_vars (
|
NewsItem env_vars (
|
||||||
|
version,
|
||||||
true,
|
true,
|
||||||
"Environment variables in the taskrc",
|
"Environment variables in the taskrc",
|
||||||
"",
|
"",
|
||||||
@@ -333,6 +355,7 @@ void CmdNews::version2_6_0 (std::vector<NewsItem>& items) {
|
|||||||
// - Reports outside of context
|
// - Reports outside of context
|
||||||
|
|
||||||
NewsItem contextless_reports (
|
NewsItem contextless_reports (
|
||||||
|
version,
|
||||||
true,
|
true,
|
||||||
"Context-less reports",
|
"Context-less reports",
|
||||||
"",
|
"",
|
||||||
@@ -354,6 +377,7 @@ void CmdNews::version2_6_0 (std::vector<NewsItem>& items) {
|
|||||||
// - Exporting a particular report
|
// - Exporting a particular report
|
||||||
|
|
||||||
NewsItem exportable_reports (
|
NewsItem exportable_reports (
|
||||||
|
version,
|
||||||
false,
|
false,
|
||||||
"Exporting a particular report",
|
"Exporting a particular report",
|
||||||
"",
|
"",
|
||||||
@@ -377,6 +401,7 @@ void CmdNews::version2_6_0 (std::vector<NewsItem>& items) {
|
|||||||
// - Multi-day holidays
|
// - Multi-day holidays
|
||||||
|
|
||||||
NewsItem multi_holidays (
|
NewsItem multi_holidays (
|
||||||
|
version,
|
||||||
false,
|
false,
|
||||||
"Multi-day holidays",
|
"Multi-day holidays",
|
||||||
"",
|
"",
|
||||||
@@ -399,6 +424,7 @@ void CmdNews::version2_6_0 (std::vector<NewsItem>& items) {
|
|||||||
// - Unicode 12
|
// - Unicode 12
|
||||||
|
|
||||||
NewsItem unicode_12 (
|
NewsItem unicode_12 (
|
||||||
|
version,
|
||||||
false,
|
false,
|
||||||
"Extended Unicode support (Unicode 12)",
|
"Extended Unicode support (Unicode 12)",
|
||||||
"",
|
"",
|
||||||
@@ -417,6 +443,7 @@ void CmdNews::version2_6_0 (std::vector<NewsItem>& items) {
|
|||||||
// - The .by attribute modifier
|
// - The .by attribute modifier
|
||||||
|
|
||||||
NewsItem by_modifier (
|
NewsItem by_modifier (
|
||||||
|
version,
|
||||||
false,
|
false,
|
||||||
"The .by attribute modifier",
|
"The .by attribute modifier",
|
||||||
"",
|
"",
|
||||||
@@ -435,6 +462,7 @@ void CmdNews::version2_6_0 (std::vector<NewsItem>& items) {
|
|||||||
// - Context-specific configuration overrides
|
// - Context-specific configuration overrides
|
||||||
|
|
||||||
NewsItem context_config (
|
NewsItem context_config (
|
||||||
|
version,
|
||||||
false,
|
false,
|
||||||
"Context-specific configuration overrides",
|
"Context-specific configuration overrides",
|
||||||
"",
|
"",
|
||||||
@@ -459,6 +487,7 @@ void CmdNews::version2_6_0 (std::vector<NewsItem>& items) {
|
|||||||
// - XDG config home support
|
// - XDG config home support
|
||||||
|
|
||||||
NewsItem xdg_support (
|
NewsItem xdg_support (
|
||||||
|
version,
|
||||||
true,
|
true,
|
||||||
"Support for XDG Base Directory Specification",
|
"Support for XDG Base Directory Specification",
|
||||||
"",
|
"",
|
||||||
@@ -487,6 +516,7 @@ void CmdNews::version2_6_0 (std::vector<NewsItem>& items) {
|
|||||||
// - Update holiday data
|
// - Update holiday data
|
||||||
|
|
||||||
NewsItem holidata_2022 (
|
NewsItem holidata_2022 (
|
||||||
|
version,
|
||||||
false,
|
false,
|
||||||
"Updated holiday data for 2022",
|
"Updated holiday data for 2022",
|
||||||
"",
|
"",
|
||||||
@@ -500,6 +530,28 @@ void CmdNews::version2_6_0 (std::vector<NewsItem>& items) {
|
|||||||
items.push_back(holidata_2022);
|
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)
|
int CmdNews::execute (std::string& output)
|
||||||
{
|
{
|
||||||
@@ -509,11 +561,13 @@ int CmdNews::execute (std::string& output)
|
|||||||
// Supress compiler warning about unused argument
|
// Supress compiler warning about unused argument
|
||||||
output = "";
|
output = "";
|
||||||
|
|
||||||
// TODO: 2.6.0 is the only version with explicit release notes, but in the
|
std::vector<NewsItem> items = NewsItem::all();
|
||||||
// future we need to only execute yet unread release notes
|
Version news_version(Context::getContext ().config.get ("news.version"));
|
||||||
std::vector<NewsItem> items;
|
Version current_version = Version::Current();
|
||||||
std::string version = "2.6.0";
|
|
||||||
version2_6_0 (items);
|
// 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 full_summary = false;
|
||||||
bool major_items = true;
|
bool major_items = true;
|
||||||
@@ -538,6 +592,12 @@ int CmdNews::execute (std::string& output)
|
|||||||
|
|
||||||
signal (SIGINT, signal_handler);
|
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
|
// Remove non-major items if displaying a non-full (abbreviated) summary
|
||||||
int total_highlights = items.size ();
|
int total_highlights = items.size ();
|
||||||
if (! full_summary)
|
if (! full_summary)
|
||||||
@@ -546,23 +606,25 @@ int CmdNews::execute (std::string& output)
|
|||||||
items.end ()
|
items.end ()
|
||||||
);
|
);
|
||||||
|
|
||||||
// Print release notes
|
|
||||||
Color bold = Color ("bold");
|
Color bold = Color ("bold");
|
||||||
std::cout << bold.colorize (format (
|
if (items.empty ()) {
|
||||||
"\n"
|
std::cout << bold.colorize ("You are up to date!\n");
|
||||||
"==========================================\n"
|
} else {
|
||||||
"Taskwarrior {1} {2} Release highlights\n"
|
// Print release notes
|
||||||
"==========================================\n",
|
std::cout << bold.colorize (format (
|
||||||
version,
|
"\n"
|
||||||
(full_summary ? "All" : (major_items ? "Major" : "Minor"))
|
"================================================\n"
|
||||||
));
|
"Taskwarrior {1} through {2} Release Highlights\n"
|
||||||
|
"================================================\n",
|
||||||
|
news_version,
|
||||||
|
current_version));
|
||||||
|
|
||||||
for (unsigned short i=0; i < items.size (); i++) {
|
for (unsigned short i=0; i < items.size (); i++) {
|
||||||
std::cout << format ("\n({1}/{2}) ", i+1, items.size ());
|
std::cout << format ("\n({1}/{2}) ", i+1, items.size ());
|
||||||
items[i].render ();
|
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 ();
|
wait_for_enter ();
|
||||||
|
|
||||||
// Display outro
|
// Display outro
|
||||||
@@ -588,9 +650,9 @@ int CmdNews::execute (std::string& output)
|
|||||||
std::cout << outro.str ();
|
std::cout << outro.str ();
|
||||||
|
|
||||||
// Set a mark in the config to remember which version's release notes were displayed
|
// Set a mark in the config to remember which version's release notes were displayed
|
||||||
if (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
|
// Revert back to default signal handling after displaying the outro
|
||||||
signal (SIGINT, SIG_DFL);
|
signal (SIGINT, SIG_DFL);
|
||||||
@@ -627,14 +689,15 @@ int CmdNews::execute (std::string& output)
|
|||||||
else
|
else
|
||||||
wait_for_enter (); // Do not display the outro and footnote at once
|
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 (
|
Context::getContext ().footnote (format (
|
||||||
"Only major highlights were displayed ({1} out of {2} total).\n"
|
"Only major highlights were displayed ({1} out of {2} total).\n"
|
||||||
"If you're interested in more release highlights, run 'task news {3} minor'.",
|
"If you're interested in more release highlights, run 'task news {3} minor'.",
|
||||||
items.size (),
|
items.size (),
|
||||||
total_highlights,
|
total_highlights,
|
||||||
version
|
current_version
|
||||||
));
|
));
|
||||||
|
}
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,9 +31,11 @@
|
|||||||
#include <Command.h>
|
#include <Command.h>
|
||||||
#include <CmdConfig.h>
|
#include <CmdConfig.h>
|
||||||
#include <CmdContext.h>
|
#include <CmdContext.h>
|
||||||
|
#include <Version.h>
|
||||||
|
|
||||||
class NewsItem {
|
class NewsItem {
|
||||||
public:
|
public:
|
||||||
|
Version _version;
|
||||||
bool _major = false;
|
bool _major = false;
|
||||||
std::string _title;
|
std::string _title;
|
||||||
std::string _bg_title;
|
std::string _bg_title;
|
||||||
@@ -42,7 +44,16 @@ public:
|
|||||||
std::string _update;
|
std::string _update;
|
||||||
std::string _reasoning;
|
std::string _reasoning;
|
||||||
std::string _actions;
|
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 (
|
NewsItem (
|
||||||
|
Version,
|
||||||
bool,
|
bool,
|
||||||
const std::string&,
|
const std::string&,
|
||||||
const std::string& = "",
|
const std::string& = "",
|
||||||
@@ -52,7 +63,6 @@ public:
|
|||||||
const std::string& = "",
|
const std::string& = "",
|
||||||
const std::string& = ""
|
const std::string& = ""
|
||||||
);
|
);
|
||||||
void render ();
|
|
||||||
};
|
};
|
||||||
|
|
||||||
class CmdNews : public Command
|
class CmdNews : public Command
|
||||||
@@ -60,7 +70,6 @@ class CmdNews : public Command
|
|||||||
public:
|
public:
|
||||||
CmdNews ();
|
CmdNews ();
|
||||||
int execute (std::string&);
|
int execute (std::string&);
|
||||||
void version2_6_0 (std::vector<NewsItem>&);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ int CmdSync::execute (std::string& output)
|
|||||||
os << "Sync server at " << origin;
|
os << "Sync server at " << origin;
|
||||||
server_ident = os.str();
|
server_ident = os.str();
|
||||||
} else {
|
} else {
|
||||||
throw std::string ("No sync.* settings are configured.");
|
throw std::string ("No sync.* settings are configured. See task-sync(5).");
|
||||||
}
|
}
|
||||||
|
|
||||||
std::stringstream out;
|
std::stringstream out;
|
||||||
|
|||||||
@@ -207,6 +207,11 @@ void feedback_unblocked (const Task& task)
|
|||||||
///////////////////////////////////////////////////////////////////////////////
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
void feedback_backlog ()
|
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"))
|
if (Context::getContext ().verbose ("sync"))
|
||||||
{
|
{
|
||||||
int count = Context::getContext ().tdb2.num_local_changes ();
|
int count = Context::getContext ().tdb2.num_local_changes ();
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
cmake_minimum_required (VERSION 3.22)
|
cmake_minimum_required (VERSION 3.22)
|
||||||
|
|
||||||
FetchContent_Declare (
|
add_subdirectory(${CMAKE_SOURCE_DIR}/src/tc/corrosion)
|
||||||
Corrosion
|
|
||||||
GIT_REPOSITORY https://github.com/corrosion-rs/corrosion.git
|
|
||||||
GIT_TAG v0.4.7
|
|
||||||
)
|
|
||||||
FetchContent_MakeAvailable(Corrosion)
|
|
||||||
|
|
||||||
# Import taskchampion-lib as a CMake library.
|
# Import taskchampion-lib as a CMake library.
|
||||||
corrosion_import_crate(
|
corrosion_import_crate(
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ tc::Server::new_local (const std::string &server_dir)
|
|||||||
TCString error;
|
TCString error;
|
||||||
auto tcserver = tc_server_new_local (tc_server_dir, &error);
|
auto tcserver = tc_server_new_local (tc_server_dir, &error);
|
||||||
if (!tcserver) {
|
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));
|
server_dir, tc_string_content (&error));
|
||||||
tc_string_free (&error);
|
tc_string_free (&error);
|
||||||
throw errmsg;
|
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) {
|
if (tc_uuid_from_str(tc_client_id, &tc_client_uuid) != TC_RESULT_OK) {
|
||||||
tc_string_free(&tc_origin);
|
tc_string_free(&tc_origin);
|
||||||
tc_string_free(&tc_encryption_secret);
|
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;
|
TCString error;
|
||||||
auto tcserver = tc_server_new_sync (tc_origin, tc_client_uuid, tc_encryption_secret, &error);
|
auto tcserver = tc_server_new_sync (tc_origin, tc_client_uuid, tc_encryption_secret, &error);
|
||||||
if (!tcserver) {
|
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));
|
origin, tc_string_content (&error));
|
||||||
tc_string_free (&error);
|
tc_string_free (&error);
|
||||||
throw errmsg;
|
throw errmsg;
|
||||||
@@ -88,7 +88,7 @@ tc::Server::new_gcp (const std::string &bucket, const std::string &credential_pa
|
|||||||
TCString error;
|
TCString error;
|
||||||
auto tcserver = tc_server_new_gcp (tc_bucket, tc_credential_path, tc_encryption_secret, &error);
|
auto tcserver = tc_server_new_gcp (tc_bucket, tc_credential_path, tc_encryption_secret, &error);
|
||||||
if (!tcserver) {
|
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));
|
bucket, tc_string_content (&error));
|
||||||
tc_string_free (&error);
|
tc_string_free (&error);
|
||||||
throw errmsg;
|
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.
|
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).
|
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.
|
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.
|
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.
|
The book documentation explains lots of the concepts in the design of TaskChampion.
|
||||||
It is linked from the README.
|
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.
|
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` is the core functionality of the application, implemented as a library
|
||||||
* `taskchampion-cli` implements the command-line interface (in `cli/`)
|
* `taskchampion-lib` implements a C API for `taskchampion`, used by Taskwarrior
|
||||||
* `taskchampion-sync-server` implements the synchronization server (in `sync-server/`)
|
* `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
|
## Making a Pull Request
|
||||||
|
|
||||||
|
|||||||
@@ -12,12 +12,11 @@ Until that is complete, the information here may be out-of-date.
|
|||||||
|
|
||||||
## Structure
|
## Structure
|
||||||
|
|
||||||
There are five crates here:
|
There are four crates here:
|
||||||
|
|
||||||
* [taskchampion](./taskchampion) - the core of the tool
|
* [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
|
* [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
|
* [xtask](./xtask) (private) - implementation of the `cargo xtask codegen` command
|
||||||
|
|
||||||
## Code Generation
|
## Code Generation
|
||||||
|
|||||||
@@ -7,18 +7,13 @@ publish = false
|
|||||||
build = "build.rs"
|
build = "build.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
taskchampion = { path = "../taskchampion", features = ["server-sync"] }
|
taskchampion = { path = "../taskchampion" }
|
||||||
taskchampion-lib = { path = "../lib" }
|
taskchampion-lib = { path = "../lib" }
|
||||||
taskchampion-sync-server = { path = "../sync-server" }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
actix-web.workspace = true
|
|
||||||
actix-rt.workspace = true
|
|
||||||
tempfile.workspace = true
|
tempfile.workspace = true
|
||||||
pretty_assertions.workspace = true
|
pretty_assertions.workspace = true
|
||||||
log.workspace = true
|
|
||||||
env_logger.workspace = true
|
|
||||||
lazy_static.workspace = true
|
lazy_static.workspace = true
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ static void test_replica_sync_local(void) {
|
|||||||
static void test_replica_remote_server(void) {
|
static void test_replica_remote_server(void) {
|
||||||
TCString err;
|
TCString err;
|
||||||
TCServer *server = tc_server_new_sync(
|
TCServer *server = tc_server_new_sync(
|
||||||
tc_string_borrow("tc.freecinc.com"),
|
tc_string_borrow("http://tc.freecinc.com"),
|
||||||
tc_uuid_new_v4(),
|
tc_uuid_new_v4(),
|
||||||
tc_string_borrow("\xf0\x28\x8c\x28"), // NOTE: not utf-8
|
tc_string_borrow("\xf0\x28\x8c\x28"), // NOTE: not utf-8
|
||||||
&err);
|
&err);
|
||||||
|
|||||||
@@ -1,97 +1,66 @@
|
|||||||
use actix_web::{App, HttpServer};
|
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use taskchampion::{Replica, ServerConfig, Status, StorageConfig, Uuid};
|
use taskchampion::{Replica, ServerConfig, Status, StorageConfig};
|
||||||
use taskchampion_sync_server::{storage::InMemoryStorage, Server};
|
use tempfile::TempDir;
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[test]
|
||||||
async fn cross_sync() -> anyhow::Result<()> {
|
fn cross_sync() -> anyhow::Result<()> {
|
||||||
async fn server() -> anyhow::Result<u16> {
|
// set up two replicas, and demonstrate replication between them
|
||||||
let _ = env_logger::builder()
|
let mut rep1 = Replica::new(StorageConfig::InMemory.into_storage()?);
|
||||||
.is_test(true)
|
let mut rep2 = Replica::new(StorageConfig::InMemory.into_storage()?);
|
||||||
.filter_level(log::LevelFilter::Trace)
|
|
||||||
.try_init();
|
|
||||||
|
|
||||||
let server = Server::new(Default::default(), Box::new(InMemoryStorage::new()));
|
let tmp_dir = TempDir::new().expect("TempDir failed");
|
||||||
let httpserver = HttpServer::new(move || App::new().configure(|sc| server.config(sc)))
|
let server_config = ServerConfig::Local {
|
||||||
.bind("0.0.0.0:0")?;
|
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
|
// add some tasks on rep1
|
||||||
let port = httpserver.addrs()[0].port();
|
let t1 = rep1.new_task(Status::Pending, "test 1".into())?;
|
||||||
actix_rt::spawn(httpserver.run());
|
let t2 = rep1.new_task(Status::Pending, "test 2".into())?;
|
||||||
Ok(port)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn client(port: u16) -> anyhow::Result<()> {
|
// modify t1
|
||||||
// set up two replicas, and demonstrate replication between them
|
let mut t1 = t1.into_mut(&mut rep1);
|
||||||
let mut rep1 = Replica::new(StorageConfig::InMemory.into_storage()?);
|
t1.start()?;
|
||||||
let mut rep2 = Replica::new(StorageConfig::InMemory.into_storage()?);
|
let t1 = t1.into_immut();
|
||||||
|
|
||||||
let client_id = Uuid::new_v4();
|
rep1.sync(&mut server, false)?;
|
||||||
let encryption_secret = b"abc123".to_vec();
|
rep2.sync(&mut server, false)?;
|
||||||
let make_server = || {
|
|
||||||
ServerConfig::Remote {
|
|
||||||
origin: format!("http://127.0.0.1:{}", port),
|
|
||||||
client_id,
|
|
||||||
encryption_secret: encryption_secret.clone(),
|
|
||||||
}
|
|
||||||
.into_server()
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut serv1 = make_server()?;
|
// those tasks should exist on rep2 now
|
||||||
let mut serv2 = make_server()?;
|
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
|
assert_eq!(t12.get_description(), "test 1");
|
||||||
let t1 = rep1.new_task(Status::Pending, "test 1".into())?;
|
assert_eq!(t12.is_active(), true);
|
||||||
let t2 = rep1.new_task(Status::Pending, "test 2".into())?;
|
assert_eq!(t22.get_description(), "test 2");
|
||||||
|
assert_eq!(t22.is_active(), false);
|
||||||
|
|
||||||
// modify t1
|
// make non-conflicting changes on the two replicas
|
||||||
let mut t1 = t1.into_mut(&mut rep1);
|
let mut t2 = t2.into_mut(&mut rep1);
|
||||||
t1.start()?;
|
t2.set_status(Status::Completed)?;
|
||||||
let t1 = t1.into_immut();
|
let t2 = t2.into_immut();
|
||||||
|
|
||||||
rep1.sync(&mut serv1, false)?;
|
let mut t12 = t12.into_mut(&mut rep2);
|
||||||
rep2.sync(&mut serv2, false)?;
|
t12.set_status(Status::Completed)?;
|
||||||
|
|
||||||
// those tasks should exist on rep2 now
|
// sync those changes back and forth
|
||||||
let t12 = rep2
|
rep1.sync(&mut server, false)?; // rep1 -> server
|
||||||
.get_task(t1.get_uuid())?
|
rep2.sync(&mut server, false)?; // server -> rep2, rep2 -> server
|
||||||
.expect("expected task 1 on rep2");
|
rep1.sync(&mut server, false)?; // server -> rep1
|
||||||
let t22 = rep2
|
|
||||||
.get_task(t2.get_uuid())?
|
|
||||||
.expect("expected task 2 on rep2");
|
|
||||||
|
|
||||||
assert_eq!(t12.get_description(), "test 1");
|
let t1 = rep1
|
||||||
assert_eq!(t12.is_active(), true);
|
.get_task(t1.get_uuid())?
|
||||||
assert_eq!(t22.get_description(), "test 2");
|
.expect("expected task 1 on rep1");
|
||||||
assert_eq!(t22.is_active(), false);
|
assert_eq!(t1.get_status(), Status::Completed);
|
||||||
|
|
||||||
// make non-conflicting changes on the two replicas
|
let t22 = rep2
|
||||||
let mut t2 = t2.into_mut(&mut rep1);
|
.get_task(t2.get_uuid())?
|
||||||
t2.set_status(Status::Completed)?;
|
.expect("expected task 2 on rep2");
|
||||||
let t2 = t2.into_immut();
|
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(())
|
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"]
|
default = ["server-sync", "server-gcp"]
|
||||||
|
|
||||||
# Support for sync to a server
|
# Support for sync to a server
|
||||||
server-sync = ["encryption", "dep:ureq"]
|
server-sync = ["encryption", "dep:ureq", "dep:url"]
|
||||||
# Support for sync to GCP
|
# Support for sync to GCP
|
||||||
server-gcp = ["cloud", "encryption", "dep:google-cloud-storage", "dep:tokio"]
|
server-gcp = ["cloud", "encryption", "dep:google-cloud-storage", "dep:tokio"]
|
||||||
# (private) Support for sync protocol encryption
|
# (private) Support for sync protocol encryption
|
||||||
@@ -43,10 +43,12 @@ byteorder.workspace = true
|
|||||||
ring.workspace = true
|
ring.workspace = true
|
||||||
google-cloud-storage.workspace = true
|
google-cloud-storage.workspace = true
|
||||||
tokio.workspace = true
|
tokio.workspace = true
|
||||||
|
url.workspace = true
|
||||||
|
|
||||||
google-cloud-storage.optional = true
|
google-cloud-storage.optional = true
|
||||||
tokio.optional = true
|
tokio.optional = true
|
||||||
ureq.optional = true
|
ureq.optional = true
|
||||||
|
url.optional = true
|
||||||
ring.optional = true
|
ring.optional = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|||||||
@@ -332,7 +332,7 @@ mod test {
|
|||||||
let version_id = Uuid::parse_str("b0517957-f912-4d49-8330-f612e73030c4").unwrap();
|
let version_id = Uuid::parse_str("b0517957-f912-4d49-8330-f612e73030c4").unwrap();
|
||||||
let encryption_secret = b"b4a4e6b7b811eda1dc1a2693ded".to_vec();
|
let encryption_secret = b"b4a4e6b7b811eda1dc1a2693ded".to_vec();
|
||||||
let client_id = Uuid::parse_str("0666d464-418a-4a08-ad53-6f15c78270cd").unwrap();
|
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)
|
(version_id, salt, encryption_secret)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use crate::server::{
|
|||||||
VersionId,
|
VersionId,
|
||||||
};
|
};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
use url::Url;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use super::encryption::{Cryptor, Sealed, Secret, Unsealed};
|
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
|
/// identify this client to the server. Multiple replicas synchronizing the same task history
|
||||||
/// should use the same client_id.
|
/// should use the same client_id.
|
||||||
pub fn new(origin: String, client_id: Uuid, encryption_secret: Vec<u8>) -> Result<SyncServer> {
|
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 {
|
Ok(SyncServer {
|
||||||
origin,
|
origin: origin.to_string(),
|
||||||
client_id,
|
client_id,
|
||||||
cryptor: Cryptor::new(client_id, &Secret(encryption_secret.to_vec()))?,
|
cryptor: Cryptor::new(client_id, &Secret(encryption_secret.to_vec()))?,
|
||||||
agent: ureq::AgentBuilder::new()
|
agent: ureq::AgentBuilder::new()
|
||||||
@@ -85,10 +94,7 @@ impl Server for SyncServer {
|
|||||||
parent_version_id: VersionId,
|
parent_version_id: VersionId,
|
||||||
history_segment: HistorySegment,
|
history_segment: HistorySegment,
|
||||||
) -> Result<(AddVersionResult, SnapshotUrgency)> {
|
) -> Result<(AddVersionResult, SnapshotUrgency)> {
|
||||||
let url = format!(
|
let url = format!("{}v1/client/add-version/{}", self.origin, parent_version_id);
|
||||||
"{}/v1/client/add-version/{}",
|
|
||||||
self.origin, parent_version_id
|
|
||||||
);
|
|
||||||
let unsealed = Unsealed {
|
let unsealed = Unsealed {
|
||||||
version_id: parent_version_id,
|
version_id: parent_version_id,
|
||||||
payload: history_segment,
|
payload: history_segment,
|
||||||
@@ -121,7 +127,7 @@ impl Server for SyncServer {
|
|||||||
|
|
||||||
fn get_child_version(&mut self, parent_version_id: VersionId) -> Result<GetVersionResult> {
|
fn get_child_version(&mut self, parent_version_id: VersionId) -> Result<GetVersionResult> {
|
||||||
let url = format!(
|
let url = format!(
|
||||||
"{}/v1/client/get-child-version/{}",
|
"{}v1/client/get-child-version/{}",
|
||||||
self.origin, parent_version_id
|
self.origin, parent_version_id
|
||||||
);
|
);
|
||||||
match self
|
match self
|
||||||
@@ -150,7 +156,7 @@ impl Server for SyncServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn add_snapshot(&mut self, version_id: VersionId, snapshot: Snapshot) -> Result<()> {
|
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 {
|
let unsealed = Unsealed {
|
||||||
version_id,
|
version_id,
|
||||||
payload: snapshot,
|
payload: snapshot,
|
||||||
@@ -166,7 +172,7 @@ impl Server for SyncServer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn get_snapshot(&mut self) -> Result<Option<(VersionId, Snapshot)>> {
|
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
|
match self
|
||||||
.agent
|
.agent
|
||||||
.get(&url)
|
.get(&url)
|
||||||
|
|||||||
@@ -56,6 +56,22 @@ class TestHooksOnExit(TestCase):
|
|||||||
logs = hook.get_logs()
|
logs = hook.get_logs()
|
||||||
self.assertEqual(logs["output"]["msgs"][0], "FEEDBACK")
|
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):
|
def test_onexit_builtin_bad(self):
|
||||||
"""on-exit-bad - a well-behaved, failing, on-exit hook."""
|
"""on-exit-bad - a well-behaved, failing, on-exit hook."""
|
||||||
hookname = 'on-exit-bad'
|
hookname = 'on-exit-bad'
|
||||||
|
|||||||
@@ -52,8 +52,8 @@ if __name__ == "__main__":
|
|||||||
unexpected = defaultdict(int)
|
unexpected = defaultdict(int)
|
||||||
passed = defaultdict(int)
|
passed = defaultdict(int)
|
||||||
|
|
||||||
file = re.compile("^# (?:./)?(\S+\.t)(?:\.exe)?$")
|
file = re.compile(r"^# (?:./)?(\S+\.t)(?:\.exe)?$")
|
||||||
timestamp = re.compile("^# (\d+(?:\.\d+)?) ==>.*$")
|
timestamp = re.compile(r"^# (\d+(?:\.\d+)?) ==>.*$")
|
||||||
|
|
||||||
expected_fail = re.compile(r"^not ok.*?#\s*TODO", re.I)
|
expected_fail = re.compile(r"^not ok.*?#\s*TODO", re.I)
|
||||||
unexpected_pass = re.compile(r"^not ok .*?#\s*FIXED", re.I)
|
unexpected_pass = re.compile(r"^not ok .*?#\s*FIXED", re.I)
|
||||||
|
|||||||
Reference in New Issue
Block a user