diff --git a/api/queries_issue.go b/api/queries_issue.go index 0bca3bdcd..679964e87 100644 --- a/api/queries_issue.go +++ b/api/queries_issue.go @@ -454,37 +454,20 @@ func IssueByNumber(client *Client, repo ghrepo.Interface, number int) (*Issue, e } func IssueSearch(client *Client, repo ghrepo.Interface, searchQuery string, limit int) (*IssuesAndTotalCount, error) { - query := - `query IssueSearch($repoName: String!, $owner: String!, $type: SearchType!, $first: Int, $after: String, $searchQuery: String!) { - repository(name: $repoName, owner: $owner) { + query := fragments + + `query IssueSearch($repo: String!, $owner: String!, $type: SearchType!, $limit: Int, $after: String, $query: String!) { + repository(name: $repo, owner: $owner) { hasIssuesEnabled } - search(type: $type, first: $first, after: $after, query: $searchQuery) { + search(type: $type, last: $limit, after: $after, query: $query) { issueCount - edges { - node { - ... on Issue { - repository { - hasIssuesEnabled - } - number - title - updatedAt - state - labels(first: 100) { - nodes { - name - } - } - } + nodes { ...issue } + pageInfo { + hasNextPage + endCursor } } - pageInfo { - hasNextPage - endCursor - } - } - }` + }` type response struct { Repository struct { @@ -492,32 +475,23 @@ func IssueSearch(client *Client, repo ghrepo.Interface, searchQuery string, limi } Search struct { IssueCount int - Edges []struct { - Node struct { - Number int - Title string - State string - UpdatedAt time.Time - Labels Labels - } - } - PageInfo struct { + Nodes []Issue + PageInfo struct { HasNextPage bool EndCursor string } } } - searchQuery = fmt.Sprintf("is:issue repo:%s/%s %s", repo.RepoOwner(), repo.RepoName(), searchQuery) - perPage := min(limit, 100) + searchQuery = fmt.Sprintf("repo:%s/%s %s", repo.RepoOwner(), repo.RepoName(), searchQuery) variables := map[string]interface{}{ - "repoName": repo.RepoName(), - "owner": repo.RepoOwner(), - "type": "ISSUE", - "first": perPage, - "searchQuery": searchQuery, + "owner": repo.RepoOwner(), + "repo": repo.RepoName(), + "type": "ISSUE", + "limit": perPage, + "query": searchQuery, } ic := IssuesAndTotalCount{} @@ -536,25 +510,18 @@ loop: ic.TotalCount = resp.Search.IssueCount - for _, i := range resp.Search.Edges { - ic.Issues = append(ic.Issues, Issue{ - Number: i.Node.Number, - Title: i.Node.Title, - State: i.Node.State, - UpdatedAt: i.Node.UpdatedAt, - Labels: i.Node.Labels, - }) + for _, issue := range resp.Search.Nodes { + ic.Issues = append(ic.Issues, issue) if len(ic.Issues) == limit { break loop } } - if resp.Search.PageInfo.HasNextPage { - variables["after"] = resp.Search.PageInfo.EndCursor - variables["first"] = min(perPage, limit-len(resp.Search.Edges)) - } else { + if !resp.Search.PageInfo.HasNextPage { break } + variables["after"] = resp.Search.PageInfo.EndCursor + variables["perPage"] = min(perPage, limit-len(ic.Issues)) } return &ic, nil diff --git a/pkg/cmd/issue/list/fixtures/issueSearch.json b/pkg/cmd/issue/list/fixtures/issueSearch.json index 997dc0ce9..b764572ff 100644 --- a/pkg/cmd/issue/list/fixtures/issueSearch.json +++ b/pkg/cmd/issue/list/fixtures/issueSearch.json @@ -5,56 +5,50 @@ }, "search": { "issueCount": 3, - "edges": [ + "nodes": [ { - "node": { - "number": 1, - "title": "number won", - "url": "https://wow.com", - "updatedAt": "2011-01-26T19:01:12Z", - "labels": { - "nodes": [ - { - "name": "label" - } - ], - "totalCount": 1 - } + "number": 1, + "title": "number won", + "url": "https://wow.com", + "updatedAt": "2011-01-26T19:01:12Z", + "labels": { + "nodes": [ + { + "name": "label" + } + ], + "totalCount": 1 } }, { - "node": { - "number": 2, - "title": "number too", - "url": "https://wow.com", - "updatedAt": "2011-01-26T19:01:12Z", - "labels": { - "nodes": [ - { - "name": "label" - } - ], - "totalCount": 1 - } + "number": 2, + "title": "number too", + "url": "https://wow.com", + "updatedAt": "2011-01-26T19:01:12Z", + "labels": { + "nodes": [ + { + "name": "label" + } + ], + "totalCount": 1 } }, { - "node": { - "number": 4, - "title": "number fore", - "url": "https://wow.com", - "updatedAt": "2011-01-26T19:01:12Z", - "labels": { - "nodes": [ - { - "name": "label" - } - ], - "totalCount": 1 - } + "number": 4, + "title": "number fore", + "url": "https://wow.com", + "updatedAt": "2011-01-26T19:01:12Z", + "labels": { + "nodes": [ + { + "name": "label" + } + ], + "totalCount": 1 } } ] } } -} \ No newline at end of file +} diff --git a/pkg/cmd/issue/list/list.go b/pkg/cmd/issue/list/list.go index 7d26c1dea..8d393d676 100644 --- a/pkg/cmd/issue/list/list.go +++ b/pkg/cmd/issue/list/list.go @@ -3,6 +3,7 @@ package list import ( "fmt" "net/http" + "strconv" "github.com/MakeNowJust/heredoc" "github.com/cli/cli/api" @@ -46,11 +47,11 @@ func NewCmdList(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman Use: "list", Short: "List and filter issues in this repository", Example: heredoc.Doc(` - $ gh issue list -l "help wanted" + $ gh issue list -l "bug" -l "help wanted" $ gh issue list -A monalisa $ gh issue list -a @me $ gh issue list --web - $ gh issue list --milestone 'MVP' + $ gh issue list --milestone "The big 1.0" $ gh issue list --search "error no:assignee sort:created-asc" `), Args: cmdutil.NoArgsQuoteReminder, @@ -86,40 +87,25 @@ func listRun(opts *ListOptions) error { if err != nil { return err } - apiClient := api.NewClientFromHTTP(httpClient) baseRepo, err := opts.BaseRepo() if err != nil { return err } - isTerminal := opts.IO.IsStdoutTTY() - - meReplacer := shared.NewMeReplacer(apiClient, baseRepo.RepoHost()) - filterAssignee, err := meReplacer.Replace(opts.Assignee) - if err != nil { - return err - } - filterAuthor, err := meReplacer.Replace(opts.Author) - if err != nil { - return err - } - filterMention, err := meReplacer.Replace(opts.Mention) - if err != nil { - return err - } - filterOptions := prShared.FilterOptions{ Entity: "issue", State: opts.State, - Assignee: filterAssignee, + Assignee: opts.Assignee, Labels: opts.Labels, - Author: filterAuthor, - Mention: filterMention, + Author: opts.Author, + Mention: opts.Mention, Milestone: opts.Milestone, Search: opts.Search, } + isTerminal := opts.IO.IsStdoutTTY() + if opts.WebMode { issueListURL := ghrepo.GenerateRepoURL(baseRepo, "issues") openURL, err := prShared.ListURLWithQuery(issueListURL, filterOptions) @@ -133,20 +119,9 @@ func listRun(opts *ListOptions) error { return utils.OpenInBrowser(openURL) } - searchQuery := prShared.IssueSearchBuild(filterOptions) - - var listResult *api.IssuesAndTotalCount - - if opts.Search != "" { - listResult, err = api.IssueSearch(apiClient, baseRepo, searchQuery, opts.LimitResults) - if err != nil { - return err - } - } else { - listResult, err = api.IssueList(apiClient, baseRepo, opts.State, opts.Labels, filterAssignee, opts.LimitResults, filterAuthor, filterMention, opts.Milestone) - if err != nil { - return err - } + listResult, err := issueList(httpClient, baseRepo, filterOptions, opts.LimitResults) + if err != nil { + return err } err = opts.IO.StartPager() @@ -156,7 +131,7 @@ func listRun(opts *ListOptions) error { defer opts.IO.StopPager() if isTerminal { - hasFilters := opts.State != "open" || len(opts.Labels) > 0 || opts.Assignee != "" || opts.Author != "" || opts.Mention != "" || opts.Milestone != "" + hasFilters := opts.State != "open" || len(opts.Labels) > 0 || opts.Assignee != "" || opts.Author != "" || opts.Mention != "" || opts.Milestone != "" || opts.Search != "" title := prShared.ListHeader(ghrepo.FullName(baseRepo), "issue", len(listResult.Issues), listResult.TotalCount, hasFilters) fmt.Fprintf(opts.IO.Out, "\n%s\n\n", title) } @@ -165,3 +140,46 @@ func listRun(opts *ListOptions) error { return nil } + +func issueList(client *http.Client, repo ghrepo.Interface, filters prShared.FilterOptions, limit int) (*api.IssuesAndTotalCount, error) { + apiClient := api.NewClientFromHTTP(client) + + if filters.Search != "" { + if milestoneNumber, err := strconv.ParseInt(filters.Milestone, 10, 32); err == nil { + milestone, err := api.MilestoneByNumber(apiClient, repo, int32(milestoneNumber)) + if err != nil { + return nil, err + } + filters.Milestone = milestone.Title + } + + searchQuery := prShared.IssueSearchBuild(filters) + return api.IssueSearch(apiClient, repo, searchQuery, limit) + } + + meReplacer := shared.NewMeReplacer(apiClient, repo.RepoHost()) + filterAssignee, err := meReplacer.Replace(filters.Assignee) + if err != nil { + return nil, err + } + filterAuthor, err := meReplacer.Replace(filters.Author) + if err != nil { + return nil, err + } + filterMention, err := meReplacer.Replace(filters.Mention) + if err != nil { + return nil, err + } + + return api.IssueList( + apiClient, + repo, + filters.State, + filters.Labels, + filterAssignee, + limit, + filterAuthor, + filterMention, + filters.Milestone, + ) +} diff --git a/pkg/cmd/issue/list/list_test.go b/pkg/cmd/issue/list/list_test.go index 4ba4db800..feb84b4e4 100644 --- a/pkg/cmd/issue/list/list_test.go +++ b/pkg/cmd/issue/list/list_test.go @@ -2,7 +2,6 @@ package list import ( "bytes" - "encoding/json" "io/ioutil" "net/http" "regexp" @@ -13,6 +12,7 @@ import ( "github.com/cli/cli/internal/config" "github.com/cli/cli/internal/ghrepo" "github.com/cli/cli/internal/run" + prShared "github.com/cli/cli/pkg/cmd/pr/shared" "github.com/cli/cli/pkg/cmdutil" "github.com/cli/cli/pkg/httpmock" "github.com/cli/cli/pkg/iostreams" @@ -148,31 +148,6 @@ No issues match your search in OWNER/REPO `, output.String()) } -func TestIssueList_atMe(t *testing.T) { - http := &httpmock.Registry{} - defer http.Verify(t) - - http.Register( - httpmock.GraphQL(`query UserCurrent\b`), - httpmock.StringResponse(`{"data": {"viewer": {"login": "monalisa"} } }`)) - http.Register( - httpmock.GraphQL(`query IssueList\b`), - httpmock.GraphQLQuery(` - { "data": { "repository": { - "hasIssuesEnabled": true, - "issues": { "nodes": [] } - } } }`, func(_ string, params map[string]interface{}) { - assert.Equal(t, "monalisa", params["assignee"].(string)) - assert.Equal(t, "monalisa", params["author"].(string)) - assert.Equal(t, "monalisa", params["mention"].(string)) - })) - - _, err := runCommand(http, true, "-a @me -A @me --mention @me") - if err != nil { - t.Errorf("error running command `issue list`: %v", err) - } -} - func TestIssueList_withInvalidLimitFlag(t *testing.T) { http := &httpmock.Registry{} defer http.Verify(t) @@ -184,36 +159,6 @@ func TestIssueList_withInvalidLimitFlag(t *testing.T) { } } -func TestIssueList_nullAssigneeLabels(t *testing.T) { - http := &httpmock.Registry{} - defer http.Verify(t) - - http.Register( - httpmock.GraphQL(`query IssueList\b`), - httpmock.StringResponse(` - { "data": { "repository": { - "hasIssuesEnabled": true, - "issues": { "nodes": [] } - } } }`), - ) - - _, err := runCommand(http, true, "") - if err != nil { - t.Errorf("error running command `issue list`: %v", err) - } - - bodyBytes, _ := ioutil.ReadAll(http.Requests[0].Body) - reqBody := struct { - Variables map[string]interface{} - }{} - _ = json.Unmarshal(bodyBytes, &reqBody) - - _, assigneeDeclared := reqBody.Variables["assignee"] - _, labelsDeclared := reqBody.Variables["labels"] - assert.Equal(t, false, assigneeDeclared) - assert.Equal(t, false, labelsDeclared) -} - func TestIssueList_disabledIssues(t *testing.T) { http := &httpmock.Registry{} defer http.Verify(t) @@ -253,78 +198,6 @@ func TestIssueList_web(t *testing.T) { assert.Equal(t, "Opening github.com/OWNER/REPO/issues in your browser.\n", output.Stderr()) } -func TestIssueList_milestoneNotFound(t *testing.T) { - http := &httpmock.Registry{} - defer http.Verify(t) - - http.Register( - httpmock.GraphQL(`query RepositoryMilestoneList\b`), - httpmock.StringResponse(` - { "data": { "repository": { "milestones": { - "nodes": [{ "title":"1.x", "id": "MDk6TWlsZXN0b25lMTIzNDU=" }], - "pageInfo": { "hasNextPage": false } - } } } } - `)) - - _, err := runCommand(http, true, "--milestone NotFound") - if err == nil || err.Error() != `no milestone found with title "NotFound"` { - t.Errorf("error running command `issue list`: %v", err) - } -} - -func TestIssueList_milestoneByNumber(t *testing.T) { - http := &httpmock.Registry{} - defer http.Verify(t) - http.Register( - httpmock.GraphQL(`query RepositoryMilestoneByNumber\b`), - httpmock.StringResponse(` - { "data": { "repository": { "milestone": { - "id": "MDk6TWlsZXN0b25lMTIzNDU=" - } } } } - `)) - http.Register( - httpmock.GraphQL(`query IssueList\b`), - httpmock.GraphQLQuery(` - { "data": { "repository": { - "hasIssuesEnabled": true, - "issues": { "nodes": [] } - } } }`, func(_ string, params map[string]interface{}) { - assert.Equal(t, "12345", params["milestone"].(string)) // Database ID for the Milestone (see #1462) - })) - - _, err := runCommand(http, true, "--milestone 13") - if err != nil { - t.Fatalf("error running issue list: %v", err) - } -} - -func TestIssueList_Search_tty(t *testing.T) { - http := &httpmock.Registry{} - defer http.Verify(t) - - http.Register( - httpmock.GraphQL(`query IssueSearch\b`), - httpmock.FileResponse("./fixtures/issueSearch.json")) - - output, err := runCommand(http, true, "--search \"auth bug\"") - if err != nil { - t.Errorf("error running command `issue list`: %v", err) - } - - out := output.String() - timeRE := regexp.MustCompile(`\d+ years`) - out = timeRE.ReplaceAllString(out, "X years") - - assert.Equal(t, heredoc.Doc(` - - Showing 3 of 3 open issues in OWNER/REPO - - #1 number won (label) about X years ago - #2 number too (label) about X years ago - #4 number fore (label) about X years ago - `), out) -} - func TestIssueList_Search_web(t *testing.T) { http := &httpmock.Registry{} defer http.Verify(t) @@ -344,5 +217,280 @@ func TestIssueList_Search_web(t *testing.T) { assert.Equal(t, "", output.String()) assert.Equal(t, "Opening github.com/OWNER/REPO/issues in your browser.\n", output.Stderr()) - +} + +func Test_issueList(t *testing.T) { + type args struct { + repo ghrepo.Interface + filters prShared.FilterOptions + limit int + } + tests := []struct { + name string + args args + httpStubs func(*httpmock.Registry) + wantErr bool + }{ + { + name: "default", + args: args{ + limit: 30, + repo: ghrepo.New("OWNER", "REPO"), + filters: prShared.FilterOptions{ + Entity: "issue", + State: "open", + }, + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.GraphQL(`query IssueList\b`), + httpmock.GraphQLQuery(` + { "data": { "repository": { + "hasIssuesEnabled": true, + "issues": { "nodes": [] } + } } }`, func(_ string, params map[string]interface{}) { + assert.Equal(t, map[string]interface{}{ + "owner": "OWNER", + "repo": "REPO", + "limit": float64(30), + "states": []interface{}{"OPEN"}, + }, params) + })) + }, + }, + { + name: "milestone by number", + args: args{ + limit: 30, + repo: ghrepo.New("OWNER", "REPO"), + filters: prShared.FilterOptions{ + Entity: "issue", + State: "open", + Milestone: "13", + }, + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.GraphQL(`query RepositoryMilestoneByNumber\b`), + httpmock.StringResponse(` + { "data": { "repository": { "milestone": { + "id": "MDk6TWlsZXN0b25lMTIzNDU=" + } } } } + `)) + reg.Register( + httpmock.GraphQL(`query IssueList\b`), + httpmock.GraphQLQuery(` + { "data": { "repository": { + "hasIssuesEnabled": true, + "issues": { "nodes": [] } + } } }`, func(_ string, params map[string]interface{}) { + assert.Equal(t, map[string]interface{}{ + "owner": "OWNER", + "repo": "REPO", + "limit": float64(30), + "states": []interface{}{"OPEN"}, + "milestone": "12345", + }, params) + })) + }, + }, + { + name: "milestone by number with search", + args: args{ + limit: 30, + repo: ghrepo.New("OWNER", "REPO"), + filters: prShared.FilterOptions{ + Entity: "issue", + State: "open", + Milestone: "13", + Search: "auth bug", + }, + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.GraphQL(`query RepositoryMilestoneByNumber\b`), + httpmock.StringResponse(` + { "data": { "repository": { "milestone": { + "title": "Big 1.0", + "id": "MDk6TWlsZXN0b25lMTIzNDU=" + } } } } + `)) + reg.Register( + httpmock.GraphQL(`query IssueSearch\b`), + httpmock.GraphQLQuery(` + { "data": { + "repository": { "hasIssuesEnabled": true }, + "search": { + "issueCount": 0, + "nodes": [] + } + } }`, func(_ string, params map[string]interface{}) { + assert.Equal(t, map[string]interface{}{ + "owner": "OWNER", + "repo": "REPO", + "limit": float64(30), + "query": "repo:OWNER/REPO is:issue is:open milestone:\"Big 1.0\" auth bug", + "type": "ISSUE", + }, params) + })) + }, + }, + { + name: "milestone by title with search", + args: args{ + limit: 30, + repo: ghrepo.New("OWNER", "REPO"), + filters: prShared.FilterOptions{ + Entity: "issue", + State: "open", + Milestone: "Big 1.0", + Search: "auth bug", + }, + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.GraphQL(`query IssueSearch\b`), + httpmock.GraphQLQuery(` + { "data": { + "repository": { "hasIssuesEnabled": true }, + "search": { + "issueCount": 0, + "nodes": [] + } + } }`, func(_ string, params map[string]interface{}) { + assert.Equal(t, map[string]interface{}{ + "owner": "OWNER", + "repo": "REPO", + "limit": float64(30), + "query": "repo:OWNER/REPO is:issue is:open milestone:\"Big 1.0\" auth bug", + "type": "ISSUE", + }, params) + })) + }, + }, + { + name: "milestone by title", + args: args{ + limit: 30, + repo: ghrepo.New("OWNER", "REPO"), + filters: prShared.FilterOptions{ + Entity: "issue", + State: "open", + Milestone: "1.x", + }, + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.GraphQL(`query RepositoryMilestoneList\b`), + httpmock.StringResponse(` + { "data": { "repository": { "milestones": { + "nodes": [{ "title":"1.x", "id": "MDk6TWlsZXN0b25lMTIzNDU=" }], + "pageInfo": { "hasNextPage": false } + } } } } + `)) + reg.Register( + httpmock.GraphQL(`query IssueList\b`), + httpmock.GraphQLQuery(` + { "data": { "repository": { + "hasIssuesEnabled": true, + "issues": { "nodes": [] } + } } }`, func(_ string, params map[string]interface{}) { + assert.Equal(t, map[string]interface{}{ + "owner": "OWNER", + "repo": "REPO", + "limit": float64(30), + "states": []interface{}{"OPEN"}, + "milestone": "12345", + }, params) + })) + }, + }, + { + name: "@me syntax", + args: args{ + limit: 30, + repo: ghrepo.New("OWNER", "REPO"), + filters: prShared.FilterOptions{ + Entity: "issue", + State: "open", + Author: "@me", + Assignee: "@me", + Mention: "@me", + }, + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.GraphQL(`query UserCurrent\b`), + httpmock.StringResponse(`{"data": {"viewer": {"login": "monalisa"} } }`)) + reg.Register( + httpmock.GraphQL(`query IssueList\b`), + httpmock.GraphQLQuery(` + { "data": { "repository": { + "hasIssuesEnabled": true, + "issues": { "nodes": [] } + } } }`, func(_ string, params map[string]interface{}) { + assert.Equal(t, map[string]interface{}{ + "owner": "OWNER", + "repo": "REPO", + "limit": float64(30), + "states": []interface{}{"OPEN"}, + "assignee": "monalisa", + "author": "monalisa", + "mention": "monalisa", + }, params) + })) + }, + }, + { + name: "@me with search", + args: args{ + limit: 30, + repo: ghrepo.New("OWNER", "REPO"), + filters: prShared.FilterOptions{ + Entity: "issue", + State: "open", + Author: "@me", + Assignee: "@me", + Mention: "@me", + Search: "auth bug", + }, + }, + httpStubs: func(reg *httpmock.Registry) { + reg.Register( + httpmock.GraphQL(`query IssueSearch\b`), + httpmock.GraphQLQuery(` + { "data": { + "repository": { "hasIssuesEnabled": true }, + "search": { + "issueCount": 0, + "nodes": [] + } + } }`, func(_ string, params map[string]interface{}) { + assert.Equal(t, map[string]interface{}{ + "owner": "OWNER", + "repo": "REPO", + "limit": float64(30), + "query": "repo:OWNER/REPO is:issue is:open assignee:@me author:@me mentions:@me auth bug", + "type": "ISSUE", + }, params) + })) + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + httpreg := &httpmock.Registry{} + defer httpreg.Verify(t) + if tt.httpStubs != nil { + tt.httpStubs(httpreg) + } + client := &http.Client{Transport: httpreg} + _, err := issueList(client, tt.args.repo, tt.args.filters, tt.args.limit) + if tt.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } } diff --git a/pkg/cmd/pr/shared/params.go b/pkg/cmd/pr/shared/params.go index c123fc363..165e39f73 100644 --- a/pkg/cmd/pr/shared/params.go +++ b/pkg/cmd/pr/shared/params.go @@ -157,17 +157,17 @@ func ListURLWithQuery(listURL string, options FilterOptions) (string, error) { if err != nil { return "", err } - query := IssueSearchBuild(options) - q := u.Query() - q.Set("q", strings.TrimSuffix(query, " ")) - u.RawQuery = q.Encode() + params := u.Query() + params.Set("q", IssueSearchBuild(options)) + u.RawQuery = params.Encode() + return u.String(), nil } func IssueSearchBuild(options FilterOptions) string { - query := fmt.Sprintf("is:%s ", options.Entity) + if options.State != "all" { query += fmt.Sprintf("is:%s ", options.State) } @@ -181,7 +181,7 @@ func IssueSearchBuild(options FilterOptions) string { query += fmt.Sprintf("author:%s ", options.Author) } if options.BaseBranch != "" { - query += fmt.Sprintf("base:%s ", options.BaseBranch) + query += fmt.Sprintf("base:%s ", quoteValueForQuery(options.BaseBranch)) } if options.Mention != "" { query += fmt.Sprintf("mentions:%s ", options.Mention) @@ -193,7 +193,7 @@ func IssueSearchBuild(options FilterOptions) string { query += options.Search } - return query + return strings.TrimSpace(query) } func quoteValueForQuery(v string) string {