use anyhow::{bail, Result};
use std::process::Command;
use std::path::PathBuf;
use std::fs;
use super::GitCli;
impl GitCli {
fn run_with_timeout(mut cmd: Command, timeout_secs: u64) -> Result<std::process::Output> {
use std::io::Read;
use std::time::{Duration, Instant};
let start = Instant::now();
let mut child = cmd.stdout(std::process::Stdio::piped()).stderr(std::process::Stdio::piped()).spawn()?;
loop {
if let Some(status) = child.try_wait()? {
let (mut stdout, mut stderr) = (Vec::new(), Vec::new());
if let Some(mut out) = child.stdout.take() { let _ = out.read_to_end(&mut stdout); }
if let Some(mut err) = child.stderr.take() { let _ = err.read_to_end(&mut stderr); }
return Ok(std::process::Output { status, stdout, stderr });
}
if start.elapsed() > Duration::from_secs(timeout_secs) {
let _ = child.kill();
let _ = child.wait();
bail!("git command timed out after {}s", timeout_secs);
}
std::thread::sleep(Duration::from_millis(200));
}
}
pub fn pull_ff_only(&self, repo_path: &str, ff_only: bool, timeout_secs: u64) -> Result<()> {
let mut cmd = Command::new("git");
cmd.args(["-C", repo_path, "pull"]);
if ff_only { cmd.arg("--ff-only"); }
let output = Self::run_with_timeout(cmd, timeout_secs)?;
if !output.status.success() { bail!("git pull failed: {}", String::from_utf8_lossy(&output.stderr)); }
Ok(())
}
pub fn pull_rebase(&self, repo_path: &str, autostash: bool, timeout_secs: u64) -> Result<()> {
let mut cmd = Command::new("git");
cmd.args(["-C", repo_path, "pull", "--rebase"]);
if autostash { cmd.arg("--autostash"); }
let output = Self::run_with_timeout(cmd, timeout_secs)?;
if !output.status.success() { bail!("git pull --rebase failed: {}", String::from_utf8_lossy(&output.stderr)); }
Ok(())
}
pub fn pull(&self, repo_path: &str, timeout_secs: u64) -> Result<()> {
let output = Self::run_with_timeout(
{ let mut cmd = Command::new("git"); cmd.args(["-C", repo_path, "pull"]); cmd },
timeout_secs
)?;
if !output.status.success() { bail!("git pull failed: {}", String::from_utf8_lossy(&output.stderr)); }
Ok(())
}
pub fn fetch_all_prune(&self, repo_path: &str) -> Result<()> {
let output = Command::new("git").args(["-C", repo_path, "fetch", "--all", "--prune"]).output()?;
if !output.status.success() { bail!("git fetch --all --prune failed: {}", String::from_utf8_lossy(&output.stderr)); }
Ok(())
}
pub fn apply_patch(&self, repo_path: &str, patch: &str, to_index: bool, reverse: bool) -> Result<()> {
use std::io::Write;
use std::process::Stdio;
let mut cmd = Command::new("git");
cmd.args(["-C", repo_path, "apply"]);
if to_index { cmd.arg("--cached"); }
if reverse { cmd.arg("--reverse"); }
let mut child = cmd.stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::piped()).spawn()?;
if let Some(mut stdin) = child.stdin.take() { stdin.write_all(patch.as_bytes())?; }
let output = child.wait_with_output()?;
if !output.status.success() { bail!("git apply failed: {}", String::from_utf8_lossy(&output.stderr)); }
Ok(())
}
pub fn read_rebase_todo(&self, repo_path: &str) -> Result<(Vec<String>, String)> {
let candidates = [
PathBuf::from(repo_path).join(".git").join("rebase-merge").join("git-rebase-todo"),
PathBuf::from(repo_path).join(".git").join("rebase-apply").join("git-rebase-todo"),
];
for path in &candidates {
if path.exists() {
let lines: Vec<String> = fs::read_to_string(path)?.lines().map(|s| s.to_string()).collect();
return Ok((lines, path.to_string_lossy().to_string()));
}
}
bail!("no rebase todo found");
}
pub fn start_rebase_interactive(&self, repo_path: &str, base: &str) -> Result<String> {
let git_dir = PathBuf::from(repo_path).join(".git");
let rebase_merge = git_dir.join("rebase-merge");
let rebase_apply = git_dir.join("rebase-apply");
if rebase_merge.exists() { fs::remove_dir_all(&rebase_merge).ok(); }
if rebase_apply.exists() { fs::remove_dir_all(&rebase_apply).ok(); }
let marker = std::env::temp_dir().join(format!("eazygit_todo_{}", std::process::id()));
let script = format!("cp \"$1\" \"{}\" && printf 'break\\n' > \"$1\"", marker.to_string_lossy());
let output = Command::new("git")
.env("GIT_SEQUENCE_EDITOR", format!("sh -c '{}' sh", script))
.args(["-C", repo_path, "rebase", "-i", base]).output()?;
if !marker.exists() { bail!("git rebase -i {} failed: {}", base, String::from_utf8_lossy(&output.stderr).lines().next().unwrap_or("")); }
let _ = fs::remove_file(&marker);
for dir in &[&rebase_merge, &rebase_apply] {
let todo = dir.join("git-rebase-todo");
if todo.exists() { return Ok(todo.to_string_lossy().to_string()); }
}
bail!("git rebase -i {} did not create todo file", base);
}
pub fn start_rebase_interactive_root(&self, repo_path: &str) -> Result<String> {
let git_dir = PathBuf::from(repo_path).join(".git");
let rebase_merge = git_dir.join("rebase-merge");
let rebase_apply = git_dir.join("rebase-apply");
if rebase_merge.exists() { fs::remove_dir_all(&rebase_merge).ok(); }
if rebase_apply.exists() { fs::remove_dir_all(&rebase_apply).ok(); }
let marker = std::env::temp_dir().join(format!("eazygit_todo_root_{}", std::process::id()));
let script = format!("cp \"$1\" \"{}\" && printf 'break\\n' > \"$1\"", marker.to_string_lossy());
let output = Command::new("git")
.env("GIT_SEQUENCE_EDITOR", format!("sh -c '{}' sh", script))
.args(["-C", repo_path, "rebase", "-i", "--root"]).output()?;
if !marker.exists() { bail!("git rebase -i --root failed: {}", String::from_utf8_lossy(&output.stderr).lines().next().unwrap_or("")); }
let _ = fs::remove_file(&marker);
for dir in &[&rebase_merge, &rebase_apply] {
let todo = dir.join("git-rebase-todo");
if todo.exists() { return Ok(todo.to_string_lossy().to_string()); }
}
bail!("git rebase -i --root did not create todo file");
}
pub fn start_rebase_with_todo(&self, repo_path: &str, base: &str, todo_content: &str, use_root: bool) -> Result<String> {
let git_dir = PathBuf::from(repo_path).join(".git");
let rebase_merge = git_dir.join("rebase-merge");
let rebase_apply = git_dir.join("rebase-apply");
if rebase_merge.exists() { fs::remove_dir_all(&rebase_merge).ok(); }
if rebase_apply.exists() { fs::remove_dir_all(&rebase_apply).ok(); }
let temp_todo = std::env::temp_dir().join(format!("eazygit_rebase_todo_{}_{}", std::process::id(), std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs()));
fs::write(&temp_todo, todo_content)?;
let script = format!("cp \"{}\" \"$1\"", temp_todo.to_string_lossy());
let rebase_args = if use_root {
vec!["-C", repo_path, "rebase", "-i", "--root"]
} else {
vec!["-C", repo_path, "rebase", "-i", base]
};
let output = Command::new("git")
.env("GIT_SEQUENCE_EDITOR", format!("sh -c '{}' sh", script))
.args(&rebase_args)
.output()?;
let _ = fs::remove_file(&temp_todo);
if !output.status.success() {
bail!("git rebase -i {} failed: {}", base, String::from_utf8_lossy(&output.stderr));
}
for dir in &[&rebase_merge, &rebase_apply] {
let todo = dir.join("git-rebase-todo");
if todo.exists() { return Ok(todo.to_string_lossy().to_string()); }
}
bail!("git rebase -i {} did not create todo file", base);
}
pub fn root_commit(&self, repo_path: &str) -> Result<String> {
let output = Command::new("git").args(["-C", repo_path, "rev-list", "--max-parents=0", "HEAD"]).output()?;
if !output.status.success() { bail!("git rev-list failed: {}", String::from_utf8_lossy(&output.stderr)); }
Ok(String::from_utf8_lossy(&output.stdout).lines().next().unwrap_or("").to_string())
}
pub fn list_pull_requests(&self, repo_path: &str, remote: &str) -> Result<Vec<(String, String)>> {
let output = Command::new("git").args(["-C", repo_path, "ls-remote", "--refs", remote, "pull/*/head"]).output()?;
if !output.status.success() { bail!("git ls-remote failed: {}", String::from_utf8_lossy(&output.stderr)); }
let mut prs = Vec::new();
for line in String::from_utf8_lossy(&output.stdout).lines() {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 2 {
if let Some(num) = parts.get(1).and_then(|r| r.trim().split('/').nth(2)) {
prs.push((num.to_string(), parts[1].to_string()));
}
}
}
Ok(prs)
}
pub fn fetch_pr(&self, repo_path: &str, remote: &str, pr: &str) -> Result<()> {
let target = format!("pull/{}/head:pr/{}", pr, pr);
let output = Command::new("git").args(["-C", repo_path, "fetch", remote, &target]).output()?;
if !output.status.success() { bail!("git fetch {} failed: {}", target, String::from_utf8_lossy(&output.stderr)); }
Ok(())
}
pub fn checkout_pr_branch(&self, repo_path: &str, pr: &str) -> Result<()> {
let branch = format!("pr/{}", pr);
let output = Command::new("git").args(["-C", repo_path, "checkout", &branch]).output()?;
if !output.status.success() { bail!("git checkout {} failed: {}", branch, String::from_utf8_lossy(&output.stderr)); }
Ok(())
}
pub fn get_config(&self, repo_path: &str, key: &str) -> Result<Option<String>> {
let output = Command::new("git").args(["-C", repo_path, "config", "--get", key]).output()?;
if output.status.success() {
Ok(Some(String::from_utf8_lossy(&output.stdout).trim().to_string()))
} else {
Ok(None)
}
}
}