use anyhow::{Context, Result};
use std::path::Path;
use std::process::{Command, Stdio};
pub trait GitRunner {
fn run(&self, args: &[&str]) -> Result<()>;
fn run_output(&self, args: &[&str]) -> Result<String>;
fn run_output_full(&self, args: &[&str]) -> Result<(String, String)>;
#[allow(dead_code)]
fn run_status(&self, args: &[&str]) -> Result<bool>;
}
pub struct RealGitRunner;
impl GitRunner for RealGitRunner {
fn run(&self, args: &[&str]) -> Result<()> {
let status = Command::new("git")
.args(args)
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status()
.context("Failed to execute git command")?;
anyhow::ensure!(status.success(), "git command failed");
Ok(())
}
fn run_output(&self, args: &[&str]) -> Result<String> {
let output = Command::new("git")
.args(args)
.output()
.context("Failed to execute git command")?;
anyhow::ensure!(output.status.success(), "git command failed");
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}
fn run_output_full(&self, args: &[&str]) -> Result<(String, String)> {
let output = Command::new("git")
.args(args)
.output()
.context("Failed to execute git command")?;
anyhow::ensure!(output.status.success(), "git command failed");
Ok((
String::from_utf8_lossy(&output.stdout).trim().to_string(),
String::from_utf8_lossy(&output.stderr).trim().to_string(),
))
}
fn run_status(&self, args: &[&str]) -> Result<bool> {
let status = Command::new("git")
.args(args)
.status()
.context("Failed to execute git command")?;
Ok(status.success())
}
}
pub fn git(args: &[&str]) -> Result<()> {
let runner = RealGitRunner;
runner.run(args)
}
pub fn git_output(args: &[&str]) -> Result<String> {
let runner = RealGitRunner;
runner.run_output(args)
}
pub fn git_output_full(args: &[&str]) -> Result<(String, String)> {
let runner = RealGitRunner;
runner.run_output_full(args)
}
pub fn validate_git_repo() -> Result<()> {
git_output(&["rev-parse", "--git-dir"]).context(
"Not in a git repository. Please run this command from within a git repository.",
)?;
Ok(())
}
pub fn is_rebase_in_progress() -> Result<bool> {
let git_dir = git_output(&["rev-parse", "--git-dir"])?;
let has_rebase_apply = Path::new(&git_dir).join("rebase-apply").exists();
let has_rebase_merge = Path::new(&git_dir).join("rebase-merge").exists();
Ok(has_rebase_apply || has_rebase_merge)
}
pub fn is_merge_in_progress() -> Result<bool> {
let git_dir = git_output(&["rev-parse", "--git-dir"])?;
Ok(Path::new(&git_dir).join("MERGE_HEAD").exists())
}
pub fn abort_rebase() -> Result<()> {
git(&["rebase", "--abort"])?;
Ok(())
}
pub fn abort_merge() -> Result<()> {
git(&["merge", "--abort"])?;
Ok(())
}
pub fn abort_rebase_or_merge() -> Result<()> {
if is_rebase_in_progress()? {
return abort_rebase();
}
if is_merge_in_progress()? {
return abort_merge();
}
anyhow::bail!("No rebase or merge in progress to abort");
}
pub fn has_conflicts_in_diff() -> bool {
let output = git_output(&["diff", "--check"]).unwrap_or_default();
!output.is_empty()
}
pub fn has_conflicts_in_unmerged() -> Result<bool> {
let status = Command::new("git")
.args(["ls-files", "-u"])
.output()
.context("Failed to check for merge conflicts")?;
Ok(!status.stdout.is_empty())
}
pub fn has_merge_conflicts() -> Result<bool> {
if has_conflicts_in_diff() {
return Ok(true);
}
has_conflicts_in_unmerged()
}
pub fn get_current_branch() -> Result<String> {
git_output(&["rev-parse", "--abbrev-ref", "HEAD"]).context("Failed to determine current branch")
}
pub fn get_commit_counts(branch: &str, upstream: &str) -> (usize, usize) {
let ahead = git_output(&["rev-list", "--count", &format!("{upstream}..{branch}")])
.unwrap_or_else(|_| "0".to_string())
.parse()
.unwrap_or(0);
let behind = git_output(&["rev-list", "--count", &format!("{branch}..{upstream}")])
.unwrap_or_else(|_| "0".to_string())
.parse()
.unwrap_or(0);
(ahead, behind)
}
pub fn get_recent_commits(count: usize) -> Result<Vec<String>> {
let output = git_output(&["log", "--format=%s", "-n", &count.to_string()])?;
Ok(output
.lines()
.map(std::string::ToString::to_string)
.collect())
}