Skip to main content

mars_agents/cli/
resolve_cmd.rs

1//! `mars resolve` — mark conflicts as resolved after user fixes them.
2
3use std::path::{Path, PathBuf};
4
5use crate::error::MarsError;
6use crate::hash;
7use crate::types::{ContentHash, DestPath};
8
9use super::output;
10
11/// Arguments for `mars resolve`.
12#[derive(Debug, clap::Args)]
13pub struct ResolveArgs {
14    /// Specific file to resolve (default: all conflicted).
15    pub file: Option<PathBuf>,
16}
17
18/// Run `mars resolve`.
19pub fn run(args: &ResolveArgs, ctx: &super::MarsContext, json: bool) -> Result<i32, MarsError> {
20    let mut lock = crate::lock::load(&ctx.managed_root)?;
21    let mut resolved_files = Vec::new();
22    let mut still_conflicted = Vec::new();
23
24    let items_to_check: Vec<DestPath> = if let Some(file) = &args.file {
25        // Check specific file
26        let rel = DestPath::from(file.as_path());
27        if lock.items.contains_key(&rel) {
28            vec![rel]
29        } else {
30            return Err(MarsError::Source {
31                source_name: "resolve".to_string(),
32                message: format!("{} is not a managed item", file.display()),
33            });
34        }
35    } else {
36        // Check all items
37        lock.items.keys().cloned().collect()
38    };
39
40    for dest_path_str in &items_to_check {
41        let disk_path = ctx.managed_root.join(dest_path_str);
42        if !disk_path.exists() {
43            continue;
44        }
45
46        // Check for conflict markers
47        if has_conflict_markers(&disk_path)? {
48            still_conflicted.push(dest_path_str.clone());
49            continue;
50        }
51
52        // File has no conflict markers — update lock checksums
53        if let Some(item) = lock.items.get_mut(dest_path_str) {
54            let new_hash = hash::compute_hash(&disk_path, item.kind)?;
55            if new_hash != item.installed_checksum {
56                item.installed_checksum = ContentHash::from(new_hash);
57                resolved_files.push(dest_path_str.to_string());
58            }
59        }
60    }
61
62    // Write updated lock
63    if !resolved_files.is_empty() {
64        crate::lock::write(&ctx.managed_root, &lock)?;
65    }
66
67    if json {
68        output::print_json(&serde_json::json!({
69            "resolved": resolved_files,
70            "still_conflicted": still_conflicted,
71        }));
72    } else {
73        for file in &resolved_files {
74            output::print_success(&format!("resolved {file}"));
75        }
76        for file in &still_conflicted {
77            eprintln!("  ! {file} still has conflict markers");
78        }
79        if resolved_files.is_empty() && still_conflicted.is_empty() {
80            output::print_info("no conflicts to resolve");
81        }
82    }
83
84    if still_conflicted.is_empty() {
85        Ok(0)
86    } else {
87        Ok(1)
88    }
89}
90
91/// Check if a file contains git conflict markers.
92fn has_conflict_markers(path: &Path) -> Result<bool, MarsError> {
93    let content = std::fs::read_to_string(path)?;
94    Ok(content.contains("<<<<<<<") && content.contains(">>>>>>>"))
95}