diff --git a/Cargo.lock b/Cargo.lock index bc971e4..e19007b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1681,6 +1681,7 @@ dependencies = [ "clap_derive", "color-eyre", "crossterm", + "futures", "ratatui", "redact", "regex", diff --git a/Cargo.toml b/Cargo.toml index 3da2c16..0df5e6d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ clap = { version = "4.5.4", features = ["derive"] } clap_derive = "4.5.4" color-eyre = "0.6.3" crossterm = "0.27.0" +futures = "0.3.30" ratatui = "0.26.2" redact = "0.1.9" regex = "1.10.3" diff --git a/src/cli.rs b/src/cli.rs index c70b1d3..4648058 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -10,9 +10,15 @@ pub struct Cli { #[command(subcommand)] pub command: Commands, - /// Tell taskr which directory to write log files to + /// The directory to write log files #[arg(long, default_value = None)] pub logs_directory: Option, + + /// Define which tracing values are logged + /// See https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html + /// for details + #[arg(long, default_value = None)] + pub tracing_env_filter: Option, } #[derive(Subcommand)] @@ -22,6 +28,12 @@ pub enum Commands { /// Lists tasks with options for syncing List(ListArgs), + + /// Sync local tasks with GitLab and Jira + Sync, + + /// Interact with the associated tasks' Jira issues + Jira(JiraArgs), } #[derive(Args)] @@ -33,14 +45,27 @@ pub struct ListArgs { pub sync: bool, } +#[derive(Args, Debug)] +pub struct JiraArgs { + #[arg(short, long)] + pub key: String, +} + impl Cli { pub fn new() -> Self { Self::parse() } } +pub async fn sync() -> Result<()> { + eprintln!("Syncing..."); + Arc::new(crate::tasks::Tasks::try_new()?).sync().await?; + eprintln!("Done!"); + Ok(()) +} + pub async fn list_tasks(args: &ListArgs) -> Result<()> { - let tasks = crate::tasks::Tasks::try_new()?; + let tasks = Arc::new(crate::tasks::Tasks::try_new()?); if args.sync { eprintln!("Syncing..."); tasks.sync().await?; @@ -58,6 +83,13 @@ pub async fn list_tasks(args: &ListArgs) -> Result<()> { Ok(()) } +pub async fn jira(args: &JiraArgs) -> Result<()> { + let tasks = crate::tasks::Tasks::try_new()?; + let issue = tasks.jira.issue(&args.key).await?; + println!("{issue:?}"); + Ok(()) +} + #[test] fn verify_cli() { use clap::CommandFactory; diff --git a/src/main.rs b/src/main.rs index 0a7af34..7a1c173 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,10 +18,19 @@ use cli::{Cli, Commands}; #[tokio::main] async fn main() -> Result<()> { let cli = Cli::new(); - observe::setup_logging(cli.logs_directory)?; + + // this guard causes logs to be flushed when dropped + // https://docs.rs/tracing-appender/latest/tracing_appender/non_blocking/struct.WorkerGuard.html + let _log_guard = observe::setup_logging(cli.logs_directory, cli.tracing_env_filter)?; + + println!("Starting..."); trace!("Starting..."); - match cli.command { + info!("Starting..."); + let result = match cli.command { + Commands::Sync => cli::sync().await, Commands::Ui(_args) => tui::run().await, Commands::List(args) => cli::list_tasks(&args).await, - } + Commands::Jira(args) => cli::jira(&args).await, + }; + result } diff --git a/src/observe.rs b/src/observe.rs index bac7c10..91ced19 100644 --- a/src/observe.rs +++ b/src/observe.rs @@ -1,32 +1,54 @@ use crate::prelude::*; -use std::path::PathBuf; +use std::path::Path; +use tracing_appender::non_blocking::WorkerGuard; use tracing_subscriber::{filter::LevelFilter, EnvFilter}; -pub fn setup_logging>(logs_directory: Option) -> Result<()> { - let default_logs_directory = || -> Result { - Ok(xdg::BaseDirectories::new()?.create_cache_directory("taskr/logs")?) - }; - let logs_directory: PathBuf = match logs_directory { - Some(p) => p.into(), - None => default_logs_directory()?, - }; - if !logs_directory.exists() { - std::fs::create_dir_all(&logs_directory)?; +pub fn setup_logging( + logs_directory: Option, + env_filter_directive: Option, +) -> Result +where + T: AsRef, + S: AsRef, +{ + let mut default_logs_dir = Default::default(); + let logs_dir = logs_directory + .as_ref() + .map(T::as_ref) + .map(Result::Ok) + .unwrap_or_else(|| { + // really want to avoid calling this so it's put in here to force it to be lazy + // also have to shove it in a function-scoped variable so the + // reference we create in this closure has the same lifetime as the function + default_logs_dir = xdg::BaseDirectories::new()?.create_cache_directory("taskr/logs")?; + Ok(default_logs_dir.as_ref()) + })?; + + eprintln!("Logs Directory: {}", logs_dir.display()); + if !logs_dir.exists() { + std::fs::create_dir_all(&logs_dir)?; } - let file_appender = tracing_appender::rolling::hourly(logs_directory, "log"); - let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender); + let file_appender = tracing_appender::rolling::hourly(logs_dir, "log"); + let (non_blocking, guard) = tracing_appender::non_blocking(file_appender); + + let env_filter = env_filter_directive + .as_ref() + .map(S::as_ref) + .unwrap_or("taskr=trace,info"); color_eyre::install().expect("Failed to install color_eyre"); let filter = EnvFilter::builder() .with_default_directive(LevelFilter::TRACE.into()) - .parse_lossy("info,taskr=trace"); + .parse_lossy(env_filter); + + eprintln!("EnvFilter Directive: {}", filter); tracing_subscriber::fmt() .with_writer(non_blocking) .with_env_filter(filter) .init(); - Ok(()) + Ok(guard) } diff --git a/src/prelude.rs b/src/prelude.rs index f51d816..6cef2d2 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -1,4 +1,7 @@ #![allow(unused_imports)] pub use crate::result::Result; +pub use regex::Regex; +pub use std::sync::Arc; +pub use tokio::sync::Mutex; pub use tracing::{debug, error, info, trace, warn}; diff --git a/src/task.rs b/src/task.rs index 2b0547d..18658da 100644 --- a/src/task.rs +++ b/src/task.rs @@ -5,13 +5,13 @@ use sled::IVec; use crate::jira::Issue; -#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] pub struct MergeRequestRef { pub url: String, pub state: String, } -#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] pub struct Task { pub jira_key: String, pub description: String, diff --git a/src/tasks.rs b/src/tasks.rs index 24c69d0..913606b 100644 --- a/src/tasks.rs +++ b/src/tasks.rs @@ -1,4 +1,5 @@ -use regex::Regex; +use crate::prelude::*; + use std::{ collections::{HashMap, HashSet}, env, @@ -21,11 +22,7 @@ pub struct Tasks { db: sled::Db, } -#[derive(Debug)] -pub struct Desyncs { - issues: Vec, - tasks: Vec, -} +const MY_DONE_STATUS_CATEGORY_KEY: &str = "done"; impl Tasks { pub fn try_new() -> Result { @@ -135,6 +132,14 @@ impl Tasks { async fn fix_dangling_task(&self, task: &Task) -> Result<()> { // check if closed, if it is, delete the task let issue = self.jira.issue(&task.jira_key).await?; + if issue.fields.status.status_category.key == MY_DONE_STATUS_CATEGORY_KEY { + info!( + "Deleting task {} due to being marked {} in Jira", + task.jira_key, MY_DONE_STATUS_CATEGORY_KEY + ); + // remote issue is done, so delete the task + self.delete(task)?; + } Ok(()) } @@ -149,7 +154,7 @@ impl Tasks { HashSet::from_iter(issues.keys().map(|s| s.to_owned())); // keys which have a task but not an issue - let _dangling_keys: HashSet = HashSet::from_iter( + let dangling_keys: HashSet = HashSet::from_iter( task_keys .difference(&issue_keys) .into_iter() @@ -166,6 +171,13 @@ impl Tasks { tasks.insert(task.jira_key.clone(), task); } + futures::future::try_join_all( + dangling_keys + .iter() + .map(|t| self.fix_dangling_task(tasks.get(t).unwrap())), + ) + .await?; + // TODO: blocking? maybe should be async? // while let Ok(_res) = rx.recv() { // awaiting all the tasks in the joinset