1#![deny(unsafe_code)]
13
14use crate::prompts::template_context::TemplateContext;
15use crate::prompts::template_engine::Template;
16use crate::workspace::Workspace;
17use std::collections::HashMap;
18use std::path::Path;
19
20#[derive(Debug, Clone)]
22pub struct FileConflict {
23 pub conflict_content: String,
25 pub current_content: String,
27}
28
29#[cfg(test)]
45#[expect(clippy::print_stderr, reason = "test-only error logging")]
46pub fn build_conflict_resolution_prompt(
47 conflicts: &HashMap<String, FileConflict>,
48 prompt_md_content: Option<&str>,
49 plan_content: Option<&str>,
50) -> String {
51 let template_content = include_str!("templates/conflict_resolution.txt");
52 let template = Template::new(template_content);
53
54 let context = format_context_section(prompt_md_content, plan_content);
55 let conflicts_section = format_conflicts_section(conflicts);
56
57 let variables = HashMap::from([
58 ("CONTEXT", context),
59 ("CONFLICTS", conflicts_section.clone()),
60 ]);
61
62 template.render(&variables).unwrap_or_else(|e| {
63 eprintln!("Warning: Failed to render conflict resolution template: {e}");
64 let fallback_template_content = include_str!("templates/conflict_resolution_fallback.txt");
65 let fallback_template = Template::new(fallback_template_content);
66 fallback_template.render(&variables).unwrap_or_else(|e| {
67 eprintln!("Critical: Failed to render fallback template: {e}");
68 format!(
69 "# MERGE CONFLICT RESOLUTION\n\nResolve these conflicts:\n\n{}",
70 &conflicts_section
71 )
72 })
73 })
74}
75
76#[must_use]
88#[expect(
89 clippy::print_stderr,
90 reason = "error logging for template rendering failures"
91)]
92pub fn build_conflict_resolution_prompt_with_context<S: std::hash::BuildHasher>(
93 context: &TemplateContext,
94 conflicts: &HashMap<String, FileConflict, S>,
95 prompt_md_content: Option<&str>,
96 plan_content: Option<&str>,
97) -> String {
98 let template_content = context
99 .registry()
100 .get_template("conflict_resolution")
101 .unwrap_or_else(|_| include_str!("templates/conflict_resolution.txt").to_string());
102 let template = Template::new(&template_content);
103
104 let ctx_section = format_context_section(prompt_md_content, plan_content);
105 let conflicts_section = format_conflicts_section(conflicts);
106
107 let variables = HashMap::from([
108 ("CONTEXT", ctx_section),
109 ("CONFLICTS", conflicts_section.clone()),
110 ]);
111
112 template.render(&variables).unwrap_or_else(|e| {
113 eprintln!("Warning: Failed to render conflict resolution template: {e}");
114 let fallback_template_content = context
116 .registry()
117 .get_template("conflict_resolution_fallback")
118 .unwrap_or_else(|_| {
119 include_str!("templates/conflict_resolution_fallback.txt").to_string()
120 });
121 let fallback_template = Template::new(&fallback_template_content);
122 fallback_template.render(&variables).unwrap_or_else(|e| {
123 eprintln!("Critical: Failed to render fallback template: {e}");
124 format!(
126 "# MERGE CONFLICT RESOLUTION\n\nResolve these conflicts:\n\n{}",
127 &conflicts_section
128 )
129 })
130 })
131}
132
133fn format_context_section(prompt_md_content: Option<&str>, plan_content: Option<&str>) -> String {
138 let prompt_part = prompt_md_content.map(|prompt_md| {
139 format!(
140 "## Task Context\n\nThe user was working on the following task:\n\n```\n{}\n```\n\n",
141 prompt_md
142 )
143 });
144
145 let plan_part = plan_content.map(|plan| {
146 format!(
147 "## Implementation Plan\n\nThe following plan was being implemented:\n\n```\n{}\n```\n\n",
148 plan
149 )
150 });
151
152 [prompt_part, plan_part]
153 .into_iter()
154 .flatten()
155 .collect::<String>()
156}
157
158fn format_conflicts_section<S: std::hash::BuildHasher>(
163 conflicts: &HashMap<String, FileConflict, S>,
164) -> String {
165 let sections: Vec<String> = conflicts
166 .iter()
167 .map(|(path, conflict)| {
168 let header = format!("### {path}\n\n");
169 let current = format!(
170 "Current state (with conflict markers):\n\n```{}\n{}\n```\n\n",
171 get_language_marker(path),
172 conflict.current_content
173 );
174 let conflict_part = if conflict.conflict_content.is_empty() {
175 String::new()
176 } else {
177 format!(
178 "Conflict sections:\n\n```{}\n{}\n```\n\n",
179 get_language_marker(path),
180 conflict.conflict_content
181 )
182 };
183 [header, current, conflict_part].join("")
184 })
185 .collect();
186
187 sections.join("")
188}
189
190fn get_language_marker(path: &str) -> String {
192 let ext = Path::new(path)
193 .extension()
194 .and_then(|e| e.to_str())
195 .unwrap_or("");
196
197 match ext {
198 "rs" => "rust",
199 "py" => "python",
200 "js" | "jsx" => "javascript",
201 "ts" | "tsx" => "typescript",
202 "go" => "go",
203 "java" => "java",
204 "c" => "c",
205 "cpp" | "cc" | "cxx" => "cpp",
206 "h" | "hpp" => "cpp",
207 "cs" => "csharp",
208 "rb" => "ruby",
209 "php" => "php",
210 "swift" => "swift",
211 "kt" | "kts" => "kotlin",
212 "scala" => "scala",
213 "sh" | "bash" | "zsh" => "bash",
214 "yml" | "yaml" => "yaml",
215 "json" => "json",
216 "toml" => "toml",
217 "xml" => "xml",
218 "html" | "htm" => "html",
219 "css" => "css",
220 "scss" | "sass" => "scss",
221 "sql" => "sql",
222 "md" | "markdown" => "markdown",
223 _ => "",
224 }
225 .to_string()
226}
227
228#[derive(Debug, Clone)]
230pub struct BranchInfo {
231 pub current_branch: String,
233 pub upstream_branch: String,
235 pub current_commits: Vec<String>,
237 pub upstream_commits: Vec<String>,
239 pub diverging_count: usize,
241}
242
243#[must_use]
256#[expect(
257 clippy::print_stderr,
258 reason = "error logging for template rendering failures"
259)]
260pub fn build_enhanced_conflict_resolution_prompt<S: std::hash::BuildHasher>(
261 context: &TemplateContext,
262 conflicts: &HashMap<String, FileConflict, S>,
263 branch_info: Option<&BranchInfo>,
264 prompt_md_content: Option<&str>,
265 plan_content: Option<&str>,
266) -> String {
267 let template_content = context
268 .registry()
269 .get_template("conflict_resolution")
270 .unwrap_or_else(|_| include_str!("templates/conflict_resolution.txt").to_string());
271 let template = Template::new(&template_content);
272
273 let ctx_section = match branch_info {
274 Some(info) => {
275 format_context_section(prompt_md_content, plan_content)
276 + &format_branch_info_section(info)
277 }
278 None => format_context_section(prompt_md_content, plan_content),
279 };
280
281 let conflicts_section = format_conflicts_section(conflicts);
282
283 let variables = HashMap::from([
284 ("CONTEXT", ctx_section),
285 ("CONFLICTS", conflicts_section.clone()),
286 ]);
287
288 template.render(&variables).unwrap_or_else(|e| {
289 eprintln!("Warning: Failed to render conflict resolution template: {e}");
290 let fallback_template_content = context
292 .registry()
293 .get_template("conflict_resolution_fallback")
294 .unwrap_or_else(|_| {
295 include_str!("templates/conflict_resolution_fallback.txt").to_string()
296 });
297 let fallback_template = Template::new(&fallback_template_content);
298 fallback_template.render(&variables).unwrap_or_else(|e| {
299 eprintln!("Critical: Failed to render fallback template: {e}");
300 format!(
302 "# MERGE CONFLICT RESOLUTION\n\nResolve these conflicts:\n\n{}",
303 &conflicts_section
304 )
305 })
306 })
307}
308
309fn format_branch_info_section(info: &BranchInfo) -> String {
314 let header = format!(
315 "## Branch Information\n\n- **Current branch**: `{}`\n- **Target branch**: `{}`\n- **Diverging commits**: {}\n\n",
316 info.current_branch, info.upstream_branch, info.diverging_count
317 );
318
319 let current_commits_section = if info.current_commits.is_empty() {
320 String::new()
321 } else {
322 let commits: Vec<String> = info
323 .current_commits
324 .iter()
325 .take(5)
326 .enumerate()
327 .map(|(i, msg)| format!("{}. {}", i + 1, msg))
328 .collect();
329 format!(
330 "### Recent commits on current branch:\n\n{}\n\n",
331 commits.join("\n")
332 )
333 };
334
335 let upstream_commits_section = if info.upstream_commits.is_empty() {
336 String::new()
337 } else {
338 let commits: Vec<String> = info
339 .upstream_commits
340 .iter()
341 .take(5)
342 .enumerate()
343 .map(|(i, msg)| format!("{}. {}", i + 1, msg))
344 .collect();
345 format!(
346 "### Recent commits on target branch:\n\n{}\n\n",
347 commits.join("\n")
348 )
349 };
350
351 [header, current_commits_section, upstream_commits_section]
352 .into_iter()
353 .filter(|s| !s.is_empty())
354 .collect()
355}
356
357pub fn collect_branch_info(
374 upstream_branch: &str,
375 executor: &dyn crate::executor::ProcessExecutor,
376) -> std::io::Result<BranchInfo> {
377 let current_branch =
379 executor.execute("git", &["rev-parse", "--abbrev-ref", "HEAD"], &[], None)?;
380
381 let current_branch = current_branch.stdout.trim().to_string();
382
383 let current_log = executor.execute("git", &["log", "--oneline", "-10", "HEAD"], &[], None)?;
385
386 let current_commits: Vec<String> = current_log
387 .stdout
388 .lines()
389 .map(std::string::ToString::to_string)
390 .collect();
391
392 let upstream_log = executor.execute(
394 "git",
395 &["log", "--oneline", "-10", upstream_branch],
396 &[],
397 None,
398 )?;
399
400 let upstream_commits: Vec<String> = upstream_log
401 .stdout
402 .lines()
403 .map(std::string::ToString::to_string)
404 .collect();
405
406 let diverging = executor.execute(
408 "git",
409 &[
410 "rev-list",
411 "--count",
412 "--left-right",
413 &format!("HEAD...{upstream_branch}"),
414 ],
415 &[],
416 None,
417 )?;
418
419 let diverging_count = diverging
420 .stdout
421 .split_whitespace()
422 .map(|s| s.parse::<usize>().unwrap_or(0))
423 .sum::<usize>();
424
425 Ok(BranchInfo {
426 current_branch,
427 upstream_branch: upstream_branch.to_string(),
428 current_commits,
429 upstream_commits,
430 diverging_count,
431 })
432}
433
434pub fn collect_conflict_info_with_workspace(
452 workspace: &dyn Workspace,
453 conflicted_paths: &[String],
454) -> std::io::Result<HashMap<String, FileConflict>> {
455 let conflicts: std::io::Result<Vec<(String, FileConflict)>> = conflicted_paths
456 .iter()
457 .map(|path| {
458 let current_content = workspace.read(Path::new(path))?;
459 let conflict_content = extract_conflict_sections_from_content(¤t_content);
460 Ok((
461 path.clone(),
462 FileConflict {
463 conflict_content,
464 current_content,
465 },
466 ))
467 })
468 .collect();
469
470 let result: HashMap<String, FileConflict> = conflicts?.into_iter().collect();
471
472 Ok(result)
473}
474
475fn extract_conflict_sections_from_content(content: &str) -> String {
476 let lines: Vec<&str> = content.lines().collect();
477
478 let conflict_sections: Vec<String> = lines
480 .iter()
481 .enumerate()
482 .filter(|(_, line)| line.trim_start().starts_with("<<<<<<<"))
483 .filter_map(|(start_idx, _)| {
484 let equals_idx = lines
486 .get(start_idx + 1..)?
487 .iter()
488 .position(|line| line.trim_start().starts_with("======="))
489 .map(|i| start_idx + 1 + i);
490
491 let end_idx = equals_idx.and_then(|eq_idx| {
493 lines
494 .get(eq_idx + 1..)?
495 .iter()
496 .position(|line| line.trim_start().starts_with(">>>>>>>"))
497 .map(|i| eq_idx + 1 + i)
498 });
499
500 let end = end_idx.unwrap_or(lines.len() - 1) + 1;
502 Some(lines.get(start_idx..end)?.join("\n"))
503 })
504 .collect();
505
506 if conflict_sections.is_empty() {
507 String::new()
508 } else {
509 conflict_sections.join("\n\n")
510 }
511}
512
513#[cfg(test)]
514mod tests;
515
516#[cfg(test)]
517mod io_tests;