This commit is contained in:
Daniel Flanagan 2024-03-20 11:35:12 -05:00
parent 1451ce8f11
commit 6e6e86da98
6 changed files with 245 additions and 52 deletions

14
Cargo.lock generated
View file

@ -135,10 +135,22 @@ checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a"
dependencies = [ dependencies = [
"android-tzdata", "android-tzdata",
"iana-time-zone", "iana-time-zone",
"js-sys",
"num-traits", "num-traits",
"serde",
"wasm-bindgen",
"windows-targets 0.52.4", "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]] [[package]]
name = "color-eyre" name = "color-eyre"
version = "0.6.3" version = "0.6.3"
@ -1324,6 +1336,8 @@ name = "tasks"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono",
"chrono-humanize",
"color-eyre", "color-eyre",
"regex", "regex",
"reqwest", "reqwest",

View file

@ -7,6 +7,8 @@ edition = "2021"
[dependencies] [dependencies]
anyhow = "1.0.81" anyhow = "1.0.81"
chrono = { version = "0.4.35", features = ["serde"] }
chrono-humanize = "0.2.3"
color-eyre = "0.6.3" color-eyre = "0.6.3"
regex = "1.10.3" regex = "1.10.3"
reqwest = { version = "0.11.26", features = ["json", "socks"] } reqwest = { version = "0.11.26", features = ["json", "socks"] }

View file

@ -1,3 +1,5 @@
use std::collections::HashMap;
use crate::{ use crate::{
client::{Client, ResourceRequest}, client::{Client, ResourceRequest},
result::Result, result::Result,
@ -27,11 +29,47 @@ pub struct User {
pub locale: String, 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<Vec<Component>>,
pub labels: Vec<String>,
pub summary: String,
pub status: IssueStatus,
pub priority: Priority,
}
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Issue { pub struct Issue {
pub id: String, pub id: String,
pub key: String, pub key: String,
pub fields: IssueFields,
} }
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
@ -98,6 +136,11 @@ impl Jira {
Ok(issues) Ok(issues)
} }
// TODO: move this somewhere nicer?
pub fn by_key(issues: Vec<Issue>) -> Result<HashMap<String, Issue>> {
Ok(issues.into_iter().map(|i| (i.key.to_owned(), i)).collect())
}
pub async fn assigned_open_issues(&self) -> Result<Vec<Issue>> { pub async fn assigned_open_issues(&self) -> Result<Vec<Issue>> {
let me = self.me().await?; let me = self.me().await?;
let jql = format!("assignee = {0} and statusCategory != Done", me.account_id); let jql = format!("assignee = {0} and statusCategory != Done", me.account_id);

View file

@ -3,6 +3,7 @@ mod config;
mod gitlab; mod gitlab;
mod jira; mod jira;
mod result; mod result;
mod task;
mod tasks; mod tasks;
use crate::result::Result; use crate::result::Result;
@ -33,10 +34,13 @@ async fn main() -> Result<()> {
async fn run() -> Result<()> { async fn run() -> Result<()> {
// print!("{ANSI_CLEAR}"); // 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 tasks = Tasks::try_new()?;
let gitlab_user = tasks.gitlab.me().await?; for t in tasks.all()?.values() {
info!("{gitlab_user:#?}"); info!("{}", t);
let jira_user = tasks.jira.me().await?; }
info!("{:?}", tasks.desyncs().await?);
Ok(()) Ok(())
} }

76
src/task.rs Normal file
View file

@ -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<MergeRequestRef>,
pub jira_priority: i64,
pub local_priority: i64,
pub status: String,
pub tags: HashSet<String>,
}
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<IVec> for Task {
type Error = anyhow::Error;
fn try_from(value: IVec) -> std::prelude::v1::Result<Self, Self::Error> {
serde_json::from_slice(&value).map_err(|e| e.into())
}
}
impl TryInto<IVec> for &Task {
type Error = anyhow::Error;
fn try_into(self) -> std::prelude::v1::Result<IVec, Self::Error> {
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<Self, Self::Error> {
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,
})
}
}

View file

@ -1,28 +1,19 @@
use std::{sync::OnceLock, collections::HashSet, env, process::Command};
use regex::Regex; 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 sled::IVec;
use crate::{gitlab::GitLab, jira::Jira, result::Result}; 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<MergeRequestRef>,
jira_priority: i64,
local_priority: i64,
status: String,
tags: HashSet<String>,
}
pub struct Tasks { pub struct Tasks {
pub gitlab: GitLab, pub gitlab: GitLab,
pub jira: Jira, pub jira: Jira,
@ -30,22 +21,6 @@ pub struct Tasks {
db: sled::Db, db: sled::Db,
} }
impl TryFrom<IVec> for Task {
type Error = anyhow::Error;
fn try_from(value: IVec) -> std::prelude::v1::Result<Self, Self::Error> {
serde_json::from_slice(&value).map_err(|e| e.into())
}
}
impl TryInto<IVec> for &Task {
type Error = anyhow::Error;
fn try_into(self) -> std::prelude::v1::Result<IVec, Self::Error> {
Ok(IVec::from(serde_json::to_vec(self)?))
}
}
#[derive(Debug)] #[derive(Debug)]
pub struct Desyncs { pub struct Desyncs {
issues: Vec<crate::jira::Issue>, issues: Vec<crate::jira::Issue>,
@ -77,12 +52,19 @@ impl Tasks {
Ok(Self { gitlab, jira, db }) Ok(Self { gitlab, jira, db })
} }
pub fn all(&self) -> Result<Vec<Task>> { pub fn all(&self) -> Result<HashMap<String, Task>> {
self.db let mut result = HashMap::new();
.open_tree("tasks")? let scan = self.db.open_tree("tasks")?.scan_prefix(&[]);
.scan_prefix(&[]) for entry in scan {
.map(|t| Task::try_from(t?.1)) match entry {
.collect::<Result<Vec<Task>>>() 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<Option<Task>> { pub fn get(&self, key: &str) -> Result<Option<Task>> {
@ -107,7 +89,7 @@ impl Tasks {
} }
pub fn purge_all(&self) -> Result<()> { pub fn purge_all(&self) -> Result<()> {
for t in self.all()? { for (_, t) in self.all()? {
self.delete(&t)?; self.delete(&t)?;
} }
self.db.flush()?; self.db.flush()?;
@ -115,18 +97,90 @@ impl Tasks {
} }
pub fn cleanup(&self, task: &mut Task) -> Result<()> { pub fn cleanup(&self, task: &mut Task) -> Result<()> {
static RE: OnceLock<Regex> = OnceLock::new(|| Regex::new(r"^\s*\:[a-zA-Z0-9_-]+\:\s*").unwrap()); static RE: OnceLock<Regex> = OnceLock::new();
let regex = RE.get_or_init(|| Regex::new(r"^\s*\:[a-zA-Z0-9_-]+\:\s*").unwrap());
let mut changed = false; 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(()) Ok(())
} }
/// fetches jira issues and compares to known local tasks /// fetches jira issues and compares to known local tasks
/// for use when sync'ing local tasks to remote state (jira, gitlab) /// for use when sync'ing local tasks to remote state (jira, gitlab)
pub async fn desyncs(&self) -> Result<Desyncs> { pub async fn sync(&self) -> Result<()> {
let issues = self.jira.assigned_open_issues().await?; let mut tasks = self.all()?;
let tasks = self.all()?; let issues = crate::jira::Jira::by_key(self.jira.assigned_open_issues().await?)?;
let task_keys: HashSet<String, RandomState> =
HashSet::from_iter(tasks.keys().map(|s| s.to_owned()));
let issue_keys: HashSet<String, RandomState> =
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<String, RandomState> = 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::<IssueSearch>(),
// ));
// }
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(())
} }
} }