Merge branch 'main' into sqlstore

This commit is contained in:
dbr 2021-09-10 09:59:35 +10:00
commit 43ca0623b1
15 changed files with 176 additions and 22 deletions

0
.changelogs/.gitignore vendored Normal file
View file

View file

@ -27,6 +27,13 @@ jobs:
path: target path: target
key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
- uses: actions-rs/toolchain@v1
with:
# Fixed version for clippy lints. Bump this as necesary. It must not
# be older than the MSRV in tests.yml.
toolchain: "1.54"
override: true
- uses: actions-rs/cargo@v1.0.1 - uses: actions-rs/cargo@v1.0.1
with: with:
command: check command: check

View file

@ -9,15 +9,19 @@ on:
jobs: jobs:
test: test:
runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
rust: rust:
- "1.47" # MSRV # MSRV; most not be higher than the clippy rust version in checks.yml
- "1.47"
- "stable" - "stable"
os:
- ubuntu-latest
- macOS-latest
- windows-latest
name: "Test - Rust ${{ matrix.rust }}" name: "Test - Rust ${{ matrix.rust }} on ${{ matrix.os }}"
runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v1 - uses: actions/checkout@v1

20
CHANGELOG.md Normal file
View file

@ -0,0 +1,20 @@
# Changelog
## [Unreleased]
Note: unreleased change log entries are kept in `.changelogs/` directory in repo root, and can be added with `./script/changelog.py add "Added thing for reason"
## 0.3.0 - 2021-01-11
- Flexible named reports
- Updates to the TaskChampion crate API
- Usability improvements
## 0.2.0 - 2020-11-30
This release is the first "MVP" version of this tool. It can do basic task operations, and supports a synchronization. Major missing features are captured in issues, but briefly:
better command-line API, similar to TaskWarrior
authentication of the replica / server protocol
encryption of replica data before transmission to the server
lots of task features (tags, annotations, dependencies, ..)
lots of CLI features (filtering, modifying, ..)

View file

@ -7,6 +7,10 @@ It also means that things are changing quickly, and lots of stuff is planned tha
If you would like to work on TaskChampion, please contact the developers (via the issue tracker) before spending a lot of time working on a pull request. If you would like to work on TaskChampion, please contact the developers (via the issue tracker) before spending a lot of time working on a pull request.
Doing so may save you some wasted time and frustration! Doing so may save you some wasted time and frustration!
A good starting point might be one of the issues tagged with ["good first issue"][first].
[first]: https://github.com/taskchampion/taskchampion/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22
# Other Ways To Help # Other Ways To Help
The best way to help this project to grow is to help spread awareness of it. The best way to help this project to grow is to help spread awareness of it.
@ -15,7 +19,6 @@ Tell your friends, post to social media, blog about it -- whatever works best!
Other ideas; Other ideas;
* Improve the documentation where it's unclear or lacking some information * Improve the documentation where it's unclear or lacking some information
* Build and maintain tools that integrate with TaskChampion * Build and maintain tools that integrate with TaskChampion
* Devise a nice TaskChampion logo
# Development Guide # Development Guide
@ -44,8 +47,19 @@ You may be able to limit the scope of what you need to understand to just one cr
You can generate the documentation for the `taskchampion` crate with `cargo doc --release --open -p taskchampion`. You can generate the documentation for the `taskchampion` crate with `cargo doc --release --open -p taskchampion`.
## Making a Pull Request ## Making a Pull Request
We expect contributors to follow the [GitHub Flow](https://guides.github.com/introduction/flow/). We expect contributors to follow the [GitHub Flow](https://guides.github.com/introduction/flow/).
Aside from that, we have no particular requirements on pull requests. Aside from that, we have no particular requirements on pull requests.
Make your patch, double-check that it's complete (tests? docs? documentation comments?), and make a new pull request. Make your patch, double-check that it's complete (tests? docs? documentation comments?), and make a new pull request.
Any non-trivial change (particularly those that change the behaviour of the application, or change the API) should be noted in the projects changelog.
In order to manage this, changelog entries are stored as text files in the `.changelog/` directory at the repository root.
To add a new changelog entry, you can simply run `python3 ./script/changelog.py add "Fixed thingo to increase zorbloxification [Issue #2](http://example.com)`
This creates a file named `./changelogs/yyyy-mm-dd-branchname.md` (timestamp, current git branch) which contains a markdown snippet.
If you don't have a Python 3 intepreter installed, you can simply create this file manually. It should contain a list item like `- Fixed thingo [...]`
Periodically (probably just before release), these changelog entries are concatenated combined together and added into the `CHANGELOG.md` file.

