From 94a6312eaf7ed115e5be708bfaaa6fbe638d75fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mislav=20Marohni=C4=87?= Date: Fri, 9 Jun 2023 17:36:19 +0200 Subject: [PATCH] :nail_care: json export --- pkg/cmd/search/code/code.go | 25 ++++++++------- pkg/cmd/search/code/code_test.go | 8 ++--- pkg/search/result.go | 22 ++++++++----- pkg/search/result_test.go | 7 ++--- pkg/search/searcher_test.go | 54 ++++++++++++++++++++------------ 5 files changed, 68 insertions(+), 48 deletions(-) diff --git a/pkg/cmd/search/code/code.go b/pkg/cmd/search/code/code.go index e79b371f6..39f9d2f0c 100644 --- a/pkg/cmd/search/code/code.go +++ b/pkg/cmd/search/code/code.go @@ -31,18 +31,17 @@ func NewCmdCode(f *cmdutil.Factory, runF func(*CodeOptions) error) *cobra.Comman } cmd := &cobra.Command{ - Use: "code []", - Short: "Search for code", + Use: "code ", + Short: "Search within code", Long: heredoc.Doc(` - Search for code on GitHub. + Search within code in GitHub repositories. - The command supports constructing queries using the GitHub search syntax, - using the parameter and qualifier flags, or a combination of the two. - - At least one search term is required when searching code. - - GitHub search syntax is documented at: + The search syntax is documented at: + + Note that these search results are powered by what is now a legacy GitHub code search engine. + The results might not match what is seen on GitHub.com, and new features like regex search + are not yet available via the GitHub API. `), Example: heredoc.Doc(` # search code matching "react" and "lifecycle" @@ -57,7 +56,7 @@ func NewCmdCode(f *cmdutil.Factory, runF func(*CodeOptions) error) *cobra.Comman # search code matching "cli" in repositories owned by microsoft organization $ gh search code cli --owner=microsoft - # search code matching "panic" in cli repository + # search code matching "panic" in the GitHub CLI repository $ gh search code panic --repo cli/cli # search code matching keyword "lint" in package.json files @@ -105,6 +104,8 @@ func NewCmdCode(f *cmdutil.Factory, runF func(*CodeOptions) error) *cobra.Comman func codeRun(opts *CodeOptions) error { io := opts.IO if opts.WebMode { + // FIXME: convert legacy `filename` and `extension` ES qualifiers to Blackbird's `path` qualifier + // when opening web search, otherwise the Blackbird search UI will complain. url := opts.Searcher.URL(opts.Query) if io.IsStdoutTTY() { fmt.Fprintf(io.ErrOut, "Opening %s in your browser.\n", text.DisplayURL(url)) @@ -140,7 +141,7 @@ func displayResults(io *iostreams.IOStreams, results search.CodeResult) error { if i > 0 { fmt.Fprint(io.Out, "\n") } - fmt.Fprintf(io.Out, "%s %s\n", cs.Blue(code.Repo.FullName), cs.GreenBold(code.Path)) + fmt.Fprintf(io.Out, "%s %s\n", cs.Blue(code.Repository.FullName), cs.GreenBold(code.Path)) for _, match := range code.TextMatches { lines := formatMatch(match.Fragment, match.Matches, io.ColorEnabled()) for _, line := range lines { @@ -154,7 +155,7 @@ func displayResults(io *iostreams.IOStreams, results search.CodeResult) error { for _, match := range code.TextMatches { lines := formatMatch(match.Fragment, match.Matches, io.ColorEnabled()) for _, line := range lines { - fmt.Fprintf(io.Out, "%s:%s: %s\n", cs.Blue(code.Repo.FullName), cs.GreenBold(code.Path), strings.TrimSpace(line)) + fmt.Fprintf(io.Out, "%s:%s: %s\n", cs.Blue(code.Repository.FullName), cs.GreenBold(code.Path), strings.TrimSpace(line)) } } } diff --git a/pkg/cmd/search/code/code_test.go b/pkg/cmd/search/code/code_test.go index fc950ffcb..253bd2889 100644 --- a/pkg/cmd/search/code/code_test.go +++ b/pkg/cmd/search/code/code_test.go @@ -159,7 +159,7 @@ func TestCodeRun(t *testing.T) { { Name: "context.go", Path: "context/context.go", - Repo: search.Repository{ + Repository: search.Repository{ FullName: "cli/cli", }, TextMatches: []search.TextMatch{ @@ -181,7 +181,7 @@ func TestCodeRun(t *testing.T) { { Name: "pr.go", Path: "pkg/cmd/pr/pr.go", - Repo: search.Repository{ + Repository: search.Repository{ FullName: "cli/cli", }, TextMatches: []search.TextMatch{ @@ -218,7 +218,7 @@ func TestCodeRun(t *testing.T) { { Name: "context.go", Path: "context/context.go", - Repo: search.Repository{ + Repository: search.Repository{ FullName: "cli/cli", }, TextMatches: []search.TextMatch{ @@ -240,7 +240,7 @@ func TestCodeRun(t *testing.T) { { Name: "pr.go", Path: "pkg/cmd/pr/pr.go", - Repo: search.Repository{ + Repository: search.Repository{ FullName: "cli/cli", }, TextMatches: []search.TextMatch{ diff --git a/pkg/search/result.go b/pkg/search/result.go index cfd78ccf1..de00001db 100644 --- a/pkg/search/result.go +++ b/pkg/search/result.go @@ -1,13 +1,13 @@ package search import ( + "encoding/json" "reflect" "strings" "time" ) var CodeFields = []string{ - "name", "path", "repository", "sha", @@ -15,10 +15,9 @@ var CodeFields = []string{ "url", } -var TextMatchFields = []string{ +var textMatchFields = []string{ "fragment", "matches", - "url", "type", "property", } @@ -116,7 +115,7 @@ type IssuesResult struct { type Code struct { Name string `json:"name"` Path string `json:"path"` - Repo Repository `json:"repository"` + Repository Repository `json:"repository"` Sha string `json:"sha"` TextMatches []TextMatch `json:"text_matches"` URL string `json:"html_url"` @@ -125,7 +124,6 @@ type Code struct { type TextMatch struct { Fragment string `json:"fragment"` Matches []Match `json:"matches"` - URL string `json:"object_url"` Type string `json:"object_type"` Property string `json:"property"` } @@ -280,12 +278,10 @@ func (code Code) ExportData(fields []string) map[string]interface{} { data := map[string]interface{}{} for _, f := range fields { switch f { - case "repository": - 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)) + matches = append(matches, match.ExportData(textMatchFields)) } data[f] = matches default: @@ -385,6 +381,16 @@ func (repo Repository) ExportData(fields []string) map[string]interface{} { return data } +func (repo Repository) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]interface{}{ + "id": repo.ID, + "nameWithOwner": repo.FullName, + "url": repo.URL, + "isPrivate": repo.IsPrivate, + "isFork": repo.IsFork, + }) +} + // The state of an issue or a pull request, may be either open or closed. // For a pull request, the "merged" state is inferred from a value for merged_at and // which we take return instead of the "closed" state. diff --git a/pkg/search/result_test.go b/pkg/search/result_test.go index 930b80351..3d9fb67b3 100644 --- a/pkg/search/result_test.go +++ b/pkg/search/result_test.go @@ -20,9 +20,9 @@ func TestCodeExportData(t *testing.T) { }{ { name: "exports requested fields", - fields: []string{"name", "path", "textMatches"}, + fields: []string{"path", "textMatches"}, code: Code{ - Repo: Repository{ + Repository: Repository{ Name: "repo", }, Path: "path", @@ -41,11 +41,10 @@ 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"}],"property":"property","type":"type","url":"url"}]}`, + output: `{"path":"path","textMatches":[{"fragment":"fragment","matches":[{"indices":[0,1],"text":"fr"}],"property":"property","type":"type"}]}`, }, } for _, tt := range tests { diff --git a/pkg/search/searcher_test.go b/pkg/search/searcher_test.go index e0c41cfca..8642feed0 100644 --- a/pkg/search/searcher_test.go +++ b/pkg/search/searcher_test.go @@ -358,10 +358,14 @@ func TestSearcherRepositories(t *testing.T) { httpStubs: func(reg *httpmock.Registry) { reg.Register( httpmock.QueryMatcher("GET", "search/repositories", values), - httpmock.JSONResponse(RepositoriesResult{ - IncompleteResults: false, - Items: []Repository{{Name: "test"}}, - Total: 1, + httpmock.JSONResponse(map[string]interface{}{ + "incomplete_results": false, + "total_count": 1, + "items": []interface{}{ + map[string]interface{}{ + "name": "test", + }, + }, }), ) }, @@ -378,10 +382,14 @@ func TestSearcherRepositories(t *testing.T) { httpStubs: func(reg *httpmock.Registry) { reg.Register( httpmock.QueryMatcher("GET", "api/v3/search/repositories", values), - httpmock.JSONResponse(RepositoriesResult{ - IncompleteResults: false, - Items: []Repository{{Name: "test"}}, - Total: 1, + httpmock.JSONResponse(map[string]interface{}{ + "incomplete_results": false, + "total_count": 1, + "items": []interface{}{ + map[string]interface{}{ + "name": "test", + }, + }, }), ) }, @@ -396,12 +404,15 @@ func TestSearcherRepositories(t *testing.T) { }, httpStubs: func(reg *httpmock.Registry) { firstReq := httpmock.QueryMatcher("GET", "search/repositories", values) - firstRes := httpmock.JSONResponse(RepositoriesResult{ - IncompleteResults: false, - Items: []Repository{{Name: "test"}}, - Total: 2, - }, - ) + firstRes := httpmock.JSONResponse(map[string]interface{}{ + "incomplete_results": false, + "total_count": 2, + "items": []interface{}{ + map[string]interface{}{ + "name": "test", + }, + }, + }) firstRes = httpmock.WithHeader(firstRes, "Link", `; rel="next"`) secondReq := httpmock.QueryMatcher("GET", "search/repositories", url.Values{ "page": []string{"2"}, @@ -411,12 +422,15 @@ func TestSearcherRepositories(t *testing.T) { "q": []string{"keyword stars:>=5 topic:topic"}, }, ) - secondRes := httpmock.JSONResponse(RepositoriesResult{ - IncompleteResults: false, - Items: []Repository{{Name: "cli"}}, - Total: 2, - }, - ) + secondRes := httpmock.JSONResponse(map[string]interface{}{ + "incomplete_results": false, + "total_count": 2, + "items": []interface{}{ + map[string]interface{}{ + "name": "cli", + }, + }, + }) reg.Register(firstReq, firstRes) reg.Register(secondReq, secondRes) },