Skip to content
Clone
b868469
73 lines 16.7 KB Markdown

git-misc

结论

  • 检查调用点 30 个 / 操作 30 个
  • 确认问题 3 条(Q 1 / B 0 / P 1 / R 1 / G 0),可疑 5 条,未实证 16 个操作
  • 14 个操作在本机 http://localhost:7051 上拿到真实响应(含 3 条 400 原文与 2 条 404 原文); 其余 16 个是写操作或需空间所有者身份(require_writer / require_owner), 匿名 demo/demorepos 够不到,只做源码逐字比对,见文末「未实证清单」。

确认问题

#类位置操作现象后端依据建议修法
1Rview/app/src/domains/repo/history/ui/commit-view.tsx:99-104;view/app/src/domains/repo/model/commits.ts:69-70diffs.diff_raw根提交(parents: [])走 const base = commit.parents[0] ?? EMPTY_TREE,EMPTY_TREE = 4b825dc642cb6eb9a060e54bf8d69288fbee4904(model/diff.ts:39)。实测该 base 返回 404,而 fetchRawDiff 没有 .catch(),Promise.all 整体 reject → 根提交页整页失败(同批的 numstat 加了 .catch(() => []),patch 没有)。实证:GET /repos/demo/demorepos/diff/raw?base=4b825dc642cb6eb9a060e54bf8d69288fbee4904&head=27c58541bd555b8b9791ed6e2d8d7bdb252230ca → {"code":404,"message":"not found","data":null,"error":null,"error_code":null,...};同一 head 换真实父提交则 200lib/service/git/src/handler/diffs/raw.rs:10-13,25(返回 AppResponseData<String>);lib/service/git/src/service/diffs.rs:96-117 diff_request 对空树做字符串特判后直接交给 baby diff,对象不在库即 GitError::NotFound → 404。根提交 27c5854… 的 parents: [] 实证自 GET /commits?limit=2&offset=2599给 fetchRawDiff 补 .catch(() => '')(与 numstat 同策略),或根提交不请求 diff 而由清单自行渲染
2Pview/app/src/domains/repo/model/archives.ts:46-48(revision);view/app/src/domains/repo/model/tree.ts:91-93(revision);view/app/src/domains/repo/model/remotes.ts:25-26(name)archives.get_archive / trees.get_tree / remotes.upsert_remotepath 参数里含 / 时路径多切一段,路由整体不匹配。实证:GET /repos/demo/demorepos/tree/HEAD/extra → {"code":404,"message":"unknown api route","data":null,"error":null,"error_code":"not_found",...};GET /repos/demo/demorepos/archives/main/1.0?format=Tar → 同样 unknown api route。UI 触发器明确:archives-settings.tsx:115-120 的 revision 是自由文本输入,placeholder 就是 main / v1.0 / sha。根因在 packages/apiclient:355 个 get*Url 全部裸插值,encodeURIComponent 出现 0 次(grep -c 实测)。query 参数不受影响——生成器用 URLSearchParams,编码正确路由本身要求单段:lib/service/git/src/handler/archives/download.rs:12 /archives/{revision}、handler/trees/read.rs:36 /tree/{revision}、handler/remotes/add.rs:23 /remotes/{name}。编码后能正确进 handler(实证 GET /tree/feature%2Ffoo 返回业务 404 not found 而非 unknown api route,说明已到达 handler 层)在调用点对 path 参数做 encodeURIComponent(invoke() 只把 pathParams 的值传给 builder,编码值会原样拼进 URL,%2F 已验证可解析)
3Qview/app/src/domains/repo/history/ui/commits-tab.tsx:232commits.list_commits字段名本身正确,但 值域越界:onClick={() => setSkip(skip + PAGE_SIZE)} 只受 !page.hasMore 约束,没有 5000 上限;提交数 >5000 的仓库翻到第 101 页后请求 offset=5050。实证:GET /commits?offset=5001 → {"code":400,"message":"offset must not exceed 5000","data":null,...};GET /commits?limit=101 → {"code":400,"message":"limit must be between 1 and 100",...}。PAGE_SIZE = 50(同文件 :10),limit 在范围内没问题lib/service/git/src/handler/commits/list.rs:19-20(limit 1-100、offset ≤5000);service/commits.rs:24-39 ListCommitsCmd 字段确为 revision/path/limit/offset/since/until/first_parent,与前端发送的 revision, offset, limit, first_parent, path 逐字一致disabled 增加 skip + PAGE_SIZE > 5000 分支并提示「请用 path/revision 缩小范围」,或改成 cursor 分页

