forked from 100Automations/true-github-contributors
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathtrueContributors-mixin.js
239 lines (219 loc) · 13.6 KB
/
trueContributors-mixin.js
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
const trueContributors = {
/**
* Method to fetch contributors list based on number of issue comments and commits across an organization
* @param {String} parameters [Parameters to be used in GitHub API request]
* @return {Array} [Array of GitHub users with data about how many commit and comment contributions they made to an organization]
*/
async listCommitCommentContributorsForOrg(parameters) {
return this._listForOrgHelper(this.listCommitCommentContributors, parameters);
},
/**
* Method to fetch commit contributors across an organization
* @param {String} parameters [Parameters to be used in GitHub API request]
* @return {Array} [Array of GitHub users with data about how many commits they made to an organization]
*/
async listCommitContributorsForOrg(parameters) {
return this._listForOrgHelper(this.listCommitContributors, parameters);
},
/**
* Method to fetch commit contributors across an organization
* @param {String} parameters [Parameters to be used in GitHub API request]
* @return {Array} [Array of GitHub users with data about how many commits they made to an organization]
*/
async listContributorsForOrg(parameters) {
return this._listForOrgHelper(this._listContributors, parameters);
},
/**
* Method to fetch contributors list based on number of issue comments across an organization
* @param {String} parameters [Parameters to be used in GitHub API request]
* @return {Array} [Array of GitHub users with data about how many issue comments they made to an organization]
*/
async listCommentContributorsForOrg(parameters) {
return this._listForOrgHelper(this.listCommentContributors, parameters);
},
/**
* Method to fetch contributors list based on number of issue comments and commits
* @param {String} parameters [Parameters to be used in GitHub API request]
* @return {Array} [Array of GitHub users with data about how many contributions they made]
*/
async listCommitCommentContributors(parameters) {
let desiredParams = this._createParamsFromObject(["owner", "repo", "since"], parameters);
// If since is a parameter, use listCommitContributors method. If not, use octokit's faster listContributors endpoint
let contributors = (desiredParams.since) ?
await this.listCommitContributors(desiredParams) :
await this._listContributors(desiredParams);
let commentContributors = await this.listCommentContributors(desiredParams);
return this._aggregateContributors(contributors.concat(commentContributors));
},
/**
* Method to fetch contributors list based on number of commits
* @param {String} parameters [Parameters to be used in GitHub API request]
* @return {Array} [Array of GitHub users with data about how many commits they made]
*/
async listCommitContributors(parameters) {
let desiredParams = this._createParamsFromObject(["owner", "repo", "sha", "path", "since", "until"], parameters);
let commits = [];
// Catch unintentional paginate errors to check if called on empty repo.
// For more, see https://github.com/octokit/plugin-paginate-rest.js/issues/158
try {
commits = await this.paginate(this.repos.listCommits, desiredParams);
} catch(err) {
// If error is regarding empty repo, use empty commits array. If not, propigate error
if(err.status != 409 || err.message != "Git Repository is empty.") {
throw err;
}
}
return this._aggregateContributions(commits, "author");
},
/**
* Method to fetch contributors for a repository list based on number of issue comments.
* @param {String} parameters [Parameters to be used in GitHub API request]
* @return {Array} [Array of GitHub users with data about how many comments they made]
*/
async listCommentContributors(parameters) {
let desiredParams = this._createParamsFromObject(["owner", "repo", "since"], parameters);
let issueComments = await this.paginate(this.issues.listCommentsForRepo, desiredParams);
return this._aggregateContributions(issueComments, "user");
},
/**
* Helper method used by organization contributor methods to call corresponding subsequent endpoint
* This helper was created to reduce the amount of reduncancy in the organization contributor fetching methods.
* @param {Function} endpoint [Subsequent endpoint to fetch contributors with]
* @param {String} parameters [Parameters to be used in GitHub API request]
*/
async _listForOrgHelper(endpoint, parameters){
if(
endpoint != this.listCommentContributors &&
endpoint != this._listContributors &&
endpoint != this.listCommitContributors &&
endpoint != this.listCommitCommentContributors
) throw new TypeError("Unexpected endpoint function provided.");
let desiredParams = this._createParamsFromObject(["org", "type"], parameters);
let repos = await this.paginate(this.repos.listForOrg, desiredParams);
let contributors = [];
for(let repo of repos) {
let repoContributors;
let params = { owner: repo.owner.login, repo: repo.name, ...parameters }
// The following if block is not pretty, but it is used to avoid issues with referenceing the "this" object.
// Ideally, I would just call "endpoint(parameters)", but that was causing issues with references to"this"
// in those functions.
if(endpoint == this.listCommentContributors) repoContributors = await this.listCommentContributors(params);
else if(endpoint == this._listContributors) repoContributors = await this._listContributors(params);
else if(endpoint == this.listCommitContributors) repoContributors = await this.listCommitContributors(params);
else if(endpoint == this.listCommitCommentContributors) repoContributors = await this.listCommitCommentContributors(params);
contributors = contributors.concat(repoContributors);
}
return this._aggregateContributors(contributors);
},
/**
* Helper method to fetch paginated list of GitHub contributors. Even though Octokit has an endpoint to
* fetch a paginated list of commit contributors (i.e octokit.paginate(octokit.repos.listContributors, ...)),
* this main reason for this helper method is to include functionality to check for an unintended Type Error
* that octokit.paginate(octokit.repos.listContributors, ...) throws when a given repo is empty. I wanted this mixin
* to not throw errors on empty repos, but rather return an empty array.
* For more information on this unintentional error, see this GitHub issue from Octokit's paginate repository;
* https://github.com/octokit/plugin-paginate-rest.js/issues/158
* @param {String} parameters [Parameters to be used in GitHub API request]
*/
async _listContributors(parameters) {
let contributors = [];
// Catch octokit.paginate errors to check if caused from calling paginate on empty repo.
try {
contributors = await this.paginate(this.repos.listContributors, parameters);
} catch(err) {
// Use octokit.repos.listContributors to check if error stems from calling paginate on empty repo.
let res = await this.repos.listContributors(parameters);
// Status 204 with message "204 No Content" means empty repo and we can ignore octokit.paginate error.
// Any other response will propigate the original error from octokit.paginate, as I am not sure why
// octokit.paginate would throw an error with any other octokit.repos.listContributors status/message.
if(res.status != 204 || res.headers.status != "204 No Content") throw err;
}
return contributors;
},
/**
* Helper method to aggregate GitHub contribution objects into an array of sorted contributors
* @param {Array} contributions [Array of GitHub contribution objects]
* @param {String} contributionIdentifier [Porperty name used in contribution object that represents contributor]
* @return {Array} [Array of GitHub users with data about how many contributions they made sorted by contribution count]
*/
_aggregateContributions(contributions, contributionIdentifier) {
if(!contributionIdentifier) throw new ReferenceError("Error: no contribution identifier was given to _aggregateContributions.");
// Convert contributions array to array of contributors
let contributorArray = contributions
.filter((contribution) => {
// Filter contributors with null values for contributionIdentifiers and throw if contributionIdentifier is not a property of the contribution
if(!contribution.hasOwnProperty(contributionIdentifier)) throw new ReferenceError(`Error: contribution ${JSON.stringify(contribution)} has no property ${contributionIdentifier}.`);
return contribution[contributionIdentifier];
})
.map((contribution) => ( {...contribution[contributionIdentifier], contributions: 1} )); // Create array of shallow user copies with added contributions property
return this._aggregateContributors(contributorArray);
},
/**
* Helper method to aggregate an array of GitHub contributor objects
* @param {Array} contributors [Array of GitHub contributor objects]
* @return {Array} [Array of GitHub users with data about how many contributions they made]
*/
_aggregateContributors(contributors) {
// Reduce contributors list to dictionary of unique contributors with total contributions
let contributorDictionary = contributors.reduce(this._reduceContributors, {});
// Convert dictionary to sorted array
return this._contributorDictToArr(contributorDictionary);
},
/**
* Helper method to pass to Array.reduce() that reduces a list of contributors
* @param {Object} contributorDict [Accumulator dictionary that will be used for reduce function]
* @param {Object} contributor [Current contributor in the array]
* @return {Object} [Dictionary of contributors mapping users to user metadata containing contribution count]
*/
_reduceContributors(contributorDict, contributor) {
if(!contributor.hasOwnProperty("contributions")) throw new ReferenceError(`Error: contributor ${JSON.stringify(contributor)} has no property: contributions.`);
if(!contributor.hasOwnProperty("id")) throw new ReferenceError(`Error: contributor ${JSON.stringify(contributor)} has no property: id.`);
// If user id for this contributor exists in dictionary, add a contribution to that user
if(contributor.id in contributorDict) {
contributorDict[contributor.id].contributions += contributor.contributions;
// If user id for this comment does not exist, add user to dictionary
} else {
contributorDict[contributor.id] = { ...contributor }
}
return contributorDict;
},
/**
* Helper method to fetch desired parameters from a given parameters object
* @param {Array} desiredParameters [Array of parameter string names]
* @param {String} givenObject [Object containing parameter names and their values]
* @return {Object} [Object with desired parameter names and their values]
*/
_createParamsFromObject(desiredParameters, givenObject) {
let parameters = {};
for(let parameter of desiredParameters) {
let parameterValue = givenObject[parameter];
if(parameterValue) parameters[parameter] = parameterValue;
}
return parameters;
},
/**
* Helper method to convert a dictionary of contributors to a sorted array of contributors
* @param {Object} dictionary [JSON contributor dictionary to be converted to array]
* @return {Array} [Array of users sorted by their contributions]
*/
_contributorDictToArr(dictionary) {
if(!dictionary) throw new ReferenceError("Error: user dictionary is not defined.");
let array = [];
for(let item in dictionary) {
array.push(dictionary[item]);
}
return array.sort(this._sortByContributions);
},
/**
* Helper sorting function method to pass to sorting function based on a property called "contributions"
* @param {Object} a [Contributor object]
* @param {Object} b [Contributor object]
* @return {Integer} [An integer used to determine order between contribution comparison]
*/
_sortByContributions(a, b) {
if(a["contributions"] == undefined || b["contributions"] == undefined) throw new ReferenceError("Error: tried to sort by contributions while object has no contribution property.");
if(typeof a["contributions"] != "number" || typeof b["contributions"] != "number") throw new TypeError("Error: tried to sort by contributions while object contribution property is not a number.");
return b["contributions"] - a["contributions"];
}
}
module.exports = trueContributors;