-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: codeowners was consuming too much memory (#107)
* fix * fix
- Loading branch information
Showing
3 changed files
with
109 additions
and
21 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,29 +1,30 @@ | ||
import { describe, expect, it } from 'vitest' | ||
import { describe, expect, test } from 'vitest' | ||
|
||
import Codeowners from './codeowners.js' | ||
|
||
describe('getOwners', () => { | ||
// Assuming src folder is owned by @fwuensche and @rchoquet | ||
it('recognizes folder patterns', async () => { | ||
test('/src/ pattern matches files inside src folder', async () => { | ||
const codeowners = new Codeowners() | ||
expect(codeowners.getOwners('src/codeowners.test.ts')).toEqual(['@fwuensche', '@rchoquet']) | ||
}) | ||
|
||
// Assuming bin folder has no defined owners, but @fwuensche is the default owner | ||
it('recognizes default owners', async () => { | ||
test('plugins/ pattern should match any folder named plugins', async () => { | ||
const codeowners = new Codeowners() | ||
expect(codeowners.getOwners('bin/commands/run.ts')).toEqual(['@fwuensche']) | ||
expect(codeowners.getOwners('src/plugins/eslint.js')).toEqual(['@fwuensche']) | ||
}) | ||
|
||
// Assuming js files are owned by @rchoquet | ||
it('recognizes file extension patterns', async () => { | ||
test('defaults to the root owner', async () => { | ||
const codeowners = new Codeowners() | ||
expect(codeowners.getOwners('bin/codeowners.js')).toEqual(['@rchoquet']) | ||
expect(codeowners.getOwners('bin/commands/push.ts')).toEqual(['@fwuensche']) | ||
}) | ||
|
||
// Assuming the file does not exist, but matches an existing pattern | ||
it('returns who would theoretically own the file even tho it does not exist', async () => { | ||
test('*.js also matches files from subfolders', async () => { | ||
const codeowners = new Codeowners() | ||
expect(codeowners.getOwners('bin/non-existing-file')).toEqual(['@fwuensche']) | ||
expect(codeowners.getOwners('bin/commands/diff.js')).toEqual(['@rchoquet']) | ||
}) | ||
|
||
test('non existing files return an empty list of owners', async () => { | ||
const codeowners = new Codeowners() | ||
expect(codeowners.getOwners('bin/non-existing-file')).toEqual([]) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,97 @@ | ||
import Codeowners from 'codeowners' | ||
import { findUpSync } from 'find-up' | ||
import fs from 'fs' | ||
import glob from 'glob' | ||
import intersection from 'lodash/intersection.js' | ||
import { isDirectory } from './file.js' | ||
import path from 'path' | ||
import trueCasePath from 'true-case-path' | ||
import uniq from 'lodash/uniq.js' | ||
|
||
// Create a subclass of Codeowners | ||
class ExtendedCodeowners extends Codeowners { | ||
getOwners: typeof this.getOwner | ||
// TODO: This should be dynamically generated from the .gitignore file | ||
const IGNORES = ['**/node_modules/**', '**/.git/**', '**/.github/**', '**/.gitlab/**', '**/docs/**'] | ||
|
||
constructor(...args: ConstructorParameters<typeof Codeowners>) { | ||
super(...args) | ||
// Point getOwners to the getOwner method (for backwards compatibility) | ||
this.getOwners = this.getOwner | ||
const { trueCasePathSync } = trueCasePath | ||
|
||
class Codeowners { | ||
ownersByFile: Record<string, string[]> | ||
|
||
constructor(currentPath?: string) { | ||
this.ownersByFile = {} | ||
this.init(currentPath) | ||
} | ||
|
||
init(currentPath?: string) { | ||
// Find the CODEOWNERS file or use the one provided as an argument | ||
const pathOrCwd = currentPath || process.cwd() | ||
const codeownersPath = findUpSync(['.github/CODEOWNERS', '.gitlab/CODEOWNERS', 'docs/CODEOWNERS', 'CODEOWNERS'], { | ||
cwd: pathOrCwd, | ||
}) | ||
|
||
if (!codeownersPath) return | ||
|
||
const codeownersFilePath = trueCasePathSync(codeownersPath) | ||
let codeownersDirectory = path.dirname(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 (codeownersDirectory.match(/\/(.github|.gitlab|docs)$/i)) codeownersDirectory = path.dirname(codeownersDirectory) | ||
|
||
const codeownersFile = path.basename(codeownersFilePath) | ||
|
||
if (codeownersFile !== 'CODEOWNERS') | ||
throw new Error(`Found a CODEOWNERS file but it was lower-cased: ${codeownersFilePath}`) | ||
|
||
if (isDirectory(codeownersFilePath)) | ||
throw new Error(`Found a CODEOWNERS but it's a directory: ${codeownersFilePath}`) | ||
|
||
const codeownersLines = fs | ||
.readFileSync(codeownersFilePath) | ||
.toString() | ||
.split(/\r\n|\r|\n/) // Split by line breaks | ||
.filter(Boolean) // Remove empty lines | ||
.map((line) => line.trim()) // Remove leading and trailing whitespace | ||
|
||
for (const line of codeownersLines) { | ||
// Remove comments | ||
if (line.startsWith('#')) continue | ||
|
||
// Split the line into path and owners | ||
const [codeownersPattern, ...owners] = line.split(/\s+/) | ||
|
||
// We do it in the order of the file, so that the last one wins | ||
|
||
for (const file of this.#globPatterns(codeownersPattern)) { | ||
this.ownersByFile[file] = uniq(owners) | ||
} | ||
} | ||
} | ||
|
||
/** Returns the files from the codebase mathing the given pattern */ | ||
#globPatterns(pattern: string) { | ||
if (pattern.includes('*')) return glob.sync(pattern.replace('*', '**/*'), { nodir: true, ignore: IGNORES }) | ||
|
||
if (pattern.endsWith('/')) { | ||
if (pattern.startsWith('/')) { | ||
return glob.sync(path.join(pattern.substring(1), '**', '*'), { nodir: true, ignore: IGNORES }) | ||
} else { | ||
return glob.sync(path.join('**', pattern, '*'), { nodir: true, ignore: IGNORES }) | ||
} | ||
} | ||
return [pattern] | ||
} | ||
|
||
getOwners(file: string) { | ||
return this.ownersByFile[file] || [] | ||
} | ||
|
||
getFiles(owners: string[]) { | ||
return uniq( | ||
Object.entries(this.ownersByFile) | ||
.filter(([, fileOwners]) => intersection(owners, fileOwners).length > 0) | ||
.map(([file]) => file) | ||
.flat() | ||
) | ||
} | ||
} | ||
|
||
export default ExtendedCodeowners | ||
export default Codeowners |