forked from Gitlink/gitlink-cli
336 lines
10 KiB
Go
336 lines
10 KiB
Go
package collaborator
|
||
|
||
import (
|
||
"fmt"
|
||
|
||
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
||
)
|
||
|
||
// userRole 保存用户名和对应角色,支持 CSV 每行指定不同 role
|
||
// 这是 collaborator 批量命令特有的需求:不同用户可能分配不同的权限
|
||
// 而 --users + --role 方式所有用户同一个 role,CSV 方式每行可不同
|
||
type userRole struct {
|
||
User string
|
||
Role string
|
||
}
|
||
|
||
func newBatchAddShortcut() *common.Shortcut {
|
||
return &common.Shortcut{
|
||
// +batch-add:批量添加多名协作者
|
||
Name: "batch-add",
|
||
Description: "批量添加多名协作者",
|
||
Long: `Add multiple collaborators to the repository in a single operation.
|
||
|
||
Provide collaborators via --users (comma-separated usernames) or --from (CSV
|
||
file with user_id and role columns). When --from supplies a role column that
|
||
value is used; otherwise the --role flag (default: write) applies.
|
||
|
||
Use --dry-run to preview which collaborators would be added without making
|
||
any changes.
|
||
|
||
Available roles: admin, write, read.`,
|
||
Example: ` # Add two collaborators with the default write role
|
||
gitlink collaborator +batch-add --users zhangsan,lisi
|
||
|
||
# Add collaborators with admin role
|
||
gitlink collaborator +batch-add --users zhangsan,lisi --role admin
|
||
|
||
# Dry-run to preview additions from a CSV file
|
||
gitlink collaborator +batch-add --from collaborators.csv --dry-run
|
||
|
||
# Combine both sources
|
||
gitlink collaborator +batch-add --users zhangsan --from collaborators.csv`,
|
||
Flags: []common.Flag{
|
||
{Name: "users", Short: "u", Usage: "Comma-separated usernames to add"},
|
||
{Name: "from", Usage: "CSV file path (columns: user_id, role)"},
|
||
{Name: "role", Short: "r", Usage: "Default role for --users: admin, write, read", Default: "write"},
|
||
{Name: "dry-run", Usage: "Preview changes without applying them", Bool: true, Default: "false"},
|
||
},
|
||
Run: runBatchAdd,
|
||
}
|
||
}
|
||
|
||
func newBatchRemoveShortcut() *common.Shortcut {
|
||
return &common.Shortcut{
|
||
// +batch-remove:批量移除多名协作者
|
||
Name: "batch-remove",
|
||
Description: "批量移除多名协作者",
|
||
Long: `Remove multiple collaborators from the repository in a single operation.
|
||
|
||
Provide collaborators via --users (comma-separated usernames) or --from (CSV
|
||
file with a user_id column).
|
||
|
||
This action is irreversible. A confirmation prompt is shown before removal
|
||
unless --yes is set.
|
||
|
||
Use --dry-run to preview which collaborators would be removed without making
|
||
any changes.`,
|
||
Example: ` # Remove two collaborators
|
||
gitlink collaborator +batch-remove --users zhangsan,lisi
|
||
|
||
# Dry-run to preview removals
|
||
gitlink collaborator +batch-remove --users zhangsan,lisi --dry-run
|
||
|
||
# Remove collaborators listed in a CSV file
|
||
gitlink collaborator +batch-remove --from users.csv`,
|
||
Flags: []common.Flag{
|
||
{Name: "users", Short: "u", Usage: "Comma-separated usernames to remove"},
|
||
{Name: "from", Usage: "CSV file path (column: user_id)"},
|
||
{Name: "dry-run", Usage: "Preview changes without applying them", Bool: true, Default: "false"},
|
||
},
|
||
Run: runBatchRemove,
|
||
}
|
||
}
|
||
|
||
func newBatchRoleShortcut() *common.Shortcut {
|
||
return &common.Shortcut{
|
||
// +batch-role:批量修改多名协作者的角色
|
||
Name: "batch-role",
|
||
Description: "批量修改多名协作者的角色",
|
||
Long: `Change the role of multiple collaborators in a single operation.
|
||
|
||
Provide collaborators via --users (comma-separated usernames) combined with
|
||
--role, or via --from (CSV file with user_id and role columns).
|
||
|
||
Use --dry-run to preview which role changes would be applied without making
|
||
any changes.
|
||
|
||
Available roles: admin, write, read.`,
|
||
Example: ` # Change two collaborators to admin
|
||
gitlink collaborator +batch-role --users zhangsan,lisi --role admin
|
||
|
||
# Dry-run to preview role changes from a CSV file
|
||
gitlink collaborator +batch-role --from roles.csv --dry-run
|
||
|
||
# Combine both sources
|
||
gitlink collaborator +batch-role --users zhangsan --role read --from roles.csv`,
|
||
Flags: []common.Flag{
|
||
{Name: "users", Short: "u", Usage: "Comma-separated usernames"},
|
||
{Name: "from", Usage: "CSV file path (columns: user_id, role)"},
|
||
{Name: "role", Short: "r", Usage: "New role for --users: admin, write, read"},
|
||
{Name: "dry-run", Usage: "Preview changes without applying them", Bool: true, Default: "false"},
|
||
},
|
||
Run: runBatchRole,
|
||
}
|
||
}
|
||
|
||
// --- batch-add ---
|
||
|
||
// [B 类批量] 逐个添加协作者——POST /collaborators
|
||
// 支持 --users + --role 统一角色 或 --from CSV 每行不同角色
|
||
func runBatchAdd(ctx *common.RuntimeContext) error {
|
||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||
return err
|
||
}
|
||
|
||
pairs, err := collectUserRoles(ctx.Arg("users"), ctx.Arg("from"), ctx.Arg("role"))
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if len(pairs) == 0 {
|
||
return fmt.Errorf("no users provided; use --users user1,user2 or --from collaborators.csv")
|
||
}
|
||
|
||
dryRun := common.ParseBool(ctx.Arg("dry-run"))
|
||
items := make([]string, 0, len(pairs))
|
||
for _, p := range pairs {
|
||
items = append(items, p.User)
|
||
}
|
||
|
||
summary := common.ProcessBatch(items, dryRun, "add", func(user string) error {
|
||
role := roleForUser(pairs, user)
|
||
payload := map[string]interface{}{
|
||
"user": map[string]string{
|
||
"user_id": user,
|
||
"role_name": role,
|
||
},
|
||
}
|
||
_, err := ctx.CallAPI("POST", fmt.Sprintf("/%s/%s/collaborators", ctx.Owner, ctx.Repo), payload)
|
||
return err
|
||
})
|
||
summary.Repository = fmt.Sprintf("%s/%s", ctx.Owner, ctx.Repo)
|
||
|
||
if err := ctx.OutputData(summary); err != nil {
|
||
return err
|
||
}
|
||
if summary.Failed > 0 {
|
||
return fmt.Errorf("%d of %d collaborator(s) failed to add", summary.Failed, summary.Total)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// --- batch-remove ---
|
||
|
||
// [B 类批量] 逐个移除协作者——DELETE /collaborators/remove(不可逆)
|
||
func runBatchRemove(ctx *common.RuntimeContext) error {
|
||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||
return err
|
||
}
|
||
|
||
users, err := collectUsers(ctx.Arg("users"), ctx.Arg("from"))
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if len(users) == 0 {
|
||
return fmt.Errorf("no users provided; use --users user1,user2 or --from users.csv")
|
||
}
|
||
|
||
dryRun := common.ParseBool(ctx.Arg("dry-run"))
|
||
|
||
if !dryRun {
|
||
if err := common.ConfirmAction(
|
||
fmt.Sprintf("remove %d collaborator(s) from %s/%s", len(users), ctx.Owner, ctx.Repo),
|
||
); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
|
||
summary := common.ProcessBatch(users, dryRun, "remove", func(user string) error {
|
||
payload := map[string]interface{}{
|
||
"user": map[string]string{
|
||
"user_id": user,
|
||
},
|
||
}
|
||
_, err := ctx.CallAPI("DELETE", fmt.Sprintf("/%s/%s/collaborators/remove", ctx.Owner, ctx.Repo), payload)
|
||
return err
|
||
})
|
||
summary.Repository = fmt.Sprintf("%s/%s", ctx.Owner, ctx.Repo)
|
||
|
||
if err := ctx.OutputData(summary); err != nil {
|
||
return err
|
||
}
|
||
if summary.Failed > 0 {
|
||
return fmt.Errorf("%d of %d collaborator(s) failed to remove", summary.Failed, summary.Total)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// --- batch-role ---
|
||
|
||
// [B 类批量] 逐个修改协作者角色——PUT /collaborators/change_role
|
||
func runBatchRole(ctx *common.RuntimeContext) error {
|
||
if err := ctx.ResolveOwnerRepo(); err != nil {
|
||
return err
|
||
}
|
||
|
||
usersValue := ctx.Arg("users")
|
||
fromValue := ctx.Arg("from")
|
||
roleValue := ctx.Arg("role")
|
||
|
||
// When --users is used without --from, --role is required.
|
||
if usersValue != "" && fromValue == "" && roleValue == "" {
|
||
return fmt.Errorf("--role is required when using --users without --from")
|
||
}
|
||
|
||
pairs, err := collectUserRoles(usersValue, fromValue, roleValue)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if len(pairs) == 0 {
|
||
return fmt.Errorf("no users provided; use --users user1,user2 --role admin or --from roles.csv")
|
||
}
|
||
|
||
dryRun := common.ParseBool(ctx.Arg("dry-run"))
|
||
items := make([]string, 0, len(pairs))
|
||
for _, p := range pairs {
|
||
items = append(items, p.User)
|
||
}
|
||
|
||
// Resolve all logins to numeric ids once via the collaborators list.
|
||
memberEnv, err := ctx.CallAPI("GET", fmt.Sprintf("/%s/%s/collaborators", ctx.Owner, ctx.Repo), nil)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
memberData, _ := memberEnv.Data.(map[string]interface{})
|
||
members, _ := memberData["members"].([]interface{})
|
||
|
||
summary := common.ProcessBatch(items, dryRun, "change-role", func(user string) error {
|
||
role := roleForUser(pairs, user)
|
||
id, found := findUserIDByLogin(members, user)
|
||
if !found {
|
||
return fmt.Errorf("collaborator %q not found", user)
|
||
}
|
||
payload := map[string]interface{}{
|
||
"user_id": id,
|
||
"role": mapRole(role),
|
||
}
|
||
_, err := ctx.CallAPI("PUT", fmt.Sprintf("/%s/%s/collaborators/change_role", ctx.Owner, ctx.Repo), payload)
|
||
return err
|
||
})
|
||
summary.Repository = fmt.Sprintf("%s/%s", ctx.Owner, ctx.Repo)
|
||
|
||
if err := ctx.OutputData(summary); err != nil {
|
||
return err
|
||
}
|
||
if summary.Failed > 0 {
|
||
return fmt.Errorf("%d of %d collaborator(s) failed to change role", summary.Failed, summary.Total)
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// --- helpers ---
|
||
|
||
// collectUserRoles 收集用户-角色对(支持 --users + --role 和 --from CSV 两种输入)
|
||
// 与 CollectStrings 不同,此函数需要同时处理 {用户, 角色} 成对数据
|
||
// --from CSV 要求有 user_id 和 role 两列,每行可指定不同角色
|
||
func collectUserRoles(usersValue, fromPath, defaultRole string) ([]userRole, error) {
|
||
var pairs []userRole
|
||
|
||
// From --users flag
|
||
for _, u := range common.ParseStringList(usersValue) {
|
||
pairs = append(pairs, userRole{User: u, Role: defaultRole})
|
||
}
|
||
|
||
// From CSV
|
||
if fromPath != "" {
|
||
rows, err := common.ReadRowsFromCSV(fromPath, []string{"user_id", "role"})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
for _, row := range rows {
|
||
role := row["role"]
|
||
if role == "" {
|
||
role = defaultRole
|
||
}
|
||
pairs = append(pairs, userRole{User: row["user_id"], Role: role})
|
||
}
|
||
}
|
||
|
||
// Deduplicate by user
|
||
seen := map[string]bool{}
|
||
deduped := make([]userRole, 0, len(pairs))
|
||
for _, p := range pairs {
|
||
if seen[p.User] {
|
||
continue
|
||
}
|
||
seen[p.User] = true
|
||
deduped = append(deduped, p)
|
||
}
|
||
return deduped, nil
|
||
}
|
||
|
||
// collectUsers 收集用户名(仅用于 batch-remove,不需要角色信息)
|
||
func collectUsers(usersValue, fromPath string) ([]string, error) {
|
||
users := common.ParseStringList(usersValue)
|
||
|
||
if fromPath != "" {
|
||
csvUsers, err := common.ReadColumnFromCSV(fromPath, []string{"user_id"})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
users = append(users, csvUsers...)
|
||
}
|
||
|
||
return common.DedupeStrings(users), nil
|
||
}
|
||
|
||
// roleForUser 在 userRole 列表中查找指定用户的角色(线性查找)
|
||
func roleForUser(pairs []userRole, user string) string {
|
||
for _, p := range pairs {
|
||
if p.User == user {
|
||
return p.Role
|
||
}
|
||
}
|
||
return "write"
|
||
}
|