mod staging;
mod commit;
mod branch;
mod remote;
mod history;
mod rebase;
mod misc;
use anyhow::Result;
use std::process::Command;
pub struct GitCli;
impl GitCli {
pub fn new() -> Self {
Self
}
}
pub(crate) fn run_git_cmd(repo_path: &str, args: &[&str]) -> Result<String> {
let mut cmd = Command::new("git");
cmd.arg("-C").arg(repo_path);
for arg in args {
cmd.arg(arg);
}
let output = cmd.output()?;
if !output.status.success() {
anyhow::bail!(
"git {} failed: {}",
args.first().unwrap_or(&""),
String::from_utf8_lossy(&output.stderr)
);
}
Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
}
pub(crate) fn run_git_cmd_ok(repo_path: &str, args: &[&str]) -> Result<()> {
run_git_cmd(repo_path, args)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
use std::fs;
use std::process::Command;
fn setup_test_repo() -> std::path::PathBuf {
let temp_dir = tempdir().expect("Failed to create temp directory");
let repo_path = temp_dir.path().to_path_buf();
Command::new("git")
.arg("init")
.arg(&repo_path)
.output()
.expect("Failed to init repo");
Command::new("git")
.args(["-C", repo_path.to_str().unwrap(), "config", "user.name", "Test"])
.output()
.expect("Failed to set user.name");
Command::new("git")
.args(["-C", repo_path.to_str().unwrap(), "config", "user.email", "test@test.com"])
.output()
.expect("Failed to set user.email");
std::mem::forget(temp_dir);
repo_path
}
#[test]
fn test_status_porcelain_parsing() {
let repo_path = setup_test_repo();
let git_cli = GitCli::new();
let test_file = repo_path.join("test.txt");
fs::write(&test_file, "content").expect("Failed to write file");
let status = git_cli.status_porcelain(repo_path.to_str().unwrap())
.expect("Failed to get status");
assert!(status.contains("test.txt") || status.contains("??"));
let _ = fs::remove_dir_all(&repo_path);
}
#[test]
fn test_staging_operations() {
let repo_path = setup_test_repo();
let git_cli = GitCli::new();
let test_file1 = repo_path.join("file1.txt");
fs::write(&test_file1, "content1").expect("Failed to write file");
Command::new("git")
.args(["-C", repo_path.to_str().unwrap(), "add", "file1.txt"])
.output()
.expect("Failed to stage");
Command::new("git")
.args(["-C", repo_path.to_str().unwrap(), "commit", "-m", "Initial"])
.output()
.expect("Failed to commit");
let test_file2 = repo_path.join("file2.txt");
fs::write(&test_file2, "content2").expect("Failed to write file");
git_cli.stage_file(repo_path.to_str().unwrap(), "file2.txt")
.expect("Failed to stage file");
let status = git_cli.status_porcelain(repo_path.to_str().unwrap())
.expect("Failed to get status");
assert!(status.contains("A ") || status.contains("file2.txt"));
git_cli.unstage_file(repo_path.to_str().unwrap(), "file2.txt")
.expect("Failed to unstage file");
let _ = fs::remove_dir_all(&repo_path);
}
#[test]
fn test_error_handling() {
let repo_path = setup_test_repo();
let git_cli = GitCli::new();
let result = git_cli.stage_file(repo_path.to_str().unwrap(), "nonexistent.txt");
assert!(result.is_ok() || result.is_err());
let _ = fs::remove_dir_all(&repo_path);
}
#[test]
fn test_branch_operations() {
let repo_path = setup_test_repo();
let git_cli = GitCli::new();
let test_file = repo_path.join("file.txt");
fs::write(&test_file, "content").expect("Failed to write file");
Command::new("git")
.args(["-C", repo_path.to_str().unwrap(), "add", "file.txt"])
.output()
.expect("Failed to stage");
Command::new("git")
.args(["-C", repo_path.to_str().unwrap(), "commit", "-m", "Initial"])
.output()
.expect("Failed to commit");
Command::new("git")
.args(["-C", repo_path.to_str().unwrap(), "branch", "test-branch"])
.output()
.expect("Failed to create branch");
Command::new("git")
.args(["-C", repo_path.to_str().unwrap(), "checkout", "test-branch"])
.output()
.expect("Failed to checkout");
let current = git_cli.current_branch(repo_path.to_str().unwrap())
.expect("Failed to get current branch");
assert_eq!(current, "test-branch");
let _ = fs::remove_dir_all(&repo_path);
}
#[test]
fn test_rebase_operations() {
let repo_path = setup_test_repo();
let git_cli = GitCli::new();
for i in 0..3 {
let file = repo_path.join(format!("file{}.txt", i));
fs::write(&file, format!("content {}", i)).expect("Failed to write");
Command::new("git")
.args(["-C", repo_path.to_str().unwrap(), "add", &format!("file{}.txt", i)])
.output()
.expect("Failed to stage");
Command::new("git")
.args(["-C", repo_path.to_str().unwrap(), "commit", "-m", &format!("Commit {}", i)])
.output()
.expect("Failed to commit");
}
let output = Command::new("git")
.args(["-C", repo_path.to_str().unwrap(), "rev-parse", "HEAD~2"])
.output()
.expect("Failed to get commit");
let base = String::from_utf8_lossy(&output.stdout).trim().to_string();
let todo_path = git_cli.start_rebase_interactive(repo_path.to_str().unwrap(), &base)
.expect("Failed to start rebase");
assert!(std::path::Path::new(&todo_path).exists());
let (lines, _) = git_cli.read_rebase_todo(repo_path.to_str().unwrap())
.expect("Failed to read todo");
assert!(!lines.is_empty());
git_cli.rebase_abort(repo_path.to_str().unwrap())
.expect("Failed to abort");
let _ = fs::remove_dir_all(&repo_path);
}
}