cascade_cli/cli/commands/
conflicts.rs1use 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 #[arg(long)]
11 pub detailed: bool,
12
13 #[arg(long)]
15 pub auto_only: bool,
16
17 #[arg(long)]
19 pub manual_only: bool,
20
21 #[arg(value_name = "FILE")]
23 pub files: Vec<String>,
24}
25
26pub async fn run(args: ConflictsArgs) -> Result<()> {
28 let git_repo = get_current_repository()?;
29
30 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 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 let analyzer = ConflictAnalyzer::new();
54 let analysis = analyzer.analyze_conflicts(&conflicted_files, git_repo.path())?;
55
56 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 if !analysis.recommendations.is_empty() {
70 Output::section("Recommendations");
71 for recommendation in &analysis.recommendations {
72 Output::sub_item(recommendation);
73 }
74 }
75
76 Output::section("File Analysis");
78
79 for file_analysis in &analysis.files {
80 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 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 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 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 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
197pub 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 display_conflict_stats(&type_counts);
235 }
236
237 #[test]
238 fn test_empty_conflict_stats() {
239 let type_counts = HashMap::new();
240
241 display_conflict_stats(&type_counts);
243 }
244}