gitlink-cli/doc/reading_notes/07_repo_batch_create.md

10 KiB
Raw Blame History

逐行讲解 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:逗号分隔的仓库名称
  • --fromCSV 文件路径
  • --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
}

执行流程

  1. 收集输入(从 --names 和/或 --from
  2. 如果不是 dry-run调用 /users/me 获取当前用户信息
  3. 初始化汇总对象
  4. 遍历每个仓库:
    • 如果是 dry-run标记为 planned
    • 否则构建请求体并调用 API
    • 记录结果
  5. 输出汇总结果

五、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 记录中获取指定列的值

逻辑

  1. 查找列名对应的索引
  2. 检查索引是否有效
  3. 返回该位置的值(去除前后空格)
  4. 如果找不到,返回空字符串

七、完整调用流程

用户命令 (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) 的切片,避免动态扩容的性能开销