gitlink-cli/shortcuts/collaborator/batch.go

319 lines
9.3 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 collaborator
import (
"fmt"
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
)
// userRole holds a username and the role to assign.
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 ---
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 ---
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 ---
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)
}
summary := common.ProcessBatch(items, dryRun, "change-role", 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("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 builds a deduplicated slice of userRole pairs from
// --users (combined with defaultRole) and/or --from CSV.
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 builds a deduplicated slice of usernames from --users and/or --from CSV.
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 looks up the role assigned to a user in the pairs slice.
func roleForUser(pairs []userRole, user string) string {
for _, p := range pairs {
if p.User == user {
return p.Role
}
}
return "write"
}