use std::path::Path;
use std::process::Command;
fn git(args: &[&str]) -> Result<String, String> {
run_git(None, args)
}
pub(crate) fn git_in(cwd: &Path, args: &[&str]) -> Result<String, String> {
run_git(Some(cwd), args)
}
fn run_git(cwd: Option<&Path>, args: &[&str]) -> Result<String, String> {
let mut cmd = Command::new("git");
cmd.args(args);
if let Some(dir) = cwd {
cmd.current_dir(dir);
}
let out = cmd.output().map_err(|e| format!("git command failed: {e}"))?;
if out.status.success() {
Ok(String::from_utf8_lossy(&out.stdout).to_string())
} else {
let stderr = String::from_utf8_lossy(&out.stderr);
let last_line = stderr
.lines()
.filter(|l| !l.trim().is_empty())
.last()
.unwrap_or("git failed")
.to_string();
Err(last_line)
}
}
pub fn ls_tree_md(ref_: &str, dir: &str) -> Result<Vec<String>, String> {
let out = git(&["ls-tree", "-r", "--name-only", ref_, "--", dir])?;
Ok(out
.lines()
.map(|l| l.trim().to_string())
.filter(|l| l.ends_with(".md") && !l.is_empty())
.collect())
}
pub fn show_ref(ref_: &str, path: &str) -> Result<String, String> {
git(&["show", &format!("{ref_}:{path}")])
}
pub fn worktree_add(path: &Path, branch: &str, ref_: Option<&str>) -> Result<(), String> {
let branch_exists = git(&["rev-parse", "--verify", &format!("refs/heads/{branch}")]).is_ok();
let mut args: Vec<&str> = vec!["worktree", "add"];
if !branch_exists {
args.push("-b");
args.push(branch);
}
args.push(path.to_str().unwrap_or_default());
if let Some(r) = ref_ {
args.push(r);
}
git(&args).map(|_| ())
}
pub fn worktree_remove(path: &Path, force: bool) -> Result<(), String> {
let mut args = vec!["worktree", "remove"];
if force {
args.push("--force");
}
args.push(path.to_str().unwrap_or_default());
git(&args).map(|_| ())
}
pub fn branch_delete(branch: &str, force: bool) -> Result<(), String> {
let flag = if force { "-D" } else { "-d" };
git(&["branch", flag, branch]).map(|_| ())
}
pub fn merge_no_ff(branch: &str) -> Result<String, String> {
git(&["merge", "--no-ff", branch])
}
pub fn checkout(branch: &str) -> Result<(), String> {
git(&["checkout", branch]).map(|_| ())
}
pub fn add_file(file: &str, cwd: &Path) -> Result<(), String> {
git_in(cwd, &["add", "--", file]).map(|_| ())
}
pub fn commit_in(message: &str, cwd: &Path) -> Result<(), String> {
git_in(cwd, &["commit", "-m", message]).map(|_| ())
}
pub fn commit(message: &str, files: &[&str]) -> Result<(), String> {
let mut args = vec!["commit", "-m", message];
if !files.is_empty() {
args.push("--");
args.extend_from_slice(files);
}
git(&args).map(|_| ())
}
pub fn diff_refs(ref1: &str, ref2: &str) -> Result<String, String> {
git(&["diff", &format!("{ref1}..{ref2}")])
}
pub fn branch_list(pattern: Option<&str>) -> Result<Vec<String>, String> {
let mut args = vec!["branch", "--list"];
if let Some(p) = pattern {
args.push(p);
}
let out = git(&args)?;
Ok(out
.lines()
.map(|l| {
if l.len() >= 2 && (&l[..2] == "* " || &l[..2] == " ") {
l[2..].trim().to_string()
} else {
l.trim().to_string()
}
})
.filter(|l| !l.is_empty())
.collect())
}
pub fn worktree_list() -> Result<Vec<String>, String> {
let out = git(&["worktree", "list", "--porcelain"])?;
Ok(out.lines().filter(|l| !l.is_empty()).map(String::from).collect())
}
pub fn rev_parse_verify(ref_: &str) -> Result<String, String> {
git(&["rev-parse", "--verify", ref_])
}
pub fn git_common_dir(cwd: &Path) -> Result<String, String> {
let out = git_in(cwd, &["rev-parse", "--git-common-dir"])?;
let gd = out.trim().trim_end_matches('/');
let path = std::path::Path::new(gd);
if path.is_relative() {
let abs = cwd.join(path);
Ok(abs.to_string_lossy().to_string())
} else {
Ok(gd.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
fn with_temp_repo<F: FnOnce(&TempDir, &Path)>(f: F) {
let tmp = TempDir::new().unwrap();
let repo = tmp.path().join("repo");
fs::create_dir_all(&repo).unwrap();
git_in(&repo, &["init", "-b", "main"]).unwrap();
git_in(&repo, &["config", "user.email", "test@test"]).unwrap();
git_in(&repo, &["config", "user.name", "Test"]).unwrap();
fs::write(repo.join("README.md"), "# test").unwrap();
git_in(&repo, &["add", "."]).unwrap();
git_in(&repo, &["commit", "-m", "init"]).unwrap();
f(&tmp, &repo);
}
#[test]
fn test_git_common_dir() {
with_temp_repo(|_tmp, repo| {
let gd = git_common_dir(repo).unwrap();
assert!(gd.ends_with(".git"), "gd = {gd}");
let p = std::path::Path::new(&gd);
assert!(p.is_absolute(), "git-common-dir should be absolute: {gd}");
});
}
#[test]
fn test_branch_list_format() {
let sample = "* main\n feature\n";
with_temp_repo(|_tmp, repo| {
git_in(repo, &["branch", "feature-a"]).unwrap();
});
}
}