gitlink-cli/shortcuts/org/batch.go

148 lines
3.8 KiB
Go

package org
import (
"encoding/csv"
"fmt"
"os"
"strconv"
"strings"
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
)
// BatchShortcuts 返回组织级批量成员管理 Shortcut
func BatchShortcuts() []*common.Shortcut {
return []*common.Shortcut{
{
Name: "batch-invite",
Description: "Batch invite members to a project (from --users list or --from CSV file)",
Flags: []common.Flag{
{Name: "users", Usage: "Comma-separated list of user IDs to invite"},
{Name: "from", Usage: "CSV file with user IDs (column: user_id)"},
{Name: "owner", Short: "o", Usage: "Project owner (e.g., zzx-coder)", Required: true},
{Name: "repo", Short: "r", Usage: "Project repo name (e.g., gitlink-cli)", Required: true},
{Name: "dry-run", Usage: "Preview operations without executing", Bool: true},
},
Run: runOrgBatchInvite,
},
{
Name: "batch-remove",
Description: "Batch remove members from a project (from --users list or --from CSV file)",
Flags: []common.Flag{
{Name: "users", Usage: "Comma-separated list of user IDs to remove"},
{Name: "from", Usage: "CSV file with user IDs (column: user_id)"},
{Name: "owner", Short: "o", Usage: "Project owner (e.g., zzx-coder)", Required: true},
{Name: "repo", Short: "r", Usage: "Project repo name (e.g., gitlink-cli)", Required: true},
{Name: "dry-run", Usage: "Preview operations without executing", Bool: true},
},
Run: runOrgBatchRemove,
},
}
}
// runOrgBatchInvite 组织级批量邀请
func runOrgBatchInvite(ctx *common.RuntimeContext) error {
userIDs, err := parseBatchUserIDs(ctx)
if err != nil {
return err
}
dryRun := ctx.Arg("dry-run") == "true"
return inviteToOrgProjects(ctx, userIDs, dryRun)
}
// runOrgBatchRemove 组织级批量移除
func runOrgBatchRemove(ctx *common.RuntimeContext) error {
userIDs, err := parseBatchUserIDs(ctx)
if err != nil {
return err
}
dryRun := ctx.Arg("dry-run") == "true"
return removeFromOrgProjects(ctx, userIDs, dryRun)
}
// parseBatchUserIDs 从 --users 或 --from CSV 解析用户 ID 列表
func parseBatchUserIDs(ctx *common.RuntimeContext) ([]int, error) {
usersStr := ctx.Arg("users")
csvFile := ctx.Arg("from")
if usersStr == "" && csvFile == "" {
return nil, fmt.Errorf("must specify --users (comma-separated user IDs) or --from (CSV file path)")
}
var userIDs []int
if usersStr != "" {
ids, err := parseUserIDList(usersStr)
if err != nil {
return nil, err
}
userIDs = append(userIDs, ids...)
}
if csvFile != "" {
ids, err := parseCSVUserIDs(csvFile)
if err != nil {
return nil, err
}
userIDs = append(userIDs, ids...)
}
if len(userIDs) == 0 {
return nil, fmt.Errorf("no valid user IDs found")
}
return userIDs, nil
}
// parseCSVUserIDs 从 CSV 文件读取 user_id 列
func parseCSVUserIDs(filePath string) ([]int, error) {
f, err := os.Open(filePath)
if err != nil {
return nil, fmt.Errorf("failed to open CSV file %s: %w", filePath, err)
}
defer f.Close()
reader := csv.NewReader(f)
records, err := reader.ReadAll()
if err != nil {
return nil, fmt.Errorf("failed to read CSV file: %w", err)
}
if len(records) < 2 {
return nil, fmt.Errorf("CSV file must have a header row and at least one data row")
}
// 查找 user_id 列
header := records[0]
colIdx := -1
for i, h := range header {
if strings.TrimSpace(h) == "user_id" {
colIdx = i
break
}
}
if colIdx == -1 {
return nil, fmt.Errorf("CSV file must have a 'user_id' column")
}
var ids []int
for _, row := range records[1:] {
if len(row) <= colIdx {
continue
}
s := strings.TrimSpace(row[colIdx])
if s == "" {
continue
}
uid, err := strconv.Atoi(s)
if err != nil {
return nil, fmt.Errorf("invalid user ID in CSV: %s", s)
}
ids = append(ids, uid)
}
return ids, nil
}