Skip to content

Latest commit

 

History

History
486 lines (350 loc) · 51.6 KB

File metadata and controls

486 lines (350 loc) · 51.6 KB

目录变更与标签 HTTP 契约(storage-only / trunk)

本页是 Mega2 storage-only(push_policy=trunk)形态下目录变更与 monorepo 标签 产品 HTTP 的单一契约正文,供外部调用方按 file:line pin。计划出处 ../plan/plan-20260917.md(ADR-LB-01..07)与跟进 ../plan/plan-20260918.md(ADR-FT-01..03)。

主消费者是 sibling Libra 的 libra mega2 browser TUI。写本页的目的,是让调用方读契约而不是猜路径。

状态标记

每条路由标注实现状态。本页由 LB-01 创建;LB-02..04 把目录删移与 tag 挂载翻为 implemented。plan-20260918 FT-01 再冻结三处新 wire(is_directory、GET list、?path=),在对应实现卡落地前标 specified——那些小节描述落地后的契约,不是今日行为。

状态 含义
implemented 程式已存在且已挂到 storage-only,可直接调用
specified-unimplemented 本页冻结 wire;storage-only 上今日不可用(404 或行为不同)
路由 状态 承接卡 今日实况
GET /api/v1/tree implemented — 可用
POST /api/v1/create-entry implemented — 可用
POST /api/v1/delete-entry implemented LB-02 / FT-02 可用:省略 is_directory 删目录;false 删文件
POST /api/v1/move-entry implemented LB-03 / FT-03 可用:省略 is_directory 移目录;false 移文件(同一 blob oid)
POST /api/v1/tags implemented LB-04 可用:storage_only_routers_with 已 merge tag_router::routers();trunk 写经 push_auth,见「鉴权」
POST /api/v1/tags/list implemented FT-04 405(不再登记 POST)
GET /api/v1/tags/list implemented FT-04 唯一 list:必填 query page、per_page、path
GET /api/v1/tags/{name} implemented LB-04 / FT-06 可用(读,不要求 Authorization)。可选 ?path=(省略或空 = /)
DELETE /api/v1/tags/{name} implemented LB-04 / FT-06 可用:鉴权 path = 选择器 path(省略或空 = /)
POST /api/v1/path/provision implemented plan-20260923 FU-07 可用:只挂 storage-only(storage_only_write_routers);Review 形态 404
POST /api/v1/import-repo/remove implemented plan-20260923 FU-20 可用:只挂 storage-only(storage_only_routers_with 合并 import_repo_router::routers());token 须覆盖 path,push_auth=none 一律 403;Review 形态 404

公共前缀 /api/v1 由外层 nest 施加。

成功包络

一律是 CommonResult<T>(src/contract/api/common.rs:4-9):

{ "req_result": true, "data": { }, "err_message": "" }

三个键始终存在:req_result: bool、data: Option<T>(失败时为 null)、err_message: String(成功时为空串)。

范围

  • 只 pin 不改语义: POST /create-entry。
  • 错误语义修正(FIX-BB-10): GET /tree:路径不存在或指向非目录时返回 404;已有目录的请求字段与成功响应不变。
  • 新增产品写: POST /delete-entry、POST /move-entry(改名 = 同 parent 的 move)。plan-20260918 用同一路径扩文件(is_directory=false)。
  • 挂载 + 鉴权 + 文档对齐: 四条 /tags*。plan-20260918 把 list 改 GET,并为 get/delete 加 ?path=。
  • ImportRepo 叶子清理(plan-20260923 FU-20): POST /import-repo/remove,见「ImportRepo 叶子清理」。
  • 目录/文件变更是父目录 tree 改写后写新 commit,经 land_api_tip_push(trunk)或既有 CL 分支(Review)前进 tip;不是 Git delete command,也不走 CL apply_changes(ADR-LB-02 / ADR-FT-01)。

已有 API 与错误语义修正

GET /api/v1/tree — implemented

