branch_party_core 0.1.1

Core library for branch-party CLI tool
Documentation
use crate::{
    Config, MergePlan, MergeStep, MergeOrderStrategy, ConflictPolicy, GitRepo, 
    Error, Result
};
use crate::party_resolver::PartyResolver;
use tracing::{debug, info, warn};

pub struct MergePlanner<'a> {
    config: &'a Config,
    git_repo: &'a GitRepo,
}

impl<'a> MergePlanner<'a> {
    pub fn new(config: &'a Config, git_repo: &'a GitRepo) -> Self {
        Self { config, git_repo }
    }

    /// Create a merge plan for a party
    pub fn create_plan(&self, party_name: &str) -> Result<MergePlan> {
        info!("Creating merge plan for party: {}", party_name);

        // Resolve the party to get all branches
        let resolver = PartyResolver::new(self.config);
        let resolved = resolver.resolve_party(party_name)?;

        let party_config = self.config.parties.get(party_name)
            .ok_or_else(|| Error::party_not_found(party_name))?;

        // Determine merge order
        let ordered_branches = self.order_branches(&resolved.branches, &party_config.merge_order)?;

        // Create destination branch name
        let destination = format!("party/{}", party_name);

        // Create merge plan
        let mut plan = MergePlan::new(
            party_name.to_string(),
            destination,
            self.config.base_branch.clone(),
            ordered_branches.clone(),
            party_config.merge_order.clone(),
            party_config.conflict_policy.clone(),
        );

        // Create merge steps
        for (i, source_branch) in ordered_branches.iter().enumerate() {
            let target_branch = if i == 0 {
                self.config.base_branch.clone()
            } else {
                plan.destination.clone()
            };

            let mut step = MergeStep::new(source_branch.clone(), target_branch.clone());
            
            // Analyze the merge to predict conflicts and fast-forwards
            if let Ok(analysis) = self.analyze_merge(source_branch, &target_branch) {
                if analysis.is_fast_forward {
                    step = step.as_fast_forward();
                }
                if !analysis.conflicted_files.is_empty() {
                    step = step.with_conflicts(analysis.conflicted_files);
                }
            }

            plan.add_step(step);
        }

        info!("Created merge plan with {} steps", plan.steps.len());
        debug!("Plan details: {} sources, {} estimated conflicts, {} fast-forwards", 
               plan.total_sources(), plan.estimated_conflicts(), plan.fast_forward_count());

        Ok(plan)
    }

    /// Order branches according to the merge strategy
    fn order_branches(&self, branches: &[String], strategy: &MergeOrderStrategy) -> Result<Vec<String>> {
        match strategy {
            MergeOrderStrategy::Listed => {
                // Keep the order as resolved (dependencies first, then listed order)
                Ok(branches.to_vec())
            }
            MergeOrderStrategy::NewestFirst => {
                self.order_by_commit_time(branches, true)
            }
            MergeOrderStrategy::OldestFirst => {
                self.order_by_commit_time(branches, false)
            }
        }
    }

    /// Order branches by commit timestamp
    fn order_by_commit_time(&self, branches: &[String], newest_first: bool) -> Result<Vec<String>> {
        let mut branch_times = Vec::new();

        for branch in branches {
            match self.git_repo.get_commit_id(branch) {
                Ok(commit_id) => {
                    // For now, we'll use a simple ordering. In a real implementation,
                    // we'd get the actual commit timestamp from git2
                    branch_times.push((branch.clone(), commit_id));
                }
                Err(_) => {
                    warn!("Could not get commit info for branch: {}", branch);
                    // Put branches we can't analyze at the end
                    branch_times.push((branch.clone(), String::new()));
                }
            }
        }

        // Sort by commit ID (this is a placeholder - real implementation would use timestamps)
        branch_times.sort_by(|a, b| {
            if newest_first {
                b.1.cmp(&a.1)
            } else {
                a.1.cmp(&b.1)
            }
        });

        Ok(branch_times.into_iter().map(|(branch, _)| branch).collect())
    }

