1use std::fmt;
7
8pub use crate::git_ops::{classify_conflict_type, parse_conflicting_files, ConflictType};
10
11#[derive(Debug, Clone, PartialEq)]
13pub enum MergeErrorKind {
14 FastForwardConflict,
15 MergeConflict,
16 BranchNotFound,
17 MainBranchNotFound,
18 SpecStatusNotMergeable,
19 NoBranchForSpec,
20 WorktreeAlreadyExists,
21 NoCommitsFound,
22 DriverMembersIncomplete,
23 MemberMergeFailed,
24 GenericMergeFailed,
25 RebaseConflict,
26 MergeStopped,
27 RebaseStopped,
28}
29
30#[derive(Debug, Clone, Default)]
32pub struct MergeContext {
33 pub spec_id: String,
34 pub spec_branch: Option<String>,
35 pub main_branch: Option<String>,
36 pub status: Option<String>,
37 pub stderr: Option<String>,
38 pub conflicting_files: Vec<String>,
39 pub incomplete_members: Vec<String>,
40 pub member_id: Option<String>,
41 pub error_message: Option<String>,
42 pub worktree_path: Option<String>,
43 pub driver_id: Option<String>,
44}
45
46#[derive(Debug, Clone)]
48pub struct MergeError {
49 pub kind: MergeErrorKind,
50 pub context: MergeContext,
51}
52
53impl MergeError {
54 pub fn new(kind: MergeErrorKind, context: MergeContext) -> Self {
55 Self { kind, context }
56 }
57
58 pub fn fast_forward_conflict(
59 spec_id: &str,
60 spec_branch: &str,
61 main_branch: &str,
62 stderr: &str,
63 ) -> Self {
64 Self::new(
65 MergeErrorKind::FastForwardConflict,
66 MergeContext {
67 spec_id: spec_id.to_string(),
68 spec_branch: Some(spec_branch.to_string()),
69 main_branch: Some(main_branch.to_string()),
70 stderr: Some(stderr.to_string()),
71 ..Default::default()
72 },
73 )
74 }
75
76 pub fn merge_conflict(spec_id: &str, spec_branch: &str, main_branch: &str) -> Self {
77 Self::new(
78 MergeErrorKind::MergeConflict,
79 MergeContext {
80 spec_id: spec_id.to_string(),
81 spec_branch: Some(spec_branch.to_string()),
82 main_branch: Some(main_branch.to_string()),
83 ..Default::default()
84 },
85 )
86 }
87
88 pub fn branch_not_found(spec_id: &str, spec_branch: &str) -> Self {
89 Self::new(
90 MergeErrorKind::BranchNotFound,
91 MergeContext {
92 spec_id: spec_id.to_string(),
93 spec_branch: Some(spec_branch.to_string()),
94 ..Default::default()
95 },
96 )
97 }
98
99 pub fn main_branch_not_found(main_branch: &str) -> Self {
100 Self::new(
101 MergeErrorKind::MainBranchNotFound,
102 MergeContext {
103 main_branch: Some(main_branch.to_string()),
104 ..Default::default()
105 },
106 )
107 }
108
109 pub fn spec_status_not_mergeable(spec_id: &str, status: &str) -> Self {
110 Self::new(
111 MergeErrorKind::SpecStatusNotMergeable,
112 MergeContext {
113 spec_id: spec_id.to_string(),
114 status: Some(status.to_string()),
115 ..Default::default()
116 },
117 )
118 }
119
120 pub fn no_branch_for_spec(spec_id: &str) -> Self {
121 Self::new(
122 MergeErrorKind::NoBranchForSpec,
123 MergeContext {
124 spec_id: spec_id.to_string(),
125 ..Default::default()
126 },
127 )
128 }
129
130 pub fn worktree_already_exists(spec_id: &str, worktree_path: &str, branch: &str) -> Self {
131 Self::new(
132 MergeErrorKind::WorktreeAlreadyExists,
133 MergeContext {
134 spec_id: spec_id.to_string(),
135 spec_branch: Some(branch.to_string()),
136 worktree_path: Some(worktree_path.to_string()),
137 ..Default::default()
138 },
139 )
140 }
141
142 pub fn no_commits_found(spec_id: &str, branch: &str) -> Self {
143 Self::new(
144 MergeErrorKind::NoCommitsFound,
145 MergeContext {
146 spec_id: spec_id.to_string(),
147 spec_branch: Some(branch.to_string()),
148 ..Default::default()
149 },
150 )
151 }
152
153 pub fn driver_members_incomplete(driver_id: &str, incomplete: &[String]) -> Self {
154 Self::new(
155 MergeErrorKind::DriverMembersIncomplete,
156 MergeContext {
157 driver_id: Some(driver_id.to_string()),
158 incomplete_members: incomplete.to_vec(),
159 ..Default::default()
160 },
161 )
162 }
163
164 pub fn member_merge_failed(driver_id: &str, member_id: &str, error: &str) -> Self {
165 Self::new(
166 MergeErrorKind::MemberMergeFailed,
167 MergeContext {
168 driver_id: Some(driver_id.to_string()),
169 member_id: Some(member_id.to_string()),
170 error_message: Some(error.to_string()),
171 ..Default::default()
172 },
173 )
174 }
175
176 pub fn generic_merge_failed(
177 spec_id: &str,
178 branch: &str,
179 main_branch: &str,
180 error: &str,
181 ) -> Self {
182 Self::new(
183 MergeErrorKind::GenericMergeFailed,
184 MergeContext {
185 spec_id: spec_id.to_string(),
186 spec_branch: Some(branch.to_string()),
187 main_branch: Some(main_branch.to_string()),
188 error_message: Some(error.to_string()),
189 ..Default::default()
190 },
191 )
192 }
193
194 pub fn rebase_conflict(spec_id: &str, branch: &str, conflicting_files: &[String]) -> Self {
195 Self::new(
196 MergeErrorKind::RebaseConflict,
197 MergeContext {
198 spec_id: spec_id.to_string(),
199 spec_branch: Some(branch.to_string()),
200 conflicting_files: conflicting_files.to_vec(),
201 ..Default::default()
202 },
203 )
204 }
205
206 pub fn merge_stopped(spec_id: &str) -> Self {
207 Self::new(
208 MergeErrorKind::MergeStopped,
209 MergeContext {
210 spec_id: spec_id.to_string(),
211 ..Default::default()
212 },
213 )
214 }
215
216 pub fn rebase_stopped(spec_id: &str) -> Self {
217 Self::new(
218 MergeErrorKind::RebaseStopped,
219 MergeContext {
220 spec_id: spec_id.to_string(),
221 ..Default::default()
222 },
223 )
224 }
225}
226
227impl fmt::Display for MergeError {
228 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
229 use MergeErrorKind::*;
230
231 match &self.kind {
232 FastForwardConflict => {
233 let spec_id = &self.context.spec_id;
234 let spec_branch = self.context.spec_branch.as_deref().unwrap_or("");
235 let main_branch = self.context.main_branch.as_deref().unwrap_or("");
236 let stderr = self.context.stderr.as_deref().unwrap_or("").trim();
237
238 write!(
239 f,
240 "Error: Cannot fast-forward merge for spec {}\n\n\
241 Context:\n\
242 \x20 - Branch: {}\n\
243 \x20 - Target: {}\n\
244 \x20 - Branches have diverged from common ancestor\n\
245 \x20 - Git output: {}\n\n\
246 Next Steps:\n\
247 \x20 1. Use no-fast-forward merge: chant merge {} --no-ff\n\
248 \x20 2. Or rebase onto {}: chant merge {} --rebase\n\
249 \x20 3. Or merge manually: git merge --no-ff {}\n\
250 \x20 4. Debug divergence: git log {} --oneline -5\n\n\
251 Tip: Use 'chant merge --help' for all available options",
252 spec_id,
253 spec_branch,
254 main_branch,
255 stderr,
256 spec_id,
257 main_branch,
258 spec_id,
259 spec_branch,
260 spec_branch
261 )
262 }
263 MergeConflict => {
264 let spec_id = &self.context.spec_id;
265 let spec_branch = self.context.spec_branch.as_deref().unwrap_or("");
266 let main_branch = self.context.main_branch.as_deref().unwrap_or("");
267
268 write!(
269 f,
270 "Error: Merge conflicts detected for spec {}\n\n\
271 Context:\n\
272 \x20 - Branch: {}\n\
273 \x20 - Target: {}\n\
274 \x20 - Conflicting changes exist between branches\n\n\
275 Diagnosis:\n\
276 \x20 - The spec branch and {} have conflicting changes\n\
277 \x20 - Merge was aborted to preserve both branches\n\n\
278 Next Steps:\n\
279 \x20 1. Auto-resolve conflicts: chant merge {} --rebase --auto\n\
280 \x20 2. Rebase first, then merge: chant merge {} --rebase\n\
281 \x20 3. Manual merge: git merge --no-ff {}\n\
282 \x20 4. Inspect conflicts: git diff {} {}\n\
283 \x20 5. View branch history: git log {} --oneline -5\n\n\
284 Documentation: See 'chant merge --help' for more options",
285 spec_id,
286 spec_branch,
287 main_branch,
288 main_branch,
289 spec_id,
290 spec_id,
291 spec_branch,
292 main_branch,
293 spec_branch,
294 spec_branch
295 )
296 }
297 BranchNotFound => {
298 let spec_id = &self.context.spec_id;
299 let spec_branch = self.context.spec_branch.as_deref().unwrap_or("");
300
301 write!(
302 f,
303 "Error: Spec branch '{}' not found for spec {}\n\n\
304 Context:\n\
305 \x20 - Expected branch: {}\n\
306 \x20 - The branch may have been deleted or never created\n\n\
307 Diagnosis:\n\
308 \x20 - Check if the spec was worked in branch mode\n\
309 \x20 - The branch may have been cleaned up after a previous merge\n\n\
310 Next Steps:\n\
311 \x20 1. List all chant branches: git branch --list 'chant/*'\n\
312 \x20 2. Check worktree status: git worktree list\n\
313 \x20 3. If branch existed, check reflog: git reflog --all\n\
314 \x20 4. If work was lost, re-execute: chant work {}\n\n\
315 Documentation: See 'chant merge --help' for more options",
316 spec_branch, spec_id, spec_branch, spec_id
317 )
318 }
319 MainBranchNotFound => {
320 let main_branch = self.context.main_branch.as_deref().unwrap_or("");
321
322 write!(
323 f,
324 "Error: Main branch '{}' does not exist\n\n\
325 Context:\n\
326 \x20 - Expected main branch: {}\n\
327 \x20 - This is typically 'main' or 'master'\n\n\
328 Diagnosis:\n\
329 \x20 - The repository may use a different default branch name\n\n\
330 Next Steps:\n\
331 \x20 1. Check available branches: git branch -a\n\
332 \x20 2. Check remote default: git remote show origin | grep 'HEAD branch'\n\
333 \x20 3. If using a different name, configure it in .chant/config.md\n\n\
334 Documentation: See 'chant merge --help' for more options",
335 main_branch, main_branch
336 )
337 }
338 SpecStatusNotMergeable => {
339 let spec_id = &self.context.spec_id;
340 let status = self.context.status.as_deref().unwrap_or("");
341
342 write!(
343 f,
344 "Error: Cannot merge spec {} (status: {})\n\n\
345 Context:\n\
346 \x20 - Spec: {}\n\
347 \x20 - Current status: {}\n\
348 \x20 - Only completed specs can be merged\n\n\
349 Next Steps:\n\
350 \x20 1. Check spec details: chant show {}\n\
351 \x20 2. If work is done, finalize first: chant finalize {}\n\
352 \x20 3. If needs attention, resolve issues and retry\n\n\
353 Documentation: See 'chant merge --help' for more options",
354 spec_id, status, spec_id, status, spec_id, spec_id
355 )
356 }
357 NoBranchForSpec => {
358 let spec_id = &self.context.spec_id;
359
360 write!(
361 f,
362 "Error: No branch found for spec {}\n\n\
363 Context:\n\
364 \x20 - Spec: {}\n\
365 \x20 - The spec is completed but has no associated branch\n\n\
366 Diagnosis:\n\
367 \x20 - The spec may have been worked in direct mode (no separate branch)\n\
368 \x20 - The branch may have been deleted after a previous merge\n\n\
369 Next Steps:\n\
370 \x20 1. Check for existing branches: git branch --list 'chant/*{}*'\n\
371 \x20 2. Check if already merged: git log --oneline --grep='chant({})'\n\
372 \x20 3. If not merged and branch lost, re-execute: chant work {}\n\n\
373 Documentation: See 'chant merge --help' for more options",
374 spec_id, spec_id, spec_id, spec_id, spec_id
375 )
376 }
377 WorktreeAlreadyExists => {
378 let spec_id = &self.context.spec_id;
379 let worktree_path = self.context.worktree_path.as_deref().unwrap_or("");
380 let branch = self.context.spec_branch.as_deref().unwrap_or("");
381
382 write!(
383 f,
384 "Error: Worktree already exists for spec {}\n\n\
385 Context:\n\
386 \x20 - Worktree path: {}\n\
387 \x20 - Branch: {}\n\
388 \x20 - A worktree at this path is already in use\n\n\
389 Diagnosis:\n\
390 \x20 - A previous execution may not have cleaned up properly\n\
391 \x20 - The worktree may still be in use by another process\n\n\
392 Next Steps:\n\
393 \x20 1. Remove manually: git worktree remove {} --force\n\
394 \x20 2. List all worktrees: git worktree list\n\
395 \x20 3. Then retry: chant work {}",
396 spec_id, worktree_path, branch, worktree_path, spec_id
397 )
398 }
399 NoCommitsFound => {
400 let spec_id = &self.context.spec_id;
401 let branch = self.context.spec_branch.as_deref().unwrap_or("");
402
403 write!(
404 f,
405 "Error: No commits found matching pattern 'chant({}):'\n\n\
406 Context:\n\
407 \x20 - Branch: {}\n\
408 \x20 - Expected pattern: 'chant({}): <description>'\n\n\
409 Diagnosis:\n\
410 \x20 - The agent may have forgotten to commit with the correct pattern\n\
411 \x20 - Commit messages must include 'chant({}):' prefix\n\n\
412 Next Steps:\n\
413 \x20 1. Check commits on branch: git log {} --oneline\n\
414 \x20 2. If commits exist but wrong pattern, amend or merge manually\n\
415 \x20 3. If no work was done, the branch may be empty\n\
416 \x20 4. Use --allow-no-commits as fallback (special cases only)\n\n\
417 Debugging: Report this if commits look correct - may be a pattern matching bug\n\n\
418 Documentation: See 'chant merge --help' for more options",
419 spec_id, branch, spec_id, spec_id, branch
420 )
421 }
422 DriverMembersIncomplete => {
423 let driver_id = self.context.driver_id.as_deref().unwrap_or("");
424 let incomplete = &self.context.incomplete_members;
425
426 write!(
427 f,
428 "Error: Cannot merge driver spec {} - members are incomplete\n\n\
429 Context:\n\
430 \x20 - Driver spec: {}\n\
431 \x20 - All member specs must be completed before merging the driver\n\n\
432 Incomplete members:\n\
433 \x20 - {}\n\n\
434 Next Steps:\n\
435 \x20 1. Check each incomplete member: chant show <member-id>\n\
436 \x20 2. Complete or cancel pending members\n\
437 \x20 3. Retry driver merge: chant merge {}\n\n\
438 Documentation: See 'chant merge --help' for more options",
439 driver_id,
440 driver_id,
441 incomplete.join("\n - "),
442 driver_id
443 )
444 }
445 MemberMergeFailed => {
446 let driver_id = self.context.driver_id.as_deref().unwrap_or("");
447 let member_id = self.context.member_id.as_deref().unwrap_or("");
448 let error = self.context.error_message.as_deref().unwrap_or("");
449
450 write!(
451 f,
452 "Error: Member spec merge failed, driver merge not attempted\n\n\
453 Context:\n\
454 \x20 - Driver spec: {}\n\
455 \x20 - Failed member: {}\n\
456 \x20 - Error: {}\n\n\
457 Next Steps:\n\
458 \x20 1. Resolve the member merge issue first\n\
459 \x20 2. Merge the member manually: chant merge {}\n\
460 \x20 3. Then retry the driver merge: chant merge {}\n\
461 \x20 4. Or use rebase: chant merge {} --rebase\n\n\
462 Documentation: See 'chant merge --help' for more options",
463 driver_id, member_id, error, member_id, driver_id, member_id
464 )
465 }
466 GenericMergeFailed => {
467 let spec_id = &self.context.spec_id;
468 let branch = self.context.spec_branch.as_deref().unwrap_or("");
469 let main_branch = self.context.main_branch.as_deref().unwrap_or("");
470 let error = self.context.error_message.as_deref().unwrap_or("").trim();
471
472 write!(
473 f,
474 "Error: Merge failed for spec {}\n\n\
475 Context:\n\
476 \x20 - Branch: {}\n\
477 \x20 - Target: {}\n\
478 \x20 - Error: {}\n\n\
479 Next Steps:\n\
480 \x20 1. Try with rebase: chant merge {} --rebase\n\
481 \x20 2. Or auto-resolve: chant merge {} --rebase --auto\n\
482 \x20 3. Manual merge: git merge --no-ff {}\n\
483 \x20 4. Debug: git log {} --online -5\n\n\
484 Documentation: See 'chant merge --help' for more options",
485 spec_id, branch, main_branch, error, spec_id, spec_id, branch, branch
486 )
487 }
488 RebaseConflict => {
489 let spec_id = &self.context.spec_id;
490 let branch = self.context.spec_branch.as_deref().unwrap_or("");
491 let conflicting_files = &self.context.conflicting_files;
492
493 write!(
494 f,
495 "Error: Rebase conflict for spec {}\n\n\
496 Context:\n\
497 \x20 - Branch: {}\n\
498 \x20 - Conflicting files:\n\
499 \x20 - {}\n\n\
500 Next Steps:\n\
501 \x20 1. Auto-resolve: chant merge {} --rebase --auto\n\
502 \x20 2. Resolve manually, then: git rebase --continue\n\
503 \x20 3. Abort rebase: git rebase --abort\n\
504 \x20 4. Try direct merge instead: git merge --no-ff {}\n\n\
505 Documentation: See 'chant merge --help' for more options",
506 spec_id,
507 branch,
508 conflicting_files.join("\n - "),
509 spec_id,
510 branch
511 )
512 }
513 MergeStopped => {
514 let spec_id = &self.context.spec_id;
515
516 write!(
517 f,
518 "Error: Merge stopped at spec {}\n\n\
519 Context:\n\
520 \x20 - Processing halted due to merge failure\n\
521 \x20 - Remaining specs were not processed\n\n\
522 Next Steps:\n\
523 \x20 1. Resolve the issue with spec {}: chant show {}\n\
524 \x20 2. Retry with continue-on-error: chant merge --all --continue-on-error\n\
525 \x20 3. Or merge specs individually: chant merge {}\n\n\
526 Documentation: See 'chant merge --help' for more options",
527 spec_id, spec_id, spec_id, spec_id
528 )
529 }
530 RebaseStopped => {
531 let spec_id = &self.context.spec_id;
532
533 write!(
534 f,
535 "Error: Merge stopped at spec {} due to rebase conflict\n\n\
536 Context:\n\
537 \x20 - Rebase encountered conflicts\n\
538 \x20 - Remaining specs were not processed\n\n\
539 Next Steps:\n\
540 \x20 1. Auto-resolve conflicts: chant merge {} --rebase --auto\n\
541 \x20 2. Use continue-on-error: chant merge --all --rebase --continue-on-error\n\
542 \x20 3. Resolve manually and retry\n\n\
543 Documentation: See 'chant merge --help' for more options",
544 spec_id, spec_id
545 )
546 }
547 }
548 }
549}
550
551pub fn fast_forward_conflict(
553 spec_id: &str,
554 spec_branch: &str,
555 main_branch: &str,
556 stderr: &str,
557) -> String {
558 MergeError::fast_forward_conflict(spec_id, spec_branch, main_branch, stderr).to_string()
559}
560
561pub fn merge_conflict(spec_id: &str, spec_branch: &str, main_branch: &str) -> String {
562 MergeError::merge_conflict(spec_id, spec_branch, main_branch).to_string()
563}
564
565pub fn branch_not_found(spec_id: &str, spec_branch: &str) -> String {
566 MergeError::branch_not_found(spec_id, spec_branch).to_string()
567}
568
569pub fn main_branch_not_found(main_branch: &str) -> String {
570 MergeError::main_branch_not_found(main_branch).to_string()
571}
572
573pub fn spec_status_not_mergeable(spec_id: &str, status: &str) -> String {
574 MergeError::spec_status_not_mergeable(spec_id, status).to_string()
575}
576
577pub fn no_branch_for_spec(spec_id: &str) -> String {
578 MergeError::no_branch_for_spec(spec_id).to_string()
579}
580
581pub fn worktree_already_exists(spec_id: &str, worktree_path: &str, branch: &str) -> String {
582 MergeError::worktree_already_exists(spec_id, worktree_path, branch).to_string()
583}
584
585pub fn no_commits_found(spec_id: &str, branch: &str) -> String {
586 MergeError::no_commits_found(spec_id, branch).to_string()
587}
588
589pub fn driver_members_incomplete(driver_id: &str, incomplete: &[String]) -> String {
590 MergeError::driver_members_incomplete(driver_id, incomplete).to_string()
591}
592
593pub fn member_merge_failed(driver_id: &str, member_id: &str, error: &str) -> String {
594 MergeError::member_merge_failed(driver_id, member_id, error).to_string()
595}
596
597pub fn generic_merge_failed(spec_id: &str, branch: &str, main_branch: &str, error: &str) -> String {
598 MergeError::generic_merge_failed(spec_id, branch, main_branch, error).to_string()
599}
600
601pub fn rebase_conflict(spec_id: &str, branch: &str, conflicting_files: &[String]) -> String {
602 MergeError::rebase_conflict(spec_id, branch, conflicting_files).to_string()
603}
604
605pub fn merge_stopped(spec_id: &str) -> String {
606 MergeError::merge_stopped(spec_id).to_string()
607}
608
609pub fn rebase_stopped(spec_id: &str) -> String {
610 MergeError::rebase_stopped(spec_id).to_string()
611}
612
613pub fn merge_conflict_detailed(
615 spec_id: &str,
616 spec_branch: &str,
617 main_branch: &str,
618 conflict_type: ConflictType,
619 conflicting_files: &[String],
620) -> String {
621 let conflict_type_str = match conflict_type {
622 ConflictType::FastForward => "Cannot fast-forward",
623 ConflictType::Content => "Content conflicts detected",
624 ConflictType::Tree => "Tree conflicts detected",
625 ConflictType::Unknown => "Merge conflicts detected",
626 };
627
628 let files_section = if conflicting_files.is_empty() {
629 " (unable to determine conflicting files)".to_string()
630 } else {
631 conflicting_files
632 .iter()
633 .map(|f| format!(" - {}", f))
634 .collect::<Vec<_>>()
635 .join("\n")
636 };
637
638 let recovery_steps = match conflict_type {
639 ConflictType::FastForward => format!(
640 "Next steps:\n\
641 \x20 1. Use no-fast-forward merge: chant merge {} --no-ff\n\
642 \x20 2. Or rebase onto {}: chant merge {} --rebase\n\
643 \x20 3. Or merge manually: git merge --no-ff {}",
644 spec_id, main_branch, spec_id, spec_branch
645 ),
646 ConflictType::Content | ConflictType::Tree | ConflictType::Unknown => format!(
647 "Next steps:\n\
648 \x20 1. Resolve conflicts manually, then: git merge --continue\n\
649 \x20 2. Or try automatic rebase: chant merge {} --rebase --auto\n\
650 \x20 3. Or abort: git merge --abort\n\n\
651 Example (resolve manually):\n\
652 \x20 $ git status # see conflicting files\n\
653 \x20 $ vim src/main.rs # edit to resolve\n\
654 \x20 $ git add src/main.rs # stage resolved file\n\
655 \x20 $ git merge --continue # complete merge",
656 spec_id
657 ),
658 };
659
660 format!(
661 "Error: {} for spec {}\n\n\
662 Context:\n\
663 \x20 - Branch: {}\n\
664 \x20 - Target: {}\n\
665 \x20 - Conflict type: {}\n\n\
666 Files with conflicts:\n\
667 {}\n\n\
668 {}\n\n\
669 Documentation: See 'chant merge --help' for more options",
670 conflict_type_str,
671 spec_id,
672 spec_branch,
673 main_branch,
674 conflict_type,
675 files_section,
676 recovery_steps
677 )
678}
679
680#[cfg(test)]
681mod tests {
682 use super::*;
683
684 #[test]
685 fn test_fast_forward_conflict_contains_spec_id() {
686 let msg = fast_forward_conflict(
687 "001-abc",
688 "chant/001-abc",
689 "main",
690 "fatal: cannot fast-forward",
691 );
692 assert!(msg.contains("001-abc"));
693 assert!(msg.contains("chant/001-abc"));
694 assert!(msg.contains("main"));
695 assert!(msg.contains("Next Steps"));
696 assert!(msg.contains("chant merge 001-abc --no-ff"));
697 assert!(msg.contains("chant merge 001-abc --rebase"));
698 }
699
700 #[test]
701 fn test_merge_conflict_contains_recovery_steps() {
702 let msg = merge_conflict("001-abc", "chant/001-abc", "main");
703 assert!(msg.contains("Merge conflicts detected"));
704 assert!(msg.contains("chant merge 001-abc --rebase --auto"));
705 assert!(msg.contains("git merge --no-ff chant/001-abc"));
706 assert!(msg.contains("Documentation"));
707 }
708
709 #[test]
710 fn test_conflict_type_display() {
711 assert_eq!(format!("{}", ConflictType::FastForward), "fast-forward");
712 assert_eq!(format!("{}", ConflictType::Content), "content");
713 assert_eq!(format!("{}", ConflictType::Tree), "tree");
714 assert_eq!(format!("{}", ConflictType::Unknown), "unknown");
715 }
716
717 #[test]
718 fn test_classify_conflict_type_fast_forward() {
719 let stderr = "fatal: Not possible to fast-forward, aborting.";
720 assert_eq!(
721 classify_conflict_type(stderr, None),
722 ConflictType::FastForward
723 );
724 }
725
726 #[test]
727 fn test_parse_conflicting_files() {
728 let status = "UU src/main.rs\nUU src/lib.rs\nM src/other.rs\n";
729 let files = parse_conflicting_files(status);
730 assert_eq!(files.len(), 2);
731 assert!(files.contains(&"src/main.rs".to_string()));
732 assert!(files.contains(&"src/lib.rs".to_string()));
733 }
734}