Skip to content

Commit

Permalink
Preserve block position (#347)
Browse files Browse the repository at this point in the history
## Description

This PR adds a new option to preserve the position of the generated
block if the user so desires. this is helpful when there's certain
strict order the developer wants to keep. Normally because there are
manual entries.

This closes #343 

Missing:

- [x] CLI option
- [x] Documentation
- [x] Logic Refactor (because it looks ugly what I did)

Extra:

So I was playing with ChatGPT, and I decided to ask for rewrites on my
code the changes are not that bad. So I left them in
  • Loading branch information
gagoar authored Mar 22, 2023
1 parent b89395e commit 932c34b
Show file tree
Hide file tree
Showing 9 changed files with 157 additions and 58 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,8 @@ You can configure `codeowners-generator` from several places:

- **groupSourceComments** (`--group-source-comments`): Instead of generating one comment per rule, enabling this flag will group them, reducing comments to one per source file. Useful if your codeowners file gets too noisy.

- **preserveBlockPosition** (`--preserve-block-position`): It will keep the generated block in the same position it was found in the CODEOWNERS file (if present). Useful for when you make manual additions.

- **customRegenerationCommand** (`--custom-regeneration-command`): Specify a custom regeneration command to be printed in the generated CODEOWNERS file, it should be mapped to run codeowners-generator (e.g. "npm run codeowners").

- **check** (`--check`): It will fail if the CODEOWNERS generated doesn't match the current (or missing) CODEOWNERS . Useful for validating that the CODEOWNERS file is not out of date during CI.
Expand Down
3 changes: 3 additions & 0 deletions __mocks__/CODEOWNERS_POPULATED_OUTPUT
Original file line number Diff line number Diff line change
Expand Up @@ -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
77 changes: 76 additions & 1 deletion __tests__/generate.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {} }
);
});
Expand Down Expand Up @@ -130,6 +135,10 @@ 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.
Expand Down Expand Up @@ -157,6 +166,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));

Expand Down
5 changes: 5 additions & 0 deletions src/bin/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ program
'Instead of generating one comment per rule, enabling this flag will group them, reducing comments to one per source file. Useful if your codeowners file gets too noisy',
false
)
.option(
'--preserve-block-position',
'It will keep the generated block in the same position it was found in the CODEOWNERS file (if present). Useful for when you make manual additions',
false
)
.option(
'--custom-regeneration-command',
'Specify a custom regeneration command to be printed in the generated CODEOWNERS file, it should be mapped to run codeowners-generator'
Expand Down
9 changes: 6 additions & 3 deletions src/commands/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ interface Options {
useMaintainers?: boolean;
useRootMaintainers?: boolean;
groupSourceComments?: boolean;
preserveBlockPosition?: boolean;
includes?: string[];
customRegenerationCommand?: string;
check?: boolean;
Expand All @@ -109,7 +110,7 @@ interface Options {
export const command = async (options: Options, command: Command): Promise<void> => {
const globalOptions = await getGlobalOptions(command);

const { verifyPaths, useMaintainers, useRootMaintainers, check } = options;
const { verifyPaths, useMaintainers, useRootMaintainers, check, preserveBlockPosition } = options;

const { output = globalOptions.output || OUTPUT } = options;

Expand All @@ -124,6 +125,7 @@ export const command = async (options: Options, command: Command): Promise<void>
useMaintainers,
useRootMaintainers,
groupSourceComments,
preserveBlockPosition,
customRegenerationCommand,
output,
});
Expand All @@ -141,8 +143,9 @@ export const command = async (options: Options, command: Command): Promise<void>
const [originalContent, newContent] = await generateOwnersFile(
output,
ownerRules,
customRegenerationCommand,
groupSourceComments
groupSourceComments,
preserveBlockPosition,
customRegenerationCommand
);

if (check) {
Expand Down
64 changes: 40 additions & 24 deletions src/utils/codeowners.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,11 @@
import fs from 'fs';
import isGlob from 'is-glob';
import {
MAINTAINERS_EMAIL_PATTERN,
contentTemplate,
CONTENT_MARK,
CHARACTER_RANGE_PATTERN,
rulesBlockTemplate,
} from './constants';
import { MAINTAINERS_EMAIL_PATTERN, CONTENT_MARK, CHARACTER_RANGE_PATTERN } from './constants';
import { dirname, join } from 'path';
import { readContent } from './readContent';
import { logger } from '../utils/debug';
import groupBy from 'lodash.groupby';
import { generatedContentTemplate, rulesBlockTemplate } from './templates';

const debug = logger('utils/codeowners');

Expand All @@ -26,27 +21,34 @@ export type ownerRule = {
glob: string;
};

const filterGeneratedContent = (content: string) => {
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[]);

return skip ? memo : [...memo, line];
}, [] as string[])
.join('\n');
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<createOwnersFileResponse> => {
let originalContent = '';

Expand All @@ -67,14 +69,28 @@ 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 = '';

const generatedContent = generatedContentTemplate(content.join('\n'), customRegenerationCommand) + '\n';

if (originalContent) {
normalizedContent = withoutGeneratedCode.reduce((memo, line, index) => {
if (preserveBlockPosition && index === blockPosition) {
memo += generatedContent;
}
memo += line + '\n';

return memo;
}, '');
}

if (!preserveBlockPosition) {
normalizedContent = normalizedContent + generatedContent;
}

return [originalContent, normalizedContent];
return [originalContent, normalizedContent.trimEnd()];
};

const parseCodeOwner = (filePath: string, codeOwnerContent: string): ownerRule[] => {
Expand Down
29 changes: 0 additions & 29 deletions src/utils/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,32 +12,3 @@ export const CHARACTER_RANGE_PATTERN = /\[(?:.-.)+\]/;
export const CONTENT_MARK = stripIndents`
#################################### Generated content - do not edit! ####################################
`;

const getContentLegend = (customRegenerationCommand?: string) => stripIndents`
# This block has been generated with codeowners-generator (for more information https://github.com/gagoar/codeowners-generator)
# ${
customRegenerationCommand ? `To re-generate, run \`${customRegenerationCommand}\`. ` : ''
}Don't worry, the content outside this block will be kept.
`;

export const contentTemplate = (
generatedContent: string,
originalContent: string,
customRegenerationCommand?: string
): string => {
return stripIndents`
${originalContent && originalContent.trimEnd()}
${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}
${entries.join('\n')}
`;
};
2 changes: 1 addition & 1 deletion src/utils/getCustomConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export type CustomConfig = {
};

export const getCustomConfiguration = async (): Promise<CustomConfig | void> => {
const loader = ora('Loading available configuration').start();
const loader = ora('Loading configuration').start();

try {
const explorer = cosmiconfig(packageJSON.name);
Expand Down
24 changes: 24 additions & 0 deletions src/utils/templates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { stripIndents } from 'common-tags';
import { CONTENT_MARK } from './constants';

const getContentLegend = (customRegenerationCommand?: string) => stripIndents`
# This block has been generated with codeowners-generator (for more information https://github.com/gagoar/codeowners-generator)
# ${
customRegenerationCommand ? `To re-generate, run \`${customRegenerationCommand}\`. ` : ''
}Don't worry, the content outside this block will be kept.
`;

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}
${entries.join('\n')}
`;
};

0 comments on commit 932c34b

Please sign in to comment.