    /// Create a merge plan from explicit branch list (for CLI usage)
    pub fn create_plan_from_branches(
        &self, 
        base_branch: &str,
        branches: &[String],
        strategy: MergeOrderStrategy,
        conflict_policy: ConflictPolicy,
    ) -> Result<MergePlan> {
        info!("Creating merge plan from explicit branch list: {:?}", branches);

        // Order branches according to strategy
        let ordered_branches = self.order_branches(branches, &strategy)?;

        // Create a temporary destination name
        let destination = format!("party/temp-{}", uuid::Uuid::new_v4().to_string()[..8].to_string());

        let mut plan = MergePlan::new(
            "temp".to_string(),
            destination,
            base_branch.to_string(),
            ordered_branches.clone(),
            strategy,
            conflict_policy,
        );

        // Create merge steps
        for (i, source_branch) in ordered_branches.iter().enumerate() {
            let target_branch = if i == 0 {
                base_branch.to_string()
            } else {
                plan.destination.clone()
            };

            let mut step = MergeStep::new(source_branch.clone(), target_branch.clone());
            
            // Analyze the merge
            if let Ok(analysis) = self.analyze_merge(source_branch, &target_branch) {
                if analysis.is_fast_forward {
                    step = step.as_fast_forward();
                }
                if !analysis.conflicted_files.is_empty() {
                    step = step.with_conflicts(analysis.conflicted_files);
                }
            }

            plan.add_step(step);
        }

        Ok(plan)
    }

    /// Analyze a potential merge to predict conflicts and fast-forwards
    fn analyze_merge(&self, source: &str, target: &str) -> Result<MergeAnalysis> {
        debug!("Analyzing merge: {} -> {}", source, target);

        // This is a simplified analysis. A full implementation would use git2 to:
        // 1. Find merge base
        // 2. Check if it's a fast-forward (merge_base == target_commit)
        // 3. Simulate merge to detect conflicts
        // 4. List conflicted files

        // For now, return a simple analysis
        Ok(MergeAnalysis {
            is_fast_forward: false,
            conflicted_files: Vec::new(),
            merge_base: None,
        })
    }

    /// Validate that all branches in a plan exist
    pub fn validate_plan(&self, plan: &MergePlan) -> Result<()> {
        // Check base branch exists
        if !self.git_repo.branch_exists(&plan.base_branch)? {
            return Err(Error::branch_not_found(&plan.base_branch));
        }

        // Check all source branches exist
        for source in &plan.sources {
            if !self.git_repo.branch_exists(source)? {
                return Err(Error::branch_not_found(source));
            }
        }

        Ok(())
    }

    /// Get statistics about a merge plan
    pub fn get_plan_stats(&self, plan: &MergePlan) -> PlanStats {
        PlanStats {
            total_merges: plan.steps.len(),
            fast_forwards: plan.fast_forward_count(),
            predicted_conflicts: plan.estimated_conflicts(),
            source_branches: plan.sources.len(),
        }
    }
}

#[derive(Debug)]
struct MergeAnalysis {
    is_fast_forward: bool,
    conflicted_files: Vec<String>,
    #[allow(dead_code)] // Reserved for future merge base analysis
    merge_base: Option<String>,
}

#[derive(Debug)]
pub struct PlanStats {
    pub total_merges: usize,
    pub fast_forwards: usize,
    pub predicted_conflicts: usize,
    pub source_branches: usize,
}

impl PlanStats {
    pub fn complexity_score(&self) -> f64 {
        // Simple complexity scoring
        let base_score = self.total_merges as f64;
        let conflict_penalty = self.predicted_conflicts as f64 * 2.0;
        let fast_forward_bonus = self.fast_forwards as f64 * -0.5;
        
        base_score + conflict_penalty + fast_forward_bonus
    }
}

