From d5e149e679e01a03ce5176126a1a329954d9871f Mon Sep 17 00:00:00 2001 From: weidongde Date: Tue, 16 Jun 2026 19:10:49 +0800 Subject: [PATCH] =?UTF-8?q?feat(template):=20=E6=96=B0=E5=A2=9E=E9=A1=B9?= =?UTF-8?q?=E7=9B=AE=E6=A8=A1=E6=9D=BF=E7=AE=A1=E7=90=86=E5=91=BD=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 template 命令,支持项目模板的增删改查操作: - +list: 列出所有模板 - +get: 获取模板详情 - +create: 创建新模板 - +update: 更新模板 - +delete: 删除模板 --- README.md | 25 +++++ README.zh-CN.md | 25 +++++ shortcuts/register.go | 3 + shortcuts/register_test.go | 2 +- shortcuts/template/template.go | 162 ++++++++++++++++++++++++++++ shortcuts/template/template_test.go | 89 +++++++++++++++ 6 files changed, 305 insertions(+), 1 deletion(-) create mode 100644 shortcuts/template/template.go create mode 100644 shortcuts/template/template_test.go diff --git a/README.md b/README.md index 71837ae..f9f7d60 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,7 @@ The official [GitLink](https://www.gitlink.org.cn) CLI tool — built for humans | 👤 User | View user profiles and info | | 📊 Profile | User ability, role, major, activity, and contribution statistics | | 📋 PM | Sprint management, kanban boards, weekly reports | +| 📝 Template | Manage project templates for issues and pull requests | | 🤖 Workflow | AI-powered issue triage, PR review, release notes | ## Installation & Quick Start @@ -373,6 +374,30 @@ 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 template details +gitlink-cli template +get --owner Gitlink --repo forgeplus -i 1 + +# Create an issue template +gitlink-cli template +create --owner Gitlink --repo forgeplus \ + -t "ProjectTemplates::Issue" -n "Bug Report" -c "## Description\n..." + +# Create a pull request template +gitlink-cli template +create --owner Gitlink --repo forgeplus \ + -t "ProjectTemplates::PullRequest" -n "Feature PR" -c "## Summary\n..." + +# Update a template +gitlink-cli template +update --owner Gitlink --repo forgeplus -i 1 -n "Updated Name" + +# Delete a template +gitlink-cli template +delete --owner Gitlink --repo forgeplus -i 1 +``` + ### Pull Requests ```bash diff --git a/README.zh-CN.md b/README.zh-CN.md index 1661a1e..4a409ba 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -118,6 +118,7 @@ | 👤 用户 | 查看用户资料和信息 | | 📊 画像 | 用户开发能力、角色定位、专业定位、近期活动、贡献热力图统计 | | 📋 项目管理 | Sprint 管理、看板、周报 | +| 📝 模板 | 管理 Issue 和 Pull Request 项目模板 | | 🤖 工作流 | AI 驱动的 Issue 分类、PR Review、Release Notes | ## 安装与快速上手 @@ -383,6 +384,30 @@ gitlink-cli label +update --owner Gitlink --repo forgeplus -i 42 -c "#00FF00" gitlink-cli label +delete --owner Gitlink --repo forgeplus -i 42 ``` +### 模板管理 + +```bash +# 列出项目模板 +gitlink-cli template +list --owner Gitlink --repo forgeplus + +# 获取模板详情 +gitlink-cli template +get --owner Gitlink --repo forgeplus -i 1 + +# 创建 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..." + +# 更新模板 +gitlink-cli template +update --owner Gitlink --repo forgeplus -i 1 -n "新名称" + +# 删除模板 +gitlink-cli template +delete --owner Gitlink --repo forgeplus -i 1 +``` + ### Pull Request ```bash diff --git a/shortcuts/register.go b/shortcuts/register.go index 00e2426..ea27013 100644 --- a/shortcuts/register.go +++ b/shortcuts/register.go @@ -23,6 +23,7 @@ import ( "github.com/gitlink-org/gitlink-cli/shortcuts/release" "github.com/gitlink-org/gitlink-cli/shortcuts/repo" "github.com/gitlink-org/gitlink-cli/shortcuts/search" + "github.com/gitlink-org/gitlink-cli/shortcuts/template" "github.com/gitlink-org/gitlink-cli/shortcuts/user" "github.com/gitlink-org/gitlink-cli/shortcuts/webhook" "github.com/gitlink-org/gitlink-cli/shortcuts/workflow" @@ -55,6 +56,7 @@ func RegisterAll(root *cobra.Command, translators ...*i18n.Translator) { "webhook": webhook.Shortcuts(tr), "health": health.Shortcuts(tr), "ignore": ignore.Shortcuts(), + "template": template.Shortcuts(), "workflow": workflow.Shortcuts(), } @@ -79,6 +81,7 @@ func RegisterAll(root *cobra.Command, translators ...*i18n.Translator) { "webhook": tr.T("cmd.webhook.short"), "health": "Project health data collection", "ignore": tr.T("cmd.ignore.short"), + "template": "项目模板操作", "workflow": "AI agent workflow analysis", } diff --git a/shortcuts/register_test.go b/shortcuts/register_test.go index 1c28241..c4bc025 100644 --- a/shortcuts/register_test.go +++ b/shortcuts/register_test.go @@ -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", - "dataset", "health", "ignore", + "dataset", "health", "ignore", "template", } groupSet := map[string]bool{} diff --git a/shortcuts/template/template.go b/shortcuts/template/template.go new file mode 100644 index 0000000..e65f4d4 --- /dev/null +++ b/shortcuts/template/template.go @@ -0,0 +1,162 @@ +package template + +import ( + "fmt" + + "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 +} + +// 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 { + return []*common.Shortcut{ + { + Name: "list", + Description: "列出项目模板", + Flags: []common.Flag{}, + Run: func(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + env, err := ctx.CallAPI("GET", templatePath(ctx), nil) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + { + Name: "get", + Description: "根据 ID 获取项目模板", + Flags: []common.Flag{ + {Name: "id", Short: "i", Usage: "模板 ID", Required: true}, + }, + Run: func(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + id, err := ctx.RequireArg("id") + if err != nil { + return err + } + env, err := ctx.CallAPI("GET", templateItemPath(ctx, id), nil) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + { + Name: "create", + Description: "创建项目模板", + 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: "update", + Description: "更新项目模板", + 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: "delete", + Description: "删除项目模板", + Flags: []common.Flag{ + {Name: "id", Short: "i", Usage: "模板 ID", Required: true}, + }, + Run: func(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return err + } + id, err := ctx.RequireArg("id") + if err != nil { + return err + } + env, err := ctx.CallAPI("DELETE", templateItemPath(ctx, id), nil) + if err != nil { + return err + } + return ctx.Output(env) + }, + }, + } +} diff --git a/shortcuts/template/template_test.go b/shortcuts/template/template_test.go new file mode 100644 index 0000000..4917ec9 --- /dev/null +++ b/shortcuts/template/template_test.go @@ -0,0 +1,89 @@ +package template + +import ( + "testing" + + "github.com/gitlink-org/gitlink-cli/shortcuts/common" +) + +func TestShortcutsReturnsAllCommands(t *testing.T) { + shortcuts := Shortcuts() + + expectedCommands := []string{"list", "get", "create", "update", "delete"} + if len(shortcuts) != len(expectedCommands) { + t.Errorf("Expected %d shortcuts, got %d", len(expectedCommands), len(shortcuts)) + } + + 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) + } + } +} + +func TestShortcutDescriptions(t *testing.T) { + shortcuts := Shortcuts() + + for _, s := range shortcuts { + if s.Description == "" { + t.Errorf("Shortcut %q has empty description", s.Name) + } + } +} + +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") + } + 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 + } + } + return nil +} + +func assertRequiredFlags(t *testing.T, shortcut *common.Shortcut, expectedFlags []string) { + 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) + } + } +}