forked from Gitlink/gitlink-cli
10 KiB
10 KiB
逐行讲解 shortcuts/repo/batch_create.go(面向 Go 小白)
文件概述
这个文件实现了 仓库批量创建功能,可以从命令行或 CSV 文件批量创建多个 GitLink 仓库。
一、包声明和导入
package repo
import (
"encoding/csv"
"fmt"
"os"
"strings"
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
)
| 导入库 | 作用 |
|---|---|
encoding/csv |
CSV 文件解析 |
fmt |
格式化输出 |
os |
文件操作 |
strings |
字符串处理 |
common |
公共工具包 |
二、结构体定义
2.1 repoCreateInput
type repoCreateInput struct {
Name string
Description string
Private bool
}
作用:存储创建单个仓库的输入参数
| 字段 | 类型 | 说明 |
|---|---|---|
Name |
string | 仓库名称 |
Description |
string | 仓库描述 |
Private |
bool | 是否私有仓库 |
2.2 repoBatchResult
type repoBatchResult struct {
Name string `json:"name" yaml:"name"`
Status string `json:"status" yaml:"status"`
Error string `json:"error,omitempty" yaml:"error,omitempty"`
}
作用:存储单个仓库创建的结果
| 字段 | 类型 | 说明 |
|---|---|---|
Name |
string | 仓库名称 |
Status |
string | 创建状态(planned/created/failed) |
Error |
string | 错误信息(如果失败) |
2.3 repoBatchSummary
type repoBatchSummary struct {
Owner string `json:"owner" yaml:"owner"`
Action string `json:"action" yaml:"action"`
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 []repoBatchResult `json:"results" yaml:"results"`
}
作用:存储批量创建的汇总结果
| 字段 | 类型 | 说明 |
|---|---|---|
Owner |
string | 仓库所有者(用户名) |
Action |
string | 操作类型(create) |
DryRun |
bool | 是否是预览模式 |
Total |
int | 总数量 |
Succeeded |
int | 成功数量 |
Failed |
int | 失败数量 |
Results |
[]repoBatchResult | 每个仓库的详细结果 |
三、命令定义
func newBatchCreateShortcut() *common.Shortcut {
return &common.Shortcut{
Name: "batch-create",
Description: "Create multiple repositories from CLI flags or a CSV file",
Flags: []common.Flag{
{Name: "names", Short: "n", Usage: "Comma-separated repository names"},
{Name: "from", Usage: "CSV file path"},
{Name: "description", Short: "d", Usage: "Shared description for all repos"},
{Name: "private", Usage: "Make repos private", Bool: true, Default: "false"},
{Name: "dry-run", Usage: "Preview without creating", Bool: true, Default: "false"},
},
Run: runBatchCreate,
}
}
Flags 参数说明:
--names/-n:逗号分隔的仓库名称--from:CSV 文件路径--description/-d:所有仓库共享的描述--private:创建私有仓库--dry-run:预览模式
四、runBatchCreate 主函数
func runBatchCreate(ctx *common.RuntimeContext) error {
var inputs []repoCreateInput
if namesStr := ctx.Arg("names"); namesStr != "" {
for _, name := range strings.Split(namesStr, ",") {
name = strings.TrimSpace(name)
if name == "" {
continue
}
inputs = append(inputs, repoCreateInput{
Name: name,
Description: ctx.Arg("description"),
Private: ctx.Arg("private") == "true",
})
}
}
if csvPath := ctx.Arg("from"); csvPath != "" {
csvInputs, err := readRepoInputsFromCSV(csvPath)
if err != nil {
return err
}
inputs = append(inputs, csvInputs...)
}
if len(inputs) == 0 {
return fmt.Errorf("no repository names provided")
}
dryRun := ctx.Arg("dry-run") == "true"
var login string
var userID int
if !dryRun {
userEnv, err := ctx.CallAPI("GET", "/users/me", nil)
if err != nil {
return fmt.Errorf("failed to get current user: %w", err)
}
userData, _ := userEnv.Data.(map[string]interface{})
login, _ = userData["login"].(string)
if login == "" {
return fmt.Errorf("cannot determine current user login")
}
if uid, ok := userData["user_id"].(float64); ok {
userID = int(uid)
}
}
summary := repoBatchSummary{
Owner: login,
Action: "create",
DryRun: dryRun,
Total: len(inputs),
Results: make([]repoBatchResult, 0, len(inputs)),
}
for _, input := range inputs {
result := repoBatchResult{Name: input.Name}
if dryRun {
result.Status = "planned"
summary.Succeeded++
summary.Results = append(summary.Results, result)
continue
}
body := map[string]interface{}{
"name": input.Name,
"repository_name": input.Name,
"user_id": userID,
}
if input.Description != "" {
body["description"] = input.Description
}
if input.Private {
body["private"] = true
}
if _, err := ctx.CallAPI("POST", fmt.Sprintf("/%s/%s", login, input.Name), body); err != nil {
result.Status = "failed"
result.Error = err.Error()
summary.Failed++
} else {
result.Status = "created"
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 repo(s) failed to create", summary.Failed, summary.Total)
}
return nil
}
执行流程:
- 收集输入(从
--names和/或--from) - 如果不是 dry-run,调用
/users/me获取当前用户信息 - 初始化汇总对象
- 遍历每个仓库:
- 如果是 dry-run,标记为 planned
- 否则构建请求体并调用 API
- 记录结果
- 输出汇总结果
五、readRepoInputsFromCSV 函数
func readRepoInputsFromCSV(path string) ([]repoCreateInput, 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)
}
if len(records) < 2 {
return nil, fmt.Errorf("CSV must have a header row and at least one data row")
}
header := records[0]
col := make(map[string]int)
for i, h := range header {
col[strings.ToLower(strings.TrimSpace(h))] = i
}
if _, ok := col["name"]; !ok {
return nil, fmt.Errorf("CSV must have a 'name' column")
}
var inputs []repoCreateInput
for _, record := range records[1:] {
name := getCol(record, col, "name")
if name == "" {
continue
}
private := false
if p := strings.ToLower(getCol(record, col, "private")); p == "true" || p == "1" {
private = true
}
inputs = append(inputs, repoCreateInput{
Name: name,
Description: getCol(record, col, "description"),
Private: private,
})
}
return inputs, nil
}
CSV 列支持:
name(必填):仓库名称description:仓库描述private:是否私有(true/false 或 1/0)
六、getCol 函数
func getCol(record []string, col map[string]int, name string) string {
if idx, ok := col[name]; ok && idx < len(record) {
return strings.TrimSpace(record[idx])
}
return ""
}
功能:从 CSV 记录中获取指定列的值
逻辑:
- 查找列名对应的索引
- 检查索引是否有效
- 返回该位置的值(去除前后空格)
- 如果找不到,返回空字符串
七、完整调用流程
用户命令 (gitlink repo batch-create -n repo-a,repo-b)
↓
解析命令行参数
↓
newBatchCreateShortcut() 返回命令定义
↓
执行 runBatchCreate 函数
↓
收集输入(解析 --names 参数)
↓
调用 /users/me 获取当前用户信息
↓
遍历每个仓库名称:
↓
构建请求体(name, repository_name, user_id)
↓
调用 POST /{login}/{repo_name} 创建仓库
↓
记录创建结果
↓
输出汇总结果
八、Go 语言知识点
1. 结构体标签(Struct Tags)
type repoBatchResult struct {
Name string `json:"name" yaml:"name"`
Status string `json:"status" yaml:"status"`
Error string `json:"error,omitempty" yaml:"error,omitempty"`
}
作用:告诉序列化库(如 JSON、YAML)如何给字段命名
json:"name":JSON 序列化时使用name作为字段名json:"error,omitempty":如果Error为空,JSON 中不包含这个字段
2. 布尔值判断
dryRun := ctx.Arg("dry-run") == "true"
注意:ctx.Arg() 返回的是字符串,需要和字符串 "true" 比较,不能直接用 bool() 转换
3. interface{} 类型断言
userData, _ := userEnv.Data.(map[string]interface{})
login, _ = userData["login"].(string)
作用:把 interface{} 类型转换成具体类型
4. float64 转 int
if uid, ok := userData["user_id"].(float64); ok {
userID = int(uid)
}
原因:JSON 解析后,数字默认是 float64 类型,需要手动转换成 int
5. defer 语句
file, err := os.Open(path)
defer file.Close()
作用:确保文件在函数返回前被关闭,防止资源泄漏
6. make 和预分配容量
Results: make([]repoBatchResult, 0, len(inputs))
作用:创建一个初始长度为 0、容量为 len(inputs) 的切片,避免动态扩容的性能开销