Files
Dharma Teja Thoutam 40fce9aba7 fix(gerrit): resolve relative submodule URLs for nested project paths (#44667)
* fix(gerrit): resolve relative submodule URLs for nested project paths

* Fix tests after rebase

---------

Co-authored-by: Felipe Santos <felipe.santos@ericsson.com>
2026-09-03 12:41:31 +00:00

183 lines
5.4 KiB
TypeScript

import { CONFIG_GIT_URL_UNAVAILABLE } from '../../../constants/error-messages.ts';
import { logger } from '../../../logger/index.ts';
import type { BranchStatus, PrState } from '../../../types/index.ts';
import { coerceArray } from '../../../util/array.ts';
import * as hostRules from '../../../util/host-rules.ts';
import { regEx } from '../../../util/regex.ts';
import { toLongCommitSha } from '../../../util/schema-utils/git.ts';
import { joinUrlParts, parseUrl } from '../../../util/url.ts';
import { hashBody } from '../pr-body.ts';
import type { GitUrlOption, Pr } from '../types.ts';
import type { GerritChange, GerritLabelTypeInfo } from './schema.ts';
import type { GerritChangeStatus, GerritRequestDetail } from './types.ts';
export const MIN_GERRIT_VERSION = '3.0.0';
export const TAG_PULL_REQUEST_BODY = 'pull-request';
const DEFAULT_SSH_PORT = `29418`;
/**
* Max comment size in Gerrit (16kiB by default)
* https://gerrit-review.googlesource.com/Documentation/config-gerrit.html#change:~:text=change.commentSizeLimit
*/
export const MAX_GERRIT_COMMENT_SIZE = 16 * 1024;
export const REQUEST_DETAILS_FOR_PRS: GerritRequestDetail[] = [
'MESSAGES', // to get the pr body
'LABELS', // to get the reviewers
'DETAILED_ACCOUNTS', // to get the reviewers usernames
'CURRENT_REVISION', // to get the commit message
'COMMIT_FOOTERS', // to get the commit message
] as const;
export function getGerritRepoUrl(
repository: string,
endpoint: string,
gitUrl: GitUrlOption | undefined,
username: string,
): string {
const endpointUrl = parseUrl(endpoint);
if (!endpointUrl) {
throw new Error(CONFIG_GIT_URL_UNAVAILABLE);
}
const url =
gitUrl === 'ssh'
? createSshUrl(endpointUrl, repository, username)
: createHttpUrl(endpointUrl, endpoint, repository);
logger.trace({ url }, 'using URL based on configured endpoint');
return url;
}
function createSshUrl(url: URL, repository: string, username: string): string {
return `ssh://${username}@${url.hostname}:${DEFAULT_SSH_PORT}/${repository}`;
}
function createHttpUrl(url: URL, endpoint: string, repository: string): string {
// Find options for current host and determine Git endpoint
const opts = hostRules.find({
hostType: 'gerrit',
url: endpoint,
});
if (!(opts.username && opts.password)) {
throw new Error(
'Init: You must configure a Gerrit Server username/password',
);
}
url.username = opts.username;
url.password = opts.password;
url.pathname = joinUrlParts(url.pathname, 'a', repository);
return url.toString();
}
export function mapPrStateToGerritFilter(state?: PrState): string | null {
switch (state) {
case 'merged':
return 'status:merged';
case 'open':
return 'status:open';
case 'closed':
return 'status:abandoned';
case '!open':
return '-status:open';
case 'all':
default:
return null;
}
}
export function mapGerritChangeToPr(
change: GerritChange,
knownProperties?: {
sourceBranch?: string;
prBody?: string;
},
): Pr | null {
const sourceBranch =
knownProperties?.sourceBranch ?? extractSourceBranch(change);
if (!sourceBranch) {
return null;
}
return {
number: change._number,
state: mapGerritChangeStateToPrState(change.status),
sourceBranch,
targetBranch: change.branch,
title: change.subject,
createdAt: convertGerritDateToISO(change.created),
labels: change.hashtags,
reviewers: coerceArray(
change.reviewers?.REVIEWER?.map((reviewer) => reviewer.username!),
),
bodyStruct: {
hash: hashBody(knownProperties?.prBody ?? findPullRequestBody(change)),
},
sha: toLongCommitSha(change.current_revision),
};
}
export function mapGerritChangeStateToPrState(
state: GerritChangeStatus,
): 'merged' | 'open' | 'closed' {
switch (state) {
case 'NEW':
return 'open';
case 'MERGED':
return 'merged';
case 'ABANDONED':
return 'closed';
}
}
export function extractSourceBranch(change: GerritChange): string | undefined {
let sourceBranch: string | undefined = undefined;
if (change.current_revision) {
const re = regEx(/^Renovate-Branch: (?<branch>.+)$/m);
const currentRevision = change.revisions![change.current_revision];
const message = currentRevision.commit_with_footers;
// v8 ignore else -- TODO: add test #40625
if (message) {
sourceBranch = re.exec(message)?.groups?.branch;
}
}
return sourceBranch ?? undefined;
}
export function findPullRequestBody(change: GerritChange): string | undefined {
const msg = Array.from(coerceArray(change.messages))
.reverse()
.find((msg) => msg.tag === TAG_PULL_REQUEST_BODY);
if (msg) {
// Gerrit adds a "Patch Set X:" prefix to comments
return msg.message.replace(regEx(/^Patch Set \d+:\n\n/), '');
}
return undefined;
}
export function mapBranchStatusToLabel(
state: BranchStatus | 'UNKNOWN', // suppress default path code removal
label: GerritLabelTypeInfo,
): number {
const numbers = Object.keys(label.values).map((x) => parseInt(x, 10));
switch (state) {
case 'green':
return Math.max(...numbers);
case 'yellow':
case 'red':
return Math.min(...numbers);
}
/* v8 ignore next -- only reachable for the artificial 'UNKNOWN' state, which callers never pass */
return label.default_value;
}
// Convert Gerrit date format to ISO format
export function convertGerritDateToISO(date: string): string {
return date.replace(' ', 'T');
}