✨ feat(vscode): 摘修复可按作者筛选,高亮发布点后第一条
This commit is contained in:
1 parent
54fc2ead4c
commit
beb7750eac
9 files changed
+137
-33
No files matched your search
@@ -501,6 +501,7 @@ export function App() {
|
||||
open={cherryPick !== null}
|
||||
branch={cherryPick?.branch ?? ''}
|
||||
groups={cherryPick?.groups ?? []}
|
||||
firstAfterReleaseSha={cherryPick?.firstAfterReleaseSha}
|
||||
onCancel={dismissCherryPick}
|
||||
onConfirm={confirmCherryPick}
|
||||
/>
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
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'
|
||||
|
||||
interface Props {
|
||||
open: boolean
|
||||
branch: string
|
||||
groups: ReleaseCommitGroup[]
|
||||
firstAfterReleaseSha?: string
|
||||
onCancel: () => void
|
||||
onConfirm: (shas: string[]) => void
|
||||
}
|
||||
@@ -14,38 +16,25 @@ function groupKey(g: ReleaseCommitGroup): string {
|
||||
return g.pr != null ? `pr-${g.pr}` : 'ungrouped'
|
||||
}
|
||||
|
||||
function filterGroups(groups: ReleaseCommitGroup[], query: string): ReleaseCommitGroup[] {
|
||||
const q = query.trim().toLowerCase()
|
||||
if (!q)
|
||||
return groups
|
||||
const out: ReleaseCommitGroup[] = []
|
||||
for (const g of groups) {
|
||||
const head = `#${g.pr ?? ''} ${g.title} ${g.authors.join(' ')}`.toLowerCase()
|
||||
if (head.includes(q)) {
|
||||
out.push(g)
|
||||
continue
|
||||
}
|
||||
const commits = g.commits.filter(c =>
|
||||
`${c.subject} ${c.author} ${c.sha} ${c.date}`.toLowerCase().includes(q),
|
||||
)
|
||||
if (commits.length > 0)
|
||||
out.push({ ...g, commits })
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
export function CherryPickModal({ open, branch, groups, onCancel, onConfirm }: Props) {
|
||||
export function CherryPickModal({ open, branch, groups, firstAfterReleaseSha, onCancel, onConfirm }: Props) {
|
||||
const authors = useMemo(() => uniqueAuthors(groups), [groups])
|
||||
const [query, setQuery] = useState('')
|
||||
const [selected, setSelected] = useState<Set<string>>(() => new Set())
|
||||
const [expanded, setExpanded] = useState<Set<string>>(() => new Set())
|
||||
const [enabledAuthors, setEnabledAuthors] = useState<Set<string>>(() => new Set())
|
||||
|
||||
useEffect(() => {
|
||||
if (!open)
|
||||
return
|
||||
setQuery('')
|
||||
setSelected(new Set())
|
||||
setExpanded(new Set(groups.filter(g => g.pr == null).map(groupKey)))
|
||||
}, [open, groups])
|
||||
setEnabledAuthors(new Set(uniqueAuthors(groups)))
|
||||
const keys = new Set(groups.filter(g => g.pr == null).map(groupKey))
|
||||
const marked = groupContainingSha(groups, firstAfterReleaseSha)
|
||||
if (marked)
|
||||
keys.add(groupKey(marked))
|
||||
setExpanded(keys)
|
||||
}, [open, groups, firstAfterReleaseSha])
|
||||
|
||||
useEffect(() => {
|
||||
if (!open)
|
||||
@@ -58,7 +47,21 @@ export function CherryPickModal({ open, branch, groups, onCancel, onConfirm }: P
|
||||
return () => document.removeEventListener('keydown', onKey)
|
||||
}, [open, onCancel])
|
||||
|
||||
const visible = useMemo(() => filterGroups(groups, query), [groups, query])
|
||||
const visible = useMemo(
|
||||
() => filterCherryPickGroups(groups, query, enabledAuthors),
|
||||
[groups, query, enabledAuthors],
|
||||
)
|
||||
|
||||
function toggleAuthor(name: string): void {
|
||||
setEnabledAuthors((prev) => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(name))
|
||||
next.delete(name)
|
||||
else
|
||||
next.add(name)
|
||||
return next
|
||||
})
|
||||
}
|
||||
|
||||
if (!open)
|
||||
return null
|
||||
@@ -119,13 +122,27 @@ export function CherryPickModal({ open, branch, groups, onCancel, onConfirm }: P
|
||||
<X className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
<div className="px-4 pt-3">
|
||||
<div className="space-y-2 px-4 pt-3">
|
||||
<input
|
||||
value={query}
|
||||
onChange={e => setQuery(e.target.value)}
|
||||
placeholder="搜索作者、PR 号或标题"
|
||||
placeholder="搜索 PR 号或标题"
|
||||
className="w-full rounded border border-[var(--vscode-input-border,transparent)] bg-[var(--vscode-input-background)] px-2 py-1.5 text-xs outline-none"
|
||||
/>
|
||||
{authors.length > 0 && (
|
||||
<div className="flex flex-wrap gap-x-3 gap-y-1">
|
||||
{authors.map(name => (
|
||||
<label key={name} className="inline-flex cursor-pointer items-center gap-1 text-xs">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={enabledAuthors.has(name)}
|
||||
onChange={() => toggleAuthor(name)}
|
||||
/>
|
||||
<span className="opacity-80">{name}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="min-h-0 flex-1 space-y-2 overflow-y-auto px-4 py-3">
|
||||
{visible.length === 0
|
||||
@@ -172,7 +189,7 @@ export function CherryPickModal({ open, branch, groups, onCancel, onConfirm }: P
|
||||
<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">
|
||||
<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)}
|
||||
@@ -183,6 +200,7 @@ export function CherryPickModal({ open, branch, groups, onCancel, onConfirm }: P
|
||||
<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 ? ' · 发布点后第一条' : ''}
|
||||
</span>
|
||||
</span>
|
||||
</label>
|
||||
|
||||
@@ -249,7 +249,7 @@ export interface UseIssuesResult {
|
||||
/** 发布按钮三个动作;running 由扩展侧状态决定,webview 不自置。 */
|
||||
runRelease: (action: ReleaseAction) => void
|
||||
/** 摘修复选单:扩展推来分组后的未发布提交。 */
|
||||
cherryPick: { branch: string, groups: ReleaseCommitGroup[] } | null
|
||||
cherryPick: { branch: string, groups: ReleaseCommitGroup[], firstAfterReleaseSha?: string } | null
|
||||
dismissCherryPick: () => void
|
||||
confirmCherryPick: (shas: string[]) => void
|
||||
/**
|
||||
@@ -342,7 +342,7 @@ export function useIssues(): UseIssuesResult {
|
||||
const [commitRunning, setCommitRunning] = useState<boolean>(false)
|
||||
const [hasChanges, setHasChanges] = useState<boolean>(false)
|
||||
const [release, setRelease] = useState<ReleaseStatus>()
|
||||
const [cherryPick, setCherryPick] = useState<{ branch: string, groups: ReleaseCommitGroup[] } | null>(null)
|
||||
const [cherryPick, setCherryPick] = useState<{ branch: string, groups: ReleaseCommitGroup[], firstAfterReleaseSha?: string } | null>(null)
|
||||
const [envLocked, setEnvLocked] = useState<boolean>(false)
|
||||
const [envFileCount, setEnvFileCount] = useState<number>(0)
|
||||
const [envLockRunning, setEnvLockRunning] = useState<boolean>(false)
|
||||
@@ -864,7 +864,7 @@ export function useIssues(): UseIssuesResult {
|
||||
setRelease(msg)
|
||||
break
|
||||
case 'release/pick-show':
|
||||
setCherryPick({ branch: msg.branch, groups: msg.groups })
|
||||
setCherryPick({ branch: msg.branch, groups: msg.groups, firstAfterReleaseSha: msg.firstAfterReleaseSha })
|
||||
break
|
||||
case 'env-lock/status': {
|
||||
setEnvLocked(msg.locked)
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { ReleaseCommitGroup } from './messages'
|
||||
import { filterCherryPickGroups, uniqueAuthors } from './cherryPickView'
|
||||
|
||||
const groups: ReleaseCommitGroup[] = [
|
||||
{
|
||||
title: '未关联 PR',
|
||||
authors: ['cruldra'],
|
||||
commits: [
|
||||
{ sha: 'a', subject: 'chore: lone', author: 'cruldra', date: '2026-09-01', body: '' },
|
||||
],
|
||||
},
|
||||
{
|
||||
pr: 260,
|
||||
title: '连续包月',
|
||||
authors: ['chehongweo', 'cruldra'],
|
||||
commits: [
|
||||
{ sha: 'b', subject: 'fix: admin', author: 'chehongweo', date: '2026-08-31', body: '' },
|
||||
{ sha: 'c', subject: 'fix: membership', author: 'cruldra', date: '2026-08-30', body: '' },
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
describe('filterCherryPickGroups', () => {
|
||||
it('只勾选的作者才出现', () => {
|
||||
const out = filterCherryPickGroups(groups, '', new Set(['chehongweo']))
|
||||
expect(out).toHaveLength(1)
|
||||
expect(out[0].pr).toBe(260)
|
||||
expect(out[0].commits.map(c => c.sha)).toEqual(['b'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('uniqueAuthors', () => {
|
||||
it('按出现顺序去重', () => {
|
||||
expect(uniqueAuthors(groups)).toEqual(['cruldra', 'chehongweo'])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,47 @@
|
||||
import type { ReleaseCommitGroup } from './messages'
|
||||
|
||||
export function uniqueAuthors(groups: ReleaseCommitGroup[]): string[] {
|
||||
const out: string[] = []
|
||||
for (const g of groups) {
|
||||
for (const a of g.authors) {
|
||||
if (!out.includes(a))
|
||||
out.push(a)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
export function filterCherryPickGroups(
|
||||
groups: ReleaseCommitGroup[],
|
||||
query: string,
|
||||
authors: ReadonlySet<string>,
|
||||
): ReleaseCommitGroup[] {
|
||||
const q = query.trim().toLowerCase()
|
||||
const out: ReleaseCommitGroup[] = []
|
||||
for (const g of groups) {
|
||||
const commits = g.commits.filter(c => authors.has(c.author))
|
||||
if (commits.length === 0)
|
||||
continue
|
||||
if (!q) {
|
||||
out.push({ ...g, authors: uniqueAuthors([{ ...g, commits }]), commits })
|
||||
continue
|
||||
}
|
||||
const head = `#${g.pr ?? ''} ${g.title} ${g.authors.join(' ')}`.toLowerCase()
|
||||
if (head.includes(q)) {
|
||||
out.push({ ...g, authors: uniqueAuthors([{ ...g, commits }]), commits })
|
||||
continue
|
||||
}
|
||||
const hit = commits.filter(c =>
|
||||
`${c.subject} ${c.author} ${c.sha} ${c.date}`.toLowerCase().includes(q),
|
||||
)
|
||||
if (hit.length > 0)
|
||||
out.push({ ...g, authors: uniqueAuthors([{ ...g, commits: hit }]), commits: hit })
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
export function groupContainingSha(groups: ReleaseCommitGroup[], sha: string | undefined): ReleaseCommitGroup | undefined {
|
||||
if (!sha)
|
||||
return undefined
|
||||
return groups.find(g => g.commits.some(c => c.sha === sha))
|
||||
}
|
||||
@@ -153,7 +153,7 @@ export type ExtensionToWebview
|
||||
running: boolean
|
||||
jenkinsConfigured: boolean
|
||||
}
|
||||
| { type: 'release/pick-show', branch: string, groups: ReleaseCommitGroup[] }
|
||||
| { type: 'release/pick-show', branch: string, groups: ReleaseCommitGroup[], firstAfterReleaseSha?: string }
|
||||
| { type: 'env-lock/status', locked: boolean, fileCount: number, failedCount?: number }
|
||||
| { type: 'issue/remove', issueNumber: number }
|
||||
| { type: 'profiles/show', data: ProfilesData }
|
||||
|
||||
Reference in new issue
Block a user