From 4ef3de522c7afef077473d2dfbe3ceeecbcefee1 Mon Sep 17 00:00:00 2001 From: Sam Jacobs Date: Fri, 12 Aug 2022 16:03:34 -0400 Subject: [PATCH] Add ability to hide files from stats and validation in .codeownersignore --- README.md | 6 +-- .../__snapshots__/audit.test.int.ts.snap | 51 +++++++++++++++++++ src/commands/audit.test.int.ts | 9 ++++ src/lib/file/getFilePaths.ts | 14 ++++- src/lib/file/readDir.test.ts | 3 +- src/lib/file/readDir.ts | 5 +- src/lib/file/readGit.ts | 5 +- src/lib/ownership/validate.ts | 14 ++++- 8 files changed, 95 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index c883ea0..032c08a 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/src/commands/__snapshots__/audit.test.int.ts.snap b/src/commands/__snapshots__/audit.test.int.ts.snap index 6a934d9..3ca792c 100644 --- a/src/commands/__snapshots__/audit.test.int.ts.snap +++ b/src/commands/__snapshots__/audit.test.int.ts.snap @@ -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`] = ` @@ -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`] = ` @@ -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`] = ` diff --git a/src/commands/audit.test.int.ts b/src/commands/audit.test.int.ts index 92cb4aa..03cb358 100644 --- a/src/commands/audit.test.int.ts +++ b/src/commands/audit.test.int.ts @@ -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'); diff --git a/src/lib/file/getFilePaths.ts b/src/lib/file/getFilePaths.ts index 7ceca0d..507c8e8 100644 --- a/src/lib/file/getFilePaths.ts +++ b/src/lib/file/getFilePaths.ts @@ -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, @@ -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 diff --git a/src/lib/file/readDir.test.ts b/src/lib/file/readDir.test.ts index 95eb84d..7f27e28 100644 --- a/src/lib/file/readDir.test.ts +++ b/src/lib/file/readDir.test.ts @@ -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'); @@ -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); diff --git a/src/lib/file/readDir.ts b/src/lib/file/readDir.ts index 1119f66..add3af8 100644 --- a/src/lib/file/readDir.ts +++ b/src/lib/file/readDir.ts @@ -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 => { +export const readDir = async (dir: string, filters: Ignore = ignore()): Promise => { 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); diff --git a/src/lib/file/readGit.ts b/src/lib/file/readGit.ts index 3a9574b..e528fab 100644 --- a/src/lib/file/readGit.ts +++ b/src/lib/file/readGit.ts @@ -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 => { +export const readGit = async (dir: string, ignores: Ignore = ignore()): Promise => { const { stdout } = await exec('git ls-files', { cwd: dir }); return stdout.split('\n').filter((filePath) => { let stats: Stats | undefined = undefined; @@ -12,7 +13,7 @@ export const readGit = async (dir: string): Promise => { } // Ignore if path is not a file - if (!stats.isFile()){ + if (!stats.isFile() || ignores.ignores(filePath)){ return false; } diff --git a/src/lib/ownership/validate.ts b/src/lib/ownership/validate.ts index ef30dbd..6f7ec0c 100644 --- a/src/lib/ownership/validate.ts +++ b/src/lib/ownership/validate.ts @@ -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; @@ -9,7 +12,16 @@ interface ValidationResults { export const validate = async (options: { codeowners: string, dir: string, root?: string }): Promise => { 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