Files
renovate/lib/modules/platform/gitea/utils.ts
Skyler Mäntysaari ab6b2309ab fix(platform/gitea): prefer fast-forward-only merge style for fast-forward automerge (#46203)
* fix(gitea): prefer fast-forward-only merge style for fast-forward automerge

Renovate's fast-forward automerge strategy only merges branches that
are already up to date with their target, so mapping it unconditionally
to Gitea/Forgejo's "rebase" (rebase-then-fast-forward) merge style
fails with a 405 on repos that only allow "Fast-forward only". Prefer
fast-forward-only when the repo allows it, falling back to rebase
otherwise.

Fixes https://github.com/renovatebot/renovate/discussions/42057

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

* Update lib/modules/platform/gitea/utils.ts

Co-authored-by: Michael Kriese <michael.kriese@gmx.de>

* test(gitea): fix getMergeMethod calls for required allowedMergeMethods param

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Michael Kriese <michael.kriese@gmx.de>
2026-09-16 10:59:36 +00:00

201 lines
5.5 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 { AllowedMergeMethods, 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,
allowedMergeMethods: AllowedMergeMethods,
): PRMergeMethod | null {
switch (strategy) {
case 'fast-forward':
// Renovate only automerges branches that are already up to date with
// their target branch, so a strict "fast-forward only" merge is safe
// and is preferred over "rebase" (rebase-then-fast-forward) when the
// repository allows it.
return allowedMergeMethods?.has('fast-forward-only')
? 'fast-forward-only'
: '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;
}
}