Merge pull request #11244 from cli/babakks/fix-query-mutation-during-pagination

Fix query object state mutation during pagination
This commit is contained in:
Andy Feller 2025-07-07 14:23:16 -04:00 committed by GitHub
commit 28c3424fde
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 229 additions and 3 deletions

View file

@ -149,15 +149,16 @@ func formatQualifiers(qs Qualifiers) []string {
}
func formatKeywords(ks []string) []string {
result := make([]string, len(ks))
for i, k := range ks {
before, after, found := strings.Cut(k, ":")
if !found {
ks[i] = quote(k)
result[i] = quote(k)
} else {
ks[i] = fmt.Sprintf("%s:%s", before, quote(after))
result[i] = fmt.Sprintf("%s:%s", before, quote(after))
}
}
return ks
return result
}
// CamelToKebab returns a copy of the string s that is converted from camel case form to '-' separated form.

View file

@ -122,6 +122,55 @@ func TestSearcherCode(t *testing.T) {
reg.Register(secondReq, secondRes)
},
},
{
name: "paginates results with quoted multi-word query (#11228)",
query: Query{
Keywords: []string{"keyword with whitespace"},
Kind: "code",
Limit: 30,
Qualifiers: Qualifiers{
Language: "go",
},
},
result: CodeResult{
IncompleteResults: false,
Items: []Code{{Name: "file.go"}, {Name: "file2.go"}},
Total: 2,
},
httpStubs: func(reg *httpmock.Registry) {
firstReq := httpmock.QueryMatcher("GET", "search/code", url.Values{
"page": []string{"1"},
"per_page": []string{"30"},
"q": []string{"\"keyword with whitespace\" language:go"},
})
firstRes := httpmock.JSONResponse(map[string]interface{}{
"incomplete_results": false,
"total_count": 2,
"items": []interface{}{
map[string]interface{}{
"name": "file.go",
},
},
})
firstRes = httpmock.WithHeader(firstRes, "Link", `<https://api.github.com/search/code?page=2&per_page=30&q=org%3Agithub>; rel="next"`)
secondReq := httpmock.QueryMatcher("GET", "search/code", url.Values{
"page": []string{"2"},
"per_page": []string{"30"},
"q": []string{"\"keyword with whitespace\" language:go"},
})
secondRes := httpmock.JSONResponse(map[string]interface{}{
"incomplete_results": false,
"total_count": 2,
"items": []interface{}{
map[string]interface{}{
"name": "file2.go",
},
},
})
reg.Register(firstReq, firstRes)
reg.Register(secondReq, secondRes)
},
},
{
name: "collect full and partial pages under total number of matching search results",
query: Query{
@ -305,6 +354,62 @@ func TestSearcherCommits(t *testing.T) {
)
},
},
{
name: "paginates results with quoted multi-word query (#11228)",
query: Query{
Keywords: []string{"keyword with whitespace"},
Kind: "commits",
Limit: 30,
Order: "desc",
Sort: "committer-date",
Qualifiers: Qualifiers{
Author: "foobar",
CommitterDate: ">2021-02-28",
},
},
result: CommitsResult{
IncompleteResults: false,
Items: []Commit{{Sha: "abc"}, {Sha: "def"}},
Total: 2,
},
httpStubs: func(reg *httpmock.Registry) {
firstReq := httpmock.QueryMatcher("GET", "search/commits", url.Values{
"page": []string{"1"},
"per_page": []string{"30"},
"order": []string{"desc"},
"sort": []string{"committer-date"},
"q": []string{"\"keyword with whitespace\" author:foobar committer-date:>2021-02-28"},
})
firstRes := httpmock.JSONResponse(map[string]interface{}{
"incomplete_results": false,
"total_count": 2,
"items": []interface{}{
map[string]interface{}{
"sha": "abc",
},
},
})
firstRes = httpmock.WithHeader(firstRes, "Link", `<https://api.github.com/search/commits?page=2&per_page=30&q=org%3Agithub>; rel="next"`)
secondReq := httpmock.QueryMatcher("GET", "search/commits", url.Values{
"page": []string{"2"},
"per_page": []string{"30"},
"order": []string{"desc"},
"sort": []string{"committer-date"},
"q": []string{"\"keyword with whitespace\" author:foobar committer-date:>2021-02-28"},
})
secondRes := httpmock.JSONResponse(map[string]interface{}{
"incomplete_results": false,
"total_count": 2,
"items": []interface{}{
map[string]interface{}{
"sha": "def",
},
},
})
reg.Register(firstReq, firstRes)
reg.Register(secondReq, secondRes)
},
},
{
name: "paginates results",
query: query,
@ -575,6 +680,62 @@ func TestSearcherRepositories(t *testing.T) {
reg.Register(secondReq, secondRes)
},
},
{
name: "paginates results with quoted multi-word query (#11228)",
query: Query{
Keywords: []string{"keyword with whitespace"},
Kind: "repositories",
Limit: 30,
Order: "desc",
Sort: "stars",
Qualifiers: Qualifiers{
Stars: ">=5",
Topic: []string{"topic"},
},
},
result: RepositoriesResult{
IncompleteResults: false,
Items: []Repository{{Name: "test"}, {Name: "cli"}},
Total: 2,
},
httpStubs: func(reg *httpmock.Registry) {
firstReq := httpmock.QueryMatcher("GET", "search/repositories", url.Values{
"page": []string{"1"},
"per_page": []string{"30"},
"order": []string{"desc"},
"sort": []string{"stars"},
"q": []string{"\"keyword with whitespace\" stars:>=5 topic:topic"},
})
firstRes := httpmock.JSONResponse(map[string]interface{}{
"incomplete_results": false,
"total_count": 2,
"items": []interface{}{
map[string]interface{}{
"name": "test",
},
},
})
firstRes = httpmock.WithHeader(firstRes, "Link", `<https://api.github.com/search/repositories?page=2&per_page=30&q=org%3Agithub>; rel="next"`)
secondReq := httpmock.QueryMatcher("GET", "search/repositories", url.Values{
"page": []string{"2"},
"per_page": []string{"30"},
"order": []string{"desc"},
"sort": []string{"stars"},
"q": []string{"\"keyword with whitespace\" stars:>=5 topic:topic"},
})
secondRes := httpmock.JSONResponse(map[string]interface{}{
"incomplete_results": false,
"total_count": 2,
"items": []interface{}{
map[string]interface{}{
"name": "cli",
},
},
})
reg.Register(firstReq, firstRes)
reg.Register(secondReq, secondRes)
},
},
{
name: "collect full and partial pages under total number of matching search results",
query: Query{
@ -805,6 +966,62 @@ func TestSearcherIssues(t *testing.T) {
reg.Register(secondReq, secondRes)
},
},
{
name: "paginates results with quoted multi-word query (#11228)",
query: Query{
Keywords: []string{"keyword with whitespace"},
Kind: "issues",
Limit: 30,
Order: "desc",
Sort: "comments",
Qualifiers: Qualifiers{
Language: "go",
Is: []string{"public", "locked"},
},
},
result: IssuesResult{
IncompleteResults: false,
Items: []Issue{{Number: 1234}, {Number: 5678}},
Total: 2,
},
httpStubs: func(reg *httpmock.Registry) {
firstReq := httpmock.QueryMatcher("GET", "search/issues", url.Values{
"page": []string{"1"},
"per_page": []string{"30"},
"order": []string{"desc"},
"sort": []string{"comments"},
"q": []string{"\"keyword with whitespace\" is:locked is:public language:go"},
})
firstRes := httpmock.JSONResponse(map[string]interface{}{
"incomplete_results": false,
"total_count": 2,
"items": []interface{}{
map[string]interface{}{
"number": 1234,
},
},
})
firstRes = httpmock.WithHeader(firstRes, "Link", `<https://api.github.com/search/issues?page=2&per_page=30&q=org%3Agithub>; rel="next"`)
secondReq := httpmock.QueryMatcher("GET", "search/issues", url.Values{
"page": []string{"2"},
"per_page": []string{"30"},
"order": []string{"desc"},
"sort": []string{"comments"},
"q": []string{"\"keyword with whitespace\" is:locked is:public language:go"},
})
secondRes := httpmock.JSONResponse(map[string]interface{}{
"incomplete_results": false,
"total_count": 2,
"items": []interface{}{
map[string]interface{}{
"number": 5678,
},
},
})
reg.Register(firstReq, firstRes)
reg.Register(secondReq, secondRes)
},
},
{
name: "collect full and partial pages under total number of matching search results",
query: Query{
@ -948,6 +1165,14 @@ func TestSearcherURL(t *testing.T) {
query: query,
url: "https://enterprise.com/search?order=desc&q=keyword+stars%3A%3E%3D5+topic%3Atopic&sort=stars&type=repositories",
},
{
name: "outputs encoded query url with quoted multi-word keywords",
query: Query{
Keywords: []string{"keyword with whitespace"},
Kind: "repositories",
},
url: "https://github.com/search?q=%22keyword+with+whitespace%22&type=repositories",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {