diff --git a/.changeset/gold-tables-matter.md b/.changeset/gold-tables-matter.md new file mode 100644 index 0000000000..ee5f4e5af3 --- /dev/null +++ b/.changeset/gold-tables-matter.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': minor +--- + +A new scaffolder action has been added: `gerrit:publish` diff --git a/.changeset/poor-years-develop.md b/.changeset/poor-years-develop.md new file mode 100644 index 0000000000..aee42bfc48 --- /dev/null +++ b/.changeset/poor-years-develop.md @@ -0,0 +1,5 @@ +--- +'@backstage/integration': patch +--- + +Gerrit Integration: Handle absolute paths in `resolveUrl` properly. diff --git a/.changeset/ten-rocks-smile.md b/.changeset/ten-rocks-smile.md new file mode 100644 index 0000000000..af178f559e --- /dev/null +++ b/.changeset/ten-rocks-smile.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': minor +--- + +Gerrit Integration: Implemented a `RepoUrlPicker` for Gerrit. diff --git a/packages/integration/src/gerrit/GerritIntegration.test.ts b/packages/integration/src/gerrit/GerritIntegration.test.ts index 6b5e228bb6..1a7bac5be6 100644 --- a/packages/integration/src/gerrit/GerritIntegration.test.ts +++ b/packages/integration/src/gerrit/GerritIntegration.test.ts @@ -97,6 +97,24 @@ describe('GerritIntegration', () => { }); }); + describe('resolves with an absolute url', () => { + it('works for valid urls', () => { + const integration = new GerritIntegration({ + host: 'gerrit-review.example.com', + gitilesBaseUrl: 'https://gerrit-review.example.com/gitiles', + } as any); + + expect( + integration.resolveUrl({ + url: '/catalog-info.yaml', + base: 'https://gerrit-review.example.com/gitiles/repo/+/refs/heads/master/', + }), + ).toBe( + 'https://gerrit-review.example.com/gitiles/repo/+/refs/heads/master/catalog-info.yaml', + ); + }); + }); + it('resolve edit URL', () => { const integration = new GerritIntegration({ host: 'gerrit-review.example.com', diff --git a/packages/integration/src/gerrit/GerritIntegration.ts b/packages/integration/src/gerrit/GerritIntegration.ts index 97eb9372c0..d57b54df8e 100644 --- a/packages/integration/src/gerrit/GerritIntegration.ts +++ b/packages/integration/src/gerrit/GerritIntegration.ts @@ -20,6 +20,7 @@ import { GerritIntegrationConfig, readGerritIntegrationConfigs, } from './config'; +import { parseGerritGitilesUrl, builldGerritGitilesUrl } from './core'; /** * A Gerrit based integration. @@ -58,6 +59,10 @@ export class GerritIntegration implements ScmIntegration { }): string { const { url, base, lineNumber } = options; let updated; + if (url.startsWith('/')) { + const { branch, project } = parseGerritGitilesUrl(this.config, base); + return builldGerritGitilesUrl(this.config, project, branch, url); + } if (url) { updated = new URL(url, base); } else { diff --git a/packages/integration/src/gerrit/core.test.ts b/packages/integration/src/gerrit/core.test.ts index 84bc1c0c19..351e7ce58e 100644 --- a/packages/integration/src/gerrit/core.test.ts +++ b/packages/integration/src/gerrit/core.test.ts @@ -20,6 +20,7 @@ import fetch from 'cross-fetch'; import { setupRequestMockHandlers } from '@backstage/test-utils'; import { GerritIntegrationConfig } from './config'; import { + builldGerritGitilesUrl, getGerritBranchApiUrl, getGerritCloneRepoUrl, getGerritRequestOptions, @@ -32,6 +33,20 @@ describe('gerrit core', () => { const worker = setupServer(); setupRequestMockHandlers(worker); + describe('builldGerritGitilesUrl', () => { + it('can create an url from arguments', () => { + const config: GerritIntegrationConfig = { + host: 'gerrit.com', + gitilesBaseUrl: 'https://gerrit.com/gitiles', + }; + expect( + builldGerritGitilesUrl(config, 'repo', 'dev', 'catalog-info.yaml'), + ).toEqual( + 'https://gerrit.com/gitiles/repo/+/refs/heads/dev/catalog-info.yaml', + ); + }); + }); + describe('getGerritRequestOptions', () => { it('adds headers when a password is specified', () => { const authRequest: GerritIntegrationConfig = { diff --git a/packages/integration/src/gerrit/core.ts b/packages/integration/src/gerrit/core.ts index eb413a2608..cd2b919e09 100644 --- a/packages/integration/src/gerrit/core.ts +++ b/packages/integration/src/gerrit/core.ts @@ -70,6 +70,26 @@ export function parseGerritGitilesUrl( }; } +/** + * Build a Gerrit Gitiles url that targets a specific path. + * + * @param config - A Gerrit provider config. + * @param project - The name of the git project + * @param branch - The branch we will target. + * @param filePath - The absolute file path. + * @public + */ +export function builldGerritGitilesUrl( + config: GerritIntegrationConfig, + project: string, + branch: string, + filePath: string, +): string { + return `${ + config.gitilesBaseUrl + }/${project}/+/refs/heads/${branch}/${trimStart(filePath, '/')}`; +} + /** * Return the authentication prefix. * diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index 7cf26aa473..84293a8538 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -247,6 +247,19 @@ export function createPublishFileAction(): TemplateAction<{ path: string; }>; +// @public +export function createPublishGerritAction(options: { + integrations: ScmIntegrationRegistry; + config: Config; +}): TemplateAction<{ + repoUrl: string; + description: string; + defaultBranch?: string | undefined; + gitCommitMessage?: string | undefined; + gitAuthorName?: string | undefined; + gitAuthorEmail?: string | undefined; +}>; + // @public export function createPublishGithubAction(options: { integrations: ScmIntegrationRegistry; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts index 0d269019df..2ffbc09ce5 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts @@ -39,6 +39,7 @@ import { createPublishBitbucketAction, createPublishBitbucketCloudAction, createPublishBitbucketServerAction, + createPublishGerritAction, createPublishGithubAction, createPublishGithubPullRequestAction, createPublishGitlabAction, @@ -111,6 +112,10 @@ export const createBuiltinActions = ( reader, additionalTemplateFilters, }), + createPublishGerritAction({ + integrations, + config, + }), createPublishGithubAction({ integrations, config, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gerrit.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gerrit.test.ts new file mode 100644 index 0000000000..0b1ffe0de6 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gerrit.test.ts @@ -0,0 +1,143 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +jest.mock('../helpers'); + +import { createPublishGerritAction } from './gerrit'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { ScmIntegrations } from '@backstage/integration'; +import { ConfigReader } from '@backstage/config'; +import { getVoidLogger } from '@backstage/backend-common'; +import { PassThrough } from 'stream'; +import { initRepoAndPush } from '../helpers'; + +describe('publish:gerrit', () => { + const config = new ConfigReader({ + integrations: { + gerrit: [ + { + host: 'gerrithost.org', + username: 'gerrituser', + password: 'usertoken', + }, + ], + }, + }); + + const description = 'for the lols'; + const integrations = ScmIntegrations.fromConfig(config); + const action = createPublishGerritAction({ integrations, config }); + const mockContext = { + input: { + repoUrl: + 'gerrithost.org?owner=owner&workspace=parent&project=project&repo=repo', + description, + }, + workspacePath: 'lol', + logger: getVoidLogger(), + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory: jest.fn(), + }; + const server = setupServer(); + setupRequestMockHandlers(server); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('should throw an error when the repoUrl is not well formed', async () => { + await expect( + action.handler({ + ...mockContext, + input: { repoUrl: 'gerrithost.org?workspace=w&repo=repo', description }, + }), + ).rejects.toThrow(/missing owner/); + + await expect( + action.handler({ + ...mockContext, + input: { repoUrl: 'gerrithost.org?workspace=w&owner=o', description }, + }), + ).rejects.toThrow(/missing repo/); + }); + + it('should throw if there is no integration config provided', async () => { + await expect( + action.handler({ + ...mockContext, + input: { + repoUrl: 'missing.com?workspace=w&owner=o&repo=repo', + description, + }, + }), + ).rejects.toThrow(/No matching integration configuration/); + }); + + it('can correctly create a new project', async () => { + expect.assertions(5); + server.use( + rest.put('https://gerrithost.org/a/projects/repo', (req, res, ctx) => { + expect(req.headers.get('Authorization')).toBe( + 'Basic Z2Vycml0dXNlcjp1c2VydG9rZW4=', + ); + expect(req.body).toEqual({ + create_empty_commit: false, + owners: ['owner'], + description, + parent: 'workspace', + }); + return res( + ctx.status(201), + ctx.set('Content-Type', 'application/json'), + ctx.json({}), + ); + }), + ); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + repoUrl: 'gerrithost.org?workspace=workspace&owner=owner&repo=repo', + }, + }); + + expect(initRepoAndPush).toHaveBeenCalledWith({ + dir: mockContext.workspacePath, + remoteUrl: 'https://gerrithost.org/a/repo', + defaultBranch: 'master', + auth: { username: 'gerrituser', password: 'usertoken' }, + logger: mockContext.logger, + commitMessage: expect.stringContaining('initial commit\n\nChange-Id:'), + gitAuthorInfo: {}, + }); + + expect(mockContext.output).toHaveBeenCalledWith( + 'remoteUrl', + 'https://gerrithost.org/a/repo', + ); + expect(mockContext.output).toHaveBeenCalledWith( + 'repoContentsUrl', + 'https://gerrithost.org/repo/+/refs/heads/master', + ); + }); + afterEach(() => { + jest.resetAllMocks(); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gerrit.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gerrit.ts new file mode 100644 index 0000000000..51355ec357 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gerrit.ts @@ -0,0 +1,216 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import crypto from 'crypto'; +import { InputError } from '@backstage/errors'; +import { Config } from '@backstage/config'; +import { + GerritIntegrationConfig, + getGerritRequestOptions, + ScmIntegrationRegistry, +} from '@backstage/integration'; +import { createTemplateAction } from '../../createTemplateAction'; +import { getRepoSourceDirectory, parseRepoUrl } from './util'; +import fetch, { Response, RequestInit } from 'node-fetch'; +import { initRepoAndPush } from '../helpers'; + +const createGerritProject = async ( + config: GerritIntegrationConfig, + options: { + projectName: string; + parent: string; + owner: string; + description: string; + }, +): Promise => { + const { projectName, parent, owner, description } = options; + + const fetchOptions: RequestInit = { + method: 'PUT', + body: JSON.stringify({ + parent, + description, + owners: [owner], + create_empty_commit: false, + }), + headers: { + ...getGerritRequestOptions(config).headers, + 'Content-Type': 'application/json', + }, + }; + const response: Response = await fetch( + `${config.baseUrl}/a/projects/${encodeURIComponent(projectName)}`, + fetchOptions, + ); + if (response.status !== 201) { + throw new Error( + `Unable to create repository, ${response.status} ${ + response.statusText + }, ${await response.text()}`, + ); + } +}; + +const generateCommitMessage = ( + config: Config, + commitSubject?: string, +): string => { + const changeId = crypto.randomBytes(20).toString('hex'); + const msg = `${ + config.getOptionalString('scaffolder.defaultCommitMessage') || commitSubject + }\n\nChange-Id: I${changeId}`; + return msg; +}; + +/** + * Creates a new action that initializes a git repository of the content in the workspace + * and publishes it to a Gerrit instance. + * @public + */ +export function createPublishGerritAction(options: { + integrations: ScmIntegrationRegistry; + config: Config; +}) { + const { integrations, config } = options; + + return createTemplateAction<{ + repoUrl: string; + description: string; + defaultBranch?: string; + gitCommitMessage?: string; + gitAuthorName?: string; + gitAuthorEmail?: string; + }>({ + id: 'publish:gerrit', + description: + 'Initializes a git repository of the content in the workspace, and publishes it to Gerrit.', + schema: { + input: { + type: 'object', + required: ['repoUrl'], + properties: { + repoUrl: { + title: 'Repository Location', + type: 'string', + }, + description: { + title: 'Repository Description', + type: 'string', + }, + defaultBranch: { + title: 'Default Branch', + type: 'string', + description: `Sets the default branch on the repository. The default value is 'master'`, + }, + gitCommitMessage: { + title: 'Git Commit Message', + type: 'string', + description: `Sets the commit message on the repository. The default value is 'initial commit'`, + }, + gitAuthorName: { + title: 'Default Author Name', + type: 'string', + description: `Sets the default author name for the commit. The default value is 'Scaffolder'`, + }, + gitAuthorEmail: { + title: 'Default Author Email', + type: 'string', + description: `Sets the default author email for the commit.`, + }, + }, + }, + output: { + type: 'object', + properties: { + remoteUrl: { + title: 'A URL to the repository with the provider', + type: 'string', + }, + repoContentsUrl: { + title: 'A URL to the root of the repository', + type: 'string', + }, + }, + }, + }, + async handler(ctx) { + const { + repoUrl, + description, + defaultBranch = 'master', + gitAuthorName, + gitAuthorEmail, + gitCommitMessage = 'initial commit', + } = ctx.input; + const { repo, host, owner, workspace } = parseRepoUrl( + repoUrl, + integrations, + ); + + const integrationConfig = integrations.gerrit.byHost(host); + + if (!integrationConfig) { + throw new InputError( + `No matching integration configuration for host ${host}, please check your integrations config`, + ); + } + + if (!owner) { + throw new InputError( + `Invalid URL provider was included in the repo URL to create ${ctx.input.repoUrl}, missing owner`, + ); + } + if (!workspace) { + throw new InputError( + `Invalid URL provider was included in the repo URL to create ${ctx.input.repoUrl}, missing workspace`, + ); + } + + await createGerritProject(integrationConfig.config, { + description, + owner: owner, + projectName: repo, + parent: workspace, + }); + const auth = { + username: integrationConfig.config.username!, + password: integrationConfig.config.password!, + }; + const gitAuthorInfo = { + name: gitAuthorName + ? gitAuthorName + : config.getOptionalString('scaffolder.defaultAuthor.name'), + email: gitAuthorEmail + ? gitAuthorEmail + : config.getOptionalString('scaffolder.defaultAuthor.email'), + }; + + const remoteUrl = `${integrationConfig.config.cloneUrl}/a/${repo}`; + await initRepoAndPush({ + dir: getRepoSourceDirectory(ctx.workspacePath, undefined), + remoteUrl, + auth, + defaultBranch, + logger: ctx.logger, + commitMessage: generateCommitMessage(config, gitCommitMessage), + gitAuthorInfo, + }); + + const repoContentsUrl = `${integrationConfig.config.gitilesBaseUrl}/${repo}/+/refs/heads/${defaultBranch}`; + ctx.output('remoteUrl', remoteUrl); + ctx.output('repoContentsUrl', repoContentsUrl); + }, + }); +} diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/index.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/index.ts index 02266b6a06..11ecd2b127 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/index.ts @@ -19,6 +19,7 @@ export { createPublishBitbucketAction } from './bitbucket'; export { createPublishBitbucketCloudAction } from './bitbucketCloud'; export { createPublishBitbucketServerAction } from './bitbucketServer'; export { createPublishFileAction } from './file'; +export { createPublishGerritAction } from './gerrit'; export { createPublishGithubAction } from './github'; export { createPublishGithubPullRequestAction } from './githubPullRequest'; export type { diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index 20945d08a6..229737eaf6 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -194,6 +194,7 @@ export interface RepoUrlPickerUiOptions { requestUserCredentials?: { secretsKey: string; additionalScopes?: { + gerrit?: string[]; github?: string[]; gitlab?: string[]; bitbucket?: string[]; diff --git a/plugins/scaffolder/src/api.ts b/plugins/scaffolder/src/api.ts index 6fc456e1fe..04103b9f8a 100644 --- a/plugins/scaffolder/src/api.ts +++ b/plugins/scaffolder/src/api.ts @@ -84,6 +84,7 @@ export class ScaffolderClient implements ScaffolderApi { ), ...this.scmIntegrationsApi.bitbucketCloud.list(), ...this.scmIntegrationsApi.bitbucketServer.list(), + ...this.scmIntegrationsApi.gerrit.list(), ...this.scmIntegrationsApi.github.list(), ...this.scmIntegrationsApi.gitlab.list(), ] diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GerritRepoPicker.test.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GerritRepoPicker.test.tsx new file mode 100644 index 0000000000..edc6732588 --- /dev/null +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GerritRepoPicker.test.tsx @@ -0,0 +1,78 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { GerritRepoPicker } from './GerritRepoPicker'; +import { render, fireEvent } from '@testing-library/react'; + +describe('BitbucketRepoPicker', () => { + describe('owner input field', () => { + it('calls onChange when the owner input changes', () => { + const onChange = jest.fn(); + const { getAllByRole } = render( + , + ); + + const ownerInput = getAllByRole('textbox')[0]; + + fireEvent.change(ownerInput, { target: { value: 'test-owner' } }); + + expect(onChange).toHaveBeenCalledWith({ owner: 'test-owner' }); + }); + }); + + describe('parent field', () => { + it('calls onChange when the parent changes', () => { + const onChange = jest.fn(); + const { getAllByRole } = render( + , + ); + + const parentInput = getAllByRole('textbox')[1]; + + fireEvent.change(parentInput, { target: { value: 'test-parent' } }); + + expect(onChange).toHaveBeenCalledWith({ workspace: 'test-parent' }); + }); + }); + + describe('repoName field', () => { + it('calls onChange when the repoName changes', () => { + const onChange = jest.fn(); + const { getAllByRole } = render( + , + ); + + const repoNameInput = getAllByRole('textbox')[2]; + + fireEvent.change(repoNameInput, { target: { value: 'test-repo' } }); + + expect(onChange).toHaveBeenCalledWith({ repoName: 'test-repo' }); + }); + }); +}); diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/GerritRepoPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GerritRepoPicker.tsx new file mode 100644 index 0000000000..4cbc0859e8 --- /dev/null +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/GerritRepoPicker.tsx @@ -0,0 +1,75 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import FormControl from '@material-ui/core/FormControl'; +import FormHelperText from '@material-ui/core/FormHelperText'; +import Input from '@material-ui/core/Input'; +import InputLabel from '@material-ui/core/InputLabel'; +import { RepoUrlPickerState } from './types'; + +export const GerritRepoPicker = (props: { + onChange: (state: RepoUrlPickerState) => void; + state: RepoUrlPickerState; + rawErrors: string[]; +}) => { + const { onChange, rawErrors, state } = props; + const { workspace, repoName, owner } = state; + return ( + <> + 0 && !workspace} + > + Owner + onChange({ owner: e.target.value })} + value={owner} + /> + The owner of the project + + 0 && !workspace} + > + Parent + onChange({ workspace: e.target.value })} + value={workspace} + /> + + The project parent that the repo will belong to + + + 0 && !repoName} + > + Repository + onChange({ repoName: e.target.value })} + value={repoName} + /> + The name of the repository + + + ); +}; diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx index fbb7bcd821..6f762933b6 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx @@ -23,6 +23,7 @@ import { GithubRepoPicker } from './GithubRepoPicker'; import { GitlabRepoPicker } from './GitlabRepoPicker'; import { AzureRepoPicker } from './AzureRepoPicker'; import { BitbucketRepoPicker } from './BitbucketRepoPicker'; +import { GerritRepoPicker } from './GerritRepoPicker'; import { FieldExtensionComponentProps } from '../../../extensions'; import { RepoUrlPickerHost } from './RepoUrlPickerHost'; import { parseRepoPickerUrl, serializeRepoPickerUrl } from './utils'; @@ -42,6 +43,7 @@ export interface RepoUrlPickerUiOptions { requestUserCredentials?: { secretsKey: string; additionalScopes?: { + gerrit?: string[]; github?: string[]; gitlab?: string[]; bitbucket?: string[]; @@ -170,6 +172,13 @@ export const RepoUrlPicker = ( onChange={updateLocalState} /> )} + {hostType === 'gerrit' && ( + + )} ); };