@nmann/codeowners
Version:
A tool for working with CODEOWNERS files
159 lines (154 loc) • 4.32 kB
JavaScript
// 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
};