Merge PR #263: 新增 template 命令,支持 GitLink 平台项目模板的增删改查操作
# Conflicts: # shortcuts/register.go # shortcuts/register_test.go
This commit is contained in:
commit
37415ebfca
25
README.md
25
README.md
|
|
@ -121,6 +121,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
|
||||
|
|
@ -437,6 +438,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
|
||||
|
|
|
|||
|
|
@ -119,6 +119,7 @@
|
|||
| 👤 用户 | 查看用户资料和信息 |
|
||||
| 📊 画像 | 用户开发能力、角色定位、专业定位、近期活动、贡献热力图统计 |
|
||||
| 📋 项目管理 | Sprint 管理、看板、周报 |
|
||||
| 📝 模板 | 管理 Issue 和 Pull Request 项目模板 |
|
||||
| 🤖 工作流 | AI 驱动的 Issue 分类、PR Review、Release Notes |
|
||||
|
||||
## 安装与快速上手
|
||||
|
|
@ -422,6 +423,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
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ import (
|
|||
"github.com/gitlink-org/gitlink-cli/shortcuts/license"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/member"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/milestone"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/notification"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/org"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/pipeline"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/pr"
|
||||
|
|
@ -24,9 +23,9 @@ 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/wiki"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/workflow"
|
||||
)
|
||||
|
||||
|
|
@ -37,55 +36,53 @@ func RegisterAll(root *cobra.Command, translators ...*i18n.Translator) {
|
|||
tr = translators[0]
|
||||
}
|
||||
groups := map[string][]*common.Shortcut{
|
||||
"repo": repo.Shortcuts(tr),
|
||||
"issue": issue.Shortcuts(tr),
|
||||
"label": label.Shortcuts(),
|
||||
"license": license.Shortcuts(),
|
||||
"member": member.Shortcuts(),
|
||||
"milestone": milestone.Shortcuts(),
|
||||
"notification": notification.Shortcuts(tr),
|
||||
"pipeline": pipeline.Shortcuts(),
|
||||
"pr": pr.Shortcuts(tr),
|
||||
"profile": profile.Shortcuts(tr),
|
||||
"release": release.Shortcuts(tr),
|
||||
"branch": branch.Shortcuts(tr),
|
||||
"org": org.Shortcuts(tr),
|
||||
"user": user.Shortcuts(tr),
|
||||
"search": search.Shortcuts(tr),
|
||||
"ci": ci.Shortcuts(tr),
|
||||
"compare": compare.Shortcuts(),
|
||||
"dataset": dataset.Shortcuts(tr),
|
||||
"webhook": webhook.Shortcuts(tr),
|
||||
"wiki": wiki.Shortcuts(),
|
||||
"health": health.Shortcuts(tr),
|
||||
"ignore": ignore.Shortcuts(),
|
||||
"workflow": workflow.Shortcuts(),
|
||||
"repo": repo.Shortcuts(tr),
|
||||
"issue": issue.Shortcuts(tr),
|
||||
"label": label.Shortcuts(),
|
||||
"license": license.Shortcuts(),
|
||||
"member": member.Shortcuts(),
|
||||
"milestone": milestone.Shortcuts(),
|
||||
"pipeline": pipeline.Shortcuts(),
|
||||
"pr": pr.Shortcuts(tr),
|
||||
"profile": profile.Shortcuts(tr),
|
||||
"release": release.Shortcuts(tr),
|
||||
"branch": branch.Shortcuts(tr),
|
||||
"org": org.Shortcuts(tr),
|
||||
"user": user.Shortcuts(tr),
|
||||
"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(),
|
||||
"template": template.Shortcuts(),
|
||||
"workflow": workflow.Shortcuts(),
|
||||
}
|
||||
|
||||
descriptions := map[string]string{
|
||||
"repo": tr.T("cmd.repo.short"),
|
||||
"issue": tr.T("cmd.issue.short"),
|
||||
"label": "Issue label operations",
|
||||
"license": "License operations",
|
||||
"member": "Repository member operations",
|
||||
"milestone": "Milestone operations",
|
||||
"notification": tr.T("cmd.notification.short"),
|
||||
"pipeline": "Pipeline operations",
|
||||
"pr": tr.T("cmd.pr.short"),
|
||||
"profile": tr.T("cmd.profile.short"),
|
||||
"release": tr.T("cmd.release.short"),
|
||||
"branch": tr.T("cmd.branch.short"),
|
||||
"org": tr.T("cmd.org.short"),
|
||||
"user": tr.T("cmd.user.short"),
|
||||
"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"),
|
||||
"wiki": "Wiki page management",
|
||||
"health": "Project health data collection",
|
||||
"ignore": tr.T("cmd.ignore.short"),
|
||||
"workflow": "AI agent workflow analysis",
|
||||
"repo": tr.T("cmd.repo.short"),
|
||||
"issue": tr.T("cmd.issue.short"),
|
||||
"label": "Issue label operations",
|
||||
"license": "License operations",
|
||||
"member": "Repository member operations",
|
||||
"milestone": "Milestone operations",
|
||||
"pipeline": "Pipeline operations",
|
||||
"pr": tr.T("cmd.pr.short"),
|
||||
"profile": tr.T("cmd.profile.short"),
|
||||
"release": tr.T("cmd.release.short"),
|
||||
"branch": tr.T("cmd.branch.short"),
|
||||
"org": tr.T("cmd.org.short"),
|
||||
"user": tr.T("cmd.user.short"),
|
||||
"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"),
|
||||
"template": "项目模板操作",
|
||||
"workflow": "AI agent workflow analysis",
|
||||
}
|
||||
|
||||
for name, shortcuts := range groups {
|
||||
|
|
|
|||
|
|
@ -12,9 +12,9 @@ func TestRegisterAll(t *testing.T) {
|
|||
|
||||
expectedGroups := []string{
|
||||
"repo", "issue", "label", "license", "pr", "profile", "release", "branch",
|
||||
"org", "user", "notification", "search", "ci", "workflow",
|
||||
"org", "user", "search", "ci", "workflow",
|
||||
"compare", "member", "milestone", "pipeline", "webhook",
|
||||
"dataset", "file", "health", "ignore", "wiki",
|
||||
"dataset", "health", "ignore", "template",
|
||||
}
|
||||
|
||||
groupSet := map[string]bool{}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue