Compare commits

..

29 Commits

Author SHA1 Message Date
Dustin J. Mitchell
0627447a6a Update for 3.0.1 (#3382)
update for 3.0.1
2024-04-20 23:18:57 +00:00
Dustin J. Mitchell
f054a4061e Remove taskchampion-sync-server (#3380)
This crate has been moved to
https://github.com/GothenburgBitFactory/taskchampion-sync-server.

The integration-tests repo used the sync server to test integration
between taskchampion and the sync-server. We should do that again, but
after taskchampion moves to its own repo (#3209). In the interim, the
cross-sync integration test can simply test syncing between local
servers, but the snapshot test is no longer useful as the local server
does not support snapshots.
2024-04-20 12:44:06 +00:00
dependabot[bot]
304b84e4da Bump rustls from 0.21.7 to 0.21.11 (#3379)
Bumps [rustls](https://github.com/rustls/rustls) from 0.21.7 to 0.21.11.
- [Release notes](https://github.com/rustls/rustls/releases)
- [Changelog](https://github.com/rustls/rustls/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rustls/rustls/compare/v/0.21.7...v/0.21.11)

---
updated-dependencies:
- dependency-name: rustls
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-19 19:32:49 -04:00
Dustin J. Mitchell
f86a069faf Fix paths generated from origin (#3372)
These mistakenly doubled the initial `/` character. This was broken in #3361.
2024-04-16 22:05:45 -04:00
ryneeverett
0944c73716 Recommend LSP's in development docs (#3370)
* Recommend LSP's in development docs

Per conversation in #3338.

There are already a lot of documented compile options so I think we're
better off suggesting that everybody create a compile_commands.json
whether or not they're using an LSP because it doesn't cost much.

While I was at it it seemed reasonable to mention rust LSP too. Now that
rls is deprecated I'm not sure there is any competitor to rust-analyzer
worth mentioning.

* Export compile commands by default.

Thanks to @felixschurk for the idea and telling me how to do it.

It took me a minute to figure out that this places the
compile_commands.json in the build directory rather than the root of the
project. But clangd still finds it there and that's a better place for
it anyway.
2024-04-16 08:19:58 -04:00
Dustin J. Mitchell
10cec507cb Check that sync.server.origin is a URL (#3361) 2024-04-16 02:11:55 +00:00
Dustin J. Mitchell
4d9bb20bdd Update task news to support 3.0.0 (#3342)
* Introduce Version, use it to check current version in custom reports
* Support multiple versions in 'task news'
2024-04-15 22:04:16 -04:00
dependabot[bot]
d243d000eb Bump env_logger from 0.10.0 to 0.10.2 (#3336)
Bumps [env_logger](https://github.com/rust-cli/env_logger) from 0.10.0 to 0.10.2.
- [Release notes](https://github.com/rust-cli/env_logger/releases)
- [Changelog](https://github.com/rust-cli/env_logger/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-cli/env_logger/compare/v0.10.0...v0.10.2)

---
updated-dependencies:
- dependency-name: env_logger
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-15 21:49:56 -04:00
Dustin J. Mitchell
9040a7eb79 Throw std::strings on sync server errors (#3362) 2024-04-15 21:49:17 -04:00
Dustin J. Mitchell
0a491f36ad Store all modified tasks for use by on-exit hook (#3352)
The on-exit hook gets all modified tasks as input, but this was omitted
in the previous release. This adds a test for the desired behavior, and
updates TDB2 to correctly store the required information.
2024-04-15 21:14:25 -04:00
dependabot[bot]
7578768d9b Bump peaceiris/actions-gh-pages from 3 to 4 (#3367)
Bumps [peaceiris/actions-gh-pages](https://github.com/peaceiris/actions-gh-pages) from 3 to 4.
- [Release notes](https://github.com/peaceiris/actions-gh-pages/releases)
- [Changelog](https://github.com/peaceiris/actions-gh-pages/blob/main/CHANGELOG.md)
- [Commits](https://github.com/peaceiris/actions-gh-pages/compare/v3...v4)

---
updated-dependencies:
- dependency-name: peaceiris/actions-gh-pages
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-15 21:12:20 -04:00
dependabot[bot]
cb0d21f96e Bump peaceiris/actions-mdbook from 1 to 2 (#3366)
Bumps [peaceiris/actions-mdbook](https://github.com/peaceiris/actions-mdbook) from 1 to 2.
- [Release notes](https://github.com/peaceiris/actions-mdbook/releases)
- [Changelog](https://github.com/peaceiris/actions-mdbook/blob/main/CHANGELOG.md)
- [Commits](https://github.com/peaceiris/actions-mdbook/compare/v1...v2)

---
updated-dependencies:
- dependency-name: peaceiris/actions-mdbook
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-15 21:11:17 -04:00
dependabot[bot]
3b414cd9bb Bump sigstore/cosign-installer from 3.4.0 to 3.5.0 (#3365)
Bumps [sigstore/cosign-installer](https://github.com/sigstore/cosign-installer) from 3.4.0 to 3.5.0.
- [Release notes](https://github.com/sigstore/cosign-installer/releases)
- [Commits](https://github.com/sigstore/cosign-installer/compare/v3.4.0...v3.5.0)

---
updated-dependencies:
- dependency-name: sigstore/cosign-installer
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-15 21:10:52 -04:00
Dustin J. Mitchell
c90eb8f71d remove reference to NEWS (#3357) 2024-04-12 22:46:09 -04:00
Dustin J. Mitchell
7c465ceb8f Remove NEWS, as it is redundant to 'task news' (#3354) 2024-04-07 21:58:27 -04:00
Dustin J. Mitchell
a6b721853b Remove 'sync' from default verbose flags (#3319)
Do not give sync feedback when not doing remote sync
2024-04-08 01:02:07 +00:00
ryneeverett
fd306712b8 Install corrosion as submodule. (#3348)
This will enable nixpkgs -- and any other distribution that builds in a
network sandbox and/or wants to use their own corrosion package rather
than building another one -- to do so without patching taskwarrior.

Since we're already using submodules for libshared I don't think this
should make the build process any more complicated for anyone else.

See
https://github.com/NixOS/nixpkgs/issues/300679#issuecomment-2041252688
for context.
2024-04-07 12:10:54 -04:00
Felix Schurk
b5aa7c6ae2 change order of hook invocation and setting task id (#3339)
this prevents that the task id is always returned as zero after a hook
is run on it
closes #3312
2024-04-05 19:45:55 -04:00
Dustin J. Mitchell
933885f21c Merge pull request #3341 from ryneeverett/sync-config-man-warning
sync: Point to manpage if unconfigured
2024-04-05 19:01:50 -04:00
ryneeverett
587f8910ef sync: Point to manpage if unconfigured
See #3340.
2024-04-05 10:35:15 -04:00
dependabot[bot]
de42c8ba34 Bump strum_macros from 0.25.0 to 0.25.3 (#3335)
Bumps [strum_macros](https://github.com/Peternator7/strum) from 0.25.0 to 0.25.3.
- [Release notes](https://github.com/Peternator7/strum/releases)
- [Changelog](https://github.com/Peternator7/strum/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Peternator7/strum/commits)

---
updated-dependencies:
- dependency-name: strum_macros
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-04 18:16:31 +00:00
Dustin J. Mitchell
8a0a98d3ef Issue a warning if .data files remain (#3321)
This will help users realize that they have updated to an incompatible
version and must export and import their tasks.
2024-03-31 18:55:30 -04:00
Felix Schurk
5a56cff88b prefix regex strings to be treated as raw strings (#3322)
Source for documentation
https://docs.python.org/3/reference/lexical_analysis.html#string-and-bytes-literals.
Closes #3316.
2024-03-31 09:08:56 -04:00
Dustin J. Mitchell
c91cef43b0 add note about ENABLE_SYNC to changelog (#3317) 2024-03-31 00:59:21 +00:00
Felix Schurk
8c2c629a4d use CMake project settings (#3315)
This adds a description as well as the homepage to the CMake settings.
Further it would also now use the numbering cheme as supposed to in
CMake, this way other people could now pin a specific version if
taskwarrior is included in another project.
Documentation from CMake is https://cmake.org/cmake/help/latest/command/project.html
2024-03-30 10:33:49 -04:00
dependabot[bot]
dfaf3dfcb2 Bump tokio from 1.36.0 to 1.37.0 (#3310)
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.36.0 to 1.37.0.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.36.0...tokio-1.37.0)

---
updated-dependencies:
- dependency-name: tokio
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-29 09:25:44 -04:00
Dustin J. Mitchell
06fdfc5af3 Edit Cargo.toml during release (#3302) 2024-03-27 22:56:13 +00:00
dependabot[bot]
19f2c0d7b4 Bump uuid from 1.7.0 to 1.8.0 (#3290)
* Bump uuid from 1.7.0 to 1.8.0

Bumps [uuid](https://github.com/uuid-rs/uuid) from 1.7.0 to 1.8.0.
- [Release notes](https://github.com/uuid-rs/uuid/releases)
- [Commits](https://github.com/uuid-rs/uuid/compare/1.7.0...1.8.0)

---
updated-dependencies:
- dependency-name: uuid
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* use as_bytes

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Dustin J. Mitchell <djmitche@google.com>
2024-03-24 22:19:27 +00:00
Dustin J. Mitchell
8bb08bf01d Add releasing docs (#3292)
add releasing docs
2024-03-24 21:32:38 +00:00
49 changed files with 514 additions and 3835 deletions

View File

@@ -52,7 +52,7 @@ jobs:
- uses: actions/checkout@v4
- name: Setup mdBook
uses: peaceiris/actions-mdbook@v1
uses: peaceiris/actions-mdbook@v2
with:
# if this changes, change it in .github/workflows/publish-docs.yml as well
mdbook-version: '0.4.10'

View File

@@ -29,7 +29,7 @@ jobs:
submodules: "recursive"
- name: Install cosign
uses: sigstore/cosign-installer@v3.4.0
uses: sigstore/cosign-installer@v3.5.0
- name: Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@v3.1.0

View File

@@ -16,7 +16,7 @@ jobs:
- uses: actions/checkout@v4
- name: Setup mdBook
uses: peaceiris/actions-mdbook@v1
uses: peaceiris/actions-mdbook@v2
with:
# if this changes, change it in .github/workflows/checks.yml as well
mdbook-version: '0.4.10'
@@ -24,7 +24,7 @@ jobs:
- run: mdbook build taskchampion/docs
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./taskchampion/docs/book

3
.gitmodules vendored
View File

@@ -1,3 +1,6 @@
[submodule "src/libshared"]
path = src/libshared
url = https://github.com/GothenburgBitFactory/libshared.git
[submodule "src/tc/corrosion"]
path = src/tc/corrosion
url = https://github.com/corrosion-rs/corrosion.git

View File

@@ -1,4 +1,12 @@
cmake_minimum_required (VERSION 3.22)
set (CMAKE_EXPORT_COMPILE_COMMANDS ON)
project (task
VERSION 3.0.1
DESCRIPTION "Taskwarrior - a command-line TODO list manager"
HOMEPAGE_URL https://taskwarrior.org/)
set (CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake")
include (FetchContent)
@@ -7,11 +15,8 @@ include (CheckStructHasMember)
set (HAVE_CMAKE true)
project (task)
include (CXXSniffer)
set (PROJECT_VERSION "3.0.0")
OPTION (ENABLE_WASM "Enable 'wasm' support" OFF)
if (ENABLE_WASM)
@@ -147,7 +152,7 @@ if (EXISTS performance)
add_subdirectory (performance EXCLUDE_FROM_ALL)
endif (EXISTS performance)
set (doc_FILES NEWS ChangeLog README.md INSTALL AUTHORS COPYING LICENSE)
set (doc_FILES ChangeLog README.md INSTALL AUTHORS COPYING LICENSE)
foreach (doc_FILE ${doc_FILES})
install (FILES ${doc_FILE} DESTINATION ${TASK_DOCDIR})
endforeach (doc_FILE)

563
Cargo.lock generated
View File

@@ -2,188 +2,6 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "actix-codec"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57a7559404a7f3573127aab53c08ce37a6c6a315c374a31070f3c91cd1b4a7fe"
dependencies = [
"bitflags 1.3.2",
"bytes",
"futures-core",
"futures-sink",
"log",
"memchr",
"pin-project-lite",
"tokio",
"tokio-util",
]
[[package]]
name = "actix-http"
version = "3.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2079246596c18b4a33e274ae10c0e50613f4d32a4198e09c7b93771013fed74"
dependencies = [
"actix-codec",
"actix-rt",
"actix-service",
"actix-utils",
"ahash 0.8.3",
"base64 0.21.0",
"bitflags 1.3.2",
"brotli",
"bytes",
"bytestring",
"derive_more",
"encoding_rs",
"flate2",
"futures-core",
"h2",
"http",
"httparse",
"httpdate",
"itoa",
"language-tags",
"local-channel",
"mime",
"percent-encoding",
"pin-project-lite",
"rand",
"sha1",
"smallvec",
"tokio",
"tokio-util",
"tracing",
"zstd",
]
[[package]]
name = "actix-macros"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "465a6172cf69b960917811022d8f29bc0b7fa1398bc4f78b3c466673db1213b6"
dependencies = [
"quote",
"syn 1.0.104",
]
[[package]]
name = "actix-router"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d66ff4d247d2b160861fa2866457e85706833527840e4133f8f49aa423a38799"
dependencies = [
"bytestring",
"http",
"regex",
"serde",
"tracing",
]
[[package]]
name = "actix-rt"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15265b6b8e2347670eb363c47fc8c75208b4a4994b27192f345fcbe707804f3e"
dependencies = [
"actix-macros",
"futures-core",
"tokio",
]
[[package]]
name = "actix-server"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e8613a75dd50cc45f473cee3c34d59ed677c0f7b44480ce3b8247d7dc519327"
dependencies = [
"actix-rt",
"actix-service",
"actix-utils",
"futures-core",
"futures-util",
"mio",
"num_cpus",
"socket2 0.4.9",
"tokio",
"tracing",
]
[[package]]
name = "actix-service"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a"
dependencies = [
"futures-core",
"paste",
"pin-project-lite",
]
[[package]]
name = "actix-utils"
version = "3.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88a1dcdff1466e3c2488e1cb5c36a71822750ad43839937f85d2f4d9f8b705d8"
dependencies = [
"local-waker",
"pin-project-lite",
]
[[package]]
name = "actix-web"
version = "4.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd3cb42f9566ab176e1ef0b8b3a896529062b4efc6be0123046095914c4c1c96"
dependencies = [
"actix-codec",
"actix-http",
"actix-macros",
"actix-router",
"actix-rt",
"actix-server",
"actix-service",
"actix-utils",
"actix-web-codegen",
"ahash 0.7.6",
"bytes",
"bytestring",
"cfg-if",
"cookie",
"derive_more",
"encoding_rs",
"futures-core",
"futures-util",
"http",
"itoa",
"language-tags",
"log",
"mime",
"once_cell",
"pin-project-lite",
"regex",
"serde",
"serde_json",
"serde_urlencoded",
"smallvec",
"socket2 0.4.9",
"time 0.3.20",
"url",
]
[[package]]
name = "actix-web-codegen"
version = "4.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2262160a7ae29e3415554a3f1fc04c764b1540c116aa524683208078b7a75bc9"
dependencies = [
"actix-router",
"proc-macro2",
"quote",
"syn 1.0.104",
]
[[package]]
name = "addr2line"
version = "0.21.0"
@@ -210,18 +28,6 @@ dependencies = [
"version_check",
]
[[package]]
name = "ahash"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f"
dependencies = [
"cfg-if",
"getrandom",
"once_cell",
"version_check",
]
[[package]]
name = "aho-corasick"
version = "1.1.2"
@@ -231,21 +37,6 @@ dependencies = [
"memchr",
]
[[package]]
name = "alloc-no-stdlib"
version = "2.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35ef4730490ad1c4eae5c4325b2a95f521d023e5c885853ff7aca0a6a1631db3"
[[package]]
name = "alloc-stdlib"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "697ed7edc0f1711de49ce108c541623a0af97c6c60b2f6e2b65229847ac843c2"
dependencies = [
"alloc-no-stdlib",
]
[[package]]
name = "android_system_properties"
version = "0.1.5"
@@ -255,55 +46,6 @@ dependencies = [
"libc",
]
[[package]]
name = "anstream"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is-terminal",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41ed9a86bf92ae6580e0a31281f65a1b1d867c0cc68d5346e2ae128dddfa6a7d"
[[package]]
name = "anstyle-parse"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e765fd216e48e067936442276d1d57399e37bce53c264d6fefbe298080cb57ee"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b"
dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "anstyle-wincon"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188"
dependencies = [
"anstyle",
"windows-sys 0.48.0",
]
[[package]]
name = "anyhow"
version = "1.0.66"
@@ -418,27 +160,6 @@ dependencies = [
"generic-array",
]
[[package]]
name = "brotli"
version = "3.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
"brotli-decompressor",
]
[[package]]
name = "brotli-decompressor"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ad2d4653bf5ca36ae797b1f4bb4dbddb60ce49ca4aed8a2ce4829f60425b80"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
]
[[package]]
name = "bumpalo"
version = "3.12.0"
@@ -457,23 +178,11 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
[[package]]
name = "bytestring"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90706ba19e97b90786e19dc0d5e2abd80008d99d4c0c5d1ad0b5e72cec7c494d"
dependencies = [
"bytes",
]
[[package]]
name = "cc"
version = "1.0.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
dependencies = [
"jobserver",
]
[[package]]
name = "cfg-if"
@@ -497,63 +206,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "clap"
version = "4.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93aae7a4192245f70fe75dd9157fc7b4a5bf53e88d30bd4396f7d8f9284d5acc"
dependencies = [
"clap_builder",
]
[[package]]
name = "clap_builder"
version = "4.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f423e341edefb78c9caba2d9c7f7687d0e72e89df3ce3394554754393ac3990"
dependencies = [
"anstream",
"anstyle",
"bitflags 1.3.2",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_lex"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b"
[[package]]
name = "colorchoice"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
[[package]]
name = "const-oid"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "convert_case"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
[[package]]
name = "cookie"
version = "0.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb"
dependencies = [
"percent-encoding",
"time 0.3.20",
"version_check",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.3"
@@ -599,19 +257,6 @@ dependencies = [
"zeroize",
]
[[package]]
name = "derive_more"
version = "0.99.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321"
dependencies = [
"convert_case",
"proc-macro2",
"quote",
"rustc_version",
"syn 1.0.104",
]
[[package]]
name = "diff"
version = "0.1.12"
@@ -643,19 +288,6 @@ dependencies = [
"cfg-if",
]
[[package]]
name = "env_logger"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85cdab6a89accf66733ad5a1693a4dcced6aeff64602b634530dd73c1f3ee9f0"
dependencies = [
"humantime",
"is-terminal",
"log",
"regex",
"termcolor",
]
[[package]]
name = "errno"
version = "0.3.1"
@@ -972,7 +604,7 @@ version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "607c8a29735385251a339424dd462993c0fed8fa09d378f259377df08c126022"
dependencies = [
"ahash 0.7.6",
"ahash",
]
[[package]]
@@ -1054,12 +686,6 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
[[package]]
name = "humantime"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "hyper"
version = "0.14.20"
@@ -1145,17 +771,12 @@ dependencies = [
name = "integration-tests"
version = "0.4.1"
dependencies = [
"actix-rt",
"actix-web",
"anyhow",
"cc",
"env_logger",
"lazy_static",
"log",
"pretty_assertions",
"taskchampion",
"taskchampion-lib",
"taskchampion-sync-server",
"tempfile",
]
@@ -1176,18 +797,6 @@ version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3"
[[package]]
name = "is-terminal"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adcf93614601c8129ddf72e2d5633df827ba6551541c6d8c59520a371475be1f"
dependencies = [
"hermit-abi 0.3.1",
"io-lifetimes",
"rustix",
"windows-sys 0.48.0",
]
[[package]]
name = "itertools"
version = "0.10.5"
@@ -1203,15 +812,6 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d"
[[package]]
name = "jobserver"
version = "0.1.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2"
dependencies = [
"libc",
]
[[package]]
name = "js-sys"
version = "0.3.59"
@@ -1235,12 +835,6 @@ dependencies = [
"simple_asn1",
]
[[package]]
name = "language-tags"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388"
[[package]]
name = "lazy_static"
version = "1.4.0"
@@ -1296,24 +890,6 @@ version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519"
[[package]]
name = "local-channel"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f303ec0e94c6c54447f84f3b0ef7af769858a9c4ef56ef2a986d3dcd4c3fc9c"
dependencies = [
"futures-core",
"futures-sink",
"futures-util",
"local-waker",
]
[[package]]
name = "local-waker"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e34f76eb3611940e0e7d53a9aaa4e6a3151f69541a282fd0dad5571420c53ff1"
[[package]]
name = "lock_api"
version = "0.4.7"
@@ -1380,7 +956,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
dependencies = [
"libc",
"log",
"wasi",
"windows-sys 0.48.0",
]
@@ -1464,12 +1039,6 @@ dependencies = [
"windows-sys 0.45.0",
]
[[package]]
name = "paste"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79"
[[package]]
name = "pem"
version = "1.1.1"
@@ -1810,12 +1379,12 @@ dependencies = [
[[package]]
name = "rustls"
version = "0.21.7"
version = "0.21.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd8d6c9f025a446bc4d18ad9632e69aec8f287aa84499ee335599fabd20c3fd8"
checksum = "7fecbfb7b1444f477b345853b1fce097a2c6fb637b2bfb87e6bc5db0f043fae4"
dependencies = [
"log",
"ring 0.16.20",
"ring 0.17.3",
"rustls-webpki",
"sct",
]
@@ -1831,12 +1400,12 @@ dependencies = [
[[package]]
name = "rustls-webpki"
version = "0.101.6"
version = "0.101.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c7d5dece342910d9ba34d259310cae3e0154b873b35408b787b59bce53d34fe"
checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765"
dependencies = [
"ring 0.16.20",
"untrusted 0.7.1",
"ring 0.17.3",
"untrusted 0.9.0",
]
[[package]]
@@ -1928,17 +1497,6 @@ dependencies = [
"serde",
]
[[package]]
name = "sha1"
version = "0.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "sha2"
version = "0.10.6"
@@ -1950,15 +1508,6 @@ dependencies = [
"digest",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0"
dependencies = [
"libc",
]
[[package]]
name = "simple_asn1"
version = "0.6.2"
@@ -2025,12 +1574,6 @@ dependencies = [
"der",
]
[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "strum"
version = "0.25.0"
@@ -2039,9 +1582,9 @@ checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125"
[[package]]
name = "strum_macros"
version = "0.25.0"
version = "0.25.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe9f3bd7d2e45dcc5e265fbb88d6513e4747d8ef9444cf01a533119bce28a157"
checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0"
dependencies = [
"heck",
"proc-macro2",
@@ -2095,6 +1638,7 @@ dependencies = [
"thiserror",
"tokio",
"ureq",
"url",
"uuid",
]
@@ -2109,27 +1653,6 @@ dependencies = [
"taskchampion",
]
[[package]]
name = "taskchampion-sync-server"
version = "0.4.1"
dependencies = [
"actix-rt",
"actix-web",
"anyhow",
"chrono",
"clap",
"env_logger",
"futures",
"log",
"pretty_assertions",
"rusqlite",
"serde",
"serde_json",
"tempfile",
"thiserror",
"uuid",
]
[[package]]
name = "tempfile"
version = "3.6.0"
@@ -2144,15 +1667,6 @@ dependencies = [
"windows-sys 0.48.0",
]
[[package]]
name = "termcolor"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
dependencies = [
"winapi-util",
]
[[package]]
name = "thiserror"
version = "1.0.37"
@@ -2227,9 +1741,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "tokio"
version = "1.36.0"
version = "1.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931"
checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787"
dependencies = [
"backtrace",
"bytes",
@@ -2238,7 +1752,6 @@ dependencies = [
"num_cpus",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2 0.5.5",
"tokio-macros",
"windows-sys 0.48.0",
@@ -2292,7 +1805,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5d0ecdcb44a79f0fe9844f0c4f33a342cbcbb5117de8001e6ba0dc2351327d09"
dependencies = [
"cfg-if",
"log",
"pin-project-lite",
"tracing-attributes",
"tracing-core",
@@ -2411,17 +1923,11 @@ version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
[[package]]
name = "utf8parse"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
name = "uuid"
version = "1.7.0"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f00cc9702ca12d3c81455259621e676d0f7251cec66a21e98fe2e9a37db93b2a"
checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0"
dependencies = [
"getrandom",
"serde",
@@ -2593,15 +2099,6 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
dependencies = [
"winapi",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
@@ -2835,33 +2332,3 @@ name = "zeroize"
version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d"
[[package]]
name = "zstd"
version = "0.12.3+zstd.1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76eea132fb024e0e13fd9c2f5d5d595d8a967aa72382ac2f9d39fcc95afd0806"
dependencies = [
"zstd-safe",
]
[[package]]
name = "zstd-safe"
version = "6.0.5+zstd.1.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d56d9e60b4b1758206c238a10165fbcae3ca37b01744e394c463463f6529d23b"
dependencies = [
"libc",
"zstd-sys",
]
[[package]]
name = "zstd-sys"
version = "2.0.8+zstd.1.5.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5556e6ee25d32df2586c098bbfa278803692a20d0ab9565e049480d52707ec8c"
dependencies = [
"cc",
"libc",
"pkg-config",
]

View File

@@ -2,7 +2,6 @@
members = [
"taskchampion/taskchampion",
"taskchampion/sync-server",
"taskchampion/lib",
"taskchampion/integration-tests",
"taskchampion/xtask",
@@ -16,17 +15,12 @@ exclude = [ "src/tc/rust" ]
# All Rust dependencies are defined here, and then referenced by the
# Cargo.toml's in the members with `foo.workspace = true`.
[workspace.dependencies]
actix-rt = "2"
actix-web = "^4.3.1"
anyhow = "1.0"
byteorder = "1.5"
cc = "1.0.73"
chrono = { version = "^0.4.22", features = ["serde"] }
clap = { version = "^4.3.0", features = ["string"] }
env_logger = "^0.10.0"
ffizz-header = "0.5"
flate2 = "1"
futures = "^0.3.25"
google-cloud-storage = { version = "0.15.0", default-features = false, features = ["rustls-tls", "auth"] }
lazy_static = "1"
libc = "0.2.136"
@@ -44,4 +38,5 @@ tempfile = "3"
tokio = { version = "1", features = ["rt-multi-thread"] }
thiserror = "1.0"
ureq = { version = "^2.9.0", features = ["tls"] }
uuid = { version = "^1.7.0", features = ["serde", "v4"] }
uuid = { version = "^1.8.0", features = ["serde", "v4"] }
url = { version = "2" }

View File

@@ -1,4 +1,15 @@
------ current release ---------------------------
3.0.1 -
- Fix an error in creation of the 3.0.0 tarball which caused builds to fail (#3302)
- Improvements to `task news`, including notes for the 3.0.0 release
- Minor improvements to documentation and error handling
- Fix incorrect task ID of 0 when using hooks (#3339)
- Issue a warning if .data files remain (#3321)
------ old releases ------------------------------
3.0.0 -
- [BREAKING CHANGE] the sync functionality has been rewritten entirely, and
@@ -19,7 +30,9 @@
- `taskd.server`
- `taskd.trust`
The Taskwarrior build no longer requires GnuTLS.
The Taskwarrior build no longer requires GnuTLS. The build option
`ENABLE_SYNC=OFF` is also no longer supported; sync support is always built
in.
Deep thanks to the following for contributions to this work:
@@ -229,8 +242,6 @@
Thanks to bharatvaj for contributing.
- TW #2581 Config entry with a trailing comment cannot be modified
------ old releases ------------------------------
2.5.3 (2021-01-05) - 2f47226f91f0b02f7617912175274d9eed85924f
- #2375 task hangs then dies when certain tasks are present in a report

152
NEWS
View File

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

View File

@@ -5,3 +5,4 @@
* [Coding Style](coding_style.md)
* [Branching Model](branching.md)
* [Rust and C++](rust-and-c++.md)
* [Releasing Taskwarrior](releasing.md)

View File

@@ -12,9 +12,13 @@ See the [TaskChampion CONTRIBUTING guide](../../../taskchampion/CONTRIBUTING.md)
* CMake 3.0 or later
* gcc 7.0 or later, clang 6.0 or later, or a compiler with full C++17 support
* libuuid (if not on macOS)
* python 3 (optional, for running the test suite)
* Rust 1.64.0 or higher (hint: use https://rustup.rs/ instead of using your system's package manager)
## Install Optional Dependencies:
* python 3 (for running the test suite)
* clangd or ccls (for C++ integration in many editors)
* rust-analyzer (for Rust integration in many editors)
## Obtain and Build Code:
The following documentation works with CMake 3.14 and later.
Here are the minimal steps to get started, using an out of source build directory and calling the underlying build tool over the CMake interface.
@@ -24,7 +28,7 @@ See the general CMake man pages or the [cmake-documentation](https://cmake.org/c
```sh
git clone https://github.com/GothenburgBitFactory/taskwarrior
cd taskwarrior
cmake -S . -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo
cmake -S . -B build -DCMAKE_BUILD_TYPE=RelWithDebInfo
cmake --build build
```
Other possible build types can be `Release` and `Debug`.

View 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

View File

@@ -73,6 +73,8 @@ Configure Taskwarrior with these details:
$ task config sync.server.origin <origin>
$ task config sync.server.client_id <client_id>
Note that the origin must include the scheme, such as 'http://' or 'https://'.
.SS Google Cloud Platform
To synchronize your tasks to GCP, use the GCP Console to create a new project,
@@ -142,16 +144,8 @@ Users are identified by a client ID, and users with different client IDs are
entirely independent. Task data is encrypted by Taskwarrior, and the sync
server never sees un-encrypted data.
To start the server, run it in your preferred HTTP hosting environment, using
`--port` to set the TCP port on which it should listen. It is recommended to
use TLS to protect communications with the server, but this is not required.
The server stores its data in a database, the path to which is given by the
`--data-dir` argument, defaulting to "/var/lib/taskchampion-sync-server".
For example:
$ taskchampion-sync-server --port 8443 --data-dir /storage/taskdata
The server is developed in
https://github.com/GothenburgBitFactory/taskchampion-sync-server.
.SS Adding a New User

View File

@@ -18,6 +18,7 @@ add_library (task STATIC CLI2.cpp CLI2.h
TDB2.cpp TDB2.h
Task.cpp Task.h
Variant.cpp Variant.h
Version.cpp Version.h
ViewTask.cpp ViewTask.h
dependency.cpp
feedback.cpp

View File

@@ -29,6 +29,7 @@
#include <iostream>
#include <sstream>
#include <algorithm>
#include <vector>
#include <list>
#include <unordered_set>
#include <stdlib.h>
@@ -57,6 +58,14 @@ TDB2::TDB2 ()
////////////////////////////////////////////////////////////////////////////////
void TDB2::open_replica (const std::string& location, bool create_if_missing)
{
File pending_data = File (location + "/pending.data");
if (pending_data.exists()) {
Color warning = Color (Context::getContext ().config.get ("color.warning"));
std::cerr << warning.colorize (
format ("Found existing '.data' files in {1}", location)) << "\n";
std::cerr << " Taskwarrior's storage format changed in 3.0, requiring a manual migration.\n";
std::cerr << " See https://github.com/GothenburgBitFactory/taskwarrior/releases.\n";
}
replica = tc::Replica(location, create_if_missing);
}
@@ -70,6 +79,8 @@ void TDB2::add (Task& task)
task.validate (true);
std::string uuid = task.get ("uuid");
changes[uuid] = task;
auto innertask = replica.import_task_with_uuid (uuid);
{
@@ -79,7 +90,7 @@ void TDB2::add (Task& task)
for (auto& attr : task.all ()) {
// TaskChampion does not store uuid or id in the taskmap
if (attr == "uuid" || attr == "id") {
continue;
continue;
}
// Use `set_status` for the task status, to get expected behavior
@@ -110,12 +121,13 @@ void TDB2::add (Task& task)
// update the cached working set with the new information
_working_set = std::make_optional (std::move (ws));
if (id.has_value ()) {
task.id = id.value();
}
// run hooks for this new task
Context::getContext ().hooks.onAdd (task);
if (id.has_value ()) {
task.id = id.value();
}
}
////////////////////////////////////////////////////////////////////////////////
@@ -141,6 +153,8 @@ void TDB2::modify (Task& task)
task.validate (false);
auto uuid = task.get ("uuid");
changes[uuid] = task;
// invoke the hook and allow it to modify the task before updating
Task original;
get (uuid, original);
@@ -200,9 +214,10 @@ const tc::WorkingSet &TDB2::working_set ()
////////////////////////////////////////////////////////////////////////////////
void TDB2::get_changes (std::vector <Task>& changes)
{
// TODO: changes in an invocation of `task` are not currently tracked, so this
// list is always empty.
std::map<std::string, Task>& changes_map = this->changes;
changes.clear();
std::transform(changes_map.begin(), changes_map.end(), std::back_inserter(changes),
[](const auto& kv) { return kv.second; });
}
////////////////////////////////////////////////////////////////////////////////

View File

@@ -84,6 +84,9 @@ private:
tc::Replica replica;
std::optional<tc::WorkingSet> _working_set;
// UUID -> Task containing all tasks modified in this invocation.
std::map<std::string, Task> changes;
const tc::WorkingSet &working_set ();
static std::string option_string (std::string input);
static void show_diff (const std::string&, const std::string&, const std::string&);

118
src/Version.cpp Normal file
View File

@@ -0,0 +1,118 @@
////////////////////////////////////////////////////////////////////////////////
//
// Copyright 2024, Dustin Mitchell.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//
// https://www.opensource.org/licenses/mit-license.php
//
////////////////////////////////////////////////////////////////////////////////
#include <Version.h>
#include <cmake.h>
#include <iostream>
#include <sstream>
#include <string>
#include <tuple>
#include <vector>
////////////////////////////////////////////////////////////////////////////////
Version::Version(std::string version) {
std::vector<int> parts;
std::string part;
std::istringstream input(version);
while (std::getline(input, part, '.')) {
int value;
// Try converting string to integer
if (std::stringstream(part) >> value && value >= 0) {
parts.push_back(value);
} else {
return;
}
}
if (parts.size() != 3) {
return;
}
major = parts[0];
minor = parts[1];
patch = parts[2];
}
////////////////////////////////////////////////////////////////////////////////
Version Version::Current() { return Version(PACKAGE_VERSION); }
////////////////////////////////////////////////////////////////////////////////
bool Version::is_valid() const { return major >= 0; }
////////////////////////////////////////////////////////////////////////////////
bool Version::operator<(const Version &other) const {
return std::tie(major, minor, patch) <
std::tie(other.major, other.minor, other.patch);
}
////////////////////////////////////////////////////////////////////////////////
bool Version::operator<=(const Version &other) const {
return std::tie(major, minor, patch) <=
std::tie(other.major, other.minor, other.patch);
}
////////////////////////////////////////////////////////////////////////////////
bool Version::operator>(const Version &other) const {
return std::tie(major, minor, patch) >
std::tie(other.major, other.minor, other.patch);
}
////////////////////////////////////////////////////////////////////////////////
bool Version::operator>=(const Version &other) const {
return std::tie(major, minor, patch) >=
std::tie(other.major, other.minor, other.patch);
}
////////////////////////////////////////////////////////////////////////////////
bool Version::operator==(const Version &other) const {
return std::tie(major, minor, patch) ==
std::tie(other.major, other.minor, other.patch);
}
////////////////////////////////////////////////////////////////////////////////
bool Version::operator!=(const Version &other) const {
std::cout << other;
return std::tie(major, minor, patch) !=
std::tie(other.major, other.minor, other.patch);
}
////////////////////////////////////////////////////////////////////////////////
Version::operator std::string() const {
std::ostringstream output;
if (is_valid()) {
output << major << '.' << minor << '.' << patch;
} else {
output << "(invalid version)";
}
return output.str();
}
////////////////////////////////////////////////////////////////////////////////
std::ostream &operator<<(std::ostream &os, const Version &version) {
os << std::string(version);
return os;
}

72
src/Version.h Normal file
View 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
////////////////////////////////////////////////////////////////////////////////

View File

@@ -35,6 +35,7 @@
#include <Context.h>
#include <Filter.h>
#include <Lexer.h>
#include <Version.h>
#include <ViewTask.h>
#include <format.h>
#include <shared.h>
@@ -250,24 +251,24 @@ int CmdCustom::execute (std::string& output)
}
// Inform user about the new release highlights if not presented yet
if (Context::getContext ().config.get ("news.version") != "2.6.0")
Version news_version(Context::getContext ().config.get ("news.version"));
Version current_version = Version::Current();
if (news_version != current_version)
{
std::random_device device;
std::mt19937 random_generator(device());
std::uniform_int_distribution<std::mt19937::result_type> twentyfive_percent(1, 4);
std::string NEWS_NOTICE = (
"Recently upgraded to 2.6.0. "
"Please run 'task news' to read highlights about the new release."
);
// 1 in 10 chance to display the message.
if (twentyfive_percent(random_generator) == 4)
{
std::ostringstream notice;
notice << "Recently upgraded to " << current_version << ". "
"Please run 'task news' to read highlights about the new release.";
if (Context::getContext ().verbose ("footnote"))
Context::getContext ().footnote (NEWS_NOTICE);
Context::getContext ().footnote (notice.str());
else if (Context::getContext ().verbose ("header"))
Context::getContext ().header (NEWS_NOTICE);
Context::getContext ().header (notice.str());
}
}

View File

@@ -38,6 +38,14 @@
#include <util.h>
#include <main.h>
/* Adding a new version:
*
* - Add a new `versionX_Y_Z` method to `NewsItem`, and add news items for the new
* release.
* - Call the new method in `NewsItem.all()`. Calls should be in version order.
* - Test with `task news`.
*/
////////////////////////////////////////////////////////////////////////////////
CmdNews::CmdNews ()
{
@@ -91,6 +99,7 @@ void wait_for_enter ()
// Holds information about single improvement / bug.
//
NewsItem::NewsItem (
Version version,
bool major,
const std::string& title,
const std::string& bg_title,
@@ -100,6 +109,7 @@ NewsItem::NewsItem (
const std::string& reasoning,
const std::string& actions
) {
_version = version;
_major = major;
_title = title;
_bg_title = bg_title;
@@ -127,7 +137,7 @@ void NewsItem::render () {
// TODO: For some reason, bold cannot be blended in 256-color terminals
// Apply this workaround of colorizing twice.
std::cout << bold.colorize (header.colorize (format ("{1}\n", _title)));
std::cout << bold.colorize (header.colorize (format ("{1} ({2})\n", _title, _version)));
if (_background.size ()) {
if (_bg_title.empty ())
_bg_title = "Background";
@@ -138,7 +148,7 @@ void NewsItem::render () {
wait_for_enter ();
std::cout << " " << underline.colorize ("What changed in 2.6.0?\n");
std::cout << " " << underline.colorize (format ("What changed in {1}?\n", _version));
if (_punchline.size ())
std::cout << footnote.colorize (format ("{1}\n", _punchline));
@@ -160,6 +170,13 @@ void NewsItem::render () {
}
}
std::vector<NewsItem> NewsItem::all () {
std::vector<NewsItem> items;
version2_6_0(items);
version3_0_0(items);
return items;
}
////////////////////////////////////////////////////////////////////////////////
// Generate the highlights for the 2.6.0 version.
//
@@ -174,7 +191,8 @@ void NewsItem::render () {
// - The .by attribute modifier
// - Exporting a report
// - Multi-day holidays
void CmdNews::version2_6_0 (std::vector<NewsItem>& items) {
void NewsItem::version2_6_0 (std::vector<NewsItem>& items) {
Version version("2.6.0");
/////////////////////////////////////////////////////////////////////////////
// - Writeable context (major)
@@ -234,6 +252,7 @@ void CmdNews::version2_6_0 (std::vector<NewsItem>& items) {
" Read more about how to use contexts in CONTEXT section of 'man task'.";
NewsItem writeable_context (
version,
true,
"'Writeable' context",
"Background - what is context?",
@@ -277,6 +296,7 @@ void CmdNews::version2_6_0 (std::vector<NewsItem>& items) {
// - 64-bit datetime support (major)
NewsItem uint64_support (
version,
false,
"Support for 64-bit timestamps and numeric values",
"",
@@ -294,6 +314,7 @@ void CmdNews::version2_6_0 (std::vector<NewsItem>& items) {
// - Waiting is a virtual status
NewsItem waiting_status (
version,
true,
"Deprecation of the status:waiting",
"",
@@ -315,6 +336,7 @@ void CmdNews::version2_6_0 (std::vector<NewsItem>& items) {
// - Support for environment variables in the taskrc
NewsItem env_vars (
version,
true,
"Environment variables in the taskrc",
"",
@@ -333,6 +355,7 @@ void CmdNews::version2_6_0 (std::vector<NewsItem>& items) {
// - Reports outside of context
NewsItem contextless_reports (
version,
true,
"Context-less reports",
"",
@@ -354,6 +377,7 @@ void CmdNews::version2_6_0 (std::vector<NewsItem>& items) {
// - Exporting a particular report
NewsItem exportable_reports (
version,
false,
"Exporting a particular report",
"",
@@ -377,6 +401,7 @@ void CmdNews::version2_6_0 (std::vector<NewsItem>& items) {
// - Multi-day holidays
NewsItem multi_holidays (
version,
false,
"Multi-day holidays",
"",
@@ -399,6 +424,7 @@ void CmdNews::version2_6_0 (std::vector<NewsItem>& items) {
// - Unicode 12
NewsItem unicode_12 (
version,
false,
"Extended Unicode support (Unicode 12)",
"",
@@ -417,6 +443,7 @@ void CmdNews::version2_6_0 (std::vector<NewsItem>& items) {
// - The .by attribute modifier
NewsItem by_modifier (
version,
false,
"The .by attribute modifier",
"",
@@ -435,6 +462,7 @@ void CmdNews::version2_6_0 (std::vector<NewsItem>& items) {
// - Context-specific configuration overrides
NewsItem context_config (
version,
false,
"Context-specific configuration overrides",
"",
@@ -459,6 +487,7 @@ void CmdNews::version2_6_0 (std::vector<NewsItem>& items) {
// - XDG config home support
NewsItem xdg_support (
version,
true,
"Support for XDG Base Directory Specification",
"",
@@ -487,6 +516,7 @@ void CmdNews::version2_6_0 (std::vector<NewsItem>& items) {
// - Update holiday data
NewsItem holidata_2022 (
version,
false,
"Updated holiday data for 2022",
"",
@@ -500,6 +530,28 @@ void CmdNews::version2_6_0 (std::vector<NewsItem>& items) {
items.push_back(holidata_2022);
}
void NewsItem::version3_0_0 (std::vector<NewsItem>& items) {
Version version("3.0.0");
NewsItem sync {
version,
/*major=*/true,
/*title=*/"New data model and sync backend",
/*bg_title=*/"",
/*background=*/"",
/*punchline=*/
"The sync functionality for Taskwarrior has been rewritten entirely, and no longer\n"
"supports taskserver/taskd. The most robust solution is a cloud-storage backend,\n"
"although a less-mature taskchampion-sync-server is also available. See `task-sync(5)`\n"
"For details. As part of this change, the on-disk storage format has also changed.\n",
/*update=*/
"This is a breaking upgrade: you must export your task database from 2.x and re-import\n"
"it into 3.x. Hooks run during task import, so if you have any hooks defined,\n"
"temporarily disable them for this operation.\n\n"
"See https://taskwarrior.org/docs/upgrade-3/ for information on upgrading to Taskwarrior 3.0.",
};
items.push_back(sync);
}
////////////////////////////////////////////////////////////////////////////////
int CmdNews::execute (std::string& output)
{
@@ -509,11 +561,13 @@ int CmdNews::execute (std::string& output)
// Supress compiler warning about unused argument
output = "";
// TODO: 2.6.0 is the only version with explicit release notes, but in the
// future we need to only execute yet unread release notes
std::vector<NewsItem> items;
std::string version = "2.6.0";
version2_6_0 (items);
std::vector<NewsItem> items = NewsItem::all();
Version news_version(Context::getContext ().config.get ("news.version"));
Version current_version = Version::Current();
// 2.6.0 is the earliest version with news support.
if (!news_version.is_valid())
news_version = Version("2.6.0");
bool full_summary = false;
bool major_items = true;
@@ -538,6 +592,12 @@ int CmdNews::execute (std::string& output)
signal (SIGINT, signal_handler);
// Remove items that have already been shown
items.erase (
std::remove_if (items.begin (), items.end (), [&](const NewsItem& n){return n._version <= news_version;}),
items.end ()
);
// Remove non-major items if displaying a non-full (abbreviated) summary
int total_highlights = items.size ();
if (! full_summary)
@@ -546,23 +606,25 @@ int CmdNews::execute (std::string& output)
items.end ()
);
// Print release notes
Color bold = Color ("bold");
std::cout << bold.colorize (format (
"\n"
"==========================================\n"
"Taskwarrior {1} {2} Release highlights\n"
"==========================================\n",
version,
(full_summary ? "All" : (major_items ? "Major" : "Minor"))
));
if (items.empty ()) {
std::cout << bold.colorize ("You are up to date!\n");
} else {
// Print release notes
std::cout << bold.colorize (format (
"\n"
"================================================\n"
"Taskwarrior {1} through {2} Release Highlights\n"
"================================================\n",
news_version,
current_version));
for (unsigned short i=0; i < items.size (); i++) {
std::cout << format ("\n({1}/{2}) ", i+1, items.size ());
items[i].render ();
for (unsigned short i=0; i < items.size (); i++) {
std::cout << format ("\n({1}/{2}) ", i+1, items.size ());
items[i].render ();
}
std::cout << "Thank you for catching up on the new features!\n";
}
std::cout << "Thank you for catching up on the new features!\n";
wait_for_enter ();
// Display outro
@@ -588,9 +650,9 @@ int CmdNews::execute (std::string& output)
std::cout << outro.str ();
// Set a mark in the config to remember which version's release notes were displayed
if (config.get ("news.version") != "2.6.0")
if (full_summary && news_version != current_version)
{
CmdConfig::setConfigVariable ("news.version", "2.6.0", false);
CmdConfig::setConfigVariable ("news.version", std::string(current_version), false);
// Revert back to default signal handling after displaying the outro
signal (SIGINT, SIG_DFL);
@@ -627,14 +689,15 @@ int CmdNews::execute (std::string& output)
else
wait_for_enter (); // Do not display the outro and footnote at once
if (! full_summary && major_items)
if (! items.empty() && ! full_summary && major_items) {
Context::getContext ().footnote (format (
"Only major highlights were displayed ({1} out of {2} total).\n"
"If you're interested in more release highlights, run 'task news {3} minor'.",
items.size (),
total_highlights,
version
current_version
));
}
return 0;
}

View File

@@ -31,9 +31,11 @@
#include <Command.h>
#include <CmdConfig.h>
#include <CmdContext.h>
#include <Version.h>
class NewsItem {
public:
Version _version;
bool _major = false;
std::string _title;
std::string _bg_title;
@@ -42,7 +44,16 @@ public:
std::string _update;
std::string _reasoning;
std::string _actions;
void render ();
static std::vector<NewsItem> all();
static void version2_6_0 (std::vector<NewsItem>&);
static void version3_0_0 (std::vector<NewsItem>&);
private:
NewsItem (
Version,
bool,
const std::string&,
const std::string& = "",
@@ -52,7 +63,6 @@ public:
const std::string& = "",
const std::string& = ""
);
void render ();
};
class CmdNews : public Command
@@ -60,7 +70,6 @@ class CmdNews : public Command
public:
CmdNews ();
int execute (std::string&);
void version2_6_0 (std::vector<NewsItem>&);
};
#endif

View File

@@ -88,7 +88,7 @@ int CmdSync::execute (std::string& output)
os << "Sync server at " << origin;
server_ident = os.str();
} else {
throw std::string ("No sync.* settings are configured.");
throw std::string ("No sync.* settings are configured. See task-sync(5).");
}
std::stringstream out;

View File

@@ -207,6 +207,11 @@ void feedback_unblocked (const Task& task)
///////////////////////////////////////////////////////////////////////////////
void feedback_backlog ()
{
// If non-local sync is not set up, do not provide this feedback.
if (Context::getContext ().config.get ("sync.encryption_secret") == "") {
return;
}
if (Context::getContext ().verbose ("sync"))
{
int count = Context::getContext ().tdb2.num_local_changes ();

View File

@@ -1,11 +1,6 @@
cmake_minimum_required (VERSION 3.22)
FetchContent_Declare (
Corrosion
GIT_REPOSITORY https://github.com/corrosion-rs/corrosion.git
GIT_TAG v0.4.7
)
FetchContent_MakeAvailable(Corrosion)
add_subdirectory(${CMAKE_SOURCE_DIR}/src/tc/corrosion)
# Import taskchampion-lib as a CMake library.
corrosion_import_crate(

View File

@@ -39,7 +39,7 @@ tc::Server::new_local (const std::string &server_dir)
TCString error;
auto tcserver = tc_server_new_local (tc_server_dir, &error);
if (!tcserver) {
auto errmsg = format ("Could not configure local server at {1}: {2}",
std::string errmsg = format ("Could not configure local server at {1}: {2}",
server_dir, tc_string_content (&error));
tc_string_free (&error);
throw errmsg;
@@ -61,13 +61,13 @@ tc::Server::new_sync (const std::string &origin, const std::string &client_id, c
if (tc_uuid_from_str(tc_client_id, &tc_client_uuid) != TC_RESULT_OK) {
tc_string_free(&tc_origin);
tc_string_free(&tc_encryption_secret);
throw "client_id must be a valid UUID";
throw format ("client_id '{1}' is not a valid UUID", client_id);
}
TCString error;
auto tcserver = tc_server_new_sync (tc_origin, tc_client_uuid, tc_encryption_secret, &error);
if (!tcserver) {
auto errmsg = format ("Could not configure connection to server at {1}: {2}",
std::string errmsg = format ("Could not configure connection to server at {1}: {2}",
origin, tc_string_content (&error));
tc_string_free (&error);
throw errmsg;
@@ -88,7 +88,7 @@ tc::Server::new_gcp (const std::string &bucket, const std::string &credential_pa
TCString error;
auto tcserver = tc_server_new_gcp (tc_bucket, tc_credential_path, tc_encryption_secret, &error);
if (!tcserver) {
auto errmsg = format ("Could not configure connection to GCP bucket {1}: {2}",
std::string errmsg = format ("Could not configure connection to GCP bucket {1}: {2}",
bucket, tc_string_content (&error));
tc_string_free (&error);
throw errmsg;

1
src/tc/corrosion Submodule

Submodule src/tc/corrosion added at 8ddd6d56ca

View File

@@ -24,11 +24,8 @@ Other ideas;
TaskChampion is a typical Rust application.
To work on TaskChampion, you'll need to [install the latest version of Rust](https://www.rust-lang.org/tools/install).
Once you've done that, run `cargo build` at the top level of this repository to build the binaries.
This will build `task` and `taskchampion-sync-server` executables in the `./target/debug` directory.
You can build optimized versions of these binaries with `cargo build --release`, but the performance difference in the resulting binaries is not noticeable, and the build process will take a long time, so this is not recommended.
## Running Test
## Running Tests
It's always a good idea to make sure tests run before you start hacking on a project.
Run `cargo test` from the top-level of this repository to run the tests.
@@ -39,13 +36,13 @@ Aside from that, start reading the docs and the source to learn more!
The book documentation explains lots of the concepts in the design of TaskChampion.
It is linked from the README.
There are three crates in this repository.
There are three important crates in this repository.
You may be able to limit the scope of what you need to understand to just one crate.
* `taskchampion` is the core functionality of the application, implemented as a library
* `taskchampion-cli` implements the command-line interface (in `cli/`)
* `taskchampion-sync-server` implements the synchronization server (in `sync-server/`)
* `taskchampion-lib` implements a C API for `taskchampion`, used by Taskwarrior
* `integration-tests` contains some tests for integrations between multiple crates.
You can generate the documentation for the `taskchampion` crate with `cargo doc --release --open -p taskchampion`.
You can generate the documentation for the `taskchampion` crate with `cargo doc --release --open -p taskchampion`.
## Making a Pull Request

View File

@@ -12,12 +12,11 @@ Until that is complete, the information here may be out-of-date.
## Structure
There are five crates here:
There are four crates here:
* [taskchampion](./taskchampion) - the core of the tool
* [taskchampion-sync-server](./sync-server) - the server against which `task sync` operates
* [taskchampion-lib](./lib) - glue code to use _taskchampion_ from C
* [integration-tests](./integration-tests) (private) - integration tests covering _taskchampion-cli_, _taskchampion-sync-server_, and _taskchampion-lib_.
* [integration-tests](./integration-tests) (private) - integration tests covering _taskchampion_ and _taskchampion-lib_.
* [xtask](./xtask) (private) - implementation of the `cargo xtask codegen` command
## Code Generation

View File

@@ -7,18 +7,13 @@ publish = false
build = "build.rs"
[dependencies]
taskchampion = { path = "../taskchampion", features = ["server-sync"] }
taskchampion = { path = "../taskchampion" }
taskchampion-lib = { path = "../lib" }
taskchampion-sync-server = { path = "../sync-server" }
[dev-dependencies]
anyhow.workspace = true
actix-web.workspace = true
actix-rt.workspace = true
tempfile.workspace = true
pretty_assertions.workspace = true
log.workspace = true
env_logger.workspace = true
lazy_static.workspace = true
[build-dependencies]

View File

@@ -186,7 +186,7 @@ static void test_replica_sync_local(void) {
static void test_replica_remote_server(void) {
TCString err;
TCServer *server = tc_server_new_sync(
tc_string_borrow("tc.freecinc.com"),
tc_string_borrow("http://tc.freecinc.com"),
tc_uuid_new_v4(),
tc_string_borrow("\xf0\x28\x8c\x28"), // NOTE: not utf-8
&err);

View File

@@ -1,97 +1,66 @@
use actix_web::{App, HttpServer};
use pretty_assertions::assert_eq;
use taskchampion::{Replica, ServerConfig, Status, StorageConfig, Uuid};
use taskchampion_sync_server::{storage::InMemoryStorage, Server};
use taskchampion::{Replica, ServerConfig, Status, StorageConfig};
use tempfile::TempDir;
#[actix_rt::test]
async fn cross_sync() -> anyhow::Result<()> {
async fn server() -> anyhow::Result<u16> {
let _ = env_logger::builder()
.is_test(true)
.filter_level(log::LevelFilter::Trace)
.try_init();
#[test]
fn cross_sync() -> anyhow::Result<()> {
// set up two replicas, and demonstrate replication between them
let mut rep1 = Replica::new(StorageConfig::InMemory.into_storage()?);
let mut rep2 = Replica::new(StorageConfig::InMemory.into_storage()?);
let server = Server::new(Default::default(), Box::new(InMemoryStorage::new()));
let httpserver = HttpServer::new(move || App::new().configure(|sc| server.config(sc)))
.bind("0.0.0.0:0")?;
let tmp_dir = TempDir::new().expect("TempDir failed");
let server_config = ServerConfig::Local {
server_dir: tmp_dir.path().to_path_buf(),
};
let mut server = server_config.into_server()?;
// bind was to :0, so the kernel will have selected an unused port
let port = httpserver.addrs()[0].port();
actix_rt::spawn(httpserver.run());
Ok(port)
}
// add some tasks on rep1
let t1 = rep1.new_task(Status::Pending, "test 1".into())?;
let t2 = rep1.new_task(Status::Pending, "test 2".into())?;
fn client(port: u16) -> anyhow::Result<()> {
// set up two replicas, and demonstrate replication between them
let mut rep1 = Replica::new(StorageConfig::InMemory.into_storage()?);
let mut rep2 = Replica::new(StorageConfig::InMemory.into_storage()?);
// modify t1
let mut t1 = t1.into_mut(&mut rep1);
t1.start()?;
let t1 = t1.into_immut();
let client_id = Uuid::new_v4();
let encryption_secret = b"abc123".to_vec();
let make_server = || {
ServerConfig::Remote {
origin: format!("http://127.0.0.1:{}", port),
client_id,
encryption_secret: encryption_secret.clone(),
}
.into_server()
};
rep1.sync(&mut server, false)?;
rep2.sync(&mut server, false)?;
let mut serv1 = make_server()?;
let mut serv2 = make_server()?;
// those tasks should exist on rep2 now
let t12 = rep2
.get_task(t1.get_uuid())?
.expect("expected task 1 on rep2");
let t22 = rep2
.get_task(t2.get_uuid())?
.expect("expected task 2 on rep2");
// add some tasks on rep1
let t1 = rep1.new_task(Status::Pending, "test 1".into())?;
let t2 = rep1.new_task(Status::Pending, "test 2".into())?;
assert_eq!(t12.get_description(), "test 1");
assert_eq!(t12.is_active(), true);
assert_eq!(t22.get_description(), "test 2");
assert_eq!(t22.is_active(), false);
// modify t1
let mut t1 = t1.into_mut(&mut rep1);
t1.start()?;
let t1 = t1.into_immut();
// make non-conflicting changes on the two replicas
let mut t2 = t2.into_mut(&mut rep1);
t2.set_status(Status::Completed)?;
let t2 = t2.into_immut();
rep1.sync(&mut serv1, false)?;
rep2.sync(&mut serv2, false)?;
let mut t12 = t12.into_mut(&mut rep2);
t12.set_status(Status::Completed)?;
// those tasks should exist on rep2 now
let t12 = rep2
.get_task(t1.get_uuid())?
.expect("expected task 1 on rep2");
let t22 = rep2
.get_task(t2.get_uuid())?
.expect("expected task 2 on rep2");
// sync those changes back and forth
rep1.sync(&mut server, false)?; // rep1 -> server
rep2.sync(&mut server, false)?; // server -> rep2, rep2 -> server
rep1.sync(&mut server, false)?; // server -> rep1
assert_eq!(t12.get_description(), "test 1");
assert_eq!(t12.is_active(), true);
assert_eq!(t22.get_description(), "test 2");
assert_eq!(t22.is_active(), false);
let t1 = rep1
.get_task(t1.get_uuid())?
.expect("expected task 1 on rep1");
assert_eq!(t1.get_status(), Status::Completed);
// make non-conflicting changes on the two replicas
let mut t2 = t2.into_mut(&mut rep1);
t2.set_status(Status::Completed)?;
let t2 = t2.into_immut();
let t22 = rep2
.get_task(t2.get_uuid())?
.expect("expected task 2 on rep2");
assert_eq!(t22.get_status(), Status::Completed);
let mut t12 = t12.into_mut(&mut rep2);
t12.set_status(Status::Completed)?;
// sync those changes back and forth
rep1.sync(&mut serv1, false)?; // rep1 -> server
rep2.sync(&mut serv2, false)?; // server -> rep2, rep2 -> server
rep1.sync(&mut serv1, false)?; // server -> rep1
let t1 = rep1
.get_task(t1.get_uuid())?
.expect("expected task 1 on rep1");
assert_eq!(t1.get_status(), Status::Completed);
let t22 = rep2
.get_task(t2.get_uuid())?
.expect("expected task 2 on rep2");
assert_eq!(t22.get_status(), Status::Completed);
Ok(())
}
let port = server().await?;
actix_rt::task::spawn_blocking(move || client(port)).await??;
Ok(())
}

View File

@@ -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(())
}

View File

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

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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())
}
}

View File

@@ -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());
}
}

View File

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

View File

@@ -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(())
}
}

View File

@@ -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>>;
}

View File

@@ -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(())
}
}

View File

@@ -15,7 +15,7 @@ rust-version = "1.70.0"
default = ["server-sync", "server-gcp"]
# Support for sync to a server
server-sync = ["encryption", "dep:ureq"]
server-sync = ["encryption", "dep:ureq", "dep:url"]
# Support for sync to GCP
server-gcp = ["cloud", "encryption", "dep:google-cloud-storage", "dep:tokio"]
# (private) Support for sync protocol encryption
@@ -43,10 +43,12 @@ byteorder.workspace = true
ring.workspace = true
google-cloud-storage.workspace = true
tokio.workspace = true
url.workspace = true
google-cloud-storage.optional = true
tokio.optional = true
ureq.optional = true
url.optional = true
ring.optional = true
[dev-dependencies]

View File

@@ -332,7 +332,7 @@ mod test {
let version_id = Uuid::parse_str("b0517957-f912-4d49-8330-f612e73030c4").unwrap();
let encryption_secret = b"b4a4e6b7b811eda1dc1a2693ded".to_vec();
let client_id = Uuid::parse_str("0666d464-418a-4a08-ad53-6f15c78270cd").unwrap();
let salt = client_id.as_ref().to_vec();
let salt = client_id.as_bytes().to_vec();
(version_id, salt, encryption_secret)
}

View File

@@ -4,6 +4,7 @@ use crate::server::{
VersionId,
};
use std::time::Duration;
use url::Url;
use uuid::Uuid;
use super::encryption::{Cryptor, Sealed, Secret, Unsealed};
@@ -28,8 +29,16 @@ impl SyncServer {
/// identify this client to the server. Multiple replicas synchronizing the same task history
/// should use the same client_id.
pub fn new(origin: String, client_id: Uuid, encryption_secret: Vec<u8>) -> Result<SyncServer> {
let origin = Url::parse(&origin)
.map_err(|_| Error::Server(format!("Could not parse {} as a URL", origin)))?;
if origin.path() != "/" {
return Err(Error::Server(format!(
"Server origin must have an empty path; got {}",
origin
)));
}
Ok(SyncServer {
origin,
origin: origin.to_string(),
client_id,
cryptor: Cryptor::new(client_id, &Secret(encryption_secret.to_vec()))?,
agent: ureq::AgentBuilder::new()
@@ -85,10 +94,7 @@ impl Server for SyncServer {
parent_version_id: VersionId,
history_segment: HistorySegment,
) -> Result<(AddVersionResult, SnapshotUrgency)> {
let url = format!(
"{}/v1/client/add-version/{}",
self.origin, parent_version_id
);
let url = format!("{}v1/client/add-version/{}", self.origin, parent_version_id);
let unsealed = Unsealed {
version_id: parent_version_id,
payload: history_segment,
@@ -121,7 +127,7 @@ impl Server for SyncServer {
fn get_child_version(&mut self, parent_version_id: VersionId) -> Result<GetVersionResult> {
let url = format!(
"{}/v1/client/get-child-version/{}",
"{}v1/client/get-child-version/{}",
self.origin, parent_version_id
);
match self
@@ -150,7 +156,7 @@ impl Server for SyncServer {
}
fn add_snapshot(&mut self, version_id: VersionId, snapshot: Snapshot) -> Result<()> {
let url = format!("{}/v1/client/add-snapshot/{}", self.origin, version_id);
let url = format!("{}v1/client/add-snapshot/{}", self.origin, version_id);
let unsealed = Unsealed {
version_id,
payload: snapshot,
@@ -166,7 +172,7 @@ impl Server for SyncServer {
}
fn get_snapshot(&mut self) -> Result<Option<(VersionId, Snapshot)>> {
let url = format!("{}/v1/client/snapshot", self.origin);
let url = format!("{}v1/client/snapshot", self.origin);
match self
.agent
.get(&url)

View File

@@ -56,6 +56,22 @@ class TestHooksOnExit(TestCase):
logs = hook.get_logs()
self.assertEqual(logs["output"]["msgs"][0], "FEEDBACK")
def test_onexit_builtin_good_gets_changed_tasks(self):
"""on-exit-good - a well-behaved, successful, on-exit hook."""
hookname = 'on-exit-good'
self.t.hooks.add_default(hookname, log=True)
code, out, err = self.t("add foo")
self.assertIn("Created task", out)
hook = self.t.hooks[hookname]
hook.assertTriggeredCount(1)
hook.assertExitcode(0)
logs = hook.get_logs()
self.assertEqual(logs["output"]["msgs"][0], "CHANGED TASK")
self.assertEqual(logs["output"]["msgs"][1], "FEEDBACK")
def test_onexit_builtin_bad(self):
"""on-exit-bad - a well-behaved, failing, on-exit hook."""
hookname = 'on-exit-bad'

View File

@@ -52,8 +52,8 @@ if __name__ == "__main__":
unexpected = defaultdict(int)
passed = defaultdict(int)
file = re.compile("^# (?:./)?(\S+\.t)(?:\.exe)?$")
timestamp = re.compile("^# (\d+(?:\.\d+)?) ==>.*$")
file = re.compile(r"^# (?:./)?(\S+\.t)(?:\.exe)?$")
timestamp = re.compile(r"^# (\d+(?:\.\d+)?) ==>.*$")
expected_fail = re.compile(r"^not ok.*?#\s*TODO", re.I)
unexpected_pass = re.compile(r"^not ok .*?#\s*FIXED", re.I)