1use std::collections::HashSet;
26
27use anyhow::Result;
28use log::{debug, error, info};
29use url::percent_encoding::{utf8_percent_encode, PATH_SEGMENT_ENCODE_SET};
30use url::Url;
31
32use crate::client::Client;
33use crate::entities::*;
34
35type ReqwestClient = reqwest::blocking::Client;
36
37impl MergeRequest {
38 pub fn list_ready_to_merge(
39 client: &Client,
40 project: &Option<String>,
41 merge_labels: &str,
42 merge_emoji: &str,
43 ) -> Result<Vec<MergeRequest>> {
44 let prefix = if let Some(aproject) = project {
45 let project_name_or_id =
46 utf8_percent_encode(aproject, PATH_SEGMENT_ENCODE_SET).to_string();
47 format!("api/v4/projects/{}/merge_requests?", project_name_or_id)
48 } else {
49 "api/v4/merge_requests?scope=assigned_to_me&".to_string()
50 };
51 let api_path = format!(
52 "{}state=opened&order_by=created_at&labels={}&my_reaction_emoji={}",
53 prefix, merge_labels, merge_emoji
54 );
55 debug!("Ready to query {:?}", api_path);
56 let mut merge_requests: Vec<MergeRequest> =
57 client.get(&api_path)?.error_for_status()?.json()?;
58 debug!("{} merge requests found", merge_requests.len());
59 merge_requests.retain(|mr| {
61 debug!("{:?}", mr);
62 !mr.work_in_progress
63 && mr.merge_status == "can_be_merged"
64 && mr.target_branch == "master"
65 });
66 Ok(merge_requests)
67 }
68 pub fn infos(&self, client: &ReqwestClient, api_server: &Url) -> Result<MergeRequestDetails> {
69 let mr_uri = format!(
70 "api/v4/projects/{}/merge_requests/{}",
71 self.project_id, self.iid
72 );
73 let info_mr_uri = api_server.join(&mr_uri)?;
74 let message = GetMergeRequestDetails::from(self);
75 let resp = client
76 .get(info_mr_uri.as_str())
77 .json(&message)
78 .send()?
79 .json()?;
80 Ok(resp)
81 }
82 fn auto_remove_branch(&self, client: &ReqwestClient, api_server: &Url) -> Result<MergeRequest> {
83 let mr_uri = format!(
84 "api/v4/projects/{}/merge_requests/{}",
85 self.project_id, self.iid
86 );
87 let update_mr_url = api_server.join(&mr_uri)?;
88 let force_merge = RemoveSourceBranch::from(self);
89 debug!(" Update url: {:?} <- {:?}", update_mr_url, force_merge);
90 let resp = client
92 .put(update_mr_url.as_str())
93 .json(&force_merge)
94 .send()?
95 .json()?;
96 Ok(resp)
97 }
98 pub fn pipelines(&self, client: &Client) -> Result<Vec<Pipeline>> {
99 let pipelines_uri = format!(
100 "api/v4/projects/{}/merge_requests/{}/pipelines",
101 self.project_id, self.iid
102 );
103 let message = ListPipelines::from(self);
104 debug!("querying pipelines: {:?} {:?}", pipelines_uri, message);
105 let resp = client.get_json(&pipelines_uri, &message)?.json()?;
106 Ok(resp)
107 }
108 fn accept(&self, client: &ReqwestClient, api_server: &Url) -> Result<MergeRequest> {
109 let accept_uri = format!(
110 "api/v4/projects/{}/merge_requests/{}/merge",
111 self.project_id, self.iid
112 );
113 let accept_mr_uri = api_server.join(&accept_uri)?;
114 let message = AcceptMergeRequest::from(self);
115 let resp = client.put(accept_mr_uri.as_str()).json(&message).send()?;
116 debug!("accept response: {:?}", resp);
117 let resp = resp.json()?;
118 Ok(resp)
119 }
120 fn rebase(&self, client: &ReqwestClient, api_server: &Url) -> Result<()> {
121 let rebase_uri = format!(
122 "api/v4/projects/{}/merge_requests/{}/rebase",
123 self.project_id, self.iid
124 );
125 let rebase_mr_url = api_server.join(&rebase_uri)?;
126 let message = RebaseBranch::from(self);
127 let _resp = client.put(rebase_mr_url.as_str()).json(&message).send()?;
128 Ok(())
129 }
130 pub fn create(client: &Client, mr: &CreateMergeRequest) -> Result<MergeRequest> {
131 let api_path = {
133 let project_name_or_id =
134 utf8_percent_encode(&mr.project_id, PATH_SEGMENT_ENCODE_SET).to_string();
135 format!("api/v4/projects/{}/merge_requests", project_name_or_id)
136 };
137 let response = client.post_json(&api_path, &mr)?;
138 let de = &mut serde_json::Deserializer::from_reader(response);
139 let result: Result<MergeRequest, _> = serde_path_to_error::deserialize(de);
140 match result {
141 Ok(mr) => Ok(mr),
142 Err(err) => {
143 error!("Path: {}", err.path().to_string());
144 error!("Body: {:?}", client.post_json(&api_path, &mr)?.bytes()?);
145 Err(err.into_inner().into())
146 }
147 }
148 }
149 pub fn single(client: &Client, project: &str, mr_iid: usize) -> Result<MergeRequest> {
150 let api_path = {
152 let project_name_or_id =
153 utf8_percent_encode(project, PATH_SEGMENT_ENCODE_SET).to_string();
154 format!(
155 "api/v4/projects/{}/merge_requests/{}",
156 project_name_or_id, mr_iid
157 )
158 };
159 let response = client.get(&api_path)?;
160 let de = &mut serde_json::Deserializer::from_reader(response);
161 let result: Result<MergeRequest, _> = serde_path_to_error::deserialize(de);
162 match result {
163 Ok(mr) => Ok(mr),
164 Err(err) => {
165 dbg!(&api_path);
166 dbg!(err.path().to_string());
167 dbg!(client.get(&api_path)?.text()?);
168 Err(err.into_inner().into())
169 }
170 }
171 }
172 pub fn list(
173 client: &Client,
174 project: &Option<String>,
175 state: &MergeRequestState,
176 labels: &Option<String>,
177 milestone: &Option<String>,
178 source_branch: &Option<String>,
179 limit: usize,
180 ) -> Result<Vec<MergeRequest>> {
181 let prefix = if let Some(aproject) = project {
182 let project_name_or_id =
183 utf8_percent_encode(aproject, PATH_SEGMENT_ENCODE_SET).to_string();
184 format!("api/v4/projects/{}", project_name_or_id)
185 } else {
186 "api/v4".to_string()
187 };
188 let scope = {
189 if project.is_some() {
190 "all"
191 } else {
192 "assigned_to_me" }
194 };
195 let mut api_path = format!(
196 "{}/merge_requests?state={}&scope={}",
197 prefix,
198 state.name(),
199 scope
200 );
201 if let Some(labels) = labels {
202 api_path = format!("{}&labels={}", api_path, labels);
203 }
204 if let Some(milestone) = milestone {
205 api_path = format!("{}&milestone={}", api_path, milestone);
206 }
207 if let Some(source_branch) = source_branch {
208 api_path = format!("{}&source_branch={}", api_path, source_branch);
209 }
210 debug!("Ready to query {:?}", api_path);
211 let mut all_merge_requests: Vec<MergeRequest> = vec![];
212 for merge_requests in client.get_paginated(&api_path) {
213 let merge_requests = merge_requests?;
214 let de = &mut serde_json::Deserializer::from_reader(merge_requests);
215 let result: Result<Vec<MergeRequest>, _> = serde_path_to_error::deserialize(de);
216 let merge_requests = match result {
217 Ok(merge_requests) => merge_requests,
218 Err(err) => {
219 dbg!(&api_path);
220 dbg!(err.path().to_string());
221 dbg!(client.get(&api_path)?.text()?);
222 return Err(err.into_inner().into());
223 }
224 };
225 all_merge_requests.extend(merge_requests.into_iter().map(|mut mr| {
226 let main_issue =
227 Issue::issue_from_branch(client, &mr.project_id.to_string(), &mr.source_branch)
228 .transpose()
229 .ok()
230 .flatten();
231 mr.main_issue = main_issue;
232 mr
233 }));
234 if all_merge_requests.len() >= limit {
235 break;
236 }
237 }
238 Ok(all_merge_requests)
239 }
240}
241
242#[derive(Debug)]
243pub enum MergeResult {
244 RebaseInProgress(MergeRequestDetails),
245 Merged(MergeRequest),
246 Rebasing(MergeRequest),
247 NothingToDo,
248}
249
250pub fn try_to_merge(
252 client: &ReqwestClient,
253 api_server: &Url,
254 mr: MergeRequest,
255) -> Result<MergeResult> {
256 let details = mr.infos(client, api_server)?;
257 debug!("Trying to merge {:?}", details);
258 if Some(true) == details.rebase_in_progress {
260 info!("Rebasing MR {:?}, need to wait", details);
261 return Ok(MergeResult::RebaseInProgress(details));
262 }
263 if let Some(pipeline) = details.pipeline {
264 if pipeline.status == "success" {
266 mr.auto_remove_branch(client, api_server)?;
267 debug!("----- Try to accept");
270 if let Ok(amr) = mr.accept(client, api_server) {
271 info!("Going to merge MR {:?}", amr);
272 return Ok(MergeResult::Merged(amr));
274 } else {
275 debug!("----- About to rebase");
277 mr.rebase(client, api_server)?;
278 return Ok(MergeResult::Rebasing(mr));
279 }
280 }
281 }
282 return Ok(MergeResult::NothingToDo);
283}
284
285pub fn mergebot(
286 client: &Client,
287 project: &Option<String>,
288 merge_labels: &str,
289 merge_emoji: &str,
290) -> Result<()> {
291 let resp = MergeRequest::list_ready_to_merge(&client, project, &merge_labels, &merge_emoji)?;
292 if resp.is_empty() {
299 info!("No MR marked for merge");
300 return Ok(());
301 }
302 let mut already_rebased = HashSet::new();
303 for mr in resp.iter() {
304 let details = mr.infos(&client.client, &client.api_server)?;
305 if let Some(pipeline) = details.pipeline {
306 if pipeline.status == "pending" || pipeline.status == "running" {
307 info!("Some pipeline is running, not a good time to do anything");
308 return Ok(());
309 }
310 }
311 if details.diverged_commits_count.unwrap_or(0) == 0 {
313 already_rebased.insert(details.iid);
314 }
315 }
316 for mr in resp.into_iter().rev() {
317 if already_rebased.is_empty() || already_rebased.contains(&mr.iid) {
320 let result = try_to_merge(&client.client, &client.api_server, mr)?;
321 match result {
322 MergeResult::NothingToDo => {
323 debug!("Check next MR...");
324 }
325 _ => {
326 info!("Some action ongoing, exiting {:?}", result);
327 break;
328 }
329 }
330 }
331 }
332 Ok(())
333}