forked from Gitlink/gitlink-cli
feat: add batch-update CSV mode
This commit is contained in:
parent
843bf0b505
commit
56f7e428a3
|
|
@ -0,0 +1,146 @@
|
|||
package issue
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
||||
)
|
||||
|
||||
var updateFieldMapping = map[string]string{
|
||||
"title": "subject",
|
||||
"body": "description",
|
||||
"state": "status_id",
|
||||
"assignee": "assigned_to_id",
|
||||
"milestone": "fixed_version_id",
|
||||
"label": "label_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: "100"},
|
||||
{Name: "delay", Usage: "Delay ms", Default: "0"},
|
||||
{Name: "verbose", Short: "v", Usage: "Per-item progress", Bool: true, Default: "false"},
|
||||
},
|
||||
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 missing number column")
|
||||
}
|
||||
|
||||
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 != "" {
|
||||
numbers = append(numbers, n)
|
||||
rowByNumber[n] = row
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dryRun := parseBool(ctx.Arg("dry-run"))
|
||||
maxItems := parseIntArg(ctx, "max", 100)
|
||||
delayMs := parseIntArg(ctx, "delay", 0)
|
||||
confirm := parseBool(ctx.Arg("confirm"))
|
||||
|
||||
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", dryRun, maxItems, delayMs, confirm, 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.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 "assigned_to_id":
|
||||
id, err := resolveAssigneeID(ctx, val)
|
||||
if err != nil {
|
||||
return fmt.Errorf("issue #%s assignee %q: %w", number, val, err)
|
||||
}
|
||||
body["assigned_to_id"] = id
|
||||
case "fixed_version_id":
|
||||
if id, err := strconv.Atoi(val); err == nil {
|
||||
body["fixed_version_id"] = id
|
||||
} else {
|
||||
id, err := ResolveMilestoneID(ctx, val)
|
||||
if err != nil {
|
||||
return fmt.Errorf("issue #%s milestone %q: %w", number, val, err)
|
||||
}
|
||||
body["fixed_version_id"] = id
|
||||
}
|
||||
case "label_ids":
|
||||
labelIDs, err := resolveLabelArgs(ctx, val, "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("issue #%s label %q: %w", number, val, err)
|
||||
}
|
||||
body["label_ids"] = labelIDs
|
||||
case "priority_id":
|
||||
if pid, err := strconv.Atoi(val); err == nil {
|
||||
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
|
||||
}
|
||||
Loading…
Reference in New Issue