Skip to content

Commit 01d3810

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

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,11 +1,14 @@
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';
77
import { GlCommand } from '../constants.commands';
8+
import type { IntegrationId } from '../constants.integrations';
9+
import type { Sources } from '../constants.telemetry';
810
import { Container } from '../container';
11+
import { RequiresIntegrationError } from '../errors';
912
import type { GitRemote } from '../git/models/remote';
1013
import type { RemoteResource } from '../git/models/remoteResource';
1114
import { RemoteResourceType } from '../git/models/remoteResource';
@@ -14,10 +17,12 @@ import { getDefaultBranchName } from '../git/utils/-webview/branch.utils';
1417
import { getBranchNameWithoutRemote, getRemoteNameFromBranchName } from '../git/utils/branch.utils';
1518
import { getHighlanderProviders } from '../git/utils/remote.utils';
1619
import { getNameFromRemoteResource } from '../git/utils/remoteResource.utils';
20+
import { remoteProviderIdToIntegrationId } from '../plus/integrations/integrationService';
21+
import { providersMetadata } from '../plus/integrations/providers/models';
1722
import { getQuickPickIgnoreFocusOut } from '../system/-webview/vscode';
18-
import { filterMap } from '../system/array';
1923
import { getSettledValue } from '../system/promise';
20-
import { CommandQuickPickItem } from './items/common';
24+
import { CommandQuickPickItem, createQuickPickItemOfT } from './items/common';
25+
import { createDirectiveQuickPickItem, Directive } from './items/directive';
2126

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

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

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

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

0 commit comments

Comments
 (0)