diff --git a/.changeset/cool-ties-share.md b/.changeset/cool-ties-share.md new file mode 100644 index 0000000000..fb76785fcf --- /dev/null +++ b/.changeset/cool-ties-share.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +Gerrit integration: Added optional `cloneUrl` string to config. diff --git a/.changeset/fair-kings-retire.md b/.changeset/fair-kings-retire.md new file mode 100644 index 0000000000..156632f6ae --- /dev/null +++ b/.changeset/fair-kings-retire.md @@ -0,0 +1,5 @@ +--- +'@backstage/integration': minor +--- + +Gerrit UrlReader: Implemented `readTree` diff --git a/docs/integrations/gerrit/locations.md b/docs/integrations/gerrit/locations.md index 8091de3a2e..dcbac65849 100644 --- a/docs/integrations/gerrit/locations.md +++ b/docs/integrations/gerrit/locations.md @@ -19,7 +19,8 @@ To use this integration, add at least one Gerrit configuration to your root `app integrations: gerrit: - host: gerrit.company.com - apiBaseUrl: https://gerrit.company.com/gerrit + baseUrl: https://gerrit.company.com/gerrit + cloneUrl: https://gerrit.company.com/clone gitilesBaseUrl: https://gerrit.company.com/gitiles username: ${GERRIT_USERNAME} password: ${GERRIT_PASSWORD} @@ -27,12 +28,14 @@ integrations: Directly under the `gerrit` key is a list of provider configurations, where you can list the Gerrit instances you want to fetch data from. Each entry is -a structure with up to four elements: +a structure with up to six elements: - `host`: The host of the Gerrit instance, e.g. `gerrit.company.com`. -- `apiBaseUrl` (optional): Needed if the Gerrit instance is not reachable at +- `baseUrl` (optional): Needed if the Gerrit instance is not reachable at the base of the `host` option (e.g. `https://gerrit.company.com`) set the address here. This is the address that you would open in a browser. +- `cloneUrl` (optional): The base url for HTTP clones. Will default to `baseUrl` if + not set. The address used to clone a repo is the `cloneUrl` plus the repo name. - `gitilesBaseUrl` (optional): This is needed for creating a valid user-friendly url that can be used for browsing the content of the provider. If not set a default value will be created in the same way as the "baseUrl" option. There is no diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index 927304bc31..961973b144 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -302,13 +302,19 @@ export type FromReadableArrayOptions = Array<{ // @public export class GerritUrlReader implements UrlReader { - constructor(integration: GerritIntegration); + constructor( + integration: GerritIntegration, + deps: { + treeResponseFactory: ReadTreeResponseFactory; + }, + workDir: string, + ); // (undocumented) static factory: ReaderFactory; // (undocumented) read(url: string): Promise; // (undocumented) - readTree(): Promise; + readTree(url: string, options?: ReadTreeOptions): Promise; // (undocumented) readUrl(url: string, options?: ReadUrlOptions): Promise; // (undocumented) diff --git a/packages/backend-common/src/reading/GerritUrlReader.test.ts b/packages/backend-common/src/reading/GerritUrlReader.test.ts index bc88fdcb20..fc27b3dd6a 100644 --- a/packages/backend-common/src/reading/GerritUrlReader.test.ts +++ b/packages/backend-common/src/reading/GerritUrlReader.test.ts @@ -14,24 +14,36 @@ * limitations under the License. */ -import { rest } from 'msw'; -import { setupServer } from 'msw/node'; import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; -import { JsonObject } from '@backstage/types'; -import { getVoidLogger } from '../logging'; -import { DefaultReadTreeResponseFactory } from './tree'; -import { UrlReaderPredicateTuple } from './types'; +import { NotModifiedError, NotFoundError } from '@backstage/errors'; import { GerritIntegration, readGerritIntegrationConfig, } from '@backstage/integration'; +import { JsonObject } from '@backstage/types'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; +import mockFs from 'mock-fs'; +import fs from 'fs-extra'; +import path from 'path'; +import { getVoidLogger } from '../logging'; +import { UrlReaderPredicateTuple } from './types'; +import { DefaultReadTreeResponseFactory } from './tree'; import { GerritUrlReader } from './GerritUrlReader'; const treeResponseFactory = DefaultReadTreeResponseFactory.create({ config: new ConfigReader({}), }); +jest.mock('../scm', () => ({ + Git: { + fromAuth: () => ({ + clone: jest.fn(() => Promise.resolve({})), + }), + }, +})); + const gerritProcessor = new GerritUrlReader( new GerritIntegration( readGerritIntegrationConfig( @@ -40,6 +52,8 @@ const gerritProcessor = new GerritUrlReader( }), ), ), + { treeResponseFactory }, + '/tmp', ); const createReader = (config: JsonObject): UrlReaderPredicateTuple[] => { @@ -54,6 +68,10 @@ describe('GerritUrlReader', () => { const worker = setupServer(); setupRequestMockHandlers(worker); + afterAll(() => { + jest.clearAllMocks(); + }); + describe('reader factory', () => { it('creates a reader.', () => { const readers = createReader({ @@ -103,7 +121,7 @@ describe('GerritUrlReader', () => { expect(predicate(new URL('https://gerrit-review.com/path'))).toBe(false); }); - it('returns false for host.', () => { + it('returns false for host.', () => { expect(predicate(new URL('https://gerrit.com/path'))).toBe(true); }); }); @@ -170,4 +188,104 @@ describe('GerritUrlReader', () => { ); }); }); + + describe('readTree', () => { + const branchAPIUrl = + 'https://gerrit.com/projects/app%2Fweb/branches/master'; + const branchAPIresponse = fs.readFileSync( + path.resolve(__dirname, '__fixtures__/gerrit/branch-info-response.txt'), + ); + const treeUrl = 'https://gerrit.com/app/web/+/refs/heads/master/'; + const etag = '52432507a70b677b5674b019c9a46b2e9f29d0a1'; + const mkdocsContent = 'great content'; + const mdContent = 'doc'; + + beforeEach(() => { + mockFs({ + '/tmp/': mockFs.directory(), + '/tmp/gerrit-clone-123abc/repo/mkdocs.yml': mkdocsContent, + '/tmp/gerrit-clone-123abc/repo/docs/first.md': mdContent, + }); + const spy = jest.spyOn(fs, 'mkdtemp'); + spy.mockImplementation(() => '/tmp/gerrit-clone-123abc'); + }); + + afterEach(() => { + mockFs.restore(); + jest.clearAllMocks(); + }); + + it('reads the wanted files correctly.', async () => { + worker.use( + rest.get(branchAPIUrl, (_, res, ctx) => { + return res(ctx.status(200), ctx.body(branchAPIresponse)); + }), + ); + + const response = await gerritProcessor.readTree(treeUrl); + + expect(response.etag).toBe(etag); + + const files = await response.files(); + expect(files.length).toBe(2); + + const docsYaml = await files[0].content(); + expect(docsYaml.toString()).toBe(mkdocsContent); + + const mdFile = await files[1].content(); + expect(mdFile.toString()).toBe(mdContent); + }); + + it('throws NotModifiedError for matching etags.', async () => { + worker.use( + rest.get(branchAPIUrl, (_, res, ctx) => { + return res(ctx.status(200), ctx.body(branchAPIresponse)); + }), + ); + + await expect(gerritProcessor.readTree(treeUrl, { etag })).rejects.toThrow( + NotModifiedError, + ); + }); + + it('throws NotFoundError if branch info not found.', async () => { + worker.use( + rest.get(branchAPIUrl, (_, res, ctx) => { + return res(ctx.status(404, 'Not found.')); + }), + ); + + await expect(gerritProcessor.readTree(treeUrl)).rejects.toThrow( + NotFoundError, + ); + }); + + it('should throw on failures while getting branch info.', async () => { + worker.use( + rest.get(branchAPIUrl, (_, res, ctx) => { + return res(ctx.status(500, 'Error')); + }), + ); + + await expect(gerritProcessor.readTree(treeUrl)).rejects.toThrow(Error); + }); + + it('should returns wanted files with a subpath', async () => { + worker.use( + rest.get(branchAPIUrl, (_, res, ctx) => { + return res(ctx.status(200), ctx.body(branchAPIresponse)); + }), + ); + + const response = await gerritProcessor.readTree(`${treeUrl}/docs`); + + expect(response.etag).toBe(etag); + + const files = await response.files(); + expect(files.length).toBe(1); + + const mdFile = await files[0].content(); + expect(mdFile.toString()).toBe(mdContent); + }); + }); }); diff --git a/packages/backend-common/src/reading/GerritUrlReader.ts b/packages/backend-common/src/reading/GerritUrlReader.ts index 7cc0b4731b..f3e8924192 100644 --- a/packages/backend-common/src/reading/GerritUrlReader.ts +++ b/packages/backend-common/src/reading/GerritUrlReader.ts @@ -14,16 +14,30 @@ * limitations under the License. */ -import { NotFoundError } from '@backstage/errors'; +import { Git } from '../scm'; +import { NotFoundError, NotModifiedError } from '@backstage/errors'; import { GerritIntegration, + getGerritCloneRepoUrl, + getGerritBranchApiUrl, getGerritFileContentsApiUrl, getGerritRequestOptions, + parseGerritJsonResponse, + parseGerritGitilesUrl, } from '@backstage/integration'; +import concatStream from 'concat-stream'; +import fs from 'fs-extra'; import fetch, { Response } from 'node-fetch'; +import os from 'os'; +import { join as joinPath } from 'path'; +import tar from 'tar'; +import { pipeline as pipelineCb, Readable } from 'stream'; +import { promisify } from 'util'; import { ReaderFactory, + ReadTreeOptions, ReadTreeResponse, + ReadTreeResponseFactory, ReadUrlOptions, ReadUrlResponse, SearchResponse, @@ -31,6 +45,11 @@ import { } from './types'; import { ScmIntegrations } from '@backstage/integration'; +const pipeline = promisify(pipelineCb); + +const createTemporaryDirectory = async (workDir: string): Promise => + await fs.mkdtemp(joinPath(workDir, '/gerrit-clone-')); + /** * Implements a {@link UrlReader} for files in Gerrit. * @@ -50,16 +69,22 @@ import { ScmIntegrations } from '@backstage/integration'; * @public */ export class GerritUrlReader implements UrlReader { - static factory: ReaderFactory = ({ config }) => { + static factory: ReaderFactory = ({ config, treeResponseFactory }) => { const integrations = ScmIntegrations.fromConfig(config); if (!integrations.gerrit) { return []; } + const workDir = + config.getOptionalString('backend.workingDirectory') ?? os.tmpdir(); return integrations.gerrit.list().map(integration => { - const reader = new GerritUrlReader(integration); + const reader = new GerritUrlReader( + integration, + { treeResponseFactory }, + workDir, + ); const predicate = (url: URL) => { const gitilesUrl = new URL(integration.config.gitilesBaseUrl!); - // If gitilesUrl is not specfified it will default to + // If gitilesUrl is not specified it will default to // "integration.config.host". return url.host === gitilesUrl.host; }; @@ -67,7 +92,11 @@ export class GerritUrlReader implements UrlReader { }); }; - constructor(private readonly integration: GerritIntegration) {} + constructor( + private readonly integration: GerritIntegration, + private readonly deps: { treeResponseFactory: ReadTreeResponseFactory }, + private readonly workDir: string, + ) {} async read(url: string): Promise { const response = await this.readUrl(url); @@ -109,8 +138,72 @@ export class GerritUrlReader implements UrlReader { ); } - async readTree(): Promise { - throw new Error('GerritReader does not implement readTree'); + async readTree( + url: string, + options?: ReadTreeOptions, + ): Promise { + const { filePath } = parseGerritGitilesUrl(this.integration.config, url); + const apiUrl = getGerritBranchApiUrl(this.integration.config, url); + let response: Response; + try { + response = await fetch(apiUrl, { + method: 'GET', + ...getGerritRequestOptions(this.integration.config), + }); + } catch (e) { + throw new Error(`Unable to read branch state ${url}, ${e}`); + } + + if (response.status === 404) { + throw new NotFoundError(`Not found: ${url}`); + } + + if (!response.ok) { + throw new Error( + `${url} could not be read as ${apiUrl}, ${response.status} ${response.statusText}`, + ); + } + const branchInfo = (await parseGerritJsonResponse(response as any)) as { + revision: string; + }; + if (options?.etag === branchInfo.revision) { + throw new NotModifiedError(); + } + + const git = Git.fromAuth({ + username: this.integration.config.username, + password: this.integration.config.password, + }); + const tempDir = await createTemporaryDirectory(this.workDir); + const cloneUrl = getGerritCloneRepoUrl(this.integration.config, url); + try { + // The "fromTarArchive" function will strip the top level directory so + // an additional directory level is created when we clone. + await git.clone({ + url: cloneUrl, + dir: joinPath(tempDir, 'repo'), + ref: branchInfo.revision, + depth: 1, + }); + + const data = await new Promise(async resolve => { + await pipeline( + tar.create({ cwd: tempDir }, ['']), + concatStream(resolve), + ); + }); + const tarArchive = Readable.from(data); + return await this.deps.treeResponseFactory.fromTarArchive({ + stream: tarArchive as unknown as Readable, + subpath: filePath === '/' ? undefined : filePath, + etag: branchInfo.revision, + filter: options?.filter, + }); + } catch (error) { + throw new Error(`Could not clone ${cloneUrl}: ${error}`); + } finally { + await fs.rm(tempDir, { recursive: true, force: true }); + } } async search(): Promise { diff --git a/packages/backend-common/src/reading/__fixtures__/gerrit/branch-info-response.txt b/packages/backend-common/src/reading/__fixtures__/gerrit/branch-info-response.txt new file mode 100644 index 0000000000..20018c1bdf --- /dev/null +++ b/packages/backend-common/src/reading/__fixtures__/gerrit/branch-info-response.txt @@ -0,0 +1,2 @@ +)]}' +{"web_links":[{"name":"browse","url":"https://gerrit.googlesource.com/app/web/+/refs/heads/master","target":"_blank"}],"ref":"refs/heads/master","revision":"52432507a70b677b5674b019c9a46b2e9f29d0a1"} diff --git a/packages/integration/api-report.md b/packages/integration/api-report.md index e208bd9062..3ffb131900 100644 --- a/packages/integration/api-report.md +++ b/packages/integration/api-report.md @@ -195,6 +195,7 @@ export class GerritIntegration implements ScmIntegration { export type GerritIntegrationConfig = { host: string; baseUrl?: string; + cloneUrl?: string; gitilesBaseUrl?: string; username?: string; password?: string; @@ -292,6 +293,18 @@ export function getBitbucketServerRequestOptions( headers: Record; }; +// @public +export function getGerritBranchApiUrl( + config: GerritIntegrationConfig, + url: string, +): string; + +// @public +export function getGerritCloneRepoUrl( + config: GerritIntegrationConfig, + url: string, +): string; + // @public export function getGerritFileContentsApiUrl( config: GerritIntegrationConfig, @@ -458,6 +471,16 @@ export interface IntegrationsByType { gitlab: ScmIntegrationsGroup; } +// @public +export function parseGerritGitilesUrl( + config: GerritIntegrationConfig, + url: string, +): { + branch: string; + filePath: string; + project: string; +}; + // @public export function parseGerritJsonResponse(response: Response): Promise; diff --git a/packages/integration/config.d.ts b/packages/integration/config.d.ts index e4aff81c6f..c14f5c76cd 100644 --- a/packages/integration/config.d.ts +++ b/packages/integration/config.d.ts @@ -108,6 +108,11 @@ export interface Config { * @visibility frontend */ baseUrl?: string; + /** + * The base url for cloning repos. + * @visibility frontend + */ + cloneUrl?: string; /** * The username to use for authenticated requests. * @visibility secret diff --git a/packages/integration/src/gerrit/config.test.ts b/packages/integration/src/gerrit/config.test.ts index 9c0d022548..f9287f6801 100644 --- a/packages/integration/src/gerrit/config.test.ts +++ b/packages/integration/src/gerrit/config.test.ts @@ -56,6 +56,7 @@ describe('readGerritIntegrationConfig', () => { buildConfig({ host: 'a.com', baseUrl: 'https://a.com/api', + cloneUrl: 'https:a.com/clone', gitilesBaseUrl: 'https://a.com/git', username: 'u', password: 'p', @@ -64,6 +65,7 @@ describe('readGerritIntegrationConfig', () => { expect(output).toEqual({ host: 'a.com', baseUrl: 'https://a.com/api', + cloneUrl: 'https:a.com/clone', gitilesBaseUrl: 'https://a.com/git', username: 'u', password: 'p', @@ -79,6 +81,7 @@ describe('readGerritIntegrationConfig', () => { expect(output).toEqual({ host: 'a.com', baseUrl: 'https://a.com', + cloneUrl: 'https://a.com', gitilesBaseUrl: 'https://a.com', username: undefined, password: undefined, @@ -110,6 +113,7 @@ describe('readGerritIntegrationConfig', () => { ).toEqual({ host: 'a.com', baseUrl: 'https://a.com/gerrit', + cloneUrl: 'https://a.com/gerrit', gitilesBaseUrl: 'https://a.com', }); }); @@ -139,6 +143,7 @@ describe('readGerritIntegrationConfigs', () => { { host: 'a.com', baseUrl: 'https://a.com/api', + cloneUrl: 'https://a.com/api', gitilesBaseUrl: 'https://a.com', username: 'u', password: 'p', @@ -146,6 +151,7 @@ describe('readGerritIntegrationConfigs', () => { { host: 'b.com', baseUrl: 'https://b.com/api', + cloneUrl: 'https://b.com/api', gitilesBaseUrl: 'https://b.com', username: undefined, password: undefined, diff --git a/packages/integration/src/gerrit/config.ts b/packages/integration/src/gerrit/config.ts index 7dbb05d15b..8558ff0c1b 100644 --- a/packages/integration/src/gerrit/config.ts +++ b/packages/integration/src/gerrit/config.ts @@ -38,6 +38,12 @@ export type GerritIntegrationConfig = { */ baseUrl?: string; + /** + * The optional base url to use for cloning a repository. If not set the + * baseUrl will be used. + */ + cloneUrl?: string; + /** * Optional base url for Gitiles. This is needed for creating a valid * user-friendly url that can be used for browsing the content of the @@ -69,6 +75,7 @@ export function readGerritIntegrationConfig( ): GerritIntegrationConfig { const host = config.getString('host'); let baseUrl = config.getOptionalString('baseUrl'); + let cloneUrl = config.getOptionalString('cloneUrl'); let gitilesBaseUrl = config.getOptionalString('gitilesBaseUrl'); const username = config.getOptionalString('username'); const password = config.getOptionalString('password'); @@ -81,6 +88,10 @@ export function readGerritIntegrationConfig( throw new Error( `Invalid Gerrit integration config, '${baseUrl}' is not a valid baseUrl`, ); + } else if (cloneUrl && !isValidUrl(cloneUrl)) { + throw new Error( + `Invalid Gerrit integration config, '${cloneUrl}' is not a valid cloneUrl`, + ); } else if (gitilesBaseUrl && !isValidUrl(gitilesBaseUrl)) { throw new Error( `Invalid Gerrit integration config, '${gitilesBaseUrl}' is not a valid gitilesBaseUrl`, @@ -96,10 +107,16 @@ export function readGerritIntegrationConfig( } else { gitilesBaseUrl = `https://${host}`; } + if (cloneUrl) { + cloneUrl = trimEnd(cloneUrl, '/'); + } else { + cloneUrl = baseUrl; + } return { host, baseUrl, + cloneUrl, gitilesBaseUrl, username, password, diff --git a/packages/integration/src/gerrit/core.test.ts b/packages/integration/src/gerrit/core.test.ts index b966a31b9e..84bc1c0c19 100644 --- a/packages/integration/src/gerrit/core.test.ts +++ b/packages/integration/src/gerrit/core.test.ts @@ -20,9 +20,11 @@ import fetch from 'cross-fetch'; import { setupRequestMockHandlers } from '@backstage/test-utils'; import { GerritIntegrationConfig } from './config'; import { + getGerritBranchApiUrl, + getGerritCloneRepoUrl, getGerritRequestOptions, parseGerritJsonResponse, - parseGitilesUrl, + parseGerritGitilesUrl, getGerritFileContentsApiUrl, } from './core'; @@ -55,7 +57,7 @@ describe('gerrit core', () => { host: 'gerrit.com', gitilesBaseUrl: 'https://gerrit.com/gitiles', }; - const { branch, filePath, project } = parseGitilesUrl( + const { branch, filePath, project } = parseGerritGitilesUrl( config, 'https://gerrit.com/gitiles/web/project/+/refs/heads/master/README.md', ); @@ -63,7 +65,7 @@ describe('gerrit core', () => { expect(branch).toEqual('master'); expect(filePath).toEqual('README.md'); - const { filePath: rootPath } = parseGitilesUrl( + const { filePath: rootPath } = parseGerritGitilesUrl( config, 'https://gerrit.com/gitiles/web/project/+/refs/heads/master', ); @@ -75,13 +77,13 @@ describe('gerrit core', () => { gitilesBaseUrl: 'https://gerrit.com', }; expect(() => - parseGitilesUrl( + parseGerritGitilesUrl( config, 'https://gerrit.com/+/refs/heads/master/README.md', ), ).toThrow(/project/); expect(() => - parseGitilesUrl( + parseGerritGitilesUrl( config, 'https://gerrit.com/web/project/+/refs/changes/1/11/master/README.md', ), @@ -89,6 +91,71 @@ describe('gerrit core', () => { }); }); + describe('getGerritBranchApiUrl', () => { + it('can create an url for anonymous access.', () => { + const config: GerritIntegrationConfig = { + host: 'gerrit.com', + baseUrl: 'https://gerrit.com', + gitilesBaseUrl: 'https://gerrit.com', + }; + const fileContentUrl = getGerritBranchApiUrl( + config, + 'https://gerrit.com/web/project/+/refs/heads/master/README.md', + ); + expect(fileContentUrl).toEqual( + 'https://gerrit.com/projects/web%2Fproject/branches/master', + ); + }); + it('can create an url for authenticated access.', () => { + const authConfig: GerritIntegrationConfig = { + host: 'gerrit.com', + baseUrl: 'https://gerrit.com', + gitilesBaseUrl: 'https://gerrit.com', + username: 'u', + password: 'u', + }; + const authFileContentUrl = getGerritBranchApiUrl( + authConfig, + 'https://gerrit.com/web/project/+/refs/heads/master/README.md', + ); + expect(authFileContentUrl).toEqual( + 'https://gerrit.com/a/projects/web%2Fproject/branches/master', + ); + }); + }); + + describe('getGerritCloneRepoUrl', () => { + it('can create an url for anonymous clone.', () => { + const config: GerritIntegrationConfig = { + host: 'gerrit.com', + cloneUrl: 'https://gerrit.com/clone', + gitilesBaseUrl: 'https://gerrit.com', + }; + const fileContentUrl = getGerritCloneRepoUrl( + config, + 'https://gerrit.com/web/project/+/refs/heads/master/README.md', + ); + expect(fileContentUrl).toEqual('https://gerrit.com/clone/web/project'); + }); + it('can create an url for authenticated clone.', () => { + const authConfig: GerritIntegrationConfig = { + host: 'gerrit.com', + baseUrl: 'https://gerrit.com', + cloneUrl: 'https://gerrit.com/clone', + gitilesBaseUrl: 'https://gerrit.com', + username: 'u', + password: 'u', + }; + const authFileContentUrl = getGerritCloneRepoUrl( + authConfig, + 'https://gerrit.com/web/project/+/refs/heads/master/README.md', + ); + expect(authFileContentUrl).toEqual( + 'https://gerrit.com/clone/a/web/project', + ); + }); + }); + describe('getGerritFileContentsApiUrl', () => { it('can create an url for anonymous access to the file fetch api.', () => { const config: GerritIntegrationConfig = { diff --git a/packages/integration/src/gerrit/core.ts b/packages/integration/src/gerrit/core.ts index 7b90df262c..eb413a2608 100644 --- a/packages/integration/src/gerrit/core.ts +++ b/packages/integration/src/gerrit/core.ts @@ -18,12 +18,6 @@ import { GerritIntegrationConfig } from '.'; const GERRIT_BODY_PREFIX = ")]}'"; -type GitFile = { - branch: string; - filePath: string; - project: string; -}; - /** * Parse a Gitiles URL and return branch, file path and project. * @@ -41,17 +35,17 @@ type GitFile = { * the urls point to an actual Gitiles installation. * * Gitiles url: - * https://g.com/optional_path/{project}/+/refs/heads/{branch}/{filePath} + * https://g.com/optional_path/\{project\}/+/refs/heads/\{branch\}/\{filePath\} * * * @param url - An URL pointing to a file stored in git. * @public */ -export function parseGitilesUrl( +export function parseGerritGitilesUrl( config: GerritIntegrationConfig, url: string, -): GitFile { +): { branch: string; filePath: string; project: string } { const urlPath = url.replace(config.gitilesBaseUrl!, ''); const parts = urlPath.split('/').filter(p => !!p); @@ -94,6 +88,39 @@ export function getAuthenticationPrefix( return config.password ? '/a/' : '/'; } +/** + * Return the url to get branch info from the Gerrit API. + * + * @param config - A Gerrit provider config. + * @param url - An url pointing to a file in git. + * @public + */ +export function getGerritBranchApiUrl( + config: GerritIntegrationConfig, + url: string, +) { + const { branch, project } = parseGerritGitilesUrl(config, url); + + return `${config.baseUrl}${getAuthenticationPrefix( + config, + )}projects/${encodeURIComponent(project)}/branches/${branch}`; +} + +/** + * Return the url to clone the repo that is referenced by the url. + * + * @param url - An url pointing to a file in git. + * @public + */ +export function getGerritCloneRepoUrl( + config: GerritIntegrationConfig, + url: string, +) { + const { project } = parseGerritGitilesUrl(config, url); + + return `${config.cloneUrl}${getAuthenticationPrefix(config)}${project}`; +} + /** * Return the url to fetch the contents of a file using the Gerrit API. * @@ -105,7 +132,7 @@ export function getGerritFileContentsApiUrl( config: GerritIntegrationConfig, url: string, ) { - const { branch, filePath, project } = parseGitilesUrl(config, url); + const { branch, filePath, project } = parseGerritGitilesUrl(config, url); return `${config.baseUrl}${getAuthenticationPrefix( config, diff --git a/packages/integration/src/gerrit/index.ts b/packages/integration/src/gerrit/index.ts index 1c54a270ac..127c949c4e 100644 --- a/packages/integration/src/gerrit/index.ts +++ b/packages/integration/src/gerrit/index.ts @@ -19,10 +19,13 @@ export { readGerritIntegrationConfigs, } from './config'; export { + getGerritBranchApiUrl, + getGerritCloneRepoUrl, getGerritFileContentsApiUrl, getGerritProjectsApiUrl, getGerritRequestOptions, parseGerritJsonResponse, + parseGerritGitilesUrl, } from './core'; export type { GerritIntegrationConfig } from './config';