use crate::cli::OperationMode;
use anyhow::{Context, Result, anyhow};
use std::path::Path;
use std::process::Command;
pub struct GitExecutor;
impl GitExecutor {
pub fn new() -> Self {
Self
}
pub fn is_git_repo() -> bool {
Command::new("git")
.args(["rev-parse", "--git-dir"])
.output()
.map(|output| output.status.success())
.unwrap_or(false)
}
pub fn get_diff(&self, mode: &OperationMode) -> Result<String> {
match mode {
OperationMode::GitWorkingDirectory => self.execute_git_diff(&["diff"]),
OperationMode::GitCached => self.execute_git_diff(&["diff", "--cached"]),
OperationMode::GitDiff { target } => self.execute_git_diff(&["diff", target]),
OperationMode::GitStatus => {
self.execute_git_diff(&["diff"])
}
OperationMode::Compare { target1, target2 } => {
if self.is_git_ref(target1)? && self.is_git_ref(target2)? {
self.execute_git_diff(&["diff", &format!("{target1}..{target2}")])
} else {
self.execute_regular_diff(target1, target2)
}
}
OperationMode::Completions { .. } => {
Err(anyhow!("Completions mode should not call get_diff"))
}
OperationMode::Invalid { reason } => Err(anyhow!("Invalid operation mode: {}", reason)),
}
}
#[allow(dead_code)]
pub fn get_changed_files(&self, mode: &OperationMode) -> Result<Vec<String>> {
match mode {
OperationMode::GitWorkingDirectory => {
self.execute_git_name_only(&["diff", "--name-only"])
}
OperationMode::GitCached => {
self.execute_git_name_only(&["diff", "--cached", "--name-only"])
}
OperationMode::GitDiff { target } => {
self.execute_git_name_only(&["diff", "--name-only", target])
}
OperationMode::GitStatus => self.execute_git_name_only(&["diff", "--name-only"]),
OperationMode::Compare { target1, target2 } => {
if self.is_git_ref(target1)? && self.is_git_ref(target2)? {
self.execute_git_name_only(&[
"diff",
"--name-only",
&format!("{target1}..{target2}"),
])
} else {
Ok(vec![target1.clone(), target2.clone()])
}
}
OperationMode::Completions { .. } => Err(anyhow!(
"Completions mode should not call get_changed_files"
)),
OperationMode::Invalid { reason } => Err(anyhow!("Invalid operation mode: {}", reason)),
}
}
pub fn get_file_diff(&self, mode: &OperationMode, file_path: &str) -> Result<String> {
match mode {
OperationMode::GitWorkingDirectory => self.execute_git_diff(&["diff", "--", file_path]),
OperationMode::GitCached => {
self.execute_git_diff(&["diff", "--cached", "--", file_path])
}
OperationMode::GitDiff { target } => {
self.execute_git_diff(&["diff", target, "--", file_path])
}
OperationMode::GitStatus => self.execute_git_diff(&["diff", "--", file_path]),
OperationMode::Compare { target1, target2 } => {
if self.is_git_ref(target1)? && self.is_git_ref(target2)? {
self.execute_git_diff(&[
"diff",
&format!("{target1}..{target2}"),
"--",
file_path,
])
} else {
self.execute_regular_diff(target1, target2)
}
}
OperationMode::Completions { .. } => {
Err(anyhow!("Completions mode should not call get_file_diff"))
}
OperationMode::Invalid { reason } => Err(anyhow!("Invalid operation mode: {}", reason)),
}
}
fn execute_git_diff(&self, args: &[&str]) -> Result<String> {
let output = Command::new("git")
.args(args)
.output()
.context("Failed to execute git diff")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(anyhow!("Git diff failed: {}", stderr));
}
String::from_utf8(output.stdout).context("Git diff output is not valid UTF-8")
}
#[allow(dead_code)]
fn execute_git_name_only(&self, args: &[&str]) -> Result<Vec<String>> {
let output = Command::new("git")
.args(args)
.output()
.context("Failed to execute git diff --name-only")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(anyhow!("Git diff --name-only failed: {}", stderr));
}
let stdout = String::from_utf8(output.stdout).context("Git output is not valid UTF-8")?;
Ok(stdout
.lines()
.filter(|line| !line.is_empty())
.map(|line| line.to_string())
.collect())
}
fn execute_regular_diff(&self, file1: &str, file2: &str) -> Result<String> {
let output = Command::new("diff")
.args(["-u", file1, file2])
.output()
.context("Failed to execute diff")?;
if output.status.code() == Some(2) {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(anyhow!("Diff command failed: {}", stderr));
}
String::from_utf8(output.stdout).context("Diff output is not valid UTF-8")
}
fn is_git_ref(&self, ref_name: &str) -> Result<bool> {
if Path::new(ref_name).exists() {
return Ok(false);
}
let output = Command::new("git")
.args(["rev-parse", "--verify", ref_name])
.output()
.context("Failed to check git ref")?;
Ok(output.status.success())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_git_executor_creation() {
let _executor = GitExecutor::new();
}
#[test]
fn test_is_git_repo() {
let result = GitExecutor::is_git_repo();
let _is_boolean = matches!(result, true | false);
}
}