import { useState } from 'react' import { Download, FileCode2, GitMerge, ListChecks, MessagesSquare, Pencil, RotateCcw, Send, ShieldCheck, Unlock, X, } from 'lucide-react' import { Alert, Button, ErrorPanel, Modal, PanelNote, modalInputClass, modalLabelClass, modalTextareaClass } from '@/shared/ui' import { fmtRelative, fmtTime, repoKey, useAsync } from '@/shared/lib' import { fetchConflictDetail } from '@/entities/repo' import { addAssignee, attachLabel, closePull, deletePull, detachLabel, getPull, getPullGate, listAssignees, listPullLabels, listRequestedReviewers, lockPull, mergePull, pullPatchUrl, refreshPullSummary, removeAssignee, removeRequestedReviewer, reopenPull, requestReviewers, unlockPull, updateBranchPull, updatePull, } from '@/entities/pull-request' import type { MergeMethod, Pull, PullGate, PullRef, ReviewerState } from '@/entities/pull-request' import { MERGE_METHOD_LABEL, REVIEWER_STATE_LABEL, TONE, userName } from '../model/meta' import { Avatar, MergeableChip, PullStateBadge, SideSection } from './parts' import { Conversation } from './conversation' import { FilesTab } from './files-tab' import { CommitsTab } from './commits-tab' import { ChecksTab } from './checks-tab' export type PullSubTab = 'conversation' | 'files' | 'commits' | 'checks' export interface PullDetailProps { space: string repo: string pullRef: string ptab: PullSubTab onPtabChange: (next: PullSubTab) => void onBackToList: () => void onOpenCommit: (sha: string) => void } const SUB_TABS: Array<{ id: PullSubTab; label: string; icon: typeof MessagesSquare }> = [ { id: 'conversation', label: '会话', icon: MessagesSquare }, { id: 'files', label: '变更文件', icon: FileCode2 }, { id: 'commits', label: '提交', icon: GitMerge }, { id: 'checks', label: '检查', icon: ListChecks }, ] /** PR 详情页:头部/操作 + 子页签 + 侧栏。 */ export function PullDetail({ space, repo, pullRef, ptab, onPtabChange, onBackToList, onOpenCommit }: PullDetailProps) { const [tick, setTick] = useState(0) const [focusFile, setFocusFile] = useState('') const [notice, setNotice] = useState(null) const [error, setError] = useState(null) const refObj: PullRef = { space, repo, pull: pullRef } const pullState = useAsync(() => getPull(refObj), [space, repo, pullRef, tick], { cacheKey: repoKey(space, repo, `pull:${pullRef}:${tick}`), }) const gateState = useAsync(() => getPullGate(refObj).catch(() => null), [space, repo, pullRef, tick]) const pull = pullState.data const gate = gateState.data const failure = pullState.error as { code?: number; message?: string } | undefined const refresh = () => setTick(t => t + 1) async function run(label: string, fn: () => Promise) { setNotice(null) setError(null) try { await fn() setNotice(`${label}完成。`) refresh() } catch (f) { setError(`${label}失败:${f instanceof Error ? f.message : '未知错误'}`) } } if (pullState.loading && !pull) return if (!pull) { return failure ? ( ) : ( ) } return (
/ #{pull.number}

{pull.title}

{userName(pull.author)} 请求将{' '} {pull.head.branch} 合并到{' '} {pull.base.branch} ·更新于 {fmtRelative(pull.updated_at ?? pull.created_at)} {pull.locked ? 已锁定 : null} {pull.auto_merge ? 自动合并已开 : null}

{notice ? {notice} : null} {error ? {error} : null}
{SUB_TABS.map(tab => { const Icon = tab.icon return ( ) })}
{ptab === 'conversation' ? ( { setFocusFile(path) onPtabChange('files') }} /> ) : null} {ptab === 'files' ? ( ) : null} {ptab === 'commits' ? : null} {ptab === 'checks' ? : null}
{pull.state === 'open' ? : null}
{pull.state === 'open' ? ( ) : pull.state === 'closed' ? ( ) : null}
领先 / 落后
{pull.commits_ahead ?? '—'} / {pull.commits_behind ?? '—'}
文件变更
{pull.files_changed}
增删行
+{pull.additions} −{pull.deletions}
创建
{fmtRelative(pull.created_at)}
{pull.merged_at ?
合并于
{fmtRelative(pull.merged_at)}
: null} {pull.merged_commit_sha ?
合并提交
{pull.merged_commit_sha.slice(0, 10)}
: null}
) } /* ------------------------------------------------------------ merge box */ function MergeBox({ refObj, pull, gate, onChanged, onError }: { refObj: PullRef pull: Pull gate: PullGate | null | undefined onChanged: () => void onError: (message: string) => void }) { const [method, setMethod] = useState(pull.merge_method || 'merge') const [title, setTitle] = useState('') const [message, setMessage] = useState('') const [deleteAfter, setDeleteAfter] = useState(false) const [busy, setBusy] = useState(false) const g = gate?.gate const conflicts = gate?.conflicts ?? pull.conflicts ?? [] const blocked = !g?.allowed && g !== null && g !== undefined const stale = (pull.commits_behind ?? 0) > 0 async function merge() { setBusy(true) try { const outcome = await mergePull(refObj, { method, title: title.trim() || null, message: message.trim() || null, delete_branch_after_merge: deleteAfter, expect_head_sha: pull.head.sha, }) onChanged() window.alert(`合并成功:${outcome.result.new_sha.slice(0, 10)}`) } catch (f) { onError(`合并失败:${f instanceof Error ? f.message : '未知错误'}`) } finally { setBusy(false) } } return ( {pull.is_draft ? (

这是草稿 PR——点击右上角「编辑」将其转为正式后再评审合并。

) : conflicts.length ? (
存在 {conflicts.length} 个冲突文件,无法直接合并:
    {conflicts.map(path => (
  • ))}
) : g && !g.allowed ? (

被保护规则拦截:{g.reasons.join(';') || `需要 ${g.required_approvals} 个批准(当前 ${g.approvals})`} {g.bypassed ? '(你可绕过)' : ''}

) : stale ? (

源分支落后于目标分支 {pull.commits_behind} 个提交。

) : (

满足合并条件{g ? `(批准 ${g.approvals}/${g.required_approvals})` : ''}。

)} {g?.outdated && stale ? ( ) : null}
setTitle(event.target.value)} />