forked from Gitlink/gitlink-cli
119 lines
3.7 KiB
Go
119 lines
3.7 KiB
Go
package issue
|
||
|
||
import (
|
||
"fmt"
|
||
"os"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
||
)
|
||
|
||
func newBatchAssignShortcut() *common.Shortcut {
|
||
return &common.Shortcut{
|
||
Name: "batch-assign",
|
||
Description: "Assign issues to a user or via CSV",
|
||
Flags: []common.Flag{
|
||
{Name: "numbers", Short: "n", Usage: "Comma-separated issue numbers"},
|
||
{Name: "from", Usage: "CSV file with number,assignee columns"},
|
||
{Name: "search", Usage: "Search keyword"},
|
||
{Name: "state", Usage: "State filter for --search"},
|
||
{Name: "assignee", Short: "a", Usage: "Assignee username or ID (uniform mode)"},
|
||
{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: runBatchAssign,
|
||
}
|
||
}
|
||
|
||
func runBatchAssign(ctx *common.RuntimeContext) error {
|
||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||
return err
|
||
}
|
||
|
||
csvPath := ctx.Arg("from")
|
||
opts := parseBatchOptions(ctx)
|
||
|
||
if csvPath != "" {
|
||
headers, rows, err := ReadCSV(csvPath)
|
||
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)")
|
||
}
|
||
assigneeCol := FindColumn(headers, "assignee", "assignee_id", "assigned_to_id")
|
||
if assigneeCol == -1 {
|
||
return fmt.Errorf("CSV 缺少经办人列(assignee/assignee_id/assigned_to_id)")
|
||
}
|
||
|
||
// 暂存原始经办人字符串,resolve 推迟到逐条 callback 内执行,
|
||
// 这样 --dry-run 不会触发任何 GET /users/search。
|
||
assigneeMap := make(map[string]string, len(rows))
|
||
numbers := make([]string, 0, len(rows))
|
||
numberSeen := make(map[string]bool)
|
||
for _, row := range rows {
|
||
if numberCol >= len(row) || assigneeCol >= len(row) {
|
||
continue
|
||
}
|
||
num := strings.TrimSpace(row[numberCol])
|
||
arg := strings.TrimSpace(row[assigneeCol])
|
||
if num == "" || arg == "" {
|
||
continue
|
||
}
|
||
if _, exists := assigneeMap[num]; exists {
|
||
fmt.Fprintf(os.Stderr, "警告:issue #%s 在 CSV 中出现多次,仅使用最后一次的经办人\n", num)
|
||
}
|
||
assigneeMap[num] = arg
|
||
if !numberSeen[num] {
|
||
numbers = append(numbers, num)
|
||
numberSeen[num] = true
|
||
}
|
||
}
|
||
if len(assigneeMap) == 0 {
|
||
return fmt.Errorf("no valid entries in CSV")
|
||
}
|
||
|
||
assignFn := func(c *common.RuntimeContext, number string) error {
|
||
arg := assigneeMap[number]
|
||
aid, err := ResolveUserID(c, arg)
|
||
if err != nil {
|
||
return fmt.Errorf("assignee %q: %w", arg, err)
|
||
}
|
||
return assignIssue(c, number, aid)
|
||
}
|
||
_, err = RunBatch(ctx, numbers, "assign", opts, assignFn)
|
||
return err
|
||
}
|
||
|
||
numbers, err := ResolveIssueNumbers(ctx, ctx.Arg("numbers"), "", ctx.Arg("search"))
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
assigneeArg := ctx.Arg("assignee")
|
||
if assigneeArg == "" {
|
||
return fmt.Errorf("--assignee is required in uniform mode")
|
||
}
|
||
|
||
// 把 ResolveUserID 推迟到逐条 callback,使 --dry-run 不会调用 GET /users/search;
|
||
// 解析失败改为按条记录在 BatchResult.Error 中。
|
||
assigneeFn := func(c *common.RuntimeContext, number string) error {
|
||
aid, err := ResolveUserID(c, assigneeArg)
|
||
if err != nil {
|
||
return fmt.Errorf("assignee %q: %w", assigneeArg, err)
|
||
}
|
||
return assignIssue(c, number, aid)
|
||
}
|
||
_, err = RunBatch(ctx, numbers, "assign", opts, assigneeFn)
|
||
return err
|
||
}
|
||
|
||
func assignIssue(ctx *common.RuntimeContext, number string, assigneeID int) error {
|
||
return patchIssue(ctx, number, map[string]interface{}{"assigner_ids": []int{assigneeID}}, "assign")
|
||
}
|
||
|