pub mod create_mr;
pub mod load_all_mrs;
pub mod push;
pub mod sync_dependent_merge_requests;
pub mod update_mr_base;
pub mod update_mr_title_description;
use std::collections::HashMap;
use bon::bon;
use enum_dispatch::enum_dispatch;
use futures::{StreamExt, stream::FuturesUnordered};
use itertools::{Either, Itertools};
use tracing::debug;
use crate::{
error::{ClonableError, Error, Result},
forge::AnyForgeMergeRequest,
submit::{
ExecuteContext,
execute::{
create_mr::CreateMRAction,
load_all_mrs::LoadAllMRsAction,
push::PushAction,
sync_dependent_merge_requests::SyncDependentMergeRequestsAction,
update_mr_base::UpdateMRBaseAction,
update_mr_title_description::UpdateMRTitleDescriptionAction,
},
},
};
#[derive(Debug, Clone, PartialEq)]
#[enum_dispatch(ExecuteAction, ActionInfo)]
pub enum Action {
Push(PushAction),
CreateMR(CreateMRAction),
UpdateMRBase(UpdateMRBaseAction),
LoadAllMRs(LoadAllMRsAction),
UpdateMRTitleDescription(UpdateMRTitleDescriptionAction),
SyncDependentMergeRequests(SyncDependentMergeRequestsAction),
}
#[derive(Debug)]
pub struct SubmissionResult {
pub merge_requests: Vec<MRUpdate>,
pub errors: Vec<ClonableError>,
pub bookmarks_pushed: Vec<String>,
}
#[derive(Debug, Clone)]
pub struct MRUpdate {
pub mr: AnyForgeMergeRequest,
pub bookmark: String,
pub update_type: MRUpdateType,
}
#[derive(Debug, Clone)]
pub enum ActionResultData {
Pushed { bookmark: String, pushed: bool },
MRCreated(MRUpdate),
MRUpdated(MRUpdate),
MRsLoaded(HashMap<String, AnyForgeMergeRequest>),
DryRun,
}
#[derive(Debug, Clone)]
pub struct ActionResult {
pub id: String,
pub data: Result<ActionResultData, ClonableError>,
}
pub struct ExecuteActionContext<'a> {
pub execute: ExecuteContext<'a>,
pub current_results: Vec<ActionResult>,
}
#[enum_dispatch]
pub trait ExecuteAction {
async fn execute(&self, ctx: ExecuteActionContext<'_>) -> Result<ActionResultData>;
}
#[enum_dispatch]
pub trait ActionInfo {
fn id(&self) -> String;
fn group_text(&self) -> String;
fn text(&self) -> String;
fn substep_text(&self) -> String;
fn plan_text(&self) -> String;
fn dependencies(&self) -> Vec<String> {
vec![]
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MRUpdateType {
Unchanged,
Created,
Updated {
old_target: Option<String>,
new_target: Option<String>,
old_title: Option<String>,
new_title: Option<String>,
old_description: Option<String>,
new_description: Option<String>,
synced_dependent_merge_requests: bool,
},
}
#[bon]
impl MRUpdateType {
#[builder]
pub fn new_updated(
old_target: Option<String>,
new_target: Option<String>,
old_title: Option<String>,
new_title: Option<String>,
old_description: Option<String>,
new_description: Option<String>,
synced_dependent_merge_requests: Option<bool>,
) -> Self {
Self::Updated {
old_target,
new_target,
old_title,
new_title,
old_description,
new_description,
synced_dependent_merge_requests: synced_dependent_merge_requests.unwrap_or(false),
}
}
}
pub async fn execute(ctx: ExecuteContext<'_>) -> Result<SubmissionResult> {
let mut merge_requests = Vec::new();
let mut errors = Vec::new();
let mut bookmarks_pushed = Vec::new();
let mut current_results: Vec<ActionResult> = Vec::new();
if ctx.dry_run {
ctx.output.log_message("DRY RUN - No changes will be made");
}
ctx.output.log_current("Preparing submission");
for batch in &ctx.plan.actions {
ctx.output.log_current(&batch.first().unwrap().group_text());
let handles = FuturesUnordered::new();
for action in batch {
let (_ok_deps, err_deps): (HashMap<_, _>, Vec<_>) = action
.dependencies()
.iter()
.map(|id| {
current_results
.iter()
.find(|result| result.id == *id)
.unwrap_or_else(|| panic!("Dependency {} not found", id))
})
.partition_map(|dep| match &dep.data {
Ok(data) => Either::Left((&dep.id, data)),
Err(error) => Either::Right((&dep.id, error)),
});
if !err_deps.is_empty() {
debug!(
"Skipping action {} because dependencies failed: {}",
action.id(),
err_deps.iter().map(|(id, _)| id).join(", ")
);
current_results.push(ActionResult {
id: action.id(),
data: Err(Error::new(format!(
"Dependencies failed: {}",
err_deps.iter().map(|(id, _)| id).join(", ")
))
.to_clonable_error()),
});
continue;
}
let action_id = action.id();
let action_ctx = ExecuteActionContext {
execute: ctx.clone(),
current_results: current_results.clone(),
};
handles.push(async move {
let output = action_ctx.execute.output;
let _substep = output.start_substep(&action.substep_text());
let result = action.execute(action_ctx).await;
(action_id, result)
});
}
let results = handles.collect::<Vec<_>>().await;
for (action_id, result) in results {
current_results.push(ActionResult {
id: action_id,
data: result.map_err(|e| e.to_clonable_error()),
});
}
}
for result in current_results {
match result.data {
Ok(ActionResultData::Pushed { bookmark, pushed }) => {
if pushed {
bookmarks_pushed.push(bookmark);
}
}
Ok(ActionResultData::MRCreated(mr_update))
| Ok(ActionResultData::MRUpdated(mr_update)) => {
merge_requests.push(mr_update);
}
Ok(ActionResultData::DryRun) | Ok(ActionResultData::MRsLoaded(_)) => {}
Err(error) => {
errors.push(error);
}
}
}
Ok(SubmissionResult {
merge_requests,
errors,
bookmarks_pushed,
})
}