Skip to content

Commit a266877

Browse files
committed
Lets user connect and integration when it is required for a cross-fork PR
(#4142, #4143)
1 parent 0b04d66 commit a266877

File tree

4 files changed

+97
-4
lines changed

4 files changed

+97
-4
lines changed

src/errors.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,3 +269,10 @@ export class RequestsAreBlockedTemporarilyError extends Error {
269269
Error.captureStackTrace?.(this, RequestsAreBlockedTemporarilyError);
270270
}
271271
}
272+
273+
export class RequiresIntegrationError extends Error {
274+
constructor(message: string) {
275+
super(message);
276+
Error.captureStackTrace?.(this, RequiresIntegrationError);
277+
}
278+
}

src/git/remotes/azure-devops.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -197,6 +197,12 @@ export class AzureDevOpsRemote extends RemoteProvider {
197197
return this.encodeUrl(`${this.baseUrl}/branchCompare?baseVersion=GB${base}&targetVersion=GB${head}`);
198198
}
199199

200+
override async isReadyForForCrossForkPullRequestUrls(): Promise<boolean> {
201+
const integrationId = remoteProviderIdToIntegrationId(this.id);
202+
const integration = integrationId && (await this.container.integrations.get(integrationId));
203+
return integration?.maybeConnected ?? integration?.isConnected() ?? false;
204+
}
205+
200206
protected override async getUrlForCreatePullRequest(
201207
base: { branch?: string; remote: { path: string; url: string } },
202208
head: { branch: string; remote: { path: string; url: string } },

src/git/remotes/remoteProvider.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,10 @@ export abstract class RemoteProvider<T extends ResourceDescriptor = ResourceDesc
186186

187187
protected abstract getUrlForComparison(base: string, head: string, notation: '..' | '...'): string | undefined;
188188

189+
async isReadyForForCrossForkPullRequestUrls(): Promise<boolean> {
190+
return Promise.resolve(true);
191+
}
192+
189193
protected getUrlForCreatePullRequest?(
190194
base: { branch?: string; remote: { path: string; url: string } },
191195
head: { branch: string; remote: { path: string; url: string } },

src/quickpicks/remoteProviderPicker.ts

Lines changed: 80 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1-
import type { Disposable, QuickInputButton } from 'vscode';
1+
import type { Disposable, QuickInputButton, QuickPickItem } from 'vscode';
22
import { env, ThemeIcon, Uri, window } from 'vscode';
33
import type { OpenOnRemoteCommandArgs } from '../commands/openOnRemote';
44
import { SetRemoteAsDefaultQuickInputButton } from '../commands/quickCommand.buttons';
55
import type { Keys } from '../constants';
66
import { GlyphChars } from '../constants';
7+
import type { IntegrationId } from '../constants.integrations';
8+
import type { Sources } from '../constants.telemetry';
79
import { Container } from '../container';
10+
import { RequiresIntegrationError } from '../errors';
811
import type { GitRemote } from '../git/models/remote';
912
import type { RemoteResource } from '../git/models/remoteResource';
1013
import { RemoteResourceType } from '../git/models/remoteResource';
@@ -13,10 +16,12 @@ import { getDefaultBranchName } from '../git/utils/-webview/branch.utils';
1316
import { getBranchNameWithoutRemote, getRemoteNameFromBranchName } from '../git/utils/branch.utils';
1417
import { getHighlanderProviders } from '../git/utils/remote.utils';
1518
import { getNameFromRemoteResource } from '../git/utils/remoteResource.utils';
19+
import { remoteProviderIdToIntegrationId } from '../plus/integrations/integrationService';
20+
import { providersMetadata } from '../plus/integrations/providers/models';
1621
import { getQuickPickIgnoreFocusOut } from '../system/-webview/vscode';
17-
import { filterMap } from '../system/array';
1822
import { getSettledValue } from '../system/promise';
19-
import { CommandQuickPickItem } from './items/common';
23+
import { CommandQuickPickItem, createQuickPickItemOfT } from './items/common';
24+
import { createDirectiveQuickPickItem, Directive } from './items/directive';
2025

2126
export class ConfigureCustomRemoteProviderCommandQuickPickItem extends CommandQuickPickItem {
2227
constructor() {
@@ -68,6 +73,20 @@ export class CopyOrOpenRemoteCommandQuickPickItem extends CommandQuickPickItem {
6873
...resource,
6974
base: { branch: branch, remote: { path: this.remote.path, url: this.remote.url } },
7075
};
76+
77+
if (
78+
resource.base.remote.url !== resource.compare.remote.url &&
79+
!(await this.remote.provider.isReadyForForCrossForkPullRequestUrls())
80+
) {
81+
const integrationId = remoteProviderIdToIntegrationId(this.remote.provider.id);
82+
const connected =
83+
integrationId && (await this.showIntegrationConnectionPicker(integrationId, 'view'));
84+
if (!connected) {
85+
throw new RequiresIntegrationError(
86+
'Cross-fork pull request URLs are not supported by this provider',
87+
);
88+
}
89+
}
7190
} else if (
7291
resource.type === RemoteResourceType.File &&
7392
resource.branchOrTag != null &&
@@ -96,11 +115,68 @@ export class CopyOrOpenRemoteCommandQuickPickItem extends CommandQuickPickItem {
96115
}),
97116
);
98117

99-
const resources = filterMap(resourcesResults, r => getSettledValue(r));
118+
const resources = resourcesResults
119+
.map(r => {
120+
if (r.status === 'fulfilled') {
121+
return r.value;
122+
}
123+
if (r.reason instanceof RequiresIntegrationError) {
124+
throw r.reason;
125+
}
126+
return undefined;
127+
})
128+
.filter((r): r is RemoteResource => r !== undefined);
100129

101130
void (await (this.clipboard ? this.remote.provider.copy(resources) : this.remote.provider.open(resources)));
102131
}
103132

133+
async showIntegrationConnectionPicker(integrationId: IntegrationId, source: Sources): Promise<boolean> {
134+
const disposables: Disposable[] = [];
135+
const quickpick = window.createQuickPick<QuickPickItem>();
136+
try {
137+
const integrationName = providersMetadata[integrationId].name;
138+
const connectItem = createQuickPickItemOfT(
139+
{
140+
label: `Connect a ${integrationName} Integration...`,
141+
detail: `Connect a ${integrationName} integration to be able to create cross-fork pull requests`,
142+
picked: true,
143+
},
144+
true,
145+
);
146+
const cancelItem = createDirectiveQuickPickItem(Directive.Cancel, false, { label: 'Cancel' });
147+
const quickpickPromise = new Promise<undefined | QuickPickItem>(resolve => {
148+
disposables.push(
149+
quickpick.onDidHide(() => resolve(undefined)),
150+
quickpick.onDidAccept(() => {
151+
if (quickpick.activeItems.length !== 0) {
152+
resolve(quickpick.activeItems[0]);
153+
}
154+
}),
155+
);
156+
});
157+
quickpick.ignoreFocusOut = getQuickPickIgnoreFocusOut();
158+
quickpick.title = `Connect a ${integrationName} Integration`;
159+
quickpick.placeholder = `Connect a ${integrationName} integration to be able to create cross-fork pull requests`;
160+
quickpick.matchOnDetail = true;
161+
quickpick.items = [connectItem, cancelItem];
162+
quickpick.show();
163+
const pick = await quickpickPromise;
164+
if (pick === connectItem) {
165+
const connected = await Container.instance.integrations.connectCloudIntegrations(
166+
{ integrationIds: [integrationId] },
167+
{
168+
source: source,
169+
},
170+
);
171+
return connected;
172+
}
173+
} finally {
174+
quickpick.dispose();
175+
disposables.forEach(d => void d.dispose());
176+
}
177+
return false;
178+
}
179+
104180
setAsDefault(): Promise<void> {
105181
return this.remote.setAsDefault(true);
106182
}

0 commit comments

Comments
 (0)