use std::path::Path; use std::process::Command; fn run_git(project_path: &str, args: &[&str]) -> Result { let output = Command::new("git") .args(args) .current_dir(project_path) .output() .map_err(|e| format!("Failed to run git: {}", e))?; if output.status.success() { Ok(String::from_utf8_lossy(&output.stdout).to_string()) } else { let stderr = String::from_utf8_lossy(&output.stderr); Err(format!("git {} failed: {}", args.join(" "), stderr)) } } pub fn create_worktree( project_path: &str, base_branch: &str, artifact_id: i32, ) -> Result<(String, String), String> { let orchai_dir = Path::new(project_path).join(".orchai").join("worktrees"); std::fs::create_dir_all(&orchai_dir) .map_err(|e| format!("Failed to create .orchai/worktrees dir: {}", e))?; let worktree_name = format!("orchai-{}", artifact_id); let worktree_path = orchai_dir.join(&worktree_name); let branch_name = format!("orchai/{}", artifact_id); let wt_path_str = worktree_path.to_str().ok_or("Invalid worktree path")?; run_git( project_path, &[ "worktree", "add", wt_path_str, "-b", &branch_name, base_branch, ], )?; Ok((wt_path_str.to_string(), branch_name)) } pub fn get_diff( project_path: &str, base_branch: &str, branch_name: &str, ) -> Result { let range = format!("{}...{}", base_branch, branch_name); run_git(project_path, &["diff", &range]) } pub fn list_commits( project_path: &str, base_branch: &str, branch_name: &str, ) -> Result, String> { let range = format!("{}..{}", base_branch, branch_name); let output = run_git(project_path, &["log", &range, "--format=%H", "--reverse"])?; Ok(output .lines() .filter(|l| !l.is_empty()) .map(String::from) .collect()) } pub fn apply_fix( project_path: &str, base_branch: &str, branch_name: &str, target_branch: &str, ) -> Result<(), String> { let commits = list_commits(project_path, base_branch, branch_name)?; if commits.is_empty() { return Err("No commits to cherry-pick".to_string()); } let current = run_git(project_path, &["rev-parse", "--abbrev-ref", "HEAD"])?; let current = current.trim(); run_git(project_path, &["checkout", target_branch])?; let mut cherry_args = vec!["cherry-pick"]; let commit_refs: Vec<&str> = commits.iter().map(|s| s.as_str()).collect(); cherry_args.extend(&commit_refs); let result = run_git(project_path, &cherry_args); if let Err(e) = &result { let _ = run_git(project_path, &["cherry-pick", "--abort"]); let _ = run_git(project_path, &["checkout", current]); return Err(format!("Cherry-pick failed (conflict?): {}", e)); } run_git(project_path, &["checkout", current])?; Ok(()) } pub fn delete_worktree( project_path: &str, worktree_path: &str, branch_name: &str, ) -> Result<(), String> { run_git( project_path, &["worktree", "remove", worktree_path, "--force"], )?; let _ = run_git(project_path, &["branch", "-D", branch_name]); Ok(()) } pub fn list_local_branches(project_path: &str) -> Result, String> { let output = run_git(project_path, &["branch", "--format=%(refname:short)"])?; Ok(output .lines() .filter(|l| !l.is_empty()) .map(String::from) .collect()) } #[cfg(test)] mod tests { use super::*; use std::process::Command; fn setup_test_repo() -> tempfile::TempDir { let dir = tempfile::tempdir().expect("create temp dir"); let path = dir.path().to_str().unwrap(); Command::new("git") .args(["init"]) .current_dir(path) .output() .unwrap(); Command::new("git") .args(["config", "user.email", "test@test.com"]) .current_dir(path) .output() .unwrap(); Command::new("git") .args(["config", "user.name", "Test"]) .current_dir(path) .output() .unwrap(); std::fs::write(dir.path().join("README.md"), "# Test").unwrap(); Command::new("git") .args(["add", "."]) .current_dir(path) .output() .unwrap(); Command::new("git") .args(["commit", "-m", "init"]) .current_dir(path) .output() .unwrap(); dir } fn current_branch(path: &str) -> String { let output = Command::new("git") .args(["rev-parse", "--abbrev-ref", "HEAD"]) .current_dir(path) .output() .unwrap(); assert!(output.status.success()); String::from_utf8_lossy(&output.stdout).trim().to_string() } #[test] fn test_create_worktree() { let dir = setup_test_repo(); let path = dir.path().to_str().unwrap(); let base_branch = current_branch(path); let (wt_path, branch) = create_worktree(path, &base_branch, 42).unwrap(); assert!(wt_path.contains("orchai-42")); assert_eq!(branch, "orchai/42"); assert!(Path::new(&wt_path).exists()); } #[test] fn test_get_diff_empty() { let dir = setup_test_repo(); let path = dir.path().to_str().unwrap(); let base_branch = current_branch(path); let (_, branch) = create_worktree(path, &base_branch, 1).unwrap(); let diff = get_diff(path, &base_branch, &branch).unwrap(); assert!(diff.is_empty(), "No changes yet, diff should be empty"); } #[test] fn test_get_diff_with_changes() { let dir = setup_test_repo(); let path = dir.path().to_str().unwrap(); let base_branch = current_branch(path); let (wt_path, branch) = create_worktree(path, &base_branch, 2).unwrap(); std::fs::write(Path::new(&wt_path).join("fix.txt"), "fixed").unwrap(); Command::new("git") .args(["add", "."]) .current_dir(&wt_path) .output() .unwrap(); Command::new("git") .args(["commit", "-m", "fix"]) .current_dir(&wt_path) .output() .unwrap(); let diff = get_diff(path, &base_branch, &branch).unwrap(); assert!(diff.contains("fix.txt")); assert!(diff.contains("+fixed")); } #[test] fn test_list_commits() { let dir = setup_test_repo(); let path = dir.path().to_str().unwrap(); let base_branch = current_branch(path); let (wt_path, branch) = create_worktree(path, &base_branch, 3).unwrap(); std::fs::write(Path::new(&wt_path).join("a.txt"), "a").unwrap(); Command::new("git") .args(["add", "."]) .current_dir(&wt_path) .output() .unwrap(); Command::new("git") .args(["commit", "-m", "first"]) .current_dir(&wt_path) .output() .unwrap(); std::fs::write(Path::new(&wt_path).join("b.txt"), "b").unwrap(); Command::new("git") .args(["add", "."]) .current_dir(&wt_path) .output() .unwrap(); Command::new("git") .args(["commit", "-m", "second"]) .current_dir(&wt_path) .output() .unwrap(); let commits = list_commits(path, &base_branch, &branch).unwrap(); assert_eq!(commits.len(), 2); } #[test] fn test_list_local_branches() { let dir = setup_test_repo(); let path = dir.path().to_str().unwrap(); let base_branch = current_branch(path); create_worktree(path, &base_branch, 10).unwrap(); let branches = list_local_branches(path).unwrap(); assert!(branches.contains(&base_branch)); assert!(branches.contains(&"orchai/10".to_string())); } #[test] fn test_delete_worktree() { let dir = setup_test_repo(); let path = dir.path().to_str().unwrap(); let base_branch = current_branch(path); let (wt_path, branch) = create_worktree(path, &base_branch, 99).unwrap(); assert!(Path::new(&wt_path).exists()); delete_worktree(path, &wt_path, &branch).unwrap(); assert!(!Path::new(&wt_path).exists()); let branches = list_local_branches(path).unwrap(); assert!(!branches.contains(&"orchai/99".to_string())); } #[test] fn test_apply_fix() { let dir = setup_test_repo(); let path = dir.path().to_str().unwrap(); let base_branch = current_branch(path); Command::new("git") .args(["branch", "feature/test"]) .current_dir(path) .output() .unwrap(); let (wt_path, branch) = create_worktree(path, &base_branch, 7).unwrap(); std::fs::write(Path::new(&wt_path).join("fix.txt"), "the fix").unwrap(); Command::new("git") .args(["add", "."]) .current_dir(&wt_path) .output() .unwrap(); Command::new("git") .args(["commit", "-m", "apply fix"]) .current_dir(&wt_path) .output() .unwrap(); apply_fix(path, &base_branch, &branch, "feature/test").unwrap(); Command::new("git") .args(["checkout", "feature/test"]) .current_dir(path) .output() .unwrap(); assert!(Path::new(path).join("fix.txt").exists()); Command::new("git") .args(["checkout", &base_branch]) .current_dir(path) .output() .unwrap(); } }