1use crate::errors::{CascadeError, Result};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4
5#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
7pub enum ConflictType {
8 Whitespace,
10 LineEnding,
12 PureAddition,
14 ImportMerge,
16 Structural,
18 ContentOverlap,
20 Complex,
22}
23
24#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
26pub enum ConflictDifficulty {
27 Easy,
29 Medium,
31 Hard,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
37pub enum ResolutionStrategy {
38 TakeOurs,
40 TakeTheirs,
42 Merge,
44 Custom(String),
46 Manual,
48}
49
50#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct ConflictRegion {
53 pub file_path: String,
55 pub start_pos: usize,
57 pub end_pos: usize,
59 pub start_line: usize,
61 pub end_line: usize,
63 pub our_content: String,
65 pub their_content: String,
67 pub conflict_type: ConflictType,
69 pub difficulty: ConflictDifficulty,
71 pub suggested_strategy: ResolutionStrategy,
73 pub context: String,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct FileConflictAnalysis {
80 pub file_path: String,
82 pub conflicts: Vec<ConflictRegion>,
84 pub overall_difficulty: ConflictDifficulty,
86 pub auto_resolvable: bool,
88 pub conflict_summary: HashMap<ConflictType, usize>,
90}
91
92#[derive(Debug, Clone, Serialize, Deserialize)]
94pub struct ConflictAnalysis {
95 pub files: Vec<FileConflictAnalysis>,
97 pub total_conflicts: usize,
99 pub auto_resolvable_count: usize,
101 pub manual_resolution_files: Vec<String>,
103 pub recommendations: Vec<String>,
105}
106
107pub struct ConflictAnalyzer {
109 file_patterns: HashMap<String, Vec<String>>,
111}
112
113impl ConflictAnalyzer {
114 pub fn new() -> Self {
116 let mut file_patterns = HashMap::new();
117
118 file_patterns.insert(
120 "rs".to_string(),
121 vec!["use ".to_string(), "extern crate ".to_string()],
122 );
123
124 file_patterns.insert(
126 "py".to_string(),
127 vec!["import ".to_string(), "from ".to_string()],
128 );
129
130 file_patterns.insert(
132 "js".to_string(),
133 vec!["import ".to_string(), "export ".to_string()],
134 );
135
136 file_patterns.insert(
137 "ts".to_string(),
138 vec![
139 "import ".to_string(),
140 "export ".to_string(),
141 "interface ".to_string(),
142 "type ".to_string(),
143 ],
144 );
145
146 Self { file_patterns }
147 }
148
149 pub fn analyze_file(&self, file_path: &str, content: &str) -> Result<FileConflictAnalysis> {
151 let conflicts = self.parse_conflict_markers(file_path, content)?;
152
153 let mut conflict_summary = HashMap::new();
154 let mut auto_resolvable_count = 0;
155
156 for conflict in &conflicts {
157 *conflict_summary
158 .entry(conflict.conflict_type.clone())
159 .or_insert(0) += 1;
160
161 if conflict.difficulty == ConflictDifficulty::Easy {
162 auto_resolvable_count += 1;
163 }
164 }
165
166 let overall_difficulty = self.assess_overall_difficulty(&conflicts);
167 let auto_resolvable = auto_resolvable_count == conflicts.len();
168
169 Ok(FileConflictAnalysis {
170 file_path: file_path.to_string(),
171 conflicts,
172 overall_difficulty,
173 auto_resolvable,
174 conflict_summary,
175 })
176 }
177
178 pub fn analyze_conflicts(
180 &self,
181 conflicted_files: &[String],
182 repo_path: &std::path::Path,
183 ) -> Result<ConflictAnalysis> {
184 let mut file_analyses = Vec::new();
185 let mut total_conflicts = 0;
186 let mut auto_resolvable_count = 0;
187 let mut manual_resolution_files = Vec::new();
188
189 for file_path in conflicted_files {
190 let full_path = repo_path.join(file_path);
191 let content = std::fs::read_to_string(&full_path)
192 .map_err(|e| CascadeError::config(format!("Failed to read {file_path}: {e}")))?;
193
194 let analysis = self.analyze_file(file_path, &content)?;
195
196 total_conflicts += analysis.conflicts.len();
197 auto_resolvable_count += analysis
198 .conflicts
199 .iter()
200 .filter(|c| c.difficulty == ConflictDifficulty::Easy)
201 .count();
202
203 if !analysis.auto_resolvable {
204 manual_resolution_files.push(file_path.clone());
205 }
206
207 file_analyses.push(analysis);
208 }
209
210 let recommendations = self.generate_recommendations(&file_analyses);
211
212 Ok(ConflictAnalysis {
213 files: file_analyses,
214 total_conflicts,
215 auto_resolvable_count,
216 manual_resolution_files,
217 recommendations,
218 })
219 }
220
221 fn parse_conflict_markers(
223 &self,
224 file_path: &str,
225 content: &str,
226 ) -> Result<Vec<ConflictRegion>> {
227 let lines: Vec<&str> = content.lines().collect();
228 let mut conflicts = Vec::new();
229 let mut i = 0;
230
231 while i < lines.len() {
232 if lines[i].starts_with("<<<<<<<") {
233 let start_line = i + 1;
235 let mut separator_line = None;
236 let mut end_line = None;
237
238 for (j, line) in lines.iter().enumerate().skip(i + 1) {
240 if line.starts_with("=======") {
241 separator_line = Some(j);
242 } else if line.starts_with(">>>>>>>") {
243 end_line = Some(j);
244 break;
245 }
246 }
247
248 if let (Some(sep), Some(end)) = (separator_line, end_line) {
249 let start_pos = lines[..i].iter().map(|l| l.len() + 1).sum::<usize>();
251 let end_pos = lines[..=end].iter().map(|l| l.len() + 1).sum::<usize>();
252
253 let our_content = lines[(i + 1)..sep].join("\n");
254 let their_content = lines[(sep + 1)..end].join("\n");
255
256 let conflict_region = self.analyze_conflict_region(
258 file_path,
259 start_pos,
260 end_pos,
261 start_line,
262 end + 1,
263 &our_content,
264 &their_content,
265 )?;
266
267 conflicts.push(conflict_region);
268 i = end;
269 } else {
270 i += 1;
271 }
272 } else {
273 i += 1;
274 }
275 }
276
277 Ok(conflicts)
278 }
279
280 #[allow(clippy::too_many_arguments)]
282 fn analyze_conflict_region(
283 &self,
284 file_path: &str,
285 start_pos: usize,
286 end_pos: usize,
287 start_line: usize,
288 end_line: usize,
289 our_content: &str,
290 their_content: &str,
291 ) -> Result<ConflictRegion> {
292 let conflict_type = self.classify_conflict_type(file_path, our_content, their_content);
293 let difficulty = self.assess_difficulty(&conflict_type, our_content, their_content);
294 let suggested_strategy =
295 self.suggest_resolution_strategy(&conflict_type, our_content, their_content);
296 let context = self.generate_context(&conflict_type, our_content, their_content);
297
298 Ok(ConflictRegion {
299 file_path: file_path.to_string(),
300 start_pos,
301 end_pos,
302 start_line,
303 end_line,
304 our_content: our_content.to_string(),
305 their_content: their_content.to_string(),
306 conflict_type,
307 difficulty,
308 suggested_strategy,
309 context,
310 })
311 }
312
313 fn classify_conflict_type(
315 &self,
316 file_path: &str,
317 our_content: &str,
318 their_content: &str,
319 ) -> ConflictType {
320 if self.normalize_whitespace(our_content) == self.normalize_whitespace(their_content) {
322 return ConflictType::Whitespace;
323 }
324
325 if self.normalize_line_endings(our_content) == self.normalize_line_endings(their_content) {
327 return ConflictType::LineEnding;
328 }
329
330 if our_content.is_empty() || their_content.is_empty() {
332 return ConflictType::PureAddition;
333 }
334
335 if self.is_import_conflict(file_path, our_content, their_content) {
337 return ConflictType::ImportMerge;
338 }
339
340 if self.is_structural_conflict(file_path, our_content, their_content) {
342 return ConflictType::Structural;
343 }
344
345 if self.has_content_overlap(our_content, their_content) {
347 return ConflictType::ContentOverlap;
348 }
349
350 ConflictType::Complex
352 }
353
354 fn assess_difficulty(
356 &self,
357 conflict_type: &ConflictType,
358 our_content: &str,
359 their_content: &str,
360 ) -> ConflictDifficulty {
361 match conflict_type {
362 ConflictType::Whitespace | ConflictType::LineEnding => ConflictDifficulty::Easy,
363 ConflictType::PureAddition => {
364 if our_content.lines().count() <= 5 && their_content.lines().count() <= 5 {
365 ConflictDifficulty::Easy
366 } else {
367 ConflictDifficulty::Medium
368 }
369 }
370 ConflictType::ImportMerge => ConflictDifficulty::Easy,
371 ConflictType::Structural => ConflictDifficulty::Medium,
372 ConflictType::ContentOverlap => ConflictDifficulty::Medium,
373 ConflictType::Complex => ConflictDifficulty::Hard,
374 }
375 }
376
377 fn suggest_resolution_strategy(
379 &self,
380 conflict_type: &ConflictType,
381 our_content: &str,
382 their_content: &str,
383 ) -> ResolutionStrategy {
384 match conflict_type {
385 ConflictType::Whitespace => {
386 if our_content.trim().len() >= their_content.trim().len() {
387 ResolutionStrategy::TakeOurs
388 } else {
389 ResolutionStrategy::TakeTheirs
390 }
391 }
392 ConflictType::LineEnding => {
393 ResolutionStrategy::Custom("Normalize to Unix line endings".to_string())
394 }
395 ConflictType::PureAddition => ResolutionStrategy::Merge,
396 ConflictType::ImportMerge => {
397 ResolutionStrategy::Custom("Sort and merge imports".to_string())
398 }
399 ConflictType::Structural => ResolutionStrategy::Manual,
400 ConflictType::ContentOverlap => ResolutionStrategy::Manual,
401 ConflictType::Complex => ResolutionStrategy::Manual,
402 }
403 }
404
405 fn generate_context(
407 &self,
408 conflict_type: &ConflictType,
409 our_content: &str,
410 their_content: &str,
411 ) -> String {
412 match conflict_type {
413 ConflictType::Whitespace => "Conflicts only in whitespace/formatting".to_string(),
414 ConflictType::LineEnding => "Conflicts only in line endings (CRLF vs LF)".to_string(),
415 ConflictType::PureAddition => {
416 format!(
417 "Both sides added content: {} vs {} lines",
418 our_content.lines().count(),
419 their_content.lines().count()
420 )
421 }
422 ConflictType::ImportMerge => "Import statements that can be merged".to_string(),
423 ConflictType::Structural => {
424 "Changes to code structure (functions, classes, etc.)".to_string()
425 }
426 ConflictType::ContentOverlap => "Overlapping changes to the same content".to_string(),
427 ConflictType::Complex => "Complex conflicts requiring manual review".to_string(),
428 }
429 }
430
431 fn is_import_conflict(&self, file_path: &str, our_content: &str, their_content: &str) -> bool {
433 let extension = file_path.split('.').next_back().unwrap_or("");
434
435 if let Some(patterns) = self.file_patterns.get(extension) {
436 let our_lines: Vec<&str> = our_content.lines().collect();
437 let their_lines: Vec<&str> = their_content.lines().collect();
438
439 let our_imports = our_lines.iter().all(|line| {
440 let trimmed = line.trim();
441 trimmed.is_empty() || patterns.iter().any(|pattern| trimmed.starts_with(pattern))
442 });
443
444 let their_imports = their_lines.iter().all(|line| {
445 let trimmed = line.trim();
446 trimmed.is_empty() || patterns.iter().any(|pattern| trimmed.starts_with(pattern))
447 });
448
449 return our_imports && their_imports;
450 }
451
452 false
453 }
454
455 fn is_structural_conflict(
457 &self,
458 file_path: &str,
459 our_content: &str,
460 their_content: &str,
461 ) -> bool {
462 let extension = file_path.split('.').next_back().unwrap_or("");
463
464 if let Some(patterns) = self.file_patterns.get(extension) {
465 let structural_keywords = patterns
466 .iter()
467 .filter(|p| {
468 !p.starts_with("import") && !p.starts_with("use") && !p.starts_with("from")
469 })
470 .collect::<Vec<_>>();
471
472 let our_has_structure = our_content.lines().any(|line| {
473 structural_keywords
474 .iter()
475 .any(|keyword| line.trim().starts_with(*keyword))
476 });
477
478 let their_has_structure = their_content.lines().any(|line| {
479 structural_keywords
480 .iter()
481 .any(|keyword| line.trim().starts_with(*keyword))
482 });
483
484 return our_has_structure || their_has_structure;
485 }
486
487 false
488 }
489
490 fn has_content_overlap(&self, our_content: &str, their_content: &str) -> bool {
492 let our_lines: Vec<&str> = our_content.lines().collect();
493 let their_lines: Vec<&str> = their_content.lines().collect();
494
495 for our_line in &our_lines {
497 if their_lines.contains(our_line) && !our_line.trim().is_empty() {
498 return true;
499 }
500 }
501
502 false
503 }
504
505 fn normalize_whitespace(&self, content: &str) -> String {
507 content
508 .lines()
509 .map(|line| line.trim())
510 .filter(|line| !line.is_empty())
511 .collect::<Vec<_>>()
512 .join("\n")
513 }
514
515 fn normalize_line_endings(&self, content: &str) -> String {
517 content.replace("\r\n", "\n").replace('\r', "\n")
518 }
519
520 fn assess_overall_difficulty(&self, conflicts: &[ConflictRegion]) -> ConflictDifficulty {
522 if conflicts.is_empty() {
523 return ConflictDifficulty::Easy;
524 }
525
526 let has_hard = conflicts
527 .iter()
528 .any(|c| c.difficulty == ConflictDifficulty::Hard);
529 let has_medium = conflicts
530 .iter()
531 .any(|c| c.difficulty == ConflictDifficulty::Medium);
532
533 if has_hard {
534 ConflictDifficulty::Hard
535 } else if has_medium {
536 ConflictDifficulty::Medium
537 } else {
538 ConflictDifficulty::Easy
539 }
540 }
541
542 fn generate_recommendations(&self, file_analyses: &[FileConflictAnalysis]) -> Vec<String> {
544 let mut recommendations = Vec::new();
545
546 let auto_resolvable_files = file_analyses.iter().filter(|f| f.auto_resolvable).count();
547
548 if auto_resolvable_files > 0 {
549 recommendations.push(format!(
550 "🤖 {auto_resolvable_files} file(s) can be automatically resolved"
551 ));
552 }
553
554 let manual_files = file_analyses.iter().filter(|f| !f.auto_resolvable).count();
555
556 if manual_files > 0 {
557 recommendations.push(format!(
558 "✋ {manual_files} file(s) require manual resolution"
559 ));
560 }
561
562 let mut type_counts = HashMap::new();
564 for analysis in file_analyses {
565 for (conflict_type, count) in &analysis.conflict_summary {
566 *type_counts.entry(conflict_type.clone()).or_insert(0) += count;
567 }
568 }
569
570 for (conflict_type, count) in type_counts {
571 match conflict_type {
572 ConflictType::Whitespace => {
573 recommendations.push(format!(
574 "🔧 {count} whitespace conflicts can be auto-formatted"
575 ));
576 }
577 ConflictType::ImportMerge => {
578 recommendations.push(format!(
579 "📦 {count} import conflicts can be merged automatically"
580 ));
581 }
582 ConflictType::Structural => {
583 recommendations.push(format!(
584 "🏗️ {count} structural conflicts need careful review"
585 ));
586 }
587 ConflictType::Complex => {
588 recommendations.push(format!(
589 "🔍 {count} complex conflicts require manual resolution"
590 ));
591 }
592 _ => {}
593 }
594 }
595
596 recommendations
597 }
598}
599
600impl Default for ConflictAnalyzer {
601 fn default() -> Self {
602 Self::new()
603 }
604}
605
606#[cfg(test)]
607mod tests {
608 use super::*;
609
610 #[test]
611 fn test_conflict_type_classification() {
612 let analyzer = ConflictAnalyzer::new();
613
614 let our_content = "function test() {\n return true;\n}";
616 let their_content = "function test() {\n return true;\n}";
617 let conflict_type = analyzer.classify_conflict_type("test.js", our_content, their_content);
618 assert_eq!(conflict_type, ConflictType::Whitespace);
619
620 let our_content = "";
622 let their_content = "import React from 'react';";
623 let conflict_type = analyzer.classify_conflict_type("test.js", our_content, their_content);
624 assert_eq!(conflict_type, ConflictType::PureAddition);
625
626 let our_content = "import { useState } from 'react';";
628 let their_content = "import { useEffect } from 'react';";
629 let conflict_type = analyzer.classify_conflict_type("test.js", our_content, their_content);
630 assert_eq!(conflict_type, ConflictType::ImportMerge);
631 }
632
633 #[test]
634 fn test_difficulty_assessment() {
635 let analyzer = ConflictAnalyzer::new();
636
637 assert_eq!(
638 analyzer.assess_difficulty(&ConflictType::Whitespace, "", ""),
639 ConflictDifficulty::Easy
640 );
641
642 assert_eq!(
643 analyzer.assess_difficulty(&ConflictType::Complex, "", ""),
644 ConflictDifficulty::Hard
645 );
646
647 assert_eq!(
648 analyzer.assess_difficulty(&ConflictType::Structural, "", ""),
649 ConflictDifficulty::Medium
650 );
651 }
652
653 #[test]
654 fn test_conflict_marker_parsing() {
655 let analyzer = ConflictAnalyzer::new();
656 let content = r#"
657line before conflict
658<<<<<<< HEAD
659our content
660=======
661their content
662>>>>>>> branch
663line after conflict
664"#;
665
666 let conflicts = analyzer
667 .parse_conflict_markers("test.txt", content)
668 .unwrap();
669 assert_eq!(conflicts.len(), 1);
670 assert_eq!(conflicts[0].our_content, "our content");
671 assert_eq!(conflicts[0].their_content, "their content");
672 }
673
674 #[test]
675 fn test_import_conflict_detection() {
676 let analyzer = ConflictAnalyzer::new();
677
678 assert!(analyzer.is_import_conflict(
680 "main.rs",
681 "use std::collections::HashMap;",
682 "use std::collections::HashSet;"
683 ));
684
685 assert!(analyzer.is_import_conflict("main.py", "import os", "import sys"));
687
688 assert!(!analyzer.is_import_conflict("main.rs", "fn main() {}", "fn test() {}"));
690 }
691}