diff --git a/Batch.md b/Batch.md index 32598d0..7814011 100644 --- a/Batch.md +++ b/Batch.md @@ -437,7 +437,7 @@ ### 数据层 + Git worktree 隔离 + 并行执行 -- **提交**: `当前待提交` +- **提交**: `ae6d3d0` | # | 任务 | 文件 | |---|------|------| diff --git a/Cargo.lock b/Cargo.lock index c0db3f3..de46f22 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -885,6 +885,7 @@ dependencies = [ "reqwest 0.12.28", "serde", "serde_json", + "tempfile", "tokio", "tracing", ] diff --git a/crates/df-ai/Cargo.toml b/crates/df-ai/Cargo.toml index 26e4f4b..c4b4079 100644 --- a/crates/df-ai/Cargo.toml +++ b/crates/df-ai/Cargo.toml @@ -18,3 +18,6 @@ reqwest = { version = "0.12", features = ["stream", "json"] } futures = "0.3" eventsource-stream = "0.2" rand = "0.8" + +[dev-dependencies] +tempfile = "3" diff --git a/crates/df-ai/src/git_worktree.rs b/crates/df-ai/src/git_worktree.rs new file mode 100644 index 0000000..b4212e1 --- /dev/null +++ b/crates/df-ai/src/git_worktree.rs @@ -0,0 +1,542 @@ +//! Git worktree 生命周期管理 — 多 Agent 并行执行的文件隔离基础 +//! +//! 设计依据:docs/02-架构设计/专项设计/多Agent并行执行与仲裁合并设计-2026-07-01.md §4.2 +//! +//! 每个 SubTask 创建独立 worktree(git worktree add + 新分支), +//! 写操作天然隔离。合并时用 git merge-tree 预检 + git merge 实际合并。 + +use std::path::{Path, PathBuf}; +use std::process::Command; + +/// worktree 管理错误 +#[derive(Debug)] +pub enum WorktreeError { + /// Git 命令执行失败(stdout/stderr 含错误信息) + GitFailed(String), + /// 工程根目录无 .git(非 Git 工程,应降级串行) + NotGitRepo, + /// worktree 目录已存在(可能上次未清理) + AlreadyExists(PathBuf), +} + +impl std::fmt::Display for WorktreeError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::GitFailed(msg) => write!(f, "Git 命令失败: {}", msg), + Self::NotGitRepo => write!(f, "非 Git 工程(无 .git 目录)"), + Self::AlreadyExists(p) => write!(f, "worktree 目录已存在: {}", p.display()), + } + } +} + +impl std::error::Error for WorktreeError {} + +/// worktree 创建结果 +#[derive(Debug, Clone)] +pub struct WorktreeHandle { + /// worktree 物理路径(SubTask 工具操作的 cwd) + pub path: PathBuf, + /// worktree 对应的分支名(merge 时用) + pub branch: String, + /// 所属 Plan id(清理时按 plan 批量) + pub plan_id: String, + /// SubTask id + pub subtask_id: String, +} + +/// 在指定工程根目录下创建 SubTask 专属 worktree。 +/// +/// - `project_root`: 工程根目录(含 .git) +/// - `plan_id`: 所属 Plan id +/// - `subtask_id`: SubTask id +/// - `base_branch`: fork 基点分支(plan 分支或主分支) +/// +/// 返回 WorktreeHandle(path + branch)。 +/// worktree 路径:`{project_root}/.devflow/wt/{plan_id}/{subtask_id}` +/// 分支名:`subtask/{plan_id}/{subtask_id}` +pub fn create_worktree( + project_root: &Path, + plan_id: &str, + subtask_id: &str, + base_branch: &str, +) -> Result { + if !project_root.join(".git").exists() { + return Err(WorktreeError::NotGitRepo); + } + + let wt_path = project_root.join(".devflow/wt").join(plan_id).join(subtask_id); + if wt_path.exists() { + return Err(WorktreeError::AlreadyExists(wt_path)); + } + + let branch = format!("subtask/{}/{}", plan_id, subtask_id); + + // git worktree add -b + let output = Command::new("git") + .args([ + "worktree", + "add", + wt_path.to_str().unwrap_or(""), + "-b", + &branch, + base_branch, + ]) + .current_dir(project_root) + .output() + .map_err(|e| WorktreeError::GitFailed(format!("git worktree add 执行失败: {}", e)))?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(WorktreeError::GitFailed(format!( + "git worktree add 失败: {}", + stderr.trim() + ))); + } + + Ok(WorktreeHandle { + path: wt_path, + branch, + plan_id: plan_id.to_string(), + subtask_id: subtask_id.to_string(), + }) +} + +/// 在 worktree 内提交所有改动(git add -A + git commit)。 +/// +/// - `wt_path`: worktree 物理路径 +/// - `message`: commit 消息 +pub fn commit_worktree(wt_path: &Path, message: &str) -> Result<(), WorktreeError> { + // git add -A + let add = Command::new("git") + .args(["add", "-A"]) + .current_dir(wt_path) + .output() + .map_err(|e| WorktreeError::GitFailed(format!("git add 失败: {}", e)))?; + if !add.status.success() { + return Err(WorktreeError::GitFailed(format!( + "git add 失败: {}", + String::from_utf8_lossy(&add.stderr).trim() + ))); + } + + // git commit -m (允许空提交,--allow-empty 防无改动时报错) + let commit = Command::new("git") + .args(["commit", "--allow-empty", "-m", message]) + .current_dir(wt_path) + .output() + .map_err(|e| WorktreeError::GitFailed(format!("git commit 失败: {}", e)))?; + if !commit.status.success() { + // commit 失败可能因无 user.email 配置(测试环境),尝试自动配 + let stderr = String::from_utf8_lossy(&commit.stderr); + if stderr.contains("user.email") || stderr.contains("user.name") { + // 降级:设置临时配置后重试 + Command::new("git") + .args(["config", "user.email", "devflow@ai.local"]) + .current_dir(wt_path) + .output() + .ok(); + Command::new("git") + .args(["config", "user.name", "DevFlow Agent"]) + .current_dir(wt_path) + .output() + .ok(); + let retry = Command::new("git") + .args(["commit", "--allow-empty", "-m", message]) + .current_dir(wt_path) + .output() + .map_err(|e| WorktreeError::GitFailed(format!("git commit 重试失败: {}", e)))?; + if !retry.status.success() { + return Err(WorktreeError::GitFailed(format!( + "git commit 重试仍失败: {}", + String::from_utf8_lossy(&retry.stderr).trim() + ))); + } + return Ok(()); + } + return Err(WorktreeError::GitFailed(format!("git commit 失败: {}", stderr.trim()))); + } + + Ok(()) +} + +/// 将 SubTask 分支 merge 到 plan 分支(在 plan worktree 内执行)。 +/// +/// 返回 Ok(true) 表示有改动被合并,Ok(false) 表示无改动(空分支)。 +/// 返回 Err 表示 merge 冲突。 +pub fn merge_branch( + plan_wt_path: &Path, + subtask_branch: &str, +) -> Result { + // 先用 merge-tree 预检 + let precheck = Command::new("git") + .args(["merge-tree", "--write-tree", "--messages", "HEAD", subtask_branch]) + .current_dir(plan_wt_path) + .output() + .map_err(|e| WorktreeError::GitFailed(format!("git merge-tree 失败: {}", e)))?; + + // merge-tree --write-tree 成功(exit 0)= 无冲突可自动合并; + // 非0(冲突)且 stdout 非空 = 有冲突 + if !precheck.status.success() { + let stdout = String::from_utf8_lossy(&precheck.stdout); + if !stdout.is_empty() { + return Ok(MergeResult::Conflict { + branch: subtask_branch.to_string(), + details: stdout.to_string(), + }); + } + } + + // 无冲突,执行实际 merge + let merge = Command::new("git") + .args(["merge", "--no-edit", subtask_branch]) + .current_dir(plan_wt_path) + .output() + .map_err(|e| WorktreeError::GitFailed(format!("git merge 失败: {}", e)))?; + + if !merge.status.success() { + // merge 冲突(git merge 返回非0) + let stderr = String::from_utf8_lossy(&merge.stderr); + let stdout = String::from_utf8_lossy(&merge.stdout); + // abort 部分合并状态(防 worktree 处于 conflicted 状态) + let _ = Command::new("git") + .args(["merge", "--abort"]) + .current_dir(plan_wt_path) + .output(); + return Ok(MergeResult::Conflict { + branch: subtask_branch.to_string(), + details: format!("{}\n{}", stdout, stderr), + }); + } + + Ok(MergeResult::Clean) +} + +/// merge 结果 +#[derive(Debug, Clone)] +pub enum MergeResult { + /// 干净合并(无冲突) + Clean, + /// 合并冲突(details 含 conflict 信息) + Conflict { branch: String, details: String }, +} + +/// 清理单个 SubTask 的 worktree(git worktree remove --force + 删分支)。 +pub fn remove_worktree(project_root: &Path, handle: &WorktreeHandle) -> Result<(), WorktreeError> { + // git worktree remove --force + let _ = Command::new("git") + .args([ + "worktree", + "remove", + "--force", + handle.path.to_str().unwrap_or(""), + ]) + .current_dir(project_root) + .output(); + + // 删除分支(-D 强制,已 merge 的分支正常删,未 merge 的也删) + let _ = Command::new("git") + .args(["branch", "-D", &handle.branch]) + .current_dir(project_root) + .output(); + + Ok(()) +} + +/// 清理某 Plan 的所有 worktree(Plan 完成/失败/取消时批量清理)。 +pub fn remove_plan_worktrees(project_root: &Path, plan_id: &str) -> Result<(), WorktreeError> { + let plan_wt_dir = project_root.join(".devflow/wt").join(plan_id); + if !plan_wt_dir.exists() { + return Ok(()); + } + + // git worktree list 获取该 plan 下所有 worktree + let list = Command::new("git") + .args(["worktree", "list", "--porcelain"]) + .current_dir(project_root) + .output() + .map_err(|e| WorktreeError::GitFailed(format!("git worktree list 失败: {}", e)))?; + + let list_str = String::from_utf8_lossy(&list.stdout); + for line in list_str.lines() { + if let Some(path_str) = line.strip_prefix("worktree ") { + let path = PathBuf::from(path_str); + // 仅清理属于本 plan 的 worktree + if path.starts_with(&plan_wt_dir) { + let _ = Command::new("git") + .args(["worktree", "remove", "--force", path_str]) + .current_dir(project_root) + .output(); + } + } + } + + // 删除该 plan 的所有子任务分支 + let branches = Command::new("git") + .args(["branch", "--list", &format!("subtask/{}/*", plan_id)]) + .current_dir(project_root) + .output() + .map_err(|e| WorktreeError::GitFailed(format!("git branch --list 失败: {}", e)))?; + + let branches_str = String::from_utf8_lossy(&branches.stdout); + for line in branches_str.lines() { + let branch = line.trim(); + if !branch.is_empty() { + let _ = Command::new("git") + .args(["branch", "-D", branch]) + .current_dir(project_root) + .output(); + } + } + + // 清理空目录 + let _ = std::fs::remove_dir_all(&plan_wt_dir); + + Ok(()) +} + +/// 检测工程是否为 Git 仓库(有 .git 目录)。 +pub fn is_git_repo(project_root: &Path) -> bool { + project_root.join(".git").exists() +} + +/// 获取工程当前分支名(用于作为 plan worktree 的 fork 基点)。 +pub fn current_branch(project_root: &Path) -> Result { + let output = Command::new("git") + .args(["rev-parse", "--abbrev-ref", "HEAD"]) + .current_dir(project_root) + .output() + .map_err(|e| WorktreeError::GitFailed(format!("git rev-parse 失败: {}", e)))?; + + if !output.status.success() { + return Err(WorktreeError::GitFailed( + String::from_utf8_lossy(&output.stderr).trim().to_string(), + )); + } + + Ok(String::from_utf8_lossy(&output.stdout).trim().to_string()) +} + +/// 创建 Plan worktree(Plan 级工作分支,所有 SubTask 分支 merge 到这里)。 +/// +/// 返回 (plan_worktree_path, plan_branch)。 +pub fn create_plan_worktree( + project_root: &Path, + plan_id: &str, +) -> Result<(PathBuf, String), WorktreeError> { + if !is_git_repo(project_root) { + return Err(WorktreeError::NotGitRepo); + } + + let base = current_branch(project_root)?; + let plan_branch = format!("plan/{}", plan_id); + let plan_wt_path = project_root.join(".devflow/wt").join(plan_id); + + if plan_wt_path.exists() { + return Err(WorktreeError::AlreadyExists(plan_wt_path)); + } + + let output = Command::new("git") + .args([ + "worktree", + "add", + plan_wt_path.to_str().unwrap_or(""), + "-b", + &plan_branch, + &base, + ]) + .current_dir(project_root) + .output() + .map_err(|e| WorktreeError::GitFailed(format!("git worktree add(plan) 失败: {}", e)))?; + + if !output.status.success() { + return Err(WorktreeError::GitFailed(format!( + "git worktree add(plan) 失败: {}", + String::from_utf8_lossy(&output.stderr).trim() + ))); + } + + Ok((plan_wt_path, plan_branch)) +} + +#[cfg(test)] +mod tests { + use super::*; + + /// 创建临时 Git 仓库 + 初始 commit + fn setup_repo() -> tempfile::TempDir { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path(); + + 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(path.join("main.rs"), "fn main() {}\n").unwrap(); + Command::new("git").args(["add", "."]).current_dir(path).output().unwrap(); + Command::new("git").args(["commit", "-m", "init"]).current_dir(path).output().unwrap(); + + dir + } + + #[test] + fn wt_01_create_and_isolate() { + let repo = setup_repo(); + let root = repo.path(); + let base = current_branch(root).unwrap(); + + let handle = create_worktree(root, "plan-test", "A", &base).unwrap(); + assert!(handle.path.exists(), "worktree 目录应存在"); + assert_eq!(handle.branch, "subtask/plan-test/A"); + + // 在 worktree 内写文件 + std::fs::write(handle.path.join("new_a.rs"), "// A 的改动").unwrap(); + // 主目录不应有此文件 + assert!(!root.join("new_a.rs").exists(), "主目录不应有 A 的改动"); + + // 清理 + remove_worktree(root, &handle).unwrap(); + } + + #[test] + fn wt_02_parallel_isolation() { + let repo = setup_repo(); + let root = repo.path(); + + let plan_wt = create_plan_worktree(root, "plan-iso").unwrap(); + let plan_branch = plan_wt.1; + + let a = create_worktree(root, "plan-iso", "A", &plan_branch).unwrap(); + let b = create_worktree(root, "plan-iso", "B", &plan_branch).unwrap(); + + // A 写 new_a.rs + std::fs::write(a.path.join("new_a.rs"), "// A").unwrap(); + // B 写 new_b.rs + std::fs::write(b.path.join("new_b.rs"), "// B").unwrap(); + + // A 的 worktree 不应有 B 的文件 + assert!(!a.path.join("new_b.rs").exists(), "A 不应看到 B 的改动"); + // B 的 worktree 不应有 A 的文件 + assert!(!b.path.join("new_a.rs").exists(), "B 不应看到 A 的改动"); + + remove_plan_worktrees(root, "plan-iso").unwrap(); + } + + #[test] + fn wt_03_commit_in_worktree() { + let repo = setup_repo(); + let root = repo.path(); + + let plan_wt = create_plan_worktree(root, "plan-commit").unwrap(); + let a = create_worktree(root, "plan-commit", "A", &plan_wt.1).unwrap(); + + // 写文件 + commit + std::fs::write(a.path.join("feature.rs"), "pub fn feature() {}").unwrap(); + commit_worktree(&a.path, "Add feature").unwrap(); + + // 验证分支有新 commit + let log = Command::new("git") + .args(["log", "--oneline", "-1"]) + .current_dir(&a.path) + .output() + .unwrap(); + let log_str = String::from_utf8_lossy(&log.stdout); + assert!(log_str.contains("Add feature"), "commit 消息应在 log 中"); + + remove_plan_worktrees(root, "plan-commit").unwrap(); + } + + #[test] + fn wt_04_merge_no_conflict() { + let repo = setup_repo(); + let root = repo.path(); + + let plan_wt = create_plan_worktree(root, "plan-merge").unwrap(); + let plan_wt_path = plan_wt.0.clone(); + let plan_branch = plan_wt.1; + + // A 改 file_a.rs, B 改 file_b.rs(不同文件,无冲突) + let a = create_worktree(root, "plan-merge", "A", &plan_branch).unwrap(); + std::fs::write(a.path.join("file_a.rs"), "// A").unwrap(); + commit_worktree(&a.path, "A changes").unwrap(); + + let b = create_worktree(root, "plan-merge", "B", &plan_branch).unwrap(); + std::fs::write(b.path.join("file_b.rs"), "// B").unwrap(); + commit_worktree(&b.path, "B changes").unwrap(); + + // merge A → plan + let result_a = merge_branch(&plan_wt_path, &a.branch).unwrap(); + assert!(matches!(result_a, MergeResult::Clean), "A merge 应无冲突"); + + // merge B → plan + let result_b = merge_branch(&plan_wt_path, &b.branch).unwrap(); + assert!(matches!(result_b, MergeResult::Clean), "B merge 应无冲突"); + + // plan worktree 应同时有 file_a.rs 和 file_b.rs + assert!(plan_wt_path.join("file_a.rs").exists(), "plan 应含 A 的改动"); + assert!(plan_wt_path.join("file_b.rs").exists(), "plan 应含 B 的改动"); + + remove_plan_worktrees(root, "plan-merge").unwrap(); + } + + #[test] + fn wt_05_merge_conflict_same_file() { + let repo = setup_repo(); + let root = repo.path(); + + // 写一个共享文件 + std::fs::write(root.join("shared.rs"), "line1\nline2\n").unwrap(); + Command::new("git").args(["add", "-A"]).current_dir(root).output().unwrap(); + Command::new("git").args(["commit", "-m", "add shared"]).current_dir(root).output().unwrap(); + + let plan_wt = create_plan_worktree(root, "plan-conf").unwrap(); + let plan_wt_path = plan_wt.0.clone(); + let plan_branch = plan_wt.1; + + // A 和 B 改同一文件的同一行 + let a = create_worktree(root, "plan-conf", "A", &plan_branch).unwrap(); + std::fs::write(a.path.join("shared.rs"), "A_version\n").unwrap(); + commit_worktree(&a.path, "A change shared").unwrap(); + + let b = create_worktree(root, "plan-conf", "B", &plan_branch).unwrap(); + std::fs::write(b.path.join("shared.rs"), "B_version\n").unwrap(); + commit_worktree(&b.path, "B change shared").unwrap(); + + // merge A → 无冲突 + let result_a = merge_branch(&plan_wt_path, &a.branch).unwrap(); + assert!(matches!(result_a, MergeResult::Clean), "首个 merge 应无冲突"); + + // merge B → 冲突 + let result_b = merge_branch(&plan_wt_path, &b.branch).unwrap(); + assert!( + matches!(result_b, MergeResult::Conflict { .. }), + "同文件同行改应产生冲突" + ); + + remove_plan_worktrees(root, "plan-conf").unwrap(); + } + + #[test] + fn wt_06_non_git_returns_error() { + let dir = tempfile::tempdir().unwrap(); + // 不 init git + let result = create_plan_worktree(dir.path(), "plan-ng"); + assert!(matches!(result, Err(WorktreeError::NotGitRepo))); + } + + #[test] + fn wt_07_cleanup_removes_worktree() { + let repo = setup_repo(); + let root = repo.path(); + + let plan_wt = create_plan_worktree(root, "plan-clean").unwrap(); + let a = create_worktree(root, "plan-clean", "A", &plan_wt.1).unwrap(); + assert!(a.path.exists()); + + remove_plan_worktrees(root, "plan-clean").unwrap(); + + // worktree 目录应被清理 + assert!(!a.path.exists(), "worktree 目录应已清理"); + // plan wt 目录也应被清理 + assert!(!plan_wt.0.exists(), "plan worktree 目录应已清理"); + } +} diff --git a/crates/df-ai/src/lib.rs b/crates/df-ai/src/lib.rs index 88d93ed..065d8f0 100644 --- a/crates/df-ai/src/lib.rs +++ b/crates/df-ai/src/lib.rs @@ -10,6 +10,7 @@ pub mod coordinator; // 会话意图识别层(纯函数,不接入 agentic loop)。依据 docs/02-架构设计/构想审查/ // 意图识别层论证-2026-06-19.md。提供 recognize / tool_subset_for / suggested_model_tier。 // 待 Phase B+C 完成 + 模型模态管理落地后再接入 agentic loop。 +pub mod git_worktree; pub mod intent; pub mod model_fetch; // model_fetch 的纯逻辑子模块(URL 拼接 / 噪音过滤 / 响应反序列化),