329 lines
9.7 KiB
Rust
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();
|
|
}
|
|
}
|