Skip to content

Support GitLab's extended syntax #45

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1 +1,22 @@
* @beaugunderson

a\ path\ with\ spaces/and\ subdirectories/ @jasonsperske

[Test Default Section] @jasonsperske
README.md

[Another Section]
package.json @example

[Still Another Section][2] @example-section-owner
/index.mjs

[Test Inline Comments] @example-inline-comment-owner # section comment
package-lock.json # inline comment

[A Section With an Override] @example-override-group-owner
codeowners.mjs @example-override-file-owner
codeowners.d.ts

[A orphaned section]
/orphaned-file.md
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ repos.getOwner('path/to/file.js'); // => array of owner strings, e.g. ['@noahm']

## CHANGELOG

### 5.1.3
- Added support for entries with escaped spaces, and inline comments

### 5.1.2
- Added code to support the Default Owners feature of GitLab

### 5.0.0

- Much-improved performance
Expand Down
88 changes: 0 additions & 88 deletions codeowners.js

This file was deleted.

116 changes: 116 additions & 0 deletions codeowners.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import findUp from "find-up";
import fs from "fs";
import path from "path";
import ignore from "ignore";
import isDirectory from "is-directory";

const SECTION_REGEX = /\[([^\]]*)\](?:\[(\d+)\])?([^#]*)(?:#(.*))?/;
const ENTRY_REGEX = /^((?:\\\s|[^\s])*)\s?([^#]*)(#.*)?$/;

/**
* @param {string} pathString the path to match
* @returns {function(): boolean}
*/
function ownerMatcher(pathString) {
const matcher = ignore().add(pathString);
return matcher.ignores.bind(matcher);
}

export default function Codeowners(currentPath, fileName = "CODEOWNERS") {
const pathOrCwd = currentPath || process.cwd();

this.codeownersFilePath = findUp.sync(
[
`.github/${fileName}`,
`.gitlab/${fileName}`,
`docs/${fileName}`,
`${fileName}`,
],
{ cwd: pathOrCwd }
);

if (!this.codeownersFilePath) {
throw new Error(`Could not find a CODEOWNERS file`);
}

this.codeownersDirectory = path.dirname(this.codeownersFilePath);

// We might have found a bare codeowners file or one inside the three supported subdirectories.
// In the latter case the project root is up another level.
if (this.codeownersDirectory.match(/\/(.github|.gitlab|docs)$/i)) {
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 (isDirectory.sync(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/)
.map((line) => line.trim())
.filter((line) => line.length && !line.startsWith("#"));

const ownerSections = [];
let currentSection = { name: null, owners: [], entries: [] };

for (const line of lines) {
if (SECTION_REGEX.test(line)) {
const groups = SECTION_REGEX.exec(line);
const name = groups[1];
const owners = (groups[3] || "").split(/\s+/).filter((v) => v.length);

ownerSections.push(currentSection);
currentSection = { name, owners, entries: [] };
continue;
}
if (ENTRY_REGEX.test(line)) {
const groups = ENTRY_REGEX.exec(line);
const pathString = groups[1].replace(/\\\s/g, " ");
const usernames = (groups[2] || "").split(/\s+/).filter((v) => v.length);

currentSection.entries.push({
path: pathString,
usernames: usernames.length
? [...usernames]
: [...currentSection.owners],
match: ownerMatcher(pathString),
});
continue;
}
console.error(`Could not parse line: ${line}`);
}

ownerSections.push(currentSection);

// reverse the owner entries to search from bottom to top
// the last matching pattern takes the most precedence
this.ownerSections = ownerSections.reverse();
}

Codeowners.prototype.getOwner = function getOwner(filePath) {
for (const section of this.ownerSections) {
const owners = [];
for (const entry of section.entries) {
if (entry.match(filePath)) {
owners.push(...entry.usernames);
}
}
if (owners.length) {
return [...new Set(owners)];
}
}

return [];
};
18 changes: 0 additions & 18 deletions codeowners.test.js

This file was deleted.

55 changes: 55 additions & 0 deletions codeowners.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/* global describe it expect */
import Codeowners from './codeowners.mjs';

const repos = new Codeowners();

describe('codeowners', () => {
it('returns owners for a file defined outside of a section', () => {
const owner = repos.getOwner('codeowners.test.mjs');
expect(owner).toEqual(['@beaugunderson']);
});

it('overrides default owners with section owners', () => {
const owner = repos.getOwner('README.md');
expect(owner).toEqual(['@jasonsperske']);
});

it('should combine orphaned files in a section with any root level owners', () => {
const owner = repos.getOwner('orphaned-file.md');
expect(owner).toEqual(['@beaugunderson']);
})

it('overrides owners defined in a file inside of an ownerless section', () => {
const owner = repos.getOwner('package.json');
expect(owner).toEqual(['@example']);
});

it('allows definition of minimum owners requiring approval', () => {
const owner = repos.getOwner('index.mjs');
expect(owner).toEqual(['@example-section-owner']);
});

it('allows inline comments', () => {
const owner = repos.getOwner('package-lock.json');
expect(owner).toEqual(['@example-inline-comment-owner']);
});

it('allows paths with spaces', () => {
const owner = repos.getOwner('a path with spaces/and subdirectories/index.mjs');
expect(owner).toEqual(['@beaugunderson', '@jasonsperske']);
});

it('allows a group to be overridden', () => {
const ownerOfDefault = repos.getOwner('codeowners.d.ts');
expect(ownerOfDefault).toEqual(['@example-override-group-owner']);
const ownerOverridden = repos.getOwner('codeowners.mjs');
expect(ownerOverridden).toEqual(['@example-override-file-owner']);
});

it('owners is a copy of internal data', () => {
repos.getOwner('codeowners.test.mjs').pop();

const owner = repos.getOwner('codeowners.test.mjs');
expect(owner).toEqual(['@beaugunderson']);
});
});
28 changes: 15 additions & 13 deletions index.js → index.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@
/* eslint-disable no-console */
// @ts-check

const findUp = require('find-up');
const fs = require('fs');
const ignore = require('ignore');
const intersection = require('lodash.intersection');
const padEnd = require('lodash.padend');
const path = require('path');
const program = require('commander');
const { walkStream } = require('@nodelib/fs.walk');
import findUp from 'find-up';
import fs from 'fs';
import ignore from 'ignore';
import intersection from 'lodash.intersection';
import padEnd from 'lodash.padend';
import path from 'path';
import program from 'commander';
import { walkStream } from '@nodelib/fs.walk';

const Codeowners = require('./codeowners.js');
import Codeowners from './codeowners.mjs';

const rootPath = process.cwd();

Expand All @@ -31,7 +31,7 @@ program
.option(
'-c, --codeowners-filename <codeowners_filename>',
'specify CODEOWNERS filename',
'CODEOWNERS'
'CODEOWNERS',
)
.action((options) => {
let codeowners;
Expand All @@ -48,7 +48,9 @@ program
const stream = walkStream(rootPath, {
deepFilter: (entry) => {
const split = entry.path.split(path.sep);
return !split.includes('node_modules') && !split.includes('.git') && !split.includes('.cache');
return (
!split.includes('node_modules') && !split.includes('.git') && !split.includes('.cache')
);
},
errorFilter: (error) =>
error.code === 'ENOENT' || error.code === 'EACCES' || error.code === 'EPERM',
Expand All @@ -67,7 +69,7 @@ program
}
} else {
console.log(
`${padEnd(relative, padding)} ${owners.length ? owners.join(' ') : 'nobody'}`
`${padEnd(relative, padding)} ${owners.length ? owners.join(' ') : 'nobody'}`,
);
}
});
Expand All @@ -83,7 +85,7 @@ program
.option(
'-c, --codeowners-filename <codeowners_filename>',
'specify CODEOWNERS filename',
'CODEOWNERS'
'CODEOWNERS',
)
.action((checkPath, users, options) => {
let codeowners;
Expand Down
Loading