integration: add support for resolving a URL to a line in a file

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2021-03-10 22:26:07 +01:00
parent cbb85fd86e
commit d4e77ec5f9
12 changed files with 130 additions and 11 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/integration': patch
---
Add option to `resolveUrl` that allows for linking to a specific line number when resolving a file URL.
+5 -1
View File
@@ -83,7 +83,11 @@ export class ScmIntegrations implements ScmIntegrationRegistry {
.find(Boolean);
}
resolveUrl(options: { url: string; base: string }): string {
resolveUrl(options: {
url: string;
base: string;
lineNumber?: number;
}): string {
const integration = this.byUrl(options.base);
if (!integration) {
return defaultScmResolveUrl(options);
@@ -63,9 +63,10 @@ describe('AzureIntegration', () => {
url: '/a.yaml',
base:
'https://dev.azure.com/organization/project/_git/repository?path=%2Ffolder%2Fcatalog-info.yaml',
lineNumber: 14,
}),
).toBe(
'https://dev.azure.com/organization/project/_git/repository?path=%2Fa.yaml',
'https://dev.azure.com/organization/project/_git/repository?path=%2Fa.yaml&line=14&lineEnd=15&lineStartColumn=1&lineEndColumn=1',
);
expect(
@@ -49,7 +49,11 @@ export class AzureIntegration implements ScmIntegration {
*
* Example base URL: https://dev.azure.com/organization/project/_git/repository?path=%2Fcatalog-info.yaml
*/
resolveUrl(options: { url: string; base: string }): string {
resolveUrl(options: {
url: string;
base: string;
lineNumber?: number;
}): string {
const { url, base } = options;
// If we can parse the url, it is absolute - then return it verbatim
@@ -72,6 +76,13 @@ export class AzureIntegration implements ScmIntegration {
const newUrl = new URL(base);
newUrl.searchParams.set('path', updatedPath);
if (options.lineNumber) {
newUrl.searchParams.set('line', String(options.lineNumber));
newUrl.searchParams.set('lineEnd', String(options.lineNumber + 1));
newUrl.searchParams.set('lineStartColumn', '1');
newUrl.searchParams.set('lineEndColumn', '1');
}
return newUrl.toString();
}
@@ -45,6 +45,20 @@ describe('BitbucketIntegration', () => {
expect(integration.title).toBe('h.com');
});
it('resolves url line number correctly', () => {
const integration = new BitbucketIntegration({ host: 'h.com' } as any);
expect(
integration.resolveUrl({
url: './a.yaml',
base: 'https://bitbucket.org/my-owner/my-project/src/master/README.md',
lineNumber: 14,
}),
).toBe(
'https://bitbucket.org/my-owner/my-project/src/master/a.yaml#a.yaml-14',
);
});
it('resolve edit URL', () => {
const integration = new BitbucketIntegration({ host: 'h.com' } as any);
@@ -49,8 +49,23 @@ export class BitbucketIntegration implements ScmIntegration {
return this.integrationConfig;
}
resolveUrl(options: { url: string; base: string }): string {
return defaultScmResolveUrl(options);
resolveUrl(options: {
url: string;
base: string;
lineNumber?: number;
}): string {
const resolved = defaultScmResolveUrl(options);
// Bitbucket line numbers use the syntax #example.txt-42, rather than #L42
if (options.lineNumber) {
const url = new URL(resolved);
const filename = url.pathname.split('/').slice(-1)[0];
url.hash = `${filename}-${options.lineNumber}`;
return url.toString();
}
return resolved;
}
resolveEditUrl(url: string): string {
@@ -58,8 +58,9 @@ describe('GitHubIntegration', () => {
url: '../a.yaml',
base:
'https://github.com/backstage/backstage/blob/master/test/README.md',
lineNumber: 17,
}),
).toBe('https://github.com/backstage/backstage/tree/master/a.yaml');
).toBe('https://github.com/backstage/backstage/tree/master/a.yaml#L17');
expect(
integration.resolveUrl({
@@ -46,7 +46,11 @@ export class GitHubIntegration implements ScmIntegration {
return this.integrationConfig;
}
resolveUrl(options: { url: string; base: string }): string {
resolveUrl(options: {
url: string;
base: string;
lineNumber?: number;
}): string {
// GitHub uses blob URLs for files and tree urls for directory listings. But
// there is a redirect from tree to blob for files, so we can always return
// tree urls here.
@@ -46,7 +46,11 @@ export class GitLabIntegration implements ScmIntegration {
return this.integrationConfig;
}
resolveUrl(options: { url: string; base: string }): string {
resolveUrl(options: {
url: string;
base: string;
lineNumber?: number;
}): string {
return defaultScmResolveUrl(options);
}
+46
View File
@@ -107,6 +107,52 @@ describe('defaultScmResolveUrl', () => {
);
});
it('works in various situations with line numbers', () => {
expect(
defaultScmResolveUrl({
url: './b.yaml',
base:
'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/folder/a.yaml?at=master',
lineNumber: 11,
}),
).toBe(
'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/folder/b.yaml?at=master#L11',
);
expect(
defaultScmResolveUrl({
url: 'b.yaml',
base:
'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/folder/a.yaml',
lineNumber: 12,
}),
).toBe(
'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/folder/b.yaml#L12',
);
expect(
defaultScmResolveUrl({
url: '/other/b.yaml',
base:
'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/folder/a.yaml',
lineNumber: 13,
}),
).toBe(
'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/other/b.yaml#L13',
);
expect(
defaultScmResolveUrl({
url: '/other/b.yaml',
base:
'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/folder/a.yaml?at=master',
lineNumber: 14,
}),
).toBe(
'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/other/b.yaml?at=master#L14',
);
});
it('works for full urls and throws away query params', () => {
expect(
defaultScmResolveUrl({
+5 -1
View File
@@ -60,8 +60,9 @@ export function basicIntegrations<T extends ScmIntegration>(
export function defaultScmResolveUrl(options: {
url: string;
base: string;
lineNumber?: number;
}): string {
const { url, base } = options;
const { url, base, lineNumber } = options;
// If it is a fully qualified URL - then return it verbatim
try {
@@ -90,5 +91,8 @@ export function defaultScmResolveUrl(options: {
}
updated.search = new URL(base).search;
if (lineNumber) {
updated.hash = `L${lineNumber}`;
}
return updated.toString();
}
+12 -2
View File
@@ -49,8 +49,13 @@ export interface ScmIntegration {
*
* @param options.url The (absolute or relative) URL or path to resolve
* @param options.base The base URL onto which this resolution happens
* @param options.lineNumber The line number in the target file to link to, starting with 1. Only applicable when linking to files.
*/
resolveUrl(options: { url: string; base: string }): string;
resolveUrl(options: {
url: string;
base: string;
lineNumber?: number;
}): string;
/**
* Resolves the edit URL for a file within the SCM system.
@@ -114,8 +119,13 @@ export interface ScmIntegrationRegistry
*
* @param options.url The (absolute or relative) URL or path to resolve
* @param options.base The base URL onto which this resolution happens
* @param options.lineNumber The line number in the target file to link to, starting with 1. Only applicable when linking to files.
*/
resolveUrl(options: { url: string; base: string }): string;
resolveUrl(options: {
url: string;
base: string;
lineNumber?: number;
}): string;
/**
* Resolves the edit URL for a file within the SCM system.