gitlink-cli/shortcuts/org/batch.go

215 lines
5.2 KiB
Go
Raw 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.

package org
import (
"encoding/csv"
"fmt"
"os"
"strings"
"time"
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
)
type batchInviteResult struct {
User string `json:"user" yaml:"user"`
Action string `json:"action" yaml:"action"`
Status string `json:"status" yaml:"status"`
Error string `json:"error,omitempty" yaml:"error,omitempty"`
}
type batchInviteSummary struct {
Org string `json:"org" yaml:"org"`
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"`
Duration string `json:"duration" yaml:"duration"`
Results []batchInviteResult `json:"results" yaml:"results"`
}
func newBatchInviteShortcut() *common.Shortcut {
return &common.Shortcut{
Name: "batch-invite",
Description: "批量邀请成员加入组织,支持逗号分隔列表或 CSV 文件",
Flags: []common.Flag{
{Name: "id", Short: "i", Usage: "组织 ID 或 login", Required: true},
{Name: "users", Short: "u", Usage: "逗号分隔的用户名或 ID例如: alice,bob,charlie"},
{Name: "from", Usage: "从 CSV 文件读取用户名。支持 user/login/user_id 列名或无表头首列"},
{Name: "role", Short: "r", Usage: "成员角色: member 或 admin", Default: "member"},
{Name: "dry-run", Usage: "仅预览将要邀请的成员,不实际执行", Bool: true, Default: "false"},
},
Run: runBatchInvite,
}
}
func runBatchInvite(ctx *common.RuntimeContext) error {
start := time.Now()
orgID, err := ctx.RequireArg("id")
if err != nil {
return err
}
role := ctx.Arg("role")
if role == "" {
role = "member"
}
if role != "member" && role != "admin" {
return fmt.Errorf("无效的角色 %q: 必须为 member 或 admin", role)
}
users, err := collectUsers(ctx.Arg("users"), ctx.Arg("from"))
if err != nil {
return err
}
if len(users) == 0 {
return fmt.Errorf("未提供用户名,请使用 --users alice,bob 或 --from users.csv")
}
dryRun := parseBool(ctx.Arg("dry-run"))
summary := batchInviteSummary{
Org: orgID,
DryRun: dryRun,
Total: len(users),
Results: make([]batchInviteResult, 0, len(users)),
}
for _, user := range users {
result := batchInviteResult{User: user, Action: "invite"}
if dryRun {
result.Status = "planned"
summary.Succeeded++
summary.Results = append(summary.Results, result)
continue
}
body := map[string]interface{}{
"user_id": user,
"role": role,
}
path := fmt.Sprintf("/organizations/%s/organization_users", orgID)
if _, err := ctx.CallAPI("POST", path, body); err != nil {
result.Status = "failed"
result.Error = err.Error()
summary.Failed++
} else {
result.Status = "invited"
summary.Succeeded++
}
summary.Results = append(summary.Results, result)
}
summary.Duration = time.Since(start).String()
if err := ctx.OutputData(summary); err != nil {
return err
}
if summary.Failed > 0 {
return fmt.Errorf("%d / %d 个成员邀请失败", summary.Failed, summary.Total)
}
return nil
}
func collectUsers(usersValue, csvPath string) ([]string, error) {
users, err := parseUserList(usersValue)
if err != nil {
return nil, err
}
if csvPath == "" {
return users, nil
}
csvUsers, err := readUsersFromCSV(csvPath)
if err != nil {
return nil, err
}
return mergeUserLists(users, csvUsers), nil
}
func parseUserList(value string) ([]string, error) {
if strings.TrimSpace(value) == "" {
return nil, nil
}
return normalizeUserIDs(strings.Split(value, ","))
}
func readUsersFromCSV(path string) ([]string, error) {
file, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("读取 CSV 文件失败: %w", err)
}
defer file.Close()
reader := csv.NewReader(file)
reader.TrimLeadingSpace = true
records, err := reader.ReadAll()
if err != nil {
return nil, fmt.Errorf("解析 CSV 文件失败: %w", err)
}
if len(records) == 0 {
return nil, nil
}
userCol := -1
startRow := 0
for i, cell := range records[0] {
switch strings.ToLower(strings.TrimSpace(cell)) {
case "user", "login", "user_id", "username":
userCol = i
startRow = 1
}
}
if userCol == -1 {
userCol = 0
}
values := make([]string, 0, len(records)-startRow)
for _, record := range records[startRow:] {
if userCol >= len(record) {
continue
}
values = append(values, record[userCol])
}
return normalizeUserIDs(values)
}
func normalizeUserIDs(values []string) ([]string, error) {
users := make([]string, 0, len(values))
seen := map[string]bool{}
for _, value := range values {
user := strings.TrimSpace(value)
if user == "" {
continue
}
if seen[user] {
continue
}
seen[user] = true
users = append(users, user)
}
return users, nil
}
func parseBool(value string) bool {
if strings.EqualFold(strings.TrimSpace(value), "true") {
return true
}
return false
}
func mergeUserLists(values ...[]string) []string {
merged := []string{}
seen := map[string]bool{}
for _, users := range values {
for _, u := range users {
if seen[u] {
continue
}
seen[u] = true
merged = append(merged, u)
}
}
return merged
}