diff --git a/.changeset/red-squids-own.md b/.changeset/red-squids-own.md new file mode 100644 index 0000000000..4f6a1635d6 --- /dev/null +++ b/.changeset/red-squids-own.md @@ -0,0 +1,11 @@ +--- +'@backstage/integration': minor +--- + +The Gerrit integration can now resolve Gitiles urls that point to the following +refs: + +- HEAD +- A SHA +- Tag +- Branch diff --git a/packages/integration/api-report.md b/packages/integration/api-report.md index cf4b380ad9..e9b67bb6c1 100644 --- a/packages/integration/api-report.md +++ b/packages/integration/api-report.md @@ -766,6 +766,18 @@ export function parseGiteaUrl( path: string; }; +// @public +export function parseGitilesUrlRef( + config: GerritIntegrationConfig, + url: string, +): { + project: string; + path: string; + ref: string; + refType: 'sha' | 'branch' | 'tag' | 'head'; + basePath: string; +}; + // @public export function parseHarnessUrl( config: HarnessIntegrationConfig, diff --git a/packages/integration/src/gerrit/GerritIntegration.test.ts b/packages/integration/src/gerrit/GerritIntegration.test.ts index 1878744f68..2cfc748550 100644 --- a/packages/integration/src/gerrit/GerritIntegration.test.ts +++ b/packages/integration/src/gerrit/GerritIntegration.test.ts @@ -83,11 +83,10 @@ describe('GerritIntegration', () => { }); describe('resolves with a relative url', () => { - it('works for valid urls', () => { - const integration = new GerritIntegration({ - host: 'gerrit-review.example.com', - } as any); - + const integration = new GerritIntegration({ + host: 'gerrit-review.example.com', + } as any); + it('works for valid urls pointing to a branch', () => { expect( integration.resolveUrl({ url: './skeleton', @@ -97,15 +96,24 @@ describe('GerritIntegration', () => { 'https://gerrit-review.example.com/gerrit/plugins/repo/+/refs/heads/master/skeleton', ); }); + it('works for urls pointing to a tag', () => { + expect( + integration.resolveUrl({ + url: './skeleton.yaml', + base: 'https://gerrit-review.example.com/gerrit/plugins/repo/+/refs/tags/v.1.2.3/src/template.yaml', + }), + ).toBe( + 'https://gerrit-review.example.com/gerrit/plugins/repo/+/refs/tags/v.1.2.3/src/skeleton.yaml', + ); + }); }); 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); - + const integration = new GerritIntegration({ + host: 'gerrit-review.example.com', + gitilesBaseUrl: 'https://gerrit-review.example.com/gitiles', + } as any); + it('works for valid urls pointing to a branch', () => { expect( integration.resolveUrl({ url: '/catalog-info.yaml', @@ -115,6 +123,16 @@ describe('GerritIntegration', () => { 'https://gerrit-review.example.com/gitiles/repo/+/refs/heads/master/catalog-info.yaml', ); }); + it('works for urls pointing to a tag', () => { + expect( + integration.resolveUrl({ + url: '/skeleton.yaml', + base: 'https://gerrit-review.example.com/gerrit/plugins/repo/+/refs/tags/v.1.2.3/src/template.yaml', + }), + ).toBe( + 'https://gerrit-review.example.com/gerrit/plugins/repo/+/refs/tags/v.1.2.3/skeleton.yaml', + ); + }); }); it('resolve edit URL', () => { diff --git a/packages/integration/src/gerrit/GerritIntegration.ts b/packages/integration/src/gerrit/GerritIntegration.ts index 6ea426e407..7e2e757c6b 100644 --- a/packages/integration/src/gerrit/GerritIntegration.ts +++ b/packages/integration/src/gerrit/GerritIntegration.ts @@ -20,7 +20,7 @@ import { GerritIntegrationConfig, readGerritIntegrationConfigs, } from './config'; -import { parseGerritGitilesUrl, buildGerritGitilesUrl } from './core'; +import { parseGitilesUrlRef } from './core'; /** * A Gerrit based integration. @@ -60,8 +60,8 @@ export class GerritIntegration implements ScmIntegration { const { url, base, lineNumber } = options; let updated; if (url.startsWith('/')) { - const { branch, project } = parseGerritGitilesUrl(this.config, base); - return buildGerritGitilesUrl(this.config, project, branch, url); + const { basePath } = parseGitilesUrlRef(this.config, base); + return basePath + url; } if (url) { updated = new URL(url, base); diff --git a/packages/integration/src/gerrit/core.test.ts b/packages/integration/src/gerrit/core.test.ts index 35632c654e..c031612c40 100644 --- a/packages/integration/src/gerrit/core.test.ts +++ b/packages/integration/src/gerrit/core.test.ts @@ -26,6 +26,7 @@ import { getGerritCloneRepoUrl, getGerritRequestOptions, parseGerritJsonResponse, + parseGitilesUrlRef, parseGerritGitilesUrl, getGerritFileContentsApiUrl, } from './core'; @@ -148,8 +149,129 @@ describe('gerrit core', () => { ).toBeUndefined(); }); }); - - describe('parseGitilesUrl', () => { + describe('parseGitilesUrlRef', () => { + const config: GerritIntegrationConfig = { + host: 'gerrit.com', + gitilesBaseUrl: 'https://gerrit.googlesource.com', + }; + it('can parse a gitiles urls that points to specific sha.', () => { + const gitUrl = parseGitilesUrlRef( + config, + 'https://gerrit.googlesource.com/modules/cached-refdb/+/157f862803d45b9d269f0e390f88aece1ded51e8/Jenkinsfile', + ); + expect(gitUrl).toEqual({ + basePath: + 'https://gerrit.googlesource.com/modules/cached-refdb/+/157f862803d45b9d269f0e390f88aece1ded51e8', + path: 'Jenkinsfile', + project: 'modules/cached-refdb', + ref: '157f862803d45b9d269f0e390f88aece1ded51e8', + refType: 'sha', + }); + }); + it('can parse gitiles urls that points to tags.', () => { + const gitUrl = parseGitilesUrlRef( + config, + 'https://gerrit.googlesource.com/modules/events-broker/+/refs/tags/v3.5.6/src/main/java/com/gerritforge/gerrit/eventbroker/BrokerApi.java', + ); + expect(gitUrl).toEqual({ + basePath: + 'https://gerrit.googlesource.com/modules/events-broker/+/refs/tags/v3.5.6', + path: 'src/main/java/com/gerritforge/gerrit/eventbroker/BrokerApi.java', + project: 'modules/events-broker', + ref: 'v3.5.6', + refType: 'tag', + }); + }); + it('can parse gitiles urls that points to HEAD.', () => { + const gitUrl = parseGitilesUrlRef( + config, + 'https://gerrit.googlesource.com/modules/events-broker/+/HEAD/src/main/java/com/gerritforge/gerrit/eventbroker/BrokerApi.java', + ); + expect(gitUrl).toEqual({ + basePath: + 'https://gerrit.googlesource.com/modules/events-broker/+/HEAD', + path: 'src/main/java/com/gerritforge/gerrit/eventbroker/BrokerApi.java', + project: 'modules/events-broker', + ref: 'HEAD', + refType: 'head', + }); + }); + it('can parse gitiles urls that points to HEAD without path.', () => { + const gitUrl = parseGitilesUrlRef( + config, + 'https://gerrit.googlesource.com/modules/events-broker/+/HEAD', + ); + expect(gitUrl).toEqual({ + basePath: + 'https://gerrit.googlesource.com/modules/events-broker/+/HEAD', + path: '/', + project: 'modules/events-broker', + ref: 'HEAD', + refType: 'head', + }); + }); + it('can parse gitiles urls that points to branches.', () => { + const gitUrl = parseGitilesUrlRef( + config, + 'https://gerrit.googlesource.com/modules/events-broker/+/refs/heads/master/src/main/java/com/gerritforge/gerrit/eventbroker/BrokerApiModule.java', + ); + expect(gitUrl).toEqual({ + basePath: + 'https://gerrit.googlesource.com/modules/events-broker/+/refs/heads/master', + path: 'src/main/java/com/gerritforge/gerrit/eventbroker/BrokerApiModule.java', + project: 'modules/events-broker', + ref: 'master', + refType: 'branch', + }); + }); + it('can parse gitiles urls that points directly to a branch without a path.', () => { + const gitUrl = parseGitilesUrlRef( + config, + 'https://gerrit.googlesource.com/modules/events-broker/+/refs/heads/master', + ); + expect(gitUrl).toEqual({ + basePath: + 'https://gerrit.googlesource.com/modules/events-broker/+/refs/heads/master', + path: '/', + project: 'modules/events-broker', + ref: 'master', + refType: 'branch', + }); + }); + it('can parse gitiles urls that points to the repo root.', () => { + const gitUrl = parseGitilesUrlRef( + config, + 'https://gerrit.googlesource.com/modules/events-broker/+/refs/heads/master/', + ); + expect(gitUrl).toEqual({ + basePath: + 'https://gerrit.googlesource.com/modules/events-broker/+/refs/heads/master', + path: '/', + project: 'modules/events-broker', + ref: 'master', + refType: 'branch', + }); + }); + it('can parse a valid authenticated gitiles url.', () => { + const gitilesConfig: GerritIntegrationConfig = { + host: 'gerrit.com', + gitilesBaseUrl: 'https://gerrit.com/gitiles', + }; + const gitUrl = parseGitilesUrlRef( + gitilesConfig, + 'https://gerrit.com/a/gitiles/web/project/+/refs/heads/master/README.md', + ); + expect(gitUrl).toEqual({ + basePath: + 'https://gerrit.com/a/gitiles/web/project/+/refs/heads/master', + path: 'README.md', + project: 'web/project', + ref: 'master', + refType: 'branch', + }); + }); + }); + describe('parseGerritGitilesUrl', () => { it('can parse a valid gitiles urls.', () => { const config: GerritIntegrationConfig = { host: 'gerrit.com', diff --git a/packages/integration/src/gerrit/core.ts b/packages/integration/src/gerrit/core.ts index b3bdbc30e4..e50ce97bbb 100644 --- a/packages/integration/src/gerrit/core.ts +++ b/packages/integration/src/gerrit/core.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { trimStart } from 'lodash'; +import { join, takeWhile, trimEnd, trimStart } from 'lodash'; import { GerritIntegrationConfig } from './config'; const GERRIT_BODY_PREFIX = ")]}'"; @@ -81,6 +81,112 @@ export function parseGerritGitilesUrl( }; } +/** + * Parses Gitiles urls and returns the following: + * + * - The project + * - The type of ref. I.e: branch name, SHA, HEAD or tag. + * - The file path from the repo root. + * - The base path as the path that points to the repo root. + * + * Supported types of gitiles urls that point to: + * + * - Branches + * - Tags + * - A commit SHA + * - HEAD + * + * @param config - A Gerrit provider config. + * @param url - An url to a file or folder in Gitiles. + * @public + */ +export function parseGitilesUrlRef( + config: GerritIntegrationConfig, + url: string, +): { + project: string; + path: string; + ref: string; + refType: 'sha' | 'branch' | 'tag' | 'head'; + basePath: string; +} { + const baseUrlParse = new URL(config.gitilesBaseUrl!); + const urlParse = new URL(url); + // Remove the gerrit authentication prefix '/a/' from the url + // In case of the gitilesBaseUrl is https://review.gerrit.com/plugins/gitiles + // and the url provided is https://review.gerrit.com/a/plugins/gitiles/... + // remove the prefix only if the pathname start with '/a/' + const urlPath = trimStart( + urlParse.pathname + .substring(urlParse.pathname.startsWith('/a/') ? 2 : 0) + .replace(baseUrlParse.pathname, ''), + '/', + ); + + // Find the project by taking everything up to "/+/". + const parts = urlPath.split('/').filter(p => !!p); + const projectParts = takeWhile(parts, p => p !== '+'); + if (projectParts.length === 0) { + throw new Error(`Unable to parse gitiles url: ${url}`); + } + // Also remove the "+" after the project. + const rest = parts.slice(projectParts.length + 1); + const project = join(projectParts, '/'); + + // match /+/HEAD/ + if (rest.length > 0 && rest[0] === 'HEAD') { + const ref = rest.shift()!; + const path = join(rest, '/'); + return { + project, + ref, + refType: 'head' as const, + path: path || '/', + basePath: trimEnd(url.replace(path, ''), '/'), + }; + } + // match /+// + if (rest.length > 0 && rest[0].length === 40) { + const ref = rest.shift()!; + const path = join(rest, '/'); + return { + project, + ref, + refType: 'sha' as const, + path: path || '/', + basePath: trimEnd(url.replace(path, ''), '/'), + }; + } + const remainingPath = join(rest, '/'); + // Regexp for matching "refs/tags/" or "refs/heads//" + const refsRegexp = /^refs\/(?heads|tags)\/(?.*?)(\/|$)/; + const result = refsRegexp.exec(remainingPath); + if (result) { + const matchString = result[0]; + let refType; + const { refsReference, ref } = result.groups || {}; + const path = remainingPath.replace(matchString, ''); + switch (refsReference) { + case 'heads': + refType = 'branch' as const; + break; + case 'tags': + refType = 'tag' as const; + break; + default: + throw new Error(`Unable to parse gitiles url: ${url}`); + } + return { + project, + ref, + refType, + path: path || '/', + basePath: trimEnd(url.replace(path, ''), '/'), + }; + } + throw new Error(`Unable to parse gitiles : ${url}`); +} + /** * Build a Gerrit Gitiles url that targets a specific path. * diff --git a/packages/integration/src/gerrit/index.ts b/packages/integration/src/gerrit/index.ts index 6c6295f7a3..f2e5934c95 100644 --- a/packages/integration/src/gerrit/index.ts +++ b/packages/integration/src/gerrit/index.ts @@ -27,6 +27,7 @@ export { getGerritRequestOptions, parseGerritJsonResponse, parseGerritGitilesUrl, + parseGitilesUrlRef, } from './core'; export type { GerritIntegrationConfig } from './config';