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

405 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 逐行讲解 shortcuts/repo/batch_create.go面向 Go 小白)
## 文件概述
这个文件实现了 **仓库批量创建**功能,可以从命令行或 CSV 文件批量创建多个 GitLink 仓库。
---
## 一、包声明和导入
```go
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
```go
type repoCreateInput struct {
Name string
Description string
Private bool
}
```
**作用**:存储创建单个仓库的输入参数
| 字段 | 类型 | 说明 |
|-----|------|------|
| `Name` | string | 仓库名称 |
| `Description` | string | 仓库描述 |
| `Private` | bool | 是否私有仓库 |
### 2.2 repoBatchResult
```go
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
```go
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 | 每个仓库的详细结果 |
---
## 三、命令定义
```go
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 主函数
```go
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 函数
```go
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 函数
```go
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
```go
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. 布尔值判断
```go
dryRun := ctx.Arg("dry-run") == "true"
```
**注意**`ctx.Arg()` 返回的是字符串,需要和字符串 `"true"` 比较,不能直接用 `bool()` 转换
### 3. interface{} 类型断言
```go
userData, _ := userEnv.Data.(map[string]interface{})
login, _ = userData["login"].(string)
```
**作用**:把 `interface{}` 类型转换成具体类型
### 4. float64 转 int
```go
if uid, ok := userData["user_id"].(float64); ok {
userID = int(uid)
}
```
**原因**JSON 解析后,数字默认是 `float64` 类型,需要手动转换成 `int`
### 5. defer 语句
```go
file, err := os.Open(path)
defer file.Close()
```
**作用**:确保文件在函数返回前被关闭,防止资源泄漏
### 6. make 和预分配容量
```go
Results: make([]repoBatchResult, 0, len(inputs))
```
**作用**:创建一个初始长度为 0、容量为 `len(inputs)` 的切片,避免动态扩容的性能开销