feat(template): 新增项目模板管理命令

新增 template 命令,支持项目模板的增删改查操作:
- +list: 列出所有模板
- +get: 获取模板详情
- +create: 创建新模板
- +update: 更新模板
- +delete: 删除模板
This commit is contained in:
weidongde 2026-06-16 19:10:49 +08:00
parent 982f2cb336
commit d5e149e679
6 changed files with 305 additions and 1 deletions

View File

@ -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

View File

@ -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

View File

@ -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",
}

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",
"dataset", "health", "ignore",
"dataset", "health", "ignore", "template",
}
groupSet := map[string]bool{}

View File

@ -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)
},
},
}
}

View File

@ -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)
}
}
}