Skip to main content

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}