diff --git a/__mocks__/CODEOWNERS_POPULATED_OUTPUT b/__mocks__/CODEOWNERS_POPULATED_OUTPUT index c56598b7..716ee3e7 100644 --- a/__mocks__/CODEOWNERS_POPULATED_OUTPUT +++ b/__mocks__/CODEOWNERS_POPULATED_OUTPUT @@ -9,3 +9,6 @@ scripts/ @myOrg/infraTeam # Rule extracted from dir1/CODEOWNERS dir1/*.ts @eeny @meeny #################################### Generated content - do not edit! #################################### + +# Another line here that should be moved to after the generated block without preserve-block-position option enabled +dir2/ @otherTeam diff --git a/__tests__/generate.spec.ts b/__tests__/generate.spec.ts index 5485c8d0..6cfbb33e 100644 --- a/__tests__/generate.spec.ts +++ b/__tests__/generate.spec.ts @@ -66,7 +66,12 @@ describe('Generate', () => { }); await generateCommand( - { output: 'CODEOWNERS', customRegenerationCommand: 'yarn codeowners-generator generate', check: true }, + { + output: 'CODEOWNERS', + customRegenerationCommand: 'yarn codeowners-generator generate', + check: true, + preserveBlockPosition: true, + }, { parent: {} } ); }); @@ -130,6 +135,9 @@ describe('Generate', () => { # We might wanna keep an eye on something else, like yml files and workflows. .github/workflows/ @myOrg/infraTeam + + # Another line here that should be moved to after the generated block without preserve-block-position option enabled + dir2/ @otherTeam #################################### Generated content - do not edit! #################################### # This block has been generated with codeowners-generator (for more information https://github.com/gagoar/codeowners-generator) # To re-generate, run \`yarn codeowners-generator generate\`. Don't worry, the content outside this block will be kept. @@ -157,6 +165,72 @@ describe('Generate', () => { ] `); }); + + it('should generate a CODEOWNERS file (re-using codeowners content, with preserve-block-position enabled)', async () => { + sync.mockReturnValueOnce(Object.keys(files)); + + sync.mockReturnValueOnce(['.gitignore']); + + const withPopulatedCodeownersFile = { + ...withGitIgnore, + CODEOWNERS: '../__mocks__/CODEOWNERS_POPULATED_OUTPUT', + }; + existsSync.mockReturnValue(true); + readFile.mockImplementation((file, callback): void => { + const fullPath = path.join( + __dirname, + withPopulatedCodeownersFile[file as keyof typeof withPopulatedCodeownersFile] + ); + const content = readFileSync(fullPath); + callback(null, content); + }); + + await generateCommand( + { + output: 'CODEOWNERS', + customRegenerationCommand: 'yarn codeowners-generator generate', + preserveBlockPosition: true, + }, + { parent: {} } + ); + expect(writeFile.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + "CODEOWNERS", + "# We are already using CODEOWNERS and we don't want to lose the content of this file. + scripts/ @myOrg/infraTeam + # We might wanna keep an eye on something else, like yml files and workflows. + .github/workflows/ @myOrg/infraTeam + + #################################### Generated content - do not edit! #################################### + # This block has been generated with codeowners-generator (for more information https://github.com/gagoar/codeowners-generator) + # To re-generate, run \`yarn codeowners-generator generate\`. Don't worry, the content outside this block will be kept. + + # Rule extracted from dir1/CODEOWNERS + /dir1/**/*.ts @eeny @meeny + # Rule extracted from dir1/CODEOWNERS + /dir1/*.ts @miny + # Rule extracted from dir1/CODEOWNERS + /dir1/**/README.md @miny + # Rule extracted from dir1/CODEOWNERS + /dir1/README.md @moe + # Rule extracted from dir2/CODEOWNERS + /dir2/**/*.ts @moe + # Rule extracted from dir2/CODEOWNERS + /dir2/dir3/*.ts @miny + # Rule extracted from dir2/CODEOWNERS + /dir2/**/*.md @meeny + # Rule extracted from dir2/CODEOWNERS + /dir2/**/dir4/ @eeny + # Rule extracted from dir2/dir3/CODEOWNERS + /dir2/dir3/**/*.ts @miny + + #################################### Generated content - do not edit! #################################### + + # Another line here that should be moved to after the generated block without preserve-block-position option enabled + dir2/ @otherTeam", + ] + `); + }); it('should generate a CODEOWNERS FILE with groupSourceComments and customRegenerationCommand', async () => { sync.mockReturnValueOnce(Object.keys(files)); diff --git a/src/commands/generate.ts b/src/commands/generate.ts index 9dd8fbd2..6c4eb21b 100644 --- a/src/commands/generate.ts +++ b/src/commands/generate.ts @@ -101,6 +101,7 @@ interface Options { useMaintainers?: boolean; useRootMaintainers?: boolean; groupSourceComments?: boolean; + preserveBlockPosition?: boolean; includes?: string[]; customRegenerationCommand?: string; check?: boolean; @@ -109,7 +110,7 @@ interface Options { export const command = async (options: Options, command: Command): Promise => { const globalOptions = await getGlobalOptions(command); - const { verifyPaths, useMaintainers, useRootMaintainers, check } = options; + const { verifyPaths, useMaintainers, useRootMaintainers, check, preserveBlockPosition } = options; const { output = globalOptions.output || OUTPUT } = options; @@ -124,6 +125,7 @@ export const command = async (options: Options, command: Command): Promise useMaintainers, useRootMaintainers, groupSourceComments, + preserveBlockPosition, customRegenerationCommand, output, }); @@ -141,8 +143,9 @@ export const command = async (options: Options, command: Command): Promise const [originalContent, newContent] = await generateOwnersFile( output, ownerRules, - customRegenerationCommand, - groupSourceComments + groupSourceComments, + preserveBlockPosition, + customRegenerationCommand ); if (check) { diff --git a/src/utils/codeowners.ts b/src/utils/codeowners.ts index 07c6934c..8078e351 100644 --- a/src/utils/codeowners.ts +++ b/src/utils/codeowners.ts @@ -6,6 +6,7 @@ import { CONTENT_MARK, CHARACTER_RANGE_PATTERN, rulesBlockTemplate, + generatedContentTemplate, } from './constants'; import { dirname, join } from 'path'; import { readContent } from './readContent'; @@ -26,27 +27,38 @@ export type ownerRule = { glob: string; }; -const filterGeneratedContent = (content: string) => { +// to fix this I will have to return an [] and every line there. +// I will have also to return the position where the mark was found. +// later If the flag is set (--preserve-block-position) I will just count the array and when the same index is hit, inject the content. + +const filterGeneratedContent = (content: string): [withoutGeneratedCode: string[], blockPosition: number] => { const lines = content.split('\n'); let skip = false; - return lines - .reduce((memo, line) => { - if (line === CONTENT_MARK) { - skip = !skip; - return memo; + let generatedBlockPosition = -1; + + const withoutGeneratedCode = lines.reduce((memo, line, index) => { + if (line === CONTENT_MARK) { + skip = !skip; + if (generatedBlockPosition === -1) { + generatedBlockPosition = index; } + return memo; + } - return skip ? memo : [...memo, line]; - }, [] as string[]) - .join('\n'); + return skip ? memo : [...memo, line]; + }, [] as string[]); + + return [withoutGeneratedCode, generatedBlockPosition]; }; + type createOwnersFileResponse = [originalContent: string, newContent: string]; export const generateOwnersFile = async ( outputFile: string, ownerRules: ownerRule[], - customRegenerationCommand?: string, - groupSourceComments = false + groupSourceComments = false, + preserveBlockPosition = false, + customRegenerationCommand?: string ): Promise => { let originalContent = ''; @@ -67,12 +79,26 @@ export const generateOwnersFile = async ( } else { content = ownerRules.map((rule) => rulesBlockTemplate(rule.filePath, [`${rule.glob} ${rule.owners.join(' ')}`])); } + const [withoutGeneratedCode, blockPosition] = filterGeneratedContent(originalContent); - const normalizedContent = contentTemplate( - content.join('\n'), - filterGeneratedContent(originalContent), - customRegenerationCommand - ); + let normalizedContent = ''; + // this block should consider the option --preserve-block-position + // contentTemplate should change, maybe to contain this logic? I'm not sure yet. + + if (preserveBlockPosition && blockPosition !== -1 && withoutGeneratedCode.length) { + normalizedContent = withoutGeneratedCode + .reduce((memo, line, index) => { + if (index === blockPosition) { + memo += generatedContentTemplate(content.join('\n'), customRegenerationCommand) + '\n'; + } + memo += `${line}\n`; + + return memo; + }, '') + .trimEnd(); + } else { + normalizedContent = contentTemplate(content.join('\n'), withoutGeneratedCode.join('\n'), customRegenerationCommand); + } return [originalContent, normalizedContent]; }; diff --git a/src/utils/constants.ts b/src/utils/constants.ts index eea36e2f..e9d0b054 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -24,17 +24,22 @@ export const contentTemplate = ( generatedContent: string, originalContent: string, customRegenerationCommand?: string -): string => { +) => { return stripIndents` - ${originalContent && originalContent.trimEnd()} + ${originalContentTemplate(originalContent)} + ${generatedContentTemplate(generatedContent, customRegenerationCommand)} +`; +}; +export const originalContentTemplate = (originalContent: string) => originalContent && originalContent.trimEnd(); +export const generatedContentTemplate = (generatedContent: string, customRegenerationCommand?: string) => { + return stripIndents` ${CONTENT_MARK} ${getContentLegend(customRegenerationCommand)}\n ${generatedContent}\n ${CONTENT_MARK}\n -`; + `; }; - export const rulesBlockTemplate = (source: string, entries: string[]): string => { return stripIndents` # Rule${entries.length > 1 ? 's' : ''} extracted from ${source} diff --git a/src/utils/getCustomConfiguration.ts b/src/utils/getCustomConfiguration.ts index dc767400..61be93eb 100644 --- a/src/utils/getCustomConfiguration.ts +++ b/src/utils/getCustomConfiguration.ts @@ -16,7 +16,7 @@ export type CustomConfig = { }; export const getCustomConfiguration = async (): Promise => { - const loader = ora('Loading available configuration').start(); + const loader = ora('Loading configuration').start(); try { const explorer = cosmiconfig(packageJSON.name);