use serde::{Deserialize, Serialize}; use std::process::Command; #[cfg(target_os = "windows")] use crate::process_ext::HideWindow; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GitStatus { pub is_repo: bool, pub branch: Option, pub upstream: Option, pub ahead: u32, pub behind: u32, pub staged: Vec, pub unstaged: Vec, pub untracked: Vec, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GitFileChange { pub path: String, pub status: String, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GitBranch { pub name: String, pub is_current: bool, pub is_remote: bool, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GitLogEntry { pub hash: String, pub short_hash: String, pub author: String, pub date: String, pub message: String, } /// Builds the WSL argument list for running a git command at a Linux path. /// Extracted for testability without requiring WSL to be available. #[cfg(any(target_os = "windows", test))] fn build_wsl_git_args<'a>(working_dir: &'a str, args: &[&'a str]) -> Vec<&'a str> { let mut wsl_args = vec!["--", "git", "-C", working_dir]; wsl_args.extend_from_slice(args); wsl_args } fn run_git_command(working_dir: &str, args: &[&str]) -> Result { #[cfg(target_os = "windows")] let output = { if working_dir.starts_with('/') { // WSL/Linux path — run git through WSL so it can resolve the path correctly. let wsl_args = build_wsl_git_args(working_dir, args); Command::new("wsl") .hide_window() .args(&wsl_args) .output() .map_err(|e| format!("Failed to execute git via WSL: {}", e))? } else { Command::new("git") .hide_window() .args(args) .current_dir(working_dir) .output() .map_err(|e| format!("Failed to execute git: {}", e))? } }; #[cfg(not(target_os = "windows"))] let output = Command::new("git") .args(args) .current_dir(working_dir) .output() .map_err(|e| format!("Failed to execute git: {}", e))?; if output.status.success() { Ok(String::from_utf8_lossy(&output.stdout).to_string()) } else { Err(String::from_utf8_lossy(&output.stderr).to_string()) } } #[tauri::command] pub fn git_status(working_dir: String) -> Result { // Check if it's a git repo let is_repo = run_git_command(&working_dir, &["rev-parse", "--git-dir"]).is_ok(); if !is_repo { return Ok(GitStatus { is_repo: false, branch: None, upstream: None, ahead: 0, behind: 0, staged: vec![], unstaged: vec![], untracked: vec![], }); } // Get current branch let branch = run_git_command(&working_dir, &["rev-parse", "--abbrev-ref", "HEAD"]) .ok() .map(|s| s.trim().to_string()); // Get upstream branch let upstream = run_git_command( &working_dir, &["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], ) .ok() .map(|s| s.trim().to_string()); // Get ahead/behind counts let (ahead, behind) = if upstream.is_some() { let rev_list = run_git_command(&working_dir, &["rev-list", "--left-right", "--count", "@{u}...HEAD"]) .unwrap_or_default(); let parts: Vec<&str> = rev_list.trim().split('\t').collect(); if parts.len() == 2 { ( parts[1].parse().unwrap_or(0), parts[0].parse().unwrap_or(0), ) } else { (0, 0) } } else { (0, 0) }; // Get status with porcelain format let status_output = run_git_command(&working_dir, &["status", "--porcelain=v1"]).unwrap_or_default(); let mut staged = vec![]; let mut unstaged = vec![]; let mut untracked = vec![]; for line in status_output.lines() { if line.len() < 3 { continue; } let index_status = line.chars().next().unwrap_or(' '); let worktree_status = line.chars().nth(1).unwrap_or(' '); let path = line[3..].to_string(); // Untracked files if index_status == '?' && worktree_status == '?' { untracked.push(path); continue; } // Staged changes (index status) if index_status != ' ' && index_status != '?' { staged.push(GitFileChange { path: path.clone(), status: match index_status { 'M' => "modified".to_string(), 'A' => "added".to_string(), 'D' => "deleted".to_string(), 'R' => "renamed".to_string(), 'C' => "copied".to_string(), _ => "unknown".to_string(), }, }); } // Unstaged changes (worktree status) if worktree_status != ' ' && worktree_status != '?' { unstaged.push(GitFileChange { path, status: match worktree_status { 'M' => "modified".to_string(), 'D' => "deleted".to_string(), _ => "unknown".to_string(), }, }); } } Ok(GitStatus { is_repo: true, branch, upstream, ahead, behind, staged, unstaged, untracked, }) } #[tauri::command] pub fn git_diff(working_dir: String, file_path: Option, staged: bool) -> Result { let mut args = vec!["diff"]; if staged { args.push("--cached"); } if let Some(ref path) = file_path { args.push("--"); args.push(path); } run_git_command(&working_dir, &args) } #[tauri::command] pub fn git_branches(working_dir: String) -> Result, String> { let output = run_git_command(&working_dir, &["branch", "-a", "--format=%(refname:short)\t%(HEAD)"])?; let branches: Vec = output .lines() .filter_map(|line| { let parts: Vec<&str> = line.split('\t').collect(); if parts.is_empty() { return None; } let name = parts[0].to_string(); let is_current = parts.get(1).map(|s| *s == "*").unwrap_or(false); let is_remote = name.starts_with("remotes/") || name.starts_with("origin/"); Some(GitBranch { name, is_current, is_remote, }) }) .collect(); Ok(branches) } #[tauri::command] pub fn git_checkout(working_dir: String, branch: String) -> Result { run_git_command(&working_dir, &["checkout", &branch]) } #[tauri::command] pub fn git_stage(working_dir: String, file_path: String) -> Result { run_git_command(&working_dir, &["add", &file_path]) } #[tauri::command] pub fn git_unstage(working_dir: String, file_path: String) -> Result { run_git_command(&working_dir, &["restore", "--staged", &file_path]) } #[tauri::command] pub fn git_stage_all(working_dir: String) -> Result { run_git_command(&working_dir, &["add", "-A"]) } #[tauri::command] pub fn git_commit(working_dir: String, message: String) -> Result { run_git_command(&working_dir, &["commit", "-m", &message]) } #[tauri::command] pub fn git_push(working_dir: String) -> Result { run_git_command(&working_dir, &["push"]) } #[tauri::command] pub fn git_pull(working_dir: String) -> Result { run_git_command(&working_dir, &["pull"]) } #[tauri::command] pub fn git_fetch(working_dir: String) -> Result { run_git_command(&working_dir, &["fetch", "--all"]) } #[tauri::command] pub fn git_log(working_dir: String, limit: Option) -> Result, String> { let limit_str = limit.unwrap_or(10).to_string(); let output = run_git_command( &working_dir, &[ "log", &format!("-{}", limit_str), "--pretty=format:%H\t%h\t%an\t%ar\t%s", ], )?; let entries: Vec = output .lines() .filter_map(|line| { let parts: Vec<&str> = line.split('\t').collect(); if parts.len() < 5 { return None; } Some(GitLogEntry { hash: parts[0].to_string(), short_hash: parts[1].to_string(), author: parts[2].to_string(), date: parts[3].to_string(), message: parts[4..].join("\t"), }) }) .collect(); Ok(entries) } #[tauri::command] pub fn git_discard(working_dir: String, file_path: String) -> Result { run_git_command(&working_dir, &["checkout", "--", &file_path]) } #[tauri::command] pub fn git_create_branch(working_dir: String, branch_name: String) -> Result { run_git_command(&working_dir, &["checkout", "-b", &branch_name]) } #[cfg(test)] mod tests { use super::*; use std::fs::{self, File}; use std::io::Write; use tempfile::TempDir; // ==================== build_wsl_git_args tests ==================== #[test] fn test_build_wsl_git_args_structure() { let args = build_wsl_git_args("/home/naomi/code/project", &["status", "--porcelain=v1"]); assert_eq!(args[0], "--"); assert_eq!(args[1], "git"); assert_eq!(args[2], "-C"); assert_eq!(args[3], "/home/naomi/code/project"); assert_eq!(args[4], "status"); assert_eq!(args[5], "--porcelain=v1"); assert_eq!(args.len(), 6); } #[test] fn test_build_wsl_git_args_no_extra_args() { let args = build_wsl_git_args("/home/user/repo", &["init"]); assert_eq!(args, vec!["--", "git", "-C", "/home/user/repo", "init"]); } // Helper to create a git repository in a temp directory fn create_test_repo() -> TempDir { let temp_dir = TempDir::new().unwrap(); let working_dir = temp_dir.path().to_string_lossy().to_string(); // Initialize git repo run_git_command(&working_dir, &["init"]).unwrap(); // Configure git user for commits run_git_command(&working_dir, &["config", "user.email", "test@example.com"]).unwrap(); run_git_command(&working_dir, &["config", "user.name", "Test User"]).unwrap(); // Disable GPG signing for tests (user may have it enabled globally) run_git_command(&working_dir, &["config", "commit.gpgsign", "false"]).unwrap(); temp_dir } // Helper to create a file in the test repo fn create_file(dir: &TempDir, name: &str, content: &str) { let file_path = dir.path().join(name); let mut file = File::create(file_path).unwrap(); file.write_all(content.as_bytes()).unwrap(); } // ==================== GitStatus struct tests ==================== #[test] fn test_git_status_serialization() { let status = GitStatus { is_repo: true, branch: Some("main".to_string()), upstream: Some("origin/main".to_string()), ahead: 2, behind: 1, staged: vec![GitFileChange { path: "file.txt".to_string(), status: "modified".to_string(), }], unstaged: vec![], untracked: vec!["new_file.txt".to_string()], }; let json = serde_json::to_string(&status).unwrap(); assert!(json.contains("\"is_repo\":true")); assert!(json.contains("\"branch\":\"main\"")); assert!(json.contains("\"ahead\":2")); assert!(json.contains("\"behind\":1")); } #[test] fn test_git_status_not_a_repo() { let status = GitStatus { is_repo: false, branch: None, upstream: None, ahead: 0, behind: 0, staged: vec![], unstaged: vec![], untracked: vec![], }; let json = serde_json::to_string(&status).unwrap(); let deserialized: GitStatus = serde_json::from_str(&json).unwrap(); assert!(!deserialized.is_repo); assert!(deserialized.branch.is_none()); } // ==================== GitFileChange struct tests ==================== #[test] fn test_git_file_change_serialization() { let change = GitFileChange { path: "src/main.rs".to_string(), status: "added".to_string(), }; let json = serde_json::to_string(&change).unwrap(); assert!(json.contains("src/main.rs")); assert!(json.contains("added")); let deserialized: GitFileChange = serde_json::from_str(&json).unwrap(); assert_eq!(deserialized.path, "src/main.rs"); assert_eq!(deserialized.status, "added"); } // ==================== GitBranch struct tests ==================== #[test] fn test_git_branch_serialization() { let branch = GitBranch { name: "feature/new-feature".to_string(), is_current: true, is_remote: false, }; let json = serde_json::to_string(&branch).unwrap(); assert!(json.contains("feature/new-feature")); assert!(json.contains("\"is_current\":true")); assert!(json.contains("\"is_remote\":false")); } #[test] fn test_git_branch_remote() { let branch = GitBranch { name: "origin/main".to_string(), is_current: false, is_remote: true, }; let json = serde_json::to_string(&branch).unwrap(); let deserialized: GitBranch = serde_json::from_str(&json).unwrap(); assert!(deserialized.is_remote); assert!(!deserialized.is_current); } // ==================== GitLogEntry struct tests ==================== #[test] fn test_git_log_entry_serialization() { let entry = GitLogEntry { hash: "abc123def456".to_string(), short_hash: "abc123d".to_string(), author: "Hikari".to_string(), date: "2 hours ago".to_string(), message: "feat: add new feature".to_string(), }; let json = serde_json::to_string(&entry).unwrap(); assert!(json.contains("abc123def456")); assert!(json.contains("Hikari")); assert!(json.contains("feat: add new feature")); } // ==================== git_status integration tests ==================== #[test] fn test_git_status_not_a_git_repo() { let temp_dir = TempDir::new().unwrap(); let working_dir = temp_dir.path().to_string_lossy().to_string(); let result = git_status(working_dir); assert!(result.is_ok()); let status = result.unwrap(); assert!(!status.is_repo); assert!(status.branch.is_none()); assert!(status.staged.is_empty()); } #[test] fn test_git_status_empty_repo() { let temp_dir = create_test_repo(); let working_dir = temp_dir.path().to_string_lossy().to_string(); let result = git_status(working_dir); assert!(result.is_ok()); let status = result.unwrap(); assert!(status.is_repo); assert!(status.staged.is_empty()); assert!(status.unstaged.is_empty()); assert!(status.untracked.is_empty()); } #[test] fn test_git_status_with_untracked_file() { let temp_dir = create_test_repo(); let working_dir = temp_dir.path().to_string_lossy().to_string(); // Create an untracked file create_file(&temp_dir, "untracked.txt", "hello"); let result = git_status(working_dir); assert!(result.is_ok()); let status = result.unwrap(); assert!(status.is_repo); assert!(status.untracked.contains(&"untracked.txt".to_string())); } #[test] fn test_git_status_with_staged_file() { let temp_dir = create_test_repo(); let working_dir = temp_dir.path().to_string_lossy().to_string(); // Create and stage a file create_file(&temp_dir, "staged.txt", "hello"); run_git_command(&working_dir, &["add", "staged.txt"]).unwrap(); let result = git_status(working_dir); assert!(result.is_ok()); let status = result.unwrap(); assert!(status.is_repo); assert!(!status.staged.is_empty()); assert_eq!(status.staged[0].path, "staged.txt"); assert_eq!(status.staged[0].status, "added"); } #[test] fn test_git_status_with_modified_file() { let temp_dir = create_test_repo(); let working_dir = temp_dir.path().to_string_lossy().to_string(); // Create, stage, and commit a file create_file(&temp_dir, "file.txt", "initial content"); run_git_command(&working_dir, &["add", "file.txt"]).unwrap(); run_git_command(&working_dir, &["commit", "-m", "initial commit"]).unwrap(); // Modify the file create_file(&temp_dir, "file.txt", "modified content"); let result = git_status(working_dir); assert!(result.is_ok()); let status = result.unwrap(); assert!(status.is_repo); assert!(!status.unstaged.is_empty()); assert_eq!(status.unstaged[0].path, "file.txt"); assert_eq!(status.unstaged[0].status, "modified"); } // ==================== git_diff integration tests ==================== #[test] fn test_git_diff_no_changes() { let temp_dir = create_test_repo(); let working_dir = temp_dir.path().to_string_lossy().to_string(); let result = git_diff(working_dir, None, false); assert!(result.is_ok()); assert!(result.unwrap().is_empty()); } #[test] fn test_git_diff_with_changes() { let temp_dir = create_test_repo(); let working_dir = temp_dir.path().to_string_lossy().to_string(); // Create and commit a file create_file(&temp_dir, "file.txt", "initial content"); run_git_command(&working_dir, &["add", "file.txt"]).unwrap(); run_git_command(&working_dir, &["commit", "-m", "initial"]).unwrap(); // Modify the file create_file(&temp_dir, "file.txt", "modified content"); let result = git_diff(working_dir, None, false); assert!(result.is_ok()); let diff = result.unwrap(); assert!(diff.contains("diff")); assert!(diff.contains("file.txt")); } #[test] fn test_git_diff_staged() { let temp_dir = create_test_repo(); let working_dir = temp_dir.path().to_string_lossy().to_string(); // Create and commit a file create_file(&temp_dir, "file.txt", "initial content"); run_git_command(&working_dir, &["add", "file.txt"]).unwrap(); run_git_command(&working_dir, &["commit", "-m", "initial"]).unwrap(); // Modify and stage the file create_file(&temp_dir, "file.txt", "modified content"); run_git_command(&working_dir, &["add", "file.txt"]).unwrap(); let result = git_diff(working_dir, None, true); assert!(result.is_ok()); let diff = result.unwrap(); assert!(diff.contains("diff")); } #[test] fn test_git_diff_specific_file() { let temp_dir = create_test_repo(); let working_dir = temp_dir.path().to_string_lossy().to_string(); // Create and commit files create_file(&temp_dir, "file1.txt", "content1"); create_file(&temp_dir, "file2.txt", "content2"); run_git_command(&working_dir, &["add", "-A"]).unwrap(); run_git_command(&working_dir, &["commit", "-m", "initial"]).unwrap(); // Modify both files create_file(&temp_dir, "file1.txt", "modified1"); create_file(&temp_dir, "file2.txt", "modified2"); // Get diff for only file1.txt let result = git_diff(working_dir, Some("file1.txt".to_string()), false); assert!(result.is_ok()); let diff = result.unwrap(); assert!(diff.contains("file1.txt")); assert!(!diff.contains("file2.txt")); } // ==================== git_branches integration tests ==================== #[test] fn test_git_branches_single_branch() { let temp_dir = create_test_repo(); let working_dir = temp_dir.path().to_string_lossy().to_string(); // Need at least one commit for branches to show create_file(&temp_dir, "file.txt", "content"); run_git_command(&working_dir, &["add", "file.txt"]).unwrap(); run_git_command(&working_dir, &["commit", "-m", "initial"]).unwrap(); let result = git_branches(working_dir); assert!(result.is_ok()); let branches = result.unwrap(); assert!(!branches.is_empty()); // Should have at least one branch (main or master) } #[test] fn test_git_branches_multiple_branches() { let temp_dir = create_test_repo(); let working_dir = temp_dir.path().to_string_lossy().to_string(); // Initial commit create_file(&temp_dir, "file.txt", "content"); run_git_command(&working_dir, &["add", "file.txt"]).unwrap(); run_git_command(&working_dir, &["commit", "-m", "initial"]).unwrap(); // Create additional branch run_git_command(&working_dir, &["branch", "feature-branch"]).unwrap(); let result = git_branches(working_dir); assert!(result.is_ok()); let branches = result.unwrap(); assert!(branches.len() >= 2); assert!(branches.iter().any(|b| b.name == "feature-branch")); } // ==================== git_stage and git_unstage tests ==================== #[test] fn test_git_stage_file() { let temp_dir = create_test_repo(); let working_dir = temp_dir.path().to_string_lossy().to_string(); create_file(&temp_dir, "file.txt", "content"); let result = git_stage(working_dir.clone(), "file.txt".to_string()); assert!(result.is_ok()); // Verify file is staged let status = git_status(working_dir).unwrap(); assert!(status.staged.iter().any(|f| f.path == "file.txt")); } #[test] fn test_git_unstage_file() { let temp_dir = create_test_repo(); let working_dir = temp_dir.path().to_string_lossy().to_string(); // First, commit a file so we have a HEAD to restore from create_file(&temp_dir, "file.txt", "initial content"); run_git_command(&working_dir, &["add", "file.txt"]).unwrap(); run_git_command(&working_dir, &["commit", "-m", "initial"]).unwrap(); // Modify and stage the file create_file(&temp_dir, "file.txt", "modified content"); run_git_command(&working_dir, &["add", "file.txt"]).unwrap(); let result = git_unstage(working_dir.clone(), "file.txt".to_string()); assert!(result.is_ok()); // Verify file is unstaged (should now be in unstaged/modified, not staged) let status = git_status(working_dir).unwrap(); assert!(!status.staged.iter().any(|f| f.path == "file.txt")); assert!(status.unstaged.iter().any(|f| f.path == "file.txt")); } #[test] fn test_git_stage_all() { let temp_dir = create_test_repo(); let working_dir = temp_dir.path().to_string_lossy().to_string(); create_file(&temp_dir, "file1.txt", "content1"); create_file(&temp_dir, "file2.txt", "content2"); let result = git_stage_all(working_dir.clone()); assert!(result.is_ok()); // Verify all files are staged let status = git_status(working_dir).unwrap(); assert_eq!(status.staged.len(), 2); } // ==================== git_commit tests ==================== #[test] fn test_git_commit() { let temp_dir = create_test_repo(); let working_dir = temp_dir.path().to_string_lossy().to_string(); create_file(&temp_dir, "file.txt", "content"); run_git_command(&working_dir, &["add", "file.txt"]).unwrap(); let result = git_commit(working_dir.clone(), "test commit message".to_string()); assert!(result.is_ok()); // Verify commit was made let log = git_log(working_dir, Some(1)).unwrap(); assert!(!log.is_empty()); assert!(log[0].message.contains("test commit message")); } #[test] fn test_git_commit_nothing_to_commit() { let temp_dir = create_test_repo(); let working_dir = temp_dir.path().to_string_lossy().to_string(); // Need initial commit first create_file(&temp_dir, "file.txt", "content"); run_git_command(&working_dir, &["add", "file.txt"]).unwrap(); run_git_command(&working_dir, &["commit", "-m", "initial"]).unwrap(); // Try to commit with nothing staged let result = git_commit(working_dir, "empty commit".to_string()); assert!(result.is_err()); // Should fail because nothing to commit } // ==================== git_log tests ==================== #[test] fn test_git_log_empty_repo() { let temp_dir = create_test_repo(); let working_dir = temp_dir.path().to_string_lossy().to_string(); let result = git_log(working_dir, Some(10)); // May fail on empty repo or return empty if let Ok(commits) = result { assert!(commits.is_empty()); } } #[test] fn test_git_log_with_commits() { let temp_dir = create_test_repo(); let working_dir = temp_dir.path().to_string_lossy().to_string(); // Make multiple commits for i in 1..=3 { create_file(&temp_dir, &format!("file{}.txt", i), "content"); run_git_command(&working_dir, &["add", "-A"]).unwrap(); run_git_command(&working_dir, &["commit", "-m", &format!("commit {}", i)]).unwrap(); } let result = git_log(working_dir, Some(10)); assert!(result.is_ok()); let log = result.unwrap(); assert_eq!(log.len(), 3); assert!(log[0].message.contains("commit 3")); // Most recent first assert!(log[2].message.contains("commit 1")); } #[test] fn test_git_log_limit() { let temp_dir = create_test_repo(); let working_dir = temp_dir.path().to_string_lossy().to_string(); // Make 5 commits for i in 1..=5 { create_file(&temp_dir, &format!("file{}.txt", i), "content"); run_git_command(&working_dir, &["add", "-A"]).unwrap(); run_git_command(&working_dir, &["commit", "-m", &format!("commit {}", i)]).unwrap(); } // Only get last 2 let result = git_log(working_dir, Some(2)); assert!(result.is_ok()); let log = result.unwrap(); assert_eq!(log.len(), 2); } // ==================== git_discard tests ==================== #[test] fn test_git_discard_changes() { let temp_dir = create_test_repo(); let working_dir = temp_dir.path().to_string_lossy().to_string(); // Create and commit a file create_file(&temp_dir, "file.txt", "original content"); run_git_command(&working_dir, &["add", "file.txt"]).unwrap(); run_git_command(&working_dir, &["commit", "-m", "initial"]).unwrap(); // Modify the file create_file(&temp_dir, "file.txt", "modified content"); // Discard changes let result = git_discard(working_dir.clone(), "file.txt".to_string()); assert!(result.is_ok()); // Verify file contents are restored let content = fs::read_to_string(temp_dir.path().join("file.txt")).unwrap(); assert_eq!(content, "original content"); } // ==================== git_create_branch tests ==================== #[test] fn test_git_create_branch() { let temp_dir = create_test_repo(); let working_dir = temp_dir.path().to_string_lossy().to_string(); // Initial commit required create_file(&temp_dir, "file.txt", "content"); run_git_command(&working_dir, &["add", "file.txt"]).unwrap(); run_git_command(&working_dir, &["commit", "-m", "initial"]).unwrap(); let result = git_create_branch(working_dir.clone(), "new-branch".to_string()); assert!(result.is_ok()); // Verify branch exists and is current let branches = git_branches(working_dir).unwrap(); assert!(branches.iter().any(|b| b.name == "new-branch" && b.is_current)); } // ==================== git_checkout tests ==================== #[test] fn test_git_checkout() { let temp_dir = create_test_repo(); let working_dir = temp_dir.path().to_string_lossy().to_string(); // Initial commit required create_file(&temp_dir, "file.txt", "content"); run_git_command(&working_dir, &["add", "file.txt"]).unwrap(); run_git_command(&working_dir, &["commit", "-m", "initial"]).unwrap(); // Create a branch run_git_command(&working_dir, &["branch", "other-branch"]).unwrap(); // Checkout the branch let result = git_checkout(working_dir.clone(), "other-branch".to_string()); assert!(result.is_ok()); // Verify current branch let branches = git_branches(working_dir).unwrap(); let current = branches.iter().find(|b| b.is_current); assert!(current.is_some()); assert_eq!(current.unwrap().name, "other-branch"); } // ==================== run_git_command tests ==================== #[test] fn test_run_git_command_success() { let temp_dir = create_test_repo(); let working_dir = temp_dir.path().to_string_lossy().to_string(); let result = run_git_command(&working_dir, &["status"]); assert!(result.is_ok()); } #[test] fn test_run_git_command_failure() { let temp_dir = TempDir::new().unwrap(); let working_dir = temp_dir.path().to_string_lossy().to_string(); // This should fail because it's not a git repo let result = run_git_command(&working_dir, &["log"]); assert!(result.is_err()); } #[test] fn test_run_git_command_invalid_dir() { let result = run_git_command("/nonexistent/path", &["status"]); assert!(result.is_err()); } }