From d160d2764518d6431cc05c2bc2819e444779c5e9 Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Sat, 13 Jun 2026 14:40:31 +0800 Subject: [PATCH] =?UTF-8?q?fix(merge):=20in-place=20=E5=90=88=E5=B9=B6?= =?UTF-8?q?=E8=84=8F=E6=A0=91=E5=AE=88=E5=8D=AB=E5=BF=BD=E7=95=A5=E6=9C=AA?= =?UTF-8?q?=E8=B7=9F=E8=B8=AA=E6=96=87=E4=BB=B6(--untracked-files=3Dno)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 之前 mergeInPrimaryCheckout 用 git status --porcelain 判脏,未跟踪文件(GEMINI.md/.claude/ 本地杂物)也算进去 → 误判拒绝合并(jiu/dudu 等带未跟踪文件的项目「通过并合并」必失败)。 改为 --untracked-files=no 只看已跟踪改动;未跟踪文件不挡合并(真要被覆盖时 git merge 自报错走 catch)。 测试加:仅未跟踪文件 → 仍 in-place 合并成功。 Co-Authored-By: Claude Opus 4.8 --- src/executor/merge.ts | 6 ++++-- test/merge.test.ts | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) 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']);