use anyhow::{anyhow, Result};
use std::path::Path;
use std::process::Command;
pub fn run(dir: Option<&Path>, args: &[&str]) -> Result<String> {
let mut cmd = Command::new("git");
cmd.args(args);
if let Some(d) = dir {
cmd.current_dir(d);
}
let output = cmd
.output()
.map_err(|e| anyhow!("Failed to invoke git: {}", e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(anyhow!("git {} failed: {}", args.join(" "), stderr.trim()));
}
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}
pub fn clone(url: &str, target_dir: &Path) -> Result<()> {
if target_dir.exists() {
return Err(anyhow!(
"Target directory already exists: {}",
target_dir.display()
));
}
let target_str = target_dir.to_string_lossy().to_string();
run(None, &["clone", "--depth", "1", url, &target_str])?;
Ok(())
}
pub fn pull(dir: &Path) -> Result<()> {
run(Some(dir), &["pull", "--ff-only"])?;
Ok(())
}
#[allow(dead_code)]
pub fn remote_url(dir: &Path) -> Result<String> {
let out = run(Some(dir), &["remote", "get-url", "origin"])?;
Ok(out.trim().to_string())
}
pub fn recent_log(dir: &Path, limit: usize) -> Result<Vec<String>> {
let limit_str = limit.to_string();
let out = run(
Some(dir),
&[
"log",
"--pretty=format:%h | %ad | %an | %s",
"--date=short",
"-n",
&limit_str,
],
)?;
Ok(out.lines().map(|s| s.to_string()).collect())
}
pub fn changed_files_recent(dir: &Path, last_n: usize) -> Result<Vec<(String, usize)>> {
let limit_str = last_n.to_string();
let out = run(
Some(dir),
&["log", "--name-only", "--pretty=format:", "-n", &limit_str],
)?;
let mut counts: std::collections::BTreeMap<String, usize> = std::collections::BTreeMap::new();
for line in out.lines() {
let name = line.trim();
if name.is_empty() {
continue;
}
*counts.entry(name.to_string()).or_insert(0) += 1;
}
let mut v: Vec<(String, usize)> = counts.into_iter().collect();
v.sort_by_key(|(_, count)| std::cmp::Reverse(*count));
Ok(v)
}
pub fn status_porcelain(dir: &Path) -> Result<Vec<String>> {
let out = run(Some(dir), &["status", "--porcelain"])?;
Ok(out.lines().map(|s| s.to_string()).collect())
}
pub fn current_branch(dir: &Path) -> Result<String> {
let out = run(Some(dir), &["rev-parse", "--abbrev-ref", "HEAD"])?;
Ok(out.trim().to_string())
}
pub fn is_repo(dir: &Path) -> bool {
run(Some(dir), &["rev-parse", "--is-inside-work-tree"]).is_ok()
}