Skip to content

Commit 1cf0a24

Browse files
committed
1 parent 4d39482 commit 1cf0a24

4 files changed

Lines changed: 95 additions & 21 deletions

File tree

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: 79 additions & 19 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,
@@ -116,23 +143,56 @@ export class UriHandler implements vscode.UriHandler {
116143
if (!params) {
117144
return;
118145
}
119-
const folderManager = this._reposManagers.getManagerForRepository(params.owner, params.repo);
120-
if (folderManager) {
121-
return performPullRequestCheckout(folderManager, params.owner, params.repo, params.pullRequestNumber);
122-
}
123-
// 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);
146+
await withCheckoutProgress(params.owner, params.repo, params.pullRequestNumber, async (progress, token) => {
147+
if (token.isCancellationRequested) {
148+
return;
149+
}
150+
const folderManager = this._reposManagers.getManagerForRepository(params.owner, params.repo);
151+
if (folderManager) {
152+
return performPullRequestCheckout(folderManager, params.owner, params.repo, params.pullRequestNumber);
153+
}
154+
// Folder not found; request workspace open then resume later.
155+
try {
156+
progress.report({ message: vscode.l10n.t('Locating workspace...') });
157+
const remoteUri = vscode.Uri.parse(`https://github.com/${params.owner}/${params.repo}`);
158+
const workspaces = await this._git.getRepositoryWorkspace(remoteUri);
159+
if (token.isCancellationRequested) {
160+
return;
161+
}
162+
if (workspaces && workspaces.length) {
163+
progress.report({ message: vscode.l10n.t('Opening workspace...') });
164+
const payload: PendingCheckoutPayload = { ...params, timestamp: Date.now() };
165+
await this._context.globalState.update(PENDING_CHECKOUT_PULL_REQUEST_KEY, payload);
166+
await vscode.commands.executeCommand('vscode.openFolder', workspaces[0]);
167+
} else {
168+
this._showCloneOffer(remoteUri, params);
169+
}
170+
} catch (e) {
171+
Logger.error(`Failed attempting workspace open for checkout PR: ${e instanceof Error ? e.message : String(e)}`, UriHandler.ID);
172+
}
173+
});
174+
}
175+
176+
private async _showCloneOffer(remoteUri: vscode.Uri, params: { owner: string; repo: string; pullRequestNumber: number }): Promise<void> {
177+
const cloneLabel = vscode.l10n.t('Clone Repository');
178+
const choice = await vscode.window.showErrorMessage(
179+
vscode.l10n.t('Could not find a folder for repository {0}/{1}. Please clone or open the repository manually.', params.owner, params.repo),
180+
cloneLabel
181+
);
182+
Logger.warn(`No repository workspace found for ${remoteUri.toString()}`, UriHandler.ID);
183+
if (choice === cloneLabel) {
184+
try {
185+
const clonedWorkspaceUri = await this._git.clone(remoteUri, { postCloneAction: 'none' });
186+
if (clonedWorkspaceUri) {
187+
const payload: PendingCheckoutPayload = { ...params, timestamp: Date.now() };
188+
await this._context.globalState.update(PENDING_CHECKOUT_PULL_REQUEST_KEY, payload);
189+
await vscode.commands.executeCommand('vscode.openFolder', clonedWorkspaceUri);
190+
} else {
191+
Logger.warn(`Clone API returned null for ${remoteUri.toString()}`, UriHandler.ID);
192+
}
193+
} catch (err) {
194+
Logger.error(`Failed to clone repository via API: ${err instanceof Error ? err.message : String(err)}`, UriHandler.ID);
133195
}
134-
} catch (e) {
135-
Logger.error(`Failed attempting workspace open for checkout PR: ${e instanceof Error ? e.message : String(e)}`, UriHandler.ID);
136196
}
137197
}
138198

0 commit comments

Comments
 (0)