diff --git a/CODEOWNERS b/CODEOWNERS index 87f0d55e..0e03fe1a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -6,8 +6,8 @@ # precedence. When someone opens a pull request that only # modifies JS files, only these and not the global # owner(s) will be requested for a review. -*.js @fwuensche +*.js @rchoquet # In this example, the user owns any files in the src directory # at the root of the repository and any of its subdirectories. -/src/ @fwuensche +/src/ @fwuensche @rchoquet diff --git a/package-lock.json b/package-lock.json index 0958f7d0..06f10941 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "dependencies": { "axios": "^1.2.3", + "codeowners": "^5.1.1", "commander": "^10.0.0", "dotenv": "^16.4.5", "esbuild": "^0.20.0", @@ -28,6 +29,7 @@ "cherry": "dist/bin/cherry.js" }, "devDependencies": { + "@types/glob": "^8.1.0", "@types/lodash": "^4.17.7", "@types/spinnies": "^0.5.3", "@types/uuid": "^10.0.0", @@ -1012,19 +1014,33 @@ "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "dev": true }, + "node_modules/@types/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@types/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-IO+MJPVhoqz+28h1qLAcBEH2+xHMK6MTyHJc7MTnnYb6wsoLR29POVGJ7LycmVXIqyy/4/2ShP5sUwTXuOwb/w==", + "dev": true, + "dependencies": { + "@types/minimatch": "^5.1.2", + "@types/node": "*" + } + }, "node_modules/@types/lodash": { "version": "4.17.7", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz", "integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==", "dev": true }, + "node_modules/@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", + "dev": true + }, "node_modules/@types/node": { "version": "22.5.4", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.4.tgz", "integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==", "dev": true, - "optional": true, - "peer": true, "dependencies": { "undici-types": "~6.19.2" } @@ -1683,6 +1699,139 @@ "node": ">=0.8" } }, + "node_modules/codeowners": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/codeowners/-/codeowners-5.1.1.tgz", + "integrity": "sha512-NKsnAQQBhdsfkm7xZb073MTlzfz9kmE9iyjIcsfU9kZMPq2E7e+O42HD+yFTIu3f1CwvnBsSFdSLjv5k6CRIZg==", + "dependencies": { + "@nodelib/fs.walk": "^1.2.6", + "commander": "^6.2.1", + "find-up": "^2.1.0", + "ignore": "^3.3.10", + "is-directory": "^0.3.1", + "lodash.intersection": "^4.4.0", + "lodash.maxby": "^4.6.0", + "lodash.padend": "^4.6.1", + "true-case-path": "^1.0.3" + }, + "bin": { + "codeowners": "index.js" + } + }, + "node_modules/codeowners/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/codeowners/node_modules/commander": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", + "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/codeowners/node_modules/find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==", + "dependencies": { + "locate-path": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/codeowners/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/codeowners/node_modules/ignore": { + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", + "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==" + }, + "node_modules/codeowners/node_modules/locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==", + "dependencies": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/codeowners/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/codeowners/node_modules/p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dependencies": { + "p-try": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/codeowners/node_modules/p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==", + "dependencies": { + "p-limit": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/codeowners/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "engines": { + "node": ">=4" + } + }, + "node_modules/codeowners/node_modules/true-case-path": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/true-case-path/-/true-case-path-1.0.3.tgz", + "integrity": "sha512-m6s2OdQe5wgpFMC+pAJ+q9djG82O2jcHPOI6RNg1yy9rCYR+WD6Nbpl32fDpfC56nirdRy+opFa/Vk7HYhqaew==", + "dependencies": { + "glob": "^7.1.2" + } + }, "node_modules/color-convert": { "version": "1.9.3", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", @@ -3076,6 +3225,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-directory": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", + "integrity": "sha512-yVChGzahRFvbkscn2MlwGismPO12i9+znNruC5gVEntG3qu0xQMzsGg/JFbrsqDOHtHFPci+V5aP5T9I+yeKqw==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -3331,12 +3488,27 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash.intersection": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.intersection/-/lodash.intersection-4.4.0.tgz", + "integrity": "sha512-N+L0cCfnqMv6mxXtSPeKt+IavbOBBSiAEkKyLasZ8BVcP9YXQgxLO12oPR8OyURwKV8l5vJKiE1M8aS70heuMg==" + }, + "node_modules/lodash.maxby": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/lodash.maxby/-/lodash.maxby-4.6.0.tgz", + "integrity": "sha512-QfTqQTwzmKxLy7VZlbx2M/ipWv8DCQ2F5BI/MRxLharOQ5V78yMSuB+JE+EuUM22txYfj09R2Q7hUlEYj7KdNg==" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lodash.padend": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/lodash.padend/-/lodash.padend-4.6.1.tgz", + "integrity": "sha512-sOQs2aqGpbl27tmCS1QNZA09Uqp01ZzWfDUoD+xzTii0E7dSQfRKcRetFwa+uXaxaqL+TKm7CgD2JdKP7aZBSw==" + }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -4050,6 +4222,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", + "engines": { + "node": ">=4" + } + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -5632,9 +5812,7 @@ "version": "6.19.8", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "dev": true, - "optional": true, - "peer": true + "dev": true }, "node_modules/uri-js": { "version": "4.4.1", diff --git a/package.json b/package.json index b40ee3d3..0812ec79 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "cherry": "tsx ./bin/cherry.ts", "prepare": "husky install && npm run build", "bump": "npm version patch && npm publish", - "test:watch": "NODE_OPTIONS=--experimental-vm-modules jest --watchAll", + "test:watch": "vitest watch", "lint": "eslint . --ext .js,.ts", "test": "sh test/fixtures/setup.sh && vitest run", "test:setup": "sh test/fixtures/setup.sh", @@ -39,6 +39,7 @@ "homepage": "https://github.com/fwuensche/cherry-cli#readme", "dependencies": { "axios": "^1.2.3", + "codeowners": "^5.1.1", "commander": "^10.0.0", "dotenv": "^16.4.5", "esbuild": "^0.20.0", @@ -54,6 +55,7 @@ "uuid": "^9.0.1" }, "devDependencies": { + "@types/glob": "^8.1.0", "@types/lodash": "^4.17.7", "@types/spinnies": "^0.5.3", "@types/uuid": "^10.0.0", diff --git a/src/codeowners.js b/src/codeowners.js deleted file mode 100644 index 97280e2f..00000000 --- a/src/codeowners.js +++ /dev/null @@ -1,78 +0,0 @@ -import { findUpSync } from 'find-up' -import fs from 'fs' -import glob from 'glob' -import intersection from 'lodash/intersection.js' -import uniq from 'lodash/uniq.js' -import path from 'path' -import trueCasePath from 'true-case-path' -import { isDirectory } from './file.js' - -const { trueCasePathSync } = trueCasePath - -class Codeowners { - constructor() { - this.ownersByFile = {} - this.init() - } - - init() { - const fileName = 'CODEOWNERS' - - const codeownersPath = findUpSync( - [`.github/${fileName}`, `.gitlab/${fileName}`, `docs/${fileName}`, `${fileName}`], - { cwd: process.cwd() } - ) - - 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 !== fileName) - throw new Error(`Found a ${fileName} file but it was lower-cased: ${codeownersFilePath}`) - - if (isDirectory(codeownersFilePath)) - throw new Error(`Found a ${fileName} but it's a directory: ${codeownersFilePath}`) - - const lines = fs - .readFileSync(codeownersFilePath) - .toString() - .split(/\r\n|\r|\n/) - .filter(Boolean) - .map((line) => line.trim()) - - for (const line of lines) { - if (line.startsWith('#')) continue - - const [codeownersPath, ...owners] = line.split(/\s+/) - for (const file of this.#globFiles(codeownersPath)) this.ownersByFile[file] = uniq(owners) - } - } - - #globFiles(codeownersPath) { - if (codeownersPath.includes('*')) return glob.sync(codeownersPath, { nodir: true }) - if (isDirectory(codeownersPath)) return glob.sync(path.join(codeownersPath, '**/*'), { nodir: true }) - return [codeownersPath] - } - - getFiles(owners) { - return uniq( - Object.entries(this.ownersByFile) - .filter(([, fileOwners]) => intersection(owners, fileOwners).length > 0) - .map(([file]) => file) - .flat() - ) - } - - getOwners(file) { - return this.ownersByFile[file] || [] - } -} - -export default Codeowners diff --git a/src/codeowners.test.ts b/src/codeowners.test.ts new file mode 100644 index 00000000..81f9e548 --- /dev/null +++ b/src/codeowners.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from 'vitest' + +import Codeowners from './codeowners.js' + +describe('getOwners', () => { + // Assuming src folder is owned by @fwuensche and @rchoquet + it('recognizes folder patterns', 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 () => { + const codeowners = new Codeowners() + expect(codeowners.getOwners('bin/commands/run.ts')).toEqual(['@fwuensche']) + }) + + // Assuming js files are owned by @rchoquet + it('recognizes file extension patterns', async () => { + const codeowners = new Codeowners() + expect(codeowners.getOwners('bin/codeowners.js')).toEqual(['@rchoquet']) + }) + + // 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 () => { + const codeowners = new Codeowners() + expect(codeowners.getOwners('bin/non-existing-file')).toEqual(['@fwuensche']) + }) +}) diff --git a/src/codeowners.ts b/src/codeowners.ts new file mode 100644 index 00000000..a6c4876b --- /dev/null +++ b/src/codeowners.ts @@ -0,0 +1,14 @@ +import Codeowners from 'codeowners' + +// Create a subclass of Codeowners +class ExtendedCodeowners extends Codeowners { + getOwners: typeof this.getOwner + + constructor(...args: ConstructorParameters) { + super(...args) + // Point getOwners to the getOwner method (for backwards compatibility) + this.getOwners = this.getOwner + } +} + +export default ExtendedCodeowners