Merge pull request 'feat(issue): add batch maintenance shortcuts' (#165) from wangyue111/gitlink-cli:feat/issue-batch-maintenance into master
This commit is contained in:
commit
982f2cb336
12
README.md
12
README.md
|
|
@ -104,7 +104,7 @@ The official [GitLink](https://www.gitlink.org.cn) CLI tool — built for humans
|
|||
| Category | Capabilities |
|
||||
|----------|-------------|
|
||||
| 📦 Repo | List, create, fork, delete repositories, view repo info, insights, and interactions |
|
||||
| 🐛 Issue | Create, update, close, batch close, comment on issues |
|
||||
| 🐛 Issue | Create, update, close, batch close/update/delete, comment on issues |
|
||||
| 🔖 Label | Create, list, update, delete issue labels |
|
||||
| 🔀 PR | Create, merge, review pull requests, view changed files |
|
||||
| 👥 Member | List, add, remove repository members, change roles, create and accept invite links |
|
||||
|
|
@ -322,6 +322,14 @@ gitlink-cli issue +batch-close --owner Gitlink --repo forgeplus --numbers 123,12
|
|||
# Batch close issues from a CSV file
|
||||
gitlink-cli issue +batch-close --owner Gitlink --repo forgeplus --from issues.csv
|
||||
|
||||
# Preview batch metadata update by API issue IDs
|
||||
# Note: --ids uses API issue IDs, not web URL issue numbers.
|
||||
gitlink-cli issue +batch-update --owner Gitlink --repo forgeplus --ids 101,102 --status-id 3 --priority-id 2 --dry-run
|
||||
|
||||
# Destructive batch delete requires both dry-run first and --yes for real execution
|
||||
gitlink-cli issue +batch-delete --owner Gitlink --repo forgeplus --ids 101,102 --dry-run
|
||||
gitlink-cli issue +batch-delete --owner Gitlink --repo forgeplus --ids 101,102 --yes
|
||||
|
||||
# Add a comment
|
||||
gitlink-cli issue +comment --owner Gitlink --repo forgeplus -i 123 -b "Fixed"
|
||||
|
||||
|
|
@ -707,7 +715,7 @@ See [skills/README.md](./skills/README.md) for details.
|
|||
|-------|-------------|
|
||||
| `gitlink-shared` | Authentication, global parameters, safety rules, API notes |
|
||||
| `gitlink-repo` | Repository operations (create, view, delete, fork, insights, etc.) |
|
||||
| `gitlink-issue` | Issue operations (create, update, close, comment, etc.) |
|
||||
| `gitlink-issue` | Issue operations (create, update, close, batch update/delete, comment, etc.) |
|
||||
| `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) |
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@
|
|||
| 分类 | 能力 |
|
||||
|------|------|
|
||||
| 📦 仓库 | 列出、创建、Fork、删除仓库,查看仓库信息、洞察数据和互动状态 |
|
||||
| 🐛 Issue | 创建、更新、关闭、批量关闭、评论 Issue |
|
||||
| 🐛 Issue | 创建、更新、关闭、批量关闭/更新/删除、评论 Issue |
|
||||
| 🔖 标签 | 创建、列出、更新、删除 Issue 标签 |
|
||||
| 🔀 PR | 创建、合并、Review Pull Request,查看变更文件 |
|
||||
| 👥 成员 | 列出、添加、移除仓库成员,调整角色,生成和接受邀请链接 |
|
||||
|
|
@ -333,6 +333,14 @@ gitlink-cli issue +batch-close --owner Gitlink --repo forgeplus --numbers 123,12
|
|||
# 从 CSV 文件批量关闭 Issue
|
||||
gitlink-cli issue +batch-close --owner Gitlink --repo forgeplus --from issues.csv
|
||||
|
||||
# 按 API issue id 预览批量更新元数据
|
||||
# 注意:--ids 是 API issue id,不是网页 URL 中的 Issue 编号。
|
||||
gitlink-cli issue +batch-update --owner Gitlink --repo forgeplus --ids 101,102 --status-id 3 --priority-id 2 --dry-run
|
||||
|
||||
# 危险批量删除必须先 dry-run,真实执行还要显式 --yes
|
||||
gitlink-cli issue +batch-delete --owner Gitlink --repo forgeplus --ids 101,102 --dry-run
|
||||
gitlink-cli issue +batch-delete --owner Gitlink --repo forgeplus --ids 101,102 --yes
|
||||
|
||||
# 添加评论
|
||||
gitlink-cli issue +comment --owner Gitlink --repo forgeplus -i 123 -b "已修复"
|
||||
|
||||
|
|
@ -581,7 +589,7 @@ git push gitlink
|
|||
|-------|------|
|
||||
| `gitlink-shared` | 认证、全局参数、安全规则、API 注意事项 |
|
||||
| `gitlink-repo` | 仓库操作(创建、查看、删除、Fork、洞察数据等) |
|
||||
| `gitlink-issue` | Issue 操作(创建、更新、关闭、评论等) |
|
||||
| `gitlink-issue` | Issue 操作(创建、更新、关闭、批量更新/删除、评论等) |
|
||||
| `gitlink-pr` | Pull Request 操作(创建、合并、Review 等) |
|
||||
| `gitlink-member` | 仓库成员与邀请链接管理 |
|
||||
| `gitlink-release` | 发布管理(创建、编辑、更新、查看、删除等) |
|
||||
|
|
|
|||
|
|
@ -0,0 +1,68 @@
|
|||
# Issue batch maintenance shortcuts
|
||||
|
||||
## Summary
|
||||
|
||||
Add OpenAPI-backed Issue batch maintenance shortcuts:
|
||||
|
||||
- `issue +batch-update` — batch update Issue status, priority, milestone, tags, and assigners by API issue IDs.
|
||||
- `issue +batch-delete` — batch delete Issues by API issue IDs with explicit confirmation.
|
||||
|
||||
This complements the existing `issue +batch-close` command. `batch-close` uses web URL issue numbers, while the OpenAPI batch update/delete endpoints use API issue IDs.
|
||||
|
||||
## OpenAPI coverage
|
||||
|
||||
| Command | Method | Endpoint |
|
||||
|---|---|---|
|
||||
| `issue +batch-update` | PATCH | `/api/v1/{owner}/{repo}/issues/batch_update.json` |
|
||||
| `issue +batch-delete` | DELETE | `/api/v1/{owner}/{repo}/issues/batch_destroy.json` |
|
||||
|
||||
## ID semantics
|
||||
|
||||
- `issue +batch-close --numbers` uses web URL Issue numbers (`project_issues_index`).
|
||||
- `issue +batch-update --ids` and `issue +batch-delete --ids` use API Issue IDs returned by Issue APIs.
|
||||
|
||||
The docs and help text explicitly call this out to avoid mixing the two ID types.
|
||||
|
||||
## Safety and usability
|
||||
|
||||
- Both commands support `--dry-run`.
|
||||
- `issue +batch-update` requires at least one update field.
|
||||
- `issue +batch-delete` is destructive and requires `--yes` for real execution.
|
||||
- ID lists are validated as positive integers and de-duplicated.
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
gitlink-cli issue +batch-update \
|
||||
--owner Gitlink \
|
||||
--repo forgeplus \
|
||||
--ids 101,102 \
|
||||
--status-id 3 \
|
||||
--priority-id 2 \
|
||||
--tag-ids 7,8 \
|
||||
--assigner-ids 11,12 \
|
||||
--dry-run
|
||||
|
||||
gitlink-cli issue +batch-delete \
|
||||
--owner Gitlink \
|
||||
--repo forgeplus \
|
||||
--ids 101,102 \
|
||||
--dry-run
|
||||
|
||||
gitlink-cli issue +batch-delete \
|
||||
--owner Gitlink \
|
||||
--repo forgeplus \
|
||||
--ids 101,102 \
|
||||
--yes
|
||||
```
|
||||
|
||||
## Tests
|
||||
|
||||
```bash
|
||||
GOPROXY=https://goproxy.cn,direct go test ./...
|
||||
go vet ./...
|
||||
go run . issue +batch-update --help
|
||||
go run . issue +batch-delete --help
|
||||
go run . issue +batch-update --owner wangyue111 --repo gitlink-cli --ids 101,102 --status-id 3 --dry-run --format json
|
||||
go run . issue +batch-delete --owner wangyue111 --repo gitlink-cli --ids 101,102 --dry-run --format json
|
||||
```
|
||||
|
|
@ -210,3 +210,185 @@ func parseBool(value string) bool {
|
|||
parsed, err := strconv.ParseBool(strings.TrimSpace(value))
|
||||
return err == nil && parsed
|
||||
}
|
||||
|
||||
type batchMaintenanceDryRun struct {
|
||||
Repository string `json:"repository" yaml:"repository"`
|
||||
DryRun bool `json:"dry_run" yaml:"dry_run"`
|
||||
Action string `json:"action" yaml:"action"`
|
||||
Method string `json:"method" yaml:"method"`
|
||||
Path string `json:"path" yaml:"path"`
|
||||
Body map[string]interface{} `json:"body" yaml:"body"`
|
||||
}
|
||||
|
||||
func newBatchUpdateShortcut() *common.Shortcut {
|
||||
return &common.Shortcut{
|
||||
Name: "batch-update",
|
||||
Description: "Batch update issue metadata by API issue IDs",
|
||||
Flags: []common.Flag{
|
||||
{Name: "ids", Usage: "Comma-separated API issue IDs, not web URL issue numbers", Required: true},
|
||||
{Name: "status-id", Usage: "Issue status ID"},
|
||||
{Name: "priority-id", Usage: "Issue priority ID"},
|
||||
{Name: "milestone-id", Usage: "Issue milestone ID"},
|
||||
{Name: "tag-ids", Usage: "Comma-separated issue tag IDs"},
|
||||
{Name: "assigner-ids", Usage: "Comma-separated assignee user IDs"},
|
||||
{Name: "dry-run", Usage: "Preview request without updating issues", Bool: true, Default: "false"},
|
||||
},
|
||||
Run: runBatchUpdate,
|
||||
}
|
||||
}
|
||||
|
||||
func newBatchDeleteShortcut() *common.Shortcut {
|
||||
return &common.Shortcut{
|
||||
Name: "batch-delete",
|
||||
Description: "Batch delete issues by API issue IDs",
|
||||
Flags: []common.Flag{
|
||||
{Name: "ids", Usage: "Comma-separated API issue IDs, not web URL issue numbers", Required: true},
|
||||
{Name: "dry-run", Usage: "Preview request without deleting issues", Bool: true, Default: "false"},
|
||||
{Name: "yes", Usage: "Confirm real batch deletion", Bool: true, Default: "false"},
|
||||
},
|
||||
Run: runBatchDelete,
|
||||
}
|
||||
}
|
||||
|
||||
func runBatchUpdate(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
body, err := buildBatchUpdateBody(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
path := fmt.Sprintf("%s/issues/batch_update", v1RepoPath(ctx))
|
||||
if parseBool(ctx.Arg("dry-run")) {
|
||||
return ctx.OutputData(batchMaintenanceDryRun{
|
||||
Repository: fmt.Sprintf("%s/%s", ctx.Owner, ctx.Repo),
|
||||
DryRun: true,
|
||||
Action: "batch_update_issues",
|
||||
Method: "PATCH",
|
||||
Path: path,
|
||||
Body: body,
|
||||
})
|
||||
}
|
||||
env, err := ctx.CallAPI("PATCH", path, body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
}
|
||||
|
||||
func runBatchDelete(ctx *common.RuntimeContext) error {
|
||||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||||
return err
|
||||
}
|
||||
ids, err := parseIntIDList(ctx.Arg("ids"), "ids")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
body := map[string]interface{}{"ids": ids}
|
||||
path := fmt.Sprintf("%s/issues/batch_destroy", v1RepoPath(ctx))
|
||||
dryRun := parseBool(ctx.Arg("dry-run"))
|
||||
if dryRun {
|
||||
return ctx.OutputData(batchMaintenanceDryRun{
|
||||
Repository: fmt.Sprintf("%s/%s", ctx.Owner, ctx.Repo),
|
||||
DryRun: true,
|
||||
Action: "batch_delete_issues",
|
||||
Method: "DELETE",
|
||||
Path: path,
|
||||
Body: body,
|
||||
})
|
||||
}
|
||||
if !parseBool(ctx.Arg("yes")) {
|
||||
return fmt.Errorf("batch-delete is destructive; run with --dry-run first, then pass --yes to confirm")
|
||||
}
|
||||
env, err := ctx.CallAPI("DELETE", path, body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ctx.Output(env)
|
||||
}
|
||||
|
||||
func buildBatchUpdateBody(ctx *common.RuntimeContext) (map[string]interface{}, error) {
|
||||
ids, err := parseIntIDList(ctx.Arg("ids"), "ids")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body := map[string]interface{}{"ids": ids}
|
||||
changed := false
|
||||
if value := ctx.Arg("status-id"); value != "" {
|
||||
id, err := parseSingleIntID(value, "status-id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body["status_id"] = id
|
||||
changed = true
|
||||
}
|
||||
if value := ctx.Arg("priority-id"); value != "" {
|
||||
id, err := parseSingleIntID(value, "priority-id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body["priority_id"] = id
|
||||
changed = true
|
||||
}
|
||||
if value := ctx.Arg("milestone-id"); value != "" {
|
||||
id, err := parseSingleIntID(value, "milestone-id")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body["milestone_id"] = id
|
||||
changed = true
|
||||
}
|
||||
if value := ctx.Arg("tag-ids"); value != "" {
|
||||
ids, err := parseIntIDList(value, "tag-ids")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body["issue_tag_ids"] = ids
|
||||
changed = true
|
||||
}
|
||||
if value := ctx.Arg("assigner-ids"); value != "" {
|
||||
ids, err := parseIntIDList(value, "assigner-ids")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body["assigner_ids"] = ids
|
||||
changed = true
|
||||
}
|
||||
if !changed {
|
||||
return nil, fmt.Errorf("no update fields provided; set at least one of --status-id, --priority-id, --milestone-id, --tag-ids, --assigner-ids")
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
func parseSingleIntID(value, field string) (int, error) {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return 0, fmt.Errorf("%s cannot be empty", field)
|
||||
}
|
||||
id, err := strconv.Atoi(value)
|
||||
if err != nil || id <= 0 {
|
||||
return 0, fmt.Errorf("invalid %s %q: must be a positive integer", field, value)
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func parseIntIDList(value, field string) ([]int, error) {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return nil, fmt.Errorf("%s cannot be empty", field)
|
||||
}
|
||||
parts := strings.Split(value, ",")
|
||||
ids := make([]int, 0, len(parts))
|
||||
seen := map[int]bool{}
|
||||
for _, part := range parts {
|
||||
id, err := parseSingleIntID(part, field)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if seen[id] {
|
||||
continue
|
||||
}
|
||||
seen[id] = true
|
||||
ids = append(ids, id)
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package issue
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
|
|
@ -213,3 +214,136 @@ func writeTempCSV(t *testing.T, content string) string {
|
|||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func TestBatchUpdateDryRun(t *testing.T) {
|
||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("dry-run should not call API, got %s %s", r.Method, r.URL.Path)
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "batch-update", map[string]string{
|
||||
"ids": "101,102",
|
||||
"status-id": "3",
|
||||
"priority-id": "2",
|
||||
"tag-ids": "7,8",
|
||||
"assigner-ids": "11",
|
||||
"dry-run": "true",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("batch-update dry-run failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchUpdateCallsAPI(t *testing.T) {
|
||||
var payload map[string]interface{}
|
||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "PATCH" || r.URL.Path != "/v1/owner/repo/issues/batch_update.json" {
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
payload = decodeJSON(t, r)
|
||||
writeJSON(t, w, map[string]interface{}{"status": 0, "message": "success"})
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
err := runShortcut(t, server, "batch-update", map[string]string{
|
||||
"ids": "101,102,101",
|
||||
"status-id": "3",
|
||||
"priority-id": "2",
|
||||
"milestone-id": "9",
|
||||
"tag-ids": "7,8",
|
||||
"assigner-ids": "11,12",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("batch-update failed: %v", err)
|
||||
}
|
||||
assertFloatSlice(t, payload["ids"], []float64{101, 102})
|
||||
assertEqual(t, payload["status_id"], float64(3))
|
||||
assertEqual(t, payload["priority_id"], float64(2))
|
||||
assertEqual(t, payload["milestone_id"], float64(9))
|
||||
assertFloatSlice(t, payload["issue_tag_ids"], []float64{7, 8})
|
||||
assertFloatSlice(t, payload["assigner_ids"], []float64{11, 12})
|
||||
}
|
||||
|
||||
func TestBatchUpdateRequiresUpdateField(t *testing.T) {
|
||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("unexpected API call: %s %s", r.Method, r.URL.Path)
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
if err := runShortcut(t, server, "batch-update", map[string]string{"ids": "101"}); err == nil {
|
||||
t.Fatal("expected error when no update fields are provided")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchUpdateRejectsInvalidIDs(t *testing.T) {
|
||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("unexpected API call: %s %s", r.Method, r.URL.Path)
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
cases := []map[string]string{
|
||||
{"ids": "abc", "status-id": "3"},
|
||||
{"ids": "101", "status-id": "bad"},
|
||||
{"ids": "101", "tag-ids": "7,,8"},
|
||||
}
|
||||
for _, args := range cases {
|
||||
if err := runShortcut(t, server, "batch-update", args); err == nil {
|
||||
t.Fatalf("expected validation error for args %#v", args)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchDeleteDryRun(t *testing.T) {
|
||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("dry-run should not call API, got %s %s", r.Method, r.URL.Path)
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
if err := runShortcut(t, server, "batch-delete", map[string]string{"ids": "101,102", "dry-run": "true"}); err != nil {
|
||||
t.Fatalf("batch-delete dry-run failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchDeleteRequiresYes(t *testing.T) {
|
||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Fatalf("unexpected API call without --yes: %s %s", r.Method, r.URL.Path)
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
if err := runShortcut(t, server, "batch-delete", map[string]string{"ids": "101"}); err == nil {
|
||||
t.Fatal("expected --yes confirmation error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBatchDeleteCallsAPIWithYes(t *testing.T) {
|
||||
var payload map[string]interface{}
|
||||
server := newIssueTestServer(t, func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "DELETE" || r.URL.Path != "/v1/owner/repo/issues/batch_destroy.json" {
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
payload = decodeJSON(t, r)
|
||||
writeJSON(t, w, map[string]interface{}{"status": 0, "message": "success"})
|
||||
})
|
||||
defer server.Close()
|
||||
|
||||
if err := runShortcut(t, server, "batch-delete", map[string]string{"ids": "101,102,101", "yes": "true"}); err != nil {
|
||||
t.Fatalf("batch-delete failed: %v", err)
|
||||
}
|
||||
assertFloatSlice(t, payload["ids"], []float64{101, 102})
|
||||
}
|
||||
|
||||
func assertFloatSlice(t *testing.T, got interface{}, want []float64) {
|
||||
t.Helper()
|
||||
items, ok := got.([]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("got %#v, want []interface{}", got)
|
||||
}
|
||||
if len(items) != len(want) {
|
||||
t.Fatalf("got len %d, want %d: %#v", len(items), len(want), got)
|
||||
}
|
||||
for i := range want {
|
||||
if items[i] != want[i] {
|
||||
t.Fatalf("item %d = %#v, want %#v", i, items[i], want[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,8 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
|
|||
tr := shortcutTranslator(translators...)
|
||||
return []*common.Shortcut{
|
||||
newBatchCloseShortcut(),
|
||||
newBatchUpdateShortcut(),
|
||||
newBatchDeleteShortcut(),
|
||||
{
|
||||
Name: "list",
|
||||
Description: tr.T("cmd.issue.list.short"),
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@ skills/
|
|||
|-------|------|----------|
|
||||
| **gitlink-shared** | 认证、全局参数、API 参考、安全规则、分支约定 | `auth login`, `auth status` |
|
||||
| **gitlink-repo** | 仓库管理与洞察 | `repo +list`, `repo +info`, `repo +languages`, `repo +contributors`, `repo +code-stats`, `repo +follow`, `repo +like` |
|
||||
| **gitlink-issue** | Issue 管理 | `issue +create`, `issue +list`, `issue +view`, `issue +close`, `issue +batch-close` |
|
||||
| **gitlink-issue** | Issue 管理 | `issue +create`, `issue +list`, `issue +view`, `issue +close`, `issue +batch-close`, `issue +batch-update`, `issue +batch-delete` |
|
||||
| **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` |
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
---
|
||||
name: gitlink-issue
|
||||
version: 2.0.0
|
||||
description: "Issue 管理:创建、查看、更新、关闭/批量关闭 Issue,添加评论。当用户需要操作 GitLink Issue 时触发。"
|
||||
description: "Issue 管理:创建、查看、更新、关闭/批量关闭/批量更新/批量删除 Issue,添加评论。当用户需要操作 GitLink Issue 时触发。"
|
||||
metadata:
|
||||
requires:
|
||||
bins: ["gitlink-cli"]
|
||||
|
|
@ -26,6 +26,8 @@ metadata:
|
|||
| `issue +update` | 更新 Issue | 是 |
|
||||
| `issue +close` | 关闭 Issue | 是 |
|
||||
| `issue +batch-close` | 批量关闭 Issue,支持 `--dry-run` 预览 | 是(dry-run 不写入) |
|
||||
| `issue +batch-update` | 按 API issue id 批量更新状态、优先级、里程碑、标签、负责人 | 是(dry-run 不写入) |
|
||||
| `issue +batch-delete` | 按 API issue id 批量删除 Issue;真实删除必须 `--yes` | 是(dry-run 不写入) |
|
||||
| `issue +comment` | 添加评论 | 是 |
|
||||
| `issue +assigners` | 查询 Issue 负责人列表 | 否(公开项目) |
|
||||
| `issue +authors` | 查询 Issue 发布人列表 | 否(公开项目) |
|
||||
|
|
@ -60,6 +62,13 @@ gitlink-cli issue +batch-close --owner myuser --repo myrepo --numbers 123,124 --
|
|||
# 从 CSV 文件批量关闭 Issue
|
||||
gitlink-cli issue +batch-close --owner myuser --repo myrepo --from issues.csv
|
||||
|
||||
# 按 API issue id 预览批量更新元数据(注意不是网页 Issue 编号)
|
||||
gitlink-cli issue +batch-update --owner myuser --repo myrepo --ids 101,102 --status-id 3 --priority-id 2 --dry-run
|
||||
|
||||
# 危险批量删除:必须先 dry-run,真实执行还要 --yes
|
||||
gitlink-cli issue +batch-delete --owner myuser --repo myrepo --ids 101,102 --dry-run
|
||||
gitlink-cli issue +batch-delete --owner myuser --repo myrepo --ids 101,102 --yes
|
||||
|
||||
# 添加评论
|
||||
gitlink-cli issue +comment --number 4 --body "已修复,请验证"
|
||||
|
||||
|
|
@ -70,6 +79,14 @@ gitlink-cli issue +assigners --owner Gitlink --repo forgeplus --keyword alice
|
|||
gitlink-cli issue +authors --owner Gitlink --repo forgeplus --keyword bob
|
||||
```
|
||||
|
||||
## 批量维护安全约束
|
||||
|
||||
- `issue +batch-close --numbers` 使用网页 URL 中的 Issue 编号,即 `project_issues_index`。
|
||||
- `issue +batch-update --ids` 和 `issue +batch-delete --ids` 使用 OpenAPI 返回的 API issue id,不是网页 Issue 编号。
|
||||
- 执行 `batch-update` / `batch-delete` 前,先用 `issue +list` 或 `issue +view` 确认 id 来源。
|
||||
- 写操作先执行 `--dry-run`,展示 `method`、`path`、`body` 给用户确认。
|
||||
- `batch-delete` 是破坏性操作,真实执行必须显式传 `--yes`。
|
||||
|
||||
## Raw API 补充
|
||||
|
||||
```bash
|
||||
|
|
|
|||
Loading…
Reference in New Issue