forked from nodejs/node
-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathfind-inactive-collaborators.mjs
More file actions
executable file
·121 lines (105 loc) · 4 KB
/
find-inactive-collaborators.mjs
File metadata and controls
executable file
·121 lines (105 loc) · 4 KB
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
#!/usr/bin/env node
// Identify inactive collaborators. "Inactive" is not quite right, as the things
// this checks for are not the entirety of collaborator activities. Still, it is
// a pretty good proxy. Feel free to suggest or implement further metrics.
import cp from 'node:child_process';
import fs from 'node:fs';
import readline from 'node:readline';
const SINCE = +process.argv[2] || 5000;
async function runGitCommand(cmd, mapFn) {
const childProcess = cp.spawn('/bin/sh', ['-c', cmd], {
cwd: new URL('..', import.meta.url),
encoding: 'utf8',
stdio: ['inherit', 'pipe', 'inherit'],
});
const lines = readline.createInterface({
input: childProcess.stdout,
});
const errorHandler = new Promise(
(_, reject) => childProcess.on('error', reject)
);
let returnValue = mapFn ? new Set() : '';
await Promise.race([errorHandler, Promise.resolve()]);
// If no mapFn, return the value. If there is a mapFn, use it to make a Set to
// return.
for await (const line of lines) {
await Promise.race([errorHandler, Promise.resolve()]);
if (mapFn) {
const val = mapFn(line);
if (val) {
returnValue.add(val);
}
} else {
returnValue += line;
}
}
return Promise.race([errorHandler, Promise.resolve(returnValue)]);
}
// Get all commit authors during the time period.
const authors = await runGitCommand(
`git shortlog -n -s --email --max-count="${SINCE}" HEAD`,
(line) => line.trim().split('\t', 2)[1]
);
// Get all commit landers during the time period.
const landers = await runGitCommand(
`git shortlog -n -s -c --email --max-count="${SINCE}" HEAD`,
(line) => line.trim().split('\t', 2)[1]
);
// Get all approving reviewers of landed commits during the time period.
const approvingReviewers = await runGitCommand(
`git log --max-count="${SINCE}" | egrep "^ Reviewed-By: "`,
(line) => /^ Reviewed-By: ([^<]+)/.exec(line)[1].trim()
);
async function getCollaboratorsFromReadme() {
const readmeText = readline.createInterface({
input: fs.createReadStream(new URL('../README.md', import.meta.url)),
crlfDelay: Infinity,
});
const returnedArray = [];
let foundCollaboratorHeading = false;
for await (const line of readmeText) {
// If we've found the collaborator heading already, stop processing at the
// next heading.
if (foundCollaboratorHeading && line.startsWith('#')) {
break;
}
const isCollaborator = foundCollaboratorHeading && line.length;
if (line === '### Collaborators') {
foundCollaboratorHeading = true;
}
if (line.startsWith('**') && isCollaborator) {
const [, name, email] = /^\*\*([^*]+)\*\* <(.+)>/.exec(line);
const mailmap = await runGitCommand(
`git check-mailmap '${name} <${email}>'`
);
if (mailmap !== `${name} <${email}>`) {
console.log(`README entry for Collaborator does not match mailmap:\n ${name} <${email}> => ${mailmap}`);
}
returnedArray.push({
name,
email,
mailmap,
});
}
}
if (!foundCollaboratorHeading) {
throw new Error('Could not find Collaborator section of README');
}
return returnedArray;
}
// Get list of current collaborators from README.md.
const collaborators = await getCollaboratorsFromReadme();
console.log(`In the last ${SINCE} commits:\n`);
console.log(`* ${authors.size.toLocaleString()} authors have made commits.`);
console.log(`* ${landers.size.toLocaleString()} landers have landed commits.`);
console.log(`* ${approvingReviewers.size.toLocaleString()} reviewers have approved landed commits.`);
console.log(`* ${collaborators.length.toLocaleString()} collaborators currently in the project.`);
const inactive = collaborators.filter((collaborator) =>
!authors.has(collaborator.mailmap) &&
!landers.has(collaborator.mailmap) &&
!approvingReviewers.has(collaborator.name)
).map((collaborator) => collaborator.name);
if (inactive.length) {
console.log('\nInactive collaborators:\n');
console.log(inactive.map((name) => `* ${name}`).join('\n'));
}