Files
renovate[bot] 9d74625d93 build(deps): update dependency prettier to v3.9.6 (main) (#45815)
* 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>
2026-09-09 13:50:03 +00:00

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(/&#8203;/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([]);
}