mod client;
mod detector;
pub mod config;
pub mod keyring;
use async_trait::async_trait;
use gitlab_tracker_core::LINKED_TICKET_SCHEMA_VERSION;
use gitlab_tracker_core::{
Activity, LabelColorMaps, LinkedTicket, TimeEntry, TimeEntryRequest, TrackerProvider,
};
pub use config::{load_or_create_config, RedmineConfig};
pub use keyring::get_or_prompt_token;
pub struct RedmineProvider {
config: RedmineConfig,
token: String,
http: reqwest::Client,
}
impl RedmineProvider {
pub fn new(config: RedmineConfig, token: String) -> Self {
Self {
config,
token,
http: reqwest::Client::new(),
}
}
}
#[async_trait]
impl TrackerProvider for RedmineProvider {
fn name(&self) -> &'static str {
"Redmine"
}
fn detect_ticket_id(&self, title: &str, description: &str) -> Option<String> {
detector::detect_ticket_id(title, description, &self.config.ticket_patterns)
}
fn label_colors(&self) -> LabelColorMaps {
let to_map = |source: &std::collections::HashMap<String, config::LabelColorConfig>| {
source
.iter()
.map(|(k, cfg)| {
let key = if k == "*" {
k.clone()
} else {
k.to_lowercase()
};
(key, (cfg.bg.clone(), cfg.fg.clone()))
})
.collect()
};
LabelColorMaps {
tracker_type: to_map(&self.config.tracker_type_colors),
priority: to_map(&self.config.priority_colors),
}
}
async fn fetch_ticket(&self, ticket_id: &str) -> Option<LinkedTicket> {
let issue =
client::fetch_issue(&self.http, &self.config.redmine_url, &self.token, ticket_id)
.await?;
Some(LinkedTicket {
schema_version: LINKED_TICKET_SCHEMA_VERSION,
id: issue.id.to_string(),
subject: issue.subject,
status: issue.status.name,
url: self.ticket_url(ticket_id),
author: issue.author.map(|u| u.name),
assignee: issue.assigned_to.map(|u| u.name),
time_estimate: issue.estimated_hours.map(|h| (h * 3600.0).round() as u32),
time_spent: issue.spent_hours.map(|h| (h * 3600.0).round() as u32),
time_remaining: issue.remaining_hours.map(|h| (h * 3600.0).round() as u32),
tracker_type: issue.tracker.map(|t| t.name),
priority: issue.priority.map(|p| p.name),
version: issue.fixed_version.map(|v| v.name),
start_date: issue.start_date,
done_ratio: issue.done_ratio,
})
}
fn ticket_url(&self, ticket_id: &str) -> String {
format!(
"{}/issues/{}",
self.config.redmine_url.trim_end_matches('/'),
ticket_id
)
}
async fn fetch_activities(&self) -> Vec<Activity> {
client::fetch_activities(&self.http, &self.config.redmine_url, &self.token).await
}
async fn fetch_time_entries(&self, ticket_id: &str) -> Vec<TimeEntry> {
client::fetch_time_entries(&self.http, &self.config.redmine_url, &self.token, ticket_id)
.await
}
async fn log_time(&self, ticket_id: &str, entry: TimeEntryRequest) -> Result<(), String> {
let issue =
client::fetch_issue(&self.http, &self.config.redmine_url, &self.token, ticket_id).await;
client::log_time(
&self.http,
&self.config.redmine_url,
&self.token,
ticket_id,
entry,
issue.as_ref(),
)
.await
}
}