eazygit 0.5.1

A fast TUI for Git with staging, conflicts, rebase, and palette-first UX
Documentation
use crate::app::AppState;
use std::path::PathBuf;
use std::fs;

/// Conflict resolution suggestion
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct ConflictSuggestion {
    pub file_path: String,
    pub suggestion: String,
    pub confidence: ConflictConfidence,
    pub resolution_type: ResolutionType,
}

/// Confidence level for conflict resolution suggestions
#[derive(Debug, Clone, PartialEq, PartialOrd)]
pub enum ConflictConfidence {
    Low,
    Medium,
    High,
}

/// Type of resolution suggested
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub enum ResolutionType {
    UseOurs,
    UseTheirs,
    Merge,
    Manual,
}

impl ConflictSuggestion {
    /// Analyze a conflict file and suggest resolution
    pub fn analyze_conflict(repo_path: &str, file_path: &str) -> Option<Self> {
        let full_path = PathBuf::from(repo_path).join(file_path);
        
        // Read conflict file
        let content = match fs::read_to_string(&full_path) {
            Ok(c) => c,
            Err(_) => return None,
        };
        
        // Count conflict markers
        let ours_markers = content.matches("<<<<<<<").count();
        let theirs_markers = content.matches(">>>>>>>").count();
        let _separator_markers = content.matches("=======").count();
        
        if ours_markers == 0 || theirs_markers == 0 {
            return None; // Not a conflict file
        }
        
        // Analyze conflict patterns
        let (suggestion, confidence, resolution_type) = Self::analyze_patterns(&content);
        
        Some(Self {
            file_path: file_path.to_string(),
            suggestion,
            confidence,
            resolution_type,
        })
    }
    
    fn analyze_patterns(content: &str) -> (String, ConflictConfidence, ResolutionType) {
        // Check for common patterns
        
        // Pattern 1: Only whitespace differences
        if Self::is_whitespace_only(content) {
            return (
                "Conflict appears to be whitespace-only. Consider using 'theirs' or 'ours' based on your project's whitespace policy.".to_string(),
                ConflictConfidence::High,
                ResolutionType::UseTheirs, // Default to theirs for whitespace
            );
        }
        
        // Pattern 2: One side is empty (deletion vs modification)
        if Self::has_empty_side(content) {
            return (
                "One side deleted the content while the other modified it. Review carefully - you may want to keep the modified version or accept the deletion.".to_string(),
                ConflictConfidence::Medium,
                ResolutionType::Manual,
            );
        }
        
        // Pattern 3: Simple addition conflicts (non-overlapping)
        if Self::is_simple_addition(content) {
            return (
                "Both sides added different content. You can likely keep both sections.".to_string(),
                ConflictConfidence::Medium,
                ResolutionType::Merge,
            );
        }
        
        // Pattern 4: Import/header conflicts
        if Self::is_import_conflict(content) {
            return (
                "Conflict in imports/headers. You may need to merge both import lists and remove duplicates.".to_string(),
                ConflictConfidence::High,
                ResolutionType::Merge,
            );
        }
        
        // Pattern 5: Version number conflicts
        if Self::is_version_conflict(content) {
            return (
                "Version number conflict detected. Usually you want to keep the higher version number.".to_string(),
                ConflictConfidence::High,
                ResolutionType::UseTheirs, // Default to theirs (usually newer)
            );
        }
        
        // Default: manual resolution needed
        (
            "Complex conflict detected. Manual resolution required. Review both versions carefully.".to_string(),
            ConflictConfidence::Low,
            ResolutionType::Manual,
        )
    }
    
    fn is_whitespace_only(content: &str) -> bool {
        // Extract conflict sections
        let sections: Vec<&str> = content
            .split("=======")
            .collect();
        
        if sections.len() < 2 {
            return false;
        }
        
        // Get ours and theirs sections (simplified - real parsing would be more complex)
        for section in sections {
            let cleaned: String = section
                .chars()
                .filter(|c| !c.is_whitespace())
                .collect();
            if !cleaned.is_empty() && cleaned.len() < 10 {
                // Very short non-whitespace content suggests whitespace conflict
                continue;
            }
        }
        
        false // Simplified - would need better parsing
    }
    
    fn has_empty_side(content: &str) -> bool {
        let parts: Vec<&str> = content.split("=======").collect();
        if parts.len() < 2 {
            return false;
        }
        
        // Check if one side is essentially empty (only whitespace/newlines)
        parts.iter().any(|part| {
            part.trim_matches(|c: char| c == '\n' || c == '\r' || c.is_whitespace())
                .is_empty()
        })
    }
    
    fn is_simple_addition(content: &str) -> bool {
        // Check if conflict markers are far apart (suggesting non-overlapping additions)
        if let Some(ours_pos) = content.find("<<<<<<<") {
            if let Some(sep_pos) = content[ours_pos..].find("=======") {
                if let Some(theirs_pos) = content[ours_pos + sep_pos..].find(">>>>>>>") {
                    // Check if sections don't overlap significantly
                    let ours_section = &content[ours_pos + 7..ours_pos + sep_pos];
                    let theirs_section = &content[ours_pos + sep_pos + 7..ours_pos + sep_pos + theirs_pos];
                    
                    // Simple heuristic: if sections are on different lines and don't share much content
                    return ours_section.lines().count() > 0 
                        && theirs_section.lines().count() > 0
                        && !ours_section.trim().is_empty()
                        && !theirs_section.trim().is_empty();
                }
            }
        }
        false
    }
    
    fn is_import_conflict(content: &str) -> bool {
        // Check for import/header patterns
        content.contains("import") 
            || content.contains("include") 
            || content.contains("#include")
            || content.contains("use ")
            || content.contains("package ")
    }
    
    fn is_version_conflict(content: &str) -> bool {
        // Check for version number patterns (simple heuristic)
        content.contains("version") || content.contains("Version")
    }
}

/// Get suggestions for all conflicts
#[allow(dead_code)]
pub fn get_conflict_suggestions(state: &AppState) -> Vec<ConflictSuggestion> {
    let conflicts: Vec<&crate::git::parsers::status::StatusEntry> = state
        .status_entries
        .iter()
        .filter(|e| e.conflict)
        .collect();
    
    conflicts
        .iter()
        .filter_map(|entry| {
            ConflictSuggestion::analyze_conflict(&state.repo_path, &entry.path)
        })
        .collect()
}