Skip to content

Commit

Permalink
Add ability to hide files from stats and validation in .codeownersignore
Browse files Browse the repository at this point in the history
  • Loading branch information
sjdave committed Aug 12, 2022
1 parent df254e3 commit a6ec8d0
Show file tree
Hide file tree
Showing 9 changed files with 96 additions and 13 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@
A CLI tool for working with GitHub CODEOWNERS.

Things it does:
* Calculate ownership stats
* Find out who owns each and every file (ignoring files listed in `.gitignore`)
* Calculate ownership stats (ignoring files listed in `.gitignore` and `.codeownersignore`)
* Find out who owns each and every file (ignoring files listed in `.gitignore` and `.codeownersignore`)
* Find out who owns a single file
* Find out who owns your staged files
* Outputs in a bunch of script friendly handy formats for integrations (CSV and JSONL)
* Validates that your CODEOWNERS file is valid
* Validates that your CODEOWNERS file is valid (ignoring files listed in `.gitignore` and `.codeownersignore`)

## Installation
Install via npm globally then run
Expand Down
51 changes: 51 additions & 0 deletions src/commands/__snapshots__/audit.test.int.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,23 @@ unloved,0,0
"
`;

exports[`audit csv should ignore ownership for ignored files: stderr 1`] = `""`;

exports[`audit csv should ignore ownership for ignored files: stdout 1`] = `
"
--- Counts ---
Total: 10 files (35 lines)
Loved: 10 files (35 lines)
Unloved: 0 files (0 lines)
--- Owners ---
@doctocat: 3 files (0 lines)
@global-owner1: 4 files (35 lines)
@global-owner2: 4 files (35 lines)
@js-owner: 2 files (0 lines)
@octocat: 1 files (0 lines)
"
`;

exports[`audit csv should list ownership for all files: stderr 1`] = `""`;

exports[`audit csv should list ownership for all files: stdout 1`] = `
Expand Down Expand Up @@ -90,6 +107,23 @@ exports[`audit jsonl should do all commands in combination when asked: stdout 1`
"
`;

exports[`audit jsonl should ignore ownership for ignored files: stderr 1`] = `""`;

exports[`audit jsonl should ignore ownership for ignored files: stdout 1`] = `
"
--- Counts ---
Total: 10 files (35 lines)
Loved: 10 files (35 lines)
Unloved: 0 files (0 lines)
--- Owners ---
@doctocat: 3 files (0 lines)
@global-owner1: 4 files (35 lines)
@global-owner2: 4 files (35 lines)
@js-owner: 2 files (0 lines)
@octocat: 1 files (0 lines)
"
`;

exports[`audit jsonl should list ownership for all files: stderr 1`] = `""`;

exports[`audit jsonl should list ownership for all files: stdout 1`] = `
Expand Down Expand Up @@ -170,6 +204,23 @@ Unloved: 0 files (0 lines)
"
`;

exports[`audit simple should ignore ownership for ignored files: stderr 1`] = `""`;

exports[`audit simple should ignore ownership for ignored files: stdout 1`] = `
"
--- Counts ---
Total: 10 files (35 lines)
Loved: 10 files (35 lines)
Unloved: 0 files (0 lines)
--- Owners ---
@doctocat: 3 files (0 lines)
@global-owner1: 4 files (35 lines)
@global-owner2: 4 files (35 lines)
@js-owner: 2 files (0 lines)
@octocat: 1 files (0 lines)
"
`;

exports[`audit simple should list ownership for all files: stderr 1`] = `""`;

exports[`audit simple should list ownership for all files: stdout 1`] = `
Expand Down
9 changes: 9 additions & 0 deletions src/commands/audit.test.int.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,15 @@ describe('audit', () => {
expect(stderr).toMatchSnapshot('stderr');
});

it('should ignore ownership for ignored files', async () => {
const { stdout, stderr } = await runCli(`audit -s ${output}`);
await writeFile(path.join(testDir, '.codeownersignore'), '.ignored-file\n.codeownersignore');
await writeFile(path.join(testDir, '.ignored-file'), 'this should be ignored');

expect(stdout).toMatchSnapshot('stdout');
expect(stderr).toMatchSnapshot('stderr');
});

it('should show only unloved files when asked', async () => {
const { stdout, stderr } = await runCli(`audit -u -o ${output}`);
expect(stdout).toMatchSnapshot('stdout');
Expand Down
14 changes: 12 additions & 2 deletions src/lib/file/getFilePaths.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { readGit } from './readGit';
import { readDir } from './readDir';
import * as path from 'path';
import fs from 'fs';
import ignore from 'ignore';

export enum FILE_DISCOVERY_STRATEGY {
FILE_SYSTEM,
Expand All @@ -10,10 +12,18 @@ export enum FILE_DISCOVERY_STRATEGY {
export const getFilePaths = async (dir: string, strategy: FILE_DISCOVERY_STRATEGY, root?: string) => {
let filePaths;

const ignores = ignore().add(['.git']);
try {
const contents = fs.readFileSync(path.resolve('.codeownersignore')).toString();
ignores.add(contents);
// tslint:disable-next-line:no-empty
} catch (e) {
}

if (strategy === FILE_DISCOVERY_STRATEGY.GIT_LS) {
filePaths = await readGit(dir);
filePaths = await readGit(dir, ignores);
} else {
filePaths = await readDir(dir, ['.git']);
filePaths = await readDir(dir, ignores);
}

if (root) { // We need to re-add the root so that later ops can find the file
Expand Down
3 changes: 2 additions & 1 deletion src/lib/file/readDir.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import fs from 'fs';
import * as underTest from './readDir';
import * as path from 'path';
import ignore from 'ignore';

jest.mock('fs');

Expand Down Expand Up @@ -70,7 +71,7 @@ describe('readDirRecursively', () => {
statSyncMock.mockReturnValue(statFake(STAT_FAKE_TYPES.FILE));

// Act
const result = await underTest.readDir('root', ['*.js']);
const result = await underTest.readDir('root', ignore().add(['*.js']));

// Assert
expect(result).toEqual(expectedFiles);
Expand Down
5 changes: 2 additions & 3 deletions src/lib/file/readDir.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,10 @@ import fs, { Stats } from 'fs';
import ignore, { Ignore } from 'ignore';
import path from 'path';

export const readDir = async (dir: string, filters: string[] = []): Promise<string[]> => {
export const readDir = async (dir: string, filters: Ignore = ignore()): Promise<string[]> => {
return new Promise((resolve, reject) => {
try {
const ignores = ignore().add(filters);
const files = walkDir(dir, '', ignores);
const files = walkDir(dir, '', filters);
resolve(files);
} catch (e) {
reject(e);
Expand Down
5 changes: 3 additions & 2 deletions src/lib/file/readGit.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import fs, { Stats } from 'fs';
import { exec } from '../util/exec';
import ignore, { Ignore } from 'ignore';

export const readGit = async (dir: string): Promise<string[]> => {
export const readGit = async (dir: string, ignores: Ignore = ignore()): Promise<string[]> => {
const { stdout } = await exec('git ls-files', { cwd: dir });
return stdout.split('\n').filter((filePath) => {
let stats: Stats | undefined = undefined;
Expand All @@ -12,7 +13,7 @@ export const readGit = async (dir: string): Promise<string[]> => {
}

// Ignore if path is not a file
if (!stats.isFile()){
if (!stats.isFile() || ignores.ignores(filePath)){
return false;
}

Expand Down
2 changes: 1 addition & 1 deletion src/lib/ownership/OwnershipEngine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export class OwnershipEngine {
const owned: FileOwnershipMatcher[] = [];

for (const line of lines) {
if (!line || line.startsWith('#')) {
if (!line || line.trimStart().startsWith('#')) {
continue;
}

Expand Down
14 changes: 13 additions & 1 deletion src/lib/ownership/validate.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { OwnershipEngine } from './OwnershipEngine';
import { readDir } from '../file/readDir';
import fs from 'fs';
import path from 'path';
import ignore from 'ignore';

interface ValidationResults {
duplicated: Set<string>;
Expand All @@ -9,7 +12,16 @@ interface ValidationResults {
export const validate = async (options: { codeowners: string, dir: string, root?: string }): Promise<ValidationResults> => {
const engine = OwnershipEngine.FromCodeownersFile(options.codeowners); // Validates code owner file

const filePaths = await readDir(options.dir, ['.git']);

const ignores = ignore().add(['.git']);
try {
const contents = fs.readFileSync(path.resolve('.codeownersignore')).toString();
ignores.add(contents);
// tslint:disable-next-line:no-empty
} catch (e) {
}

const filePaths = await readDir(options.dir, ignores);

for (const file of filePaths) {
engine.calcFileOwnership(file); // Test each file against rule set
Expand Down

0 comments on commit a6ec8d0

Please sign in to comment.