feat(label): add guarded batch label shortcuts
This commit is contained in:
parent
71ca2bb683
commit
5f2dba4974
15
README.md
15
README.md
|
|
@ -392,8 +392,19 @@ gitlink-cli label +create --owner Gitlink --repo forgeplus -n bug -d "Something
|
|||
# Update a label (unspecified fields are preserved)
|
||||
gitlink-cli label +update --owner Gitlink --repo forgeplus -i 42 -c "#00FF00"
|
||||
|
||||
# Delete a label
|
||||
gitlink-cli label +delete --owner Gitlink --repo forgeplus -i 42
|
||||
# Delete a label safely
|
||||
gitlink-cli label +delete --owner Gitlink --repo forgeplus -i 42 --dry-run
|
||||
gitlink-cli label +delete --owner Gitlink --repo forgeplus -i 42 --yes
|
||||
|
||||
# Batch create labels safely
|
||||
gitlink-cli label +batch-create --owner Gitlink --repo forgeplus \
|
||||
--labels 'bug:#ee0701:Bug fixes;feature:#0075ca:New features' --dry-run
|
||||
gitlink-cli label +batch-create --owner Gitlink --repo forgeplus \
|
||||
--labels 'bug:#ee0701:Bug fixes;feature:#0075ca:New features' --yes
|
||||
|
||||
# Batch delete labels safely
|
||||
gitlink-cli label +batch-delete --owner Gitlink --repo forgeplus --ids 3,5,8 --dry-run
|
||||
gitlink-cli label +batch-delete --owner Gitlink --repo forgeplus --ids 3,5,8 --yes
|
||||
```
|
||||
|
||||
### Pull Requests
|
||||
|
|
|
|||
|
|
@ -402,8 +402,19 @@ gitlink-cli label +create --owner Gitlink --repo forgeplus -n bug -d "功能缺
|
|||
# 更新标签(未指定的字段会被保留)
|
||||
gitlink-cli label +update --owner Gitlink --repo forgeplus -i 42 -c "#00FF00"
|
||||
|
||||
# 删除标签
|
||||
gitlink-cli label +delete --owner Gitlink --repo forgeplus -i 42
|
||||
# 安全删除标签
|
||||
gitlink-cli label +delete --owner Gitlink --repo forgeplus -i 42 --dry-run
|
||||
gitlink-cli label +delete --owner Gitlink --repo forgeplus -i 42 --yes
|
||||
|
||||
# 安全批量创建标签
|
||||
gitlink-cli label +batch-create --owner Gitlink --repo forgeplus \
|
||||
--labels 'bug:#ee0701:Bug 修复;feature:#0075ca:新功能' --dry-run
|
||||
gitlink-cli label +batch-create --owner Gitlink --repo forgeplus \
|
||||
--labels 'bug:#ee0701:Bug 修复;feature:#0075ca:新功能' --yes
|
||||
|
||||
# 安全批量删除标签
|
||||
gitlink-cli label +batch-delete --owner Gitlink --repo forgeplus --ids 3,5,8 --dry-run
|
||||
gitlink-cli label +batch-delete --owner Gitlink --repo forgeplus --ids 3,5,8 --yes
|
||||
```
|
||||
|
||||
### Pull Request
|
||||
|
|
|
|||
|
|
@ -0,0 +1,77 @@
|
|||
# Label batch safety shortcuts
|
||||
|
||||
## Background
|
||||
|
||||
The label shortcut group already supported listing, creating, updating, and
|
||||
deleting issue labels. Deletion executed immediately, and larger taxonomy setup
|
||||
or cleanup workflows still required repeated manual commands or Raw API calls.
|
||||
|
||||
This change adds safer destructive operations and first-class batch helpers.
|
||||
|
||||
## New and changed shortcuts
|
||||
|
||||
- `label +delete` now supports `--dry-run` and requires `--yes` for real
|
||||
deletion.
|
||||
- `label +batch-create` creates multiple labels from a semicolon-separated
|
||||
`name:color:description` list.
|
||||
- `label +batch-delete` deletes multiple labels from a comma-separated ID list.
|
||||
|
||||
## Safety model
|
||||
|
||||
Destructive or multi-write commands should be previewed first:
|
||||
|
||||
```bash
|
||||
gitlink-cli label +delete --owner Gitlink --repo forgeplus -i 42 --dry-run
|
||||
gitlink-cli label +batch-create --owner Gitlink --repo forgeplus \
|
||||
--labels 'bug:#ee0701:Bug fixes;feature:#0075ca:New features' --dry-run
|
||||
gitlink-cli label +batch-delete --owner Gitlink --repo forgeplus --ids 3,5,8 --dry-run
|
||||
```
|
||||
|
||||
After confirmation, pass `--yes`:
|
||||
|
||||
```bash
|
||||
gitlink-cli label +delete --owner Gitlink --repo forgeplus -i 42 --yes
|
||||
gitlink-cli label +batch-create --owner Gitlink --repo forgeplus \
|
||||
--labels 'bug:#ee0701:Bug fixes;feature:#0075ca:New features' --yes
|
||||
gitlink-cli label +batch-delete --owner Gitlink --repo forgeplus --ids 3,5,8 --yes
|
||||
```
|
||||
|
||||
## Parsing rules
|
||||
|
||||
- `label +batch-create --labels` uses semicolons between labels and colons
|
||||
inside each label spec: `name:color:description`.
|
||||
- Missing colors default to `#1E90FF`.
|
||||
- Colors are validated as `#RGB` or `#RRGGBB` before any API call.
|
||||
- `label +batch-delete --ids` accepts comma-separated positive integer IDs and
|
||||
removes duplicates before making requests.
|
||||
|
||||
## Documentation updates
|
||||
|
||||
- README and README.zh-CN include safe delete and batch examples.
|
||||
- `skills/gitlink-label` documents the new shortcuts and safety model.
|
||||
- `skills/gitlink-issue-tag` now recommends label shortcuts instead of Raw API
|
||||
calls for common label workflows.
|
||||
- `skills/gitlink-stale-issue-manager` uses `label +batch-create` for stale
|
||||
label bootstrap steps.
|
||||
|
||||
## Tests
|
||||
|
||||
Unit tests cover:
|
||||
|
||||
- single delete dry-run and `--yes` confirmation;
|
||||
- batch-create dry-run/default preview and real API calls;
|
||||
- batch-delete dry-run/default preview, de-duplication, and real API calls;
|
||||
- parser validation for label specs, colors, and ID lists;
|
||||
- partial batch failure reporting.
|
||||
|
||||
Suggested verification:
|
||||
|
||||
```bash
|
||||
go test ./shortcuts/label ./shortcuts
|
||||
```
|
||||
|
||||
Full project verification:
|
||||
|
||||
```bash
|
||||
go test ./...
|
||||
```
|
||||
|
|
@ -75,25 +75,60 @@ func Shortcuts() []*common.Shortcut {
|
|||
Description: "Delete an issue label",
|
||||
Flags: []common.Flag{
|
||||
{Name: "id", Short: "i", Usage: "Label ID", Required: true},
|
||||
{Name: "dry-run", Usage: "Preview the delete request without removing the label", Bool: true, Default: "false"},
|
||||
{Name: "yes", Usage: "Confirm label deletion", Bool: true, Default: "false"},
|
||||
},
|
||||
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", labelItemPath(ctx, id), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
Run: runDelete,
|
||||
},
|
||||
{
|
||||
Name: "batch-create",
|
||||
Description: "Batch create issue labels from semicolon-separated specs",
|
||||
Flags: []common.Flag{
|
||||
{Name: "labels", Usage: "Semicolon-separated label specs: name:color:description", Required: true},
|
||||
{Name: "dry-run", Usage: "Preview labels without creating them", Bool: true, Default: "false"},
|
||||
{Name: "yes", Usage: "Confirm real batch label creation", Bool: true, Default: "false"},
|
||||
},
|
||||
Run: runBatchCreate,
|
||||
},
|
||||
{
|
||||
Name: "batch-delete",
|
||||
Description: "Batch delete issue labels by IDs",
|
||||
Flags: []common.Flag{
|
||||
{Name: "ids", Usage: "Comma-separated label IDs", Required: true},
|
||||
{Name: "dry-run", Usage: "Preview labels without deleting them", Bool: true, Default: "false"},
|
||||
{Name: "yes", Usage: "Confirm real batch label deletion", Bool: true, Default: "false"},
|
||||
},
|
||||
Run: runBatchDelete,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type labelSpec struct {
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Description string `json:"description" yaml:"description"`
|
||||
Color string `json:"color" yaml:"color"`
|
||||
}
|
||||
|
||||
type labelBatchRequest struct {
|
||||
Method string `json:"method" yaml:"method"`
|
||||
Path string `json:"path" yaml:"path"`
|
||||
Body map[string]interface{} `json:"body,omitempty" yaml:"body,omitempty"`
|
||||
}
|
||||
|
||||
type labelBatchPreview struct {
|
||||
Repository string `json:"repository" yaml:"repository"`
|
||||
DryRun bool `json:"dry_run" yaml:"dry_run"`
|
||||
Action string `json:"action" yaml:"action"`
|
||||
Requests []labelBatchRequest `json:"requests" yaml:"requests"`
|
||||
}
|
||||
|
||||
type labelBatchResult struct {
|
||||
Request labelBatchRequest `json:"request" yaml:"request"`
|
||||
OK bool `json:"ok" yaml:"ok"`
|
||||
Data interface{} `json:"data,omitempty" yaml:"data,omitempty"`
|
||||
Error string `json:"error,omitempty" yaml:"error,omitempty"`
|
||||
}
|
||||
|
||||
func runCreate(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
|
|
@ -163,6 +198,131 @@ func runUpdate(ctx *common.RuntimeContext) error {
|
|||
return ctx.Output(env)
|
||||
}
|
||||
|
||||
func runDelete(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
id, err := ctx.RequireArg("id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
path := labelItemPath(ctx, id)
|
||||
if ctx.Arg("dry-run") == "true" {
|
||||
return ctx.OutputData(labelBatchPreview{
|
||||
Repository: repositoryName(ctx),
|
||||
DryRun: true,
|
||||
Action: "delete_label",
|
||||
Requests: []labelBatchRequest{{
|
||||
Method: "DELETE",
|
||||
Path: path,
|
||||
}},
|
||||
})
|
||||
}
|
||||
if ctx.Arg("yes") != "true" {
|
||||
return fmt.Errorf("label delete is destructive; run with --dry-run first, then pass --yes to confirm")
|
||||
}
|
||||
env, err := ctx.CallAPI("DELETE", path, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
}
|
||||
|
||||
func runBatchCreate(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
specs, err := parseLabelSpecs(ctx.Arg("labels"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
requests := make([]labelBatchRequest, 0, len(specs))
|
||||
for _, spec := range specs {
|
||||
requests = append(requests, labelBatchRequest{
|
||||
Method: "POST",
|
||||
Path: labelPath(ctx),
|
||||
Body: map[string]interface{}{
|
||||
"name": spec.Name,
|
||||
"description": spec.Description,
|
||||
"color": spec.Color,
|
||||
},
|
||||
})
|
||||
}
|
||||
if ctx.Arg("dry-run") == "true" || ctx.Arg("yes") != "true" {
|
||||
return ctx.OutputData(labelBatchPreview{
|
||||
Repository: repositoryName(ctx),
|
||||
DryRun: true,
|
||||
Action: "batch_create_labels",
|
||||
Requests: requests,
|
||||
})
|
||||
}
|
||||
return runLabelBatchRequests(ctx, "batch_create_labels", requests)
|
||||
}
|
||||
|
||||
func runBatchDelete(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
ids, err := parseLabelIDList(ctx.Arg("ids"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
requests := make([]labelBatchRequest, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
requests = append(requests, labelBatchRequest{
|
||||
Method: "DELETE",
|
||||
Path: labelItemPath(ctx, id),
|
||||
})
|
||||
}
|
||||
if ctx.Arg("dry-run") == "true" || ctx.Arg("yes") != "true" {
|
||||
return ctx.OutputData(labelBatchPreview{
|
||||
Repository: repositoryName(ctx),
|
||||
DryRun: true,
|
||||
Action: "batch_delete_labels",
|
||||
Requests: requests,
|
||||
})
|
||||
}
|
||||
return runLabelBatchRequests(ctx, "batch_delete_labels", requests)
|
||||
}
|
||||
|
||||
func runLabelBatchRequests(ctx *common.RuntimeContext, action string, requests []labelBatchRequest) error {
|
||||
results := make([]labelBatchResult, 0, len(requests))
|
||||
succeeded := 0
|
||||
failed := 0
|
||||
for _, request := range requests {
|
||||
env, err := ctx.CallAPI(request.Method, request.Path, request.Body)
|
||||
result := labelBatchResult{Request: request}
|
||||
if err != nil {
|
||||
result.OK = false
|
||||
result.Error = err.Error()
|
||||
failed++
|
||||
} else {
|
||||
result.OK = env.OK
|
||||
result.Data = env.Data
|
||||
if env.OK {
|
||||
succeeded++
|
||||
} else {
|
||||
failed++
|
||||
}
|
||||
}
|
||||
results = append(results, result)
|
||||
}
|
||||
if err := ctx.OutputData(map[string]interface{}{
|
||||
"repository": repositoryName(ctx),
|
||||
"action": action,
|
||||
"count": len(requests),
|
||||
"succeeded": succeeded,
|
||||
"failed": failed,
|
||||
"results": results,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if failed > 0 {
|
||||
return fmt.Errorf("%d of %d label request(s) failed", failed, len(requests))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// fetchLabel looks up a single label by id from the list endpoint. GitLink does
|
||||
// not expose a single-label GET, so we page through the list and match by id.
|
||||
// A nil result (label not found) is not an error: the caller falls back to the
|
||||
|
|
@ -200,6 +360,77 @@ func labelItemPath(ctx *common.RuntimeContext, id string) string {
|
|||
return fmt.Sprintf("%s/%s", labelPath(ctx), url.PathEscape(id))
|
||||
}
|
||||
|
||||
func parseLabelSpecs(raw string) ([]labelSpec, error) {
|
||||
if strings.TrimSpace(raw) == "" {
|
||||
return nil, fmt.Errorf("--labels is required")
|
||||
}
|
||||
parts := strings.Split(raw, ";")
|
||||
specs := make([]labelSpec, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
part = strings.TrimSpace(part)
|
||||
if part == "" {
|
||||
continue
|
||||
}
|
||||
fields := strings.SplitN(part, ":", 3)
|
||||
name := strings.TrimSpace(fields[0])
|
||||
if name == "" {
|
||||
return nil, fmt.Errorf("invalid label spec %q: name is required", part)
|
||||
}
|
||||
color := defaultLabelColor
|
||||
if len(fields) > 1 && strings.TrimSpace(fields[1]) != "" {
|
||||
color = strings.TrimSpace(fields[1])
|
||||
}
|
||||
if err := validateColor(color); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
description := ""
|
||||
if len(fields) > 2 {
|
||||
description = strings.TrimSpace(fields[2])
|
||||
}
|
||||
specs = append(specs, labelSpec{
|
||||
Name: name,
|
||||
Description: description,
|
||||
Color: color,
|
||||
})
|
||||
}
|
||||
if len(specs) == 0 {
|
||||
return nil, fmt.Errorf("--labels must contain at least one label spec")
|
||||
}
|
||||
return specs, nil
|
||||
}
|
||||
|
||||
func parseLabelIDList(raw string) ([]string, error) {
|
||||
if strings.TrimSpace(raw) == "" {
|
||||
return nil, fmt.Errorf("--ids is required")
|
||||
}
|
||||
parts := strings.Split(raw, ",")
|
||||
ids := make([]string, 0, len(parts))
|
||||
seen := map[string]bool{}
|
||||
for _, part := range parts {
|
||||
id := strings.TrimSpace(part)
|
||||
if id == "" {
|
||||
continue
|
||||
}
|
||||
n, err := strconv.Atoi(id)
|
||||
if err != nil || n <= 0 {
|
||||
return nil, fmt.Errorf("invalid label id %q: use positive integer IDs", id)
|
||||
}
|
||||
if seen[id] {
|
||||
continue
|
||||
}
|
||||
seen[id] = true
|
||||
ids = append(ids, id)
|
||||
}
|
||||
if len(ids) == 0 {
|
||||
return nil, fmt.Errorf("--ids must contain at least one label ID")
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
func repositoryName(ctx *common.RuntimeContext) string {
|
||||
return fmt.Sprintf("%s/%s", ctx.Owner, ctx.Repo)
|
||||
}
|
||||
|
||||
func validateColor(color string) error {
|
||||
if !hexColorPattern.MatchString(color) {
|
||||
return fmt.Errorf("invalid --color value %q: use a hex color like #1E90FF or #abc", color)
|
||||
|
|
|
|||
|
|
@ -143,11 +143,157 @@ func TestLabelDelete(t *testing.T) {
|
|||
})
|
||||
defer server.Close()
|
||||
|
||||
if err := runLabelShortcut(t, server, "delete", map[string]string{"id": "7"}); err != nil {
|
||||
if err := runLabelShortcut(t, server, "delete", map[string]string{"id": "7", "yes": "true"}); err != nil {
|
||||
t.Fatalf("delete shortcut failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLabelDeleteDryRunDoesNotCallAPI(t *testing.T) {
|
||||
server := newLabelTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("delete dry-run should not call API, got: %s %s", r.Method, r.URL.Path)
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
if err := runLabelShortcut(t, server, "delete", map[string]string{"id": "7", "dry-run": "true"}); err != nil {
|
||||
t.Fatalf("delete dry-run failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLabelDeleteRequiresYes(t *testing.T) {
|
||||
server := newLabelTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("delete without --yes should not call API, got: %s %s", r.Method, r.URL.Path)
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runLabelShortcut(t, server, "delete", map[string]string{"id": "7"})
|
||||
if err == nil {
|
||||
t.Fatal("expected delete to require --yes")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLabelBatchCreateDryRunDoesNotCallAPI(t *testing.T) {
|
||||
server := newLabelTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("batch-create dry-run should not call API, got: %s %s", r.Method, r.URL.Path)
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runLabelShortcut(t, server, "batch-create", map[string]string{
|
||||
"labels": "bug:#ee0701:Bug fixes;feature:#0075ca:New features",
|
||||
"dry-run": "true",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("batch-create dry-run failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLabelBatchCreateDefaultsToDryRunWithoutYes(t *testing.T) {
|
||||
server := newLabelTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("batch-create without --yes should not call API, got: %s %s", r.Method, r.URL.Path)
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runLabelShortcut(t, server, "batch-create", map[string]string{
|
||||
"labels": "bug:#ee0701:Bug fixes",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("batch-create default dry-run failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLabelBatchCreateWithYesCallsEndpoints(t *testing.T) {
|
||||
var payloads []map[string]interface{}
|
||||
server := newLabelTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
assertRequest(t, r, "POST", "/v1/owner/repo/issue_tags.json")
|
||||
payloads = append(payloads, decodeJSON(t, r))
|
||||
writeJSON(t, w, map[string]interface{}{"status": 0, "message": "success"})
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runLabelShortcut(t, server, "batch-create", map[string]string{
|
||||
"labels": "bug:#ee0701:Bug fixes;feature:#0075ca:New features",
|
||||
"yes": "true",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("batch-create failed: %v", err)
|
||||
}
|
||||
if len(payloads) != 2 {
|
||||
t.Fatalf("got %d payloads, want 2", len(payloads))
|
||||
}
|
||||
assertEqual(t, payloads[0]["name"], "bug")
|
||||
assertEqual(t, payloads[0]["color"], "#ee0701")
|
||||
assertEqual(t, payloads[0]["description"], "Bug fixes")
|
||||
assertEqual(t, payloads[1]["name"], "feature")
|
||||
}
|
||||
|
||||
func TestLabelBatchCreateReportsPartialFailure(t *testing.T) {
|
||||
calls := 0
|
||||
server := newLabelTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
assertRequest(t, r, "POST", "/v1/owner/repo/issue_tags.json")
|
||||
calls++
|
||||
if calls == 2 {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("server error"))
|
||||
return
|
||||
}
|
||||
writeJSON(t, w, map[string]interface{}{"status": 0, "message": "success"})
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runLabelShortcut(t, server, "batch-create", map[string]string{
|
||||
"labels": "bug:#ee0701:Bug fixes;feature:#0075ca:New features",
|
||||
"yes": "true",
|
||||
})
|
||||
if err == nil {
|
||||
t.Fatal("expected partial failure to return an error")
|
||||
}
|
||||
if calls != 2 {
|
||||
t.Fatalf("got %d calls, want 2", calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLabelBatchDeleteDryRunDoesNotCallAPI(t *testing.T) {
|
||||
server := newLabelTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("batch-delete dry-run should not call API, got: %s %s", r.Method, r.URL.Path)
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
if err := runLabelShortcut(t, server, "batch-delete", map[string]string{"ids": "7,8", "dry-run": "true"}); err != nil {
|
||||
t.Fatalf("batch-delete dry-run failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLabelBatchDeleteDefaultsToDryRunWithoutYes(t *testing.T) {
|
||||
server := newLabelTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("batch-delete without --yes should not call API, got: %s %s", r.Method, r.URL.Path)
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
if err := runLabelShortcut(t, server, "batch-delete", map[string]string{"ids": "7,8"}); err != nil {
|
||||
t.Fatalf("batch-delete default dry-run failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLabelBatchDeleteWithYesCallsEndpoints(t *testing.T) {
|
||||
var paths []string
|
||||
server := newLabelTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "DELETE" {
|
||||
t.Fatalf("got method %s, want DELETE", r.Method)
|
||||
}
|
||||
paths = append(paths, r.URL.Path)
|
||||
writeJSON(t, w, map[string]interface{}{"status": 0, "message": "success"})
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
if err := runLabelShortcut(t, server, "batch-delete", map[string]string{"ids": "7,8,7", "yes": "true"}); err != nil {
|
||||
t.Fatalf("batch-delete failed: %v", err)
|
||||
}
|
||||
if len(paths) != 2 {
|
||||
t.Fatalf("got %d calls, want 2", len(paths))
|
||||
}
|
||||
assertEqual(t, paths[0], "/v1/owner/repo/issue_tags/7.json")
|
||||
assertEqual(t, paths[1], "/v1/owner/repo/issue_tags/8.json")
|
||||
}
|
||||
|
||||
func TestValidateColor(t *testing.T) {
|
||||
valid := []string{"#1E90FF", "#abc", "#ABCDEF", "#000"}
|
||||
for _, c := range valid {
|
||||
|
|
@ -170,6 +316,54 @@ func TestLabelIDString(t *testing.T) {
|
|||
assertEqual(t, labelIDString(nil), "")
|
||||
}
|
||||
|
||||
func TestParseLabelSpecs(t *testing.T) {
|
||||
specs, err := parseLabelSpecs("bug:#ee0701:Bug fixes; docs::Documentation")
|
||||
if err != nil {
|
||||
t.Fatalf("parseLabelSpecs failed: %v", err)
|
||||
}
|
||||
if len(specs) != 2 {
|
||||
t.Fatalf("got %d specs, want 2", len(specs))
|
||||
}
|
||||
assertEqual(t, specs[0].Name, "bug")
|
||||
assertEqual(t, specs[0].Color, "#ee0701")
|
||||
assertEqual(t, specs[0].Description, "Bug fixes")
|
||||
assertEqual(t, specs[1].Name, "docs")
|
||||
assertEqual(t, specs[1].Color, defaultLabelColor)
|
||||
assertEqual(t, specs[1].Description, "Documentation")
|
||||
}
|
||||
|
||||
func TestParseLabelSpecsRejectsInvalidInput(t *testing.T) {
|
||||
invalid := []string{"", ":#ee0701:missing name", "bug:red:bad color"}
|
||||
for _, value := range invalid {
|
||||
if _, err := parseLabelSpecs(value); err == nil {
|
||||
t.Fatalf("expected %q to be invalid", value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseLabelIDList(t *testing.T) {
|
||||
ids, err := parseLabelIDList("7, 8,7, 9")
|
||||
if err != nil {
|
||||
t.Fatalf("parseLabelIDList failed: %v", err)
|
||||
}
|
||||
want := []string{"7", "8", "9"}
|
||||
if len(ids) != len(want) {
|
||||
t.Fatalf("got %v, want %v", ids, want)
|
||||
}
|
||||
for i := range want {
|
||||
assertEqual(t, ids[i], want[i])
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseLabelIDListRejectsInvalidInput(t *testing.T) {
|
||||
invalid := []string{"", "0", "-1", "abc", "7,abc"}
|
||||
for _, value := range invalid {
|
||||
if _, err := parseLabelIDList(value); err == nil {
|
||||
t.Fatalf("expected %q to be invalid", value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func runLabelShortcut(t *testing.T, server *httptest.Server, name string, args map[string]string) error {
|
||||
t.Helper()
|
||||
shortcut := findLabelShortcut(t, name)
|
||||
|
|
|
|||
|
|
@ -5,14 +5,14 @@ description: "项目标记管理:查看、创建、修改、删除 GitLink 仓
|
|||
metadata:
|
||||
requires:
|
||||
bins: ["gitlink-cli"]
|
||||
cliHelp: "gitlink-cli api --help"
|
||||
cliHelp: "gitlink-cli label --help"
|
||||
---
|
||||
|
||||
# gitlink-tag(项目标记管理)
|
||||
|
||||
**CRITICAL — 开始前必须先阅读 [`../gitlink-shared/SKILL.md`](../gitlink-shared/SKILL.md),其中包含认证、权限处理和 API 注意事项。**
|
||||
**CRITICAL — 所有写入/删除操作前,务必先确认用户意图。**
|
||||
**CRITICAL — 项目标记通过 `gitlink-cli api` 操作,无需本地 git 命令。**
|
||||
**CRITICAL — 项目标记优先通过 `gitlink-cli label` 快捷命令操作,无需本地 git 命令。**
|
||||
|
||||
> **前置条件:** 先阅读 [`../gitlink-shared/SKILL.md`](../gitlink-shared/SKILL.md)
|
||||
|
||||
|
|
@ -23,22 +23,25 @@ metadata:
|
|||
1. **查看标记** — 列出仓库所有项目标记,支持关键词搜索和精简模式
|
||||
2. **创建标记** — 创建新标记,设置名称、描述和颜色
|
||||
3. **修改标记** — 修改已有标记的名称、描述或颜色
|
||||
4. **删除标记** — 删除不再需要的标记
|
||||
4. **删除标记** — 安全删除不再需要的标记
|
||||
5. **批量操作** — 批量创建/删除标记,支持 dry-run 预览和 `--yes` 确认
|
||||
|
||||
---
|
||||
|
||||
## API 能力说明
|
||||
## Shortcut 能力说明
|
||||
|
||||
GitLink API 对项目标记(issue_tags)的完整支持:
|
||||
GitLink CLI 对项目标记(issue_tags)的完整支持:
|
||||
|
||||
| 操作 | HTTP 方法 | API 路径 | 说明 |
|
||||
|------|-----------|---------|------|
|
||||
| 查询标记列表 | GET | `/v1/{owner}/{repo}/issue_tags` | 支持 keyword/only_name/sort_by/sort_direction/limit/page 参数 |
|
||||
| 创建标记 | POST | `/v1/{owner}/{repo}/issue_tags` | 请求体:{name, description, color} |
|
||||
| 修改标记 | PATCH | `/v1/{owner}/{repo}/issue_tags/{id}` | 请求体:{name, description, color},路径参数 id 为标记 ID |
|
||||
| 删除标记 | DELETE | `/v1/{owner}/{repo}/issue_tags/{id}` | 路径参数 id 为标记 ID |
|
||||
| 操作 | Shortcut | 说明 |
|
||||
|------|----------|------|
|
||||
| 查询标记列表 | `label +list` | 支持 keyword/only_name/sort_by/sort_direction 参数 |
|
||||
| 创建标记 | `label +create` | 参数:name、description、color |
|
||||
| 修改标记 | `label +update` | 按 ID 修改,未指定字段会保留 |
|
||||
| 删除标记 | `label +delete` | 按 ID 删除,支持 `--dry-run` 和 `--yes` |
|
||||
| 批量创建 | `label +batch-create` | 解析 `name:color:description` 列表,支持 `--dry-run` 和 `--yes` |
|
||||
| 批量删除 | `label +batch-delete` | 解析逗号分隔 ID,支持 `--dry-run` 和 `--yes` |
|
||||
|
||||
> **核心原则:** 所有操作均通过 `gitlink-cli api` 调用,无需本地 git 命令。
|
||||
> **核心原则:** 优先使用 `gitlink-cli label`。Raw API 仅在快捷命令无法覆盖的调试场景使用。
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -48,7 +51,7 @@ GitLink API 对项目标记(issue_tags)的完整支持:
|
|||
|
||||
```bash
|
||||
# 获取项目标记完整列表(含描述、颜色、关联 Issue 数量等)
|
||||
gitlink-cli api GET /v1/:owner/:repo/issue_tags --format json
|
||||
gitlink-cli label +list --owner <owner> --repo <repo> --format json
|
||||
```
|
||||
|
||||
**返回数据结构**:
|
||||
|
|
@ -71,7 +74,7 @@ gitlink-cli api GET /v1/:owner/:repo/issue_tags --format json
|
|||
|
||||
```bash
|
||||
# 搜索名称中包含关键词的标记
|
||||
gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'keyword=bug' --format json
|
||||
gitlink-cli label +list --owner <owner> --repo <repo> --keyword bug --format json
|
||||
```
|
||||
|
||||
**支持的查询参数**:
|
||||
|
|
@ -87,7 +90,7 @@ gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'keyword=bug' --format j
|
|||
|
||||
```bash
|
||||
# 仅返回名称和 ID(适用于选择标记、快速浏览等场景)
|
||||
gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'only_name=true' --format json
|
||||
gitlink-cli label +list --owner <owner> --repo <repo> --only-name true --format json
|
||||
```
|
||||
|
||||
**返回示例**:
|
||||
|
|
@ -107,13 +110,13 @@ gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'only_name=true' --forma
|
|||
|
||||
```bash
|
||||
# 按 Issue 数量倒序排列(找出最常用的标记)
|
||||
gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'order_by=issues_count&order_direction=desc' --format json
|
||||
gitlink-cli label +list --owner <owner> --repo <repo> --sort-by issues_count --sort-direction desc --format json
|
||||
|
||||
# 按创建时间正序排列(最早创建的排前面)
|
||||
gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'order_by=created_on&order_direction=asc' --format json
|
||||
gitlink-cli label +list --owner <owner> --repo <repo> --sort-by created_on --sort-direction asc --format json
|
||||
|
||||
# 按更新时间倒序排列(最近更新的排前面)
|
||||
gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'order_by=updated_on&order_direction=desc' --format json
|
||||
gitlink-cli label +list --owner <owner> --repo <repo> --sort-by updated_on --sort-direction desc --format json
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -124,7 +127,7 @@ gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'order_by=updated_on&ord
|
|||
|
||||
```bash
|
||||
# 创建一个项目标记
|
||||
gitlink-cli api POST /v1/:owner/:repo/issue_tags --body '{"name":"测试11","description":"111","color":"#54ff85"}' --format json
|
||||
gitlink-cli label +create --owner <owner> --repo <repo> -n "测试11" -d "111" -c "#54ff85" --format json
|
||||
```
|
||||
|
||||
**请求体参数**:
|
||||
|
|
@ -165,21 +168,22 @@ AI 创建标记时,如用户未指定颜色,可按标记用途推荐默认
|
|||
|
||||
```bash
|
||||
# 创建标记后,通过关键词搜索确认
|
||||
gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'keyword=测试11' --format json
|
||||
gitlink-cli label +list --owner <owner> --repo <repo> --keyword "测试11" --format json
|
||||
```
|
||||
|
||||
### 2.4 批量创建标记
|
||||
|
||||
当用户需要一次创建多个标记时,逐个调用创建 API:
|
||||
当用户需要一次创建多个标记时,优先使用批量快捷命令:
|
||||
|
||||
```bash
|
||||
# 批量创建标记(逐个调用)
|
||||
gitlink-cli api POST /v1/:owner/:repo/issue_tags --body '{"name":"bug","description":"Bug 修复","color":"#ee0701"}' --format json
|
||||
gitlink-cli api POST /v1/:owner/:repo/issue_tags --body '{"name":"feature","description":"新功能","color":"#0075ca"}' --format json
|
||||
gitlink-cli api POST /v1/:owner/:repo/issue_tags --body '{"name":"documentation","description":"文档相关","color":"#0075ca"}' --format json
|
||||
# 批量创建标记,先 dry-run 再确认
|
||||
gitlink-cli label +batch-create --owner <owner> --repo <repo> \
|
||||
--labels 'bug:#ee0701:Bug 修复;feature:#0075ca:新功能;documentation:#0075ca:文档相关' --dry-run
|
||||
gitlink-cli label +batch-create --owner <owner> --repo <repo> \
|
||||
--labels 'bug:#ee0701:Bug 修复;feature:#0075ca:新功能;documentation:#0075ca:文档相关' --yes
|
||||
|
||||
# 验证创建结果
|
||||
gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'only_name=true' --format json
|
||||
gitlink-cli label +list --owner <owner> --repo <repo> --only-name true --format json
|
||||
```
|
||||
|
||||
> **⚠️ 批量创建前,先查询现有标记,避免创建重复名称的标记。**
|
||||
|
|
@ -194,11 +198,11 @@ gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'only_name=true' --forma
|
|||
|
||||
```bash
|
||||
# Step 1:查询标记列表,获取目标标记的 ID
|
||||
gitlink-cli api GET /v1/:owner/:repo/issue_tags --format json
|
||||
gitlink-cli label +list --owner <owner> --repo <repo> --format json
|
||||
# 在返回结果中找到目标标记的 id 字段
|
||||
|
||||
# Step 2:使用 ID 修改标记
|
||||
gitlink-cli api PATCH /v1/:owner/:repo/issue_tags/:id --body '{"name":"测试11","description":"1112","color":"#54ff85"}' --format json
|
||||
gitlink-cli label +update --owner <owner> --repo <repo> -i <id> -n "测试11" -d "1112" -c "#54ff85" --format json
|
||||
```
|
||||
|
||||
**请求体参数**(与创建相同):
|
||||
|
|
@ -224,40 +228,40 @@ gitlink-cli api PATCH /v1/:owner/:repo/issue_tags/:id --body '{"name":"测试11"
|
|||
|
||||
```bash
|
||||
# 查询获取 ID
|
||||
gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'keyword=旧名称' --format json
|
||||
gitlink-cli label +list --owner <owner> --repo <repo> --keyword "旧名称" --format json
|
||||
# 假设返回 id=5
|
||||
|
||||
# 修改名称(保持描述和颜色不变)
|
||||
gitlink-cli api PATCH /v1/:owner/:repo/issue_tags/5 --body '{"name":"新名称","description":"原描述","color":"#ee0701"}' --format json
|
||||
gitlink-cli label +update --owner <owner> --repo <repo> -i 5 -n "新名称" --format json
|
||||
```
|
||||
|
||||
**修改标记颜色**:
|
||||
|
||||
```bash
|
||||
# 查询获取 ID
|
||||
gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'keyword=bug' --format json
|
||||
gitlink-cli label +list --owner <owner> --repo <repo> --keyword bug --format json
|
||||
# 假设返回 id=3
|
||||
|
||||
# 修改颜色(保持名称和描述不变)
|
||||
gitlink-cli api PATCH /v1/:owner/:repo/issue_tags/3 --body '{"name":"bug","description":"Bug 修复","color":"#ff0000"}' --format json
|
||||
gitlink-cli label +update --owner <owner> --repo <repo> -i 3 -c "#ff0000" --format json
|
||||
```
|
||||
|
||||
**修改标记描述**:
|
||||
|
||||
```bash
|
||||
# 查询获取 ID
|
||||
gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'keyword=feature' --format json
|
||||
gitlink-cli label +list --owner <owner> --repo <repo> --keyword feature --format json
|
||||
# 假设返回 id=7
|
||||
|
||||
# 修改描述
|
||||
gitlink-cli api PATCH /v1/:owner/:repo/issue_tags/7 --body '{"name":"feature","description":"新的功能需求描述","color":"#0075ca"}' --format json
|
||||
gitlink-cli label +update --owner <owner> --repo <repo> -i 7 -d "新的功能需求描述" --format json
|
||||
```
|
||||
|
||||
### 3.3 修改后验证
|
||||
|
||||
```bash
|
||||
# 修改后查询确认变更已生效
|
||||
gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'keyword=修改后的名称' --format json
|
||||
gitlink-cli label +list --owner <owner> --repo <repo> --keyword "修改后的名称" --format json
|
||||
```
|
||||
|
||||
> **⚠️ 修改标记名称后,已关联该标记的 Issue 会自动更新为新名称。**
|
||||
|
|
@ -272,11 +276,12 @@ gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'keyword=修改后的名
|
|||
|
||||
```bash
|
||||
# Step 1:查询标记列表,获取目标标记的 ID
|
||||
gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'keyword=要删除的标记名' --format json
|
||||
gitlink-cli label +list --owner <owner> --repo <repo> --keyword "要删除的标记名" --format json
|
||||
# 在返回结果中找到目标标记的 id 字段
|
||||
|
||||
# Step 2:删除标记
|
||||
gitlink-cli api DELETE /v1/:owner/:repo/issue_tags/:id --format json
|
||||
# Step 2:先预览删除,再确认执行
|
||||
gitlink-cli label +delete --owner <owner> --repo <repo> -i <id> --dry-run
|
||||
gitlink-cli label +delete --owner <owner> --repo <repo> -i <id> --yes
|
||||
```
|
||||
|
||||
**返回示例**:
|
||||
|
|
@ -292,7 +297,7 @@ gitlink-cli api DELETE /v1/:owner/:repo/issue_tags/:id --format json
|
|||
|
||||
```bash
|
||||
# Step 1:查看所有标记,确认要删除的目标
|
||||
gitlink-cli api GET /v1/:owner/:repo/issue_tags --format json
|
||||
gitlink-cli label +list --owner <owner> --repo <repo> --format json
|
||||
|
||||
# Step 2:记录目标标记的 ID 和关联 Issue 数量
|
||||
# 假设目标标记 id=5, name="deprecated", issues_count=3
|
||||
|
|
@ -301,10 +306,11 @@ gitlink-cli api GET /v1/:owner/:repo/issue_tags --format json
|
|||
# ⚠️ 删除标记后,关联的 Issue 将失去该标记
|
||||
|
||||
# Step 4:执行删除
|
||||
gitlink-cli api DELETE /v1/:owner/:repo/issue_tags/5 --format json
|
||||
gitlink-cli label +delete --owner <owner> --repo <repo> -i 5 --dry-run
|
||||
gitlink-cli label +delete --owner <owner> --repo <repo> -i 5 --yes
|
||||
|
||||
# Step 5:验证删除结果
|
||||
gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'keyword=deprecated' --format json
|
||||
gitlink-cli label +list --owner <owner> --repo <repo> --keyword deprecated --format json
|
||||
# total_count 应为 0
|
||||
```
|
||||
|
||||
|
|
@ -312,18 +318,17 @@ gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'keyword=deprecated' --f
|
|||
|
||||
```bash
|
||||
# Step 1:查询所有标记
|
||||
gitlink-cli api GET /v1/:owner/:repo/issue_tags --format json
|
||||
gitlink-cli label +list --owner <owner> --repo <repo> --format json
|
||||
|
||||
# Step 2:AI 根据用户意图筛选要删除的标记,列出 ID 列表
|
||||
# 假设要删除 id=3, id=5, id=8
|
||||
|
||||
# Step 3:逐个删除
|
||||
gitlink-cli api DELETE /v1/:owner/:repo/issue_tags/3 --format json
|
||||
gitlink-cli api DELETE /v1/:owner/:repo/issue_tags/5 --format json
|
||||
gitlink-cli api DELETE /v1/:owner/:repo/issue_tags/8 --format json
|
||||
# Step 3:批量删除,先 dry-run 再确认
|
||||
gitlink-cli label +batch-delete --owner <owner> --repo <repo> --ids 3,5,8 --dry-run
|
||||
gitlink-cli label +batch-delete --owner <owner> --repo <repo> --ids 3,5,8 --yes
|
||||
|
||||
# Step 4:验证删除结果
|
||||
gitlink-cli api GET /v1/:owner/:repo/issue_tags --format json
|
||||
gitlink-cli label +list --owner <owner> --repo <repo> --format json
|
||||
```
|
||||
|
||||
> **⚠️ 批量删除是危险操作,必须先列出待删除标记清单让用户确认后再执行。**
|
||||
|
|
@ -336,7 +341,7 @@ gitlink-cli api GET /v1/:owner/:repo/issue_tags --format json
|
|||
|
||||
```bash
|
||||
# 获取所有标记及关联 Issue 数量
|
||||
gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'order_by=issues_count&order_direction=asc' --format json
|
||||
gitlink-cli label +list --owner <owner> --repo <repo> --sort-by issues_count --sort-direction asc --format json
|
||||
```
|
||||
|
||||
**AI 分析规则**:
|
||||
|
|
@ -355,14 +360,10 @@ gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'order_by=issues_count&o
|
|||
|
||||
```bash
|
||||
# 基础标记集(适用于大多数项目)
|
||||
gitlink-cli api POST /v1/:owner/:repo/issue_tags --body '{"name":"bug","description":"Bug 修复或问题报告","color":"#ee0701"}' --format json
|
||||
gitlink-cli api POST /v1/:owner/:repo/issue_tags --body '{"name":"feature","description":"新功能需求","color":"#0075ca"}' --format json
|
||||
gitlink-cli api POST /v1/:owner/:repo/issue_tags --body '{"name":"enhancement","description":"功能优化或改进","color":"#5319e7"}' --format json
|
||||
gitlink-cli api POST /v1/:owner/:repo/issue_tags --body '{"name":"documentation","description":"文档相关","color":"#0075ca"}' --format json
|
||||
gitlink-cli api POST /v1/:owner/:repo/issue_tags --body '{"name":"good-first-issue","description":"适合新贡献者的问题","color":"#008672"}' --format json
|
||||
gitlink-cli api POST /v1/:owner/:repo/issue_tags --body '{"name":"help-wanted","description":"需要帮助的问题","color":"#008672"}' --format json
|
||||
gitlink-cli api POST /v1/:owner/:repo/issue_tags --body '{"name":"question","description":"使用疑问","color":"#fbca04"}' --format json
|
||||
gitlink-cli api POST /v1/:owner/:repo/issue_tags --body '{"name":"wontfix","description":"不会处理的问题","color":"#fbca04"}' --format json
|
||||
gitlink-cli label +batch-create --owner <owner> --repo <repo> --dry-run \
|
||||
--labels 'bug:#ee0701:Bug 修复或问题报告;feature:#0075ca:新功能需求;enhancement:#5319e7:功能优化或改进;documentation:#0075ca:文档相关;good-first-issue:#008672:适合新贡献者的问题;help-wanted:#008672:需要帮助的问题;question:#fbca04:使用疑问;wontfix:#fbca04:不会处理的问题'
|
||||
gitlink-cli label +batch-create --owner <owner> --repo <repo> --yes \
|
||||
--labels 'bug:#ee0701:Bug 修复或问题报告;feature:#0075ca:新功能需求;enhancement:#5319e7:功能优化或改进;documentation:#0075ca:文档相关;good-first-issue:#008672:适合新贡献者的问题;help-wanted:#008672:需要帮助的问题;question:#fbca04:使用疑问;wontfix:#fbca04:不会处理的问题'
|
||||
```
|
||||
|
||||
### 5.3 合并相似标记
|
||||
|
|
@ -371,7 +372,7 @@ gitlink-cli api POST /v1/:owner/:repo/issue_tags --body '{"name":"wontfix","desc
|
|||
|
||||
```bash
|
||||
# Step 1:查询所有标记,AI 识别相似标记对
|
||||
gitlink-cli api GET /v1/:owner/:repo/issue_tags --format json
|
||||
gitlink-cli label +list --owner <owner> --repo <repo> --format json
|
||||
|
||||
# Step 2:假设 "bug"(id=3) 和 "defect"(id=7) 需要合并,保留 "bug"
|
||||
|
||||
|
|
@ -381,23 +382,24 @@ gitlink-cli issue +list --state open --owner <owner> --repo <repo> --format json
|
|||
# AI 筛选标记为 "defect" 的 Issue,将其改为 "bug"
|
||||
|
||||
# Step 4:删除 "defect" 标记
|
||||
gitlink-cli api DELETE /v1/:owner/:repo/issue_tags/7 --format json
|
||||
gitlink-cli label +delete --owner <owner> --repo <repo> -i 7 --dry-run
|
||||
gitlink-cli label +delete --owner <owner> --repo <repo> -i 7 --yes
|
||||
|
||||
# Step 5:验证
|
||||
gitlink-cli api GET /v1/:owner/:repo/issue_tags --format json
|
||||
gitlink-cli label +list --owner <owner> --repo <repo> --format json
|
||||
```
|
||||
|
||||
### 5.4 查询参数组合使用
|
||||
|
||||
```bash
|
||||
# 搜索关键词 + 仅返回名称
|
||||
gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'keyword=bug&only_name=true' --format json
|
||||
gitlink-cli label +list --owner <owner> --repo <repo> --keyword bug --only-name true --format json
|
||||
|
||||
# 按 Issue 数量倒序 + 仅返回名称(快速查看热门标记)
|
||||
gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'order_by=issues_count&order_direction=desc&only_name=true' --format json
|
||||
gitlink-cli label +list --owner <owner> --repo <repo> --sort-by issues_count --sort-direction desc --only-name true --format json
|
||||
|
||||
# 按更新时间倒序(查看最近活跃的标记)
|
||||
gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'order_by=updated_on&order_direction=desc' --format json
|
||||
gitlink-cli label +list --owner <owner> --repo <repo> --sort-by updated_on --sort-direction desc --format json
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -408,57 +410,58 @@ gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'order_by=updated_on&ord
|
|||
|
||||
```bash
|
||||
# Step 1:获取项目标记完整列表
|
||||
gitlink-cli api GET /v1/:owner/:repo/issue_tags --format json
|
||||
gitlink-cli label +list --owner <owner> --repo <repo> --format json
|
||||
|
||||
# Step 2(可选):搜索特定标记
|
||||
gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'keyword=bug' --format json
|
||||
gitlink-cli label +list --owner <owner> --repo <repo> --keyword bug --format json
|
||||
|
||||
# Step 3(可选):查看精简列表
|
||||
gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'only_name=true' --format json
|
||||
gitlink-cli label +list --owner <owner> --repo <repo> --only-name true --format json
|
||||
```
|
||||
|
||||
### 6.2 创建标记流程
|
||||
|
||||
```bash
|
||||
# Step 1:查看现有标记,避免重复
|
||||
gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'only_name=true' --format json
|
||||
gitlink-cli label +list --owner <owner> --repo <repo> --only-name true --format json
|
||||
|
||||
# Step 2:创建标记
|
||||
gitlink-cli api POST /v1/:owner/:repo/issue_tags --body '{"name":"新标记","description":"标记描述","color":"#54ff85"}' --format json
|
||||
gitlink-cli label +create --owner <owner> --repo <repo> -n "新标记" -d "标记描述" -c "#54ff85" --format json
|
||||
|
||||
# Step 3:验证创建结果
|
||||
gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'keyword=新标记' --format json
|
||||
gitlink-cli label +list --owner <owner> --repo <repo> --keyword "新标记" --format json
|
||||
```
|
||||
|
||||
### 6.3 修改标记流程
|
||||
|
||||
```bash
|
||||
# Step 1:查询目标标记,获取 ID
|
||||
gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'keyword=要修改的标记' --format json
|
||||
gitlink-cli label +list --owner <owner> --repo <repo> --keyword "要修改的标记" --format json
|
||||
# 记录目标标记的 id
|
||||
|
||||
# Step 2:修改标记(使用 ID)
|
||||
gitlink-cli api PATCH /v1/:owner/:repo/issue_tags/:id --body '{"name":"修改后名称","description":"修改后描述","color":"#ff0000"}' --format json
|
||||
gitlink-cli label +update --owner <owner> --repo <repo> -i <id> -n "修改后名称" -d "修改后描述" -c "#ff0000" --format json
|
||||
|
||||
# Step 3:验证修改结果
|
||||
gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'keyword=修改后名称' --format json
|
||||
gitlink-cli label +list --owner <owner> --repo <repo> --keyword "修改后名称" --format json
|
||||
```
|
||||
|
||||
### 6.4 删除标记流程
|
||||
|
||||
```bash
|
||||
# Step 1:查询目标标记,获取 ID 和关联 Issue 数量
|
||||
gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'keyword=要删除的标记' --format json
|
||||
gitlink-cli label +list --owner <owner> --repo <repo> --keyword "要删除的标记" --format json
|
||||
# 记录目标标记的 id 和 issues_count
|
||||
|
||||
# Step 2:确认删除意图(⚠️ 如 issues_count > 0 需特别提醒)
|
||||
# ⚠️ 删除标记后,关联的 Issue 将失去该标记
|
||||
|
||||
# Step 3:删除标记
|
||||
gitlink-cli api DELETE /v1/:owner/:repo/issue_tags/:id --format json
|
||||
gitlink-cli label +delete --owner <owner> --repo <repo> -i <id> --dry-run
|
||||
gitlink-cli label +delete --owner <owner> --repo <repo> -i <id> --yes
|
||||
|
||||
# Step 4:验证删除结果
|
||||
gitlink-cli api GET /v1/:owner/:repo/issue_tags --format json
|
||||
gitlink-cli label +list --owner <owner> --repo <repo> --format json
|
||||
```
|
||||
|
||||
---
|
||||
|
|
@ -553,9 +556,10 @@ gitlink-cli api GET /v1/:owner/:repo/issue_tags --format json
|
|||
用户:"帮我的新仓库创建一套 Issue 标签"
|
||||
|
||||
AI 执行:
|
||||
1. gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'only_name=true' --format json
|
||||
1. gitlink-cli label +list --owner <owner> --repo <repo> --only-name true --format json
|
||||
→ 确认当前标记列表为空
|
||||
2. 逐个创建基础标记(bug/feature/enhancement/documentation/good-first-issue/help-wanted/question/wontfix)
|
||||
2. gitlink-cli label +batch-create --owner <owner> --repo <repo> --labels '<基础标记集>' --dry-run
|
||||
3. 用户确认后执行 gitlink-cli label +batch-create --owner <owner> --repo <repo> --labels '<基础标记集>' --yes
|
||||
3. 验证创建结果
|
||||
4. 输出创建报告
|
||||
```
|
||||
|
|
@ -566,9 +570,9 @@ AI 执行:
|
|||
用户:"把 bug 标签的颜色改成红色"
|
||||
|
||||
AI 执行:
|
||||
1. gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'keyword=bug' --format json
|
||||
1. gitlink-cli label +list --owner <owner> --repo <repo> --keyword bug --format json
|
||||
→ 获取 id 和当前信息
|
||||
2. gitlink-cli api PATCH /v1/:owner/:repo/issue_tags/:id --body '{"name":"bug","description":"原描述","color":"#ee0701"}' --format json
|
||||
2. gitlink-cli label +update --owner <owner> --repo <repo> -i <id> -c "#ee0701" --format json
|
||||
3. 验证修改结果
|
||||
4. 输出修改报告
|
||||
```
|
||||
|
|
@ -579,10 +583,11 @@ AI 执行:
|
|||
用户:"删除没有关联任何 Issue 的标签"
|
||||
|
||||
AI 执行:
|
||||
1. gitlink-cli api GET /v1/:owner/:repo/issue_tags --query 'order_by=issues_count&order_direction=asc' --format json
|
||||
1. gitlink-cli label +list --owner <owner> --repo <repo> --sort-by issues_count --sort-direction asc --format json
|
||||
2. AI 筛选 issues_count == 0 的标记
|
||||
3. 列出待删除标记清单,请用户确认
|
||||
4. 确认后逐个删除
|
||||
4. 先执行 gitlink-cli label +batch-delete --owner <owner> --repo <repo> --ids <ids> --dry-run
|
||||
5. 用户确认后执行 gitlink-cli label +batch-delete --owner <owner> --repo <repo> --ids <ids> --yes
|
||||
5. 输出删除报告
|
||||
```
|
||||
|
||||
|
|
@ -592,7 +597,7 @@ AI 执行:
|
|||
用户:"检查有没有重复的标签"
|
||||
|
||||
AI 执行:
|
||||
1. gitlink-cli api GET /v1/:owner/:repo/issue_tags --format json
|
||||
1. gitlink-cli label +list --owner <owner> --repo <repo> --format json
|
||||
2. AI 分析语义相似的标记对(如 bug/defect、feature/enhancement)
|
||||
3. 列出建议合并的标记对,请用户确认
|
||||
4. 执行合并(迁移 Issue 标记 → 删除冗余标记)
|
||||
|
|
@ -612,4 +617,3 @@ AI 执行:
|
|||
- ⚠️ **标记名称唯一**:同一仓库下标记名称不能重复
|
||||
- ✅ **排序参数**:`sort_by` 支持 `updated_on`、`created_on`、`issues_count`,`sort_direction` 支持 `desc`、`asc`
|
||||
- ✅ **所有操作通过 gitlink-cli api**:无需本地 git 命令,所有增删查改均通过 API 完成
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: gitlink-label
|
||||
version: 1.0.0
|
||||
description: "Issue label management: list, create, update, and delete GitLink issue labels (项目标记). Triggered when a user needs to manage labels, set up a triage taxonomy, or tag issues."
|
||||
version: 2.0.0
|
||||
description: "Issue label management: list, create, update, delete, and batch manage GitLink issue labels (项目标记). Triggered when a user needs to manage labels, set up a triage taxonomy, or tag issues."
|
||||
metadata:
|
||||
requires:
|
||||
bins: ["gitlink-cli"]
|
||||
|
|
@ -11,7 +11,7 @@ metadata:
|
|||
# gitlink-label
|
||||
|
||||
**CRITICAL**: Read [`../gitlink-shared/SKILL.md`](../gitlink-shared/SKILL.md) before starting. It covers authentication, permissions, global flags, and GitLink API behavior.
|
||||
**CRITICAL**: Confirm user intent before running write or destructive operations such as `+create`, `+update`, or `+delete`.
|
||||
**CRITICAL**: Confirm user intent before running write or destructive operations such as `+create`, `+update`, `+delete`, `+batch-create`, or `+batch-delete`.
|
||||
**CRITICAL**: Use `gitlink-cli` for GitLink resources. Do not use GitHub-only tools such as `gh`.
|
||||
|
||||
## Shortcuts
|
||||
|
|
@ -21,7 +21,9 @@ metadata:
|
|||
| `label +list` | List issue labels | Read |
|
||||
| `label +create` | Create an issue label | Write |
|
||||
| `label +update` | Update a label, preserving unspecified fields | Write |
|
||||
| `label +delete` | Delete an issue label | Destructive |
|
||||
| `label +delete` | Delete an issue label with dry-run/yes protection | Destructive |
|
||||
| `label +batch-create` | Batch create issue labels from `name:color:description` specs | Write |
|
||||
| `label +batch-delete` | Batch delete issue labels by IDs with dry-run/yes protection | Destructive |
|
||||
|
||||
## Examples
|
||||
|
||||
|
|
@ -38,8 +40,19 @@ gitlink-cli label +create --owner Gitlink --repo forgeplus -n bug -d "Something
|
|||
# Update only the color; name and description are preserved
|
||||
gitlink-cli label +update --owner Gitlink --repo forgeplus -i 42 -c "#00FF00"
|
||||
|
||||
# Delete a label
|
||||
gitlink-cli label +delete --owner Gitlink --repo forgeplus -i 42
|
||||
# Delete a label safely
|
||||
gitlink-cli label +delete --owner Gitlink --repo forgeplus -i 42 --dry-run
|
||||
gitlink-cli label +delete --owner Gitlink --repo forgeplus -i 42 --yes
|
||||
|
||||
# Batch create labels safely
|
||||
gitlink-cli label +batch-create --owner Gitlink --repo forgeplus \
|
||||
--labels 'bug:#ee0701:Bug fixes;feature:#0075ca:New features' --dry-run
|
||||
gitlink-cli label +batch-create --owner Gitlink --repo forgeplus \
|
||||
--labels 'bug:#ee0701:Bug fixes;feature:#0075ca:New features' --yes
|
||||
|
||||
# Batch delete labels safely
|
||||
gitlink-cli label +batch-delete --owner Gitlink --repo forgeplus --ids 3,5,8 --dry-run
|
||||
gitlink-cli label +batch-delete --owner Gitlink --repo forgeplus --ids 3,5,8 --yes
|
||||
```
|
||||
|
||||
## Parameters
|
||||
|
|
@ -49,23 +62,27 @@ gitlink-cli label +delete --owner Gitlink --repo forgeplus -i 42
|
|||
| `+list` | `--keyword`, `--only-name`, `--sort-by` (updated_on / created_on / issues_count), `--sort-direction` (asc / desc) |
|
||||
| `+create` | `--name` (required), `--description`, `--color` (hex, default `#1E90FF`) |
|
||||
| `+update` | `--id` (required) plus at least one of `--name`, `--description`, `--color` |
|
||||
| `+delete` | `--id` (required) |
|
||||
| `+delete` | `--id` (required), `--dry-run`, `--yes` |
|
||||
| `+batch-create` | `--labels` (semicolon-separated `name:color:description` specs), `--dry-run`, `--yes` |
|
||||
| `+batch-delete` | `--ids` (comma-separated label IDs), `--dry-run`, `--yes` |
|
||||
|
||||
## API Notes
|
||||
|
||||
- Labels map to the GitLink "项目标记" / `issue_tags` API: `/api/v1/{owner}/{repo}/issue_tags`.
|
||||
- `--color` must be a hex value (`#RGB` or `#RRGGBB`); it is validated client-side before the API call.
|
||||
- `+update` first fetches the label's current values from the list endpoint and merges the requested changes, so fields you do not pass are preserved (the API requires `name`, `description`, and `color` together).
|
||||
- `+delete`, `+batch-create`, and `+batch-delete` should be run with `--dry-run` first, then repeated with `--yes` after confirmation.
|
||||
- `+batch-create` defaults missing colors to `#1E90FF`; duplicate IDs in `+batch-delete` are de-duplicated client-side.
|
||||
- To attach a label to an issue, pass its id via the issue update API field `issue_tag_ids` (see `gitlink-issue`); use `label +list --only-name true` to resolve label ids quickly.
|
||||
|
||||
## Typical workflow: bootstrap a triage taxonomy
|
||||
|
||||
```bash
|
||||
# Create a consistent label set for issue triage
|
||||
gitlink-cli label +create -n bug -c "#D73A4A" -d "Confirmed defect"
|
||||
gitlink-cli label +create -n enhancement -c "#A2EEEF" -d "Feature request"
|
||||
gitlink-cli label +create -n question -c "#D876E3" -d "Needs clarification"
|
||||
gitlink-cli label +create -n security -c "#B60205" -d "Security-sensitive"
|
||||
gitlink-cli label +batch-create --dry-run \
|
||||
--labels 'bug:#D73A4A:Confirmed defect;enhancement:#A2EEEF:Feature request;question:#D876E3:Needs clarification;security:#B60205:Security-sensitive'
|
||||
gitlink-cli label +batch-create --yes \
|
||||
--labels 'bug:#D73A4A:Confirmed defect;enhancement:#A2EEEF:Feature request;question:#D876E3:Needs clarification;security:#B60205:Security-sensitive'
|
||||
|
||||
# Verify the taxonomy
|
||||
gitlink-cli label +list --only-name true --format json
|
||||
|
|
|
|||
|
|
@ -123,22 +123,17 @@ gitlink-cli api GET /v1/:owner/:repo/issues/:number/journals?category=comment&pa
|
|||
|
||||
```bash
|
||||
# Step 1:获取仓库现有标签列表
|
||||
gitlink-cli api GET /v1/:owner/:repo/issue_tags --format json
|
||||
gitlink-cli label +list --owner <owner> --repo <repo> --format json
|
||||
|
||||
# Step 2:检查目标标签是否已存在(AI 在返回结果中查找)
|
||||
# 如果目标标签不存在,则创建:
|
||||
|
||||
# 创建"迟缓"标签(30-59天)
|
||||
gitlink-cli api POST /v1/:owner/:repo/issue_tags --body '{"name":"迟缓","description":"近期活动频率明显下降,需关注但尚未停滞","color":"#fbca04"}' --format json
|
||||
|
||||
# 创建"不活跃"标签(60-89天)
|
||||
gitlink-cli api POST /v1/:owner/:repo/issue_tags --body '{"name":"不活跃","description":"长期无更新或互动,可能已失去推进动力","color":"#d93f0b"}' --format json
|
||||
|
||||
# 创建"过期"标签(≥90天)
|
||||
gitlink-cli api POST /v1/:owner/:repo/issue_tags --body '{"name":"过期","description":"已超出合理响应周期,建议关闭或重新评估","color":"#b60205"}' --format json
|
||||
# 如果目标标签不存在,则批量创建。先 dry-run,再在用户确认后 --yes:
|
||||
gitlink-cli label +batch-create --owner <owner> --repo <repo> \
|
||||
--labels '迟缓:#fbca04:近期活动频率明显下降,需关注但尚未停滞;不活跃:#d93f0b:长期无更新或互动,可能已失去推进动力;过期:#b60205:已超出合理响应周期,建议关闭或重新评估' --dry-run
|
||||
gitlink-cli label +batch-create --owner <owner> --repo <repo> \
|
||||
--labels '迟缓:#fbca04:近期活动频率明显下降,需关注但尚未停滞;不活跃:#d93f0b:长期无更新或互动,可能已失去推进动力;过期:#b60205:已超出合理响应周期,建议关闭或重新评估' --yes
|
||||
|
||||
# Step 3:重新获取标签列表,确认标签 ID
|
||||
gitlink-cli api GET /v1/:owner/:repo/issue_tags --format json
|
||||
gitlink-cli label +list --owner <owner> --repo <repo> --format json
|
||||
```
|
||||
|
||||
**标签与过期等级对应关系**:
|
||||
|
|
@ -428,11 +423,12 @@ gitlink-cli api GET /v1/:owner/:repo/issues/:number/journals --format json
|
|||
|
||||
# Step 5(执行 — 用户确认后):
|
||||
# a. 预创建标签(确保标签存在)
|
||||
gitlink-cli api GET /v1/:owner/:repo/issue_tags --format json
|
||||
gitlink-cli label +list --owner <owner> --repo <repo> --format json
|
||||
# 检查迟缓/不活跃/过期标签是否存在,不存在则创建:
|
||||
gitlink-cli api POST /v1/:owner/:repo/issue_tags --body '{"name":"迟缓","description":"近期活动频率明显下降,需关注但尚未停滞","color":"#fbca04"}' --format json
|
||||
gitlink-cli api POST /v1/:owner/:repo/issue_tags --body '{"name":"不活跃","description":"长期无更新或互动,可能已失去推进动力","color":"#d93f0b"}' --format json
|
||||
gitlink-cli api POST /v1/:owner/:repo/issue_tags --body '{"name":"过期","description":"已超出合理响应周期,建议关闭或重新评估","color":"#b60205"}' --format json
|
||||
gitlink-cli label +batch-create --owner <owner> --repo <repo> \
|
||||
--labels '迟缓:#fbca04:近期活动频率明显下降,需关注但尚未停滞;不活跃:#d93f0b:长期无更新或互动,可能已失去推进动力;过期:#b60205:已超出合理响应周期,建议关闭或重新评估' --dry-run
|
||||
gitlink-cli label +batch-create --owner <owner> --repo <repo> \
|
||||
--labels '迟缓:#fbca04:近期活动频率明显下降,需关注但尚未停滞;不活跃:#d93f0b:长期无更新或互动,可能已失去推进动力;过期:#b60205:已超出合理响应周期,建议关闭或重新评估' --yes
|
||||
|
||||
# b. 对 30-59 天 Issue:打"迟缓"标签 + 发提醒评论
|
||||
gitlink-cli api PATCH /v1/:owner/:repo/issues/:id --body '{"issue_tag_ids":[<tag_id>]}' --format json
|
||||
|
|
@ -559,4 +555,4 @@ AI 应解析为:
|
|||
- ✅ **关闭操作不可逆**:虽然维护者可以重新打开,但评论通知已发出,应谨慎
|
||||
- ✅ **建议定期执行**:推荐每周执行一次,保持 Issue 列表健康
|
||||
- ⚠️ **标签操作**:打标签通过 `gitlink-cli api PATCH /v1/:owner/:repo/issues/:id --body '{"issue_tag_ids":[<tag_id>]}'` 完成,`issue_tag_ids` 为完整替换,需包含已有标签 ID
|
||||
- ⚠️ **标签预创建**:打标签前必须先查询标签列表,确认目标标签存在,不存在则先通过 `POST /v1/:owner/:repo/issue_tags` 创建
|
||||
- ⚠️ **标签预创建**:打标签前必须先用 `gitlink-cli label +list` 查询标签列表,确认目标标签存在;不存在则先用 `gitlink-cli label +batch-create --dry-run` 预览并在确认后 `--yes` 创建
|
||||
|
|
|
|||
Loading…
Reference in New Issue