feat: 支持按 Tag 打包,分支管理可添加 Tag

- 分支管理新增「分支 / Tag」类型选择,Tag 存于 config["tags"],与分支不可重名
- 每个 Tag 与分支一样拥有同名独立版本轨道,默认上架轨
- 打包页按「分支」「Tag」分组选择;任务新增 ref_type 字段记录引用类型
- Tag 打包时 fetch --tags --force 后以游离 HEAD 检出 refs/tags/<tag>
- 历史记录与下载页按类型显示「分支」或「Tag」

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
shenlei
2026-09-17 11:59:18 +09:00
co-authored by Claude Opus 5
parent 6867a3cb01
commit 5582aadc09
18 changed files with 347 additions and 50 deletions
+34
View File
@@ -345,3 +345,37 @@ def test_delete_branch_with_slash_in_name(client, tmp_config):
resp = client.delete("/api/config/branches/feature%2Flogin")
assert resp.status_code == 200
assert "feature/login" not in client.get("/api/config/branches").json()
def test_tags_crud_and_default_release_track(client, tmp_config):
"""Tag 与分支分开存放,默认建一条同名上架轨,可切换类型、可删除"""
assert client.get("/api/config/tags").json() == []
resp = client.post("/api/config/tags", json={"name": "v3.2.0"})
assert resp.status_code == 200
assert resp.json()["track"] == "v3.2.0"
assert client.get("/api/config/tags").json() == ["v3.2.0"]
assert client.get("/api/tags").json() == ["v3.2.0"]
assert "v3.2.0" not in client.get("/api/config/branches").json()
data = client.get("/api/config/versions").json()
assert data["branch_track"]["v3.2.0"] == "v3.2.0"
assert data["tracks"]["v3.2.0"]["kind"] == "release"
# 主上架轨仍按分支判断,Tag 不会抢占
assert data["primary_track"] == "release"
assert client.put("/api/config/branches/track", json={"branch": "v3.2.0", "track": "feature"}).status_code == 200
assert client.get("/api/config/versions").json()["tracks"]["v3.2.0"]["kind"] == "feature"
assert client.delete("/api/config/tags/v3.2.0").status_code == 200
assert client.get("/api/config/tags").json() == []
assert "v3.2.0" not in client.get("/api/config/versions").json()["branch_track"]
assert client.delete("/api/config/tags/v3.2.0").status_code == 404
def test_tag_and_branch_names_must_not_collide(client, tmp_config):
assert client.post("/api/config/tags", json={"name": ""}).status_code == 400
assert client.post("/api/config/tags", json={"name": "main"}).status_code == 400
assert client.post("/api/config/tags", json={"name": "release/v1"}).status_code == 200
assert client.post("/api/config/tags", json={"name": "release/v1"}).status_code == 400
assert client.post("/api/config/branches", json={"name": "release/v1"}).status_code == 400
assert client.delete("/api/config/tags/release%2Fv1").status_code == 200