forked from Gitlink/gitlink-cli
150 lines
4.0 KiB
Go
150 lines
4.0 KiB
Go
package issue
|
||
|
||
import (
|
||
"fmt"
|
||
"os"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
||
)
|
||
|
||
var updateFieldMapping = map[string]string{
|
||
"title": "subject",
|
||
"body": "description",
|
||
"state": "status_id",
|
||
"assignee": "assigner_ids",
|
||
"milestone": "milestone_id",
|
||
"label": "issue_tag_ids",
|
||
"priority": "priority_id",
|
||
}
|
||
|
||
func newBatchUpdateShortcut() *common.Shortcut {
|
||
return &common.Shortcut{
|
||
Name: "batch-update",
|
||
Description: "Update multiple issues via CSV",
|
||
Flags: []common.Flag{
|
||
{Name: "from", Usage: "CSV file path with updates", Required: true},
|
||
{Name: "dry-run", Usage: "Preview", Bool: true, Default: "false"},
|
||
{Name: "confirm", Usage: "Confirm", Bool: true, Default: "false"},
|
||
{Name: "max", Usage: "Max items", Default: strconv.Itoa(defaultBatchMaxItems)},
|
||
{Name: "delay", Usage: "Delay ms", Default: strconv.Itoa(defaultBatchDelayMs)},
|
||
},
|
||
Run: runBatchUpdate,
|
||
}
|
||
}
|
||
|
||
func runBatchUpdate(ctx *common.RuntimeContext) error {
|
||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||
return err
|
||
}
|
||
|
||
headers, rows, err := ReadCSV(ctx.Arg("from"))
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
numberCol := FindColumn(headers, "number", "issue_number", "project_issues_index")
|
||
if numberCol == -1 {
|
||
return fmt.Errorf("CSV 缺少编号列(number/issue_number/project_issues_index)")
|
||
}
|
||
|
||
numbers := make([]string, 0, len(rows))
|
||
rowByNumber := make(map[string][]string)
|
||
for _, row := range rows {
|
||
if numberCol < len(row) {
|
||
n := strings.TrimSpace(row[numberCol])
|
||
if n != "" {
|
||
if _, exists := rowByNumber[n]; !exists {
|
||
numbers = append(numbers, n)
|
||
} else {
|
||
fmt.Fprintf(os.Stderr, "警告:issue #%s 在 CSV 中出现多次,仅使用最后一次的数据\n", n)
|
||
}
|
||
rowByNumber[n] = row
|
||
}
|
||
}
|
||
}
|
||
|
||
opts := parseBatchOptions(ctx)
|
||
|
||
updateFn := func(c *common.RuntimeContext, number string) error {
|
||
row, ok := rowByNumber[number]
|
||
if !ok {
|
||
return fmt.Errorf("no CSV data for issue #%s", number)
|
||
}
|
||
return applyIssueUpdates(c, number, row, headers)
|
||
}
|
||
_, err = RunBatch(ctx, numbers, "update", opts, updateFn)
|
||
return err
|
||
}
|
||
|
||
func applyIssueUpdates(ctx *common.RuntimeContext, number string, row []string, headers []string) error {
|
||
current, err := fetchIssueData(ctx, number)
|
||
if err != nil {
|
||
return fmt.Errorf("fetch issue: %w", err)
|
||
}
|
||
|
||
body := map[string]interface{}{
|
||
"subject": current.Subject,
|
||
"description": current.Description,
|
||
}
|
||
|
||
for i, colName := range headers {
|
||
colName = strings.ToLower(strings.TrimSpace(colName))
|
||
apiField, ok := updateFieldMapping[colName]
|
||
if !ok || i >= len(row) {
|
||
continue
|
||
}
|
||
val := strings.TrimSpace(row[i])
|
||
if val == "" {
|
||
continue
|
||
}
|
||
|
||
switch apiField {
|
||
case "subject":
|
||
body["subject"] = val
|
||
case "description":
|
||
body["description"] = val
|
||
case "status_id":
|
||
sid, err := normalizeIssueStatus(val)
|
||
if err != nil {
|
||
return fmt.Errorf("issue #%s state %q: %w", number, val, err)
|
||
}
|
||
body["status_id"] = sid
|
||
case "assigner_ids":
|
||
id, err := ResolveUserID(ctx, val)
|
||
if err != nil {
|
||
return fmt.Errorf("issue #%s assignee %q: %w", number, val, err)
|
||
}
|
||
body["assigner_ids"] = []int{id}
|
||
case "milestone_id":
|
||
if id, err := strconv.Atoi(val); err == nil {
|
||
body["milestone_id"] = id
|
||
} else {
|
||
id, err := ResolveMilestoneID(ctx, val)
|
||
if err != nil {
|
||
return fmt.Errorf("issue #%s milestone %q: %w", number, val, err)
|
||
}
|
||
body["milestone_id"] = id
|
||
}
|
||
case "issue_tag_ids":
|
||
labelIDs, err := resolveLabelArgs(ctx, val, "")
|
||
if err != nil {
|
||
return fmt.Errorf("issue #%s label %q: %w", number, val, err)
|
||
}
|
||
body["issue_tag_ids"] = labelIDs
|
||
case "priority_id":
|
||
pid, err := strconv.Atoi(val)
|
||
if err != nil {
|
||
return fmt.Errorf("issue #%s priority %q: must be a numeric priority_id", number, val)
|
||
}
|
||
body["priority_id"] = pid
|
||
}
|
||
}
|
||
_, err = ctx.CallAPI("PATCH", fmt.Sprintf("%s/issues/%s", v1RepoPath(ctx), number), body)
|
||
if err != nil {
|
||
return fmt.Errorf("update issue: %w", err)
|
||
}
|
||
return nil
|
||
}
|