✨ feat(vscode): 摘修复按 PR 分组框选,可搜作者和标题

This commit is contained in:
cruldra committed 2026-09-01 17:27:27 +08:00
1 parent 0031851df3
commit 3e3750c494
9 files changed
+353 -42

No files matched your search

+62 -4
View File
@@ -24,6 +24,14 @@ export interface UnreleasedCommit {
subject: string
author: string
date: string
body: string
}
export interface ReleaseCommitGroup {
pr?: number
title: string
authors: string[]
commits: UnreleasedCommit[]
}
export class ReleaseError extends Error {
@@ -71,6 +79,53 @@ export function pickCurrentRelease(prefix: string, remoteBranches: string[]): st
return remoteBranches.filter(b => b.startsWith(prefix)).sort().at(-1)
}
export function parsePrNumber(text: string): number | undefined {
const close = /\b(?:closes|fixes|resolves)\s+#(\d+)/i.exec(text)
if (close)
return Number(close[1])
const paren = /\(#(\d+)\)/.exec(text)
if (paren)
return Number(paren[1])
return undefined
}
function stripPrFromSubject(subject: string): string {
return subject.replace(/\s*\(#\d+\)\s*$/, '').trim()
}
/** 同 PR 收一组(新→旧首次出现顺序),认不出 PR 的进「未关联 PR」。 */
export function groupCommitsByPr(commits: UnreleasedCommit[]): ReleaseCommitGroup[] {
const prGroups = new Map<number, ReleaseCommitGroup>()
const prOrder: number[] = []
const ungrouped: UnreleasedCommit[] = []
for (const c of commits) {
const pr = parsePrNumber(`${c.subject}\n${c.body}`)
if (pr === undefined) {
ungrouped.push(c)
continue
}
let g = prGroups.get(pr)
if (!g) {
g = { pr, title: stripPrFromSubject(c.subject), authors: [], commits: [] }
prGroups.set(pr, g)
prOrder.push(pr)
}
g.commits.push(c)
if (!g.authors.includes(c.author))
g.authors.push(c.author)
}
const out = prOrder.map(n => prGroups.get(n)!)
if (ungrouped.length > 0) {
const authors: string[] = []
for (const c of ungrouped) {
if (!authors.includes(c.author))
authors.push(c.author)
}
out.push({ title: '未关联 PR', authors, commits: ungrouped })
}
return out
}
async function remoteBranchNames(cwd: string, prefix: string): Promise<string[]> {
const out = await mustGit(cwd, ['for-each-ref', '--format=%(refname:strip=3)', `refs/remotes/origin/${prefix}`], '读取远端发布分支失败')
return out ? out.split('\n') : []
@@ -110,14 +165,17 @@ export async function listReleaseBranches(o: { cwd: string, prefix: string }): P
export async function listUnreleasedCommits(o: { cwd: string, devBranch: string, current: string }): Promise<UnreleasedCommit[]> {
const out = await mustGit(
o.cwd,
['log', '--right-only', '--cherry-pick', '--no-merges', '--format=%H%x00%s%x00%an%x00%as', unreleasedRange(o.current, o.devBranch)],
['log', '--right-only', '--cherry-pick', '--no-merges', '--format=%H%x00%s%x00%an%x00%as%x00%b%x1e', unreleasedRange(o.current, o.devBranch)],
'读取未发布提交失败',
)
if (!out)
return []
return out.split('\n').map((line) => {
const [sha, subject, author, date] = line.split('\0')
return { sha, subject, author, date }
return 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() }]
})
}