Skip to main content

cli/cli/commands/
resolve.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Resolve command implementation.
3
4use std::fs;
5
6use anyhow::{Result, anyhow};
7use repo::Repository;
8use serde::Serialize;
9
10use crate::cli::{Cli, should_output_json};
11
12#[derive(Serialize)]
13struct ResolveOutput {
14    message: String,
15    resolved: Vec<String>,
16    remaining: Vec<String>,
17}
18
19#[derive(Serialize)]
20struct ConflictList {
21    conflicts: Vec<String>,
22}
23
24pub fn cmd_resolve(
25    cli: &Cli,
26    path: Option<String>,
27    all: bool,
28    list: bool,
29    ours: bool,
30    theirs: bool,
31    abort: bool,
32) -> Result<()> {
33    let repo = Repository::open(cli.repo.as_ref().unwrap_or(&std::env::current_dir()?))?;
34    let merge_manager = repo.merge_state_manager();
35
36    if abort {
37        return cmd_resolve_abort(&repo, &merge_manager, cli);
38    }
39
40    if list {
41        return cmd_resolve_list(&repo, &merge_manager, cli);
42    }
43
44    if all {
45        return cmd_resolve_all(&repo, &merge_manager, cli, ours, theirs);
46    }
47
48    let Some(path) = path else {
49        return Err(anyhow!(
50            "Specify a file to resolve, or use --all, --list, or --abort"
51        ));
52    };
53
54    cmd_resolve_file(&repo, &merge_manager, cli, &path, ours, theirs)
55}
56
57fn cmd_resolve_abort(
58    repo: &Repository,
59    merge_manager: &repo::MergeStateManager,
60    cli: &Cli,
61) -> Result<()> {
62    abort_merge_state(repo, merge_manager)?;
63
64    if should_output_json(cli, Some(repo.config())) {
65        println!(
66            "{}",
67            serde_json::to_string(&ResolveOutput {
68                message: "Merge aborted".to_string(),
69                resolved: vec![],
70                remaining: vec![],
71            })?
72        );
73    } else {
74        println!("Merge aborted");
75    }
76
77    Ok(())
78}
79
80pub(crate) fn abort_merge_state(
81    repo: &Repository,
82    merge_manager: &repo::MergeStateManager,
83) -> Result<()> {
84    let merge_state = merge_manager
85        .load()?
86        .ok_or_else(|| anyhow!("No merge in progress"))?;
87    repo.fast_forward_attached(&merge_state.ours)?;
88    merge_manager.abort()?;
89    Ok(())
90}
91
92fn cmd_resolve_list(
93    repo: &Repository,
94    merge_manager: &repo::MergeStateManager,
95    cli: &Cli,
96) -> Result<()> {
97    let unresolved = merge_manager.unresolved()?;
98
99    if should_output_json(cli, Some(repo.config())) {
100        println!(
101            "{}",
102            serde_json::to_string(&ConflictList {
103                conflicts: unresolved.clone(),
104            })?
105        );
106    } else if unresolved.is_empty() {
107        println!("No unresolved conflicts");
108    } else {
109        for path in &unresolved {
110            println!("{}", path);
111        }
112    }
113
114    Ok(())
115}
116
117fn cmd_resolve_all(
118    repo: &Repository,
119    merge_manager: &repo::MergeStateManager,
120    cli: &Cli,
121    ours: bool,
122    theirs: bool,
123) -> Result<()> {
124    let unresolved = merge_manager.unresolved()?;
125
126    if unresolved.is_empty() {
127        return Err(anyhow!("No conflicts to resolve"));
128    }
129
130    for path in &unresolved {
131        resolve_file_with_version(repo, path, ours, theirs)?;
132        merge_manager.resolve(path)?;
133    }
134
135    let remaining = merge_manager.unresolved()?;
136
137    if should_output_json(cli, Some(repo.config())) {
138        println!(
139            "{}",
140            serde_json::to_string(&ResolveOutput {
141                message: format!("Resolved {} conflict(s)", unresolved.len()),
142                resolved: unresolved.clone(),
143                remaining: remaining.clone(),
144            })?
145        );
146    } else {
147        println!("Resolved {} conflict(s)", unresolved.len());
148        for path in &unresolved {
149            println!("  {}", path);
150        }
151        if !remaining.is_empty() {
152            println!("Remaining: {} conflict(s)", remaining.len());
153        }
154    }
155
156    Ok(())
157}
158
159fn cmd_resolve_file(
160    repo: &Repository,
161    merge_manager: &repo::MergeStateManager,
162    cli: &Cli,
163    path: &str,
164    ours: bool,
165    theirs: bool,
166) -> Result<()> {
167    resolve_file_with_version(repo, path, ours, theirs)?;
168    merge_manager.resolve(path)?;
169
170    let remaining = merge_manager.unresolved()?;
171
172    if should_output_json(cli, Some(repo.config())) {
173        println!(
174            "{}",
175            serde_json::to_string(&ResolveOutput {
176                message: format!("Resolved {}", path),
177                resolved: vec![path.to_string()],
178                remaining,
179            })?
180        );
181    } else {
182        println!("Resolved {}", path);
183        if !remaining.is_empty() {
184            println!("{} conflict(s) remaining", remaining.len());
185        }
186    }
187
188    Ok(())
189}
190
191fn resolve_file_with_version(
192    repo: &Repository,
193    path: &str,
194    ours: bool,
195    theirs: bool,
196) -> Result<()> {
197    if !ours && !theirs {
198        return Ok(());
199    }
200
201    let merge_state = repo
202        .merge_state_manager()
203        .load()?
204        .ok_or_else(|| anyhow!("No merge in progress"))?;
205
206    let full_path = repo.root().join(path);
207
208    if ours {
209        let our_state = repo
210            .store()
211            .get_state(&merge_state.ours)?
212            .ok_or_else(|| anyhow!("Our state not found"))?;
213        let our_tree = repo.store().get_tree(&our_state.tree)?.unwrap_or_default();
214
215        if let Some(entry) = our_tree.get(path) {
216            let blob = repo.require_blob(&entry.hash)?;
217            fs::write(&full_path, blob.content())?;
218        }
219    } else if theirs {
220        let their_state = repo
221            .store()
222            .get_state(&merge_state.theirs)?
223            .ok_or_else(|| anyhow!("Their state not found"))?;
224        let their_tree = repo
225            .store()
226            .get_tree(&their_state.tree)?
227            .unwrap_or_default();
228
229        if let Some(entry) = their_tree.get(path) {
230            let blob = repo.require_blob(&entry.hash)?;
231            fs::write(&full_path, blob.content())?;
232        }
233    }
234
235    Ok(())
236}