Skip to main content

chant/
merge_errors.rs

1//! Actionable error messages for merge operations.
2//!
3//! Provides structured error messages with context, diagnosis,
4//! and concrete next steps to help users recover from merge failures.
5
6use std::fmt;
7
8// Re-export git_ops types and functions for backward compatibility
9pub use crate::git_ops::{classify_conflict_type, parse_conflicting_files, ConflictType};
10
11/// Merge error kind - the type of merge failure
12#[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/// Context information for a merge error
31#[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/// Structured merge error with kind and context
47#[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
551// Legacy functions for backward compatibility
552pub 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
613/// Detailed merge conflict error with file list and recovery steps.
614pub 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}