orchai/src-tauri/src/services/worktree_manager.rs

329 lines
9.7 KiB
Rust

use std::path::Path;
use std::process::Command;
fn run_git(project_path: &str, args: &[&str]) -> Result<String, String> {
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<String, String> {
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<Vec<String>, 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<Vec<String>, 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();
}
}