Skip to main content

weave_core/
git.rs

1use std::path::{Path, PathBuf};
2use std::process::Command;
3
4/// Find the root of the git repository by walking up from the current directory.
5pub 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
16/// Find the root of the git repository that contains the given path.
17/// Uses `git -C <dir> rev-parse --show-toplevel` so it works regardless of CWD.
18pub 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
37/// Find the merge base between two refs.
38pub 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
52/// Show file content at a given revision.
53pub 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
62/// Get files changed in both branches relative to their merge base.
63pub 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
91/// Get files changed between two refs.
92pub 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
107/// Read a file from the working tree relative to a root path.
108pub 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}