diff --git a/tools/find-inactive-collaborators.mjs b/tools/find-inactive-collaborators.mjs new file mode 100755 index 00000000000000..578adf05fb78c7 --- /dev/null +++ b/tools/find-inactive-collaborators.mjs @@ -0,0 +1,95 @@ +#!/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] || '6 months ago'; + +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) + ); + const returnedSet = new Set(); + await Promise.race([errorHandler, Promise.resolve()]); + for await (const line of lines) { + await Promise.race([errorHandler, Promise.resolve()]); + const val = mapFn(line); + if (val) { + returnedSet.add(val); + } + } + return Promise.race([errorHandler, Promise.resolve(returnedSet)]); +} + +// Get all commit authors during the time period. +const authors = await runGitCommand( + `git shortlog -n -s --since="${SINCE}"`, + (line) => line.trim().split('\t', 2)[1] +); + +// Get all commit landers during the time period. +const landers = await runGitCommand( + `git shortlog -n -s -c --since="${SINCE}"`, + (line) => line.trim().split('\t', 2)[1] +); + +// Get all approving reviewers of landed commits during the time period. +const approvingReviewers = await runGitCommand( + `git log --since="${SINCE}" | egrep "^ Reviewed-By: "`, + (line) => /^ Reviewed-By: ([^<]+)/.exec(line)[1].trim() +); + +async function retrieveCollaboratorsFromReadme() { + const readmeText = readline.createInterface({ + input: fs.createReadStream(new URL('../README.md', import.meta.url)), + crlfDelay: Infinity, + }); + const returnedArray = []; + let processingCollaborators = false; + for await (const line of readmeText) { + const isCollaborator = processingCollaborators && line.length; + if (line === '### Collaborators') { + processingCollaborators = true; + } + if (line === '### Collaborator emeriti') { + processingCollaborators = false; + break; + } + if (line.startsWith('**') && isCollaborator) { + returnedArray.push(line.split('**', 2)[1].trim()); + } + } + return returnedArray; +} + +// Get list of current collaborators from README.md. +const collaborators = await retrieveCollaboratorsFromReadme(); + +console.log(`${authors.size.toLocaleString()} authors have made commits since ${SINCE}.`); +console.log(`${landers.size.toLocaleString()} landers have landed commits since ${SINCE}.`); +console.log(`${approvingReviewers.size.toLocaleString()} reviewers have approved landed commits since ${SINCE}.`); +console.log(`${collaborators.length.toLocaleString()} collaborators currently in the project.`); + +const inactive = collaborators.filter((collaborator) => + !authors.has(collaborator) && + !landers.has(collaborator) && + !approvingReviewers.has(collaborator) +); + +if (inactive.length) { + console.log('\nInactive collaborators:'); + console.log(inactive.join('\n')); +}