Skip to content

Commit 1f88596

Browse files
LawnGnomeeseliger
andauthored
Render alert fields when performing searches (#221)
This wires up a common renderer to the `actions exec` and `search` commands to render any alert that comes back in a (hopefully) human readable form. Co-authored-by: Erik Seliger <[email protected]>
1 parent 708844e commit 1f88596

File tree

7 files changed

+231
-2
lines changed

7 files changed

+231
-2
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ All notable changes to `src-cli` are documented in this file.
1515

1616
- Pull missing docker images automatically. [#191](https://github.com/sourcegraph/src-cli/pull/191)
1717

18+
- Searches that result in errors will now display any alerts returned by Sourcegraph, including suggestions for how the search could be corrected. [#221](https://github.com/sourcegraph/src-cli/pull/221)
19+
1820
### Changed
1921

2022
- The terminal UI has been replaced by the logger-based UI that was previously only visible in verbose-mode (`-v`). [#228](https://github.com/sourcegraph/src-cli/pull/228)

cmd/src/actions_exec.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,7 @@ query ActionRepos($query: String!) {
483483
}
484484
}
485485
}
486+
...SearchResultsAlertFields
486487
}
487488
}
488489
}
@@ -500,7 +501,8 @@ fragment repositoryFields on Repository {
500501
}
501502
}
502503
}
503-
`
504+
` + searchResultsAlertFragment
505+
504506
type Repository struct {
505507
ID, Name string
506508
ExternalRepository struct {
@@ -527,6 +529,7 @@ fragment repositoryFields on Repository {
527529
}
528530
Repository Repository `json:"repository"`
529531
}
532+
Alert searchResultsAlert
530533
}
531534
}
532535
} `json:"data,omitempty"`
@@ -634,6 +637,12 @@ fragment repositoryFields on Repository {
634637
yellow.Fprintf(os.Stderr, "WARNING: No repositories matched by scopeQuery\n")
635638
}
636639

640+
if content, err := result.Data.Search.Results.Alert.Render(); err != nil {
641+
yellow.Fprint(os.Stderr, err)
642+
} else {
643+
os.Stderr.WriteString(content)
644+
}
645+
637646
return repos, nil
638647
}
639648

cmd/src/colors.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,13 @@ var ansiColors = map[string]string{
3737
"search-commit-author": fg256Color(2),
3838
"search-commit-subject": fg256Color(68),
3939
"search-commit-date": fg256Color(23),
40+
41+
// Search alert specific colors.
42+
"search-alert-title": fg256Color(124),
43+
"search-alert-description": fg256Color(124),
44+
"search-alert-proposed-title": "",
45+
"search-alert-proposed-query": fg256Color(69),
46+
"search-alert-proposed-description": "",
4047
}
4148

4249
// Borrowed from https://github.com/acarl005/stripansi/blob/master/stripansi.go

cmd/src/format.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,16 @@ func parseTemplate(text string) (*template.Template, error) {
148148

149149
return buf.String()
150150
},
151+
152+
// Alert rendering
153+
"searchAlertRender": func(alert searchResultsAlert) string {
154+
if content, err := alert.Render(); err != nil {
155+
fmt.Fprintf(os.Stderr, "Error rendering search alert: %v\n", err)
156+
return ""
157+
} else {
158+
return content
159+
}
160+
},
151161
})
152162
return tmpl.Parse(text)
153163
}

cmd/src/search.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,10 +211,11 @@ Other tips:
211211
}
212212
resultCount
213213
elapsedMilliseconds
214+
...SearchResultsAlertFields
214215
}
215216
}
216217
}
217-
`
218+
` + searchResultsAlertFragment
218219

