Skip to content
Clone
b868469
58 lines 16.6 KB Markdown

pulls · reviews · protection(含 branch_protection / tag_protection)

结论

  • 检查调用点 54 个 / 操作 54 个(pulls 35、reviews 8、protection 5、branch_protection 3、tag_protection 3;每操作恰好 1 处调用,集中在 view/app/src/domains/pulls/model/api.ts 43 处与 view/app/src/domains/repo/model/protection.ts 11 处)
  • 确认问题 1 条(Q 0 / B 0 / P 0 / R 1 / G 0),可疑 4 条,未实证 51 条
  • 实证面:pulls.list_pulls、pulls.list_label_options 拿到 200;pulls.list_pull_comments 拿到 400(query 字段面)。其余 51 条因需登录会话(写操作 / require_repo_admin)且 demo/demorepos 无任何 PR 数据,未能取得有效响应
  • 关键结论:本域 query 与 body 的字段名全部与后端一致(ListPullQuery 的 10 个字段、CreatePullCommentCmd 的 7 个字段、保护规则 15 个字段、whitelist 3 个字段均逐一对齐);唯一确认的错配是响应形状(R)。

确认问题

#类位置操作现象后端依据建议修法
1Rview/app/src/domains/pulls/model/api.ts:145-146(调用点 view/app/src/domains/pulls/ui/pull-detail.tsx:481)pulls.update_branch_pull(POST …/pulls/{pull_ref}/update-branch)前端用 oneOf<Pull> 解包,按 PullDto 使用返回值(res.title/res.state/res.number)。实际 data 是 {pull, result},顶层没有这些字段。当前调用点只 await 不读返回值,所以尚未暴露;一旦有人读返回值即拿到 undefinedlib/service/pulls/src/handlers/pulls/update_branch_pull.rs:13-16 pub struct UpdateBranchDto { pull: PullDto, result: MergeResultDto };同文件 :25 body = AppResponseData<UpdateBranchDto>;生成类型 packages/apiclient/pulls/update-branch-pull.schemas.ts EnvelopeUpdateBranchDtoData = { pull: PullDto; result: MergeResultDto }改为 oneOf<NonNullable<EnvelopeUpdateBranchDto['data']>> 并在调用点取 res.pull;或让函数直接 return unwrapApi(...).then(r => r.pull)

可疑(需人工确认)

