UNPKG

@nmann/codeowners

Version:

A tool for working with CODEOWNERS files

159 lines (154 loc) 4.32 kB
// src/codeowners.ts import fs from "node:fs"; import path from "node:path"; import { findUpSync } from "find-up"; import ignore from "ignore"; import tcp from "true-case-path"; // src/contact-info.ts var ContactInfo = class { fields = []; owners = []; /** * Consume one line of contact info * @param line Contact info line, with leading '##' intact */ addLine(line) { const dataItems = line.replace(/^##\s*/, "").split(" "); if (!this.fields.length) { this.fields = dataItems; return; } const ownerInfo = {}; for (const field of this.fields) { const value = dataItems.shift(); if (value) { ownerInfo[field] = value; } } this.owners.push(ownerInfo); } }; // src/utils.ts import { statSync } from "node:fs"; function isDirectorySync(filepath) { if (typeof filepath !== "string") { throw new Error("expected filepath to be a string"); } const stat = statSync(filepath, { throwIfNoEntry: false }); if (!stat) return false; return stat.isDirectory(); } function times(count, cb, indexAt = 0) { const ret = []; for (let i = indexAt; i < count + indexAt; i++) { ret.push(cb(i)); } return ret; } function padEnd(input, length) { const strLength = length ? input.length : 0; return length && strLength < length ? input + times(length - strLength, () => " ") : input || ""; } function intersection(a, b) { const aItems = new Set(a); const ret = []; for (const item of b) { if (aItems.has(item)) { ret.push(item); } } return ret; } // src/codeowners.ts function ownerMatcher(pathString) { const matcher = ignore().add(pathString); return matcher.ignores.bind(matcher); } var PARENT_FOLDERS = [".bitbucket", ".github", ".gitlab", "docs"]; var CODEOWNERS = "CODEOWNERS"; var Codeowners = class { codeownersFilePath; codeownersDirectory; contactInfo; ownerEntries = []; pathsByOwner = {}; constructor(currentPath = process.cwd(), fileName = CODEOWNERS) { const contactInfo = new ContactInfo(); const foundPath = findUpSync( PARENT_FOLDERS.map((folder) => path.join(folder, fileName)).concat(fileName), { cwd: currentPath } ); if (!foundPath) { throw new Error("Could not find a CODEOWNERS file"); } this.codeownersFilePath = tcp.trueCasePathSync(foundPath); this.codeownersDirectory = path.dirname(this.codeownersFilePath); if (PARENT_FOLDERS.includes(path.basename(this.codeownersDirectory))) { this.codeownersDirectory = path.dirname(this.codeownersDirectory); } const codeownersFile = path.basename(this.codeownersFilePath); if (codeownersFile !== fileName) { throw new Error( `Found a ${fileName} file but it was lower-cased: ${this.codeownersFilePath}` ); } if (isDirectorySync(this.codeownersFilePath)) { throw new Error(`Found a ${fileName} but it's a directory: ${this.codeownersFilePath}`); } const lines = fs.readFileSync(this.codeownersFilePath).toString().split(/\r\n|\r|\n/); for (const line of lines) { if (!line) { continue; } if (line.startsWith("##")) { contactInfo.addLine(line); continue; } if (line.startsWith("#")) { continue; } const [pathString, ...usernames] = line.split(/\s+/); const matcher = ownerMatcher(pathString); this.ownerEntries.push({ path: pathString, usernames, match(pathname) { return matcher(path.relative(currentPath, pathname)); } }); for (const owner of usernames) { if (!this.pathsByOwner[owner]) { this.pathsByOwner[owner] = []; } this.pathsByOwner[owner].push(pathString); } } this.ownerEntries.reverse(); this.contactInfo = contactInfo.owners; } getOwner(filePath) { for (const entry of this.ownerEntries) { if (entry.match(filePath)) { return [...entry.usernames]; } } return EMPTY_ARRAY; } getPathsForOwner(owner) { if (this.pathsByOwner[owner]) { return this.pathsByOwner[owner].slice(); } return []; } }; var codeowners_default = Codeowners; var EMPTY_ARRAY = []; export { padEnd, intersection, Codeowners, codeowners_default };