current commit
250129.1 · chore: migrate to ESLint v9 and update deps (#902)
import fs from 'node:fs';

const TSC_TITLE = '#### TSC voting members';
const TSC_REGULAR_TITLE = '#### TSC regular members';
const TSCE_TITLE = '#### TSC emeriti members';
const CL_TITLE = '### Collaborators';
const CLE_TITLE = '### Collaborator emeriti';
const CONTACT_RE = /\* +\[(.+?)\]\(.+?\) +-\s+\*\*([^*]+?)\*\* +(?:&lt;|\\<|<<)([^>]+?)(?:&gt;|>)/mg;

const TSC = 'TSC';
const COLLABORATOR = 'COLLABORATOR';

export class Collaborator {
  constructor(login, name, email, type) {
    this.login = login;  // This is not lowercased
    this.name = name;
    this.email = email;
    this.type = type;
  }

  isActor(actor) {
    if (!actor || !actor.login) {  // ghost
      return false;
    }
    return actor.login.toLowerCase() === this.login.toLowerCase();
  }

  isTSC() {
    return this.type === TSC;
  }

  getName() {
    return `${this.name} (@${this.login})`;
  }

  getContact() {
    return `${this.name} <${this.email}>`;
  }
}

Collaborator.TYPES = {
  TSC, COLLABORATOR
};

export async function getCollaborators(cli, request, argv) {
  const { readme, owner, repo } = argv;
  let readmeText;
  if (readme) {
    cli.updateSpinner(`Reading collaborator contacts from ${readme}`);
    readmeText = fs.readFileSync(readme, 'utf8');
  } else {
    cli.updateSpinner(
      'Getting collaborator contacts from README of nodejs/node');
    const url = 'https://raw.githubusercontent.com/nodejs/node/HEAD/README.md';
    readmeText = await request.text(url);
  }

  let collaborators;
  try {
    collaborators = parseCollaborators(readmeText, cli);
  } catch (err) {
    const readmePath = readme || `${owner}/${repo}/README.md`;
    cli.stopSpinner(`Failed to get collaborator info from ${readmePath}`,
      cli.SPINNER_STATUS.FAILED);
    throw err;
  }
  return collaborators;
}

function parseCollaborators(readme, cli) {
  // This is more or less taken from
  // https://github.com/rvagg/archived-iojs-tools/blob/main/pr-metadata/pr-metadata.js
  const collaborators = new Map();
  let m;

  const tscIndex = readme.toUpperCase().indexOf(TSC_TITLE.toUpperCase());
  const tscrIndex = readme.toUpperCase().indexOf(TSC_REGULAR_TITLE.toUpperCase());
  const tsceIndex = readme.toUpperCase().indexOf(TSCE_TITLE.toUpperCase());
  const clIndex = readme.toUpperCase().indexOf(CL_TITLE.toUpperCase());
  const cleIndex = readme.toUpperCase().indexOf(CLE_TITLE.toUpperCase());

  if (tscIndex === -1) {
    throw new Error(`Couldn't find ${TSC_TITLE} in the README`);
  }
  if (tscrIndex === -1) {
    throw new Error(`Couldn't find ${TSC_REGULAR_TITLE} in the README`);
  }
  if (tsceIndex === -1) {
    throw new Error(`Couldn't find ${TSCE_TITLE} in the README`);
  }
  if (clIndex === -1) {
    throw new Error(`Couldn't find ${CL_TITLE} in the README`);
  }
  if (cleIndex === -1) {
    throw new Error(`Couldn't find ${CLE_TITLE} in the README`);
  }

  if (!(tscIndex < tscrIndex &&
        tscrIndex < tsceIndex &&
        tsceIndex < clIndex &&
        clIndex < cleIndex)) {
    cli.warn('Contacts in the README is out of order, ' +
             'analysis could go wrong.', { newline: true });
  }

  // We also assume that TSC & TSC Emeriti are also listed as collaborators
  CONTACT_RE.lastIndex = tscIndex;
  while ((m = CONTACT_RE.exec(readme)) && CONTACT_RE.lastIndex < tscrIndex) {
    const login = m[1].toLowerCase();
    const user = new Collaborator(m[1], m[2], m[3], TSC);
    collaborators.set(login, user);
  }

  CONTACT_RE.lastIndex = clIndex;
  while ((m = CONTACT_RE.exec(readme)) &&
    CONTACT_RE.lastIndex < cleIndex) {
    const login = m[1].toLowerCase();
    if (!collaborators.get(login)) {
      const user = new Collaborator(m[1], m[2], m[3], COLLABORATOR);
      collaborators.set(login, user);
    }
  }

  if (!collaborators.size) {
    throw new Error('Could not find any collaborators');
  }

  return collaborators;
}

/**
 * @param {Map<string, Collaborator>} collaborators
 * @param {{login?: string}} user
 */
export function isCollaborator(collaborators, user) {
  return (user && user.login &&  // could be a ghost
    collaborators.get(user.login.toLowerCase()));
}