forked from Gitlink/gitlink-cli
319 lines
9.3 KiB
Go
319 lines
9.3 KiB
Go
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"
|
||
}
|