diff --git a/cmd/src/doc.go b/cmd/src/doc.go index d433aa24cb..8132189c87 100644 --- a/cmd/src/doc.go +++ b/cmd/src/doc.go @@ -63,7 +63,6 @@ Examples: "extsvc": &extsvcCommands, "code-intel": &codeintelCommands, "repos": &reposCommands, - "users": &usersCommands, } pending := out.Pending(output.Line("", output.StylePending, "Rendering Markdown...")) diff --git a/cmd/src/run_migration_compat.go b/cmd/src/run_migration_compat.go index 5a9296d5e9..6671b07da3 100644 --- a/cmd/src/run_migration_compat.go +++ b/cmd/src/run_migration_compat.go @@ -25,8 +25,10 @@ var migratedCommands = map[string]*cli.Command{ // instead of writing lots of plumbing to handle an alias, lets just register it explicitly for now "codeowner": codeownersCommand, "login": loginCommand, - "orgs": orgsCommand, + "orgs": orgsCommand, "org": orgsCommand, + "users": usersCommand, + "user": usersCommand, "version": versionCommand, } diff --git a/cmd/src/users.go b/cmd/src/users.go index 3b915d75e1..5fa4c99c49 100644 --- a/cmd/src/users.go +++ b/cmd/src/users.go @@ -1,14 +1,28 @@ package main import ( - "flag" - "fmt" + "github.com/sourcegraph/src-cli/internal/clicompat" + "github.com/urfave/cli/v3" ) -var usersCommands commander +var usersCommand = clicompat.Wrap(&cli.Command{ + Name: "users", + Aliases: []string{"user"}, + Usage: "manages users", + UsageText: "src users [command options]", + Description: usersExamples, + HideVersion: true, + Commands: []*cli.Command{ + usersListCommand, + usersGetCommand, + usersCreateCommand, + usersDeleteCommand, + usersPruneCommand, + usersTagCommand, + }, +}) -func init() { - usage := `'src users' is a tool that manages users on a Sourcegraph instance. +const usersExamples = `'src users' is a tool that manages users on a Sourcegraph instance. Usage: @@ -26,23 +40,6 @@ The commands are: Use "src users [command] -h" for more information about a command. ` - flagSet := flag.NewFlagSet("users", flag.ExitOnError) - handler := func(args []string) error { - usersCommands.run(flagSet, "src users", usage, args) - return nil - } - - // Register the command. - commands = append(commands, &command{ - flagSet: flagSet, - aliases: []string{"user"}, - handler: handler, - usageFunc: func() { - fmt.Println(usage) - }, - }) -} - const userFragment = ` fragment UserFields on User { id diff --git a/cmd/src/users_create.go b/cmd/src/users_create.go index b33b5808ba..71ebe7dad8 100644 --- a/cmd/src/users_create.go +++ b/cmd/src/users_create.go @@ -2,14 +2,13 @@ package main import ( "context" - "flag" "fmt" - "github.com/sourcegraph/src-cli/internal/api" + "github.com/sourcegraph/src-cli/internal/clicompat" + "github.com/urfave/cli/v3" ) -func init() { - usage := ` +const usersCreateExamples = ` Examples: Create a user account: @@ -18,25 +17,36 @@ Examples: ` - flagSet := flag.NewFlagSet("create", flag.ExitOnError) - usageFunc := func() { - fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src users %s':\n", flagSet.Name()) - flagSet.PrintDefaults() - fmt.Println(usage) - } - var ( - usernameFlag = flagSet.String("username", "", `The new user's username. (required)`) - emailFlag = flagSet.String("email", "", `The new user's email address. (required)`) - resetPasswordURLFlag = flagSet.Bool("reset-password-url", false, `Print the reset password URL to manually send to the new user.`) - apiFlags = api.NewFlags(flagSet) - ) +var usersCreateCommand = clicompat.Wrap(&cli.Command{ + Name: "create", + Usage: "creates a user account", + UsageText: "src users create [options]", + Description: usersCreateExamples, + HideVersion: true, + Flags: clicompat.WithAPIFlags( + &cli.StringFlag{ + Name: "username", + Usage: "The new user's username.", + Required: true, + Validator: requiresNotEmpty("provide a username name using -username"), + }, + &cli.StringFlag{ + Name: "email", + Usage: "The new user's email address", + Required: true, + Validator: requiresNotEmpty("provide a email name using -email"), + }, + &cli.BoolFlag{ + Name: "reset-password-url", + Usage: "Print the reset password URL to manually send to the new user.", + }, + ), + Action: func(ctx context.Context, cmd *cli.Command) error { + username := cmd.String("username") + email := cmd.String("email") + resetPasswordURL := cmd.Bool("reset-password-url") - handler := func(args []string) error { - if err := flagSet.Parse(args); err != nil { - return err - } - - client := cfg.apiClient(apiFlags, flagSet.Output()) + client := cfg.apiClient(clicompat.APIFlagsFromCmd(cmd), cmd.Writer) query := `mutation CreateUser( $username: String!, @@ -56,24 +66,22 @@ Examples: } } if ok, err := client.NewRequest(query, map[string]any{ - "username": *usernameFlag, - "email": *emailFlag, - }).Do(context.Background(), &result); err != nil || !ok { + "username": username, + "email": email, + }).Do(ctx, &result); err != nil || !ok { return err } - fmt.Printf("User %q created.\n", *usernameFlag) - if *resetPasswordURLFlag && result.CreateUser.ResetPasswordURL != "" { - fmt.Println() - fmt.Printf("\tReset pasword URL: %s\n", result.CreateUser.ResetPasswordURL) + if _, err := fmt.Fprintf(cmd.Writer, "User %q created.\n", username); err != nil { + return err + } + if resetPasswordURL && result.CreateUser.ResetPasswordURL != "" { + if _, err := fmt.Fprintln(cmd.Writer); err != nil { + return err + } + _, err := fmt.Fprintf(cmd.Writer, "\tReset pasword URL: %s\n", result.CreateUser.ResetPasswordURL) + return err } return nil - } - - // Register the command. - usersCommands = append(usersCommands, &command{ - flagSet: flagSet, - handler: handler, - usageFunc: usageFunc, - }) -} + }, +}) diff --git a/cmd/src/users_delete.go b/cmd/src/users_delete.go index c4b59b6c3f..47f46503ad 100644 --- a/cmd/src/users_delete.go +++ b/cmd/src/users_delete.go @@ -3,17 +3,16 @@ package main import ( "bufio" "context" - "flag" "fmt" "os" "strconv" "strings" - "github.com/sourcegraph/src-cli/internal/api" + "github.com/sourcegraph/src-cli/internal/clicompat" + "github.com/urfave/cli/v3" ) -func init() { - usage := ` +const usersDeleteExamples = ` Examples: Delete a user account by ID: @@ -30,25 +29,23 @@ Examples: ` - flagSet := flag.NewFlagSet("delete", flag.ExitOnError) - usageFunc := func() { - fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src users %s':\n", flagSet.Name()) - flagSet.PrintDefaults() - fmt.Println(usage) - } - var ( - userIDFlag = flagSet.String("id", "", `The ID of the user to delete.`) - apiFlags = api.NewFlags(flagSet) - ) - - handler := func(args []string) error { - if err := flagSet.Parse(args); err != nil { - return err - } - - client := cfg.apiClient(apiFlags, flagSet.Output()) - - if *userIDFlag == "" { +var usersDeleteCommand = clicompat.Wrap(&cli.Command{ + Name: "delete", + Usage: "deletes a user account", + UsageText: "src users delete [options]", + Description: usersDeleteExamples, + HideVersion: true, + Flags: clicompat.WithAPIFlags( + &cli.StringFlag{ + Name: "id", + Usage: "The ID of the user to delete.", + }, + ), + Action: func(ctx context.Context, cmd *cli.Command) error { + userID := cmd.String("id") + client := cfg.apiClient(clicompat.APIFlagsFromCmd(cmd), cmd.Writer) + + if userID == "" { query := `query UsersTotalCountCountUsers { users { totalCount } }` var result struct { @@ -56,12 +53,14 @@ Examples: TotalCount int } } - ok, err := client.NewQuery(query).Do(context.Background(), &result) + ok, err := client.NewQuery(query).Do(ctx, &result) if err != nil || !ok { return err } - fmt.Printf("No user ID specified. This would delete %d users.\nType in this number to confirm and hit return: ", result.Users.TotalCount) + if _, err := fmt.Fprintf(cmd.Writer, "No user ID specified. This would delete %d users.\nType in this number to confirm and hit return: ", result.Users.TotalCount); err != nil { + return err + } reader := bufio.NewReader(os.Stdin) text, err := reader.ReadString('\n') if err != nil { @@ -74,8 +73,8 @@ Examples: } if count != result.Users.TotalCount { - fmt.Println("Number does not match. Aborting.") - return nil + _, err := fmt.Fprintln(cmd.Writer, "Number does not match. Aborting.") + return err } } @@ -93,19 +92,12 @@ Examples: DeleteUser struct{} } if ok, err := client.NewRequest(query, map[string]any{ - "user": *userIDFlag, - }).Do(context.Background(), &result); err != nil || !ok { + "user": userID, + }).Do(ctx, &result); err != nil || !ok { return err } - fmt.Printf("User with ID %q deleted.\n", *userIDFlag) - return nil - } - - // Register the command. - usersCommands = append(usersCommands, &command{ - flagSet: flagSet, - handler: handler, - usageFunc: usageFunc, - }) -} + _, err := fmt.Fprintf(cmd.Writer, "User with ID %q deleted.\n", userID) + return err + }, +}) diff --git a/cmd/src/users_get.go b/cmd/src/users_get.go index 6b27865f19..cf496a04a4 100644 --- a/cmd/src/users_get.go +++ b/cmd/src/users_get.go @@ -2,16 +2,15 @@ package main import ( "context" - "flag" - "fmt" "github.com/sourcegraph/sourcegraph/lib/errors" "github.com/sourcegraph/src-cli/internal/api" + "github.com/sourcegraph/src-cli/internal/clicompat" + "github.com/urfave/cli/v3" ) -func init() { - usage := ` +const usersGetExamples = ` Examples: Get user with username alice: @@ -20,31 +19,39 @@ Examples: ` - flagSet := flag.NewFlagSet("get", flag.ExitOnError) - usageFunc := func() { - fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src users %s':\n", flagSet.Name()) - flagSet.PrintDefaults() - fmt.Println(usage) - } - var ( - usernameFlag = flagSet.String("username", "", `Look up user by username. (e.g. "alice")`) - emailFlag = flagSet.String("email", "", `Look up user by email. (e.g. "alice@sourcegraph.com")`) - formatFlag = flagSet.String("f", "{{.|json}}", `Format for the output, using the syntax of Go package text/template. (e.g. "{{.ID}}: {{.Username}} ({{.DisplayName}})")`) - apiFlags = api.NewFlags(flagSet) - ) - - handler := func(args []string) error { - if err := flagSet.Parse(args); err != nil { - return err - } - - client := cfg.apiClient(apiFlags, flagSet.Output()) - - if usernameFlag != nil && *usernameFlag != "" && emailFlag != nil && *emailFlag != "" { +var usersGetCommand = clicompat.Wrap(&cli.Command{ + Name: "get", + Usage: "gets a user", + UsageText: "src users get [options]", + Description: usersGetExamples, + HideVersion: true, + Flags: clicompat.WithAPIFlags( + &cli.StringFlag{ + Name: "username", + Usage: `Look up user by username. (e.g. "alice")`, + }, + &cli.StringFlag{ + Name: "email", + Usage: `Look up user by email. (e.g. "alice@sourcegraph.com")`, + }, + &cli.StringFlag{ + Name: "f", + Value: "{{.|json}}", + Usage: `Format for the output, using the syntax of Go package text/template. (e.g. "{{.ID}}: {{.Username}} ({{.DisplayName}})")`, + }, + ), + Action: func(ctx context.Context, cmd *cli.Command) error { + username := cmd.String("username") + email := cmd.String("email") + format := cmd.String("f") + + client := cfg.apiClient(clicompat.APIFlagsFromCmd(cmd), cmd.Writer) + + if username != "" && email != "" { return errors.New("cannot specify both email and username") } - tmpl, err := parseTemplate(*formatFlag) + tmpl, err := parseTemplate(format) if err != nil { return err } @@ -65,19 +72,12 @@ Examples: User *User } if ok, err := client.NewRequest(query, map[string]any{ - "username": api.NullString(*usernameFlag), - "email": api.NullString(*emailFlag), - }).Do(context.Background(), &result); err != nil || !ok { + "username": api.NullString(username), + "email": api.NullString(email), + }).Do(ctx, &result); err != nil || !ok { return err } return execTemplate(tmpl, result.User) - } - - // Register the command. - usersCommands = append(usersCommands, &command{ - flagSet: flagSet, - handler: handler, - usageFunc: usageFunc, - }) -} + }, +}) diff --git a/cmd/src/users_list.go b/cmd/src/users_list.go index a72e1d8a3e..93dae9601c 100644 --- a/cmd/src/users_list.go +++ b/cmd/src/users_list.go @@ -2,14 +2,13 @@ package main import ( "context" - "flag" - "fmt" "github.com/sourcegraph/src-cli/internal/api" + "github.com/sourcegraph/src-cli/internal/clicompat" + "github.com/urfave/cli/v3" ) -func init() { - usage := ` +const usersListExamples = ` Examples: List users: @@ -26,36 +25,48 @@ Examples: ` - flagSet := flag.NewFlagSet("list", flag.ExitOnError) - usageFunc := func() { - fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src users %s':\n", flagSet.Name()) - flagSet.PrintDefaults() - fmt.Println(usage) - } - var ( - firstFlag = flagSet.Int("first", 1000, "Returns the first n users from the list.") - queryFlag = flagSet.String("query", "", `Returns users whose names match the query. (e.g. "alice")`) - tagFlag = flagSet.String("tag", "", `Returns users with the given tag.`) - formatFlag = flagSet.String("f", "{{.Username}}", `Format for the output, using the syntax of Go package text/template. (e.g. "{{.ID}}: {{.Username}} ({{.DisplayName}})" or "{{.|json}}")`) - apiFlags = api.NewFlags(flagSet) - ) - - handler := func(args []string) error { - if err := flagSet.Parse(args); err != nil { - return err - } - - ctx := context.Background() - client := cfg.apiClient(apiFlags, flagSet.Output()) - - tmpl, err := parseTemplate(*formatFlag) +var usersListCommand = clicompat.Wrap(&cli.Command{ + Name: "list", + Usage: "lists users", + UsageText: "src users list [options]", + Description: usersListExamples, + HideVersion: true, + Flags: clicompat.WithAPIFlags( + &cli.IntFlag{ + Name: "first", + Value: 1000, + Usage: "Returns the first n users from the list.", + }, + &cli.StringFlag{ + Name: "query", + Usage: `Returns users whose names match the query. (e.g. "alice")`, + }, + &cli.StringFlag{ + Name: "tag", + Usage: `Returns users with the given tag.`, + }, + &cli.StringFlag{ + Name: "f", + Value: "{{.Username}}", + Usage: `Format for the output, using the syntax of Go package text/template. (e.g. "{{.ID}}: {{.Username}} ({{.DisplayName}})" or "{{.|json}}")`, + }, + ), + Action: func(ctx context.Context, cmd *cli.Command) error { + first := cmd.Int("first") + queryValue := cmd.String("query") + tag := cmd.String("tag") + format := cmd.String("f") + + client := cfg.apiClient(clicompat.APIFlagsFromCmd(cmd), cmd.Writer) + + tmpl, err := parseTemplate(format) if err != nil { return err } vars := map[string]any{ - "first": api.NullInt(*firstFlag), - "query": api.NullString(*queryFlag), - "tag": api.NullString(*tagFlag), + "first": api.NullInt(first), + "query": api.NullString(queryValue), + "tag": api.NullString(tag), } queryTagVar := "" queryTag := "" @@ -94,12 +105,5 @@ first: $first, } } return nil - } - - // Register the command. - usersCommands = append(usersCommands, &command{ - flagSet: flagSet, - handler: handler, - usageFunc: usageFunc, - }) -} + }, +}) diff --git a/cmd/src/users_prune.go b/cmd/src/users_prune.go index d67fff6be0..c4bf3a044e 100644 --- a/cmd/src/users_prune.go +++ b/cmd/src/users_prune.go @@ -2,7 +2,6 @@ package main import ( "context" - "flag" "fmt" "os" "strings" @@ -11,10 +10,11 @@ import ( "github.com/jedib0t/go-pretty/v6/table" "github.com/sourcegraph/src-cli/internal/api" + "github.com/sourcegraph/src-cli/internal/clicompat" + "github.com/urfave/cli/v3" ) -func init() { - usage := ` +const usersPruneExamples = ` This command removes users from a Sourcegraph instance who have been inactive for 60 or more days. Admin accounts are omitted by default. Examples: @@ -24,32 +24,49 @@ Examples: $ src users prune -remove-admin -remove-null-users ` - flagSet := flag.NewFlagSet("prune", flag.ExitOnError) - usageFunc := func() { - fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src users %s':\n", flagSet.Name()) - flagSet.PrintDefaults() - fmt.Println(usage) - } - var ( - daysToDelete = flagSet.Int("days", 60, "Days threshold on which to remove users, must be 60 days or greater and defaults to this value ") - removeAdmin = flagSet.Bool("remove-admin", false, "prune admin accounts") - removeNoLastActive = flagSet.Bool("remove-null-users", false, "removes users with no last active value") - skipConfirmation = flagSet.Bool("force", false, "skips user confirmation step allowing programmatic use") - displayUsersToDelete = flagSet.Bool("display-users", false, "display table of users to be deleted by prune") - apiFlags = api.NewFlags(flagSet) - ) - - handler := func(args []string) error { - if err := flagSet.Parse(args); err != nil { +var usersPruneCommand = clicompat.Wrap(&cli.Command{ + Name: "prune", + Usage: "deletes inactive users", + UsageText: "src users prune [options]", + Description: usersPruneExamples, + HideVersion: true, + Flags: clicompat.WithAPIFlags( + &cli.IntFlag{ + Name: "days", + Value: 60, + Usage: "Days threshold on which to remove users, must be 60 days or greater and defaults to this value ", + }, + &cli.BoolFlag{ + Name: "remove-admin", + Usage: "prune admin accounts", + }, + &cli.BoolFlag{ + Name: "remove-null-users", + Usage: "removes users with no last active value", + }, + &cli.BoolFlag{ + Name: "force", + Usage: "skips user confirmation step allowing programmatic use", + }, + &cli.BoolFlag{ + Name: "display-users", + Usage: "display table of users to be deleted by prune", + }, + ), + Action: func(ctx context.Context, cmd *cli.Command) error { + daysToDelete := cmd.Int("days") + removeAdmin := cmd.Bool("remove-admin") + removeNoLastActive := cmd.Bool("remove-null-users") + skipConfirmation := cmd.Bool("force") + displayUsersToDelete := cmd.Bool("display-users") + + if daysToDelete < 60 { + _, err := fmt.Fprintln(cmd.Writer, "-days flag must be set to 60 or greater") return err } - if *daysToDelete < 60 { - fmt.Println("-days flag must be set to 60 or greater") - return nil - } - ctx := context.Background() - client := cfg.apiClient(apiFlags, flagSet.Output()) + apiFlags := clicompat.APIFlagsFromCmd(cmd) + client := cfg.apiClient(apiFlags, cmd.Writer) // get current user so as not to delete issuer of the prune request currentUserQuery := `query getCurrentUser { currentUser { username }}` @@ -58,7 +75,7 @@ Examples: Username string } } - if ok, err := cfg.apiClient(apiFlags, flagSet.Output()).NewRequest(currentUserQuery, nil).Do(context.Background(), ¤tUserResult); err != nil || !ok { + if ok, err := client.NewRequest(currentUserQuery, nil).Do(ctx, ¤tUserResult); err != nil || !ok { return err } @@ -71,7 +88,7 @@ Examples: } } } - if ok, err := cfg.apiClient(apiFlags, flagSet.Output()).NewRequest(totalUsersQuery, nil).Do(context.Background(), &totalUsers); err != nil || !ok { + if ok, err := client.NewRequest(totalUsersQuery, nil).Do(ctx, &totalUsers); err != nil || !ok { return err } @@ -140,15 +157,15 @@ Examples: return err } // don't remove users with no last active value unless option flag is set - if !hasLastActive && !*removeNoLastActive { + if !hasLastActive && !removeNoLastActive { continue } // don't remove admins unless option flag is set - if !*removeAdmin && user.SiteAdmin { + if !removeAdmin && user.SiteAdmin { continue } // remove users who have been inactive for longer than the threshold set by the -days flag - if daysSinceLastUse <= *daysToDelete && hasLastActive { + if daysSinceLastUse <= daysToDelete && hasLastActive { continue } // serialize user to print in table as part of confirmUserRemoval, add to delete slice @@ -156,7 +173,7 @@ Examples: usersToDelete = append(usersToDelete, userToDelete) } - if *skipConfirmation { + if skipConfirmation { for _, user := range usersToDelete { if err := removeUser(user.User, client, ctx); err != nil { return err @@ -166,11 +183,11 @@ Examples: } // confirm and remove users - if confirmed, _ := confirmUserRemoval(usersToDelete, *daysToDelete, *displayUsersToDelete); !confirmed { - fmt.Println("Aborting removal") + if confirmed, _ := confirmUserRemoval(usersToDelete, daysToDelete, displayUsersToDelete); !confirmed { + fmt.Fprintln(cmd.Writer, "Aborting removal") return nil } else { - fmt.Println("REMOVING USERS") + fmt.Fprintln(cmd.Writer, "REMOVING USERS") for _, user := range usersToDelete { if err := removeUser(user.User, client, ctx); err != nil { return err @@ -179,15 +196,8 @@ Examples: } return nil - } - - // Register the command. - usersCommands = append(usersCommands, &command{ - flagSet: flagSet, - handler: handler, - usageFunc: usageFunc, - }) -} + }, +}) // computes days since last usage from current day and time and aggregated_user_statistics.lastActiveAt, uses time.Parse func computeDaysSinceLastUse(user SiteUser) (timeDiff int, hasLastActive bool, _ error) { diff --git a/cmd/src/users_tag.go b/cmd/src/users_tag.go index 4404c88c19..d18b0c0f13 100644 --- a/cmd/src/users_tag.go +++ b/cmd/src/users_tag.go @@ -2,14 +2,12 @@ package main import ( "context" - "flag" - "fmt" - "github.com/sourcegraph/src-cli/internal/api" + "github.com/sourcegraph/src-cli/internal/clicompat" + "github.com/urfave/cli/v3" ) -func init() { - usage := ` +const usersTagExamples = ` Examples: Add a tag "foo" to a user: @@ -28,25 +26,36 @@ Related examples: ` - flagSet := flag.NewFlagSet("tag", flag.ExitOnError) - usageFunc := func() { - fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src users %s':\n", flagSet.Name()) - flagSet.PrintDefaults() - fmt.Println(usage) - } - var ( - userIDFlag = flagSet.String("user-id", "", `The ID of the user to tag. (required)`) - tagFlag = flagSet.String("tag", "", `The tag to set on the user. (required)`) - removeFlag = flagSet.Bool("remove", false, `Remove the tag. (default: add the tag`) - apiFlags = api.NewFlags(flagSet) - ) - - handler := func(args []string) error { - if err := flagSet.Parse(args); err != nil { - return err - } - - client := cfg.apiClient(apiFlags, flagSet.Output()) +var usersTagCommand = clicompat.Wrap(&cli.Command{ + Name: "tag", + Usage: "add/remove a tag on a user", + UsageText: "src users tag [options]", + Description: usersTagExamples, + HideVersion: true, + Flags: clicompat.WithAPIFlags( + &cli.StringFlag{ + Name: "user-id", + Usage: "The ID of the user to tag.", + Required: true, + Validator: requiresNotEmpty("provide a user ID by using -user-id"), + }, + &cli.StringFlag{ + Name: "tag", + Usage: "The tag to set on the user.", + Required: true, + Validator: requiresNotEmpty("provide a tag by using -tag"), + }, + &cli.BoolFlag{ + Name: "remove", + Usage: "Remove the tag. (default: add the tag)", + }, + ), + Action: func(ctx context.Context, cmd *cli.Command) error { + userID := cmd.String("user-id") + tag := cmd.String("tag") + remove := cmd.Bool("remove") + + client := cfg.apiClient(clicompat.APIFlagsFromCmd(cmd), cmd.Writer) query := `mutation SetUserTag( $user: ID!, @@ -63,17 +72,10 @@ Related examples: }` _, err := client.NewRequest(query, map[string]any{ - "user": *userIDFlag, - "tag": *tagFlag, - "present": !*removeFlag, - }).Do(context.Background(), &struct{}{}) + "user": userID, + "tag": tag, + "present": !remove, + }).Do(ctx, &struct{}{}) return err - } - - // Register the command. - usersCommands = append(usersCommands, &command{ - flagSet: flagSet, - handler: handler, - usageFunc: usageFunc, - }) -} + }, +}) diff --git a/lib/batches/batch_spec.go b/lib/batches/batch_spec.go index a9ccc6e146..269093a82d 100644 --- a/lib/batches/batch_spec.go +++ b/lib/batches/batch_spec.go @@ -185,6 +185,7 @@ func parseBatchSpec(schema string, data []byte) (*BatchSpec, error) { return &spec, errs } + // docker uses Golang's `encoding/csv` library to parse arguments passed to `--mount` const invalidMountCharacters = ",\"\n\r"