forked from Gitlink/gitlink-cli
669 lines
21 KiB
Go
669 lines
21 KiB
Go
package issue
|
||
|
||
import (
|
||
"fmt"
|
||
"net/url"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"github.com/gitlink-org/gitlink-cli/internal/output"
|
||
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
||
)
|
||
|
||
// v1RepoPath returns the v1 API path prefix: /v1/{owner}/{repo}
|
||
func v1RepoPath(ctx *common.RuntimeContext) string {
|
||
return fmt.Sprintf("/v1/%s/%s", ctx.Owner, ctx.Repo)
|
||
}
|
||
|
||
type existingIssue struct {
|
||
Subject string
|
||
Description string
|
||
}
|
||
|
||
func Shortcuts() []*common.Shortcut {
|
||
return []*common.Shortcut{
|
||
newBatchCloseShortcut(),
|
||
newBatchUpdateShortcut(),
|
||
newBatchDestroyShortcut(),
|
||
// +list:列出当前仓库的疑修(支持按状态/里程碑/标签筛选)
|
||
{
|
||
Name: "list",
|
||
Description: "列出疑修",
|
||
Long: `List issues in the current repository.
|
||
|
||
Shows issue number, title, status, and other details.
|
||
Use --state to filter by status, --milestone, --assignee, --label,
|
||
--keyword for additional filtering, and --page and --limit for pagination.`,
|
||
Example: ` # List open issues (default)
|
||
gitlink issue +list
|
||
|
||
# List closed issues
|
||
gitlink issue +list --state closed
|
||
|
||
# Show 50 issues per page
|
||
gitlink issue +list --limit 50
|
||
|
||
# Filter by milestone and assignee
|
||
gitlink issue +list --milestone 3 --assignee 5
|
||
|
||
# Search by keyword
|
||
gitlink issue +list -k "login bug"
|
||
|
||
# Sort by created date
|
||
gitlink issue +list --sort created_on
|
||
|
||
# Sort by updated date descending
|
||
gitlink issue +list --sort updated_on --sort-direction desc`,
|
||
Flags: []common.Flag{
|
||
{Name: "state", Short: "s", Usage: "Filter by state: open, closed, all", Default: "open"},
|
||
{Name: "page", Short: "p", Usage: "Page number", Default: "1"},
|
||
{Name: "limit", Short: "l", Usage: "Items per page", Default: "20"},
|
||
{Name: "milestone", Short: "m", Usage: "Filter by milestone ID"},
|
||
{Name: "assignee", Short: "a", Usage: "Filter by assignee user ID"},
|
||
{Name: "label", Usage: "Filter by label ID"},
|
||
{Name: "keyword", Short: "k", Usage: "Search by keyword"},
|
||
{Name: "sort", Usage: "Sort field (e.g. created_on, updated_on)"},
|
||
{Name: "sort-direction", Usage: "Sort direction: asc or desc"},
|
||
},
|
||
Run: func(ctx *common.RuntimeContext) error {
|
||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||
return err
|
||
}
|
||
q := url.Values{}
|
||
q.Set("page", ctx.Arg("page"))
|
||
q.Set("limit", ctx.Arg("limit"))
|
||
if s := ctx.Arg("state"); s != "" {
|
||
q.Set("state", s)
|
||
}
|
||
// [P2 参数补全] 以下 6 个查询参数原 CLI 未暴露,但 GitLink API 支持
|
||
// CLI 参数名 → API 字段名的映射(两者经常不一致)
|
||
if m := ctx.Arg("milestone"); m != "" {
|
||
q.Set("fixed_version_id", m) // CLI "milestone" → API "fixed_version_id"
|
||
}
|
||
if a := ctx.Arg("assignee"); a != "" {
|
||
q.Set("assigned_to_id", a) // CLI "assignee" → API "assigned_to_id"
|
||
}
|
||
if l := ctx.Arg("label"); l != "" {
|
||
q.Set("issue_tag_id", l) // CLI "label" → API "issue_tag_id"
|
||
}
|
||
if k := ctx.Arg("keyword"); k != "" {
|
||
q.Set("keyword", k) // 关键词搜索(名称一致,无需映射)
|
||
}
|
||
if s := ctx.Arg("sort"); s != "" {
|
||
q.Set("sort", s) // 排序字段(如 created_on, updated_on)
|
||
}
|
||
if d := ctx.Arg("sort-direction"); d != "" {
|
||
q.Set("sort_direction", d) // 排序方向 asc/desc(实测 API 用 sort+sort_direction,不是 sort_by)
|
||
}
|
||
env, err := ctx.CallAPIWithQuery("GET", v1RepoPath(ctx)+"/issues", q)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
normalizeIssueListIDs(env)
|
||
return ctx.Output(env)
|
||
},
|
||
},
|
||
// +create:在当前仓库创建新疑修(需 --title,可附带描述/指派人/里程碑/标签)
|
||
{
|
||
Name: "create",
|
||
Description: "创建新疑修",
|
||
Long: `Create a new issue in the current repository.
|
||
|
||
Requires a title (--title). You may optionally set a description,
|
||
assignee, milestone, and label at creation time.`,
|
||
Example: ` # Create an issue with just a title
|
||
gitlink issue +create --title "Fix login bug"
|
||
|
||
# Create with description and assignee
|
||
gitlink issue +create --title "Fix login bug" --body "Steps to reproduce..." --assignee zhangsan
|
||
|
||
# Create with milestone and label
|
||
gitlink issue +create --title "New feature" --milestone 3 --label 5`,
|
||
Flags: []common.Flag{
|
||
{Name: "title", Short: "t", Usage: "Issue title", Required: true},
|
||
{Name: "body", Short: "b", Usage: "Issue description"},
|
||
{Name: "assignee", Short: "a", Usage: "Assignee login"},
|
||
{Name: "milestone", Short: "m", Usage: "Milestone ID"},
|
||
{Name: "label", Usage: "Label ID"},
|
||
},
|
||
Run: func(ctx *common.RuntimeContext) error {
|
||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||
return err
|
||
}
|
||
title, err := ctx.RequireArg("title")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
body := map[string]interface{}{
|
||
"subject": title,
|
||
"status_id": 1, // 1 = open (required by v1 API)
|
||
"priority_id": 2, // 2 = normal
|
||
"done_ratio": 0,
|
||
}
|
||
if desc := ctx.Arg("body"); desc != "" {
|
||
body["description"] = desc
|
||
}
|
||
if a := ctx.Arg("assignee"); a != "" {
|
||
body["assigned_to_id"] = a // CLI "assignee" → API "assigned_to_id"
|
||
}
|
||
if m := ctx.Arg("milestone"); m != "" {
|
||
body["fixed_version_id"] = m // CLI "milestone" → API "fixed_version_id"
|
||
}
|
||
// [P1 Bug#1 修复] 原代码只在 Flags 中定义了 label 参数,Run 函数里没有取值写入请求体
|
||
// 导致用户传 --label 5 不报错,但创建的 issue 没有标签
|
||
// 修复:从 ctx.Arg("label") 取值,映射到 API 字段名 "issue_tag_id"
|
||
if l := ctx.Arg("label"); l != "" {
|
||
body["issue_tag_id"] = l // CLI "label" → API "issue_tag_id"
|
||
}
|
||
env, err := ctx.CallAPI("POST", v1RepoPath(ctx)+"/issues", body)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return ctx.Output(env)
|
||
},
|
||
},
|
||
// +view:查看指定疑修的详情(标题/描述/状态/指派人等)
|
||
{
|
||
Name: "view",
|
||
Description: "查看疑修详情",
|
||
Long: `View detailed information about a specific issue.
|
||
|
||
Displays the issue title, description, status, assignee,
|
||
and other metadata. Use the issue number as shown in the web URL.`,
|
||
Example: ` # View issue #42
|
||
gitlink issue +view --number 42
|
||
|
||
# Short form
|
||
gitlink issue +view -n 42`,
|
||
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 err
|
||
}
|
||
number, err := ctx.RequireArg("number")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
env, err := ctx.CallAPI("GET", fmt.Sprintf("%s/issues/%s", v1RepoPath(ctx), number), nil)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return ctx.Output(env)
|
||
},
|
||
},
|
||
// +close:关闭疑修(保留标题和描述,可选附上说明性评论)
|
||
{
|
||
Name: "close",
|
||
Description: "关闭疑修",
|
||
Long: `Close an existing issue by setting its status to closed.
|
||
|
||
The issue number is the one shown in the web URL (project-level index).
|
||
This preserves the existing title and description.
|
||
Use --comment to leave an explanation when closing.`,
|
||
Example: ` # Close issue #42
|
||
gitlink issue +close --number 42
|
||
|
||
# Short form
|
||
gitlink issue +close -n 42
|
||
|
||
# Close with a comment
|
||
gitlink issue +close -n 42 --comment "Fixed in commit abc123"`,
|
||
Flags: []common.Flag{
|
||
{Name: "number", Short: "n", Usage: "Issue number (as shown in the web URL)", Required: true},
|
||
{Name: "comment", Short: "c", Usage: "Comment to add when closing"},
|
||
},
|
||
Run: func(ctx *common.RuntimeContext) error {
|
||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||
return err
|
||
}
|
||
number, err := ctx.RequireArg("number")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
current, err := fetchExistingIssue(ctx, number)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
body := map[string]interface{}{
|
||
"subject": current.Subject,
|
||
"description": current.Description,
|
||
"status_id": 5, // 5 = closed
|
||
}
|
||
env, err := ctx.CallAPI("PATCH", fmt.Sprintf("%s/issues/%s", v1RepoPath(ctx), number), body)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
// [P2 参数补全] 关闭(PATCH)和评论(POST journals)是两个独立 API,代码串行调用
|
||
// 关闭 issue 本身是一个 PATCH 请求,评论是另一个 POST 请求到 journals 端点
|
||
if comment := ctx.Arg("comment"); comment != "" {
|
||
_, _ = ctx.CallAPI("POST",
|
||
fmt.Sprintf("%s/issues/%s/journals", v1RepoPath(ctx), number),
|
||
map[string]interface{}{"notes": comment})
|
||
}
|
||
return ctx.Output(env)
|
||
},
|
||
},
|
||
// +update:更新疑修的字段(标题/描述/状态/里程碑/指派人/标签/优先级等)
|
||
{
|
||
Name: "update",
|
||
Description: "更新疑修",
|
||
Long: `Update the title, description, status, or other fields of an existing issue.
|
||
|
||
At least one of --title, --body, --state, --milestone, --assignee,
|
||
--label, or --priority must be provided.
|
||
The --state flag accepts "open", "closed", or a numeric status_id.`,
|
||
Example: ` # Change the title
|
||
gitlink issue +update --number 42 --title "Updated title"
|
||
|
||
# Change description
|
||
gitlink issue +update -n 42 --body "New description"
|
||
|
||
# Reopen a closed issue
|
||
gitlink issue +update -n 42 --state open
|
||
|
||
# Update title and close at the same time
|
||
gitlink issue +update -n 42 --title "Fixed" --state closed
|
||
|
||
# Assign to a user and set milestone
|
||
gitlink issue +update -n 42 --assignee 5 --milestone 3
|
||
|
||
# Set label and priority
|
||
gitlink issue +update -n 42 --label 7 --priority 1`,
|
||
Flags: []common.Flag{
|
||
{Name: "number", Short: "n", Usage: "Issue number (as shown in the web URL)", Required: true},
|
||
{Name: "title", Short: "t", Usage: "New title"},
|
||
{Name: "body", Short: "b", Usage: "New description"},
|
||
{Name: "state", Short: "s", Usage: "New state: open, closed, or numeric status_id"},
|
||
{Name: "milestone", Short: "m", Usage: "Milestone ID"},
|
||
{Name: "assignee", Short: "a", Usage: "Assignee user ID"},
|
||
{Name: "label", Usage: "Label ID"},
|
||
{Name: "priority", Usage: "Priority ID"},
|
||
},
|
||
Run: func(ctx *common.RuntimeContext) error {
|
||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||
return err
|
||
}
|
||
number, err := ctx.RequireArg("number")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
title := ctx.Arg("title")
|
||
description := ctx.Arg("body")
|
||
state := ctx.Arg("state")
|
||
milestone := ctx.Arg("milestone")
|
||
assignee := ctx.Arg("assignee")
|
||
label := ctx.Arg("label")
|
||
priority := ctx.Arg("priority")
|
||
if title == "" && description == "" && state == "" &&
|
||
milestone == "" && assignee == "" && label == "" && priority == "" {
|
||
return fmt.Errorf("at least one of --title, --body, --state, --milestone, --assignee, --label, or --priority is required")
|
||
}
|
||
|
||
current, err := fetchExistingIssue(ctx, number)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
// [P2 参数补全] 保留原值,只修改用户指定的字段
|
||
// GitLink PATCH API 要求 subject 和 description 不能为空,所以必须先查出原值
|
||
body := map[string]interface{}{
|
||
"subject": current.Subject,
|
||
"description": current.Description,
|
||
}
|
||
if t := ctx.Arg("title"); t != "" {
|
||
body["subject"] = t
|
||
}
|
||
if b := ctx.Arg("body"); b != "" {
|
||
body["description"] = b
|
||
}
|
||
if s := ctx.Arg("state"); s != "" {
|
||
statusID, err := normalizeIssueStatus(s)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
body["status_id"] = statusID
|
||
}
|
||
// [P2 参数补全] 以下 4 个参数原 CLI 未暴露
|
||
if m := ctx.Arg("milestone"); m != "" {
|
||
body["fixed_version_id"] = m // CLI "milestone" → API "fixed_version_id"
|
||
}
|
||
if a := ctx.Arg("assignee"); a != "" {
|
||
body["assigned_to_id"] = a // CLI "assignee" → API "assigned_to_id"
|
||
}
|
||
if l := ctx.Arg("label"); l != "" {
|
||
body["issue_tag_id"] = l // CLI "label" → API "issue_tag_id"
|
||
}
|
||
if p := ctx.Arg("priority"); p != "" {
|
||
body["priority_id"] = p // CLI "priority" → API "priority_id"
|
||
}
|
||
env, err := ctx.CallAPI("PATCH", fmt.Sprintf("%s/issues/%s", v1RepoPath(ctx), number), body)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return ctx.Output(env)
|
||
},
|
||
},
|
||
// +comment:在指定疑修下添加一条评论(--number 和 --body 必填)
|
||
{
|
||
Name: "comment",
|
||
Description: "在疑修下添加评论",
|
||
Long: `Add a comment (journal entry) to an existing issue.
|
||
|
||
Both --number and --body are required.`,
|
||
Example: ` # Comment on issue #42
|
||
gitlink issue +comment --number 42 --body "This is now fixed."
|
||
|
||
# Short form
|
||
gitlink issue +comment -n 42 -b "LGTM"`,
|
||
Flags: []common.Flag{
|
||
{Name: "number", Short: "n", Usage: "Issue number (as shown in the web URL)", Required: true},
|
||
{Name: "body", Short: "b", Usage: "Comment body", Required: true},
|
||
},
|
||
Run: func(ctx *common.RuntimeContext) error {
|
||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||
return err
|
||
}
|
||
number, err := ctx.RequireArg("number")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
body, err := ctx.RequireArg("body")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
payload := map[string]interface{}{
|
||
"notes": body,
|
||
}
|
||
env, err := ctx.CallAPI("POST", fmt.Sprintf("%s/issues/%s/journals", v1RepoPath(ctx), number), payload)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return ctx.Output(env)
|
||
},
|
||
},
|
||
// +comments:列出指定疑修下的评论与动态记录
|
||
{
|
||
Name: "comments",
|
||
Description: "列出疑修的评论与动态",
|
||
Flags: []common.Flag{
|
||
{Name: "number", Short: "n", Usage: "Issue number (as shown in the web URL)", Required: true},
|
||
{Name: "page", Short: "p", Usage: "Page number", Default: "1"},
|
||
{Name: "limit", Short: "l", Usage: "Items per page", Default: "20"},
|
||
},
|
||
Run: func(ctx *common.RuntimeContext) error {
|
||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||
return err
|
||
}
|
||
number, err := ctx.RequireArg("number")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
q := url.Values{}
|
||
q.Set("page", ctx.Arg("page"))
|
||
q.Set("limit", ctx.Arg("limit"))
|
||
env, err := ctx.CallAPIWithQuery("GET", fmt.Sprintf("%s/issues/%s/journals", v1RepoPath(ctx), number), q)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return ctx.Output(env)
|
||
},
|
||
},
|
||
// +update-comment:修改疑修下的某条评论内容
|
||
{
|
||
Name: "update-comment",
|
||
Description: "修改疑修下的评论",
|
||
Flags: []common.Flag{
|
||
{Name: "number", Short: "n", Usage: "Issue number (as shown in the web URL)", Required: true},
|
||
{Name: "id", Usage: "Comment journal ID", Required: true},
|
||
{Name: "body", Short: "b", Usage: "New comment body", Required: true},
|
||
},
|
||
Run: func(ctx *common.RuntimeContext) error {
|
||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||
return err
|
||
}
|
||
number, err := ctx.RequireArg("number")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
id, err := ctx.RequireArg("id")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
body, err := ctx.RequireArg("body")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
payload := map[string]interface{}{
|
||
"notes": body,
|
||
}
|
||
env, err := ctx.CallAPI("PATCH", fmt.Sprintf("%s/issues/%s/journals/%s", v1RepoPath(ctx), number, id), payload)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return ctx.Output(env)
|
||
},
|
||
},
|
||
// +delete-comment:删除疑修下的某条评论
|
||
{
|
||
Name: "delete-comment",
|
||
Description: "删除疑修下的评论",
|
||
Flags: []common.Flag{
|
||
{Name: "number", Short: "n", Usage: "Issue number (as shown in the web URL)", Required: true},
|
||
{Name: "id", Usage: "Comment journal ID", Required: true},
|
||
},
|
||
Run: func(ctx *common.RuntimeContext) error {
|
||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||
return err
|
||
}
|
||
number, err := ctx.RequireArg("number")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
id, err := ctx.RequireArg("id")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
env, err := ctx.CallAPI("DELETE", fmt.Sprintf("%s/issues/%s/journals/%s", v1RepoPath(ctx), number, id), nil)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return ctx.Output(env)
|
||
},
|
||
},
|
||
// +reply-comment:列出某条评论的子回复
|
||
{
|
||
Name: "reply-comment",
|
||
Description: "列出评论的子回复",
|
||
Flags: []common.Flag{
|
||
{Name: "number", Short: "n", Usage: "Issue number (as shown in the web URL)", Required: true},
|
||
{Name: "id", Usage: "Parent comment journal ID", Required: true},
|
||
},
|
||
Run: func(ctx *common.RuntimeContext) error {
|
||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||
return err
|
||
}
|
||
number, err := ctx.RequireArg("number")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
id, err := ctx.RequireArg("id")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
env, err := ctx.CallAPI("GET", fmt.Sprintf("%s/issues/%s/journals/%s/children_journals", v1RepoPath(ctx), number, id), nil)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return ctx.Output(env)
|
||
},
|
||
},
|
||
// +delete:删除指定疑修
|
||
{
|
||
Name: "delete",
|
||
Description: "删除疑修",
|
||
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 err
|
||
}
|
||
number, err := ctx.RequireArg("number")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
env, err := ctx.CallAPI("DELETE", fmt.Sprintf("%s/issues/%s", v1RepoPath(ctx), number), nil)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return ctx.Output(env)
|
||
},
|
||
},
|
||
// +batch-update:通过原生批量接口批量更新多个疑修(按数据库 ID)
|
||
{
|
||
Name: "batch-update",
|
||
Description: "批量更新多个疑修",
|
||
Flags: []common.Flag{
|
||
{Name: "ids", Usage: "Comma-separated issue IDs", Required: true},
|
||
{Name: "state", Short: "s", Usage: "New state: open, closed"},
|
||
{Name: "assignee", Short: "a", Usage: "Assignee login"},
|
||
},
|
||
Run: func(ctx *common.RuntimeContext) error {
|
||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||
return err
|
||
}
|
||
idsStr, err := ctx.RequireArg("ids")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
body := map[string]interface{}{
|
||
"ids": parseIDList(idsStr),
|
||
}
|
||
if s := ctx.Arg("state"); s != "" {
|
||
statusID, err := normalizeIssueStatus(s)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
body["status_id"] = statusID
|
||
}
|
||
if a := ctx.Arg("assignee"); a != "" {
|
||
body["assigned_to_id"] = a
|
||
}
|
||
env, err := ctx.CallAPI("PATCH", v1RepoPath(ctx)+"/issues/batch_update", body)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return ctx.Output(env)
|
||
},
|
||
},
|
||
// +batch-destroy:通过原生批量接口批量删除多个疑修(按数据库 ID)
|
||
{
|
||
Name: "batch-destroy",
|
||
Description: "批量删除多个疑修",
|
||
Flags: []common.Flag{
|
||
{Name: "ids", Usage: "Comma-separated issue IDs", Required: true},
|
||
},
|
||
Run: func(ctx *common.RuntimeContext) error {
|
||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||
return err
|
||
}
|
||
idsStr, err := ctx.RequireArg("ids")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
body := map[string]interface{}{
|
||
"ids": parseIDList(idsStr),
|
||
}
|
||
env, err := ctx.CallAPI("DELETE", v1RepoPath(ctx)+"/issues/batch_destroy", body)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return ctx.Output(env)
|
||
},
|
||
},
|
||
}
|
||
}
|
||
|
||
// normalizeIssueListIDs adds "number" (project_issues_index) and renames
|
||
// "id" to "database_id" so the user-facing output uses the project-level
|
||
// issue number, not the global database primary key.
|
||
func normalizeIssueListIDs(env *output.Envelope) {
|
||
data, ok := env.Data.(map[string]interface{})
|
||
if !ok {
|
||
return
|
||
}
|
||
issues, ok := data["issues"].([]interface{})
|
||
if !ok {
|
||
return
|
||
}
|
||
for i, item := range issues {
|
||
issue, ok := item.(map[string]interface{})
|
||
if !ok {
|
||
continue
|
||
}
|
||
// Copy project_issues_index to top-level "number"
|
||
if num, ok := issue["project_issues_index"]; ok {
|
||
issue["number"] = num
|
||
}
|
||
// Rename "id" (global database PK) to "database_id"
|
||
if id, ok := issue["id"]; ok {
|
||
issue["database_id"] = id
|
||
delete(issue, "id")
|
||
}
|
||
issues[i] = issue
|
||
}
|
||
}
|
||
|
||
func fetchExistingIssue(ctx *common.RuntimeContext, number string) (*existingIssue, error) {
|
||
getEnv, err := ctx.CallAPI("GET", fmt.Sprintf("%s/issues/%s", v1RepoPath(ctx), number), nil)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
issueData, ok := getEnv.Data.(map[string]interface{})
|
||
if !ok {
|
||
return nil, fmt.Errorf("failed to parse issue data")
|
||
}
|
||
subject, _ := issueData["subject"].(string)
|
||
if subject == "" {
|
||
return nil, fmt.Errorf("failed to parse issue subject")
|
||
}
|
||
description, _ := issueData["description"].(string)
|
||
return &existingIssue{
|
||
Subject: subject,
|
||
Description: description,
|
||
}, nil
|
||
}
|
||
|
||
func normalizeIssueStatus(state string) (interface{}, error) {
|
||
switch strings.ToLower(strings.TrimSpace(state)) {
|
||
case "open":
|
||
return 1, nil
|
||
case "closed":
|
||
return 5, nil
|
||
default:
|
||
if id, err := strconv.Atoi(state); err == nil {
|
||
return id, nil
|
||
}
|
||
return nil, fmt.Errorf("invalid --state %q: use open, closed, or a numeric status_id", state)
|
||
}
|
||
}
|
||
|
||
// parseIDList splits a comma-separated string into an int slice.
|
||
func parseIDList(s string) []int {
|
||
parts := strings.Split(s, ",")
|
||
ids := make([]int, 0, len(parts))
|
||
for _, p := range parts {
|
||
p = strings.TrimSpace(p)
|
||
if p == "" {
|
||
continue
|
||
}
|
||
if id, err := strconv.Atoi(p); err == nil {
|
||
ids = append(ids, id)
|
||
}
|
||
}
|
||
return ids
|
||
}
|