-
Notifications
You must be signed in to change notification settings - Fork 1
/
github.go
174 lines (147 loc) · 5.55 KB
/
github.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
package gddoexp
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"time"
)
const (
// githubRateLimitResetHeader is the HTTP header label that stores the time
// at which the current rate limit window resets in UTC epoch seconds.
githubRateLimitResetHeader = "X-Ratelimit-Reset"
)
// httpClient contains all used methods to perform requests in Github API.
// This is useful for mocking and building tests.
type httpClient interface {
Get(string) (*http.Response, error)
}
// HTTPClient is going to be used to perform the Github HTTP requests. We use
// a global variable, as it is safe for concurrent use by multiple goroutines
var HTTPClient httpClient
// IsCacheResponse detects if a HTTP response was retrieved from cache or
// not.
var IsCacheResponse func(*http.Response) bool
func init() {
HTTPClient = new(http.Client)
}
// GithubAuth store the authentication information to allow a less
// restrictive rate limit in Github API. Authenticated requests can make up
// to 5000 requests per hour, otherwise you will be limited in 60 requests
// per hour (https://developer.github.com/v3/#rate-limiting).
type GithubAuth struct {
ID string
Secret string
}
// String build the Github authentication in the request query string format.
func (g GithubAuth) String() string {
return fmt.Sprintf("client_id=%s&client_secret=%s", g.ID, g.Secret)
}
// githubRepository stores the information of a repository. For more
// information check: https://developer.github.com/v3/repos/#get
type githubRepository struct {
CreatedAt time.Time `json:"created_at"`
Fork bool `json:"fork"`
ForksCount int `json:"forks_count"`
NetworkCount int `json:"network_count"`
StargazersCount int `json:"stargazers_count"`
UpdatedAt time.Time `json:"updated_at"`
}
// getGithubRepository will retrieve the path project information. For a
// better rate limit the requests must be authenticated, for more information
// check: https://developer.github.com/v3/search/#rate-limit. This function
// also returns if the response was retrieved from a local cache.
func getGithubRepository(path string, auth *GithubAuth) (repository githubRepository, cache bool, err error) {
normalizedPath, err := normalizePath(path)
if err != nil {
// as we didn't perform any request yet, we can return a cache hit to
// reuse the token
return repository, true, err
}
url := "https://api.github.com/repos/" + normalizedPath
if auth != nil {
url += "?" + auth.String()
}
cache, err = doGithub(path, url, &repository)
return repository, cache, err
}
// githubCommits stores the information of all commits from a repository. For more
// information check:
// https://developer.github.com/v3/repos/commits/#list-commits-on-a-repository.
// Note that there's a difference between the author and the committer:
// http://stackoverflow.com/questions/6755824/what-is-the-difference-between-author-and-committer-in-git
type githubCommits []struct {
Commit struct {
Author struct {
Date time.Time `json:"date"`
} `json:"author"`
} `json:"commit"`
}
// getCommits will retrieve the commits from a Github repository. For a
// better rate limit the requests must be authenticated, for more information
// check: https://developer.github.com/v3/search/#rate-limit. This function
// also returns if the response was retrieved from a local cache.
func getCommits(path string, auth *GithubAuth) (commits githubCommits, cache bool, err error) {
// we don't need to check the error here, because when we retrieved the
// repository we already checked for it
normalizedPath, _ := normalizePath(path)
url := fmt.Sprintf("https://api.github.com/repos/%s/commits", normalizedPath)
if auth != nil {
url += "?" + auth.String()
}
cache, err = doGithub(path, url, &commits)
return commits, cache, err
}
// normalizePath identify if the path is from Github and normalize it for the
// Github API request.
func normalizePath(path string) (string, error) {
if !strings.HasPrefix(path, "github.com/") {
return "", NewError(path, ErrorCodeNonGithub, nil)
}
normalizedPath := strings.TrimPrefix(path, "github.com/")
normalizedPath = strings.Join(strings.Split(normalizedPath, "/")[:2], "/")
return normalizedPath, nil
}
// doGithub is the low level function that do actually the work of querying
// Github API and parsing the response. It is also responsible for verifying if
// the response was a cache hit or not. If Github API answers with Forbidden
// status code, the function will sleep until the next available round to query
// again for the information.
func doGithub(path, url string, obj interface{}) (cache bool, err error) {
query:
rsp, err := HTTPClient.Get(url)
if err != nil {
return false, NewError(path, ErrorCodeGithubFetch, err)
}
defer func() {
if rsp.Body != nil {
// for now we aren't checking the error
rsp.Body.Close()
}
}()
switch rsp.StatusCode {
case http.StatusOK:
// valid response
case http.StatusForbidden:
epoch, err := strconv.ParseInt(rsp.Header.Get(githubRateLimitResetHeader), 10, 64)
if err != nil {
return false, NewError(path, ErrorCodeGithubForbidden, nil)
}
resetAt := time.Unix(epoch, 0)
time.Sleep(resetAt.Sub(time.Now()))
goto query
case http.StatusNotFound:
return false, NewError(path, ErrorCodeGithubNotFound, nil)
default:
return false, NewError(path, ErrorCodeGithubStatusCode, nil)
}
decoder := json.NewDecoder(rsp.Body)
if err := decoder.Decode(&obj); err != nil {
return false, NewError(path, ErrorCodeGithubParse, err)
}
if IsCacheResponse != nil {
cache = IsCacheResponse(rsp)
}
return cache, nil
}