use std::path::{Path, PathBuf};
use tracing::debug;
use crate::CoreError;
pub trait GitOps: Send + Sync {
fn worktree_add(&self, path: &Path, branch: &str, base: &str) -> Result<(), CoreError>;
fn worktree_remove(&self, path: &Path, force: bool) -> Result<(), CoreError>;
fn worktree_prune(&self) -> Result<(), CoreError>;
fn branch_delete(&self, branch: &str) -> Result<(), CoreError>;
fn add(&self, cwd: &Path, paths: &[&str]) -> Result<(), CoreError>;
fn has_staged_changes(&self, cwd: &Path) -> bool;
fn commit(&self, cwd: &Path, message: &str) -> Result<(), CoreError>;
fn diff(&self, cwd: &Path, base: &str) -> Result<String, CoreError>;
fn log_oneline(&self, cwd: &Path, range: &str) -> Result<String, CoreError>;
fn push(&self, cwd: &Path, branch: &str) -> Result<(), CoreError>;
fn detect_default_branch(&self) -> String;
}
#[derive(Debug)]
pub struct DefaultGitOps {
project_root: PathBuf,
}
impl DefaultGitOps {
pub fn new(project_root: PathBuf) -> Self {
Self { project_root }
}
}
impl GitOps for DefaultGitOps {
fn worktree_add(&self, path: &Path, branch: &str, base: &str) -> Result<(), CoreError> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).map_err(CoreError::IoError)?;
}
run_git(
&self.project_root,
&[
"worktree",
"add",
&path.display().to_string(),
"-b",
branch,
base,
],
)?;
Ok(())
}
fn worktree_remove(&self, path: &Path, force: bool) -> Result<(), CoreError> {
let path_str = path.display().to_string();
let mut args = vec!["worktree", "remove", &path_str];
if force {
args.push("--force");
}
run_git(&self.project_root, &args)?;
Ok(())
}
fn worktree_prune(&self) -> Result<(), CoreError> {
run_git(&self.project_root, &["worktree", "prune"])?;
Ok(())
}
fn branch_delete(&self, branch: &str) -> Result<(), CoreError> {
run_git(&self.project_root, &["branch", "-D", branch])?;
Ok(())
}
fn add(&self, cwd: &Path, paths: &[&str]) -> Result<(), CoreError> {
let mut args = vec!["add"];
args.extend(paths);
run_git(cwd, &args)?;
Ok(())
}
fn has_staged_changes(&self, cwd: &Path) -> bool {
run_git(cwd, &["diff", "--cached", "--quiet"]).is_err()
}
fn commit(&self, cwd: &Path, message: &str) -> Result<(), CoreError> {
run_git(cwd, &["commit", "-m", message])?;
Ok(())
}
fn diff(&self, cwd: &Path, base: &str) -> Result<String, CoreError> {
run_git(cwd, &["diff", base, "HEAD"])
}
fn log_oneline(&self, cwd: &Path, range: &str) -> Result<String, CoreError> {
run_git(cwd, &["log", range, "--oneline", "--no-decorate"])
}
fn push(&self, cwd: &Path, branch: &str) -> Result<(), CoreError> {
run_git(cwd, &["push", "origin", branch])?;
Ok(())
}
fn detect_default_branch(&self) -> String {
let output = std::process::Command::new("git")
.args(["symbolic-ref", "refs/remotes/origin/HEAD", "--short"])
.current_dir(&self.project_root)
.output();
if let Ok(output) = output
&& output.status.success()
{
let branch = String::from_utf8_lossy(&output.stdout).trim().to_string();
if let Some(name) = branch.strip_prefix("origin/") {
return name.to_string();
}
return branch;
}
"main".to_string()
}
}
fn run_git(cwd: &Path, args: &[&str]) -> Result<String, CoreError> {
debug!(cwd = %cwd.display(), args = ?args, "git");
let output = std::process::Command::new("git")
.args(args)
.current_dir(cwd)
.output()
.map_err(CoreError::IoError)?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(CoreError::GitError(format!(
"git {} failed: {stderr}",
args.join(" "),
)));
}
Ok(String::from_utf8_lossy(&output.stdout).to_string())
}