可疑(需人工确认)

#位置操作疑点已排除的可能
1view/app/src/domains/repo/history/ui/commit-view.tsx:101-104diffs.get_diff前端注释(model/commits.ts:46)称「shortstat 500s on the server today, so totals are summed client-side」,据此放弃 view=shortstat 改本地累加。实测 GET /diff?view=shortstat → 200,返回 {"additions":76,"deletions":1,"files_changed":2}不是 400/500 契约问题(端点正常);也不是形状误用(ShortStat 字段与本地累加结果同义)。属过时前提,功能不受影响,故未计入确认问题
2view/app/src/domains/repo/model/blame.ts:32-39submodules.submodule_resolve注释写「Resolve a submodule path at revision to its repository path (space/name)」,但后端返回的是 gitlink 提交 sha。实证 GET /submodules/resolve?revision=HEAD&path=browser-extension → {"code":200,...,"data":"385d36c0807221e6674d1325ef06505746c49ceb",...}返回类型 string 与 AppResponseData<String> 一致,UI(submodules-section.tsx:51)只把它当不透明字符串渲染,不会崩。故只是注释失配,不是契约错配
3view/app/src/domains/repo/branches/ui/submodules-section.tsx:49submodules.submodule_resolve后端只在根树里按 e.name == path 精确匹配(service/submodules.rs:71-79:get_tree_entries(&revision, Path::new("")))。.gitmodules 允许嵌套路径(如 vendor/lib),这类 entry 会被传进来但根树里没有同名条目 → GitError::NotFound已实证 path=server(非子模块)→ 404、path=browser-extension/embed → 200;本仓库没有嵌套子模块,无法造出「嵌套路径应当成功却 404」的反例,故列可疑
4view/app/src/domains/repo/model/tree.ts:89-97trees.get_tree参数 show_trees: true 在非递归分支里是空转:handler/trees/read.rs:86-92 的 tree_read 根本不接收 show_trees(只有 recurse 分支的 tree_walk 用它)。实证 GET /tree/HEAD?path= 与 ?path=&show_trees=true 都返回 32 条、modes=['Blob','Submodule','Tree'],完全一致不是功能缺陷——tree_read 默认就包含 Tree 条目,前端去掉也不会坏。只是参数无效,易误导读代码的人
5view/app/src/domains/repo/model/tree.ts:5-12trees.get_tree文件头注释断言 oid 是 { "Sha1": [20 raw bytes] }、name 已含仓库根相对路径(assets/logo.jpg)。实测两者都不成立:oid 是纯 hex 字符串("8358e08199cf3c8db41dacbe06298fca81a10b40"),子目录列举时 name 是 basename(path=.github 返回 [{"name":"FUNDING.yml",...}] 而非 .github/FUNDING.yml)normalizeTreeRows / oidToHex 同时兼容 string 与 {Sha1:[…]}、basename 与 root-relative 两种形态,两种现实都归一正确,未产生实际错配。属文档漂移

补充一条环境观察(不计入上表):GET /diff?…&bogus=1 返回 200,未知 query 参数被静默忽略,与 openapi 前言「未知参数同样返回 400」不符。影响取证——本域内 Q 类错误不会以 400 形式暴露,只能靠结构体逐字比对发现。

已核对无问题

