1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204
|
package code
import (
"fmt"
"strings"
"github.com/MakeNowJust/heredoc"
"github.com/cli/cli/v2/internal/browser"
"github.com/cli/cli/v2/internal/text"
"github.com/cli/cli/v2/pkg/cmd/search/shared"
"github.com/cli/cli/v2/pkg/cmdutil"
"github.com/cli/cli/v2/pkg/iostreams"
"github.com/cli/cli/v2/pkg/search"
"github.com/spf13/cobra"
)
type CodeOptions struct {
Browser browser.Browser
Exporter cmdutil.Exporter
IO *iostreams.IOStreams
Query search.Query
Searcher search.Searcher
WebMode bool
}
func NewCmdCode(f *cmdutil.Factory, runF func(*CodeOptions) error) *cobra.Command {
opts := &CodeOptions{
Browser: f.Browser,
IO: f.IOStreams,
Query: search.Query{Kind: search.KindCode},
}
cmd := &cobra.Command{
Use: "code <query>",
Short: "Search within code",
Long: heredoc.Doc(`
Search within code in GitHub repositories.
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"
$ gh search code react lifecycle
# search code matching "error handling"
$ gh search code "error handling"
# search code matching "deque" in Python files
$ gh search code deque --language=python
# search code matching "cli" in repositories owned by microsoft organization
$ gh search code cli --owner=microsoft
# 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
$ gh search code lint --filename package.json
`),
RunE: func(c *cobra.Command, args []string) error {
if len(args) == 0 && c.Flags().NFlag() == 0 {
return cmdutil.FlagErrorf("specify search keywords or flags")
}
if opts.Query.Limit < 1 || opts.Query.Limit > shared.SearchMaxResults {
return cmdutil.FlagErrorf("`--limit` must be between 1 and 1000")
}
opts.Query.Keywords = args
if runF != nil {
return runF(opts)
}
var err error
opts.Searcher, err = shared.Searcher(f)
if err != nil {
return err
}
return codeRun(opts)
},
}
// Output flags
cmdutil.AddJSONFlags(cmd, &opts.Exporter, search.CodeFields)
cmd.Flags().BoolVarP(&opts.WebMode, "web", "w", false, "Open the search query in the web browser")
// Query parameter flags
cmd.Flags().IntVarP(&opts.Query.Limit, "limit", "L", 30, "Maximum number of code results to fetch")
// Query qualifier flags
cmd.Flags().StringVar(&opts.Query.Qualifiers.Extension, "extension", "", "Filter on file extension")
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")
cmd.Flags().StringVarP(&opts.Query.Qualifiers.Language, "language", "", "", "Filter results by language")
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().StringSliceVar(&opts.Query.Qualifiers.User, "owner", nil, "Filter on owner")
return cmd
}
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))
}
return opts.Browser.Browse(url)
}
io.StartProgressIndicator()
results, err := opts.Searcher.Code(opts.Query)
io.StopProgressIndicator()
if err != nil {
return err
}
if len(results.Items) == 0 && opts.Exporter == nil {
return cmdutil.NewNoResultsError("no code results matched your search")
}
if err := io.StartPager(); err == nil {
defer io.StopPager()
} else {
fmt.Fprintf(io.ErrOut, "failed to start pager: %v\n", err)
}
if opts.Exporter != nil {
return opts.Exporter.Write(io, results.Items)
}
return displayResults(io, results)
}
func displayResults(io *iostreams.IOStreams, results search.CodeResult) error {
cs := io.ColorScheme()
if io.IsStdoutTTY() {
fmt.Fprintf(io.Out, "\nShowing %d of %d results\n\n", len(results.Items), results.Total)
for i, code := range results.Items {
if i > 0 {
fmt.Fprint(io.Out, "\n")
}
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 {
fmt.Fprintf(io.Out, "\t%s\n", strings.TrimSpace(line))
}
}
}
return nil
}
for _, code := range results.Items {
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.Repository.FullName), cs.GreenBold(code.Path), strings.TrimSpace(line))
}
}
}
return nil
}
func formatMatch(t string, matches []search.Match, colorize bool) []string {
startIndices := map[int]struct{}{}
endIndices := map[int]struct{}{}
for _, m := range matches {
if len(m.Indices) < 2 {
continue
}
startIndices[m.Indices[0]] = struct{}{}
endIndices[m.Indices[1]] = struct{}{}
}
var lines []string
var b strings.Builder
var found bool
for i, c := range t {
if c == '\n' {
if found {
lines = append(lines, b.String())
}
found = false
b.Reset()
continue
}
if _, ok := startIndices[i]; ok {
if colorize {
b.WriteString("\x1b[30;43m") // black text on yellow background
}
found = true
} else if _, ok := endIndices[i]; ok {
if colorize {
b.WriteString("\x1b[m") // color reset
}
}
b.WriteRune(c)
}
if found {
lines = append(lines, b.String())
}
return lines
}
|