View file

@ -1,5 +1,6 @@
# Release process # Release process
1. Ensure the changelog is updated with everything from the `.changelogs` directory. `python3 ./script/changelog.py build` will output a Markdown snippet to include in `CHANGELOG.md` then `rm .changelog/*.txt`
1. Run `git pull upstream main` 1. Run `git pull upstream main`
1. Run `cargo test` 1. Run `cargo test`
1. Run `cargo clean && cargo clippy` 1. Run `cargo clean && cargo clippy`

View file

@ -154,11 +154,18 @@ fn named_date<Tz: TimeZone>(
move |input: &str| { move |input: &str| {
let local_today = now.with_timezone(&local).date(); let local_today = now.with_timezone(&local).date();
let remaining = &input[input.len()..]; let remaining = &input[input.len()..];
let day_index = local_today.weekday().num_days_from_monday();
match input { match input {
"yesterday" => Ok((remaining, local_today - Duration::days(1))), "yesterday" => Ok((remaining, local_today - Duration::days(1))),
"today" => Ok((remaining, local_today)), "today" => Ok((remaining, local_today)),
"tomorrow" => Ok((remaining, local_today + Duration::days(1))), "tomorrow" => Ok((remaining, local_today + Duration::days(1))),
// TODO: lots more! // TODO: lots more!
"eod" => Ok((remaining,local_today + Duration::days(1))),
"sod" => Ok((remaining,local_today)),
"eow" => Ok((remaining,local_today + Duration::days((6-day_index).into()))),
"eoww" => Ok((remaining,local_today + Duration::days((5-day_index).into()))),
"sow" => Ok((remaining,local_today + Duration::days((6-day_index).into()))),
"soww" => Ok((remaining,local_today + Duration::days((7-day_index).into()))),
_ => Err(Err::Error(Error::new(input, ErrorKind::Tag))), _ => Err(Err::Error(Error::new(input, ErrorKind::Tag))),
} }
.map(|(rem, dt)| (rem, dt.and_hms(0, 0, 0).with_timezone(&Utc))) .map(|(rem, dt)| (rem, dt.and_hms(0, 0, 0).with_timezone(&Utc)))
@ -301,6 +308,12 @@ mod test {
#[case::today_from_evening(ldt(2021, 3, 1, 21, 30, 30), "today", ld(2021, 3, 1))] #[case::today_from_evening(ldt(2021, 3, 1, 21, 30, 30), "today", ld(2021, 3, 1))]
#[case::tomorrow(ld(2021, 3, 1), "tomorrow", ld(2021, 3, 2))] #[case::tomorrow(ld(2021, 3, 1), "tomorrow", ld(2021, 3, 2))]
#[case::tomorow_from_evening(ldt(2021, 3, 1, 21, 30, 30), "tomorrow", ld(2021, 3, 2))] #[case::tomorow_from_evening(ldt(2021, 3, 1, 21, 30, 30), "tomorrow", ld(2021, 3, 2))]
#[case::end_of_week(ld(2021,8,25,), "eow", ld(2021,8,29))]
#[case::end_of_work_week(ld(2021,8,25), "eoww", ld(2021,8,28))]
#[case::start_of_week(ld(2021,8,25), "sow", ld(2021,8,29))]
#[case::start_of_work_week(ld(2021,8,25), "soww", ld(2021,8,30))]
#[case::end_of_today(ld(2021,8,25), "eod", ld(2021,8,26))]
#[case::start_of_today(ld(2021,8,25), "sod", ld(2021,8,25))]
fn test_local_timestamp( fn test_local_timestamp(
#[case] now: Box<dyn Fn(FixedOffset) -> DateTime<Utc>>, #[case] now: Box<dyn Fn(FixedOffset) -> DateTime<Utc>>,
#[values(*IST, *UTC_FO, *HST)] tz: FixedOffset, #[values(*IST, *UTC_FO, *HST)] tz: FixedOffset,

View file

@ -125,7 +125,7 @@ fn get_server(settings: &Settings) -> anyhow::Result<Box<dyn Server>> {
settings.server_origin.as_ref(), settings.server_origin.as_ref(),
settings.encryption_secret.as_ref(), settings.encryption_secret.as_ref(),
) { ) {
let client_key = Uuid::parse_str(&client_key)?; let client_key = Uuid::parse_str(client_key)?;
log::debug!("Using sync-server with origin {}", origin); log::debug!("Using sync-server with origin {}", origin);
log::debug!("Sync client ID: {}", client_key); log::debug!("Sync client ID: {}", client_key);

View file

@ -30,11 +30,11 @@ pub(super) fn apply_modification(
} }
for tag in modification.add_tags.iter() { for tag in modification.add_tags.iter() {
task.add_tag(&tag)?; task.add_tag(tag)?;
} }
for tag in modification.remove_tags.iter() { for tag in modification.remove_tags.iter() {
task.remove_tag(&tag)?; task.remove_tag(tag)?;
} }
if let Some(wait) = modification.wait { if let Some(wait) = modification.wait {

View file

@ -31,8 +31,7 @@ For the public TaskChampion Rust API, see the `taskchampion` crate.
*/ */
use std::os::unix::ffi::OsStringExt; use std::ffi::OsString;
use std::string::FromUtf8Error;
// NOTE: it's important that this 'mod' comes first so that the macros can be used in other modules // NOTE: it's important that this 'mod' comes first so that the macros can be used in other modules
mod macros; mod macros;
@ -63,8 +62,8 @@ pub fn main() -> Result<(), Error> {
// parse the command line into a vector of &str, failing if // parse the command line into a vector of &str, failing if
// there are invalid utf-8 sequences. // there are invalid utf-8 sequences.
let argv: Vec<String> = std::env::args_os() let argv: Vec<String> = std::env::args_os()
.map(|oss| String::from_utf8(oss.into_vec())) .map(|oss| oss.into_string())
.collect::<Result<_, FromUtf8Error>>() .collect::<Result<_, OsString>>()
.map_err(|_| Error::for_arguments("arguments must be valid utf-8"))?; .map_err(|_| Error::for_arguments("arguments must be valid utf-8"))?;
let argv: Vec<&str> = argv.iter().map(|s| s.as_ref()).collect(); let argv: Vec<&str> = argv.iter().map(|s| s.as_ref()).collect();

View file

@ -130,7 +130,7 @@ impl TryFrom<&toml::Value> for Report {
.map(|(i, v)| { .map(|(i, v)| {
v.as_str() v.as_str()
.ok_or_else(|| anyhow!(".filter[{}]: not a string", i)) .ok_or_else(|| anyhow!(".filter[{}]: not a string", i))
.and_then(|s| Condition::parse_str(&s)) .and_then(|s| Condition::parse_str(s))
.map_err(|e| anyhow!(".filter[{}]: {}", i, e)) .map_err(|e| anyhow!(".filter[{}]: {}", i, e))
}) })
.collect::<Result<Vec<_>>>()?, .collect::<Result<Vec<_>>>()?,

View file

@ -97,7 +97,7 @@ impl Settings {
"server_dir", "server_dir",
"reports", "reports",
]; ];
let table = table_with_keys(&config_toml, &table_keys)?; let table = table_with_keys(config_toml, &table_keys)?;
fn get_str_cfg<F: FnOnce(String)>( fn get_str_cfg<F: FnOnce(String)>(
table: &Table, table: &Table,
@ -184,10 +184,24 @@ impl Settings {
.ok_or_else(|| anyhow!("Could not determine config file name"))? .ok_or_else(|| anyhow!("Could not determine config file name"))?
}; };
let mut document = fs::read_to_string(filename.clone()) let exists = filename.exists();
.context("Could not read existing configuration file")?
.parse::<Document>() // try to create the parent directory if the file does not exist
.context("Could not parse existing configuration file")?; if !exists {
if let Some(dir) = filename.parent() {
fs::create_dir_all(dir)?;
}
}
// start with the existing document, or a blank document
let mut document = if exists {
fs::read_to_string(filename.clone())
.context("Could not read existing configuration file")?
.parse::<Document>()
.context("Could not parse existing configuration file")?
} else {
Document::new()
};
// set the value as the correct type // set the value as the correct type
match key { match key {

Binary file not shown.

After

Width:  |  Height:  |  Size: 8 KiB

View file

@ -28,3 +28,21 @@ Some of the units allow an adjectival form, such as `daily` or `annually`; this
[ISO 8601 standard durations](https://en.wikipedia.org/wiki/ISO_8601#Durations) are also allowed. [ISO 8601 standard durations](https://en.wikipedia.org/wiki/ISO_8601#Durations) are also allowed.
While the standard does not specify the length of "P1Y" or "P1M", Taskchampion treats those as 365 and 30 days, respectively. While the standard does not specify the length of "P1Y" or "P1M", Taskchampion treats those as 365 and 30 days, respectively.
## Named Timestamps
Some commonly used named timestamps
* `today` Start of today
* `yesterday` Start of yesterday
* `tomorrow` Start of tomorrow
* `sod` Start of today
* `eod` End of today
* `sow` Start of the next week
* `eow` End of the week
* `eoww` End of work week
* `soww` Start of the next work week
![named timestamp](images/name_timestamp.png)

64
scripts/changelog.py Executable file
View file

@ -0,0 +1,64 @@
#!/usr/bin/env python3
import os
import argparse
import datetime
import subprocess
from typing import List
def ymd():
return datetime.datetime.now().strftime("%Y-%m-%d")
def git_current_branch() -> str :
out = subprocess.check_output(["git", "branch", "--show-current"])
return out.strip().decode("utf-8")
def get_dir() -> str:
here = os.path.dirname(os.path.abspath(__file__))
return os.path.join(
here,
"../.changelogs")
def get_changefiles() -> List[str]:
changedir = get_dir()
changefiles = []
for f in os.listdir(changedir):
if f.endswith(".md") and not f.startswith("."):
changefiles.append(os.path.join(changedir, f))
return changefiles
def cmd_add(args):
text = args.text.strip()
if not text.startswith("- "):
text = "- %s" % text
timestamp = ymd()
branchname = git_current_branch()
fname = os.path.join(get_dir(), "%s-%s.md" % (timestamp, branchname))
with open(fname, "a") as f:
f.write(text)
f.write("\n")
def cmd_build(args):
print("## x.y.z - %s" % (ymd()))
for e in get_changefiles():
print(open(e).read().strip())
def main() -> None:
parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(title='Sub commands', dest='command')
subparsers.required = True
parser_add = subparsers.add_parser('add')
parser_add.add_argument("text")
parser_add.set_defaults(func=cmd_add)
parser_build = subparsers.add_parser('build')
parser_build.set_defaults(func=cmd_build)
args = parser.parse_args()
args.func(args)
if __name__ == "__main__":
main()