1use crate::core::merge::MergeStrategy;
2use crate::core::{find_repo_root, get_current_branch, write_ref, Commit, Object, ObjectHash};
3use crate::response::ResolveResponse;
4use crate::storage::{Index, ObjectStore};
5use std::fs;
6use std::str::FromStr;
7
8pub fn execute(
16 file: Option<String>,
17 strategy: Option<String>,
18 all: bool,
19 finish: bool,
20) -> Result<ResolveResponse, crate::errors::LitError> {
21 let repo_root = find_repo_root()?;
22 let store = ObjectStore::new(&repo_root);
23
24 let merge_dir = repo_root.join(".lit").join("merge");
25 if !merge_dir.exists() {
26 return Err("No merge in progress. Nothing to resolve.".into());
27 }
28
29 if finish {
30 return finalize_merge(&repo_root, &store);
31 }
32
33 let strategy = match &strategy {
34 Some(s) => MergeStrategy::from_str(s)?,
35 None => return Err("--strategy is required for resolve (ours or theirs)".into()),
36 };
37
38 let conflicts_path = merge_dir.join("conflicts.json");
40 let conflicts_data = fs::read_to_string(&conflicts_path)
41 .map_err(|e| format!("Failed to read conflict state: {}", e))?;
42 let file_results: Vec<crate::core::merge::FileMergeResult> =
43 serde_json::from_str(&conflicts_data)
44 .map_err(|e| format!("Failed to parse conflict state: {}", e))?;
45
46 let merge_head_str = fs::read_to_string(merge_dir.join("MERGE_HEAD"))
47 .map_err(|e| format!("Failed to read MERGE_HEAD: {}", e))?
48 .trim()
49 .to_string();
50 let merge_head = ObjectHash::from_hex(merge_head_str);
51
52 let head_hash_str = crate::core::read_head(&repo_root)?;
53 let head_hash = ObjectHash::from_hex(head_hash_str);
54
55 let ours_tree = get_commit_tree(&store, &head_hash)?;
57 let theirs_tree = get_commit_tree(&store, &merge_head)?;
58 let ours_files = crate::core::diff::collect_tree_files(&ours_tree, &store, "")?;
59 let theirs_files = crate::core::diff::collect_tree_files(&theirs_tree, &store, "")?;
60
61 let ours_map: std::collections::HashMap<String, ObjectHash> = ours_files.into_iter().collect();
62 let theirs_map: std::collections::HashMap<String, ObjectHash> =
63 theirs_files.into_iter().collect();
64
65 let mut resolved_files = Vec::new();
66 let mut index = Index::load(&repo_root)?;
67
68 let conflicting: Vec<&crate::core::merge::FileMergeResult> = file_results
69 .iter()
70 .filter(|r| r.status == crate::core::merge::FileMergeStatus::Conflict)
71 .collect();
72
73 for conflict in &conflicting {
74 let should_resolve = all || file.as_deref() == Some(&conflict.path);
75 if !should_resolve {
76 continue;
77 }
78
79 let chosen_hash = match strategy {
80 MergeStrategy::Ours => ours_map.get(&conflict.path),
81 MergeStrategy::Theirs => theirs_map.get(&conflict.path),
82 MergeStrategy::Recursive => {
83 return Err(
84 "Cannot use 'recursive' strategy for resolve. Use 'ours' or 'theirs'.".into(),
85 );
86 }
87 };
88
89 if let Some(hash) = chosen_hash {
90 let entry = crate::storage::index::IndexEntry {
92 path: conflict.path.clone(),
93 hash: hash.to_string(),
94 mode: "100644".to_string(),
95 };
96 index.entries.insert(conflict.path.clone(), entry);
97
98 if let Ok(Object::Blob(blob)) = store.read(hash) {
100 let file_path = repo_root.join(&conflict.path);
101 if let Some(parent) = file_path.parent() {
102 let _ = fs::create_dir_all(parent);
103 }
104 fs::write(&file_path, &blob.content)
105 .map_err(|e| format!("Failed to write {}: {}", conflict.path, e))?;
106 }
107
108 resolved_files.push(conflict.path.clone());
109 }
110 }
111
112 if resolved_files.is_empty() {
113 return Err(if let Some(f) = &file {
114 format!("No conflict found for file '{}'", f).into()
115 } else {
116 "No conflicts to resolve".into()
117 });
118 }
119
120 index.save(&repo_root)?;
122
123 let remaining: Vec<&crate::core::merge::FileMergeResult> = file_results
125 .iter()
126 .filter(|r| {
127 r.status == crate::core::merge::FileMergeStatus::Conflict
128 && !resolved_files.contains(&r.path)
129 })
130 .collect();
131
132 if remaining.is_empty() {
133 let _ = fs::remove_file(&conflicts_path);
135 Ok(ResolveResponse {
136 resolved_files,
137 remaining_conflicts: 0,
138 merge_complete: false,
139 message: "All conflicts resolved. Run 'lit resolve --continue' to finalize the merge."
140 .to_string(),
141 })
142 } else {
143 let updated_data = serde_json::to_string_pretty(&remaining)
145 .map_err(|e| format!("Failed to serialize: {}", e))?;
146 fs::write(&conflicts_path, updated_data)
147 .map_err(|e| format!("Failed to update conflict state: {}", e))?;
148
149 Ok(ResolveResponse {
150 resolved_files,
151 remaining_conflicts: remaining.len(),
152 merge_complete: false,
153 message: format!("{} conflict(s) remaining", remaining.len()),
154 })
155 }
156}
157
158fn finalize_merge(
160 repo_root: &std::path::Path,
161 store: &ObjectStore,
162) -> Result<ResolveResponse, crate::errors::LitError> {
163 let merge_dir = repo_root.join(".lit").join("merge");
164
165 let conflicts_path = merge_dir.join("conflicts.json");
167 if conflicts_path.exists() {
168 let data = fs::read_to_string(&conflicts_path)
169 .map_err(|e| format!("Failed to read conflicts: {}", e))?;
170 let remaining: Vec<crate::core::merge::FileMergeResult> =
171 serde_json::from_str(&data).unwrap_or_default();
172 let conflict_count = remaining
173 .iter()
174 .filter(|r| r.status == crate::core::merge::FileMergeStatus::Conflict)
175 .count();
176 if conflict_count > 0 {
177 return Err(format!(
178 "{} unresolved conflict(s) remain. Resolve them first.",
179 conflict_count
180 )
181 .into());
182 }
183 }
184
185 let merge_head_str = fs::read_to_string(merge_dir.join("MERGE_HEAD"))
187 .map_err(|e| format!("Failed to read MERGE_HEAD: {}", e))?
188 .trim()
189 .to_string();
190 let merge_head = ObjectHash::from_hex(merge_head_str);
191
192 let head_hash_str = crate::core::read_head(repo_root)?;
193 let head_hash = ObjectHash::from_hex(head_hash_str);
194
195 let index = Index::load(repo_root)?;
197 let tree_hash = build_tree_from_index(&index, store)?;
198
199 let author = std::env::var("USER")
201 .or_else(|_| std::env::var("USERNAME"))
202 .unwrap_or_else(|_| "Unknown".to_string());
203
204 let commit = Commit::new(
205 tree_hash,
206 vec![head_hash, merge_head],
207 author,
208 "Merge commit (conflicts resolved)".to_string(),
209 );
210
211 let commit_obj = Object::Commit(commit);
212 let commit_hash = store.write(&commit_obj)?;
213
214 let current_branch = get_current_branch(repo_root).unwrap_or_else(|_| "main".to_string());
216 write_ref(
217 repo_root,
218 &format!("heads/{}", current_branch),
219 commit_hash.as_str(),
220 )?;
221
222 let _ = fs::remove_dir_all(&merge_dir);
224
225 Ok(ResolveResponse {
226 resolved_files: vec![],
227 remaining_conflicts: 0,
228 merge_complete: true,
229 message: format!("Merge complete: {}", commit_hash.short()),
230 })
231}
232
233fn get_commit_tree(
234 store: &ObjectStore,
235 commit_hash: &ObjectHash,
236) -> Result<crate::core::Tree, crate::errors::LitError> {
237 let commit = match store.read(commit_hash)? {
238 Object::Commit(c) => c,
239 _ => return Err(format!("Expected commit object for {}", commit_hash).into()),
240 };
241 match store.read(&commit.tree)? {
242 Object::Tree(t) => Ok(t),
243 _ => Err(format!("Expected tree object for {}", commit.tree).into()),
244 }
245}
246
247fn build_tree_from_index(
249 index: &Index,
250 store: &ObjectStore,
251) -> Result<ObjectHash, crate::errors::LitError> {
252 use crate::core::Tree;
253 use std::collections::HashMap;
254
255 let mut tree_map: HashMap<String, Vec<(String, String, String)>> = HashMap::new();
256
257 for entry in index.sorted_entries() {
258 let parts: Vec<&str> = entry.path.split('/').collect();
259 if parts.len() == 1 {
260 tree_map.entry("".to_string()).or_default().push((
261 parts[0].to_string(),
262 entry.hash.clone(),
263 entry.mode.clone(),
264 ));
265 } else {
266 let dir = parts[0].to_string();
267 tree_map.entry(dir).or_default().push((
268 parts[1..].join("/"),
269 entry.hash.clone(),
270 entry.mode.clone(),
271 ));
272 }
273 }
274
275 let mut root_tree = Tree::new();
276
277 if let Some(root_files) = tree_map.get("") {
278 for (name, hash, mode) in root_files {
279 root_tree.add_entry(
280 mode.clone(),
281 name.clone(),
282 ObjectHash::from_hex(hash.clone()),
283 "blob".to_string(),
284 );
285 }
286 }
287
288 for dir in tree_map.keys() {
289 if !dir.is_empty() {
290 let mut subtree = Tree::new();
291 if let Some(files) = tree_map.get(dir) {
292 for (name, hash, mode) in files {
293 subtree.add_entry(
294 mode.clone(),
295 name.clone(),
296 ObjectHash::from_hex(hash.clone()),
297 "blob".to_string(),
298 );
299 }
300 }
301 let subtree_object = Object::Tree(subtree);
302 let subtree_hash = store.write(&subtree_object)?;
303 root_tree.add_entry(
304 "040000".to_string(),
305 dir.clone(),
306 subtree_hash,
307 "tree".to_string(),
308 );
309 }
310 }
311
312 let tree_object = Object::Tree(root_tree);
313 store.write(&tree_object).map_err(Into::into)
314}