Skip to content

Commit 29ccb97

Browse files
trlypeterguy
andauthored
feat: Add search-jobs CLI commands for managing search jobs (#1145)
* feat: Add search-jobs CLI commands for managing search jobs Add new `src search-jobs` command with subcommands to manage search jobs: - create: Create new search jobs with query validation - get: Retrieve search job details by ID - list: List search jobs with sorting and pagination - cancel: Cancel running search jobs - delete: Remove search jobs The command provides functionality to: - Format output using Go templates - Sort and filter search jobs - Track job status * feat: implement search-jobs logs command search_jobs_logs: Implement a new search-jobs subcommand to retrieve jobs logs from the configured Sourcegraph instance. search_jobs_get.go: Extract the GraphQL query logic for fetching a search job into a separate helper function to improve code organization and reusability. This change: - Creates new getSearchJob helper function that encapsulates the GraphQL query logic - Simplifies the main handler function by delegating job fetching - Maintains existing functionality while improving code structure * feat: Add search jobs results retrieval command Add new command to retrieve search job results in JSONL format with the following capabilities: - Fetch results using search job ID - Optional file output with -out flag * cleanup trailing whitespace * remove tests as they are not functionally testing production code * Fix linter issues. Add support for job id numbers (#1151) * feat: Add search jobs restart command This commit introduces the `search-jobs restart` command, enabling users to restart a search job by its ID. The command retrieves the query from the original job and creates a new search job with the same query. * (lint) removed unused testutil * refactor: Redesign search-jobs command structure with builder pattern This commit refactors the search-jobs commands to: - Implement a builder pattern for consistent command creation - Add column-based output format with customizable columns - Support JSON output format for programmatic access - Improve argument handling by using positional arguments for IDs - Separate command logic from presentation for better testability - Extract common functionality into reusable helper functions - Enhance usage documentation with better examples - Remove SearchJobID parsing functions in favor of direct ID handling * refactor: make search-jobs command builder methods private * (fix) do not display command success output when using -get-curl * (fix) clean up inconsistencies in usage text --------- Co-authored-by: Peter Guy <[email protected]>
1 parent a7dfc60 commit 29ccb97

11 files changed

+1049
-0
lines changed

cmd/src/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ The commands are:
6060
repos,repo manages repositories
6161
sbom manages SBOM (Software Bill of Materials) data
6262
search search for results on Sourcegraph
63+
search-jobs manages search jobs
6364
serve-git serves your local git repositories over HTTP for Sourcegraph to pull
6465
users,user manages users
6566
codeowners manages code ownership information

cmd/src/search_jobs.go

Lines changed: 297 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,297 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"flag"
6+
"fmt"
7+
"strings"
8+
9+
"github.com/sourcegraph/src-cli/internal/api"
10+
"github.com/sourcegraph/src-cli/internal/cmderrors"
11+
)
12+
13+
// searchJobFragment is a GraphQL fragment that defines the fields to be queried
14+
// for a SearchJob. It includes the job's ID, query, state, creator information,
15+
// timestamps, URLs, and repository statistics.
16+
const searchJobFragment = `
17+
fragment SearchJobFields on SearchJob {
18+
id
19+
query
20+
state
21+
creator {
22+
username
23+
}
24+
createdAt
25+
startedAt
26+
finishedAt
27+
URL
28+
logURL
29+
repoStats {
30+
total
31+
completed
32+
failed
33+
inProgress
34+
}
35+
}`
36+
37+
// SearchJob represents a search job with its metadata, including the search query,
38+
// execution state, creator information, timestamps, URLs, and repository statistics.
39+
type SearchJob struct {
40+
ID string
41+
Query string
42+
State string
43+
Creator struct {
44+
Username string
45+
}
46+
CreatedAt string
47+
StartedAt string
48+
FinishedAt string
49+
URL string
50+
LogURL string
51+
RepoStats struct {
52+
Total int
53+
Completed int
54+
Failed int
55+
InProgress int
56+
}
57+
}
58+
59+
// availableColumns defines the available column names for output
60+
var availableColumns = map[string]bool{
61+
"id": true,
62+
"query": true,
63+
"state": true,
64+
"username": true,
65+
"createdat": true,
66+
"startedat": true,
67+
"finishedat": true,
68+
"url": true,
69+
"logurl": true,
70+
"total": true,
71+
"completed": true,
72+
"failed": true,
73+
"inprogress": true,
74+
}
75+
76+
// defaultColumns defines the default columns to display
77+
var defaultColumns = []string{"id", "username", "state", "query"}
78+
79+
// SearchJobCommandBuilder helps build search job commands with common flags and options
80+
type SearchJobCommandBuilder struct {
81+
Name string
82+
Usage string
83+
Flags *flag.FlagSet
84+
ApiFlags *api.Flags
85+
}
86+
87+
// Global variables
88+
var searchJobsCommands commander
89+
90+
// newSearchJobCommand creates a new search job command builder
91+
func newSearchJobCommand(name string, usage string) *SearchJobCommandBuilder {
92+
flagSet := flag.NewFlagSet(name, flag.ExitOnError)
93+
return &SearchJobCommandBuilder{
94+
Name: name,
95+
Usage: usage,
96+
Flags: flagSet,
97+
ApiFlags: api.NewFlags(flagSet),
98+
}
99+
}
100+
101+
// build creates and registers the command
102+
func (b *SearchJobCommandBuilder) build(handlerFunc func(*flag.FlagSet, *api.Flags, []string, bool, api.Client) error) {
103+
columnsFlag := b.Flags.String("c", strings.Join(defaultColumns, ","),
104+
"Comma-separated list of columns to display. Available: id,query,state,username,createdat,startedat,finishedat,url,logurl,total,completed,failed,inprogress")
105+
jsonFlag := b.Flags.Bool("json", false, "Output results as JSON for programmatic access")
106+
107+
usageFunc := func() {
108+
fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src search-jobs %s':\n", b.Name)
109+
b.Flags.PrintDefaults()
110+
fmt.Println(b.Usage)
111+
}
112+
113+
handler := func(args []string) error {
114+
if err := parseSearchJobsArgs(b.Flags, args); err != nil {
115+
return err
116+
}
117+
118+
// Parse columns
119+
columns := parseColumns(*columnsFlag)
120+
121+
client := createSearchJobsClient(b.Flags, b.ApiFlags)
122+
123+
return handlerFunc(b.Flags, b.ApiFlags, columns, *jsonFlag, client)
124+
}
125+
126+
searchJobsCommands = append(searchJobsCommands, &command{
127+
flagSet: b.Flags,
128+
handler: handler,
129+
usageFunc: usageFunc,
130+
})
131+
}
132+
133+
// parseColumns parses and validates the columns flag
134+
func parseColumns(columnsFlag string) []string {
135+
if columnsFlag == "" {
136+
return defaultColumns
137+
}
138+
139+
columns := strings.Split(columnsFlag, ",")
140+
var validColumns []string
141+
142+
for _, col := range columns {
143+
col = strings.ToLower(strings.TrimSpace(col))
144+
if availableColumns[col] {
145+
validColumns = append(validColumns, col)
146+
}
147+
}
148+
149+
if len(validColumns) == 0 {
150+
return defaultColumns
151+
}
152+
153+
return validColumns
154+
}
155+
156+
// createSearchJobsClient creates a reusable API client for search jobs commands
157+
func createSearchJobsClient(out *flag.FlagSet, apiFlags *api.Flags) api.Client {
158+
return api.NewClient(api.ClientOpts{
159+
Endpoint: cfg.Endpoint,
160+
AccessToken: cfg.AccessToken,
161+
Out: out.Output(),
162+
Flags: apiFlags,
163+
})
164+
}
165+
166+
// parseSearchJobsArgs parses command arguments with the provided flag set
167+
// and returns an error if parsing fails
168+
func parseSearchJobsArgs(flagSet *flag.FlagSet, args []string) error {
169+
if err := flagSet.Parse(args); err != nil {
170+
return err
171+
}
172+
return nil
173+
}
174+
175+
// validateJobID validates that a job ID was provided
176+
func validateJobID(args []string) (string, error) {
177+
if len(args) != 1 {
178+
return "", cmderrors.Usage("must provide a search job ID")
179+
}
180+
return args[0], nil
181+
}
182+
183+
// displaySearchJob formats and outputs a search job based on selected columns or JSON
184+
func displaySearchJob(job *SearchJob, columns []string, asJSON bool) error {
185+
if asJSON {
186+
return outputAsJSON(job)
187+
}
188+
return outputAsColumns(job, columns)
189+
}
190+
191+
// displaySearchJobs formats and outputs multiple search jobs
192+
func displaySearchJobs(jobs []SearchJob, columns []string, asJSON bool) error {
193+
if asJSON {
194+
return outputAsJSON(jobs)
195+
}
196+
197+
for _, job := range jobs {
198+
if err := outputAsColumns(&job, columns); err != nil {
199+
return err
200+
}
201+
}
202+
return nil
203+
}
204+
205+
// outputAsJSON outputs data as JSON
206+
func outputAsJSON(data interface{}) error {
207+
jsonBytes, err := json.MarshalIndent(data, "", " ")
208+
if err != nil {
209+
return err
210+
}
211+
fmt.Println(string(jsonBytes))
212+
return nil
213+
}
214+
215+
// outputAsColumns outputs a search job as tab-delimited columns
216+
func outputAsColumns(job *SearchJob, columns []string) error {
217+
values := make([]string, 0, len(columns))
218+
219+
for _, col := range columns {
220+
switch col {
221+
case "id":
222+
values = append(values, job.ID)
223+
case "query":
224+
values = append(values, job.Query)
225+
case "state":
226+
values = append(values, job.State)
227+
case "username":
228+
values = append(values, job.Creator.Username)
229+
case "createdat":
230+
values = append(values, job.CreatedAt)
231+
case "startedat":
232+
values = append(values, job.StartedAt)
233+
case "finishedat":
234+
values = append(values, job.FinishedAt)
235+
case "url":
236+
values = append(values, job.URL)
237+
case "logurl":
238+
values = append(values, job.LogURL)
239+
case "total":
240+
values = append(values, fmt.Sprintf("%d", job.RepoStats.Total))
241+
case "completed":
242+
values = append(values, fmt.Sprintf("%d", job.RepoStats.Completed))
243+
case "failed":
244+
values = append(values, fmt.Sprintf("%d", job.RepoStats.Failed))
245+
case "inprogress":
246+
values = append(values, fmt.Sprintf("%d", job.RepoStats.InProgress))
247+
}
248+
}
249+
250+
fmt.Println(strings.Join(values, "\t"))
251+
return nil
252+
}
253+
254+
// init registers the 'src search-jobs' command with the CLI. It provides subcommands
255+
// for managing search jobs, including creating, listing, getting, canceling and deleting
256+
// jobs. The command uses a flagset for parsing options and displays usage information
257+
// when help is requested.
258+
func init() {
259+
usage := `'src search-jobs' is a tool that manages search jobs on a Sourcegraph instance.
260+
261+
Usage:
262+
263+
src search-jobs command [command options]
264+
265+
The commands are:
266+
267+
cancel cancels a search job by ID
268+
create creates a search job
269+
delete deletes a search job by ID
270+
get gets a search job by ID
271+
list lists search jobs
272+
logs fetches logs for a search job by ID
273+
restart restarts a search job by ID
274+
results fetches results for a search job by ID
275+
276+
Common options for all commands:
277+
-c Select columns to display (e.g., -c id,query,state,username)
278+
-json Output results in JSON format
279+
280+
Use "src search-jobs [command] -h" for more information about a command.
281+
`
282+
283+
flagSet := flag.NewFlagSet("search-jobs", flag.ExitOnError)
284+
handler := func(args []string) error {
285+
searchJobsCommands.run(flagSet, "src search-jobs", usage, args)
286+
return nil
287+
}
288+
289+
commands = append(commands, &command{
290+
flagSet: flagSet,
291+
aliases: []string{"search-job"},
292+
handler: handler,
293+
usageFunc: func() {
294+
fmt.Println(usage)
295+
},
296+
})
297+
}

cmd/src/search_jobs_cancel.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"flag"
6+
"fmt"
7+
8+
"github.com/sourcegraph/src-cli/internal/api"
9+
)
10+
11+
// GraphQL mutation constants
12+
const cancelSearchJobMutation = `mutation CancelSearchJob($id: ID!) {
13+
cancelSearchJob(id: $id) {
14+
alwaysNil
15+
}
16+
}`
17+
18+
// cancelSearchJob cancels a search job with the given ID
19+
func cancelSearchJob(client api.Client, jobID string) error {
20+
var result struct {
21+
CancelSearchJob struct {
22+
AlwaysNil bool
23+
}
24+
}
25+
26+
if ok, err := client.NewRequest(cancelSearchJobMutation, map[string]interface{}{
27+
"id": jobID,
28+
}).Do(context.Background(), &result); err != nil || !ok {
29+
return err
30+
}
31+
32+
return nil
33+
}
34+
35+
// displayCancelSuccessMessage outputs a success message for the canceled job
36+
func displayCancelSuccessMessage(out *flag.FlagSet, jobID string) {
37+
fmt.Fprintf(out.Output(), "Search job %s canceled successfully\n", jobID)
38+
}
39+
40+
// init registers the 'cancel' subcommand for search jobs, which allows users to cancel
41+
// a running search job by its ID. It sets up the command's flag parsing, usage information,
42+
// and handles the GraphQL mutation to cancel the specified search job.
43+
func init() {
44+
usage := `
45+
Examples:
46+
47+
Cancel a search job by ID:
48+
49+
$ src search-jobs cancel U2VhcmNoSm9iOjY5
50+
51+
Arguments:
52+
The ID of the search job to cancel.
53+
54+
The cancel command stops a running search job and outputs a confirmation message.
55+
`
56+
57+
cmd := newSearchJobCommand("cancel", usage)
58+
59+
cmd.build(func(flagSet *flag.FlagSet, apiFlags *api.Flags, columns []string, asJSON bool, client api.Client) error {
60+
61+
jobID, err := validateJobID(flagSet.Args())
62+
if err != nil {
63+
return err
64+
}
65+
66+
if err := cancelSearchJob(client, jobID); err != nil {
67+
return err
68+
}
69+
70+
if apiFlags.GetCurl() {
71+
return nil
72+
}
73+
74+
displayCancelSuccessMessage(flagSet, jobID)
75+
76+
return nil
77+
})
78+
}

0 commit comments

Comments
 (0)