#位置操作疑点已排除的可能
1view/app/src/domains/pulls/model/api.ts:96-110(尤其 :105),触发点 view/app/src/domains/pulls/model/filters.tsx:10-41 的 all 选项 + pulls-tab.tsx:74pulls.list_my_pulls(GET /pulls/@me/{scope})收件箱把 state 写成 opts.state !== 'all' && opts.state !== 'draft' ? opts.state : 'open',即 all 被折成 open。后端 list_inbox 明确支持 "all" => base(不加状态过滤),因此“全部”页签静默丢掉 closed/merged 的 PR;draft 有客户端补偿过滤(pulls-tab.tsx:78 pull.is_draft),all 没有不是 400 名称不符——all 是后端合法值(lib/service/pulls/src/services/inbox.rs:68-77 的 match 分支);也不是注释里说明的 draft 映射(注释只解释了 draft)
2view/app/src/domains/repo/model/protection.ts:1-6(注释)、:101-121tag_protection.create_tag_policy / get_tag_policy / update_tag_policy / delete_tag_policy / list_tag_policies领域层注释断言“tag policies themselves have no HTTP surface yet — whitelist rows can only be managed against a known policy UUID”,据此 TagProtectionPanel 要求用户手抄 policy UUID(protection-settings.tsx:419-424 的 tagPolicyIdPlaceholder 输入框)。实际后端五个 tag 策略 CRUD 端点齐全,且 OPS 表也已注册不是幽灵端点:路由在 lib/service/git/src/routes.rs:310-319(/tag-protection、/tag-protection/{policy_id}),handler 在 lib/service/git/src/handler/tag_protection/*,OPS 条目在 view/app/src/services/api/ops.ts:588-604。属“后端有能力、前端未接”的覆盖缺口,注释已过时
3view/app/src/domains/repo/model/protection.ts:57-67protection.update_policy(PATCH …/branch-protection/{policy_id})updatePolicy 无条件把 protected_file_patterns / unprotected_file_patterns 补成 null(cmd.x ?? null),被当成“清空”语义发送。后端 UpdateBranchPolicyCmd 这两个字段是 Option<String> 而非 Option<Option<String>>,null 与“缺省”在反序列化后同为 None,apply_flags 走 if let Some(v) 直接跳过 → 发 null 等于不变。文档宣称的三态 PATCH(README/envelope 注释“field null → cleared”)在本端点不成立不是 B 类字段名错配——字段名前后端与生成类型完全一致;也不是当前就会踩的 bug:UI 清空走的是空串 ''('' ?? null = ''),由 apply_flags 真正写空
4view/app/src/domains/repo/settings/ui/sections/protection-settings.tsx:48(label 取 repo.protection.field.enableForcePush)、i18n zh-CN/repo.ts:437 = “禁止强推(force push)”protection.create_policy / update_policy 的 enable_force_push字段名是 enable_force_push(“启用强推”),UI 文案与其 chip 逻辑(policy.enable_force_push ? '禁止强推' : null,protection-settings.tsx:226)却按“该位为真=已禁止强推”呈现。若 KenShell 侧语义是“允许强推”,则勾选“禁止强推”会反向打开强推不是字段名/类型错配——proto/kenshell/v1/ref.proto:91 确有 bool enable_force_push = 14,前后端与生成类型一致。语义无法在本仓库取证:执行侧在 KenShell,proto 该字段无注释,lib/** 内除 handler 外无消费点

已核对无问题

操作依据
pulls.list_pulls前端 api.ts:79-91 发 state/draft/author/base/head/search/sort/order/limit/offset;后端 ListPullQuery(lib/service/pulls/src/services/pulls.rs:99-120,:100 #[serde(deny_unknown_fields)])字段逐一对应,无多无少。实证 200:curl 'http://localhost:7051/api/v1/repos/demo/demorepos/pulls?state=all&draft=false&sort=created&order=desc&limit=20&offset=0' → {"code":200,...,"limit":20,"offset":0}。另实证前端 state:'draft'→'open' 的映射是必需的:?state=draft → 400 {"code":400,"message":"unknown state \draft`",...,"error_code":"bad_request"}`
pulls.list_pulls 未知字段实证 400 原文点名允许集,证明“未知 query 是 400 不是忽略”:{"code":400,"message":"invalid query parameters","data":null,"error":"Query deserialize error: unknown field `bogus`, expected one of `state`, `author`, `base`, `head`, `draft`, `search`, `sort`, `order`, `limit`, `offset`","error_code":null}
pulls.list_pulls 的 label/milestone该端点不接受标签与里程碑过滤,实证:?label=bug → 400 Query deserialize error: unknown field 'label'。前端当前未发这两项(若后续加筛选必须改后端)。sort 只接受 number|created|updated(?sort=bogus → 400 unknown variant 'bogus', expected one of 'number','created','updated'),与生成类型 PullSort 一致
pulls.list_pulls 分页边界limit=0 → 400 limit must be between 1 and 100;offset=5001 → 400 offset must not exceed 5000。前端 pulls-tab.tsx:14 PAGE_SIZE = 20、limit: 50(收件箱)均在 1..=100 内
pulls.list_my_pulls 路径与 query路径 /pulls/@me/{scope}(lib/service/pulls/src/routes.rs:118)与生成 URL 一致;scope 取值 created|review-requested|reviewed 与 InboxPath 文档 + :41-49 的 match 分支完全一致;query 仅 state/limit/offset,与 InboxQuery(services/inbox.rs:22-32)一致
pulls.list_pull_comments / list_pull_events 的 query后端均用 PageQuery(lib/infra/api/src/pagination.rs:10-15,#[serde(deny_unknown_fields)],仅 limit/offset),与生成 ListPullCommentsParams/ListPullEventsParams 一致。实证:…/pulls/1/comments?bogus=1 → 400 Query deserialize error: unknown field \bogus`, expected `limit` or `offset`;不带未知字段则越过 query 校验进入业务(…?limit=1→ 404pull request not found`,因 demo 仓库无 PR)
pulls.create_pullapi.ts:118-125 发 title/head_branch/base_branch/body/draft/issue_id;CreatePullCmd(services/pulls.rs:122-132)字段名与类型一致,issue_id:null→None 合法
pulls.update_pull(PATCH 三态)api.ts:130 直传 PullPatchCmd;UpdatePullCmd(services/pulls.rs:134-149)对 body/auto_merge/issue_id 用 deserialize_opt_opt,缺省=不变、null=清空、值=设置。前端 pull-detail.tsx:638 的 body 恒为字符串(清空发 ""),title: undefined 会被 JSON 丢弃 → 均落在预期语义
pulls.close_pull / reopen_pull后端 POST…/close、POST…/reopen 返回 PullDto(close_pull.rs:18、reopen_pull.rs:18),与前端 oneOf<Pull> 一致
pulls.merge_pull路径 {ITEM}/merge + POST;body MergePullCmd(lib/service/pulls/src/services/merge.rs:14-30)6 个字段与前端 MergeCmd 一致;method 枚举 MergeMethod = "merge"|"squash"|"rebase"(snake_case),与 models::pull_request::pull_requests::MergeMethod 的序列化一致;响应 MergePullDto{pull,result,summary} 与前端 MergeOutcome 一致
pulls.lock_pull / unlock_pullPUT …/lock 与 DELETE …/lock(routes.rs:60),两个生成客户端 URL 相同、方法不同,OPS 表分别映射正确;响应被 ignore() 丢弃,不受形状影响
pulls.create_pull_comment(行内评论)api.ts:194-205 发 body/parent_comment_id/path/line/side/commit_sha/review_id;CreatePullCommentCmd(lib/service/pulls/src/services/comments.rs:25-41)7 个字段名与类型逐一对应,line: i32 vs 前端 number、side: Option<String> 自由值 left|right 一致。files-tab.tsx:88 的 commit_sha: headSha 取自 pull-detail.tsx:234 headSha={pull.head.sha}(是 sha 不是 branch/label),未误用
pulls.update_pull_comment / delete / resolve / unresolve路径 {ITEM}/comments/{comment_id}(PATCH/DELETE)与 …/resolve(PUT/DELETE);CommentPath(handlers/comments/update_pull_comment.rs:11-17)段序 space_name/repo_name/pull_ref/comment_id 与前端 {...pathOf(ref), comment_id} 一致;UpdatePullCommentCmd{body:String} 与前端 {body} 一致
pulls.list_pull_comment_tree返回 Vec<PullCommentThreadDto>(list_pull_comment_tree.rs:18),前端按数组解包(listOf),PullCommentThread = {root, replies},conversation.tsx:64/218/282 正好读 root/replies
pulls.list_pull_commits / get_pull_files均为数组(Vec<MergeCommitDto> / Vec<ChangedPathDiffDto>),前端 listOf;commits-tab.tsx:34-58 读 sha/subject/author_name/author_email/author_at/parents 与 MergeCommitDto 一致;model/meta.ts:137-145 正确把 file.path.new_path/old_path 与 lines_added/removed 映射出来(未把 path 对象当字符串用)
pulls.get_pull_checks / get_pull_gatePullChecksDto{head_sha,statuses}、PullGateDto{head_sha,mergeable,conflicts,gate,pending_review_requests};pull-detail.tsx:458/469 读 g.required_approvals/g.approvals,二者确实在 MergeGateDto(lib/service/pulls/src/services/approvals.rs:17-26)内
pulls.refresh_pull_summary后端 POST {ITEM}/summary 返回 PullSummaryDto(refresh_pull_summary.rs:19),前端 oneOf<PullSummary> 对应 EnvelopePullSummaryDto['data']
pulls.list_pull_events 响应后端 Page<PullEventDto>(services/events.rs:37-47),前端手写的 PullEvent(pull.ts:72-80)字段与 PullEventDto 一一对应(id/pull_request_id/event_type/actor/commit_sha/content/created_at)
pulls.list_pull_assignees / get_pull 等单对象端点返回 PullDto/Vec<PullAssigneeDto>,前端 oneOf/listOf 分支正确;pageOf 只用于真分页端点(list_pulls/list_my_pulls/comments/events)
pulls.add_pull_assignee / remove_pull_assignee路径 {ITEM}/assignees/{user_ref}(add_pull_assignee.rs:11-17,user_ref: String 接受用户名或 UUID),POST/DELETE 双方法;前端 pathOf+user_ref 段序一致
pulls.attach_pull_label / detach_pull_label / list_pull_labels / list_label_optionsAttachLabelCmd{label:String}(packages/apiclient/pulls/attach-pull-label.schemas.ts)与前端 {label} 一致;label_ref:String(detach_pull_label.rs:10-16);list_label_options 路由注册在 {ITEM} 之前(routes.rs:55-57),实证 GET /api/v1/repos/demo/demorepos/pulls/label-options → 200(未被 {pull_ref} 吞掉)
pulls.link_issue_pull / unlink_issue_pull / list_issue_pulls路径段顺序 issues/{space_name}/boards/{board_id}/issues/{issue_id}/pulls[/{relation_id}] 与生成 URL 完全一致;LinkIssueCmd{space_name,repo_name,pull_ref} 与前端 data 一致;board_id/relation_id 为 Uuid,前端传字符串 UUID
pulls.get_pull_patch前端不走 Apis,api.ts:163-164 手工拼 /api/v1/repos/{s}/{r}/pulls/{pull}/patch,与 routes.rs:58 的 {ITEM}/patch 一致(浏览器直连下载,不涉及信封)
reviews.submit_review / publish_reviewbody 均为 SubmitReviewCmd(services/reviews.rs:49-56):state: ReviewIntent(approve|request_changes|comment|pending,#[serde(rename_all="snake_case")] 见 :21-28)、body、comments: Vec<InlineDraft>;InlineDraft{path,line: i32,side,body} 与前端 {path,line,side,body} 一致。PATCH 语义不适用(POST 整体提交)
reviews.get_review / list_reviewsGET {ITEM}/reviews/{review_id} 单对象、GET {ITEM}/reviews 数组(list_reviews.rs:18),前端分别 oneOf/listOf;PullReviewDto 的正文叫 content(:65),前端 conversation.tsx:332 body: review.content ?? null 用的是正确字段名;ReviewState 取值 approved|changes_requested|commented|pending|dismissed 与 conversation.tsx:370/397 的判断一致
reviews.dismiss_reviewPUT {ITEM}/reviews/{review_id}/dismiss,ReviewPath{space_name,repo_name,pull_ref,review_id:Uuid} 段序与前端 pathOf+review_id 一致,无 body
reviews.request_reviewersRequestReviewersCmd(services/reviewers.rs:52-58):reviewers: Vec<String>、required: bool 且 #[serde(default = "default_required")] 默认 true。前端 api.ts:249 发 required: required ?? undefined,但唯一调用点 pull-detail.tsx:718/735 的 useState(true) 恒传布尔 → 与后端默认一致,未踩“缺省即计入必需审批”的坑
reviews.list_requested_reviewers / delete_requested_reviewer均返回/作用于 Vec<PullReviewerDto>;路径 …/requested-reviewers/{user_id},ReviewerPath.user_id: i64,前端传 number(:253-254)一致
protection.* 5 个操作(分支策略 CRUD)生成 URL /api/v1/repos/{s}/{r}/branch-protection[/{policy_id}](packages/apiclient/protection/*.ts)与 lib/service/git/src/routes.rs:291-299 完全一致(注意:openapi tag 名是 protection,路径前缀是 branch-protection,两者不冲突);body 15 个字段名与 CreateBranchPolicyCmd(services/protection.rs:101-131)、UpdateBranchPolicyCmd(:157-188)逐一对应,required_approvals 前后端同名(不存在 required_reviewers),生成类型 CreateBranchPolicyCmd/UpdateBranchPolicyCmd(packages/apiclient/protection/*.schemas.ts)亦一致;list_policies 返回数组(EnvelopeVecBranchPolicyDto)、get/create/update 返回单对象,前端 listPolicies 用数组分支、其余 oneOf,正确(未实证:require_repo_admin,匿名 401)
branch_protection.* 3 个操作路径 /branch-protection/{policy_id}/whitelists[/{whitelist_id}] 与 routes.rs:301-307 一致;AddBranchWhitelistCmd{kind, user_id: Option<i64>, group_id: Option<Uuid>}(services/whitelists.rs:49-53)与前端 data 一致,缺省字段反序列化为 None(serde 对 Option 缺省即 None),前端显式补 null 同样安全;WhitelistKind 取值 Push|Merge|Approval PascalCase(services/whitelists.rs:12-16,无 rename_all),前端 KIND_LABEL 与 useState<WhitelistKind>('Push')(protection-settings.tsx:28-36/292)完全吻合——虽与“枚举一律 snake_case”的约定不符,但前后端内部自洽,不构成错配
tag_protection.* 3 个操作(whitelist)路径 /tag-protection/{policy_id}/whitelists[/{user_id}] 与 routes.rs:321-326 一致;AddTagWhitelistCmd{user_id: i64}(services/whitelists.rs:63-66)与前端 {user_id: number} 一致,user_id 来自 Number(...) 而非字符串(protection-settings.tsx:430),不会触发 i64 反序列化失败