操作依据
diffs.get_diff(view 枚举)后端 handler/diffs/get_diff.rs:12-19 DiffView 为 #[serde(rename_all="lowercase")] 的 Shortstat/Numstat/Files/Tree,前端 commits.ts:52 发 'numstat' 正确。大小写实证:view=Numstat → {"code":400,...,"error":"Query deserialize error: unknown variant Numstat, expected one of shortstat, numstat, files, tree"};view=bogus 同。生成类型 DiffView 与之一致
diffs.get_diff(use_merge_base)仅 Tree 分支使用(get_diff.rs:87-92);前端不传(commits.ts:52 只发 base/head/view)。实证 view=numstat&use_merge_base=true → 200(被接受并忽略),无副作用
diffs.diff_raw(返回形状)后端 handler/diffs/raw.rs:25 声明 AppResponseData<String>,实测是 JSON envelope 内 data 为字符串({"code":200,...,"data":"diff --git a/...),并非裸 text/plain。前端 unwrapApi<string>(commits.ts:70)消费方式正确
diffs.get_diff(numstat 行字段)rpcgit/diff/types.rs:60-65 NumStat{additions,deletions,path,old_path};前端 model/diff.ts:184-189 NumstatRow 同名同义。实证返回 {"additions":73,"deletions":0,"old_path":null,"path":"server/…"}
diffs.get_diff(非法 base/head)不可解析 revision → 404(实证 base=doesnotexist → {"code":404,"message":"not found"}),不是 400;前端按通用错误处理,无形状假设
trees.get_tree(query 名)handler/trees/read.rs:20-32 允许 path/recurse/resolve/show_trees/view;前端 {path, show_trees} 均在集合内。注意后端是 recurse 不是 recursive,前端未误用
trees.get_tree(path 语义)path 为空字符串时被 invoke() 的 query() 丢弃(services/api/index.ts:61-70 过滤 ''),后端 #[serde(default)] 补 "",等价。子目录实证 path=.github 正常返回
blobs.blob_contentservice/blobs.rs:30-36 BlobContentDto{oid,data:Vec<u8>,size,is_binary,truncated} → data 序列化为数字数组。实证 data:[118,45,101,110,118,...]。前端 readme.ts:150-153 用 Array.from(blob.data) 转字节、blob.is_binary/blob.truncated/blob.size 全部按此消费。路径 3 段 {space_name, repo_name, oid} 与 /blobs/{oid}/content 一致
commits.list_commits(字段)前端发 revision/offset/limit/first_parent/path,service/commits.rs:24-39 ListCommitsCmd 逐字包含;limit:50、offset:0 边界内。实证 200 且 limit=1 生效
commits.list_commits(has_more)service/commits.rs:74-95 CommitsPage{items,total,limit,offset,has_more,diff_view_style};前端 commits.ts:32-36 读 page.items/total/has_more。实证 "total":2601,"limit":1,"offset":0,"has_more":true
blame.blamehandler/blame.rs:10-18 query=revision,path,range_start,range_end;前端只发 revision,path,合法。返回 Vec<BlameRangeDto>,实测 {"starting_line":1,"ending_line":2,"commit_sha":"ac0b1d4…","abbreviated_oid":"ac0b1d4","path":null},前端 blameLineMap(blame.ts:54-61)用的 starting_line/ending_line 存在
conflicts.get_conflictshandler/conflicts/get_conflicts.rs:10-16 query=base,head,ancestor;前端 pulls/model/api.ts:156-160 发 {base,head} 合法。实证 {"mergeable":true,"conflicts":[],"fast_forwardable":true},与 MergeStatusDto 一致
conflicts.conflict_detailhandler/conflicts/detail.rs:10-15 query=base,head,path;前端 blame.ts:26-29 三字段齐全。返回 ConflictDetailDto{path,content:Option<String>} 与前端 {path, content?} 一致。正例未实证:demo/demorepos 无冲突对,base=HEAD~1&head=HEAD&path=README.md 返回 404
submodules.submodule_listhandler/submodules/list.rs:10-13 query=revision;前端 blame.ts:44-47 只发 revision。返回 Vec<SubmoduleDto{path,url,branch,commit_sha}>,实证 [{"path":"browser-extension","url":"","branch":null,"commit_sha":"385d36c…"},…],submodules-section.tsx 消费的四个字段全部存在
archives.get_archive(format 枚举)service/archives.rs:21-25 的 format 是 rpcgit::archive::types::ArchiveFormat(rpcgit/archive/types.rs:5-11,无 rename_all)→ serde 只认 Tar/TarGz/TarBz2/Zip。前端 archives.ts:30-35 QUERY_FORMAT 映射正是这四个值,正确。实证 format=tar_gz → {"code":400,...,"error":"Query deserialize error: unknown variant tar_gz, expected one of Tar, TarGz, TarBz2, Zip"};format=Tar → 通过解析,仅因无产物报 invalid argument: archive artifact not found
archives.create_archive(body)handler/archives/create.rs:30-36 ArchiveCreateCmd{revision,format:ArchiveFormatDto,path_filter},而 ArchiveFormatDto 是 #[serde(rename_all="snake_case")](同文件 :11)→ 只认 tar/tar_gz/tar_bz2/zip。前端 POST 传 cmd.format 的 snake_case 值、GET 才映射 PascalCase,两边分别正确。path_filter: null 对 Option<String> + #[serde(default)] 等价于 None
archives.get_archive(返回值/无轮询)ArchiveResultDto{format:String,size:i64,cache_key,download_url:Option<String>} 没有状态字段;前端 archives-settings.tsx:140-183 三处都是一次性调用后读 download_url/size,不做轮询,不存在「轮询错字段」
archives.create_bundlehandler/archives/create_bundle.rs:10-24 无 query/body,路径 2 段;前端 archives.ts:26 仅传 pathParams,一致
remotes.list_remotes返回裸 Vec<RemoteDto>(handler/remotes/list.rs:17,24),前端 unwrapApi<RemoteRow[]>(remotes.ts:21)按数组消费。实证 "data":[]
remotes.upsert_remote / removehandler/remotes/add.rs:16-23、remove.rs:16-19:path={space_name,repo_name,name} 3 段、body 仅 {url};前端 remotes.ts:25-27、:32 一致。返回 AppResponseData<NoData>,前端 unwrapApi<null> 与 data:null 对应
remotes.fetch / push / clone_importbody 分别为 RemoteFetchBody{remote,auth}、RemotePushBody{remote,refspec,auth}、RemoteImportBody{url,auth}(handler/remotes/{fetch,push,clone_import}.rs)。RemoteAuthDto{token:Option<String>,username:Option<String>}(service/remotes.rs:28-34)⊇ 前端 auth: {token};auth: null 与 Option<RemoteAuthDto> + #[serde(default)] 等价。refspec: null 同理
lfs.list_lockshandler/lfs/list_locks.rs:10-16 query=cursor,limit;前端 lfs.ts:15 发 {cursor, limit:50}(cursor 为空时回落 undefined 因而被丢弃)。返回 {locks,next_cursor?}(lfs/types.rs:110-115),实证 {"locks":[]}(无 next_cursor 时不出现,skip_serializing_if),前端 page.data?.locks ?? [] 兼容
lfs.verify_lockshandler/lfs/verify_locks.rs:10-14 query=limit,是 POST;生成签名 verifyLocks(spaceName, repoName, params?, options?) 与 OPS {query:true, body:false} 吻合。返回 {ours,theirs,next_cursor?},前端 locks-settings.tsx:58-61 读 ours/theirs 正确
lfs.lockhandler/lfs/lock.rs:10-15 body={path,oid};前端 lfs.ts:23 发 {path, oid: null},Option<String> + #[serde(default)] 接受 null
lfs.unlock / unlock_by_idhandler/lfs/unlock.rs:9-14 query=path,force;unlock_by_id.rs:9-20 path={space_name,repo_name,lock_id} + query=force。前端 lfs.ts:31、:39 键名与顺序一致。注意 path 走 query(URLSearchParams 编码),不受上文 P 类缺陷影响
cleanup.list_cleanup_objectslib/service/webapi/src/cleanup.rs:250,257 返回裸数组 AppResponseData<Vec<CleanupObjectDto>>;前端 rows<CleanupObject>(space-admin.ts:376)按数组消费,正确。path:1 = /spaces/{space_name}/cleanup 单段一致
cleanup.set_cleanup_visibility / set_cleanup_archivebody 分别 SetCleanupVisibilityCmd{is_private}(cleanup.rs:71-74)、SetRepoArchivedCmd{archived}(:76-79);前端 space-admin.ts:382、:390 逐字一致。path:3 分别对应 /visibility 与 /archive 两个后缀
cleanup.delete_cleanup_object / clear_cleanup_overwrites无 body,path:3 对应 :539 与 :668 的 {kind}/{object_id}[/overwrites];前端 space-admin.ts:396、:403 一致。返回 AppResponseData<NoData> ↔ unwrapApi<null>
push.push_configlib/service/user/src/handlers/push_config.rs:13-17,21 → PushConfigDto{enabled,public_key}、path="/push/config"、无参数。前端 notifications-section.tsx:26-31 类型 {enabled, public_key} 一致。实证 {"enabled":true,"public_key":"BCRWNH5S0jM0wMo0…"}

未实证清单

操作原因
remotes.upsert_remote / remove / fetch / push / clone_import写操作,需目标仓库写权限;匿名会话被拒。仅做源码比对
lfs.lock / unlock / unlock_by_id / verify_locks写操作(require_writer),同上
archives.create_archive / create_bundleAuthedUser + require_writer,同上;format 枚举已在 GET 上实证同一 serde 规则
cleanup.*(4 写 + 1 读,共 5 个)require_owner(仅空间所有者);list_cleanup_objects 同样受 AuthedUser + require_owner 限制,匿名不可达
conflicts.conflict_detail 正例被测仓库无冲突文件对,只能确认 query 名与 DTO 字段