From 8e43e2d6c319d48c6d3d301fb43eaf565a596fd3 Mon Sep 17 00:00:00 2001 From: Peter Guy Date: Tue, 4 Mar 2025 17:02:23 -0800 Subject: [PATCH] Fix linter issues. Add support for job id numbers --- cmd/src/format.go | 14 ++++ cmd/src/search_jobs.go | 79 +++++++++++++++----- cmd/src/search_jobs_cancel.go | 15 ++-- cmd/src/search_jobs_create.go | 7 +- cmd/src/search_jobs_delete.go | 13 ++-- cmd/src/search_jobs_get.go | 127 +++++++++++++++++---------------- cmd/src/search_jobs_list.go | 125 ++++++++++++++++---------------- cmd/src/search_jobs_logs.go | 19 ++--- cmd/src/search_jobs_results.go | 14 ++-- 9 files changed, 231 insertions(+), 182 deletions(-) diff --git a/cmd/src/format.go b/cmd/src/format.go index 2c08d4fcc1..2cbe9226e9 100644 --- a/cmd/src/format.go +++ b/cmd/src/format.go @@ -80,6 +80,20 @@ func parseTemplate(text string) (*template.Template, error) { } return humanize.Time(t), nil }, + "searchJobIDNumber": func(id string) string { + sjid, err := ParseSearchJobID(id) + if err != nil { + return id + } + return fmt.Sprintf("%d", sjid.Number()) + }, + "searchJobIDCanonical": func(id string) string { + sjid, err := ParseSearchJobID(id) + if err != nil { + return id + } + return sjid.Canonical() + }, // Register search-specific template functions "searchSequentialLineNumber": searchTemplateFuncs["searchSequentialLineNumber"], diff --git a/cmd/src/search_jobs.go b/cmd/src/search_jobs.go index ca8cf03bea..efe024e8af 100644 --- a/cmd/src/search_jobs.go +++ b/cmd/src/search_jobs.go @@ -1,10 +1,13 @@ package main import ( + "encoding/base64" "flag" "fmt" - "text/template" - "os" + "regexp" + "strconv" + + "github.com/sourcegraph/src-cli/internal/cmderrors" ) // searchJobFragment is a GraphQL fragment that defines the fields to be queried @@ -51,6 +54,8 @@ The commands are: delete deletes a search job by ID get gets a search job by ID list lists search jobs + logs outputs the logs for a search job by ID + results outputs the results for a search job by ID Use "src search-jobs [command] -h" for more information about a command. ` @@ -71,22 +76,12 @@ Use "src search-jobs [command] -h" for more information about a command. }) } -// printSearchJob formats and prints a search job to stdout using the provided format template. -// Returns an error if the template parsing or execution fails. -func printSearchJob(job *SearchJob, format string) error { - tmpl, err := template.New("searchJob").Parse(format) - if err != nil { - return err - } - return tmpl.Execute(os.Stdout, job) -} - // SearchJob represents a search job with its metadata, including the search query, // execution state, creator information, timestamps, URLs, and repository statistics. type SearchJob struct { - ID string - Query string - State string + ID string + Query string + State string Creator struct { Username string } @@ -96,9 +91,55 @@ type SearchJob struct { URL string LogURL string RepoStats struct { - Total int - Completed int - Failed int - InProgress int + Total int + Completed int + Failed int + InProgress int + } +} + +type SearchJobID struct { + number uint64 +} + +func ParseSearchJobID(input string) (*SearchJobID, error) { + // accept either: + // - the numeric job id (non-negative integer) + // - the plain text SearchJob: form of the id + // - the base64-encoded "SearchJob:" string + + if input == "" { + return nil, cmderrors.Usage("must provide a search job ID") + } + + // Try to decode if it's base64 first + if decoded, err := base64.StdEncoding.DecodeString(input); err == nil { + input = string(decoded) } + + // Match either "SearchJob:" or "" + re := regexp.MustCompile(`^(?:SearchJob:)?(\d+)$`) + matches := re.FindStringSubmatch(input) + if matches == nil { + return nil, fmt.Errorf("invalid ID format: must be a non-negative integer, 'SearchJob:', or that string base64-encoded") + } + + number, err := strconv.ParseUint(matches[1], 10, 64) + if err != nil { + return nil, fmt.Errorf("invalid ID format: must be a 64-bit non-negative integer") + } + + return &SearchJobID{number: number}, nil +} + +func (id *SearchJobID) String() string { + return fmt.Sprintf("SearchJob:%d", id.Number()) +} + +func (id *SearchJobID) Canonical() string { + return base64.StdEncoding.EncodeToString([]byte(id.String())) +} + +func (id *SearchJobID) Number() uint64 { + return id.number } diff --git a/cmd/src/search_jobs_cancel.go b/cmd/src/search_jobs_cancel.go index df77d710ce..7ddfca3dbd 100644 --- a/cmd/src/search_jobs_cancel.go +++ b/cmd/src/search_jobs_cancel.go @@ -4,8 +4,8 @@ import ( "context" "flag" "fmt" + "github.com/sourcegraph/src-cli/internal/api" - "github.com/sourcegraph/src-cli/internal/cmderrors" ) const CancelSearchJobMutation = `mutation CancelSearchJob($id: ID!) { @@ -23,7 +23,7 @@ Examples: Cancel a search job: - $ src search-jobs cancel --id U2VhcmNoSm9iOjY5 + $ src search-jobs cancel -id 999 ` flagSet := flag.NewFlagSet("cancel", flag.ExitOnError) usageFunc := func() { @@ -33,8 +33,8 @@ Examples: } var ( - idFlag = flagSet.String("id", "", "ID of the search job to cancel") - apiFlags = api.NewFlags(flagSet) + idFlag = flagSet.String("id", "", "ID of the search job to cancel") + apiFlags = api.NewFlags(flagSet) ) handler := func(args []string) error { @@ -49,8 +49,9 @@ Examples: Flags: apiFlags, }) - if *idFlag == "" { - return cmderrors.Usage("must provide a search job ID") + jobID, err := ParseSearchJobID(*idFlag) + if err != nil { + return err } query := CancelSearchJobMutation @@ -62,7 +63,7 @@ Examples: } if ok, err := client.NewRequest(query, map[string]interface{}{ - "id": *idFlag, + "id": api.NullString(jobID.Canonical()), }).Do(context.Background(), &result); err != nil || !ok { return err } diff --git a/cmd/src/search_jobs_create.go b/cmd/src/search_jobs_create.go index 95d795f4f4..495c3ca884 100644 --- a/cmd/src/search_jobs_create.go +++ b/cmd/src/search_jobs_create.go @@ -4,6 +4,7 @@ import ( "context" "flag" "fmt" + "github.com/sourcegraph/src-cli/internal/api" "github.com/sourcegraph/src-cli/internal/cmderrors" ) @@ -43,9 +44,9 @@ Examples: } var ( - queryFlag = flagSet.String("query", "", "Search query") - formatFlag = flagSet.String("f", "{{.ID}}: {{.Creator.Username}} {{.State}} ({{.Query}})", `Format for the output, using the syntax of Go package text/template. (e.g. "{{.ID}}: {{.Creator.Username}} ({{.Query}})" or "{{.|json}}")`) - apiFlags = api.NewFlags(flagSet) + queryFlag = flagSet.String("query", "", "Search query") + formatFlag = flagSet.String("f", "{{searchJobIDNumber .ID}}: {{.Creator.Username}} {{.State}} ({{.Query}})", `Format for the output, using the syntax of Go package text/template. (e.g. "{{.ID}}: {{.Creator.Username}} ({{.Query}})" or "{{.|json}}")`) + apiFlags = api.NewFlags(flagSet) ) handler := func(args []string) error { diff --git a/cmd/src/search_jobs_delete.go b/cmd/src/search_jobs_delete.go index 01b9b98b1b..239f4dbc9c 100644 --- a/cmd/src/search_jobs_delete.go +++ b/cmd/src/search_jobs_delete.go @@ -4,8 +4,8 @@ import ( "context" "flag" "fmt" + "github.com/sourcegraph/src-cli/internal/api" - "github.com/sourcegraph/src-cli/internal/cmderrors" ) const DeleteSearchJobQuery = `mutation DeleteSearchJob($id: ID!) { @@ -23,7 +23,7 @@ Examples: Delete a search job by ID: - $ src search-jobs delete U2VhcmNoSm9iOjY5 + $ src search-jobs delete -id 999 ` flagSet := flag.NewFlagSet("delete", flag.ExitOnError) @@ -34,7 +34,7 @@ Examples: } var ( - idFlag = flagSet.String("id", "", "ID of the search job to delete") + idFlag = flagSet.String("id", "", "ID of the search job to delete") apiFlags = api.NewFlags(flagSet) ) @@ -50,8 +50,9 @@ Examples: Flags: apiFlags, }) - if *idFlag == "" { - return cmderrors.Usage("must provide a search job ID") + jobID, err := ParseSearchJobID(*idFlag) + if err != nil { + return err } var result struct { @@ -61,7 +62,7 @@ Examples: } if ok, err := client.NewRequest(DeleteSearchJobQuery, map[string]interface{}{ - "id": *idFlag, + "id": api.NullString(jobID.Canonical()), }).Do(context.Background(), &result); err != nil || !ok { return err } diff --git a/cmd/src/search_jobs_get.go b/cmd/src/search_jobs_get.go index ffa10b68da..9f908d34c6 100644 --- a/cmd/src/search_jobs_get.go +++ b/cmd/src/search_jobs_get.go @@ -1,11 +1,11 @@ package main import ( - "context" - "flag" - "fmt" - "github.com/sourcegraph/src-cli/internal/api" - "github.com/sourcegraph/src-cli/internal/cmderrors" + "context" + "flag" + "fmt" + + "github.com/sourcegraph/src-cli/internal/api" ) const GetSearchJobQuery = `query SearchJob($id: ID!) { @@ -20,73 +20,76 @@ const GetSearchJobQuery = `query SearchJob($id: ID!) { // init registers the "get" subcommand for search-jobs which retrieves a search job by ID. // It supports formatting the output using Go templates and requires authentication via API flags. func init() { - usage := ` + usage := ` Examples: Get a search job by ID: - $ src search-jobs get U2VhcmNoSm9iOjY5 + $ src search-jobs get -id 999 ` - flagSet := flag.NewFlagSet("get", flag.ExitOnError) - usageFunc := func() { - fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src search-jobs %s':\n", flagSet.Name()) - flagSet.PrintDefaults() - fmt.Println(usage) - } - - var ( - idFlag = flagSet.String("id", "", "ID of the search job") - formatFlag = flagSet.String("f", "{{.ID}}: {{.Creator.Username}} {{.State}} ({{.Query}})", `Format for the output, using the syntax of Go package text/template. (e.g. "{{.ID}}: {{.Creator.Username}} ({{.Query}})" or "{{.|json}}")`) - apiFlags = api.NewFlags(flagSet) - ) - - handler := func(args []string) error { - if err := flagSet.Parse(args); err != nil { - return err - } - - client := api.NewClient(api.ClientOpts{ - Endpoint: cfg.Endpoint, - AccessToken: cfg.AccessToken, - Out: flagSet.Output(), - Flags: apiFlags, - }) - - tmpl, err := parseTemplate(*formatFlag) - if err != nil { - return err - } - - if *idFlag == "" { - return cmderrors.Usage("must provide a search job ID") - } - - job, err := getSearchJob(client, *idFlag) - if err != nil { - return err - } - return execTemplate(tmpl, job) - } - searchJobsCommands = append(searchJobsCommands, &command{ - flagSet: flagSet, - handler: handler, - usageFunc: usageFunc, - }) + flagSet := flag.NewFlagSet("get", flag.ExitOnError) + usageFunc := func() { + fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src search-jobs %s':\n", flagSet.Name()) + flagSet.PrintDefaults() + fmt.Println(usage) + } + + var ( + idFlag = flagSet.String("id", "", "ID of the search job") + formatFlag = flagSet.String("f", "{{searchJobIDNumber .ID}}: {{.Creator.Username}} {{.State}} ({{.Query}})", `Format for the output, using the syntax of Go package text/template. (e.g. "{{.ID}}: {{.Creator.Username}} ({{.Query}})" or "{{.|json}}")`) + apiFlags = api.NewFlags(flagSet) + ) + + handler := func(args []string) error { + if err := flagSet.Parse(args); err != nil { + return err + } + + client := api.NewClient(api.ClientOpts{ + Endpoint: cfg.Endpoint, + AccessToken: cfg.AccessToken, + Out: flagSet.Output(), + Flags: apiFlags, + }) + + tmpl, err := parseTemplate(*formatFlag) + if err != nil { + return err + } + + job, err := getSearchJob(client, *idFlag) + if err != nil { + return err + } + + return execTemplate(tmpl, job) + } + searchJobsCommands = append(searchJobsCommands, &command{ + flagSet: flagSet, + handler: handler, + usageFunc: usageFunc, + }) } func getSearchJob(client api.Client, id string) (*SearchJob, error) { - query := GetSearchJobQuery + SearchJobFragment - var result struct { - Node *SearchJob - } + jobID, err := ParseSearchJobID(id) + if err != nil { + return nil, err + } - if ok, err := client.NewRequest(query, map[string]interface{}{ - "id": api.NullString(id), - }).Do(context.Background(), &result); err != nil || !ok { - return nil, err - } + query := GetSearchJobQuery + SearchJobFragment + + var result struct { + Node *SearchJob + } + + if ok, err := client.NewRequest(query, map[string]interface{}{ + "id": api.NullString(jobID.Canonical()), + }).Do(context.Background(), &result); err != nil || !ok { + return nil, err + } - return result.Node, nil + return result.Node, nil } diff --git a/cmd/src/search_jobs_list.go b/cmd/src/search_jobs_list.go index da9ced137f..813bf241fe 100644 --- a/cmd/src/search_jobs_list.go +++ b/cmd/src/search_jobs_list.go @@ -4,6 +4,7 @@ import ( "context" "flag" "fmt" + "github.com/sourcegraph/src-cli/internal/api" "github.com/sourcegraph/src-cli/internal/cmderrors" ) @@ -30,94 +31,94 @@ Examples: List all search jobs in ascending order: - $ src search-jobs list --asc + $ src search-jobs list -asc Limit the number of search jobs returned: - $ src search-jobs list --limit 5 + $ src search-jobs list -limit 5 Order search jobs by a field (must be one of: QUERY, CREATED_AT, STATE): - $ src search-jobs list --order-by QUERY + $ src search-jobs list -order-by QUERY ` - flagSet := flag.NewFlagSet("list", flag.ExitOnError) - usageFunc := func() { - fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src search-jobs %s':\n", flagSet.Name()) - flagSet.PrintDefaults() - fmt.Println(usage) - } - - var ( - formatFlag = flagSet.String("f", "{{.ID}}: {{.Creator.Username}} {{.State}}", `Format for the output, using the syntax of Go package text/template. (e.g. "{{.ID}}: {{.Creator.Username}} ({{.Query}})" or "{{.|json}}")`) - limitFlag = flagSet.Int("limit", 10, "Limit the number of search jobs returned") - ascFlag = flagSet.Bool("asc", false, "Sort search jobs in ascending order") - orderByFlag = flagSet.String("order-by", "CREATED_AT", "Sort search jobs by a field") - apiFlags = api.NewFlags(flagSet) - ) - - validOrderBy := map[string]bool{ - "QUERY": true, - "CREATED_AT": true, - "STATE": true, - } - - handler := func(args []string) error { - if err := flagSet.Parse(args); err != nil { - return err - } + flagSet := flag.NewFlagSet("list", flag.ExitOnError) + usageFunc := func() { + fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src search-jobs %s':\n", flagSet.Name()) + flagSet.PrintDefaults() + fmt.Println(usage) + } + + var ( + formatFlag = flagSet.String("f", "{{searchJobIDNumber .ID}}: {{.Creator.Username}} {{.State}}", `Format for the output, using the syntax of Go package text/template. (e.g. "{{.ID}}: {{.Creator.Username}} ({{.Query}})" or "{{.|json}}")`) + limitFlag = flagSet.Int("limit", 10, "Limit the number of search jobs returned") + ascFlag = flagSet.Bool("asc", false, "Sort search jobs in ascending order") + orderByFlag = flagSet.String("order-by", "CREATED_AT", "Sort search jobs by a field") + apiFlags = api.NewFlags(flagSet) + ) + + validOrderBy := map[string]bool{ + "QUERY": true, + "CREATED_AT": true, + "STATE": true, + } + + handler := func(args []string) error { + if err := flagSet.Parse(args); err != nil { + return err + } - client := api.NewClient(api.ClientOpts{ - Endpoint: cfg.Endpoint, - AccessToken: cfg.AccessToken, - Out: flagSet.Output(), - Flags: apiFlags, - }) + client := api.NewClient(api.ClientOpts{ + Endpoint: cfg.Endpoint, + AccessToken: cfg.AccessToken, + Out: flagSet.Output(), + Flags: apiFlags, + }) if *limitFlag < 1 { return cmderrors.Usage("limit flag must be greater than 0") } - if !validOrderBy[*orderByFlag] { - return cmderrors.Usage("order-by must be one of: QUERY, CREATED_AT, STATE") - } + if !validOrderBy[*orderByFlag] { + return cmderrors.Usage("order-by must be one of: QUERY, CREATED_AT, STATE") + } tmpl, err := parseTemplate(*formatFlag) if err != nil { return err } - query := ListSearchJobsQuery + SearchJobFragment + query := ListSearchJobsQuery + SearchJobFragment - var result struct { - SearchJobs struct { - Nodes []SearchJob - } - } + var result struct { + SearchJobs struct { + Nodes []SearchJob + } + } - if ok,err := client.NewRequest(query, map[string]interface{}{ - "first": *limitFlag, - "descending": !*ascFlag, - "orderBy": *orderByFlag, + if ok, err := client.NewRequest(query, map[string]interface{}{ + "first": *limitFlag, + "descending": !*ascFlag, + "orderBy": *orderByFlag, }).Do(context.Background(), &result); err != nil || !ok { return err } - if len(result.SearchJobs.Nodes) == 0 { - return cmderrors.ExitCode(1, fmt.Errorf("no search jobs found")) - } + if len(result.SearchJobs.Nodes) == 0 { + return cmderrors.ExitCode(1, fmt.Errorf("no search jobs found")) + } - for _, job := range result.SearchJobs.Nodes { - if err := execTemplate(tmpl, job); err != nil { - return err - } - } + for _, job := range result.SearchJobs.Nodes { + if err := execTemplate(tmpl, job); err != nil { + return err + } + } - return nil - } + return nil + } - searchJobsCommands = append(searchJobsCommands, &command{ - flagSet: flagSet, - handler: handler, - usageFunc: usageFunc, - }) + searchJobsCommands = append(searchJobsCommands, &command{ + flagSet: flagSet, + handler: handler, + usageFunc: usageFunc, + }) } diff --git a/cmd/src/search_jobs_logs.go b/cmd/src/search_jobs_logs.go index daa7f108ef..5c3e85b760 100644 --- a/cmd/src/search_jobs_logs.go +++ b/cmd/src/search_jobs_logs.go @@ -6,11 +6,10 @@ import ( "io" "net/http" "os" + "github.com/sourcegraph/src-cli/internal/api" - "github.com/sourcegraph/src-cli/internal/cmderrors" ) - // init registers the 'logs' subcommand for search jobs, which allows users to view // logs for a specific search job by its ID. The command requires a search job ID // and uses the configured API client to fetch and display the logs. @@ -19,7 +18,7 @@ func init() { Examples: View the logs of a search job: - $ src search-jobs logs U2VhcmNoSm9iOjY5 + $ src search-jobs logs -id 999 Save the logs to a file: $ src search-jobs logs U2VhcmNoSm9iOjY5 -out logs.csv @@ -33,8 +32,8 @@ Examples: } var ( - idFlag = flagSet.String("id", "", "ID of the search job to view logs for") - outFlag = flagSet.String("out", "", "File path to save the logs (optional)") + idFlag = flagSet.String("id", "", "ID of the search job to view logs for") + outFlag = flagSet.String("out", "", "File path to save the logs (optional)") apiFlags = api.NewFlags(flagSet) ) @@ -43,10 +42,6 @@ Examples: return err } - if *idFlag == "" { - return cmderrors.Usage("must provide a search job ID") - } - client := api.NewClient(api.ClientOpts{ Endpoint: cfg.Endpoint, AccessToken: cfg.AccessToken, @@ -54,10 +49,6 @@ Examples: Flags: apiFlags, }) - if *idFlag == "" { - return cmderrors.Usage("must provide a search job ID") - } - job, err := getSearchJob(client, *idFlag) if err != nil { return err @@ -105,4 +96,4 @@ Examples: handler: handler, usageFunc: usageFunc, }) -} \ No newline at end of file +} diff --git a/cmd/src/search_jobs_results.go b/cmd/src/search_jobs_results.go index 505a4606a9..a3f042b828 100644 --- a/cmd/src/search_jobs_results.go +++ b/cmd/src/search_jobs_results.go @@ -6,8 +6,8 @@ import ( "io" "net/http" "os" + "github.com/sourcegraph/src-cli/internal/api" - "github.com/sourcegraph/src-cli/internal/cmderrors" ) func init() { @@ -15,10 +15,10 @@ func init() { Examples: Get the results of a search job: - $ src search-jobs results -id U2VhcmNoSm9iOjY5 + $ src search-jobs results -id 999 Save search results to a file: - $ src search-jobs results -id U2VhcmNoSm9iOjY5 -out results.jsonl + $ src search-jobs results -id 999 -out results.jsonl ` flagSet := flag.NewFlagSet("results", flag.ExitOnError) @@ -29,8 +29,8 @@ Examples: } var ( - idFlag = flagSet.String("id", "", "ID of the search job to get results for") - outFlag = flagSet.String("out", "", "File path to save the results (optional)") + idFlag = flagSet.String("id", "", "ID of the search job to get results for") + outFlag = flagSet.String("out", "", "File path to save the results (optional)") apiFlags = api.NewFlags(flagSet) ) @@ -39,10 +39,6 @@ Examples: return err } - if *idFlag == "" { - return cmderrors.Usage("must provide a search job ID") - } - client := api.NewClient(api.ClientOpts{ Endpoint: cfg.Endpoint, AccessToken: cfg.AccessToken,