mirror of
https://github.com/GothenburgBitFactory/taskwarrior.git
synced 2025-07-07 20:06:36 +02:00
ta import-tdb2
This commit is contained in:
parent
162a9eae95
commit
69d052603d
8 changed files with 200 additions and 4 deletions
|
@ -60,6 +60,9 @@ pub(crate) enum Subcommand {
|
||||||
Gc,
|
Gc,
|
||||||
Sync,
|
Sync,
|
||||||
Import,
|
Import,
|
||||||
|
ImportTDB2 {
|
||||||
|
path: String,
|
||||||
|
},
|
||||||
Undo,
|
Undo,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -75,6 +78,7 @@ impl Subcommand {
|
||||||
Gc::parse,
|
Gc::parse,
|
||||||
Sync::parse,
|
Sync::parse,
|
||||||
Import::parse,
|
Import::parse,
|
||||||
|
ImportTDB2::parse,
|
||||||
Undo::parse,
|
Undo::parse,
|
||||||
// This must come last since it accepts arbitrary report names
|
// This must come last since it accepts arbitrary report names
|
||||||
Report::parse,
|
Report::parse,
|
||||||
|
@ -91,6 +95,7 @@ impl Subcommand {
|
||||||
Gc::get_usage(u);
|
Gc::get_usage(u);
|
||||||
Sync::get_usage(u);
|
Sync::get_usage(u);
|
||||||
Import::get_usage(u);
|
Import::get_usage(u);
|
||||||
|
ImportTDB2::get_usage(u);
|
||||||
Undo::get_usage(u);
|
Undo::get_usage(u);
|
||||||
Report::get_usage(u);
|
Report::get_usage(u);
|
||||||
}
|
}
|
||||||
|
@ -457,6 +462,37 @@ impl Import {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct ImportTDB2;
|
||||||
|
|
||||||
|
impl ImportTDB2 {
|
||||||
|
fn parse(input: ArgList) -> IResult<ArgList, Subcommand> {
|
||||||
|
fn to_subcommand(input: (&str, &str)) -> Result<Subcommand, ()> {
|
||||||
|
Ok(Subcommand::ImportTDB2 {
|
||||||
|
path: input.1.into(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
map_res(
|
||||||
|
pair(arg_matching(literal("import-tdb2")), arg_matching(any)),
|
||||||
|
to_subcommand,
|
||||||
|
)(input)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_usage(u: &mut usage::Usage) {
|
||||||
|
u.subcommands.push(usage::Subcommand {
|
||||||
|
name: "import-tdb2",
|
||||||
|
syntax: "import-tdb2 <directory>",
|
||||||
|
summary: "Import tasks from the TaskWarrior data directory",
|
||||||
|
description: "
|
||||||
|
Import tasks into this replica from a TaskWarrior data directory. If tasks in the
|
||||||
|
import already exist, they are 'merged'. This mode of import supports UDAs better
|
||||||
|
than the `import` subcommand, but requires access to the \"raw\" TaskWarrior data.
|
||||||
|
|
||||||
|
This command supports task directories written by TaskWarrior-2.6.1 or later.
|
||||||
|
",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct Undo;
|
struct Undo;
|
||||||
|
|
||||||
impl Undo {
|
impl Undo {
|
||||||
|
|
1
cli/src/invocation/cmd/completed.data
Normal file
1
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"]
|
|
@ -4,10 +4,13 @@ use serde::{self, Deserialize, Deserializer};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use taskchampion::{Replica, Uuid};
|
use taskchampion::{Replica, Uuid};
|
||||||
use termcolor::WriteColor;
|
use termcolor::{Color, ColorSpec, WriteColor};
|
||||||
|
|
||||||
pub(crate) fn execute<W: WriteColor>(w: &mut W, replica: &mut Replica) -> Result<(), crate::Error> {
|
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.")?;
|
writeln!(w, "Importing tasks from stdin.")?;
|
||||||
|
w.reset()?;
|
||||||
|
|
||||||
let mut tasks: Vec<HashMap<String, Value>> =
|
let mut tasks: Vec<HashMap<String, Value>> =
|
||||||
serde_json::from_reader(std::io::stdin()).map_err(|_| anyhow!("Invalid JSON"))?;
|
serde_json::from_reader(std::io::stdin()).map_err(|_| anyhow!("Invalid JSON"))?;
|
||||||
|
|
||||||
|
@ -15,7 +18,10 @@ pub(crate) fn execute<W: WriteColor>(w: &mut W, replica: &mut Replica) -> Result
|
||||||
import_task(w, replica, task_json)?;
|
import_task(w, replica, task_json)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
w.set_color(ColorSpec::new().set_bold(true))?;
|
||||||
writeln!(w, "{} tasks imported.", tasks.len())?;
|
writeln!(w, "{} tasks imported.", tasks.len())?;
|
||||||
|
w.reset()?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -130,10 +136,12 @@ fn import_task<W: WriteColor>(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
w.set_color(ColorSpec::new().set_fg(Some(Color::Yellow)))?;
|
||||||
|
write!(w, "{}", uuid)?;
|
||||||
|
w.reset()?;
|
||||||
writeln!(
|
writeln!(
|
||||||
w,
|
w,
|
||||||
"{} {}",
|
" {}",
|
||||||
uuid,
|
|
||||||
description.unwrap_or_else(|| "(no description)".into())
|
description.unwrap_or_else(|| "(no description)".into())
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
|
142
cli/src/invocation/cmd/import_tdb2.rs
Normal file
142
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.create_task(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 chrono::{TimeZone, Utc};
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
use std::convert::TryInto;
|
||||||
|
use taskchampion::{Priority, 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(), 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(), Priority::M);
|
||||||
|
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(())
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ pub(crate) mod config;
|
||||||
pub(crate) mod gc;
|
pub(crate) mod gc;
|
||||||
pub(crate) mod help;
|
pub(crate) mod help;
|
||||||
pub(crate) mod import;
|
pub(crate) mod import;
|
||||||
|
pub(crate) mod import_tdb2;
|
||||||
pub(crate) mod info;
|
pub(crate) mod info;
|
||||||
pub(crate) mod modify;
|
pub(crate) mod modify;
|
||||||
pub(crate) mod report;
|
pub(crate) mod report;
|
||||||
|
|
1
cli/src/invocation/cmd/pending.data
Normal file
1
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"]
|
|
@ -97,6 +97,13 @@ pub(crate) fn invoke(command: Command, settings: Settings) -> Result<(), crate::
|
||||||
return cmd::import::execute(&mut w, &mut replica);
|
return cmd::import::execute(&mut w, &mut replica);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Command {
|
||||||
|
subcommand: Subcommand::ImportTDB2 { path },
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
return cmd::import_tdb2::execute(&mut w, &mut replica, path.as_ref());
|
||||||
|
}
|
||||||
|
|
||||||
Command {
|
Command {
|
||||||
subcommand: Subcommand::Undo,
|
subcommand: Subcommand::Undo,
|
||||||
..
|
..
|
||||||
|
|
|
@ -22,7 +22,7 @@ pub(crate) struct Attr {
|
||||||
|
|
||||||
impl File {
|
impl File {
|
||||||
pub(crate) fn from_str(input: &str) -> Result<File, ()> {
|
pub(crate) fn from_str(input: &str) -> Result<File, ()> {
|
||||||
Ok(File::parse(input).map(|(_, res)| res).map_err(|_| ())?)
|
File::parse(input).map(|(_, res)| res).map_err(|_| ())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse(input: &str) -> IResult<&str, File> {
|
fn parse(input: &str) -> IResult<&str, File> {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue