diff --git a/src/main/java/com/claudecode/config/AppConfig.java b/src/main/java/com/claudecode/config/AppConfig.java
index 1bc3e2e..4d1def2 100644
--- a/src/main/java/com/claudecode/config/AppConfig.java
+++ b/src/main/java/com/claudecode/config/AppConfig.java
@@ -119,7 +119,9 @@ public class AppConfig {
new SkillTool(),
new SendMessageTool(),
new ListMcpResourcesTool(),
- new ReadMcpResourceTool()
+ new ReadMcpResourceTool(),
+ new EnterWorktreeTool(),
+ new ExitWorktreeTool()
);
// P2: 注册 MCP 工具桥接(将远程 MCP 工具映射为本地工具)
diff --git a/src/main/java/com/claudecode/tool/impl/EnterWorktreeTool.java b/src/main/java/com/claudecode/tool/impl/EnterWorktreeTool.java
new file mode 100644
index 0000000..f38029f
--- /dev/null
+++ b/src/main/java/com/claudecode/tool/impl/EnterWorktreeTool.java
@@ -0,0 +1,126 @@
+package com.claudecode.tool.impl;
+
+import com.claudecode.tool.Tool;
+import com.claudecode.tool.ToolContext;
+import com.claudecode.worktree.WorktreeManager;
+import com.claudecode.worktree.WorktreeManager.WorktreeCreateResult;
+import com.claudecode.worktree.WorktreeManager.WorktreeSession;
+
+import java.nio.file.Path;
+import java.util.Map;
+
+/**
+ * 进入 Git Worktree 工具 —— 对应 claude-code/src/tools/EnterWorktreeTool。
+ *
+ * 为当前会话创建隔离的 git worktree,切换工作目录。
+ * 用于并行工作场景,每个 worktree 有独立的分支和文件系统视图。
+ *
+ * 输入参数:
+ *
+ * - name — worktree 名称 (slug),可选,默认自动生成
+ * - base_branch — 基准分支,可选
+ *
+ */
+public class EnterWorktreeTool implements Tool {
+
+ public static final String WORKTREE_MANAGER_KEY = "__worktree_manager__";
+
+ @Override
+ public String name() {
+ return "EnterWorktree";
+ }
+
+ @Override
+ public String description() {
+ return """
+ Create and enter a git worktree for isolated parallel work. \
+ This creates a new branch and working directory, allowing you \
+ to make changes without affecting the main working tree. \
+ Use ExitWorktree to leave and optionally clean up.""";
+ }
+
+ @Override
+ public String inputSchema() {
+ return """
+ {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string",
+ "description": "Worktree name (slug). Max 64 chars, [a-zA-Z0-9._-/] only. If not provided, a name will be generated."
+ },
+ "base_branch": {
+ "type": "string",
+ "description": "Base branch to create worktree from. Defaults to current HEAD."
+ }
+ }
+ }""";
+ }
+
+ @Override
+ public String execute(Map input, ToolContext context) {
+ try {
+ WorktreeManager manager = getOrCreateManager(context);
+
+ // 检查是否已在 worktree 中
+ if (manager.getCurrentSession() != null) {
+ return "Error: Already in a worktree session: " + manager.getCurrentSession().slug() +
+ ". Use ExitWorktree first.";
+ }
+
+ // 检测 Git 根目录
+ Path gitRoot = WorktreeManager.findGitRoot(context.getWorkDir());
+ if (gitRoot == null) {
+ return "Error: Not in a git repository. Worktree requires a git repo.";
+ }
+
+ String slug = input != null ? (String) input.get("name") : null;
+ if (slug == null || slug.isBlank()) {
+ // 自动生成 slug
+ slug = "session-" + System.currentTimeMillis() % 100000;
+ }
+
+ String baseBranch = input != null ? (String) input.get("base_branch") : null;
+
+ // 创建/恢复 worktree
+ WorktreeSession session = manager.enterWorktree(slug, gitRoot, baseBranch);
+
+ // 更新工作目录(ToolContext 层面)
+ // 注意:Java 不像 Node.js 那样能轻易切换 process CWD,
+ // 我们通过 ToolContext 来管理"逻辑工作目录"
+ context.set("__original_work_dir__", context.getWorkDir().toString());
+
+ StringBuilder sb = new StringBuilder();
+ sb.append("✓ Entered worktree: ").append(session.slug()).append("\n");
+ sb.append(" Path: ").append(session.worktreePath()).append("\n");
+ sb.append(" Branch: ").append(session.worktreeBranch()).append("\n");
+ if (session.headCommit() != null) {
+ sb.append(" HEAD: ").append(session.headCommit(), 0,
+ Math.min(8, session.headCommit().length())).append("\n");
+ }
+ if (session.worktreePath().equals(session.worktreePath())) {
+ sb.append(" Status: ").append(
+ session.createdAt() != null ? "created" : "resumed").append("\n");
+ }
+ sb.append("\nAll file operations will now work in the worktree directory.");
+ sb.append("\nUse ExitWorktree to return to the original directory.");
+
+ return sb.toString();
+ } catch (IllegalArgumentException e) {
+ return "Error: Invalid worktree name — " + e.getMessage();
+ } catch (IllegalStateException e) {
+ return "Error: " + e.getMessage();
+ } catch (Exception e) {
+ return "Error creating worktree: " + e.getMessage();
+ }
+ }
+
+ private WorktreeManager getOrCreateManager(ToolContext context) {
+ WorktreeManager manager = context.get(WORKTREE_MANAGER_KEY);
+ if (manager == null) {
+ manager = new WorktreeManager();
+ context.set(WORKTREE_MANAGER_KEY, manager);
+ }
+ return manager;
+ }
+}
diff --git a/src/main/java/com/claudecode/tool/impl/ExitWorktreeTool.java b/src/main/java/com/claudecode/tool/impl/ExitWorktreeTool.java
new file mode 100644
index 0000000..bef6c2c
--- /dev/null
+++ b/src/main/java/com/claudecode/tool/impl/ExitWorktreeTool.java
@@ -0,0 +1,103 @@
+package com.claudecode.tool.impl;
+
+import com.claudecode.tool.Tool;
+import com.claudecode.tool.ToolContext;
+import com.claudecode.worktree.WorktreeManager;
+import com.claudecode.worktree.WorktreeManager.WorktreeSession;
+
+import java.util.Map;
+
+/**
+ * 退出 Git Worktree 工具 —— 对应 claude-code/src/tools/ExitWorktreeTool。
+ *
+ * 退出当前 worktree,可选择保留或删除。
+ *
+ * - keep — 保留 worktree 在磁盘上(可后续恢复)
+ * - remove — 删除 worktree 和对应分支
+ *
+ */
+public class ExitWorktreeTool implements Tool {
+
+ @Override
+ public String name() {
+ return "ExitWorktree";
+ }
+
+ @Override
+ public String description() {
+ return """
+ Exit the current git worktree session. Choose to 'keep' the \
+ worktree on disk for later resumption, or 'remove' it to clean \
+ up the worktree directory and branch. If removing with uncommitted \
+ changes, set discard_changes to true.""";
+ }
+
+ @Override
+ public String inputSchema() {
+ return """
+ {
+ "type": "object",
+ "properties": {
+ "action": {
+ "type": "string",
+ "enum": ["keep", "remove"],
+ "description": "'keep' preserves worktree on disk, 'remove' deletes it and the branch"
+ },
+ "discard_changes": {
+ "type": "boolean",
+ "description": "When true, force removal even with uncommitted changes"
+ }
+ },
+ "required": ["action"]
+ }""";
+ }
+
+ @Override
+ public String execute(Map input, ToolContext context) {
+ try {
+ WorktreeManager manager = context.get(EnterWorktreeTool.WORKTREE_MANAGER_KEY);
+ if (manager == null || manager.getCurrentSession() == null) {
+ return "Error: Not currently in a worktree session. Use EnterWorktree first.";
+ }
+
+ WorktreeSession session = manager.getCurrentSession();
+ String action = input != null ? (String) input.get("action") : "keep";
+ boolean discardChanges = input != null &&
+ Boolean.TRUE.equals(input.get("discard_changes"));
+
+ StringBuilder sb = new StringBuilder();
+
+ if ("remove".equals(action)) {
+ boolean removed = manager.cleanupWorktree(discardChanges);
+ if (removed) {
+ sb.append("✓ Worktree removed: ").append(session.slug()).append("\n");
+ sb.append(" Deleted: ").append(session.worktreePath()).append("\n");
+ sb.append(" Branch deleted: ").append(session.worktreeBranch()).append("\n");
+ } else {
+ return "Error: Failed to remove worktree. " +
+ "Try with discard_changes=true to force removal.";
+ }
+ } else {
+ manager.keepWorktree();
+ sb.append("✓ Exited worktree: ").append(session.slug()).append("\n");
+ sb.append(" Preserved at: ").append(session.worktreePath()).append("\n");
+ sb.append(" Branch: ").append(session.worktreeBranch()).append("\n");
+ sb.append(" You can re-enter later with EnterWorktree name=")
+ .append(session.slug()).append("\n");
+ }
+
+ // 恢复原始工作目录
+ String originalDir = context.get("__original_work_dir__");
+ if (originalDir != null) {
+ sb.append("\nRestored to: ").append(originalDir);
+ }
+
+ return sb.toString();
+ } catch (IllegalStateException e) {
+ return "Error: " + e.getMessage() +
+ "\nSet discard_changes=true to force removal.";
+ } catch (Exception e) {
+ return "Error exiting worktree: " + e.getMessage();
+ }
+ }
+}
diff --git a/src/main/java/com/claudecode/worktree/WorktreeManager.java b/src/main/java/com/claudecode/worktree/WorktreeManager.java
new file mode 100644
index 0000000..0e65db6
--- /dev/null
+++ b/src/main/java/com/claudecode/worktree/WorktreeManager.java
@@ -0,0 +1,465 @@
+package com.claudecode.worktree;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.nio.file.*;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Pattern;
+
+/**
+ * Git Worktree 生命周期管理 —— 对应 claude-code/src/utils/worktree.ts。
+ *
+ * 管理 worktree 的创建、恢复、清理和验证。
+ * 支持以下工作流:
+ *
+ * - EnterWorktreeTool 创建隔离的工作分支
+ * - ExitWorktreeTool 退出并保留/删除 worktree
+ * - Agent 自动创建独立 worktree(并行隔离)
+ * - 会话恢复时重新进入 worktree
+ *
+ */
+public class WorktreeManager {
+
+ private static final Logger log = LoggerFactory.getLogger(WorktreeManager.class);
+
+ /** Slug 验证:每个段只允许字母数字和 ._- */
+ private static final Pattern VALID_SLUG_SEGMENT = Pattern.compile("^[a-zA-Z0-9._-]+$");
+ private static final int MAX_SLUG_LENGTH = 64;
+ private static final String WORKTREE_BRANCH_PREFIX = "worktree-";
+
+ /** 当前活跃的 worktree 会话(每个 JVM 实例最多一个用户级 worktree) */
+ private volatile WorktreeSession currentSession;
+
+ // ==================== Worktree 会话数据 ====================
+
+ public record WorktreeSession(
+ String slug,
+ Path worktreePath,
+ String worktreeBranch,
+ String headCommit,
+ Path originalCwd,
+ Path gitRoot,
+ Instant createdAt
+ ) {}
+
+ public record WorktreeCreateResult(
+ Path worktreePath,
+ String worktreeBranch,
+ String headCommit,
+ Path gitRoot,
+ boolean existed
+ ) {}
+
+ // ==================== Slug 验证 ====================
+
+ /**
+ * 验证 worktree slug 格式和安全性。
+ *
+ * 规则:
+ *
+ * - 最大 64 字符
+ * - 每段只允许 [a-zA-Z0-9._-]
+ * - 不允许 . 或 .. 段(防路径遍历)
+ * - 不允许前导/尾随 /
+ * - / 在分支名和目录名中映射为 +
+ *
+ *
+ * @throws IllegalArgumentException 验证失败时
+ */
+ public static void validateSlug(String slug) {
+ if (slug == null || slug.isBlank()) {
+ throw new IllegalArgumentException("Worktree slug cannot be empty");
+ }
+ if (slug.length() > MAX_SLUG_LENGTH) {
+ throw new IllegalArgumentException(
+ "Worktree slug too long (max " + MAX_SLUG_LENGTH + "): " + slug.length());
+ }
+ if (slug.startsWith("/") || slug.endsWith("/")) {
+ throw new IllegalArgumentException("Worktree slug cannot start/end with /");
+ }
+
+ String[] segments = slug.split("/");
+ for (String segment : segments) {
+ if (segment.isEmpty()) {
+ throw new IllegalArgumentException("Worktree slug has empty segment");
+ }
+ if (".".equals(segment) || "..".equals(segment)) {
+ throw new IllegalArgumentException("Worktree slug cannot contain . or .. segments");
+ }
+ if (!VALID_SLUG_SEGMENT.matcher(segment).matches()) {
+ throw new IllegalArgumentException(
+ "Invalid characters in slug segment '" + segment +
+ "': only [a-zA-Z0-9._-] allowed");
+ }
+ }
+ }
+
+ /**
+ * 将 slug 中的 / 映射为 + 防止 D/F 冲突。
+ */
+ public static String flattenSlug(String slug) {
+ return slug.replace('/', '+');
+ }
+
+ /**
+ * 从 slug 生成分支名。
+ */
+ public static String branchName(String slug) {
+ return WORKTREE_BRANCH_PREFIX + flattenSlug(slug);
+ }
+
+ // ==================== Worktree 创建 ====================
+
+ /**
+ * 为会话创建或恢复 worktree。
+ *
+ * @param slug worktree 名称
+ * @param gitRoot Git 仓库根目录
+ * @param baseBranch 基准分支(null 时使用 HEAD)
+ * @return 创建结果
+ */
+ public WorktreeCreateResult createOrResume(String slug, Path gitRoot, String baseBranch) throws IOException {
+ validateSlug(slug);
+
+ String flat = flattenSlug(slug);
+ String branch = branchName(slug);
+ Path worktreeDir = gitRoot.resolve(".claude").resolve("worktrees").resolve(flat);
+
+ // 快速恢复:已存在则直接返回
+ if (Files.isDirectory(worktreeDir)) {
+ String headCommit = readWorktreeHead(worktreeDir);
+ log.info("Resuming existing worktree: {} at {}", slug, worktreeDir);
+ // 更新 mtime 防止被 GC
+ try {
+ Files.setLastModifiedTime(worktreeDir, java.nio.file.attribute.FileTime.from(Instant.now()));
+ } catch (IOException ignored) {}
+ return new WorktreeCreateResult(worktreeDir, branch, headCommit, gitRoot, true);
+ }
+
+ // 确保父目录存在
+ Files.createDirectories(worktreeDir.getParent());
+
+ // 获取基准分支
+ if (baseBranch == null || baseBranch.isBlank()) {
+ baseBranch = execGit(gitRoot, "rev-parse", "--abbrev-ref", "HEAD").trim();
+ }
+
+ // git worktree add -B
+ String result = execGit(gitRoot,
+ "worktree", "add", "-B", branch,
+ worktreeDir.toString(), baseBranch);
+ log.info("Created worktree: {} → {} (branch: {})", slug, worktreeDir, branch);
+
+ String headCommit = readWorktreeHead(worktreeDir);
+ return new WorktreeCreateResult(worktreeDir, branch, headCommit, gitRoot, false);
+ }
+
+ /**
+ * 为 Agent 创建轻量级 worktree(不修改全局状态)。
+ */
+ public WorktreeCreateResult createAgentWorktree(String slug, Path gitRoot) throws IOException {
+ validateSlug(slug);
+ return createOrResume(slug, gitRoot, null);
+ }
+
+ // ==================== Worktree 退出 ====================
+
+ /**
+ * 退出 worktree 但保留在磁盘上。
+ */
+ public void keepWorktree() {
+ if (currentSession == null) return;
+ log.info("Keeping worktree: {} at {}", currentSession.slug, currentSession.worktreePath);
+ currentSession = null;
+ }
+
+ /**
+ * 退出 worktree 并清理(删除 worktree + 分支)。
+ *
+ * @param discardChanges true 时强制删除(即使有未提交的修改)
+ */
+ public boolean cleanupWorktree(boolean discardChanges) throws IOException {
+ if (currentSession == null) return false;
+
+ WorktreeSession session = currentSession;
+
+ // 检查是否有未提交修改
+ if (!discardChanges && hasUncommittedChanges(session.worktreePath)) {
+ throw new IllegalStateException(
+ "Worktree has uncommitted changes. Use discard_changes=true to force removal.");
+ }
+
+ return removeWorktree(session.worktreePath, session.worktreeBranch, session.gitRoot);
+ }
+
+ /**
+ * 删除指定的 worktree 和分支。
+ */
+ public boolean removeWorktree(Path worktreePath, String branch, Path gitRoot) {
+ try {
+ // git worktree remove --force
+ execGit(gitRoot, "worktree", "remove", "--force", worktreePath.toString());
+
+ // 等待 git 锁释放
+ Thread.sleep(100);
+
+ // git branch -D
+ if (branch != null) {
+ try {
+ execGit(gitRoot, "branch", "-D", branch);
+ } catch (IOException e) {
+ log.debug("Branch deletion failed (may not exist): {}", e.getMessage());
+ }
+ }
+
+ currentSession = null;
+ log.info("Removed worktree: {} (branch: {})", worktreePath, branch);
+ return true;
+ } catch (Exception e) {
+ log.error("Failed to remove worktree: {}", worktreePath, e);
+ return false;
+ }
+ }
+
+ /**
+ * 删除 Agent 的轻量级 worktree。
+ */
+ public boolean removeAgentWorktree(Path worktreePath, String branch, Path gitRoot) {
+ return removeWorktree(worktreePath, branch, gitRoot);
+ }
+
+ // ==================== 会话管理 ====================
+
+ /**
+ * 进入 worktree(设置为当前会话)。
+ */
+ public WorktreeSession enterWorktree(String slug, Path gitRoot, String baseBranch) throws IOException {
+ if (currentSession != null) {
+ throw new IllegalStateException("Already in a worktree session: " + currentSession.slug);
+ }
+
+ Path originalCwd = Path.of(System.getProperty("user.dir"));
+ WorktreeCreateResult result = createOrResume(slug, gitRoot, baseBranch);
+
+ currentSession = new WorktreeSession(
+ slug, result.worktreePath, result.worktreeBranch,
+ result.headCommit, originalCwd, gitRoot, Instant.now()
+ );
+
+ return currentSession;
+ }
+
+ /**
+ * 恢复之前的 worktree 会话(--resume 时使用)。
+ */
+ public void restoreSession(WorktreeSession session) {
+ this.currentSession = session;
+ }
+
+ public WorktreeSession getCurrentSession() {
+ return currentSession;
+ }
+
+ // ==================== Symlink 大目录 ====================
+
+ /**
+ * 为大型目录创建符号链接(如 node_modules, .next, dist)。
+ */
+ public void symlinkDirectories(Path repoRoot, Path worktreePath, List dirs) {
+ if (dirs == null || dirs.isEmpty()) return;
+
+ for (String dir : dirs) {
+ if (dir.contains("..") || dir.startsWith("/")) {
+ log.warn("Skipping unsafe symlink directory: {}", dir);
+ continue;
+ }
+
+ Path source = repoRoot.resolve(dir);
+ Path target = worktreePath.resolve(dir);
+
+ try {
+ if (!Files.isDirectory(source)) {
+ log.debug("Symlink source doesn't exist yet: {}", source);
+ continue;
+ }
+ if (Files.exists(target)) {
+ log.debug("Symlink target already exists: {}", target);
+ continue;
+ }
+ Files.createDirectories(target.getParent());
+ Files.createSymbolicLink(target, source);
+ log.info("Symlinked: {} → {}", target, source);
+ } catch (IOException e) {
+ log.warn("Failed to create symlink {} → {}: {}", target, source, e.getMessage());
+ }
+ }
+ }
+
+ // ==================== 过期清理(GC) ====================
+
+ /** Agent worktree 的临时命名模式 */
+ private static final Pattern EPHEMERAL_PATTERN = Pattern.compile(
+ "^(agent-a[0-9a-f]{7}|wf_[0-9a-f]{8}-[0-9a-f]{3}-\\d+|bridge-.+|job-.+-[0-9a-f]{8})$");
+
+ /**
+ * 清理过期的 Agent worktree(GC)。
+ *
+ * @param cutoff 截止时间,mtime 早于此时间的才会被清理
+ * @return 清理的 worktree 数量
+ */
+ public int cleanupStaleWorktrees(Path gitRoot, Instant cutoff) {
+ Path worktreeBase = gitRoot.resolve(".claude").resolve("worktrees");
+ if (!Files.isDirectory(worktreeBase)) return 0;
+
+ int cleaned = 0;
+ try (DirectoryStream stream = Files.newDirectoryStream(worktreeBase)) {
+ for (Path entry : stream) {
+ if (!Files.isDirectory(entry)) continue;
+
+ String name = entry.getFileName().toString();
+
+ // 只清理临时命名的 worktree
+ if (!EPHEMERAL_PATTERN.matcher(name).matches()) continue;
+
+ // 跳过当前会话的 worktree
+ if (currentSession != null && entry.equals(currentSession.worktreePath)) continue;
+
+ // 检查 mtime
+ Instant mtime = Files.getLastModifiedTime(entry).toInstant();
+ if (mtime.isAfter(cutoff)) continue;
+
+ // 检查是否有未提交的修改
+ if (hasUncommittedChanges(entry)) {
+ log.debug("Skipping stale worktree with changes: {}", name);
+ continue;
+ }
+
+ // 安全删除
+ String branch = WORKTREE_BRANCH_PREFIX + name;
+ if (removeWorktree(entry, branch, gitRoot)) {
+ cleaned++;
+ }
+ }
+ } catch (IOException e) {
+ log.error("Failed to scan worktree directory: {}", worktreeBase, e);
+ }
+
+ if (cleaned > 0) {
+ log.info("Cleaned up {} stale agent worktrees", cleaned);
+ }
+ return cleaned;
+ }
+
+ // ==================== Git 工具方法 ====================
+
+ /**
+ * 读取 worktree 的 HEAD commit SHA。
+ */
+ private String readWorktreeHead(Path worktreePath) {
+ try {
+ // 尝试直接读取 .git 文件获取 HEAD
+ Path gitFile = worktreePath.resolve(".git");
+ if (Files.isRegularFile(gitFile)) {
+ String content = Files.readString(gitFile).trim();
+ if (content.startsWith("gitdir:")) {
+ Path gitDir = Path.of(content.substring("gitdir:".length()).trim());
+ if (!gitDir.isAbsolute()) {
+ gitDir = worktreePath.resolve(gitDir);
+ }
+ Path headFile = gitDir.resolve("HEAD");
+ if (Files.isRegularFile(headFile)) {
+ String headContent = Files.readString(headFile).trim();
+ if (headContent.startsWith("ref:")) {
+ // 解析 symbolic ref
+ String ref = headContent.substring(4).trim();
+ Path refFile = gitDir.resolve(ref);
+ if (Files.isRegularFile(refFile)) {
+ return Files.readString(refFile).trim();
+ }
+ } else {
+ return headContent; // 直接是 SHA
+ }
+ }
+ }
+ }
+ // 回退到 git rev-parse
+ return execGit(worktreePath, "rev-parse", "HEAD").trim();
+ } catch (Exception e) {
+ log.debug("Failed to read HEAD for worktree {}: {}", worktreePath, e.getMessage());
+ return null;
+ }
+ }
+
+ /**
+ * 检查 worktree 是否有未提交的修改。
+ */
+ private boolean hasUncommittedChanges(Path worktreePath) {
+ try {
+ String status = execGit(worktreePath, "status", "--porcelain");
+ return !status.isBlank();
+ } catch (IOException e) {
+ // Git 命令失败 → fail-closed(假设有修改)
+ return true;
+ }
+ }
+
+ /**
+ * 检测 git 仓库根目录。
+ */
+ public static Path findGitRoot(Path dir) {
+ try {
+ String result = execGit(dir, "rev-parse", "--show-toplevel");
+ return Path.of(result.trim());
+ } catch (IOException e) {
+ return null;
+ }
+ }
+
+ /**
+ * 执行 git 命令并返回 stdout。
+ */
+ private static String execGit(Path workDir, String... args) throws IOException {
+ List command = new ArrayList<>();
+ command.add("git");
+ command.addAll(Arrays.asList(args));
+
+ ProcessBuilder pb = new ProcessBuilder(command)
+ .directory(workDir.toFile())
+ .redirectErrorStream(false);
+ pb.environment().put("GIT_TERMINAL_PROMPT", "0");
+
+ Process process;
+ try {
+ process = pb.start();
+ } catch (IOException e) {
+ throw new IOException("Failed to start git: " + e.getMessage(), e);
+ }
+
+ String stdout, stderr;
+ try {
+ stdout = new String(process.getInputStream().readAllBytes()).trim();
+ stderr = new String(process.getErrorStream().readAllBytes()).trim();
+
+ if (!process.waitFor(30, TimeUnit.SECONDS)) {
+ process.destroyForcibly();
+ throw new IOException("Git command timed out: " + String.join(" ", args));
+ }
+ } catch (InterruptedException e) {
+ process.destroyForcibly();
+ Thread.currentThread().interrupt();
+ throw new IOException("Git command interrupted", e);
+ }
+
+ if (process.exitValue() != 0) {
+ throw new IOException("Git command failed (exit " + process.exitValue() + "): " +
+ String.join(" ", args) + "\n" + stderr);
+ }
+
+ return stdout;
+ }
+}