Skip to content

Commit 191f73c

Browse files
committed
feat: create contributors leaderboard for all the repos of an owner
1 parent ba35c94 commit 191f73c

File tree

1 file changed

+221
-0
lines changed

1 file changed

+221
-0
lines changed

contributors.js

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
/**
2+
* @file Functions to analyze and archive meaningful github contributors data
3+
* @example To archive contributors leaderboard data in csv file, run `node contributors.js`
4+
*/
5+
6+
const https = require('https');
7+
8+
//INPUTS
9+
const REPO_OWNER = "Git-Commit-Show";//Change this to the repo that you
10+
const GITHUB_PERSONAL_TOKEN = "";//When used, it will increase the API limits from 60 to 5000/hr
11+
//End of inputs
12+
13+
const GITHUB_REQUEST_OPTIONS = {
14+
headers: {
15+
"User-Agent": "gh-contributors",
16+
"Content-Type": "application/json"
17+
}
18+
}
19+
20+
if(GITHUB_PERSONAL_TOKEN){
21+
GITHUB_REQUEST_OPTIONS.headers["Authorization"] = "token "+GITHUB_PERSONAL_TOKEN;
22+
}
23+
24+
/**
25+
* Get all github repos of an owner(user/org)
26+
* @param {String} owner
27+
* @param {Number} pageNo
28+
* @returns Promise<Array<Object> | String> JSON array of data on success, error on failure
29+
* @example getAllRepos('myorghandle').then((repos) => console.log(repos)).catch((err) => console.log(err))
30+
*/
31+
async function getAllRepos(owner, pageNo = 1) {
32+
return new Promise((resolve, reject) => {
33+
let url = `https://api.github.com/orgs/${owner}/repos?per_page=100&page=${pageNo}`;
34+
console.log(url);
35+
https.get(url, GITHUB_REQUEST_OPTIONS, (res) => {
36+
console.log('statusCode:', res.statusCode);
37+
// console.log('headers:', res.headers);
38+
let data = '';
39+
res.on('data', (d) => {
40+
data += d;
41+
})
42+
res.on('end', async () => {
43+
console.log("Repo list request finished")
44+
// console.log(data)
45+
let dataJsonArray = JSON.parse(data);
46+
if (dataJsonArray.length == 100) {
47+
//It might have more data on the next page
48+
pageNo++;
49+
try {
50+
let dataFromNextPage = await getAllRepos(owner, pageNo);
51+
dataJsonArray.push(...dataFromNextPage);
52+
} catch (err) {
53+
console.log("No more pagination needed")
54+
}
55+
}
56+
resolve(dataJsonArray);
57+
})
58+
}).on('error', (e) => {
59+
console.error(e);
60+
reject(e)
61+
});
62+
})
63+
}
64+
65+
/**
66+
* Get contributors for a Github repo
67+
* @param {*} fullRepoName e.g. myorghandle/myreponame
68+
* @param {*} pageNo
69+
* @returns Promise<Array<Object> | String>
70+
* @example getRepoContributors('myorghandle/myreponame').then((contributors) => console.log(contributors)).catch((err) => console.log(err))
71+
*/
72+
async function getRepoContributors(fullRepoName, pageNo = 1) {
73+
return new Promise((resolve, reject) => {
74+
let url = `https://api.github.com/repos/${fullRepoName}/contributors?per_page=100&page=${pageNo}`;
75+
console.log(url);
76+
https.get(url, GITHUB_REQUEST_OPTIONS, (res) => {
77+
console.log('statusCode:', res.statusCode);
78+
// console.log('headers:', res.headers);
79+
let data = '';
80+
res.on('data', (d) => {
81+
data += d;
82+
})
83+
res.on('end', async () => {
84+
console.log("Contributors request finished for " + fullRepoName)
85+
// console.log(data)
86+
let dataJsonArray = JSON.parse(data);
87+
if (dataJsonArray.length == 100) {
88+
//It might have more data on the next page
89+
pageNo++;
90+
try {
91+
let dataFromNextPage = await getRepoContributors(fullRepoName, pageNo);
92+
dataJsonArray.push(...dataFromNextPage);
93+
} catch (err) {
94+
console.log("No more pagination needed")
95+
}
96+
}
97+
resolve(dataJsonArray);
98+
})
99+
}).on('error', (e) => {
100+
console.error(e);
101+
reject(e)
102+
});
103+
})
104+
}
105+
106+
/**
107+
* Get all contributors across all the repos of an owner
108+
* @param {*} owner github user or org handle
109+
*/
110+
async function getAllContributors(owner) {
111+
let repos = await getAllRepos(owner);
112+
if (!repos || repos.length < 1) {
113+
console.log("Error in getting repos for " + owner)
114+
throw ("Error in getting repos for " + owner)
115+
}
116+
console.log(repos.length + " " + owner + " repos found")
117+
// console.log(repos)
118+
let allContributors = [];
119+
for (let i = 0; i < repos.length - 1; i++) {
120+
if(repos[i].fork || repos[i].private) {
121+
// Exclude forks repos and private repos from the analysis
122+
console.log("Excluding "+repos[i].full_name);
123+
continue;
124+
}
125+
let c = await getRepoContributors(repos[i].full_name);
126+
// Add repo info in the contributor object
127+
// so later we can use this info to discover repos that a contributor has contributed to
128+
c.forEach((item) => item.repo = repos[i].full_name);
129+
console.log(c.length + " contributors found for " + repos[i].full_name);
130+
if (c) allContributors.push(...c);
131+
}
132+
console.log("allContributors count without aggregation " + allContributors.length);
133+
// Remove duplicates in contributors list and sum total contributions for each contributor
134+
let finalListOfContributors = aggregateAllContributors(allContributors).sort(function (contributor1, contributor2) {
135+
// Sort the array in descending order of contributions
136+
return contributor2.contributions - contributor1.contributions
137+
})
138+
// Sort the repos field in order of descending contributions count
139+
finalListOfContributors.forEach((contributor) => {
140+
contributor.repos = sortReposByContributionsCount(contributor.repos);
141+
contributor.topContributedRepo = contributor.repos[0].repo_full_name;
142+
contributor.allContributedRepos = contributor.repos.map((repoContributionMap) => repoContributionMap.repo_full_name).join(" | ")
143+
})
144+
console.log("finalListOfContributors count with aggregation" + finalListOfContributors.length);
145+
return finalListOfContributors;
146+
}
147+
148+
/**
149+
* Adds up all the contributions by a contributor to different repos
150+
* @param {*} contributors
151+
*/
152+
function aggregateAllContributors(contributors) {
153+
return contributors.reduce(function (grouped, currentItem) {
154+
// Skipping the bots and other non individual user
155+
if (currentItem.type !== "User") {
156+
return grouped;
157+
}
158+
let found = false;
159+
grouped.forEach(function (contributor) {
160+
if (contributor.login == currentItem.login) {
161+
found = true;
162+
contributor.repos.push({ repo_full_name: currentItem.repo, contributions: currentItem.contributions });
163+
contributor.contributions += currentItem.contributions;
164+
console.log("Aggregated contributions of " + contributor.login + " - " + contributor.contributions);
165+
}
166+
})
167+
if (!found) {
168+
currentItem.repos = [{ repo_full_name: currentItem.repo, contributions: currentItem.contributions }];
169+
grouped.push(currentItem);
170+
}
171+
return grouped;
172+
}, [])
173+
}
174+
175+
/**
176+
* Lists all the repos a contributor has contributed to sorted by # of contributions
177+
* @param {Array<Object>} repoContributionMappingArray e.g. [{ repo_full_name, contributions }]
178+
* @returns {String} e.g. orghandle/repo1,orghandle/repo2
179+
*/
180+
function sortReposByContributionsCount(repoContributionMappingArray){
181+
return repoContributionMappingArray.sort((repoContributionMapping1, repoContributionMapping2) => {
182+
return repoContributionMapping2.contributions - repoContributionMapping1.contributions
183+
})
184+
}
185+
186+
function writeContributorLeaderboardToFile(contributors) {
187+
const fs = require('fs');
188+
let ghContributorLeaderboard = contributors.map((contributor) => {
189+
return ["@" + contributor.login, contributor.contributions, contributor.html_url, contributor.avatar_url, contributor.topContributedRepo, contributor.allContributedRepos].join();
190+
}).join("\n");
191+
ghContributorLeaderboard = "Github Username,Total Contributions,Profile,Avatar,Most Contribution To,Contributed To\n" + ghContributorLeaderboard;
192+
fs.writeFile("./gh-contributors-leaderboard.csv", ghContributorLeaderboard, { flag: 'a+' }, function (err) {
193+
if (err) {
194+
return console.log(err);
195+
}
196+
console.log("The file was saved!");
197+
});
198+
}
199+
200+
/**
201+
* Archives contributors leaderboard data sorted by contrbutions in a file
202+
* @param {*} owner
203+
*/
204+
async function archiveContributorsLeaderboard(owner) {
205+
let contributors = await getAllContributors();
206+
if (!contributors || contributors.length < 1) {
207+
console.log("Failed to get contributors for "+owner);
208+
return;
209+
}
210+
211+
// Summary - handles of contributors sorted by their contributions
212+
let ghHandles = contributors.map((contributor) => "@" + contributor.login)
213+
console.log(ghHandles.join(", "))
214+
215+
// Write the complete leaderboard data to a file
216+
writeContributorLeaderboardToFile(contributors);
217+
218+
return ghHandles;
219+
}
220+
221+
archiveContributorsLeaderboard(REPO_OWNER)

0 commit comments

Comments
 (0)