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 = [
|
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",
|
||||||
|
|
|
@ -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"] }
|
||||||
|
|
43
src/jira.rs
43
src/jira.rs
|
@ -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);
|
||||||
|
|
12
src/main.rs
12
src/main.rs
|
@ -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
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 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(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue