feat: add git tag shortcut group
This commit is contained in:
parent
71ca2bb683
commit
bfb4ba98bb
18
README.md
18
README.md
|
|
@ -109,6 +109,7 @@ The official [GitLink](https://www.gitlink.org.cn) CLI tool — built for humans
|
|||
| 🔀 PR | Create, merge, review pull requests, view changed files |
|
||||
| 👥 Member | List, add, remove repository members, change roles, create and accept invite links |
|
||||
| 🌿 Branch | Create, delete, list, protect, unprotect branches |
|
||||
| 🏷️ Tag | List, search, view, and delete Git tags |
|
||||
| 🏷️ Release | Create, edit, update, view, delete releases |
|
||||
| 🏢 Org | Manage organizations, members, teams |
|
||||
| 🔧 CI | View builds, logs, CI/CD operations |
|
||||
|
|
@ -453,6 +454,22 @@ gitlink-cli branch +protect --name main
|
|||
gitlink-cli branch +unprotect --name main
|
||||
```
|
||||
|
||||
### Tag Management
|
||||
|
||||
```bash
|
||||
# List tags with commit details
|
||||
gitlink-cli tag +list --owner Gitlink --repo forgeplus --page 1 --limit 20
|
||||
|
||||
# List tag names without pagination
|
||||
gitlink-cli tag +names --owner Gitlink --repo forgeplus -k v1
|
||||
|
||||
# View a tag
|
||||
gitlink-cli tag +view --owner Gitlink --repo forgeplus --name v1.0.0
|
||||
|
||||
# Delete a tag
|
||||
gitlink-cli tag +delete --owner Gitlink --repo forgeplus --name release/v1.0.0
|
||||
```
|
||||
|
||||
### Release Management
|
||||
|
||||
```bash
|
||||
|
|
@ -742,6 +759,7 @@ See [skills/README.md](./skills/README.md) for details.
|
|||
| `gitlink-pr` | Pull request operations (create, merge, review, etc.) |
|
||||
| `gitlink-member` | Repository member and invite link management |
|
||||
| `gitlink-branch` | Branch management (create, delete, list, protect, unprotect) |
|
||||
| `gitlink-tag` | Git tag operations (list, search, view, delete) |
|
||||
| `gitlink-release` | Release management (create, edit, update, view, delete, etc.) |
|
||||
| `gitlink-ci` | CI/CD operations (builds, logs, etc.) |
|
||||
| `gitlink-pipeline` | Pipeline workflow operations (runs, logs, enable, disable, delete, etc.) |
|
||||
|
|
|
|||
|
|
@ -109,6 +109,7 @@
|
|||
| 🔀 PR | 创建、合并、Review Pull Request,查看变更文件 |
|
||||
| 👥 成员 | 列出、添加、移除仓库成员,调整角色,生成和接受邀请链接 |
|
||||
| 🌿 分支 | 创建、删除、保护分支 |
|
||||
| 🏷️ 标签 | 列出、搜索、查看、删除 Git 标签 |
|
||||
| 🏷️ 发布 | 创建、编辑、更新、查看、删除 Release |
|
||||
| 🏢 组织 | 管理组织、成员、团队 |
|
||||
| 🔧 CI | 查看构建、日志、CI/CD 操作 |
|
||||
|
|
@ -444,6 +445,22 @@ gitlink-cli pr +review --owner Gitlink --repo forgeplus -i 42 --status approved
|
|||
gitlink-cli pr +review --owner Gitlink --repo forgeplus -i 42 --status approved -c "LGTM"
|
||||
```
|
||||
|
||||
### 标签管理
|
||||
|
||||
```bash
|
||||
# 列出标签及提交详情
|
||||
gitlink-cli tag +list --owner Gitlink --repo forgeplus --page 1 --limit 20
|
||||
|
||||
# 无分页列出标签名称
|
||||
gitlink-cli tag +names --owner Gitlink --repo forgeplus -k v1
|
||||
|
||||
# 查看标签详情
|
||||
gitlink-cli tag +view --owner Gitlink --repo forgeplus --name v1.0.0
|
||||
|
||||
# 删除标签
|
||||
gitlink-cli tag +delete --owner Gitlink --repo forgeplus --name release/v1.0.0
|
||||
```
|
||||
|
||||
### 发布管理
|
||||
|
||||
```bash
|
||||
|
|
@ -615,6 +632,7 @@ git push gitlink
|
|||
| `gitlink-issue` | Issue 操作(创建、更新、关闭、批量更新/删除、评论等) |
|
||||
| `gitlink-pr` | Pull Request 操作(创建、合并、Review 等) |
|
||||
| `gitlink-member` | 仓库成员与邀请链接管理 |
|
||||
| `gitlink-tag` | Git 标签管理(列表、搜索、查看、删除) |
|
||||
| `gitlink-release` | 发布管理(创建、编辑、更新、查看、删除等) |
|
||||
| `gitlink-org` | 组织管理(成员、团队等) |
|
||||
| `gitlink-ci` | CI/CD 操作(构建、日志等) |
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
# Tag Shortcut
|
||||
|
||||
## Summary
|
||||
|
||||
Adds a `tag` shortcut group for Git repository tags. This complements release management by exposing the underlying tag inspection and deletion APIs directly.
|
||||
|
||||
## Commands
|
||||
|
||||
| Command | Purpose |
|
||||
|---------|---------|
|
||||
| `gitlink-cli tag +list` | List repository tags with commit details using the paginated v1 API |
|
||||
| `gitlink-cli tag +names` | List tag names without pagination and optionally filter by keyword |
|
||||
| `gitlink-cli tag +view` | View a single tag by name |
|
||||
| `gitlink-cli tag +delete` | Delete a tag by name |
|
||||
|
||||
## Validation
|
||||
|
||||
- `--page` and `--limit` must be positive integers.
|
||||
- `--name` is required and trimmed for view/delete operations.
|
||||
- Tag names are URL path escaped before calling v1 item endpoints, so names such as `release/v1.0.0` do not break routing.
|
||||
|
||||
## Tests
|
||||
|
||||
The unit tests verify paginated listing, name-only lookup, view/delete endpoint paths, tag-name escaping, and invalid input handling.
|
||||
|
|
@ -93,6 +93,11 @@
|
|||
"cmd.search.repos.short": "Search repositories",
|
||||
"cmd.search.short": "Search operations",
|
||||
"cmd.search.users.short": "Search users",
|
||||
"cmd.tag.delete.short": "Delete a repository tag",
|
||||
"cmd.tag.list.short": "List repository tags with commit details",
|
||||
"cmd.tag.names.short": "List tag names without pagination",
|
||||
"cmd.tag.short": "Git tag operations",
|
||||
"cmd.tag.view.short": "View repository tag details",
|
||||
"cmd.user.info.short": "Show user profile",
|
||||
"cmd.user.me.short": "Show current authenticated user",
|
||||
"cmd.user.short": "User operations",
|
||||
|
|
@ -217,6 +222,8 @@
|
|||
"flag.search.keyword": "Search keyword",
|
||||
"flag.sort_by": "Sort field",
|
||||
"flag.sort_direction": "Sort direction: asc, desc",
|
||||
"flag.tag.keyword": "Search keyword for tag names",
|
||||
"flag.tag.name": "Tag name",
|
||||
"flag.user": "User login (default: current user)",
|
||||
"flag.user.login": "User login name",
|
||||
"flag.webhook.active": "Whether the webhook is active: true or false",
|
||||
|
|
|
|||
|
|
@ -93,6 +93,11 @@
|
|||
"cmd.search.repos.short": "搜索仓库",
|
||||
"cmd.search.short": "搜索操作",
|
||||
"cmd.search.users.short": "搜索用户",
|
||||
"cmd.tag.delete.short": "删除仓库标签",
|
||||
"cmd.tag.list.short": "列出仓库标签及提交详情",
|
||||
"cmd.tag.names.short": "列出标签名称",
|
||||
"cmd.tag.short": "Git 标签操作",
|
||||
"cmd.tag.view.short": "查看仓库标签详情",
|
||||
"cmd.user.info.short": "显示用户资料",
|
||||
"cmd.user.me.short": "显示当前认证用户",
|
||||
"cmd.user.short": "用户操作",
|
||||
|
|
@ -217,6 +222,8 @@
|
|||
"flag.search.keyword": "搜索关键词",
|
||||
"flag.sort_by": "排序字段",
|
||||
"flag.sort_direction": "排序方向:asc、desc",
|
||||
"flag.tag.keyword": "标签名称搜索关键词",
|
||||
"flag.tag.name": "标签名称",
|
||||
"flag.user": "用户登录名(默认:当前用户)",
|
||||
"flag.user.login": "用户登录名",
|
||||
"flag.webhook.active": "Webhook 是否启用:true 或 false",
|
||||
|
|
|
|||
|
|
@ -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/tag"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/user"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/webhook"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/wiki"
|
||||
|
|
@ -50,6 +51,7 @@ func RegisterAll(root *cobra.Command, translators ...*i18n.Translator) {
|
|||
"org": org.Shortcuts(tr),
|
||||
"user": user.Shortcuts(tr),
|
||||
"search": search.Shortcuts(tr),
|
||||
"tag": tag.Shortcuts(tr),
|
||||
"ci": ci.Shortcuts(tr),
|
||||
"compare": compare.Shortcuts(),
|
||||
"dataset": dataset.Shortcuts(tr),
|
||||
|
|
@ -75,6 +77,7 @@ func RegisterAll(root *cobra.Command, translators ...*i18n.Translator) {
|
|||
"org": tr.T("cmd.org.short"),
|
||||
"user": tr.T("cmd.user.short"),
|
||||
"search": tr.T("cmd.search.short"),
|
||||
"tag": tr.T("cmd.tag.short"),
|
||||
"ci": tr.T("cmd.ci.short"),
|
||||
"compare": "Compare branches, tags, or commits",
|
||||
"dataset": tr.T("cmd.dataset.short"),
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ func TestRegisterAll(t *testing.T) {
|
|||
|
||||
expectedGroups := []string{
|
||||
"repo", "issue", "label", "license", "pr", "profile", "release", "branch",
|
||||
"org", "user", "search", "ci", "workflow",
|
||||
"org", "user", "search", "tag", "ci", "workflow",
|
||||
"compare", "member", "milestone", "pipeline", "webhook",
|
||||
"dataset", "health", "ignore", "wiki",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,164 @@
|
|||
package tag
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/internal/i18n"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
||||
)
|
||||
|
||||
func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
|
||||
tr := i18n.Default()
|
||||
if len(translators) > 0 && translators[0] != nil {
|
||||
tr = translators[0]
|
||||
}
|
||||
return []*common.Shortcut{
|
||||
{
|
||||
Name: "list",
|
||||
Description: tr.T("cmd.tag.list.short"),
|
||||
Flags: []common.Flag{
|
||||
{Name: "page", Short: "p", Usage: tr.T("flag.page"), Default: "1"},
|
||||
{Name: "limit", Short: "l", Usage: tr.T("flag.limit"), Default: "20"},
|
||||
},
|
||||
Run: runList,
|
||||
},
|
||||
{
|
||||
Name: "names",
|
||||
Description: tr.T("cmd.tag.names.short"),
|
||||
Flags: []common.Flag{
|
||||
{Name: "keyword", Short: "k", Usage: tr.T("flag.tag.keyword")},
|
||||
},
|
||||
Run: runNames,
|
||||
},
|
||||
{
|
||||
Name: "view",
|
||||
Description: tr.T("cmd.tag.view.short"),
|
||||
Flags: []common.Flag{
|
||||
{Name: "name", Short: "n", Usage: tr.T("flag.tag.name"), Required: true},
|
||||
},
|
||||
Run: runView,
|
||||
},
|
||||
{
|
||||
Name: "delete",
|
||||
Description: tr.T("cmd.tag.delete.short"),
|
||||
Flags: []common.Flag{
|
||||
{Name: "name", Short: "n", Usage: tr.T("flag.tag.name"), Required: true},
|
||||
},
|
||||
Run: runDelete,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func runList(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
query, err := paginationQuery(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
env, err := ctx.CallAPIWithQuery("GET", tagPath(ctx), query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
}
|
||||
|
||||
func runNames(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
query := url.Values{}
|
||||
query.Set("only_name", "true")
|
||||
if keyword := strings.TrimSpace(ctx.Arg("keyword")); keyword != "" {
|
||||
query.Set("name", keyword)
|
||||
}
|
||||
env, err := ctx.CallAPIWithQuery("GET", ctx.RepoPath()+"/tags", query)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
}
|
||||
|
||||
func runView(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
name, err := requireTagName(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
env, err := ctx.CallAPI("GET", tagItemPath(ctx, name), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
}
|
||||
|
||||
func runDelete(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
name, err := requireTagName(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
env, err := ctx.CallAPI("DELETE", tagItemPath(ctx, name), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
}
|
||||
|
||||
func tagPath(ctx *common.RuntimeContext) string {
|
||||
return fmt.Sprintf("/v1/%s/%s/tags", ctx.Owner, ctx.Repo)
|
||||
}
|
||||
|
||||
func tagItemPath(ctx *common.RuntimeContext, name string) string {
|
||||
return fmt.Sprintf("%s/%s", tagPath(ctx), url.PathEscape(name))
|
||||
}
|
||||
|
||||
func paginationQuery(ctx *common.RuntimeContext) (url.Values, error) {
|
||||
page, err := positiveIntArg(defaultString(ctx.Arg("page"), "1"), "page")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
limit, err := positiveIntArg(defaultString(ctx.Arg("limit"), "20"), "limit")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
query := url.Values{}
|
||||
query.Set("page", strconv.Itoa(page))
|
||||
query.Set("limit", strconv.Itoa(limit))
|
||||
return query, nil
|
||||
}
|
||||
|
||||
func defaultString(value, fallback string) string {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return fallback
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func positiveIntArg(value, name string) (int, error) {
|
||||
parsed, err := strconv.Atoi(strings.TrimSpace(value))
|
||||
if err != nil || parsed <= 0 {
|
||||
return 0, fmt.Errorf("invalid --%s %q: use a positive integer", name, value)
|
||||
}
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
func requireTagName(ctx *common.RuntimeContext) (string, error) {
|
||||
name, err := ctx.RequireArg("name")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
return "", fmt.Errorf("required flag --name is empty")
|
||||
}
|
||||
return name, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,179 @@
|
|||
package tag
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/internal/client"
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
||||
)
|
||||
|
||||
func TestTagListUsesV1Pagination(t *testing.T) {
|
||||
server := newTagTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
assertRequest(t, r, "GET", "/v1/owner/repo/tags.json")
|
||||
assertEqual(t, r.URL.Query().Get("page"), "2")
|
||||
assertEqual(t, r.URL.Query().Get("limit"), "50")
|
||||
writeJSON(t, w, map[string]interface{}{"total_count": 1, "tags": []interface{}{}})
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runTagShortcut(t, server, "list", map[string]string{
|
||||
"page": "2",
|
||||
"limit": "50",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("list shortcut failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTagListUsesDefaultPagination(t *testing.T) {
|
||||
server := newTagTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
assertRequest(t, r, "GET", "/v1/owner/repo/tags.json")
|
||||
assertEqual(t, r.URL.Query().Get("page"), "1")
|
||||
assertEqual(t, r.URL.Query().Get("limit"), "20")
|
||||
writeJSON(t, w, map[string]interface{}{"total_count": 0, "tags": []interface{}{}})
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
if err := runTagShortcut(t, server, "list", nil); err != nil {
|
||||
t.Fatalf("list shortcut failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTagNamesUsesNameSearch(t *testing.T) {
|
||||
server := newTagTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
assertRequest(t, r, "GET", "/owner/repo/tags.json")
|
||||
assertEqual(t, r.URL.Query().Get("only_name"), "true")
|
||||
assertEqual(t, r.URL.Query().Get("name"), "v1")
|
||||
writeJSON(t, w, map[string]interface{}{
|
||||
"total_count": 1,
|
||||
"tags": []interface{}{map[string]interface{}{"name": "v1.0"}},
|
||||
})
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
if err := runTagShortcut(t, server, "names", map[string]string{"keyword": " v1 "}); err != nil {
|
||||
t.Fatalf("names shortcut failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTagViewEscapesTagName(t *testing.T) {
|
||||
server := newTagTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
assertRequest(t, r, "GET", "/v1/owner/repo/tags/release%2Fv1.0.json")
|
||||
writeJSON(t, w, map[string]interface{}{"name": "release/v1.0"})
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
if err := runTagShortcut(t, server, "view", map[string]string{"name": "release/v1.0"}); err != nil {
|
||||
t.Fatalf("view shortcut failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTagDeleteEscapesTagName(t *testing.T) {
|
||||
server := newTagTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
assertRequest(t, r, "DELETE", "/v1/owner/repo/tags/release%2Fv1.0.json")
|
||||
writeJSON(t, w, map[string]interface{}{"status": 0, "message": "success"})
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
if err := runTagShortcut(t, server, "delete", map[string]string{"name": "release/v1.0"}); err != nil {
|
||||
t.Fatalf("delete shortcut failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTagValidation(t *testing.T) {
|
||||
server := newTagTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("invalid input should not call API, got: %s %s", r.Method, r.URL.Path)
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
shortcut string
|
||||
args map[string]string
|
||||
}{
|
||||
{
|
||||
name: "invalid page",
|
||||
shortcut: "list",
|
||||
args: map[string]string{"page": "0", "limit": "20"},
|
||||
},
|
||||
{
|
||||
name: "invalid limit",
|
||||
shortcut: "list",
|
||||
args: map[string]string{"page": "1", "limit": "many"},
|
||||
},
|
||||
{
|
||||
name: "empty tag name",
|
||||
shortcut: "view",
|
||||
args: map[string]string{"name": " "},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if err := runTagShortcut(t, server, tc.shortcut, tc.args); err == nil {
|
||||
t.Fatal("expected validation error")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func runTagShortcut(t *testing.T, server *httptest.Server, name string, args map[string]string) error {
|
||||
t.Helper()
|
||||
shortcut := findTagShortcut(t, name)
|
||||
ctx := &common.RuntimeContext{
|
||||
Client: &client.Client{
|
||||
HTTP: server.Client(),
|
||||
BaseURL: server.URL,
|
||||
},
|
||||
Owner: "owner",
|
||||
Repo: "repo",
|
||||
Format: "json",
|
||||
Args: args,
|
||||
}
|
||||
if ctx.Args == nil {
|
||||
ctx.Args = map[string]string{}
|
||||
}
|
||||
return shortcut.Run(ctx)
|
||||
}
|
||||
|
||||
func findTagShortcut(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 newTagTestServer(t *testing.T, handler http.HandlerFunc) *httptest.Server {
|
||||
t.Helper()
|
||||
return httptest.NewServer(handler)
|
||||
}
|
||||
|
||||
func assertRequest(t *testing.T, r *http.Request, method, path string) {
|
||||
t.Helper()
|
||||
if r.Method != method || r.URL.EscapedPath() != path {
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.EscapedPath())
|
||||
}
|
||||
}
|
||||
|
||||
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 fmt.Sprintf("%v", got) != fmt.Sprintf("%v", want) {
|
||||
t.Fatalf("got %v (%T), want %v (%T)", got, got, want, want)
|
||||
}
|
||||
}
|
||||
|
|
@ -83,6 +83,8 @@ skills/
|
|||
│ ├── SKILL.md # 分支操作指南
|
||||
│ └── examples/
|
||||
│ └── branch-workflow.md # 分支工作流
|
||||
├── gitlink-tag/ # Git 标签管理
|
||||
│ └── SKILL.md # 标签操作指南
|
||||
├── gitlink-release/ # 版本发布
|
||||
│ ├── SKILL.md # Release 操作指南
|
||||
│ └── references/ # Release 命令参考文档
|
||||
|
|
@ -132,6 +134,7 @@ skills/
|
|||
| **gitlink-pr** | Pull Request | `pr +list`, `pr +create`, `pr +view`, `pr +merge`, `pr +versions`, `pr +version-diff`, `pr +reviews`, `pr +review` |
|
||||
| **gitlink-member** | 仓库成员管理 | `member +list`, `member +add`, `member +batch-add`, `member +role`, `member +invite-link` |
|
||||
| **gitlink-branch** | 分支管理 | `branch +list`, `branch +create`, `branch +delete`, `branch +protect` |
|
||||
| **gitlink-tag** | Git 标签管理 | `tag +list`, `tag +names`, `tag +view`, `tag +delete` |
|
||||
| **gitlink-release** | 版本发布 | `release +list`, `release +create`, `release +edit`, `release +update`, `release +view` |
|
||||
|
||||
### 辅助 Skills
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
---
|
||||
name: gitlink-tag
|
||||
version: 1.0.0
|
||||
description: "Git tag management: list tags, search names, view tag details, and delete tags."
|
||||
metadata:
|
||||
requires:
|
||||
bins: ["gitlink-cli"]
|
||||
cliHelp: "gitlink-cli tag --help"
|
||||
---
|
||||
|
||||
# gitlink-tag
|
||||
|
||||
Use this skill when an agent needs to inspect or manage Git tags in a GitLink repository.
|
||||
|
||||
## Shortcuts
|
||||
|
||||
| Shortcut | Purpose |
|
||||
|----------|---------|
|
||||
| `tag +list` | List tags with commit details |
|
||||
| `tag +names` | List tag names without pagination |
|
||||
| `tag +view` | View a tag by name |
|
||||
| `tag +delete` | Delete a tag by name |
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
gitlink-cli tag +list --owner Gitlink --repo forgeplus --page 1 --limit 20
|
||||
gitlink-cli tag +names --owner Gitlink --repo forgeplus -k v1
|
||||
gitlink-cli tag +view --owner Gitlink --repo forgeplus --name v1.0.0
|
||||
gitlink-cli tag +delete --owner Gitlink --repo forgeplus --name release/v1.0.0
|
||||
```
|
||||
|
||||
## Safety Notes
|
||||
|
||||
- Confirm the target repository before deleting a tag.
|
||||
- Use `tag +names` or `tag +list` first when the exact tag name is unknown.
|
||||
- Tags may contain slashes; pass the full tag name to `--name`.
|
||||
Loading…
Reference in New Issue