WIP
This commit is contained in:
parent
1451ce8f11
commit
6e6e86da98
6 changed files with 245 additions and 52 deletions
14
Cargo.lock
generated
14
Cargo.lock
generated
|
@ -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",
|
||||
|
|
|
@ -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"] }
|
||||
|
|
43
src/jira.rs
43
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<Vec<Component>>,
|
||||
pub labels: Vec<String>,
|
||||
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<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>> {
|
||||
let me = self.me().await?;
|
||||
let jql = format!("assignee = {0} and statusCategory != Done", me.account_id);
|
||||
|
|
12
src/main.rs
12
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(())
|
||||
}
|
||||
|
|
76
src/task.rs
Normal file
76
src/task.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
150
src/tasks.rs
150
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<MergeRequestRef>,
|
||||
jira_priority: i64,
|
||||
local_priority: i64,
|
||||
status: String,
|
||||
tags: HashSet<String>,
|
||||
}
|
||||
|
||||
pub struct Tasks {
|
||||
pub gitlab: GitLab,
|
||||
pub jira: Jira,
|
||||
|
@ -30,22 +21,6 @@ pub struct Tasks {
|
|||
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)]
|
||||
pub struct Desyncs {
|
||||
issues: Vec<crate::jira::Issue>,
|
||||
|
@ -77,12 +52,19 @@ impl Tasks {
|
|||
Ok(Self { gitlab, jira, db })
|
||||
}
|
||||
|
||||
pub fn all(&self) -> Result<Vec<Task>> {
|
||||
self.db
|
||||
.open_tree("tasks")?
|
||||
.scan_prefix(&[])
|
||||
.map(|t| Task::try_from(t?.1))
|
||||
.collect::<Result<Vec<Task>>>()
|
||||
pub fn all(&self) -> Result<HashMap<String, Task>> {
|
||||
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<Option<Task>> {
|
||||
|
@ -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<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;
|
||||
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<Desyncs> {
|
||||
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<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(())
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue