forked from Gitlink/gitlink-cli
166 lines
4.1 KiB
Go
166 lines
4.1 KiB
Go
package repo
|
|
|
|
import (
|
|
"encoding/csv"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
|
)
|
|
|
|
func newBatchUpdateShortcut() *common.Shortcut {
|
|
return &common.Shortcut{
|
|
Name: "batch-update",
|
|
Description: "Update settings for multiple repositories",
|
|
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"},
|
|
{Name: "private", Usage: "Set repos to private", Bool: true, Default: "false"},
|
|
{Name: "public", Usage: "Set repos to public", Bool: true, Default: "false"},
|
|
{Name: "dry-run", Usage: "Preview without making changes", Bool: true, Default: "false"},
|
|
},
|
|
Run: runBatchUpdate,
|
|
}
|
|
}
|
|
|
|
func runBatchUpdate(ctx *common.RuntimeContext) error {
|
|
if err := ctx.ResolveOwnerRepo(); err != nil {
|
|
return err
|
|
}
|
|
|
|
var repoNames []string
|
|
if namesStr := ctx.Arg("names"); namesStr != "" {
|
|
for _, name := range strings.Split(namesStr, ",") {
|
|
name = strings.TrimSpace(name)
|
|
if name != "" {
|
|
repoNames = append(repoNames, name)
|
|
}
|
|
}
|
|
}
|
|
if csvPath := ctx.Arg("from"); csvPath != "" {
|
|
csvNames, err := readNamesFromCSV(csvPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
repoNames = append(repoNames, csvNames...)
|
|
}
|
|
if len(repoNames) == 0 {
|
|
return fmt.Errorf("no repository names provided; use -n repo-a,repo-b or --from repos.csv")
|
|
}
|
|
|
|
setPrivate := ctx.Arg("private") == "true"
|
|
setPublic := ctx.Arg("public") == "true"
|
|
if setPrivate && setPublic {
|
|
return fmt.Errorf("cannot use both --private and --public")
|
|
}
|
|
changeVisibility := setPrivate || setPublic
|
|
|
|
dryRun := ctx.Arg("dry-run") == "true"
|
|
desc := ctx.Arg("description")
|
|
|
|
summary := repoBatchSummary{
|
|
Owner: ctx.Owner,
|
|
Action: "update",
|
|
DryRun: dryRun,
|
|
Total: len(repoNames),
|
|
Results: make([]repoBatchResult, 0, len(repoNames)),
|
|
}
|
|
|
|
for _, name := range repoNames {
|
|
result := repoBatchResult{Name: name}
|
|
if dryRun {
|
|
result.Status = "planned"
|
|
summary.Succeeded++
|
|
summary.Results = append(summary.Results, result)
|
|
continue
|
|
}
|
|
|
|
// Fetch current repo info to get required fields for PATCH
|
|
current, err := ctx.CallAPI("GET", fmt.Sprintf("/%s/%s", ctx.Owner, name), nil)
|
|
if err != nil {
|
|
result.Status = "failed"
|
|
result.Error = fmt.Sprintf("fetch repo: %v", err)
|
|
summary.Failed++
|
|
summary.Results = append(summary.Results, result)
|
|
continue
|
|
}
|
|
|
|
curData, _ := current.Data.(map[string]interface{})
|
|
curName, _ := curData["name"].(string)
|
|
identifier, _ := curData["identifier"].(string)
|
|
|
|
body := map[string]interface{}{
|
|
"name": curName,
|
|
"identifier": identifier,
|
|
}
|
|
if desc != "" {
|
|
body["description"] = desc
|
|
}
|
|
if changeVisibility {
|
|
body["private"] = setPrivate
|
|
}
|
|
|
|
if _, err := ctx.CallAPI("PATCH", fmt.Sprintf("/%s/%s", ctx.Owner, name), body); err != nil {
|
|
result.Status = "failed"
|
|
result.Error = err.Error()
|
|
summary.Failed++
|
|
} else {
|
|
result.Status = "updated"
|
|
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 update", summary.Failed, summary.Total)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func readNamesFromCSV(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)
|
|
}
|
|
if len(records) < 2 {
|
|
return nil, fmt.Errorf("CSV must have a header row and at least one data row")
|
|
}
|
|
|
|
header := records[0]
|
|
nameCol := -1
|
|
for i, h := range header {
|
|
if strings.ToLower(strings.TrimSpace(h)) == "name" {
|
|
nameCol = i
|
|
break
|
|
}
|
|
}
|
|
if nameCol == -1 {
|
|
nameCol = 0
|
|
}
|
|
|
|
var names []string
|
|
for _, record := range records[1:] {
|
|
if nameCol >= len(record) {
|
|
continue
|
|
}
|
|
name := strings.TrimSpace(record[nameCol])
|
|
if name != "" {
|
|
names = append(names, name)
|
|
}
|
|
}
|
|
return names, nil
|
|
}
|