十四、Git Worktree 隔离执行

本篇讲解 src/worktree.ts------如何用 git worktree 创建隔离的执行副本,让 AI 助手的修改不影响原项目,除非你主动 promote。

1. 为什么需要 Worktree?

默认情况下,AI 助手直接在项目根目录执行命令(executionSurface: 'direct')。改了就改了,删了就删了。

但如果你想让 AI 助手"先在副本里改,改好了再合并回原项目"呢?

bash 复制代码
your-project/                    ← 主仓库
${元数据目录}/worktrees/
    └── 1704830400000/           ← worktree 副本(独立分支)
        └── src/
        └── package.json

AI 助手在 worktree 副本里工作,改什么都只影响副本。等你确认没问题了,再 promoteWorktree() 把改动合并回主仓库。

2. Git Worktree 是什么?

Git worktree 是 Git 自带的功能,允许你从同一个仓库检出多个工作目录,每个目录对应不同的分支。

bash 复制代码
# 在 ${元数据目录}/worktrees/1704830400000/ 创建一个新分支的 worktree
git worktree add -b ${分支名前缀, 自定义的固定值}/1704830400000 ${元数据目录}/worktrees/1704830400000

这个新目录和主仓库共享 .git,但文件系统是独立的。

3. 核心函数

3.1 isGitRepository

typescript 复制代码
export function isGitRepository(dir: string): boolean {
  return fs.existsSync(path.join(dir, '.git'));
}

3.2 createGitWorktree

typescript 复制代码
/**
 * 创建一个 git worktree 隔离副本。
 *
 * 在 worktreesDir 下创建一个新目录,并用 git worktree add 检出到一个新分支。
 * 新分支名为 "前缀/时间戳"(如 nio-fe-codebuddy-agent/1704830400000),
 * 方便一眼区分沙箱自动创建的分支和人工创建的分支。
 *
 * @param options.repoRoot - 主仓库根目录
 * @param options.worktreesDir - worktree 存放目录(如 .nio-fe-codebuddy/worktrees/)
 * @param options.branchPrefix - 分支名前缀(如 nio-fe-codebuddy-agent)
 * @returns GitWorktreeHandle,包含 worktree 路径和分支名
 */
export async function createGitWorktree(options: {
  repoRoot: string;
  worktreesDir: string;
  branchPrefix: string;
}): Promise<GitWorktreeHandle> {
  const repoRoot = path.resolve(options.repoRoot);
  // 不是 git 仓库就无法创建 worktree
  if (!isGitRepository(repoRoot)) {
    throw new Error(`Not a git repository: ${repoRoot}`);
  }

  // 确保 worktrees 目录存在
  fs.mkdirSync(options.worktreesDir, { recursive: true });

  // 用时间戳作为后缀,保证每次创建的分支名和目录名唯一
  const suffix = `${Date.now()}`;
  // 分支名 = 前缀/时间戳,如 nio-fe-codebuddy-agent/1704830400000
  const branchName = `${options.branchPrefix}/${suffix}`;
  // worktree 目录 = worktreesDir/时间戳,如 .nio-fe-codebuddy/worktrees/1704830400000
  const worktreePath = path.join(options.worktreesDir, suffix);

  // 执行 git worktree add -b <分支名> <目录>,创建新分支并检出
  await runGit(repoRoot, ['worktree', 'add', '-b', branchName, worktreePath]);
  return { worktreePath, branchName };
}

步骤

  1. 确保是 git 仓库
  2. 创建 worktrees 目录
  3. 用时间戳生成唯一的分支名和路径
  4. 执行 git worktree add

