gitlab_tracker_redmine/config.rs
1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4/// Regex patterns applied to MR titles and descriptions to detect Redmine ticket IDs.
5///
6/// Each pattern must contain exactly one capture group `(\d+)` that matches the numeric ID.
7pub fn default_patterns() -> Vec<String> {
8 vec![
9 // Plain "#1234" reference (most common convention)
10 r"#(\d+)".to_string(),
11 // "refs #1234" or "fixes #1234" keywords
12 r"(?i)(?:refs|fixes|closes|resolves)\s+#(\d+)".to_string(),
13 // Direct Redmine URL embedded in the description
14 r"/issues/(\d+)".to_string(),
15 ]
16}
17
18/// Colour pair (background + foreground) for a badge label in the Inspector panel.
19///
20/// Colour values are the same strings accepted by `AppConfig::parse_color`:
21/// named colours (`"red"`, `"cyan"`, …) or hex codes (`"#ff6600"`).
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct LabelColorConfig {
24 /// Background colour of the badge.
25 pub bg: String,
26 /// Foreground (text) colour of the badge.
27 pub fg: String,
28}
29
30/// Per-project Redmine integration configuration embedded in `projects.toml`
31/// under `[project.redmine]`.
32///
33/// Each GitLab project can point to a **different** Redmine instance, enabling
34/// multi-tenant setups where project A uses `redmine-a.example.com` and project B
35/// uses `redmine-b.example.com`. The API token for each instance is stored
36/// separately in the OS keyring, keyed by the Redmine URL.
37///
38/// Example in `projects.toml`:
39/// ```toml
40/// [[project]]
41/// name = "Backend — Client A"
42/// project_id = "12345678"
43/// gitlab_url = "https://gitlab.com"
44///
45/// [project.redmine]
46/// url = "https://redmine-a.example.com"
47///
48/// [project.redmine.tracker_type_colors]
49/// "Bug" = { bg = "red", fg = "white" }
50/// ```
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct RedmineConfig {
53 /// Base URL of the Redmine instance, e.g. "https://redmine.example.com".
54 /// Must not have a trailing slash.
55 pub url: String,
56
57 /// Regex patterns used to detect ticket IDs in the MR title and description.
58 /// Each pattern must expose the numeric ticket ID in capture group 1.
59 #[serde(default = "default_patterns")]
60 pub ticket_patterns: Vec<String>,
61
62 /// Colour overrides for Redmine tracker-type labels displayed as badges
63 /// in the Inspector panel (e.g. "Evolution", "Bug", "Support").
64 ///
65 /// Keys are matched case-insensitively against the `tracker` field returned
66 /// by the Redmine API. Use `"*"` as a catch-all fallback.
67 ///
68 /// Example:
69 /// ```toml
70 /// [project.redmine.tracker_type_colors]
71 /// "Bug" = { bg = "red", fg = "white" }
72 /// "Evolution" = { bg = "cyan", fg = "black" }
73 /// "Support" = { bg = "yellow", fg = "black" }
74 /// "*" = { bg = "dark_gray", fg = "white" }
75 /// ```
76 #[serde(default)]
77 pub tracker_type_colors: HashMap<String, LabelColorConfig>,
78
79 /// Colour overrides for Redmine priority labels displayed as badges
80 /// in the Inspector panel (e.g. "Regular", "High", "Urgent").
81 ///
82 /// Keys are matched case-insensitively against the `priority` field returned
83 /// by the Redmine API. Use `"*"` as a catch-all fallback.
84 ///
85 /// Example:
86 /// ```toml
87 /// [project.redmine.priority_colors]
88 /// "Low" = { bg = "dark_gray", fg = "white" }
89 /// "Regular" = { bg = "dark_gray", fg = "white" }
90 /// "High" = { bg = "yellow", fg = "black" }
91 /// "Urgent" = { bg = "red", fg = "white" }
92 /// "*" = { bg = "dark_gray", fg = "white" }
93 /// ```
94 #[serde(default)]
95 pub priority_colors: HashMap<String, LabelColorConfig>,
96}
97
98impl Default for RedmineConfig {
99 fn default() -> Self {
100 Self {
101 url: String::new(),
102 ticket_patterns: default_patterns(),
103 tracker_type_colors: HashMap::new(),
104 priority_colors: HashMap::new(),
105 }
106 }
107}
108
109impl RedmineConfig {
110 /// Returns `true` when the URL is non-empty after trimming.
111 /// Used to gate the integration without unwrapping an `Option<RedmineConfig>`.
112 pub fn is_active(&self) -> bool {
113 !self.url.trim().is_empty()
114 }
115
116 /// Applies the `REDMINE_URL` environment variable override when set,
117 /// returning whether the value was changed.
118 pub fn apply_env_override(&mut self) -> bool {
119 if let Ok(url) = std::env::var("REDMINE_URL") {
120 let url = url.trim().to_string();
121 if !url.is_empty() {
122 self.url = url;
123 return true;
124 }
125 }
126 false
127 }
128}