branch_party_core 0.1.1

Core library for branch-party CLI tool
Documentation
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
    }

    /// Execute a merge plan
    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(),
        );

        // Pre-flight checks
        self.validate_execution_environment(plan, options)?;

        // Setup progress bar
        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)?;
            }
        }

        // Finalize 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");

        // Check if working tree is clean (unless explicitly allowed)
        self.git_repo.ensure_clean_working_tree(options.allow_dirty)?;

        // Validate all branches exist
        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);

            // Simulate the merge result
            let result = if step.is_fast_forward {
                // Fast-forward merges typically succeed
                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() {
                // Simulate conflicts
                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 {
                // Simulate regular merge
                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");

        // Create or reset the party branch
        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);

                    // Decide whether to continue or abort
                    match plan.conflict_policy.default {
                        ConflictMode::Manual => {
                            // In real implementation, this would prompt the user
                            error!("Manual conflict resolution required");
                            break;
                        }
                        _ => {
                            debug!("Continuing with next merge step");
                        }
                    }
                }
            }
        }

        // Push if requested and all merges were successful
        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);
                }
            }
        }

        // Create a tag for this run
        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);

        // Check for path-specific overrides first
        for override_rule in &policy.overrides {
            if self.path_matches(&override_rule.path, step) {
                return self.apply_conflict_resolution(&override_rule.mode, step);
            }
        }

        // Apply default policy
        self.apply_conflict_resolution(&policy.default, step)
    }

    fn path_matches(&self, _pattern: &str, _step: &crate::MergeStep) -> bool {
        // In a real implementation, this would check if any conflicted files
        // match the glob pattern. For now, we'll just return false.
        false
    }

    fn apply_conflict_resolution(&self, mode: &ConflictMode, step: &crate::MergeStep) -> Result<MergeResult> {
        match mode {
            ConflictMode::Ours => {
                debug!("Resolving conflicts using 'ours' strategy");
                // In real implementation, would use git checkout --ours
                Ok(MergeResult::success(step.source.clone(), "conflict_resolved_ours".to_string()))
            }
            ConflictMode::Theirs => {
                debug!("Resolving conflicts using 'theirs' strategy");
                // In real implementation, would use git checkout --theirs
                Ok(MergeResult::success(step.source.clone(), "conflict_resolved_theirs".to_string()))
            }
            ConflictMode::Union => {
                debug!("Resolving conflicts using 'union' strategy");
                // In real implementation, would use git merge -X union
                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))
            }
        }
    }

    /// Check if the executor can continue after a conflict
    pub fn can_continue_after_conflict(&self, result: &MergeResult) -> bool {
        match result.status {
            crate::report::MergeStatus::Conflicted => false,
            crate::report::MergeStatus::Failed => false,
            _ => true,
        }
    }
}

/// Create an execution options builder for fluent configuration
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);

        // Just test that we can create the executor
        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);
    }
}