Merge PR #274: feat(shortcuts): add template shortcuts
# Conflicts: # README.zh-CN.md # shortcuts/template/template.go # shortcuts/template/template_test.go
This commit is contained in:
commit
3f13d9e838
21
README.md
21
README.md
|
|
@ -396,6 +396,27 @@ gitlink-cli label +update --owner Gitlink --repo forgeplus -i 42 -c "#00FF00"
|
|||
gitlink-cli label +delete --owner Gitlink --repo forgeplus -i 42
|
||||
```
|
||||
|
||||
### Template Management
|
||||
|
||||
```bash
|
||||
# List project templates
|
||||
gitlink-cli template +list --owner Gitlink --repo forgeplus
|
||||
|
||||
# Get a template by id
|
||||
gitlink-cli template +get --owner Gitlink --repo forgeplus -i 42
|
||||
|
||||
# Create an issue template
|
||||
gitlink-cli template +create --owner Gitlink --repo forgeplus \
|
||||
-t "ProjectTemplates::Issue" -n "Bug Report" -c "## Description\n## Steps to Reproduce\n## Expected Behavior"
|
||||
|
||||
# Update a template
|
||||
gitlink-cli template +update --owner Gitlink --repo forgeplus \
|
||||
-i 5 -t "ProjectTemplates::Issue" -n "Bug Report v2" -c "## Updated content"
|
||||
|
||||
# Delete a template
|
||||
gitlink-cli template +delete --owner Gitlink --repo forgeplus -i 5
|
||||
```
|
||||
|
||||
### Pull Requests
|
||||
|
||||
```bash
|
||||
|
|
|
|||
117
README.zh-CN.md
117
README.zh-CN.md
|
|
@ -108,7 +108,7 @@
|
|||
| 🔖 标签 | 创建、列出、更新、删除 Issue 标签 |
|
||||
| 🔀 PR | 创建、合并、Review Pull Request,查看变更文件 |
|
||||
| 👥 成员 | 列出、添加、移除仓库成员,调整角色,生成和接受邀请链接 |
|
||||
| 🌿 分支 | 创建、删除、恢复、筛选、保护分支,并切换默认分支 |
|
||||
| 🌿 分支 | 创建、删除、保护分支 |
|
||||
| 🏷️ 发布 | 创建、编辑、更新、查看、删除 Release |
|
||||
| 🏢 组织 | 管理组织、成员、团队 |
|
||||
| 🔧 CI | 查看构建、日志、CI/CD 操作 |
|
||||
|
|
@ -119,7 +119,6 @@
|
|||
| 👤 用户 | 查看用户资料和信息 |
|
||||
| 📊 画像 | 用户开发能力、角色定位、专业定位、近期活动、贡献热力图统计 |
|
||||
| 📋 项目管理 | Sprint 管理、看板、周报 |
|
||||
| 📝 模板 | 管理 Issue 和 Pull Request 项目模板 |
|
||||
| 🤖 工作流 | AI 驱动的 Issue 分类、PR Review、Release Notes |
|
||||
|
||||
## 安装与快速上手
|
||||
|
|
@ -239,11 +238,6 @@ gitlink-cli repo +readme --owner Gitlink --repo forgeplus --ref master
|
|||
gitlink-cli repo +tree --owner Gitlink --repo forgeplus --ref master
|
||||
gitlink-cli repo +tree --owner Gitlink --repo forgeplus --path src --ref main
|
||||
|
||||
# 读取原始文件和常见依赖清单
|
||||
gitlink-cli repo +raw --owner Gitlink --repo forgeplus --path LICENSE --ref master
|
||||
gitlink-cli repo +file-exists --owner Gitlink --repo forgeplus --path package.json --ref master
|
||||
gitlink-cli repo +manifest --owner Gitlink --repo forgeplus --kind go --ref master
|
||||
|
||||
# 查看语言占比
|
||||
gitlink-cli repo +languages --owner Gitlink --repo forgeplus
|
||||
|
||||
|
|
@ -348,13 +342,13 @@ gitlink-cli issue +create --owner Gitlink --repo forgeplus -t "Bug: 登录失败
|
|||
gitlink-cli issue +create --owner Gitlink --repo forgeplus -t "Bug: 登录失败" --priority-id 3 --tag-ids 4,5 --assigner-ids 7
|
||||
|
||||
# 查看 Issue
|
||||
gitlink-cli issue +view --owner Gitlink --repo forgeplus --number 123
|
||||
gitlink-cli issue +view --owner Gitlink --repo forgeplus -i 123
|
||||
|
||||
# 更新 Issue 元数据
|
||||
gitlink-cli issue +update --owner Gitlink --repo forgeplus --number 123 --priority-id 4 --branch bugfix/login --due-date 2026-06-15
|
||||
|
||||
# 关闭 Issue
|
||||
gitlink-cli issue +close --owner Gitlink --repo forgeplus --number 123
|
||||
gitlink-cli issue +close --owner Gitlink --repo forgeplus -i 123
|
||||
|
||||
# 预览批量关闭,不修改数据
|
||||
gitlink-cli issue +batch-close --owner Gitlink --repo forgeplus --numbers 123,124 --dry-run
|
||||
|
|
@ -370,39 +364,8 @@ gitlink-cli issue +batch-update --owner Gitlink --repo forgeplus --ids 101,102 -
|
|||
gitlink-cli issue +batch-delete --owner Gitlink --repo forgeplus --ids 101,102 --dry-run
|
||||
gitlink-cli issue +batch-delete --owner Gitlink --repo forgeplus --ids 101,102 --yes
|
||||
|
||||
# 将筛选后的 Issue 导出为 CSV,便于离线分析或生成周报
|
||||
gitlink-cli issue +export --owner Gitlink --repo forgeplus --state open --keyword bug --export-format csv --output issues.csv
|
||||
|
||||
# 添加评论
|
||||
gitlink-cli issue +comment --owner Gitlink --repo forgeplus --number 123 -b "已修复"
|
||||
|
||||
# 查看、更新、删除 Issue 评论/操作记录
|
||||
gitlink-cli issue +comments --owner Gitlink --repo forgeplus --number 123 --category comment
|
||||
gitlink-cli issue +comment-update --owner Gitlink --repo forgeplus --number 123 --comment-id 58 -b "更新评论" --dry-run
|
||||
gitlink-cli issue +comment-delete --owner Gitlink --repo forgeplus --number 123 --comment-id 58 --dry-run
|
||||
gitlink-cli issue +comment-children --owner Gitlink --repo forgeplus --number 123 --comment-id 58
|
||||
|
||||
# 回复评论并携带附件和 @ 用户
|
||||
gitlink-cli issue +comment --owner Gitlink --repo forgeplus --number 123 -b "请查看日志" --parent-id 456 --reply-id 456 --attachment-ids 7,8 --receivers alice,bob
|
||||
|
||||
# 列出评论;需要操作记录时可传 --category all
|
||||
gitlink-cli issue +comments --owner Gitlink --repo forgeplus --number 123 --category comment --keyword fixed
|
||||
|
||||
# 更新或删除评论
|
||||
gitlink-cli issue +comment-update --owner Gitlink --repo forgeplus --number 123 --comment-id 456 -b "更新后的评论"
|
||||
gitlink-cli issue +comment-delete --owner Gitlink --repo forgeplus --number 123 --comment-id 456
|
||||
|
||||
# 列出评论下的回复
|
||||
gitlink-cli issue +comment-replies --owner Gitlink --repo forgeplus --number 123 --comment-id 456
|
||||
|
||||
# 回复评论并列出某条评论的回复
|
||||
gitlink-cli issue +comment --owner Gitlink --repo forgeplus -i 123 -b "同意" --reply-to 456
|
||||
gitlink-cli issue +comment-replies --owner Gitlink --repo forgeplus -n 123 -c 456
|
||||
|
||||
# 列出、编辑、删除议题评论
|
||||
gitlink-cli issue +comments --owner Gitlink --repo forgeplus -n 123 --category comment
|
||||
gitlink-cli issue +comment-edit --owner Gitlink --repo forgeplus -n 123 -c 456 -b "更新内容"
|
||||
gitlink-cli issue +comment-delete --owner Gitlink --repo forgeplus -n 123 -c 456
|
||||
gitlink-cli issue +comment --owner Gitlink --repo forgeplus -i 123 -b "已修复"
|
||||
|
||||
# 列出 Issue 负责人
|
||||
gitlink-cli issue +assigners --owner Gitlink --repo forgeplus
|
||||
|
|
@ -449,22 +412,19 @@ gitlink-cli label +delete --owner Gitlink --repo forgeplus -i 42
|
|||
# 列出项目模板
|
||||
gitlink-cli template +list --owner Gitlink --repo forgeplus
|
||||
|
||||
# 获取模板详情
|
||||
gitlink-cli template +get --owner Gitlink --repo forgeplus -i 1
|
||||
# 按 ID 获取模板详情
|
||||
gitlink-cli template +get --owner Gitlink --repo forgeplus -i 42
|
||||
|
||||
# 创建 Issue 模板
|
||||
gitlink-cli template +create --owner Gitlink --repo forgeplus \
|
||||
-t "ProjectTemplates::Issue" -n "Bug 报告" -c "## 描述\n..."
|
||||
|
||||
# 创建 Pull Request 模板
|
||||
gitlink-cli template +create --owner Gitlink --repo forgeplus \
|
||||
-t "ProjectTemplates::PullRequest" -n "功能 PR" -c "## 概述\n..."
|
||||
-t "ProjectTemplates::Issue" -n "缺陷报告" -c "## 问题描述\n## 复现步骤\n## 期望行为"
|
||||
|
||||
# 更新模板
|
||||
gitlink-cli template +update --owner Gitlink --repo forgeplus -i 1 -n "新名称"
|
||||
gitlink-cli template +update --owner Gitlink --repo forgeplus \
|
||||
-i 5 -t "ProjectTemplates::Issue" -n "缺陷报告 v2" -c "## 更新后的内容"
|
||||
|
||||
# 删除模板
|
||||
gitlink-cli template +delete --owner Gitlink --repo forgeplus -i 1
|
||||
gitlink-cli template +delete --owner Gitlink --repo forgeplus -i 5
|
||||
```
|
||||
|
||||
### Pull Request
|
||||
|
|
@ -503,43 +463,6 @@ gitlink-cli pr +reviews --owner Gitlink --repo forgeplus -i 42
|
|||
# 创建 PR 审查(支持 dry-run 预览)
|
||||
gitlink-cli pr +review --owner Gitlink --repo forgeplus -i 42 --status approved -c "LGTM" --dry-run
|
||||
gitlink-cli pr +review --owner Gitlink --repo forgeplus -i 42 --status approved -c "LGTM"
|
||||
|
||||
# 查看和管理 PR Review 行评论
|
||||
gitlink-cli pr +review-comments --owner Gitlink --repo forgeplus -i 42 --state opened
|
||||
gitlink-cli pr +review-comment --owner Gitlink --repo forgeplus -i 42 --review-id 10 --commit abc123 --line-code abc123_0_10 --path README.md --note "请修改" --dry-run
|
||||
gitlink-cli pr +review-comment-update --owner Gitlink --repo forgeplus -i 42 --comment-id 200 --state resolved --dry-run
|
||||
gitlink-cli pr +review-comment-delete --owner Gitlink --repo forgeplus -i 42 --comment-id 200 --dry-run
|
||||
```
|
||||
|
||||
### 分支管理
|
||||
|
||||
```bash
|
||||
# 列出分支
|
||||
gitlink-cli branch +list --owner Gitlink --repo forgeplus
|
||||
|
||||
# 列出已删除分支,或按关键字筛选
|
||||
gitlink-cli branch +list --owner Gitlink --repo forgeplus --state deleted --keyword release/
|
||||
|
||||
# 列出全部分支及归档下载链接
|
||||
gitlink-cli branch +all --owner Gitlink --repo forgeplus
|
||||
|
||||
# 创建分支
|
||||
gitlink-cli branch +create --name feature/new-feature
|
||||
|
||||
# 删除分支
|
||||
gitlink-cli branch +delete --name feature/old-feature
|
||||
|
||||
# 切换默认分支
|
||||
gitlink-cli branch +set-default --name main
|
||||
|
||||
# 恢复已删除分支
|
||||
gitlink-cli branch +restore --branch-id 7 --name feature/old-feature
|
||||
|
||||
# 设置分支保护
|
||||
gitlink-cli branch +protect --name main
|
||||
|
||||
# 移除分支保护
|
||||
gitlink-cli branch +unprotect --name main
|
||||
```
|
||||
|
||||
### 发布管理
|
||||
|
|
@ -584,25 +507,6 @@ gitlink-cli pipeline +disable --owner Gitlink --repo forgeplus --id 7 --workflow
|
|||
gitlink-cli pipeline +delete --owner Gitlink --repo forgeplus --id 7 --dry-run
|
||||
```
|
||||
|
||||
### 项目管理
|
||||
|
||||
`pm` 暴露 GitLink 项目管理数据,覆盖看板、Sprint、周报、标签、流水线和 Action 运行记录。
|
||||
PM 命令需要传入数字项目 ID。
|
||||
|
||||
```bash
|
||||
# 查询 PM 看板和 Sprint Issue
|
||||
gitlink-cli pm +dashboards --project-id 123 --limit 20
|
||||
gitlink-cli pm +sprint-issues --project-id 123 --page 1 --limit 20
|
||||
|
||||
# 查询周报 Issue 和 PM Issue 标签
|
||||
gitlink-cli pm +weekly-issues --project-id 123
|
||||
gitlink-cli pm +issue-tags --project-id 123
|
||||
|
||||
# 查询 PM 流水线和 Action 运行记录
|
||||
gitlink-cli pm +pipelines --project-id 123
|
||||
gitlink-cli pm +action-runs --project-id 123
|
||||
```
|
||||
|
||||
### 忽略文件模板
|
||||
|
||||
```bash
|
||||
|
|
@ -768,7 +672,6 @@ gitlink-cli/
|
|||
│ ├── org/ # 组织 shortcuts
|
||||
│ ├── ci/ # CI shortcuts
|
||||
│ ├── pipeline/ # Pipeline shortcuts
|
||||
│ ├── pm/ # 项目管理 shortcuts
|
||||
│ ├── search/ # 搜索 shortcuts
|
||||
│ ├── user/ # 用户 shortcuts
|
||||
│ └── register.go # 注册入口
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
# Template shortcut
|
||||
|
||||
新增 `template` Shortcut 组,补齐 GitLink 项目模板(Project Templates)OpenAPI 的常用操作封装:
|
||||
|
||||
- `template +list`
|
||||
- `template +get`
|
||||
- `template +create`
|
||||
- `template +update`
|
||||
- `template +delete`
|
||||
|
||||
|
|
@ -110,6 +110,12 @@
|
|||
"cmd.search.repos.short": "Search repositories",
|
||||
"cmd.search.short": "Search operations",
|
||||
"cmd.search.users.short": "Search users",
|
||||
"cmd.template.create.short": "Create a project template",
|
||||
"cmd.template.delete.short": "Delete a project template",
|
||||
"cmd.template.get.short": "Get a project template by id",
|
||||
"cmd.template.list.short": "List project templates",
|
||||
"cmd.template.short": "Project template operations",
|
||||
"cmd.template.update.short": "Update a project template",
|
||||
"cmd.user.info.short": "Show user profile",
|
||||
"cmd.user.me.short": "Show current authenticated user",
|
||||
"cmd.user.short": "User operations",
|
||||
|
|
@ -131,6 +137,7 @@
|
|||
"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.template.invalid_type": "invalid --type {type}; expected one of {types}",
|
||||
"error.unsupported_language": "unsupported language: {lang}",
|
||||
"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",
|
||||
|
|
@ -258,6 +265,10 @@
|
|||
"flag.webhook.secret": "Webhook secret",
|
||||
"flag.webhook.secret_update": "Webhook secret. Pass it again if the server does not return existing secrets.",
|
||||
"flag.webhook.type": "Webhook type: gitea/slack/discord/dingtalk/telegram/msteams/feishu/matrix/jianmu/softbot",
|
||||
"flag.template.content": "Template content",
|
||||
"flag.template.id": "Template ID",
|
||||
"flag.template.name": "Template name",
|
||||
"flag.template.type": "Template type, for example: ProjectTemplates::Issue",
|
||||
"flag.webhook.url": "Webhook target URL",
|
||||
"output.auth.env_hint": " Or set {env} environment variable",
|
||||
"output.auth.login_hint": " Run: gitlink-cli auth login",
|
||||
|
|
|
|||
|
|
@ -110,6 +110,12 @@
|
|||
"cmd.search.repos.short": "搜索仓库",
|
||||
"cmd.search.short": "搜索操作",
|
||||
"cmd.search.users.short": "搜索用户",
|
||||
"cmd.template.create.short": "创建项目模板",
|
||||
"cmd.template.delete.short": "删除项目模板",
|
||||
"cmd.template.get.short": "根据 ID 获取项目模板",
|
||||
"cmd.template.list.short": "列出项目模板",
|
||||
"cmd.template.short": "项目模板操作",
|
||||
"cmd.template.update.short": "更新项目模板",
|
||||
"cmd.user.info.short": "显示用户资料",
|
||||
"cmd.user.me.short": "显示当前认证用户",
|
||||
"cmd.user.short": "用户操作",
|
||||
|
|
@ -131,6 +137,7 @@
|
|||
"error.dataset.delete_confirm": "删除数据集附件具有破坏性;请先 --dry-run 预览,再传 --yes 确认",
|
||||
"error.missing_required_flag": "缺少必需参数 --{name}",
|
||||
"error.profile.user_required": "无法确定目标用户;请通过 --user 指定,或先运行 gitlink-cli auth login 登录",
|
||||
"error.template.invalid_type": "无效的 --type {type};应为以下之一:{types}",
|
||||
"error.unsupported_language": "不支持的语言:{lang}",
|
||||
"flag.api.batch_continue_on_error": "批处理请求失败后继续执行后续请求",
|
||||
"flag.api.batch_dry_run": "预览批处理请求,不发送远端请求",
|
||||
|
|
@ -258,6 +265,10 @@
|
|||
"flag.webhook.secret": "Webhook 密钥",
|
||||
"flag.webhook.secret_update": "Webhook 密钥。如果服务端不返回已有密钥,请再次传入。",
|
||||
"flag.webhook.type": "Webhook 类型:gitea/slack/discord/dingtalk/telegram/msteams/feishu/matrix/jianmu/softbot",
|
||||
"flag.template.content": "模板内容",
|
||||
"flag.template.id": "模板 ID",
|
||||
"flag.template.name": "模板名称",
|
||||
"flag.template.type": "模板类型,例如:ProjectTemplates::Issue",
|
||||
"flag.webhook.url": "Webhook 目标 URL",
|
||||
"output.auth.env_hint": " 或设置 {env} 环境变量",
|
||||
"output.auth.login_hint": " 运行:gitlink-cli auth login",
|
||||
|
|
|
|||
|
|
@ -60,6 +60,7 @@ func RegisterAll(root *cobra.Command, translators ...*i18n.Translator) {
|
|||
"ignore": ignore.Shortcuts(),
|
||||
"template": template.Shortcuts(),
|
||||
"workflow": workflow.Shortcuts(),
|
||||
"template": template.Shortcuts(tr),
|
||||
}
|
||||
|
||||
descriptions := map[string]string{
|
||||
|
|
@ -86,6 +87,7 @@ func RegisterAll(root *cobra.Command, translators ...*i18n.Translator) {
|
|||
"ignore": tr.T("cmd.ignore.short"),
|
||||
"template": "项目模板操作",
|
||||
"workflow": "AI agent workflow analysis",
|
||||
"template": tr.T("cmd.template.short"),
|
||||
}
|
||||
|
||||
for name, shortcuts := range groups {
|
||||
|
|
|
|||
|
|
@ -1,36 +1,33 @@
|
|||
package template
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/internal/i18n"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
||||
)
|
||||
|
||||
// v1RepoPath returns the v1 API path prefix: /v1/{owner}/{repo}
|
||||
func v1RepoPath(ctx *common.RuntimeContext) string {
|
||||
return fmt.Sprintf("/v1/%s/%s", ctx.Owner, ctx.Repo)
|
||||
}
|
||||
|
||||
// templatePath returns the API path for template collection.
|
||||
func templatePath(ctx *common.RuntimeContext) string {
|
||||
return v1RepoPath(ctx) + "/project_templates"
|
||||
}
|
||||
|
||||
// templateItemPath returns the API path for a single template.
|
||||
func templateItemPath(ctx *common.RuntimeContext, id string) string {
|
||||
return templatePath(ctx) + "/" + id
|
||||
// templateTypes enumerates the supported project template types.
|
||||
var templateTypes = []string{
|
||||
"ProjectTemplates::Issue",
|
||||
"ProjectTemplates::PullRequest",
|
||||
"ProjectTemplates::Commit",
|
||||
}
|
||||
|
||||
// Shortcuts returns project template management shortcuts.
|
||||
//
|
||||
// Project templates are reusable content templates (e.g., issue templates)
|
||||
// that can be applied when creating new resources.
|
||||
func Shortcuts() []*common.Shortcut {
|
||||
// Project templates provide reusable content scaffolds for issues, pull
|
||||
// requests and commits. Until now they could only be managed through the raw
|
||||
// REST API; these shortcuts expose list/get/create/update/delete as
|
||||
// first-class commands.
|
||||
func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
|
||||
tr := shortcutTranslator(translators...)
|
||||
return []*common.Shortcut{
|
||||
{
|
||||
Name: "list",
|
||||
Description: "列出项目模板",
|
||||
Flags: []common.Flag{},
|
||||
Description: tr.T("cmd.template.list.short"),
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
|
|
@ -44,9 +41,9 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
{
|
||||
Name: "get",
|
||||
Description: "根据 ID 获取项目模板",
|
||||
Description: tr.T("cmd.template.get.short"),
|
||||
Flags: []common.Flag{
|
||||
{Name: "id", Short: "i", Usage: "模板 ID", Required: true},
|
||||
{Name: "id", Short: "i", Usage: tr.T("flag.template.id"), Required: true},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
|
|
@ -65,83 +62,30 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
{
|
||||
Name: "create",
|
||||
Description: "创建项目模板",
|
||||
Description: tr.T("cmd.template.create.short"),
|
||||
Flags: []common.Flag{
|
||||
{Name: "type", Short: "t", Usage: "模板类型(如:ProjectTemplates::Issue、ProjectTemplates::PullRequest)", Required: true},
|
||||
{Name: "name", Short: "n", Usage: "模板名称", Required: true},
|
||||
{Name: "content", Short: "c", Usage: "模板内容(支持 Markdown)", Required: true},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
templateType, err := ctx.RequireArg("type")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
name, err := ctx.RequireArg("name")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
content, err := ctx.RequireArg("content")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
payload := map[string]interface{}{
|
||||
"type": templateType,
|
||||
"name": name,
|
||||
"content": content,
|
||||
}
|
||||
env, err := ctx.CallAPI("POST", templatePath(ctx), payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
{Name: "type", Short: "t", Usage: tr.T("flag.template.type"), Required: true},
|
||||
{Name: "name", Short: "n", Usage: tr.T("flag.template.name"), Required: true},
|
||||
{Name: "content", Short: "c", Usage: tr.T("flag.template.content"), Required: true},
|
||||
},
|
||||
Run: runCreate,
|
||||
},
|
||||
{
|
||||
Name: "update",
|
||||
Description: "更新项目模板",
|
||||
Description: tr.T("cmd.template.update.short"),
|
||||
Flags: []common.Flag{
|
||||
{Name: "id", Short: "i", Usage: "模板 ID", Required: true},
|
||||
{Name: "type", Short: "t", Usage: "模板类型(如:ProjectTemplates::Issue、ProjectTemplates::PullRequest)"},
|
||||
{Name: "name", Short: "n", Usage: "模板名称"},
|
||||
{Name: "content", Short: "c", Usage: "模板内容(支持 Markdown)"},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
id, err := ctx.RequireArg("id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// At least one field must be provided for update
|
||||
if ctx.Arg("type") == "" && ctx.Arg("name") == "" && ctx.Arg("content") == "" {
|
||||
return fmt.Errorf("至少需要提供 --type、--name 或 --content 中的一个")
|
||||
}
|
||||
payload := map[string]interface{}{}
|
||||
if t := ctx.Arg("type"); t != "" {
|
||||
payload["type"] = t
|
||||
}
|
||||
if n := ctx.Arg("name"); n != "" {
|
||||
payload["name"] = n
|
||||
}
|
||||
if c := ctx.Arg("content"); c != "" {
|
||||
payload["content"] = c
|
||||
}
|
||||
env, err := ctx.CallAPI("PUT", templateItemPath(ctx, id), payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
{Name: "id", Short: "i", Usage: tr.T("flag.template.id"), Required: true},
|
||||
{Name: "type", Short: "t", Usage: tr.T("flag.template.type"), Required: true},
|
||||
{Name: "name", Short: "n", Usage: tr.T("flag.template.name"), Required: true},
|
||||
{Name: "content", Short: "c", Usage: tr.T("flag.template.content"), Required: true},
|
||||
},
|
||||
Run: runUpdate,
|
||||
},
|
||||
{
|
||||
Name: "delete",
|
||||
Description: "删除项目模板",
|
||||
Description: tr.T("cmd.template.delete.short"),
|
||||
Flags: []common.Flag{
|
||||
{Name: "id", Short: "i", Usage: "模板 ID", Required: true},
|
||||
{Name: "id", Short: "i", Usage: tr.T("flag.template.id"), Required: true},
|
||||
},
|
||||
Run: func(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
|
|
@ -160,3 +104,102 @@ func Shortcuts() []*common.Shortcut {
|
|||
},
|
||||
}
|
||||
}
|
||||
|
||||
func runCreate(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
payload, err := buildPayload(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
env, err := ctx.CallAPI("POST", templatePath(ctx), payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
}
|
||||
|
||||
func runUpdate(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
id, err := ctx.RequireArg("id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
payload, err := buildPayload(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
env, err := ctx.CallAPI("PUT", templateItemPath(ctx, id), payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
}
|
||||
|
||||
// buildPayload collects the required type/name/content fields and validates
|
||||
// the template type against the known set.
|
||||
func buildPayload(ctx *common.RuntimeContext) (map[string]interface{}, error) {
|
||||
typ, err := ctx.RequireArg("type")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !isValidTemplateType(typ) {
|
||||
var msg string
|
||||
if ctx.Tr != nil {
|
||||
msg = ctx.Tr.Tf("error.template.invalid_type", i18n.Args{
|
||||
"type": typ,
|
||||
"types": fmt.Sprintf("%v", templateTypes),
|
||||
})
|
||||
} else {
|
||||
msg = fmt.Sprintf("invalid --type %q; expected one of %v", typ, templateTypes)
|
||||
}
|
||||
return nil, errors.New(msg)
|
||||
}
|
||||
name, err := ctx.RequireArg("name")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
content, err := ctx.RequireArg("content")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"type": typ,
|
||||
"name": name,
|
||||
"content": content,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func isValidTemplateType(value string) bool {
|
||||
for _, candidate := range templateTypes {
|
||||
if candidate == value {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func templatePath(ctx *common.RuntimeContext) string {
|
||||
return fmt.Sprintf("/v1/%s/%s/project_templates", ctx.Owner, ctx.Repo)
|
||||
}
|
||||
|
||||
func templateItemPath(ctx *common.RuntimeContext, id string) string {
|
||||
return fmt.Sprintf("%s/%s", templatePath(ctx), url.PathEscape(id))
|
||||
}
|
||||
|
||||
// setQueryIfPresent is a small helper kept for future list-filter extensions.
|
||||
func setQueryIfPresent(q url.Values, name, value string) {
|
||||
if value != "" {
|
||||
q.Set(name, value)
|
||||
}
|
||||
}
|
||||
|
||||
func shortcutTranslator(translators ...*i18n.Translator) *i18n.Translator {
|
||||
if len(translators) > 0 && translators[0] != nil {
|
||||
return translators[0]
|
||||
}
|
||||
return i18n.Default()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,89 +1,260 @@
|
|||
package template
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/internal/client"
|
||||
"github.com/gitlink-org/gitlink-cli/internal/i18n"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
||||
)
|
||||
|
||||
func TestShortcutsReturnsAllCommands(t *testing.T) {
|
||||
shortcuts := Shortcuts()
|
||||
func TestTemplateList(t *testing.T) {
|
||||
server := newTemplateTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
assertRequest(t, r, "GET", "/v1/owner/repo/project_templates.json")
|
||||
writeJSON(t, w, map[string]interface{}{
|
||||
"total_count": 1,
|
||||
"project_templates": []interface{}{
|
||||
map[string]interface{}{
|
||||
"id": float64(1),
|
||||
"type": "ProjectTemplates::Issue",
|
||||
"name": "bug修复模板",
|
||||
"content": "## 问题描述\n[描述]",
|
||||
"created_at": "2026-01-13 17:30:20",
|
||||
"updated_at": "2026-01-13 17:30:20",
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
expectedCommands := []string{"list", "get", "create", "update", "delete"}
|
||||
if len(shortcuts) != len(expectedCommands) {
|
||||
t.Errorf("Expected %d shortcuts, got %d", len(expectedCommands), len(shortcuts))
|
||||
err := runTemplateShortcut(t, server, "list", map[string]string{})
|
||||
if err != nil {
|
||||
t.Fatalf("list shortcut failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemplateGet(t *testing.T) {
|
||||
server := newTemplateTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
assertRequest(t, r, "GET", "/v1/owner/repo/project_templates/42.json")
|
||||
writeJSON(t, w, map[string]interface{}{
|
||||
"project_template": map[string]interface{}{
|
||||
"id": float64(42),
|
||||
"type": "ProjectTemplates::Issue",
|
||||
"name": "功能请求模板",
|
||||
"content": "## 功能描述\n[描述]",
|
||||
"created_at": "2026-01-22 11:15:04",
|
||||
"updated_at": "2026-01-22 11:15:04",
|
||||
},
|
||||
})
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runTemplateShortcut(t, server, "get", map[string]string{"id": "42"})
|
||||
if err != nil {
|
||||
t.Fatalf("get shortcut failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemplateGetRequiresID(t *testing.T) {
|
||||
server := newTemplateTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("get without id should not call API, got: %s %s", r.Method, r.URL.Path)
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runTemplateShortcut(t, server, "get", map[string]string{})
|
||||
if err == nil {
|
||||
t.Fatal("expected get without id to return an error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemplateCreatePayload(t *testing.T) {
|
||||
var payload map[string]interface{}
|
||||
server := newTemplateTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
assertRequest(t, r, "POST", "/v1/owner/repo/project_templates.json")
|
||||
payload = decodeJSON(t, r)
|
||||
writeJSON(t, w, map[string]interface{}{"status": 0, "message": "success"})
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runTemplateShortcut(t, server, "create", map[string]string{
|
||||
"type": "ProjectTemplates::Issue",
|
||||
"name": "测试issue模板",
|
||||
"content": "模板内容",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create shortcut failed: %v", err)
|
||||
}
|
||||
|
||||
for i, cmd := range expectedCommands {
|
||||
if shortcuts[i].Name != cmd {
|
||||
t.Errorf("Expected shortcut %q at index %d, got %q", cmd, i, shortcuts[i].Name)
|
||||
assertEqual(t, payload["type"], "ProjectTemplates::Issue")
|
||||
assertEqual(t, payload["name"], "测试issue模板")
|
||||
assertEqual(t, payload["content"], "模板内容")
|
||||
}
|
||||
|
||||
func TestTemplateCreateRejectsInvalidType(t *testing.T) {
|
||||
server := newTemplateTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("invalid type should not call API, got: %s %s", r.Method, r.URL.Path)
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runTemplateShortcut(t, server, "create", map[string]string{
|
||||
"type": "InvalidType",
|
||||
"name": "test",
|
||||
"content": "content",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected invalid type to return an error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemplateUpdatePayload(t *testing.T) {
|
||||
var payload map[string]interface{}
|
||||
server := newTemplateTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
assertRequest(t, r, "PUT", "/v1/owner/repo/project_templates/5.json")
|
||||
payload = decodeJSON(t, r)
|
||||
writeJSON(t, w, map[string]interface{}{"status": 0, "message": "success"})
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runTemplateShortcut(t, server, "update", map[string]string{
|
||||
"id": "5",
|
||||
"type": "ProjectTemplates::Issue",
|
||||
"name": "更新模板",
|
||||
"content": "更新内容",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("update shortcut failed: %v", err)
|
||||
}
|
||||
|
||||
assertEqual(t, payload["type"], "ProjectTemplates::Issue")
|
||||
assertEqual(t, payload["name"], "更新模板")
|
||||
assertEqual(t, payload["content"], "更新内容")
|
||||
}
|
||||
|
||||
func TestTemplateDelete(t *testing.T) {
|
||||
server := newTemplateTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
assertRequest(t, r, "DELETE", "/v1/owner/repo/project_templates/5.json")
|
||||
writeJSON(t, w, map[string]interface{}{"status": 0, "message": "success"})
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runTemplateShortcut(t, server, "delete", map[string]string{"id": "5"})
|
||||
if err != nil {
|
||||
t.Fatalf("delete shortcut failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemplateDeleteRequiresID(t *testing.T) {
|
||||
server := newTemplateTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("delete without id should not call API, got: %s %s", r.Method, r.URL.Path)
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runTemplateShortcut(t, server, "delete", map[string]string{})
|
||||
if err == nil {
|
||||
t.Fatal("expected delete without id to return an error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsValidTemplateType(t *testing.T) {
|
||||
for _, valid := range templateTypes {
|
||||
if !isValidTemplateType(valid) {
|
||||
t.Fatalf("expected %q to be valid", valid)
|
||||
}
|
||||
}
|
||||
invalid := []string{"", "InvalidType", "projecttemplates::issue", "PR"}
|
||||
for _, v := range invalid {
|
||||
if isValidTemplateType(v) {
|
||||
t.Fatalf("expected %q to be invalid", v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestShortcutDescriptions(t *testing.T) {
|
||||
shortcuts := Shortcuts()
|
||||
|
||||
for _, s := range shortcuts {
|
||||
if s.Description == "" {
|
||||
t.Errorf("Shortcut %q has empty description", s.Name)
|
||||
}
|
||||
func TestTemplatePath(t *testing.T) {
|
||||
ctx := &common.RuntimeContext{Owner: "gitlink", Repo: "forgeplus"}
|
||||
got := templatePath(ctx)
|
||||
want := "/v1/gitlink/forgeplus/project_templates"
|
||||
if got != want {
|
||||
t.Fatalf("got %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequiredFlags(t *testing.T) {
|
||||
shortcuts := Shortcuts()
|
||||
|
||||
// Test create command required flags
|
||||
createCmd := findShortcut(shortcuts, "create")
|
||||
if createCmd == nil {
|
||||
t.Fatal("create command not found")
|
||||
func TestTemplateItemPath(t *testing.T) {
|
||||
ctx := &common.RuntimeContext{Owner: "gitlink", Repo: "forgeplus"}
|
||||
got := templateItemPath(ctx, "42")
|
||||
want := "/v1/gitlink/forgeplus/project_templates/42"
|
||||
if got != want {
|
||||
t.Fatalf("got %q, want %q", got, want)
|
||||
}
|
||||
assertRequiredFlags(t, createCmd, []string{"type", "name", "content"})
|
||||
|
||||
// Test get command required flags
|
||||
getCmd := findShortcut(shortcuts, "get")
|
||||
if getCmd == nil {
|
||||
t.Fatal("get command not found")
|
||||
}
|
||||
assertRequiredFlags(t, getCmd, []string{"id"})
|
||||
|
||||
// Test update command required flags
|
||||
updateCmd := findShortcut(shortcuts, "update")
|
||||
if updateCmd == nil {
|
||||
t.Fatal("update command not found")
|
||||
}
|
||||
assertRequiredFlags(t, updateCmd, []string{"id"})
|
||||
|
||||
// Test delete command required flags
|
||||
deleteCmd := findShortcut(shortcuts, "delete")
|
||||
if deleteCmd == nil {
|
||||
t.Fatal("delete command not found")
|
||||
}
|
||||
assertRequiredFlags(t, deleteCmd, []string{"id"})
|
||||
}
|
||||
|
||||
func findShortcut(shortcuts []*common.Shortcut, name string) *common.Shortcut {
|
||||
for _, s := range shortcuts {
|
||||
if s.Name == name {
|
||||
return s
|
||||
// --- helpers ---
|
||||
|
||||
func runTemplateShortcut(t *testing.T, server *httptest.Server, name string, args map[string]string) error {
|
||||
t.Helper()
|
||||
shortcut := findTemplateShortcut(t, name)
|
||||
ctx := &common.RuntimeContext{
|
||||
Client: &client.Client{
|
||||
HTTP: server.Client(),
|
||||
BaseURL: server.URL,
|
||||
},
|
||||
Owner: "owner",
|
||||
Repo: "repo",
|
||||
Format: "json",
|
||||
Args: args,
|
||||
Tr: i18n.Default(),
|
||||
}
|
||||
if ctx.Args == nil {
|
||||
ctx.Args = map[string]string{}
|
||||
}
|
||||
return shortcut.Run(ctx)
|
||||
}
|
||||
|
||||
func findTemplateShortcut(t *testing.T, name string) *common.Shortcut {
|
||||
t.Helper()
|
||||
for _, shortcut := range Shortcuts() {
|
||||
if shortcut.Name == name {
|
||||
return shortcut
|
||||
}
|
||||
}
|
||||
t.Fatalf("shortcut %q not found", name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func assertRequiredFlags(t *testing.T, shortcut *common.Shortcut, expectedFlags []string) {
|
||||
func newTemplateTestServer(t *testing.T, handler http.HandlerFunc) *httptest.Server {
|
||||
t.Helper()
|
||||
for _, expectedFlag := range expectedFlags {
|
||||
found := false
|
||||
for _, flag := range shortcut.Flags {
|
||||
if flag.Name == expectedFlag && flag.Required {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("Shortcut %q: expected required flag %q not found", shortcut.Name, expectedFlag)
|
||||
}
|
||||
return httptest.NewServer(handler)
|
||||
}
|
||||
|
||||
func assertRequest(t *testing.T, r *http.Request, method, path string) {
|
||||
t.Helper()
|
||||
if r.Method != method || r.URL.Path != path {
|
||||
t.Fatalf("got request %s %s, want %s %s", r.Method, r.URL.Path, method, path)
|
||||
}
|
||||
}
|
||||
|
||||
func decodeJSON(t *testing.T, r *http.Request) map[string]interface{} {
|
||||
t.Helper()
|
||||
var payload map[string]interface{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||
t.Fatalf("failed to decode request body: %v", err)
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
func writeJSON(t *testing.T, w http.ResponseWriter, payload interface{}) {
|
||||
t.Helper()
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(payload); err != nil {
|
||||
t.Fatalf("failed to write response: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func assertEqual(t *testing.T, got interface{}, want interface{}) {
|
||||
t.Helper()
|
||||
if got != want {
|
||||
t.Fatalf("got %v (%T), want %v (%T)", got, got, want, want)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue