mirror of
https://github.com/renovatebot/renovate.git
synced 2026-11-02 02:29:51 +00:00
9d74625d93
* build(deps): update dependency prettier to v3.9.6 | datasource | package | from | to | | ---------- | -------- | ----- | ----- | | npm | prettier | 3.8.1 | 3.9.6 | * chore: prettier fixes --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Michael Kriese <michael.kriese@mend.io>
603 lines
16 KiB
TypeScript
603 lines
16 KiB
TypeScript
import { isTruthy, isUndefined } from '@sindresorhus/is';
|
|
import semver from 'semver';
|
|
import { logger } from '../../../logger/index.ts';
|
|
import type { BranchStatus } from '../../../types/index.ts';
|
|
import { parseJson } from '../../../util/common.ts';
|
|
import { getEnv } from '../../../util/env.ts';
|
|
import * as git from '../../../util/git/index.ts';
|
|
import type { VirtualBranch } from '../../../util/git/types.ts';
|
|
import { setBaseUrl } from '../../../util/http/gerrit.ts';
|
|
import { coerceObject } from '../../../util/object.ts';
|
|
import { regEx } from '../../../util/regex.ts';
|
|
import { toLongCommitSha } from '../../../util/schema-utils/git.ts';
|
|
import { ensureTrailingSlash } from '../../../util/url.ts';
|
|
import type {
|
|
BranchStatusConfig,
|
|
CreatePRConfig,
|
|
EnsureCommentConfig,
|
|
EnsureCommentRemovalConfigByContent,
|
|
EnsureCommentRemovalConfigByTopic,
|
|
EnsureIssueConfig,
|
|
EnsureIssueResult,
|
|
FindPRConfig,
|
|
Issue,
|
|
MergePRConfig,
|
|
PlatformParams,
|
|
PlatformResult,
|
|
Pr,
|
|
RepoParams,
|
|
RepoResult,
|
|
UpdatePrConfig,
|
|
} from '../types.ts';
|
|
import { repoFingerprint } from '../util.ts';
|
|
import { smartTruncate } from '../utils/pr-body.ts';
|
|
import { readOnlyIssueBody } from '../utils/read-only-issue-body.ts';
|
|
import { client } from './client.ts';
|
|
import type { GerritLabels, GerritProjectInfo } from './schema.ts';
|
|
import { configureScm, pushForReview } from './scm.ts';
|
|
import {
|
|
MAX_GERRIT_COMMENT_SIZE,
|
|
REQUEST_DETAILS_FOR_PRS,
|
|
TAG_PULL_REQUEST_BODY,
|
|
extractSourceBranch,
|
|
getGerritRepoUrl,
|
|
mapBranchStatusToLabel,
|
|
mapGerritChangeToPr,
|
|
} from './utils.ts';
|
|
|
|
export const id = 'gerrit';
|
|
export const experimental = true;
|
|
|
|
const defaults: {
|
|
endpoint?: string;
|
|
} = {};
|
|
|
|
let config: {
|
|
repository?: string;
|
|
head?: string;
|
|
config?: GerritProjectInfo;
|
|
labels: GerritLabels;
|
|
gerritUsername?: string;
|
|
} = {
|
|
labels: {},
|
|
};
|
|
|
|
export function writeToConfig(newConfig: typeof config): void {
|
|
config = { ...config, ...newConfig };
|
|
}
|
|
|
|
export async function initPlatform({
|
|
endpoint,
|
|
username,
|
|
password,
|
|
}: PlatformParams): Promise<PlatformResult> {
|
|
logger.debug(`initPlatform(${endpoint!}, ${username!})`);
|
|
if (!endpoint) {
|
|
throw new Error('Init: You must configure a Gerrit Server endpoint');
|
|
}
|
|
if (!(username && password)) {
|
|
throw new Error(
|
|
'Init: You must configure a Gerrit Server username/password',
|
|
);
|
|
}
|
|
config.gerritUsername = username;
|
|
defaults.endpoint = ensureTrailingSlash(endpoint);
|
|
setBaseUrl(defaults.endpoint);
|
|
|
|
let gerritVersion: string;
|
|
try {
|
|
const env = getEnv();
|
|
/* v8 ignore if: experimental feature */
|
|
if (env.RENOVATE_X_PLATFORM_VERSION) {
|
|
gerritVersion = env.RENOVATE_X_PLATFORM_VERSION;
|
|
} else {
|
|
gerritVersion = await client.getGerritVersion({
|
|
username,
|
|
password,
|
|
});
|
|
}
|
|
} catch (err) {
|
|
logger.debug(
|
|
{ err },
|
|
'Error authenticating with Gerrit. Check your credentials',
|
|
);
|
|
throw new Error('Init: Authentication failure');
|
|
}
|
|
|
|
logger.debug(`Gerrit version is: ${gerritVersion}`);
|
|
// Example: 3.13.0-rc3-148-gb478dbbb57
|
|
const parsed = semver.parse(gerritVersion);
|
|
if (!parsed) {
|
|
throw new Error(`Unable to parse Gerrit version: ${gerritVersion}`);
|
|
}
|
|
gerritVersion = `${parsed.major}.${parsed.minor}.${parsed.patch}`;
|
|
client.setGerritVersion(gerritVersion);
|
|
|
|
const platformConfig: PlatformResult = {
|
|
endpoint: defaults.endpoint,
|
|
};
|
|
return platformConfig;
|
|
}
|
|
|
|
/**
|
|
* Get all state="ACTIVE" and type="CODE" repositories from gerrit
|
|
*/
|
|
export async function getRepos(): Promise<string[]> {
|
|
logger.debug(`getRepos()`);
|
|
return await client.getRepos();
|
|
}
|
|
|
|
/**
|
|
* Clone repository to local directory
|
|
* @param config
|
|
*/
|
|
export async function initRepo({
|
|
repository,
|
|
cloneSubmodules,
|
|
cloneSubmodulesFilter,
|
|
gitUrl,
|
|
}: RepoParams): Promise<RepoResult> {
|
|
logger.debug(`initRepo(${repository}, ${gitUrl})`);
|
|
const projectInfo = await client.getProjectInfo(repository);
|
|
const branchInfo = await client.getBranchInfo(repository);
|
|
|
|
config = {
|
|
...config,
|
|
repository,
|
|
head: branchInfo.revision,
|
|
config: projectInfo,
|
|
labels: coerceObject(projectInfo.labels),
|
|
};
|
|
|
|
//abandon "open" and "rejected" changes at startup
|
|
const rejectedChanges = await client.findChanges(repository, {
|
|
branchName: '',
|
|
state: 'open',
|
|
label: '-2',
|
|
});
|
|
for (const change of rejectedChanges) {
|
|
await client.abandonChange(
|
|
change._number,
|
|
'This change has been abandoned as it was voted with Code-Review -2.',
|
|
);
|
|
logger.info(
|
|
`Abandoned change ${change._number} with Code-Review -2 in repository ${repository}`,
|
|
);
|
|
}
|
|
|
|
// Collect open Gerrit changes to initialize as virtual branches.
|
|
// This allows DefaultGitScm to treat them as regular Git branches.
|
|
const openChanges = await client.findChanges(repository, {
|
|
branchName: '',
|
|
state: 'open',
|
|
requestDetails: ['CURRENT_REVISION', 'COMMIT_FOOTERS'],
|
|
});
|
|
const virtualBranches: Record<string, VirtualBranch> = {};
|
|
for (const change of openChanges) {
|
|
const branchName = extractSourceBranch(change);
|
|
if (!branchName) {
|
|
continue;
|
|
}
|
|
const sha = toLongCommitSha(change.current_revision);
|
|
const ref = change.revisions![sha].ref;
|
|
virtualBranches[branchName] = { ref, sha };
|
|
}
|
|
|
|
const virtualBranchCount = Object.keys(virtualBranches).length;
|
|
if (virtualBranchCount > 0) {
|
|
logger.debug(
|
|
`Will fetch ${virtualBranchCount} Gerrit changes as virtual branches`,
|
|
);
|
|
}
|
|
|
|
const baseUrl = defaults.endpoint!;
|
|
const url = getGerritRepoUrl(
|
|
repository,
|
|
baseUrl,
|
|
gitUrl,
|
|
config.gerritUsername!,
|
|
);
|
|
configureScm(repository, config.labels);
|
|
await git.initRepo({
|
|
url,
|
|
cloneSubmodules,
|
|
cloneSubmodulesFilter,
|
|
virtualBranches,
|
|
});
|
|
|
|
const repoConfig: RepoResult = {
|
|
defaultBranch: config.head!,
|
|
isFork: false,
|
|
repoFingerprint: repoFingerprint(repository, baseUrl),
|
|
};
|
|
return repoConfig;
|
|
}
|
|
|
|
export async function findPr(findPRConfig: FindPRConfig): Promise<Pr | null> {
|
|
const change = (
|
|
await client.findChanges(config.repository!, {
|
|
...findPRConfig,
|
|
singleChange: true,
|
|
requestDetails: REQUEST_DETAILS_FOR_PRS,
|
|
})
|
|
).pop();
|
|
return change
|
|
? mapGerritChangeToPr(change, {
|
|
sourceBranch: findPRConfig.branchName,
|
|
})
|
|
: null;
|
|
}
|
|
|
|
export async function getPr(number: number): Promise<Pr | null> {
|
|
try {
|
|
const change = await client.getChange(number, REQUEST_DETAILS_FOR_PRS);
|
|
return mapGerritChangeToPr(change);
|
|
} catch (err) {
|
|
if (err.statusCode === 404) {
|
|
return null;
|
|
}
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
export async function updatePr(prConfig: UpdatePrConfig): Promise<void> {
|
|
logger.debug(`updatePr(${prConfig.number}, ${prConfig.prTitle})`);
|
|
if (prConfig.prBody) {
|
|
await client.addMessageIfNotAlreadyExists(
|
|
prConfig.number,
|
|
prConfig.prBody,
|
|
TAG_PULL_REQUEST_BODY,
|
|
);
|
|
}
|
|
await client.setHashtags(prConfig.number, {
|
|
add: prConfig.addLabels,
|
|
remove: prConfig.removeLabels,
|
|
});
|
|
if (prConfig.targetBranch) {
|
|
await client.moveChange(prConfig.number, prConfig.targetBranch);
|
|
}
|
|
if (prConfig.state && prConfig.state === 'closed') {
|
|
await client.abandonChange(prConfig.number);
|
|
}
|
|
}
|
|
|
|
export async function createPr(prConfig: CreatePRConfig): Promise<Pr | null> {
|
|
logger.debug(
|
|
`createPr(${prConfig.sourceBranch}, ${prConfig.prTitle}, ${
|
|
prConfig.labels?.toString() ?? ''
|
|
})`,
|
|
);
|
|
|
|
logger.debug(
|
|
`Pushing commit to refs/for/${prConfig.targetBranch} to create Gerrit change`,
|
|
);
|
|
const pushResult = await pushForReview({
|
|
sourceRef: prConfig.sourceBranch,
|
|
targetBranch: prConfig.targetBranch,
|
|
files: [],
|
|
autoApprove: prConfig.platformPrOptions?.autoApprove,
|
|
labels: prConfig.labels ?? undefined,
|
|
});
|
|
|
|
if (!pushResult) {
|
|
throw new Error(
|
|
`Failed to push commit to refs/for/${prConfig.targetBranch} to create Gerrit change`,
|
|
);
|
|
}
|
|
|
|
const change = (
|
|
await client.findChanges(config.repository!, {
|
|
branchName: prConfig.sourceBranch,
|
|
targetBranch: prConfig.targetBranch,
|
|
state: 'open',
|
|
singleChange: true,
|
|
requestDetails: REQUEST_DETAILS_FOR_PRS,
|
|
})
|
|
).pop();
|
|
if (isUndefined(change)) {
|
|
throw new Error(
|
|
`Could not find the Gerrit change after pushing to refs/for/${prConfig.targetBranch}`,
|
|
);
|
|
}
|
|
|
|
// Register the virtual branch now that the change exists and we have the ref.
|
|
const sha = toLongCommitSha(change.current_revision);
|
|
const ref = change.revisions![sha].ref;
|
|
await git.setVirtualBranch(prConfig.sourceBranch, ref, sha);
|
|
|
|
await client.addMessage(
|
|
change._number,
|
|
prConfig.prBody,
|
|
TAG_PULL_REQUEST_BODY,
|
|
);
|
|
return mapGerritChangeToPr(change, {
|
|
sourceBranch: prConfig.sourceBranch,
|
|
prBody: prConfig.prBody,
|
|
});
|
|
}
|
|
|
|
export async function getBranchPr(
|
|
branchName: string,
|
|
targetBranch?: string,
|
|
): Promise<Pr | null> {
|
|
const change = await client.getBranchChange(config.repository!, {
|
|
branchName,
|
|
state: 'open',
|
|
targetBranch,
|
|
requestDetails: REQUEST_DETAILS_FOR_PRS,
|
|
});
|
|
|
|
return change
|
|
? mapGerritChangeToPr(change, {
|
|
sourceBranch: branchName,
|
|
})
|
|
: null;
|
|
}
|
|
|
|
export async function getPrList(): Promise<Pr[]> {
|
|
const changes = await client.findChanges(config.repository!, {
|
|
branchName: '',
|
|
requestDetails: REQUEST_DETAILS_FOR_PRS,
|
|
});
|
|
return changes.map((change) => mapGerritChangeToPr(change)).filter(isTruthy);
|
|
}
|
|
|
|
export async function mergePr(config: MergePRConfig): Promise<boolean> {
|
|
logger.debug(
|
|
`mergePr(${config.id}, ${config.branchName!}, ${config.strategy!})`,
|
|
);
|
|
try {
|
|
const change = await client.submitChange(config.id);
|
|
return change.status === 'MERGED';
|
|
} catch (err) {
|
|
if (err.statusCode === 409) {
|
|
logger.warn(
|
|
{ err },
|
|
"Can't submit the change, because the submit rule doesn't allow it.",
|
|
);
|
|
return false;
|
|
}
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* BranchStatus for Gerrit assumes that the branchName refers to a change.
|
|
* @param branchName
|
|
*/
|
|
export async function getBranchStatus(
|
|
branchName: string,
|
|
): Promise<BranchStatus> {
|
|
logger.debug(`getBranchStatus(${branchName})`);
|
|
const change = (
|
|
await client.findChanges(config.repository!, {
|
|
state: 'open',
|
|
branchName,
|
|
singleChange: true,
|
|
requestDetails: ['LABELS', 'SUBMITTABLE', 'CHECK'],
|
|
})
|
|
).pop();
|
|
if (change) {
|
|
const hasProblems = change.problems && change.problems.length > 0;
|
|
if (hasProblems) {
|
|
return 'red';
|
|
}
|
|
const hasBlockingLabels = Object.values(coerceObject(change.labels)).some(
|
|
(label) => label.blocking,
|
|
);
|
|
if (hasBlockingLabels) {
|
|
return 'red';
|
|
}
|
|
// v8 ignore else -- TODO: add test #40625
|
|
if (change.submittable) {
|
|
return 'green';
|
|
}
|
|
}
|
|
return 'yellow';
|
|
}
|
|
|
|
/**
|
|
* check the gerrit-change for the presence of the corresponding "$context" Gerrit label if configured,
|
|
* return 'yellow' if not configured or not set
|
|
* @param branchName
|
|
* @param context renovate/stability-days || ...
|
|
*/
|
|
export async function getBranchStatusCheck(
|
|
branchName: string,
|
|
context: string,
|
|
): Promise<BranchStatus | null> {
|
|
const labelConfig = config.labels[context];
|
|
if (labelConfig) {
|
|
const change = (
|
|
await client.findChanges(config.repository!, {
|
|
branchName,
|
|
state: 'open',
|
|
singleChange: true,
|
|
requestDetails: ['LABELS'],
|
|
})
|
|
).pop();
|
|
// v8 ignore else -- TODO: add test #40625
|
|
if (change) {
|
|
const label = change.labels![context];
|
|
// v8 ignore else -- TODO: add test #40625
|
|
if (label) {
|
|
// Check for rejected or blocking first, as a label could have both rejected and approved
|
|
if (label.rejected || label.blocking) {
|
|
return 'red';
|
|
}
|
|
// v8 ignore else -- TODO: add test #40625
|
|
if (label.approved) {
|
|
return 'green';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return 'yellow';
|
|
}
|
|
|
|
/**
|
|
* Apply the branch state $context to the corresponding gerrit label (if available)
|
|
* context === "renovate/stability-days" / "renovate/merge-confidence" and state === "green"/...
|
|
* @param branchStatusConfig
|
|
*/
|
|
export async function setBranchStatus(
|
|
branchStatusConfig: BranchStatusConfig,
|
|
): Promise<void> {
|
|
const label = config.labels[branchStatusConfig.context];
|
|
const labelValue =
|
|
label && mapBranchStatusToLabel(branchStatusConfig.state, label);
|
|
if (branchStatusConfig.context && labelValue) {
|
|
const change = (
|
|
await client.findChanges(config.repository!, {
|
|
branchName: branchStatusConfig.branchName,
|
|
state: 'open',
|
|
singleChange: true,
|
|
requestDetails: ['LABELS'],
|
|
})
|
|
).pop();
|
|
|
|
const labelKey = branchStatusConfig.context;
|
|
if (!change?.labels || !Object.hasOwn(change.labels, labelKey)) {
|
|
return;
|
|
}
|
|
|
|
await client.setLabel(change._number, labelKey, labelValue);
|
|
}
|
|
}
|
|
|
|
export async function getRawFile(
|
|
fileName: string,
|
|
repoName?: string,
|
|
branchOrTag?: string,
|
|
): Promise<string | null> {
|
|
const repo = repoName ?? config.repository;
|
|
if (!repo) {
|
|
logger.debug('No repo so cannot getRawFile');
|
|
return null;
|
|
}
|
|
// v8 ignore next -- TODO: add test #40625
|
|
const branch =
|
|
branchOrTag ??
|
|
(repo === config.repository ? (config.head ?? 'HEAD') : 'HEAD');
|
|
const result = await client.getFile(repo, branch, fileName);
|
|
return result;
|
|
}
|
|
|
|
export async function getJsonFile(
|
|
fileName: string,
|
|
repoName?: string,
|
|
branchOrTag?: string,
|
|
): Promise<any> {
|
|
const raw = await getRawFile(fileName, repoName, branchOrTag);
|
|
return parseJson(raw, fileName);
|
|
}
|
|
|
|
export async function addReviewers(
|
|
number: number,
|
|
reviewers: string[],
|
|
): Promise<void> {
|
|
await client.addReviewers(number, reviewers);
|
|
}
|
|
|
|
/**
|
|
* add "CC" (only one possible)
|
|
*/
|
|
export async function addAssignees(
|
|
number: number,
|
|
assignees: string[],
|
|
): Promise<void> {
|
|
// v8 ignore else -- TODO: add test #40625
|
|
if (assignees.length) {
|
|
// v8 ignore else -- TODO: add test #40625
|
|
if (assignees.length > 1) {
|
|
logger.debug(
|
|
`addAssignees(${number}, ${assignees.toString()}) called with more then one assignee! Gerrit only supports one assignee! Using the first from list.`,
|
|
);
|
|
}
|
|
await client.addAssignee(number, assignees[0]);
|
|
}
|
|
}
|
|
|
|
export async function ensureComment(
|
|
ensureComment: EnsureCommentConfig,
|
|
): Promise<boolean> {
|
|
logger.debug(
|
|
`ensureComment(${ensureComment.number}, ${ensureComment.topic!}, ${
|
|
ensureComment.content
|
|
})`,
|
|
);
|
|
await client.addMessageIfNotAlreadyExists(
|
|
ensureComment.number,
|
|
ensureComment.content,
|
|
ensureComment.topic ?? undefined,
|
|
);
|
|
return true;
|
|
}
|
|
|
|
export function massageMarkdown(prBody: string, rebaseLabel: string): string {
|
|
return (
|
|
smartTruncate(readOnlyIssueBody(prBody), maxBodyLength())
|
|
.replace('Branch creation', 'Change creation')
|
|
.replace(
|
|
'close this Pull Request unmerged',
|
|
'abandon or vote this change with Code-Review -2',
|
|
)
|
|
.replace(
|
|
'Close this PR',
|
|
'Abandon or vote this change with Code-Review -2',
|
|
)
|
|
.replace(
|
|
'you tick the rebase/retry checkbox',
|
|
`you add the _${rebaseLabel}_ hashtag to this change`,
|
|
)
|
|
.replace(
|
|
'checking the rebase/retry box above',
|
|
`adding the _${rebaseLabel}_ hashtag to this change`,
|
|
)
|
|
.replace(regEx(/\b(?:Pull Request|PR)/g), 'change')
|
|
// Remove HTML tags not supported in Gerrit markdown
|
|
.replace(regEx(/<\/?summary>/g), '**')
|
|
.replace(regEx(/<\/?(?:details|blockquote)>/g), '')
|
|
.replace(regEx(`\n---\n\n.*?<!-- rebase-check -->.*?\n`), '')
|
|
.replace(regEx(/<!--renovate-(?:debug|config-hash):.*?-->/g), '')
|
|
// Remove zero-width-space not supported in Gerrit markdown
|
|
.replace(regEx(/​/g), '')
|
|
);
|
|
}
|
|
|
|
export function maxBodyLength(): number {
|
|
return MAX_GERRIT_COMMENT_SIZE;
|
|
}
|
|
|
|
export async function deleteLabel(
|
|
number: number,
|
|
label: string,
|
|
): Promise<void> {
|
|
await client.setHashtags(number, { remove: [label] });
|
|
}
|
|
|
|
export function ensureCommentRemoval(
|
|
_ensureCommentRemoval:
|
|
EnsureCommentRemovalConfigByTopic | EnsureCommentRemovalConfigByContent,
|
|
): Promise<void> {
|
|
return Promise.resolve();
|
|
}
|
|
|
|
export function ensureIssueClosing(_title: string): Promise<void> {
|
|
return Promise.resolve();
|
|
}
|
|
|
|
export function ensureIssue(
|
|
_issueConfig: EnsureIssueConfig,
|
|
): Promise<EnsureIssueResult | null> {
|
|
return Promise.resolve(null);
|
|
}
|
|
|
|
export function findIssue(_title: string): Promise<Issue | null> {
|
|
return Promise.resolve(null);
|
|
}
|
|
|
|
export function getIssueList(): Promise<Issue[]> {
|
|
return Promise.resolve([]);
|
|
}
|