forked from Gitlink/gitlink-cli
316 lines
9.2 KiB
Go
316 lines
9.2 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{
|
|
Name: "batch-add",
|
|
Description: "Add multiple collaborators to the repository",
|
|
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{
|
|
Name: "batch-remove",
|
|
Description: "Remove multiple collaborators from the repository",
|
|
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{
|
|
Name: "batch-role",
|
|
Description: "Change role for multiple collaborators",
|
|
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"
|
|
}
|