🐛 fix(vscode): 摘修复列表与角标认 cherry-pick -x 尾注,解过冲突的复制品不再重复显示
解过冲突的 cherry-pick 复制品补丁内容已变,按文件指纹对不上,列表和角标 都把它当没摘。现在额外扫发布分支独有提交的说明,`(cherry picked from commit <sha>)` 尾注命中的来源提交直接判已摘,列表过滤与 rev-list 计数同步扣掉。 Claude-Session: https://claude.ai/code/session_018HDxMmNDyfLa3bMTUctBzt
This commit is contained in:
1 parent
e99aed15c2
commit
df0660cff7
4 files changed
+42
-9
No files matched your search
+1
-1
@@ -24,7 +24,7 @@
|
||||
5. **完成**:拖到「完成」列。有 PR 就由插件合并(冲突时开 `issue-N-冲突解决` 会话,解决后再拖一次),然后关工单、把会话记录拷回主仓库、删 worktree、删 feature 分支。
|
||||
6. **回退**:进行中 / 审查中的卡可「重置为待办」,关 PR、删 worktree、清分支和实施痕迹,spec / plan 保留。
|
||||
7. **移交**:前置条件:两台机器的 spx 都已 `cd cli && make install` 到含 `handoffAttachmentId` 的版本,否则移交期间旧 spx 的 state 写入会被 schema 拒绝。工单 tab 头部「移交」→ 选同事。插件校验 worktree 已全部 push,关掉该工单的终端,把头脑风暴 / 实施 / 测试的 claude 会话(jsonl + 子目录)和 codex 审查会话打成 `spx-handoff-issue-N.tgz` 挂到工单附件,写 `handoffAttachmentId` / `handoffFrom`,把工单指派给对方,最后删本机 worktree 与本地分支(远端分支保留)。对方看板上该卡显示「待接管」,工单 tab 点「接管」:从 origin 重建 worktree(`git worktree add -B <branch> … origin/<branch>`)、把会话文件装到本机 `~/.claude/projects/` 对应目录(先清同 sid 副本)与 `~/.codex/sessions/`、写本机字段、清移交字段并删附件。之后 resume / 合并 / 冲突解决 / 测试与本机实施的工单无异。
|
||||
8. **发布**:工具栏「发布」按钮下拉三项。「切新发布」从 `origin/<devBranch>` 切当天的 `<releaseBranchPrefix>YYYY-MM-DD` 并推远端(纯远端,不建本地分支),配了 `jenkinsJob` 会顺带问要不要立刻部署。「摘修复到当期发布」列出 `devBranch` 上还没进发布分支的提交,多选后在发布 worktree(`worktreeDirectory` 模板,目录名为分支名把 `/` 换成 `-`)里按序 `cherry-pick -x` 并推远端,成功后删 worktree。中途冲突则保留冲突现场、远端不变,开 `<发布分支>-冲突解决` 会话让 claude 解冲突、`cherry-pick --continue`、按序摘完剩余提交再推远端;上次冲突还没处理完时再摘会被拒绝。「部署到正式服」选一个发布分支(默认当期,选旧的即回滚)执行 `opencli jks build <jenkinsJob> -p REF=<分支> -p REMOTE_HOST=<jenkinsProdHost>`。
|
||||
8. **发布**:工具栏「发布」按钮下拉三项。「切新发布」从 `origin/<devBranch>` 切当天的 `<releaseBranchPrefix>YYYY-MM-DD` 并推远端(纯远端,不建本地分支),配了 `jenkinsJob` 会顺带问要不要立刻部署。「摘修复到当期发布」列出 `devBranch` 上还没进发布分支的提交(按文件改动指纹比对,另认 `cherry-pick -x` 留下的「cherry picked from commit」尾注,所以解过冲突、内容已变的复制品也算已摘;手工 pick 请带 `-x`),多选后在发布 worktree(`worktreeDirectory` 模板,目录名为分支名把 `/` 换成 `-`)里按序 `cherry-pick -x` 并推远端,成功后删 worktree。中途冲突则保留冲突现场、远端不变,开 `<发布分支>-冲突解决` 会话让 claude 解冲突、`cherry-pick --continue`、按序摘完剩余提交再推远端;上次冲突还没处理完时再摘会被拒绝。「部署到正式服」选一个发布分支(默认当期,选旧的即回滚)执行 `opencli jks build <jenkinsJob> -p REF=<分支> -p REMOTE_HOST=<jenkinsProdHost>`。
|
||||
|
||||
其他看板规则:
|
||||
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"publisher": "clurdra",
|
||||
"name": "superpowers-vscode-clurdra",
|
||||
"displayName": "Superpowers-clurdra",
|
||||
"version": "0.2.120",
|
||||
"version": "0.2.121",
|
||||
"packageManager": "pnpm@10.27.0",
|
||||
"description": "Superpowers specs and plans Kanban explorer",
|
||||
"author": "clurdra",
|
||||
|
||||
@@ -162,12 +162,23 @@ export async function getReleaseStatus(o: { cwd: string, devBranch: string, pref
|
||||
const current = pickCurrentRelease(o.prefix, await remoteBranchNames(o.cwd, o.prefix))
|
||||
if (!current)
|
||||
return base
|
||||
const count = await mustGit(
|
||||
const shas = await mustGit(
|
||||
o.cwd,
|
||||
['rev-list', '--count', '--right-only', '--cherry-pick', '--no-merges', unreleasedRange(current, o.devBranch)],
|
||||
['rev-list', '--right-only', '--cherry-pick', '--no-merges', unreleasedRange(current, o.devBranch)],
|
||||
'统计未发布提交失败',
|
||||
)
|
||||
return { devBranch: o.devBranch, current, unreleased: Number(count) }
|
||||
const picked = await cherryPickedSources(o.cwd, `origin/${o.devBranch}..origin/${current}`)
|
||||
const unreleased = shas.split('\n').filter(sha => sha && !picked.has(sha)).length
|
||||
return { devBranch: o.devBranch, current, unreleased }
|
||||
}
|
||||
|
||||
/**
|
||||
* `cherry-pick -x` 在提交说明末尾留的来源提交号。解过冲突的复制品补丁内容已经变了,
|
||||
* 指纹对不上,只剩这条线索能认出它摘过。
|
||||
*/
|
||||
async function cherryPickedSources(cwd: string, range: string): Promise<Set<string>> {
|
||||
const bodies = await mustGit(cwd, ['log', '--no-merges', '--format=%b', range], '读取发布分支提交说明失败')
|
||||
return new Set(Array.from(bodies.matchAll(/\(cherry picked from commit ([0-9a-f]{40})\)/g), m => m[1]))
|
||||
}
|
||||
|
||||
/** 新→旧;不 fetch,调用方自己决定何时刷新远端 */
|
||||
@@ -276,20 +287,24 @@ export async function listUnreleasedCommits(o: { cwd: string, devBranch: string,
|
||||
)
|
||||
if (!out)
|
||||
return []
|
||||
const releaseOnly = `origin/${o.devBranch}..origin/${o.current}`
|
||||
const picked = await cherryPickedSources(o.cwd, releaseOnly)
|
||||
const commits = out.split('\x1E').flatMap((chunk) => {
|
||||
const trimmed = chunk.replace(/^\n+/, '').trimEnd()
|
||||
if (!trimmed)
|
||||
return []
|
||||
const [sha, subject, author, date, body = ''] = trimmed.split('\0')
|
||||
return [{ sha, subject, author, date, body: body.trim() }]
|
||||
return picked.has(sha) ? [] : [{ sha, subject, author, date, body: body.trim() }]
|
||||
})
|
||||
if (commits.length === 0)
|
||||
return []
|
||||
const perCommit = await fileChangeKeysByCommit(o.cwd, ['--right-only', '--cherry-pick', unreleasedRange(o.current, o.devBranch)])
|
||||
const files = new Set<string>()
|
||||
for (const keys of perCommit.values()) {
|
||||
for (const key of keys)
|
||||
for (const c of commits) {
|
||||
for (const key of perCommit.get(c.sha) ?? [])
|
||||
files.add(key.slice(0, key.indexOf('\0')))
|
||||
}
|
||||
const onRelease = await fileChangeKeysByCommit(o.cwd, [`origin/${o.devBranch}..origin/${o.current}`], files)
|
||||
const onRelease = await fileChangeKeysByCommit(o.cwd, [releaseOnly], files)
|
||||
const released = new Set<string>()
|
||||
for (const keys of onRelease.values()) {
|
||||
for (const key of keys)
|
||||
|
||||
@@ -201,6 +201,24 @@ describe('集成', () => {
|
||||
const list = await listUnreleasedCommits({ cwd: fx.repo, devBranch: 'main', current: 'release/2026-08-31' })
|
||||
expect(list.map(c => c.subject)).toEqual(['feat: other'])
|
||||
})
|
||||
it('解过冲突的复制品内容不同,但带 -x 尾注就算已摘,角标计数同步扣掉', async () => {
|
||||
fx = await setup()
|
||||
await cutRelease({ cwd: fx.repo, devBranch: 'main', branch: 'release/2026-08-31' })
|
||||
const fix = commit(fx.peer, 'a.txt', 'a2', 'fix a')
|
||||
commit(fx.peer, 'c.txt', 'c', 'feat: other')
|
||||
git(fx.peer, ['push', 'origin', 'main'])
|
||||
git(fx.peer, ['fetch'])
|
||||
git(fx.peer, ['checkout', '-b', 'release/2026-08-31', 'origin/release/2026-08-31'])
|
||||
writeFileSync(path.join(fx.peer, 'a.txt'), 'a2 resolved differently\n')
|
||||
git(fx.peer, ['add', 'a.txt'])
|
||||
git(fx.peer, ['commit', '-m', `fix a\n\n(cherry picked from commit ${fix})`])
|
||||
git(fx.peer, ['push', 'origin', 'release/2026-08-31'])
|
||||
git(fx.repo, ['fetch', 'origin'])
|
||||
const list = await listUnreleasedCommits({ cwd: fx.repo, devBranch: 'main', current: 'release/2026-08-31' })
|
||||
expect(list.map(c => c.subject)).toEqual(['feat: other'])
|
||||
const s = await getReleaseStatus({ cwd: fx.repo, devBranch: 'main', prefix: 'release/' })
|
||||
expect(s.unreleased).toBe(1)
|
||||
})
|
||||
it('标题相同但改的文件不同,仍算未发布', async () => {
|
||||
fx = await setup()
|
||||
await cutRelease({ cwd: fx.repo, devBranch: 'main', branch: 'release/2026-08-31' })
|
||||
|
||||
Reference in new issue
Block a user