Skip to content

Commit ac01ae2

Browse files
committed
Retrieve target repoId for AzureDevOps
(#4142, #4143)
1 parent 6e542e7 commit ac01ae2

File tree

6 files changed

+141
-14
lines changed

6 files changed

+141
-14
lines changed

src/git/remotes/azure-devops.ts

+55-8
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import type { Range, Uri } from 'vscode';
22
import type { AutolinkReference, DynamicAutolinkReference } from '../../autolinks/models/autolinks';
3+
import type { Container } from '../../container';
4+
import { HostingIntegration } from '../../plus/integrations/integration';
5+
import { remoteProviderIdToIntegrationId } from '../../plus/integrations/integrationService';
36
import type { Brand, Unbrand } from '../../system/brand';
47
import type { Repository } from '../models/repository';
58
import type { GkProviderId } from '../models/repositoryIdentities';
@@ -17,7 +20,14 @@ const rangeRegex = /line=(\d+)(?:&lineEnd=(\d+))?/;
1720

1821
export class AzureDevOpsRemote extends RemoteProvider {
1922
private readonly project: string | undefined;
20-
constructor(domain: string, path: string, protocol?: string, name?: string, legacy: boolean = false) {
23+
constructor(
24+
private readonly container: Container,
25+
domain: string,
26+
path: string,
27+
protocol?: string,
28+
name?: string,
29+
legacy: boolean = false,
30+
) {
2131
let repoProject;
2232
if (sshDomainRegex.test(domain)) {
2333
path = path.replace(sshPathRegex, '');
@@ -186,16 +196,30 @@ export class AzureDevOpsRemote extends RemoteProvider {
186196
return this.encodeUrl(`${this.baseUrl}/branchCompare?baseVersion=GB${base}&targetVersion=GB${head}`);
187197
}
188198

189-
protected override getUrlForCreatePullRequest(
199+
protected override async getUrlForCreatePullRequest(
190200
base: { branch?: string; remote: { path: string; url: string } },
191201
head: { branch: string; remote: { path: string; url: string } },
192-
): string | undefined {
193-
const query = new URLSearchParams({ sourceRef: head.branch, targetRef: base.branch ?? '' });
194-
// TODO: figure this out
195-
// query.set('sourceRepositoryId', compare.repoId);
196-
// query.set('targetRepositoryId', base.repoId);
202+
): Promise<string | undefined> {
203+
const query = new URLSearchParams({ sourceRef: head.branch, targetRef: base.branch ?? '', title: 'OLOLO' });
204+
if (base.remote.url !== head.remote.url) {
205+
const { org: baseOrg, project: baseProject, repo: baseName } = parseAzureUrl(base.remote.url);
206+
const targetDesc = { project: baseProject, name: baseName, owner: baseOrg };
207+
208+
const integrationId = remoteProviderIdToIntegrationId(this.id);
209+
const integration = integrationId && (await this.container.integrations.get(integrationId));
210+
let targetRepoId = undefined;
211+
if (integration?.isConnected && integration instanceof HostingIntegration) {
212+
targetRepoId = (await integration.getRepoInfo(targetDesc))?.id;
213+
}
197214

198-
return `${this.encodeUrl(`${this.baseUrl}/pullrequestcreate`)}?${query.toString()}`;
215+
if (!targetRepoId) {
216+
return undefined; //?? or throw or trigger error showing
217+
}
218+
query.set('targetRepositoryId', targetRepoId);
219+
// query.set('sourceRepositoryId', compare.repoId); // ?? looks like not needed
220+
}
221+
222+
return `${this.encodeUrl(`${this.getRepoBaseUrl(head.remote.path)}/pullrequestcreate`)}?${query.toString()}`;
199223
}
200224

201225
protected getUrlForFile(fileName: string, branch?: string, sha?: string, range?: Range): string {
@@ -219,3 +243,26 @@ export class AzureDevOpsRemote extends RemoteProvider {
219243
return this.encodeUrl(`${this.baseUrl}?path=/${fileName}${line}`);
220244
}
221245
}
246+
247+
const azureSshUrlRegex = /^(?:[^@]+@)?([^:]+):v\d\//;
248+
function parseAzureUrl(url: string): { org: string; project: string; repo: string } {
249+
if (azureSshUrlRegex.test(url)) {
250+
url = url.replace(azureSshUrlRegex, '');
251+
}
252+
const match = orgAndProjectRegex.exec(url);
253+
if (match != null) {
254+
const [, org, project, rest] = match;
255+
return { org: org, project: project, repo: rest };
256+
}
257+
258+
// Azure DevOps allows projects and repository names with spaces. In that situation,
259+
// the `path` will be previously encoded during git clone
260+
// revert that encoding to avoid double-encoding by gitlens during copy remote and open remote
261+
//path = decodeURIComponent(path);
262+
263+
return {
264+
org: '',
265+
project: '',
266+
repo: '',
267+
};
268+
}

src/git/remotes/remoteProviders.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ const builtInProviders: RemoteProviders = [
4141
{
4242
custom: false,
4343
matcher: /\bdev\.azure\.com$/i,
44-
creator: (_container: Container, domain: string, path: string) => new AzureDevOpsRemote(domain, path),
44+
creator: (container: Container, domain: string, path: string) => new AzureDevOpsRemote(container, domain, path),
4545
},
4646
{
4747
custom: true,
@@ -56,8 +56,8 @@ const builtInProviders: RemoteProviders = [
5656
{
5757
custom: false,
5858
matcher: /\bvisualstudio\.com$/i,
59-
creator: (_container: Container, domain: string, path: string) =>
60-
new AzureDevOpsRemote(domain, path, undefined, undefined, true),
59+
creator: (container: Container, domain: string, path: string) =>
60+
new AzureDevOpsRemote(container, domain, path, undefined, undefined, true),
6161
},
6262
{
6363
custom: false,
@@ -136,8 +136,8 @@ export function loadRemoteProviders(
136136
function getCustomProviderCreator(cfg: RemotesConfig) {
137137
switch (cfg.type) {
138138
case 'AzureDevOps':
139-
return (_container: Container, domain: string, path: string) =>
140-
new AzureDevOpsRemote(domain, path, cfg.protocol, cfg.name, true);
139+
return (container: Container, domain: string, path: string) =>
140+
new AzureDevOpsRemote(container, domain, path, cfg.protocol, cfg.name, true);
141141
case 'Bitbucket':
142142
return (_container: Container, domain: string, path: string) =>
143143
new BitbucketRemote(domain, path, cfg.protocol, cfg.name, true);

src/plus/integrations/integration.ts

+15
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import type {
4444
ProviderPullRequest,
4545
ProviderRepoInput,
4646
ProviderReposInput,
47+
ProviderRepository,
4748
} from './providers/models';
4849
import { IssueFilter, PagingMode, PullRequestFilter } from './providers/models';
4950
import type { ProvidersApi } from './providers/providersApi';
@@ -780,6 +781,20 @@ export abstract class HostingIntegration<
780781
return defaultBranch;
781782
}
782783

784+
async getRepoInfo(repo: {
785+
owner: string;
786+
name: string;
787+
project?: string;
788+
}): Promise<ProviderRepository | undefined> {
789+
return this.getProviderRepoInfo?.(repo);
790+
}
791+
792+
getProviderRepoInfo?(repo: {
793+
owner: string;
794+
name: string;
795+
project?: string;
796+
}): Promise<ProviderRepository | undefined>;
797+
783798
protected abstract getProviderDefaultBranch(
784799
{ accessToken }: ProviderAuthenticationSession,
785800
repo: T,

src/plus/integrations/providers/azureDevOps.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import type {
1717
AzureRemoteRepositoryDescriptor,
1818
AzureRepositoryDescriptor,
1919
} from './azure/models';
20-
import type { ProviderPullRequest } from './models';
20+
import type { ProviderPullRequest, ProviderRepository } from './models';
2121
import { fromProviderIssue, fromProviderPullRequest, providersMetadata } from './models';
2222

2323
const metadata = providersMetadata[HostingIntegrationId.AzureDevOps];
@@ -300,6 +300,17 @@ export class AzureDevOpsIntegration extends HostingIntegration<
300300
return Promise.resolve(undefined);
301301
}
302302

303+
public override async getProviderRepoInfo(repo: {
304+
owner: string;
305+
name: string;
306+
project: string;
307+
}): Promise<ProviderRepository | undefined> {
308+
const api = await this.getProvidersApi();
309+
return api.getRepo(this.id, repo.owner, repo.name, repo.project, {
310+
accessToken: this._session?.accessToken,
311+
});
312+
}
313+
303314
protected override async getProviderRepositoryMetadata(
304315
_session: AuthenticationSession,
305316
_repo: AzureRepositoryDescriptor,

src/plus/integrations/providers/models.ts

+11
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,15 @@ export interface PageInfo {
226226
nextPage?: number | null;
227227
}
228228

229+
export type GetRepoFn = (
230+
input: ProviderRepoInput,
231+
options?: EnterpriseOptions,
232+
) => Promise<{ data: ProviderRepository }>;
233+
export type GetRepoOfProjectFn = (
234+
input: ProviderRepoInput & { project: string },
235+
options?: EnterpriseOptions,
236+
) => Promise<{ data: ProviderRepository }>;
237+
229238
export type GetPullRequestsForReposFn = (
230239
input: (GetPullRequestsForReposInput | GetPullRequestsForRepoIdsInput) & PagingInput,
231240
options?: EnterpriseOptions,
@@ -350,6 +359,8 @@ export type GetIssuesForResourceForCurrentUserFn = (
350359

351360
export interface ProviderInfo extends ProviderMetadata {
352361
provider: GitHub | GitLab | Bitbucket | Jira | Trello | AzureDevOps;
362+
getRepoFn?: GetRepoFn;
363+
getRepoOfProjectFn?: GetRepoOfProjectFn;
353364
getPullRequestsForReposFn?: GetPullRequestsForReposFn;
354365
getPullRequestsForRepoFn?: GetPullRequestsForRepoFn;
355366
getPullRequestsForUserFn?: GetPullRequestsForUserFn;

src/plus/integrations/providers/providersApi.ts

+43
Original file line numberDiff line numberDiff line change
@@ -215,6 +215,7 @@ export class ProvidersApi {
215215
[HostingIntegrationId.AzureDevOps]: {
216216
...providersMetadata[HostingIntegrationId.AzureDevOps],
217217
provider: providerApis.azureDevOps,
218+
getRepoOfProjectFn: providerApis.azureDevOps.getRepo.bind(providerApis.azureDevOps),
218219
getCurrentUserFn: providerApis.azureDevOps.getCurrentUser.bind(
219220
providerApis.azureDevOps,
220221
) as GetCurrentUserFn,
@@ -442,6 +443,48 @@ export class ProvidersApi {
442443
}
443444
}
444445

446+
async getRepo(
447+
providerId: IntegrationId,
448+
owner: string,
449+
name: string,
450+
project?: string,
451+
options?: { accessToken?: string; isPAT?: boolean; baseUrl?: string },
452+
): Promise<ProviderRepository | undefined> {
453+
if (providerId === HostingIntegrationId.AzureDevOps && project != null) {
454+
const { provider, token } = await this.ensureProviderTokenAndFunction(
455+
providerId,
456+
'getRepoOfProjectFn',
457+
options?.accessToken,
458+
);
459+
460+
try {
461+
const result = await provider['getRepoOfProjectFn']?.(
462+
{ namespace: owner, name: name, project: project },
463+
{ token: token, isPAT: options?.isPAT, baseUrl: options?.baseUrl },
464+
);
465+
return result?.data;
466+
} catch (e) {
467+
return this.handleProviderError<ProviderRepository>(providerId, token, e);
468+
}
469+
} else {
470+
const { provider, token } = await this.ensureProviderTokenAndFunction(
471+
providerId,
472+
'getRepoFn',
473+
options?.accessToken,
474+
);
475+
476+
try {
477+
const result = await provider['getRepoFn']?.(
478+
{ namespace: owner, name: name, project: project },
479+
{ token: token, isPAT: options?.isPAT, baseUrl: options?.baseUrl },
480+
);
481+
return result?.data;
482+
} catch (e) {
483+
return this.handleProviderError<ProviderRepository>(providerId, token, e);
484+
}
485+
}
486+
}
487+
445488
async getCurrentUser(
446489
providerId: IntegrationId,
447490
options?: { accessToken?: string; isPAT?: boolean; baseUrl?: string },

0 commit comments

Comments
 (0)