项 值
鉴权 无(不送 Authorization)
Query path(可选,#[serde(default = "default_path")] → /)、refs(可选,#[serde(default)] → 空串 = 当前 tip)
成功 200 + CommonResult<TreeResponse>
路径不存在或指向非目录 404 + 失败 CommonResult(req_result=false、data=null、err_message 点名请求路径或首个缺失前缀)

data.tree_items[] 每项为 { name, path, content_type }(TreeBriefItem)。

data.file_tree 是祖先辅助结构,不得当导航权威——导航只用 tree_items。

POST /api/v1/create-entry — implemented

字段 规则
is_directory 必填 bool。目录取 true
name 必填
path 必填。父目录,rooted;根下用 /
content 目录创建时可省略或为 null;is_directory=false 时必填(缺失见「错误映射」)
author_username / author_email 可选
skip_build 可选,#[serde(default)] → false。Libra 一律送 true
mode 可选,默认 EditCLMode::TryReuse(None)。调用方应省略该键,不得送字符串 "try_reuse"

成功 data 为 CreateEntryResult { commit_id, new_oid, path, cl_link };trunk 上 cl_link 必为 null;path 只作回执。

is_directory=true 时服务端写一个带时间戳的 .gitkeep 占位。

新增:目录变更

与 create-entry 对齐,用 parent path + name,不用单一绝对路径字段当权威。HTTP 状态同样是 200 + CommonResult(不用 204)。

以下两表逐行复制自 ADR-LB-03,并由 ADR-FT-01 扩 is_directory。

POST /api/v1/delete-entry — implemented(LB-02 / FT-02)

{
  "path": "/project",
  "name": "old-dir",
  "is_directory": true,
  "author_username": null,
  "skip_build": true
}
字段 规则
path 必填。父目录,rooted,默认语义与 create-entry 相同(根下用 /)
name 必填。要删的项名;禁止 /、.、..、分隔符、NUL、控制字符
is_directory 可选。bool,#[serde(default = "default_is_directory")] → true。true = 目录(Tree);false = 文件(Blob 或 BlobExecutable)。省略与显式 true 同义。
author_username 可选。Option<String>;可省略或 JSON null。不参与鉴权。
skip_build 可选。bool,#[serde(default)] → false。Libra 一律送 true。与 create-entry 同形。
目标 必须已存在且 mode 与 is_directory 相符;禁止删 /
鉴权 authorize_trunk_api_write(path),path = 父目录
HTTP 200 + CommonResult(不用 204)
成功 data { "commit_id": "<hex>", "path": "/project/old-dir", "cl_link": null };无 new_oid。path 只作回执。

空目录与非空目录是同一条删除语义(删掉父 tree 中的该 item);不提供「只删空目录」。

落地事实(LB-02,mono_api_service.rs 的 delete_monorepo_entry):

  • path/name 先过 validate_entry_target(src/ceres/model/git.rs):path 须 rooted(空串等于 /,容忍一个尾随 /),组件不得为空、.、..;name 为单一组件,禁 /、\、.、..、NUL 与控制字符。不合规一律 400。
  • 父目录被删空时,服务端补写一个带时间戳的 .gitkeep,父目录保留为空目录——与 create-entry 表示新建空目录的方式一致;Git 无法在路径上表示空 tree,这是唯一能保住父目录的做法。
  • 同名的 blob 与 tree 可以并存(create-entry 的重名检查按 mode 区分)。is_directory=true(或缺省)只匹配 Tree;false 只匹配 Blob 或 BlobExecutable。父 tree 完全没有该 name 才是 404;同名但 mode 不符是 400(「不是目录」或「不是文件」)。
  • 一次删除 = 一次 commit(父链 tree 改写 + .gitkeep 可选 blob),trunk 经 land_api_tip_push 前进 tip,Review 走既有 CL 分支(EditCLMode::TryReuse(None),与 create-entry 相同的政策分流)。
  • trunk 上父目录为 /(即删除顶层目录)时,产品写从不在 / 落地,返回 400 MONO_PATH_NOT_ALLOWED: …(plan-20260923.md ADR-FU-06;此前为 B0 的 no non-root path tip under / for trunk API write);Review 形态则在 / 的 CL 上进行。
  • commit_id 在 trunk 上是落地后的 tip;path 只作回执。

POST /api/v1/move-entry — implemented(LB-03 / FT-03)

{
  "from_path": "/project",
  "from_name": "old-dir",
  "to_path": "/project/other",
  "to_name": "new-dir",
  "is_directory": true,
  "author_username": null,
  "skip_build": true
}
字段 规则
from_path / from_name 必填。源父 + 源名;源 mode 必须与 is_directory 相符
to_path / to_name 必填。目标父 + 目标名;目标父必须已存在且为 directory
is_directory 可选。同 delete-entry:缺省 true;false 移文件(保留 Blob / BlobExecutable)
author_username 可选。同 delete-entry。
skip_build 可选。同 delete-entry;Libra 一律送 true。
改名 from_path == to_path 且 from_name != to_name
拒绝 源目标相同;目标名已存在;把目录移进自己的子树;is_directory=true 时的 file 源(或 false 时的 directory 源);/;traversal;import_dir
鉴权 两个 path(from_path 与 to_path)都必须通过 authorize_trunk_api_write;任一失败则不写
HTTP 200 + CommonResult(不用 204)
成功 data { "commit_id": "<hex>", "from_path": "/project/old-dir", "to_path": "/project/other/new-dir", "cl_link": null };无 new_oid。返回路径只作回执。

落地事实(LB-03,mono_api_service.rs 的 move_monorepo_entry):

  • 两组 path/name 都先过 validate_entry_target(规则同 delete-entry);父路径经 normalize_parent_path 归一(空串 = /,容忍一个尾随 /),改名 = 归一后 from_path == to_path 且名字不同。
  • 校验顺序:源目标相同 → 移进自己的子树(仅 is_directory=true:目标父 = 源目录或其后代;文件源不做子树检查)→ 目标父落在 ImportRepo 下(router 只按 from_path 分派,monorepo handler 自查 git_repo 后以 409 拒绝)→ 源父不存在 / 源不存在 / 源 mode 不符 → 目标父不存在 → 目标名已存在(任何 mode 的同名项都算已存在)。全部检查在任何写入之前完成。省略字段只移目录;is_directory=false 移 Blob / BlobExecutable 并保留同一 oid。
  • 改写 = 源父 tree 去掉该项、目标父 tree 插入同一 oid 与同一 mode(改名只改 TreeItem.name),两条父链自底向上重算到根,一次 commit;目标父的项按 Git 顺序排序;源父被移空时补写带时间戳的 .gitkeep(同 delete-entry)。
  • 落地路径 = 两个父目录的最深公共目录:trunk 上经 land_api_tip_push 前进覆盖该路径的最深非根 path tip(resolve_trunk_land_path,AW-03:落地 /project/a(本身无 tip)时前进的是 /project 的 tip),因此跨顶层目录的移动(公共目录为 /)在 trunk 上返回 400 MONO_PATH_NOT_ALLOWED(产品写从不在 / 落地,plan-20260923.md ADR-FU-06;此前为 B0 文本 no non-root path tip …);Review 形态在该公共目录(或 /)的 CL 上进行,与 create/delete 相同的政策分流。
  • 鉴权:from_path 与 to_path 各调一次 trunk_write_requester,任一失败(401/403)即拒绝,此时尚未读任何 tree。

POST /api/v1/path/provision — implemented(plan-20260923 FU-07)

幂等的路径开通(mkdir -p):一次 commit 补齐目标路径上全部缺失的目录层级(叶子目录放一个内容每次唯一的 .gitkeep),由开通专用、绑定单一根树快照的建树逻辑构建,经 commit_tree_update 与 MonoWriteQueue 落地;整条路径已是目录时零写(plan-20260923.md ADR-FU-05)。只挂在 storage-only 表面(preview_router::storage_only_write_routers,由 api_router::storage_only_routers_with 合并);Review 形态不挂(path_provision_is_storage_only)。

{ "path": "/project/workbuddy/demo" }
字段 规则
path 必填。规范绝对路径,原样必须等于其规范形态(strict_creation_path_input:拒绝相对路径、. / .. 段、重复或尾斜杠、NUL、\ 与其它控制字符如换行 / 制表)

成功(HTTP 200):

{ "req_result": true, "data": { "path": "/project/workbuddy/demo", "created": true, "commit_id": "<hex>" }, "err_message": "" }

created: false 且 commit_id: null 表示路径已是目录,没有写入(重复调用恒返回该结果)。commit message 为 provision <path>;全新栈上与其它产品写一样落在惰性物化的一级根上(ADR-FU-06)。

判定顺序:

  1. 严格输入校验(400 MONO_PATH_INVALID)。
  2. 鉴权预检:以目标路径调用 authorize_trunk_api_write(401 / 403),此前不读树。
  3. 每轮:import 命名空间守卫与创建分类(400 MONO_PATH_NOT_ALLOWED / MONO_PATH_INVALID;/ 为 MONO_PATH_INVALID)→ 逐级遍历,已存在的组件不是目录 → 409 MONO_PATH_CONFLICT(点名该组件)→ 求出最高新建组件(第一个缺失组件;无缺失时即目标)→ 以它再次鉴权(403)→ 落地。
  4. 落地(apply)从同一份根树快照重新推导最高新建组件并在该快照上建树:推导结果与已鉴权的计划不同(例如计划之后某个容器被删除)→ 不写入、重新规划并重新鉴权;新建层级的位置出现任何同名条目(包括计划之后才出现的文件)→ 409 MONO_PATH_CONFLICT;落点 tip 的 tree 与快照不一致(期间有别的写落地),或队列以固定文本拒绝(ADR-TP-10 同路径冲突、non-fast-forward、墓碑、缺失路径)→ 重新规划并重新鉴权。带 [code:…] 的错误(路径策略、4xx)与其它错误立即返回,重试判定不对用户路径做子串匹配。至多 10 轮(线性退避,末轮不等待)。每次尝试的 .gitkeep 内容唯一,两个并发尝试不会构造出相同的 commit 而被队列当作同一操作重放。由此,一次开通创建的层级恒为某一轮已鉴权的最高新建组件及其下层;并发开通同一路径时恰有一个请求 created: true,其余在重新规划后得到 created: false。

鉴权按最高新建组件:只覆盖 /project/team/x 的 token 不能顺带创建尚不存在的容器 /project/team(403);容器由覆盖它的 token 开通之后,同一 token 即可开通 /project/team/x。每轮重新鉴权,且落地与本轮计划绑定(见上),落地重试不会扩大写范围。

错误:400 MONO_PATH_NOT_ALLOWED(根外路径、import_dir 命名空间)/ MONO_PATH_INVALID;401 / 403(token);409 MONO_PATH_CONFLICT(组件是文件)/ MONO_PATH_UNINITIALIZED。错误文本即 PathPolicyError 原文(../errors.md),经 CommonResult 返回。运行时 OpenAPI 声明 200 / 400 / 401 / 403 / 409(path_provision_openapi)。请求体本身无法解析时(缺 path、非 JSON、Content-Type 不是 application/json)由 axum 的 Json 提取器直接返回纯文本 4xx(422 / 400 / 415),不是 CommonResult,也不在 OpenAPI 中声明。

已知限制(登记于 plan-20260923.md):

  • 嵌套 import_dir(如 /third-party/vendor)时,在其严格祖先根(/third-party)之下开通返回 409 MONO_PATH_UNINITIALIZED;该消息提示的开通动作正是本端点,此时提示是循环的——祖先根永不物化(GC-FU-04),目前只能用 git push 直接创建更深的路径(DEFER-FU-13)。
  • root_dirs 中新增、但初始化时尚未创建的一级根(改 root_dirs 并重启之后)无法经本端点开通:唯一可能的落点是 /,产品写从不在 / 落地,返回 400 MONO_PATH_NOT_ALLOWED(点名 /)。新增根需先经 git push 创建(DEFER-FU-14)。

错误映射

情况 今日实况 / 落地要求
目录重复名 今日 create-entry 返回 500 + err_message:"Internal server error"。GitError::CustomError("Duplicate name") 没有 [code:] 前缀(mono_api_service.rs:2203、:2344),落到 ApiError::internal(common/errors/api.rs),而 IntoResponse 对 5xx 一律改写为 "Internal server error"。LB-02/03 的 delete/move 必须用 [code:400] 前缀返回可诊断 4xx,不得复制这个缺陷
is_directory=false 缺 content 同上,今日为 500("content is required for file creation" 亦无 [code:] 前缀,mono_api_service.rs:2121)
tag 名非法 今日已是 400(tag_router.rs 的 validate_tag_name → ApiError::bad_request)
tag 已存在 400("[code:400] Tag '{}' already exists",mono_api_service.rs:1677/:1690)
tag 不存在(get) 404;wire err_message = Tag '<name>' not found。由 router 直接构造(tag_router.rs:299-302),服务层 get_tag 只返回 Ok(None),不产生 [code:]
tag 不存在(delete) 404;服务层抛 "[code:404] Tag not found"(mono_api_service.rs:1846),wire err_message = Tag not found(前缀被剥掉,见下)
delete-entry:目标是文件 400,wire err_message = '<name>' is not a directory(服务端 [code:400] 前缀被剥掉)
delete-entry:缺父目录 / 缺目标 404,parent directory <path> not found / entry '<name>' not found under <path>
delete-entry:父路径穿过一个文件 400,parent path <path> is not a directory
delete-entry:path/name 不合规(未 rooted、./../空组件、分隔符、控制字符) 400,validate_entry_target 的诊断原文
delete-entry:ImportRepo(import_dir 下) 目标在存活 ImportRepo 内部(router 按 git_repo 分派给 ImportApiService):409,import dir does not support delete entry;其余 import_dir 命名空间目标(import_dir 本身、从父目录删除挂载叶子——不论该仓库是否存活、已无存活 git_repo 行的路径;嵌套 import_dir 时还包括其严格祖先):400 MONO_PATH_NOT_ALLOWED: …(plan-20260923 ADR-FU-06,两种形态均适用)
move-entry:源目标相同 400,source and destination are the same: <path>
move-entry:目标名已存在(任何 mode) 400,'<to_name>' already exists under <to_path>
move-entry:移进自己的子树 400,cannot move <src> into its own subtree <to_path>
move-entry:源是文件(省略 / is_directory=true) 400,'<from_name>' is not a directory
move-entry:源是目录且 is_directory=false 400,'<from_name>' is not a file
move-entry:from_path/from_name/to_path/to_name 不合规 400,validate_entry_target 的诊断原文
move-entry:源父 / 目标父不存在;源不存在 404,source parent <path> not found / destination parent <path> not found / entry '<name>' not found under <path>
move-entry:源父 / 目标父路径穿过文件 400,source parent path <path> is not a directory / destination parent path <path> is not a directory
move-entry:源或目标在 ImportRepo 下 源在存活 ImportRepo 内部(分派给 ImportApiService)或目标父是存活 ImportRepo:409,import dir does not support move entry;其余源 / 目标落在 import_dir 命名空间(嵌套 import_dir 时源还包括其严格祖先):400 MONO_PATH_NOT_ALLOWED: …(ADR-FU-06,两种形态均适用)
create-entry / edit-save:import_dir 命名空间内、无存活 ImportRepo 的路径 400 MONO_PATH_NOT_ALLOWED: …(ADR-FU-06,两种形态均适用;存活 ImportRepo 内部的写由 ImportApiService 处理;处理期间该仓库被清理见下一行)
ImportRepo 产品写遇清理(edit/save、POST /tags、DELETE /tags/{name};请求已分派给存活 ImportRepo 的 ImportApiService,处理期间该仓库被 detach,plan-20260923 ADR-FU-09 第 5 条,FU-19) 409,wire err_message = IMPORT_REPO_REMOVED: "<path>" was removed; push again to import it anew(码与含义见 错误码 的 ImportRepoError 小节)。本次写不落行:edit/save 的对象行经 save_entry 栅栏,默认分支 ref 在自己的带锁事务里写(二者之间遇 detach 时,已提交的对象行由清扫删除);annotated tag 的 git_tag 行与 ref 同一事务(ref 写失败时两者皆无),lightweight tag 只写 ref;tag 删除的 ref 与 git_tag 行同一事务,被拒的删除不改任何行(含不存在的名字)。预读因行已被删除而失败时同样答 409,存活仓库的 404 / 400 不受影响(存活仓库缺默认分支 / commit / tree 为 404)。detach 在分派之前已提交的请求不再到达 ImportApiService:edit/save 为上一行的 400 MONO_PATH_NOT_ALLOWED,tag 路由按 Monorepo 语义处理(计划 DEFER-FU-38)。事务的锁序与 detach 等待时长见 trunk-push.md 的「receive-pack 写路径存活栅栏」
trunk 全新栈:写路径无非根 tip 合法路径落在惰性物化的一级根上;首组件不在 root_dirs 中(含在 / 下新建顶层)→ 400 MONO_PATH_NOT_ALLOWED,点名用户写的路径;跨顶层 move 与删除顶层目录(落点只能是 /)→ 400 MONO_PATH_NOT_ALLOWED,点名 /;嵌套 import_dir 的严格祖先根 → 409 MONO_PATH_UNINITIALIZED,点名写入所在目录(新建目录时为该目录本身)(ADR-FU-06)
缺目录 delete / move 与 GET /tree 均返回 404(ADR-LB-03、FIX-BB-10);GET /tree 的路径穿过或指向文件也返回 404。create-entry 会自动补建缺失的父层级而不是 404
鉴权失败 见「鉴权」

[code:NNN] 前缀是本仓真正的 4xx 约定(mono_api_service.rs:1097/:1677/:1690/:1846 都在用)。缺前缀的 CustomError 会静默变成 500 并丢失原文。

该前缀只是服务端内部标记,不会出现在 wire 上。 ApiError 的 IntoResponse 与 map_ceres_error(均在 common/errors/api.rs)都会在写入 err_message 前把它剥掉;map_ceres_error 只识别 400、404 与 409(409 自 FU-19 起),其余为 500。所以上表引号里的服务端字面量与调用方实际收到的 err_message 不同:例如 tag 重名的 wire 值是 Tag 'foo' already exists,不含 [code:400]。调用方不要按该前缀做字符串匹配。

标签(monorepo tags)

服务层 MonoApiService 的 create_tag / list_tags / get_tag / delete_tag 已实现;LB-04 起 storage_only_routers_with(src/api/api_router.rs)merge 了 tag_router::routers()(tag_router.rs 的 routers),四条路由在 storage-only / trunk 与 Review 上都可用(storage-only OpenAPI 由 server::http_server::tests::storage_only_openapi_* 与 tag_router::tag_routes_registered_on_storage_only_routers 锁定)。trunk 写鉴权见「鉴权」。

Git 客户端 push Tag 仍然禁止(见使用指南);Tag 只能通过本节 HTTP API 管理。

POST /api/v1/tags — create — implemented(LB-04)

CreateTagRequest:

字段 规则
name 必填。服务端校验(validate_tag_name,tag_router.rs:96-141,违反即 400):非空;name.len() <= 255(字节,非字符);不含 ..;不含 @{;不含 //;不以 .lock 结尾;不含禁用字符 —— ASCII 空格、~、^、:、?、*、[、\(tag_router.rs:125 的 forbidden 数组,逐字为 [' ', '~', '^', ':', '?', '*', '[', '\\']);不含 NUL 与任何 char::is_control() 字符
target 可选;serde alias target_commit
path_context 可选;省略则 handler 用 /(也是 trunk 鉴权 path)
tagger_name / tagger_email / message 可选

无请求键 tagger——tagger 只是 TagResponse 的响应字符串。

成功:200 + CommonResult<TagResponse>。

OpenAPI 与运行时一致为 200:handler 返回 Json<CommonResult<TagResponse>>(tag_router.rs:166),LB-04 把 utoipa 注解从 201 改为 200(tag_router.rs:158);模块级回归 tag_create_openapi_status_is_200 与 IT tag_create_unauth_401(运行时 /api/openapi.json)都断言 200 在、201 不在。

GET /api/v1/tags/list — list — implemented(FT-04)

落地后方法是 GET,不是 POST。 查询串为三个必填键:

键 规则
page 必填 u64。从 1 起(内部 page.saturating_sub(1),故 page=0 等同 page=1)
per_page 必填 u64,必须 ≥ 1。handler 在分页前拒绝 per_page=0,返回 400 + CommonResult([code:400] per_page must be >= 1)。禁止把 0 交给 sea-orm paginate
path 必填。path context;trim().is_empty() 视同 /。调用方列 root 应显式送 path=/

缺键或非法数字(含 page=abc、缺 path)由 axum 0.8 Query<T> 在进 handler 前返回 400 FailedToDeserializeQueryString(不是 422)。

成功:200 + CommonResult<TagListResponse>,其中 TagListResponse = CommonPage<TagResponse> = { "total": <u64>, "items": [ TagResponse… ] }。

POST /api/v1/tags/list — list — 405(FT-04)

对该路径发 POST 必须 405。调用方改走 GET /api/v1/tags/list?page=&per_page=&path=。不再接受 JSON PageParams<String>。该路径没有成功体。

total = DB 里符合过滤的注解 tag 数 加上本次请求扫描到、且已扣除与本页 annotated 重名后的全部 lightweight ref 数。注意该加数在 .take(need) 之前就已算出,所以其中可能包含并未进入本页 items 的 refs。因此 total 随页而变,不是稳定的全局计数;分页请以 items 长度与 per_page 判断,不要把 total 当权威总量。注解 tag 按 mega_tag.path 过滤,轻量 ref 按 mega_refs.path 过滤。

GET /api/v1/tags/{name} / DELETE /api/v1/tags/{name} — get / delete — implemented(LB-04 / FT-06)

两条路由都带查询键 path(可选;省略或空 / 空白 = /)。查找键是 (path, name),不是全局 name。delete 的 trunk 鉴权 path = 该选择器 path。create 写入 mega_tag.path;list 按 path 过滤注解 tag 与轻量 ref。

  • get 成功:200 + CommonResult<TagResponse>;该 path 下 tag 不存在 → 404。
  • delete 成功:200 + CommonResult<DeleteTagResponse>({ deleted_tag, message });该 path 下 tag 不存在 → 404。

TagResponse 字段:name、tag_id、object_id、object_type、tagger、message、created_at。七个字段全部是非 Option 的 String(ceres/model/tag.rs:37-52),键始终存在;created_at 是字符串不是数值时间戳;list / get 回传的 lightweight tag tagger 与 message 为空串(mono_api_service.rs:1755-1756、:1803-1804);create 的回应里 lightweight tag 的 tagger 是 tagger_name / tagger_email 的组合、两者都缺省时为 unknown(:1661-1666、:2668),message 为空串。

ImportRepo 叶子清理(plan-20260923 FU-20)

POST /api/v1/import-repo/remove — implemented(plan-20260923 FU-20)

用途。 清理 import_dir 之下的一个 ImportRepo 叶子:经写入队列 detach(B3 op = detach:删除 git_repo 与 import_refs 行、从根树移除挂载叶子),再在本请求的预算内清扫对象行。机制见 trunk-push.md 与 ../plan/plan-20260923.md 的 ADR-FU-09 / ADR-FU-10,本节只写 HTTP 契约。只挂 storage-only(storage_only_routers_with 合并 import_repo_router::routers();单测 api::router::tests::import_repo_remove_is_storage_only 锁定 Review 不挂)。

请求。 新操作只给 path;续做再给上一次答复中的 cleanup_id:

{ "path": "/third-party/acme/lib" }
{ "path": "/third-party/acme/lib", "cleanup_id": 7340032 }
字段 类型 说明
path string,必填 已注册 ImportRepo 的存储路径:规范形式、组件级严格位于 import_dir 之下。拒绝 NUL、\、相对路径、. 与 .. 段、重复或结尾斜杠、首尾空白、import_dir 本身与其外的路径;NUL 以外的控制字符不拒绝(计划 DEFER-FU-44)。.git 是普通字符:Git URL …/lib.git 对应的 path 是 /third-party/acme/lib
cleanup_id i64,可选 上一次答复中的 cleanup_id(同一 path);省略或 null 为新操作

未知字段(例如拼错的 cleanupId)→ axum 纯文本 422,以免续做被当成新操作。

判定顺序。

  1. axum Json 提取器:Content-Type、JSON 语法与字段类型(纯文本 400 / 413 / 415 / 422,不是 CommonResult)。
  2. 严格路径校验 → 400 IMPORT_REPO_PATH_INVALID:先于鉴权,只回显调用方输入与公开的 import_dir(Git 面相反,先 401 / 403)。
  3. 鉴权(authorize_import_repo_removal,见「鉴权」)→ 固定体 401 / 403。
  4. 清理入口(FU-17):所有查询都在此之后,全部为绑定参数——仓库按精确路径查找,子仓检查为转义的前缀扫描,台账按 id 或 (path, state) 读取。

成功体。 data 四键恒在:

{ "req_result": true, "data": { "path": "/third-party/acme/lib", "outcome": "removed", "repo_id": 7340037, "cleanup_id": 7340032 }, "err_message": "" }

repo_id 与 cleanup_id 为 64 位整数,可能超过 2^53(JavaScript 客户端须用 BigInt);outcome = "absent" 时两者为 null。

结局(在请求结束时判定)。

请求 结局
新操作,path 有存活仓库 先做无写子仓预检(有子仓 → 409,不入队);detach 得到新 cleanup_id,先清扫本请求自己的台账行:完成 → removed(本行 id;剩余预算续做同路径更早的待续行,但不改变答复);超出预算 → pending(本行 id)
新操作,path 无存活仓库 按 id 续做至多 16 行待续台账(至多 100 条清扫语句):已无剩余 → absent(两 id 为 null,即使本请求做了清扫);否则 pending,指名最小的剩余行(超过 16 行时可能是本请求未触及的行)
续做(带 cleanup_id) 只处理该台账行,永不 detach:不存在或属于另一路径 → 404(同一文本);已清扫或本次完成 → removed(幂等);否则 pending(同 id)

repo_id 恒为该台账行的仓库。removed 不等于该路径已排空:更早的待续行由之后只带 path 的请求续做(计划 DEFER-FU-06)。

客户端协议。

  • 遇 pending:带返回的 cleanup_id 续做,直到 removed。续做对同路径的重新导入安全。
  • 要排空路径:重复只带 path 的请求,直到 absent。只带 path 的请求会 detach 当时存活的仓库,包括期间重新导入的仓库。
  • 丢失 cleanup_id 且没有重新导入时,只带 path 的请求会续做待续行:在本次预算内清扫完则答 absent(两 id 为 null),否则以 pending 返回最小的待续 cleanup_id。
  • 500 可重试(见下表);续做重试总是安全的,只带 path 的重试会 detach 期间重新导入的仓库。

错误。

情况 状态 err_message / 体
Review 形态 404 路由未挂载
非 POST 405 空体,allow: POST
缺 Content-Type: application/json / JSON 语法错 / 缺 path、字段类型错、未知字段 / 超过默认 2 MiB 415 / 400 / 422 / 413 axum 纯文本(不是 CommonResult)
严格路径校验失败 400 IMPORT_REPO_PATH_INVALID: "<输入>": <原因>(import_dir 本身、在其之外、非规范形式时 did you mean "<规范形式>"?、NUL、\)
push_auth=token,无凭据或 token 未知 401 固定体 authentication required
token 未覆盖 path;或 push_auth=none / 未配置 403 固定体 forbidden
续做的 cleanup_id 不存在或属于另一路径 404 IMPORT_REPO_CLEANUP_NOT_FOUND: no cleanup "<id>" for "<path>"
目标之下仍有已注册 ImportRepo 409 IMPORT_REPO_HAS_CHILDREN: "<path>" contains other ImportRepos; remove them first
写入队列暂停 / 硬停 / 满、detach 轮次未完成、存储错误 500 Internal server error,可重试(计划 DEFER-FU-42)

400 / 401 / 403 / 404 不写任何行;409 不写台账、审计、ref、对象与根树(入口的无写预检不入队;由写入队列锁内再检拒绝时留下一行 Failed 队列行)。未授权调用方对「不存在 / 叶子 / 有子仓的父」三类路径得到逐字节相同的体(响应头除 Date 与按请求的 x-request-id 外一致,无 WWW-Authenticate),error 日志只含固定文本。码的含义见 错误码 的 ImportRepoError 小节;运行时 OpenAPI 声明 200 / 400 / 401 / 403 / 404 / 409 / 500(IT import_repo_remove_openapi)。

保留策略。

  • 对象存储中的 blob 字节保留(计划 DEFER-FU-01);清扫删除 git_commit、git_tree、git_blob、git_tag 行。
  • detach 删除 git_repo 与 import_refs 行,从根树移除挂载叶子,并修剪 import_dir 之下因此变空的、由挂载创建的目录(ADR-FU-09 第 2 条)。
  • 台账 import_repo_cleanups 与 audit_logs(kind = import_repo.remove,phase 为 detached / swept,requester 为 token 名)永久保留。
  • 清理不可逆;再次推送即以新的 repo_id 重新导入。

已知限制。

  • absent 不等于该路径无人服务:存活 ImportRepo 的子路径由父仓库服务;子仓被清理后,该路径的 tag 与编辑路由作用于存活的父仓库,而 Git clone 该路径为 404(计划 DEFER-FU-39)。
  • 被清理路径上的 POST /api/v1/tags 按 Monorepo 语义处理,可在 import_dir 之下写出 Monorepo tag(计划 DEFER-FU-38)。
  • 清理只删除规范行:FU-15 保留的别名行(如 P/)不删、不算子仓、严格入口无法点名,之后以别名拼写的 Git 请求会使 P 重新存活(计划 DEFER-FU-10)。
  • 子仓检查为顺序扫描(DEFER-FU-27);请求可能较长、无服务端超时,断连按崩溃点语义恢复(DEFER-FU-32);import_dir 本身的存量行无法点名(DEFER-FU-09);400 回显输入的长度与控制字符见 DEFER-FU-43 / DEFER-FU-44。

鉴权

目录变更写复用既有 authorize_trunk_api_write(src/api/api_write_auth.rs),与 LFS / create-entry 同一威胁模型;ImportRepo 清理端点例外,用更严格的 authorize_import_repo_removal(见「ImportRepo 叶子清理」)。

整节的形态前提: 这道闸仅在 push_policy=trunk(含 storage-only)时生效。Review 形态下 trunk_write_requester 返回 Ok(None) 并跳过鉴权(preview_router.rs:475-485;LB-04 起为 pub(crate),tag_router 复用同一实现),目录变更走既有 CL 分支,tag 写沿用 Review 既有面。

不需要 Authorization

GET /tree、GET /tags/list、GET /tags/{name} —— 调用方不送、server 不要求 Authorization。

需要 Authorization(trunk 写)

路由 状态
POST /create-entry implemented——今日确实鉴权(preview_router.rs:114-117 取 HeaderMap 并调 trunk_write_requester)
POST /delete-entry implemented(LB-02)——delete_entry(preview_router.rs:138)取 HeaderMap 并调 trunk_write_requester(path = 父目录),鉴权先于任何存储访问
POST /move-entry implemented(LB-03)——move_entry(preview_router.rs:166)对 from_path 与 to_path 各调一次 trunk_write_requester,任一失败即拒绝,先于任何存储访问
POST /path/provision implemented(plan-20260923 FU-07)——先以目标路径调用 authorize_trunk_api_write(先于任何树读取),再每轮以最高新建组件重新鉴权,见该小节
POST /import-repo/remove implemented(plan-20260923 FU-20)——严格路径校验之后、任何查询之前以 authorize_import_repo_removal 鉴权;只接受覆盖目标路径的 token;push_auth=none 或未配置一律 403;401 / 403 为固定体
POST /tags / DELETE /tags/{name} implemented(LB-04 / FT-06)——create_tag 以 path_context.unwrap_or("/")、delete_tag 以选择器 path(省略或空 = /)各调一次 trunk_write_requester,先于任何存储访问

落地状态(LB-04,安全相关): create_tag(tag_router.rs:162)与 delete_tag(:321)都接收 HeaderMap,并在任何存储访问之前调用 trunk_write_requester;这关闭了计划里的 GAP-LB-04(裸挂会让 token 部署匿名写 refs/tags)。Review 形态该函数返回 Ok(None),tag 写沿用 Review 既有面(cedar_guard 只覆盖 /cl,不覆盖 /tags)——本卡不为 Review 新增 401(IT tag_review_form_no_trunk_gate)。

trunk / storage-only 上的 push_auth 行为(IT tag_create_unauth_401 / tag_delete_unauth_401 / tag_auth_none_write_ok):

push_auth 行为
token Bearer,或 Basic 的密码栏
none 无 header 即可写(目录写把 requester 记为 anonymous 进 push_queue;tag 写不记录 requester,两个 handler 丢弃返回值)(POST /import-repo/remove 除外:一律 403)

状态码分类(api_write_auth.rs 的 authorize_trunk_api_write):

  • 凭据缺失,或提供了但查不到对应 push token → 401(两者都走 api_write_auth_challenge)
  • 凭据识别成功、但 paths 未覆盖目标 path → 403
  • push_auth 未配置(None)→ 401,fail-closed
  • 例外:POST /import-repo/remove 在 push_auth 不为 token(含未配置)时一律 403;两种拒绝都只用固定体(401 authentication required、403 forbidden),不回显路径

错误响应、JSON 与 trace 不得回显 token。

哪些 tag 写需要能覆盖 / 的 token

本小节描述 trunk / storage-only 形态(IT tag_write_path_scoped_token_403);Review 形态不经这道闸。

tag 写的鉴权 path 不是你操作的业务路径:

  • create 用 path_context.as_deref().unwrap_or("/");path_context = "/project" 之类被 token 覆盖的 path 可过闸,tag 落在该 path 的 mega_refs / mega_tag.path 下;
  • delete 用查询 path(省略或空 = /)。覆盖 /project 的 token 可删该 path 的 tag;删 root tag 仍需覆盖 /。

因此 push_tokens.paths = ["/project"] 这类不含 / 的 token:

  • 对省略 path(即 /)的 tag delete → 403;对 ?path=/project → 可过闸;
  • 对省略或显式 path_context = "/" 的 create → 403。

因此:省略 path 的 delete 与 root / 缺省 path_context 的 create 必须持有能覆盖 / 的 token(paths 省略或为空 = whole repo);?path=/project 的 delete 与 非根 path_context 的 create 可用覆盖该 path 的 token。Libra pin 八行见下节(FT-08 已重钉)。

契约页不写真实凭据;示例一律用 secret-ok 一类占位。

形态差异

形态 目录变更落地 cl_link
trunk / storage-only land_api_tip_push 必为 null
Review 既有 find_or_create_cl_for_edit CL 分支 可为非 null

POST /path/provision 只挂 storage-only(preview_router::storage_only_write_routers,由 storage_only_routers_with 合并;单测 path_provision_is_storage_only 锁定 Review 的 routers() 不含它)。POST /import-repo/remove 同样只挂 storage-only(import_repo_router::routers(),单测 import_repo_remove_is_storage_only)。write_routers(preview_router.rs)同时挂在 Review 与 trunk;LB-02 / LB-03 已把 delete-entry 与 move-entry 登记进同一函数,因此两者在两种形态下都可用(storage-only OpenAPI 由 server::http_server::tests::storage_only_openapi_* 锁定)。tag_router::routers() 同样两边都挂:Review 的 routers() 原本就有,LB-04 把它 merge 进 storage_only_routers_with(api_router.rs:83);三份 OpenAPI 锁(storage-only / trunk / OAuth)都断言 /tags、/tags/list、/tags/{name}。

ImportRepo(import_dir 下)对 delete/move 必须返回 409(计划门 delete_entry_reject_import_repo_409,见 plan-20260917 LB-02 卡「判据规范 — EX-LB-01」门 11;该门只规定状态码)。LB-02 的 delete_monorepo_entry(import_api_service.rs)返回 [code:409] import dir does not support delete entry,wire 上 err_message = import dir does not support delete entry;LB-03 的 move 同样:源在 ImportRepo 下由 ImportApiService 返回 [code:409] import dir does not support move entry,目标在 ImportRepo 下由 monorepo handler 自查 git_repo 后返回同一文本。实现上必须带 [code:409] 前缀——这是推导出来的必要条件,不是计划原文:GitError::CustomError 只有带该前缀才会被 common/errors/api.rs 中 From<E> for ApiError 的 409 分支映射成 StatusCode::CONFLICT。另有一个陷阱: map_ceres_error(common/errors/api.rs)FU-19 之前只识别 400 与 404,FU-19 起增加 409(ImportRepo tag 写的 IMPORT_REPO_REMOVED),其余一律 500,而它正是 tag_router.rs 的惯用写法。因此 delete/move 走裸 ?(From<E> for ApiError,如 preview_router 的 delete_entry)那条路径;FU-19 之前若用 map_ceres_error 包装,带 [code:409] 也会落成 500;delete/move 仍走裸 ?。今日 create-entry 的同类拒绝(ImportApiService::create_monorepo_entry 的 CustomError("import dir does not support create entry"))没有前缀,因而落成 500 + "Internal server error"——见「错误映射」,delete/move 不得复制该缺陷。其 Git 多分支与客户端 tag 语义不受本计划影响。

Git 可见性

目录变更成功后,对同一 path tip 的 git clone / git fetch + git pull 必须看到删除结果或新路径。

tag create/delete 之后,GET /tags/{name} 与 GET /tags/list 必须在同一 path 下一致。

非目标

  • 不预览/编辑 blob;不把建文件从既有 POST /create-entry(is_directory=false)拆到新路径。
  • 不改 GET /tree / POST /create-entry / POST /edit/save 的既有请求字段或成功语义。
  • 不以 Git receive-pack delete command、parent-path 客户端 push 或 CL apply_changes 冒充产品 HTTP。
  • 不新增 DELETE /tags/delete-file / POST /move-file 之类平行路径;文件删移扩既有 delete-entry / move-entry。
  • 不把 GET list 做成「GET+POST 双挂」窗口(FT-04 卸 POST,对该路径 POST → 405)。
  • 不校验 tag target commit 是否属于该 path 子树(DEFER-FT-02 / DEFER-LB-11 目标归属)。
  • 不回收被删/移入口后的 blob / LFS / Media 物件(DEFER-FT-01)。
  • 不接入 Review OAuth / Cedar enforce;不为 storage-only 打开 SSH receive-pack。
  • 不实现 Libra 客户端(DEP-LB-03 / DEFER-FT-03)。
  • 本页 Libra pin 八行由 FT-08 重钉到 FT-07 tip(v0.11.3 / 75ca189)。

Libra pin(FT-08;核对日 2026-09-18,本仓 revision:tag v0.11.3 / 提交 75ca189)

供 Libra 计划(../libra/docs/development/plan/plan-20260912.md)把 DEP-MB-04(delete / move)与 DEP-MB-05(四条 /tags*)覆盖的表面从 outgoing 改为 incoming,并吸收 plan-20260918 的破坏性 list 改 GET、is_directory、以及 ?path=。tree 与 create-entry 两行供重核 Libra 既有的 pin。行号以上述 revision 为准;本卡不改 ../libra/**。

# 方法与路径 成功 HTTP 鉴权(trunk / storage-only) 请求字段 成功 data 字段 本仓 file:line
1 GET /api/v1/tree?path=<dir> 200;路径不存在或指向非目录 404 不要求 Authorization query CodePreviewQuery:path(可选,缺省 /)、refs(可选,缺省空串 = 当前 tip;可为 40 位完整 commit SHA 或 tag 名);没有 oid 键(oid 属于另一条路由 GET /api/v1/file/tree 的 TreeQuery——api_router.rs 以 .route() 直挂、不进 OpenAPI,与 /tree 无关) TreeResponse:tree_items[](TreeBriefItem:name、path、content_type)与 file_tree(map:祖先路径 → FileTreeItem { tree_items, total_count },仅辅助结构,见上文 GET /tree 节);没有顶层 total_count preview_router.rs:266(path)/ :276 get_tree_info;git.rs:53 CodePreviewQuery、:155 TreeBriefItem、:514 TreeResponse
2 POST /api/v1/create-entry 200 push_auth(token:Bearer / Basic 密码栏;none:无 header) CreateEntryInfo:is_directory、name、path;可选 content、author_username、author_email、skip_build(默认 false)、mode(EditCLMode,serde snake_case 外部标签枚举:"force_create" 或 {"try_reuse": <cl_link 或 null>},缺省 try_reuse(null);只影响 Review 形态的 CL 复用,trunk 忽略) CreateEntryResult:commit_id、new_oid、path、cl_link(trunk 必为 null) preview_router.rs:112;git.rs:13 / :239
3 POST /api/v1/delete-entry 200 同上;鉴权 path = path(父目录) DeleteEntryInfo:path、name;可选 is_directory(bool,default_is_directory → true;false 删文件)、author_username、skip_build(默认 false;Libra 送 true) DeleteEntryResult:commit_id、path、cl_link(trunk null);无 new_oid preview_router.rs:138;git.rs:253 / :280
4 POST /api/v1/move-entry 200 同上;from_path 与 to_path 各鉴权一次 MoveEntryInfo:from_path、from_name、to_path、to_name;可选 is_directory(同 delete:缺省 true;false 移文件并保留同一 blob oid)、author_username、skip_build(默认 false;Libra 送 true) MoveEntryResult:commit_id、from_path、to_path、cl_link(trunk null);无 new_oid preview_router.rs:166;git.rs:293 / :339
5 POST /api/v1/tags 200(不是 201) push_auth;鉴权 path = path_context(缺省 /) CreateTagRequest:name;可选 target(alias target_commit)、path_context、tagger_name、tagger_email、message(非空 message 即注解 tag,空串按轻量 tag 处理) TagResponse:name、tag_id、object_id、object_type、tagger、message、created_at(全为字符串) tag_router.rs:165;tag.rs:19 / :37
6 GET /api/v1/tags/list 200 不要求 Authorization query TagListQuery 三键必填:page(u64,从 1 起)、per_page(u64,必须 ≥ 1)、path(path context;空 / 空白视同 /)。缺键或非法数字 → axum 0.8 Query 400 纯文本。per_page=0 → handler 400 + CommonResult。对该路径 POST → 405 TagListResponse:total、items[](TagResponse) tag_router.rs:226;tag.rs:60
7 GET /api/v1/tags/{name}?path= 200;该 path 下不存在 404 不要求 Authorization 路径参数 name;可选 query ?path=(TagPathQuery;省略或空 / 空白 = /)。查找键 (path, name) TagResponse tag_router.rs:283;tag.rs:69
8 DELETE /api/v1/tags/{name}?path= 200;该 path 下不存在 404 push_auth;鉴权 path = 选择器 path(省略或空 = /)。覆盖 /project 的 token 可删该 path 的 tag;删 root tag 仍需覆盖 / 路径参数 name;可选 query ?path=(同 get) DeleteTagResponse:deleted_tag、message tag_router.rs:332;tag.rs:82

Libra 必须按以下事实实现,不得反向假设:

  • 挂载锚点(Libra 的 incoming 条件要求):delete-entry / move-entry 登记在 storage-only 与 Review 共用的 write_routers(preview_router.rs:57-63);四条 /tags* 由 storage_only_routers_with merge tag_router::routers()(api_router.rs:83),三份 OpenAPI 锁与运行时 /api/openapi.json 都含它们。list 在 OpenAPI 上只登记 GET。
  • list 是唯一 GET;page、per_page、path 三键必填。POST /tags/list → 405。per_page=0 是 handler 400 + CommonResult(不再 panic)。缺 query 键是 extractor 400 纯文本,不是 422。
  • get / delete 有 path 选择器 ?path=(省略或空 = /)。查找、create 重名与 list 注解过滤都是 (path, name)(隔离已落地)。delete 的鉴权 path = 该选择器,不是固定 /。
  • trunk / storage-only 上目录写与 tag 写都不建 CL:目录写(create / delete / move)回应的 cl_link 必为 null,tag 回应没有 cl_link 键;delete / move 的回应没有 new_oid,以 commit_id 为凭。
  • handler 的成功回应与应用错误(ApiError)外层是 CommonResult(req_result、data、err_message);err_message 不含 [code:NNN] 前缀。例外: axum Query / Json extractor 的拒绝在 handler 之前以纯文本回应,不经 ApiError、没有 CommonResult 外层:缺 query 键 / 非法数字(如 GET /tags/list 少 path)→ 400;JSON 语法非法 → 400;Content-Type 不对 → 415。Libra 解析前先看状态码,且 400 既可能是应用错误(带外层)也可能是 extractor 的纯文本拒绝。
  • 错误码分类:401(无凭据 / 凭据不识别)、403(token paths 未覆盖鉴权 path)、400(校验 / 目标错误 / 错 mode;或 extractor 的纯文本拒绝,见上)、404(父 tree 完全无名 / 该 path 下 tag 不存在)、405(POST /tags/list)、409(ImportRepo 下的 delete / move;ImportRepo 在 edit-save 或 tag 写处理期间被清理时的 IMPORT_REPO_REMOVED)。另: create-entry 的目录重名与 is_directory=false 缺 content 今日是 500(见「错误映射」),不得按 400 假设。

建议 Libra 侧动作:把 DEP-MB-04 / DEP-MB-05 由 outgoing 改为 incoming(引用本节的 revision 与行号),并据此重核 MB-07 与 MB-10(list 必须改 GET + 三 query;delete/move 接受 is_directory;get/delete 带 ?path=;delete 鉴权 path = 选择器;cl_link / new_oid 不变)。

相关文档

  • ../user-guide.zh.md —— 用户可见规则:Tag 通过 HTTP API 管理,Git 客户端不能推送 Tag。本页以 API 契约和 OpenAPI 定义为准。
  • ../plan/plan-20260918.md —— 文件删移、GET list、path 级 tag 跟进
  • ../deploy-trunk.md —— storage-only 运维手册与产品 API 写契约
  • ../plan/plan-20260904.md —— create-entry / edit/save + push_auth + land_api_tip_push 的来源计划
  • integration.md —— 集成测试与黑盒矩阵
  • ../errors.md —— 错误码(ImportRepoError:IMPORT_REPO_PATH_INVALID、IMPORT_REPO_HAS_CHILDREN、IMPORT_REPO_CLEANUP_NOT_FOUND)
  • trunk-push.md —— ImportRepo 的 detach、清扫与存活栅栏