💅 json export

This commit is contained in:
Mislav Marohnić 2023-06-09 17:36:19 +02:00
parent 46797e20e5
commit 94a6312eaf
No known key found for this signature in database
5 changed files with 68 additions and 48 deletions

View file

@ -31,18 +31,17 @@ func NewCmdCode(f *cmdutil.Factory, runF func(*CodeOptions) error) *cobra.Comman
}
cmd := &cobra.Command{
Use: "code [<query>]",
Short: "Search for code",
Use: "code <query>",
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:
<https://docs.github.com/search-github/searching-on-github/searching-code>
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))
}
}
}

View file

@ -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{

View file

@ -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.

View file

@ -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 {

View file

@ -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", `<https://api.github.com/search/repositories?page=2&per_page=100&q=org%3Agithub>; 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)
},