From 623e065efa28d7f9591f4dfb4e9d50586a8f0d4a Mon Sep 17 00:00:00 2001 From: chroe <454604461@qq.com> Date: Thu, 4 Jun 2026 09:00:23 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0reopen/batch-reopen?= =?UTF-8?q?=E5=91=BD=E4=BB=A4=20+=20=E4=BF=AE=E5=A4=8Dassign=E4=BD=BF?= =?UTF-8?q?=E7=94=A8PATCH=E4=BB=A3=E6=9B=BFPUT?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- shortcuts/issue/batch.go | 96 +++++++++++++++++++++++++++++++++++++++- shortcuts/issue/issue.go | 42 +++++++++++++++++- showcase/index.html | 10 +++-- 3 files changed, 141 insertions(+), 7 deletions(-) diff --git a/shortcuts/issue/batch.go b/shortcuts/issue/batch.go index 3ea0dbe..490f3a0 100644 --- a/shortcuts/issue/batch.go +++ b/shortcuts/issue/batch.go @@ -12,6 +12,7 @@ import ( ) const closedIssueStatusID = 5 +const openIssueStatusID = 1 type batchResult struct { Number string `json:"number" yaml:"number"` @@ -114,6 +115,90 @@ func closeIssue(ctx *common.RuntimeContext, number string) error { return nil } +func newBatchReopenShortcut() *common.Shortcut { + return &common.Shortcut{ + Name: "batch-reopen", + Description: "Reopen multiple closed issues by issue numbers or a CSV file", + Flags: []common.Flag{ + {Name: "numbers", Short: "n", Usage: "Comma-separated issue numbers from the web URL, for example: 1,2,3"}, + {Name: "from", Usage: "Read issue numbers from a CSV file. Supports a number/issue_number/project_issues_index column or first column without header"}, + {Name: "dry-run", Usage: "Preview the issues that would be reopened without changing them", Bool: true, Default: "false"}, + }, + Run: runBatchReopen, + } +} + +func runBatchReopen(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return fmt.Errorf("解析仓库信息失败: %w", err) + } + + start := time.Now() + + numbers, err := collectIssueNumbers(ctx.Arg("numbers"), ctx.Arg("from")) + if err != nil { + return err + } + if len(numbers) == 0 { + return fmt.Errorf("未提供 Issue 编号,请使用 --numbers 1,2,3 或 --from issues.csv") + } + + dryRun := parseBool(ctx.Arg("dry-run")) + summary := batchSummary{ + Repository: fmt.Sprintf("%s/%s", ctx.Owner, ctx.Repo), + DryRun: dryRun, + Total: len(numbers), + Results: make([]batchResult, 0, len(numbers)), + } + + for _, number := range numbers { + result := batchResult{Number: number, Action: "reopen"} + if dryRun { + result.Status = "planned" + summary.Succeeded++ + summary.Results = append(summary.Results, result) + continue + } + + if err := reopenIssue(ctx, number); err != nil { + result.Status = "failed" + result.Error = err.Error() + summary.Failed++ + } else { + result.Status = "reopened" + summary.Succeeded++ + } + summary.Results = append(summary.Results, result) + } + + summary.Duration = time.Since(start).String() + + if err := ctx.OutputData(summary); err != nil { + return err + } + if summary.Failed > 0 { + return fmt.Errorf("%d / %d 个 Issue 重新打开失败", summary.Failed, summary.Total) + } + return nil +} + +func reopenIssue(ctx *common.RuntimeContext, number string) error { + current, err := fetchExistingIssue(ctx, number) + if err != nil { + return fmt.Errorf("获取 Issue 详情: %w", err) + } + + body := map[string]interface{}{ + "subject": current.Subject, + "description": current.Description, + "status_id": openIssueStatusID, + } + if _, err := ctx.CallAPI("PATCH", fmt.Sprintf("%s/issues/%s", v1RepoPath(ctx), number), body); err != nil { + return fmt.Errorf("重新打开 Issue: %w", err) + } + return nil +} + func newBatchAssignShortcut() *common.Shortcut { return &common.Shortcut{ Name: "batch-assign", @@ -121,7 +206,7 @@ func newBatchAssignShortcut() *common.Shortcut { Flags: []common.Flag{ {Name: "numbers", Short: "n", Usage: "Comma-separated issue numbers from the web URL, for example: 1,2,3"}, {Name: "from", Usage: "Read issue numbers from a CSV file. Supports a number/issue_number/project_issues_index column or first column without header"}, - {Name: "user", Short: "u", Usage: "Assignee user login", Required: true}, + {Name: "user", Short: "u", Usage: "Assignee user ID (numeric)", Required: true}, {Name: "dry-run", Usage: "Preview the issues that would be assigned without changing them", Bool: true, Default: "false"}, }, Run: runBatchAssign, @@ -184,10 +269,17 @@ func runBatchAssign(ctx *common.RuntimeContext) error { } func assignIssue(ctx *common.RuntimeContext, number, user string) error { + current, err := fetchExistingIssue(ctx, number) + if err != nil { + return fmt.Errorf("获取 Issue 详情: %w", err) + } + body := map[string]interface{}{ + "subject": current.Subject, + "description": current.Description, "assigned_to_id": user, } - if _, err := ctx.CallAPI("PUT", fmt.Sprintf("%s/issues/%s/assignees", v1RepoPath(ctx), number), body); err != nil { + if _, err := ctx.CallAPI("PATCH", fmt.Sprintf("%s/issues/%s", v1RepoPath(ctx), number), body); err != nil { return fmt.Errorf("分配 Issue: %w", err) } return nil diff --git a/shortcuts/issue/issue.go b/shortcuts/issue/issue.go index e8deb70..2e4e6bf 100644 --- a/shortcuts/issue/issue.go +++ b/shortcuts/issue/issue.go @@ -23,6 +23,7 @@ type existingIssue struct { func Shortcuts() []*common.Shortcut { return []*common.Shortcut{ newBatchCloseShortcut(), + newBatchReopenShortcut(), newBatchAssignShortcut(), newBatchLabelShortcut(), newBatchMilestoneShortcut(), @@ -143,6 +144,37 @@ func Shortcuts() []*common.Shortcut { return ctx.Output(env) }, }, + { + Name: "reopen", + Description: "Reopen a closed issue", + Flags: []common.Flag{ + {Name: "number", Short: "n", Usage: "Issue number (as shown in the web URL)", Required: true}, + }, + Run: func(ctx *common.RuntimeContext) error { + if err := ctx.ResolveOwnerRepo(); err != nil { + return fmt.Errorf("解析仓库信息失败: %w", err) + } + number, err := ctx.RequireArg("number") + if err != nil { + return err + } + current, err := fetchExistingIssue(ctx, number) + if err != nil { + return fmt.Errorf("重新打开 Issue 失败: %w", err) + } + + body := map[string]interface{}{ + "subject": current.Subject, + "description": current.Description, + "status_id": 1, // 1 = open + } + env, err := ctx.CallAPI("PATCH", fmt.Sprintf("%s/issues/%s", v1RepoPath(ctx), number), body) + if err != nil { + return fmt.Errorf("重新打开 Issue 失败: %w", err) + } + return ctx.Output(env) + }, + }, { Name: "update", Description: "Update an issue", @@ -201,7 +233,7 @@ func Shortcuts() []*common.Shortcut { Description: "Assign an issue to a user", Flags: []common.Flag{ {Name: "number", Short: "n", Usage: "Issue number (as shown in the web URL)", Required: true}, - {Name: "user", Short: "u", Usage: "Assignee user login", Required: true}, + {Name: "user", Short: "u", Usage: "Assignee user ID (numeric)", Required: true}, }, Run: func(ctx *common.RuntimeContext) error { if err := ctx.ResolveOwnerRepo(); err != nil { @@ -215,10 +247,16 @@ func Shortcuts() []*common.Shortcut { if err != nil { return err } + current, err := fetchExistingIssue(ctx, number) + if err != nil { + return fmt.Errorf("分配 Issue 失败: %w", err) + } body := map[string]interface{}{ + "subject": current.Subject, + "description": current.Description, "assigned_to_id": user, } - env, err := ctx.CallAPI("PUT", fmt.Sprintf("%s/issues/%s/assignees", v1RepoPath(ctx), number), body) + env, err := ctx.CallAPI("PATCH", fmt.Sprintf("%s/issues/%s", v1RepoPath(ctx), number), body) if err != nil { return fmt.Errorf("分配 Issue 失败: %w", err) } diff --git a/showcase/index.html b/showcase/index.html index e8ed581..94dc7f8 100644 --- a/showcase/index.html +++ b/showcase/index.html @@ -188,12 +188,16 @@ const MODULES = [ ] }, { - name: "issue", title: "批量 Issue 操作", - desc: "批量管理疑修(Issue):批量关闭、分配、设置标签和里程碑,支持逗号列表或 CSV 文件导入", + name: "issue", title: "Issue 管理", + desc: "管理疑修(Issue):单个关闭/重新打开,批量关闭/重新打开/分配/设置标签和里程碑,支持逗号列表或 CSV 文件导入", color: "#13c2c2", commands: [ + { name: "close", desc: "关闭 Issue", detail: "关闭指定编号的 Issue", params: [{k:"number",l:"Issue编号",v:"1"}] }, + { name: "reopen", desc: "重新打开 Issue", detail: "重新打开已关闭的 Issue", params: [{k:"number",l:"Issue编号",v:"1"}] }, + { name: "assign", desc: "分配 Issue", detail: "将 Issue 分配给指定用户。user 是用户数字ID,可从成员管理卡片查看", params: [{k:"number",l:"Issue编号",v:"1"},{k:"user",l:"负责人用户ID",v:"149027"}] }, { name: "batch-close", desc: "批量关闭 Issue", detail: "按编号批量关闭 Issue,支持 --numbers 逗号分隔或 --from CSV 文件。开启预览模式可先查看将执行的操作而不实际执行", params: [{k:"numbers",l:"Issue编号",v:"1,2,3"},{k:"dry-run",l:"预览模式",v:"false",bool:true}] }, - { name: "batch-assign", desc: "批量分配 Issue", detail: "将多个 Issue 批量分配给指定用户处理,用户名为 GitLink 登录名", params: [{k:"numbers",l:"Issue编号",v:"1,2,3"},{k:"user",l:"负责人用户名",v:"chroe"},{k:"dry-run",l:"预览模式",v:"false",bool:true}] }, + { name: "batch-reopen", desc: "批量重新打开 Issue", detail: "批量重新打开已关闭的 Issue,支持 --numbers 逗号分隔或 --from CSV 文件", params: [{k:"numbers",l:"Issue编号",v:"1,2,3"},{k:"dry-run",l:"预览模式",v:"false",bool:true}] }, + { name: "batch-assign", desc: "批量分配 Issue", detail: "将多个 Issue 批量分配给指定用户。user 是用户数字ID,可从成员管理卡片查看", params: [{k:"numbers",l:"Issue编号",v:"1,2,3"},{k:"user",l:"负责人用户ID",v:"149027"},{k:"dry-run",l:"预览模式",v:"false",bool:true}] }, { name: "batch-label", desc: "批量设置标签", detail: "批量为 Issue 添加或移除标签。标签ID 先用 label +list 查看", params: [{k:"numbers",l:"Issue编号",v:"1,2,3"},{k:"add",l:"添加标签ID",v:""},{k:"remove",l:"移除标签ID",v:""},{k:"dry-run",l:"预览模式",v:"false",bool:true}] }, { name: "batch-milestone", desc: "批量设置里程碑", detail: "批量将 Issue 关联到指定的里程碑。里程碑ID 先用 milestone +list 查看", params: [{k:"numbers",l:"Issue编号",v:"1,2,3"},{k:"milestone",l:"里程碑ID",v:"2760"},{k:"dry-run",l:"预览模式",v:"false",bool:true}] }, ]