Skip to content

Commit 39c6f5e

Browse files
authored
Better handling for cases where the pr can't be checked out (#8092)
* Better handling for cases where the pr can't be checked out Fixes #8086 Fixes https://github.com/microsoft/vscode/issues/275358 Fixes https://github.com/microsoft/vscode/issues/275156 Fixes https://github.com/microsoft/vscode/issues/275355 * Reduce duplication
1 parent f201216 commit 39c6f5e

File tree

4 files changed

+92
-16
lines changed

4 files changed

+92
-16
lines changed

src/api/api.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,7 @@ export interface IGit {
240240

241241
registerPostCommitCommandsProvider?(provider: PostCommitCommandsProvider): Disposable;
242242
getRepositoryWorkspace?(uri: Uri): Promise<Uri[] | null>;
243+
clone?(uri: Uri, options?: CloneOptions): Promise<Uri | null>;
243244
}
244245

245246
export interface TitleAndDescriptionProvider {

src/api/api1.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import * as vscode from 'vscode';
77
import { API, IGit, PostCommitCommandsProvider, Repository, ReviewerCommentsProvider, TitleAndDescriptionProvider } from './api';
8-
import { APIState, PublishEvent } from '../@types/git';
8+
import { APIState, CloneOptions, PublishEvent } from '../@types/git';
99
import { Disposable } from '../common/lifecycle';
1010
import Logger from '../common/logger';
1111
import { TernarySearchTree } from '../common/utils';
@@ -93,6 +93,15 @@ export class GitApiImpl extends Disposable implements API, IGit {
9393
return null;
9494
}
9595

96+
async clone(uri: vscode.Uri, options?: CloneOptions): Promise<vscode.Uri | null> {
97+
for (const [, provider] of this._providers) {
98+
if (provider.clone) {
99+
return provider.clone(uri, options);
100+
}
101+
}
102+
return null;
103+
}
104+
96105

97106
public get repositories(): Repository[] {
98107
const ret: Repository[] = [];

src/gitProviders/builtinGit.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import * as vscode from 'vscode';
7-
import { APIState, GitAPI, GitExtension, PublishEvent } from '../@types/git';
7+
import { APIState, CloneOptions, GitAPI, GitExtension, PublishEvent } from '../@types/git';
88
import { IGit, Repository } from '../api/api';
99
import { commands } from '../common/executeCommands';
1010
import { Disposable } from '../common/lifecycle';
@@ -50,6 +50,10 @@ export class BuiltinGitProvider extends Disposable implements IGit {
5050
return this._gitAPI.getRepositoryWorkspace(uri);
5151
}
5252

53+
clone(uri: vscode.Uri, options?: CloneOptions): Promise<vscode.Uri | null> {
54+
return this._gitAPI.clone(uri, options);
55+
}
56+
5357
static async createProvider(): Promise<BuiltinGitProvider | undefined> {
5458
const extension = vscode.extensions.getExtension<GitExtension>('vscode.git');
5559
if (extension) {

src/uriHandler.ts

Lines changed: 76 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,33 @@ interface PendingCheckoutPayload {
2222
timestamp: number; // epoch millis when the pending checkout was stored
2323
}
2424

25+
function withCheckoutProgress<T>(owner: string, repo: string, prNumber: number, task: (progress: vscode.Progress<{ message?: string; increment?: number }>, token: vscode.CancellationToken) => Promise<T>): Promise<T> {
26+
return vscode.window.withProgress({
27+
location: vscode.ProgressLocation.Notification,
28+
title: vscode.l10n.t('Checking out pull request #{0} from {1}/{2}...', prNumber, owner, repo),
29+
cancellable: true
30+
}, async (progress, token) => {
31+
if (token.isCancellationRequested) {
32+
return Promise.resolve(undefined as unknown as T);
33+
}
34+
return task(progress, token);
35+
}) as Promise<T>;
36+
}
37+
2538
async function performPullRequestCheckout(folderManager: FolderRepositoryManager, owner: string, repo: string, prNumber: number): Promise<void> {
2639
try {
2740
const pullRequest = await folderManager.resolvePullRequest(owner, repo, prNumber);
2841
if (!pullRequest) {
42+
vscode.window.showErrorMessage(vscode.l10n.t('Pull request #{0} not found in {1}/{2}.', prNumber, owner, repo));
2943
Logger.warn(`Pull request #${prNumber} not found for checkout.`, UriHandler.ID);
3044
return;
3145
}
46+
47+
const proceed = await showCheckoutPrompt(owner, repo, prNumber);
48+
if (!proceed) {
49+
return;
50+
}
51+
3252
await vscode.commands.executeCommand('pr.pick', pullRequest);
3353
} catch (e) {
3454
Logger.error(`Error during pull request checkout: ${e instanceof Error ? e.message : String(e)}`, UriHandler.ID);
@@ -48,11 +68,11 @@ export async function resumePendingCheckout(context: vscode.ExtensionContext, re
4868
return;
4969
}
5070
const attempt = async () => {
51-
const fm = reposManager.getManagerForRepository(pending.owner, pending.repo);
52-
if (!fm) {
71+
const folderManager = reposManager.getManagerForRepository(pending.owner, pending.repo);
72+
if (!folderManager) {
5373
return false;
5474
}
55-
await performPullRequestCheckout(fm, pending.owner, pending.repo, pending.pullRequestNumber);
75+
await performPullRequestCheckout(folderManager, pending.owner, pending.repo, pending.pullRequestNumber);
5676
await context.globalState.update(PENDING_CHECKOUT_PULL_REQUEST_KEY, undefined);
5777
return true;
5878
};
@@ -65,6 +85,13 @@ export async function resumePendingCheckout(context: vscode.ExtensionContext, re
6585
}
6686
}
6787

88+
export async function showCheckoutPrompt(owner: string, repo: string, prNumber: number): Promise<boolean> {
89+
const message = vscode.l10n.t('Checkout pull request #{0} from {1}/{2}?', prNumber, owner, repo);
90+
const confirm = vscode.l10n.t('Checkout');
91+
const selection = await vscode.window.showInformationMessage(message, { modal: true }, confirm);
92+
return selection === confirm;
93+
}
94+
6895
export class UriHandler implements vscode.UriHandler {
6996
public static readonly ID = 'UriHandler';
7097
constructor(private readonly _reposManagers: RepositoriesManager,
@@ -111,6 +138,12 @@ export class UriHandler implements vscode.UriHandler {
111138
return PullRequestOverviewPanel.createOrShow(this._telemetry, this._context.extensionUri, folderManager, pullRequest);
112139
}
113140

141+
private async _savePendingCheckoutAndOpenFolder(params: { owner: string; repo: string; pullRequestNumber: number }, folderUri: vscode.Uri): Promise<void> {
142+
const payload: PendingCheckoutPayload = { ...params, timestamp: Date.now() };
143+
await this._context.globalState.update(PENDING_CHECKOUT_PULL_REQUEST_KEY, payload);
144+
await vscode.commands.executeCommand('vscode.openFolder', folderUri);
145+
}
146+
114147
private async _checkoutPullRequest(uri: vscode.Uri): Promise<void> {
115148
const params = fromOpenOrCheckoutPullRequestWebviewUri(uri);
116149
if (!params) {
@@ -121,18 +154,47 @@ export class UriHandler implements vscode.UriHandler {
121154
return performPullRequestCheckout(folderManager, params.owner, params.repo, params.pullRequestNumber);
122155
}
123156
// Folder not found; request workspace open then resume later.
124-
try {
125-
const remoteUri = vscode.Uri.parse(`https://github.com/${params.owner}/${params.repo}`);
126-
const workspaces = await this._git.getRepositoryWorkspace(remoteUri);
127-
if (workspaces && workspaces.length) {
128-
const payload: PendingCheckoutPayload = { ...params, timestamp: Date.now() };
129-
await this._context.globalState.update(PENDING_CHECKOUT_PULL_REQUEST_KEY, payload);
130-
await vscode.commands.executeCommand('vscode.openFolder', workspaces[0]);
131-
} else {
132-
Logger.warn(`No repository workspace found for ${remoteUri.toString()}`, UriHandler.ID);
157+
await withCheckoutProgress(params.owner, params.repo, params.pullRequestNumber, async (progress, token) => {
158+
if (token.isCancellationRequested) {
159+
return;
160+
}
161+
try {
162+
progress.report({ message: vscode.l10n.t('Locating workspace...') });
163+
const remoteUri = vscode.Uri.parse(`https://github.com/${params.owner}/${params.repo}`);
164+
const workspaces = await this._git.getRepositoryWorkspace(remoteUri);
165+
if (token.isCancellationRequested) {
166+
return;
167+
}
168+
if (workspaces && workspaces.length) {
169+
progress.report({ message: vscode.l10n.t('Opening workspace...') });
170+
await this._savePendingCheckoutAndOpenFolder(params, workspaces[0]);
171+
} else {
172+
this._showCloneOffer(remoteUri, params);
173+
}
174+
} catch (e) {
175+
Logger.error(`Failed attempting workspace open for checkout PR: ${e instanceof Error ? e.message : String(e)}`, UriHandler.ID);
176+
}
177+
});
178+
}
179+
180+
private async _showCloneOffer(remoteUri: vscode.Uri, params: { owner: string; repo: string; pullRequestNumber: number }): Promise<void> {
181+
const cloneLabel = vscode.l10n.t('Clone Repository');
182+
const choice = await vscode.window.showErrorMessage(
183+
vscode.l10n.t('Could not find a folder for repository {0}/{1}. Please clone or open the repository manually.', params.owner, params.repo),
184+
cloneLabel
185+
);
186+
Logger.warn(`No repository workspace found for ${remoteUri.toString()}`, UriHandler.ID);
187+
if (choice === cloneLabel) {
188+
try {
189+
const clonedWorkspaceUri = await this._git.clone(remoteUri, { postCloneAction: 'none' });
190+
if (clonedWorkspaceUri) {
191+
await this._savePendingCheckoutAndOpenFolder(params, clonedWorkspaceUri);
192+
} else {
193+
Logger.warn(`Clone API returned null for ${remoteUri.toString()}`, UriHandler.ID);
194+
}
195+
} catch (err) {
196+
Logger.error(`Failed to clone repository via API: ${err instanceof Error ? err.message : String(err)}`, UriHandler.ID);
133197
}
134-
} catch (e) {
135-
Logger.error(`Failed attempting workspace open for checkout PR: ${e instanceof Error ? e.message : String(e)}`, UriHandler.ID);
136198
}
137199
}
138200

0 commit comments

Comments
 (0)