mirror of
https://github.com/GothenburgBitFactory/taskwarrior.git
synced 2025-07-07 20:06:36 +02:00
move contents of taskchampion repo to tc/
This commit is contained in:
parent
73baefa0a5
commit
2a92b2a4b9
219 changed files with 0 additions and 0 deletions
77
rust/cli/src/invocation/cmd/add.rs
Normal file
77
rust/cli/src/invocation/cmd/add.rs
Normal file
|
@ -0,0 +1,77 @@
|
|||
use crate::argparse::DescriptionMod;
|
||||
use crate::invocation::{apply_modification, ResolvedModification};
|
||||
use taskchampion::{Replica, Status};
|
||||
use termcolor::WriteColor;
|
||||
|
||||
pub(in crate::invocation) fn execute<W: WriteColor>(
|
||||
w: &mut W,
|
||||
replica: &mut Replica,
|
||||
mut modification: ResolvedModification,
|
||||
) -> Result<(), crate::Error> {
|
||||
// extract the description from the modification to handle it specially
|
||||
let description = match modification.0.description {
|
||||
DescriptionMod::Set(ref s) => s.clone(),
|
||||
_ => "(no description)".to_owned(),
|
||||
};
|
||||
modification.0.description = DescriptionMod::None;
|
||||
|
||||
let task = replica.new_task(Status::Pending, description).unwrap();
|
||||
let mut task = task.into_mut(replica);
|
||||
apply_modification(&mut task, &modification)?;
|
||||
writeln!(w, "added task {}", task.get_uuid())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::argparse::Modification;
|
||||
use crate::invocation::test::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_add() {
|
||||
let mut w = test_writer();
|
||||
let mut replica = test_replica();
|
||||
let modification = ResolvedModification(Modification {
|
||||
description: DescriptionMod::Set(s!("my description")),
|
||||
..Default::default()
|
||||
});
|
||||
execute(&mut w, &mut replica, modification).unwrap();
|
||||
|
||||
// check that the task appeared..
|
||||
let working_set = replica.working_set().unwrap();
|
||||
let task = replica
|
||||
.get_task(working_set.by_index(1).unwrap())
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(task.get_description(), "my description");
|
||||
assert_eq!(task.get_status(), Status::Pending);
|
||||
|
||||
assert_eq!(w.into_string(), format!("added task {}\n", task.get_uuid()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_add_with_tags() {
|
||||
let mut w = test_writer();
|
||||
let mut replica = test_replica();
|
||||
let modification = ResolvedModification(Modification {
|
||||
description: DescriptionMod::Set(s!("my description")),
|
||||
add_tags: vec![tag!("tag1")].drain(..).collect(),
|
||||
..Default::default()
|
||||
});
|
||||
execute(&mut w, &mut replica, modification).unwrap();
|
||||
|
||||
// check that the task appeared..
|
||||
let working_set = replica.working_set().unwrap();
|
||||
let task = replica
|
||||
.get_task(working_set.by_index(1).unwrap())
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(task.get_description(), "my description");
|
||||
assert_eq!(task.get_status(), Status::Pending);
|
||||
assert!(task.has_tag(&tag!("tag1")));
|
||||
|
||||
assert_eq!(w.into_string(), format!("added task {}\n", task.get_uuid()));
|
||||
}
|
||||
}
|
1
rust/cli/src/invocation/cmd/completed.data
Normal file
1
rust/cli/src/invocation/cmd/completed.data
Normal file
|
@ -0,0 +1 @@
|
|||
[description:"&open;TEST&close; foo" entry:"1554074416" modified:"1554074416" priority:"M" status:"completed" uuid:"4578fb67-359b-4483-afe4-fef15925ccd6"]
|
69
rust/cli/src/invocation/cmd/config.rs
Normal file
69
rust/cli/src/invocation/cmd/config.rs
Normal file
|
@ -0,0 +1,69 @@
|
|||
use crate::argparse::ConfigOperation;
|
||||
use crate::settings::Settings;
|
||||
use termcolor::{ColorSpec, WriteColor};
|
||||
|
||||
pub(crate) fn execute<W: WriteColor>(
|
||||
w: &mut W,
|
||||
config_operation: ConfigOperation,
|
||||
settings: &Settings,
|
||||
) -> Result<(), crate::Error> {
|
||||
match config_operation {
|
||||
ConfigOperation::Set(key, value) => {
|
||||
let filename = settings.set(&key, &value)?;
|
||||
write!(w, "Set configuration value ")?;
|
||||
w.set_color(ColorSpec::new().set_bold(true))?;
|
||||
write!(w, "{}", &key)?;
|
||||
w.set_color(ColorSpec::new().set_bold(false))?;
|
||||
write!(w, " in ")?;
|
||||
w.set_color(ColorSpec::new().set_bold(true))?;
|
||||
writeln!(w, "{:?}.", filename)?;
|
||||
w.set_color(ColorSpec::new().set_bold(false))?;
|
||||
}
|
||||
ConfigOperation::Path => {
|
||||
if let Some(ref filename) = settings.filename {
|
||||
writeln!(w, "{}", filename.to_string_lossy())?;
|
||||
} else {
|
||||
return Err(anyhow::anyhow!("No configuration filename found").into());
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::invocation::test::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::fs;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn test_config_set() {
|
||||
let cfg_dir = TempDir::new().unwrap();
|
||||
let cfg_file = cfg_dir.path().join("foo.toml");
|
||||
fs::write(
|
||||
cfg_file.clone(),
|
||||
"# store data everywhere\ndata_dir = \"/nowhere\"\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let settings = Settings::load_from_file(cfg_file.clone(), true).unwrap();
|
||||
|
||||
let mut w = test_writer();
|
||||
|
||||
execute(
|
||||
&mut w,
|
||||
ConfigOperation::Set("data_dir".to_owned(), "/somewhere".to_owned()),
|
||||
&settings,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(w.into_string().starts_with("Set configuration value "));
|
||||
|
||||
let updated_toml = fs::read_to_string(cfg_file.clone()).unwrap();
|
||||
assert_eq!(
|
||||
updated_toml,
|
||||
"# store data everywhere\ndata_dir = \"/somewhere\"\n"
|
||||
);
|
||||
}
|
||||
}
|
26
rust/cli/src/invocation/cmd/gc.rs
Normal file
26
rust/cli/src/invocation/cmd/gc.rs
Normal file
|
@ -0,0 +1,26 @@
|
|||
use taskchampion::Replica;
|
||||
use termcolor::WriteColor;
|
||||
|
||||
pub(crate) fn execute<W: WriteColor>(w: &mut W, replica: &mut Replica) -> Result<(), crate::Error> {
|
||||
log::debug!("rebuilding working set");
|
||||
replica.rebuild_working_set(true)?;
|
||||
log::debug!("expiring old tasks");
|
||||
replica.expire_tasks()?;
|
||||
writeln!(w, "garbage collected.")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::invocation::test::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_gc() {
|
||||
let mut w = test_writer();
|
||||
let mut replica = test_replica();
|
||||
execute(&mut w, &mut replica).unwrap();
|
||||
assert_eq!(&w.into_string(), "garbage collected.\n")
|
||||
}
|
||||
}
|
30
rust/cli/src/invocation/cmd/help.rs
Normal file
30
rust/cli/src/invocation/cmd/help.rs
Normal file
|
@ -0,0 +1,30 @@
|
|||
use crate::usage::Usage;
|
||||
use termcolor::WriteColor;
|
||||
|
||||
pub(crate) fn execute<W: WriteColor>(
|
||||
w: &mut W,
|
||||
command_name: String,
|
||||
summary: bool,
|
||||
) -> Result<(), crate::Error> {
|
||||
let usage = Usage::new();
|
||||
usage.write_help(w, command_name.as_ref(), summary)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::invocation::test::*;
|
||||
|
||||
#[test]
|
||||
fn test_summary() {
|
||||
let mut w = test_writer();
|
||||
execute(&mut w, s!("ta"), true).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_long() {
|
||||
let mut w = test_writer();
|
||||
execute(&mut w, s!("ta"), false).unwrap();
|
||||
}
|
||||
}
|
142
rust/cli/src/invocation/cmd/import_tdb2.rs
Normal file
142
rust/cli/src/invocation/cmd/import_tdb2.rs
Normal file
|
@ -0,0 +1,142 @@
|
|||
use crate::tdb2;
|
||||
use anyhow::anyhow;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use taskchampion::{Replica, Uuid};
|
||||
use termcolor::{Color, ColorSpec, WriteColor};
|
||||
|
||||
pub(crate) fn execute<W: WriteColor>(
|
||||
w: &mut W,
|
||||
replica: &mut Replica,
|
||||
path: &str,
|
||||
) -> Result<(), crate::Error> {
|
||||
let path: PathBuf = path.into();
|
||||
|
||||
let mut count = 0;
|
||||
for file in &["pending.data", "completed.data"] {
|
||||
let file = path.join(file);
|
||||
w.set_color(ColorSpec::new().set_bold(true))?;
|
||||
writeln!(w, "Importing tasks from {:?}.", file)?;
|
||||
w.reset()?;
|
||||
|
||||
let data = fs::read_to_string(file)?;
|
||||
let content =
|
||||
tdb2::File::from_str(&data).map_err(|_| anyhow!("Could not parse TDB2 file format"))?;
|
||||
count += content.lines.len();
|
||||
for line in content.lines {
|
||||
import_task(w, replica, line)?;
|
||||
}
|
||||
}
|
||||
w.set_color(ColorSpec::new().set_bold(true))?;
|
||||
writeln!(w, "{} tasks imported.", count)?;
|
||||
w.reset()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn import_task<W: WriteColor>(
|
||||
w: &mut W,
|
||||
replica: &mut Replica,
|
||||
mut line: tdb2::Line,
|
||||
) -> anyhow::Result<()> {
|
||||
let mut uuid = None;
|
||||
for attr in line.attrs.iter() {
|
||||
if &attr.name == "uuid" {
|
||||
uuid = Some(Uuid::parse_str(&attr.value)?);
|
||||
break;
|
||||
}
|
||||
}
|
||||
let uuid = uuid.ok_or_else(|| anyhow!("task has no uuid"))?;
|
||||
replica.import_task_with_uuid(uuid)?;
|
||||
|
||||
let mut description = None;
|
||||
for attr in line.attrs.drain(..) {
|
||||
// oddly, TaskWarrior represents [ and ] with their HTML entity equivalents
|
||||
let value = attr.value.replace("&open;", "[").replace("&close;", "]");
|
||||
match attr.name.as_ref() {
|
||||
// `uuid` was already handled
|
||||
"uuid" => {}
|
||||
|
||||
// everything else is inserted directly
|
||||
_ => {
|
||||
if attr.name == "description" {
|
||||
// keep a copy of the description for console output
|
||||
description = Some(value.clone());
|
||||
}
|
||||
replica.update_task(uuid, attr.name, Some(value))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
w.set_color(ColorSpec::new().set_fg(Some(Color::Yellow)))?;
|
||||
write!(w, "{}", uuid)?;
|
||||
w.reset()?;
|
||||
writeln!(
|
||||
w,
|
||||
" {}",
|
||||
description.unwrap_or_else(|| "(no description)".into())
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::invocation::test::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::convert::TryInto;
|
||||
use taskchampion::chrono::{TimeZone, Utc};
|
||||
use taskchampion::Status;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn test_import() -> anyhow::Result<()> {
|
||||
let mut w = test_writer();
|
||||
let mut replica = test_replica();
|
||||
let tmp_dir = TempDir::new()?;
|
||||
|
||||
fs::write(
|
||||
tmp_dir.path().join("pending.data"),
|
||||
include_bytes!("pending.data"),
|
||||
)?;
|
||||
fs::write(
|
||||
tmp_dir.path().join("completed.data"),
|
||||
include_bytes!("completed.data"),
|
||||
)?;
|
||||
|
||||
execute(&mut w, &mut replica, tmp_dir.path().to_str().unwrap())?;
|
||||
|
||||
let task = replica
|
||||
.get_task(Uuid::parse_str("f19086c2-1f8d-4a6c-9b8d-f94901fb8e62").unwrap())
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(task.get_description(), "snake 🐍");
|
||||
assert_eq!(task.get_status(), Status::Pending);
|
||||
assert_eq!(task.get_priority(), "M");
|
||||
assert_eq!(task.get_wait(), None);
|
||||
assert_eq!(
|
||||
task.get_modified(),
|
||||
Some(Utc.ymd(2022, 1, 8).and_hms(19, 33, 5))
|
||||
);
|
||||
assert!(task.has_tag(&"reptile".try_into().unwrap()));
|
||||
assert!(!task.has_tag(&"COMPLETED".try_into().unwrap()));
|
||||
|
||||
let task = replica
|
||||
.get_task(Uuid::parse_str("4578fb67-359b-4483-afe4-fef15925ccd6").unwrap())
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(task.get_description(), "[TEST] foo");
|
||||
assert_eq!(task.get_status(), Status::Completed);
|
||||
assert_eq!(task.get_priority(), "M".to_string());
|
||||
assert_eq!(task.get_wait(), None);
|
||||
assert_eq!(
|
||||
task.get_modified(),
|
||||
Some(Utc.ymd(2019, 3, 31).and_hms(23, 20, 16))
|
||||
);
|
||||
assert!(!task.has_tag(&"reptile".try_into().unwrap()));
|
||||
assert!(task.has_tag(&"COMPLETED".try_into().unwrap()));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
265
rust/cli/src/invocation/cmd/import_tw.rs
Normal file
265
rust/cli/src/invocation/cmd/import_tw.rs
Normal file
|
@ -0,0 +1,265 @@
|
|||
use anyhow::{anyhow, bail};
|
||||
use serde::{self, Deserialize, Deserializer};
|
||||
use serde_json::Value;
|
||||
use std::collections::HashMap;
|
||||
use taskchampion::chrono::{DateTime, TimeZone, Utc};
|
||||
use taskchampion::{Replica, Uuid};
|
||||
use termcolor::{Color, ColorSpec, WriteColor};
|
||||
|
||||
pub(crate) fn execute<W: WriteColor>(w: &mut W, replica: &mut Replica) -> Result<(), crate::Error> {
|
||||
w.set_color(ColorSpec::new().set_bold(true))?;
|
||||
writeln!(w, "Importing tasks from stdin.")?;
|
||||
w.reset()?;
|
||||
|
||||
let mut tasks: Vec<HashMap<String, Value>> =
|
||||
serde_json::from_reader(std::io::stdin()).map_err(|_| anyhow!("Invalid JSON"))?;
|
||||
|
||||
for task_json in tasks.drain(..) {
|
||||
import_task(w, replica, task_json)?;
|
||||
}
|
||||
|
||||
w.set_color(ColorSpec::new().set_bold(true))?;
|
||||
writeln!(w, "{} tasks imported.", tasks.len())?;
|
||||
w.reset()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Convert the given value to a string, failing on compound types (arrays
|
||||
/// and objects).
|
||||
fn stringify(v: Value) -> anyhow::Result<String> {
|
||||
Ok(match v {
|
||||
Value::String(s) => s,
|
||||
Value::Number(n) => n.to_string(),
|
||||
Value::Bool(true) => "true".to_string(),
|
||||
Value::Bool(false) => "false".to_string(),
|
||||
Value::Null => "null".to_string(),
|
||||
_ => bail!("{:?} cannot be converted to a string", v),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn deserialize_tw_datetime<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
const FORMAT: &str = "%Y%m%dT%H%M%SZ";
|
||||
let s = String::deserialize(deserializer)?;
|
||||
Utc.datetime_from_str(&s, FORMAT)
|
||||
.map_err(serde::de::Error::custom)
|
||||
}
|
||||
|
||||
/// Deserialize a string in the TaskWarrior format into a DateTime
|
||||
#[derive(Deserialize)]
|
||||
struct TwDateTime(#[serde(deserialize_with = "deserialize_tw_datetime")] DateTime<Utc>);
|
||||
|
||||
impl TwDateTime {
|
||||
/// Generate the data-model style UNIX timestamp for this DateTime
|
||||
fn tc_timestamp(&self) -> String {
|
||||
self.0.timestamp().to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct Annotation {
|
||||
entry: TwDateTime,
|
||||
description: String,
|
||||
}
|
||||
|
||||
fn import_task<W: WriteColor>(
|
||||
w: &mut W,
|
||||
replica: &mut Replica,
|
||||
mut task_json: HashMap<String, Value>,
|
||||
) -> anyhow::Result<()> {
|
||||
let uuid = task_json
|
||||
.get("uuid")
|
||||
.ok_or_else(|| anyhow!("task has no uuid"))?;
|
||||
let uuid = uuid
|
||||
.as_str()
|
||||
.ok_or_else(|| anyhow!("uuid is not a string"))?;
|
||||
let uuid = Uuid::parse_str(uuid)?;
|
||||
replica.import_task_with_uuid(uuid)?;
|
||||
|
||||
let mut description = None;
|
||||
for (k, v) in task_json.drain() {
|
||||
match k.as_ref() {
|
||||
// `id` is the working-set ID and is not stored
|
||||
"id" => {}
|
||||
|
||||
// `urgency` is also calculated and not stored
|
||||
"urgency" => {}
|
||||
|
||||
// `uuid` was already handled
|
||||
"uuid" => {}
|
||||
|
||||
// `annotations` is a sub-aray
|
||||
"annotations" => {
|
||||
let annotations: Vec<Annotation> = serde_json::from_value(v)?;
|
||||
for ann in annotations {
|
||||
let k = format!("annotation_{}", ann.entry.tc_timestamp());
|
||||
replica.update_task(uuid, k, Some(ann.description))?;
|
||||
}
|
||||
}
|
||||
|
||||
// `depends` is a sub-aray
|
||||
"depends" => {
|
||||
let deps: Vec<String> = serde_json::from_value(v)?;
|
||||
for dep in deps {
|
||||
let k = format!("dep_{}", dep);
|
||||
replica.update_task(uuid, k, Some("".to_owned()))?;
|
||||
}
|
||||
}
|
||||
|
||||
// `tags` is a sub-aray
|
||||
"tags" => {
|
||||
let tags: Vec<String> = serde_json::from_value(v)?;
|
||||
for tag in tags {
|
||||
let k = format!("tag_{}", tag);
|
||||
replica.update_task(uuid, k, Some("".to_owned()))?;
|
||||
}
|
||||
}
|
||||
|
||||
// convert all datetimes -> epoch integers
|
||||
"end" | "entry" | "modified" | "wait" | "due" => {
|
||||
let v: TwDateTime = serde_json::from_value(v)?;
|
||||
replica.update_task(uuid, k, Some(v.tc_timestamp()))?;
|
||||
}
|
||||
|
||||
// everything else is inserted directly
|
||||
_ => {
|
||||
let v = stringify(v)?;
|
||||
if k == "description" {
|
||||
// keep a copy of the description for console output
|
||||
description = Some(v.clone());
|
||||
}
|
||||
replica.update_task(uuid, k, Some(v))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
w.set_color(ColorSpec::new().set_fg(Some(Color::Yellow)))?;
|
||||
write!(w, "{}", uuid)?;
|
||||
w.reset()?;
|
||||
writeln!(
|
||||
w,
|
||||
" {}",
|
||||
description.unwrap_or_else(|| "(no description)".into())
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::invocation::test::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use std::convert::TryInto;
|
||||
use taskchampion::chrono::{TimeZone, Utc};
|
||||
use taskchampion::Status;
|
||||
|
||||
#[test]
|
||||
fn stringify_string() {
|
||||
assert_eq!(stringify(json!("foo")).unwrap(), "foo".to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stringify_number() {
|
||||
assert_eq!(stringify(json!(2.14)).unwrap(), "2.14".to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stringify_bool() {
|
||||
assert_eq!(stringify(json!(true)).unwrap(), "true".to_string());
|
||||
assert_eq!(stringify(json!(false)).unwrap(), "false".to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stringify_null() {
|
||||
assert_eq!(stringify(json!(null)).unwrap(), "null".to_string());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stringify_invalid() {
|
||||
assert!(stringify(json!([1])).is_err());
|
||||
assert!(stringify(json!({"a": 1})).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_import() -> anyhow::Result<()> {
|
||||
let mut w = test_writer();
|
||||
let mut replica = test_replica();
|
||||
|
||||
let task_json = serde_json::from_value(json!({
|
||||
"id": 0,
|
||||
"description": "repair window",
|
||||
"end": "20211231T175614Z", // TODO (#327)
|
||||
"entry": "20211117T022410Z", // TODO (#326)
|
||||
"modified": "20211231T175614Z",
|
||||
"priority": "M",
|
||||
"status": "completed",
|
||||
"uuid": "fa01e916-1587-4c7d-a646-f7be62be8ee7",
|
||||
"wait": "20211225T001523Z",
|
||||
"due": "20211225T040000Z", // TODO (#82)
|
||||
|
||||
// TODO: recurrence (#81)
|
||||
"imask": 2,
|
||||
"recur": "monthly",
|
||||
"rtype": "periodic",
|
||||
"mask": "+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--",
|
||||
|
||||
// (legacy) UDAs
|
||||
"githubcreatedon": "20211110T175919Z",
|
||||
"githubnamespace": "djmitche",
|
||||
"githubnumber": 228,
|
||||
|
||||
"tags": [
|
||||
"house"
|
||||
],
|
||||
"depends": [ // TODO (#84)
|
||||
"4f71035d-1704-47f0-885c-6f9134bcefb2"
|
||||
],
|
||||
"annotations": [
|
||||
{
|
||||
"entry": "20211223T142031Z",
|
||||
"description": "ordered from website"
|
||||
}
|
||||
],
|
||||
"urgency": 4.16849
|
||||
}))?;
|
||||
import_task(&mut w, &mut replica, task_json)?;
|
||||
|
||||
let task = replica
|
||||
.get_task(Uuid::parse_str("fa01e916-1587-4c7d-a646-f7be62be8ee7").unwrap())
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert_eq!(task.get_description(), "repair window");
|
||||
assert_eq!(task.get_status(), Status::Completed);
|
||||
assert_eq!(task.get_priority(), "M".to_string());
|
||||
assert_eq!(
|
||||
task.get_wait(),
|
||||
Some(Utc.ymd(2021, 12, 25).and_hms(00, 15, 23))
|
||||
);
|
||||
assert_eq!(
|
||||
task.get_modified(),
|
||||
Some(Utc.ymd(2021, 12, 31).and_hms(17, 56, 14))
|
||||
);
|
||||
assert!(task.has_tag(&"house".try_into().unwrap()));
|
||||
assert!(!task.has_tag(&"PENDING".try_into().unwrap()));
|
||||
assert_eq!(
|
||||
task.get_annotations().collect::<Vec<_>>(),
|
||||
vec![taskchampion::Annotation {
|
||||
entry: Utc.ymd(2021, 12, 23).and_hms(14, 20, 31),
|
||||
description: "ordered from website".into(),
|
||||
}]
|
||||
);
|
||||
assert_eq!(
|
||||
task.get_legacy_uda("githubcreatedon"),
|
||||
Some("20211110T175919Z")
|
||||
);
|
||||
assert_eq!(task.get_legacy_uda("githubnamespace"), Some("djmitche"));
|
||||
assert_eq!(task.get_legacy_uda("githubnumber"), Some("228"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
117
rust/cli/src/invocation/cmd/info.rs
Normal file
117
rust/cli/src/invocation/cmd/info.rs
Normal file
|
@ -0,0 +1,117 @@
|
|||
use crate::argparse::Filter;
|
||||
use crate::invocation::filtered_tasks;
|
||||
use crate::table;
|
||||
use prettytable::{cell, row, Table};
|
||||
use taskchampion::{Replica, Status};
|
||||
use termcolor::WriteColor;
|
||||
|
||||
pub(crate) fn execute<W: WriteColor>(
|
||||
w: &mut W,
|
||||
replica: &mut Replica,
|
||||
filter: Filter,
|
||||
debug: bool,
|
||||
) -> Result<(), crate::Error> {
|
||||
let working_set = replica.working_set()?;
|
||||
|
||||
for task in filtered_tasks(replica, &filter)? {
|
||||
let uuid = task.get_uuid();
|
||||
|
||||
let mut t = Table::new();
|
||||
t.set_format(table::format());
|
||||
if debug {
|
||||
t.set_titles(row![b->"key", b->"value"]);
|
||||
for (k, v) in task.get_taskmap().iter() {
|
||||
t.add_row(row![k, v]);
|
||||
}
|
||||
} else {
|
||||
t.add_row(row![b->"Uuid", uuid]);
|
||||
if let Some(i) = working_set.by_uuid(uuid) {
|
||||
t.add_row(row![b->"Id", i]);
|
||||
}
|
||||
t.add_row(row![b->"Description", task.get_description()]);
|
||||
t.add_row(row![b->"Status", task.get_status()]);
|
||||
t.add_row(row![b->"Active", task.is_active()]);
|
||||
let mut tags: Vec<_> = task.get_tags().map(|t| format!("+{}", t)).collect();
|
||||
if !tags.is_empty() {
|
||||
tags.sort();
|
||||
t.add_row(row![b->"Tags", tags.join(" ")]);
|
||||
}
|
||||
if let Some(wait) = task.get_wait() {
|
||||
t.add_row(row![b->"Wait", wait]);
|
||||
}
|
||||
let mut annotations: Vec<_> = task.get_annotations().collect();
|
||||
annotations.sort();
|
||||
for ann in annotations {
|
||||
t.add_row(row![b->"Annotation", format!("{}: {}", ann.entry, ann.description)]);
|
||||
}
|
||||
|
||||
let mut deps: Vec<_> = task.get_dependencies().collect();
|
||||
deps.sort();
|
||||
for dep in deps {
|
||||
let mut descr = None;
|
||||
if let Some(task) = replica.get_task(dep)? {
|
||||
if task.get_status() == Status::Pending {
|
||||
if let Some(i) = working_set.by_uuid(dep) {
|
||||
descr = Some(format!("{} - {}", i, task.get_description()))
|
||||
} else {
|
||||
descr = Some(format!("{} - {}", dep, task.get_description()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(descr) = descr {
|
||||
t.add_row(row![b->"Depends On", descr]);
|
||||
}
|
||||
}
|
||||
}
|
||||
t.print(w)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::argparse::{Condition, TaskId};
|
||||
use crate::invocation::test::*;
|
||||
|
||||
use taskchampion::Status;
|
||||
|
||||
#[test]
|
||||
fn test_info() {
|
||||
let mut w = test_writer();
|
||||
let mut replica = test_replica();
|
||||
replica.new_task(Status::Pending, s!("my task")).unwrap();
|
||||
|
||||
let filter = Filter {
|
||||
..Default::default()
|
||||
};
|
||||
let debug = false;
|
||||
execute(&mut w, &mut replica, filter, debug).unwrap();
|
||||
assert!(w.into_string().contains("my task"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deps() {
|
||||
let mut w = test_writer();
|
||||
let mut replica = test_replica();
|
||||
let t1 = replica.new_task(Status::Pending, s!("my task")).unwrap();
|
||||
let t2 = replica
|
||||
.new_task(Status::Pending, s!("dunno, depends"))
|
||||
.unwrap();
|
||||
let mut t2 = t2.into_mut(&mut replica);
|
||||
t2.add_dependency(t1.get_uuid()).unwrap();
|
||||
let t2 = t2.into_immut();
|
||||
|
||||
let filter = Filter {
|
||||
conditions: vec![Condition::IdList(vec![TaskId::Uuid(t2.get_uuid())])],
|
||||
};
|
||||
let debug = false;
|
||||
execute(&mut w, &mut replica, filter, debug).unwrap();
|
||||
let s = w.into_string();
|
||||
// length of whitespace between these two strings is not important
|
||||
assert!(s.contains("Depends On"));
|
||||
assert!(s.contains("1 - my task"));
|
||||
}
|
||||
}
|
14
rust/cli/src/invocation/cmd/mod.rs
Normal file
14
rust/cli/src/invocation/cmd/mod.rs
Normal file
|
@ -0,0 +1,14 @@
|
|||
//! Responsible for executing commands as parsed by [`crate::argparse`].
|
||||
|
||||
pub(crate) mod add;
|
||||
pub(crate) mod config;
|
||||
pub(crate) mod gc;
|
||||
pub(crate) mod help;
|
||||
pub(crate) mod import_tdb2;
|
||||
pub(crate) mod import_tw;
|
||||
pub(crate) mod info;
|
||||
pub(crate) mod modify;
|
||||
pub(crate) mod report;
|
||||
pub(crate) mod sync;
|
||||
pub(crate) mod undo;
|
||||
pub(crate) mod version;
|
106
rust/cli/src/invocation/cmd/modify.rs
Normal file
106
rust/cli/src/invocation/cmd/modify.rs
Normal file
|
@ -0,0 +1,106 @@
|
|||
use crate::argparse::Filter;
|
||||
use crate::invocation::util::{confirm, summarize_task};
|
||||
use crate::invocation::{apply_modification, filtered_tasks, ResolvedModification};
|
||||
use crate::settings::Settings;
|
||||
use taskchampion::Replica;
|
||||
use termcolor::WriteColor;
|
||||
|
||||
/// confirm modification of more than `modificationt_count_prompt` tasks, defaulting to 3
|
||||
fn check_modification<W: WriteColor>(
|
||||
w: &mut W,
|
||||
settings: &Settings,
|
||||
affected_tasks: usize,
|
||||
) -> Result<bool, crate::Error> {
|
||||
let setting = settings.modification_count_prompt.unwrap_or(3);
|
||||
if setting == 0 || affected_tasks <= setting as usize {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
let prompt = format!("Operation will modify {} tasks; continue?", affected_tasks,);
|
||||
if confirm(&prompt)? {
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
writeln!(w, "Cancelled")?;
|
||||
|
||||
// only show this help if the setting is not set
|
||||
if settings.modification_count_prompt.is_none() {
|
||||
writeln!(
|
||||
w,
|
||||
"Set the `modification_count_prompt` setting to avoid this prompt:"
|
||||
)?;
|
||||
writeln!(
|
||||
w,
|
||||
" ta config set modification_count_prompt {}",
|
||||
affected_tasks + 1
|
||||
)?;
|
||||
writeln!(w, "Set it to 0 to disable the prompt entirely")?;
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
pub(in crate::invocation) fn execute<W: WriteColor>(
|
||||
w: &mut W,
|
||||
replica: &mut Replica,
|
||||
settings: &Settings,
|
||||
filter: Filter,
|
||||
modification: ResolvedModification,
|
||||
) -> Result<(), crate::Error> {
|
||||
let tasks = filtered_tasks(replica, &filter)?;
|
||||
|
||||
if !check_modification(w, settings, tasks.size_hint().0)? {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for task in tasks {
|
||||
let mut task = task.into_mut(replica);
|
||||
|
||||
apply_modification(&mut task, &modification)?;
|
||||
|
||||
let task = task.into_immut();
|
||||
let summary = summarize_task(replica, &task)?;
|
||||
writeln!(w, "modified task {}", summary)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::argparse::{DescriptionMod, Modification};
|
||||
use crate::invocation::test::test_replica;
|
||||
use crate::invocation::test::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use taskchampion::Status;
|
||||
|
||||
#[test]
|
||||
fn test_modify() {
|
||||
let mut w = test_writer();
|
||||
let mut replica = test_replica();
|
||||
let settings = Settings::default();
|
||||
|
||||
let task = replica
|
||||
.new_task(Status::Pending, s!("old description"))
|
||||
.unwrap();
|
||||
|
||||
let filter = Filter {
|
||||
..Default::default()
|
||||
};
|
||||
let modification = ResolvedModification(Modification {
|
||||
description: DescriptionMod::Set(s!("new description")),
|
||||
..Default::default()
|
||||
});
|
||||
execute(&mut w, &mut replica, &settings, filter, modification).unwrap();
|
||||
|
||||
// check that the task appeared..
|
||||
let task = replica.get_task(task.get_uuid()).unwrap().unwrap();
|
||||
assert_eq!(task.get_description(), "new description");
|
||||
assert_eq!(task.get_status(), Status::Pending);
|
||||
|
||||
assert_eq!(
|
||||
w.into_string(),
|
||||
format!("modified task 1 - new description\n")
|
||||
);
|
||||
}
|
||||
}
|
1
rust/cli/src/invocation/cmd/pending.data
Normal file
1
rust/cli/src/invocation/cmd/pending.data
Normal file
|
@ -0,0 +1 @@
|
|||
[description:"snake 🐍" entry:"1641670385" modified:"1641670385" priority:"M" status:"pending" tag_reptile:"" uuid:"f19086c2-1f8d-4a6c-9b8d-f94901fb8e62"]
|
43
rust/cli/src/invocation/cmd/report.rs
Normal file
43
rust/cli/src/invocation/cmd/report.rs
Normal file
|
@ -0,0 +1,43 @@
|
|||
use crate::argparse::Filter;
|
||||
use crate::invocation::display_report;
|
||||
use crate::settings::Settings;
|
||||
use taskchampion::Replica;
|
||||
use termcolor::WriteColor;
|
||||
|
||||
pub(crate) fn execute<W: WriteColor>(
|
||||
w: &mut W,
|
||||
replica: &mut Replica,
|
||||
settings: &Settings,
|
||||
report_name: String,
|
||||
filter: Filter,
|
||||
) -> Result<(), crate::Error> {
|
||||
display_report(w, replica, settings, report_name, filter)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::argparse::Filter;
|
||||
use crate::invocation::test::*;
|
||||
|
||||
use taskchampion::Status;
|
||||
|
||||
#[test]
|
||||
fn test_report() {
|
||||
let mut w = test_writer();
|
||||
let mut replica = test_replica();
|
||||
replica.new_task(Status::Pending, s!("my task")).unwrap();
|
||||
|
||||
// The function being tested is only one line long, so this is sort of an integration test
|
||||
// for display_report.
|
||||
|
||||
let settings = Default::default();
|
||||
let report_name = "next".to_owned();
|
||||
let filter = Filter {
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
execute(&mut w, &mut replica, &settings, report_name, filter).unwrap();
|
||||
assert!(w.into_string().contains("my task"));
|
||||
}
|
||||
}
|
58
rust/cli/src/invocation/cmd/sync.rs
Normal file
58
rust/cli/src/invocation/cmd/sync.rs
Normal file
|
@ -0,0 +1,58 @@
|
|||
use crate::settings::Settings;
|
||||
use taskchampion::{server::Server, Error as TCError, Replica};
|
||||
use termcolor::WriteColor;
|
||||
|
||||
pub(crate) fn execute<W: WriteColor>(
|
||||
w: &mut W,
|
||||
replica: &mut Replica,
|
||||
settings: &Settings,
|
||||
server: &mut Box<dyn Server>,
|
||||
) -> Result<(), crate::Error> {
|
||||
match replica.sync(server, settings.avoid_snapshots) {
|
||||
Ok(()) => {
|
||||
writeln!(w, "sync complete.")?;
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => match e.downcast() {
|
||||
Ok(TCError::OutOfSync) => {
|
||||
writeln!(w, "This replica cannot be synchronized with the server.")?;
|
||||
writeln!(
|
||||
w,
|
||||
"It may be too old, or some other failure may have occurred."
|
||||
)?;
|
||||
writeln!(
|
||||
w,
|
||||
"To start fresh, remove the local task database and run `ta sync` again."
|
||||
)?;
|
||||
writeln!(
|
||||
w,
|
||||
"Note that doing so will lose any un-synchronized local changes."
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
Ok(e) => Err(e.into()),
|
||||
Err(e) => Err(e.into()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::invocation::test::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn test_add() {
|
||||
let mut w = test_writer();
|
||||
let mut replica = test_replica();
|
||||
let server_dir = TempDir::new().unwrap();
|
||||
let mut server = test_server(&server_dir);
|
||||
let settings = Settings::default();
|
||||
|
||||
// Note that the details of the actual sync are tested thoroughly in the taskchampion crate
|
||||
execute(&mut w, &mut replica, &settings, &mut server).unwrap();
|
||||
assert_eq!(&w.into_string(), "sync complete.\n")
|
||||
}
|
||||
}
|
28
rust/cli/src/invocation/cmd/undo.rs
Normal file
28
rust/cli/src/invocation/cmd/undo.rs
Normal file
|
@ -0,0 +1,28 @@
|
|||
use taskchampion::Replica;
|
||||
use termcolor::WriteColor;
|
||||
|
||||
pub(crate) fn execute<W: WriteColor>(w: &mut W, replica: &mut Replica) -> Result<(), crate::Error> {
|
||||
if replica.undo()? {
|
||||
writeln!(w, "Undo successful.")?;
|
||||
} else {
|
||||
writeln!(w, "Nothing to undo.")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::invocation::test::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_undo() {
|
||||
let mut w = test_writer();
|
||||
let mut replica = test_replica();
|
||||
|
||||
// Note that the details of the actual undo operation are tested thoroughly in the taskchampion crate
|
||||
execute(&mut w, &mut replica).unwrap();
|
||||
assert_eq!(&w.into_string(), "Nothing to undo.\n")
|
||||
}
|
||||
}
|
32
rust/cli/src/invocation/cmd/version.rs
Normal file
32
rust/cli/src/invocation/cmd/version.rs
Normal file
|
@ -0,0 +1,32 @@
|
|||
use crate::built_info;
|
||||
use termcolor::{ColorSpec, WriteColor};
|
||||
|
||||
pub(crate) fn execute<W: WriteColor>(w: &mut W) -> Result<(), crate::Error> {
|
||||
write!(w, "TaskChampion ")?;
|
||||
w.set_color(ColorSpec::new().set_bold(true))?;
|
||||
write!(w, "{}", built_info::PKG_VERSION)?;
|
||||
w.reset()?;
|
||||
|
||||
if let (Some(version), Some(dirty)) = (built_info::GIT_VERSION, built_info::GIT_DIRTY) {
|
||||
if dirty {
|
||||
write!(w, " (git version: {} with un-committed changes)", version)?;
|
||||
} else {
|
||||
write!(w, " (git version: {})", version)?;
|
||||
};
|
||||
}
|
||||
writeln!(w)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::invocation::test::*;
|
||||
|
||||
#[test]
|
||||
fn test_version() {
|
||||
let mut w = test_writer();
|
||||
execute(&mut w).unwrap();
|
||||
assert!(w.into_string().starts_with("TaskChampion "));
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue