diff --git a/src/extension.ts b/src/extension.ts index aca5026bd2..24396f5d0b 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -26,6 +26,7 @@ import { createExperimentationService, ExperimentationTelemetry } from './experi import { CopilotRemoteAgentManager } from './github/copilotRemoteAgent'; import { CredentialStore } from './github/credentials'; import { FolderRepositoryManager } from './github/folderRepositoryManager'; +import { FolderRepositoryManagerResolver } from './github/folderRepositoryManagerResolver'; import { OverviewRestorer } from './github/overviewRestorer'; import { RepositoriesManager } from './github/repositoriesManager'; import { registerBuiltinGitProvider, registerLiveShareGitProvider } from './gitProviders/api'; @@ -292,7 +293,9 @@ async function init( context.subscriptions.push(new GitLensIntegration()); - context.subscriptions.push(new OverviewRestorer(reposManager, telemetry, context, credentialStore)); + const folderRepositoryManagerResolver = new FolderRepositoryManagerResolver(context, reposManager, telemetry); + context.subscriptions.push(folderRepositoryManagerResolver); + context.subscriptions.push(new OverviewRestorer(reposManager, telemetry, context, credentialStore, folderRepositoryManagerResolver)); await vscode.commands.executeCommand('setContext', 'github:initialized', true); @@ -302,7 +305,7 @@ async function init( await resumePendingCheckout(reviewsManager, context, reposManager); initChat(context, credentialStore, reposManager); - context.subscriptions.push(vscode.window.registerUriHandler(new UriHandler(reposManager, reviewsManager, telemetry, context, git))); + context.subscriptions.push(vscode.window.registerUriHandler(new UriHandler(reposManager, reviewsManager, telemetry, context, git, folderRepositoryManagerResolver))); // Make sure any compare changes tabs, which come from the create flow, are closed. CompareChanges.closeTabs(); diff --git a/src/github/externalUriOpener.ts b/src/github/externalUriOpener.ts index 14d5d56d73..56113d3566 100644 --- a/src/github/externalUriOpener.ts +++ b/src/github/externalUriOpener.ts @@ -4,29 +4,20 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { RemoteOnlyRepository } from '../api/remoteOnlyRepository'; -import { Disposable } from '../common/lifecycle'; -import { IThemeWatcher } from '../themeWatcher'; -import { CredentialStore } from './credentials'; -import { FolderRepositoryManager } from './folderRepositoryManager'; +import { FolderRepositoryManagerResolver } from './folderRepositoryManagerResolver'; import { IssueOverviewPanel } from './issueOverview'; import { PullRequestOverviewPanel } from './pullRequestOverview'; -import { RepositoriesManager } from './repositoriesManager'; -import { GitApiImpl } from '../api/api1'; import { getGitHubIssueOrPullRequestUriOpenerPriority, openWithDefaultExternalOpener, parseGitHubIssueOrPullRequestUri } from '../common/externalUri'; +import { Disposable } from '../common/lifecycle'; import { OPEN_PULL_LINKS, PR_SETTINGS_NAMESPACE } from '../common/settingKeys'; import { ITelemetry } from '../common/telemetry'; import { EXTENSION_ID } from '../constants'; -import { CreatePullRequestHelper } from '../view/createPullRequestHelper'; -import { ThemeData } from '../view/theme'; class GitHubIssueOrPullRequestExternalUriOpener extends Disposable implements vscode.ExternalUriOpener { - private _remoteFolderRepositoryManager: FolderRepositoryManager | undefined; constructor( private readonly _context: vscode.ExtensionContext, - private readonly _repositoriesManager: RepositoriesManager, - private readonly _credentialStore: CredentialStore, + private readonly _folderRepositoryManagerResolver: FolderRepositoryManagerResolver, private readonly _telemetry: ITelemetry, ) { super(); @@ -51,7 +42,7 @@ class GitHubIssueOrPullRequestExternalUriOpener extends Disposable implements vs return; } - const folderRepositoryManager = this.getFolderRepositoryManager(identity.owner, identity.repo); + const folderRepositoryManager = this._folderRepositoryManagerResolver.getManagerForRepository(identity.owner, identity.repo); if (identity.kind === 'pullRequest') { const pullRequest = await folderRepositoryManager.resolvePullRequest(identity.owner, identity.repo, identity.number, true); if (token.isCancellationRequested) { @@ -91,43 +82,12 @@ class GitHubIssueOrPullRequestExternalUriOpener extends Disposable implements vs return vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).get(OPEN_PULL_LINKS, true); } - private getFolderRepositoryManager(owner: string, repo: string): FolderRepositoryManager { - const existingManager = this._repositoriesManager.getManagerForRepository(owner, repo) - ?? this._repositoriesManager.folderManagers[0]; - if (existingManager) { - return existingManager; - } - if (this._remoteFolderRepositoryManager) { - return this._remoteFolderRepositoryManager; - } - - const repository = this._register(new RemoteOnlyRepository()); - const git = this._register(new GitApiImpl(this._repositoriesManager)); - const createPullRequestHelper = this._register(new CreatePullRequestHelper()); - const onDidChangeTheme = this._register(new vscode.EventEmitter()); - const themeWatcher: IThemeWatcher = { - onDidChangeTheme: onDidChangeTheme.event, - themeData: undefined, - }; - this._remoteFolderRepositoryManager = this._register(new FolderRepositoryManager( - -1, - this._context, - repository, - this._telemetry, - git, - this._credentialStore, - createPullRequestHelper, - themeWatcher, - )); - return this._remoteFolderRepositoryManager; - } } export function registerGitHubIssueOrPullRequestExternalUriOpener( context: vscode.ExtensionContext, - repositoriesManager: RepositoriesManager, - credentialStore: CredentialStore, + folderRepositoryManagerResolver: FolderRepositoryManagerResolver, telemetry: ITelemetry, ): vscode.Disposable { - return new GitHubIssueOrPullRequestExternalUriOpener(context, repositoriesManager, credentialStore, telemetry); + return new GitHubIssueOrPullRequestExternalUriOpener(context, folderRepositoryManagerResolver, telemetry); } diff --git a/src/github/folderRepositoryManagerResolver.ts b/src/github/folderRepositoryManagerResolver.ts new file mode 100644 index 0000000000..c89a44fae2 --- /dev/null +++ b/src/github/folderRepositoryManagerResolver.ts @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { FolderRepositoryManager } from './folderRepositoryManager'; +import { RepositoriesManager } from './repositoriesManager'; +import { GitApiImpl } from '../api/api1'; +import { RemoteOnlyRepository } from '../api/remoteOnlyRepository'; +import { Disposable } from '../common/lifecycle'; +import { ITelemetry } from '../common/telemetry'; +import { IThemeWatcher } from '../themeWatcher'; +import { CreatePullRequestHelper } from '../view/createPullRequestHelper'; +import { ThemeData } from '../view/theme'; + +export class FolderRepositoryManagerResolver extends Disposable { + private _remoteFolderRepositoryManager: FolderRepositoryManager | undefined; + + constructor( + private readonly _context: vscode.ExtensionContext, + private readonly _repositoriesManager: RepositoriesManager, + private readonly _telemetry: ITelemetry, + ) { + super(); + } + + getManagerForRepository(owner: string, repo: string): FolderRepositoryManager { + const existingManager = this._repositoriesManager.getManagerForRepository(owner, repo) + ?? this._repositoriesManager.folderManagers[0]; + if (existingManager) { + return existingManager; + } + if (this._remoteFolderRepositoryManager) { + return this._remoteFolderRepositoryManager; + } + + const repository = this._register(new RemoteOnlyRepository()); + const git = this._register(new GitApiImpl(this._repositoriesManager)); + const createPullRequestHelper = this._register(new CreatePullRequestHelper()); + const onDidChangeTheme = this._register(new vscode.EventEmitter()); + const themeWatcher: IThemeWatcher = { + onDidChangeTheme: onDidChangeTheme.event, + themeData: undefined, + }; + this._remoteFolderRepositoryManager = this._register(new FolderRepositoryManager( + -1, + this._context, + repository, + this._telemetry, + git, + this._repositoriesManager.credentialStore, + createPullRequestHelper, + themeWatcher, + )); + return this._remoteFolderRepositoryManager; + } +} diff --git a/src/github/overviewRestorer.ts b/src/github/overviewRestorer.ts index 2ad97d6dc5..a9df4941ee 100644 --- a/src/github/overviewRestorer.ts +++ b/src/github/overviewRestorer.ts @@ -7,6 +7,7 @@ import * as vscode from 'vscode'; import { CredentialStore } from './credentials'; import { registerGitHubIssueOrPullRequestExternalUriOpener } from './externalUriOpener'; import { FolderRepositoryManager } from './folderRepositoryManager'; +import { FolderRepositoryManagerResolver } from './folderRepositoryManagerResolver'; import { GitHubRepository } from './githubRepository'; import { IssueOverviewPanel } from './issueOverview'; import { PullRequestOverviewPanel } from './pullRequestOverview'; @@ -22,12 +23,13 @@ export class OverviewRestorer extends Disposable implements vscode.WebviewPanelS constructor(private readonly _repositoriesManager: RepositoriesManager, private readonly _telemetry: ITelemetry, private readonly _context: vscode.ExtensionContext, - private readonly _credentialStore: CredentialStore + private readonly _credentialStore: CredentialStore, + folderRepositoryManagerResolver: FolderRepositoryManagerResolver, ) { super(); this._register(vscode.window.registerWebviewPanelSerializer(IssueOverviewPanel.viewType, this)); this._register(vscode.window.registerWebviewPanelSerializer(PullRequestOverviewPanel.viewType, this)); - this._register(registerGitHubIssueOrPullRequestExternalUriOpener(_context, _repositoriesManager, _credentialStore, _telemetry)); + this._register(registerGitHubIssueOrPullRequestExternalUriOpener(_context, folderRepositoryManagerResolver, _telemetry)); } async deserializeWebviewPanel(webviewPanel: vscode.WebviewPanel, state: PullRequest): Promise { diff --git a/src/test/github/externalUriOpener.test.ts b/src/test/github/externalUriOpener.test.ts index 7cd2f6496a..8620234300 100644 --- a/src/test/github/externalUriOpener.test.ts +++ b/src/test/github/externalUriOpener.test.ts @@ -10,6 +10,7 @@ import { RemoteOnlyRepository } from '../../api/remoteOnlyRepository'; import { CredentialStore } from '../../github/credentials'; import { registerGitHubIssueOrPullRequestExternalUriOpener } from '../../github/externalUriOpener'; import { FolderRepositoryManager } from '../../github/folderRepositoryManager'; +import { FolderRepositoryManagerResolver } from '../../github/folderRepositoryManagerResolver'; import { RepositoriesManager } from '../../github/repositoriesManager'; import { MockExtensionContext } from '../mocks/mockExtensionContext'; import { MockTelemetry } from '../mocks/mockTelemetry'; @@ -30,6 +31,7 @@ describe('GitHubIssueOrPullRequestExternalUriOpener', () => { const telemetry = new MockTelemetry(); const credentialStore = new CredentialStore(telemetry, context); const repositoriesManager = new RepositoriesManager(credentialStore, telemetry); + const folderRepositoryManagerResolver = new FolderRepositoryManagerResolver(context, repositoriesManager, telemetry); let opener: vscode.ExternalUriOpener | undefined; sandbox.stub(vscode.window, 'registerExternalUriOpener').callsFake((_id, value) => { opener = value; @@ -43,8 +45,7 @@ describe('GitHubIssueOrPullRequestExternalUriOpener', () => { const registration = registerGitHubIssueOrPullRequestExternalUriOpener( context, - repositoriesManager, - credentialStore, + folderRepositoryManagerResolver, telemetry, ); const uri = vscode.Uri.parse('https://github.com/microsoft/vscode/issues/1'); @@ -56,6 +57,7 @@ describe('GitHubIssueOrPullRequestExternalUriOpener', () => { assert.strictEqual(repositoriesManager.folderManagers.length, 0); assert.strictEqual(resolveIssue.callCount, 1); registration.dispose(); + folderRepositoryManagerResolver.dispose(); repositoriesManager.dispose(); credentialStore.dispose(); }); diff --git a/src/test/uriHandler.test.ts b/src/test/uriHandler.test.ts new file mode 100644 index 0000000000..39c88ad11b --- /dev/null +++ b/src/test/uriHandler.test.ts @@ -0,0 +1,66 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { default as assert } from 'assert'; +import { createSandbox, SinonSandbox } from 'sinon'; +import * as vscode from 'vscode'; +import { GitApiImpl } from '../api/api1'; +import { RemoteOnlyRepository } from '../api/remoteOnlyRepository'; +import { CredentialStore } from '../github/credentials'; +import { FolderRepositoryManagerResolver } from '../github/folderRepositoryManagerResolver'; +import { PullRequestOverviewPanel } from '../github/pullRequestOverview'; +import { RepositoriesManager } from '../github/repositoriesManager'; +import { UriHandler } from '../uriHandler'; +import { ReviewsManager } from '../view/reviewsManager'; +import { MockExtensionContext } from './mocks/mockExtensionContext'; +import { MockTelemetry } from './mocks/mockTelemetry'; + +describe('UriHandler', function () { + let context: MockExtensionContext; + let credentialStore: CredentialStore; + let folderRepositoryManagerResolver: FolderRepositoryManagerResolver; + let git: GitApiImpl; + let repositoriesManager: RepositoriesManager; + let sandbox: SinonSandbox; + let telemetry: MockTelemetry; + + beforeEach(function () { + sandbox = createSandbox(); + context = new MockExtensionContext(); + telemetry = new MockTelemetry(); + credentialStore = new CredentialStore(telemetry, context); + repositoriesManager = new RepositoriesManager(credentialStore, telemetry); + git = new GitApiImpl(repositoriesManager); + folderRepositoryManagerResolver = new FolderRepositoryManagerResolver(context, repositoriesManager, telemetry); + }); + + afterEach(function () { + folderRepositoryManagerResolver.dispose(); + git.dispose(); + repositoriesManager.dispose(); + credentialStore.dispose(); + context.dispose(); + sandbox.restore(); + }); + + it('opens a pull request with a remote-only manager when no workspace is open', async function () { + const createOrShow = sandbox.stub(PullRequestOverviewPanel, 'createOrShow').resolves(); + const handler = new UriHandler( + repositoriesManager, + {} as ReviewsManager, + telemetry, + context, + git, + folderRepositoryManagerResolver, + ); + const uri = vscode.Uri.parse('vscode://github.vscode-pull-request-github/open-pull-request-webview?uri=https://github.com/microsoft/vscode/pull/1'); + + await handler.handleUri(uri); + + assert.strictEqual(createOrShow.callCount, 1); + assert.ok(createOrShow.firstCall.args[2].repository instanceof RemoteOnlyRepository); + assert.strictEqual(repositoriesManager.folderManagers.length, 0); + }); +}); diff --git a/src/uriHandler.ts b/src/uriHandler.ts index f884f7c39c..11d70805f2 100644 --- a/src/uriHandler.ts +++ b/src/uriHandler.ts @@ -10,6 +10,7 @@ import Logger from './common/logger'; import { ITelemetry } from './common/telemetry'; import { fromOpenIssueWebviewUri, fromOpenOrCheckoutPullRequestWebviewUri, UriHandlerPaths } from './common/uri'; import { FolderRepositoryManager } from './github/folderRepositoryManager'; +import { FolderRepositoryManagerResolver } from './github/folderRepositoryManagerResolver'; import { IssueOverviewPanel } from './github/issueOverview'; import { PullRequestModel } from './github/pullRequestModel'; import { PullRequestOverviewPanel } from './github/pullRequestOverview'; @@ -106,7 +107,8 @@ export class UriHandler implements vscode.UriHandler { private readonly _reviewsManagers: ReviewsManager, private readonly _telemetry: ITelemetry, private readonly _context: vscode.ExtensionContext, - private readonly _git: GitApiImpl + private readonly _git: GitApiImpl, + private readonly _folderRepositoryManagerResolver: FolderRepositoryManagerResolver, ) { } async handleUri(uri: vscode.Uri): Promise { @@ -129,7 +131,7 @@ export class UriHandler implements vscode.UriHandler { if (!params) { return; } - const folderManager = this._reposManagers.getManagerForRepository(params.owner, params.repo) ?? this._reposManagers.folderManagers[0]; + const folderManager = this._folderRepositoryManagerResolver.getManagerForRepository(params.owner, params.repo); const identity = { owner: params.owner, repo: params.repo, number: params.issueNumber }; return IssueOverviewPanel.createOrShow(this._telemetry, this._context.extensionUri, folderManager, identity); } @@ -141,7 +143,7 @@ export class UriHandler implements vscode.UriHandler { Logger.error('Failed to parse pull request URI.', UriHandler.ID); return; } - const folderManager = this._reposManagers.getManagerForRepository(params.owner, params.repo) ?? this._reposManagers.folderManagers[0]; + const folderManager = this._folderRepositoryManagerResolver.getManagerForRepository(params.owner, params.repo); return { folderManager, identity: { owner: params.owner, repo: params.repo, number: params.pullRequestNumber } }; }