mirror of
https://github.com/renovatebot/renovate.git
synced 2025-01-28 15:36:27 +00:00
Merge a0c37f83e8
into 070b78a040
This commit is contained in:
commit
ad1c7e5ef4
5 changed files with 216 additions and 21 deletions
58
lib/modules/platform/gitlab/code-owners.ts
Normal file
58
lib/modules/platform/gitlab/code-owners.ts
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
import ignore from 'ignore';
|
||||||
|
import { regEx } from '../../../util/regex';
|
||||||
|
import type { FileOwnerRule } from '../types';
|
||||||
|
|
||||||
|
export class CodeOwnersParser {
|
||||||
|
private currentSection: { name: string; defaultUsers: string[] };
|
||||||
|
private internalRules: FileOwnerRule[];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.currentSection = { name: '', defaultUsers: [] };
|
||||||
|
this.internalRules = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
private changeCurrentSection(line: string): void {
|
||||||
|
const [name, ...usernames] = line.split(regEx(/\s+/));
|
||||||
|
this.currentSection = { name, defaultUsers: usernames };
|
||||||
|
}
|
||||||
|
|
||||||
|
private addRule(rule: FileOwnerRule): void {
|
||||||
|
this.internalRules.push(rule);
|
||||||
|
}
|
||||||
|
|
||||||
|
private extractOwnersFromLine(
|
||||||
|
line: string,
|
||||||
|
defaultUsernames: string[],
|
||||||
|
): FileOwnerRule {
|
||||||
|
const [pattern, ...usernames] = line.split(regEx(/\s+/));
|
||||||
|
const matchPattern = ignore().add(pattern);
|
||||||
|
return {
|
||||||
|
usernames: usernames.length > 0 ? usernames : defaultUsernames,
|
||||||
|
pattern,
|
||||||
|
score: pattern.length,
|
||||||
|
match: (path: string) => matchPattern.ignores(path),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
parseLine(line: string): CodeOwnersParser {
|
||||||
|
if (CodeOwnersParser.isSectionHeader(line)) {
|
||||||
|
this.changeCurrentSection(line);
|
||||||
|
} else {
|
||||||
|
const rule = this.extractOwnersFromLine(
|
||||||
|
line,
|
||||||
|
this.currentSection.defaultUsers,
|
||||||
|
);
|
||||||
|
this.addRule(rule);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
get rules(): FileOwnerRule[] {
|
||||||
|
return this.internalRules;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static isSectionHeader(line: string): boolean {
|
||||||
|
return line.startsWith('[') || line.startsWith('^[');
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,7 +25,7 @@ import { setBaseUrl } from '../../../util/http/gitlab';
|
||||||
import type { HttpResponse } from '../../../util/http/types';
|
import type { HttpResponse } from '../../../util/http/types';
|
||||||
import { parseInteger } from '../../../util/number';
|
import { parseInteger } from '../../../util/number';
|
||||||
import * as p from '../../../util/promises';
|
import * as p from '../../../util/promises';
|
||||||
import { regEx } from '../../../util/regex';
|
import { newlineRegex, regEx } from '../../../util/regex';
|
||||||
import { sanitize } from '../../../util/sanitize';
|
import { sanitize } from '../../../util/sanitize';
|
||||||
import {
|
import {
|
||||||
ensureTrailingSlash,
|
ensureTrailingSlash,
|
||||||
|
@ -39,6 +39,7 @@ import type {
|
||||||
EnsureCommentConfig,
|
EnsureCommentConfig,
|
||||||
EnsureCommentRemovalConfig,
|
EnsureCommentRemovalConfig,
|
||||||
EnsureIssueConfig,
|
EnsureIssueConfig,
|
||||||
|
FileOwnerRule,
|
||||||
FindPRConfig,
|
FindPRConfig,
|
||||||
GitUrlOption,
|
GitUrlOption,
|
||||||
Issue,
|
Issue,
|
||||||
|
@ -54,6 +55,7 @@ import type {
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { repoFingerprint } from '../util';
|
import { repoFingerprint } from '../util';
|
||||||
import { smartTruncate } from '../utils/pr-body';
|
import { smartTruncate } from '../utils/pr-body';
|
||||||
|
import { CodeOwnersParser } from './code-owners';
|
||||||
import {
|
import {
|
||||||
getMemberUserIDs,
|
getMemberUserIDs,
|
||||||
getMemberUsernames,
|
getMemberUsernames,
|
||||||
|
@ -1478,3 +1480,23 @@ export async function expandGroupMembers(
|
||||||
}
|
}
|
||||||
return expandedReviewersOrAssignees;
|
return expandedReviewersOrAssignees;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function extractRulesFromCodeOwnersFile(
|
||||||
|
codeOwnersFile: string,
|
||||||
|
): FileOwnerRule[] {
|
||||||
|
const parser = new CodeOwnersParser();
|
||||||
|
|
||||||
|
const cleanedLines = codeOwnersFile
|
||||||
|
.split(newlineRegex)
|
||||||
|
// Remove comments
|
||||||
|
.map((line) => line.split('#')[0])
|
||||||
|
// Remove empty lines
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter(is.nonEmptyString);
|
||||||
|
|
||||||
|
for (const line of cleanedLines) {
|
||||||
|
parser.parseLine(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parser.rules;
|
||||||
|
}
|
||||||
|
|
|
@ -221,6 +221,13 @@ export interface AutodiscoverConfig {
|
||||||
projects?: string[];
|
projects?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FileOwnerRule {
|
||||||
|
usernames: string[];
|
||||||
|
pattern: string;
|
||||||
|
score: number;
|
||||||
|
match: (path: string) => boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Platform {
|
export interface Platform {
|
||||||
findIssue(title: string): Promise<Issue | null>;
|
findIssue(title: string): Promise<Issue | null>;
|
||||||
getIssueList(): Promise<Issue[]>;
|
getIssueList(): Promise<Issue[]>;
|
||||||
|
@ -280,6 +287,7 @@ export interface Platform {
|
||||||
filterUnavailableUsers?(users: string[]): Promise<string[]>;
|
filterUnavailableUsers?(users: string[]): Promise<string[]>;
|
||||||
commitFiles?(config: CommitFilesConfig): Promise<LongCommitSha | null>;
|
commitFiles?(config: CommitFilesConfig): Promise<LongCommitSha | null>;
|
||||||
expandGroupMembers?(reviewersOrAssignees: string[]): Promise<string[]>;
|
expandGroupMembers?(reviewersOrAssignees: string[]): Promise<string[]>;
|
||||||
|
extractRulesFromCodeOwnersFile?(fileContent: string): FileOwnerRule[];
|
||||||
|
|
||||||
maxBodyLength(): number;
|
maxBodyLength(): number;
|
||||||
labelCharLimit?(): number;
|
labelCharLimit?(): number;
|
||||||
|
|
|
@ -2,12 +2,19 @@ import { codeBlock } from 'common-tags';
|
||||||
import { mock } from 'jest-mock-extended';
|
import { mock } from 'jest-mock-extended';
|
||||||
import { fs, git } from '../../../../../test/util';
|
import { fs, git } from '../../../../../test/util';
|
||||||
import type { Pr } from '../../../../modules/platform';
|
import type { Pr } from '../../../../modules/platform';
|
||||||
|
import { platform } from '../../../../modules/platform';
|
||||||
|
import * as gitlab from '../../../../modules/platform/gitlab';
|
||||||
import { codeOwnersForPr } from './code-owners';
|
import { codeOwnersForPr } from './code-owners';
|
||||||
|
|
||||||
jest.mock('../../../../util/fs');
|
jest.mock('../../../../util/fs');
|
||||||
jest.mock('../../../../util/git');
|
jest.mock('../../../../util/git');
|
||||||
|
jest.mock('../../../../modules/platform');
|
||||||
|
|
||||||
describe('workers/repository/update/pr/code-owners', () => {
|
describe('workers/repository/update/pr/code-owners', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
platform.extractRulesFromCodeOwnersFile = undefined;
|
||||||
|
});
|
||||||
|
|
||||||
describe('codeOwnersForPr', () => {
|
describe('codeOwnersForPr', () => {
|
||||||
let pr: Pr;
|
let pr: Pr;
|
||||||
|
|
||||||
|
@ -170,6 +177,102 @@ describe('workers/repository/update/pr/code-owners', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('supports Gitlab sections', () => {
|
||||||
|
beforeAll(() => {
|
||||||
|
platform.extractRulesFromCodeOwnersFile =
|
||||||
|
gitlab.extractRulesFromCodeOwnersFile;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns section code owner', async () => {
|
||||||
|
fs.readLocalFile.mockResolvedValueOnce(
|
||||||
|
['[team] @jimmy', '*'].join('\n'),
|
||||||
|
);
|
||||||
|
git.getBranchFiles.mockResolvedValueOnce(['README.md']);
|
||||||
|
|
||||||
|
const codeOwners = await codeOwnersForPr(pr);
|
||||||
|
|
||||||
|
expect(codeOwners).toEqual(['@jimmy']);
|
||||||
|
});
|
||||||
|
|
||||||
|
const codeOwnerFileWithDefaultApproval = codeBlock`
|
||||||
|
# Required for all files
|
||||||
|
* @general-approvers
|
||||||
|
|
||||||
|
[Documentation] @docs-team
|
||||||
|
docs/
|
||||||
|
README.md
|
||||||
|
*.txt
|
||||||
|
|
||||||
|
# Invalid section
|
||||||
|
Something before [Tests] @tests-team
|
||||||
|
tests/
|
||||||
|
|
||||||
|
# Optional section
|
||||||
|
^[Optional] @optional-team
|
||||||
|
optional/
|
||||||
|
|
||||||
|
[Database] @database-team
|
||||||
|
model/db/
|
||||||
|
config/db/database-setup.md @docs-team
|
||||||
|
`;
|
||||||
|
|
||||||
|
it('returns code owners of multiple sections', async () => {
|
||||||
|
fs.readLocalFile.mockResolvedValueOnce(
|
||||||
|
codeOwnerFileWithDefaultApproval,
|
||||||
|
);
|
||||||
|
git.getBranchFiles.mockResolvedValueOnce([
|
||||||
|
'config/db/database-setup.md',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const codeOwners = await codeOwnersForPr(pr);
|
||||||
|
|
||||||
|
expect(codeOwners).toEqual(['@docs-team', '@general-approvers']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns default owners when none is explicitly set', async () => {
|
||||||
|
fs.readLocalFile.mockResolvedValueOnce(
|
||||||
|
codeOwnerFileWithDefaultApproval,
|
||||||
|
);
|
||||||
|
git.getBranchFiles.mockResolvedValueOnce(['model/db/CHANGELOG.txt']);
|
||||||
|
|
||||||
|
const codeOwners = await codeOwnersForPr(pr);
|
||||||
|
|
||||||
|
expect(codeOwners).toEqual([
|
||||||
|
'@database-team',
|
||||||
|
'@docs-team',
|
||||||
|
'@general-approvers',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parses only sections that start at the beginning of a line', async () => {
|
||||||
|
fs.readLocalFile.mockResolvedValueOnce(
|
||||||
|
codeOwnerFileWithDefaultApproval,
|
||||||
|
);
|
||||||
|
git.getBranchFiles.mockResolvedValueOnce(['tests/setup.ts']);
|
||||||
|
|
||||||
|
const codeOwners = await codeOwnersForPr(pr);
|
||||||
|
|
||||||
|
expect(codeOwners).not.toInclude('@tests-team');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns code owners for optional sections', async () => {
|
||||||
|
fs.readLocalFile.mockResolvedValueOnce(
|
||||||
|
codeOwnerFileWithDefaultApproval,
|
||||||
|
);
|
||||||
|
git.getBranchFiles.mockResolvedValueOnce([
|
||||||
|
'optional/optional-file.txt',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const codeOwners = await codeOwnersForPr(pr);
|
||||||
|
|
||||||
|
expect(codeOwners).toEqual([
|
||||||
|
'@optional-team',
|
||||||
|
'@docs-team',
|
||||||
|
'@general-approvers',
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('does not require all files to match a single rule, regression test for #12611', async () => {
|
it('does not require all files to match a single rule, regression test for #12611', async () => {
|
||||||
fs.readLocalFile.mockResolvedValueOnce(
|
fs.readLocalFile.mockResolvedValueOnce(
|
||||||
codeBlock`
|
codeBlock`
|
||||||
|
|
|
@ -1,16 +1,15 @@
|
||||||
import is from '@sindresorhus/is';
|
import is from '@sindresorhus/is';
|
||||||
import ignore from 'ignore';
|
import ignore from 'ignore';
|
||||||
import { logger } from '../../../../logger';
|
import { logger } from '../../../../logger';
|
||||||
import type { Pr } from '../../../../modules/platform';
|
import type { FileOwnerRule, Pr } from '../../../../modules/platform';
|
||||||
|
import { platform } from '../../../../modules/platform';
|
||||||
import { readLocalFile } from '../../../../util/fs';
|
import { readLocalFile } from '../../../../util/fs';
|
||||||
import { getBranchFiles } from '../../../../util/git';
|
import { getBranchFiles } from '../../../../util/git';
|
||||||
import { newlineRegex, regEx } from '../../../../util/regex';
|
import { newlineRegex, regEx } from '../../../../util/regex';
|
||||||
|
|
||||||
interface FileOwnerRule {
|
interface FileOwnersScore {
|
||||||
usernames: string[];
|
file: string;
|
||||||
pattern: string;
|
userScoreMap: Map<string, number>;
|
||||||
score: number;
|
|
||||||
match: (path: string) => boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractOwnersFromLine(line: string): FileOwnerRule {
|
function extractOwnersFromLine(line: string): FileOwnerRule {
|
||||||
|
@ -24,11 +23,6 @@ function extractOwnersFromLine(line: string): FileOwnerRule {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FileOwnersScore {
|
|
||||||
file: string;
|
|
||||||
userScoreMap: Map<string, number>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function matchFileToOwners(
|
function matchFileToOwners(
|
||||||
file: string,
|
file: string,
|
||||||
rules: FileOwnerRule[],
|
rules: FileOwnerRule[],
|
||||||
|
@ -75,6 +69,22 @@ function getOwnerList(filesWithOwners: FileOwnersScore[]): OwnerFileScore[] {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractRulesFromCodeOwnersFile(
|
||||||
|
codeOwnersFile: string,
|
||||||
|
): FileOwnerRule[] {
|
||||||
|
return (
|
||||||
|
codeOwnersFile
|
||||||
|
.split(newlineRegex)
|
||||||
|
// Remove comments
|
||||||
|
.map((line) => line.split('#')[0])
|
||||||
|
// Remove empty lines
|
||||||
|
.map((line) => line.trim())
|
||||||
|
.filter(is.nonEmptyString)
|
||||||
|
// Extract pattern & usernames
|
||||||
|
.map(extractOwnersFromLine)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export async function codeOwnersForPr(pr: Pr): Promise<string[]> {
|
export async function codeOwnersForPr(pr: Pr): Promise<string[]> {
|
||||||
logger.debug('Searching for CODEOWNERS file');
|
logger.debug('Searching for CODEOWNERS file');
|
||||||
try {
|
try {
|
||||||
|
@ -102,15 +112,9 @@ export async function codeOwnersForPr(pr: Pr): Promise<string[]> {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert CODEOWNERS file into list of matching rules
|
// Convert CODEOWNERS file into list of matching rules
|
||||||
const fileOwnerRules = codeOwnersFile
|
const fileOwnerRules = platform.extractRulesFromCodeOwnersFile
|
||||||
.split(newlineRegex)
|
? platform.extractRulesFromCodeOwnersFile(codeOwnersFile)
|
||||||
// Remove comments
|
: extractRulesFromCodeOwnersFile(codeOwnersFile);
|
||||||
.map((line) => line.split('#')[0])
|
|
||||||
// Remove empty lines
|
|
||||||
.map((line) => line.trim())
|
|
||||||
.filter(is.nonEmptyString)
|
|
||||||
// Extract pattern & usernames
|
|
||||||
.map(extractOwnersFromLine);
|
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
{ prFiles, fileOwnerRules },
|
{ prFiles, fileOwnerRules },
|
||||||
|
|
Loading…
Reference in a new issue