use std::collections::HashSet;
use anyhow::Result;
use log::{debug, error, info};
use url::percent_encoding::{utf8_percent_encode, PATH_SEGMENT_ENCODE_SET};
use url::Url;
use crate::client::Client;
use crate::entities::*;
type ReqwestClient = reqwest::blocking::Client;
impl MergeRequest {
pub fn list_ready_to_merge(
client: &Client,
project: &Option<String>,
merge_labels: &str,
merge_emoji: &str,
) -> Result<Vec<MergeRequest>> {
let prefix = if let Some(aproject) = project {
let project_name_or_id =
utf8_percent_encode(aproject, PATH_SEGMENT_ENCODE_SET).to_string();
format!("api/v4/projects/{}/merge_requests?", project_name_or_id)
} else {
"api/v4/merge_requests?scope=assigned_to_me&".to_string()
};
let api_path = format!(
"{}state=opened&order_by=created_at&labels={}&my_reaction_emoji={}",
prefix, merge_labels, merge_emoji
);
debug!("Ready to query {:?}", api_path);
let mut merge_requests: Vec<MergeRequest> =
client.get(&api_path)?.error_for_status()?.json()?;
debug!("{} merge requests found", merge_requests.len());
merge_requests.retain(|mr| {
debug!("{:?}", mr);
!mr.work_in_progress
&& mr.merge_status == "can_be_merged"
&& mr.target_branch == "master"
});
Ok(merge_requests)
}
pub fn infos(&self, client: &ReqwestClient, api_server: &Url) -> Result<MergeRequestDetails> {
let mr_uri = format!(
"api/v4/projects/{}/merge_requests/{}",
self.project_id, self.iid
);
let info_mr_uri = api_server.join(&mr_uri)?;
let message = GetMergeRequestDetails::from(self);
let resp = client
.get(info_mr_uri.as_str())
.json(&message)
.send()?
.json()?;
Ok(resp)
}
fn auto_remove_branch(&self, client: &ReqwestClient, api_server: &Url) -> Result<MergeRequest> {
let mr_uri = format!(
"api/v4/projects/{}/merge_requests/{}",
self.project_id, self.iid
);
let update_mr_url = api_server.join(&mr_uri)?;
let force_merge = RemoveSourceBranch::from(self);
debug!(" Update url: {:?} <- {:?}", update_mr_url, force_merge);
let resp = client
.put(update_mr_url.as_str())
.json(&force_merge)
.send()?
.json()?;
Ok(resp)
}
pub fn pipelines(&self, client: &Client) -> Result<Vec<Pipeline>> {
let pipelines_uri = format!(
"api/v4/projects/{}/merge_requests/{}/pipelines",
self.project_id, self.iid
);
let message = ListPipelines::from(self);
debug!("querying pipelines: {:?} {:?}", pipelines_uri, message);
let resp = client.get_json(&pipelines_uri, &message)?.json()?;
Ok(resp)
}
fn accept(&self, client: &ReqwestClient, api_server: &Url) -> Result<MergeRequest> {
let accept_uri = format!(
"api/v4/projects/{}/merge_requests/{}/merge",
self.project_id, self.iid
);
let accept_mr_uri = api_server.join(&accept_uri)?;
let message = AcceptMergeRequest::from(self);
let resp = client.put(accept_mr_uri.as_str()).json(&message).send()?;
debug!("accept response: {:?}", resp);
let resp = resp.json()?;
Ok(resp)
}
fn rebase(&self, client: &ReqwestClient, api_server: &Url) -> Result<()> {
let rebase_uri = format!(
"api/v4/projects/{}/merge_requests/{}/rebase",
self.project_id, self.iid
);
let rebase_mr_url = api_server.join(&rebase_uri)?;
let message = RebaseBranch::from(self);
let _resp = client.put(rebase_mr_url.as_str()).json(&message).send()?;
Ok(())
}
pub fn create(client: &Client, mr: &CreateMergeRequest) -> Result<MergeRequest> {
let api_path = {
let project_name_or_id =
utf8_percent_encode(&mr.project_id, PATH_SEGMENT_ENCODE_SET).to_string();
format!("api/v4/projects/{}/merge_requests", project_name_or_id)
};
let response = client.post_json(&api_path, &mr)?;
let de = &mut serde_json::Deserializer::from_reader(response);
let result: Result<MergeRequest, _> = serde_path_to_error::deserialize(de);
match result {
Ok(mr) => Ok(mr),
Err(err) => {
error!("Path: {}", err.path().to_string());
error!("Body: {:?}", client.post_json(&api_path, &mr)?.bytes()?);
Err(err.into_inner().into())
}
}
}
pub fn single(client: &Client, project: &str, mr_iid: usize) -> Result<MergeRequest> {
let api_path = {
let project_name_or_id =
utf8_percent_encode(project, PATH_SEGMENT_ENCODE_SET).to_string();
format!(
"api/v4/projects/{}/merge_requests/{}",
project_name_or_id, mr_iid
)
};
let response = client.get(&api_path)?;
let de = &mut serde_json::Deserializer::from_reader(response);
let result: Result<MergeRequest, _> = serde_path_to_error::deserialize(de);
match result {
Ok(mr) => Ok(mr),
Err(err) => {
dbg!(&api_path);
dbg!(err.path().to_string());
dbg!(client.get(&api_path)?.text()?);
Err(err.into_inner().into())
}
}
}
pub fn list(
client: &Client,
project: &Option<String>,
state: &MergeRequestState,
labels: &Option<String>,
milestone: &Option<String>,
source_branch: &Option<String>,
limit: usize,
) -> Result<Vec<MergeRequest>> {
let prefix = if let Some(aproject) = project {
let project_name_or_id =
utf8_percent_encode(aproject, PATH_SEGMENT_ENCODE_SET).to_string();
format!("api/v4/projects/{}", project_name_or_id)
} else {
"api/v4".to_string()
};
let scope = {
if project.is_some() {
"all"
} else {
"assigned_to_me" }
};
let mut api_path = format!(
"{}/merge_requests?state={}&scope={}",
prefix,
state.name(),
scope
);
if let Some(labels) = labels {
api_path = format!("{}&labels={}", api_path, labels);
}
if let Some(milestone) = milestone {
api_path = format!("{}&milestone={}", api_path, milestone);
}
if let Some(source_branch) = source_branch {
api_path = format!("{}&source_branch={}", api_path, source_branch);
}
debug!("Ready to query {:?}", api_path);
let mut all_merge_requests: Vec<MergeRequest> = vec![];
for merge_requests in client.get_paginated(&api_path) {
let merge_requests = merge_requests?;
let de = &mut serde_json::Deserializer::from_reader(merge_requests);
let result: Result<Vec<MergeRequest>, _> = serde_path_to_error::deserialize(de);
let merge_requests = match result {
Ok(merge_requests) => merge_requests,
Err(err) => {
dbg!(&api_path);
dbg!(err.path().to_string());
dbg!(client.get(&api_path)?.text()?);
return Err(err.into_inner().into());
}
};
all_merge_requests.extend(merge_requests.into_iter().map(|mut mr| {
let main_issue =
Issue::issue_from_branch(client, &mr.project_id.to_string(), &mr.source_branch)
.transpose()
.ok()
.flatten();
mr.main_issue = main_issue;
mr
}));
if all_merge_requests.len() >= limit {
break;
}
}
Ok(all_merge_requests)
}
}
#[derive(Debug)]
pub enum MergeResult {
RebaseInProgress(MergeRequestDetails),
Merged(MergeRequest),
Rebasing(MergeRequest),
NothingToDo,
}
pub fn try_to_merge(
client: &ReqwestClient,
api_server: &Url,
mr: MergeRequest,
) -> Result<MergeResult> {
let details = mr.infos(client, api_server)?;
debug!("Trying to merge {:?}", details);
if Some(true) == details.rebase_in_progress {
info!("Rebasing MR {:?}, need to wait", details);
return Ok(MergeResult::RebaseInProgress(details));
}
if let Some(pipeline) = details.pipeline {
if pipeline.status == "success" {
mr.auto_remove_branch(client, api_server)?;
debug!("----- Try to accept");
if let Ok(amr) = mr.accept(client, api_server) {
info!("Going to merge MR {:?}", amr);
return Ok(MergeResult::Merged(amr));
} else {
debug!("----- About to rebase");
mr.rebase(client, api_server)?;
return Ok(MergeResult::Rebasing(mr));
}
}
}
return Ok(MergeResult::NothingToDo);
}
pub fn mergebot(
client: &Client,
project: &Option<String>,
merge_labels: &str,
merge_emoji: &str,
) -> Result<()> {
let resp = MergeRequest::list_ready_to_merge(&client, project, &merge_labels, &merge_emoji)?;
if resp.is_empty() {
info!("No MR marked for merge");
return Ok(());
}
let mut already_rebased = HashSet::new();
for mr in resp.iter() {
let details = mr.infos(&client.client, &client.api_server)?;
if let Some(pipeline) = details.pipeline {
if pipeline.status == "pending" || pipeline.status == "running" {
info!("Some pipeline is running, not a good time to do anything");
return Ok(());
}
}
if details.diverged_commits_count.unwrap_or(0) == 0 {
already_rebased.insert(details.iid);
}
}
for mr in resp.into_iter().rev() {
if already_rebased.is_empty() || already_rebased.contains(&mr.iid) {
let result = try_to_merge(&client.client, &client.api_server, mr)?;
match result {
MergeResult::NothingToDo => {
debug!("Check next MR...");
}
_ => {
info!("Some action ongoing, exiting {:?}", result);
break;
}
}
}
}
Ok(())
}