1use 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}