lean_ctx/core/providers/
gitlab.rs1use 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}