gitlab_tracker_redmine/
lib.rs1mod client;
2mod detector;
3
4pub mod columns;
5pub mod config;
6pub mod filters;
7pub mod keyring;
8pub mod shortcuts;
9
10use async_trait::async_trait;
11use gitlab_tracker_core::LINKED_TICKET_SCHEMA_VERSION;
12use gitlab_tracker_core::{
13 Activity, LabelColorMaps, LinkedTicket, TimeEntry, TimeEntryRequest, TrackerProvider,
14};
15
16pub use config::RedmineConfig;
17pub use keyring::get_or_prompt_token;
18
19pub struct RedmineProvider {
24 config: RedmineConfig,
25 token: String,
29 http: reqwest::Client,
31}
32
33impl RedmineProvider {
34 pub fn new(config: RedmineConfig, token: String) -> Self {
36 Self {
37 config,
38 token,
39 http: reqwest::Client::new(),
40 }
41 }
42}
43
44#[async_trait]
45impl TrackerProvider for RedmineProvider {
46 fn name(&self) -> &'static str {
47 "Redmine"
48 }
49
50 fn detect_ticket_id(&self, title: &str, description: &str) -> Option<String> {
51 detector::detect_ticket_id(title, description, &self.config.ticket_patterns)
52 }
53
54 fn label_colors(&self) -> LabelColorMaps {
59 let to_map = |source: &std::collections::HashMap<String, config::LabelColorConfig>| {
60 source
61 .iter()
62 .map(|(k, cfg)| {
63 let key = if k == "*" {
66 k.clone()
67 } else {
68 k.to_lowercase()
69 };
70 (key, (cfg.bg.clone(), cfg.fg.clone()))
71 })
72 .collect()
73 };
74
75 LabelColorMaps {
76 tracker_type: to_map(&self.config.tracker_type_colors),
77 priority: to_map(&self.config.priority_colors),
78 }
79 }
80
81 async fn fetch_ticket(&self, ticket_id: &str) -> Option<LinkedTicket> {
82 let issue =
83 client::fetch_issue(&self.http, &self.config.url, &self.token, ticket_id).await?;
84
85 Some(LinkedTicket {
86 schema_version: LINKED_TICKET_SCHEMA_VERSION,
87 id: issue.id.to_string(),
88 subject: issue.subject,
89 status: issue.status.name,
90 url: self.ticket_url(ticket_id),
91 author: issue.author.map(|u| u.name),
92 assignee: issue.assigned_to.map(|u| u.name),
93 time_estimate: issue.estimated_hours.map(|h| (h * 3600.0).round() as u32),
95 time_spent: issue.spent_hours.map(|h| (h * 3600.0).round() as u32),
96 time_remaining: issue.remaining_hours.map(|h| (h * 3600.0).round() as u32),
97 tracker_type: issue.tracker.map(|t| t.name),
98 priority: issue.priority.map(|p| p.name),
99 version: issue.fixed_version.map(|v| v.name),
100 start_date: issue.start_date,
101 done_ratio: issue.done_ratio,
102 })
103 }
104
105 fn ticket_url(&self, ticket_id: &str) -> String {
106 format!(
107 "{}/issues/{}",
108 self.config.url.trim_end_matches('/'),
109 ticket_id
110 )
111 }
112
113 async fn fetch_activities(&self) -> Vec<Activity> {
117 client::fetch_activities(&self.http, &self.config.url, &self.token).await
118 }
119
120 async fn fetch_time_entries(&self, ticket_id: &str) -> Vec<TimeEntry> {
124 client::fetch_time_entries(&self.http, &self.config.url, &self.token, ticket_id).await
125 }
126
127 async fn log_time(
136 &self,
137 ticket_id: &str,
138 entry: TimeEntryRequest,
139 ) -> Result<(), gitlab_tracker_core::TrackerError> {
140 let issue = client::fetch_issue(&self.http, &self.config.url, &self.token, ticket_id).await;
143
144 client::log_time(
145 &self.http,
146 &self.config.url,
147 &self.token,
148 ticket_id,
149 entry,
150 issue.as_ref(),
151 )
152 .await
153 .map_err(gitlab_tracker_core::TrackerError::Other)
154 }
155}