From 6e6e86da9836739e895505a77c44af2ff6bd7cf5 Mon Sep 17 00:00:00 2001 From: Daniel Flanagan Date: Wed, 20 Mar 2024 11:35:12 -0500 Subject: [PATCH] WIP --- Cargo.lock | 14 +++++ Cargo.toml | 2 + src/jira.rs | 43 +++++++++++++++ src/main.rs | 12 +++-- src/task.rs | 76 ++++++++++++++++++++++++++ src/tasks.rs | 150 ++++++++++++++++++++++++++++++++++----------------- 6 files changed, 245 insertions(+), 52 deletions(-) create mode 100644 src/task.rs diff --git a/Cargo.lock b/Cargo.lock index 2c2d8f3..9890c43 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -135,10 +135,22 @@ checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" dependencies = [ "android-tzdata", "iana-time-zone", + "js-sys", "num-traits", + "serde", + "wasm-bindgen", "windows-targets 0.52.4", ] +[[package]] +name = "chrono-humanize" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799627e6b4d27827a814e837b9d8a504832086081806d45b1afa34dc982b023b" +dependencies = [ + "chrono", +] + [[package]] name = "color-eyre" version = "0.6.3" @@ -1324,6 +1336,8 @@ name = "tasks" version = "0.1.0" dependencies = [ "anyhow", + "chrono", + "chrono-humanize", "color-eyre", "regex", "reqwest", diff --git a/Cargo.toml b/Cargo.toml index b5ee3a8..2b0d62e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,8 @@ edition = "2021" [dependencies] anyhow = "1.0.81" +chrono = { version = "0.4.35", features = ["serde"] } +chrono-humanize = "0.2.3" color-eyre = "0.6.3" regex = "1.10.3" reqwest = { version = "0.11.26", features = ["json", "socks"] } diff --git a/src/jira.rs b/src/jira.rs index b8ad29d..6b329ec 100644 --- a/src/jira.rs +++ b/src/jira.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use crate::{ client::{Client, ResourceRequest}, result::Result, @@ -27,11 +29,47 @@ pub struct User { pub locale: String, } +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Component { + pub name: String, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Priority { + pub id: String, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct IssueStatusCategory { + pub key: String, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct IssueStatus { + pub name: String, + pub status_category: IssueStatusCategory, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct IssueFields { + pub components: Option>, + pub labels: Vec, + pub summary: String, + pub status: IssueStatus, + pub priority: Priority, +} + #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct Issue { pub id: String, pub key: String, + pub fields: IssueFields, } #[derive(Deserialize, Debug)] @@ -98,6 +136,11 @@ impl Jira { Ok(issues) } + // TODO: move this somewhere nicer? + pub fn by_key(issues: Vec) -> Result> { + Ok(issues.into_iter().map(|i| (i.key.to_owned(), i)).collect()) + } + pub async fn assigned_open_issues(&self) -> Result> { let me = self.me().await?; let jql = format!("assignee = {0} and statusCategory != Done", me.account_id); diff --git a/src/main.rs b/src/main.rs index 08313ed..7b8481e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,7 @@ mod config; mod gitlab; mod jira; mod result; +mod task; mod tasks; use crate::result::Result; @@ -33,10 +34,13 @@ async fn main() -> Result<()> { async fn run() -> Result<()> { // print!("{ANSI_CLEAR}"); + // let gitlab_user = tasks.gitlab.me().await?; + // info!("{gitlab_user:#?}"); + // let jira_user = tasks.jira.me().await?; + // info!("{:?}", tasks.sync().await?); let tasks = Tasks::try_new()?; - let gitlab_user = tasks.gitlab.me().await?; - info!("{gitlab_user:#?}"); - let jira_user = tasks.jira.me().await?; - info!("{:?}", tasks.desyncs().await?); + for t in tasks.all()?.values() { + info!("{}", t); + } Ok(()) } diff --git a/src/task.rs b/src/task.rs new file mode 100644 index 0000000..c315e84 --- /dev/null +++ b/src/task.rs @@ -0,0 +1,76 @@ +use std::{collections::HashSet, fmt::Display}; + +use serde::{Deserialize, Serialize}; +use sled::IVec; + +use crate::jira::Issue; + +#[derive(Serialize, Deserialize, Debug)] +pub struct MergeRequestRef { + pub url: String, + pub state: String, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Task { + pub jira_key: String, + pub description: String, + pub merge_requests: Vec, + pub jira_priority: i64, + pub local_priority: i64, + pub status: String, + pub tags: HashSet, +} + +impl Display for Task { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let Self { + description, + jira_key, + merge_requests, + jira_priority, + local_priority, + status, + tags, + } = self; + f.write_fmt(format_args!("{jira_key}: {status} {description}",)) + } +} + +impl TryFrom for Task { + type Error = anyhow::Error; + + fn try_from(value: IVec) -> std::prelude::v1::Result { + serde_json::from_slice(&value).map_err(|e| e.into()) + } +} + +impl TryInto for &Task { + type Error = anyhow::Error; + + fn try_into(self) -> std::prelude::v1::Result { + Ok(IVec::from(serde_json::to_vec(self)?)) + } +} + +impl TryFrom<&Issue> for Task { + type Error = anyhow::Error; + + fn try_from(value: &Issue) -> std::prelude::v1::Result { + let mut tags = HashSet::from_iter(value.fields.labels.iter().map(|s| s.to_owned())); + if let Some(cs) = &value.fields.components { + for c in cs.iter().map(|c| c.name.to_owned()) { + tags.insert(c); + } + } + Ok(Self { + jira_key: value.key.to_owned(), + description: value.fields.summary.to_owned(), + merge_requests: vec![], + jira_priority: value.fields.priority.id.parse()?, + local_priority: value.fields.priority.id.parse()?, + status: value.fields.status.status_category.key.to_owned(), + tags, + }) + } +} diff --git a/src/tasks.rs b/src/tasks.rs index ffcb93b..f141dfe 100644 --- a/src/tasks.rs +++ b/src/tasks.rs @@ -1,28 +1,19 @@ -use std::{sync::OnceLock, collections::HashSet, env, process::Command}; use regex::Regex; +use std::{ + collections::{HashMap, HashSet}, + env, + hash::RandomState, + process::Command, + sync::OnceLock, +}; +use tracing::info; + +use crate::task::Task; -use serde::{Deserialize, Serialize}; use sled::IVec; use crate::{gitlab::GitLab, jira::Jira, result::Result}; -#[derive(Serialize, Deserialize, Debug)] -pub struct MergeRequestRef { - url: String, - state: String, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct Task { - jira_key: String, - description: String, - merge_requests: Vec, - jira_priority: i64, - local_priority: i64, - status: String, - tags: HashSet, -} - pub struct Tasks { pub gitlab: GitLab, pub jira: Jira, @@ -30,22 +21,6 @@ pub struct Tasks { db: sled::Db, } -impl TryFrom for Task { - type Error = anyhow::Error; - - fn try_from(value: IVec) -> std::prelude::v1::Result { - serde_json::from_slice(&value).map_err(|e| e.into()) - } -} - -impl TryInto for &Task { - type Error = anyhow::Error; - - fn try_into(self) -> std::prelude::v1::Result { - Ok(IVec::from(serde_json::to_vec(self)?)) - } -} - #[derive(Debug)] pub struct Desyncs { issues: Vec, @@ -77,12 +52,19 @@ impl Tasks { Ok(Self { gitlab, jira, db }) } - pub fn all(&self) -> Result> { - self.db - .open_tree("tasks")? - .scan_prefix(&[]) - .map(|t| Task::try_from(t?.1)) - .collect::>>() + pub fn all(&self) -> Result> { + let mut result = HashMap::new(); + let scan = self.db.open_tree("tasks")?.scan_prefix(&[]); + for entry in scan { + match entry { + Ok((_key, val)) => { + let task = Task::try_from(val)?; + result.insert(task.jira_key.to_owned(), task); + } + Err(e) => return Err(e.into()), + } + } + Ok(result) } pub fn get(&self, key: &str) -> Result> { @@ -107,7 +89,7 @@ impl Tasks { } pub fn purge_all(&self) -> Result<()> { - for t in self.all()? { + for (_, t) in self.all()? { self.delete(&t)?; } self.db.flush()?; @@ -115,18 +97,90 @@ impl Tasks { } pub fn cleanup(&self, task: &mut Task) -> Result<()> { - static RE: OnceLock = OnceLock::new(|| Regex::new(r"^\s*\:[a-zA-Z0-9_-]+\:\s*").unwrap()); + static RE: OnceLock = OnceLock::new(); + let regex = RE.get_or_init(|| Regex::new(r"^\s*\:[a-zA-Z0-9_-]+\:\s*").unwrap()); let mut changed = false; - t.description.match + if let Some(m) = regex.find(&task.description) { + task.description = task.description[..m.range().end].to_owned(); + changed = true; + } + if changed { + self.save(task)?; + } + Ok(()) + } + + pub fn cleanup_all(&self) -> Result<()> { + for (_, mut t) in self.all()? { + self.cleanup(&mut t)?; + } + self.db.flush()?; + Ok(()) + } + + /// for a task that has no associated open and appropriately assigned jira issue + async fn fix_dangling_task(&self, task: &Task) -> Result<()> { Ok(()) } /// fetches jira issues and compares to known local tasks /// for use when sync'ing local tasks to remote state (jira, gitlab) - pub async fn desyncs(&self) -> Result { - let issues = self.jira.assigned_open_issues().await?; - let tasks = self.all()?; + pub async fn sync(&self) -> Result<()> { + let mut tasks = self.all()?; + let issues = crate::jira::Jira::by_key(self.jira.assigned_open_issues().await?)?; + let task_keys: HashSet = + HashSet::from_iter(tasks.keys().map(|s| s.to_owned())); + let issue_keys: HashSet = + HashSet::from_iter(issues.keys().map(|s| s.to_owned())); - Ok(Desyncs { issues, tasks }) + // keys which have a task but not an issue + let _dangling_keys: HashSet = HashSet::from_iter( + task_keys + .difference(&issue_keys) + .into_iter() + .map(|s| s.to_owned()), + ); + + for key in issue_keys.difference(&task_keys) { + let issue = issues.get(key).unwrap(); + let mut task: Task = issue.try_into()?; + self.cleanup(&mut task); + self.save(&task); + tasks.insert(task.jira_key.clone(), task); + } + + info!("Creating new tasks from issues without a task"); + + // TODO: blocking? maybe should be async? + // while let Ok(_res) = rx.recv() { + // awaiting all the tasks in the joinset + // } + + // fix_missing_task( + + // let mut futures = Vec::with_capacity((results.total / results.max_results) + 1); + + // for i in 1..futures.capacity() { + // let start_at = i * results.max_results; + // debug!("Fetching issues for jql {} (page {})", jql, i); + // futures.push(spawn( + // self.client + // .build(Method::GET, "/rest/api/3/search")? + // .query(&[("jql", jql), ("startAt", &start_at.to_string())]) + // .res::(), + // )); + // } + + info!("Checking Jira issues for updates for existing tasks"); + info!("Checking missing Jira issues for updates for existing tasks"); + + // save last sync date + let now = chrono::Local::now(); + + self.db + .open_tree("task:meta")? + .insert(b"last_sync_date", serde_json::to_vec(&now)?); + + Ok(()) } }