✨ feat(vscode): 摘修复识别 alembic 迁移,接不上发布分支链的高亮禁用不可摘

按 writing-alembic-migrations 的规矩判断:迁移的前任必须已在发布分支上,
且发布分支上该前任不能已有别的后继;根迁移接不上已有链、删迁移会断链也不可摘。
列表里带迁移的提交打「迁移」角标,不可摘的高亮警告色、禁用勾选并写明原因;
确认接口再校验一遍,绕过前端也摘不进去。

Claude-Session: https://claude.ai/code/session_01QWxLb6X8Vd2G5vHnp6R7qm
This commit is contained in:
cruldra committed 2026-09-08 11:44:24 +08:00
1 parent df0660cff7
commit 028195ab2b
8 files changed
+261 -26

No files matched your search

+46 -1
View File
@@ -1,5 +1,5 @@
import { execFileSync } from 'node:child_process'
import { existsSync, promises as fsp, rmSync, writeFileSync } from 'node:fs'
import { existsSync, promises as fsp, mkdirSync, rmSync, writeFileSync } from 'node:fs'
import * as os from 'node:os'
import * as path from 'node:path'
import { afterEach, describe, expect, it } from 'vitest'
@@ -12,6 +12,7 @@ import {
listReleaseBranches,
listUnreleasedCommits,
normalizeReleasePrefix,
parseAlembicRevision,
parseGiteaMergeSubject,
pickCurrentRelease,
todayReleaseBranch,
@@ -39,6 +40,10 @@ function commit(cwd: string, file: string, content: string, msg: string): string
git(cwd, ['commit', '-m', msg])
return git(cwd, ['rev-parse', 'HEAD'])
}
function migration(cwd: string, file: string, rev: string, down: string | null, msg: string): string {
mkdirSync(path.dirname(path.join(cwd, file)), { recursive: true })
return commit(cwd, file, `revision = '${rev}'\ndown_revision = ${down === null ? 'None' : `'${down}'`}\n`, msg)
}
let fx: Fx | undefined
afterEach(() => {
if (fx)
@@ -66,6 +71,13 @@ describe('纯函数', () => {
})
expect(parseGiteaMergeSubject('fix: no merge')).toBeUndefined()
})
it('parseAlembicRevision 认裸赋值、带类型标注、元组和 None', () => {
expect(parseAlembicRevision('revision = \'a1\'\ndown_revision = \'a0\'\n')).toEqual({ revision: 'a1', downRevisions: ['a0'] })
expect(parseAlembicRevision('revision: str = "a1"\ndown_revision: Union[str, None] = "a0"\nbranch_labels = None\n')).toEqual({ revision: 'a1', downRevisions: ['a0'] })
expect(parseAlembicRevision('revision = \'m\'\ndown_revision = (\'a\', \'b\')\n')).toEqual({ revision: 'm', downRevisions: ['a', 'b'] })
expect(parseAlembicRevision('revision: str = \'root\'\ndown_revision: Union[str, None] = None\n')).toEqual({ revision: 'root', downRevisions: [] })
expect(parseAlembicRevision('print(1)\n')).toBeUndefined()
})
it('groupCommitsByPr 按 Gitea 给的 pr 号分组,说明里没有 Closes 也能归组', () => {
const groups = groupCommitsByPr([
{ sha: '3e519ea', subject: 'fix(admin): 会员档位隐藏', author: 'chehongweo', date: '2026-08-31', body: '', pr: 260, prTitle: '连续包月会员订阅体系' },
@@ -237,4 +249,37 @@ describe('集成', () => {
const list = await listUnreleasedCommits({ cwd: fx.repo, devBranch: 'main', current: 'release/2026-08-31' })
expect(list.map(c => c.subject)).toEqual(['fix(jobs): 一次性命令收口'])
})
it('带迁移的提交:前任在发布分支上才能摘,前任缺失的标不可摘,不带迁移的不打标', async () => {
fx = await setup()
migration(fx.peer, 'alembic/versions/0001_init.py', '0001', null, 'feat: init schema')
git(fx.peer, ['push', 'origin', 'main'])
await cutRelease({ cwd: fx.repo, devBranch: 'main', branch: 'release/2026-08-31' })
migration(fx.peer, 'alembic/versions/0002_x.py', '0002', '0001', 'feat: m2')
migration(fx.peer, 'alembic/versions/0003_y.py', '0003', '0002', 'feat: m3')
commit(fx.peer, 'b.txt', 'b', 'fix: plain')
git(fx.peer, ['push', 'origin', 'main'])
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(['fix: plain', 'feat: m3', 'feat: m2'])
expect(list[0].migration).toBeUndefined()
expect(list[1].migration?.files).toEqual(['alembic/versions/0003_y.py'])
expect(list[1].migration?.blocked).toContain('0002')
expect(list[2].migration).toEqual({ files: ['alembic/versions/0002_x.py'] })
})
it('发布分支上同一前任已有后继 → 多 head,不可摘', async () => {
fx = await setup()
migration(fx.peer, 'alembic/versions/0001_init.py', '0001', null, 'feat: init schema')
git(fx.peer, ['push', 'origin', 'main'])
await cutRelease({ cwd: fx.repo, devBranch: 'main', branch: 'release/2026-08-31' })
migration(fx.peer, 'alembic/versions/0002_x.py', '0002', '0001', 'feat: m2')
git(fx.peer, ['push', 'origin', 'main'])
git(fx.peer, ['fetch'])
git(fx.peer, ['checkout', '-b', 'release/2026-08-31', 'origin/release/2026-08-31'])
migration(fx.peer, 'alembic/versions/0002_hotfix.py', '0002b', '0001', 'fix: hotfix schema')
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: m2'])
expect(list[0].migration?.blocked).toContain('多个 head')
})
})