mirror of
https://github.com/GothenburgBitFactory/taskwarrior.git
synced 2025-08-21 07:43:08 +02:00
Merge branch 'main' into sqlstore
This commit is contained in:
commit
43ca0623b1
15 changed files with 176 additions and 22 deletions
0
.changelogs/.gitignore
vendored
Normal file
0
.changelogs/.gitignore
vendored
Normal file
7
.github/workflows/checks.yml
vendored
7
.github/workflows/checks.yml
vendored
|
@ -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
|
||||||
|
|
12
.github/workflows/tests.yml
vendored
12
.github/workflows/tests.yml
vendored
|
@ -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
20
CHANGELOG.md
Normal 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, ..)
|
|
@ -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.
|
||||||
|
|
|
@ -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`
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
|
|
@ -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<_>>>()?,
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
BIN
docs/src/images/name_timestamp.png
Normal file
BIN
docs/src/images/name_timestamp.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 8 KiB |
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|

|
||||||
|
|
64
scripts/changelog.py
Executable file
64
scripts/changelog.py
Executable 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()
|
Loading…
Add table
Add a link
Reference in a new issue