incorporate code review feedback

This commit is contained in:
Josh Kraft 2023-05-08 17:02:53 -06:00
parent 43d5345b8b
commit 88887c8d55
7 changed files with 87 additions and 67 deletions

View file

@ -29,7 +29,7 @@ func NewCmdCode(f *cmdutil.Factory, runF func(*CodeOptions) error) *cobra.Comman
opts := &CodeOptions{ opts := &CodeOptions{
Browser: f.Browser, Browser: f.Browser,
IO: f.IOStreams, IO: f.IOStreams,
Query: search.Query{Kind: search.KindCode, TextMatch: true}, Query: search.Query{Kind: search.KindCode},
} }
cmd := &cobra.Command{ cmd := &cobra.Command{
@ -59,9 +59,6 @@ func NewCmdCode(f *cmdutil.Factory, runF func(*CodeOptions) error) *cobra.Comman
# search code matching "cli" in repositories owned by microsoft organization # search code matching "cli" in repositories owned by microsoft organization
$ gh search code cli --owner=microsoft $ gh search code cli --owner=microsoft
# search code matching "html" in repositories owned by octocat
$ gh search code html --owner=octocat
# search code matching "panic" in cli repository # search code matching "panic" in cli repository
$ gh search code panic --repo cli/cli $ gh search code panic --repo cli/cli
@ -99,8 +96,8 @@ func NewCmdCode(f *cmdutil.Factory, runF func(*CodeOptions) error) *cobra.Comman
cmd.Flags().StringVar(&opts.Query.Qualifiers.Extension, "extension", "", "Filter on file extension") cmd.Flags().StringVar(&opts.Query.Qualifiers.Extension, "extension", "", "Filter on file extension")
cmd.Flags().StringVar(&opts.Query.Qualifiers.Filename, "filename", "", "Filter on filename") cmd.Flags().StringVar(&opts.Query.Qualifiers.Filename, "filename", "", "Filter on filename")
cmdutil.StringSliceEnumFlag(cmd, &opts.Query.Qualifiers.In, "match", "", nil, []string{"file", "path"}, "Restrict search to file contents or file path") cmdutil.StringSliceEnumFlag(cmd, &opts.Query.Qualifiers.In, "match", "", nil, []string{"file", "path"}, "Restrict search to file contents or file path")
cmd.Flags().StringVarP(&opts.Query.Qualifiers.Language, "language", "l", "", "Filter results by language") cmd.Flags().StringVarP(&opts.Query.Qualifiers.Language, "language", "", "", "Filter results by language")
cmd.Flags().StringSliceVar(&opts.Query.Qualifiers.Repo, "repo", nil, "Filter on repository") cmd.Flags().StringSliceVarP(&opts.Query.Qualifiers.Repo, "repo", "R", nil, "Filter on repository")
cmd.Flags().StringVar(&opts.Query.Qualifiers.Size, "size", "", "Filter on size range, in kilobytes") cmd.Flags().StringVar(&opts.Query.Qualifiers.Size, "size", "", "Filter on size range, in kilobytes")
cmd.Flags().StringSliceVar(&opts.Query.Qualifiers.User, "owner", nil, "Filter on owner") cmd.Flags().StringSliceVar(&opts.Query.Qualifiers.User, "owner", nil, "Filter on owner")
@ -131,7 +128,7 @@ func codeRun(opts *CodeOptions) error {
fmt.Fprintf(io.ErrOut, "failed to start pager: %v\n", err) fmt.Fprintf(io.ErrOut, "failed to start pager: %v\n", err)
} }
if opts.Exporter != nil { if opts.Exporter != nil {
return opts.Exporter.Write(io, results) return opts.Exporter.Write(io, results.Items)
} }
return displayResults(io, results, opts.Query.Limit) return displayResults(io, results, opts.Query.Limit)
@ -141,33 +138,28 @@ func displayResults(io *iostreams.IOStreams, results search.CodeResult, limit in
cs := io.ColorScheme() cs := io.ColorScheme()
tp := tableprinter.New(io) tp := tableprinter.New(io)
displayed := 0 displayed := 0
outer:
for _, code := range results.Items { for _, code := range results.Items {
if displayed == limit {
break
}
if io.IsStdoutTTY() { if io.IsStdoutTTY() {
header := fmt.Sprintf("%s %s", cs.GreenBold(code.Repo.FullName), cs.GreenBold(code.Path)) header := fmt.Sprintf("%s %s", cs.GreenBold(code.Repo.FullName), cs.GreenBold(code.Path))
tp.AddField(header) tp.AddField(header)
tp.EndRow() tp.EndRow()
} }
row := 0 i := 1
for _, textMatch := range code.TextMatches { for _, match := range code.TextMatches {
out, shouldPrint := buildOutput(textMatch, cs) fragments := formatMatch(cs, match)
for i, line := range strings.Split(out, "\n") { for _, fragment := range fragments {
if !shouldPrint[i] {
continue
}
if displayed == limit {
break
}
if io.IsStdoutTTY() { if io.IsStdoutTTY() {
tp.AddField(fmt.Sprintf("%s: %s", cs.CyanBold(strconv.Itoa(row+1)), line)) tp.AddField(fmt.Sprintf("%s: %s", strconv.Itoa(i), fragment))
} else { } else {
tp.AddField(fmt.Sprintf("%s %s: %s", code.Repo.FullName, code.Path, line)) tp.AddField(fmt.Sprintf("%s %s: %s", code.Repo.FullName, code.Path, fragment))
} }
tp.EndRow() tp.EndRow()
row++ i++
displayed++ displayed++
if displayed == limit {
break outer
}
} }
} }
} }
@ -178,27 +170,32 @@ func displayResults(io *iostreams.IOStreams, results search.CodeResult, limit in
return tp.Render() return tp.Render()
} }
func buildOutput(tm search.TextMatch, cs *iostreams.ColorScheme) (string, map[int]bool) { func formatMatch(cs *iostreams.ColorScheme, tm search.TextMatch) []string {
shouldHighlight := getHighlightIndices(tm.Matches) shouldHighlight := matchHighlightMask(tm.Fragment, tm.Matches)
linesToPrint := make(map[int]bool) var lines []string
line := 0 var found bool
var out strings.Builder var b strings.Builder
for i, c := range tm.Fragment { for i, c := range tm.Fragment {
if shouldHighlight[i] {
out.WriteString(cs.CyanBold(string(c)))
linesToPrint[line] = true
} else {
out.WriteRune(c)
}
if c == '\n' { if c == '\n' {
line++ if found {
lines = append(lines, b.String())
}
found = false
b.Reset()
continue
}
if shouldHighlight[i] {
b.WriteString(cs.CyanBold(string(c)))
found = true
} else {
b.WriteRune(c)
} }
} }
return out.String(), linesToPrint return lines
} }
func getHighlightIndices(matches []search.Match) map[int]bool { func matchHighlightMask(fragment string, matches []search.Match) []bool {
m := make(map[int]bool) m := make([]bool, len(fragment))
for _, match := range matches { for _, match := range matches {
start := match.Indices[0] start := match.Indices[0]
stop := match.Indices[1] stop := match.Indices[1]

View file

@ -32,10 +32,9 @@ func TestNewCmdCode(t *testing.T) {
input: "react lifecycle", input: "react lifecycle",
output: CodeOptions{ output: CodeOptions{
Query: search.Query{ Query: search.Query{
Keywords: []string{"react", "lifecycle"}, Keywords: []string{"react", "lifecycle"},
Kind: "code", Kind: "code",
Limit: 30, Limit: 30,
TextMatch: true,
}, },
}, },
}, },
@ -44,10 +43,9 @@ func TestNewCmdCode(t *testing.T) {
input: "--web", input: "--web",
output: CodeOptions{ output: CodeOptions{
Query: search.Query{ Query: search.Query{
Keywords: []string{}, Keywords: []string{},
Kind: "code", Kind: "code",
Limit: 30, Limit: 30,
TextMatch: true,
}, },
WebMode: true, WebMode: true,
}, },
@ -57,10 +55,9 @@ func TestNewCmdCode(t *testing.T) {
input: "--limit 10", input: "--limit 10",
output: CodeOptions{ output: CodeOptions{
Query: search.Query{ Query: search.Query{
Keywords: []string{}, Keywords: []string{},
Kind: "code", Kind: "code",
Limit: 10, Limit: 10,
TextMatch: true,
}, },
}, },
}, },
@ -83,10 +80,9 @@ func TestNewCmdCode(t *testing.T) {
`, `,
output: CodeOptions{ output: CodeOptions{
Query: search.Query{ Query: search.Query{
Keywords: []string{}, Keywords: []string{},
Kind: "code", Kind: "code",
Limit: 30, Limit: 30,
TextMatch: true,
Qualifiers: search.Qualifiers{ Qualifiers: search.Qualifiers{
Extension: "extension", Extension: "extension",
Filename: "filename", Filename: "filename",
@ -141,7 +137,6 @@ func TestCodeRun(t *testing.T) {
Language: "go", Language: "go",
Repo: []string{"cli/cli"}, Repo: []string{"cli/cli"},
}, },
TextMatch: true,
} }
tests := []struct { tests := []struct {
errMsg string errMsg string

View file

@ -23,7 +23,6 @@ type Query struct {
Page int Page int
Qualifiers Qualifiers Qualifiers Qualifiers
Sort string Sort string
TextMatch bool
} }
type Qualifiers struct { type Qualifiers struct {

View file

@ -15,6 +15,14 @@ var CodeFields = []string{
"url", "url",
} }
var TextMatchFields = []string{
"fragment",
"matches",
"url",
"type",
"property",
}
var CommitFields = []string{ var CommitFields = []string{
"author", "author",
"commit", "commit",
@ -115,11 +123,11 @@ type Code struct {
} }
type TextMatch struct { type TextMatch struct {
Fragment string `json:"fragment"` Fragment string `json:"fragment"`
Matches []Match `json:"matches"` Matches []Match `json:"matches"`
ObjectURL string `json:"object_url"` URL string `json:"object_url"`
ObjectType string `json:"object_type"` Type string `json:"object_type"`
Property string `json:"property"` Property string `json:"property"`
} }
type Match struct { type Match struct {
@ -274,6 +282,25 @@ func (code Code) ExportData(fields []string) map[string]interface{} {
switch f { switch f {
case "repository": case "repository":
data[f] = code.Repo.ExportData(RepositoryFields) data[f] = code.Repo.ExportData(RepositoryFields)
case "textMatches":
matches := make([]interface{}, 0, len(code.TextMatches))
for _, match := range code.TextMatches {
matches = append(matches, match.ExportData(TextMatchFields))
}
data[f] = matches
default:
sf := fieldByName(v, f)
data[f] = sf.Interface()
}
}
return data
}
func (textMatch TextMatch) ExportData(fields []string) map[string]interface{} {
v := reflect.ValueOf(textMatch)
data := map[string]interface{}{}
for _, f := range fields {
switch f {
default: default:
sf := fieldByName(v, f) sf := fieldByName(v, f)
data[f] = sf.Interface() data[f] = sf.Interface()

View file

@ -39,10 +39,13 @@ func TestCodeExportData(t *testing.T) {
}, },
}, },
}, },
Property: "property",
Type: "type",
URL: "url",
}, },
}, },
}, },
output: `{"name":"name","path":"path","textMatches":[{"fragment":"fragment","matches":[{"indices":[0,1],"text":"fr"}],"object_url":"","object_type":"","property":""}]}`, output: `{"name":"name","path":"path","textMatches":[{"fragment":"fragment","matches":[{"indices":[0,1],"text":"fr"}],"property":"property","type":"type","url":"url"}]}`,
}, },
} }
for _, tt := range tests { for _, tt := range tests {

View file

@ -173,7 +173,7 @@ func (s searcher) search(query Query, result interface{}) (*http.Response, error
} }
req.Header.Set("Content-Type", "application/json; charset=utf-8") req.Header.Set("Content-Type", "application/json; charset=utf-8")
req.Header.Set("Accept", "application/vnd.github.v3+json") req.Header.Set("Accept", "application/vnd.github.v3+json")
if query.TextMatch { if query.Kind == KindCode {
req.Header.Set("Accept", "application/vnd.github.text-match+json") req.Header.Set("Accept", "application/vnd.github.text-match+json")
} }

View file

@ -12,10 +12,9 @@ import (
func TestSearcherCode(t *testing.T) { func TestSearcherCode(t *testing.T) {
query := Query{ query := Query{
Keywords: []string{"keyword"}, Keywords: []string{"keyword"},
Kind: "code", Kind: "code",
Limit: 30, Limit: 30,
TextMatch: true,
Qualifiers: Qualifiers{ Qualifiers: Qualifiers{
Language: "go", Language: "go",
}, },