Skip to content

Commit c0d6020

Browse files
Copilotalexr00
andauthored
Open PR links from chat without a workspace (#8933)
* Initial plan * Open PR links without a workspace Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Rename * Fix merge loss --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com>
1 parent ff4b74f commit c0d6020

7 files changed

Lines changed: 148 additions & 55 deletions

File tree

‎src/extension.ts‎

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { createExperimentationService, ExperimentationTelemetry } from './experi
2626
import { CopilotRemoteAgentManager } from './github/copilotRemoteAgent';
2727
import { CredentialStore } from './github/credentials';
2828
import { FolderRepositoryManager } from './github/folderRepositoryManager';
29+
import { FolderRepositoryManagerResolver } from './github/folderRepositoryManagerResolver';
2930
import { OverviewRestorer } from './github/overviewRestorer';
3031
import { RepositoriesManager } from './github/repositoriesManager';
3132
import { registerBuiltinGitProvider, registerLiveShareGitProvider } from './gitProviders/api';
@@ -292,7 +293,9 @@ async function init(
292293

293294
context.subscriptions.push(new GitLensIntegration());
294295

295-
context.subscriptions.push(new OverviewRestorer(reposManager, telemetry, context, credentialStore));
296+
const folderRepositoryManagerResolver = new FolderRepositoryManagerResolver(context, reposManager, telemetry);
297+
context.subscriptions.push(folderRepositoryManagerResolver);
298+
context.subscriptions.push(new OverviewRestorer(reposManager, telemetry, context, credentialStore, folderRepositoryManagerResolver));
296299

297300
await vscode.commands.executeCommand('setContext', 'github:initialized', true);
298301

@@ -302,7 +305,7 @@ async function init(
302305
await resumePendingCheckout(reviewsManager, context, reposManager);
303306

304307
initChat(context, credentialStore, reposManager);
305-
context.subscriptions.push(vscode.window.registerUriHandler(new UriHandler(reposManager, reviewsManager, telemetry, context, git)));
308+
context.subscriptions.push(vscode.window.registerUriHandler(new UriHandler(reposManager, reviewsManager, telemetry, context, git, folderRepositoryManagerResolver)));
306309

307310
// Make sure any compare changes tabs, which come from the create flow, are closed.
308311
CompareChanges.closeTabs();

‎src/github/externalUriOpener.ts‎

Lines changed: 6 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -4,29 +4,20 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import * as vscode from 'vscode';
7-
import { RemoteOnlyRepository } from '../api/remoteOnlyRepository';
8-
import { Disposable } from '../common/lifecycle';
9-
import { IThemeWatcher } from '../themeWatcher';
10-
import { CredentialStore } from './credentials';
11-
import { FolderRepositoryManager } from './folderRepositoryManager';
7+
import { FolderRepositoryManagerResolver } from './folderRepositoryManagerResolver';
128
import { IssueOverviewPanel } from './issueOverview';
139
import { PullRequestOverviewPanel } from './pullRequestOverview';
14-
import { RepositoriesManager } from './repositoriesManager';
15-
import { GitApiImpl } from '../api/api1';
1610
import { getGitHubIssueOrPullRequestUriOpenerPriority, openWithDefaultExternalOpener, parseGitHubIssueOrPullRequestUri } from '../common/externalUri';
11+
import { Disposable } from '../common/lifecycle';
1712
import { OPEN_PULL_LINKS, PR_SETTINGS_NAMESPACE } from '../common/settingKeys';
1813
import { ITelemetry } from '../common/telemetry';
1914
import { EXTENSION_ID } from '../constants';
20-
import { CreatePullRequestHelper } from '../view/createPullRequestHelper';
21-
import { ThemeData } from '../view/theme';
2215

2316
class GitHubIssueOrPullRequestExternalUriOpener extends Disposable implements vscode.ExternalUriOpener {
24-
private _remoteFolderRepositoryManager: FolderRepositoryManager | undefined;
2517

2618
constructor(
2719
private readonly _context: vscode.ExtensionContext,
28-
private readonly _repositoriesManager: RepositoriesManager,
29-
private readonly _credentialStore: CredentialStore,
20+
private readonly _folderRepositoryManagerResolver: FolderRepositoryManagerResolver,
3021
private readonly _telemetry: ITelemetry,
3122
) {
3223
super();
@@ -51,7 +42,7 @@ class GitHubIssueOrPullRequestExternalUriOpener extends Disposable implements vs
5142
return;
5243
}
5344

54-
const folderRepositoryManager = this.getFolderRepositoryManager(identity.owner, identity.repo);
45+
const folderRepositoryManager = this._folderRepositoryManagerResolver.getManagerForRepository(identity.owner, identity.repo);
5546
if (identity.kind === 'pullRequest') {
5647
const pullRequest = await folderRepositoryManager.resolvePullRequest(identity.owner, identity.repo, identity.number, true);
5748
if (token.isCancellationRequested) {
@@ -91,43 +82,12 @@ class GitHubIssueOrPullRequestExternalUriOpener extends Disposable implements vs
9182
return vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).get<boolean>(OPEN_PULL_LINKS, true);
9283
}
9384

94-
private getFolderRepositoryManager(owner: string, repo: string): FolderRepositoryManager {
95-
const existingManager = this._repositoriesManager.getManagerForRepository(owner, repo)
96-
?? this._repositoriesManager.folderManagers[0];
97-
if (existingManager) {
98-
return existingManager;
99-
}
100-
if (this._remoteFolderRepositoryManager) {
101-
return this._remoteFolderRepositoryManager;
102-
}
103-
104-
const repository = this._register(new RemoteOnlyRepository());
105-
const git = this._register(new GitApiImpl(this._repositoriesManager));
106-
const createPullRequestHelper = this._register(new CreatePullRequestHelper());
107-
const onDidChangeTheme = this._register(new vscode.EventEmitter<ThemeData | undefined>());
108-
const themeWatcher: IThemeWatcher = {
109-
onDidChangeTheme: onDidChangeTheme.event,
110-
themeData: undefined,
111-
};
112-
this._remoteFolderRepositoryManager = this._register(new FolderRepositoryManager(
113-
-1,
114-
this._context,
115-
repository,
116-
this._telemetry,
117-
git,
118-
this._credentialStore,
119-
createPullRequestHelper,
120-
themeWatcher,
121-
));
122-
return this._remoteFolderRepositoryManager;
123-
}
12485
}
12586

12687
export function registerGitHubIssueOrPullRequestExternalUriOpener(
12788
context: vscode.ExtensionContext,
128-
repositoriesManager: RepositoriesManager,
129-
credentialStore: CredentialStore,
89+
folderRepositoryManagerResolver: FolderRepositoryManagerResolver,
13090
telemetry: ITelemetry,
13191
): vscode.Disposable {
132-
return new GitHubIssueOrPullRequestExternalUriOpener(context, repositoriesManager, credentialStore, telemetry);
92+
return new GitHubIssueOrPullRequestExternalUriOpener(context, folderRepositoryManagerResolver, telemetry);
13393
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import * as vscode from 'vscode';
7+
import { FolderRepositoryManager } from './folderRepositoryManager';
8+
import { RepositoriesManager } from './repositoriesManager';
9+
import { GitApiImpl } from '../api/api1';
10+
import { RemoteOnlyRepository } from '../api/remoteOnlyRepository';
11+
import { Disposable } from '../common/lifecycle';
12+
import { ITelemetry } from '../common/telemetry';
13+
import { IThemeWatcher } from '../themeWatcher';
14+
import { CreatePullRequestHelper } from '../view/createPullRequestHelper';
15+
import { ThemeData } from '../view/theme';
16+
17+
export class FolderRepositoryManagerResolver extends Disposable {
18+
private _remoteFolderRepositoryManager: FolderRepositoryManager | undefined;
19+
20+
constructor(
21+
private readonly _context: vscode.ExtensionContext,
22+
private readonly _repositoriesManager: RepositoriesManager,
23+
private readonly _telemetry: ITelemetry,
24+
) {
25+
super();
26+
}
27+
28+
getManagerForRepository(owner: string, repo: string): FolderRepositoryManager {
29+
const existingManager = this._repositoriesManager.getManagerForRepository(owner, repo)
30+
?? this._repositoriesManager.folderManagers[0];
31+
if (existingManager) {
32+
return existingManager;
33+
}
34+
if (this._remoteFolderRepositoryManager) {
35+
return this._remoteFolderRepositoryManager;
36+
}
37+
38+
const repository = this._register(new RemoteOnlyRepository());
39+
const git = this._register(new GitApiImpl(this._repositoriesManager));
40+
const createPullRequestHelper = this._register(new CreatePullRequestHelper());
41+
const onDidChangeTheme = this._register(new vscode.EventEmitter<ThemeData | undefined>());
42+
const themeWatcher: IThemeWatcher = {
43+
onDidChangeTheme: onDidChangeTheme.event,
44+
themeData: undefined,
45+
};
46+
this._remoteFolderRepositoryManager = this._register(new FolderRepositoryManager(
47+
-1,
48+
this._context,
49+
repository,
50+
this._telemetry,
51+
git,
52+
this._repositoriesManager.credentialStore,
53+
createPullRequestHelper,
54+
themeWatcher,
55+
));
56+
return this._remoteFolderRepositoryManager;
57+
}
58+
}

‎src/github/overviewRestorer.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import * as vscode from 'vscode';
77
import { CredentialStore } from './credentials';
88
import { registerGitHubIssueOrPullRequestExternalUriOpener } from './externalUriOpener';
99
import { FolderRepositoryManager } from './folderRepositoryManager';
10+
import { FolderRepositoryManagerResolver } from './folderRepositoryManagerResolver';
1011
import { GitHubRepository } from './githubRepository';
1112
import { IssueOverviewPanel } from './issueOverview';
1213
import { PullRequestOverviewPanel } from './pullRequestOverview';
@@ -22,12 +23,13 @@ export class OverviewRestorer extends Disposable implements vscode.WebviewPanelS
2223
constructor(private readonly _repositoriesManager: RepositoriesManager,
2324
private readonly _telemetry: ITelemetry,
2425
private readonly _context: vscode.ExtensionContext,
25-
private readonly _credentialStore: CredentialStore
26+
private readonly _credentialStore: CredentialStore,
27+
folderRepositoryManagerResolver: FolderRepositoryManagerResolver,
2628
) {
2729
super();
2830
this._register(vscode.window.registerWebviewPanelSerializer(IssueOverviewPanel.viewType, this));
2931
this._register(vscode.window.registerWebviewPanelSerializer(PullRequestOverviewPanel.viewType, this));
30-
this._register(registerGitHubIssueOrPullRequestExternalUriOpener(_context, _repositoriesManager, _credentialStore, _telemetry));
32+
this._register(registerGitHubIssueOrPullRequestExternalUriOpener(_context, folderRepositoryManagerResolver, _telemetry));
3133
}
3234

3335
async deserializeWebviewPanel(webviewPanel: vscode.WebviewPanel, state: PullRequest): Promise<void> {

‎src/test/github/externalUriOpener.test.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { RemoteOnlyRepository } from '../../api/remoteOnlyRepository';
1010
import { CredentialStore } from '../../github/credentials';
1111
import { registerGitHubIssueOrPullRequestExternalUriOpener } from '../../github/externalUriOpener';
1212
import { FolderRepositoryManager } from '../../github/folderRepositoryManager';
13+
import { FolderRepositoryManagerResolver } from '../../github/folderRepositoryManagerResolver';
1314
import { RepositoriesManager } from '../../github/repositoriesManager';
1415
import { MockExtensionContext } from '../mocks/mockExtensionContext';
1516
import { MockTelemetry } from '../mocks/mockTelemetry';
@@ -30,6 +31,7 @@ describe('GitHubIssueOrPullRequestExternalUriOpener', () => {
3031
const telemetry = new MockTelemetry();
3132
const credentialStore = new CredentialStore(telemetry, context);
3233
const repositoriesManager = new RepositoriesManager(credentialStore, telemetry);
34+
const folderRepositoryManagerResolver = new FolderRepositoryManagerResolver(context, repositoriesManager, telemetry);
3335
let opener: vscode.ExternalUriOpener | undefined;
3436
sandbox.stub(vscode.window, 'registerExternalUriOpener').callsFake((_id, value) => {
3537
opener = value;
@@ -43,8 +45,7 @@ describe('GitHubIssueOrPullRequestExternalUriOpener', () => {
4345

4446
const registration = registerGitHubIssueOrPullRequestExternalUriOpener(
4547
context,
46-
repositoriesManager,
47-
credentialStore,
48+
folderRepositoryManagerResolver,
4849
telemetry,
4950
);
5051
const uri = vscode.Uri.parse('https://github.com/microsoft/vscode/issues/1');
@@ -56,6 +57,7 @@ describe('GitHubIssueOrPullRequestExternalUriOpener', () => {
5657
assert.strictEqual(repositoriesManager.folderManagers.length, 0);
5758
assert.strictEqual(resolveIssue.callCount, 1);
5859
registration.dispose();
60+
folderRepositoryManagerResolver.dispose();
5961
repositoriesManager.dispose();
6062
credentialStore.dispose();
6163
});

‎src/test/uriHandler.test.ts‎

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import { default as assert } from 'assert';
7+
import { createSandbox, SinonSandbox } from 'sinon';
8+
import * as vscode from 'vscode';
9+
import { GitApiImpl } from '../api/api1';
10+
import { RemoteOnlyRepository } from '../api/remoteOnlyRepository';
11+
import { CredentialStore } from '../github/credentials';
12+
import { FolderRepositoryManagerResolver } from '../github/folderRepositoryManagerResolver';
13+
import { PullRequestOverviewPanel } from '../github/pullRequestOverview';
14+
import { RepositoriesManager } from '../github/repositoriesManager';
15+
import { UriHandler } from '../uriHandler';
16+
import { ReviewsManager } from '../view/reviewsManager';
17+
import { MockExtensionContext } from './mocks/mockExtensionContext';
18+
import { MockTelemetry } from './mocks/mockTelemetry';
19+
20+
describe('UriHandler', function () {
21+
let context: MockExtensionContext;
22+
let credentialStore: CredentialStore;
23+
let folderRepositoryManagerResolver: FolderRepositoryManagerResolver;
24+
let git: GitApiImpl;
25+
let repositoriesManager: RepositoriesManager;
26+
let sandbox: SinonSandbox;
27+
let telemetry: MockTelemetry;
28+
29+
beforeEach(function () {
30+
sandbox = createSandbox();
31+
context = new MockExtensionContext();
32+
telemetry = new MockTelemetry();
33+
credentialStore = new CredentialStore(telemetry, context);
34+
repositoriesManager = new RepositoriesManager(credentialStore, telemetry);
35+
git = new GitApiImpl(repositoriesManager);
36+
folderRepositoryManagerResolver = new FolderRepositoryManagerResolver(context, repositoriesManager, telemetry);
37+
});
38+
39+
afterEach(function () {
40+
folderRepositoryManagerResolver.dispose();
41+
git.dispose();
42+
repositoriesManager.dispose();
43+
credentialStore.dispose();
44+
context.dispose();
45+
sandbox.restore();
46+
});
47+
48+
it('opens a pull request with a remote-only manager when no workspace is open', async function () {
49+
const createOrShow = sandbox.stub(PullRequestOverviewPanel, 'createOrShow').resolves();
50+
const handler = new UriHandler(
51+
repositoriesManager,
52+
{} as ReviewsManager,
53+
telemetry,
54+
context,
55+
git,
56+
folderRepositoryManagerResolver,
57+
);
58+
const uri = vscode.Uri.parse('vscode://github.vscode-pull-request-github/open-pull-request-webview?uri=https://github.com/microsoft/vscode/pull/1');
59+
60+
await handler.handleUri(uri);
61+
62+
assert.strictEqual(createOrShow.callCount, 1);
63+
assert.ok(createOrShow.firstCall.args[2].repository instanceof RemoteOnlyRepository);
64+
assert.strictEqual(repositoriesManager.folderManagers.length, 0);
65+
});
66+
});

‎src/uriHandler.ts‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import Logger from './common/logger';
1010
import { ITelemetry } from './common/telemetry';
1111
import { fromOpenIssueWebviewUri, fromOpenOrCheckoutPullRequestWebviewUri, UriHandlerPaths } from './common/uri';
1212
import { FolderRepositoryManager } from './github/folderRepositoryManager';
13+
import { FolderRepositoryManagerResolver } from './github/folderRepositoryManagerResolver';
1314
import { IssueOverviewPanel } from './github/issueOverview';
1415
import { PullRequestModel } from './github/pullRequestModel';
1516
import { PullRequestOverviewPanel } from './github/pullRequestOverview';
@@ -106,7 +107,8 @@ export class UriHandler implements vscode.UriHandler {
106107
private readonly _reviewsManagers: ReviewsManager,
107108
private readonly _telemetry: ITelemetry,
108109
private readonly _context: vscode.ExtensionContext,
109-
private readonly _git: GitApiImpl
110+
private readonly _git: GitApiImpl,
111+
private readonly _folderRepositoryManagerResolver: FolderRepositoryManagerResolver,
110112
) { }
111113

112114
async handleUri(uri: vscode.Uri): Promise<void> {
@@ -129,7 +131,7 @@ export class UriHandler implements vscode.UriHandler {
129131
if (!params) {
130132
return;
131133
}
132-
const folderManager = this._reposManagers.getManagerForRepository(params.owner, params.repo) ?? this._reposManagers.folderManagers[0];
134+
const folderManager = this._folderRepositoryManagerResolver.getManagerForRepository(params.owner, params.repo);
133135
const identity = { owner: params.owner, repo: params.repo, number: params.issueNumber };
134136
return IssueOverviewPanel.createOrShow(this._telemetry, this._context.extensionUri, folderManager, identity);
135137
}
@@ -141,7 +143,7 @@ export class UriHandler implements vscode.UriHandler {
141143
Logger.error('Failed to parse pull request URI.', UriHandler.ID);
142144
return;
143145
}
144-
const folderManager = this._reposManagers.getManagerForRepository(params.owner, params.repo) ?? this._reposManagers.folderManagers[0];
146+
const folderManager = this._folderRepositoryManagerResolver.getManagerForRepository(params.owner, params.repo);
145147
return { folderManager, identity: { owner: params.owner, repo: params.repo, number: params.pullRequestNumber } };
146148
}
147149

0 commit comments

Comments
 (0)