mirror of
https://github.com/renovatebot/renovate.git
synced 2026-10-31 01:37:24 +00:00
44160755f9
* refactor(platform): share one implementation between gitea and forgejo The `forgejo` platform was copied from `gitea` in #37075 and the two have drifted since: fixes landed on one side only (zod validation #43900, empty email #44068, null issues #44048, team reviewers #39533, merge-style fallback #37768 vs #38040), and lint sweeps had to be applied to both copies by hand. Both platforms now build on `lib/modules/platform/gitea-common/`, which holds the zod schemas, API helpers, utils, PR cache and the platform object itself. `createPlatform()` closes over its own state, so `gitea` and `forgejo` keep separate config, caches and host types. `gitea/index.ts` and `forgejo/index.ts` are thin wrappers that supply the platform id, default endpoint, http client and the version gates that genuinely differ. Reconciliations, forgejo's behaviour adopted for gitea: - Responses are validated with zod instead of `getJsonUnchecked` casts. Gitea therefore tolerates empty emails, null PRs in list responses and lenient repo/issue payloads the same way Forgejo already did. - `getRepoContents` returns a discriminated union, so `getRawFile` returns null for dir/symlink/submodule entries instead of reading a missing field. The gitea preset resolver now throws PRESET_INVALID for non-file entries, like the forgejo one. - An unknown `default_merge_style` (e.g. `manually-merged`) is caught by the schema and skipped, instead of aborting the repository with REPOSITORY_BLOCKED from `isAllowed()`. - `initRepo()` resolves whether the repo owner is an org and only fetches org labels when it is, adding one `/orgs/{owner}` request and an `orgs` entry in the gitea repository cache. - Reviewers prefixed with `team:` are sent as `team_reviewers`. Gitea keeps its `v1.14.0` guard for requesting reviewers at all. - `getBranchStatusCheck()` maps unknown commit status values through the schema's `unknown` fallback rather than warning and returning yellow. Gitea's behaviour is kept where it is deliberately different: the Forgejo instance detection and its deprecation notice, the `1.24.0` (or `10.0.0` for a detected Forgejo) native automerge gate, and the reviewer version guard are passed in as options. Two shared log messages became static so they can be grouped in metrics: `{platform}-native automerge: fail` is now `Platform-native automerge: fail` and `{Platform} getRepos() error` is now `Error auto-discovering repositories`, both with the platform id in the metadata. The dead `defaults.hostType` field is gone. * refactor(platform): keep the shared gitea implementation under gitea/ Drop the `gitea-common/` folder. The zod schemas, API helpers, utils, PR cache and `createPlatform()` live in `lib/modules/platform/gitea/` under their original file names, and `forgejo/` imports them from there. The preset resolvers pass `giteaHttp` and `forgejoHttp` to `getRepoContents()` directly, which removes the two bound wrappers. The gitea changelog source uses the shared `RepoContents` type like the forgejo one already does. * refactor(http): derive ForgejoHttp from GiteaHttp The two clients were identical apart from the default `hostType`. `ForgejoHttp` now extends `GiteaHttp` and only keeps its own base url, so the pagination logic exists once. The platform helpers accept `GiteaHttp` directly instead of the `HttpBase` alias. Also covers the `updatePr()` branch where the updated pull request cannot be converted and therefore is not cached. * refactor(platform): create the gitea and forgejo http clients in index.ts `forgejo-helper.ts` only held the `ForgejoHttp` instance, so it is gone and `forgejo/index.ts` creates the client next to the platform. Gitea does the same for symmetry, which leaves `gitea-helper.ts` with only the shared API helpers. * refactor(platform): fetch gitea labels and issues with await `getLabelList()` and `getIssueList()` still memoize the in-flight promise, but build it from small async functions instead of `.then()` and `.catch()` chains. * refactor(platform): keep declaration order of gitea/index.ts Order the module the way it was before `createPlatform()` so the diff against main aligns: platform id and http client after the repo config interface, pure helpers before the factory, and `maxBodyLength()` after the platform object. * refactor(platform): move gitea label fetching to labels.ts `fetchLabelList()` takes the http client and the repository details instead of closing over the platform state, and gets its own spec. The factory only keeps the memoizing `getLabelList()`. * refactor(platform): move gitea label lookup to labels.ts * refactor(platform): create the gitea pr cache per platform `GiteaPrCache` is now created once per platform with its http client and platform id, and `initRepo()` records the repository. The per-repository state moves to a private `RepoPrCache` which is bound to the repository cache on first use and reused until the next `initRepo()`, instead of being rebuilt on every lookup.
194 lines
5.1 KiB
TypeScript
194 lines
5.1 KiB
TypeScript
import { isNonEmptyArray } from '@sindresorhus/is';
|
|
import type { MergeStrategy } from '../../../config/types.ts';
|
|
import { CONFIG_GIT_URL_UNAVAILABLE } from '../../../constants/error-messages.ts';
|
|
import { logger } from '../../../logger/index.ts';
|
|
import { coerceArray } from '../../../util/array.ts';
|
|
import * as hostRules from '../../../util/host-rules.ts';
|
|
import { regEx } from '../../../util/regex.ts';
|
|
import { parseUrl } from '../../../util/url.ts';
|
|
import { getPrBodyStruct } from '../pr-body.ts';
|
|
import type { GitUrlOption, Pr } from '../types.ts';
|
|
import type { PR, PRMergeMethod, Repo } from './schema.ts';
|
|
import type { GiteaPlatformKey } from './types.ts';
|
|
|
|
export function smartLinks(body: string): string {
|
|
return body
|
|
?.replace(regEx(/\]\(\.\.\/issues\//g), '](issues/')
|
|
.replace(regEx(/\]\(\.\.\/pull\//g), '](pulls/');
|
|
}
|
|
|
|
export function trimTrailingApiPath(url: string): string {
|
|
return url?.replace(regEx(/api\/v1\/?$/g), '');
|
|
}
|
|
|
|
export function getRepoUrl(
|
|
repo: Repo,
|
|
gitUrl: GitUrlOption | undefined,
|
|
endpoint: string,
|
|
hostType: string,
|
|
): string {
|
|
if (gitUrl === 'ssh') {
|
|
if (!repo.ssh_url) {
|
|
throw new Error(CONFIG_GIT_URL_UNAVAILABLE);
|
|
}
|
|
logger.debug(`Using SSH URL: ${repo.ssh_url}`);
|
|
return repo.ssh_url;
|
|
}
|
|
|
|
// Find options for current host and determine Git endpoint
|
|
const opts = hostRules.find({
|
|
hostType,
|
|
url: endpoint,
|
|
});
|
|
|
|
if (gitUrl === 'endpoint') {
|
|
const url = parseUrl(endpoint);
|
|
if (!url) {
|
|
throw new Error(CONFIG_GIT_URL_UNAVAILABLE);
|
|
}
|
|
url.username = opts.token ?? '';
|
|
url.pathname = `${url.pathname}${repo.full_name}.git`;
|
|
logger.debug(
|
|
{ url: url.toString() },
|
|
'using URL based on configured endpoint',
|
|
);
|
|
return url.toString();
|
|
}
|
|
|
|
if (!repo.clone_url) {
|
|
throw new Error(CONFIG_GIT_URL_UNAVAILABLE);
|
|
}
|
|
|
|
logger.debug(`Using HTTP URL: ${repo.clone_url}`);
|
|
const repoUrl = parseUrl(repo.clone_url);
|
|
if (!repoUrl) {
|
|
throw new Error(CONFIG_GIT_URL_UNAVAILABLE);
|
|
}
|
|
repoUrl.username = opts.token ?? '';
|
|
return repoUrl.toString();
|
|
}
|
|
|
|
export function getMergeMethod(
|
|
strategy: MergeStrategy | undefined,
|
|
): PRMergeMethod | null {
|
|
switch (strategy) {
|
|
case 'fast-forward':
|
|
return 'rebase';
|
|
case 'merge-commit':
|
|
return 'merge';
|
|
case 'rebase':
|
|
return 'rebase-merge';
|
|
case 'squash':
|
|
return strategy;
|
|
case 'auto':
|
|
default:
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export const API_PATH = '/api/v1';
|
|
|
|
/** Human readable name of each platform, used in log messages. */
|
|
export const platformDisplayName = {
|
|
gitea: 'Gitea',
|
|
forgejo: 'Forgejo',
|
|
} as const satisfies Record<GiteaPlatformKey, string>;
|
|
|
|
export const DRAFT_PREFIX = 'WIP: ';
|
|
const reconfigurePrRegex = regEx(/reconfigure$/);
|
|
|
|
export function toRenovatePR(data: PR, author: string | null): Pr | null {
|
|
if (
|
|
!data.base?.ref ||
|
|
!data.head?.label ||
|
|
!data.head?.sha ||
|
|
!data.head?.repo?.full_name
|
|
) {
|
|
logger.trace(
|
|
`Skipping Pull Request #${data.number} due to missing base and/or head branch`,
|
|
);
|
|
return null;
|
|
}
|
|
|
|
const createdBy = data.user?.login;
|
|
if (
|
|
createdBy &&
|
|
author &&
|
|
!reconfigurePrRegex.test(data.head.label) &&
|
|
createdBy !== author
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
let title = data.title;
|
|
let isDraft = false;
|
|
if (title.startsWith(DRAFT_PREFIX)) {
|
|
title = title.substring(DRAFT_PREFIX.length);
|
|
isDraft = true;
|
|
}
|
|
const labels = coerceArray(data.labels).map((l) => l.name);
|
|
|
|
return {
|
|
labels,
|
|
number: data.number,
|
|
state: data.merged ? 'merged' : data.state,
|
|
title,
|
|
isDraft,
|
|
bodyStruct: getPrBodyStruct(data.body),
|
|
sha: data.head.sha,
|
|
sourceBranch: data.head.label,
|
|
targetBranch: data.base.ref,
|
|
sourceRepo: data.head.repo.full_name,
|
|
createdAt: data.created_at,
|
|
cannotMergeReason: data.mergeable
|
|
? undefined
|
|
: `pr.mergeable="${data.mergeable}"`,
|
|
hasAssignees: !!(data.assignee?.login ?? isNonEmptyArray(data.assignees)),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Check if a repository is usable.
|
|
* A repo isn't usable if one of the following conditions is met:
|
|
* - The repo is a `mirror`
|
|
* - We don't have pull or push permissions
|
|
* - Pull requests are disabled
|
|
* @param repo Repo to check
|
|
* @returns `true` if the repository is usable, `false` otherwise
|
|
*/
|
|
export function usableRepo(repo: Repo): boolean {
|
|
if (repo.mirror === true) {
|
|
return false;
|
|
}
|
|
|
|
if (repo.permissions.pull === false || repo.permissions.push === false) {
|
|
logger.debug(
|
|
`Skipping repository ${repo.full_name} because of missing pull or push permissions`,
|
|
);
|
|
return false;
|
|
}
|
|
|
|
if (repo.has_pull_requests === false) {
|
|
logger.debug(
|
|
`Skipping repository ${repo.full_name} because pull requests are disabled`,
|
|
);
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
export function isAllowed(style: PRMergeMethod, repo: Repo): boolean {
|
|
switch (style) {
|
|
case 'merge':
|
|
return repo.allow_merge_commits;
|
|
case 'rebase':
|
|
return repo.allow_rebase;
|
|
case 'rebase-merge':
|
|
return repo.allow_rebase_explicit;
|
|
case 'squash':
|
|
return repo.allow_squash_merge;
|
|
case 'fast-forward-only':
|
|
return repo.allow_fast_forward_only_merge;
|
|
}
|
|
}
|