1use crate::prelude::*;
64use crate::forge::RepoBranch; use gitlab_crate as gitlab;
67
68use gitlab::*;
69use gitlab::api::Query as _;
70
71#[derive(Debug)]
73pub struct Lab {
74 host: String,
75 gl: gitlab::Gitlab,
76 cache_user: IdCacheData<Lab, UserId>,
77 cache_proj: IdCacheData<Lab, ProjectId>,
78}
79
80impl From<(Gitlab, String)> for Lab {
81 fn from((gl, host): (Gitlab, String)) -> Self { Lab {
82 gl, host,
83 cache_user: default(),
84 cache_proj: default(),
85 } }
86}
87
88impl Lab {
89 #[throws(FE)]
94 pub fn new(config: &forge::Config) -> Box<dyn Forge> {
95 let gl = gitlab::Gitlab::new(
96 config.host.clone(),
97 config.get_token()?.ok_or_else(
98 || FE::TokenAlwaysRequired(Kind::GitLab)
99 )?.0,
100 ).map_err(|e| FE::ClientCreationFailed(e.into()))?;
101 Box::new(Lab::from((gl, config.host.clone()))) as _
102 }
103}
104
105impl TryFrom<IssueMrStatus> for
106 gitlab::api::projects::merge_requests::MergeRequestState
107{
108 type Error = FE;
109 #[throws(FE)]
110 fn try_from(st: IssueMrStatus) -> Self {
111 use IssueMrStatus as F;
112 use gitlab::api::projects::merge_requests::MergeRequestState as G;
113 match st {
114 F::Open => G::Opened,
115 F::Closed => G::Closed,
116 F::Merged => G::Merged,
117 F::Unrepresentable => throw!(FE::UnsupportedState(
118 RemoteObjectKind::MergeReq,
119 format!("{:?}", st),
120 )),
121 }
122 }
123}
124
125impl ForgeMethods for Lab {
126 fn clear_id_caches(&mut self) {
127 self.cache_user.clear();
128 self.cache_proj.clear();
129 }
130
131 fn host(&self) -> &str { &self.host }
132 fn kind(&self) -> Kind { Kind::GitLab }
133
134 #[throws(FE)]
135 fn request(&mut self, req: &Req) -> Resp {
136 let req_dbg = || format!("{:?}", req);
137 let e_build = |e:String|
138 FE::OperationBuildFailed(anyhow!(e).context(req_dbg()));
139 let e_query = |e|
140 FE::UncleassifiedOperationError(AE::new(e).context(req_dbg()));
141 let e_process = |e|
142 FE::ResultsProcessingFailed(AE::new(e).context(req_dbg()));
143
144 match req {
145
146 Req::MergeRequests(q) => {
147 let target_project: ProjectId =
148 self.name2id_required(&q.target_repo)?;
149 let target_project = target_project.value();
150
151 let d = if let Some(number) = &q.number {
152 let number: u64 = number.parse().map_err(
153 |e: ParseIntError| FE::InvalidIdSyntax(
154 RemoteObjectKind::MergeReq, number.into(), e.to_string()
155 ))?;
156
157 let mut d = api::projects::merge_requests::MergeRequest::builder();
158 d.project(target_project);
159 d.merge_request(number);
160
161 let d = d.build().map_err(e_build)?;
162
163 debug!("MergeRequests query {:?}", &d);
164
165 let d: Option<gitlab::types::MergeRequest> =
166 d.query(&mut self.gl).map_err(e_query)?;
167
168 d.into_iter().collect_vec()
169
170 } else {
171
172 let mut d = api::projects::merge_requests::MergeRequests::builder();
173
174 d.project(target_project);
175
176 if let Some(author) = &q.author {
177 let user: UserId = self.name2id_required(author)?;
178 d.author(user.value());
179 }
180
181 if let Some(statuses) = &q.statuses {
182 match statuses.iter().take(2).collect_vec().as_slice() {
183 [] => return Resp::MergeRequests { mrs: vec![] },
184 &[&state] => { d.state(state.try_into()?); },
185 [_,_,..] => { },
186 }
187 }
188
189 if let Some(source_branch) = &q.source_branch {
190 d.source_branch(source_branch);
191 }
192
193 d.with_merge_status_recheck(true);
194
195 let d = d.build().map_err(e_build)?;
196 debug!("MergeRequests query {:?}", &d);
197
198 let d = api::paged(d, api::Pagination::All);
199 let d: Vec<gitlab::types::MergeRequest> =
200 d.query(&mut self.gl).map_err(e_query)?;
201
202 d
203 };
204
205 debug!("MergeRequests reply {:?}", &d);
206
207 let mrs = d.into_iter().map(|g| Ok::<_,FE>(
208 Resp_MergeRequest {
209 number: g.iid.value().to_string(),
210 author: g.author.username,
211 state: IssueMrState {
212 locked: match g.discussion_locked {
213 None | Some(false) => IssueMrLocked::Unlocked,
214 Some(true) => IssueMrLocked::Locked,
215 },
216 status: {
217 use IssueMrStatus as F;
218 if let Some(_) = g.merged_at { F::Merged }
219 else if let Some(_) = g.closed_at { F::Merged }
220 else { F::Open }
221 },
222 },
223 source: RepoBranch {
224 repo: self.id2name_required(g.source_project_id)?.to_string(),
225 branch: g.source_branch,
226 },
227 target: RepoBranch {
228 repo: self.id2name_required(g.target_project_id)?.to_string(),
229 branch: g.target_branch,
230 },
231 }
232 ))
233 .filter_ok(filter_mergerequests(&q))
234 .collect::<Result<Vec<_>,_>>()
235 .map_err(e_process)?;
236
237 Resp::MergeRequests { mrs }
238 }
239
240 Req::CreateMergeRequest(Req_CreateMergeRequest {
241 title, description,
242 target: RepoBranch { repo: target_repo, branch: target_branch },
243 source: RepoBranch { repo: source_repo, branch: source_branch },
244 _non_exhaustive, }) => {
246 let mut d = api::projects::merge_requests::CreateMergeRequest
247 ::builder();
248
249 let target_proj: ProjectId = self.name2id_required(&target_repo)?;
250 d.target_project_id(target_proj.value());
251 d.target_branch(target_branch);
252
253 let source_proj: ProjectId = self.name2id_required(&source_repo)?;
254 d.project(source_proj.value());
255 d.source_branch(source_branch);
256
257 d.title(title);
258 d.description(description);
259
260 let d = d.build().map_err(e_build)?;
261 debug!("CreateMergeRequest query {:?}", &d);
262
263 let d: gitlab::types::MergeRequest =
264 d.query(&mut self.gl).map_err(e_query)?;
265
266 Resp::CreateMergeRequest {
267 number: d.iid.value().to_string(),
268 }
269 }
270
271 q@ Req::_NonExhaustive() => panic!("bad request {:?}", q),
272 }
273 }
274}
275
276impl Lab {
277 #[throws(FE)]
278 fn idcache_some_lookup<BQ,Q,RR,PR,EC,O>(
279 &mut self,
280 query_what: &str,
281 error_context: EC,
282 build_query: BQ,
283 process_results: PR,
284 ) -> O
285 where
286 BQ: FnOnce() -> Result<Q, String>,
287 Q: gitlab::api::Endpoint,
288 PR: FnOnce(RR) -> Result<O, anyhow::Error>,
289 EC: FnOnce() -> String,
290 RR: DeserializeOwned,
291 {
292 (||{
293 let raw_results: RR =
294 build_query()
295 .map_err(|s| anyhow!("build {} query: {}", query_what, s))?
296 .query(&mut self.gl)
297 .with_context(|| format!("perform {} query", query_what))?;
298
299 let output = process_results(raw_results)?;
300 Ok::<_,AE>(output)
301 })()
302 .with_context(error_context)
303 .map_err(FE::AncillaryOperationFailed)?
304 }
305}
306
307impl IdCache<UserId> for Lab {
308 const UNKNOWN: RemoteObjectKind = RemoteObjectKind::User;
309
310 #[throws(FE)]
311 fn name2id_lookup(&mut self, username: &str) -> Option<UserId> {
312 self.idcache_some_lookup(
313 "User",
314 || format!("username {:?} lookup failed", username),
315 ||{
316 gitlab::api::users::Users::builder()
317 .username(username)
318 .build()
319 },
320 |user: Vec<UserBasic>| Ok::<_,AE>({
321 match user.as_slice() {
322 [user] => Some(user.id),
323 [] => None,
324 _ => throw!(anyhow!("multiple users found!")),
325 }
326 }),
327 )?
328 }
329
330 #[throws(FE)]
331 fn id2name_lookup(&mut self, user: UserId) -> Option<String> {
332 self.idcache_some_lookup(
333 "User",
334 || format!("userid {:?} lookup failed", user),
335 || Ok(
336 gitlab::api::users::User::builder()
337 .user(user.value())
338 .build()?
339 ),
340 |ub: Option<UserBasic>| Ok(
341 ub.map(|ub| ub.name)
342 ),
343 )?
344 }
345
346 fn id_cache(&mut self) -> &mut IdCacheData<Self, UserId> {
347 &mut self.cache_user
348 }
349}
350
351impl IdCache<ProjectId> for Lab {
352 const UNKNOWN: RemoteObjectKind = RemoteObjectKind::Repo;
353
354 #[throws(FE)]
355 fn name2id_lookup(&mut self, repo: &str) -> Option<ProjectId> {
356 self.idcache_some_lookup(
357 "Project",
358 || format!("repo (project) {:?} lookup failed", repo),
359 || Ok(
360 gitlab::api::projects::Project::builder()
361 .project(repo)
362 .build()?
363 ),
364 |proj: Option<Project>| Ok(
365 proj.map(|proj| proj.id)
366 ),
367 )?
368 }
369
370 #[throws(FE)]
371 fn id2name_lookup(&mut self, id: ProjectId) -> Option<String> {
372 self.idcache_some_lookup(
373 "Project",
374 || format!("repo (project) id {:?} lookup failed", id),
375 || Ok(
376 gitlab::api::projects::Project::builder()
377 .project(id.value())
378 .build()?
379 ),
380 |proj: Option<Project>| Ok(
381 proj.map(|proj| proj.path_with_namespace)
382 ),
383 )?
384 }
385
386 fn id_cache(&mut self) -> &mut IdCacheData<Self, ProjectId> {
387 &mut self.cache_proj
388 }
389}