Skip to content

Commit c6fdad3

Browse files
brikis98mbal
andauthored
Add support for pagination (#85)
* fixes issues 26 & 46. Added support for pagination * Clarified what getNextPath returns Co-authored-by: Yevgeniy Brikman <[email protected]> * Added Regex and test cases for getNextPath Used Regex to parse Link headers instead of splits and index arthemtic. getNextPath now returns `string` and `*FetchError`. * Add .iml files to .gitignore * Implement minor cleanup * Refactor code and tests * Make comment clearer Co-authored-by: mbal <[email protected]>
1 parent 37e8f10 commit c6fdad3

File tree

3 files changed

+99
-25
lines changed

3 files changed

+99
-25
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
22
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
33
.idea
4+
*.iml
45

56
# Covers Visual Studio Code
67
.vscode

github.go

Lines changed: 64 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -107,31 +107,37 @@ func FetchTags(githubRepoUrl string, githubToken string, instance GitHubInstance
107107
return tagsString, wrapError(err)
108108
}
109109

110-
url := createGitHubRepoUrlForPath(repo, "tags")
111-
resp, err := callGitHubApi(repo, url, map[string]string{})
112-
if err != nil {
113-
return tagsString, err
114-
}
110+
// Set per_page to 100, which is the max, to reduce network calls
111+
tagsUrl := formatUrl(repo, createGitHubRepoUrlForPath(repo, "tags?per_page=100"))
112+
for tagsUrl != "" {
113+
resp, err := callGitHubApiRaw(tagsUrl, "GET", repo.Token, map[string]string{})
114+
if err != nil {
115+
return tagsString, err
116+
}
115117

116-
// Convert the response body to a byte array
117-
buf := new(bytes.Buffer)
118-
_, goErr := buf.ReadFrom(resp.Body)
119-
if goErr != nil {
120-
return tagsString, wrapError(goErr)
121-
}
122-
jsonResp := buf.Bytes()
118+
// Convert the response body to a byte array
119+
buf := new(bytes.Buffer)
120+
_, goErr := buf.ReadFrom(resp.Body)
121+
if goErr != nil {
122+
return tagsString, wrapError(goErr)
123+
}
124+
jsonResp := buf.Bytes()
123125

124-
// Extract the JSON into our array of gitHubTagsCommitApiResponse's
125-
var tags []GitHubTagsApiResponse
126-
if err := json.Unmarshal(jsonResp, &tags); err != nil {
127-
return tagsString, wrapError(err)
128-
}
126+
// Extract the JSON into our array of gitHubTagsCommitApiResponse's
127+
var tags []GitHubTagsApiResponse
128+
if err := json.Unmarshal(jsonResp, &tags); err != nil {
129+
return tagsString, wrapError(err)
130+
}
129131

130-
for _, tag := range tags {
131-
// Skip tags that are not semantically versioned so that they don't cause errors. (issue #75)
132-
if _, err := version.NewVersion(tag.Name); err == nil {
133-
tagsString = append(tagsString, tag.Name)
132+
for _, tag := range tags {
133+
// Skip tags that are not semantically versioned so that they don't cause errors. (issue #75)
134+
if _, err := version.NewVersion(tag.Name); err == nil {
135+
tagsString = append(tagsString, tag.Name)
136+
}
134137
}
138+
139+
// Get paginated tags (issue #26 and #46)
140+
tagsUrl = getNextUrl(resp.Header.Get("link"))
135141
}
136142

137143
return tagsString, nil
@@ -203,17 +209,50 @@ func createGitHubRepoUrlForPath(repo GitHubRepo, path string) string {
203209
return fmt.Sprintf("repos/%s/%s/%s", repo.Owner, repo.Name, path)
204210
}
205211

212+
var nextLinkRegex = regexp.MustCompile(`<(.+?)>;\s*rel="next"`)
213+
214+
// Get the next page URL from the given link header returned by the GitHub API. If there is no next page, return an
215+
// empty string. The link header is expected to be of the form:
216+
//
217+
// <url>; rel="next", <url>; rel="last"
218+
//
219+
func getNextUrl(links string) string {
220+
if len(links) == 0 {
221+
return ""
222+
}
223+
224+
for _, link := range strings.Split(links, ",") {
225+
urlMatches := nextLinkRegex.FindStringSubmatch(link)
226+
if len(urlMatches) == 2 {
227+
return strings.TrimSpace(urlMatches[1])
228+
}
229+
}
230+
231+
return ""
232+
}
233+
234+
// Format a URL for calling the GitHub API for the given repo and path
235+
func formatUrl(repo GitHubRepo, path string) string {
236+
return fmt.Sprintf("https://"+repo.ApiUrl+"/%s", path)
237+
}
238+
206239
// Call the GitHub API at the given path and return the HTTP response
207240
func callGitHubApi(repo GitHubRepo, path string, customHeaders map[string]string) (*http.Response, *FetchError) {
241+
return callGitHubApiRaw(formatUrl(repo, path), "GET", repo.Token, customHeaders)
242+
}
243+
244+
// Call the GitHub API at the given URL, using the given HTTP method, and passing the given token and headers, and
245+
// return the response
246+
func callGitHubApiRaw(url string, method string, token string, customHeaders map[string]string) (*http.Response, *FetchError) {
208247
httpClient := &http.Client{}
209248

210-
request, err := http.NewRequest("GET", fmt.Sprintf("https://"+repo.ApiUrl+"/%s", path), nil)
249+
request, err := http.NewRequest(method, url, nil)
211250
if err != nil {
212251
return nil, wrapError(err)
213252
}
214253

215-
if repo.Token != "" {
216-
request.Header.Set("Authorization", fmt.Sprintf("token %s", repo.Token))
254+
if token != "" {
255+
request.Header.Set("Authorization", fmt.Sprintf("token %s", token))
217256
}
218257

219258
for headerName, headerValue := range customHeaders {
@@ -236,7 +275,7 @@ func callGitHubApi(repo GitHubRepo, path string, customHeaders map[string]string
236275
respBody := buf.String()
237276

238277
// We leverage the HTTP Response Code as our ErrorCode here.
239-
return nil, newError(resp.StatusCode, fmt.Sprintf("Received HTTP Response %d while fetching releases for GitHub URL %s. Full HTTP response: %s", resp.StatusCode, repo.Url, respBody))
278+
return nil, newError(resp.StatusCode, fmt.Sprintf("Received HTTP Response %d while fetching releases for GitHub URL %s. Full HTTP response: %s", resp.StatusCode, url, respBody))
240279
}
241280

242281
return resp, nil

github_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package main
22

33
import (
4+
"github.com/stretchr/testify/require"
45
"io/ioutil"
56
"os"
67
"reflect"
@@ -57,6 +58,39 @@ func TestGetListOfReleasesFromGitHubRepo(t *testing.T) {
5758
}
5859
}
5960

61+
func TestGetNextPath(t *testing.T) {
62+
t.Parallel()
63+
64+
cases := []struct {
65+
name string
66+
links string
67+
expectedUrl string
68+
}{
69+
{"next-and-last-urls", `<https://api.github.com/repos/123456789/example-repo/tags?per_page=100&page=2>; rel="next", <https://api.github.com/repos/123456789/example-repo/tags?per_page=100&page=3>; rel="last"`, "https://api.github.com/repos/123456789/example-repo/tags?per_page=100&page=2"},
70+
{"next-and-last-urls-no-whitespace", `<https://api.github.com/repos/123456789/example-repo/tags?per_page=100&page=2>;rel="next",<https://api.github.com/repos/123456789/example-repo/tags?per_page=100&page=3>;rel="last"`, "https://api.github.com/repos/123456789/example-repo/tags?per_page=100&page=2"},
71+
{"next-and-last-urls-extra-whitespace", ` <https://api.github.com/repos/123456789/example-repo/tags?per_page=100&page=2>; rel="next", <https://api.github.com/repos/123456789/example-repo/tags?per_page=100&page=3> ; rel="last"`, "https://api.github.com/repos/123456789/example-repo/tags?per_page=100&page=2"},
72+
{"first-and-next-urls", `<https://api.github.com/repos/123456789/example-repo/tags?page=1>; rel="first", <https://api.github.com/repos/123456789/example-repo/tags?per_page=100&page=2>; rel="next"`, "https://api.github.com/repos/123456789/example-repo/tags?per_page=100&page=2"},
73+
{"next-only", `<https://api.github.com/repos/123456789/example-repo/tags?per_page=100&page=2>; rel="next"`, "https://api.github.com/repos/123456789/example-repo/tags?per_page=100&page=2"},
74+
{"first-and-last-urls", `<https://api.github.com/repos/123456789/example-repo/tags?page=1>; rel="first", <https://api.github.com/repos/123456789/example-repo/tags?per_page=100&page=2>; rel="last"`, ""},
75+
{"empty", ``, ""},
76+
{"garbage", `junk not related to links header at all`, ""},
77+
}
78+
79+
for _, tc := range cases {
80+
// The following is necessary to make sure tc's values don't
81+
// get updated due to concurrency within the scope of t.Run(..) below
82+
tc := tc
83+
84+
t.Run(tc.name, func(t *testing.T) {
85+
t.Parallel()
86+
87+
nextUrl := getNextUrl(tc.links)
88+
require.Equal(t, tc.expectedUrl, nextUrl)
89+
})
90+
}
91+
92+
}
93+
6094
func TestParseUrlIntoGithubInstance(t *testing.T) {
6195
t.Parallel()
6296

0 commit comments

Comments
 (0)