本页是 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()(api_router.rs:83);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(省略或空 = /) |
公共前缀 /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 不改语义:
GET /tree、POST /create-entry。 - 新增产品写:
POST /delete-entry、POST /move-entry(改名 = 同 parent 的 move)。plan-20260918 用同一路径扩文件(is_directory=false)。 - 挂载 + 鉴权 + 文档对齐: 四条
/tags*。plan-20260918 把 list 改 GET,并为 get/delete 加?path=。 - 目录/文件变更是父目录 tree 改写后写新 commit,经
land_api_tip_push(trunk)或既有 CL 分支(Review)前进 tip;不是 Git delete command,也不走 CLapply_changes(ADR-LB-02 / ADR-FT-01)。
| 项 | 值 |
|---|---|
| 鉴权 | 无(不送 Authorization) |
| Query | path(可选,#[serde(default = "default_path")] → /)、refs(可选,#[serde(default)] → 空串 = 当前 tip) |
| 成功 | 200 + CommonResult<TreeResponse> |
data.tree_items[] 每项为 { name, path, content_type }(TreeBriefItem)。
data.file_tree是祖先辅助结构,不得当导航权威——导航只用tree_items。
| 字段 | 规则 |
|---|---|
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。
{
"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 上父目录为
/(即删除顶层目录)时,B0 拒绝根 tip 经 MonoWriteQueue 前进,返回 400(no non-root path tip under / for trunk API write);Review 形态则在/的 CL 上进行。 commit_id在 trunk 上是落地后的 tip;path只作回执。
{
"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 上因 B0 返回 400;Review 形态在该公共目录(或/)的 CL 上进行,与 create/delete 相同的政策分流。 - 鉴权:
from_path与to_path各调一次trunk_write_requester,任一失败(401/403)即拒绝,此时尚未读任何 tree。
| 情况 | 今日实况 / 落地要求 |
|---|---|
| 目录重复名 | 今日 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 下) |
409,import dir does not support delete entry |
| 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 下 | 409,import dir does not support move entry |
| 缺目录(delete / move 均已按此落地) | 404(ADR-LB-03)。注意这不是 GET /tree / create-entry 的今日行为:今日 GET /tree 对不存在的 path 返回 200 + tree_items: [](search_tree_by_path 取不到时返回 Ok(None),get_tree_info 再把它映射成 Ok(vec![])——tree_ops.rs:93/:98/:256),而 create-entry 会自动补建缺失的父层级而不是 404 |
| 鉴权失败 | 见「鉴权」 |
[code:NNN]前缀是本仓真正的 4xx 约定(mono_api_service.rs:1097/:1677/:1690/:1846都在用)。缺前缀的CustomError会静默变成 500 并丢失原文。该前缀只是服务端内部标记,不会出现在 wire 上。
IntoResponse(common/errors/api.rs:85-89)与map_ceres_error(:198-199)都会在写入err_message前把它剥掉。所以上表引号里的服务端字面量与调用方实际收到的err_message不同:例如 tag 重名的 wire 值是Tag 'foo' already exists,不含[code:400]。调用方不要按该前缀做字符串匹配。
服务层 MonoApiService 的 create_tag / list_tags / get_tag / delete_tag 已实现;LB-04 起 storage_only_routers_with(src/api/api_router.rs:83)merge 了 tag_router::routers()(tag_router.rs:20),四条路由在 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 仍然禁止(见 ../monorepo.md);tag 只能走本节 HTTP。
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与 ITtag_create_unauth_401(运行时/api/openapi.json)都断言200在、201不在。
落地后方法是 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 必须 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过滤。
两条路由都带查询键 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 为空串。
目录变更写复用既有 authorize_trunk_api_write(src/api/api_write_auth.rs),与 LFS / create-entry 同一威胁模型。
整节的形态前提: 这道闸仅在
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 既有面。
GET /tree、GET /tags/list、GET /tags/{name} —— 调用方不送、server 不要求 Authorization。
| 路由 | 状态 |
|---|---|
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 /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(ITtag_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 丢弃返回值) |
状态码分类(api_write_auth.rs:33-48):
- 凭据缺失,或提供了但查不到对应 push token → 401(两者都走
api_write_auth_challenge) - 凭据识别成功、但
paths未覆盖目标 path → 403 push_auth未配置(None)→ 401,fail-closed
错误响应、JSON 与 trace 不得回显 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 |
write_routers(preview_router.rs:57-63)同时挂在 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:175 映射成 StatusCode::CONFLICT。另有一个陷阱: map_ceres_error(common/errors/api.rs:194-208)只识别 400 与 404,其余一律 _ => ApiError::internal → 500,而它正是 tag_router.rs 的惯用写法。因此 delete/move 必须走裸 ?(From<E> for ApiError,如 preview_router.rs:121)那条路径;若用 map_ceres_error 包装,即使带了 [code:409] 仍会落成 500 并使该门失败。今日 create-entry 的同类拒绝(import_api_service.rs:56-64 的 CustomError("import dir does not support create entry"))没有前缀,因而落成 500 + "Internal server error"——见「错误映射」,delete/move 不得复制该缺陷。其 Git 多分支与客户端 tag 语义不受本计划影响。
目录变更成功后,对同一 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
targetcommit 是否属于该 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 计划(
../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 | 不要求 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:226(path)/ :233 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_withmergetag_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]前缀。例外: axumQuery/Jsonextractor 的拒绝在 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)。另: 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 不变)。
../monorepo.md—— 产品规则;tag 只能走 HTTP,Git 客户端禁 tag。该文的 API 表已对齐 GET。wire 以本页为准../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—— 集成测试与黑盒矩阵