From 953ef18e8790954919c30c0ea8adb08157114fee Mon Sep 17 00:00:00 2001 From: Dominic Seitz <dominic.seitz@gmx.de> Date: Thu, 9 Jun 2022 15:48:40 +0200 Subject: [PATCH] feat(gitea): Support gitUrl (#14947) * feat: Support gitUrl on gitea platform * refactor: use query token instead of auth header * refactor: debug message style * refactor: use url property query * refactor: use basic http auth for gitea * test: add gitea tests for gitUrl property * refactor: capitalising abbreviations Co-authored-by: HonkingGoose <34918129+HonkingGoose@users.noreply.github.com> * refactor: move getRepoUrl to utils * fix: add missing property ssh_url to fix linting * fix: utils strict mode issues * Merge branch 'main' into main * refactor: use endpoint without api path slicing * refactor: use different null check * refactor: make urls optional Co-authored-by: Michael Kriese <michael.kriese@visualon.de> * test: prettier fix * test: update test * refactor: throw error if clone_url is missing * test: refactor empty clone_url test * refactor: change empty clone_url logic * refactor: change imports Co-authored-by: Michael Kriese <michael.kriese@visualon.de> * test: add token tests * refactor: replace deprecated url module * refactor: add null checks * test: add tests for null checks * test: use host rule implementation instead of mock * refactor: remove explicit typing Co-authored-by: Michael Kriese <michael.kriese@visualon.de> * test: update mocking Co-authored-by: Michael Kriese <michael.kriese@visualon.de> * test: change dynamic imports * Update lib/modules/platform/gitea/utils.ts Co-authored-by: HonkingGoose <34918129+HonkingGoose@users.noreply.github.com> Co-authored-by: Michael Kriese <michael.kriese@visualon.de> Co-authored-by: Rhys Arkins <rhys@arkins.net> --- .../platform/gitea/gitea-helper.spec.ts | 1 + lib/modules/platform/gitea/gitea-helper.ts | 3 +- lib/modules/platform/gitea/index.spec.ts | 168 +++++++++++++++++- lib/modules/platform/gitea/index.ts | 15 +- lib/modules/platform/gitea/utils.spec.ts | 27 ++- lib/modules/platform/gitea/utils.ts | 54 ++++++ 6 files changed, 252 insertions(+), 16 deletions(-) diff --git a/lib/modules/platform/gitea/gitea-helper.spec.ts b/lib/modules/platform/gitea/gitea-helper.spec.ts index 7d82268dc6..d67aed8b1a 100644 --- a/lib/modules/platform/gitea/gitea-helper.spec.ts +++ b/lib/modules/platform/gitea/gitea-helper.spec.ts @@ -30,6 +30,7 @@ describe('modules/platform/gitea/gitea-helper', () => { allow_merge_commits: true, allow_squash_merge: true, clone_url: 'https://gitea.renovatebot.com/some/repo.git', + ssh_url: 'git@gitea.renovatebot.com/some/repo.git', default_branch: 'master', full_name: 'some/repo', archived: false, diff --git a/lib/modules/platform/gitea/gitea-helper.ts b/lib/modules/platform/gitea/gitea-helper.ts index 1a0b5e3041..ec257948d5 100644 --- a/lib/modules/platform/gitea/gitea-helper.ts +++ b/lib/modules/platform/gitea/gitea-helper.ts @@ -64,7 +64,8 @@ export interface Repo { allow_rebase_explicit: boolean; allow_squash_merge: boolean; archived: boolean; - clone_url: string; + clone_url?: string; + ssh_url?: string; default_branch: string; empty: boolean; fork: boolean; diff --git a/lib/modules/platform/gitea/index.spec.ts b/lib/modules/platform/gitea/index.spec.ts index c2edace2c2..0e8582b100 100644 --- a/lib/modules/platform/gitea/index.spec.ts +++ b/lib/modules/platform/gitea/index.spec.ts @@ -5,8 +5,10 @@ import type { RepoParams, RepoResult, } from '..'; -import { partial } from '../../../../test/util'; +import { mocked, partial } from '../../../../test/util'; +import { PlatformId } from '../../../constants'; import { + CONFIG_GIT_URL_UNAVAILABLE, REPOSITORY_ACCESS_FORBIDDEN, REPOSITORY_ARCHIVED, REPOSITORY_BLOCKED, @@ -31,6 +33,7 @@ describe('modules/platform/gitea/index', () => { let helper: jest.Mocked<typeof import('./gitea-helper')>; let logger: jest.Mocked<typeof _logger>; let gitvcs: jest.Mocked<typeof _git>; + let hostRules: jest.Mocked<typeof import('../../../util/host-rules')>; const mockCommitHash = '0d9c7726c3d628b7e28af234595cfd20febdbf8e'; @@ -44,6 +47,7 @@ describe('modules/platform/gitea/index', () => { const mockRepo = partial<ght.Repo>({ allow_rebase: true, clone_url: 'https://gitea.renovatebot.com/some/repo.git', + ssh_url: 'git@gitea.renovatebot.com/some/repo.git', default_branch: 'master', full_name: 'some/repo', permissions: { @@ -172,11 +176,13 @@ describe('modules/platform/gitea/index', () => { jest.mock('../../../logger'); gitea = await import('.'); - helper = (await import('./gitea-helper')) as any; - logger = (await import('../../../logger')).logger as any; + helper = mocked(await import('./gitea-helper')); + logger = mocked((await import('../../../logger')).logger); gitvcs = require('../../../util/git'); gitvcs.isBranchStale.mockResolvedValue(false); gitvcs.getBranchCommit.mockReturnValue(mockCommitHash); + hostRules = mocked(await import('../../../util/host-rules')); + hostRules.clear(); setBaseUrl('https://gitea.renovatebot.com/'); }); @@ -338,6 +344,162 @@ describe('modules/platform/gitea/index', () => { }) ).toMatchSnapshot(); }); + + it('should use clone_url of repo if gitUrl is not specified', async () => { + expect.assertions(1); + + helper.getRepo.mockResolvedValueOnce(mockRepo); + const repoCfg: RepoParams = { + repository: mockRepo.full_name, + }; + await gitea.initRepo(repoCfg); + + expect(gitvcs.initRepo).toHaveBeenCalledWith( + expect.objectContaining({ url: mockRepo.clone_url }) + ); + }); + + it('should use clone_url of repo if gitUrl has value default', async () => { + expect.assertions(1); + + helper.getRepo.mockResolvedValueOnce(mockRepo); + const repoCfg: RepoParams = { + repository: mockRepo.full_name, + gitUrl: 'default', + }; + await gitea.initRepo(repoCfg); + + expect(gitvcs.initRepo).toHaveBeenCalledWith( + expect.objectContaining({ url: mockRepo.clone_url }) + ); + }); + + it('should use ssh_url of repo if gitUrl has value ssh', async () => { + expect.assertions(1); + + helper.getRepo.mockResolvedValueOnce(mockRepo); + const repoCfg: RepoParams = { + repository: mockRepo.full_name, + gitUrl: 'ssh', + }; + await gitea.initRepo(repoCfg); + + expect(gitvcs.initRepo).toHaveBeenCalledWith( + expect.objectContaining({ url: mockRepo.ssh_url }) + ); + }); + + it('should abort when gitUrl has value ssh but ssh_url is empty', async () => { + expect.assertions(1); + + helper.getRepo.mockResolvedValueOnce({ ...mockRepo, ssh_url: undefined }); + const repoCfg: RepoParams = { + repository: mockRepo.full_name, + gitUrl: 'ssh', + }; + + await expect(gitea.initRepo(repoCfg)).rejects.toThrow( + CONFIG_GIT_URL_UNAVAILABLE + ); + }); + + it('should use generated url of repo if gitUrl has value endpoint', async () => { + expect.assertions(1); + + helper.getRepo.mockResolvedValueOnce(mockRepo); + const repoCfg: RepoParams = { + repository: mockRepo.full_name, + gitUrl: 'endpoint', + }; + await gitea.initRepo(repoCfg); + + expect(gitvcs.initRepo).toHaveBeenCalledWith( + expect.objectContaining({ + url: `https://gitea.com/${mockRepo.full_name}.git`, + }) + ); + }); + + it('should abort when clone_url is empty', async () => { + expect.assertions(1); + + helper.getRepo.mockResolvedValueOnce({ + ...mockRepo, + clone_url: undefined, + }); + const repoCfg: RepoParams = { + repository: mockRepo.full_name, + }; + + await expect(gitea.initRepo(repoCfg)).rejects.toThrow( + CONFIG_GIT_URL_UNAVAILABLE + ); + }); + + it('should use given access token if gitUrl has value endpoint', async () => { + expect.assertions(1); + + const token = 'abc'; + hostRules.add({ + hostType: PlatformId.Gitea, + matchHost: 'https://gitea.com/', + token, + }); + + helper.getRepo.mockResolvedValueOnce(mockRepo); + const repoCfg: RepoParams = { + repository: mockRepo.full_name, + gitUrl: 'endpoint', + }; + await gitea.initRepo(repoCfg); + + const url = new URL(`${mockRepo.clone_url}`); + url.username = token; + expect(gitvcs.initRepo).toHaveBeenCalledWith( + expect.objectContaining({ + url: `https://${token}@gitea.com/${mockRepo.full_name}.git`, + }) + ); + }); + + it('should use given access token if gitUrl is not specified', async () => { + expect.assertions(1); + + const token = 'abc'; + hostRules.add({ + hostType: PlatformId.Gitea, + matchHost: 'https://gitea.com/', + token, + }); + + helper.getRepo.mockResolvedValueOnce(mockRepo); + const repoCfg: RepoParams = { + repository: mockRepo.full_name, + }; + await gitea.initRepo(repoCfg); + + const url = new URL(`${mockRepo.clone_url}`); + url.username = token; + expect(gitvcs.initRepo).toHaveBeenCalledWith( + expect.objectContaining({ url: url.toString() }) + ); + }); + + it('should abort when clone_url is not valid', async () => { + expect.assertions(1); + + helper.getRepo.mockResolvedValueOnce({ + ...mockRepo, + clone_url: 'abc', + }); + const repoCfg: RepoParams = { + repository: mockRepo.full_name, + }; + + await expect(gitea.initRepo(repoCfg)).rejects.toThrow( + CONFIG_GIT_URL_UNAVAILABLE + ); + }); }); describe('setBranchStatus', () => { diff --git a/lib/modules/platform/gitea/index.ts b/lib/modules/platform/gitea/index.ts index 29842a7937..97e437db5e 100644 --- a/lib/modules/platform/gitea/index.ts +++ b/lib/modules/platform/gitea/index.ts @@ -1,4 +1,3 @@ -import URL from 'url'; import is from '@sindresorhus/is'; import JSON5 from 'json5'; import semver from 'semver'; @@ -14,7 +13,6 @@ import { import { logger } from '../../../logger'; import { BranchStatus, PrState, VulnerabilityAlert } from '../../../types'; import * as git from '../../../util/git'; -import * as hostRules from '../../../util/host-rules'; import { setBaseUrl } from '../../../util/http/gitea'; import { sanitize } from '../../../util/sanitize'; import { ensureTrailingSlash } from '../../../util/url'; @@ -38,7 +36,7 @@ import type { } from '../types'; import { smartTruncate } from '../utils/pr-body'; import * as helper from './gitea-helper'; -import { smartLinks, trimTrailingApiPath } from './utils'; +import { getRepoUrl, smartLinks, trimTrailingApiPath } from './utils'; interface GiteaRepoConfig { repository: string; @@ -240,6 +238,7 @@ const platform: Platform = { async initRepo({ repository, cloneSubmodules, + gitUrl, }: RepoParams): Promise<RepoResult> { let repo: helper.Repo; @@ -298,18 +297,12 @@ const platform: Platform = { config.defaultBranch = repo.default_branch; logger.debug(`${repository} default branch = ${config.defaultBranch}`); - // Find options for current host and determine Git endpoint - const opts = hostRules.find({ - hostType: PlatformId.Gitea, - url: defaults.endpoint, - }); - const gitEndpoint = URL.parse(repo.clone_url); - gitEndpoint.auth = opts.token ?? null; + const url = getRepoUrl(repo, gitUrl, defaults.endpoint); // Initialize Git storage await git.initRepo({ ...config, - url: URL.format(gitEndpoint), + url, }); // Reset cached resources diff --git a/lib/modules/platform/gitea/utils.spec.ts b/lib/modules/platform/gitea/utils.spec.ts index a091434e5f..f7982d4c6d 100644 --- a/lib/modules/platform/gitea/utils.spec.ts +++ b/lib/modules/platform/gitea/utils.spec.ts @@ -1,6 +1,22 @@ -import { trimTrailingApiPath } from './utils'; +import { partial } from '../../../../test/util'; +import { CONFIG_GIT_URL_UNAVAILABLE } from '../../../constants/error-messages'; +import type { Repo } from './gitea-helper'; +import { getRepoUrl, trimTrailingApiPath } from './utils'; describe('modules/platform/gitea/utils', () => { + const mockRepo = partial<Repo>({ + allow_rebase: true, + clone_url: 'https://gitea.renovatebot.com/some/repo.git', + ssh_url: 'git@gitea.renovatebot.com/some/repo.git', + default_branch: 'master', + full_name: 'some/repo', + permissions: { + pull: true, + push: true, + admin: false, + }, + }); + it('trimTrailingApiPath', () => { expect(trimTrailingApiPath('https://gitea.renovatebot.com/api/v1')).toBe( 'https://gitea.renovatebot.com/' @@ -18,4 +34,13 @@ describe('modules/platform/gitea/utils', () => { trimTrailingApiPath('https://gitea.renovatebot.com/api/gitea/api/v1') ).toBe('https://gitea.renovatebot.com/api/gitea/'); }); + + describe('getRepoUrl', () => { + it('should abort when endpoint is not valid', () => { + expect.assertions(1); + expect(() => getRepoUrl(mockRepo, 'endpoint', 'abc')).toThrow( + CONFIG_GIT_URL_UNAVAILABLE + ); + }); + }); }); diff --git a/lib/modules/platform/gitea/utils.ts b/lib/modules/platform/gitea/utils.ts index 927ce0a395..5a119a5c47 100644 --- a/lib/modules/platform/gitea/utils.ts +++ b/lib/modules/platform/gitea/utils.ts @@ -1,4 +1,11 @@ +import { PlatformId } from '../../../constants'; +import { CONFIG_GIT_URL_UNAVAILABLE } from '../../../constants/error-messages'; +import { logger } from '../../../logger'; +import * as hostRules from '../../../util/host-rules'; import { regEx } from '../../../util/regex'; +import { parseUrl } from '../../../util/url'; +import type { GitUrlOption } from '../types'; +import type { Repo } from './gitea-helper'; export function smartLinks(body: string): string { return body?.replace(regEx(/\]\(\.\.\/pull\//g), '](pulls/'); @@ -7,3 +14,50 @@ export function smartLinks(body: string): string { export function trimTrailingApiPath(url: string): string { return url?.replace(regEx(/api\/v1\/?$/g), ''); } + +export function getRepoUrl( + repo: Repo, + gitUrl: GitUrlOption | undefined, + endpoint: string +): string { + if (gitUrl === 'ssh') { + if (!repo.ssh_url) { + throw new Error(CONFIG_GIT_URL_UNAVAILABLE); + } + logger.debug({ url: repo.ssh_url }, `using SSH URL`); + return repo.ssh_url; + } + + // Find options for current host and determine Git endpoint + const opts = hostRules.find({ + hostType: PlatformId.Gitea, + url: endpoint, + }); + + if (gitUrl === 'endpoint') { + const url = parseUrl(endpoint); + if (!url) { + throw new Error(CONFIG_GIT_URL_UNAVAILABLE); + } + url.protocol = url.protocol?.slice(0, -1) ?? 'https'; + 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({ url: repo.clone_url }, `using HTTP URL`); + const repoUrl = parseUrl(repo.clone_url); + if (!repoUrl) { + throw new Error(CONFIG_GIT_URL_UNAVAILABLE); + } + repoUrl.username = opts.token ?? ''; + return repoUrl.toString(); +} -- GitLab