Repository: cli/cli
Pull request #5410 (2022-04-05 21:32:45)

Make location an optional flag when creating codespaces
- Rely on server-side location determination by default. Both the [machines endpoint](https://docs.github.com/en/rest/reference/codespaces#list-available-machine-types-for-a-repository) and the [creation endpoint](https://docs.github.com/en/rest/reference/codespaces#create-a-codespace-for-the-authenticated-user) itself treat the `location` parameter as optional.
- Allow specifying a location directly with the `l` flag, for instance: `gh codespace create -l WestEurope`
- Remove unused functions (including the API call recommending a location) that has the CLI determine the location up front

I manually tested the following:
- `gh codespace` behaved normally and gave me a `WestUs2` codespace
- `gh codespace create -l WestEurope` behaved normally and gave me a `WestEurope` codespace



Patch:
diff --git a/docs/install_linux.md b/docs/install_linux.md
index 1dc9007a0..2c3579d65 100644
--- a/docs/install_linux.md
+++ b/docs/install_linux.md
@@ -32,7 +32,6 @@ sudo apt install gh
 Install from our package repository for immediate access to latest releases:
 
 ```bash
-sudo dnf install 'dnf-command(config-manager)'
 sudo dnf config-manager --add-repo https://cli.github.com/packages/rpm/gh-cli.repo
 sudo dnf install gh
 ```
diff --git a/internal/codespaces/api/api.go b/internal/codespaces/api/api.go
index 49b41b7e5..8e3008d8d 100644
--- a/internal/codespaces/api/api.go
+++ b/internal/codespaces/api/api.go
@@ -432,40 +432,6 @@ func (a *API) StopCodespace(ctx context.Context, codespaceName string) error {
 	return nil
 }
 
-type getCodespaceRegionLocationResponse struct {
-	Current string `json:"current"`
-}
-
-// GetCodespaceRegionLocation returns the closest codespace location for the user.
-func (a *API) GetCodespaceRegionLocation(ctx context.Context) (string, error) {
-	req, err := http.NewRequest(http.MethodGet, a.vscsAPI+"/api/v1/locations", nil)
-	if err != nil {
-		return "", fmt.Errorf("error creating request: %w", err)
-	}
-
-	resp, err := a.do(ctx, req, req.URL.String())
-	if err != nil {
-		return "", fmt.Errorf("error making request: %w", err)
-	}
-	defer resp.Body.Close()
-
-	if resp.StatusCode != http.StatusOK {
-		return "", api.HandleHTTPError(resp)
-	}
-
-	b, err := ioutil.ReadAll(resp.Body)
-	if err != nil {
-		return "", fmt.Errorf("error reading response body: %w", err)
-	}
-
-	var response getCodespaceRegionLocationResponse
-	if err := json.Unmarshal(b, &response); err != nil {
-		return "", fmt.Errorf("error unmarshaling response: %w", err)
-	}
-
-	return response.Current, nil
-}
-
 type Machine struct {
 	Name                 string `json:"name"`
 	DisplayName          string `json:"display_name"`
diff --git a/pkg/cmd/codespace/common.go b/pkg/cmd/codespace/common.go
index aec386f6c..dde1a0f30 100644
--- a/pkg/cmd/codespace/common.go
+++ b/pkg/cmd/codespace/common.go
@@ -71,7 +71,6 @@ type apiClient interface {
 	EditCodespace(ctx context.Context, codespaceName string, params *api.EditCodespaceParams) (*api.Codespace, error)
 	GetRepository(ctx context.Context, nwo string) (*api.Repository, error)
 	AuthorizedKeys(ctx context.Context, user string) ([]byte, error)
-	GetCodespaceRegionLocation(ctx context.Context) (string, error)
 	GetCodespacesMachines(ctx context.Context, repoID int, branch, location string) ([]*api.Machine, error)
 	GetCodespaceRepositoryContents(ctx context.Context, codespace *api.Codespace, path string) ([]byte, error)
 	GetCodespaceRepoSuggestions(ctx context.Context, partialSearch string, params api.RepoSearchParameters) ([]string, error)
diff --git a/pkg/cmd/codespace/create.go b/pkg/cmd/codespace/create.go
index 81bb57bc4..74a1fe830 100644
--- a/pkg/cmd/codespace/create.go
+++ b/pkg/cmd/codespace/create.go
@@ -18,6 +18,7 @@ import (
 type createOptions struct {
 	repo              string
 	branch            string
+	location          string
 	machine           string
 	showStatus        bool
 	permissionsOptOut bool
@@ -38,6 +39,7 @@ func newCreateCmd(app *App) *cobra.Command {
 
 	createCmd.Flags().StringVarP(&opts.repo, "repo", "r", "", "repository name with owner: user/repo")
 	createCmd.Flags().StringVarP(&opts.branch, "branch", "b", "", "repository branch")
+	createCmd.Flags().StringVarP(&opts.location, "location", "l", "", "location: {EastUs|SouthEastAsia|WestEurope|WestUs2} (determined automatically if not provided)")
 	createCmd.Flags().StringVarP(&opts.machine, "machine", "m", "", "hardware specifications for the VM")
 	createCmd.Flags().BoolVarP(&opts.permissionsOptOut, "default-permissions", "", false, "do not prompt to accept additional permissions requested by the codespace")
 	createCmd.Flags().BoolVarP(&opts.showStatus, "status", "s", false, "show status of post-create command and dotfiles")
@@ -54,14 +56,14 @@ func (a *App) Create(ctx context.Context, opts createOptions) error {
 	vscsTarget := os.Getenv("VSCS_TARGET")
 	vscsTargetUrl := os.Getenv("VSCS_TARGET_URL")
 
-	locationCh := getLocation(ctx, vscsLocation, a.apiClient)
-
 	userInputs := struct {
 		Repository string
 		Branch     string
+		Location   string
 	}{
 		Repository: opts.repo,
 		Branch:     opts.branch,
+		Location:   opts.location,
 	}
 
 	if userInputs.Repository == "" {
@@ -94,6 +96,10 @@ func (a *App) Create(ctx context.Context, opts createOptions) error {
 		}
 	}
 
+	if userInputs.Location == "" && vscsLocation != "" {
+		userInputs.Location = vscsLocation
+	}
+
 	a.StartProgressIndicatorWithLabel("Fetching repository")
 	repository, err := a.apiClient.GetRepository(ctx, userInputs.Repository)
 	a.StopProgressIndicator()
@@ -106,12 +112,7 @@ func (a *App) Create(ctx context.Context, opts createOptions) error {
 		branch = repository.DefaultBranch
 	}
 
-	locationResult := <-locationCh
-	if locationResult.Err != nil {
-		return fmt.Errorf("error getting codespace region location: %w", locationResult.Err)
-	}
-
-	machine, err := getMachineName(ctx, a.apiClient, repository.ID, opts.machine, branch, locationResult.Location)
+	machine, err := getMachineName(ctx, a.apiClient, repository.ID, opts.machine, branch, userInputs.Location)
 	if err != nil {
 		return fmt.Errorf("error getting machine type: %w", err)
 	}
@@ -123,7 +124,7 @@ func (a *App) Create(ctx context.Context, opts createOptions) error {
 		RepositoryID:       repository.ID,
 		Branch:             branch,
 		Machine:            machine,
-		Location:           locationResult.Location,
+		Location:           userInputs.Location,
 		VSCSTarget:         vscsTarget,
 		VSCSTargetURL:      vscsTargetUrl,
 		IdleTimeoutMinutes: int(opts.idleTimeout.Minutes()),
@@ -287,30 +288,6 @@ func (a *App) showStatus(ctx context.Context, codespace *api.Codespace) error {
 	return nil
 }
 
-type locationResult struct {
-	Location string
-	Err      error
-}
-
-// getLocation fetches the closest Codespace datacenter
-// region/location to the user, unless the 'vscsLocationOverride' override is set
-func getLocation(ctx context.Context, vscsLocationOverride string, apiClient apiClient) <-chan locationResult {
-	ch := make(chan locationResult, 1)
-
-	// Developer override is set, return the override
-	if vscsLocationOverride != "" {
-		ch <- locationResult{vscsLocationOverride, nil}
-		return ch
-	}
-
-	// Dynamically fetch the region location
-	go func() {
-		location, err := apiClient.GetCodespaceRegionLocation(ctx)
-		ch <- locationResult{location, err}
-	}()
-	return ch
-}
-
 // getMachineName prompts the user to select the machine type, or validates the machine if non-empty.
 func getMachineName(ctx context.Context, apiClient apiClient, repoID int, machine, branch, location string) (string, error) {
 	machines, err := apiClient.GetCodespacesMachines(ctx, repoID, branch, location)
diff --git a/pkg/cmd/gist/edit/edit.go b/pkg/cmd/gist/edit/edit.go
index ff4e59b98..830e6bb4d 100644
--- a/pkg/cmd/gist/edit/edit.go
+++ b/pkg/cmd/gist/edit/edit.go
@@ -244,9 +244,11 @@ func editRun(opts *EditOptions) error {
 			filesToUpdate[filename] = text
 		}
 
-		if !opts.IO.CanPrompt() ||
-			len(candidates) == 1 ||
-			opts.EditFilename != "" {
+		if !opts.IO.CanPrompt() {
+			break
+		}
+
+		if len(candidates) == 1 {
 			break
 		}
 
diff --git a/pkg/cmd/issue/list/http.go b/pkg/cmd/issue/list/http.go
index db7633157..40a32d748 100644
--- a/pkg/cmd/issue/list/http.go
+++ b/pkg/cmd/issue/list/http.go
@@ -143,18 +143,15 @@ func searchIssues(client *api.Client, repo ghrepo.Interface, filters prShared.Fi
 		}
 	}
 
-	filters.Repo = ghrepo.FullName(repo)
-	filters.Entity = "issue"
-	q := prShared.SearchQueryBuild(filters)
-
 	perPage := min(limit, 100)
+	searchQuery := fmt.Sprintf("repo:%s/%s %s", repo.RepoOwner(), repo.RepoName(), prShared.SearchQueryBuild(filters))
 
 	variables := map[string]interface{}{
 		"owner": repo.RepoOwner(),
 		"repo":  repo.RepoName(),
 		"type":  "ISSUE",
 		"limit": perPage,
-		"query": q,
+		"query": searchQuery,
 	}
 
 	ic := api.IssuesAndTotalCount{SearchCapped: limit > 1000}
diff --git a/pkg/cmd/pr/list/http.go b/pkg/cmd/pr/list/http.go
index 5887551cf..ae99ef33e 100644
--- a/pkg/cmd/pr/list/http.go
+++ b/pkg/cmd/pr/list/http.go
@@ -7,10 +7,11 @@ import (
 	"github.com/cli/cli/v2/api"
 	"github.com/cli/cli/v2/internal/ghrepo"
 	prShared "github.com/cli/cli/v2/pkg/cmd/pr/shared"
+	"github.com/cli/cli/v2/pkg/githubsearch"
 )
 
 func shouldUseSearch(filters prShared.FilterOptions) bool {
-	return filters.Draft != nil || filters.Author != "" || filters.Assignee != "" || filters.Search != "" || len(filters.Labels) > 0
+	return filters.Draft != "" || filters.Author != "" || filters.Assignee != "" || filters.Search != "" || len(filters.Labels) > 0
 }
 
 func listPullRequests(httpClient *http.Client, repo ghrepo.Interface, filters prShared.FilterOptions, limit int) (*api.PullRequestAndTotalCount, error) {
@@ -158,12 +159,41 @@ func searchPullRequests(httpClient *http.Client, repo ghrepo.Interface, filters
 			}
 		}`
 
-	filters.Repo = ghrepo.FullName(repo)
-	filters.Entity = "pr"
-	q := prShared.SearchQueryBuild(filters)
+	q := githubsearch.NewQuery()
+	q.SetType(githubsearch.PullRequest)
+	q.InRepository(ghrepo.FullName(repo))
+	q.AddQuery(filters.Search)
+
+	switch filters.State {
+	case "open":
+		q.SetState(githubsearch.Open)
+	case "closed":
+		q.SetState(githubsearch.Closed)
+	case "merged":
+		q.SetState(githubsearch.Merged)
+	}
+
+	if filters.Author != "" {
+		q.AuthoredBy(filters.Author)
+	}
+	if filters.Assignee != "" {
+		q.AssignedTo(filters.Assignee)
+	}
+	for _, label := range filters.Labels {
+		q.AddLabel(label)
+	}
+	if filters.BaseBranch != "" {
+		q.SetBaseBranch(filters.BaseBranch)
+	}
+
+	if filters.Draft != "" {
+		q.SetDraft(filters.Draft)
+	}
 
 	pageLimit := min(limit, 100)
-	variables := map[string]interface{}{"q": q}
+	variables := map[string]interface{}{
+		"q": q.String(),
+	}
 
 	res := api.PullRequestAndTotalCount{SearchCapped: limit > 1000}
 	var check = make(map[int]struct{})
diff --git a/pkg/cmd/pr/list/list.go b/pkg/cmd/pr/list/list.go
index 577943774..b8e6b8ff5 100644
--- a/pkg/cmd/pr/list/list.go
+++ b/pkg/cmd/pr/list/list.go
@@ -38,7 +38,7 @@ type ListOptions struct {
 	Author     string
 	Assignee   string
 	Search     string
-	Draft      *bool
+	Draft      string
 }
 
 func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Command {
@@ -48,6 +48,7 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
 		Browser:    f.Browser,
 	}
 
+	var draft bool
 	var appAuthor string
 
 	cmd := &cobra.Command{
@@ -82,6 +83,10 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
 				return cmdutil.FlagErrorf("invalid value for --limit: %v", opts.LimitResults)
 			}
 
+			if cmd.Flags().Changed("draft") {
+				opts.Draft = strconv.FormatBool(draft)
+			}
+
 			if cmd.Flags().Changed("author") && cmd.Flags().Changed("app") {
 				return cmdutil.FlagErrorf("specify only `--author` or `--app`")
 			}
@@ -107,7 +112,7 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman
 	cmd.Flags().StringVar(&appAuthor, "app", "", "Filter by GitHub App author")
 	cmd.Flags().StringVarP(&opts.Assignee, "assignee", "a", "", "Filter by assignee")
 	cmd.Flags().StringVarP(&opts.Search, "search", "S", "", "Search pull requests with `query`")
-	cmdutil.NilBoolFlag(cmd, &opts.Draft, "draft", "d", "Filter by draft state")
+	cmd.Flags().BoolVarP(&draft, "draft", "d", false, "Filter by draft state")
 
 	cmdutil.AddJSONFlags(cmd, &opts.Exporter, api.PullRequestFields)
 
diff --git a/pkg/cmd/pr/shared/params.go b/pkg/cmd/pr/shared/params.go
index 833b986d1..a5986541d 100644
--- a/pkg/cmd/pr/shared/params.go
+++ b/pkg/cmd/pr/shared/params.go
@@ -7,7 +7,7 @@ import (
 
 	"github.com/cli/cli/v2/api"
 	"github.com/cli/cli/v2/internal/ghrepo"
-	"github.com/cli/cli/v2/pkg/search"
+	"github.com/cli/cli/v2/pkg/githubsearch"
 	"github.com/google/shlex"
 )
 
@@ -148,19 +148,18 @@ func AddMetadataToIssueParams(client *api.Client, baseRepo ghrepo.Interface, par
 }
 
 type FilterOptions struct {
+	Entity     string
+	State      string
 	Assignee   string
+	Labels     []string
 	Author     string
 	BaseBranch string
-	Draft      *bool
-	Entity     string
-	Fields     []string
 	HeadBranch string
-	Labels     []string
 	Mention    string
 	Milestone  string
-	Repo       string
 	Search     string
-	State      string
+	Draft      string
+	Fields     []string
 }
 
 func (opts *FilterOptions) IsDefault() bool {
@@ -208,23 +207,49 @@ func ListURLWithQuery(listURL string, options FilterOptions) (string, error) {
 }
 
 func SearchQueryBuild(options FilterOptions) string {
-	q := search.Query{
-		Qualifiers: search.Qualifiers{
-			Assignee:  options.Assignee,
-			Author:    options.Author,
-			Base:      options.BaseBranch,
-			Draft:     options.Draft,
-			Head:      options.HeadBranch,
-			Label:     options.Labels,
-			Mentions:  options.Mention,
-			Milestone: options.Milestone,
-			Repo:      []string{options.Repo},
-			State:     options.State,
-			Type:      options.Entity,
-		},
+	q := githubsearch.NewQuery()
+	switch options.Entity {
+	case "issue":
+		q.SetType(githubsearch.Issue)
+	case "pr":
+		q.SetType(githubsearch.PullRequest)
+	}
+
+	switch options.State {
+	case "open":
+		q.SetState(githubsearch.Open)
+	case "closed":
+		q.SetState(githubsearch.Closed)
+	case "merged":
+		q.SetState(githubsearch.Merged)
+	}
+
+	if options.Assignee != "" {
+		q.AssignedTo(options.Assignee)
+	}
+	for _, label := range options.Labels {
+		q.AddLabel(label)
+	}
+	if options.Author != "" {
+		q.AuthoredBy(options.Author)
+	}
+	if options.BaseBranch != "" {
+		q.SetBaseBranch(options.BaseBranch)
+	}
+	if options.HeadBranch != "" {
+		q.SetHeadBranch(options.HeadBranch)
+	}
+	if options.Mention != "" {
+		q.Mentions(options.Mention)
+	}
+	if options.Milestone != "" {
+		q.InMilestone(options.Milestone)
 	}
 	if options.Search != "" {
-		return fmt.Sprintf("%s %s", options.Search, q.String())
+		q.AddQuery(options.Search)
+	}
+	if options.Draft != "" {
+		q.SetDraft(options.Draft)
 	}
 	return q.String()
 }
diff --git a/pkg/cmd/repo/list/http.go b/pkg/cmd/repo/list/http.go
index c6f8844a9..fa5319304 100644
--- a/pkg/cmd/repo/list/http.go
+++ b/pkg/cmd/repo/list/http.go
@@ -6,7 +6,7 @@ import (
 	"strings"
 
 	"github.com/cli/cli/v2/api"
-	"github.com/cli/cli/v2/pkg/search"
+	"github.com/cli/cli/v2/pkg/githubsearch"
 	"github.com/shurcooL/githubv4"
 )
 
@@ -179,37 +179,40 @@ pagination:
 }
 
 func searchQuery(owner string, filter FilterOptions) string {
+	q := githubsearch.NewQuery()
+	q.SortBy(githubsearch.UpdatedAt, githubsearch.Desc)
+
 	if owner == "" {
-		owner = "@me"
+		q.OwnedBy("@me")
+	} else {
+		q.OwnedBy(owner)
 	}
 
-	fork := "true"
 	if filter.Fork {
-		fork = "only"
-	} else if filter.Source {
-		fork = "false"
+		q.OnlyForks()
+	} else {
+		q.IncludeForks(!filter.Source)
+	}
+
+	if filter.Language != "" {
+		q.SetLanguage(filter.Language)
+	}
+
+	if filter.Topic != "" {
+		q.SetTopic(filter.Topic)
+	}
+
+	switch filter.Visibility {
+	case "public":
+		q.SetVisibility(githubsearch.Public)
+	case "private":
+		q.SetVisibility(githubsearch.Private)
 	}
 
-	var archived *bool
 	if filter.Archived {
-		trueBool := true
-		archived = &trueBool
-	}
-	if filter.NonArchived {
-		falseBool := false
-		archived = &falseBool
-	}
-
-	q := search.Query{
-		Keywords: []string{"sort:updated-desc"},
-		Qualifiers: search.Qualifiers{
-			Archived: archived,
-			Fork:     fork,
-			Is:       []string{filter.Visibility},
-			Language: filter.Language,
-			Org:      owner,
-			Topic:    []string{filter.Topic},
-		},
+		q.SetArchived(true)
+	} else if filter.NonArchived {
+		q.SetArchived(false)
 	}
 
 	return q.String()
diff --git a/pkg/githubsearch/query.go b/pkg/githubsearch/query.go
new file mode 100644
index 000000000..a8f3005a9
--- /dev/null
+++ b/pkg/githubsearch/query.go
@@ -0,0 +1,221 @@
+package githubsearch
+
+import (
+	"fmt"
+	"strings"
+)
+
+type EntityType string
+type State string
+type RepoVisibility string
+type SortField string
+type SortDirection int
+
+const (
+	Asc SortDirection = iota
+	Desc
+
+	UpdatedAt SortField = "updated"
+	CreatedAt SortField = "created"
+
+	Issue       EntityType = "issue"
+	PullRequest EntityType = "pr"
+
+	Open   State = "open"
+	Closed State = "closed"
+	Merged State = "merged"
+
+	Public  RepoVisibility = "public"
+	Private RepoVisibility = "private"
+)
+
+func NewQuery() *Query {
+	return &Query{}
+}
+
+type Query struct {
+	repo  string
+	owner string
+	sort  string
+	query string
+
+	entity     string
+	state      string
+	baseBranch string
+	headBranch string
+	labels     []string
+	assignee   string
+	author     string
+	mentions   string
+	milestone  string
+
+	language   string
+	topic      string
+	forkState  string
+	visibility string
+	isArchived *bool
+	draft      string
+}
+
+func (q *Query) InRepository(nameWithOwner string) {
+	q.repo = nameWithOwner
+}
+
+func (q *Query) OwnedBy(owner string) {
+	q.owner = owner
+}
+
+func (q *Query) SortBy(field SortField, direction SortDirection) {
+	var dir string
+	switch direction {
+	case Asc:
+		dir = "asc"
+	case Desc:
+		dir = "desc"
+	}
+	q.sort = fmt.Sprintf("%s-%s", field, dir)
+}
+
+func (q *Query) AddQuery(query string) {
+	q.query = query
+}
+
+func (q *Query) SetType(t EntityType) {
+	q.entity = string(t)
+}
+
+func (q *Query) SetState(s State) {
+	q.state = string(s)
+}
+
+func (q *Query) SetBaseBranch(name string) {
+	q.baseBranch = name
+}
+
+func (q *Query) SetHeadBranch(name string) {
+	q.headBranch = name
+}
+
+func (q *Query) AssignedTo(user string) {
+	q.assignee = user
+}
+
+func (q *Query) AuthoredBy(user string) {
+	q.author = user
+}
+
+func (q *Query) Mentions(handle string) {
+	q.mentions = handle
+}
+
+func (q *Query) InMilestone(title string) {
+	q.milestone = title
+}
+
+func (q *Query) AddLabel(name string) {
+	q.labels = append(q.labels, name)
+}
+
+func (q *Query) SetLanguage(name string) {
+	q.language = name
+}
+
+func (q *Query) SetTopic(name string) {
+	q.topic = name
+}
+
+func (q *Query) SetVisibility(visibility RepoVisibility) {
+	q.visibility = string(visibility)
+}
+
+func (q *Query) OnlyForks() {
+	q.forkState = "only"
+}
+
+func (q *Query) IncludeForks(include bool) {
+	q.forkState = fmt.Sprintf("%v", include)
+}
+
+func (q *Query) SetArchived(isArchived bool) {
+	q.isArchived = &isArchived
+}
+
+func (q *Query) SetDraft(draft string) {
+	q.draft = draft
+}
+
+func (q *Query) String() string {
+	var qs string
+
+	// context
+	if q.repo != "" {
+		qs += fmt.Sprintf("repo:%s ", q.repo)
+	} else if q.owner != "" {
+		qs += fmt.Sprintf("user:%s ", q.owner)
+	}
+
+	// type
+	if q.entity != "" {
+		qs += fmt.Sprintf("is:%s ", q.entity)
+	}
+	if q.state != "" {
+		qs += fmt.Sprintf("is:%s ", q.state)
+	}
+
+	// repositories
+	if q.visibility != "" {
+		qs += fmt.Sprintf("is:%s ", q.visibility)
+	}
+	if q.language != "" {
+		qs += fmt.Sprintf("language:%s ", quote(q.language))
+	}
+	if q.topic != "" {
+		qs += fmt.Sprintf("topic:%s ", quote(q.topic))
+	}
+	if q.forkState != "" {
+		qs += fmt.Sprintf("fork:%s ", q.forkState)
+	}
+	if q.isArchived != nil {
+		qs += fmt.Sprintf("archived:%v ", *q.isArchived)
+	}
+
+	// issues
+	if q.assignee != "" {
+		qs += fmt.Sprintf("assignee:%s ", q.assignee)
+	}
+	for _, label := range q.labels {
+		qs += fmt.Sprintf("label:%s ", quote(label))
+	}
+	if q.author != "" {
+		qs += fmt.Sprintf("author:%s ", q.author)
+	}
+	if q.mentions != "" {
+		qs += fmt.Sprintf("mentions:%s ", q.mentions)
+	}
+	if q.milestone != "" {
+		qs += fmt.Sprintf("milestone:%s ", quote(q.milestone))
+	}
+
+	// pull requests
+	if q.baseBranch != "" {
+		qs += fmt.Sprintf("base:%s ", quote(q.baseBranch))
+	}
+	if q.headBranch != "" {
+		qs += fmt.Sprintf("head:%s ", quote(q.headBranch))
+	}
+	if q.draft != "" {
+		qs += fmt.Sprintf("draft:%v ", q.draft)
+	}
+
+	if q.sort != "" {
+		qs += fmt.Sprintf("sort:%s ", q.sort)
+	}
+	return strings.TrimRight(qs+q.query, " ")
+}
+
+func quote(v string) string {
+	if strings.ContainsAny(v, " \"\t\r\n") {
+		return fmt.Sprintf("%q", v)
+	}
+	return v
+}
diff --git a/pkg/search/query.go b/pkg/search/query.go
index 895f4e157..865afb433 100644
--- a/pkg/search/query.go
+++ b/pkg/search/query.go
@@ -74,7 +74,7 @@ func (q Query) String() string {
 	qualifiers := formatQualifiers(q.Qualifiers)
 	keywords := formatKeywords(q.Keywords)
 	all := append(keywords, qualifiers...)
-	return strings.TrimSpace(strings.Join(all, " "))
+	return strings.Join(all, " ")
 }
 
 func (q Qualifiers) Map() map[string][]string {
@@ -99,9 +99,6 @@ func (q Qualifiers) Map() map[string][]string {
 			}
 			s := []string{}
 			for i := 0; i < value.Len(); i++ {
-				if value.Index(i).IsZero() {
-					continue
-				}
 				s = append(s, fmt.Sprintf("%v", value.Index(i)))
 			}
 			m[key] = s


Test patch:
diff --git a/pkg/cmd/codespace/create_test.go b/pkg/cmd/codespace/create_test.go
index 2d417620d..76f4af88c 100644
--- a/pkg/cmd/codespace/create_test.go
+++ b/pkg/cmd/codespace/create_test.go
@@ -27,9 +27,6 @@ func TestApp_Create(t *testing.T) {
 			name: "create codespace with default branch and 30m idle timeout",
 			fields: fields{
 				apiClient: &apiClientMock{
-					GetCodespaceRegionLocationFunc: func(ctx context.Context) (string, error) {
-						return "EUROPE", nil
-					},
 					GetRepositoryFunc: func(ctx context.Context, nwo string) (*api.Repository, error) {
 						return &api.Repository{
 							ID:            1234,
@@ -74,9 +71,6 @@ func TestApp_Create(t *testing.T) {
 			name: "create codespace that requires accepting additional permissions",
 			fields: fields{
 				apiClient: &apiClientMock{
-					GetCodespaceRegionLocationFunc: func(ctx context.Context) (string, error) {
-						return "EUROPE", nil
-					},
 					GetRepositoryFunc: func(ctx context.Context, nwo string) (*api.Repository, error) {
 						return &api.Repository{
 							ID:            1234,
diff --git a/pkg/cmd/issue/list/list_test.go b/pkg/cmd/issue/list/list_test.go
index b892d99c5..78415aac4 100644
--- a/pkg/cmd/issue/list/list_test.go
+++ b/pkg/cmd/issue/list/list_test.go
@@ -227,7 +227,7 @@ func TestIssueList_web(t *testing.T) {
 
 	assert.Equal(t, "", stdout.String())
 	assert.Equal(t, "Opening github.com/OWNER/REPO/issues in your browser.\n", stderr.String())
-	browser.Verify(t, "https://github.com/OWNER/REPO/issues?q=assignee%3Apeter+author%3Ajohn+label%3Abug+label%3Adocs+mentions%3Afrank+milestone%3Av1.1+state%3Aall+type%3Aissue")
+	browser.Verify(t, "https://github.com/OWNER/REPO/issues?q=is%3Aissue+assignee%3Apeter+label%3Abug+label%3Adocs+author%3Ajohn+mentions%3Afrank+milestone%3Av1.1")
 }
 
 func Test_issueList(t *testing.T) {
@@ -302,7 +302,7 @@ func Test_issueList(t *testing.T) {
 							"owner": "OWNER",
 							"repo":  "REPO",
 							"limit": float64(30),
-							"query": "milestone:1.x repo:OWNER/REPO state:open type:issue",
+							"query": "repo:OWNER/REPO is:issue is:open milestone:1.x",
 							"type":  "ISSUE",
 						}, params)
 					}))
@@ -334,7 +334,7 @@ func Test_issueList(t *testing.T) {
 							"owner": "OWNER",
 							"repo":  "REPO",
 							"limit": float64(30),
-							"query": "milestone:1.x repo:OWNER/REPO state:open type:issue",
+							"query": "repo:OWNER/REPO is:issue is:open milestone:1.x",
 							"type":  "ISSUE",
 						}, params)
 					}))
@@ -405,7 +405,7 @@ func Test_issueList(t *testing.T) {
 							"owner": "OWNER",
 							"repo":  "REPO",
 							"limit": float64(30),
-							"query": "auth bug assignee:@me author:@me mentions:@me repo:OWNER/REPO state:open type:issue",
+							"query": "repo:OWNER/REPO is:issue is:open assignee:@me author:@me mentions:@me auth bug",
 							"type":  "ISSUE",
 						}, params)
 					}))
@@ -437,7 +437,7 @@ func Test_issueList(t *testing.T) {
 							"owner": "OWNER",
 							"repo":  "REPO",
 							"limit": float64(30),
-							"query": `label:"one world" label:hello repo:OWNER/REPO state:open type:issue`,
+							"query": `repo:OWNER/REPO is:issue is:open label:hello label:"one world"`,
 							"type":  "ISSUE",
 						}, params)
 					}))
diff --git a/pkg/cmd/pr/list/http_test.go b/pkg/cmd/pr/list/http_test.go
index 95230245b..bdb136100 100644
--- a/pkg/cmd/pr/list/http_test.go
+++ b/pkg/cmd/pr/list/http_test.go
@@ -87,7 +87,7 @@ func Test_listPullRequests(t *testing.T) {
 					httpmock.GraphQL(`query PullRequestSearch\b`),
 					httpmock.GraphQLQuery(`{"data":{}}`, func(query string, vars map[string]interface{}) {
 						want := map[string]interface{}{
-							"q":     `label:"one world" label:hello repo:OWNER/REPO state:open type:pr`,
+							"q":     `repo:OWNER/REPO is:pr is:open label:hello label:"one world"`,
 							"limit": float64(30),
 						}
 						if !reflect.DeepEqual(vars, want) {
@@ -111,7 +111,7 @@ func Test_listPullRequests(t *testing.T) {
 					httpmock.GraphQL(`query PullRequestSearch\b`),
 					httpmock.GraphQLQuery(`{"data":{}}`, func(query string, vars map[string]interface{}) {
 						want := map[string]interface{}{
-							"q":     "author:monalisa repo:OWNER/REPO state:open type:pr",
+							"q":     "repo:OWNER/REPO is:pr is:open author:monalisa",
 							"limit": float64(30),
 						}
 						if !reflect.DeepEqual(vars, want) {
@@ -135,7 +135,7 @@ func Test_listPullRequests(t *testing.T) {
 					httpmock.GraphQL(`query PullRequestSearch\b`),
 					httpmock.GraphQLQuery(`{"data":{}}`, func(query string, vars map[string]interface{}) {
 						want := map[string]interface{}{
-							"q":     "one world in:title repo:OWNER/REPO state:open type:pr",
+							"q":     "repo:OWNER/REPO is:pr is:open one world in:title",
 							"limit": float64(30),
 						}
 						if !reflect.DeepEqual(vars, want) {
diff --git a/pkg/cmd/pr/list/list_test.go b/pkg/cmd/pr/list/list_test.go
index 7de70d649..a2db7eb04 100644
--- a/pkg/cmd/pr/list/list_test.go
+++ b/pkg/cmd/pr/list/list_test.go
@@ -183,7 +183,7 @@ func TestPRList_filteringAssignee(t *testing.T) {
 	http.Register(
 		httpmock.GraphQL(`query PullRequestSearch\b`),
 		httpmock.GraphQLQuery(`{}`, func(_ string, params map[string]interface{}) {
-			assert.Equal(t, `assignee:hubot base:develop label:"needs tests" repo:OWNER/REPO state:merged type:pr`, params["q"].(string))
+			assert.Equal(t, `repo:OWNER/REPO is:pr is:merged assignee:hubot label:"needs tests" base:develop`, params["q"].(string))
 		}))
 
 	_, err := runCommand(http, true, `-s merged -l "needs tests" -a hubot -B develop`)
@@ -201,12 +201,12 @@ func TestPRList_filteringDraft(t *testing.T) {
 		{
 			name:          "draft",
 			cli:           "--draft",
-			expectedQuery: `draft:true repo:OWNER/REPO state:open type:pr`,
+			expectedQuery: `repo:OWNER/REPO is:pr is:open draft:true`,
 		},
 		{
 			name:          "non-draft",
 			cli:           "--draft=false",
-			expectedQuery: `draft:false repo:OWNER/REPO state:open type:pr`,
+			expectedQuery: `repo:OWNER/REPO is:pr is:open draft:false`,
 		},
 	}
 
@@ -238,22 +238,22 @@ func TestPRList_filteringAuthor(t *testing.T) {
 		{
 			name:          "author @me",
 			cli:           `--author "@me"`,
-			expectedQuery: `author:@me repo:OWNER/REPO state:open type:pr`,
+			expectedQuery: `repo:OWNER/REPO is:pr is:open author:@me`,
 		},
 		{
 			name:          "author user",
 			cli:           `--author "monalisa"`,
-			expectedQuery: `author:monalisa repo:OWNER/REPO state:open type:pr`,
+			expectedQuery: `repo:OWNER/REPO is:pr is:open author:monalisa`,
 		},
 		{
 			name:          "app author",
 			cli:           `--author "app/dependabot"`,
-			expectedQuery: `author:app/dependabot repo:OWNER/REPO state:open type:pr`,
+			expectedQuery: `repo:OWNER/REPO is:pr is:open author:app/dependabot`,
 		},
 		{
 			name:          "app author with app option",
 			cli:           `--app "dependabot"`,
-			expectedQuery: `author:app/dependabot repo:OWNER/REPO state:open type:pr`,
+			expectedQuery: `repo:OWNER/REPO is:pr is:open author:app/dependabot`,
 		},
 	}
 
@@ -292,17 +292,17 @@ func TestPRList_web(t *testing.T) {
 		{
 			name:               "filters",
 			cli:                "-a peter -l bug -l docs -L 10 -s merged -B trunk",
-			expectedBrowserURL: "https://github.com/OWNER/REPO/pulls?q=assignee%3Apeter+base%3Atrunk+label%3Abug+label%3Adocs+state%3Amerged+type%3Apr",
+			expectedBrowserURL: "https://github.com/OWNER/REPO/pulls?q=is%3Apr+is%3Amerged+assignee%3Apeter+label%3Abug+label%3Adocs+base%3Atrunk",
 		},
 		{
 			name:               "draft",
 			cli:                "--draft=true",
-			expectedBrowserURL: "https://github.com/OWNER/REPO/pulls?q=draft%3Atrue+state%3Aopen+type%3Apr",
+			expectedBrowserURL: "https://github.com/OWNER/REPO/pulls?q=is%3Apr+is%3Aopen+draft%3Atrue",
 		},
 		{
 			name:               "non-draft",
 			cli:                "--draft=0",
-			expectedBrowserURL: "https://github.com/OWNER/REPO/pulls?q=draft%3Afalse+state%3Aopen+type%3Apr",
+			expectedBrowserURL: "https://github.com/OWNER/REPO/pulls?q=is%3Apr+is%3Aopen+draft%3Afalse",
 		},
 	}
 
diff --git a/pkg/cmd/pr/shared/params_test.go b/pkg/cmd/pr/shared/params_test.go
index 5f5e674cc..d7077e0d1 100644
--- a/pkg/cmd/pr/shared/params_test.go
+++ b/pkg/cmd/pr/shared/params_test.go
@@ -12,9 +12,6 @@ import (
 )
 
 func Test_listURLWithQuery(t *testing.T) {
-	trueBool := true
-	falseBool := false
-
 	type args struct {
 		listURL string
 		options FilterOptions
@@ -35,7 +32,7 @@ func Test_listURLWithQuery(t *testing.T) {
 					State:  "open",
 				},
 			},
-			want:    "https://example.com/path?a=b&q=state%3Aopen+type%3Aissue",
+			want:    "https://example.com/path?a=b&q=is%3Aissue+is%3Aopen",
 			wantErr: false,
 		},
 		{
@@ -45,10 +42,10 @@ func Test_listURLWithQuery(t *testing.T) {
 				options: FilterOptions{
 					Entity: "pr",
 					State:  "open",
-					Draft:  &trueBool,
+					Draft:  "true",
 				},
 			},
-			want:    "https://example.com/path?q=draft%3Atrue+state%3Aopen+type%3Apr",
+			want:    "https://example.com/path?q=is%3Apr+is%3Aopen+draft%3Atrue",
 			wantErr: false,
 		},
 		{
@@ -58,10 +55,10 @@ func Test_listURLWithQuery(t *testing.T) {
 				options: FilterOptions{
 					Entity: "pr",
 					State:  "open",
-					Draft:  &falseBool,
+					Draft:  "false",
 				},
 			},
-			want:    "https://example.com/path?q=draft%3Afalse+state%3Aopen+type%3Apr",
+			want:    "https://example.com/path?q=is%3Apr+is%3Aopen+draft%3Afalse",
 			wantErr: false,
 		},
 		{
@@ -78,7 +75,7 @@ func Test_listURLWithQuery(t *testing.T) {
 					Mention:    "nu",
 				},
 			},
-			want:    "https://example.com/path?q=assignee%3Abo+author%3Aka+base%3Atrunk+head%3Abug-fix+mentions%3Anu+state%3Aopen+type%3Aissue",
+			want:    "https://example.com/path?q=is%3Aissue+is%3Aopen+assignee%3Abo+author%3Aka+mentions%3Anu+base%3Atrunk+head%3Abug-fix",
 			wantErr: false,
 		},
 		{
@@ -92,7 +89,7 @@ func Test_listURLWithQuery(t *testing.T) {
 					Milestone: `Codename "What Was Missing"`,
 				},
 			},
-			want:    "https://example.com/path?q=label%3A%22help+wanted%22+label%3Adocs+milestone%3A%22Codename+%5C%22What+Was+Missing%5C%22%22+state%3Aopen+type%3Apr",
+			want:    "https://example.com/path?q=is%3Apr+is%3Aopen+label%3Adocs+label%3A%22help+wanted%22+milestone%3A%22Codename+%5C%22What+Was+Missing%5C%22%22",
 			wantErr: false,
 		},
 	}
diff --git a/pkg/cmd/repo/list/http_test.go b/pkg/cmd/repo/list/http_test.go
index e7e0d52de..854a89e82 100644
--- a/pkg/cmd/repo/list/http_test.go
+++ b/pkg/cmd/repo/list/http_test.go
@@ -57,7 +57,7 @@ func Test_listReposWithLanguage(t *testing.T) {
 	assert.Equal(t, "octocat/hello-world", res.Repositories[0].NameWithOwner)
 
 	assert.Equal(t, float64(10), searchData.Variables["perPage"])
-	assert.Equal(t, `sort:updated-desc fork:true language:go org:@me`, searchData.Variables["query"])
+	assert.Equal(t, `user:@me language:go fork:true sort:updated-desc`, searchData.Variables["query"])
 }
 
 func Test_searchQuery(t *testing.T) {
@@ -72,14 +72,14 @@ func Test_searchQuery(t *testing.T) {
 	}{
 		{
 			name: "blank",
-			want: "sort:updated-desc fork:true org:@me",
+			want: "user:@me fork:true sort:updated-desc",
 		},
 		{
 			name: "in org",
 			args: args{
 				owner: "cli",
 			},
-			want: "sort:updated-desc fork:true org:cli",
+			want: "user:cli fork:true sort:updated-desc",
 		},
 		{
 			name: "only public",
@@ -89,7 +89,7 @@ func Test_searchQuery(t *testing.T) {
 					Visibility: "public",
 				},
 			},
-			want: "sort:updated-desc fork:true is:public org:@me",
+			want: "user:@me is:public fork:true sort:updated-desc",
 		},
 		{
 			name: "only private",
@@ -99,7 +99,7 @@ func Test_searchQuery(t *testing.T) {
 					Visibility: "private",
 				},
 			},
-			want: "sort:updated-desc fork:true is:private org:@me",
+			want: "user:@me is:private fork:true sort:updated-desc",
 		},
 		{
 			name: "only forks",
@@ -109,7 +109,7 @@ func Test_searchQuery(t *testing.T) {
 					Fork: true,
 				},
 			},
-			want: "sort:updated-desc fork:only org:@me",
+			want: "user:@me fork:only sort:updated-desc",
 		},
 		{
 			name: "no forks",
@@ -119,7 +119,7 @@ func Test_searchQuery(t *testing.T) {
 					Source: true,
 				},
 			},
-			want: "sort:updated-desc fork:false org:@me",
+			want: "user:@me fork:false sort:updated-desc",
 		},
 		{
 			name: "with language",
@@ -129,7 +129,7 @@ func Test_searchQuery(t *testing.T) {
 					Language: "ruby",
 				},
 			},
-			want: `sort:updated-desc fork:true language:ruby org:@me`,
+			want: `user:@me language:ruby fork:true sort:updated-desc`,
 		},
 		{
 			name: "only archived",
@@ -139,7 +139,7 @@ func Test_searchQuery(t *testing.T) {
 					Archived: true,
 				},
 			},
-			want: "sort:updated-desc archived:true fork:true org:@me",
+			want: "user:@me fork:true archived:true sort:updated-desc",
 		},
 		{
 			name: "only non-archived",
@@ -149,7 +149,7 @@ func Test_searchQuery(t *testing.T) {
 					NonArchived: true,
 				},
 			},
-			want: "sort:updated-desc archived:false fork:true org:@me",
+			want: "user:@me fork:true archived:false sort:updated-desc",
 		},
 	}
 	for _, tt := range tests {