✨ feat(vscode): 摘修复识别 alembic 迁移,接不上发布分支链的高亮禁用不可摘
按 writing-alembic-migrations 的规矩判断:迁移的前任必须已在发布分支上, 且发布分支上该前任不能已有别的后继;根迁移接不上已有链、删迁移会断链也不可摘。 列表里带迁移的提交打「迁移」角标,不可摘的高亮警告色、禁用勾选并写明原因; 确认接口再校验一遍,绕过前端也摘不进去。 Claude-Session: https://claude.ai/code/session_01QWxLb6X8Vd2G5vHnp6R7qm
This commit is contained in:
1 parent
df0660cff7
commit
028195ab2b
8 files changed
+261
-26
No files matched your search
@@ -1,7 +1,7 @@
|
||||
import type { ReleaseCommitGroup } from '../lib/messages'
|
||||
import { ChevronRight, X } from 'lucide-react'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
import { filterCherryPickGroups, groupContainingSha, uniqueAuthors } from '../lib/cherryPickView'
|
||||
import { filterCherryPickGroups, groupContainingSha, pickableShas, uniqueAuthors } from '../lib/cherryPickView'
|
||||
|
||||
interface Props {
|
||||
open: boolean
|
||||
@@ -78,7 +78,7 @@ export function CherryPickModal({ open, branch, groups, firstAfterReleaseSha, on
|
||||
}
|
||||
|
||||
function toggleGroupSelected(g: ReleaseCommitGroup): void {
|
||||
const shas = g.commits.map(c => c.sha)
|
||||
const shas = pickableShas(g.commits)
|
||||
const allOn = shas.every(s => selected.has(s))
|
||||
setSelected((prev) => {
|
||||
const next = new Set(prev)
|
||||
@@ -150,7 +150,8 @@ export function CherryPickModal({ open, branch, groups, firstAfterReleaseSha, on
|
||||
: visible.map((g) => {
|
||||
const key = groupKey(g)
|
||||
const openGroup = query.trim() !== '' || expanded.has(key)
|
||||
const shas = g.commits.map(c => c.sha)
|
||||
const shas = pickableShas(g.commits)
|
||||
const blockedCount = g.commits.length - shas.length
|
||||
const allOn = shas.length > 0 && shas.every(s => selected.has(s))
|
||||
const someOn = shas.some(s => selected.has(s))
|
||||
return (
|
||||
@@ -171,11 +172,13 @@ export function CherryPickModal({ open, branch, groups, firstAfterReleaseSha, on
|
||||
</span>
|
||||
<span className="shrink-0 text-[11px] opacity-60">
|
||||
{`${g.authors.join(' · ')} · ${g.commits.length} 条`}
|
||||
{blockedCount > 0 ? ` · ${blockedCount} 条带迁移不可摘` : ''}
|
||||
</span>
|
||||
</button>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={allOn}
|
||||
disabled={shas.length === 0}
|
||||
ref={(el) => {
|
||||
if (el)
|
||||
el.indeterminate = someOn && !allOn
|
||||
@@ -187,25 +190,47 @@ export function CherryPickModal({ open, branch, groups, firstAfterReleaseSha, on
|
||||
</div>
|
||||
{openGroup && (
|
||||
<ul className="border-t border-[var(--vscode-panel-border)] py-1">
|
||||
{g.commits.map(c => (
|
||||
<li key={c.sha}>
|
||||
<label className={`flex cursor-pointer items-start gap-2 px-3 py-1.5 hover:bg-white/5 ${c.sha === firstAfterReleaseSha ? 'bg-[var(--vscode-editor-findMatchHighlightBackground)]' : ''}`}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selected.has(c.sha)}
|
||||
onChange={() => toggleSha(c.sha)}
|
||||
className="mt-0.5"
|
||||
/>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block truncate">{c.subject}</span>
|
||||
<span className="block text-[11px] opacity-60">
|
||||
{`${c.sha.slice(0, 7)} · ${c.author} · ${c.date}`}
|
||||
{c.sha === firstAfterReleaseSha ? ' · 发布点后第一条' : ''}
|
||||
{g.commits.map((c) => {
|
||||
const blocked = c.migration?.blocked
|
||||
const rowBg = blocked
|
||||
? 'bg-[var(--vscode-inputValidation-warningBackground)]'
|
||||
: c.sha === firstAfterReleaseSha ? 'bg-[var(--vscode-editor-findMatchHighlightBackground)]' : ''
|
||||
return (
|
||||
<li key={c.sha}>
|
||||
<label className={`flex items-start gap-2 px-3 py-1.5 ${blocked ? 'cursor-not-allowed' : 'cursor-pointer hover:bg-white/5'} ${rowBg}`}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selected.has(c.sha)}
|
||||
disabled={!!blocked}
|
||||
onChange={() => toggleSha(c.sha)}
|
||||
className="mt-0.5"
|
||||
/>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="flex min-w-0 items-center gap-1.5">
|
||||
{c.migration && (
|
||||
<span
|
||||
title={c.migration.files.join('\n')}
|
||||
className="shrink-0 rounded border border-current px-1 text-[10px] leading-4 opacity-80"
|
||||
>
|
||||
迁移
|
||||
</span>
|
||||
)}
|
||||
<span className="min-w-0 flex-1 truncate">{c.subject}</span>
|
||||
</span>
|
||||
<span className="block text-[11px] opacity-60">
|
||||
{`${c.sha.slice(0, 7)} · ${c.author} · ${c.date}`}
|
||||
{c.sha === firstAfterReleaseSha ? ' · 发布点后第一条' : ''}
|
||||
</span>
|
||||
{blocked && (
|
||||
<span className="block text-[11px] text-[var(--vscode-editorWarning-foreground)]">
|
||||
{`不可摘:${blocked}`}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
</li>
|
||||
))}
|
||||
</label>
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
</section>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ReleaseCommitGroup } from './messages'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { filterCherryPickGroups, uniqueAuthors } from './cherryPickView'
|
||||
import { filterCherryPickGroups, pickableShas, uniqueAuthors } from './cherryPickView'
|
||||
|
||||
const groups: ReleaseCommitGroup[] = [
|
||||
{
|
||||
@@ -35,3 +35,14 @@ describe('uniqueAuthors', () => {
|
||||
expect(uniqueAuthors(groups)).toEqual(['cruldra', 'chehongweo'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('pickableShas', () => {
|
||||
it('带不兼容迁移的提交不能勾;兼容的迁移照常', () => {
|
||||
const commits = [
|
||||
{ sha: 'a', subject: 'ok', author: 'x', date: '', body: '' },
|
||||
{ sha: 'b', subject: 'mig ok', author: 'x', date: '', body: '', migration: { files: ['alembic/versions/0002.py'] } },
|
||||
{ sha: 'c', subject: 'mig bad', author: 'x', date: '', body: '', migration: { files: ['alembic/versions/0003.py'], blocked: '上游迁移 0002 不在 release/x' } },
|
||||
]
|
||||
expect(pickableShas(commits)).toEqual(['a', 'b'])
|
||||
})
|
||||
})
|
||||
@@ -1,4 +1,9 @@
|
||||
import type { ReleaseCommitGroup } from './messages'
|
||||
import type { ReleaseCommit, ReleaseCommitGroup } from './messages'
|
||||
|
||||
/** 能勾选的提交:带不兼容迁移的不算 */
|
||||
export function pickableShas(commits: ReleaseCommit[]): string[] {
|
||||
return commits.filter(c => !c.migration?.blocked).map(c => c.sha)
|
||||
}
|
||||
|
||||
export function uniqueAuthors(groups: ReleaseCommitGroup[]): string[] {
|
||||
const out: string[] = []
|
||||
|
||||
@@ -29,6 +29,8 @@ export interface ReleaseCommit {
|
||||
author: string
|
||||
date: string
|
||||
body: string
|
||||
/** 带 alembic 迁移文件时才有;blocked 有值就不能摘 */
|
||||
migration?: { files: string[], blocked?: string }
|
||||
}
|
||||
|
||||
export interface ReleaseCommitGroup {
|
||||
|
||||
Reference in new issue
Block a user