feat(dataset): add dataset query shortcuts
Add a `dataset` shortcut group for querying GitLink research datasets,
which previously had no shortcut coverage:
- dataset +list --ids <ids> -> GET /v1/project_datasets
- dataset +view -> GET /v1/project_datasets (project ID
resolved from --owner/--repo)
Only the platform-wide query endpoint is available on production
gitlink.org.cn; the documented per-repo dataset CRUD routes return 404
there (verified against the API), so this group wraps the query endpoint
for both listing and per-repo viewing.
Includes unit tests, bilingual (en-US/zh-CN) i18n help text, README
updates, and a change note.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
52b7093846
commit
7f731eb04f
17
README.md
17
README.md
|
|
@ -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 |
|
||||
| 📋 PM | Sprint management, kanban boards, weekly reports |
|
||||
| 🤖 Workflow | AI-powered issue triage, PR review, release notes |
|
||||
|
|
@ -576,6 +577,22 @@ 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` 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 (project ID resolved from --owner/--repo)
|
||||
gitlink-cli dataset +view --owner Gitlink --repo forgeplus
|
||||
```
|
||||
|
||||
> Note: only the platform-wide dataset query endpoint is available on production
|
||||
> gitlink.org.cn; the per-repo dataset CRUD routes are not yet deployed there.
|
||||
|
||||
### Raw API
|
||||
|
||||
For endpoints not covered by shortcuts, use the Raw API directly:
|
||||
|
|
|
|||
|
|
@ -106,6 +106,7 @@
|
|||
| 🔧 CI | 查看构建、日志、CI/CD 操作 |
|
||||
| ⚙️ Pipeline | 运行、查看、启停、删除流水线工作流并查询日志 |
|
||||
| 🔍 搜索 | 搜索仓库、用户 |
|
||||
| 📊 数据集 | 按项目查询科研数据集 |
|
||||
| 👤 用户 | 查看用户资料和信息 |
|
||||
| 📋 项目管理 | Sprint 管理、看板、周报 |
|
||||
| 🤖 工作流 | AI 驱动的 Issue 分类、PR Review、Release Notes |
|
||||
|
|
@ -455,6 +456,20 @@ gitlink-cli search +repos -k "machine learning"
|
|||
gitlink-cli search +users -k "zhangsan"
|
||||
```
|
||||
|
||||
### 数据集
|
||||
|
||||
`dataset` 查询 GitLink 科研数据集(标题、描述、论文内容、许可证、所属项目)。
|
||||
|
||||
```bash
|
||||
# 按数字项目 ID 列出一个或多个项目的数据集
|
||||
gitlink-cli dataset +list --ids 5988
|
||||
|
||||
# 查看仓库的数据集(project_id 从 --owner/--repo 解析)
|
||||
gitlink-cli dataset +view --owner Gitlink --repo forgeplus
|
||||
```
|
||||
|
||||
> 注意:生产环境 gitlink.org.cn 仅提供平台级数据集查询端点;按仓库的数据集增删改查路由尚未在生产部署。
|
||||
|
||||
### Raw API
|
||||
|
||||
Shortcuts 未覆盖的接口可通过 Raw API 直接调用:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,55 @@
|
|||
# Dataset Shortcuts
|
||||
|
||||
## Summary
|
||||
|
||||
Adds a new `dataset` shortcut group for 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 +list --ids <ids>` | List datasets for one or more projects | `GET /v1/project_datasets` |
|
||||
| `gitlink-cli dataset +view` | View a repository's dataset | `GET /v1/project_datasets` (project ID resolved from `--owner/--repo`) |
|
||||
|
||||
## Behaviour
|
||||
|
||||
- `dataset +list --ids 1,2,3` queries datasets by comma-separated numeric
|
||||
project IDs. IDs are validated client-side before the request.
|
||||
- `dataset +view --owner X --repo Y` resolves the repository's numeric project
|
||||
ID from the repository info endpoint, then queries the dataset for that
|
||||
project. Pass `--project-id` to skip resolution.
|
||||
|
||||
## Scope note (verified against production)
|
||||
|
||||
The dataset CRUD routes documented under `/api/v1/{owner}/{repo}/dataset`
|
||||
(`POST`/`PUT`/`GET`) are **not deployed on the production `gitlink.org.cn`
|
||||
host** — they return `404 您访问的页面不存在` even for a repository's owner. Only
|
||||
the platform-wide query endpoint `GET /api/v1/project_datasets` is available in
|
||||
production, so this group wraps that endpoint for both listing and per-repo
|
||||
viewing. Create/update/attachment-delete can be added once the corresponding
|
||||
routes are live on production.
|
||||
|
||||
## Tests
|
||||
|
||||
Unit tests cover `--ids` normalization (whitespace, ordering) and validation,
|
||||
the missing/invalid `--ids` guards, project ID auto-resolution from repo info
|
||||
for `+view`, explicit/invalid `--project-id`, and HTTP error handling.
|
||||
|
||||
## 中文说明
|
||||
|
||||
### 变更内容
|
||||
|
||||
- 新增 `dataset` 命令组:
|
||||
- `dataset +list --ids 1,2,3` 按数字项目 ID 查询数据集
|
||||
- `dataset +view --owner X --repo Y` 自动解析仓库 project_id 后查询该仓库数据集(可用 `--project-id` 跳过解析)
|
||||
- 数据集含 `paper_content`、license、所属项目等科研相关元数据,服务科研数据发现场景。
|
||||
|
||||
### 范围说明(已对生产环境验证)
|
||||
|
||||
文档中 `/api/v1/{owner}/{repo}/dataset` 的增删改查路由在生产 `gitlink.org.cn`
|
||||
**未部署**(即使对仓库 owner 也返回 `404 页面不存在`)。生产可用的只有平台级查询
|
||||
端点 `GET /api/v1/project_datasets`,故本命令组基于该端点实现列表与按仓库查看。待
|
||||
对应路由在生产上线后,可补充创建/更新/附件删除。
|
||||
|
|
@ -21,6 +21,11 @@
|
|||
"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.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.view.long": "View the dataset of a repository. The project ID is resolved from --owner/--repo, or pass --project-id.",
|
||||
"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",
|
||||
|
|
@ -90,13 +95,13 @@
|
|||
"error.config.save_failed": "failed to save config: {message}",
|
||||
"error.missing_required_flag": "required flag --{name} is missing",
|
||||
"error.unsupported_language": "unsupported language: {lang}",
|
||||
"flag.api.body": "Request body (JSON string)",
|
||||
"flag.api.body_file": "Read request body JSON from a file",
|
||||
"flag.api.body_stdin": "Read request body JSON from stdin",
|
||||
"flag.api.batch_continue_on_error": "Continue running remaining batch requests after a failure",
|
||||
"flag.api.batch_dry_run": "Preview batch requests without sending remote requests",
|
||||
"flag.api.batch_file": "Read an API batch plan from a JSON file",
|
||||
"flag.api.batch_var": "Override a batch template variable (key=value, repeatable)",
|
||||
"flag.api.body": "Request body (JSON string)",
|
||||
"flag.api.body_file": "Read request body JSON from a file",
|
||||
"flag.api.body_stdin": "Read request body JSON from stdin",
|
||||
"flag.api.header": "Additional headers (key:value)",
|
||||
"flag.api.query": "Query parameters (key=val&key2=val2)",
|
||||
"flag.auth.token": "Login by pasting an existing token",
|
||||
|
|
@ -106,6 +111,8 @@
|
|||
"flag.ci.stage": "Stage number",
|
||||
"flag.ci.step": "Step number",
|
||||
"flag.comment.body": "Comment body",
|
||||
"flag.dataset.ids": "Comma-separated project IDs to query datasets for",
|
||||
"flag.dataset.project_id": "GitLink project ID. If omitted, it is resolved from --owner/--repo.",
|
||||
"flag.debug": "Enable debug output",
|
||||
"flag.description": "Description",
|
||||
"flag.doctor.skip_network": "Skip authenticated API connectivity checks",
|
||||
|
|
@ -113,6 +120,8 @@
|
|||
"flag.format": "Output format: json, table, yaml (default: table)",
|
||||
"flag.issue.add_label": "Label to add to each matching issue",
|
||||
"flag.issue.assignee": "Assignee login",
|
||||
"flag.issue.assignee_id": "Assignee user ID",
|
||||
"flag.issue.author_id": "Author user ID",
|
||||
"flag.issue.batch.reason": "Optional reason shown in the batch result",
|
||||
"flag.issue.batch.yes": "Execute remote operations. Without this flag the command is dry-run only.",
|
||||
"flag.issue.batch_close.older_than_days": "Required safety filter; must be at least 7",
|
||||
|
|
@ -120,8 +129,6 @@
|
|||
"flag.issue.batch_label.state": "Filter by issue state",
|
||||
"flag.issue.batch_list.limit": "Maximum issues to return, capped at 100",
|
||||
"flag.issue.batch_process.limit": "Maximum issues to process, capped at 100",
|
||||
"flag.issue.assignee_id": "Assignee user ID",
|
||||
"flag.issue.author_id": "Author user ID",
|
||||
"flag.issue.body": "Issue description",
|
||||
"flag.issue.label": "Label ID",
|
||||
"flag.issue.label_filter": "Filter by existing label",
|
||||
|
|
@ -149,14 +156,14 @@
|
|||
"flag.pr.file": "Filter diff by file path",
|
||||
"flag.pr.head": "Source branch",
|
||||
"flag.pr.id": "PR number",
|
||||
"flag.pr.milestone_id": "Milestone ID",
|
||||
"flag.pr.merge_method": "Merge method: merge, rebase, squash",
|
||||
"flag.pr.milestone_id": "Milestone ID",
|
||||
"flag.pr.priority_id": "Priority ID",
|
||||
"flag.pr.review_commit": "Commit SHA to attach the review to",
|
||||
"flag.pr.review_content": "Review content",
|
||||
"flag.pr.reviewer_id": "Reviewer user ID",
|
||||
"flag.pr.review_status": "Review status: common, approved, rejected",
|
||||
"flag.pr.review_status_filter": "Filter review status: common, approved, rejected",
|
||||
"flag.pr.reviewer_id": "Reviewer user ID",
|
||||
"flag.pr.state": "Filter: open, merged, closed",
|
||||
"flag.pr.tag_id": "Issue tag ID",
|
||||
"flag.pr.title": "PR title",
|
||||
|
|
@ -194,7 +201,6 @@
|
|||
"output.auth.login_hint": " Run: gitlink-cli auth login",
|
||||
"output.config.file": "Config file: {path}",
|
||||
"output.config.not_set": "(not set)",
|
||||
"output.version": "gitlink-cli {version}",
|
||||
"output.doctor.api_auth.config_skipped": "API authentication check skipped because the configuration file is invalid.",
|
||||
"output.doctor.api_auth.failed": "Authenticated API request failed: {message}",
|
||||
"output.doctor.api_auth.no_login": "Authenticated API response did not include a login field.",
|
||||
|
|
@ -217,6 +223,7 @@
|
|||
"output.doctor.suggestion.check_token": "Check whether the stored token is valid, or run gitlink-cli auth login again.",
|
||||
"output.doctor.suggestion.fix_config_yaml": "Fix the YAML syntax in the gitlink-cli config file.",
|
||||
"output.doctor.suggestion.pass_owner_repo": "Run the command with --owner and --repo when not inside a GitLink repository.",
|
||||
"output.version": "gitlink-cli {version}",
|
||||
"prompt.auth.password": "Password: ",
|
||||
"prompt.auth.token": "Paste your access token: ",
|
||||
"prompt.auth.username": "Username/Email/Phone: ",
|
||||
|
|
|
|||
|
|
@ -21,6 +21,11 @@
|
|||
"cmd.config.list.short": "列出所有配置项",
|
||||
"cmd.config.set.short": "设置配置项",
|
||||
"cmd.config.short": "管理 gitlink-cli 配置",
|
||||
"cmd.dataset.list.long": "按数字项目 ID 列出一个或多个 GitLink 项目的数据集。",
|
||||
"cmd.dataset.list.short": "按项目 ID 列出数据集",
|
||||
"cmd.dataset.short": "数据集操作",
|
||||
"cmd.dataset.view.long": "查看仓库的数据集。项目 ID 从 --owner/--repo 解析,也可用 --project-id 指定。",
|
||||
"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",
|
||||
|
|
@ -90,13 +95,13 @@
|
|||
"error.config.save_failed": "保存配置失败:{message}",
|
||||
"error.missing_required_flag": "缺少必需参数 --{name}",
|
||||
"error.unsupported_language": "不支持的语言:{lang}",
|
||||
"flag.api.body": "请求体(JSON 字符串)",
|
||||
"flag.api.body_file": "从文件读取 JSON 请求体",
|
||||
"flag.api.body_stdin": "从标准输入读取 JSON 请求体",
|
||||
"flag.api.batch_continue_on_error": "批处理请求失败后继续执行后续请求",
|
||||
"flag.api.batch_dry_run": "预览批处理请求,不发送远端请求",
|
||||
"flag.api.batch_file": "从 JSON 文件读取 API 批处理计划",
|
||||
"flag.api.batch_var": "覆盖批处理模板变量(key=value,可重复)",
|
||||
"flag.api.body": "请求体(JSON 字符串)",
|
||||
"flag.api.body_file": "从文件读取 JSON 请求体",
|
||||
"flag.api.body_stdin": "从标准输入读取 JSON 请求体",
|
||||
"flag.api.header": "附加请求头(key:value)",
|
||||
"flag.api.query": "查询参数(key=val&key2=val2)",
|
||||
"flag.auth.token": "通过粘贴已有 Token 登录",
|
||||
|
|
@ -106,6 +111,8 @@
|
|||
"flag.ci.stage": "阶段编号",
|
||||
"flag.ci.step": "步骤编号",
|
||||
"flag.comment.body": "评论内容",
|
||||
"flag.dataset.ids": "用于查询数据集的项目 ID,逗号分隔",
|
||||
"flag.dataset.project_id": "GitLink 项目 ID。省略时从 --owner/--repo 解析。",
|
||||
"flag.debug": "启用调试输出",
|
||||
"flag.description": "描述",
|
||||
"flag.doctor.skip_network": "跳过需要访问 GitLink 的认证连通性检查",
|
||||
|
|
@ -113,6 +120,8 @@
|
|||
"flag.format": "输出格式:json、table、yaml(默认:table)",
|
||||
"flag.issue.add_label": "要添加到每个匹配议题的标签",
|
||||
"flag.issue.assignee": "负责人登录名",
|
||||
"flag.issue.assignee_id": "负责人用户 ID",
|
||||
"flag.issue.author_id": "作者用户 ID",
|
||||
"flag.issue.batch.reason": "批量结果中显示的可选原因",
|
||||
"flag.issue.batch.yes": "执行远端操作。未传入该参数时仅 dry-run。",
|
||||
"flag.issue.batch_close.older_than_days": "必需的安全筛选条件;至少为 7",
|
||||
|
|
@ -120,8 +129,6 @@
|
|||
"flag.issue.batch_label.state": "按议题状态筛选",
|
||||
"flag.issue.batch_list.limit": "最多返回的议题数,上限 100",
|
||||
"flag.issue.batch_process.limit": "最多处理的议题数,上限 100",
|
||||
"flag.issue.assignee_id": "负责人用户 ID",
|
||||
"flag.issue.author_id": "作者用户 ID",
|
||||
"flag.issue.body": "议题描述",
|
||||
"flag.issue.label": "标签 ID",
|
||||
"flag.issue.label_filter": "按已有标签筛选",
|
||||
|
|
@ -149,14 +156,14 @@
|
|||
"flag.pr.file": "按文件路径筛选 diff",
|
||||
"flag.pr.head": "源分支",
|
||||
"flag.pr.id": "PR 编号",
|
||||
"flag.pr.milestone_id": "里程碑 ID",
|
||||
"flag.pr.merge_method": "合并方式:merge、rebase、squash",
|
||||
"flag.pr.milestone_id": "里程碑 ID",
|
||||
"flag.pr.priority_id": "优先级 ID",
|
||||
"flag.pr.review_commit": "关联评审的 Commit SHA",
|
||||
"flag.pr.review_content": "评审内容",
|
||||
"flag.pr.reviewer_id": "评审人用户 ID",
|
||||
"flag.pr.review_status": "评审状态:common、approved、rejected",
|
||||
"flag.pr.review_status_filter": "按评审状态筛选:common、approved、rejected",
|
||||
"flag.pr.reviewer_id": "评审人用户 ID",
|
||||
"flag.pr.state": "筛选:open、merged、closed",
|
||||
"flag.pr.tag_id": "议题标签 ID",
|
||||
"flag.pr.title": "PR 标题",
|
||||
|
|
@ -194,7 +201,6 @@
|
|||
"output.auth.login_hint": " 运行:gitlink-cli auth login",
|
||||
"output.config.file": "配置文件:{path}",
|
||||
"output.config.not_set": "(未设置)",
|
||||
"output.version": "gitlink-cli {version}",
|
||||
"output.doctor.api_auth.config_skipped": "配置文件无效,已跳过 API 认证检查。",
|
||||
"output.doctor.api_auth.failed": "认证 API 请求失败:{message}",
|
||||
"output.doctor.api_auth.no_login": "认证 API 响应中缺少 login 字段。",
|
||||
|
|
@ -217,6 +223,7 @@
|
|||
"output.doctor.suggestion.check_token": "检查已保存的 Token 是否有效,或重新运行 gitlink-cli auth login。",
|
||||
"output.doctor.suggestion.fix_config_yaml": "修复 gitlink-cli 配置文件中的 YAML 语法。",
|
||||
"output.doctor.suggestion.pass_owner_repo": "不在 GitLink 仓库目录内时,请通过 --owner 和 --repo 指定仓库。",
|
||||
"output.version": "gitlink-cli {version}",
|
||||
"prompt.auth.password": "密码:",
|
||||
"prompt.auth.token": "粘贴你的访问 Token:",
|
||||
"prompt.auth.username": "用户名/邮箱/手机号:",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,152 @@
|
|||
// Package dataset implements shortcuts for querying GitLink research datasets.
|
||||
//
|
||||
// GitLink exposes dataset metadata (title, description, paper_content, license,
|
||||
// owning project) through the platform-wide query endpoint
|
||||
// GET /api/v1/project_datasets. The per-repository dataset CRUD routes
|
||||
// documented under /api/v1/{owner}/{repo}/dataset are not deployed on the
|
||||
// production gitlink.org.cn host, so this package wraps the query endpoint and
|
||||
// resolves a repository's project ID to offer both a list and a per-repo view.
|
||||
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 query shortcuts.
|
||||
func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
|
||||
tr := shortcutTranslator(translators...)
|
||||
|
||||
return []*common.Shortcut{
|
||||
{
|
||||
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
|
||||
}
|
||||
return queryDatasets(ctx, normalized)
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "view",
|
||||
Description: tr.T("cmd.dataset.view.short"),
|
||||
Long: tr.T("cmd.dataset.view.long"),
|
||||
Flags: []common.Flag{
|
||||
{Name: "project-id", Usage: tr.T("flag.dataset.project_id")},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
projectID := strings.TrimSpace(ctx.Arg("project-id"))
|
||||
if projectID == "" {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
resolved, err := resolveProjectID(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
projectID = strconv.FormatInt(resolved, 10)
|
||||
} else if _, err := strconv.ParseInt(projectID, 10, 64); err != nil {
|
||||
return fmt.Errorf("invalid --project-id %q: use a numeric project ID", projectID)
|
||||
}
|
||||
return queryDatasets(ctx, projectID)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// queryDatasets calls the platform dataset query endpoint with a comma-separated
|
||||
// list of project IDs.
|
||||
func queryDatasets(ctx *common.RuntimeContext, ids string) error {
|
||||
q := url.Values{}
|
||||
q.Set("ids", ids)
|
||||
env, err := ctx.CallAPIWithQuery("GET", "/v1/project_datasets", q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
}
|
||||
|
||||
// normalizeIDs validates a comma-separated list of positive integer project IDs
|
||||
// and returns it without surrounding whitespace.
|
||||
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
|
||||
}
|
||||
|
||||
// resolveProjectID resolves the numeric GitLink project ID from the current
|
||||
// owner/repo via the repository info endpoint.
|
||||
func resolveProjectID(ctx *common.RuntimeContext) (int64, error) {
|
||||
env, err := ctx.CallAPI("GET", ctx.RepoPath(), nil)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("resolve project id: %w", err)
|
||||
}
|
||||
data, ok := env.Data.(map[string]interface{})
|
||||
if !ok {
|
||||
return 0, fmt.Errorf("resolve project id: unexpected repository response")
|
||||
}
|
||||
for _, key := range []string{"id", "project_id"} {
|
||||
if id, ok := projectIDValue(data[key]); ok {
|
||||
return id, nil
|
||||
}
|
||||
}
|
||||
return 0, fmt.Errorf("resolve project id: repository response did not include id")
|
||||
}
|
||||
|
||||
func projectIDValue(value interface{}) (int64, bool) {
|
||||
switch v := value.(type) {
|
||||
case float64:
|
||||
if v > 0 {
|
||||
return int64(v), true
|
||||
}
|
||||
case int:
|
||||
if v > 0 {
|
||||
return int64(v), true
|
||||
}
|
||||
case int64:
|
||||
if v > 0 {
|
||||
return v, true
|
||||
}
|
||||
case string:
|
||||
if n, err := strconv.ParseInt(strings.TrimSpace(v), 10, 64); err == nil && n > 0 {
|
||||
return n, true
|
||||
}
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func shortcutTranslator(translators ...*i18n.Translator) *i18n.Translator {
|
||||
if len(translators) > 0 && translators[0] != nil {
|
||||
return translators[0]
|
||||
}
|
||||
return i18n.Default()
|
||||
}
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
package dataset
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"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)
|
||||
}
|
||||
|
||||
// --- 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 TestDatasetListMissingIDs(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatal("no API call expected when ids missing")
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
if err := runShortcut(t, server, "list", map[string]string{}); err == nil {
|
||||
t.Fatal("expected error for missing --ids")
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
// --- view ---
|
||||
|
||||
func TestDatasetViewResolvesProjectID(t *testing.T) {
|
||||
var sawRepo, sawQuery bool
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/alice/demo.json":
|
||||
sawRepo = true
|
||||
writeJSON(w, map[string]interface{}{"id": float64(5988)})
|
||||
case "/v1/project_datasets.json":
|
||||
sawQuery = true
|
||||
if got := r.URL.Query().Get("ids"); got != "5988" {
|
||||
t.Fatalf("ids = %q, want 5988", got)
|
||||
}
|
||||
writeJSON(w, map[string]interface{}{"total_count": float64(1), "project_datasets": []interface{}{}})
|
||||
default:
|
||||
t.Fatalf("unexpected path: %s", r.URL.Path)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
if err := runShortcut(t, server, "view", map[string]string{}); err != nil {
|
||||
t.Fatalf("view failed: %v", err)
|
||||
}
|
||||
if !sawRepo || !sawQuery {
|
||||
t.Fatalf("expected repo+query calls, got repo=%v query=%v", sawRepo, sawQuery)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatasetViewExplicitProjectID(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 != "42" {
|
||||
t.Fatalf("ids = %q, want 42", got)
|
||||
}
|
||||
writeJSON(w, map[string]interface{}{"total_count": float64(0), "project_datasets": []interface{}{}})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
if err := runShortcut(t, server, "view", map[string]string{"project-id": "42"}); err != nil {
|
||||
t.Fatalf("view failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatasetViewInvalidProjectID(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatal("no API call expected for invalid project id")
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
if err := runShortcut(t, server, "view", map[string]string{"project-id": "x"}); err == nil {
|
||||
t.Fatal("expected error for invalid --project-id")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatasetViewHTTPError(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, _ = w.Write([]byte("server error"))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
if err := runShortcut(t, server, "view", map[string]string{"project-id": "42"}); err == nil {
|
||||
t.Fatal("expected error for HTTP 500")
|
||||
}
|
||||
}
|
||||
|
|
@ -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/issue"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/label"
|
||||
|
|
@ -47,6 +48,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),
|
||||
"workflow": workflow.Shortcuts(),
|
||||
|
|
@ -68,6 +70,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",
|
||||
"workflow": "AI agent workflow analysis",
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ func TestRegisterAll(t *testing.T) {
|
|||
"repo", "issue", "label", "license", "pr", "release", "branch",
|
||||
"org", "user", "search", "ci", "workflow",
|
||||
"compare", "member", "milestone", "pipeline", "webhook",
|
||||
"health",
|
||||
"dataset", "health",
|
||||
}
|
||||
|
||||
groupSet := map[string]bool{}
|
||||
|
|
|
|||
Loading…
Reference in New Issue