Improve config options

This commit is contained in:
Daniel Flanagan 2024-04-17 10:26:15 -05:00
parent 1b70e7c9bd
commit c08929c765
8 changed files with 109 additions and 29 deletions

1
Cargo.lock generated
View file

@ -1681,6 +1681,7 @@ dependencies = [
"clap_derive", "clap_derive",
"color-eyre", "color-eyre",
"crossterm", "crossterm",
"futures",
"ratatui", "ratatui",
"redact", "redact",
"regex", "regex",

View file

@ -11,6 +11,7 @@ clap = { version = "4.5.4", features = ["derive"] }
clap_derive = "4.5.4" clap_derive = "4.5.4"
color-eyre = "0.6.3" color-eyre = "0.6.3"
crossterm = "0.27.0" crossterm = "0.27.0"
futures = "0.3.30"
ratatui = "0.26.2" ratatui = "0.26.2"
redact = "0.1.9" redact = "0.1.9"
regex = "1.10.3" regex = "1.10.3"

View file

@ -10,9 +10,15 @@ pub struct Cli {
#[command(subcommand)] #[command(subcommand)]
pub command: Commands, pub command: Commands,
/// Tell taskr which directory to write log files to /// The directory to write log files
#[arg(long, default_value = None)] #[arg(long, default_value = None)]
pub logs_directory: Option<String>, pub logs_directory: Option<String>,
/// 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<String>,
} }
#[derive(Subcommand)] #[derive(Subcommand)]
@ -22,6 +28,12 @@ pub enum Commands {
/// Lists tasks with options for syncing /// Lists tasks with options for syncing
List(ListArgs), List(ListArgs),
/// Sync local tasks with GitLab and Jira
Sync,
/// Interact with the associated tasks' Jira issues
Jira(JiraArgs),
} }
#[derive(Args)] #[derive(Args)]
@ -33,14 +45,27 @@ pub struct ListArgs {
pub sync: bool, pub sync: bool,
} }
#[derive(Args, Debug)]
pub struct JiraArgs {
#[arg(short, long)]
pub key: String,
}
impl Cli { impl Cli {
pub fn new() -> Self { pub fn new() -> Self {
Self::parse() 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<()> { 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 { if args.sync {
eprintln!("Syncing..."); eprintln!("Syncing...");
tasks.sync().await?; tasks.sync().await?;
@ -58,6 +83,13 @@ pub async fn list_tasks(args: &ListArgs) -> Result<()> {
Ok(()) 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] #[test]
fn verify_cli() { fn verify_cli() {
use clap::CommandFactory; use clap::CommandFactory;

View file

@ -18,10 +18,19 @@ use cli::{Cli, Commands};
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> Result<()> {
let cli = Cli::new(); 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..."); trace!("Starting...");
match cli.command { info!("Starting...");
let result = match cli.command {
Commands::Sync => cli::sync().await,
Commands::Ui(_args) => tui::run().await, Commands::Ui(_args) => tui::run().await,
Commands::List(args) => cli::list_tasks(&args).await, Commands::List(args) => cli::list_tasks(&args).await,
} Commands::Jira(args) => cli::jira(&args).await,
};
result
} }

View file

@ -1,32 +1,54 @@
use crate::prelude::*; use crate::prelude::*;
use std::path::PathBuf; use std::path::Path;
use tracing_appender::non_blocking::WorkerGuard;
use tracing_subscriber::{filter::LevelFilter, EnvFilter}; use tracing_subscriber::{filter::LevelFilter, EnvFilter};
pub fn setup_logging<T: Into<PathBuf>>(logs_directory: Option<T>) -> Result<()> { pub fn setup_logging<T, S>(
let default_logs_directory = || -> Result<PathBuf> { logs_directory: Option<T>,
Ok(xdg::BaseDirectories::new()?.create_cache_directory("taskr/logs")?) env_filter_directive: Option<S>,
}; ) -> Result<WorkerGuard>
let logs_directory: PathBuf = match logs_directory { where
Some(p) => p.into(), T: AsRef<Path>,
None => default_logs_directory()?, S: AsRef<str>,
}; {
if !logs_directory.exists() { let mut default_logs_dir = Default::default();
std::fs::create_dir_all(&logs_directory)?; 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 file_appender = tracing_appender::rolling::hourly(logs_dir, "log");
let (non_blocking, _guard) = tracing_appender::non_blocking(file_appender); 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"); color_eyre::install().expect("Failed to install color_eyre");
let filter = EnvFilter::builder() let filter = EnvFilter::builder()
.with_default_directive(LevelFilter::TRACE.into()) .with_default_directive(LevelFilter::TRACE.into())
.parse_lossy("info,taskr=trace"); .parse_lossy(env_filter);
eprintln!("EnvFilter Directive: {}", filter);
tracing_subscriber::fmt() tracing_subscriber::fmt()
.with_writer(non_blocking) .with_writer(non_blocking)
.with_env_filter(filter) .with_env_filter(filter)
.init(); .init();
Ok(()) Ok(guard)
} }

View file

@ -1,4 +1,7 @@
#![allow(unused_imports)] #![allow(unused_imports)]
pub use crate::result::Result; 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}; pub use tracing::{debug, error, info, trace, warn};

View file

@ -5,13 +5,13 @@ use sled::IVec;
use crate::jira::Issue; use crate::jira::Issue;
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
pub struct MergeRequestRef { pub struct MergeRequestRef {
pub url: String, pub url: String,
pub state: String, pub state: String,
} }
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq)] #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
pub struct Task { pub struct Task {
pub jira_key: String, pub jira_key: String,
pub description: String, pub description: String,

View file

@ -1,4 +1,5 @@
use regex::Regex; use crate::prelude::*;
use std::{ use std::{
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
env, env,
@ -21,11 +22,7 @@ pub struct Tasks {
db: sled::Db, db: sled::Db,
} }
#[derive(Debug)] const MY_DONE_STATUS_CATEGORY_KEY: &str = "done";
pub struct Desyncs {
issues: Vec<crate::jira::Issue>,
tasks: Vec<Task>,
}
impl Tasks { impl Tasks {
pub fn try_new() -> Result<Self> { pub fn try_new() -> Result<Self> {
@ -135,6 +132,14 @@ impl Tasks {
async fn fix_dangling_task(&self, task: &Task) -> Result<()> { async fn fix_dangling_task(&self, task: &Task) -> Result<()> {
// check if closed, if it is, delete the task // check if closed, if it is, delete the task
let issue = self.jira.issue(&task.jira_key).await?; 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(()) Ok(())
} }
@ -149,7 +154,7 @@ impl Tasks {
HashSet::from_iter(issues.keys().map(|s| s.to_owned())); HashSet::from_iter(issues.keys().map(|s| s.to_owned()));
// keys which have a task but not an issue // keys which have a task but not an issue
let _dangling_keys: HashSet<String, RandomState> = HashSet::from_iter( let dangling_keys: HashSet<String, RandomState> = HashSet::from_iter(
task_keys task_keys
.difference(&issue_keys) .difference(&issue_keys)
.into_iter() .into_iter()
@ -166,6 +171,13 @@ impl Tasks {
tasks.insert(task.jira_key.clone(), task); 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? // TODO: blocking? maybe should be async?
// while let Ok(_res) = rx.recv() { // while let Ok(_res) = rx.recv() {
// awaiting all the tasks in the joinset // awaiting all the tasks in the joinset