-
-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathindex.ts
316 lines (295 loc) · 7.4 KB
/
index.ts
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
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
/* eslint camelcase:0 */
// Import
import { StrictUnion } from 'simplytyped'
import fetch from 'cross-fetch'
import { getHeaders } from 'githubauthreq'
import Pool from 'native-promise-pool'
import { env } from 'process'
const { GITHUB_API = 'https://api.github.com' } = env
export function halt(milliseconds: number) {
if (milliseconds < 1000) {
console.warn(
'halt accepts milliseconds, you may have attempted to send it seconds, as you sent a value below 1000 milliseconds'
)
}
return new Promise(function (resolve, reject) {
setTimeout(resolve, milliseconds)
})
}
// =================================
// Types
/**
* GitHub's error response
* https://developer.github.com/v3/
*/
export interface Error {
message: string
documentation_url?: string
errors?: Array<{
resource: string
field: string
code: string
}>
}
/**
* GitHub's Repository Response will either be a repository, or if invalid, an error
*/
export type RepositoryResponse = StrictUnion<Error | Repository>
/**
* GitHub's Search Response will either be a search result, or if invalid, an error
*/
export type SearchResponse = StrictUnion<Error | Search>
/**
* GitHub's response to searching for repositories
* https://developer.github.com/v3/search/#search-repositories
*/
export interface Search {
total_count: number
incomplete_results: boolean
items: SearchRepository[]
}
/**
* Search results return a subset of the full repository results
*/
export interface SearchRepository {
id: number
node_id: string
name: string
full_name: string
owner: Owner
private: boolean
html_url: string
description: string
fork: boolean
url: string
created_at: Date
updated_at: Date
pushed_at: Date
homepage: string
size: number
stargazers_count: number
watchers_count: number
language: string
forks_count: number
open_issues_count: number
master_branch: string
default_branch: string
score: number
}
/**
* The owner of a repository
*/
export interface Owner {
login: string
id: number
node_id: string
avatar_url: string
gravatar_id: string
url: string
received_events_url: string
type: string
}
/**
* GitHub's response to getting a repository
* https://developer.github.com/v3/repos/#get
*/
export interface Repository extends SearchRepository {
archive_url: string
assignees_url: string
blobs_url: string
branches_url: string
collaborators_url: string
comments_url: string
commits_url: string
compare_url: string
contents_url: string
contributors_url: string
deployments_url: string
downloads_url: string
events_url: string
forks_url: string
git_commits_url: string
git_refs_url: string
git_tags_url: string
git_url: string
issue_comment_url: string
issue_events_url: string
issues_url: string
keys_url: string
labels_url: string
languages_url: string
merges_url: string
milestones_url: string
notifications_url: string
pulls_url: string
releases_url: string
ssh_url: string
stargazers_url: string
statuses_url: string
subscribers_url: string
subscription_url: string
tags_url: string
teams_url: string
trees_url: string
clone_url: string
mirror_url: string
hooks_url: string
svn_url: string
is_template: boolean
topics: string[]
has_issues: boolean
has_projects: boolean
has_wiki: boolean
has_pages: boolean
has_downloads: boolean
archived: boolean
disabled: boolean
visibility: string
permissions: Permissions
allow_rebase_merge: boolean
template_repository: null
allow_squash_merge: boolean
allow_merge_commit: boolean
subscribers_count: number
network_count: number
license?: License
organization?: Organization
parent?: Repository
source?: Repository
}
/**
* The license of a repository
*/
export interface License {
key: string
name: string
spdx_id: string
url: string
node_id: string
}
/**
* The organisation of a respository
*/
export interface Organization {
login: string
id: number
node_id: string
avatar_url: string
gravatar_id: string
url: string
html_url: string
followers_url: string
following_url: string
gists_url: string
starred_url: string
subscriptions_url: string
organizations_url: string
repos_url: string
events_url: string
received_events_url: string
type: string
site_admin: boolean
}
/**
* The permissions of a repository
*/
export interface Permissions {
admin: boolean
push: boolean
pull: boolean
}
/**
* Search Query Options
*/
export interface SearchOptions {
/** If you wish to skip the first page, then set this param, defaults to 1 */
page?: number
/** If you wish to change the amount of items returned per page, then set this param */
size?: number
/** If you wish to fetch unlimited pages, set this to zero, if you wish to fetch a specific amount of pages, then set this accordingly, defaults to `10` */
pages?: number
}
// =================================
// Fetch Repository
/**
* Fetch data for Repostiory from Repository Name
* @param repoFullName Repostory name, such as `'bevry/getrepos'`
*/
export async function getRepo(repoFullName: string): Promise<Repository> {
const url = `${GITHUB_API}/repos/${repoFullName}`
const resp = await fetch(url, {
headers: getHeaders(),
})
if (resp.status === 429) {
// wait a minute
console.warn(
`${url} returned 429, too many requests, trying again in a minute`
)
await halt(60 * 1000)
return getRepo(repoFullName)
}
const data = (await resp.json()) as RepositoryResponse
if (data && data.message) throw data.message
if (!data || !data.full_name) throw new Error('response was not a repository')
return data as Repository
}
/**
* Fetch data for Repositories from Repository Names
* @param repoFullNames Array of repository names, such as `['bevry/getcontributors', 'bevry/getrepos']`
*/
export async function getRepos(
repoFullNames: string[],
concurrency: number = 0
): Promise<Repository[]> {
const pool = new Pool(concurrency)
return await Promise.all(
repoFullNames.map((repoFullName) => pool.open(() => getRepo(repoFullName)))
)
}
// =================================
// Fetch from Search
/**
* Fetch data for Repostiories from a Search, will iterate all subsequent pages
* @param query The search query to send to GitHub, such as `@bevry/getcontributors @bevry/getrepos`
*/
export async function getReposFromSearch(
query: string,
opts: SearchOptions = {}
): Promise<SearchRepository[]> {
// defaults
if (opts.page == null) opts.page = 1
if (opts.pages == null) opts.pages = 10
if (opts.size == null) opts.size = 100
// fetch
const url = `${GITHUB_API}/search/repositories?page=${opts.page}&per_page=${
opts.size
}&q=${encodeURIComponent(query)}`
const resp = await fetch(url, {
headers: getHeaders(),
})
const data = (await resp.json()) as SearchResponse
if (data && data.message) throw data.message
if (!data || !data.items || !Array.isArray(data.items))
throw new Error('response was not the format we expected')
if (data.items.length === 0) return []
const within = opts.pages === 0 || opts.page < opts.pages
if (data.items.length === opts.size && within)
return data.items.concat(
await getReposFromSearch(
query,
Object.assign({}, opts, { page: opts.page + 1 })
)
)
return data.items
}
/**
* Fetch data for Repostiories from Users
* @param users Fetch repositories for these users, such as `['bevry', 'browserstate']`
*/
export async function getReposFromUsers(
users: string[],
opts: SearchOptions = {}
): Promise<SearchRepository[]> {
const query = users.map((name) => `@${name}`).join('%20')
return await getReposFromSearch(query, opts)
}