Repository: cli/cli
Pull request #11211 (2025-07-03 05:56:54)

Transform `extension` and `filename` qualifiers into `path` qualifier for web code search
Fixes: #11029

Acceptance criteria: https://github.com/cli/cli/issues/11029#issuecomment-2984406886

This PR fixes the `search code` command when either the `--extension` or `--filename` flags were used in conjunction with the `--web` flag. The `extension` and `filename` qualifiers that these flags corresponded are no longer supported and cause an error for the user. To fix this the code does a simple conversion of the old `extension` and `filename` qualifiers into the supported `path` qualifier. This conversion only takes place when targeting `github.com` and does not occur when targeting GHES.

One thing to note is that I chose to use this simple format `path := fmt.Sprintf("%s.%s", filename, extension)` to construct the `path` qualifier which differs slightly from the acceptance criteria. I did this as it gave search results that better mirrored the API search results in my brief testing. Let me know if you would like me to change this. 


Patch:
diff --git a/pkg/cmd/search/code/code.go b/pkg/cmd/search/code/code.go
index 88f84a125..d03526c4d 100644
--- a/pkg/cmd/search/code/code.go
+++ b/pkg/cmd/search/code/code.go
@@ -6,16 +6,19 @@ import (
 
 	"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
@@ -26,6 +29,7 @@ type CodeOptions struct {
 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},
 	}
@@ -104,8 +108,26 @@ 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.
+		// 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))
diff --git a/pkg/search/query.go b/pkg/search/query.go
index 4e1990ea2..c517f90d8 100644
--- a/pkg/search/query.go
+++ b/pkg/search/query.go
@@ -64,6 +64,7 @@ type Qualifiers struct {
 	Milestone           string
 	No                  []string
 	Parent              string
+	Path                string
 	Project             string
 	Pushed              string
 	Reactions           string


Test patch:
diff --git a/pkg/cmd/search/code/code_test.go b/pkg/cmd/search/code/code_test.go
index 4b493c6d5..efb5f4b57 100644
--- a/pkg/cmd/search/code/code_test.go
+++ b/pkg/cmd/search/code/code_test.go
@@ -6,6 +6,9 @@ import (
 	"testing"
 
 	"github.com/cli/cli/v2/internal/browser"
+	"github.com/cli/cli/v2/internal/config"
+	"github.com/cli/cli/v2/internal/gh"
+	ghmock "github.com/cli/cli/v2/internal/gh/mock"
 	"github.com/cli/cli/v2/pkg/cmdutil"
 	"github.com/cli/cli/v2/pkg/iostreams"
 	"github.com/cli/cli/v2/pkg/search"
@@ -146,6 +149,7 @@ func TestCodeRun(t *testing.T) {
 		wantErr    bool
 		wantStderr string
 		wantStdout string
+		wantBrowse string
 	}{
 		{
 			name: "displays results tty",
@@ -294,8 +298,7 @@ func TestCodeRun(t *testing.T) {
 		{
 			name: "opens browser for web mode tty",
 			opts: &CodeOptions{
-				Browser: &browser.Stub{},
-				Query:   query,
+				Query: query,
 				Searcher: &search.SearcherMock{
 					URLFunc: func(query search.Query) string {
 						return "https://github.com/search?type=code&q=map+repo%3Acli%2Fcli"
@@ -305,12 +308,12 @@ func TestCodeRun(t *testing.T) {
 			},
 			tty:        true,
 			wantStderr: "Opening https://github.com/search in your browser.\n",
+			wantBrowse: "https://github.com/search?type=code&q=map+repo%3Acli%2Fcli",
 		},
 		{
 			name: "opens browser for web mode notty",
 			opts: &CodeOptions{
-				Browser: &browser.Stub{},
-				Query:   query,
+				Query: query,
 				Searcher: &search.SearcherMock{
 					URLFunc: func(query search.Query) string {
 						return "https://github.com/search?type=code&q=map+repo%3Acli%2Fcli"
@@ -318,6 +321,70 @@ func TestCodeRun(t *testing.T) {
 				},
 				WebMode: true,
 			},
+			wantBrowse: "https://github.com/search?type=code&q=map+repo%3Acli%2Fcli",
+		},
+		{
+			name: "converts filename and extension qualifiers for github.com web search",
+			opts: &CodeOptions{
+				Config: func() (gh.Config, error) { return config.NewBlankConfig(), nil },
+				Query: search.Query{
+					Keywords: []string{"map"},
+					Kind:     "code",
+					Limit:    30,
+					Qualifiers: search.Qualifiers{
+						Filename:  "testing",
+						Extension: "go",
+					},
+				},
+				Searcher: search.NewSearcher(nil, "github.com"),
+				WebMode:  true,
+			},
+			wantBrowse: "https://github.com/search?q=map+path%3Atesting.go&type=code",
+		},
+		{
+			name: "properly handles extension with dot prefix when converting to path qualifier",
+			opts: &CodeOptions{
+				Config: func() (gh.Config, error) { return config.NewBlankConfig(), nil },
+				Query: search.Query{
+					Keywords: []string{"map"},
+					Kind:     "code",
+					Limit:    30,
+					Qualifiers: search.Qualifiers{
+						Filename:  "testing",
+						Extension: ".cpp",
+					},
+				},
+				Searcher: search.NewSearcher(nil, "github.com"),
+				WebMode:  true,
+			},
+			wantBrowse: "https://github.com/search?q=map+path%3Atesting.cpp&type=code",
+		},
+		{
+			name: "does not convert filename and extension qualifiers for GHES web search",
+			opts: &CodeOptions{
+				Config: func() (gh.Config, error) {
+					cfg := &ghmock.ConfigMock{
+						AuthenticationFunc: func() gh.AuthConfig {
+							authCfg := &config.AuthConfig{}
+							authCfg.SetDefaultHost("example.com", "GH_HOST")
+							return authCfg
+						},
+					}
+					return cfg, nil
+				},
+				Query: search.Query{
+					Keywords: []string{"map"},
+					Kind:     "code",
+					Limit:    30,
+					Qualifiers: search.Qualifiers{
+						Filename:  "testing",
+						Extension: "go",
+					},
+				},
+				Searcher: search.NewSearcher(nil, "example.com"),
+				WebMode:  true,
+			},
+			wantBrowse: "https://example.com/search?q=map+extension%3Ago+filename%3Atesting&type=code",
 		},
 	}
 
@@ -327,6 +394,8 @@ func TestCodeRun(t *testing.T) {
 		ios.SetStdoutTTY(tt.tty)
 		ios.SetStderrTTY(tt.tty)
 		tt.opts.IO = ios
+		browser := &browser.Stub{}
+		tt.opts.Browser = browser
 		t.Run(tt.name, func(t *testing.T) {
 			err := codeRun(tt.opts)
 			if tt.wantErr {
@@ -337,6 +406,7 @@ func TestCodeRun(t *testing.T) {
 			}
 			assert.Equal(t, tt.wantStdout, stdout.String())
 			assert.Equal(t, tt.wantStderr, stderr.String())
+			browser.Verify(t, tt.wantBrowse)
 		})
 	}
 }