/// Format a merge plan for display
pub fn format_plan(plan: &MergePlan, verbose: bool) -> String {
    let mut output = String::new();
    
    output.push_str(&format!("Merge Plan for party '{}'\n", plan.party_name));
    output.push_str(&format!("Destination: {}\n", plan.destination));
    output.push_str(&format!("Base: {}\n", plan.base_branch));
    output.push_str(&format!("Strategy: {}\n", plan.strategy));
    output.push_str(&format!("Sources: {} branches\n\n", plan.sources.len()));
    
    if verbose {
        output.push_str("Merge Steps:\n");
        for (i, step) in plan.steps.iter().enumerate() {
            let step_type = if step.is_fast_forward { 
                "[FF]" 
            } else if !step.expected_conflicts.is_empty() { 
                "[CONFLICT]" 
            } else { 
                "[MERGE]" 
            };
            
            output.push_str(&format!("  {}. {} {} -> {}\n", 
                i + 1, step_type, step.source, step.target));
            
            if !step.expected_conflicts.is_empty() {
                output.push_str(&format!("     Potential conflicts: {}\n", 
                    step.expected_conflicts.join(", ")));
            }
        }
    } else {
        // Summary view
        let stats = PlanStats {
            total_merges: plan.steps.len(),
            fast_forwards: plan.fast_forward_count(),
            predicted_conflicts: plan.estimated_conflicts(),
            source_branches: plan.sources.len(),
        };
        
        output.push_str(&format!("Summary:\n"));
        output.push_str(&format!("  {} merge steps\n", stats.total_merges));
        output.push_str(&format!("  {} fast-forwards\n", stats.fast_forwards));
        output.push_str(&format!("  {} predicted conflicts\n", stats.predicted_conflicts));
        output.push_str(&format!("  Complexity score: {:.1}\n", stats.complexity_score()));
    }
    
    output
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::{Config, Party, AutoUpdateConfig};
    use std::collections::HashMap;
    use tempfile::TempDir;

    fn create_test_config() -> Config {
        let mut parties = HashMap::new();
        
        parties.insert("qa".to_string(), Party {
            members: vec!["feature/a".to_string(), "feature/b".to_string()],
            merge_order: MergeOrderStrategy::Listed,
            conflict_policy: ConflictPolicy::default(),
        });

        Config {
            base_branch: "main".to_string(),
            parties: HashMap::new(),
            auto_update: AutoUpdateConfig::default(),
        }
    }

    #[test]
    fn test_plan_creation() {
        let config = create_test_config();
        let temp_dir = TempDir::new().unwrap();
        
        // Create a test git repo (this would need proper setup in real tests)
        let repo = git2::Repository::init(temp_dir.path()).unwrap();
        let git_repo = GitRepo::open(temp_dir.path()).unwrap();
        
        let planner = MergePlanner::new(&config, &git_repo);
        
        // This test would need actual git setup to work properly
        // For now, just test that the planner can be created
        assert!(planner.config.parties.contains_key("qa"));
    }

    #[test]
    fn test_branch_ordering() {
        let config = create_test_config();
        let temp_dir = TempDir::new().unwrap();
        let repo = git2::Repository::init(temp_dir.path()).unwrap();
        let git_repo = GitRepo::open(temp_dir.path()).unwrap();
        
        let planner = MergePlanner::new(&config, &git_repo);
        
        let branches = vec!["feature/a".to_string(), "feature/b".to_string()];
        let ordered = planner.order_branches(&branches, &MergeOrderStrategy::Listed).unwrap();
        
        assert_eq!(ordered, branches);
    }

    #[test]
    fn test_plan_stats() {
        let plan = MergePlan::new(
            "test".to_string(),
            "party/test".to_string(),
            "main".to_string(),
            vec!["feature/a".to_string(), "feature/b".to_string()],
            MergeOrderStrategy::Listed,
            ConflictPolicy::default(),
        );

        let stats = PlanStats {
            total_merges: 2,
            fast_forwards: 1,
            predicted_conflicts: 0,
            source_branches: 2,
        };

        assert_eq!(stats.complexity_score(), 1.5); // 2 - 0.5 = 1.5
    }
}