package code import ( "fmt" "strings" "github.com/MakeNowJust/heredoc" "github.com/cli/cli/v2/internal/browser" "github.com/cli/cli/v2/internal/gh" "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" ghauth "github.com/cli/go-gh/v2/pkg/auth" "github.com/spf13/cobra" ) type CodeOptions struct { Browser browser.Browser Config func() (gh.Config, error) 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, Config: f.Config, IO: f.IOStreams, Query: search.Query{Kind: search.KindCode}, } cmd := &cobra.Command{ Use: "code ", Short: "Search within code", Long: heredoc.Docf(` Search within code in GitHub repositories. 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 %[1]sgithub.com%[1]s, and new features like regex search are not yet available via the GitHub API. For more information on handling search queries containing a hyphen, run %[1]sgh search --help%[1]s. `, "`"), 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 { // Convert `filename` and `extension` legacy search qualifiers to the new code search's `path` // qualifier when used with `--web` because they are incompatible. if opts.Query.Qualifiers.Filename != "" || opts.Query.Qualifiers.Extension != "" { cfg, err := opts.Config() if err != nil { return err } host, _ := cfg.Authentication().DefaultHost() // FIXME: Remove this check once GHES supports the new `path` search qualifier. if !ghauth.IsEnterprise(host) { filename := opts.Query.Qualifiers.Filename extension := opts.Query.Qualifiers.Extension if extension != "" && !strings.HasPrefix(extension, ".") { extension = "." + extension } opts.Query.Qualifiers.Filename = "" opts.Query.Qualifiers.Extension = "" opts.Query.Qualifiers.Path = fmt.Sprintf("%s%s", filename, extension) } } 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) 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) 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, io *iostreams.IOStreams) []string { cs := io.ColorScheme() 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 { b.WriteString(cs.HighlightStart()) found = true } else if _, ok := endIndices[i]; ok { b.WriteString(cs.Reset()) } b.WriteRune(c) } if found { lines = append(lines, b.String()) } return lines }