Merge pull request 'feat: 新增 dataset 数据集管理快捷命令(list/view/create/update/delete-attachment)' (#243) from luwanzhou/gitlink-cli:feat/dataset-shortcuts into master

This commit is contained in:
wbtiger 2026-06-14 18:51:49 +08:00
commit 789db5e5ae
9 changed files with 615 additions and 1 deletions

View File

@ -107,6 +107,7 @@ The official [GitLink](https://www.gitlink.org.cn) CLI tool — built for humans
| ⚙️ Pipeline | Run, inspect, enable, disable, delete pipeline workflows and logs |
| 🔔 Webhook | Manage repo webhooks and test deliveries |
| 🔍 Search | Search repositories, users |
| 📊 Dataset | Query research datasets by project |
| 👤 User | View user profiles and info |
| 📊 Profile | User ability, role, major, activity, and contribution statistics |
| 📋 PM | Sprint management, kanban boards, weekly reports |
@ -610,6 +611,32 @@ Safety:
- `workflow +pr-summary` does not comment, approve, reject, or merge pull requests.
- `workflow +repo-report` aggregates health, issue triage, and PR review summary signals without remote writes.
### Dataset
`dataset` manages and queries GitLink research datasets (title, description,
paper content, license, owning project).
```bash
# List datasets for one or more projects (by numeric project ID)
gitlink-cli dataset +list --ids 5988
# View a repository's dataset and attachments
gitlink-cli dataset +view --owner Gitlink --repo forgeplus
# Create / update a repository's dataset (preview first with --dry-run)
gitlink-cli dataset +create --owner me --repo proj -t "My dataset" -d "..." --license-id 359 --dry-run
gitlink-cli dataset +update --owner me --repo proj -t "My dataset" -d "updated"
# Delete a dataset attachment (destructive: preview, then confirm with --yes)
gitlink-cli dataset +delete-attachment --owner me --repo proj --uuid <uuid> --dry-run
gitlink-cli dataset +delete-attachment --owner me --repo proj --uuid <uuid> --yes
```
> Note: `dataset +list` (platform dataset query) is verified on production
> gitlink.org.cn. The per-repo `+view`/`+create`/`+update` routes follow the
> published OpenAPI contract but are not yet deployed on production (they return
> 404 there); they will work once the platform enables them.
### Raw API
For endpoints not covered by shortcuts, use the Raw API directly:

View File

@ -106,6 +106,7 @@
| 🔧 CI | 查看构建、日志、CI/CD 操作 |
| ⚙️ Pipeline | 运行、查看、启停、删除流水线工作流并查询日志 |
| 🔍 搜索 | 搜索仓库、用户 |
| 📊 数据集 | 按项目查询科研数据集 |
| 👤 用户 | 查看用户资料和信息 |
| 📊 画像 | 用户开发能力、角色定位、专业定位、近期活动、贡献热力图统计 |
| 📋 项目管理 | Sprint 管理、看板、周报 |
@ -489,6 +490,27 @@ gitlink-cli profile +activity
gitlink-cli profile +contribution --user zhangsan --year 2025
```
### 数据集
`dataset` 管理并查询 GitLink 科研数据集(标题、描述、论文内容、许可证、所属项目)。
```bash
# 按数字项目 ID 列出一个或多个项目的数据集
gitlink-cli dataset +list --ids 5988
# 查看仓库的数据集及其附件
gitlink-cli dataset +view --owner Gitlink --repo forgeplus
# 创建 / 更新仓库数据集(先用 --dry-run 预览)
gitlink-cli dataset +create --owner me --repo proj -t "我的数据集" -d "..." --license-id 359 --dry-run
gitlink-cli dataset +update --owner me --repo proj -t "我的数据集" -d "更新"
# 删除数据集附件(破坏性:先预览,再用 --yes 确认)
gitlink-cli dataset +delete-attachment --owner me --repo proj --uuid <uuid> --dry-run
gitlink-cli dataset +delete-attachment --owner me --repo proj --uuid <uuid> --yes
```
> 注意:`dataset +list`(平台数据集查询)已在生产 gitlink.org.cn 验证可用。按仓库的 `+view`/`+create`/`+update` 遵循已发布的 OpenAPI 契约,但生产环境尚未部署(当前返回 404待平台上线后即可生效。
### Raw API
Shortcuts 未覆盖的接口可通过 Raw API 直接调用:

View File

@ -0,0 +1,70 @@
# Dataset Shortcuts
## Summary
Adds a new `dataset` shortcut group for managing and querying GitLink research
datasets, which previously had no shortcut coverage. Datasets carry
research-oriented metadata (title, description, `paper_content`, license, owning
project) that is valuable for research/scientometric scenarios.
## Commands
| Command | Purpose | Endpoint |
|---------|---------|----------|
| `gitlink-cli dataset +view` | View a repository's dataset and attachments | `GET /v1/{owner}/{repo}/dataset` |
| `gitlink-cli dataset +list --ids <ids>` | List datasets for one or more projects | `GET /v1/project_datasets` |
| `gitlink-cli dataset +create` | Create a repository's dataset | `POST /v1/{owner}/{repo}/dataset` |
| `gitlink-cli dataset +update` | Update a repository's dataset | `PUT /v1/{owner}/{repo}/dataset` |
| `gitlink-cli dataset +delete-attachment --uuid <uuid>` | Delete a dataset attachment | `DELETE /attachments/{uuid}` |
## Behaviour
- `+view` paginates attachments via `--page`/`--limit`.
- `+list --ids 1,2,3` queries datasets by comma-separated numeric project IDs;
IDs are validated client-side before the request.
- `+create`/`+update` send `title`, `description`, optional `license-id`
(validated as a positive integer) and `paper-content`. Both support
`--dry-run` to preview the request body without writing.
- `+delete-attachment` is destructive: it requires `--dry-run` preview or an
explicit `--yes` confirmation before issuing the DELETE.
## Production status (verified)
Verified against production `gitlink.org.cn`:
- `GET /v1/project_datasets` (`+list`) — **available and verified** (e.g.
`--ids 5988` returns the forgeplus dataset).
- The per-repository routes `/v1/{owner}/{repo}/dataset`
(`+view`/`+create`/`+update`) currently return `404` on production www
(confirmed even for a repository's own owner; not reachable on the gateway
host either). They follow the documented contract and are expected to work
once the platform deploys these routes. `+delete-attachment` targets the
generic attachments endpoint.
The commands and request shapes match the published OpenAPI spec, so they are
ready the moment the routes go live; unit tests exercise every command against a
mock server.
## Tests
Unit tests cover the view path with pagination, `--ids` normalization and
validation, create/update request bodies and `license-id` validation, dry-run
previews, and the destructive-delete confirmation guard (`--yes`).
## 中文说明
### 变更内容
- 新增 `dataset` 命令组:`+view`、`+list`、`+create`、`+update`、`+delete-attachment`。
- `+view` 支持 `--page`/`--limit` 对附件分页;`+list --ids` 按项目 ID 查询。
- `+create`/`+update` 发送 `title`/`description`/可选 `license-id`/`paper-content`,均支持 `--dry-run` 预览。
- `+delete-attachment` 为破坏性操作,需 `--dry-run` 预览或显式 `--yes` 确认。
### 生产状态(已验证)
- `GET /v1/project_datasets``+list`)在生产**可用并已验证**(如 `--ids 5988` 返回 forgeplus 数据集)。
- `/v1/{owner}/{repo}/dataset``+view`/`+create`/`+update` 当前在生产 www 返回 `404`(即使对仓库 owner 也如此gateway 也未托管)。实现严格遵循已发布的 OpenAPI 契约,待平台部署后即可生效;单测以 mock 覆盖全部命令。
### 相对文档契约的增强
双语 i18n 帮助文案、写操作 `--dry-run` 预览、破坏性删除 `--yes` 二次确认、`license-id` 正整数校验。

View File

@ -21,6 +21,17 @@
"cmd.config.list.short": "List all configuration values",
"cmd.config.set.short": "Set a configuration value",
"cmd.config.short": "Manage gitlink-cli configuration",
"cmd.dataset.create.long": "Create the dataset of a repository with a title, description, optional license and research paper content.",
"cmd.dataset.create.short": "Create a repository's dataset",
"cmd.dataset.delete_attachment.long": "Delete a dataset attachment by its UUID. This is destructive: preview with --dry-run, then pass --yes to confirm.",
"cmd.dataset.delete_attachment.short": "Delete a dataset attachment by UUID",
"cmd.dataset.list.long": "List datasets for one or more GitLink projects by their numeric project IDs.",
"cmd.dataset.list.short": "List datasets by project IDs",
"cmd.dataset.short": "Dataset operations",
"cmd.dataset.update.long": "Update the dataset of a repository (title, description, optional license and research paper content).",
"cmd.dataset.update.short": "Update a repository's dataset",
"cmd.dataset.view.long": "View a repository's dataset and its attachments. Use --page/--limit to paginate attachments.",
"cmd.dataset.view.short": "View a repository's dataset",
"cmd.doctor.long": "Run local diagnostics for gitlink-cli configuration, authentication, repository context and API connectivity.",
"cmd.doctor.short": "Diagnose gitlink-cli environment problems",
"cmd.issue.batch_close.long": "Close filtered issues in bulk.\n\nThis command defaults to dry-run mode and only prints matching issues.\nPass --yes to execute remote close operations. Use restrictive filters and a small limit.\n\nExamples:\n gitlink-cli issue +batch-close --owner Gitlink --repo gitlink-cli --older-than-days 60 --limit 20\n gitlink-cli issue +batch-close --owner Gitlink --repo gitlink-cli --older-than-days 60 --limit 20 --yes",
@ -99,6 +110,7 @@
"error.auth.store_token_failed": "failed to store token: {message}",
"error.auth.token_empty": "token cannot be empty",
"error.config.save_failed": "failed to save config: {message}",
"error.dataset.delete_confirm": "dataset attachment deletion is destructive; run --dry-run first, then pass --yes to confirm",
"error.missing_required_flag": "required flag --{name} is missing",
"error.profile.user_required": "could not determine target user; pass --user or run gitlink-cli auth login",
"error.unsupported_language": "unsupported language: {lang}",
@ -118,6 +130,17 @@
"flag.ci.stage": "Stage number",
"flag.ci.step": "Step number",
"flag.comment.body": "Comment body",
"flag.dataset.description": "Dataset description",
"flag.dataset.dry_run": "Preview the request without writing the dataset",
"flag.dataset.dry_run_delete": "Preview the request without deleting the attachment",
"flag.dataset.ids": "Comma-separated project IDs to query datasets for",
"flag.dataset.license_id": "License ID",
"flag.dataset.limit": "Attachment page size",
"flag.dataset.page": "Attachment page number",
"flag.dataset.paper_content": "Research paper content",
"flag.dataset.title": "Dataset title",
"flag.dataset.uuid": "Attachment UUID",
"flag.dataset.yes": "Confirm the destructive deletion",
"flag.debug": "Enable debug output",
"flag.description": "Description",
"flag.doctor.skip_network": "Skip authenticated API connectivity checks",

View File

@ -21,6 +21,17 @@
"cmd.config.list.short": "列出所有配置项",
"cmd.config.set.short": "设置配置项",
"cmd.config.short": "管理 gitlink-cli 配置",
"cmd.dataset.create.long": "为仓库创建数据集,包含标题、描述、可选许可证和研究论文内容。",
"cmd.dataset.create.short": "创建仓库数据集",
"cmd.dataset.delete_attachment.long": "按 UUID 删除数据集附件。该操作具有破坏性:先用 --dry-run 预览,再传 --yes 确认。",
"cmd.dataset.delete_attachment.short": "按 UUID 删除数据集附件",
"cmd.dataset.list.long": "按数字项目 ID 列出一个或多个 GitLink 项目的数据集。",
"cmd.dataset.list.short": "按项目 ID 列出数据集",
"cmd.dataset.short": "数据集操作",
"cmd.dataset.update.long": "更新仓库数据集(标题、描述、可选许可证和研究论文内容)。",
"cmd.dataset.update.short": "更新仓库数据集",
"cmd.dataset.view.long": "查看仓库的数据集及其附件。用 --page/--limit 对附件分页。",
"cmd.dataset.view.short": "查看仓库数据集",
"cmd.doctor.long": "诊断 gitlink-cli 的配置、认证、仓库上下文和 API 连通性问题。",
"cmd.doctor.short": "诊断 gitlink-cli 环境问题",
"cmd.issue.batch_close.long": "批量关闭筛选后的议题。\n\n该命令默认处于 dry-run 模式,只打印匹配的议题。\n传入 --yes 后执行远端关闭操作。请使用严格筛选条件和较小 limit。\n\n示例\n gitlink-cli issue +batch-close --owner Gitlink --repo gitlink-cli --older-than-days 60 --limit 20\n gitlink-cli issue +batch-close --owner Gitlink --repo gitlink-cli --older-than-days 60 --limit 20 --yes",
@ -99,6 +110,7 @@
"error.auth.store_token_failed": "保存 Token 失败:{message}",
"error.auth.token_empty": "Token 不能为空",
"error.config.save_failed": "保存配置失败:{message}",
"error.dataset.delete_confirm": "删除数据集附件具有破坏性;请先 --dry-run 预览,再传 --yes 确认",
"error.missing_required_flag": "缺少必需参数 --{name}",
"error.profile.user_required": "无法确定目标用户;请通过 --user 指定,或先运行 gitlink-cli auth login 登录",
"error.unsupported_language": "不支持的语言:{lang}",
@ -118,6 +130,17 @@
"flag.ci.stage": "阶段编号",
"flag.ci.step": "步骤编号",
"flag.comment.body": "评论内容",
"flag.dataset.description": "数据集描述",
"flag.dataset.dry_run": "预览请求,不写入数据集",
"flag.dataset.dry_run_delete": "预览请求,不删除附件",
"flag.dataset.ids": "用于查询数据集的项目 ID逗号分隔",
"flag.dataset.license_id": "许可证 ID",
"flag.dataset.limit": "附件每页数量",
"flag.dataset.page": "附件页码",
"flag.dataset.paper_content": "研究论文内容",
"flag.dataset.title": "数据集标题",
"flag.dataset.uuid": "附件 UUID",
"flag.dataset.yes": "确认执行破坏性删除",
"flag.debug": "启用调试输出",
"flag.description": "描述",
"flag.doctor.skip_network": "跳过需要访问 GitLink 的认证连通性检查",

View File

@ -0,0 +1,218 @@
// Package dataset implements shortcuts for managing and querying GitLink
// research datasets: per-repository dataset detail/create/update, the
// platform-wide dataset query, and dataset attachment deletion.
//
// Datasets carry research-oriented metadata (title, description, paper_content,
// license, owning project) that is valuable for research/scientometric
// scenarios. Per-repository CRUD wraps /api/v1/{owner}/{repo}/dataset; the
// platform query wraps /api/v1/project_datasets.
package dataset
import (
"fmt"
"net/url"
"strconv"
"strings"
"github.com/gitlink-org/gitlink-cli/internal/i18n"
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
)
// Shortcuts returns dataset management and query shortcuts.
func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
tr := shortcutTranslator(translators...)
writeFlags := []common.Flag{
{Name: "title", Short: "t", Usage: tr.T("flag.dataset.title"), Required: true},
{Name: "description", Short: "d", Usage: tr.T("flag.dataset.description"), Required: true},
{Name: "license-id", Usage: tr.T("flag.dataset.license_id")},
{Name: "paper-content", Usage: tr.T("flag.dataset.paper_content")},
{Name: "dry-run", Usage: tr.T("flag.dataset.dry_run"), Bool: true, Default: "false"},
}
return []*common.Shortcut{
{
Name: "view",
Description: tr.T("cmd.dataset.view.short"),
Long: tr.T("cmd.dataset.view.long"),
Flags: []common.Flag{
{Name: "page", Short: "p", Usage: tr.T("flag.dataset.page"), Default: "1"},
{Name: "limit", Short: "l", Usage: tr.T("flag.dataset.limit"), Default: "20"},
},
Run: func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
q := url.Values{}
setIfPresent(q, "page", ctx.Arg("page"))
setIfPresent(q, "limit", ctx.Arg("limit"))
env, err := ctx.CallAPIWithQuery("GET", repoDatasetPath(ctx), q)
if err != nil {
return err
}
return ctx.Output(env)
},
},
{
Name: "list",
Description: tr.T("cmd.dataset.list.short"),
Long: tr.T("cmd.dataset.list.long"),
Flags: []common.Flag{
{Name: "ids", Usage: tr.T("flag.dataset.ids"), Required: true},
},
Run: func(ctx *common.RuntimeContext) error {
ids, err := ctx.RequireArg("ids")
if err != nil {
return err
}
normalized, err := normalizeIDs(ids)
if err != nil {
return err
}
q := url.Values{}
q.Set("ids", normalized)
env, err := ctx.CallAPIWithQuery("GET", "/v1/project_datasets", q)
if err != nil {
return err
}
return ctx.Output(env)
},
},
{
Name: "create",
Description: tr.T("cmd.dataset.create.short"),
Long: tr.T("cmd.dataset.create.long"),
Flags: writeFlags,
Run: runWrite("POST", "create_dataset"),
},
{
Name: "update",
Description: tr.T("cmd.dataset.update.short"),
Long: tr.T("cmd.dataset.update.long"),
Flags: writeFlags,
Run: runWrite("PUT", "update_dataset"),
},
{
Name: "delete-attachment",
Description: tr.T("cmd.dataset.delete_attachment.short"),
Long: tr.T("cmd.dataset.delete_attachment.long"),
Flags: []common.Flag{
{Name: "uuid", Short: "u", Usage: tr.T("flag.dataset.uuid"), Required: true},
{Name: "dry-run", Usage: tr.T("flag.dataset.dry_run_delete"), Bool: true, Default: "false"},
{Name: "yes", Usage: tr.T("flag.dataset.yes"), Bool: true, Default: "false"},
},
Run: runDeleteAttachment,
},
}
}
// runWrite builds the create/update handlers, which share the same request body.
func runWrite(method, action string) func(ctx *common.RuntimeContext) error {
return func(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
body, err := datasetBody(ctx)
if err != nil {
return err
}
path := repoDatasetPath(ctx)
if ctx.Arg("dry-run") == "true" {
return ctx.OutputData(map[string]interface{}{
"dry_run": true, "action": action, "method": method, "path": path, "body": body,
})
}
env, err := ctx.CallAPI(method, path, body)
if err != nil {
return err
}
return ctx.Output(env)
}
}
func runDeleteAttachment(ctx *common.RuntimeContext) error {
uuid, err := ctx.RequireArg("uuid")
if err != nil {
return err
}
uuid = strings.TrimSpace(uuid)
path := fmt.Sprintf("/attachments/%s", uuid)
if ctx.Arg("dry-run") == "true" {
return ctx.OutputData(map[string]interface{}{
"dry_run": true, "action": "delete_dataset_attachment", "method": "DELETE", "path": path, "uuid": uuid,
})
}
if ctx.Arg("yes") != "true" {
return fmt.Errorf("%s", ctx.Tr.T("error.dataset.delete_confirm"))
}
env, err := ctx.CallAPI("DELETE", path, nil)
if err != nil {
return err
}
return ctx.Output(env)
}
// datasetBody builds the create/update request body and validates inputs.
func datasetBody(ctx *common.RuntimeContext) (map[string]interface{}, error) {
title, err := ctx.RequireArg("title")
if err != nil {
return nil, err
}
description, err := ctx.RequireArg("description")
if err != nil {
return nil, err
}
body := map[string]interface{}{
"title": strings.TrimSpace(title),
"description": strings.TrimSpace(description),
}
if v := strings.TrimSpace(ctx.Arg("license-id")); v != "" {
n, err := strconv.Atoi(v)
if err != nil || n <= 0 {
return nil, fmt.Errorf("invalid --license-id %q: use a positive integer", v)
}
body["license_id"] = n
}
if v := strings.TrimSpace(ctx.Arg("paper-content")); v != "" {
body["paper_content"] = v
}
return body, nil
}
func repoDatasetPath(ctx *common.RuntimeContext) string {
return "/v1" + ctx.RepoPath() + "/dataset"
}
// normalizeIDs validates a comma-separated list of positive integer project IDs.
func normalizeIDs(raw string) (string, error) {
parts := strings.Split(raw, ",")
cleaned := make([]string, 0, len(parts))
for _, part := range parts {
part = strings.TrimSpace(part)
if part == "" {
continue
}
n, err := strconv.ParseInt(part, 10, 64)
if err != nil || n <= 0 {
return "", fmt.Errorf("invalid --ids %q: use comma-separated positive project IDs", raw)
}
cleaned = append(cleaned, strconv.FormatInt(n, 10))
}
if len(cleaned) == 0 {
return "", fmt.Errorf("invalid --ids %q: provide at least one project ID", raw)
}
return strings.Join(cleaned, ","), nil
}
func setIfPresent(q url.Values, key, value string) {
if v := strings.TrimSpace(value); v != "" {
q.Set(key, v)
}
}
func shortcutTranslator(translators ...*i18n.Translator) *i18n.Translator {
if len(translators) > 0 && translators[0] != nil {
return translators[0]
}
return i18n.Default()
}

View File

@ -0,0 +1,228 @@
package dataset
import (
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"testing"
"github.com/gitlink-org/gitlink-cli/internal/client"
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
)
func runShortcut(t *testing.T, server *httptest.Server, name string, args map[string]string) error {
t.Helper()
shortcut := findShortcut(t, name)
ctx := &common.RuntimeContext{
Client: &client.Client{HTTP: server.Client(), BaseURL: server.URL},
Owner: "alice",
Repo: "demo",
Format: "json",
Args: args,
}
return shortcut.Run(ctx)
}
func findShortcut(t *testing.T, name string) *common.Shortcut {
t.Helper()
for _, s := range Shortcuts() {
if s.Name == name {
return s
}
}
t.Fatalf("shortcut %q not found", name)
return nil
}
func writeJSON(w http.ResponseWriter, v interface{}) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(v)
}
func decodeBody(t *testing.T, r *http.Request) map[string]interface{} {
t.Helper()
data, _ := io.ReadAll(r.Body)
var m map[string]interface{}
if err := json.Unmarshal(data, &m); err != nil {
t.Fatalf("decode body: %v (raw: %s)", err, string(data))
}
return m
}
// --- view ---
func TestDatasetView(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v1/alice/demo/dataset.json" {
t.Fatalf("unexpected path: %s", r.URL.Path)
}
if got := r.URL.Query().Get("page"); got != "2" {
t.Fatalf("page = %q, want 2", got)
}
if got := r.URL.Query().Get("limit"); got != "5" {
t.Fatalf("limit = %q, want 5", got)
}
writeJSON(w, map[string]interface{}{"id": float64(1), "attachments": []interface{}{}})
}))
defer server.Close()
if err := runShortcut(t, server, "view", map[string]string{"page": "2", "limit": "5"}); err != nil {
t.Fatalf("view failed: %v", err)
}
}
// --- list ---
func TestDatasetListNormalizesIDs(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v1/project_datasets.json" {
t.Fatalf("unexpected path: %s", r.URL.Path)
}
if got := r.URL.Query().Get("ids"); got != "1,2,3" {
t.Fatalf("ids = %q, want 1,2,3", got)
}
writeJSON(w, map[string]interface{}{"total_count": float64(0), "project_datasets": []interface{}{}})
}))
defer server.Close()
if err := runShortcut(t, server, "list", map[string]string{"ids": " 1, 2 ,3 "}); err != nil {
t.Fatalf("list failed: %v", err)
}
}
func TestDatasetListInvalidIDs(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Fatal("no API call expected for invalid ids")
}))
defer server.Close()
if err := runShortcut(t, server, "list", map[string]string{"ids": "abc"}); err == nil {
t.Fatal("expected error for non-numeric --ids")
}
}
// --- create ---
func TestDatasetCreate(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v1/alice/demo/dataset.json" {
t.Fatalf("unexpected path: %s", r.URL.Path)
}
if r.Method != http.MethodPost {
t.Fatalf("method = %s, want POST", r.Method)
}
body := decodeBody(t, r)
if body["title"] != "DS" || body["description"] != "desc" {
t.Fatalf("unexpected body: %v", body)
}
if body["license_id"] != float64(359) {
t.Fatalf("license_id = %v, want 359", body["license_id"])
}
writeJSON(w, map[string]interface{}{"status": float64(0), "message": "success"})
}))
defer server.Close()
args := map[string]string{"title": "DS", "description": "desc", "license-id": "359", "paper-content": "x"}
if err := runShortcut(t, server, "create", args); err != nil {
t.Fatalf("create failed: %v", err)
}
}
func TestDatasetCreateMissingTitle(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Fatal("no API call expected without required flags")
}))
defer server.Close()
if err := runShortcut(t, server, "create", map[string]string{"description": "desc"}); err == nil {
t.Fatal("expected error for missing --title")
}
}
func TestDatasetCreateInvalidLicense(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Fatal("no API call expected for invalid license id")
}))
defer server.Close()
args := map[string]string{"title": "DS", "description": "desc", "license-id": "abc"}
if err := runShortcut(t, server, "create", args); err == nil {
t.Fatal("expected error for invalid --license-id")
}
}
func TestDatasetCreateDryRun(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Fatal("no API call expected in dry-run")
}))
defer server.Close()
args := map[string]string{"title": "DS", "description": "desc", "dry-run": "true"}
if err := runShortcut(t, server, "create", args); err != nil {
t.Fatalf("create dry-run failed: %v", err)
}
}
// --- update ---
func TestDatasetUpdate(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/v1/alice/demo/dataset.json" {
t.Fatalf("unexpected path: %s", r.URL.Path)
}
if r.Method != http.MethodPut {
t.Fatalf("method = %s, want PUT", r.Method)
}
writeJSON(w, map[string]interface{}{"status": float64(0), "message": "success"})
}))
defer server.Close()
args := map[string]string{"title": "DS2", "description": "desc2"}
if err := runShortcut(t, server, "update", args); err != nil {
t.Fatalf("update failed: %v", err)
}
}
// --- delete-attachment ---
func TestDatasetDeleteAttachmentDryRun(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Fatal("no API call expected in dry-run")
}))
defer server.Close()
args := map[string]string{"uuid": "abc-123", "dry-run": "true"}
if err := runShortcut(t, server, "delete-attachment", args); err != nil {
t.Fatalf("delete dry-run failed: %v", err)
}
}
func TestDatasetDeleteAttachmentRequiresConfirm(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Fatal("no API call expected without --yes")
}))
defer server.Close()
if err := runShortcut(t, server, "delete-attachment", map[string]string{"uuid": "abc-123"}); err == nil {
t.Fatal("expected error without --yes confirmation")
}
}
func TestDatasetDeleteAttachmentConfirmed(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/attachments/abc-123.json" {
t.Fatalf("unexpected path: %s", r.URL.Path)
}
if r.Method != http.MethodDelete {
t.Fatalf("method = %s, want DELETE", r.Method)
}
writeJSON(w, map[string]interface{}{"status": float64(0), "message": "删除成功"})
}))
defer server.Close()
args := map[string]string{"uuid": "abc-123", "yes": "true"}
if err := runShortcut(t, server, "delete-attachment", args); err != nil {
t.Fatalf("delete failed: %v", err)
}
}

View File

@ -8,6 +8,7 @@ import (
"github.com/gitlink-org/gitlink-cli/shortcuts/ci"
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
"github.com/gitlink-org/gitlink-cli/shortcuts/compare"
"github.com/gitlink-org/gitlink-cli/shortcuts/dataset"
"github.com/gitlink-org/gitlink-cli/shortcuts/health"
"github.com/gitlink-org/gitlink-cli/shortcuts/ignore"
"github.com/gitlink-org/gitlink-cli/shortcuts/issue"
@ -50,6 +51,7 @@ func RegisterAll(root *cobra.Command, translators ...*i18n.Translator) {
"search": search.Shortcuts(tr),
"ci": ci.Shortcuts(tr),
"compare": compare.Shortcuts(),
"dataset": dataset.Shortcuts(tr),
"webhook": webhook.Shortcuts(tr),
"health": health.Shortcuts(tr),
"ignore": ignore.Shortcuts(),
@ -73,6 +75,7 @@ func RegisterAll(root *cobra.Command, translators ...*i18n.Translator) {
"search": tr.T("cmd.search.short"),
"ci": tr.T("cmd.ci.short"),
"compare": "Compare branches, tags, or commits",
"dataset": tr.T("cmd.dataset.short"),
"webhook": tr.T("cmd.webhook.short"),
"health": "Project health data collection",
"ignore": tr.T("cmd.ignore.short"),

View File

@ -14,7 +14,7 @@ func TestRegisterAll(t *testing.T) {
"repo", "issue", "label", "license", "pr", "profile", "release", "branch",
"org", "user", "search", "ci", "workflow",
"compare", "member", "milestone", "pipeline", "webhook",
"health", "ignore",
"dataset", "health", "ignore",
}
groupSet := map[string]bool{}