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::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 mars_dir = ctx.project_root.join(".mars");
21    let lock_path = mars_dir.join("sync.lock");
22    let _sync_lock = crate::fs::FileLock::acquire(&lock_path)?;
23
24    let mut lock = crate::lock::load(&ctx.project_root)?;
25    let mut resolved_files = Vec::new();
26    let mut still_conflicted = Vec::new();
27
28    let items_to_check: Vec<DestPath> = if let Some(file) = &args.file {
29        // Check specific file
30        let rel_input = if file.is_absolute() {
31            file.strip_prefix(&mars_dir).map_or_else(
32                |_| file.to_string_lossy().to_string(),
33                |r| r.to_string_lossy().to_string(),
34            )
35        } else {
36            file.to_string_lossy().to_string()
37        };
38        let rel = DestPath::new(&rel_input).map_err(|e| MarsError::Source {
39            source_name: "resolve".to_string(),
40            message: format!("invalid managed item path `{}`: {e}", file.display()),
41        })?;
42        if lock.contains_dest_path(&rel) {
43            vec![rel]
44        } else {
45            return Err(MarsError::Source {
46                source_name: "resolve".to_string(),
47                message: format!("{} is not a managed item", file.display()),
48            });
49        }
50    } else {
51        // Check all items
52        lock.all_output_dest_paths().cloned().collect()
53    };
54
55    for dest_path_str in &items_to_check {
56        let disk_path = dest_path_str.resolve(&mars_dir);
57        if !disk_path.exists() {
58            continue;
59        }
60
61        // Check for conflict markers
62        if crate::merge::file_has_conflict_markers(&disk_path) {
63            still_conflicted.push(dest_path_str.clone());
64            continue;
65        }
66
67        // File has no conflict markers — update lock checksums in the matching output record.
68        'outer: for item_v2 in lock.items.values_mut() {
69            for output in &mut item_v2.outputs {
70                if &output.dest_path == dest_path_str {
71                    let new_hash = hash::compute_hash(&disk_path, item_v2.kind)?;
72                    if new_hash != output.installed_checksum {
73                        output.installed_checksum = ContentHash::from(new_hash);
74                        resolved_files.push(dest_path_str.to_string());
75                    }
76                    break 'outer;
77                }
78            }
79        }
80    }
81
82    // Write updated lock
83    if !resolved_files.is_empty() {
84        crate::lock::write(&ctx.project_root, &lock)?;
85    }
86
87    if json {
88        output::print_json(&serde_json::json!({
89            "resolved": resolved_files,
90            "still_conflicted": still_conflicted,
91        }));
92    } else {
93        for file in &resolved_files {
94            output::print_success(&format!("resolved {file}"));
95        }
96        for file in &still_conflicted {
97            eprintln!("  ! {file} still has conflict markers");
98        }
99        if resolved_files.is_empty() && still_conflicted.is_empty() {
100            output::print_info("no conflicts to resolve");
101        }
102    }
103
104    if still_conflicted.is_empty() {
105        Ok(0)
106    } else {
107        Ok(1)
108    }
109}