From d17ae99aedafbd57cab97503daf9ab0a0bb0175c Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Thu, 6 Aug 2020 18:40:52 -0700 Subject: [PATCH] api: move GraphQL functionality to a new package --- cmd/src/actions_exec.go | 50 ++--- cmd/src/actions_scope_query.go | 5 +- cmd/src/api.go | 249 ++-------------------- cmd/src/campaigns_add_changesets.go | 67 +++--- cmd/src/campaigns_create.go | 52 ++--- cmd/src/campaigns_list.go | 37 ++-- cmd/src/config.go | 48 +++-- cmd/src/config_edit.go | 20 +- cmd/src/config_get.go | 35 +-- cmd/src/config_list.go | 35 +-- cmd/src/extensions_copy.go | 80 +++---- cmd/src/extensions_delete.go | 27 +-- cmd/src/extensions_get.go | 25 +-- cmd/src/extensions_list.go | 37 ++-- cmd/src/extensions_publish.go | 41 ++-- cmd/src/extsvc.go | 17 +- cmd/src/extsvc_edit.go | 32 +-- cmd/src/extsvc_list.go | 21 +- cmd/src/format.go | 3 +- cmd/src/main.go | 13 ++ cmd/src/orgs_create.go | 29 +-- cmd/src/orgs_delete.go | 27 +-- cmd/src/orgs_get.go | 25 +-- cmd/src/orgs_list.go | 37 ++-- cmd/src/orgs_members_add.go | 29 +-- cmd/src/orgs_members_remove.go | 29 +-- cmd/src/patch_sets_create_from_patches.go | 33 +-- cmd/src/repos_delete.go | 34 +-- cmd/src/repos_enable_disable.go | 48 ++--- cmd/src/repos_get.go | 28 ++- cmd/src/repos_list.go | 61 +++--- cmd/src/search.go | 65 +++--- cmd/src/users_create.go | 37 ++-- cmd/src/users_delete.go | 27 +-- cmd/src/users_get.go | 25 +-- cmd/src/users_list.go | 40 ++-- cmd/src/users_tag.go | 23 +- internal/api/api.go | 230 ++++++++++++++++++++ internal/api/errors.go | 11 + internal/api/flags.go | 27 +++ internal/api/nullable.go | 19 ++ 41 files changed, 919 insertions(+), 859 deletions(-) create mode 100644 internal/api/api.go create mode 100644 internal/api/errors.go create mode 100644 internal/api/flags.go create mode 100644 internal/api/nullable.go diff --git a/cmd/src/actions_exec.go b/cmd/src/actions_exec.go index 81368c29af..42e870573b 100644 --- a/cmd/src/actions_exec.go +++ b/cmd/src/actions_exec.go @@ -21,6 +21,7 @@ import ( "github.com/ghodss/yaml" "github.com/mattn/go-isatty" "github.com/pkg/errors" + "github.com/sourcegraph/src-cli/internal/api" "github.com/sourcegraph/src-cli/internal/campaigns" ) @@ -123,7 +124,7 @@ Format of the action JSON files: includeUnsupportedFlag = flagSet.Bool("include-unsupported", false, "When specified, also repos from unsupported codehosts are processed. Those can be created once the integration is done.") - apiFlags = newAPIFlags(flagSet) + apiFlags = api.NewFlags(flagSet) ) handler := func(args []string) error { @@ -212,6 +213,7 @@ Format of the action JSON files: os.Exit(2) }() + client := cfg.apiClient(apiFlags, flagSet.Output()) logger := campaigns.NewActionLogger(*verbose, *keepLogsFlag) // Fetch Docker images etc. @@ -232,7 +234,7 @@ Format of the action JSON files: // Query repos over which to run action logger.Infof("Querying %s for repositories matching '%s'...\n", cfg.Endpoint, action.ScopeQuery) - repos, err := actionRepos(ctx, action.ScopeQuery, *includeUnsupportedFlag, logger) + repos, err := actionRepos(ctx, client, action.ScopeQuery, *includeUnsupportedFlag, logger) if err != nil { return err } @@ -310,7 +312,7 @@ Format of the action JSON files: return err } - return createPatchSetFromPatches(apiFlags, patches, tmpl, 100) + return createPatchSetFromPatches(ctx, client, patches, tmpl, 100) } // Register the command. @@ -321,7 +323,7 @@ Format of the action JSON files: }) } -func actionRepos(ctx context.Context, scopeQuery string, includeUnsupported bool, logger *campaigns.ActionLogger) ([]campaigns.ActionRepo, error) { +func actionRepos(ctx context.Context, client api.Client, scopeQuery string, includeUnsupported bool, logger *campaigns.ActionLogger) ([]campaigns.ActionRepo, error) { hasCount, err := regexp.MatchString(`count:\d+`, scopeQuery) if err != nil { return nil, err @@ -403,31 +405,13 @@ fragment repositoryFields on Repository { } `json:"errors,omitempty"` } - if err := (&apiRequest{ - query: query, - vars: map[string]interface{}{ - "query": scopeQuery, - }, - // Do not unpack errors and return error. Instead we want to go through - // the results and check whether they're complete. - // If we don't do this and the query returns an error for _one_ - // repository because that is still cloning, we don't get any repositories. - // Instead we simply want to skip those repositories that are still - // being cloned. - dontUnpackErrors: true, - result: &result, - }).do(); err != nil { - - // Ignore exitCodeError with error == nil, because we explicitly set - // dontUnpackErrors, which can lead to an empty exitCodeErr being - // returned. - exitCodeErr, ok := err.(*exitCodeError) - if !ok { - return nil, err - } - if exitCodeErr.error != nil { - return nil, exitCodeErr - } + ok, err := client.NewRequest(query, map[string]interface{}{ + "query": scopeQuery, + }).DoRaw(ctx, &result) + if err != nil { + return nil, err + } else if !ok { + return nil, nil } skipped := []string{} @@ -449,7 +433,7 @@ fragment repositoryFields on Repository { // Skip repos from unsupported code hosts but don't report them explicitly. if !includeUnsupported { - ok, err := isCodeHostSupportedForCampaigns(repo.ExternalRepository.ServiceType) + ok, err := isCodeHostSupportedForCampaigns(ctx, client, repo.ExternalRepository.ServiceType) if err != nil { return nil, errors.Wrap(err, "failed code host check") } @@ -541,11 +525,13 @@ var codeHostCampaignVersions = map[string]*minimumVersionDate{ }, } -func isCodeHostSupportedForCampaigns(kind string) (bool, error) { +func isCodeHostSupportedForCampaigns(ctx context.Context, client api.Client, kind string) (bool, error) { // TODO(LawnGnome): this is a temporary hack; I intend to improve our // testing story including mocking requests to Sourcegraph as part of // https://github.com/sourcegraph/sourcegraph/issues/12333 - return isCodeHostSupportedForCampaignsImpl(kind, getSourcegraphVersion) + return isCodeHostSupportedForCampaignsImpl(kind, func() (string, error) { + return getSourcegraphVersion(ctx, client) + }) } func isCodeHostSupportedForCampaignsImpl(kind string, getVersion func() (string, error)) (bool, error) { diff --git a/cmd/src/actions_scope_query.go b/cmd/src/actions_scope_query.go index 9de8686af7..4750bbd8e7 100644 --- a/cmd/src/actions_scope_query.go +++ b/cmd/src/actions_scope_query.go @@ -10,6 +10,7 @@ import ( "github.com/ghodss/yaml" "github.com/pkg/errors" + "github.com/sourcegraph/src-cli/internal/api" "github.com/sourcegraph/src-cli/internal/campaigns" ) @@ -35,6 +36,7 @@ Examples: var ( fileFlag = flagSet.String("f", "-", "The action file. If not given or '-' standard input is used. (Required)") includeUnsupportedFlag = flagSet.Bool("include-unsupported", false, "When specified, also repos from unsupported codehosts are processed. Those can be created once the integration is done.") + apiFlags = api.NewFlags(flagSet) ) handler := func(args []string) error { @@ -71,6 +73,7 @@ Examples: } ctx := context.Background() + client := cfg.apiClient(apiFlags, flagSet.Output()) if *verbose { log.Printf("# scopeQuery in action definition: %s\n", action.ScopeQuery) @@ -81,7 +84,7 @@ Examples: } logger := campaigns.NewActionLogger(*verbose, false) - repos, err := actionRepos(ctx, action.ScopeQuery, *includeUnsupportedFlag, logger) + repos, err := actionRepos(ctx, client, action.ScopeQuery, *includeUnsupportedFlag, logger) if err != nil { return err } diff --git a/cmd/src/api.go b/cmd/src/api.go index b9fc68bcbf..adb2aca91f 100644 --- a/cmd/src/api.go +++ b/cmd/src/api.go @@ -1,19 +1,17 @@ package main import ( - "bytes" + "context" "encoding/json" "errors" "flag" "fmt" "io/ioutil" - "log" - "net/http" "os" "strings" - "github.com/kballard/go-shellquote" "github.com/mattn/go-isatty" + "github.com/sourcegraph/src-cli/internal/api" ) func init() { @@ -53,7 +51,7 @@ Examples: var ( queryFlag = flagSet.String("query", "", "GraphQL query to execute, e.g. 'query { currentUser { username } }' (stdin otherwise)") varsFlag = flagSet.String("vars", "", `GraphQL query variables to include as JSON string, e.g. '{"var": "val", "var2": "val2"}'`) - apiFlags = newAPIFlags(flagSet) + apiFlags = api.NewFlags(flagSet) ) handler := func(args []string) error { @@ -95,242 +93,23 @@ Examples: // Perform the request. var result interface{} - return (&apiRequest{ - query: query, - vars: vars, - result: &result, - done: func() error { - // Print the formatted JSON. - f, err := marshalIndent(result) - if err != nil { - return err - } - fmt.Println(string(f)) - return nil - }, - flags: apiFlags, - dontUnpackErrors: true, - }).do() - } - - // Register the command. - commands = append(commands, &command{ - flagSet: flagSet, - handler: handler, - usageFunc: usageFunc, - }) -} - -// gqlURL returns the URL to the GraphQL endpoint for the given Sourcegraph -// instance. -func gqlURL(endpoint string) string { - return endpoint + "/.api/graphql" -} - -// curlCmd returns the curl command to perform the given GraphQL query. Bash-only. -func curlCmd(endpoint, accessToken string, additionalHeaders map[string]string, query string, vars map[string]interface{}) (string, error) { - data, err := json.Marshal(map[string]interface{}{ - "query": query, - "variables": vars, - }) - if err != nil { - return "", err - } - - s := "curl \\\n" - if accessToken != "" { - s += fmt.Sprintf(" %s \\\n", shellquote.Join("-H", "Authorization: token "+accessToken)) - } - for k, v := range additionalHeaders { - s += fmt.Sprintf(" %s \\\n", shellquote.Join("-H", k+": "+v)) - } - s += fmt.Sprintf(" %s \\\n", shellquote.Join("-d", string(data))) - s += fmt.Sprintf(" %s", shellquote.Join(gqlURL(endpoint))) - return s, nil -} - -// apiRequest represents a GraphQL API request. -type apiRequest struct { - query string // the GraphQL query - vars map[string]interface{} // the GraphQL query variables - result interface{} // where to store the result - done func() error // a function to invoke for handling the response. If nil, flags like -get-curl are ignored. - flags *apiFlags // the API flags previously created via newAPIFlags - - // If true, errors will not be unpacked. - // - // Consider a GraphQL response like: - // - // {"data": {...}, "errors": ["something went really wrong"]} - // - // 'error unpacking' refers to how we will check if there are any `errors` - // present in the response (if there are, we will report them on the command - // line separately AND exit with a proper error code), and if there are no - // errors `result` will contain only the `{...}` object. - // - // When true, the entire response object is stored in `result` -- as if you - // ran the curl query yourself. - dontUnpackErrors bool -} - -// do performs the API request. If a.flags specify something like -get-curl -// then it is handled immediately and a.done is not invoked. Otherwise, once -// the request is finished a.done is invoked to handle the response (which is -// stored in a.result). -func (a *apiRequest) do() error { - if a.done != nil { - // Handle the get-curl flag now. - if *a.flags.getCurl { - curl, err := curlCmd(cfg.Endpoint, cfg.AccessToken, cfg.AdditionalHeaders, a.query, a.vars) - if err != nil { - return err - } - fmt.Println(curl) - return nil - } - } else { - a.done = func() error { return nil } - } - - // Create the JSON object. - var buf bytes.Buffer - if err := json.NewEncoder(&buf).Encode(map[string]interface{}{ - "query": a.query, - "variables": a.vars, - }); err != nil { - return err - } - - // Create the HTTP request. - req, err := http.NewRequest("POST", gqlURL(cfg.Endpoint), nil) - if err != nil { - return err - } - if cfg.AccessToken != "" { - req.Header.Set("Authorization", "token "+cfg.AccessToken) - } - if *a.flags.trace { - req.Header.Set("X-Sourcegraph-Should-Trace", "true") - } - for k, v := range cfg.AdditionalHeaders { - req.Header.Set(k, v) - } - req.Body = ioutil.NopCloser(&buf) - - // Perform the request. - resp, err := http.DefaultClient.Do(req) - if err != nil { - return err - } - defer resp.Body.Close() - - // Check trace header before we potentially early exit - if *a.flags.trace { - log.Printf("x-trace: %s", resp.Header.Get("x-trace")) - } - - // Our request may have failed before the reaching GraphQL endpoint, so - // confirm the status code. You can test this easily with e.g. an invalid - // endpoint like -endpoint=https://google.com - if resp.StatusCode != http.StatusOK { - if resp.StatusCode == http.StatusUnauthorized && isatty.IsCygwinTerminal(os.Stdout.Fd()) { - fmt.Println("You may need to specify or update your access token to use this endpoint.") - fmt.Println("See https://github.com/sourcegraph/src-cli#authentication") - fmt.Println("") - } - body, err := ioutil.ReadAll(resp.Body) - if err != nil { + if ok, err := cfg.apiClient(apiFlags, flagSet.Output()).NewRequest(query, vars).DoRaw(context.Background(), &result); err != nil || !ok { return err } - return fmt.Errorf("error: %s\n\n%s", resp.Status, body) - } - - // Decode the response. - var result struct { - Data interface{} `json:"data,omitempty"` - Errors interface{} `json:"errors,omitempty"` - } - if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { - return err - } - // Handle the case of not unpacking errors. - if a.dontUnpackErrors { - if err := jsonCopy(a.result, result); err != nil { - return err - } - if err := a.done(); err != nil { + // Print the formatted JSON. + f, err := marshalIndent(result) + if err != nil { return err } - if result.Errors != nil { - return &exitCodeError{error: nil, exitCode: graphqlErrorsExitCode} - } + fmt.Println(string(f)) return nil } - // Handle the case of unpacking errors. - if result.Errors != nil { - return &exitCodeError{ - error: fmt.Errorf("GraphQL errors:\n%s", &graphqlError{result.Errors}), - exitCode: graphqlErrorsExitCode, - } - } - if err := jsonCopy(a.result, result.Data); err != nil { - return err - } - return a.done() -} - -// apiFlags represents generic API flags available in all commands that perform -// API requests. e.g. the ability to turn any CLI command into a curl command. -type apiFlags struct { - getCurl *bool - trace *bool -} - -// newAPIFlags creates the API flags. It should be invoked once at flag setup -// time. -func newAPIFlags(flagSet *flag.FlagSet) *apiFlags { - return &apiFlags{ - getCurl: flagSet.Bool("get-curl", false, "Print the curl command for executing this query and exit (WARNING: includes printing your access token!)"), - trace: flagSet.Bool("trace", false, "Log the trace ID for requests. See https://docs.sourcegraph.com/admin/observability/tracing"), - } -} - -// jsonCopy is a cheaty method of copying an already-decoded JSON (src) -// response into its destination (dst) that would usually be passed to e.g. -// json.Unmarshal. -// -// We could do this with reflection, obviously, but it would be much more -// complex and JSON re-marshaling should be cheap enough anyway. Can improve in -// the future. -func jsonCopy(dst, src interface{}) error { - data, err := json.Marshal(src) - if err != nil { - return err - } - return json.NewDecoder(bytes.NewReader(data)).Decode(dst) -} - -type graphqlError struct { - Errors interface{} -} - -func (g *graphqlError) Error() string { - j, _ := marshalIndent(g.Errors) - return string(j) -} - -func nullInt(n int) *int { - if n == -1 { - return nil - } - return &n -} - -func nullString(s string) *string { - if s == "" { - return nil - } - return &s + // Register the command. + commands = append(commands, &command{ + flagSet: flagSet, + handler: handler, + usageFunc: usageFunc, + }) } diff --git a/cmd/src/campaigns_add_changesets.go b/cmd/src/campaigns_add_changesets.go index 98a399ead6..ca495f0d04 100644 --- a/cmd/src/campaigns_add_changesets.go +++ b/cmd/src/campaigns_add_changesets.go @@ -1,9 +1,12 @@ package main import ( + "context" "errors" "flag" "fmt" + + "github.com/sourcegraph/src-cli/internal/api" ) func init() { @@ -40,7 +43,7 @@ Notes: var ( campaignIDFlag = flagSet.String("campaign", "", "ID of campaign to which to add changesets. (required)") repoNameFlag = flagSet.String("repo-name", "", "Name of repository to which the changesets belong. (required)") - apiFlags = newAPIFlags(flagSet) + apiFlags = api.NewFlags(flagSet) ) handler := func(args []string) error { @@ -63,17 +66,20 @@ Notes: externalIDs := args[2:] - repoID, err := getRepoID(apiFlags, *repoNameFlag) + ctx := context.Background() + client := cfg.apiClient(apiFlags, flagSet.Output()) + + repoID, err := getRepoID(ctx, client, *repoNameFlag) if err != nil { return err } - changesetIDs, err := createChangesets(apiFlags, repoID, externalIDs) + changesetIDs, err := createChangesets(ctx, client, repoID, externalIDs) if err != nil { return err } - err = addChangesets(apiFlags, *campaignIDFlag, changesetIDs) + err = addChangesets(ctx, client, *campaignIDFlag, changesetIDs) if err != nil { return err } @@ -94,20 +100,12 @@ Notes: const getRepoIDQuery = `query Repository($name: String) { repository(name: $name) { id } }` -func getRepoID(f *apiFlags, name string) (string, error) { +func getRepoID(ctx context.Context, client api.Client, name string) (string, error) { var result struct{ Repository struct{ ID string } } - req := &apiRequest{ - query: getRepoIDQuery, - vars: map[string]interface{}{"name": name}, - result: &result, - flags: f, - } - - err := req.do() - if err != nil { - return "", err - } + _, err := client.NewRequest(getRepoIDQuery, map[string]interface{}{ + "name": name, + }).Do(ctx, &result) return result.Repository.ID, err } @@ -119,7 +117,7 @@ mutation CreateChangesets($input: [CreateChangesetInput!]!) { } }` -func createChangesets(f *apiFlags, repoID string, externalIDs []string) ([]string, error) { +func createChangesets(ctx context.Context, client api.Client, repoID string, externalIDs []string) ([]string, error) { var result struct { CreateChangesets []struct { ID string `json:"id"` @@ -136,15 +134,9 @@ func createChangesets(f *apiFlags, repoID string, externalIDs []string) ([]strin var changesetIDs []string - req := &apiRequest{ - query: createChangesetsQuery, - vars: map[string]interface{}{"input": pairs}, - result: &result, - flags: f, - } - - err := req.do() - if err != nil { + if ok, err := client.NewRequest(createChangesetsQuery, map[string]interface{}{ + "input": pairs, + }).Do(ctx, &result); err != nil || !ok { return changesetIDs, err } @@ -154,7 +146,7 @@ func createChangesets(f *apiFlags, repoID string, externalIDs []string) ([]strin fmt.Printf("Created %d changesets.\n", len(changesetIDs)) - return changesetIDs, err + return changesetIDs, nil } const addChangesetsQuery = ` @@ -168,7 +160,7 @@ mutation AddChangesetsToCampaign($campaign: ID!, $changesets: [ID!]!) { } ` -func addChangesets(f *apiFlags, campaignID string, changesetIDs []string) error { +func addChangesets(ctx context.Context, client api.Client, campaignID string, changesetIDs []string) error { var result struct { AddChangesetsToCampaign struct { ID string `json:"id"` @@ -178,22 +170,13 @@ func addChangesets(f *apiFlags, campaignID string, changesetIDs []string) error } `json:"addChangesetsToCampaign"` } - req := &apiRequest{ - query: addChangesetsQuery, - vars: map[string]interface{}{ - "campaign": campaignID, - "changesets": changesetIDs, - }, - result: &result, - flags: f, - } - - err := req.do() - if err != nil { + if ok, err := client.NewRequest(addChangesetsQuery, map[string]interface{}{ + "campaign": campaignID, + "changesets": changesetIDs, + }).Do(ctx, &result); err != nil || !ok { return err } fmt.Printf("Added changeset to campaign. Changesets now in campaign: %d\n", result.AddChangesetsToCampaign.Changesets.TotalCount) - - return err + return nil } diff --git a/cmd/src/campaigns_create.go b/cmd/src/campaigns_create.go index 9ceb3130f0..336d2510db 100644 --- a/cmd/src/campaigns_create.go +++ b/cmd/src/campaigns_create.go @@ -3,6 +3,7 @@ package main import ( "bufio" "bytes" + "context" "flag" "fmt" "io/ioutil" @@ -13,6 +14,7 @@ import ( "github.com/Masterminds/semver" "github.com/pkg/errors" + "github.com/sourcegraph/src-cli/internal/api" ) func init() { @@ -53,7 +55,7 @@ Examples: changesetsFlag = flagSet.Int("changesets", 1000, "Returns the first n changesets per campaign.") formatFlag = flagSet.String("f", "{{friendlyCampaignCreatedMessage .}}", `Format for the output, using the syntax of Go package text/template. (e.g. "{{.ID}}: {{.Name}}") or "{{.|json}}")`) - apiFlags = newAPIFlags(flagSet) + apiFlags = api.NewFlags(flagSet) ) handler := func(args []string) error { @@ -88,9 +90,12 @@ Examples: return &usageError{errors.New("campaign description cannot be blank")} } + ctx := context.Background() + client := cfg.apiClient(apiFlags, flagSet.Output()) + if *patchsetIDFlag != "" { // We only need to check for -branch if the Sourcegraph version is >= 3.13 - version, err := getSourcegraphVersion() + version, err := getSourcegraphVersion(ctx, client) if err != nil { return err } @@ -112,13 +117,7 @@ Examples: CurrentUser *User } - req := &apiRequest{ - query: currentUserIDQuery, - result: ¤tUserResult, - flags: apiFlags, - } - err := req.do() - if err != nil { + if _, err := client.NewQuery(currentUserIDQuery).Do(ctx, ¤tUserResult); err != nil { return err } if currentUserResult.CurrentUser.ID == "" { @@ -136,7 +135,7 @@ Examples: "name": name, "description": description, "namespace": namespace, - "patchSet": nullString(*patchsetIDFlag), + "patchSet": api.NullString(*patchsetIDFlag), "branch": *branchFlag, } @@ -144,18 +143,14 @@ Examples: CreateCampaign Campaign } - return (&apiRequest{ - query: campaignFragment + createcampaignMutation, - vars: map[string]interface{}{ - "input": input, - "changesetsFirst": nullInt(*changesetsFlag), - }, - result: &result, - done: func() error { - return execTemplate(tmpl, result.CreateCampaign) - }, - flags: apiFlags, - }).do() + if ok, err := client.NewRequest(campaignFragment+createcampaignMutation, map[string]interface{}{ + "input": input, + "changesetsFirst": api.NullInt(*changesetsFlag), + }).Do(ctx, &result); err != nil || !ok { + return err + } + + return execTemplate(tmpl, result.CreateCampaign) } // Register the command. @@ -283,22 +278,15 @@ const sourcegraphVersionQuery = `query SourcegraphVersion { } ` -func getSourcegraphVersion() (string, error) { +func getSourcegraphVersion(ctx context.Context, client api.Client) (string, error) { var sourcegraphVersion struct { Site struct { ProductVersion string } } - err := (&apiRequest{ - query: sourcegraphVersionQuery, - result: &sourcegraphVersion, - }).do() - if err != nil { - return "", err - } - - return sourcegraphVersion.Site.ProductVersion, nil + _, err := client.NewQuery(sourcegraphVersionQuery).Do(ctx, &sourcegraphVersion) + return sourcegraphVersion.Site.ProductVersion, err } func sourcegraphVersionCheck(version, constraint, minDate string) (bool, error) { diff --git a/cmd/src/campaigns_list.go b/cmd/src/campaigns_list.go index a1c788bd5a..cfa2c0c56a 100644 --- a/cmd/src/campaigns_list.go +++ b/cmd/src/campaigns_list.go @@ -1,9 +1,12 @@ package main import ( + "context" "flag" "fmt" "time" + + "github.com/sourcegraph/src-cli/internal/api" ) func init() { @@ -34,7 +37,7 @@ Examples: firstFlag = flagSet.Int("first", 1000, "Returns the first n campaigns.") changesetsFlag = flagSet.Int("changesets", 1000, "Returns the first n changesets per campaign.") formatFlag = flagSet.String("f", "{{.ID}}: {{.Name}}", `Format for the output, using the syntax of Go package text/template. (e.g. "{{.ID}}: {{.Name}}") or "{{.|json}}")`) - apiFlags = newAPIFlags(flagSet) + apiFlags = api.NewFlags(flagSet) ) handler := func(args []string) error { @@ -58,29 +61,27 @@ query Campaigns($first: Int, $changesetsFirst: Int) { } ` + client := cfg.apiClient(apiFlags, flagSet.Output()) + var result struct { Campaigns struct { Nodes []Campaign } } - return (&apiRequest{ - query: query, - vars: map[string]interface{}{ - "first": nullInt(*firstFlag), - "changesetsFirst": nullInt(*changesetsFlag), - }, - result: &result, - done: func() error { - for _, c := range result.Campaigns.Nodes { - if err := execTemplate(tmpl, c); err != nil { - return err - } - } - return nil - }, - flags: apiFlags, - }).do() + if ok, err := client.NewRequest(query, map[string]interface{}{ + "first": api.NullInt(*firstFlag), + "changesetsFirst": api.NullInt(*changesetsFlag), + }).Do(context.Background(), &result); err != nil || !ok { + return err + } + + for _, c := range result.Campaigns.Nodes { + if err := execTemplate(tmpl, c); err != nil { + return err + } + } + return nil } // Register the command. diff --git a/cmd/src/config.go b/cmd/src/config.go index f94766664e..239210054e 100644 --- a/cmd/src/config.go +++ b/cmd/src/config.go @@ -1,9 +1,12 @@ package main import ( + "context" "errors" "flag" "fmt" + + "github.com/sourcegraph/src-cli/internal/api" ) var configCommands commander @@ -113,39 +116,31 @@ type KeyPath struct { Index int `json:"index,omitempty"` } -func getViewerUserID() (string, error) { - var result struct { - CurrentUser *struct{ ID string } - } - req := &apiRequest{ - query: ` +func getViewerUserID(ctx context.Context, client api.Client) (string, error) { + query := ` query ViewerUserID { currentUser { id } } -`, - result: &result, +` + + var result struct { + CurrentUser *struct{ ID string } } - if err := req.do(); err != nil { + + if _, err := client.NewQuery(query).Do(ctx, &result); err != nil { return "", err } + if result.CurrentUser == nil || result.CurrentUser.ID == "" { return "", errors.New("unable to determine current user ID (see https://github.com/sourcegraph/src-cli#authentication)") } return result.CurrentUser.ID, nil } -func getSettingsSubjectLatestSettingsID(subjectID string) (*int, error) { - var result struct { - SettingsSubject *struct { - LatestSettings *struct { - ID int - } - } - } - req := &apiRequest{ - query: ` +func getSettingsSubjectLatestSettingsID(ctx context.Context, client api.Client, subjectID string) (*int, error) { + query := ` query SettingsSubjectLatestSettingsID($subject: ID!) { settingsSubject(id: $subject) { latestSettings { @@ -153,13 +148,20 @@ query SettingsSubjectLatestSettingsID($subject: ID!) { } } } -`, - vars: map[string]interface{}{"subject": subjectID}, - result: &result, +` + + var result struct { + SettingsSubject *struct { + LatestSettings *struct { + ID int + } + } } - if err := req.do(); err != nil { + + if _, err := client.NewQuery(query).Do(ctx, &result); err != nil { return nil, err } + if result.SettingsSubject == nil { return nil, fmt.Errorf("unable to find settings subject with ID %s", subjectID) } diff --git a/cmd/src/config_edit.go b/cmd/src/config_edit.go index b243f9521a..c2ab35eac0 100644 --- a/cmd/src/config_edit.go +++ b/cmd/src/config_edit.go @@ -1,10 +1,13 @@ package main import ( + "context" "errors" "flag" "fmt" "io/ioutil" + + "github.com/sourcegraph/src-cli/internal/api" ) func init() { @@ -45,7 +48,7 @@ Examples: valueFlag = flagSet.String("value", "", "The value for the settings property (when used with -property).") valueFileFlag = flagSet.String("value-file", "", "Read the value from this file instead of from the -value command-line option.") overwriteFlag = flagSet.Bool("overwrite", false, "Overwrite the entire settings with the value given in -value (not just a single property).") - apiFlags = newAPIFlags(flagSet) + apiFlags = api.NewFlags(flagSet) ) handler := func(args []string) error { @@ -74,9 +77,12 @@ Examples: return &usageError{errors.New("either -value or -value-file must be used")} } + ctx := context.Background() + client := cfg.apiClient(apiFlags, flagSet.Output()) + var subjectID string if *subjectFlag == "" { - userID, err := getViewerUserID() + userID, err := getViewerUserID(ctx, client) if err != nil { return err } @@ -85,7 +91,7 @@ Examples: subjectID = *subjectFlag } - lastID, err := getSettingsSubjectLatestSettingsID(subjectID) + lastID, err := getSettingsSubjectLatestSettingsID(ctx, client, subjectID) if err != nil { return err } @@ -116,12 +122,8 @@ mutation EditSettings($input: SettingsMutationGroupInput!, $edit: SettingsEdit!) ViewerSettings *SettingsCascade SettingsSubject *SettingsSubject } - return (&apiRequest{ - query: query, - vars: queryVars, - result: &result, - flags: apiFlags, - }).do() + _, err = client.NewRequest(query, queryVars).Do(ctx, &result) + return err } // Register the command. diff --git a/cmd/src/config_get.go b/cmd/src/config_get.go index b0f7742b9a..fa98fdf2d4 100644 --- a/cmd/src/config_get.go +++ b/cmd/src/config_get.go @@ -1,8 +1,11 @@ package main import ( + "context" "flag" "fmt" + + "github.com/sourcegraph/src-cli/internal/api" ) func init() { @@ -32,7 +35,7 @@ Examples: var ( subjectFlag = flagSet.String("subject", "", "The ID of the settings subject whose settings to get. (default: authenticated user)") formatFlag = flagSet.String("f", "{{.|jsonIndent}}", `Format for the output, using the syntax of Go package text/template. (e.g. "{{.|json}}")`) - apiFlags = newAPIFlags(flagSet) + apiFlags = api.NewFlags(flagSet) ) handler := func(args []string) error { @@ -53,29 +56,27 @@ Examples: } else { query = settingsSubjectCascadeQuery queryVars = map[string]interface{}{ - "subject": nullString(*subjectFlag), + "subject": api.NullString(*subjectFlag), } } + client := cfg.apiClient(apiFlags, flagSet.Output()) + var result struct { ViewerSettings *SettingsCascade SettingsSubject *SettingsSubject } - return (&apiRequest{ - query: query, - vars: queryVars, - result: &result, - done: func() error { - var final string - if result.ViewerSettings != nil { - final = result.ViewerSettings.Final - } else if result.SettingsSubject != nil { - final = result.SettingsSubject.SettingsCascade.Final - } - return execTemplate(tmpl, final) - }, - flags: apiFlags, - }).do() + if ok, err := client.NewRequest(query, queryVars).Do(context.Background(), &result); err != nil || !ok { + return err + } + + var final string + if result.ViewerSettings != nil { + final = result.ViewerSettings.Final + } else if result.SettingsSubject != nil { + final = result.SettingsSubject.SettingsCascade.Final + } + return execTemplate(tmpl, final) } // Register the command. diff --git a/cmd/src/config_list.go b/cmd/src/config_list.go index c1b64b9a03..a761a143f9 100644 --- a/cmd/src/config_list.go +++ b/cmd/src/config_list.go @@ -3,6 +3,9 @@ package main import ( "flag" "fmt" + + "github.com/sourcegraph/src-cli/internal/api" + "golang.org/x/net/context" ) func init() { @@ -28,7 +31,7 @@ Examples: var ( subjectFlag = flagSet.String("subject", "", "The ID of the settings subject whose settings to list. (default: authenticated user)") formatFlag = flagSet.String("f", "", `Format for the output, using the syntax of Go package text/template. (e.g. "{{.|json}}")`) - apiFlags = newAPIFlags(flagSet) + apiFlags = api.NewFlags(flagSet) ) handler := func(args []string) error { @@ -60,29 +63,27 @@ Examples: } else { query = settingsSubjectCascadeQuery queryVars = map[string]interface{}{ - "subject": nullString(*subjectFlag), + "subject": api.NullString(*subjectFlag), } } + client := cfg.apiClient(apiFlags, flagSet.Output()) + var result struct { ViewerSettings *SettingsCascade SettingsSubject *SettingsSubject } - return (&apiRequest{ - query: query, - vars: queryVars, - result: &result, - done: func() error { - var cascade *SettingsCascade - if result.ViewerSettings != nil { - cascade = result.ViewerSettings - } else if result.SettingsSubject != nil { - cascade = &result.SettingsSubject.SettingsCascade - } - return execTemplate(tmpl, cascade) - }, - flags: apiFlags, - }).do() + if ok, err := client.NewRequest(query, queryVars).Do(context.Background(), &result); err != nil || !ok { + return err + } + + var cascade *SettingsCascade + if result.ViewerSettings != nil { + cascade = result.ViewerSettings + } else if result.SettingsSubject != nil { + cascade = &result.SettingsSubject.SettingsCascade + } + return execTemplate(tmpl, cascade) } // Register the command. diff --git a/cmd/src/extensions_copy.go b/cmd/src/extensions_copy.go index 10ebde7608..b41b6ff9d1 100644 --- a/cmd/src/extensions_copy.go +++ b/cmd/src/extensions_copy.go @@ -1,11 +1,14 @@ package main import ( + "context" "flag" "fmt" "io/ioutil" "net/http" "strings" + + "github.com/sourcegraph/src-cli/internal/api" ) func withCfg(new *config, f func()) { @@ -29,7 +32,7 @@ Copy an extension from Sourcegraph.com to your private registry. var ( extensionIDFlag = flagSet.String("extension-id", "", `The in https://sourcegraph.com/extensions/ (e.g. sourcegraph/java)`) currentUserFlag = flagSet.String("current-user", "", `The current user`) - apiFlags = newAPIFlags(flagSet) + apiFlags = api.NewFlags(flagSet) ) handler := func(args []string) error { @@ -54,6 +57,10 @@ Copy an extension from Sourcegraph.com to your private registry. } extensionName := extensionIDParts[1] + ctx := context.Background() + client := cfg.apiClient(apiFlags, flagSet.Output()) + ok := false + var extensionResult struct { ExtensionRegistry struct { Extension struct { @@ -66,8 +73,7 @@ Copy an extension from Sourcegraph.com to your private registry. } withCfg(&config{Endpoint: "https://sourcegraph.com"}, func() { - err = (&apiRequest{ - query: `query GetExtension( + query := `query GetExtension( $extensionID: String! ){ extensionRegistry{ @@ -78,15 +84,13 @@ Copy an extension from Sourcegraph.com to your private registry. } } } -}`, - vars: map[string]interface{}{ - "extensionID": extensionID, - }, - result: &extensionResult, - flags: apiFlags, - }).do() +}` + + ok, err = client.NewRequest(query, map[string]interface{}{ + "extensionID": extensionID, + }).Do(ctx, &extensionResult) }) - if err != nil { + if err != nil || !ok { return err } @@ -108,18 +112,7 @@ Copy an extension from Sourcegraph.com to your private registry. fmt.Printf("bundle: %s\n", string(bundle[0:100])) fmt.Printf("manifest: %s\n", string(manifest[0:])) - var publishResult struct { - ExtensionRegistry struct { - PublishExtension struct { - Extension struct { - ExtensionID string - URL string - } - } - } - } - return (&apiRequest{ - query: `mutation PublishExtension( + query := `mutation PublishExtension( $extensionID: String!, $manifest: String!, $bundle: String, @@ -136,22 +129,31 @@ Copy an extension from Sourcegraph.com to your private registry. } } } -}`, - vars: map[string]interface{}{ - "extensionID": currentUser + "/" + extensionName, - "manifest": string(manifest), - "bundle": bundle, - }, - result: &publishResult, - done: func() error { - fmt.Println("Extension published!") - fmt.Println() - fmt.Printf("\tExtension ID: %s\n\n", publishResult.ExtensionRegistry.PublishExtension.Extension.ExtensionID) - fmt.Printf("View, enable, and configure it at: %s\n", cfg.Endpoint+publishResult.ExtensionRegistry.PublishExtension.Extension.URL) - return nil - }, - flags: apiFlags, - }).do() +}` + + var publishResult struct { + ExtensionRegistry struct { + PublishExtension struct { + Extension struct { + ExtensionID string + URL string + } + } + } + } + if ok, err := client.NewRequest(query, map[string]interface{}{ + "extensionID": currentUser + "/" + extensionName, + "manifest": string(manifest), + "bundle": bundle, + }).Do(ctx, &publishResult); err != nil || !ok { + return err + } + + fmt.Println("Extension published!") + fmt.Println() + fmt.Printf("\tExtension ID: %s\n\n", publishResult.ExtensionRegistry.PublishExtension.Extension.ExtensionID) + fmt.Printf("View, enable, and configure it at: %s\n", cfg.Endpoint+publishResult.ExtensionRegistry.PublishExtension.Extension.URL) + return nil } // Register the command. diff --git a/cmd/src/extensions_delete.go b/cmd/src/extensions_delete.go index ef324f4d47..863c2c655b 100644 --- a/cmd/src/extensions_delete.go +++ b/cmd/src/extensions_delete.go @@ -1,8 +1,11 @@ package main import ( + "context" "flag" "fmt" + + "github.com/sourcegraph/src-cli/internal/api" ) func init() { @@ -27,7 +30,7 @@ Examples: } var ( extensionIDFlag = flagSet.String("id", "", `The ID (GraphQL API ID, not extension ID) of the extension to delete.`) - apiFlags = newAPIFlags(flagSet) + apiFlags = api.NewFlags(flagSet) ) handler := func(args []string) error { @@ -36,6 +39,8 @@ Examples: return err } + client := cfg.apiClient(apiFlags, flagSet.Output()) + query := `mutation DeleteExtension( $extension: ID! ) { @@ -53,18 +58,14 @@ Examples: DeleteExtension struct{} } } - return (&apiRequest{ - query: query, - vars: map[string]interface{}{ - "extension": *extensionIDFlag, - }, - result: &result, - done: func() error { - fmt.Printf("Extension with ID %q deleted.\n", *extensionIDFlag) - return nil - }, - flags: apiFlags, - }).do() + if ok, err := client.NewRequest(query, map[string]interface{}{ + "extension": *extensionIDFlag, + }).Do(context.Background(), &result); err != nil || !ok { + return err + } + + fmt.Printf("Extension with ID %q deleted.\n", *extensionIDFlag) + return nil } // Register the command. diff --git a/cmd/src/extensions_get.go b/cmd/src/extensions_get.go index e286c4cfb8..664d4a84a4 100644 --- a/cmd/src/extensions_get.go +++ b/cmd/src/extensions_get.go @@ -1,8 +1,11 @@ package main import ( + "context" "flag" "fmt" + + "github.com/sourcegraph/src-cli/internal/api" ) func init() { @@ -25,7 +28,7 @@ Examples: var ( extensionIDFlag = flagSet.String("extension-id", "", `Look up extension by extension ID. (e.g. "alice/myextension")`) formatFlag = flagSet.String("f", "{{.|json}}", `Format for the output, using the syntax of Go package text/template. (e.g. "{{.ExtensionID}}: {{.Manifest.Title}} ({{.RemoteURL}})" or "{{.|json}}")`) - apiFlags = newAPIFlags(flagSet) + apiFlags = api.NewFlags(flagSet) ) handler := func(args []string) error { @@ -36,6 +39,8 @@ Examples: return err } + client := cfg.apiClient(apiFlags, flagSet.Output()) + query := `query RegistryExtension( $extensionID: String!, ) { @@ -58,17 +63,13 @@ Examples: Extension *Extension } } - return (&apiRequest{ - query: query, - vars: map[string]interface{}{ - "extensionID": extensionID, - }, - result: &result, - done: func() error { - return execTemplate(tmpl, result.ExtensionRegistry.Extension) - }, - flags: apiFlags, - }).do() + if ok, err := client.NewRequest(query, map[string]interface{}{ + "extensionID": extensionID, + }).Do(context.Background(), &result); err != nil || !ok { + return err + } + + return execTemplate(tmpl, result.ExtensionRegistry.Extension) } // Register the command. diff --git a/cmd/src/extensions_list.go b/cmd/src/extensions_list.go index fa88de164c..b6cbe5dea6 100644 --- a/cmd/src/extensions_list.go +++ b/cmd/src/extensions_list.go @@ -1,8 +1,11 @@ package main import ( + "context" "flag" "fmt" + + "github.com/sourcegraph/src-cli/internal/api" ) func init() { @@ -33,7 +36,7 @@ Examples: firstFlag = flagSet.Int("first", 1000, "Returns the first n extensions from the list. (use -1 for unlimited)") queryFlag = flagSet.String("query", "", `Returns extensions whose extension IDs match the query. (e.g. "myextension")`) formatFlag = flagSet.String("f", "{{.ExtensionID}}", `Format for the output, using the syntax of Go package text/template. (e.g. "{{.ExtensionID}}: {{.Manifest.Description}} ({{.RemoteURL}})" or "{{.|json}}")`) - apiFlags = newAPIFlags(flagSet) + apiFlags = api.NewFlags(flagSet) ) handler := func(args []string) error { @@ -44,6 +47,8 @@ Examples: return err } + client := cfg.apiClient(apiFlags, flagSet.Output()) + query := `query RegistryExtensions( $first: Int, $query: String, @@ -67,23 +72,19 @@ Examples: } } } - return (&apiRequest{ - query: query, - vars: map[string]interface{}{ - "first": nullInt(*firstFlag), - "query": nullString(*queryFlag), - }, - result: &result, - done: func() error { - for _, extension := range result.ExtensionRegistry.Extensions.Nodes { - if err := execTemplate(tmpl, extension); err != nil { - return err - } - } - return nil - }, - flags: apiFlags, - }).do() + if ok, err := client.NewRequest(query, map[string]interface{}{ + "first": api.NullInt(*firstFlag), + "query": api.NullString(*queryFlag), + }).Do(context.Background(), &result); err != nil || !ok { + return err + } + + for _, extension := range result.ExtensionRegistry.Extensions.Nodes { + if err := execTemplate(tmpl, extension); err != nil { + return err + } + } + return nil } // Register the command. diff --git a/cmd/src/extensions_publish.go b/cmd/src/extensions_publish.go index 712537b4c2..46ac856780 100644 --- a/cmd/src/extensions_publish.go +++ b/cmd/src/extensions_publish.go @@ -1,6 +1,7 @@ package main import ( + "context" "encoding/json" "errors" "flag" @@ -10,6 +11,8 @@ import ( "os/exec" "path/filepath" "strings" + + "github.com/sourcegraph/src-cli/internal/api" ) func init() { @@ -48,7 +51,7 @@ Notes: urlFlag = flagSet.String("url", "", `Override the URL for the bundle. (example: set to http://localhost:1234/myext.js for local dev with parcel)`) manifestFlag = flagSet.String("manifest", "package.json", `The extension manifest file.`) forceFlag = flagSet.Bool("force", false, `Force publish the extension, even if there are validation problems or other warnings.`) - apiFlags = newAPIFlags(flagSet) + apiFlags = api.NewFlags(flagSet) ) handler := func(args []string) error { @@ -93,6 +96,8 @@ Notes: } } + client := cfg.apiClient(apiFlags, flagSet.Output()) + query := `mutation PublishExtension( $extensionID: String!, $manifest: String!, @@ -126,25 +131,21 @@ Notes: } } } - return (&apiRequest{ - query: query, - vars: map[string]interface{}{ - "extensionID": extensionID, - "manifest": string(manifest), - "bundle": bundle, - "sourceMap": sourceMap, - "force": *forceFlag, - }, - result: &result, - done: func() error { - fmt.Println("Extension published!") - fmt.Println() - fmt.Printf("\tExtension ID: %s\n\n", result.ExtensionRegistry.PublishExtension.Extension.ExtensionID) - fmt.Printf("View, enable, and configure it at: %s\n", cfg.Endpoint+result.ExtensionRegistry.PublishExtension.Extension.URL) - return nil - }, - flags: apiFlags, - }).do() + if ok, err := client.NewRequest(query, map[string]interface{}{ + "extensionID": extensionID, + "manifest": string(manifest), + "bundle": bundle, + "sourceMap": sourceMap, + "force": *forceFlag, + }).Do(context.Background(), &result); err != nil || !ok { + return err + } + + fmt.Println("Extension published!") + fmt.Println() + fmt.Printf("\tExtension ID: %s\n\n", result.ExtensionRegistry.PublishExtension.Extension.ExtensionID) + fmt.Printf("View, enable, and configure it at: %s\n", cfg.Endpoint+result.ExtensionRegistry.PublishExtension.Extension.URL) + return nil } // Register the command. diff --git a/cmd/src/extsvc.go b/cmd/src/extsvc.go index 72f28dc3ce..d44aab88e9 100644 --- a/cmd/src/extsvc.go +++ b/cmd/src/extsvc.go @@ -1,9 +1,12 @@ package main import ( + "context" "errors" "flag" "fmt" + + "github.com/sourcegraph/src-cli/internal/api" ) var extsvcCommands commander @@ -48,22 +51,18 @@ type externalService struct { CreatedAt, UpdatedAt string } -func lookupExternalService(byID, byName string) (*externalService, error) { +func lookupExternalService(ctx context.Context, client api.Client, byID, byName string) (*externalService, error) { var result struct { ExternalServices struct { Nodes []*externalService } } - err := (&apiRequest{ - query: externalServicesListQuery, - vars: map[string]interface{}{ - "first": 99999, - }, - result: &result, - }).do() - if err != nil { + if ok, err := client.NewRequest(externalServicesListQuery, map[string]interface{}{ + "first": 99999, + }).Do(ctx, &result); err != nil || !ok { return nil, err } + for _, svc := range result.ExternalServices.Nodes { if byID != "" && svc.ID == byID { return svc, nil diff --git a/cmd/src/extsvc_edit.go b/cmd/src/extsvc_edit.go index 504b356fa1..90d1df0a9d 100644 --- a/cmd/src/extsvc_edit.go +++ b/cmd/src/extsvc_edit.go @@ -1,6 +1,7 @@ package main import ( + "context" "encoding/json" "errors" "flag" @@ -11,6 +12,7 @@ import ( isatty "github.com/mattn/go-isatty" "github.com/sourcegraph/jsonx" + "github.com/sourcegraph/src-cli/internal/api" ) func init() { @@ -42,19 +44,22 @@ Examples: idFlag = flagSet.String("id", "", "ID of the external service to edit") renameFlag = flagSet.String("rename", "", "when specified, renames the external service") excludeRepositoriesFlag = flagSet.String("exclude-repos", "", "when specified, add these repositories to the exclusion list") - apiFlags = newAPIFlags(flagSet) + apiFlags = api.NewFlags(flagSet) ) handler := func(args []string) (err error) { flagSet.Parse(args) + ctx := context.Background() + client := cfg.apiClient(apiFlags, flagSet.Output()) + // Determine ID of external service we will edit. if *nameFlag == "" && *idFlag == "" { return &usageError{errors.New("one of -name or -id flag must be specified")} } id := *idFlag if id == "" { - svc, err := lookupExternalService("", *nameFlag) + svc, err := lookupExternalService(ctx, client, "", *nameFlag) if err != nil { return err } @@ -80,7 +85,7 @@ Examples: if *excludeRepositoriesFlag != "" { if len(updateJSON) == 0 { // We need to fetch the current JSON then. - svc, err := lookupExternalService(id, "") + svc, err := lookupExternalService(ctx, client, id, "") if err != nil { return err } @@ -110,20 +115,15 @@ Examples: "input": updateExternalServiceInput, } var result struct{} // TODO: future: allow formatting resulting external service - err = (&apiRequest{ - query: externalServicesUpdateMutation, - vars: queryVars, - result: &result, - done: func() error { - fmt.Println("External service updated:", id) - return nil - }, - flags: apiFlags, - }).do() - if err != nil && strings.Contains(err.Error(), "Additional property exclude is not allowed") { - return errors.New(`specified external service does not support repository "exclude" list`) + if ok, err := client.NewRequest(externalServicesUpdateMutation, queryVars).Do(ctx, &result); err != nil { + if strings.Contains(err.Error(), "Additional property exclude is not allowed") { + return errors.New(`specified external service does not support repository "exclude" list`) + } + return err + } else if ok { + fmt.Println("External service updated:", id) } - return err + return nil } // Register the command. diff --git a/cmd/src/extsvc_list.go b/cmd/src/extsvc_list.go index da2c0e4677..ec495df60f 100644 --- a/cmd/src/extsvc_list.go +++ b/cmd/src/extsvc_list.go @@ -1,8 +1,11 @@ package main import ( + "context" "flag" "fmt" + + "github.com/sourcegraph/src-cli/internal/api" ) func init() { @@ -28,7 +31,7 @@ Examples: var ( firstFlag = flagSet.Int("first", -1, "Return only the first n external services. (use -1 for unlimited)") formatFlag = flagSet.String("f", "", `Format for the output, using the syntax of Go package text/template. (e.g. "{{.|json}}")`) - apiFlags = newAPIFlags(flagSet) + apiFlags = api.NewFlags(flagSet) ) handler := func(args []string) error { @@ -51,19 +54,17 @@ Examples: return err } + ctx := context.Background() + client := cfg.apiClient(apiFlags, flagSet.Output()) + queryVars := map[string]interface{}{ "first": first, } var result externalServicesListResult - return (&apiRequest{ - query: externalServicesListQuery, - vars: queryVars, - result: &result, - done: func() error { - return execTemplate(tmpl, result.ExternalServices) - }, - flags: apiFlags, - }).do() + if ok, err := client.NewRequest(externalServicesListQuery, queryVars).Do(ctx, &result); err != nil || !ok { + return err + } + return execTemplate(tmpl, result.ExternalServices) } // Register the command. diff --git a/cmd/src/format.go b/cmd/src/format.go index 6ddfc3c8da..93b2367e28 100644 --- a/cmd/src/format.go +++ b/cmd/src/format.go @@ -2,6 +2,7 @@ package main import ( "bytes" + "context" "encoding/json" "fmt" "net/url" @@ -106,7 +107,7 @@ func parseTemplate(text string) (*template.Template, error) { // Hacky to do this in a formatting helper, but better than // globally querying the version and only using it here for now. - version, err := getSourcegraphVersion() + version, err := getSourcegraphVersion(context.Background(), cfg.apiClient(nil, os.Stdout)) if err != nil { // We ignore the error and return what we have return buf.String() diff --git a/cmd/src/main.go b/cmd/src/main.go index 196e122f67..9f7033150f 100644 --- a/cmd/src/main.go +++ b/cmd/src/main.go @@ -3,6 +3,7 @@ package main import ( "encoding/json" "flag" + "io" "io/ioutil" "log" "os" @@ -11,6 +12,7 @@ import ( "strings" "github.com/pkg/errors" + "github.com/sourcegraph/src-cli/internal/api" ) const usageText = `src is a tool that provides access to Sourcegraph instances. @@ -76,6 +78,17 @@ type config struct { AdditionalHeaders map[string]string `json:"additionalHeaders"` } +// apiClient returns an api.Client built from the configuration. +func (c *config) apiClient(flags *api.Flags, out io.Writer) api.Client { + return api.NewClient(api.ClientOpts{ + Endpoint: c.Endpoint, + AccessToken: c.AccessToken, + AdditionalHeaders: c.AdditionalHeaders, + Flags: flags, + Out: out, + }) +} + // readConfig reads the config file from the given path. func readConfig() (*config, error) { cfgPath := *configPath diff --git a/cmd/src/orgs_create.go b/cmd/src/orgs_create.go index 2045a677a4..8c1075b6fb 100644 --- a/cmd/src/orgs_create.go +++ b/cmd/src/orgs_create.go @@ -1,8 +1,11 @@ package main import ( + "context" "flag" "fmt" + + "github.com/sourcegraph/src-cli/internal/api" ) func init() { @@ -24,12 +27,14 @@ Examples: var ( nameFlag = flagSet.String("name", "", `The new organization's name. (required)`) displayNameFlag = flagSet.String("display-name", "", `The new organization's display name.`) - apiFlags = newAPIFlags(flagSet) + apiFlags = api.NewFlags(flagSet) ) handler := func(args []string) error { flagSet.Parse(args) + client := cfg.apiClient(apiFlags, flagSet.Output()) + query := `mutation CreateOrg( $name: String!, $displayName: String!, @@ -45,19 +50,15 @@ Examples: var result struct { CreateOrg Org } - return (&apiRequest{ - query: query, - vars: map[string]interface{}{ - "name": *nameFlag, - "displayName": *displayNameFlag, - }, - result: &result, - done: func() error { - fmt.Printf("Organization %q created.\n", *nameFlag) - return nil - }, - flags: apiFlags, - }).do() + if ok, err := client.NewRequest(query, map[string]interface{}{ + "name": *nameFlag, + "displayName": *displayNameFlag, + }).Do(context.Background(), &result); err != nil || !ok { + return err + } + + fmt.Printf("Organization %q created.\n", *nameFlag) + return nil } // Register the command. diff --git a/cmd/src/orgs_delete.go b/cmd/src/orgs_delete.go index 21d3f00680..0f349bca8e 100644 --- a/cmd/src/orgs_delete.go +++ b/cmd/src/orgs_delete.go @@ -1,8 +1,11 @@ package main import ( + "context" "flag" "fmt" + + "github.com/sourcegraph/src-cli/internal/api" ) func init() { @@ -31,12 +34,14 @@ Examples: } var ( orgIDFlag = flagSet.String("id", "", `The ID of the organization to delete.`) - apiFlags = newAPIFlags(flagSet) + apiFlags = api.NewFlags(flagSet) ) handler := func(args []string) error { flagSet.Parse(args) + client := cfg.apiClient(apiFlags, flagSet.Output()) + query := `mutation DeleteOrganization( $organization: ID! ) { @@ -50,18 +55,14 @@ Examples: var result struct { DeleteOrganization struct{} } - return (&apiRequest{ - query: query, - vars: map[string]interface{}{ - "organization": *orgIDFlag, - }, - result: &result, - done: func() error { - fmt.Printf("Organization with ID %q deleted.\n", *orgIDFlag) - return nil - }, - flags: apiFlags, - }).do() + if ok, err := client.NewRequest(query, map[string]interface{}{ + "organization": *orgIDFlag, + }).Do(context.Background(), &result); err != nil || !ok { + return err + } + + fmt.Printf("Organization with ID %q deleted.\n", *orgIDFlag) + return nil } // Register the command. diff --git a/cmd/src/orgs_get.go b/cmd/src/orgs_get.go index cb2e10f96e..424fee35fd 100644 --- a/cmd/src/orgs_get.go +++ b/cmd/src/orgs_get.go @@ -1,8 +1,11 @@ package main import ( + "context" "flag" "fmt" + + "github.com/sourcegraph/src-cli/internal/api" ) func init() { @@ -28,12 +31,14 @@ Examples: var ( nameFlag = flagSet.String("name", "", `Look up organization by name. (e.g. "abc-org")`) formatFlag = flagSet.String("f", "{{.|json}}", `Format for the output, using the syntax of Go package text/template. (e.g. "{{.ID}}: {{.Name}} ({{.DisplayName}})")`) - apiFlags = newAPIFlags(flagSet) + apiFlags = api.NewFlags(flagSet) ) handler := func(args []string) error { flagSet.Parse(args) + client := cfg.apiClient(apiFlags, flagSet.Output()) + tmpl, err := parseTemplate(*formatFlag) if err != nil { return err @@ -52,17 +57,13 @@ Examples: var result struct { Organization *Org } - return (&apiRequest{ - query: query, - vars: map[string]interface{}{ - "name": *nameFlag, - }, - result: &result, - done: func() error { - return execTemplate(tmpl, result.Organization) - }, - flags: apiFlags, - }).do() + if ok, err := client.NewRequest(query, map[string]interface{}{ + "name": *nameFlag, + }).Do(context.Background(), &result); err != nil || !ok { + return err + } + + return execTemplate(tmpl, result.Organization) } // Register the command. diff --git a/cmd/src/orgs_list.go b/cmd/src/orgs_list.go index f48230c2fb..0acb9f3c9f 100644 --- a/cmd/src/orgs_list.go +++ b/cmd/src/orgs_list.go @@ -1,8 +1,11 @@ package main import ( + "context" "flag" "fmt" + + "github.com/sourcegraph/src-cli/internal/api" ) func init() { @@ -33,12 +36,14 @@ Examples: firstFlag = flagSet.Int("first", 1000, "Returns the first n organizations from the list. (use -1 for unlimited)") queryFlag = flagSet.String("query", "", `Returns organizations whose names match the query. (e.g. "alice")`) formatFlag = flagSet.String("f", "{{.Name}}", `Format for the output, using the syntax of Go package text/template. (e.g. "{{.ID}}: {{.Name}} ({{.DisplayName}})" or "{{.|json}}")`) - apiFlags = newAPIFlags(flagSet) + apiFlags = api.NewFlags(flagSet) ) handler := func(args []string) error { flagSet.Parse(args) + client := cfg.apiClient(apiFlags, flagSet.Output()) + tmpl, err := parseTemplate(*formatFlag) if err != nil { return err @@ -63,23 +68,19 @@ Examples: Nodes []Org } } - return (&apiRequest{ - query: query, - vars: map[string]interface{}{ - "first": nullInt(*firstFlag), - "query": nullString(*queryFlag), - }, - result: &result, - done: func() error { - for _, org := range result.Organizations.Nodes { - if err := execTemplate(tmpl, org); err != nil { - return err - } - } - return nil - }, - flags: apiFlags, - }).do() + if ok, err := client.NewRequest(query, map[string]interface{}{ + "first": api.NullInt(*firstFlag), + "query": api.NullString(*queryFlag), + }).Do(context.Background(), &result); err != nil || !ok { + return err + } + + for _, org := range result.Organizations.Nodes { + if err := execTemplate(tmpl, org); err != nil { + return err + } + } + return nil } // Register the command. diff --git a/cmd/src/orgs_members_add.go b/cmd/src/orgs_members_add.go index a9f67e5e42..e313abab93 100644 --- a/cmd/src/orgs_members_add.go +++ b/cmd/src/orgs_members_add.go @@ -1,8 +1,11 @@ package main import ( + "context" "flag" "fmt" + + "github.com/sourcegraph/src-cli/internal/api" ) func init() { @@ -24,12 +27,14 @@ Examples: var ( orgIDFlag = flagSet.String("org-id", "", "ID of organization to which to add member. (required)") usernameFlag = flagSet.String("username", "", "Username of user to add as member. (required)") - apiFlags = newAPIFlags(flagSet) + apiFlags = api.NewFlags(flagSet) ) handler := func(args []string) error { flagSet.Parse(args) + client := cfg.apiClient(apiFlags, flagSet.Output()) + query := `mutation AddUserToOrganization( $organization: ID!, $username: String!, @@ -45,19 +50,15 @@ Examples: var result struct { AddUserToOrganization struct{} } - return (&apiRequest{ - query: query, - vars: map[string]interface{}{ - "organization": *orgIDFlag, - "username": *usernameFlag, - }, - result: &result, - done: func() error { - fmt.Printf("User %q added as member to organization with ID %q.\n", *usernameFlag, *orgIDFlag) - return nil - }, - flags: apiFlags, - }).do() + if ok, err := client.NewRequest(query, map[string]interface{}{ + "organization": *orgIDFlag, + "username": *usernameFlag, + }).Do(context.Background(), &result); err != nil || !ok { + return err + } + + fmt.Printf("User %q added as member to organization with ID %q.\n", *usernameFlag, *orgIDFlag) + return nil } // Register the command. diff --git a/cmd/src/orgs_members_remove.go b/cmd/src/orgs_members_remove.go index ad25bad905..5bc9d34299 100644 --- a/cmd/src/orgs_members_remove.go +++ b/cmd/src/orgs_members_remove.go @@ -1,8 +1,11 @@ package main import ( + "context" "flag" "fmt" + + "github.com/sourcegraph/src-cli/internal/api" ) func init() { @@ -23,12 +26,14 @@ Examples: var ( orgIDFlag = flagSet.String("org-id", "", "ID of organization from which to remove member. (required)") userIDFlag = flagSet.String("user-id", "", "ID of user to remove as member. (required)") - apiFlags = newAPIFlags(flagSet) + apiFlags = api.NewFlags(flagSet) ) handler := func(args []string) error { flagSet.Parse(args) + client := cfg.apiClient(apiFlags, flagSet.Output()) + query := `mutation RemoveUserFromOrg( $orgID: ID!, $userID: ID!, @@ -44,19 +49,15 @@ Examples: var result struct { RemoveUserFromOrg struct{} } - return (&apiRequest{ - query: query, - vars: map[string]interface{}{ - "orgID": *orgIDFlag, - "userID": *userIDFlag, - }, - result: &result, - done: func() error { - fmt.Printf("User %q removed as member from organization with ID %q.\n", *userIDFlag, *orgIDFlag) - return nil - }, - flags: apiFlags, - }).do() + if ok, err := client.NewRequest(query, map[string]interface{}{ + "orgID": *orgIDFlag, + "userID": *userIDFlag, + }).Do(context.Background(), &result); err != nil || !ok { + return err + } + + fmt.Printf("User %q removed as member from organization with ID %q.\n", *userIDFlag, *orgIDFlag) + return nil } // Register the command. diff --git a/cmd/src/patch_sets_create_from_patches.go b/cmd/src/patch_sets_create_from_patches.go index 86113f6da3..3c4f96dc8e 100644 --- a/cmd/src/patch_sets_create_from_patches.go +++ b/cmd/src/patch_sets_create_from_patches.go @@ -1,6 +1,7 @@ package main import ( + "context" "encoding/json" "flag" "fmt" @@ -10,6 +11,7 @@ import ( "github.com/mattn/go-isatty" "github.com/pkg/errors" + "github.com/sourcegraph/src-cli/internal/api" "github.com/sourcegraph/src-cli/internal/campaigns" ) @@ -48,7 +50,7 @@ Examples: var ( patchesFlag = flagSet.Int("patches", 1000, "Returns the first n patches in the patch set.") formatFlag = flagSet.String("f", "{{friendlyPatchSetCreatedMessage .}}", `Format for the output, using the syntax of Go package text/template. (e.g. "{{.ID}}: {{len .Patches}} patches") or "{{.|json}}")`) - apiFlags = newAPIFlags(flagSet) + apiFlags = api.NewFlags(flagSet) ) handler := func(args []string) error { @@ -68,7 +70,10 @@ Examples: return errors.Wrap(err, "invalid JSON patches input") } - return createPatchSetFromPatches(apiFlags, patches, tmpl, *patchesFlag) + ctx := context.Background() + client := cfg.apiClient(apiFlags, flagSet.Output()) + + return createPatchSetFromPatches(ctx, client, patches, tmpl, *patchesFlag) } // Register the command. @@ -88,7 +93,8 @@ mutation CreatePatchSetFromPatches($patches: [PatchInput!]!) { ` func createPatchSetFromPatches( - apiFlags *apiFlags, + ctx context.Context, + client api.Client, patches []campaigns.PatchInput, tmpl *template.Template, numChangesets int, @@ -99,7 +105,7 @@ func createPatchSetFromPatches( CreatePatchSetFromPatches PatchSet } - version, err := getSourcegraphVersion() + version, err := getSourcegraphVersion(ctx, client) if err != nil { return err } @@ -125,16 +131,11 @@ func createPatchSetFromPatches( patches = patchesWithoutBaseRef } - return (&apiRequest{ - query: query, - vars: map[string]interface{}{"patches": patches}, - result: &result, - done: func() error { - if err := execTemplate(tmpl, result.CreatePatchSetFromPatches); err != nil { - return err - } - return nil - }, - flags: apiFlags, - }).do() + if ok, err := client.NewRequest(query, map[string]interface{}{ + "patches": patches, + }).Do(ctx, &result); err != nil || !ok { + return err + } + + return execTemplate(tmpl, result.CreatePatchSetFromPatches) } diff --git a/cmd/src/repos_delete.go b/cmd/src/repos_delete.go index d0b0730d8a..98445442d0 100644 --- a/cmd/src/repos_delete.go +++ b/cmd/src/repos_delete.go @@ -1,16 +1,18 @@ package main import ( + "context" "flag" "fmt" multierror "github.com/hashicorp/go-multierror" "github.com/pkg/errors" + "github.com/sourcegraph/src-cli/internal/api" ) func init() { flagSet := flag.NewFlagSet("delete", flag.ExitOnError) - apiFlags := newAPIFlags(flagSet) + apiFlags := api.NewFlags(flagSet) printUsage := func() { fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src repos %s'\n", flagSet.Name()) @@ -27,8 +29,8 @@ Examples: fmt.Fprint(flag.CommandLine.Output(), examples) } - deleteRepository := func(repoName string) error { - repoID, err := fetchRepositoryID(repoName) + deleteRepository := func(ctx context.Context, client api.Client, repoName string) error { + repoID, err := fetchRepositoryID(ctx, client, repoName) if err != nil { return err } @@ -39,25 +41,25 @@ Examples: } }` var result struct{} - return (&apiRequest{ - query: query, - vars: map[string]interface{}{ - "repoID": repoID, - }, - result: &result, - done: func() error { - fmt.Fprintf(flag.CommandLine.Output(), "Repository %q deleted\n", repoName) - return nil - }, - flags: apiFlags, - }).do() + if ok, err := client.NewRequest(query, map[string]interface{}{ + "repoID": repoID, + }).Do(ctx, &result); err != nil || !ok { + return err + } + + fmt.Fprintf(flag.CommandLine.Output(), "Repository %q deleted\n", repoName) + return nil } deleteRepositories := func(args []string) error { flagSet.Parse(args) + + ctx := context.Background() + client := cfg.apiClient(apiFlags, flagSet.Output()) + var errs *multierror.Error for _, repoName := range flagSet.Args() { - err := deleteRepository(repoName) + err := deleteRepository(ctx, client, repoName) if err != nil { err = errors.Wrapf(err, "Failed to delete repository %q", repoName) errs = multierror.Append(errs, err) diff --git a/cmd/src/repos_enable_disable.go b/cmd/src/repos_enable_disable.go index 1a088a3cc9..3bd74fcbd4 100644 --- a/cmd/src/repos_enable_disable.go +++ b/cmd/src/repos_enable_disable.go @@ -1,11 +1,13 @@ package main import ( + "context" "flag" "fmt" multierror "github.com/hashicorp/go-multierror" "github.com/pkg/errors" + "github.com/sourcegraph/src-cli/internal/api" ) func init() { @@ -35,10 +37,10 @@ func initReposEnableDisable(cmdName string, enable bool, usage string) { flagSet.PrintDefaults() fmt.Println(usage) } - apiFlags := newAPIFlags(flagSet) + apiFlags := api.NewFlags(flagSet) - setRepositoryEnabled := func(repoName string, enabled bool) error { - repoID, err := fetchRepositoryID(repoName) + setRepositoryEnabled := func(ctx context.Context, client api.Client, repoName string, enabled bool) error { + repoID, err := fetchRepositoryID(ctx, client, repoName) if err != nil { return err } @@ -50,27 +52,26 @@ func initReposEnableDisable(cmdName string, enable bool, usage string) { }` var result struct{} - return (&apiRequest{ - query: query, - vars: map[string]interface{}{ - "repoID": repoID, - "enabled": enabled, - }, - result: &result, - done: func() error { - fmt.Printf("repository %sd: %s\n", cmdName, repoName) - return nil - }, - flags: apiFlags, - }).do() + if ok, err := client.NewRequest(query, map[string]interface{}{ + "repoID": repoID, + "enabled": enabled, + }).Do(ctx, &result); err != nil || !ok { + return err + } + + fmt.Printf("repository %sd: %s\n", cmdName, repoName) + return nil } handler := func(args []string) error { flagSet.Parse(args) + ctx := context.Background() + client := cfg.apiClient(apiFlags, flagSet.Output()) + var errs *multierror.Error for _, repoName := range flagSet.Args() { - if err := setRepositoryEnabled(repoName, enable); err != nil { + if err := setRepositoryEnabled(ctx, client, repoName, enable); err != nil { err = errors.Wrapf(err, "Failed to %s repository %q", cmdName, repoName) errs = multierror.Append(errs, err) } @@ -86,7 +87,7 @@ func initReposEnableDisable(cmdName string, enable bool, usage string) { }) } -func fetchRepositoryID(repoName string) (string, error) { +func fetchRepositoryID(ctx context.Context, client api.Client, repoName string) (string, error) { query := `query RepositoryID($repoName: String!) { repository(name: $repoName) { id @@ -98,14 +99,9 @@ func fetchRepositoryID(repoName string) (string, error) { ID string } } - err := (&apiRequest{ - query: query, - vars: map[string]interface{}{ - "repoName": repoName, - }, - result: &result, - }).do() - if err != nil { + if ok, err := client.NewRequest(query, map[string]interface{}{ + "repoName": repoName, + }).Do(ctx, &result); err != nil || !ok { return "", err } if result.Repository.ID == "" { diff --git a/cmd/src/repos_get.go b/cmd/src/repos_get.go index cd45b92690..026b70ab30 100644 --- a/cmd/src/repos_get.go +++ b/cmd/src/repos_get.go @@ -1,8 +1,11 @@ package main import ( + "context" "flag" "fmt" + + "github.com/sourcegraph/src-cli/internal/api" ) func init() { @@ -24,12 +27,14 @@ Examples: var ( nameFlag = flagSet.String("name", "", "The name of the repository. (required)") formatFlag = flagSet.String("f", "{{.ID}}", `Format for the output, using the syntax of Go package text/template. (e.g. "{{.ID}}: {{.Name}}") or "{{.|json}}")`) - apiFlags = newAPIFlags(flagSet) + apiFlags = api.NewFlags(flagSet) ) handler := func(args []string) error { flagSet.Parse(args) + client := cfg.apiClient(apiFlags, flagSet.Output()) + tmpl, err := parseTemplate(*formatFlag) if err != nil { return err @@ -49,20 +54,13 @@ Examples: var result struct { Repository Repository } - return (&apiRequest{ - query: query, - vars: map[string]interface{}{ - "name": *nameFlag, - }, - result: &result, - done: func() error { - if err := execTemplate(tmpl, result.Repository); err != nil { - return err - } - return nil - }, - flags: apiFlags, - }).do() + if ok, err := client.NewRequest(query, map[string]interface{}{ + "name": *nameFlag, + }).Do(context.Background(), &result); err != nil || !ok { + return err + } + + return execTemplate(tmpl, result.Repository) } // Register the command. diff --git a/cmd/src/repos_list.go b/cmd/src/repos_list.go index 401c433503..855d5db9c9 100644 --- a/cmd/src/repos_list.go +++ b/cmd/src/repos_list.go @@ -1,9 +1,12 @@ package main import ( + "context" "flag" "fmt" "strings" + + "github.com/sourcegraph/src-cli/internal/api" ) func init() { @@ -46,12 +49,14 @@ Examples: descendingFlag = flagSet.Bool("descending", false, "Whether or not results should be in descending order.") namesWithoutHostFlag = flagSet.Bool("names-without-host", false, "Whether or not repository names should be printed without the hostname (or other first path component). If set, -f is ignored.") formatFlag = flagSet.String("f", "{{.Name}}", `Format for the output, using the syntax of Go package text/template. (e.g. "{{.ID}}: {{.Name}}") or "{{.|json}}")`) - apiFlags = newAPIFlags(flagSet) + apiFlags = api.NewFlags(flagSet) ) handler := func(args []string) error { flagSet.Parse(args) + client := cfg.apiClient(apiFlags, flagSet.Output()) + tmpl, err := parseTemplate(*formatFlag) if err != nil { return err @@ -99,35 +104,31 @@ Examples: Nodes []Repository } } - return (&apiRequest{ - query: query, - vars: map[string]interface{}{ - "first": nullInt(*firstFlag), - "query": nullString(*queryFlag), - "cloned": *clonedFlag, - "notCloned": *notClonedFlag, - "indexed": *indexedFlag, - "notIndexed": *notIndexedFlag, - "orderBy": orderBy, - "descending": *descendingFlag, - }, - result: &result, - done: func() error { - for _, repo := range result.Repositories.Nodes { - if *namesWithoutHostFlag { - firstSlash := strings.Index(repo.Name, "/") - fmt.Println(repo.Name[firstSlash+len("/"):]) - continue - } - - if err := execTemplate(tmpl, repo); err != nil { - return err - } - } - return nil - }, - flags: apiFlags, - }).do() + if ok, err := client.NewRequest(query, map[string]interface{}{ + "first": api.NullInt(*firstFlag), + "query": api.NullString(*queryFlag), + "cloned": *clonedFlag, + "notCloned": *notClonedFlag, + "indexed": *indexedFlag, + "notIndexed": *notIndexedFlag, + "orderBy": orderBy, + "descending": *descendingFlag, + }).Do(context.Background(), &result); err != nil || !ok { + return err + } + + for _, repo := range result.Repositories.Nodes { + if *namesWithoutHostFlag { + firstSlash := strings.Index(repo.Name, "/") + fmt.Println(repo.Name[firstSlash+len("/"):]) + continue + } + + if err := execTemplate(tmpl, repo); err != nil { + return err + } + } + return nil } // Register the command. diff --git a/cmd/src/search.go b/cmd/src/search.go index 2f5893ef85..047f504d4c 100644 --- a/cmd/src/search.go +++ b/cmd/src/search.go @@ -2,6 +2,7 @@ package main import ( "bytes" + "context" "errors" "flag" "fmt" @@ -15,6 +16,7 @@ import ( "time" isatty "github.com/mattn/go-isatty" + "github.com/sourcegraph/src-cli/internal/api" "jaytaylor.com/html2text" ) @@ -50,7 +52,7 @@ Other tips: var ( jsonFlag = flagSet.Bool("json", false, "Whether or not to output results as JSON") explainJSONFlag = flagSet.Bool("explain-json", false, "Explain the JSON output schema and exit.") - apiFlags = newAPIFlags(flagSet) + apiFlags = api.NewFlags(flagSet) lessFlag = flagSet.Bool("less", true, "Pipe output to 'less -R' (only if stdout is terminal, and not json flag)") ) @@ -98,6 +100,8 @@ Other tips: return lessCmd.Run() } + client := cfg.apiClient(apiFlags, flagSet.Output()) + query := `fragment FileMatchFields on FileMatch { repository { name @@ -226,41 +230,34 @@ Other tips: } } - return (&apiRequest{ - query: query, - vars: map[string]interface{}{ - "query": nullString(queryString), - }, - result: &result, - done: func() error { - improved := searchResultsImproved{ - SourcegraphEndpoint: cfg.Endpoint, - Query: queryString, - Site: result.Site, - searchResults: result.Search.Results, - } + if ok, err := client.NewRequest(query, map[string]interface{}{ + "query": api.NullString(queryString), + }).Do(context.Background(), &result); err != nil || !ok { + return err + } - if *jsonFlag { - // Print the formatted JSON. - f, err := marshalIndent(improved) - if err != nil { - return err - } - fmt.Println(string(f)) - return nil - } + improved := searchResultsImproved{ + SourcegraphEndpoint: cfg.Endpoint, + Query: queryString, + Site: result.Site, + searchResults: result.Search.Results, + } - tmpl, err := parseTemplate(searchResultsTemplate) - if err != nil { - return err - } - if err := execTemplate(tmpl, improved); err != nil { - return err - } - return nil - }, - flags: apiFlags, - }).do() + if *jsonFlag { + // Print the formatted JSON. + f, err := marshalIndent(improved) + if err != nil { + return err + } + fmt.Println(string(f)) + return nil + } + + tmpl, err := parseTemplate(searchResultsTemplate) + if err != nil { + return err + } + return execTemplate(tmpl, improved) } // Register the command. diff --git a/cmd/src/users_create.go b/cmd/src/users_create.go index 8147f8373b..8c3da22b42 100644 --- a/cmd/src/users_create.go +++ b/cmd/src/users_create.go @@ -1,8 +1,11 @@ package main import ( + "context" "flag" "fmt" + + "github.com/sourcegraph/src-cli/internal/api" ) func init() { @@ -25,12 +28,14 @@ Examples: 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 = newAPIFlags(flagSet) + apiFlags = api.NewFlags(flagSet) ) handler := func(args []string) error { flagSet.Parse(args) + client := cfg.apiClient(apiFlags, flagSet.Output()) + query := `mutation CreateUser( $username: String!, $email: String!, @@ -48,23 +53,19 @@ Examples: ResetPasswordURL string } } - return (&apiRequest{ - query: query, - vars: map[string]interface{}{ - "username": *usernameFlag, - "email": *emailFlag, - }, - result: &result, - done: func() error { - fmt.Printf("User %q created.\n", *usernameFlag) - if *resetPasswordURLFlag && result.CreateUser.ResetPasswordURL != "" { - fmt.Println() - fmt.Printf("\tReset pasword URL: %s\n", result.CreateUser.ResetPasswordURL) - } - return nil - }, - flags: apiFlags, - }).do() + if ok, err := client.NewRequest(query, map[string]interface{}{ + "username": *usernameFlag, + "email": *emailFlag, + }).Do(context.Background(), &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) + } + return nil } // Register the command. diff --git a/cmd/src/users_delete.go b/cmd/src/users_delete.go index 4adacbd887..cbe1f2e595 100644 --- a/cmd/src/users_delete.go +++ b/cmd/src/users_delete.go @@ -1,8 +1,11 @@ package main import ( + "context" "flag" "fmt" + + "github.com/sourcegraph/src-cli/internal/api" ) func init() { @@ -31,12 +34,14 @@ Examples: } var ( userIDFlag = flagSet.String("id", "", `The ID of the user to delete.`) - apiFlags = newAPIFlags(flagSet) + apiFlags = api.NewFlags(flagSet) ) handler := func(args []string) error { flagSet.Parse(args) + client := cfg.apiClient(apiFlags, flagSet.Output()) + query := `mutation DeleteUser( $user: ID! ) { @@ -50,18 +55,14 @@ Examples: var result struct { DeleteUser struct{} } - return (&apiRequest{ - query: query, - vars: map[string]interface{}{ - "user": *userIDFlag, - }, - result: &result, - done: func() error { - fmt.Printf("User with ID %q deleted.\n", *userIDFlag) - return nil - }, - flags: apiFlags, - }).do() + if ok, err := client.NewRequest(query, map[string]interface{}{ + "user": *userIDFlag, + }).Do(context.Background(), &result); err != nil || !ok { + return err + } + + fmt.Printf("User with ID %q deleted.\n", *userIDFlag) + return nil } // Register the command. diff --git a/cmd/src/users_get.go b/cmd/src/users_get.go index 0aa5a126c3..b49f8940d7 100644 --- a/cmd/src/users_get.go +++ b/cmd/src/users_get.go @@ -1,8 +1,11 @@ package main import ( + "context" "flag" "fmt" + + "github.com/sourcegraph/src-cli/internal/api" ) func init() { @@ -24,12 +27,14 @@ Examples: var ( usernameFlag = flagSet.String("username", "", `Look up user by username. (e.g. "alice")`) formatFlag = flagSet.String("f", "{{.|json}}", `Format for the output, using the syntax of Go package text/template. (e.g. "{{.ID}}: {{.Username}} ({{.DisplayName}})")`) - apiFlags = newAPIFlags(flagSet) + apiFlags = api.NewFlags(flagSet) ) handler := func(args []string) error { flagSet.Parse(args) + client := cfg.apiClient(apiFlags, flagSet.Output()) + tmpl, err := parseTemplate(*formatFlag) if err != nil { return err @@ -48,17 +53,13 @@ Examples: var result struct { User *User } - return (&apiRequest{ - query: query, - vars: map[string]interface{}{ - "username": *usernameFlag, - }, - result: &result, - done: func() error { - return execTemplate(tmpl, result.User) - }, - flags: apiFlags, - }).do() + if ok, err := client.NewRequest(query, map[string]interface{}{ + "username": *usernameFlag, + }).Do(context.Background(), &result); err != nil || !ok { + return err + } + + return execTemplate(tmpl, result.User) } // Register the command. diff --git a/cmd/src/users_list.go b/cmd/src/users_list.go index 5838cb07ec..6b8db9350b 100644 --- a/cmd/src/users_list.go +++ b/cmd/src/users_list.go @@ -1,8 +1,11 @@ package main import ( + "context" "flag" "fmt" + + "github.com/sourcegraph/src-cli/internal/api" ) func init() { @@ -38,20 +41,23 @@ Examples: 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 = newAPIFlags(flagSet) + apiFlags = api.NewFlags(flagSet) ) handler := func(args []string) error { flagSet.Parse(args) + ctx := context.Background() + client := cfg.apiClient(apiFlags, flagSet.Output()) + tmpl, err := parseTemplate(*formatFlag) if err != nil { return err } vars := map[string]interface{}{ - "first": nullInt(*firstFlag), - "query": nullString(*queryFlag), - "tag": nullString(*tagFlag), + "first": api.NullInt(*firstFlag), + "query": api.NullString(*queryFlag), + "tag": api.NullString(*tagFlag), } queryTagVar := "" queryTag := "" @@ -65,7 +71,7 @@ Examples: ` + queryTagVar + ` ) { users( - first: $first, +first: $first, query: $query, ` + queryTag + ` ) { @@ -80,20 +86,16 @@ Examples: Nodes []User } } - return (&apiRequest{ - query: query, - vars: vars, - result: &result, - done: func() error { - for _, user := range result.Users.Nodes { - if err := execTemplate(tmpl, user); err != nil { - return err - } - } - return nil - }, - flags: apiFlags, - }).do() + if ok, err := client.NewRequest(query, vars).Do(ctx, &result); err != nil || !ok { + return err + } + + for _, user := range result.Users.Nodes { + if err := execTemplate(tmpl, user); err != nil { + return err + } + } + return nil } // Register the command. diff --git a/cmd/src/users_tag.go b/cmd/src/users_tag.go index afedd04246..d068d62c55 100644 --- a/cmd/src/users_tag.go +++ b/cmd/src/users_tag.go @@ -1,8 +1,11 @@ package main import ( + "context" "flag" "fmt" + + "github.com/sourcegraph/src-cli/internal/api" ) func init() { @@ -35,12 +38,14 @@ Related examples: 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 = newAPIFlags(flagSet) + apiFlags = api.NewFlags(flagSet) ) handler := func(args []string) error { flagSet.Parse(args) + client := cfg.apiClient(apiFlags, flagSet.Output()) + query := `mutation SetUserTag( $user: ID!, $tag: String!, @@ -55,16 +60,12 @@ Related examples: } }` - return (&apiRequest{ - query: query, - vars: map[string]interface{}{ - "user": *userIDFlag, - "tag": *tagFlag, - "present": !*removeFlag, - }, - result: &struct{}{}, - flags: apiFlags, - }).do() + _, err := client.NewRequest(query, map[string]interface{}{ + "user": *userIDFlag, + "tag": *tagFlag, + "present": !*removeFlag, + }).Do(context.Background(), &struct{}{}) + return err } // Register the command. diff --git a/internal/api/api.go b/internal/api/api.go new file mode 100644 index 0000000000..31643d7345 --- /dev/null +++ b/internal/api/api.go @@ -0,0 +1,230 @@ +// Package api provides a basic client library for the Sourcegraph GraphQL API. +package api + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "os" + + "github.com/hashicorp/go-multierror" + "github.com/kballard/go-shellquote" + "github.com/mattn/go-isatty" + "github.com/pkg/errors" +) + +// Client instances provide methods to create API requests. +type Client interface { + // NewQuery is a convenience method to create a GraphQL request without + // variables. + NewQuery(query string) Request + + // NewRequest creates a GraphQL request. + NewRequest(query string, vars map[string]interface{}) Request +} + +// Request instances represent GraphQL requests. +type Request interface { + // Do actions the request. Normally, this means that the request is + // transmitted and the response is unmarshalled into result. + // + // If no data was available to be unmarshalled — for example, due to the + // -get-curl flag being set — then ok will return false. + Do(ctx context.Context, result interface{}) (ok bool, err error) + + // DoRaw has the same behaviour as Do, with one exception: the result will + // not be unwrapped, and will include the GraphQL errors. Therefore the + // structure that is provided as the result should have top level Data and + // Errors keys for the GraphQL wrapper to be unmarshalled into. + DoRaw(ctx context.Context, result interface{}) (ok bool, err error) +} + +// client is the internal concrete type implementing Client. +type client struct { + opts ClientOpts +} + +// request is the internal concrete type implementing Request. +type request struct { + client *client + query string + vars map[string]interface{} +} + +// ClientOpts encapsulates the options given to NewClient. +type ClientOpts struct { + Endpoint string + AccessToken string + AdditionalHeaders map[string]string + + // Flags are the standard API client flags provided by NewFlags. If nil, + // default values will be used. + Flags *Flags + + // Out is the writer that will be used when outputting diagnostics, such as + // curl commands when -get-curl is enabled. + Out io.Writer +} + +// NewClient creates a new API client. +func NewClient(opts ClientOpts) Client { + if opts.Out == nil { + panic("unexpected nil out option") + } + + flags := opts.Flags + if flags == nil { + flags = defaultFlags() + } + + return &client{ + opts: ClientOpts{ + Endpoint: opts.Endpoint, + AccessToken: opts.AccessToken, + AdditionalHeaders: opts.AdditionalHeaders, + Flags: flags, + Out: opts.Out, + }, + } +} + +func (c *client) NewQuery(query string) Request { + return c.NewRequest(query, nil) +} + +func (c *client) NewRequest(query string, vars map[string]interface{}) Request { + return &request{ + client: c, + query: query, + vars: vars, + } +} + +func (c *client) url() string { + return c.opts.Endpoint + "/.api/graphql" +} + +func (r *request) do(ctx context.Context, result interface{}) (bool, error) { + if *r.client.opts.Flags.getCurl { + curl, err := r.curlCmd() + if err != nil { + return false, err + } + r.client.opts.Out.Write([]byte(curl + "\n")) + return false, nil + } + + // Create the JSON object. + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(map[string]interface{}{ + "query": r.query, + "variables": r.vars, + }); err != nil { + return false, err + } + + // Create the HTTP request. + req, err := http.NewRequestWithContext(ctx, "POST", r.client.url(), nil) + if err != nil { + return false, err + } + if r.client.opts.AccessToken != "" { + req.Header.Set("Authorization", "token "+r.client.opts.AccessToken) + } + if *r.client.opts.Flags.trace { + req.Header.Set("X-Sourcegraph-Should-Trace", "true") + } + for k, v := range r.client.opts.AdditionalHeaders { + req.Header.Set(k, v) + } + req.Body = ioutil.NopCloser(&buf) + + // Perform the request. + resp, err := http.DefaultClient.Do(req) + if err != nil { + return false, err + } + defer resp.Body.Close() + + // Check trace header before we potentially early exit + if *r.client.opts.Flags.trace { + r.client.opts.Out.Write([]byte(fmt.Sprintf("x-trace: %s\n", resp.Header.Get("x-trace")))) + } + + // Our request may have failed before reaching the GraphQL endpoint, so + // confirm the status code. You can test this easily with e.g. an invalid + // endpoint like -endpoint=https://google.com + if resp.StatusCode != http.StatusOK { + if resp.StatusCode == http.StatusUnauthorized && isatty.IsCygwinTerminal(os.Stdout.Fd()) { + fmt.Println("You may need to specify or update your access token to use this endpoint.") + fmt.Println("See https://github.com/sourcegraph/src-cli#authentication") + fmt.Println("") + } + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + return false, err + } + return false, fmt.Errorf("error: %s\n\n%s", resp.Status, body) + } + + // Decode the response. + if err := json.NewDecoder(resp.Body).Decode(result); err != nil { + return false, err + } + + return true, nil +} + +func (r *request) Do(ctx context.Context, result interface{}) (bool, error) { + raw := rawResult{Data: result} + ok, err := r.do(ctx, &raw) + if err != nil { + return false, err + } else if !ok { + return false, nil + } + + // Handle the case of unpacking errors. + if raw.Errors != nil { + var errs *multierror.Error + for _, err := range raw.Errors { + errs = multierror.Append(errs, &graphqlError{err}) + } + return false, errors.Wrap(errs, "GraphQL errors") + } + return true, nil +} + +func (r *request) DoRaw(ctx context.Context, result interface{}) (bool, error) { + return r.do(ctx, result) +} + +type rawResult struct { + Data interface{} `json:"data,omitempty"` + Errors []interface{} `json:"errors,omitempty"` +} + +func (r *request) curlCmd() (string, error) { + data, err := json.Marshal(map[string]interface{}{ + "query": r.query, + "variables": r.vars, + }) + if err != nil { + return "", err + } + + s := "curl \\\n" + if r.client.opts.AccessToken != "" { + s += fmt.Sprintf(" %s \\\n", shellquote.Join("-H", "Authorization: token "+r.client.opts.AccessToken)) + } + for k, v := range r.client.opts.AdditionalHeaders { + s += fmt.Sprintf(" %s \\\n", shellquote.Join("-H", k+": "+v)) + } + s += fmt.Sprintf(" %s \\\n", shellquote.Join("-d", string(data))) + s += fmt.Sprintf(" %s", shellquote.Join(r.client.url())) + return s, nil +} diff --git a/internal/api/errors.go b/internal/api/errors.go new file mode 100644 index 0000000000..5e6d860171 --- /dev/null +++ b/internal/api/errors.go @@ -0,0 +1,11 @@ +package api + +import "encoding/json" + +// graphqlError wraps a raw JSON error returned from a GraphQL endpoint. +type graphqlError struct{ v interface{} } + +func (g *graphqlError) Error() string { + j, _ := json.MarshalIndent(g.v, "", " ") + return string(j) +} diff --git a/internal/api/flags.go b/internal/api/flags.go new file mode 100644 index 0000000000..97b6982374 --- /dev/null +++ b/internal/api/flags.go @@ -0,0 +1,27 @@ +package api + +import "flag" + +// Flags encapsulates the standard flags that should be added to all commands +// that issue API requests. +type Flags struct { + getCurl *bool + trace *bool +} + +// NewFlags instantiates a new Flags structure and attaches flags to the given +// flag set. +func NewFlags(flagSet *flag.FlagSet) *Flags { + return &Flags{ + getCurl: flagSet.Bool("get-curl", false, "Print the curl command for executing this query and exit (WARNING: includes printing your access token!)"), + trace: flagSet.Bool("trace", false, "Log the trace ID for requests. See https://docs.sourcegraph.com/admin/observability/tracing"), + } +} + +func defaultFlags() *Flags { + d := false + return &Flags{ + getCurl: &d, + trace: &d, + } +} diff --git a/internal/api/nullable.go b/internal/api/nullable.go new file mode 100644 index 0000000000..49df760b89 --- /dev/null +++ b/internal/api/nullable.go @@ -0,0 +1,19 @@ +package api + +// NullInt returns a nullable int for use in a GraphQL variable, where -1 is +// treated as a nil value. +func NullInt(n int) *int { + if n == -1 { + return nil + } + return &n +} + +// NullString returns a nullable string for use in a GraphQL variable, where "" +// is treated as a nil value. +func NullString(s string) *string { + if s == "" { + return nil + } + return &s +}