219220
var result struct {
220221
Site struct {
@@ -281,6 +282,7 @@ type searchResults struct {
281282
Cloning, Missing, Timedout []map[string]interface{}
282283
ResultCount int
283284
ElapsedMilliseconds int
285+
Alert searchResultsAlert
284286
}
285287

286288
// searchResultsImproved is a superset of what the GraphQL API returns. It
@@ -569,6 +571,9 @@ const searchResultsTemplate = `{{- /* ignore this line for template formatting s
569571
{{- with .Timedout}}{{color "warning"}}{{"\n"}}({{len .}}) timed out:{{color "nc"}} {{join (repoNames .) ", "}}{{end -}}
570572
{{"\n"}}
571573
574+
{{- /* Any alert returned from the search */ -}}
575+
{{- searchAlertRender .Alert -}}
576+
572577
{{- /* Rendering of results */ -}}
573578
{{- range .Results -}}
574579
{{- if ne .__typename "Repository" -}}

cmd/src/search_alert.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+
"strings"
5+
"text/template"
6+
7+
"github.com/pkg/errors"
8+
)
9+
10+
var searchResultsAlertTemplate *template.Template
11+
12+
func init() {
13+
var err error
14+
15+
if searchResultsAlertTemplate, err = parseTemplate(searchResultsAlertTemplateContent); err != nil {
16+
// This shouldn't fail, since we control the template content via a
17+
// constant below.
18+
panic(err)
19+
}
20+
}
21+
22+
// searchResultsAlert is a type that can be used to unmarshal values returned by
23+
// the searchResultsAlertFragment GraphQL fragment below.
24+
type searchResultsAlert struct {
25+
Title string
26+
Description string
27+
ProposedQueries []struct {
28+
Description string
29+
Query string
30+
}
31+
}
32+
33+
// Render renders an alert to a string ready to be output to a console,
34+
// respecting the colour configuration in use by the current process. If the
35+
// alert is empty, then an empty string will be returned.
36+
func (alert *searchResultsAlert) Render() (string, error) {
37+
b := &strings.Builder{}
38+
if err := searchResultsAlertTemplate.Execute(b, alert); err != nil {
39+
return "", errors.Wrap(err, "rendering alert template")
40+
}
41+
return b.String(), nil
42+
}
43+
44+
// searchResultsAlertFragment provides a GraphQL fragment that can be used to
45+
// hydrate a searchResultsAlert instance.
46+
const searchResultsAlertFragment = `
47+
fragment SearchResultsAlertFields on SearchResults {
48+
alert {
49+
title
50+
description
51+
proposedQueries {
52+
description
53+
query
54+
}
55+
}
56+
}
57+
`
58+
59+
const searchResultsAlertTemplateContent = `
60+
{{- if gt (len .Title) 0 -}}
61+
{{- color "search-alert-title"}}❗{{.Title}}{{color "nc"}}{{"\n"}}
62+
{{- end -}}
63+
64+
{{- if gt (len .Description) 0 -}}
65+
{{- color "search-alert-description"}} {{.Description}}{{color "nc"}}{{"\n"}}
66+
{{- end -}}
67+
68+
{{- if gt (len .ProposedQueries) 0 -}}
69+
{{- color "search-alert-proposed-title"}} Did you mean:{{color "nc" -}}
70+
{{- "\n" -}}
71+
{{- range .ProposedQueries -}}
72+
{{- color "search-alert-proposed-query"}} {{.Query}}{{color "nc" -}}
73+
{{- " - " -}}
74+
{{- color "search-alert-proposed-description"}}{{.Description}}{{color "nc" -}}
75+
{{- "\n" -}}
76+
{{- end -}}
77+
{{- end -}}
78+
`

cmd/src/search_alert_test.go

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"regexp"
6+
"strings"
7+
"testing"
8+
9+
"github.com/google/go-cmp/cmp"
10+
)
11+
12+
func TestRender(t *testing.T) {
13+
full := &searchResultsAlert{
14+
Title: "foo",
15+
Description: "bar",
16+
ProposedQueries: []struct {
17+
Description string
18+
Query string
19+
}{
20+
{
21+
Description: "quux",
22+
Query: "xyz:abc",
23+
},
24+
{
25+
Description: "baz",
26+
Query: "def:ghi",
27+
},
28+
},
29+
}
30+
31+
zero := &searchResultsAlert{}
32+
33+
t.Run("zero value", func(t *testing.T) {
34+
content, err := zero.Render()
35+
if err != nil {
36+
t.Errorf("unexpected error rendering zero alert: %v", err)
37+
}
38+
39+
if content != "" {
40+
t.Errorf("unexpected content for zero alert: %s", content)
41+
}
42+
})
43+
44+
t.Run("no description", func(t *testing.T) {
45+
alert := *full
46+
alert.Description = zero.Description
47+
48+
content, err := alert.Render()
49+
if err != nil {
50+
t.Errorf("unexpected error rendering alert without description: %v", err)
51+
}
52+
53+
expected := subColorCodes(strings.Join([]string{
54+
"[[search-alert-title]]❗foo[[nc]]\n",
55+
"[[search-alert-proposed-title]] Did you mean:[[nc]]\n",
56+
"[[search-alert-proposed-query]] xyz:abc[[nc]] - [[search-alert-proposed-description]]quux[[nc]]\n",
57+
"[[search-alert-proposed-query]] def:ghi[[nc]] - [[search-alert-proposed-description]]baz[[nc]]\n",
58+
}, ""))
59+
if diff := cmp.Diff(expected, content); diff != "" {
60+
t.Errorf("alert without description content incorrect: %s", diff)
61+
}
62+
})
63+
64+
t.Run("no proposed queries", func(t *testing.T) {
65+
alert := *full
66+
alert.ProposedQueries = zero.ProposedQueries
67+
68+
content, err := alert.Render()
69+
if err != nil {
70+
t.Errorf("unexpected error rendering alert without queries: %v", err)
71+
}
72+
73+
expected := subColorCodes(strings.Join([]string{
74+
"[[search-alert-title]]❗foo[[nc]]\n",
75+
"[[search-alert-description]] bar[[nc]]\n",
76+
}, ""))
77+
if diff := cmp.Diff(expected, content); diff != "" {
78+
t.Errorf("alert without queries content incorrect: %s", diff)
79+
}
80+
})
81+
82+
t.Run("full", func(t *testing.T) {
83+
content, err := full.Render()
84+
if err != nil {
85+
t.Errorf("unexpected error rendering full alert: %v", err)
86+
}
87+
88+
expected := subColorCodes(strings.Join([]string{
89+
"[[search-alert-title]]❗foo[[nc]]\n",
90+
"[[search-alert-description]] bar[[nc]]\n",
91+
"[[search-alert-proposed-title]] Did you mean:[[nc]]\n",
92+
"[[search-alert-proposed-query]] xyz:abc[[nc]] - [[search-alert-proposed-description]]quux[[nc]]\n",
93+
"[[search-alert-proposed-query]] def:ghi[[nc]] - [[search-alert-proposed-description]]baz[[nc]]\n",
94+
}, ""))
95+
if diff := cmp.Diff(expected, content); diff != "" {
96+
t.Errorf("full alert content incorrect: %s", diff)
97+
}
98+
})
99+
}
100+
101+
var subColorCodesRegex = regexp.MustCompile(`\[\[[a-zA-Z0-9-]+\]\]`)
102+
103+
// subColorCodes provides ad-hoc templating of just ANSI colour codes from our
104+
// ansiColors map, using a [[colour-code]] syntax.
105+
func subColorCodes(in string) string {
106+
// We could use text/template here, but at a certain point it feels like
107+
// we're just reinventing the template string that's a const in
108+
// search_alert.go. This allows us to express the colour codes in the
109+
// expected string literals above while hopefully maintaining some meaning
110+
// to the tests.
111+
return subColorCodesRegex.ReplaceAllStringFunc(in, func(code string) string {
112+
esc, ok := ansiColors[strings.Trim(code, "[]")]
113+
if !ok {
114+
panic(fmt.Sprintf("cannot find colour %s", code))
115+
}
116+
return esc
117+
})
118+
}

0 commit comments

Comments
 (0)