✨ feat(vscode): 摘修复冲突不再中止,保留 worktree 自动开冲突解决会话接手

摘修复改在固定的发布 worktree(worktreeDirectory 模板,目录名为分支名
/ 换 -)里按序 cherry-pick -x。全部成功推远端并删 worktree;中途冲突则
保留冲突现场、远端不变,开 `<发布分支>-冲突解决` claude 会话解冲突、
--continue、按原顺序摘完剩余提交再推远端。同名终端活着直接注入提示词。

- 上次冲突没处理完再摘会被拒绝;目录模板解析到工作区本身也拒绝
- 目录被非 worktree 的垃圾占着时清掉重建
- 测试覆盖成功清理、冲突接力(含 -x 尾注保留)、拒绝工作区本身
- CLAUDE.md 补一条:收尾三步默认直接做,不问不提醒

Claude-Session: https://claude.ai/code/session_018HDxMmNDyfLa3bMTUctBzt
This commit is contained in:
cruldra committed 2026-09-06 13:32:47 +08:00
1 parent ee33ae2440
commit 706057075d
6 files changed
+186 -24

No files matched your search

+36 -4
View File
@@ -1,5 +1,5 @@
import { execFileSync } from 'node:child_process'
import { promises as fsp, rmSync, writeFileSync } from 'node:fs'
import { existsSync, promises as fsp, rmSync, writeFileSync } from 'node:fs'
import * as os from 'node:os'
import * as path from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
@@ -104,23 +104,55 @@ describe('集成', () => {
expect(s).toMatchObject({ current: 'release/2026-08-31', unreleased: 2 })
const list = await listUnreleasedCommits({ cwd: fx.repo, devBranch: 'main', current: 'release/2026-08-31' })
expect(list.map(c => c.subject)).toEqual(['feat c', 'fix b'])
await cherryPickToRelease({ cwd: fx.repo, branch: 'release/2026-08-31', shas: [fix] })
// 目录被一个不是 worktree 的垃圾目录占着,应当清掉重建而不是撞路径
const worktreePath = path.join(fx.root, 'wt', 'release-2026-08-31')
await fsp.mkdir(worktreePath, { recursive: true })
writeFileSync(path.join(worktreePath, 'junk.txt'), 'junk\n')
await expect(cherryPickToRelease({ cwd: fx.repo, branch: 'release/2026-08-31', shas: [fix], worktreePath })).resolves.toBeUndefined()
expect(git(fx.origin, ['log', '--format=%s', 'release/2026-08-31'])).toBe('fix b\ninit')
expect(git(fx.repo, ['worktree', 'list']).split('\n')).toHaveLength(1)
expect(existsSync(worktreePath)).toBe(false)
s = await getReleaseStatus({ cwd: fx.repo, devBranch: 'main', prefix: 'release/' })
expect(s.unreleased).toBe(1)
})
it('cherry-pick 冲突 → 抛错、远端不变、worktree 已清理', async () => {
it('cherry-pick 冲突 → 返回冲突现场、worktree 留在中间态、远端不变;没处理完拒绝再摘,处理完复用同一 worktree', async () => {
fx = await setup()
await cutRelease({ cwd: fx.repo, devBranch: 'main', branch: 'release/2026-08-31' })
commit(fx.peer, 'a.txt', 'a2', 'change a')
const conflict = commit(fx.peer, 'a.txt', 'a3', 'change a again')
const after = commit(fx.peer, 'd.txt', 'd', 'feat d')
git(fx.peer, ['push', 'origin', 'main'])
const before = git(fx.origin, ['rev-parse', 'release/2026-08-31'])
await expect(cherryPickToRelease({ cwd: fx.repo, branch: 'release/2026-08-31', shas: [conflict] })).rejects.toThrow(/冲突/)
const worktreePath = path.join(fx.root, 'wt', 'release-2026-08-31')
const o = { cwd: fx.repo, branch: 'release/2026-08-31', worktreePath }
await expect(cherryPickToRelease({ ...o, shas: [conflict, after] })).resolves.toEqual({
sha: conflict,
subject: 'change a again',
worktreePath,
remaining: [after],
})
expect(git(fx.origin, ['rev-parse', 'release/2026-08-31'])).toBe(before)
expect(git(fx.repo, ['worktree', 'list']).split('\n')).toHaveLength(2)
expect(git(worktreePath, ['rev-parse', '--verify', 'CHERRY_PICK_HEAD'])).toBe(conflict)
await expect(cherryPickToRelease({ ...o, shas: [after] })).rejects.toThrow(/还没处理完/)
// 模拟冲突解决会话:解冲突、continue、推远端
writeFileSync(path.join(worktreePath, 'a.txt'), 'a3\n')
git(worktreePath, ['add', 'a.txt'])
git(worktreePath, ['-c', 'core.editor=true', 'cherry-pick', '--continue'])
git(worktreePath, ['push', 'origin', 'HEAD:refs/heads/release/2026-08-31'])
expect(git(fx.origin, ['log', '-1', '--format=%b', 'release/2026-08-31'])).toContain(`(cherry picked from commit ${conflict})`)
await expect(cherryPickToRelease({ ...o, shas: [after] })).resolves.toBeUndefined()
expect(git(fx.origin, ['log', '--format=%s', 'release/2026-08-31'])).toBe('feat d\nchange a again\ninit')
expect(git(fx.repo, ['worktree', 'list']).split('\n')).toHaveLength(1)
})
it('发布 worktree 目录解析到工作区本身 → 拒绝', async () => {
fx = await setup()
await cutRelease({ cwd: fx.repo, devBranch: 'main', branch: 'release/2026-08-31' })
const fix = commit(fx.peer, 'b.txt', 'b', 'fix b')
git(fx.peer, ['push', 'origin', 'main'])
await expect(cherryPickToRelease({ cwd: fx.repo, branch: 'release/2026-08-31', shas: [fix], worktreePath: fx.repo })).rejects.toThrow(/工作区本身/)
expect(git(fx.repo, ['status', '--porcelain'])).toBe('')
})
it('listReleaseBranches 新→旧', async () => {
fx = await setup()
await cutRelease({ cwd: fx.repo, devBranch: 'main', branch: 'release/2026-08-01' })