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:
@@ -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.
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user