feat: 添加reopen/batch-reopen命令 + 修复assign使用PATCH代替PUT

This commit is contained in:
chroe 2026-06-04 09:00:23 +08:00
parent 1004bddc8d
commit 623e065efa
3 changed files with 141 additions and 7 deletions

View File

@ -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

View File

@ -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)
}

View File

@ -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}] },
]