13 KiB
逐行讲解 shortcuts/issue/batch.go(面向 Go 小白)
文件概述
这个文件实现了 Issue 批量操作功能,可以对多个 Issue 进行批量关闭、修改状态、修改优先级、分配人和修改标签等操作。
一、包声明和导入
package issue
import (
"encoding/csv"
"fmt"
"os"
"strconv"
"strings"
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
)
| 导入库 | 作用 |
|---|---|
encoding/csv |
CSV 文件解析,用于从文件读取 Issue 编号 |
fmt |
格式化输出 |
os |
文件操作,用于打开 CSV 文件 |
strconv |
字符串和数字之间的转换 |
strings |
字符串处理 |
common |
公共工具包 |
二、常量定义
const (
priorityLow = 1
priorityNormal = 2
priorityHigh = 3
priorityUrgent = 4
)
优先级常量:定义了 Issue 优先级对应的数字 ID
const (
statusNew = 1
statusInProgress = 2
statusResolved = 3
statusClosed = 5
statusRejected = 6
)
状态常量:定义了 Issue 状态对应的数字 ID
const (
trackerBug = 1
trackerFeature = 2
trackerSupport = 3
trackerDoc = 4
trackerTest = 5
trackerDuplicate = 6
trackerQuestion = 7
)
类型常量:定义了 Issue 类型对应的数字 ID
三、名称映射表
var priorityNames = map[int]string{
priorityLow: "low",
priorityNormal: "normal",
priorityHigh: "high",
priorityUrgent: "urgent",
}
var statusNames = map[int]string{
statusNew: "new",
statusInProgress: "in-progress",
statusResolved: "resolved",
statusClosed: "closed",
statusRejected: "rejected",
}
var trackerNames = map[int]string{
trackerBug: "bug",
trackerFeature: "feature",
// ...
}
作用:把数字 ID 转换成可读的英文名称,方便输出结果
四、标签 ID 映射
var tagIDs = map[string]int{
"缺陷": 315526,
"功能": 315527,
"文档": 315533,
"重复": 315525,
"疑问": 315528,
"支持": 315529,
"任务": 315530,
"测试": 315534,
"协助": 315531,
"搁置": 315532,
}
作用:中文标签名称到 GitLink 标签 ID 的映射
注意:这些 ID 是从网页端 DevTools 抓包获取的,不同项目可能不同
五、结果结构体
type BatchResult struct {
Number string `json:"number" yaml:"number"`
Action string `json:"action" yaml:"action"`
Status string `json:"status" yaml:"status"`
Error string `json:"error,omitempty" yaml:"error,omitempty"`
}
单个操作结果:记录单个 Issue 的操作结果
type BatchSummary struct {
Repository string `json:"repository" yaml:"repository"`
Action string `json:"action" yaml:"action"`
Value string `json:"value,omitempty" yaml:"value,omitempty"`
DryRun bool `json:"dry_run" yaml:"dry_run"`
Total int `json:"total" yaml:"total"`
Succeeded int `json:"succeeded" yaml:"succeeded"`
Failed int `json:"failed" yaml:"failed"`
Results []BatchResult `json:"results" yaml:"results"`
}
批量操作汇总:记录整个批量操作的统计信息
六、批量操作命令
6.1 batch-close 命令
func newBatchCloseShortcut() *common.Shortcut {
return &common.Shortcut{
Name: "batch-close",
Description: "Close multiple issues by issue numbers or a CSV file",
Flags: []common.Flag{
{Name: "numbers", Short: "n", Usage: "Comma-separated issue numbers"},
{Name: "from", Usage: "Read issue numbers from a CSV file"},
{Name: "dry-run", Usage: "Preview without making changes", Bool: true, Default: "false"},
},
Run: runBatchClose,
}
}
执行函数:
func runBatchClose(ctx *common.RuntimeContext) error {
if err := ctx.ResolveOwnerRepo(); err != nil {
return err
}
numbers, err := collectIssueNumbers(ctx.Arg("numbers"), ctx.Arg("from"))
if err != nil {
return err
}
if len(numbers) == 0 {
return fmt.Errorf("no issue numbers provided")
}
dryRun := parseBool(ctx.Arg("dry-run"))
summary := BatchSummary{
Repository: fmt.Sprintf("%s/%s", ctx.Owner, ctx.Repo),
Action: "close",
DryRun: dryRun,
Total: len(numbers),
Results: make([]BatchResult, 0, len(numbers)),
}
for _, number := range numbers {
result := BatchResult{Number: number, Action: "close"}
if dryRun {
result.Status = "planned"
summary.Succeeded++
summary.Results = append(summary.Results, result)
continue
}
if err := updateIssueField(ctx, number, map[string]interface{}{"status_id": statusClosed}); err != nil {
result.Status = "failed"
result.Error = err.Error()
summary.Failed++
} else {
result.Status = "closed"
summary.Succeeded++
}
summary.Results = append(summary.Results, result)
}
if err := ctx.OutputData(summary); err != nil {
return err
}
if summary.Failed > 0 {
return fmt.Errorf("%d of %d issue(s) failed", summary.Failed, summary.Total)
}
return nil
}
执行流程:
- 解析仓库信息
- 收集 Issue 编号(从
--numbers参数或 CSV 文件) - 初始化
BatchSummary汇总对象 - 遍历每个 Issue 编号:
- 如果是 dry-run,直接标记为 planned
- 否则调用
updateIssueField更新状态为 closed
- 输出汇总结果
6.2 batch-status 命令
func runBatchStatus(ctx *common.RuntimeContext) error {
// ... 解析参数
state := ctx.Arg("state")
statusID, err := parseStatus(state)
if err != nil {
return err
}
// ... 遍历更新
updateIssueField(ctx, number, map[string]interface{}{"status_id": statusID})
}
功能:批量修改 Issue 状态
参数:--state 指定目标状态(new/in-progress/resolved/closed/rejected)
6.3 batch-priority 命令
func runBatchPriority(ctx *common.RuntimeContext) error {
// ...
priority := ctx.Arg("priority")
priorityID, err := parsePriority(priority)
// ...
updateIssueField(ctx, number, map[string]interface{}{"priority_id": priorityID})
}
功能:批量修改 Issue 优先级
参数:--priority 指定目标优先级(low/normal/high/urgent)
6.4 batch-assign 命令
func runBatchAssign(ctx *common.RuntimeContext) error {
// ...
assignee := ctx.Arg("assignee")
var assigneeID interface{}
if !dryRun {
id, err := resolveUserID(ctx, assignee)
assigneeID = id
}
// ...
updateIssueField(ctx, number, map[string]interface{}{"assigned_to_id": assigneeID})
}
功能:批量分配 Issue 给指定用户
亮点:需要先把用户名转换成用户 ID
6.5 batch-label 命令
func runBatchLabel(ctx *common.RuntimeContext) error {
// ...
label := ctx.Arg("label")
trackerID, err := parseTracker(label)
// ...
updateIssueField(ctx, number, map[string]interface{}{"issue_tag_ids": []int{trackerID}})
}
功能:批量修改 Issue 的标签
参数:--label 可以是英文(bug/feature)或中文(缺陷/功能)
七、核心辅助函数
7.1 updateIssueField
func updateIssueField(ctx *common.RuntimeContext, number string, fields map[string]interface{}) error {
current, err := fetchExistingIssue(ctx, number)
if err != nil {
return fmt.Errorf("fetch issue #%s: %w", number, err)
}
body := map[string]interface{}{
"subject": current.Subject,
"description": current.Description,
}
for k, v := range fields {
body[k] = v
}
if _, err := ctx.CallAPI("PATCH", fmt.Sprintf("%s/issues/%s", v1RepoPath(ctx), number), body); err != nil {
return fmt.Errorf("update issue #%s: %w", number, err)
}
return nil
}
功能:更新 Issue 的指定字段
关键点:
- 先调用
fetchExistingIssue获取当前 Issue 的标题和描述 - 必须在请求体中包含
subject和description,否则会被清空 - 把要更新的字段合并到 body 中
- 发送 PATCH 请求
7.2 resolveUserID
func resolveUserID(ctx *common.RuntimeContext, login string) (interface{}, error) {
if id, err := strconv.Atoi(login); err == nil {
return id, nil
}
env, err := ctx.CallAPI("GET", fmt.Sprintf("/users/%s", login), nil)
if err != nil {
return nil, fmt.Errorf("lookup user %q: %w", login, err)
}
data, ok := env.Data.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("unexpected response for user %q", login)
}
idFloat, ok := data["id"].(float64)
if ok {
return int(idFloat), nil
}
userIDFloat, ok := data["user_id"].(float64)
if ok {
return int(userIDFloat), nil
}
return nil, fmt.Errorf("cannot determine user ID for %q", login)
}
功能:把用户名转换成用户 ID
工作原理:
- 如果输入已经是数字,直接返回
- 否则调用
/users/{login}API 获取用户信息 - 从响应中提取
id或user_id字段 - API 返回的数字是 float64 类型,需要转换成 int
7.3 collectIssueNumbers
func collectIssueNumbers(numbersValue, csvPath string) ([]string, error) {
numbers, err := parseIssueNumbers(numbersValue)
if err != nil {
return nil, err
}
if csvPath == "" {
return numbers, nil
}
csvNumbers, err := readIssueNumbersFromCSV(csvPath)
if err != nil {
return nil, err
}
return mergeIssueNumbers(numbers, csvNumbers), nil
}
功能:从 --numbers 参数和 CSV 文件中收集 Issue 编号
7.4 readIssueNumbersFromCSV
func readIssueNumbersFromCSV(path string) ([]string, error) {
file, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("read CSV: %w", err)
}
defer file.Close()
reader := csv.NewReader(file)
reader.TrimLeadingSpace = true
records, err := reader.ReadAll()
if err != nil {
return nil, fmt.Errorf("parse CSV: %w", err)
}
numberColumn := -1
startRow := 0
for i, cell := range records[0] {
switch strings.ToLower(strings.TrimSpace(cell)) {
case "number", "issue_number", "project_issues_index":
numberColumn = i
startRow = 1
}
}
if numberColumn == -1 {
numberColumn = 0
}
values := make([]string, 0, len(records)-startRow)
for _, record := range records[startRow:] {
if numberColumn >= len(record) {
continue
}
values = append(values, record[numberColumn])
}
return normalizeIssueNumbers(values)
}
功能:从 CSV 文件读取 Issue 编号
智能表头识别:
- 自动识别
number、issue_number、project_issues_index列 - 如果没有匹配的表头,默认使用第一列
- 跳过表头行,从第二行开始读取
八、类型转换函数
func parseStatus(state string) (int, error) {
switch strings.ToLower(strings.TrimSpace(state)) {
case "new":
return statusNew, nil
case "in-progress", "in_progress", "inprogress":
return statusInProgress, nil
// ...
default:
if id, err := strconv.Atoi(state); err == nil {
return id, nil
}
return 0, fmt.Errorf("invalid state %q", state)
}
}
功能:把用户输入的状态字符串转换成数字 ID
容错处理:
- 支持多种写法:
in-progress、in_progress、inprogress - 如果输入是数字,直接返回
parsePriority 和 parseTracker 函数类似
九、Go 语言知识点
1. const 常量定义
const (
priorityLow = 1
priorityNormal = 2
)
在 const 块中,后续常量会继承前一个常量的值并自动加1
2. defer 语句
file, err := os.Open(path)
defer file.Close()
defer 会在函数返回前执行,确保文件被关闭
3. map 遍历
for k, v := range fields {
body[k] = v
}
遍历 map 的键值对
4. type assertion(类型断言)
data, ok := env.Data.(map[string]interface{})
if !ok {
return nil, fmt.Errorf("unexpected response")
}
把接口类型转换成具体类型,ok 表示转换是否成功
5. strconv.Atoi
id, err := strconv.Atoi(login)
把字符串转换成整数,如果失败返回错误