diff --git a/src/executor/merge.ts b/src/executor/merge.ts index d8d4439..e4e8533 100644 --- a/src/executor/merge.ts +++ b/src/executor/merge.ts @@ -29,7 +29,9 @@ export function mergeWorktreeDirFor(taskId: string): string { */ /** * 目标分支正被主检出(repoPath)占用时的合并:直接在主检出里 `git merge --no-ff`。 - * 这是用户手动合并会做的事,安全;但要求工作区干净,否则可能与未提交改动纠缠 → 拒绝。 + * 这是用户手动合并会做的事,安全;但要求工作区无【已跟踪】的未提交改动,否则可能纠缠 → 拒绝。 + * 注意:只看已跟踪改动(--untracked-files=no)。未跟踪文件(项目常有的本地杂物 GEMINI.md/.claude/…) + * 不算脏——它们不会被合并纠缠;万一某未跟踪文件恰好会被合并覆盖,git merge 会自己报错、走下方 catch。 */ async function mergeInPrimaryCheckout( repoPath: string, @@ -37,7 +39,7 @@ async function mergeInPrimaryCheckout( defaultBranch: string, taskId: string, ): Promise { - const dirty = (await git(repoPath, ['status', '--porcelain']).catch(() => '')).trim(); + const dirty = (await git(repoPath, ['status', '--porcelain', '--untracked-files=no']).catch(() => '')).trim(); if (dirty) { return { ok: false, diff --git a/test/merge.test.ts b/test/merge.test.ts index 1e8c09c..6c39657 100644 --- a/test/merge.test.ts +++ b/test/merge.test.ts @@ -140,6 +140,22 @@ test('mergeBranch:main 被主检出占用但工作区有未提交改动 → assert.equal(gitSync(repo, ['rev-parse', 'main']).trim(), mainBefore, 'main 无损'); }); +test('mergeBranch:main 被主检出占用、仅有未跟踪文件 → 不算脏,仍 in-place 合并成功', async (t) => { + const repo = setup(t); + gitSync(repo, ['checkout', '-b', 'maestro/tsk_ut']); + commitFile(repo, 'a.txt', 'A\n', 'maestro(tsk_ut): add a'); + gitSync(repo, ['checkout', 'main']); + // 主检出有未跟踪文件(项目常见本地杂物,如 GEMINI.md/.claude/…)——不应挡合并 + writeFileSync(join(repo, 'LOCAL_NOTES.md'), 'untracked junk\n'); + const mainBefore = gitSync(repo, ['rev-parse', 'main']).trim(); + + const r = await mergeBranch(repo, 'maestro/tsk_ut', 'main', 'tsk_ut'); + assert.equal(r.ok, true, r.error); + assert.notEqual(gitSync(repo, ['rev-parse', 'main']).trim(), mainBefore, 'main 应前进'); + assert.equal(gitSync(repo, ['show', 'main:a.txt']), 'A\n'); + assert.ok(existsSync(join(repo, 'LOCAL_NOTES.md')), '未跟踪文件应原样保留'); +}); + test('mergeBranch:重复合并(分支已在 main 里)→ ok,不新建提交', async (t) => { const repo = setup(t); gitSync(repo, ['checkout', '-b', 'maestro/tsk_re']);