fix(merge): in-place 合并脏树守卫忽略未跟踪文件(--untracked-files=no)
之前 mergeInPrimaryCheckout 用 git status --porcelain 判脏,未跟踪文件(GEMINI.md/.claude/ 本地杂物)也算进去 → 误判拒绝合并(jiu/dudu 等带未跟踪文件的项目「通过并合并」必失败)。 改为 --untracked-files=no 只看已跟踪改动;未跟踪文件不挡合并(真要被覆盖时 git merge 自报错走 catch)。 测试加:仅未跟踪文件 → 仍 in-place 合并成功。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -29,7 +29,9 @@ export function mergeWorktreeDirFor(taskId: string): string {
|
|||||||
*/
|
*/
|
||||||
/**
|
/**
|
||||||
* 目标分支正被主检出(repoPath)占用时的合并:直接在主检出里 `git merge --no-ff`。
|
* 目标分支正被主检出(repoPath)占用时的合并:直接在主检出里 `git merge --no-ff`。
|
||||||
* 这是用户手动合并会做的事,安全;但要求工作区干净,否则可能与未提交改动纠缠 → 拒绝。
|
* 这是用户手动合并会做的事,安全;但要求工作区无【已跟踪】的未提交改动,否则可能纠缠 → 拒绝。
|
||||||
|
* 注意:只看已跟踪改动(--untracked-files=no)。未跟踪文件(项目常有的本地杂物 GEMINI.md/.claude/…)
|
||||||
|
* 不算脏——它们不会被合并纠缠;万一某未跟踪文件恰好会被合并覆盖,git merge 会自己报错、走下方 catch。
|
||||||
*/
|
*/
|
||||||
async function mergeInPrimaryCheckout(
|
async function mergeInPrimaryCheckout(
|
||||||
repoPath: string,
|
repoPath: string,
|
||||||
@@ -37,7 +39,7 @@ async function mergeInPrimaryCheckout(
|
|||||||
defaultBranch: string,
|
defaultBranch: string,
|
||||||
taskId: string,
|
taskId: string,
|
||||||
): Promise<MergeResult> {
|
): Promise<MergeResult> {
|
||||||
const dirty = (await git(repoPath, ['status', '--porcelain']).catch(() => '')).trim();
|
const dirty = (await git(repoPath, ['status', '--porcelain', '--untracked-files=no']).catch(() => '')).trim();
|
||||||
if (dirty) {
|
if (dirty) {
|
||||||
return {
|
return {
|
||||||
ok: false,
|
ok: false,
|
||||||
|
|||||||
@@ -140,6 +140,22 @@ test('mergeBranch:main 被主检出占用但工作区有未提交改动 →
|
|||||||
assert.equal(gitSync(repo, ['rev-parse', 'main']).trim(), mainBefore, '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) => {
|
test('mergeBranch:重复合并(分支已在 main 里)→ ok,不新建提交', async (t) => {
|
||||||
const repo = setup(t);
|
const repo = setup(t);
|
||||||
gitSync(repo, ['checkout', '-b', 'maestro/tsk_re']);
|
gitSync(repo, ['checkout', '-b', 'maestro/tsk_re']);
|
||||||
|
|||||||
Reference in New Issue
Block a user