Implement readTree on BitBucketUrlReader - Add getBitbucketDefaultBranch

This commit is contained in:
Kim S. Ly
2020-12-15 00:48:03 -05:00
parent 1d1c2860f1
commit bbd0e354ff
6 changed files with 177 additions and 42 deletions
@@ -127,7 +127,7 @@ describe('BitbucketUrlReader', () => {
);
const response = await processor.readTree(
'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/docs',
'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/docs?at=some-branch',
);
const files = await response.files();
@@ -138,19 +138,6 @@ describe('BitbucketUrlReader', () => {
expect(indexMarkdownFile.toString()).toBe('# Test\n');
});
it('must specify a branch', async () => {
const processor = new BitbucketUrlReader(
{ host: 'bitbucket.org', apiBaseUrl: 'https://api.bitbucket.org/2.0' },
{ treeResponseFactory },
);
await expect(
processor.readTree('https://bitbucket.org/backstage/mock'),
).rejects.toThrow(
'Bitbucket URL must contain branch to be able to fetch tree',
);
});
it('returns the wanted files from an archive with a subpath', async () => {
worker.use(
rest.get(
@@ -16,6 +16,7 @@
import {
BitbucketIntegrationConfig,
getBitbucketDefaultBranch,
getBitbucketDownloadUrl,
getBitbucketFileFetchUrl,
getBitbucketRequestOptions,
@@ -24,7 +25,7 @@ import {
import fetch from 'cross-fetch';
import parseGitUri from 'git-url-parse';
import { Readable } from 'stream';
import { InputError, NotFoundError } from '../errors';
import { NotFoundError } from '../errors';
import { ReadTreeResponseFactory } from './tree';
import {
ReaderFactory,
@@ -101,19 +102,13 @@ export class BitbucketUrlReader implements UrlReader {
options?: ReadTreeOptions,
): Promise<ReadTreeResponse> {
const gitUrl: parseGitUri.GitUrl = parseGitUri(url);
const { name: repoName, owner: project, ref, resource, filepath } = gitUrl;
const { name: repoName, owner: project, resource, filepath } = gitUrl;
const isHosted = resource === 'bitbucket.org';
if (isHosted && !ref) {
// TODO(freben): We should add support for defaulting to the default branch
throw new InputError(
'Bitbucket URL must contain branch to be able to fetch tree',
);
}
const downloadUrl = await getBitbucketDownloadUrl(url, this.config);
const response = await fetch(
getBitbucketDownloadUrl(url, this.config),
downloadUrl,
getBitbucketRequestOptions(this.config),
);
if (!response.ok) {
@@ -126,7 +121,7 @@ export class BitbucketUrlReader implements UrlReader {
let folderPath = `${project}-${repoName}`;
if (isHosted) {
const lastCommitShortHash = await this.getLastCommitShortHash(gitUrl);
const lastCommitShortHash = await this.getLastCommitShortHash(url);
folderPath = `${project}-${repoName}-${lastCommitShortHash}`;
}
@@ -146,12 +141,13 @@ export class BitbucketUrlReader implements UrlReader {
return `bitbucket{host=${host},authed=${authed}}`;
}
private async getLastCommitShortHash(
gitUrl: parseGitUri.GitUrl,
): Promise<String> {
const { name: repoName, owner: project, ref } = gitUrl;
private async getLastCommitShortHash(url: string): Promise<String> {
const { name: repoName, owner: project, ref } = parseGitUri(url);
const branch = ref ? ref : 'master';
let branch = ref;
if (!branch) {
branch = await getBitbucketDefaultBranch(url, this.config);
}
const commitsApiUrl = `${this.config.apiBaseUrl}/repositories/${project}/${repoName}/commits/${branch}`;
const commitsResponse = await fetch(
+1
View File
@@ -35,6 +35,7 @@
},
"devDependencies": {
"@backstage/cli": "^0.4.1",
"@backstage/test-utils": "^0.1.5",
"@types/jest": "^26.0.7",
"msw": "^0.21.2"
},
+115 -8
View File
@@ -14,14 +14,21 @@
* limitations under the License.
*/
import { rest } from 'msw';
import { setupServer } from 'msw/node';
import { msw } from '@backstage/test-utils';
import { BitbucketIntegrationConfig } from './config';
import {
getBitbucketDefaultBranch,
getBitbucketDownloadUrl,
getBitbucketFileFetchUrl,
getBitbucketRequestOptions,
} from './core';
describe('bitbucket core', () => {
const worker = setupServer();
msw.setupDefaultHandlers(worker);
describe('getBitbucketRequestOptions', () => {
it('inserts a token when needed', () => {
const withToken: BitbucketIntegrationConfig = {
@@ -103,40 +110,84 @@ describe('bitbucket core', () => {
});
describe('getBitbucketDownloadUrl', () => {
it('add path param if a path is specified', () => {
it('add path param if a path is specified for Bitbucket Server', async () => {
const defaultBranchResponse = {
displayId: 'main',
};
worker.use(
rest.get(
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/branches/default',
(_, res, ctx) =>
res(
ctx.status(200),
ctx.set('Content-Type', 'application/json'),
ctx.json(defaultBranchResponse),
),
),
);
const config: BitbucketIntegrationConfig = {
host: 'bitbucket.mycompany.net',
apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0',
};
const result = getBitbucketDownloadUrl(
const result = await getBitbucketDownloadUrl(
'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/docs',
config,
);
expect(result).toEqual(
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/archive?format=zip&prefix=backstage-mock&path=docs',
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/archive?format=zip&at=main&prefix=backstage-mock&path=docs',
);
});
it('do not add path param if no path is specified', () => {
it('do not add path param if no path is specified for Bitbucket Server', async () => {
const defaultBranchResponse = {
displayId: 'main',
};
worker.use(
rest.get(
// TODO: Change URL to https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/branches/default when git-url-parse bug is fixed
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/projects/backstage/repos/mock/repos/browse/branches/default',
(_, res, ctx) =>
res(
ctx.status(200),
ctx.set('Content-Type', 'application/json'),
ctx.json(defaultBranchResponse),
),
),
);
const config: BitbucketIntegrationConfig = {
host: 'bitbucket.mycompany.net',
apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0',
};
const result = getBitbucketDownloadUrl(
'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse',
const result = await getBitbucketDownloadUrl(
'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse?at=main',
config,
);
expect(new URL(result).searchParams.get('format')).toEqual('zip');
expect(new URL(result).searchParams.get('at')).toEqual('main');
expect(new URL(result).searchParams.get('prefix')).not.toBeNull();
expect(new URL(result).searchParams.get('path')).toBeNull();
});
it('do not add path param if the repository is hosted on bitbucket.org', () => {
it('get by branch for Bitbucket Server', async () => {
const config: BitbucketIntegrationConfig = {
host: 'bitbucket.mycompany.net',
apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0',
};
const result = await getBitbucketDownloadUrl(
'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/docs?at=some-branch',
config,
);
expect(result).toEqual(
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/archive?format=zip&at=some-branch&prefix=backstage-mock&path=docs',
);
});
it('do not add path param for Bitbucket Cloud', async () => {
const config: BitbucketIntegrationConfig = {
host: 'bitbucket.org',
apiBaseUrl: 'https://api.bitbucket.org/2.0',
};
const result = getBitbucketDownloadUrl(
const result = await getBitbucketDownloadUrl(
'https://bitbucket.org/backstage/mock/src/master',
config,
);
@@ -145,4 +196,60 @@ describe('bitbucket core', () => {
);
});
});
describe('getBitbucketDefaultBranch', () => {
it('return default branch for Bitbucket Cloud', async () => {
const repoInfoResponse = {
mainbranch: {
name: 'main',
},
};
worker.use(
rest.get(
'https://api.bitbucket.org/2.0/repositories/backstage/mock',
(_, res, ctx) =>
res(
ctx.status(200),
ctx.set('Content-Type', 'application/json'),
ctx.json(repoInfoResponse),
),
),
);
const config: BitbucketIntegrationConfig = {
host: 'bitbucket.org',
apiBaseUrl: 'https://api.bitbucket.org/2.0',
};
const defaultBranch = await getBitbucketDefaultBranch(
'https://bitbucket.org/backstage/mock/src/main',
config,
);
expect(defaultBranch).toEqual('main');
});
it('return default branch for Bitbucket Server', async () => {
const defaultBranchResponse = {
displayId: 'main',
};
worker.use(
rest.get(
'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/branches/default',
(_, res, ctx) =>
res(
ctx.status(200),
ctx.set('Content-Type', 'application/json'),
ctx.json(defaultBranchResponse),
),
),
);
const config: BitbucketIntegrationConfig = {
host: 'bitbucket.mycompany.net',
apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0',
};
const defaultBranch = await getBitbucketDefaultBranch(
'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/README.md',
config,
);
expect(defaultBranch).toEqual('main');
});
});
});
+47 -4
View File
@@ -14,19 +14,58 @@
* limitations under the License.
*/
import fetch from 'cross-fetch';
import parseGitUrl from 'git-url-parse';
import { BitbucketIntegrationConfig } from './config';
/**
* Given a URL pointing to a path on a provider, returns the default branch.
*
* @param url A URL pointing to a path
* @param config The relevant provider config
*/
export async function getBitbucketDefaultBranch(
url: string,
config: BitbucketIntegrationConfig,
): Promise<string> {
const { name: repoName, owner: project, resource } = parseGitUrl(url);
const isHosted = resource === 'bitbucket.org';
const branchUrl = isHosted
? `${config.apiBaseUrl}/repositories/${project}/${repoName}`
: `${config.apiBaseUrl}/projects/${project}/repos/${repoName}/branches/default`;
const response = await fetch(branchUrl, getBitbucketRequestOptions(config));
if (!response.ok) {
const message = `Failed to retrieve default branch from ${branchUrl}, ${response.status} ${response.statusText}`;
throw new Error(message);
}
let defaultBranch;
if (isHosted) {
const repoInfo = await response.json();
defaultBranch = repoInfo.mainbranch.name;
} else {
const { displayId } = await response.json();
defaultBranch = displayId;
}
if (!defaultBranch) {
throw new Error(`Failed to read default branch from ${branchUrl}`);
}
return defaultBranch;
}
/**
* Given a URL pointing to a path on a provider, returns a URL that is suitable
* for downloading the subtree.
*
* @param url A URL pointing to a path
* @param config The relevant provider config
*/
export function getBitbucketDownloadUrl(
export async function getBitbucketDownloadUrl(
url: string,
config: BitbucketIntegrationConfig,
): string {
): Promise<string> {
const {
name: repoName,
owner: project,
@@ -38,13 +77,17 @@ export function getBitbucketDownloadUrl(
const isHosted = resource === 'bitbucket.org';
let branch = ref;
if (!branch) {
branch = await getBitbucketDefaultBranch(url, config);
}
// path will limit the downloaded content
// /docs will only download the docs folder and everything below it
// /docs/index.md will download the docs folder and everything below it
const path = filepath ? `&path=${encodeURIComponent(filepath)}` : '';
const archiveUrl = isHosted
? `${protocol}://${resource}/${project}/${repoName}/get/${ref}.zip`
: `${config.apiBaseUrl}/projects/${project}/repos/${repoName}/archive?format=zip&prefix=${project}-${repoName}${path}`;
? `${protocol}://${resource}/${project}/${repoName}/get/${branch}.zip`
: `${config.apiBaseUrl}/projects/${project}/repos/${repoName}/archive?format=zip&at=${branch}&prefix=${project}-${repoName}${path}`;
return archiveUrl;
}
@@ -20,6 +20,7 @@ export {
} from './config';
export type { BitbucketIntegrationConfig } from './config';
export {
getBitbucketDefaultBranch,
getBitbucketDownloadUrl,
getBitbucketFileFetchUrl,
getBitbucketRequestOptions,