Skip to main content

lean_ctx/core/providers/
gitlab.rs

1use super::cache;
2use super::config::GitLabConfig;
3use super::{ProviderItem, ProviderResult};
4
5const DEFAULT_PER_PAGE: usize = 20;
6const CACHE_TTL_SECS: u64 = 120;
7
8pub fn list_issues(
9    config: &GitLabConfig,
10    state: Option<&str>,
11    labels: Option<&str>,
12    limit: Option<usize>,
13) -> Result<ProviderResult, String> {
14    let project = config
15        .project_path
16        .as_deref()
17        .ok_or("No project path configured. Set CI_PROJECT_PATH or configure git remote.")?;
18    let encoded = urlencoding::encode(project);
19    let per_page = limit.unwrap_or(DEFAULT_PER_PAGE).min(100);
20
21    let mut url =
22        format!("/projects/{encoded}/issues?per_page={per_page}&order_by=updated_at&sort=desc");
23    if let Some(s) = state {
24        url.push_str(&format!("&state={s}"));
25    }
26    if let Some(l) = labels {
27        url.push_str(&format!("&labels={l}"));
28    }
29
30    let cache_key = format!("gitlab:issues:{project}:{state:?}:{labels:?}:{per_page}");
31    if let Some(cached) = cache::get_cached(&cache_key) {
32        if let Ok(result) = serde_json::from_str::<ProviderResult>(&cached) {
33            return Ok(result);
34        }
35    }
36
37    let body = api_get(config, &url)?;
38    let items: Vec<serde_json::Value> =
39        serde_json::from_str(&body).map_err(|e| format!("JSON parse error: {e}"))?;
40
41    let result = ProviderResult {
42        provider: "gitlab".to_string(),
43        resource_type: "issues".to_string(),
44        total_count: None,
45        truncated: items.len() >= per_page,
46        items: items.iter().map(parse_issue).collect(),
47    };
48
49    if let Ok(json) = serde_json::to_string(&result) {
50        cache::set_cached(&cache_key, &json, CACHE_TTL_SECS);
51    }
52    Ok(result)
53}
54
55pub fn show_issue(config: &GitLabConfig, iid: u64) -> Result<ProviderResult, String> {
56    let project = config
57        .project_path
58        .as_deref()
59        .ok_or("No project path configured.")?;
60    let encoded = urlencoding::encode(project);
61    let url = format!("/projects/{encoded}/issues/{iid}");
62
63    let body = api_get(config, &url)?;
64    let issue: serde_json::Value =
65        serde_json::from_str(&body).map_err(|e| format!("JSON parse error: {e}"))?;
66
67    Ok(ProviderResult {
68        provider: "gitlab".to_string(),
69        resource_type: "issue".to_string(),
70        total_count: Some(1),
71        truncated: false,
72        items: vec![parse_issue(&issue)],
73    })
74}
75
76pub fn list_mrs(
77    config: &GitLabConfig,
78    state: Option<&str>,
79    limit: Option<usize>,
80) -> Result<ProviderResult, String> {
81    let project = config
82        .project_path
83        .as_deref()
84        .ok_or("No project path configured.")?;
85    let encoded = urlencoding::encode(project);
86    let per_page = limit.unwrap_or(DEFAULT_PER_PAGE).min(100);
87
88    let mut url = format!(
89        "/projects/{encoded}/merge_requests?per_page={per_page}&order_by=updated_at&sort=desc"
90    );
91    if let Some(s) = state {
92        url.push_str(&format!("&state={s}"));
93    }
94
95    let cache_key = format!("gitlab:mrs:{project}:{state:?}:{per_page}");
96    if let Some(cached) = cache::get_cached(&cache_key) {
97        if let Ok(result) = serde_json::from_str::<ProviderResult>(&cached) {
98            return Ok(result);
99        }
100    }
101
102    let body = api_get(config, &url)?;
103    let items: Vec<serde_json::Value> =
104        serde_json::from_str(&body).map_err(|e| format!("JSON parse error: {e}"))?;
105
106    let result = ProviderResult {
107        provider: "gitlab".to_string(),
108        resource_type: "merge_requests".to_string(),
109        total_count: None,
110        truncated: items.len() >= per_page,
111        items: items.iter().map(parse_mr).collect(),
112    };
113
114    if let Ok(json) = serde_json::to_string(&result) {
115        cache::set_cached(&cache_key, &json, CACHE_TTL_SECS);
116    }
117    Ok(result)
118}
119
120pub fn list_pipelines(
121    config: &GitLabConfig,
122    status: Option<&str>,
123    limit: Option<usize>,
124) -> Result<ProviderResult, String> {
125    let project = config
126        .project_path
127        .as_deref()
128        .ok_or("No project path configured.")?;
129    let encoded = urlencoding::encode(project);
130    let per_page = limit.unwrap_or(DEFAULT_PER_PAGE).min(100);
131
132    let mut url =
133        format!("/projects/{encoded}/pipelines?per_page={per_page}&order_by=updated_at&sort=desc");
134    if let Some(s) = status {
135        url.push_str(&format!("&status={s}"));
136    }
137
138    let body = api_get(config, &url)?;
139    let items: Vec<serde_json::Value> =
140        serde_json::from_str(&body).map_err(|e| format!("JSON parse error: {e}"))?;
141
142    Ok(ProviderResult {
143        provider: "gitlab".to_string(),
144        resource_type: "pipelines".to_string(),
145        total_count: None,
146        truncated: items.len() >= per_page,
147        items: items
148            .iter()
149            .map(|p| ProviderItem {
150                id: p["id"].as_u64().unwrap_or(0).to_string(),
151                title: p["ref"].as_str().unwrap_or("").to_string(),
152                state: p["status"].as_str().map(std::string::ToString::to_string),
153                author: None,
154                created_at: p["created_at"]
155                    .as_str()
156                    .map(std::string::ToString::to_string),
157                updated_at: p["updated_at"]
158                    .as_str()
159                    .map(std::string::ToString::to_string),
160                url: p["web_url"].as_str().map(std::string::ToString::to_string),
161                labels: Vec::new(),
162                body: None,
163            })
164            .collect(),
165    })
166}
167
168fn api_get(config: &GitLabConfig, endpoint: &str) -> Result<String, String> {
169    let url = config.api_url(endpoint);
170    let response = ureq::get(&url)
171        .header("PRIVATE-TOKEN", &config.token)
172        .call()
173        .map_err(|e| format!("GitLab API error: {e}"))?;
174
175    if response.status() != 200 {
176        return Err(format!("GitLab API returned status {}", response.status()));
177    }
178
179    response
180        .into_body()
181        .read_to_string()
182        .map_err(|e| format!("Failed to read response: {e}"))
183}
184
185fn parse_issue(v: &serde_json::Value) -> ProviderItem {
186    ProviderItem {
187        id: v["iid"].as_u64().unwrap_or(0).to_string(),
188        title: v["title"].as_str().unwrap_or("").to_string(),
189        state: v["state"].as_str().map(std::string::ToString::to_string),
190        author: v["author"]["username"]
191            .as_str()
192            .map(std::string::ToString::to_string),
193        created_at: v["created_at"]
194            .as_str()
195            .map(std::string::ToString::to_string),
196        updated_at: v["updated_at"]
197            .as_str()
198            .map(std::string::ToString::to_string),
199        url: v["web_url"].as_str().map(std::string::ToString::to_string),
200        labels: v["labels"]
201            .as_array()
202            .map(|arr| {
203                arr.iter()
204                    .filter_map(|l| l.as_str().map(std::string::ToString::to_string))
205                    .collect()
206            })
207            .unwrap_or_default(),
208        body: v["description"]
209            .as_str()
210            .map(std::string::ToString::to_string),
211    }
212}
213
214fn parse_mr(v: &serde_json::Value) -> ProviderItem {
215    ProviderItem {
216        id: v["iid"].as_u64().unwrap_or(0).to_string(),
217        title: v["title"].as_str().unwrap_or("").to_string(),
218        state: v["state"].as_str().map(std::string::ToString::to_string),
219        author: v["author"]["username"]
220            .as_str()
221            .map(std::string::ToString::to_string),
222        created_at: v["created_at"]
223            .as_str()
224            .map(std::string::ToString::to_string),
225        updated_at: v["updated_at"]
226            .as_str()
227            .map(std::string::ToString::to_string),
228        url: v["web_url"].as_str().map(std::string::ToString::to_string),
229        labels: v["labels"]
230            .as_array()
231            .map(|arr| {
232                arr.iter()
233                    .filter_map(|l| l.as_str().map(std::string::ToString::to_string))
234                    .collect()
235            })
236            .unwrap_or_default(),
237        body: v["description"]
238            .as_str()
239            .map(std::string::ToString::to_string),
240    }
241}