返回的 GitWorktreeHandle 包含两个信息:

  • worktreePath:worktree 目录的绝对路径(作为 executionRoot
  • branchName:创建的分支名

3.3 removeGitWorktree

typescript 复制代码
/**
 * 删除一个 git worktree 及其关联的分支。
 *
 * 先用 git worktree remove --force 强制移除目录,
 * 再用 git branch -D 删除关联的分支(失败不报错,分支可能已不存在)。
 *
 * @param repoRoot - 主仓库根目录
 * @param handle - createGitWorktree 返回的句柄
 */
export async function removeGitWorktree(
  repoRoot: string,
  handle: GitWorktreeHandle,
): Promise<void> {
  const root = path.resolve(repoRoot);

  // 如果 worktree 目录还存在,强制移除
  if (fs.existsSync(handle.worktreePath)) {
    await runGit(root, ['worktree', 'remove', '--force', handle.worktreePath]);
  }

  // 删除关联分支(catch 忽略错误,分支可能已随 worktree 一起清理)
  await runGit(root, ['branch', '-D', handle.branchName]).catch(() => undefined);
}

两步清理

  1. 删除 worktree 目录(git worktree remove --force
  2. 删除对应分支(git branch -D

3.4 promoteGitWorktree

typescript 复制代码
/**
 * 将 worktree 的修改合并回主仓库(promote 操作)。
 *
 * 前提条件:主仓库和 worktree 都必须是干净状态(没有未提交的修改)。
 * 合并使用 --no-ff(不快进),确保产生一个合并提交,方便追溯。
 *
 * @param repoRoot - 主仓库根目录
 * @param handle - createGitWorktree 返回的句柄
 * @returns merged 表示是否合并成功,message 是结果描述
 */
export async function promoteGitWorktree(
  repoRoot: string,
  handle: GitWorktreeHandle,
): Promise<{ merged: boolean; message: string }> {
  const root = path.resolve(repoRoot);
  const worktreeBranch = handle.branchName;

  // 前置检查 1:主仓库必须是干净状态
  if (!(await isGitTreeClean(root))) {
    return {
      merged: false,
      message: 'Cannot promote: main repository has uncommitted changes.',
    };
  }

  // 前置检查 2:worktree 也必须是干净状态
  if (!(await isGitTreeClean(handle.worktreePath))) {
    return {
      merged: false,
      message: 'Cannot promote: worktree has uncommitted changes.',
    };
  }

  // 前置检查 3:主仓库当前必须在某个分支上(不能是 detached HEAD)
  let currentBranch: string;
  try {
    currentBranch = (await runGit(root, ['rev-parse', '--abbrev-ref', 'HEAD'])).stdout.trim();
  } catch {
    return { merged: false, message: 'Cannot promote: failed to read current branch.' };
  }

  if (!currentBranch || currentBranch === 'HEAD') {
    return { merged: false, message: 'Cannot promote: detached HEAD on main repo.' };
  }

  // 执行合并:--no-ff 保证产生合并提交,方便追溯 worktree 的改动
  try {
    await runGit(root, [
      'merge',
      '--no-ff',
      worktreeBranch,
      '-m',
      `Promote agent worktree ${worktreeBranch}`,
    ]);
    return { merged: true, message: `Merged ${worktreeBranch} into ${currentBranch}.` };
  } catch (error) {
    return {
      merged: false,
      message: `Merge failed: ${error instanceof Error ? error.message : String(error)}`,
    };
  }
}

promote 条件

  1. 主仓库没有未提交的更改
  2. worktree 也没有未提交的更改
  3. 主仓库不在 detached HEAD 状态
  4. 合并成功

4. isGitTreeClean

typescript 复制代码
/**
 * 检查一个 git 目录是否是干净状态(没有未提交的修改)。
 * 用 git status --porcelain 判断:输出为空 = 干净。
 */
export async function isGitTreeClean(dir: string): Promise<boolean> {
  const result = await runGit(dir, ['status', '--porcelain']);
  return result.stdout.trim() === '';
}

git status --porcelain 输出为空表示工作目录干净。

5. runGit------底层 Git 执行

typescript 复制代码
/**
 * 执行 git 命令的通用工具函数。
 *
 * @param cwd - 执行目录
 * @param args - git 子命令和参数
 * @returns 退出码、stdout、stderr
 * @throws 非 0 退出码时抛出错误
 */
function runGit(
  cwd: string,
  args: string[],
): Promise<{ code: number; stdout: string; stderr: string }> {
  return new Promise((resolve, reject) => {
    const child = spawn('git', args, { cwd, stdio: ['ignore', 'pipe', 'pipe'] });
    let stdout = '';
    let stderr = '';

    child.stdout?.on('data', (chunk: Buffer) => { stdout += chunk.toString(); });
    child.stderr?.on('data', (chunk: Buffer) => { stderr += chunk.toString(); });

    child.on('error', (error) => { reject(error); });

    child.on('close', (code) => {
      const exitCode = typeof code === 'number' ? code : 1;
      if (exitCode !== 0) {
        reject(new Error(stderr.trim() || stdout.trim() || `git ${args.join(' ')} failed`));
        return;
      }
      resolve({ code: exitCode, stdout, stderr });
    });
  });
}

注意 :这里的 runGitexecutor.ts 里的 spawnAndWait 不同。它:

  • 不需要超时(git 操作通常很快)
  • 不需要 IO 回调
  • 需要收集 stdout/stderr 文本
  • 非零退出码会 reject

6. 生命周期

ini 复制代码
AgentSandbox.initialize()
    │
    ├── executionSurface === 'worktree'
    │       │
    │       ▼ createGitWorktree()
    │       │
    │       └── executionRoot = worktreePath
    │
    └── executionSurface === 'direct'
            │
            └── executionRoot = projectRoot

AgentSandbox.executeCommand(...)
    │  所有命令在 executionRoot 下执行

AgentSandbox.promoteWorktree()
    │  把 worktree 分支合并回主仓库

AgentSandbox.dispose()
    │  清理 worktree
    │
    └── removeGitWorktree()

7. 小结

函数 作用
isGitRepository 检查目录是否是 git 仓库
createGitWorktree 创建隔离的 worktree 副本
removeGitWorktree 清理 worktree 和分支
promoteGitWorktree 将 worktree 合并回主仓库
isGitTreeClean 检查工作目录是否干净

核心思想:在副本里改,改好了再合并。改不好?直接删副本,原项目完好无损。

相关推荐
安全指北针1 小时前
大模型时代,谁在领跑中国AI安全赛道?中国AI安全产品市场分析
人工智能
KaMeidebaby1 小时前
卡梅德生物技术快报|纯化重组蛋白实操详解
人工智能·python·tcp/ip·算法·机器学习
Cloud_Shy6181 小时前
解读《Effective Python 3rd Edition》:从练气到老魔(第五章 Item 30 - 32)
开发语言·人工智能·笔记·python·学习方法
YueTann1 小时前
OpenRLHF设计
人工智能
云烟成雨TD1 小时前
Spring AI 1.x 系列【52】可观测集成 SkyWalking
人工智能·spring·skywalking
云烟成雨TD1 小时前
Spring AI 1.x 系列【57】动态工具发现:Tool Search Tool
java·人工智能·spring
AndrewHZ1 小时前
【LLM技术全景】规模定律与模型演进:为什么模型越大越强?
人工智能·gpt·深度学习·语言模型·llm·openai·规模定律
galaxylove1 小时前
Gartner发布创新洞察:AI SOC智能体加速通信运营商安全运营转型
大数据·人工智能·安全
甩手网软件2 小时前
Shopee2026新规:费率重构与履约收紧下,卖家如何破局?
大数据·人工智能