GerritUrlReader: Implemented "readTree"
"readTree" has been implemented for the "GerritUrlReader". Gerrit have a REST API's to download repo contents but there are a number of limitations that makes it unusable. This implementation works as follows: * The project and branch is parsed from the url. * The current revision is fetched from the Gerrit REST API. * The revision string is used as "etag". * If the etag has changed a temporary directory is created. * The project is cloned to the temporary directory. * The cloned content is read into a "Readable Stream". * The temporary directory is removed. * "readTree" returns a response using "fromTarArchive" as read from the temporary directory. Also added an option to specify the base "cloneUrl" has been added to the gerrit integration config. Signed-off-by: Niklas Aronsson <niklasar@axis.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/backend-common': patch
|
||||
---
|
||||
|
||||
Gerrit integration: Added optional `cloneUrl` string to config.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/integration': minor
|
||||
---
|
||||
|
||||
Gerrit UrlReader: Implemented `readTree`
|
||||
@@ -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
|
||||
|
||||
@@ -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<Buffer>;
|
||||
// (undocumented)
|
||||
readTree(): Promise<ReadTreeResponse>;
|
||||
readTree(url: string, options?: ReadTreeOptions): Promise<ReadTreeResponse>;
|
||||
// (undocumented)
|
||||
readUrl(url: string, options?: ReadUrlOptions): Promise<ReadUrlResponse>;
|
||||
// (undocumented)
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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<string> =>
|
||||
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<Buffer> {
|
||||
const response = await this.readUrl(url);
|
||||
@@ -109,8 +138,72 @@ export class GerritUrlReader implements UrlReader {
|
||||
);
|
||||
}
|
||||
|
||||
async readTree(): Promise<ReadTreeResponse> {
|
||||
throw new Error('GerritReader does not implement readTree');
|
||||
async readTree(
|
||||
url: string,
|
||||
options?: ReadTreeOptions,
|
||||
): Promise<ReadTreeResponse> {
|
||||
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<Buffer>(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<SearchResponse> {
|
||||
|
||||
@@ -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"}
|
||||
@@ -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<string, string>;
|
||||
};
|
||||
|
||||
// @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<GitLabIntegration>;
|
||||
}
|
||||
|
||||
// @public
|
||||
export function parseGerritGitilesUrl(
|
||||
config: GerritIntegrationConfig,
|
||||
url: string,
|
||||
): {
|
||||
branch: string;
|
||||
filePath: string;
|
||||
project: string;
|
||||
};
|
||||
|
||||
// @public
|
||||
export function parseGerritJsonResponse(response: Response): Promise<unknown>;
|
||||
|
||||
|
||||
Vendored
+5
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -19,10 +19,13 @@ export {
|
||||
readGerritIntegrationConfigs,
|
||||
} from './config';
|
||||
export {
|
||||
getGerritBranchApiUrl,
|
||||
getGerritCloneRepoUrl,
|
||||
getGerritFileContentsApiUrl,
|
||||
getGerritProjectsApiUrl,
|
||||
getGerritRequestOptions,
|
||||
parseGerritJsonResponse,
|
||||
parseGerritGitilesUrl,
|
||||
} from './core';
|
||||
|
||||
export type { GerritIntegrationConfig } from './config';
|
||||
|
||||
Reference in New Issue
Block a user