Merge pull request #7134 from backstage/rugvip/azure

integrations,backend-common: Fix azure URL parsing and readTree/search
This commit is contained in:
Ben Lambert
2021-09-10 13:53:35 +02:00
committed by GitHub
10 changed files with 494 additions and 165 deletions
+7
View File
@@ -0,0 +1,7 @@
---
'@backstage/integration': patch
---
Fix Azure URL handling to properly support both repo shorthand (`/owner/_git/project`) and full URLs (`/owner/project/_git/repo`).
Fix Azure DevOps Server URL handling by being able to parse URLs with hosts other than `dev.azure.com`. Note that the `api-version` used for API requests is currently `6.0`, meaning you need to support at least this version in your Azure DevOps Server instance.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-common': patch
---
Fix Azure `readTree` and `search` handling to properly support paths.
@@ -81,14 +81,14 @@ describe('AzureUrlReader', () => {
url: 'https://dev.azure.com/org-name/project-name/_git/repo-name?path=my-template.yaml&version=GBmaster',
config: createConfig(),
response: expect.objectContaining({
url: 'https://dev.azure.com/org-name/project-name/_apis/git/repositories/repo-name/items?path=my-template.yaml&version=master',
url: 'https://dev.azure.com/org-name/project-name/_apis/git/repositories/repo-name/items?api-version=6.0&path=my-template.yaml&version=master',
}),
},
{
url: 'https://dev.azure.com/org-name/project-name/_git/repo-name?path=my-template.yaml',
config: createConfig(),
response: expect.objectContaining({
url: 'https://dev.azure.com/org-name/project-name/_apis/git/repositories/repo-name/items?path=my-template.yaml',
url: 'https://dev.azure.com/org-name/project-name/_apis/git/repositories/repo-name/items?api-version=6.0&path=my-template.yaml',
}),
},
{
@@ -125,14 +125,12 @@ describe('AzureUrlReader', () => {
{
url: 'https://api.com/a/b/blob/master/path/to/c.yaml',
config: createConfig(),
error:
'Incorrect URL: https://api.com/a/b/blob/master/path/to/c.yaml, Error: Wrong Azure Devops URL or Invalid file path',
error: 'Azure URL must point to a git repository',
},
{
url: 'com/a/b/blob/master/path/to/c.yaml',
config: createConfig(),
error:
'Incorrect URL: com/a/b/blob/master/path/to/c.yaml, TypeError: Invalid URL: com/a/b/blob/master/path/to/c.yaml',
error: 'Invalid URL: com/a/b/blob/master/path/to/c.yaml',
},
{
url: '',
@@ -23,11 +23,9 @@ import {
ScmIntegrations,
} from '@backstage/integration';
import fetch from 'cross-fetch';
import parseGitUrl from 'git-url-parse';
import { Minimatch } from 'minimatch';
import { Readable } from 'stream';
import { NotFoundError, NotModifiedError } from '@backstage/errors';
import { stripFirstDirectoryFromPath } from './tree/util';
import {
ReadTreeResponseFactory,
ReaderFactory,
@@ -129,28 +127,38 @@ export class AzureUrlReader implements UrlReader {
throw new Error(message);
}
// When downloading a zip archive from azure on a subpath we get an extra directory
// layer added at the top. With for example the file /a/b/c.txt and a download of
// /a/b, we'll see /b/c.txt in the zip archive. This picks out /b so that we can remove it.
let subpath;
const path = new URL(url).searchParams.get('path');
if (path) {
subpath = path.split('/').filter(Boolean).slice(-1)[0];
}
return await this.deps.treeResponseFactory.fromZipArchive({
stream: archiveAzureResponse.body as unknown as Readable,
etag: commitSha,
filter: options?.filter,
subpath,
});
}
async search(url: string, options?: SearchOptions): Promise<SearchResponse> {
const { filepath } = parseGitUrl(url);
const matcher = new Minimatch(filepath);
const treeUrl = new URL(url);
const path = treeUrl.searchParams.get('path');
const matcher = path && new Minimatch(path.replace(/^\/+/, ''));
// TODO(freben): For now, read the entire repo and filter through that. In
// a future improvement, we could be smart and try to deduce that non-glob
// prefixes (like for filepaths such as some-prefix/**/a.yaml) can be used
// to get just that part of the repo.
const treeUrl = new URL(url);
treeUrl.searchParams.delete('path');
treeUrl.pathname = treeUrl.pathname.replace(/\/+$/, '');
const tree = await this.readTree(treeUrl.toString(), {
etag: options?.etag,
filter: path => matcher.match(stripFirstDirectoryFromPath(path)),
filter: p => (matcher ? matcher.match(p) : true),
});
const files = await tree.files();
@@ -60,22 +60,43 @@ describe('AzureIntegration', () => {
expect(
integration.resolveUrl({
url: '/a.yaml',
base: 'https://dev.azure.com/organization/project/_git/repository?path=%2Ffolder%2Fcatalog-info.yaml',
base: 'https://internal.com/organization/project/_git/repository?path=%2Ffolder%2Fcatalog-info.yaml',
lineNumber: 14,
}),
).toBe(
'https://dev.azure.com/organization/project/_git/repository?path=%2Fa.yaml&line=14&lineEnd=15&lineStartColumn=1&lineEndColumn=1',
'https://internal.com/organization/project/_git/repository?path=%2Fa.yaml&line=14&lineEnd=15&lineStartColumn=1&lineEndColumn=1',
);
expect(
integration.resolveUrl({
url: './a.yaml',
base: 'https://dev.azure.com/organization/project/_git/repository',
base: 'https://dev.azure.com/organization/_git/project',
}),
).toBe('https://dev.azure.com/organization/_git/project?path=%2Fa.yaml');
expect(
integration.resolveUrl({
url: 'https://dev.azure.com/organization/_git/project?path=%2Fa.yaml',
base: 'https://dev.azure.com/organization/_git/project',
}),
).toBe('https://dev.azure.com/organization/_git/project?path=%2Fa.yaml');
expect(
integration.resolveUrl({
url: 'https://dev.azure.com/other-organization/_git/other-project?path=%2Fa.yaml',
base: 'https://dev.azure.com/organization/_git/project',
}),
).toBe(
'https://dev.azure.com/organization/project/_git/repository?path=%2Fa.yaml',
'https://dev.azure.com/other-organization/_git/other-project?path=%2Fa.yaml',
);
expect(
integration.resolveUrl({
url: './a.yaml',
base: 'http://not-azure.com/organization/_git/project',
}),
).toBe('http://not-azure.com/organization/_git/project?path=%2Fa.yaml');
expect(
integration.resolveUrl({
url: 'https://absolute.com/path',
@@ -14,9 +14,9 @@
* limitations under the License.
*/
import parseGitUrl from 'git-url-parse';
import { basicIntegrations, isValidUrl } from '../helpers';
import { ScmIntegration, ScmIntegrationsFactory } from '../types';
import { AzureUrl } from './AzureUrl';
import { AzureIntegrationConfig, readAzureIntegrationConfigs } from './config';
export class AzureIntegration implements ScmIntegration {
@@ -61,29 +61,27 @@ export class AzureIntegration implements ScmIntegration {
return url;
}
const parsed = parseGitUrl(base);
const { organization, owner, name, filepath } = parsed;
try {
const azureUrl = AzureUrl.fromRepoUrl(base);
const newUrl = new URL(base);
// If not an actual file path within a repo, treat the URL as raw
if (!organization || !owner || !name) {
// We lean on the URL path resolution logic to resolve the path param
const mockBaseUrl = new URL(`https://a.com${azureUrl.getPath() ?? ''}`);
const updatedPath = new URL(url, mockBaseUrl).pathname;
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();
} catch {
// If not an actual file path within a repo, treat the URL as raw
return new URL(url, base).toString();
}
const path = filepath?.replace(/^\//, '') || '';
const mockBaseUrl = new URL(`https://a.com/${path}`);
const updatedPath = new URL(url, mockBaseUrl).pathname;
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();
}
resolveEditUrl(url: string): string {
@@ -0,0 +1,177 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { AzureUrl } from './AzureUrl';
describe('AzureUrl', () => {
it('should work with the short URL form', () => {
const url = AzureUrl.fromRepoUrl(
'https://dev.azure.com/my-org/_git/my-project',
);
expect(url.getOwner()).toBe('my-org');
expect(url.getProject()).toBe('my-project');
expect(url.getRepo()).toBe('my-project');
expect(url.getRef()).toBeUndefined();
expect(url.getPath()).toBeUndefined();
expect(url.toRepoUrl()).toBe(
'https://dev.azure.com/my-org/_git/my-project',
);
expect(() => url.toFileUrl()).toThrow(
'Azure URL must point to a specific path to be able to download a file',
);
expect(url.toArchiveUrl()).toBe(
'https://dev.azure.com/my-org/my-project/_apis/git/repositories/my-project/items?recursionLevel=full&download=true&api-version=6.0',
);
expect(url.toCommitsUrl()).toBe(
'https://dev.azure.com/my-org/my-project/_apis/git/repositories/my-project/commits?api-version=6.0',
);
});
it('should work with the short URL form with a path', () => {
const url = AzureUrl.fromRepoUrl(
'https://dev.azure.com/my-org/_git/my-project?path=%2Ftest.yaml',
);
expect(url.getOwner()).toBe('my-org');
expect(url.getProject()).toBe('my-project');
expect(url.getRepo()).toBe('my-project');
expect(url.getRef()).toBeUndefined();
expect(url.getPath()).toBe('/test.yaml');
expect(url.toRepoUrl()).toBe(
'https://dev.azure.com/my-org/_git/my-project?path=%2Ftest.yaml',
);
expect(url.toFileUrl()).toBe(
'https://dev.azure.com/my-org/my-project/_apis/git/repositories/my-project/items?api-version=6.0&path=%2Ftest.yaml',
);
expect(url.toArchiveUrl()).toBe(
'https://dev.azure.com/my-org/my-project/_apis/git/repositories/my-project/items?recursionLevel=full&download=true&api-version=6.0&scopePath=%2Ftest.yaml',
);
expect(url.toCommitsUrl()).toBe(
'https://dev.azure.com/my-org/my-project/_apis/git/repositories/my-project/commits?api-version=6.0',
);
});
it('should work with the short URL form with a path and ref', () => {
const url = AzureUrl.fromRepoUrl(
'https://dev.azure.com/my-org/_git/my-project?path=%2Ftest.yaml&version=GBtest-branch',
);
expect(url.getOwner()).toBe('my-org');
expect(url.getProject()).toBe('my-project');
expect(url.getRepo()).toBe('my-project');
expect(url.getRef()).toBe('test-branch');
expect(url.getPath()).toBe('/test.yaml');
expect(url.toRepoUrl()).toBe(
'https://dev.azure.com/my-org/_git/my-project?path=%2Ftest.yaml&version=GBtest-branch',
);
expect(url.toFileUrl()).toBe(
'https://dev.azure.com/my-org/my-project/_apis/git/repositories/my-project/items?api-version=6.0&path=%2Ftest.yaml&version=test-branch',
);
expect(url.toArchiveUrl()).toBe(
'https://dev.azure.com/my-org/my-project/_apis/git/repositories/my-project/items?recursionLevel=full&download=true&api-version=6.0&scopePath=%2Ftest.yaml&version=test-branch',
);
expect(url.toCommitsUrl()).toBe(
'https://dev.azure.com/my-org/my-project/_apis/git/repositories/my-project/commits?api-version=6.0&searchCriteria.itemVersion.version=test-branch',
);
});
it('should work with the long URL', () => {
const url = AzureUrl.fromRepoUrl(
'http://my-host/my-org/my-project/_git/my-repo',
);
expect(url.getOwner()).toBe('my-org');
expect(url.getProject()).toBe('my-project');
expect(url.getRepo()).toBe('my-repo');
expect(url.getRef()).toBeUndefined();
expect(url.getPath()).toBeUndefined();
expect(url.toRepoUrl()).toBe(
'http://my-host/my-org/my-project/_git/my-repo',
);
expect(() => url.toFileUrl()).toThrow(
'Azure URL must point to a specific path to be able to download a file',
);
expect(url.toArchiveUrl()).toBe(
'http://my-host/my-org/my-project/_apis/git/repositories/my-repo/items?recursionLevel=full&download=true&api-version=6.0',
);
expect(url.toCommitsUrl()).toBe(
'http://my-host/my-org/my-project/_apis/git/repositories/my-repo/commits?api-version=6.0',
);
});
it('should work with the long URL form with a path and ref', () => {
const url = AzureUrl.fromRepoUrl(
'http://my-host/my-org/my-project/_git/my-repo?path=%2Ffolder&version=GBtest-branch',
);
expect(url.getOwner()).toBe('my-org');
expect(url.getProject()).toBe('my-project');
expect(url.getRepo()).toBe('my-repo');
expect(url.getRef()).toBe('test-branch');
expect(url.getPath()).toBe('/folder');
expect(url.toRepoUrl()).toBe(
'http://my-host/my-org/my-project/_git/my-repo?path=%2Ffolder&version=GBtest-branch',
);
expect(url.toFileUrl()).toBe(
'http://my-host/my-org/my-project/_apis/git/repositories/my-repo/items?api-version=6.0&path=%2Ffolder&version=test-branch',
);
expect(url.toArchiveUrl()).toBe(
'http://my-host/my-org/my-project/_apis/git/repositories/my-repo/items?recursionLevel=full&download=true&api-version=6.0&scopePath=%2Ffolder&version=test-branch',
);
expect(url.toCommitsUrl()).toBe(
'http://my-host/my-org/my-project/_apis/git/repositories/my-repo/commits?api-version=6.0&searchCriteria.itemVersion.version=test-branch',
);
});
it('should reject non-branch refs', () => {
expect(() =>
AzureUrl.fromRepoUrl(
'https://dev.azure.com/my-org/_git/my-project?version=GC6eead79870d998a3befd4bc7c72cc89e446f2970',
),
).toThrow('Azure URL version must point to a git branch');
});
it('should reject non-repo URLs', () => {
expect(() =>
AzureUrl.fromRepoUrl('https://dev.azure.com/my-org/_git'),
).toThrow('Azure URL must point to a git repository');
expect(() =>
AzureUrl.fromRepoUrl('https://dev.azure.com/my-org/_git/'),
).toThrow('Azure URL must point to a git repository');
expect(() =>
AzureUrl.fromRepoUrl('https://dev.azure.com/my-org/my-project/'),
).toThrow('Azure URL must point to a git repository');
expect(() =>
AzureUrl.fromRepoUrl('https://dev.azure.com/my-org/my-project/_not-git'),
).toThrow('Azure URL must point to a git repository');
expect(() =>
AzureUrl.fromRepoUrl(
'https://dev.azure.com/my-org/my-project/_not-git/my-repo',
),
).toThrow('Azure URL must point to a git repository');
expect(() =>
AzureUrl.fromRepoUrl(
'https://dev.azure.com/my-org/_workitems/recentlyupdated/',
),
).toThrow('Azure URL must point to a git repository');
});
});
+231
View File
@@ -0,0 +1,231 @@
/*
* Copyright 2021 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const VERSION_PREFIX_GIT_BRANCH = 'GB';
export class AzureUrl {
/**
* Parses an azure URL as copied from the browser address bar.
*
* Throws an error if the URL is not a valid azure repo URL.
*/
static fromRepoUrl(repoUrl: string): AzureUrl {
const url = new URL(repoUrl);
let owner;
let project;
let repo;
const parts = url.pathname.split('/').map(part => decodeURIComponent(part));
if (parts[2] === '_git') {
owner = parts[1];
project = repo = parts[3];
} else if (parts[3] === '_git') {
owner = parts[1];
project = parts[2];
repo = parts[4];
}
if (!owner || !project || !repo) {
throw new Error('Azure URL must point to a git repository');
}
const path = url.searchParams.get('path') ?? undefined;
let ref;
const version = url.searchParams.get('version');
if (version) {
const prefix = version.slice(0, 2);
if (prefix !== 'GB') {
throw new Error('Azure URL version must point to a git branch');
}
ref = version.slice(2);
}
return new AzureUrl(url.origin, owner, project, repo, path, ref);
}
#origin: string;
#owner: string;
#project: string;
#repo: string;
#path?: string;
#ref?: string;
private constructor(
origin: string,
owner: string,
project: string,
repo: string,
path?: string,
ref?: string,
) {
this.#origin = origin;
this.#owner = owner;
this.#project = project;
this.#repo = repo;
this.#path = path;
this.#ref = ref;
}
#baseUrl = (...parts: string[]): URL => {
const url = new URL(this.#origin);
url.pathname = parts.map(part => encodeURIComponent(part)).join('/');
return url;
};
/**
* Returns a repo URL that can be used to navigate to the resource in azure.
*
* Throws an error if the URL is not a valid azure repo URL.
*/
toRepoUrl(): string {
let url;
if (this.#project === this.#repo) {
url = this.#baseUrl(this.#owner, '_git', this.#repo);
} else {
url = this.#baseUrl(this.#owner, this.#project, '_git', this.#repo);
}
if (this.#path) {
url.searchParams.set('path', this.#path);
}
if (this.#ref) {
url.searchParams.set('version', VERSION_PREFIX_GIT_BRANCH + this.#ref);
}
return url.toString();
}
/**
* Returns the file download URL for this azure resource.
*
* Throws an error if the URL does not point to a file.
*/
toFileUrl(): string {
if (!this.#path) {
throw new Error(
'Azure URL must point to a specific path to be able to download a file',
);
}
const url = this.#baseUrl(
this.#owner,
this.#project,
'_apis',
'git',
'repositories',
this.#repo,
'items',
);
url.searchParams.set('api-version', '6.0');
url.searchParams.set('path', this.#path);
if (this.#ref) {
url.searchParams.set('version', this.#ref);
}
return url.toString();
}
/**
* Returns the archive download URL for this azure resource.
*
* Throws an error if the URL does not point to a repo.
*/
toArchiveUrl(): string {
const url = this.#baseUrl(
this.#owner,
this.#project,
'_apis',
'git',
'repositories',
this.#repo,
'items',
);
url.searchParams.set('recursionLevel', 'full');
url.searchParams.set('download', 'true');
url.searchParams.set('api-version', '6.0');
if (this.#path) {
url.searchParams.set('scopePath', this.#path);
}
if (this.#ref) {
url.searchParams.set('version', this.#ref);
}
return url.toString();
}
/**
* Returns the API url for fetching commits from a branch for this azure resource.
*
* Throws an error if the URL does not point to a commit.
*/
toCommitsUrl(): string {
const url = this.#baseUrl(
this.#owner,
this.#project,
'_apis',
'git',
'repositories',
this.#repo,
'commits',
);
url.searchParams.set('api-version', '6.0');
if (this.#ref) {
url.searchParams.set('searchCriteria.itemVersion.version', this.#ref);
}
return url.toString();
}
/**
* Returns the name of the owner, a user or an organization.
*/
getOwner(): string {
return this.#owner;
}
/**
* Returns the name of the project.
*/
getProject(): string {
return this.#project;
}
/**
* Returns the name of the repo.
*/
getRepo(): string {
return this.#repo;
}
/**
* Returns the file path within the repo if the URL contains one.
*/
getPath(): string | undefined {
return this.#path;
}
/**
* Returns the git ref in the repo if the URL contains one.
*/
getRef(): string | undefined {
return this.#ref;
}
}
+7 -9
View File
@@ -45,22 +45,22 @@ describe('azure core', () => {
{
url: 'https://dev.azure.com/org-name/project-name/_git/repo-name?path=my-template.yaml&version=GBmaster',
result:
'https://dev.azure.com/org-name/project-name/_apis/git/repositories/repo-name/items?path=my-template.yaml&version=master',
'https://dev.azure.com/org-name/project-name/_apis/git/repositories/repo-name/items?api-version=6.0&path=my-template.yaml&version=master',
},
{
url: 'https://dev.azure.com/org-name/project-name/_git/repo-name?path=my-template.yaml',
result:
'https://dev.azure.com/org-name/project-name/_apis/git/repositories/repo-name/items?path=my-template.yaml',
'https://dev.azure.com/org-name/project-name/_apis/git/repositories/repo-name/items?api-version=6.0&path=my-template.yaml',
},
{
url: 'https://api.com/org-name/project-name/_git/repo-name?path=my-template.yaml',
result:
'https://api.com/org-name/project-name/_apis/git/repositories/repo-name/items?path=my-template.yaml',
'https://api.com/org-name/project-name/_apis/git/repositories/repo-name/items?api-version=6.0&path=my-template.yaml',
},
{
url: 'https://api.com/org-name/project-name/_git/repo-name?path=my-template.yaml&version=GBmaster',
result:
'https://api.com/org-name/project-name/_apis/git/repositories/repo-name/items?path=my-template.yaml&version=master',
'https://api.com/org-name/project-name/_apis/git/repositories/repo-name/items?api-version=6.0&path=my-template.yaml&version=master',
},
])('should handle happy path %#', async ({ url, result }) => {
expect(getAzureFileFetchUrl(url)).toBe(result);
@@ -69,13 +69,11 @@ describe('azure core', () => {
it.each([
{
url: 'https://api.com/a/b/blob/master/path/to/c.yaml',
error:
'Incorrect URL: https://api.com/a/b/blob/master/path/to/c.yaml, Error: Wrong Azure Devops URL or Invalid file path',
error: 'Azure URL must point to a git repository',
},
{
url: 'com/a/b/blob/master/path/to/c.yaml',
error:
'Incorrect URL: com/a/b/blob/master/path/to/c.yaml, TypeError: Invalid URL: com/a/b/blob/master/path/to/c.yaml',
error: 'Invalid URL: com/a/b/blob/master/path/to/c.yaml',
},
])('should handle error path %#', ({ url, error }) => {
expect(() => getAzureFileFetchUrl(url)).toThrow(error);
@@ -95,7 +93,7 @@ describe('azure core', () => {
const result = getAzureDownloadUrl(
'https://dev.azure.com/organization/project/_git/repository?path=%2Fdocs',
);
expect(new URL(result).searchParams.get('scopePath')).toEqual('docs');
expect(new URL(result).searchParams.get('scopePath')).toEqual('/docs');
});
it.each([
+4 -118
View File
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import parseGitUrl from 'git-url-parse';
import { AzureUrl } from './AzureUrl';
import { AzureIntegrationConfig } from './config';
/**
@@ -28,53 +28,7 @@ import { AzureIntegrationConfig } from './config';
* @param url A URL pointing to a file
*/
export function getAzureFileFetchUrl(url: string): string {
try {
const parsedUrl = new URL(url);
const [empty, userOrOrg, project, srcKeyword, repoName] =
parsedUrl.pathname.split('/');
const path = parsedUrl.searchParams.get('path') || '';
const ref = parsedUrl.searchParams.get('version')?.substr(2);
if (
empty !== '' ||
userOrOrg === '' ||
project === '' ||
srcKeyword !== '_git' ||
repoName === '' ||
path === '' ||
ref === ''
) {
throw new Error('Wrong Azure Devops URL or Invalid file path');
}
// transform to api
parsedUrl.pathname = [
empty,
userOrOrg,
project,
'_apis',
'git',
'repositories',
repoName,
'items',
].join('/');
const queryParams = [`path=${path}`];
if (ref) {
queryParams.push(`version=${ref}`);
}
parsedUrl.search = queryParams.join('&');
parsedUrl.protocol = 'https';
return parsedUrl.toString();
} catch (e) {
throw new Error(`Incorrect URL: ${url}, ${e}`);
}
return AzureUrl.fromRepoUrl(url).toFileUrl();
}
/**
@@ -84,33 +38,7 @@ export function getAzureFileFetchUrl(url: string): string {
* @param url A URL pointing to a path
*/
export function getAzureDownloadUrl(url: string): string {
const {
name: repoName,
owner: project,
organization,
protocol,
resource,
filepath,
} = parseGitUrl(url);
// scopePath will limit the downloaded content
// /docs will only download the docs folder and everything below it
// /docs/index.md will only download index.md but put it in the root of the archive
const scopePath = filepath
? `&scopePath=${encodeURIComponent(filepath)}`
: '';
if (resource === 'dev.azure.com') {
return `${protocol}://${resource}/${organization}/${project}/_apis/git/repositories/${repoName}/items?recursionLevel=full&download=true&api-version=6.0${scopePath}`;
}
// For Azure DevOps Server `parseGitUrl` returns the same values
// for `organization` and `project` like this: `organization/project/_git`
// so we drop `project` and then strip `/_git` from `organization`
return `${protocol}://${resource}/${organization.replace(
'/_git',
'',
)}/_apis/git/repositories/${repoName}/items?recursionLevel=full&download=true&api-version=6.0${scopePath}`;
return AzureUrl.fromRepoUrl(url).toArchiveUrl();
}
/**
@@ -119,49 +47,7 @@ export function getAzureDownloadUrl(url: string): string {
* @param url A URL pointing to a repository or a sub-path
*/
export function getAzureCommitsUrl(url: string): string {
try {
const parsedUrl = new URL(url);
const [empty, userOrOrg, project, srcKeyword, repoName] =
parsedUrl.pathname.split('/');
// Remove the "GB" from "GBmain" for example.
const ref = parsedUrl.searchParams.get('version')?.substr(2);
if (
!!empty ||
!userOrOrg ||
!project ||
srcKeyword !== '_git' ||
!repoName
) {
throw new Error('Wrong Azure Devops URL');
}
// transform to commits api
parsedUrl.pathname = [
empty,
userOrOrg,
project,
'_apis',
'git',
'repositories',
repoName,
'commits',
].join('/');
const queryParams = [];
if (ref) {
queryParams.push(`searchCriteria.itemVersion.version=${ref}`);
}
parsedUrl.search = queryParams.join('&');
parsedUrl.protocol = 'https';
return parsedUrl.toString();
} catch (e) {
throw new Error(`Incorrect URL: ${url}, ${e}`);
}
return AzureUrl.fromRepoUrl(url).toCommitsUrl();
}
/**