forked from Gitlink/gitlink-cli
197 lines
5.0 KiB
Go
197 lines
5.0 KiB
Go
package repo
|
|
|
|
import (
|
|
"encoding/csv"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
|
)
|
|
|
|
type repoCreateInput struct {
|
|
Name string
|
|
Description string
|
|
Private bool
|
|
}
|
|
|
|
type repoBatchResult struct {
|
|
Name string `json:"name" yaml:"name"`
|
|
Status string `json:"status" yaml:"status"`
|
|
Error string `json:"error,omitempty" yaml:"error,omitempty"`
|
|
}
|
|
|
|
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"`
|
|
}
|
|
|
|
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, e.g. repo-a,repo-b"},
|
|
{Name: "from", Usage: "CSV file path"},
|
|
{Name: "description", Short: "d", Usage: "Shared description for all repos (inline mode)"},
|
|
{Name: "private", Usage: "Make repos private", Bool: true, Default: "false"},
|
|
{Name: "dry-run", Usage: "Preview without creating", Bool: true, Default: "false"},
|
|
},
|
|
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; use -n repo-a,repo-b or --from repos.csv")
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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 ""
|
|
}
|