cascade_cli/cli/commands/
conflicts.rs

1use crate::cli::output::Output;
2use crate::errors::Result;
3use crate::git::{get_current_repository, ConflictAnalyzer, ConflictType};
4use clap::Args;
5use std::collections::HashMap;
6
7#[derive(Debug, Args)]
8pub struct ConflictsArgs {
9    /// Show detailed information about each conflict
10    #[arg(long)]
11    pub detailed: bool,
12
13    /// Only show conflicts that can be auto-resolved
14    #[arg(long)]
15    pub auto_only: bool,
16
17    /// Only show conflicts that require manual resolution
18    #[arg(long)]
19    pub manual_only: bool,
20
21    /// Analyze specific files (if not provided, analyzes all conflicted files)
22    #[arg(value_name = "FILE")]
23    pub files: Vec<String>,
24}
25
26/// Analyze conflicts in the repository
27pub async fn run(args: ConflictsArgs) -> Result<()> {
28    let git_repo = get_current_repository()?;
29
30    // Check if there are any conflicts
31    let has_conflicts = git_repo.has_conflicts()?;
32
33    if !has_conflicts {
34        Output::success("No conflicts found in the repository");
35        return Ok(());
36    }
37
38    // Get conflicted files
39    let conflicted_files = if args.files.is_empty() {
40        git_repo.get_conflicted_files()?
41    } else {
42        args.files
43    };
44
45    if conflicted_files.is_empty() {
46        Output::success("No conflicted files found");
47        return Ok(());
48    }
49
50    Output::section("Conflict Analysis");
51
52    // Analyze conflicts
53    let analyzer = ConflictAnalyzer::new();
54    let analysis = analyzer.analyze_conflicts(&conflicted_files, git_repo.path())?;
55
56    // Display summary
57    Output::sub_item(format!("Total conflicted files: {}", analysis.files.len()));
58    Output::sub_item(format!("Total conflicts: {}", analysis.total_conflicts));
59    Output::sub_item(format!(
60        "Auto-resolvable: {}",
61        analysis.auto_resolvable_count
62    ));
63    Output::sub_item(format!(
64        "Manual resolution needed: {}",
65        analysis.total_conflicts - analysis.auto_resolvable_count
66    ));
67
68    // Display recommendations
69    if !analysis.recommendations.is_empty() {
70        Output::section("Recommendations");
71        for recommendation in &analysis.recommendations {
72            Output::sub_item(recommendation);
73        }
74    }
75
76    // Display file analysis
77    Output::section("File Analysis");
78
79    for file_analysis in &analysis.files {
80        // Apply filters
81        if args.auto_only && !file_analysis.auto_resolvable {
82            continue;
83        }
84        if args.manual_only && file_analysis.auto_resolvable {
85            continue;
86        }
87
88        let status_icon = if file_analysis.auto_resolvable {
89            "🤖"
90        } else {
91            "✋"
92        };
93
94        let difficulty_desc = match file_analysis.overall_difficulty {
95            crate::git::conflict_analysis::ConflictDifficulty::Easy => "Easy",
96            crate::git::conflict_analysis::ConflictDifficulty::Medium => "Medium",
97            crate::git::conflict_analysis::ConflictDifficulty::Hard => "Hard",
98        };
99
100        Output::sub_item(format!(
101            "{} {} ({} conflicts, {} difficulty)",
102            status_icon,
103            file_analysis.file_path,
104            file_analysis.conflicts.len(),
105            difficulty_desc
106        ));
107
108        if args.detailed {
109            // Show conflict type breakdown
110            let mut type_summary = Vec::new();
111            for (conflict_type, count) in &file_analysis.conflict_summary {
112                let type_name = match conflict_type {
113                    ConflictType::Whitespace => "Whitespace",
114                    ConflictType::LineEnding => "Line Endings",
115                    ConflictType::PureAddition => "Pure Addition",
116                    ConflictType::ImportMerge => "Import Merge",
117                    ConflictType::Structural => "Structural",
118                    ConflictType::ContentOverlap => "Content Overlap",
119                    ConflictType::Complex => "Complex",
120                };
121                type_summary.push(format!("{type_name}: {count}"));
122            }
123
124            if !type_summary.is_empty() {
125                Output::sub_item(format!("Types: {}", type_summary.join(", ")));
126            }
127
128            // Show individual conflicts
129            for (i, conflict) in file_analysis.conflicts.iter().enumerate() {
130                let conflict_type = match conflict.conflict_type {
131                    ConflictType::Whitespace => "📝 Whitespace",
132                    ConflictType::LineEnding => "â†Šī¸  Line Endings",
133                    ConflictType::PureAddition => "➕ Addition",
134                    ConflictType::ImportMerge => "đŸ“Ļ Import",
135                    ConflictType::Structural => "đŸ—ī¸  Structural",
136                    ConflictType::ContentOverlap => "🔄 Overlap",
137                    ConflictType::Complex => "🔍 Complex",
138                };
139
140                let strategy_desc = match &conflict.suggested_strategy {
141                    crate::git::conflict_analysis::ResolutionStrategy::TakeOurs => "Take ours",
142                    crate::git::conflict_analysis::ResolutionStrategy::TakeTheirs => "Take theirs",
143                    crate::git::conflict_analysis::ResolutionStrategy::Merge => "Merge both",
144                    crate::git::conflict_analysis::ResolutionStrategy::Custom(desc) => desc,
145                    crate::git::conflict_analysis::ResolutionStrategy::Manual => {
146                        "Manual resolution"
147                    }
148                };
149
150                Output::sub_item(format!(
151                    "{}. {} (lines {}-{}) - {}",
152                    i + 1,
153                    conflict_type,
154                    conflict.start_line,
155                    conflict.end_line,
156                    strategy_desc
157                ));
158
159                if !conflict.context.is_empty() {
160                    Output::sub_item(format!("   Context: {}", conflict.context));
161                }
162            }
163        }
164    }
165
166    // Display manual resolution files
167    if !analysis.manual_resolution_files.is_empty() {
168        Output::section("Files Requiring Manual Resolution");
169        for file in &analysis.manual_resolution_files {
170            Output::sub_item(format!("✋ {file}"));
171        }
172
173        Output::tip("Use 'ca conflicts --detailed' to see specific conflict types");
174        Output::tip("Use 'git mergetool' or your editor to resolve manual conflicts");
175    }
176
177    // Display auto-resolvable files
178    let auto_resolvable_files: Vec<&str> = analysis
179        .files
180        .iter()
181        .filter(|f| f.auto_resolvable)
182        .map(|f| f.file_path.as_str())
183        .collect();
184
185    if !auto_resolvable_files.is_empty() {
186        Output::section("Auto-resolvable Files");
187        for file in &auto_resolvable_files {
188            Output::sub_item(format!("🤖 {file}"));
189        }
190
191        Output::tip("These conflicts can be automatically resolved during rebase/sync");
192    }
193
194    Ok(())
195}
196
197/// Display conflict statistics
198pub fn display_conflict_stats(type_counts: &HashMap<ConflictType, usize>) {
199    if type_counts.is_empty() {
200        return;
201    }
202
203    Output::section("Conflict Types");
204
205    for (conflict_type, count) in type_counts {
206        let (icon, description) = match conflict_type {
207            ConflictType::Whitespace => ("📝", "Whitespace/formatting differences"),
208            ConflictType::LineEnding => ("â†Šī¸", "Line ending differences (CRLF vs LF)"),
209            ConflictType::PureAddition => ("➕", "Both sides added content"),
210            ConflictType::ImportMerge => ("đŸ“Ļ", "Import statements that can be merged"),
211            ConflictType::Structural => ("đŸ—ī¸", "Code structure changes"),
212            ConflictType::ContentOverlap => ("🔄", "Overlapping content changes"),
213            ConflictType::Complex => ("🔍", "Complex conflicts"),
214        };
215
216        Output::sub_item(format!("{icon} {description} - {count} conflicts"));
217    }
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223
224    use std::collections::HashMap;
225
226    #[test]
227    fn test_conflict_stats_display() {
228        let mut type_counts = HashMap::new();
229        type_counts.insert(ConflictType::Whitespace, 3);
230        type_counts.insert(ConflictType::ImportMerge, 2);
231        type_counts.insert(ConflictType::Complex, 1);
232
233        // This test just ensures the function doesn't panic
234        display_conflict_stats(&type_counts);
235    }
236
237    #[test]
238    fn test_empty_conflict_stats() {
239        let type_counts = HashMap::new();
240
241        // Should handle empty stats gracefully
242        display_conflict_stats(&type_counts);
243    }
244}