1use std::path::{Path, PathBuf};
2use std::process::Command;
3
4pub fn find_repo_root() -> Result<PathBuf, Box<dyn std::error::Error>> {
6 let output = Command::new("git")
7 .args(["rev-parse", "--show-toplevel"])
8 .output()?;
9 if !output.status.success() {
10 return Err("Not inside a git repository".into());
11 }
12 let root = String::from_utf8_lossy(&output.stdout).trim().to_string();
13 Ok(PathBuf::from(root))
14}
15
16pub fn find_repo_root_from_path(path: &Path) -> Result<PathBuf, Box<dyn std::error::Error>> {
19 let dir = if path.is_dir() {
20 path.to_path_buf()
21 } else {
22 path.parent()
23 .map(|p| if p.as_os_str().is_empty() { Path::new(".") } else { p })
24 .unwrap_or(Path::new("."))
25 .to_path_buf()
26 };
27 let output = Command::new("git")
28 .args(["-C", &dir.to_string_lossy(), "rev-parse", "--show-toplevel"])
29 .output()?;
30 if !output.status.success() {
31 return Err(format!("Not inside a git repository: {}", dir.display()).into());
32 }
33 let root = String::from_utf8_lossy(&output.stdout).trim().to_string();
34 Ok(PathBuf::from(root))
35}
36
37pub fn find_merge_base(head: &str, branch: &str) -> Result<String, Box<dyn std::error::Error>> {
39 let output = Command::new("git")
40 .args(["merge-base", head, branch])
41 .output()?;
42 if !output.status.success() {
43 return Err(format!(
44 "Failed to find merge base between '{}' and '{}'. Are both branches valid?",
45 head, branch
46 )
47 .into());
48 }
49 Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
50}
51
52pub fn git_show(rev: &str, file: &str) -> Result<String, Box<dyn std::error::Error>> {
54 let spec = format!("{}:{}", rev, file);
55 let output = Command::new("git").args(["show", &spec]).output()?;
56 if !output.status.success() {
57 return Err(format!("git show {} failed", spec).into());
58 }
59 Ok(String::from_utf8_lossy(&output.stdout).to_string())
60}
61
62pub fn get_changed_files(
64 merge_base: &str,
65 head: &str,
66 branch: &str,
67) -> Result<Vec<String>, Box<dyn std::error::Error>> {
68 let ours_output = Command::new("git")
69 .args(["diff", "--name-only", merge_base, head])
70 .output()?;
71 let ours_files: std::collections::HashSet<String> =
72 String::from_utf8_lossy(&ours_output.stdout)
73 .lines()
74 .map(|s| s.to_string())
75 .collect();
76
77 let theirs_output = Command::new("git")
78 .args(["diff", "--name-only", merge_base, branch])
79 .output()?;
80 let theirs_files: std::collections::HashSet<String> =
81 String::from_utf8_lossy(&theirs_output.stdout)
82 .lines()
83 .map(|s| s.to_string())
84 .collect();
85
86 let mut both: Vec<String> = ours_files.intersection(&theirs_files).cloned().collect();
87 both.sort();
88 Ok(both)
89}
90
91pub fn diff_files(
93 base_ref: &str,
94 target_ref: &str,
95) -> Result<Vec<String>, Box<dyn std::error::Error>> {
96 let output = Command::new("git")
97 .args(["diff", "--name-only", base_ref, target_ref])
98 .output()?;
99 let files: Vec<String> = String::from_utf8_lossy(&output.stdout)
100 .lines()
101 .filter(|s| !s.is_empty())
102 .map(|s| s.to_string())
103 .collect();
104 Ok(files)
105}
106
107pub fn read_file(root: &Path, file_path: &str) -> Result<String, Box<dyn std::error::Error>> {
109 let full = root.join(file_path);
110 Ok(std::fs::read_to_string(full)?)
111}