Skip to content

Commit bdbe215

Browse files
committed
fix/repo-list: print partial data when there are graphql errors (#1282)
* update GraphQLErrors to have constructor + handle paths * add test for repo list with some repos failing but not all * update src repo list to handle graphql errors - errors are treated as warnings when getting partial data - if we have no data and just errors that is a fatal error * add comment * Handle partial repo list warnings
1 parent b791337 commit bdbe215

File tree

3 files changed

+424
-43
lines changed

3 files changed

+424
-43
lines changed

cmd/src/repos_list.go

Lines changed: 164 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,157 @@ package main
22

33
import (
44
"context"
5+
"encoding/json"
56
"flag"
67
"fmt"
78
"strings"
89

910
"github.com/sourcegraph/src-cli/internal/api"
1011
)
1112

13+
type reposListOptions struct {
14+
first int
15+
query string
16+
cloned bool
17+
notCloned bool
18+
indexed bool
19+
notIndexed bool
20+
orderBy string
21+
descending bool
22+
}
23+
24+
type repositoriesListResult struct {
25+
Data struct {
26+
Repositories struct {
27+
Nodes []Repository `json:"nodes"`
28+
} `json:"repositories"`
29+
} `json:"data"`
30+
Errors []json.RawMessage `json:"errors,omitempty"`
31+
}
32+
33+
// listRepositories returns the repositories from the response, any GraphQL
34+
// errors returned alongside data (should be treated as warnings), and
35+
// a hard error when the query fails without usable repository data.
36+
func listRepositories(ctx context.Context, client api.Client, params reposListOptions) ([]Repository, api.GraphQlErrors, error) {
37+
query := `query Repositories(
38+
$first: Int,
39+
$query: String,
40+
$cloned: Boolean,
41+
$notCloned: Boolean,
42+
$indexed: Boolean,
43+
$notIndexed: Boolean,
44+
$orderBy: RepositoryOrderBy,
45+
$descending: Boolean,
46+
) {
47+
repositories(
48+
first: $first,
49+
query: $query,
50+
cloned: $cloned,
51+
notCloned: $notCloned,
52+
indexed: $indexed,
53+
notIndexed: $notIndexed,
54+
orderBy: $orderBy,
55+
descending: $descending,
56+
) {
57+
nodes {
58+
...RepositoryFields
59+
}
60+
}
61+
}
62+
` + repositoryFragment
63+
64+
var result repositoriesListResult
65+
ok, err := client.NewRequest(query, map[string]any{
66+
"first": api.NullInt(params.first),
67+
"query": api.NullString(params.query),
68+
"cloned": params.cloned,
69+
"notCloned": params.notCloned,
70+
"indexed": params.indexed,
71+
"notIndexed": params.notIndexed,
72+
"orderBy": params.orderBy,
73+
"descending": params.descending,
74+
}).DoRaw(ctx, &result)
75+
if err != nil || !ok {
76+
return nil, nil, err
77+
}
78+
repos := result.Data.Repositories.Nodes
79+
if len(result.Errors) == 0 {
80+
return repos, nil, nil
81+
}
82+
83+
errors := api.NewGraphQlErrors(result.Errors)
84+
if len(repos) > 0 {
85+
return repos, errors, nil
86+
}
87+
88+
return nil, nil, errors
89+
}
90+
91+
func gqlErrorPathString(pathSegment any) (string, bool) {
92+
value, ok := pathSegment.(string)
93+
return value, ok
94+
}
95+
96+
func gqlErrorIndex(pathSegment any) (int, bool) {
97+
switch value := pathSegment.(type) {
98+
case float64:
99+
index := int(value)
100+
return index, float64(index) == value && index >= 0
101+
case int:
102+
return value, value >= 0
103+
default:
104+
return 0, false
105+
}
106+
}
107+
108+
func gqlWarningPath(graphQLError *api.GraphQlError) string {
109+
path, err := graphQLError.Path()
110+
if err != nil || len(path) == 0 {
111+
return ""
112+
}
113+
114+
var b strings.Builder
115+
for _, pathSegment := range path {
116+
if segment, ok := gqlErrorPathString(pathSegment); ok {
117+
if b.Len() > 0 {
118+
b.WriteByte('.')
119+
}
120+
b.WriteString(segment)
121+
continue
122+
}
123+
124+
if index, ok := gqlErrorIndex(pathSegment); ok {
125+
fmt.Fprintf(&b, "[%d]", index)
126+
}
127+
}
128+
129+
return b.String()
130+
}
131+
132+
func gqlWarningMessage(graphQLError *api.GraphQlError) string {
133+
message, err := graphQLError.Message()
134+
if err != nil || message == "" {
135+
return graphQLError.Error()
136+
}
137+
return message
138+
}
139+
140+
func formatRepositoryListWarnings(warnings api.GraphQlErrors) string {
141+
var b strings.Builder
142+
fmt.Fprintf(&b, "warnings: %d errors during listing\n", len(warnings))
143+
for _, warning := range warnings {
144+
path := gqlWarningPath(warning)
145+
message := gqlWarningMessage(warning)
146+
if path != "" {
147+
fmt.Fprintf(&b, "%s - %s\n", path, message)
148+
} else {
149+
fmt.Fprintf(&b, "%s\n", message)
150+
}
151+
fmt.Fprintf(&b, "%s\n", warning.Error())
152+
}
153+
return b.String()
154+
}
155+
12156
func init() {
13157
usage := `
14158
Examples:
@@ -64,33 +208,6 @@ Examples:
64208
return err
65209
}
66210

67-
query := `query Repositories(
68-
$first: Int,
69-
$query: String,
70-
$cloned: Boolean,
71-
$notCloned: Boolean,
72-
$indexed: Boolean,
73-
$notIndexed: Boolean,
74-
$orderBy: RepositoryOrderBy,
75-
$descending: Boolean,
76-
) {
77-
repositories(
78-
first: $first,
79-
query: $query,
80-
cloned: $cloned,
81-
notCloned: $notCloned,
82-
indexed: $indexed,
83-
notIndexed: $notIndexed,
84-
orderBy: $orderBy,
85-
descending: $descending,
86-
) {
87-
nodes {
88-
...RepositoryFields
89-
}
90-
}
91-
}
92-
` + repositoryFragment
93-
94211
var orderBy string
95212
switch *orderByFlag {
96213
case "name":
@@ -101,25 +218,22 @@ Examples:
101218
return fmt.Errorf("invalid -order-by flag value: %q", *orderByFlag)
102219
}
103220

104-
var result struct {
105-
Repositories struct {
106-
Nodes []Repository
107-
}
108-
}
109-
if ok, err := client.NewRequest(query, map[string]any{
110-
"first": api.NullInt(*firstFlag),
111-
"query": api.NullString(*queryFlag),
112-
"cloned": *clonedFlag,
113-
"notCloned": *notClonedFlag,
114-
"indexed": *indexedFlag,
115-
"notIndexed": *notIndexedFlag,
116-
"orderBy": orderBy,
117-
"descending": *descendingFlag,
118-
}).Do(context.Background(), &result); err != nil || !ok {
221+
// if we get repos and errors during a listing, we consider the errors as warnings and the data partially complete
222+
repos, warnings, err := listRepositories(context.Background(), client, reposListOptions{
223+
first: *firstFlag,
224+
query: *queryFlag,
225+
cloned: *clonedFlag,
226+
notCloned: *notClonedFlag,
227+
indexed: *indexedFlag,
228+
notIndexed: *notIndexedFlag,
229+
orderBy: orderBy,
230+
descending: *descendingFlag,
231+
})
232+
if err != nil {
119233
return err
120234
}
121235

122-
for _, repo := range result.Repositories.Nodes {
236+
for _, repo := range repos {
123237
if *namesWithoutHostFlag {
124238
firstSlash := strings.Index(repo.Name, "/")
125239
fmt.Println(repo.Name[firstSlash+len("/"):])
@@ -130,6 +244,13 @@ Examples:
130244
return err
131245
}
132246
}
247+
if len(warnings) > 0 {
248+
if *verbose {
249+
fmt.Fprint(flagSet.Output(), formatRepositoryListWarnings(warnings))
250+
} else {
251+
fmt.Fprintf(flagSet.Output(), "warning: %d errors during listing; rerun with -v to inspect them\n", len(warnings))
252+
}
253+
}
133254
return nil
134255
}
135256

0 commit comments

Comments
 (0)