⚡ perf(vscode): 摘修复指纹一次算完,不再逐文件起进程

This commit is contained in:
cruldra committed 2026-09-01 20:11:41 +08:00
1 parent dd3b7aaa24
commit 90600cf070
2 files changed
+44 -56

No files matched your search

+1 -1
View File
@@ -2,7 +2,7 @@
"publisher": "clurdra",
"name": "superpowers-vscode-clurdra",
"displayName": "Superpowers-clurdra",
"version": "0.2.115",
"version": "0.2.116",
"packageManager": "pnpm@10.27.0",
"description": "Superpowers specs and plans Kanban explorer",
"author": "clurdra",
+43 -55
View File
@@ -48,7 +48,7 @@ interface Run { ok: boolean, stdout: string, stderr: string }
function runGit(cwd: string, args: string[], timeoutMs = 30_000): Promise<Run> {
return new Promise((resolve) => {
execFile('git', ['-C', cwd, ...args], { timeout: timeoutMs, encoding: 'utf8' }, (err, stdout, stderr) => {
execFile('git', ['-C', cwd, ...args], { timeout: timeoutMs, encoding: 'utf8', maxBuffer: 512 * 1024 * 1024 }, (err, stdout, stderr) => {
resolve({ ok: !err, stdout: stdout ?? '', stderr: stderr ?? '' })
})
})
@@ -56,7 +56,7 @@ function runGit(cwd: string, args: string[], timeoutMs = 30_000): Promise<Run> {
function runGitStdin(args: string[], input: string, timeoutMs = 30_000): Promise<Run> {
return new Promise((resolve) => {
const child = execFile('git', args, { timeout: timeoutMs, encoding: 'utf8' }, (err, stdout, stderr) => {
const child = execFile('git', args, { timeout: timeoutMs, encoding: 'utf8', maxBuffer: 512 * 1024 * 1024 }, (err, stdout, stderr) => {
resolve({ ok: !err, stdout: stdout ?? '', stderr: stderr ?? '' })
})
child.stdin?.end(input)
@@ -218,61 +218,46 @@ export function attachPulls<T extends { sha: string }>(
})
}
/** 发布分支上已有的提交标题。拆开摘过的会留下同标题多条,整包再摘必冲突。 */
async function commitFiles(cwd: string, sha: string): Promise<string[]> {
const out = await mustGit(cwd, ['diff-tree', '--no-commit-id', '--name-only', '-r', sha], '读取提交文件列表失败')
return out ? out.split('\n').filter(Boolean) : []
}
type FileChangeKeys = Map<string, string[]>
async function fileChangeId(cwd: string, sha: string, file: string): Promise<string | null> {
const shown = await runGit(cwd, ['show', '--pretty=format:', '--full-index', sha, '--', file])
if (!shown.ok || !shown.stdout.trim())
return null
const id = await runGitStdin(['patch-id', '--stable'], shown.stdout)
const hash = id.stdout.trim().split(/\s+/)[0]
return hash || null
}
async function commitFileChangeKeys(cwd: string, sha: string): Promise<string[]> {
const files = await commitFiles(cwd, sha)
const keys: string[] = []
for (const file of files) {
const id = await fileChangeId(cwd, sha, file)
if (id)
keys.push(`${file}\0${id}`)
}
return keys
}
/** 发布分支上每个文件已有的改动指纹。用来认出「一次提交被拆成多次摘过去」的情况。 */
async function releaseFileChangeKeys(cwd: string, current: string, devBranch: string, files: Set<string>): Promise<Set<string>> {
const keys = new Set<string>()
if (files.size === 0)
return keys
const log = await mustGit(
cwd,
['log', '--no-merges', '--name-only', '--format=%n%H', `origin/${devBranch}..origin/${current}`],
'读取发布分支改动失败',
)
/** 一次 `git log -p` 切出每个提交每个文件的改动,再一次 `git patch-id` 算完指纹。 */
async function fileChangeKeysByCommit(cwd: string, logArgs: string[], onlyFiles?: Set<string>): Promise<FileChangeKeys> {
const out = new Map<string, string[]>()
const log = await mustGit(cwd, ['log', '-p', '--no-merges', '--format=commit %H', ...logArgs], '读取提交改动失败')
if (!log)
return out
const blocks: Array<{ sha: string, file: string, text: string[] }> = []
let sha = ''
const hits: Array<{ sha: string, files: string[] }> = []
for (const line of log.split('\n')) {
if (/^[0-9a-f]{40}$/.test(line)) {
sha = line
hits.push({ sha, files: [] })
const commit = /^commit ([0-9a-f]{40})$/.exec(line)
if (commit) {
sha = commit[1]
out.set(sha, [])
continue
}
if (sha && files.has(line))
hits[hits.length - 1]?.files.push(line)
const header = /^diff --git "?a\/.*? "?b\/(.+?)"?$/.exec(line)
if (header) {
blocks.push({ sha, file: header[1], text: [line] })
continue
}
for (const hit of hits) {
for (const file of hit.files) {
const id = await fileChangeId(cwd, hit.sha, file)
if (id)
keys.add(`${file}\0${id}`)
blocks[blocks.length - 1]?.text.push(line)
}
const wanted = blocks.filter(b => !onlyFiles || onlyFiles.has(b.file))
if (wanted.length === 0)
return out
const stream = wanted
.map((b, i) => `commit ${i.toString(16).padStart(40, '0')}\n${b.text.join('\n')}\n`)
.join('')
const ids = await runGitStdin(['patch-id', '--stable'], stream)
for (const line of ids.stdout.split('\n')) {
const [id, marker] = line.trim().split(/\s+/)
if (!id || !marker)
continue
const block = wanted[Number.parseInt(marker, 16)]
if (block)
out.get(block.sha)?.push(`${block.file}\0${id}`)
}
return keys
return out
}
export async function listUnreleasedCommits(o: { cwd: string, devBranch: string, current: string }): Promise<UnreleasedCommit[]> {
@@ -290,17 +275,20 @@ export async function listUnreleasedCommits(o: { cwd: string, devBranch: string,
const [sha, subject, author, date, body = ''] = trimmed.split('\0')
return [{ sha, subject, author, date, body: body.trim() }]
})
const perCommitKeys = new Map<string, string[]>()
const perCommit = await fileChangeKeysByCommit(o.cwd, ['--right-only', '--cherry-pick', unreleasedRange(o.current, o.devBranch)])
const files = new Set<string>()
for (const c of commits) {
const keys = await commitFileChangeKeys(o.cwd, c.sha)
perCommitKeys.set(c.sha, keys)
for (const keys of perCommit.values()) {
for (const key of keys)
files.add(key.slice(0, key.indexOf('\0')))
}
const released = await releaseFileChangeKeys(o.cwd, o.current, o.devBranch, files)
const onRelease = await fileChangeKeysByCommit(o.cwd, [`origin/${o.devBranch}..origin/${o.current}`], files)
const released = new Set<string>()
for (const keys of onRelease.values()) {
for (const key of keys)
released.add(key)
}
return commits.filter((c) => {
const keys = perCommitKeys.get(c.sha) ?? []
const keys = perCommit.get(c.sha) ?? []
return keys.length === 0 || keys.some(k => !released.has(k))
})
}