feat(gerrit-integration): Update the gerrit url resolver

Currently the only Gitiles url that can be resolved is urls
that point to the tip of a specific branch.

This change adds support for resolving urls that points to the
following refs:

* HEAD
* A commit sha
* Tag

The old Gitiles url parser may be deprecated.

Signed-off-by: Niklas Aronsson <niklasar@axis.com>
This commit is contained in:
Niklas Aronsson
2024-02-08 12:16:48 +01:00
parent 75ac0a8703
commit 1573014554
7 changed files with 287 additions and 17 deletions
+11
View File
@@ -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
+12
View File
@@ -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,
@@ -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', () => {
@@ -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);
+124 -2
View File
@@ -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',
+107 -1
View File
@@ -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 <project>/+/HEAD/<path>
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 <project>/+/<sha>/<path>
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/<tag>" or "refs/heads/<branch>/"
const refsRegexp = /^refs\/(?<refsReference>heads|tags)\/(?<ref>.*?)(\/|$)/;
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.
*
+1
View File
@@ -27,6 +27,7 @@ export {
getGerritRequestOptions,
parseGerritJsonResponse,
parseGerritGitilesUrl,
parseGitilesUrlRef,
} from './core';
export type { GerritIntegrationConfig } from './config';