use crate::{
GitRepo, MergePlan, RunReport, MergeResult, ConflictPolicy, ConflictMode,
Error, Result, GitMergeResult
};
use std::time::Instant;
use tracing::{info, warn, debug, error};
use indicatif::{ProgressBar, ProgressStyle};
pub struct MergeExecutor<'a> {
git_repo: &'a GitRepo,
progress_bar: Option<ProgressBar>,
}
#[derive(Debug)]
pub enum ExecutionMode {
DryRun,
Execute,
}
#[derive(Debug)]
pub struct ExecutionOptions {
pub mode: ExecutionMode,
pub push_after_success: bool,
pub allow_dirty: bool,
pub auto_resolve_conflicts: bool,
}
impl Default for ExecutionOptions {
fn default() -> Self {
Self {
mode: ExecutionMode::Execute,
push_after_success: false,
allow_dirty: false,
auto_resolve_conflicts: true,
}
}
}
impl<'a> MergeExecutor<'a> {
pub fn new(git_repo: &'a GitRepo) -> Self {
Self {
git_repo,
progress_bar: None,
}
}
pub fn with_progress_bar(mut self, show_progress: bool) -> Self {
if show_progress {
let pb = ProgressBar::new(0);
pb.set_style(ProgressStyle::default_bar()
.template("{spinner:.green} [{elapsed_precise}] [{bar:40.cyan/blue}] {pos}/{len} {msg}")
.unwrap()
.progress_chars("#>-"));
self.progress_bar = Some(pb);
}
self
}
pub fn execute_plan(&self, plan: &MergePlan, options: &ExecutionOptions) -> Result<RunReport> {
info!("Executing merge plan for party: {}", plan.party_name);
let start_time = Instant::now();
let mut report = RunReport::new(
plan.destination.clone(),
plan.base_branch.clone(),
plan.sources.clone(),
plan.strategy.to_string(),
);
self.validate_execution_environment(plan, options)?;
if let Some(ref pb) = self.progress_bar {
pb.set_length(plan.steps.len() as u64);
pb.set_message("Preparing merge...");
}
match options.mode {
ExecutionMode::DryRun => {
info!("Running in dry-run mode - no changes will be made");
self.execute_dry_run(plan, &mut report)?;
}
ExecutionMode::Execute => {
info!("Executing merge plan");
self.execute_real_merge(plan, options, &mut report)?;
}
}
report.set_duration(start_time.elapsed().as_millis() as u64);
if let Some(ref pb) = self.progress_bar {
pb.finish_with_message("Complete");
}
info!("Merge execution completed in {}ms", report.duration_ms);
Ok(report)
}
fn validate_execution_environment(&self, plan: &MergePlan, options: &ExecutionOptions) -> Result<()> {
debug!("Validating execution environment");
self.git_repo.ensure_clean_working_tree(options.allow_dirty)?;
if !self.git_repo.branch_exists(&plan.base_branch)? {
return Err(Error::branch_not_found(&plan.base_branch));
}
for source in &plan.sources {
if !self.git_repo.branch_exists(source)? {
return Err(Error::branch_not_found(source));
}
}
Ok(())
}
fn execute_dry_run(&self, plan: &MergePlan, report: &mut RunReport) -> Result<()> {
info!("Performing dry run simulation");
for (i, step) in plan.steps.iter().enumerate() {
if let Some(ref pb) = self.progress_bar {
pb.set_position(i as u64);
pb.set_message(format!("Simulating: {}", step.source));
}
debug!("Simulating merge: {} -> {}", step.source, step.target);
let result = if step.is_fast_forward {
let commit_id = self.git_repo.get_commit_id(&step.source)
.unwrap_or_else(|_| "simulated_commit".to_string());
MergeResult::success(step.source.clone(), commit_id)
} else if !step.expected_conflicts.is_empty() {
let conflicts = step.expected_conflicts.iter()
.map(|path| crate::report::ConflictDetail {
path: path.clone(),
resolution: "simulated".to_string(),
details: "Would require manual resolution".to_string(),
})
.collect();
MergeResult::conflicted(step.source.clone(), conflicts)
} else {
let commit_id = format!("merge_{}", i);
MergeResult::success(step.source.clone(), commit_id)
};
report.add_result(result);
}
Ok(())
}
fn execute_real_merge(&self, plan: &MergePlan, options: &ExecutionOptions, report: &mut RunReport) -> Result<()> {
info!("Executing real merge operations");
let party_branch = self.git_repo.create_or_reset_party_branch(&plan.party_name, &plan.base_branch)?;
report.mark_branch_created();
let mut successful_merges = 0;
for (i, step) in plan.steps.iter().enumerate() {
if let Some(ref pb) = self.progress_bar {
pb.set_position(i as u64);
pb.set_message(format!("Merging: {}", step.source));
}
info!("Executing merge step {}: {} -> {}", i + 1, step.source, step.target);
match self.execute_merge_step(step, &plan.conflict_policy, options) {
Ok(result) => {
info!("Merge step {} completed successfully", i + 1);
successful_merges += 1;
report.add_result(result);
}
Err(e) => {
warn!("Merge step {} failed: {}", i + 1, e);
let result = MergeResult::failed(step.source.clone(), e.to_string());
report.add_result(result);
match plan.conflict_policy.default {
ConflictMode::Manual => {
error!("Manual conflict resolution required");
break;
}
_ => {
debug!("Continuing with next merge step");
}
}
}
}
}
if options.push_after_success && successful_merges == plan.steps.len() {
info!("Pushing party branch to remote");
match self.git_repo.push_branch(&party_branch) {
Ok(()) => {
report.mark_pushed();
info!("Successfully pushed party branch");
}
Err(e) => {
warn!("Failed to push party branch: {}", e);
}
}
}
let tag_name = format!("party-{}", chrono::Utc::now().format("%Y%m%d.%H%M%S"));
report.set_tag(tag_name);
Ok(())
}
fn execute_merge_step(
&self,
step: &crate::MergeStep,
conflict_policy: &ConflictPolicy,
_options: &ExecutionOptions,
) -> Result<MergeResult> {
debug!("Executing merge step: {} -> {}", step.source, step.target);
match self.git_repo.merge_branch(&step.source)? {
GitMergeResult::FastForward(commit_hash) => {
info!("Fast-forward merge completed");
Ok(MergeResult::success(step.source.clone(), commit_hash))
}
GitMergeResult::Merged(commit_hash) => {
info!("Regular merge completed");
Ok(MergeResult::success(step.source.clone(), commit_hash))
}
GitMergeResult::Conflicts => {
info!("Merge conflicts detected");
self.handle_conflicts(step, conflict_policy)
}
}
}
fn handle_conflicts(&self, step: &crate::MergeStep, policy: &ConflictPolicy) -> Result<MergeResult> {
debug!("Handling conflicts for step: {}", step.source);
for override_rule in &policy.overrides {
if self.path_matches(&override_rule.path, step) {
return self.apply_conflict_resolution(&override_rule.mode, step);
}
}
self.apply_conflict_resolution(&policy.default, step)
}
fn path_matches(&self, _pattern: &str, _step: &crate::MergeStep) -> bool {
false
}
fn apply_conflict_resolution(&self, mode: &ConflictMode, step: &crate::MergeStep) -> Result<MergeResult> {
match mode {
ConflictMode::Ours => {
debug!("Resolving conflicts using 'ours' strategy");
Ok(MergeResult::success(step.source.clone(), "conflict_resolved_ours".to_string()))
}
ConflictMode::Theirs => {
debug!("Resolving conflicts using 'theirs' strategy");
Ok(MergeResult::success(step.source.clone(), "conflict_resolved_theirs".to_string()))
}
ConflictMode::Union => {
debug!("Resolving conflicts using 'union' strategy");
Ok(MergeResult::success(step.source.clone(), "conflict_resolved_union".to_string()))
}
ConflictMode::Manual => {
debug!("Manual conflict resolution required");
let conflicts = vec![crate::report::ConflictDetail {
path: "unknown".to_string(),
resolution: "manual".to_string(),
details: "Manual resolution required".to_string(),
}];
Ok(MergeResult::conflicted(step.source.clone(), conflicts))
}
}
}
pub fn can_continue_after_conflict(&self, result: &MergeResult) -> bool {
match result.status {
crate::report::MergeStatus::Conflicted => false,
crate::report::MergeStatus::Failed => false,
_ => true,
}
}
}
pub struct ExecutionOptionsBuilder {
options: ExecutionOptions,
}
impl ExecutionOptionsBuilder {
pub fn new() -> Self {
Self {
options: ExecutionOptions::default(),
}
}
pub fn dry_run(mut self) -> Self {
self.options.mode = ExecutionMode::DryRun;
self
}
pub fn push_after_success(mut self) -> Self {
self.options.push_after_success = true;
self
}
pub fn allow_dirty(mut self) -> Self {
self.options.allow_dirty = true;
self
}
pub fn disable_auto_conflict_resolution(mut self) -> Self {
self.options.auto_resolve_conflicts = false;
self
}
pub fn build(self) -> ExecutionOptions {
self.options
}
}
impl Default for ExecutionOptionsBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{MergePlan, MergeStep, MergeOrderStrategy, ConflictPolicy};
use tempfile::TempDir;
#[test]
fn test_execution_options_builder() {
let options = ExecutionOptionsBuilder::new()
.dry_run()
.allow_dirty()
.build();
match options.mode {
ExecutionMode::DryRun => {},
_ => panic!("Expected dry run mode"),
}
assert!(options.allow_dirty);
}
#[test]
fn test_merge_executor_creation() {
let temp_dir = TempDir::new().unwrap();
let git_repo = GitRepo::open(temp_dir.path()).unwrap();
let executor = MergeExecutor::new(&git_repo);
assert!(executor.progress_bar.is_none());
}
#[test]
fn test_execution_options_defaults() {
let options = ExecutionOptions::default();
match options.mode {
ExecutionMode::Execute => {},
_ => panic!("Expected execute mode by default"),
}
assert!(!options.push_after_success);
assert!(!options.allow_dirty);
assert!(options.auto_resolve_conflicts);
}
}