Skip to content

Commit 2dedf1a

Browse files
committed
Merge branch 'release/2.0.1'
2 parents f1b8fae + 5538622 commit 2dedf1a

File tree

3 files changed

+279
-278
lines changed

3 files changed

+279
-278
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "github-lang-getter",
3-
"version": "2.0.0",
3+
"version": "2.0.1",
44
"description": "Gets a Github user's programming language distribution across repositories and commits",
55
"main": "./dist/index.js",
66
"scripts": {

src/github-lang-getter.js

Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
require('dotenv').config();
2+
3+
const _ = require('lodash');
4+
const detect = require('language-detect');
5+
const different = require('different');
6+
const inspector = require('schema-inspector');
7+
const parse = require('parse-link-header');
8+
const request = require('request-promise-native');
9+
10+
const API_BASE_URL = 'https://api.github.com';
11+
12+
// Standard options for making requests to Github API
13+
const baseOpts = {
14+
headers: {
15+
'Accept': 'application/vnd.github.v3+json',
16+
'User-Agent': 'Request-Promise'
17+
},
18+
qs: {
19+
per_page: 100, // eslint-disable-line
20+
},
21+
json: true,
22+
resolveWithFullResponse: true
23+
};
24+
25+
/**
26+
* Gets a Github user's repository programming language distribution
27+
* @param {String} visibility Type of repositories to find (can be all, public, or private)
28+
* @param {Object} token Github personal access token
29+
* @return {Promise} Resolves if API request performed successfully
30+
* Rejects if parameters are invalid, or error occurs with API request
31+
*/
32+
module.exports.getRepoLanguages = async (visibility, token) => {
33+
// First get the User's repositories
34+
const repoResponses = await getUserRepos(visibility, token);
35+
36+
// Parse the repos json from the response bodies
37+
const repos = _.flatMap(repoResponses, 'body');
38+
39+
// Map Promises for each URL to resolve to the total language byte count
40+
const urls = _.map(repos, 'languages_url');
41+
const promises = _.map(urls, _.curry(createAPIRequestPromise)(token, null));
42+
43+
// Resolve the Promises and map the responses
44+
const langResponses = await Promise.all(promises);
45+
const results = _.map(langResponses, 'body');
46+
47+
// Count bytes per language
48+
const totals = {};
49+
_.each(results, (obj) => {
50+
_.each(obj, (val, key) => {
51+
if (!totals[key]) totals[key] = 0;
52+
totals[key] += obj[key];
53+
});
54+
});
55+
56+
return totals;
57+
};
58+
59+
/**
60+
* Gets a Github user's commit programming language distribution
61+
* @param {String} visibility Type of repositories to find (can be all, public, or private)
62+
* @param {Object} token Github personal access token
63+
* @return {Promise} Resolves if API request performed successfully
64+
* Rejects if parameters are invalid, or error occurs with API request
65+
*/
66+
module.exports.getCommitLanguages = async (visibility, token) => {
67+
// First get the user's repositories
68+
const repoResponses = await getUserRepos(visibility, token);
69+
70+
// Parse the repos json from the response bodies
71+
const repos = _.flatMap(repoResponses, 'body');
72+
73+
// Map repos to array of repo commits URLs
74+
const repoCommitUrls = _.map(repos, (repo) => {
75+
return repo.url + '/commits'
76+
});
77+
78+
// Get URLs of all individual commits
79+
const commitUrls = await getCommitsFromRepos(repoCommitUrls, token);
80+
81+
// Create Promises for individual commits
82+
const promises = _.map(commitUrls, _.curry(createAPIRequestPromise)(token, null));
83+
const commitResponses = await Promise.all(promises);
84+
85+
// Parse the commits json from the response bodies
86+
const commits = _.map(commitResponses, 'body');
87+
88+
// Map our result data and return it
89+
return mapCommitsToResult(commits);
90+
};
91+
92+
/**
93+
* Gets the list of individual commit URLs from a list of repository URLs
94+
* @param {String} repoUrls Github repository URLs to find commits for
95+
* @param {Object} token Github personal access token
96+
* @return {Array} List of individual commit URLs
97+
*/
98+
async function getCommitsFromRepos(repoUrls, token) {
99+
// Get Github username from access token
100+
const options = _.defaultsDeep({
101+
uri: API_BASE_URL + '/user',
102+
qs: {
103+
access_token: token, // eslint-disable-line
104+
},
105+
resolveWithFullResponse: false
106+
}, baseOpts);
107+
108+
const user = await request(options);
109+
110+
// Map a Promise for each repo commit URL
111+
let promises = _.map(repoUrls, _.curry(getRepoCommits)(user.login, token));
112+
promises = promises.map((p) => p.then((v) => v, (e) => ({ error: e })));
113+
114+
const responses = await Promise.all(promises);
115+
116+
// Get commits from Promise reponse bodies
117+
let commitsLists = [];
118+
responses.forEach((result) => {
119+
if (result.error) {
120+
// TODO: Promise may be rejected in certain cases, should we do anything here?
121+
} else {
122+
_.each(result, (value) => {
123+
commitsLists = commitsLists.concat(value.body);
124+
});
125+
}
126+
});
127+
128+
// Return array of individual commit urls
129+
return _.chain(commitsLists).filter((c) => {
130+
return c && c.author && c.author.login === user.login;
131+
}).map('url').value();
132+
}
133+
134+
/**
135+
* Maps Github commit objects to language usage data
136+
* @param {Array} commits Commits to get data for
137+
* @return {Object} Commits language usage data
138+
*/
139+
function mapCommitsToResult(commits) {
140+
const totals = {}; // To store our result data
141+
_.each(commits, (commit) => {
142+
const commitLangs = []; // Store all the languages present in commit
143+
_.each(commit.files, (file) => {
144+
const language = detect.filename(file.filename);
145+
if (language) {
146+
// Create empty object to hold total values
147+
if (!totals[language]) totals[language] = {
148+
bytes: 0,
149+
commits: 0
150+
};
151+
152+
// Add one to the language commit count if we haven't already
153+
if (!commitLangs.includes(language)) {
154+
commitLangs.push(language);
155+
totals[language].commits += 1;
156+
}
157+
158+
// Parse Git diff
159+
different.parseDiffFromString('diff\n' + file.patch, (diff) => {
160+
// Sum number of bytes from additions and add to results
161+
const byteCount = _.reduce(diff[0].additions, (sum, line) => {
162+
return line.length;
163+
}, 0);
164+
165+
totals[language].bytes += byteCount;
166+
});
167+
}
168+
});
169+
});
170+
171+
return totals;
172+
}
173+
174+
/**
175+
* Gets a list of Github user's repositories from the Github API
176+
* @param {String} visibility Type of repositories to find (can be all, public, or private)
177+
* @param {String} token Github personal access token
178+
* @return {Promise} Resolves if repo URLs are obtained
179+
* Rejects if an error occurs obtaining URLs
180+
*/
181+
async function getUserRepos(visibility, token) {
182+
// First validate the user token input
183+
const validation = {
184+
type: 'string'
185+
};
186+
const result = inspector.validate(validation, token);
187+
if (!result.valid) throw Error(result.format());
188+
189+
// Form options for API request
190+
const url = API_BASE_URL + '/user/repos';
191+
const options = _.defaultsDeep({
192+
uri: url,
193+
qs: {
194+
access_token: token, // eslint-disable-line
195+
visibility: visibility
196+
}
197+
}, baseOpts);
198+
199+
const response = await request(options);
200+
const link = parse(response.headers.link);
201+
const promises = []; // To store the promises to resolve the other pages of repos
202+
203+
if (link) { // Get the other pages of results if necessary
204+
const start = Number(link.next.page), end = Number(link.last.page);
205+
for (let page = start; page <= end; page++) {
206+
promises.push(_.curry(createAPIRequestPromise)(token, {
207+
page: page,
208+
visibility: visibility
209+
}, url));
210+
}
211+
}
212+
213+
// Return the first response plus the Promise that will resolve the rest
214+
return [response].concat(await Promise.all(promises));
215+
}
216+
217+
/**
218+
* Creates and returns a promise to resolve to all of the commits for a Github repo
219+
* @param {String} username Github username
220+
* @param {String} token Github personal access token
221+
* @param {String} repoUrl Github repository URL
222+
* @return {Promise} Promise to resolve repo commits
223+
*/
224+
async function getRepoCommits(username, token, repoUrl) {
225+
// Form options for API request
226+
const options = _.defaultsDeep({
227+
uri: repoUrl,
228+
qs: {
229+
access_token: token, // eslint-disable-line
230+
}
231+
}, baseOpts);
232+
if (username) options.qs.author = username;
233+
234+
const response = await request(options);
235+
const promises = []; // To store the promises to resolve the other pages of commits
236+
const link = parse(response.headers.link);
237+
238+
if (link) { // Get the other pages of results if necessary
239+
const start = Number(link.next.page), end = Number(link.last.page);
240+
for (let page = start; page <= end; page++) {
241+
promises.push(_.curry(createAPIRequestPromise)(token, {
242+
page: page,
243+
author: username
244+
}, repoUrl));
245+
}
246+
}
247+
248+
// Return the first response plus the Promise that will resolve the rest
249+
return [response].concat(await Promise.all(promises));
250+
}
251+
252+
/**
253+
* Creates and returns a promise to resolve to a specific Github API request result
254+
* @param {String} token Github personal access token
255+
* @param {Object} qs Extra query string parameters
256+
* @param {String} url Github API URL
257+
* @return {Promise} Promise to be resolved somewhere
258+
*/
259+
function createAPIRequestPromise(token, qs, url) {
260+
// Form options for API request
261+
const options = _.defaultsDeep({
262+
uri: url,
263+
qs: {
264+
access_token: token, // eslint-disable-line
265+
}
266+
}, baseOpts);
267+
268+
// Attach extra query string parameters to the request options
269+
if (qs) {
270+
for (let [key, val] of Object.entries(qs)) {
271+
options.qs[key] = val;
272+
}
273+
}
274+
275+
// Perform API request and handle result appropriately
276+
return request(options);
277+
}

0 commit comments

Comments
 (0)